#!/bin/bash # tui-install.sh — TUI installer for the_miro's Arch dotfiles # -u: treat unset variables as errors; -o pipefail: a pipe fails if any stage fails. # Intentionally omits -e so individual module failures can be handled explicitly. set -uo pipefail # ── Paths ───────────────────────────────────────────────────────────────────── # Resolve the installer's own directory regardless of where the script was called from. # BASH_SOURCE[0] stays correct even when the script is sourced rather than executed directly. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Dotfiles root is one level up from setup/. DOTFILES_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" MODULES="$DOTFILES_DIR/setup/modules" APPS="$MODULES/optional-Modules/apps" LOG="$HOME/dotfiles-install.log" # Private scratch space for the custom DIALOGRC theme file and a temporary colors.conf. # mktemp -d creates a uniquely named directory that only this process owns. TMP_D="$(mktemp -d)" # Cleanup trap: remove the scratch dir and restore the terminal state on any exit signal. # tput reset issues a full terminal reset sequence; stty sane is the bare-console fallback. # '|| true' ensures the trap never returns a non-zero code, which would mask the real exit status. trap 'rm -rf "$TMP_D"; tput reset 2>/dev/null || stty sane 2>/dev/null || true' EXIT INT TERM HUP # Allow the caller to inject a different answerfile path via the ANSWERFILE env var. # The default /answerfile.json is a conventional location for CI/PXE boot images. ANSWERFILE="${ANSWERFILE:-/answerfile.json}" ANSWERFILE_MODE=false # Enable unattended mode only when the answerfile is actually present on disk. [[ -f "$ANSWERFILE" ]] && ANSWERFILE_MODE=true BACKTITLE="the_miro's Arch Dotfiles" # ── Terminal dimensions (bare console safe) ─────────────────────────────────── # tput may fail on a bare TTY with no TERM set; fall back to safe 80×24 defaults # so dialog sizing arithmetic never operates on empty strings. TERM_H=$(tput lines 2>/dev/null || echo 24) TERM_W=$(tput cols 2>/dev/null || echo 80) # ── Cyberqueer dialog theme ─────────────────────────────────────────────────── # dialog reads its color scheme from $DIALOGRC at startup. # Writing it to TMP_D (rather than ~/.dialogrc) avoids permanently altering # the user's existing dialog configuration on the live system. export DIALOGRC="$TMP_D/dialogrc" # The heredoc uses single-quoted 'EOF' so no variable expansion happens inside; # every value is a literal dialog color tuple: (FG, BG, BOLD). cat > "$DIALOGRC" <<'EOF' use_shadow = ON use_colors = ON screen_color = (BLACK,BLACK,ON) shadow_color = (BLACK,BLACK,ON) title_color = (MAGENTA,BLACK,ON) border_color = (MAGENTA,BLACK,ON) button_active_color = (BLACK,MAGENTA,ON) button_inactive_color = (WHITE,BLACK,OFF) button_key_active_color = (BLACK,CYAN,ON) button_key_inactive_color = (CYAN,BLACK,ON) button_label_active_color = (BLACK,MAGENTA,ON) button_label_inactive_color = (WHITE,BLACK,OFF) inputbox_color = (WHITE,BLACK,OFF) inputbox_border_color = (MAGENTA,BLACK,ON) menubox_color = (WHITE,BLACK,OFF) menubox_border_color = (MAGENTA,BLACK,ON) item_color = (WHITE,BLACK,OFF) item_selected_color = (BLACK,MAGENTA,ON) tag_color = (CYAN,BLACK,ON) tag_selected_color = (BLACK,CYAN,ON) tag_key_color = (CYAN,BLACK,ON) tag_key_selected_color = (BLACK,CYAN,ON) check_color = (WHITE,BLACK,OFF) check_selected_color = (BLACK,MAGENTA,ON) uarrow_color = (MAGENTA,BLACK,ON) darrow_color = (MAGENTA,BLACK,ON) EOF # ── State ───────────────────────────────────────────────────────────────────── # STEP counts completed modules; TOTAL is pre-computed by count_steps() before # installation begins so run_module() can print accurate [N/TOTAL] progress. STEP=0 TOTAL=0 # ── Helpers ─────────────────────────────────────────────────────────────────── require_dialog() { # Short-circuit if dialog is already present; otherwise bootstrap it with pacman. # This allows the script to be run on a fresh Arch install that hasn't yet installed dialog. command -v dialog &>/dev/null && return echo "dialog not found — installing..." sudo pacman -S --noconfirm dialog || { echo "Failed to install dialog."; exit 1; } } require_jq() { # Same bootstrap pattern as require_dialog: jq is only needed in answerfile mode # but we install it on demand to avoid a hard dependency for interactive use. command -v jq &>/dev/null && return echo "jq not found — installing..." sudo pacman -S --noconfirm jq || { echo "Failed to install jq."; exit 1; } } die() { # Fatal error helper: clear the dialog overlay before printing so the message # is readable, then exit 1. Uses stderr to keep it out of any $() capture. clear printf "\n Error: %s\n\n" "$1" >&2 exit 1 } log_sep() { # Write a visible separator plus a timestamp to the log before each module run. # Appended with >> so earlier log entries are never overwritten mid-install. printf "\n══════════════════════════════════\n %s\n %s\n" "$1" "$(date)" >> "$LOG" } run_module() { local label="$1" script="$2" STEP=$(( STEP + 1 )) log_sep "[$STEP/$TOTAL] $label" # Clear the dialog overlay so module output scrolls cleanly on the raw terminal. clear printf "\n\033[1;35m [$STEP/$TOTAL] %s\033[0m\n" "$label" printf "\033[35m ─────────────────────────────────────────────\033[0m\n\n" # Run the module script, merging stderr into stdout, and tee to the log. # PIPESTATUS[0] captures the exit code of 'bash "$script"' even though # 'tee' is the last command in the pipe (and would always succeed). local rc=0 bash "$script" 2>&1 | tee -a "$LOG" || rc=${PIPESTATUS[0]} if [[ $rc -ne 0 ]]; then if [[ $ANSWERFILE_MODE == true ]]; then # Unattended mode: log the failure and keep going; aborting mid-CI # would require a full manual re-run. printf "\n Warning: %s exited with code %d — continuing.\n" "$label" "$rc" | tee -a "$LOG" else # Interactive mode: ask the user whether to abort or continue. # 'dialog --yesno' returns 0 for Yes and 1 for No; the '|| { ...; exit 1; }' # fires on No, aborting the install. dialog --backtitle "$BACKTITLE" \ --title " Module Failed " \ --yesno "$label exited with code $rc.\n\nContinue anyway?" 8 54 \ || { clear; exit 1; } fi fi } count_steps() { # Pre-count the total number of modules that will actually run so that # run_module() can display accurate [N/TOTAL] progress from the first step. # Each glob test mirrors the corresponding run_module() call below; they # must stay in sync whenever a new module is added or removed. local c="$1" de="$2" a="${3:-}" TOTAL=0 # Base components: each keyword maps to exactly one module script. [[ "$c" == *"pkg"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *"core"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *"svc"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *"shell"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *"plymouth"* ]] && TOTAL=$(( TOTAL + 1 )) # A non-"none" DE selection always installs exactly one DE module. [[ "$de" != "none" ]] && TOTAL=$(( TOTAL + 1 )) # Optional app modules: one glob check per app, one increment per match. # Glob syntax *"tag"* matches if the space-separated SELECTED_APPS string # contains the tag anywhere — works because tags never share substrings. # BEGIN GENERATED MODULES: module-counters [[ "$a" == *"ollama"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"llama-cpp"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"open-webui"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"claude"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"networking-cli"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"disk-recovery"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"himalaya"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"mail-notmuch"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"caldav-sync"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"wireshark"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"anti-malware"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"gnuplot"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"blender-povray"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"toot"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"db-clients"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"mysql"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"productivity"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"python"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"k8s"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"docker"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"tlp"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"butter"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"localsend"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"croc"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"opendeck"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"localtunnel"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"timeshift"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"zfs"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"wprs"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"plymouth"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"plymouth-custom"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"steam"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"vesktop"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"spotify"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"prism"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"vintagestory"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"openarena"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"tetris"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"doom"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"sauerbraten"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"stuntrally"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"onlyoffice"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"xournal"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"rnote"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"obsidian"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"tangent-notes"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"ffmpeg"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"sox"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"imagemagick"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"yt-dlp"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"gimp"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"inkscape"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"krita"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"kdenlive"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"openshot"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"shotcut"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"ardour"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"audacity"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"lmms"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"mixxx"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"cecilia"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"chromium"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"firefox-browser"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"zen-browser"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"nyxt"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"librewolf"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"min-browser"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"vscodium"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"zed-ide"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"geany"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"codeblocks"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"kate"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"rdp-client"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"lamco-rdp-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"qemu"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"freeipa-client"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 )) # END GENERATED MODULES: module-counters } # ── Answerfile ──────────────────────────────────────────────────────────────── # AF_* variables hold values parsed from the JSON answerfile. # They are initialised to safe defaults so the rest of the script can reference # them unconditionally whether or not answerfile mode is active. AF_HOSTNAME="" AF_COMPONENTS="" AF_DE="none" AF_APPS="" AF_SHELL_RC="dotfiles" AF_COLOR_TEXT="" AF_COLOR_BG="" AF_COLOR_HIGHLIGHT="" AF_COLOR_DARK="" AF_COLOR_RED="" load_answerfile() { require_jq # -r outputs raw strings without JSON quoting. # '// ""' / '// "none"' are jq's null-coalescing operator: if the key is # absent from the file the expression yields the fallback instead. AF_HOSTNAME=$(jq -r '.hostname // ""' "$ANSWERFILE") # JSON arrays are flattened to a space-separated string to match the format # that COMPONENTS and SELECTED_APPS expect throughout the rest of the script. AF_COMPONENTS=$(jq -r '(.components // []) | join(" ")' "$ANSWERFILE") AF_DE=$(jq -r '.desktop_environment // "none"' "$ANSWERFILE") AF_APPS=$(jq -r '(.apps // []) | join(" ")' "$ANSWERFILE") AF_SHELL_RC=$(jq -r '.shell_rc // "dotfiles"' "$ANSWERFILE") # Color values are optional; an empty string means "keep the repo default". AF_COLOR_TEXT=$(jq -r '.colors.COLOR_TEXT // ""' "$ANSWERFILE") AF_COLOR_BG=$(jq -r '.colors.COLOR_BG // ""' "$ANSWERFILE") AF_COLOR_HIGHLIGHT=$(jq -r '.colors.COLOR_HIGHLIGHT // ""' "$ANSWERFILE") AF_COLOR_DARK=$(jq -r '.colors.COLOR_DARK // ""' "$ANSWERFILE") AF_COLOR_RED=$(jq -r '.colors.COLOR_RED // ""' "$ANSWERFILE") } # ── MAC address helper ──────────────────────────────────────────────────────── get_mac_suffix() { # Returns the MAC address of the first non-loopback interface, colons stripped. # Used to make answerfile-derived hostnames unique per machine (e.g. "myhost-aabbccddee"). local mac # awk state machine: sets iface=1 when it sees an interface line whose name # does NOT start with "lo" ([^l][^o] skips "lo"), then captures the # 'link/ether XX:XX:XX:XX:XX:XX' line and exits immediately after the first hit. mac=$(ip link show 2>/dev/null \ | awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}') # Strip colons from the MAC so it is safe to embed in a hostname (only [a-z0-9-] allowed). printf '%s' "${mac//:/}" } # ── Preflight ───────────────────────────────────────────────────────────────── if [[ $EUID -eq 0 ]]; then # Root context (e.g. archiso chroot): shim sudo as a passthrough # Module scripts call 'sudo' for privilege operations; when we are already root # there is no sudo binary (or it may be absent), so inject a fake one that is # just 'exec "$@"' — transparently forwarding all arguments. mkdir -p "$TMP_D/bin" printf '#!/bin/bash\nexec "$@"\n' > "$TMP_D/bin/sudo" chmod +x "$TMP_D/bin/sudo" # Prepend to PATH so this shim takes precedence over any real sudo. export PATH="$TMP_D/bin:$PATH" fi # Hard guard: pacman is the package manager used by every module; without it # the installer cannot function on this OS. command -v pacman &>/dev/null || die "pacman not found — Arch Linux required." # Ensure the dialog binary is available before we try to use it. require_dialog if $ANSWERFILE_MODE; then load_answerfile printf "Answerfile mode: %s\n" "$ANSWERFILE" | tee -a "$LOG" fi # Network check: -c1 sends one ICMP echo, -W3 waits up to 3 seconds for reply. # We test against archlinux.org because that's where pacman will fetch packages. if ! ping -c1 -W3 archlinux.org &>/dev/null; then if $ANSWERFILE_MODE; then # In unattended mode just warn; the caller decides whether the image # is expected to be offline (e.g. local mirror pre-seeded in pacman). printf "Warning: no internet connection detected.\n" | tee -a "$LOG" else # Give the user a chance to plug in a cable or run iwctl before we # re-test. A second failed ping offers a soft-abort rather than a # hard failure, in case the user wants to proceed with a local mirror. dialog --backtitle "$BACKTITLE" \ --title " No Network Detected " \ --msgbox "\n No internet connection found.\n\n Wired: ensure the cable is plugged in.\n WiFi: switch to another TTY (Alt+F2)\n and run: iwctl\n\n Press OK once connected.\n" 13 58 if ! ping -c1 -W3 archlinux.org &>/dev/null; then dialog --backtitle "$BACKTITLE" \ --title " Still Offline " \ --yesno "\n Still no internet connection.\n\n Packages cannot be downloaded without network access.\n\n Continue anyway?" 11 58 \ || { clear; echo "Aborted — no network."; exit 1; } fi fi fi # Truncate the log file now that preflight passed; all prior output was to stdout. # Entries appended from this point on are the authoritative install log. > "$LOG" printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >> "$LOG" # ── Welcome ─────────────────────────────────────────────────────────────────── if ! $ANSWERFILE_MODE; then dialog --backtitle "$BACKTITLE" \ --title " Welcome " \ --msgbox "\n\ the_miro's Arch dotfiles installer\n\ Cyberqueer · Wayland · Hyprland\n\ ─────────────────────────────────────────\n\ \n\ Arch Linux — network admin, development & gaming\n\ \n\ Source: $DOTFILES_DIR\n\ Log: $LOG\n" 14 62 fi # ── Hostname ────────────────────────────────────────────────────────────────── HOSTNAME_SET="" if $ANSWERFILE_MODE; then if [[ -n "$AF_HOSTNAME" ]]; then # Append the stripped MAC address to the base name from the answerfile. # This makes each cloned machine's hostname unique without needing per-host # answerfiles (useful for mass-provisioning identical images). MAC=$(get_mac_suffix) HOSTNAME_SET="${AF_HOSTNAME}-${MAC}" printf "Hostname (from answerfile + MAC): %s\n" "$HOSTNAME_SET" | tee -a "$LOG" fi else # dialog --inputbox: 3>&1 1>&2 2>&3 swaps fd1 and fd2 so the user's typed # value (written to stdout by dialog) is captured by the $() subshell, while # dialog's UI (which needs stderr for the terminal) flows to the actual terminal. # The '|| HOSTNAME_INPUT=""' handles the user pressing Esc (dialog returns 1). HOSTNAME_INPUT=$(dialog --backtitle "$BACKTITLE" \ --title " Hostname " \ --inputbox "\n Hostname for this machine (leave blank to keep default).\n" 9 54 "" \ 3>&1 1>&2 2>&3) || HOSTNAME_INPUT="" HOSTNAME_SET="$HOSTNAME_INPUT" fi if [[ -n "$HOSTNAME_SET" ]]; then # hostnamectl is the preferred method on systemd systems; fall back to writing # /etc/hostname directly for minimal chroot environments that lack systemd. sudo hostnamectl set-hostname "$HOSTNAME_SET" 2>/dev/null \ || echo "$HOSTNAME_SET" | sudo tee /etc/hostname > /dev/null printf "Hostname set: %s\n" "$HOSTNAME_SET" >> "$LOG" fi # ── Component selection ─────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then COMPONENTS="$AF_COMPONENTS" else # dialog --checklist args: height width list-height, then triplets of tag desc state. # All four components are pre-selected ("on") because they form the expected base. # The 3>&1 1>&2 2>&3 fd swap captures dialog's output (the selected tags) via $(). # Esc / Cancel returns exit code 1; the '|| { ...; exit 0; }' treats that as a clean abort. COMPONENTS=$(dialog --backtitle "$BACKTITLE" \ --title " Select Components " \ --checklist "Space toggles · Enter confirms · Esc quits" 16 68 5 \ "pkg" "Package managers yay · nvm · rust" on \ "core" "Core packages 100+ base system packages" on \ "svc" "Core services NetworkManager · cronie · fail2ban" on \ "shell" "Shell setup zsh · nvim · yazi · micro · starship" on \ "plymouth" "Plymouth boot splash — skull logo + spinner" on \ 3>&1 1>&2 2>&3) || { clear; echo "Aborted."; exit 0; } fi # ── DE selection ────────────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then DE="$AF_DE" else # dialog --menu is a single-choice list; it outputs the selected tag. # Esc returns exit code 1 — '|| DE="none"' defaults to skipping the DE, # preserving whatever DE the user already has installed. DE=$(dialog --backtitle "$BACKTITLE" \ --title " Desktop Environment " \ --menu "Select a desktop environment · Esc / none to skip:" 24 72 11 \ "hyprlua" "HyprLua — Hyprland with Lua config (recommended)" \ "niri" "Niri — scrollable-tiling Wayland compositor" \ "hyprland" "Hyprland — Wayland WM, hyprlang config (legacy)" \ "sway" "Sway — Wayland tiling WM" \ "kde-plasma" "KDE Plasma — feature-rich Wayland/X11 DE" \ "gnome" "GNOME — modern Wayland DE" \ "cosmic" "COSMIC — Rust-built Wayland DE (System76)" \ "xfce" "XFCE — lightweight X11 DE" \ "lxqt" "LXQt — lightweight Qt X11 DE" \ "none" "Skip DE installation" \ 3>&1 1>&2 2>&3) || DE="none" fi # ── Apps selection ──────────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then SELECTED_APPS="$AF_APPS" else # Cap the dialog box at 40 rows but shrink to fit smaller terminals. # The list height is the dialog height minus 8 rows of chrome (title, borders, # prompt, buttons), with a minimum of 4 to remain usable on tiny screens. _APP_H=$(( TERM_H - 2 < 40 ? TERM_H - 2 : 40 )) _APP_LIST_H=$(( _APP_H - 8 < 4 ? 4 : _APP_H - 8 )) # BEGIN GENERATED MODULES: module-checklist SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \ --title " Applications " \ --checklist "Optional applications — installed after base components:" "$_APP_H" 76 "$_APP_LIST_H" \ "ollama" "ollama local LLM runner and API server" off \ "llama-cpp" "llama-cpp standalone LLM inference CLI and server" off \ "open-webui" "open-webui browser UI for Ollama and LLM backends" off \ "claude" "claude Anthropic Claude Code CLI via npm" off \ "networking-cli" "networking-cli nmap, nethogs, mitmproxy, httpie" off \ "disk-recovery" "disk-recovery ddrescue and f3 disk recovery tools" off \ "himalaya" "himalaya terminal email client (AUR)" off \ "mail-notmuch" "mail-notmuch isync, msmtp, notmuch, alot mail stack" off \ "caldav-sync" "caldav-sync vdirsyncer and khal CalDAV calendar sync" off \ "ssh-server" "ssh-server openssh with key-auth and systemd unit enabled" off \ "wireshark" "wireshark network packet analyser GUI" off \ "anti-malware" "anti-malware ClamAV, rkhunter, chkrootkit" off \ "gnuplot" "gnuplot scientific plotting tool" off \ "blender-povray" "blender-povray 3D modelling and ray-tracing (Blender + POV-Ray)" off \ "toot" "toot Mastodon CLI client (AUR)" off \ "db-clients" "db-clients pgcli and mycli interactive database CLIs" off \ "mysql" "mysql MariaDB server with initial setup" off \ "productivity" "productivity taskwarrior, watson, jrnl — task management and time tracking" off \ "python" "python pyright, pipx, pynvim Python tooling" on \ "k8s" "k8s kubectl and podman-desktop Kubernetes tools" off \ "docker" "docker docker and docker-compose" off \ "podman" "podman rootless containers with buildah" off \ "cockpit" "cockpit web UI for machines and containers" off \ "tlp" "tlp laptop battery optimisation" off \ "butter" "butter btrfs snapshot backup manager (AUR)" off \ "localsend" "localsend LAN file transfer, AirDrop-like (AUR)" off \ "croc" "croc cross-platform encrypted file transfer" off \ "opendeck" "opendeck Stream Deck controller — ydotool + OpenDeck (Flatpak)" off \ "localtunnel" "localtunnel expose localhost over a public URL" off \ "timeshift" "timeshift system snapshot and backup with autosnap" off \ "zfs" "zfs zfs-dkms kernel module" off \ "wprs" "wprs Wayland proxy for remote sessions (wprs-git, AUR)" off \ "plymouth" "plymouth boot splash — bundled skull logo and spinner" on \ "plymouth-custom" "plymouth-custom boot splash with a user-supplied image" off \ "steam" "steam Steam gaming platform" off \ "vesktop" "vesktop Discord client with Vencord theme" off \ "spotify" "spotify Spotify launcher with Spicetify theming" off \ "prism" "prism PrismLauncher Minecraft launcher (Flatpak)" off \ "vintagestory" "vintagestory Vintage Story survival game (AUR)" off \ "openarena" "openarena open-source Quake III Arena" off \ "tetris" "tetris bastet and vitetris terminal Tetris" off \ "doom" "doom Chocolate Doom with Freedoom data" off \ "sauerbraten" "sauerbraten Sauerbraten open-source FPS (Cube 2)" off \ "stuntrally" "stuntrally Stunt Rally racing game (Flatpak)" off \ "onlyoffice" "onlyoffice office suite — Docs, Sheets, Slides (AUR)" on \ "xournal" "xournal note-taking and PDF annotator" off \ "rnote" "rnote handwriting and note-taking with stylus support (Flatpak)" off \ "obsidian" "obsidian knowledge base and Markdown note-taking (Flatpak)" off \ "tangent-notes" "tangent-notes networked Markdown note-taking (Flatpak)" off \ "ffmpeg" "ffmpeg GStreamer codecs and ffmpegthumbnailer" off \ "sox" "sox command-line audio processing toolkit" off \ "imagemagick" "imagemagick image manipulation suite" off \ "yt-dlp" "yt-dlp YouTube and media downloader" off \ "gimp" "gimp GNU Image Manipulation Program" off \ "inkscape" "inkscape vector graphics editor" off \ "krita" "krita digital painting and illustration" off \ "kdenlive" "kdenlive KDE non-linear video editor" off \ "openshot" "openshot cross-platform video editor" off \ "shotcut" "shotcut cross-platform video editor" off \ "ardour" "ardour professional DAW" off \ "audacity" "audacity multi-track audio editor" off \ "lmms" "lmms Linux MultiMedia Studio music production" off \ "mixxx" "mixxx DJ mixing software" off \ "cecilia" "cecilia audio synthesis and signal processing (AUR)" off \ "chromium" "chromium open-source Chromium browser (official)" off \ "firefox-browser" "firefox-browser Mozilla Firefox (official)" on \ "zen-browser" "zen-browser privacy-focused Firefox fork (AUR)" off \ "nyxt" "nyxt keyboard-driven hackable browser (AUR)" off \ "librewolf" "librewolf hardened Firefox fork (AUR)" off \ "min-browser" "min-browser minimal Electron browser (AUR)" off \ "vscodium" "vscodium telemetry-free VS Code build (AUR)" off \ "zed-ide" "zed-ide high-performance Rust IDE (official)" off \ "geany" "geany lightweight IDE with plugins (official)" off \ "codeblocks" "codeblocks C/C++ IDE (official)" off \ "kate" "kate KDE advanced text editor (official)" off \ "rdp-client" "rdp-client Remmina with FreeRDP and VNC plugins" off \ "lamco-rdp-server" "lamco-rdp-server native Wayland RDP server (AUR, Rust)" off \ "qemu" "qemu full QEMU/KVM stack with virt-manager" off \ "freeipa-client" "freeipa-client sssd and ipa-client-install with auto-enrollment" off \ "freeipa-server" "freeipa-server interactive FreeIPA server setup with client generator" off \ 3>&1 1>&2 2>&3) || SELECTED_APPS="" # END GENERATED MODULES: module-checklist fi # ── Shell RC preference ─────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then SHELL_RC="$AF_SHELL_RC" else SHELL_RC=$(dialog --backtitle "$BACKTITLE" \ --title " Shell Config for New Users " \ --menu "\n Should new users on this machine inherit the dotfiles' rc files?\n (Controls what gets copied to /etc/skel)\n" 13 68 2 \ "dotfiles" "Use the_miro's .zshrc / .bashrc / .vimrc from dotfiles" \ "defaults" "Skip — use system defaults for new users" \ 3>&1 1>&2 2>&3) || SHELL_RC="defaults" fi # ── Confirmation (interactive mode only) ────────────────────────────────────── if ! $ANSWERFILE_MODE; then # Build a human-readable summary of everything that will be installed so the # user can review the full list before any changes are made to the system. SUMMARY="" [[ -n "$HOSTNAME_SET" ]] && SUMMARY+=" ✦ Hostname: $HOSTNAME_SET\n" [[ "$COMPONENTS" == *"pkg"* ]] && SUMMARY+=" ✦ Package managers (yay, nvm, rust)\n" [[ "$COMPONENTS" == *"core"* ]] && SUMMARY+=" ✦ Core packages\n" [[ "$COMPONENTS" == *"svc"* ]] && SUMMARY+=" ✦ Core services\n" [[ "$COMPONENTS" == *"shell"* ]] && SUMMARY+=" ✦ Shell setup\n" [[ "$COMPONENTS" == *"plymouth"* ]] && SUMMARY+=" ✦ Plymouth boot splash\n" [[ "$DE" != "none" && "$DE" != "" ]] && SUMMARY+=" ✦ Desktop environment: $DE\n" [[ "$SHELL_RC" == "dotfiles" ]] && SUMMARY+=" ✦ Shell rc files → /etc/skel (dotfiles)\n" \ || SUMMARY+=" ✦ Shell rc files → /etc/skel (system defaults)\n" if [[ -n "$SELECTED_APPS" ]]; then SUMMARY+="\n Applications:\n" # BEGIN GENERATED MODULES: module-summary [[ "$SELECTED_APPS" == *"ollama"* ]] && SUMMARY+=" ✦ ollama\n" [[ "$SELECTED_APPS" == *"llama-cpp"* ]] && SUMMARY+=" ✦ llama-cpp\n" [[ "$SELECTED_APPS" == *"open-webui"* ]] && SUMMARY+=" ✦ open-webui\n" [[ "$SELECTED_APPS" == *"claude"* ]] && SUMMARY+=" ✦ claude\n" [[ "$SELECTED_APPS" == *"networking-cli"* ]] && SUMMARY+=" ✦ networking-cli\n" [[ "$SELECTED_APPS" == *"disk-recovery"* ]] && SUMMARY+=" ✦ disk-recovery\n" [[ "$SELECTED_APPS" == *"himalaya"* ]] && SUMMARY+=" ✦ himalaya\n" [[ "$SELECTED_APPS" == *"mail-notmuch"* ]] && SUMMARY+=" ✦ mail-notmuch\n" [[ "$SELECTED_APPS" == *"caldav-sync"* ]] && SUMMARY+=" ✦ caldav-sync\n" [[ "$SELECTED_APPS" == *"ssh-server"* ]] && SUMMARY+=" ✦ ssh-server\n" [[ "$SELECTED_APPS" == *"wireshark"* ]] && SUMMARY+=" ✦ wireshark\n" [[ "$SELECTED_APPS" == *"anti-malware"* ]] && SUMMARY+=" ✦ anti-malware\n" [[ "$SELECTED_APPS" == *"gnuplot"* ]] && SUMMARY+=" ✦ gnuplot\n" [[ "$SELECTED_APPS" == *"blender-povray"* ]] && SUMMARY+=" ✦ blender-povray\n" [[ "$SELECTED_APPS" == *"toot"* ]] && SUMMARY+=" ✦ toot\n" [[ "$SELECTED_APPS" == *"db-clients"* ]] && SUMMARY+=" ✦ db-clients\n" [[ "$SELECTED_APPS" == *"mysql"* ]] && SUMMARY+=" ✦ mysql\n" [[ "$SELECTED_APPS" == *"productivity"* ]] && SUMMARY+=" ✦ productivity\n" [[ "$SELECTED_APPS" == *"python"* ]] && SUMMARY+=" ✦ python\n" [[ "$SELECTED_APPS" == *"k8s"* ]] && SUMMARY+=" ✦ k8s\n" [[ "$SELECTED_APPS" == *"docker"* ]] && SUMMARY+=" ✦ docker\n" [[ "$SELECTED_APPS" == *"podman"* ]] && SUMMARY+=" ✦ podman\n" [[ "$SELECTED_APPS" == *"cockpit"* ]] && SUMMARY+=" ✦ cockpit\n" [[ "$SELECTED_APPS" == *"tlp"* ]] && SUMMARY+=" ✦ tlp\n" [[ "$SELECTED_APPS" == *"butter"* ]] && SUMMARY+=" ✦ butter\n" [[ "$SELECTED_APPS" == *"localsend"* ]] && SUMMARY+=" ✦ localsend\n" [[ "$SELECTED_APPS" == *"croc"* ]] && SUMMARY+=" ✦ croc\n" [[ "$SELECTED_APPS" == *"opendeck"* ]] && SUMMARY+=" ✦ opendeck\n" [[ "$SELECTED_APPS" == *"localtunnel"* ]] && SUMMARY+=" ✦ localtunnel\n" [[ "$SELECTED_APPS" == *"timeshift"* ]] && SUMMARY+=" ✦ timeshift\n" [[ "$SELECTED_APPS" == *"zfs"* ]] && SUMMARY+=" ✦ zfs\n" [[ "$SELECTED_APPS" == *"wprs"* ]] && SUMMARY+=" ✦ wprs\n" [[ "$SELECTED_APPS" == *"plymouth"* ]] && SUMMARY+=" ✦ plymouth\n" [[ "$SELECTED_APPS" == *"plymouth-custom"* ]] && SUMMARY+=" ✦ plymouth-custom\n" [[ "$SELECTED_APPS" == *"steam"* ]] && SUMMARY+=" ✦ steam\n" [[ "$SELECTED_APPS" == *"vesktop"* ]] && SUMMARY+=" ✦ vesktop\n" [[ "$SELECTED_APPS" == *"spotify"* ]] && SUMMARY+=" ✦ spotify\n" [[ "$SELECTED_APPS" == *"prism"* ]] && SUMMARY+=" ✦ prism\n" [[ "$SELECTED_APPS" == *"vintagestory"* ]] && SUMMARY+=" ✦ vintagestory\n" [[ "$SELECTED_APPS" == *"openarena"* ]] && SUMMARY+=" ✦ openarena\n" [[ "$SELECTED_APPS" == *"tetris"* ]] && SUMMARY+=" ✦ tetris\n" [[ "$SELECTED_APPS" == *"doom"* ]] && SUMMARY+=" ✦ doom\n" [[ "$SELECTED_APPS" == *"sauerbraten"* ]] && SUMMARY+=" ✦ sauerbraten\n" [[ "$SELECTED_APPS" == *"stuntrally"* ]] && SUMMARY+=" ✦ stuntrally\n" [[ "$SELECTED_APPS" == *"onlyoffice"* ]] && SUMMARY+=" ✦ onlyoffice\n" [[ "$SELECTED_APPS" == *"xournal"* ]] && SUMMARY+=" ✦ xournal\n" [[ "$SELECTED_APPS" == *"rnote"* ]] && SUMMARY+=" ✦ rnote\n" [[ "$SELECTED_APPS" == *"obsidian"* ]] && SUMMARY+=" ✦ obsidian\n" [[ "$SELECTED_APPS" == *"tangent-notes"* ]] && SUMMARY+=" ✦ tangent-notes\n" [[ "$SELECTED_APPS" == *"ffmpeg"* ]] && SUMMARY+=" ✦ ffmpeg\n" [[ "$SELECTED_APPS" == *"sox"* ]] && SUMMARY+=" ✦ sox\n" [[ "$SELECTED_APPS" == *"imagemagick"* ]] && SUMMARY+=" ✦ imagemagick\n" [[ "$SELECTED_APPS" == *"yt-dlp"* ]] && SUMMARY+=" ✦ yt-dlp\n" [[ "$SELECTED_APPS" == *"gimp"* ]] && SUMMARY+=" ✦ gimp\n" [[ "$SELECTED_APPS" == *"inkscape"* ]] && SUMMARY+=" ✦ inkscape\n" [[ "$SELECTED_APPS" == *"krita"* ]] && SUMMARY+=" ✦ krita\n" [[ "$SELECTED_APPS" == *"kdenlive"* ]] && SUMMARY+=" ✦ kdenlive\n" [[ "$SELECTED_APPS" == *"openshot"* ]] && SUMMARY+=" ✦ openshot\n" [[ "$SELECTED_APPS" == *"shotcut"* ]] && SUMMARY+=" ✦ shotcut\n" [[ "$SELECTED_APPS" == *"ardour"* ]] && SUMMARY+=" ✦ ardour\n" [[ "$SELECTED_APPS" == *"audacity"* ]] && SUMMARY+=" ✦ audacity\n" [[ "$SELECTED_APPS" == *"lmms"* ]] && SUMMARY+=" ✦ lmms\n" [[ "$SELECTED_APPS" == *"mixxx"* ]] && SUMMARY+=" ✦ mixxx\n" [[ "$SELECTED_APPS" == *"cecilia"* ]] && SUMMARY+=" ✦ cecilia\n" [[ "$SELECTED_APPS" == *"chromium"* ]] && SUMMARY+=" ✦ chromium\n" [[ "$SELECTED_APPS" == *"firefox-browser"* ]] && SUMMARY+=" ✦ firefox-browser\n" [[ "$SELECTED_APPS" == *"zen-browser"* ]] && SUMMARY+=" ✦ zen-browser\n" [[ "$SELECTED_APPS" == *"nyxt"* ]] && SUMMARY+=" ✦ nyxt\n" [[ "$SELECTED_APPS" == *"librewolf"* ]] && SUMMARY+=" ✦ librewolf\n" [[ "$SELECTED_APPS" == *"min-browser"* ]] && SUMMARY+=" ✦ min-browser\n" [[ "$SELECTED_APPS" == *"vscodium"* ]] && SUMMARY+=" ✦ vscodium\n" [[ "$SELECTED_APPS" == *"zed-ide"* ]] && SUMMARY+=" ✦ zed-ide\n" [[ "$SELECTED_APPS" == *"geany"* ]] && SUMMARY+=" ✦ geany\n" [[ "$SELECTED_APPS" == *"codeblocks"* ]] && SUMMARY+=" ✦ codeblocks\n" [[ "$SELECTED_APPS" == *"kate"* ]] && SUMMARY+=" ✦ kate\n" [[ "$SELECTED_APPS" == *"rdp-client"* ]] && SUMMARY+=" ✦ rdp-client\n" [[ "$SELECTED_APPS" == *"lamco-rdp-server"* ]] && SUMMARY+=" ✦ lamco-rdp-server\n" [[ "$SELECTED_APPS" == *"qemu"* ]] && SUMMARY+=" ✦ qemu\n" [[ "$SELECTED_APPS" == *"freeipa-client"* ]] && SUMMARY+=" ✦ freeipa-client\n" [[ "$SELECTED_APPS" == *"freeipa-server"* ]] && SUMMARY+=" ✦ freeipa-server\n" # END GENERATED MODULES: module-summary fi # Size the confirmation dialog to the terminal, capped at 24 rows. _CONF_H=$(( TERM_H - 2 < 24 ? TERM_H - 2 : 24 )) dialog --backtitle "$BACKTITLE" \ --title " Confirm Installation " \ --yesno "\n Components to install:\n\n${SUMMARY}\n Log: $LOG\n\n Proceed?" \ "$_CONF_H" 62 || { clear; echo "Aborted."; exit 0; } fi # Pre-count all selected steps before installation starts so run_module() can # display [N/TOTAL] with an accurate denominator from the very first module. count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS" # ── Installation: base components ───────────────────────────────────────────── # Each guard uses glob matching against the space-separated COMPONENTS string. # Order matters: package managers must be installed before packages that need yay/rust. [[ "$COMPONENTS" == *"pkg"* ]] && run_module "Package Managers" "$MODULES/package-managers.sh" [[ "$COMPONENTS" == *"core"* ]] && run_module "Core Packages" "$MODULES/core-packages.sh" [[ "$COMPONENTS" == *"svc"* ]] && run_module "Core Services" "$MODULES/core.sh" [[ "$COMPONENTS" == *"shell"* ]] && run_module "Shell Setup" "$MODULES/shell-setup.sh" [[ "$COMPONENTS" == *"plymouth"* ]] && run_module "Plymouth" "$MODULES/optional-Modules/plymouth.sh" # Route the single selected DE value to its corresponding install script. # "none" is the skip sentinel — no case branch matches it intentionally. if [[ "$DE" != "none" ]]; then case "$DE" in hyprlua) run_module "HyprLua" "$MODULES/Desktop-Environments/hyprlua.sh" ;; niri) run_module "Niri" "$MODULES/Desktop-Environments/niri.sh" ;; hyprland) run_module "Hyprland" "$MODULES/Desktop-Environments/hyprland.sh" ;; sway) run_module "Sway" "$MODULES/Desktop-Environments/sway.sh" ;; kde-plasma) run_module "KDE Plasma" "$MODULES/Desktop-Environments/kde-plasma.sh" ;; gnome) run_module "GNOME" "$MODULES/Desktop-Environments/gnome.sh" ;; cosmic) run_module "COSMIC" "$MODULES/Desktop-Environments/cosmic.sh" ;; xfce) run_module "XFCE" "$MODULES/Desktop-Environments/xfce.sh" ;; lxqt) run_module "LXQt" "$MODULES/Desktop-Environments/lxqt.sh" ;; esac fi # ── Installation: applications ──────────────────────────────────────────────── # Same guard pattern as base components. Each line is independent; a missing # module script will be caught by run_module()'s error handling rather than # silently skipped — 'bash "$script"' will exit non-zero if the file is absent. # BEGIN GENERATED MODULES: module-conflicts if [[ "$SELECTED_APPS" == *"plymouth"* && "$SELECTED_APPS" == *"plymouth-custom"* ]]; then warn "plymouth and plymouth-custom are mutually exclusive — skipping plymouth-custom" SELECTED_APPS="${SELECTED_APPS/plymouth-custom/}" fi if [[ "$SELECTED_APPS" == *"plymouth-custom"* && "$SELECTED_APPS" == *"plymouth"* ]]; then warn "plymouth-custom and plymouth are mutually exclusive — skipping plymouth" SELECTED_APPS="${SELECTED_APPS/plymouth/}" fi # END GENERATED MODULES: module-conflicts # BEGIN GENERATED MODULES: module-dispatch [[ "$SELECTED_APPS" == *"ollama"* ]] && run_module "ollama" "$APPS/ollama.sh" [[ "$SELECTED_APPS" == *"llama-cpp"* ]] && run_module "llama-cpp" "$APPS/llama-cpp.sh" [[ "$SELECTED_APPS" == *"open-webui"* ]] && run_module "open-webui" "$APPS/open-webui.sh" [[ "$SELECTED_APPS" == *"claude"* ]] && run_module "claude" "$APPS/claude.sh" [[ "$SELECTED_APPS" == *"networking-cli"* ]] && run_module "networking-cli" "$APPS/networking-cli.sh" [[ "$SELECTED_APPS" == *"disk-recovery"* ]] && run_module "disk-recovery" "$APPS/disk-recovery.sh" [[ "$SELECTED_APPS" == *"himalaya"* ]] && run_module "himalaya" "$APPS/himalaya.sh" [[ "$SELECTED_APPS" == *"mail-notmuch"* ]] && run_module "mail-notmuch" "$APPS/mail-notmuch.sh" [[ "$SELECTED_APPS" == *"caldav-sync"* ]] && run_module "caldav-sync" "$APPS/caldav-sync.sh" [[ "$SELECTED_APPS" == *"ssh-server"* ]] && run_module "ssh-server" "$APPS/ssh-server.sh" [[ "$SELECTED_APPS" == *"wireshark"* ]] && run_module "wireshark" "$APPS/wireshark.sh" [[ "$SELECTED_APPS" == *"anti-malware"* ]] && run_module "anti-malware" "$APPS/anti-malware.sh" [[ "$SELECTED_APPS" == *"gnuplot"* ]] && run_module "gnuplot" "$APPS/gnuplot.sh" [[ "$SELECTED_APPS" == *"blender-povray"* ]] && run_module "blender-povray" "$APPS/blender-povray.sh" [[ "$SELECTED_APPS" == *"toot"* ]] && run_module "toot" "$APPS/toot.sh" [[ "$SELECTED_APPS" == *"db-clients"* ]] && run_module "db-clients" "$APPS/db-clients.sh" [[ "$SELECTED_APPS" == *"mysql"* ]] && run_module "mysql" "$APPS/mysql.sh" [[ "$SELECTED_APPS" == *"productivity"* ]] && run_module "productivity" "$APPS/productivity.sh" [[ "$SELECTED_APPS" == *"python"* ]] && run_module "python" "$APPS/python.sh" [[ "$SELECTED_APPS" == *"k8s"* ]] && run_module "k8s" "$APPS/k8s.sh" [[ "$SELECTED_APPS" == *"docker"* ]] && run_module "docker" "$APPS/docker.sh" [[ "$SELECTED_APPS" == *"podman"* ]] && run_module "podman" "$APPS/podman.sh" [[ "$SELECTED_APPS" == *"cockpit"* ]] && run_module "cockpit" "$APPS/cockpit.sh" [[ "$SELECTED_APPS" == *"tlp"* ]] && run_module "tlp" "$APPS/tlp.sh" [[ "$SELECTED_APPS" == *"butter"* ]] && run_module "butter" "$APPS/butter.sh" [[ "$SELECTED_APPS" == *"localsend"* ]] && run_module "localsend" "$APPS/localsend.sh" [[ "$SELECTED_APPS" == *"croc"* ]] && run_module "croc" "$APPS/croc.sh" [[ "$SELECTED_APPS" == *"opendeck"* ]] && run_module "opendeck" "$APPS/opendeck.sh" [[ "$SELECTED_APPS" == *"localtunnel"* ]] && run_module "localtunnel" "$APPS/localtunnel.sh" [[ "$SELECTED_APPS" == *"timeshift"* ]] && run_module "timeshift" "$APPS/timeshift.sh" [[ "$SELECTED_APPS" == *"zfs"* ]] && run_module "zfs" "$APPS/zfs.sh" [[ "$SELECTED_APPS" == *"wprs"* ]] && run_module "wprs" "$APPS/wprs.sh" [[ "$SELECTED_APPS" == *"plymouth"* ]] && run_module "plymouth" "$APPS/plymouth.sh" [[ "$SELECTED_APPS" == *"plymouth-custom"* ]] && run_module "plymouth-custom" "$APPS/plymouth-custom.sh" [[ "$SELECTED_APPS" == *"steam"* ]] && run_module "steam" "$APPS/steam.sh" [[ "$SELECTED_APPS" == *"vesktop"* ]] && run_module "vesktop" "$APPS/vesktop.sh" [[ "$SELECTED_APPS" == *"spotify"* ]] && run_module "spotify" "$APPS/spotify.sh" [[ "$SELECTED_APPS" == *"prism"* ]] && run_module "prism" "$APPS/prism.sh" [[ "$SELECTED_APPS" == *"vintagestory"* ]] && run_module "vintagestory" "$APPS/vintagestory.sh" [[ "$SELECTED_APPS" == *"openarena"* ]] && run_module "openarena" "$APPS/openarena.sh" [[ "$SELECTED_APPS" == *"tetris"* ]] && run_module "tetris" "$APPS/tetris.sh" [[ "$SELECTED_APPS" == *"doom"* ]] && run_module "doom" "$APPS/doom.sh" [[ "$SELECTED_APPS" == *"sauerbraten"* ]] && run_module "sauerbraten" "$APPS/sauerbraten.sh" [[ "$SELECTED_APPS" == *"stuntrally"* ]] && run_module "stuntrally" "$APPS/stuntrally.sh" [[ "$SELECTED_APPS" == *"onlyoffice"* ]] && run_module "onlyoffice" "$APPS/onlyoffice.sh" [[ "$SELECTED_APPS" == *"xournal"* ]] && run_module "xournal" "$APPS/xournal.sh" [[ "$SELECTED_APPS" == *"rnote"* ]] && run_module "rnote" "$APPS/rnote.sh" [[ "$SELECTED_APPS" == *"obsidian"* ]] && run_module "obsidian" "$APPS/obsidian.sh" [[ "$SELECTED_APPS" == *"tangent-notes"* ]] && run_module "tangent-notes" "$APPS/tangent-notes.sh" [[ "$SELECTED_APPS" == *"ffmpeg"* ]] && run_module "ffmpeg" "$APPS/ffmpeg.sh" [[ "$SELECTED_APPS" == *"sox"* ]] && run_module "sox" "$APPS/sox.sh" [[ "$SELECTED_APPS" == *"imagemagick"* ]] && run_module "imagemagick" "$APPS/imagemagick.sh" [[ "$SELECTED_APPS" == *"yt-dlp"* ]] && run_module "yt-dlp" "$APPS/yt-dlp.sh" [[ "$SELECTED_APPS" == *"gimp"* ]] && run_module "gimp" "$APPS/gimp.sh" [[ "$SELECTED_APPS" == *"inkscape"* ]] && run_module "inkscape" "$APPS/inkscape.sh" [[ "$SELECTED_APPS" == *"krita"* ]] && run_module "krita" "$APPS/krita.sh" [[ "$SELECTED_APPS" == *"kdenlive"* ]] && run_module "kdenlive" "$APPS/kdenlive.sh" [[ "$SELECTED_APPS" == *"openshot"* ]] && run_module "openshot" "$APPS/openshot.sh" [[ "$SELECTED_APPS" == *"shotcut"* ]] && run_module "shotcut" "$APPS/shotcut.sh" [[ "$SELECTED_APPS" == *"ardour"* ]] && run_module "ardour" "$APPS/ardour.sh" [[ "$SELECTED_APPS" == *"audacity"* ]] && run_module "audacity" "$APPS/audacity.sh" [[ "$SELECTED_APPS" == *"lmms"* ]] && run_module "lmms" "$APPS/lmms.sh" [[ "$SELECTED_APPS" == *"mixxx"* ]] && run_module "mixxx" "$APPS/mixxx.sh" [[ "$SELECTED_APPS" == *"cecilia"* ]] && run_module "cecilia" "$APPS/cecilia.sh" [[ "$SELECTED_APPS" == *"chromium"* ]] && run_module "chromium" "$APPS/chromium.sh" [[ "$SELECTED_APPS" == *"firefox-browser"* ]] && run_module "firefox-browser" "$APPS/firefox-browser.sh" [[ "$SELECTED_APPS" == *"zen-browser"* ]] && run_module "zen-browser" "$APPS/zen-browser.sh" [[ "$SELECTED_APPS" == *"nyxt"* ]] && run_module "nyxt" "$APPS/nyxt.sh" [[ "$SELECTED_APPS" == *"librewolf"* ]] && run_module "librewolf" "$APPS/librewolf.sh" [[ "$SELECTED_APPS" == *"min-browser"* ]] && run_module "min-browser" "$APPS/min-browser.sh" [[ "$SELECTED_APPS" == *"vscodium"* ]] && run_module "vscodium" "$APPS/vscodium.sh" [[ "$SELECTED_APPS" == *"zed-ide"* ]] && run_module "zed-ide" "$APPS/zed-ide.sh" [[ "$SELECTED_APPS" == *"geany"* ]] && run_module "geany" "$APPS/geany.sh" [[ "$SELECTED_APPS" == *"codeblocks"* ]] && run_module "codeblocks" "$APPS/codeblocks.sh" [[ "$SELECTED_APPS" == *"kate"* ]] && run_module "kate" "$APPS/kate.sh" [[ "$SELECTED_APPS" == *"rdp-client"* ]] && run_module "rdp-client" "$APPS/rdp-client.sh" [[ "$SELECTED_APPS" == *"lamco-rdp-server"* ]] && run_module "lamco-rdp-server" "$APPS/lamco-rdp-server.sh" [[ "$SELECTED_APPS" == *"qemu"* ]] && run_module "qemu" "$APPS/qemu.sh" [[ "$SELECTED_APPS" == *"freeipa-client"* ]] && run_module "freeipa-client" "$APPS/freeipa-client.sh" [[ "$SELECTED_APPS" == *"freeipa-server"* ]] && run_module "freeipa-server" "$APPS/freeipa-server.sh" # END GENERATED MODULES: module-dispatch # ── Colorway (final step) ───────────────────────────────────────────────────── # Read defaults from repo colors.conf for pre-population. # Running after all modules ensures apply-theme.sh can re-process configs # that were just symlinked or written by the DE/app modules. declare -A _cdef if [[ -f "$DOTFILES_DIR/colors.conf" ]]; then # Parse 'KEY=VALUE' lines, skipping comments and blank lines. # k="${k%%[[:space:]]*}" strips any trailing whitespace from the key. # v="${v%%#*}" strips inline comments; v="${v//[[:space:]]/}" removes all spaces; # v="${v^^}" uppercases the hex value so comparisons are case-insensitive. while IFS='=' read -r k v; do k="${k%%[[:space:]]*}" [[ "$k" =~ ^[[:space:]]*# || -z "$k" ]] && continue v="${v%%#*}"; v="${v//[[:space:]]/}"; v="${v^^}" _cdef[$k]="$v" done < "$DOTFILES_DIR/colors.conf" fi # Fall back to hard-coded defaults if colors.conf is absent or missing a key. DEF_TEXT="${_cdef[COLOR_TEXT]:-D6ABAB}" DEF_BG="${_cdef[COLOR_BG]:-1A1A1A}" DEF_HIGHLIGHT="${_cdef[COLOR_HIGHLIGHT]:-E40046}" DEF_DARK="${_cdef[COLOR_DARK]:-5018DD}" DEF_RED="${_cdef[COLOR_RED]:-F50505}" _write_colors_conf() { # Write a normalized colors.conf to a temporary path. # ${t^^} uppercases each hex value so apply-theme.sh always sees consistent casing. # The file is written to TMP_D so it never clobbers the repo's own colors.conf. local out="$1" t="$2" b="$3" h="$4" d="$5" r="$6" printf 'COLOR_TEXT=%s\nCOLOR_BG=%s\nCOLOR_HIGHLIGHT=%s\nCOLOR_DARK=%s\nCOLOR_RED=%s\n' \ "${t^^}" "${b^^}" "${h^^}" "${d^^}" "${r^^}" > "$out" } if $ANSWERFILE_MODE; then # Apply colors from answerfile if any are set. # The concatenated string test is a concise way to check if at least one # color field is non-empty without needing five separate [[ -n ]] guards. if [[ -n "$AF_COLOR_TEXT$AF_COLOR_BG$AF_COLOR_HIGHLIGHT$AF_COLOR_DARK$AF_COLOR_RED" ]]; then TMP_COLORS="$TMP_D/colors.conf" # Per-field fallback: if the answerfile omits a color, use the repo default # so apply-theme.sh always receives a complete, five-color config file. _write_colors_conf "$TMP_COLORS" \ "${AF_COLOR_TEXT:-$DEF_TEXT}" \ "${AF_COLOR_BG:-$DEF_BG}" \ "${AF_COLOR_HIGHLIGHT:-$DEF_HIGHLIGHT}" \ "${AF_COLOR_DARK:-$DEF_DARK}" \ "${AF_COLOR_RED:-$DEF_RED}" printf "Applying colorway from answerfile...\n" | tee -a "$LOG" # '|| true' suppresses a non-zero exit from apply-theme.sh so a colorway # failure never aborts an otherwise successful unattended install. bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true fi else # Interactive: show color form dialog # dialog --form: each field is specified as label row col default frow fcol flen max. # The fd swap (3>&1 1>&2 2>&3) captures the form output (one value per line) via $(). # Esc / Cancel sets COLORWAY_RAW="" so the colorway step is simply skipped. COLORWAY_RAW=$(dialog --backtitle "$BACKTITLE" \ --title " Colorway (optional) " \ --form "\n Customize theme colors — bare 6-digit hex, no #.\n Leave unchanged to skip colorway setup.\n" \ 16 62 5 \ "COLOR_TEXT " 1 1 "$DEF_TEXT" 1 18 10 7 \ "COLOR_BG " 2 1 "$DEF_BG" 2 18 10 7 \ "COLOR_HIGHLIGHT " 3 1 "$DEF_HIGHLIGHT" 3 18 10 7 \ "COLOR_DARK " 4 1 "$DEF_DARK" 4 18 10 7 \ "COLOR_RED " 5 1 "$DEF_RED" 5 18 10 7 \ 3>&1 1>&2 2>&3) || COLORWAY_RAW="" if [[ -n "$COLORWAY_RAW" ]]; then # mapfile reads the newline-separated form output into an array. # _cv[0] = COLOR_TEXT line, _cv[1] = COLOR_BG, ... through _cv[4]. mapfile -t _cv <<< "$COLORWAY_RAW" N_TEXT="${_cv[0]:-$DEF_TEXT}" N_BG="${_cv[1]:-$DEF_BG}" N_HIGHLIGHT="${_cv[2]:-$DEF_HIGHLIGHT}" N_DARK="${_cv[3]:-$DEF_DARK}" N_RED="${_cv[4]:-$DEF_RED}" # Only invoke apply-theme.sh if at least one color actually changed. # ${VAR^^} uppercases before comparison to treat 'e40046' == 'E40046'. # This avoids a redundant theme rebuild when the user just pressed Enter # on every field without changing anything. if [[ "${N_TEXT^^}" != "$DEF_TEXT" || \ "${N_BG^^}" != "$DEF_BG" || \ "${N_HIGHLIGHT^^}" != "$DEF_HIGHLIGHT" || \ "${N_DARK^^}" != "$DEF_DARK" || \ "${N_RED^^}" != "$DEF_RED" ]]; then TMP_COLORS="$TMP_D/colors.conf" _write_colors_conf "$TMP_COLORS" "$N_TEXT" "$N_BG" "$N_HIGHLIGHT" "$N_DARK" "$N_RED" clear printf "\n\033[1;35m Applying colorway...\033[0m\n\n" bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true fi fi fi # ── Sync user config to /etc/skel ───────────────────────────────────────────── # /etc/skel is the skeleton directory Linux copies to new user home dirs on creation. # Propagating the post-install config here means any new user added to this machine # automatically gets the full dotfiles environment without a manual copy step. if [[ -d "$HOME/.config" ]]; then printf "\n Syncing ~/.config to /etc/skel...\n" sudo mkdir -p /etc/skel/.config # '.' suffix on the source copies directory contents rather than the directory # itself, making the merge non-destructive if /etc/skel/.config already exists. sudo cp -r "$HOME/.config/." /etc/skel/.config/ fi [[ -d "$HOME/.themes" ]] && { sudo mkdir -p /etc/skel/.themes; sudo cp -r "$HOME/.themes/." /etc/skel/.themes/; } # Copy shell rc files to skel only if the user opted in to the dotfiles configs. if [[ "$SHELL_RC" == "dotfiles" ]]; then [[ -f "$HOME/.zshrc" ]] && sudo cp "$HOME/.zshrc" /etc/skel/.zshrc [[ -f "$HOME/.bashrc" ]] && sudo cp "$HOME/.bashrc" /etc/skel/.bashrc [[ -f "$HOME/.vimrc" ]] && sudo cp "$HOME/.vimrc" /etc/skel/.vimrc fi # ── Done ────────────────────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then printf "\nDone. Log: %s\n" "$LOG" else dialog --backtitle "$BACKTITLE" \ --title " Done " \ --msgbox "\n All selected components installed.\n\n Log: $LOG\n\n A reboot may be required for all changes to take effect.\n" 12 58 clear printf "\n Done. Log: %s\n\n" "$LOG" fi