atomic-linux/airootfs/usr/share/Brain_Shell/dots-extra/install-arch.sh

519 lines
21 KiB
Bash
Executable file

#!/bin/bash
# ─────────────────────────────────────────────────────────────────────────────
# Brain Shell — Arch Linux Installer
# Invoked by install.sh: $1=HYPRLAND_CONF $2=BACKUP_DIR $3=CONFIG_TYPE
# ─────────────────────────────────────────────────────────────────────────────
set -eo pipefail
# ── Arguments (validated up-front) ───────────────────────────────────────────
HYPRLAND_CONF="${1:?Missing arg: HYPRLAND_CONF path}"
BACKUP_DIR="${2:?Missing arg: BACKUP_DIR}"
CONFIG_TYPE="${3:?Missing arg: CONFIG_TYPE (conf|lua)}"
REPO_DIR="$HOME/.local/src/Brain_Shell"
# ── Colors ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'
DIM='\033[2m'; NC='\033[0m'
# ── Logging ───────────────────────────────────────────────────────────────────
log_info() { echo -e " ${BLUE}·${NC} $1"; }
log_ok() { echo -e " ${GREEN}✓${NC} $1"; }
log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
log_error() { echo -e " ${RED}✗${NC} $1" >&2; }
die() { echo ""; log_error "$1"; exit 1; }
TOTAL_STEPS=6
step() {
echo ""
echo -e "${BOLD}${CYAN} [$1/$TOTAL_STEPS] $2${NC}"
echo -e " ${DIM}$(printf '%.0s─' {1..50})${NC}"
}
# ── Failure Tracking ──────────────────────────────────────────────────────────
# Packages that couldn't be installed are collected here and shown in the
# final summary with manual fix commands instead of aborting the whole install.
declare -a FAILED_PKGS=()
# ══════════════════════════════════════════════════════════════════════════════
# PACKAGE HELPERS
# ══════════════════════════════════════════════════════════════════════════════
# pacman_install <pkg> [<pkg> ...]
#
# Strategy (three attempts, most to least aggressive):
#
# 1. Bulk install — fastest; skips already-installed packages via --needed.
#
# 2. Per-package retry — if the bulk transaction fails because ONE package
# has a conflict, the entire batch is rejected. Retrying individually
# isolates which package is actually broken so the rest can still install.
#
# 3. --overwrite='*' per package — resolves FILE-OWNERSHIP conflicts, where
# two packages both claim the same path. Safe in practice: the new package
# just wins the ownership. This does NOT help with hard PKGBUILD conflicts
# (ConflictsWith). Those need manual resolution (see summary output).
#
pacman_install() {
local -a pkgs=("$@")
local total=${#pkgs[@]}
log_info "Installing $total packages via pacman..."
# Attempt 1 — bulk
if sudo pacman -S --needed --noconfirm "${pkgs[@]}" 2>/dev/null; then
log_ok "All $total packages installed."
return 0
fi
# Bulk failed — at least one conflict. Fall back to one-by-one.
echo ""
log_warn "Bulk install hit a conflict. Retrying individually..."
echo ""
local installed=0 already=0
local -a failed=()
for pkg in "${pkgs[@]}"; do
# Skip silently if already present
if pacman -Qi "$pkg" &>/dev/null; then
already=$(( already + 1 ))
continue
fi
printf " ${DIM}%-32s${NC} " "$pkg"
# Attempt 2 — standard per-package
if sudo pacman -S --needed --noconfirm "$pkg" &>/dev/null; then
echo -e "${GREEN}✓${NC}"
installed=$(( installed + 1 ))
continue
fi
# Attempt 3 — overwrite (file-ownership conflicts)
if sudo pacman -S --needed --noconfirm --overwrite='*' "$pkg" &>/dev/null; then
echo -e "${YELLOW}✓ overwrite${NC}"
installed=$(( installed + 1 ))
continue
fi
# Hard conflict — needs manual resolution
echo -e "${RED}✗ conflict${NC}"
failed+=("$pkg")
done
echo ""
log_ok "$installed installed, $already already present"
if [[ ${#failed[@]} -gt 0 ]]; then
log_warn "${#failed[@]} package(s) failed (hard conflict — see summary):"
for pkg in "${failed[@]}"; do
log_warn " $pkg"
FAILED_PKGS+=("pacman:$pkg")
done
fi
}
# aur_install <helper> <pkg> [<pkg> ...]
#
# Installs packages individually via yay/paru. AUR helpers build from source,
# so they don't support the same --overwrite shortcut. Failures are tracked but
# non-fatal; quickshell is checked explicitly afterwards.
#
aur_install() {
local helper="$1"; shift
local -a pkgs=("$@")
local -a failed=()
echo ""
for pkg in "${pkgs[@]}"; do
printf " ${DIM}%-32s${NC} " "$pkg"
if $helper -Q "$pkg" &>/dev/null; then
echo -e "${GREEN}✓ already installed${NC}"
continue
fi
if $helper -S --noconfirm "$pkg" &>/dev/null; then
echo -e "${GREEN}✓${NC}"
else
echo -e "${RED}✗${NC}"
failed+=("$pkg")
fi
done
echo ""
if [[ ${#failed[@]} -gt 0 ]]; then
log_warn "${#failed[@]} AUR package(s) failed (see summary):"
for pkg in "${failed[@]}"; do
log_warn " $pkg"
FAILED_PKGS+=("aur:$pkg")
done
else
log_ok "All AUR packages installed."
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# STEP 1 — AUR Helper
# ══════════════════════════════════════════════════════════════════════════════
step 1 "AUR Helper"
AUR_HELPER=""
if command -v yay &>/dev/null; then
AUR_HELPER="yay"
log_ok "yay detected"
elif command -v paru &>/dev/null; then
AUR_HELPER="paru"
log_ok "paru detected"
else
log_warn "No AUR helper found (yay / paru)."
echo ""
echo -e " ${BOLD}Select one to install:${NC}"
echo " 1) yay — more interactive, widely used"
echo " 2) paru — faster builds, more features"
echo " 3) Skip — pacman-only (quickshell will be missing)"
echo ""
read -rp " Choice [1/2/3]: " _aur_choice < /dev/tty
_bootstrap_aur_helper() {
local name="$1"
log_info "Bootstrapping $name from AUR..."
sudo pacman -S --needed --noconfirm git base-devel
local tmp; tmp=$(mktemp -d)
git clone "https://aur.archlinux.org/${name}.git" "$tmp/$name"
( cd "$tmp/$name" && makepkg -si --noconfirm )
rm -rf "$tmp"
log_ok "$name installed."
}
case "$_aur_choice" in
1) _bootstrap_aur_helper yay; AUR_HELPER="yay" ;;
2) _bootstrap_aur_helper paru; AUR_HELPER="paru" ;;
3)
log_warn "Skipping AUR helper."
log_warn "quickshell is required — install yay or paru later and re-run."
AUR_HELPER="none"
;;
*) die "Invalid choice." ;;
esac
fi
# ══════════════════════════════════════════════════════════════════════════════
# STEP 2 — Pacman Packages
# ══════════════════════════════════════════════════════════════════════════════
step 2 "Pacman Packages"
PACMAN_DEPS=(
# Qt6 runtime
qt6-base qt6-declarative qt6-multimedia qt6-5compat qt6ct
# Audio / PipeWire
pipewire pipewire-pulse wireplumber
# Media & player control
playerctl mpv-mpris mpd-mpris
# Network / Bluetooth
networkmanager bluez bluez-utils
# System services
brightnessctl upower libnotify polkit
python wl-clipboard slurp xdg-user-dirs
# Screen recording
wf-recorder cava
# Wallpaper / theming
imagemagick
# Input simulation
wtype
# Hardware sensors
lm_sensors rfkill
# Hyprland ecosystem (hyprshutdown is AUR-only — kept out of here)
hyprland hyprsunset hyprlock hyprpolkitagent hypridle
xdg-desktop-portal-hyprland
# Fonts
ttf-jetbrains-mono-nerd ttf-nerd-fonts-symbols-common
)
log_info "Syncing package database..."
sudo pacman -Syu --noconfirm 2>/dev/null || {
log_warn "System update failed — continuing with current DB. Some packages may be stale."
}
pacman_install "${PACMAN_DEPS[@]}"
# ══════════════════════════════════════════════════════════════════════════════
# STEP 3 — AUR Packages
# ══════════════════════════════════════════════════════════════════════════════
step 3 "AUR Packages"
AUR_DEPS=(
quickshell # REQUIRED — the shell runtime
awww # animation daemon
matugen # Material You color generation
envycontrol # GPU switching
auto-cpufreq # CPU power management
nbfc-linux # fan control
cliphist # clipboard history
hyprshutdown # power menu backend
grimblast-git # screenshot tool
)
if [[ "$AUR_HELPER" == "none" ]]; then
log_warn "No AUR helper — skipping all AUR packages."
for pkg in "${AUR_DEPS[@]}"; do
FAILED_PKGS+=("aur:$pkg (no helper)")
done
else
log_info "Using: $AUR_HELPER"
aur_install "$AUR_HELPER" "${AUR_DEPS[@]}"
fi
# quickshell is non-negotiable
if ! "$AUR_HELPER" -Q quickshell &>/dev/null 2>&1; then
die "quickshell failed to install. Brain Shell cannot run without it."
fi
# ══════════════════════════════════════════════════════════════════════════════
# STEP 4 — Systemd Services
# ══════════════════════════════════════════════════════════════════════════════
step 4 "Systemd Services"
_svc_system() {
sudo systemctl enable --now "$1" 2>/dev/null \
&& log_ok "system: $1" \
|| log_warn "system: $1 (failed to enable — may not apply to your setup)"
}
_svc_user() {
systemctl --user enable --now "$1" 2>/dev/null \
&& log_ok "user: $1" \
|| log_warn "user: $1 (failed to enable)"
}
_svc_system NetworkManager
_svc_system bluetooth
_svc_system upower
_svc_user pipewire
_svc_user pipewire-pulse
_svc_user wireplumber
# ══════════════════════════════════════════════════════════════════════════════
# STEP 5 — Hyprland Config
# ══════════════════════════════════════════════════════════════════════════════
step 5 "Hyprland Config"
# Marker used to detect whether the block was already appended
_MARKER="quickshell.*Brain_Shell"
_append_conf() {
cat << 'EOF' >> "$1"
# Brain Shell Autostarts
exec-once = awww-daemon
exec-once = hypridle -c $HOME/.local/src/Brain_Shell/src/config/hypridle.conf
exec-once = quickshell -c $HOME/.local/src/Brain_Shell/.
exec-once = systemctl --user start hyprpolkitagent
exec-once = wl-paste --type text --watch cliphist store
exec-once = wl-paste --type image --watch cliphist store
EOF
}
_append_lua() {
cat << 'EOF' >> "$1"
-- Brain Shell Autostarts
hl.on("hyprland.start", function()
hl.exec_cmd("awww-daemon")
hl.exec_cmd("hypridle -c " .. os.getenv("HOME") .. "/.local/src/Brain_Shell/src/config/hypridle.conf")
hl.exec_cmd("quickshell -c " .. os.getenv("HOME") .. "/.local/src/Brain_Shell")
hl.exec_cmd("systemctl --user start hyprpolkitagent")
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
end)
EOF
}
if grep -q "$_MARKER" "$HYPRLAND_CONF" 2>/dev/null; then
log_warn "Autostart block already present — skipping."
else
case "$CONFIG_TYPE" in
conf)
_append_conf "$HYPRLAND_CONF"
log_ok "Autostart block appended to hyprland.conf"
;;
lua)
# Extra safety backup before touching a Lua config
cp "$HYPRLAND_CONF" "${HYPRLAND_CONF}.pre-brain-shell"
log_info "Safety backup: ${HYPRLAND_CONF}.pre-brain-shell"
_append_lua "$HYPRLAND_CONF"
log_ok "Autostart block appended to hyprland.lua"
;;
*)
log_warn "Unknown config type '$CONFIG_TYPE' — skipping Hyprland config update."
;;
esac
fi
# ══════════════════════════════════════════════════════════════════════════════
# STEP 6 — Brain Shell Config & Keybind Check
# ══════════════════════════════════════════════════════════════════════════════
step 6 "Brain Shell Config"
USER_DATA="$HOME/.config/Brain_Shell/src/user_data"
mkdir -p "$USER_DATA" \
"$HOME/.config/hypr/shaders" \
"$HOME/.config/matugen/templates"
# Copy hypridle config; -n = do not overwrite if already customised
if cp -n "$REPO_DIR/src/config/hypridle.conf" "$HOME/.config/hypr/" 2>/dev/null; then
log_ok "hypridle.conf → ~/.config/hypr/"
else
log_info "hypridle.conf already exists — not overwritten"
fi
printf '{"configProvider": "%s"}\n' "$CONFIG_TYPE" > "$USER_DATA/config_Provider.json"
printf '{}\n' > "$USER_DATA/keybinds.json"
log_ok "Config dirs created"
log_ok "config_Provider.json → $CONFIG_TYPE"
log_info "Initializing cache directories..."
mkdir -p "$HOME/.cache/brain-shell"
touch "$HOME/.cache/brain-shell/colors.json"
mkdir -p "$HOME/Pictures/Wallpapers"
cp -n -r "$REPO_DIR/src/assets/wallpapers"/* "$HOME/Pictures/Wallpapers/" 2>/dev/null || true
log_ok "Cache directories initialized"
# ── Keybind Conflict Detection ────────────────────────────────────────────────
echo ""
log_info "Checking keybind conflicts against active Hyprland session..."
python3 << 'PYEOF' || log_warn "Keybind check skipped (Python error or no Hyprland session)."
import subprocess, json, os, sys
DEFAULTS = {
"dashboard-home": {"mods": "SUPER", "key": "D", "label": "Dashboard: System"},
"dashboard-stats": {"mods": "CTRL + SHIFT", "key": "ESCAPE", "label": "Dashboard: Home"},
"dashboard-kanban": {"mods": "SUPER", "key": "Z", "label": "Dashboard: Tasks"},
"dashboard-launcher": {"mods": "SUPER", "key": "Q", "label": "Dashboard: Apps"},
"dashboard-config": {"mods": "SUPER", "key": "C", "label": "Dashboard: Config"},
"PowerMenu-toggle": {"mods": "SUPER", "key": "ESCAPE", "label": "Power Menu"},
"notification-toggle": {"mods": "SUPER", "key": "N", "label": "Notifications"},
"wallpaper-toggle": {"mods": "SUPER", "key": "W", "label": "Wallpaper"},
"clipboard-toggle": {"mods": "SUPER", "key": "V", "label": "Clipboard"},
"wifi-toggle": {"mods": "SUPER + ALT", "key": "W", "label": "Network: Wi-Fi"},
"bluetooth-toggle": {"mods": "SUPER + ALT", "key": "B", "label": "Network: Bluetooth"},
"vpn-toggle": {"mods": "SUPER + ALT", "key": "G", "label": "Network: VPN"},
"hotspot-toggle": {"mods": "SUPER + ALT", "key": "H", "label": "Network: Hotspot"},
"audioOut-toggle": {"mods": "SUPER", "key": "A", "label": "Audio: Output"},
"audioIn-toggle": {"mods": "SUPER + ALT", "key": "I", "label": "Audio: Input"},
"audioMix-toggle": {"mods": "SUPER", "key": "M", "label": "Audio: Mixer"},
"focus-toggle": {"mods": "SUPER", "key": "B", "label": "Focus Mode"},
"screenrec-on": {"mods": "ALT", "key": "F9", "label": "Screen Record"},
}
MOD_BITS = {"SHIFT": 1, "CTRL": 4, "ALT": 8, "SUPER": 64}
def mods_to_mask(mods_str):
mask = 0
for part in mods_str.upper().split("+"):
mask |= MOD_BITS.get(part.strip(), 0)
return mask
try:
raw = subprocess.check_output(["hyprctl", "binds", "-j"], stderr=subprocess.DEVNULL).decode()
hypr_binds = json.loads(raw)
except Exception:
print(" \033[2m(not inside Hyprland — skipping live conflict check)\033[0m")
sys.exit(0)
conflicts = {}
for action, data in DEFAULTS.items():
mask = mods_to_mask(data["mods"])
key = data["key"].lower()
for hb in hypr_binds:
if hb.get("submap", "") or hb.get("mouse"):
continue
if hb.get("modmask") == mask and str(hb.get("key", "")).lower() == key:
desc = hb.get("dispatcher", "")
arg = hb.get("arg", "")
conflicts[action] = {
"bind": f"{data['mods']} + {data['key']}",
"label": data["label"],
"used_by": f"{desc} {arg}".strip(),
}
break
if not conflicts:
print(" \033[0;32m✓\033[0m No keybind conflicts detected.")
sys.exit(0)
print(f"\n \033[0;31m✗\033[0m {len(conflicts)} conflict(s) found:\n")
unbound = {}
for action, info in conflicts.items():
print(f" \033[1m{info['bind']:<24}\033[0m {info['label']}")
print(f" {'':24} already used by: {info['used_by']}\n")
unbound[action] = {"mods": "", "key": ""}
config_path = os.path.expanduser("~/.config/Brain_Shell/src/user_data/keybinds.json")
with open(config_path, "w") as f:
json.dump(unbound, f, indent=2)
print(" \033[1;33m⚠\033[0m Conflicting binds left unbound in Brain Shell.")
print(" Re-assign them: Dashboard → Config → Keybinds\n")
PYEOF
# ══════════════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════════════
echo ""
echo -e " ${DIM}$(printf '%.0s─' {1..50})${NC}"
if [[ ${#FAILED_PKGS[@]} -eq 0 ]]; then
log_ok "Arch installation complete — no failures."
echo ""
else
log_warn "Installation finished with ${#FAILED_PKGS[@]} unresolved package(s)."
echo ""
echo -e " ${BOLD}Retry commands:${NC}"
for entry in "${FAILED_PKGS[@]}"; do
_src="${entry%%:*}"
_pkg="${entry#*:}"
# Strip any parenthetical note before displaying the install command
_pkg_name="${_pkg%% (*}"
if [[ "$_src" == "pacman" ]]; then
log_info "sudo pacman -S $_pkg_name"
else
log_info "$AUR_HELPER -S $_pkg_name"
fi
done
echo ""
echo -e " ${BOLD}Resolving hard package conflicts (ConflictsWith):${NC}"
log_info "Find what conflicts: pacman -Si <pkg> | grep Conflicts"
log_info "Remove the old one: sudo pacman -Rdd <conflicting-pkg>"
log_info "Then retry: sudo pacman -S <pkg>"
echo ""
fi
exit 0