#!/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 # ── Cyberqueer CLI palette ──────────────────────────────────────────────────── # Plain ANSI escape codes used by the prompt helpers below. These keep the # magenta/cyan "cyberqueer" look without depending on the `dialog` binary, so # the installer runs on a bare console with nothing but bash + coreutils. # tput is not used here because it may be unavailable on a minimal TTY. C_RESET=$'\033[0m' C_TITLE=$'\033[1;35m' # bold magenta — section headers C_ACCENT=$'\033[36m' # cyan — prompts and selected marks C_BOLD=$'\033[1m' C_DIM=$'\033[2m' # ── Plain-CLI prompt helpers ────────────────────────────────────────────────── # These reimplement the subset of `dialog` widgets the installer needs using # only `read`. Widgets that return a value (input/menu/checklist/form) print # their UI to stderr and the chosen value(s) to stdout, mirroring dialog's # `3>&1 1>&2 2>&3` fd-swap convention so existing `$(...)` capture sites work # unchanged. All interactive reads come from /dev/tty so the helpers keep # working even when stdin is redirected. ui_header() { # Styled section header printed to the given fd (default stdout). printf '\n%s──── %s ────%s\n\n' "$C_TITLE" "$1" "$C_RESET" } ui_msgbox() { # Informational box: print title + body, wait for Enter to acknowledge. local title="$1" body="$2" ui_header "$title" printf '%b\n' "$body" printf '\n%s Press Enter to continue…%s' "$C_DIM" "$C_RESET" read -r _ %s' "$C_ACCENT" "$C_RESET" } >&2 read -r val &2 read -r choice = 1 && choice <= ${#tags[@]} )); then printf '%s' "${tags[$((choice - 1))]}" return 0 fi done } ui_checklist() { # Multi-select list. Args: title prompt tag1 desc1 state1 tag2 desc2 state2 … # state is "on"/"off" for the initial selection. Long lists are paginated to # the terminal height so they never scroll off-screen: the screen is cleared # and redrawn each round, with n/p moving between pages. The user toggles # items by number and/or range (e.g. "1 3 5-8") — numbering is global, so any # item can be toggled from any page — then presses Enter to confirm. Entering # 'q' aborts (returns 1). The space-separated selected tags go to stdout. local title="$1" prompt="$2"; shift 2 local -a tags=() descs=() state=() while [[ $# -gt 0 ]]; do tags+=("$1"); descs+=("$2"); state+=("$3"); shift 3; done local n=${#tags[@]} i mark line tok lo hi j # Reserve rows for the header, prompt and footer; the remainder hold items. # tput may be absent on a bare TTY, so fall back to a safe 24-row assumption. local rows per pg=0 pages start end rows=$(tput lines 2>/dev/null || echo 24) [[ "$rows" =~ ^[0-9]+$ ]] || rows=24 per=$(( rows - 9 )); (( per < 1 )) && per=1 pages=$(( (n + per - 1) / per )); (( pages < 1 )) && pages=1 while true; do (( pg < 0 )) && pg=0 (( pg >= pages )) && pg=$(( pages - 1 )) start=$(( pg * per )); end=$(( start + per )); (( end > n )) && end=$n { # \033[2J\033[H clears the screen and homes the cursor so each page # paints in place rather than scrolling — a stable, scrollable view. printf '\033[2J\033[H' ui_header "$title" printf '%b' "$prompt" (( pages > 1 )) && printf ' %s(page %d/%d)%s' "$C_DIM" $((pg + 1)) "$pages" "$C_RESET" printf '\n\n' for (( i = start; i < end; i++ )); do if [[ "${state[$i]}" == on ]]; then mark="${C_ACCENT}[x]${C_RESET}"; else mark='[ ]'; fi printf ' %s %s%3d%s %s\n' "$mark" "$C_ACCENT" $((i + 1)) "$C_RESET" "${descs[$i]}" done if (( pages > 1 )); then printf '\n%s number/range toggles (1 3 5-8) · n/p next/prev page · Enter confirms · q aborts%s\n' "$C_DIM" "$C_RESET" else printf '\n%s Toggle items by number/range (e.g. 1 3 5-8); Enter confirms, q aborts.%s\n' "$C_DIM" "$C_RESET" fi printf '%s > %s' "$C_ACCENT" "$C_RESET" } >&2 read -r line = 1 && j <= n )) || continue if [[ "${state[$((j - 1))]}" == on ]]; then state[$((j - 1))]=off; else state[$((j - 1))]=on; fi done done done local out="" for i in "${!tags[@]}"; do [[ "${state[$i]}" == on ]] && out+="${tags[$i]} " done printf '%s' "${out% }" } ui_form() { # Sequential field entry. Args: title prompt label1 default1 label2 default2 … # Prints one entered value per line to stdout (blank field → its default), # matching the newline-separated output of `dialog --form`. local title="$1" prompt="$2"; shift 2 { ui_header "$title" printf '%b\n' "$prompt" } >&2 local label def v out="" while [[ $# -gt 0 ]]; do label="$1"; def="$2"; shift 2 printf '%s %s%s%s [%s]: ' "$C_ACCENT" "$C_BOLD" "$label" "$C_RESET$C_ACCENT" "$def" >&2 printf '%s' "$C_RESET" >&2 read -r v /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 screen 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 } warn() { # Non-fatal notice: in answerfile mode just log it; interactively show a # message box that blocks until the user acknowledges with Enter. if $ANSWERFILE_MODE; then printf "\n Warning: %s\n" "$1" | tee -a "$LOG" else ui_msgbox " Module Conflict " " $1" fi } 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 selection screen 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. # ui_yesno returns 0 for Yes and 1 for No; the '|| { ...; exit 1; }' # fires on No, aborting the install. ui_yesno " Module Failed " "$label exited with code $rc.\n\nContinue anyway?" no \ || { 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 )) # A non-"none" DE selection always installs exactly one DE module. [[ "$de" != "none" ]] && TOTAL=$(( TOTAL + 1 )) # Optional app modules: one word-boundary check per app, one increment per match. # Padding $a with spaces and matching " tag " prevents partial-name false matches # (e.g. "plymouth" must not match against "plymouth-custom"). # 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_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. # 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") } # ── 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." 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. ui_msgbox " No Network Detected " " 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 Reconnect, then press Enter." if ! ping -c1 -W3 archlinux.org &>/dev/null; then ui_yesno " Still Offline " " Still no internet connection.\n\n Packages cannot be downloaded without network access.\n\n Continue anyway?" no \ || { 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. # ':' is the no-op command so the redirection has an explicit target. : > "$LOG" printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >> "$LOG" # ── Welcome ─────────────────────────────────────────────────────────────────── if ! $ANSWERFILE_MODE; then ui_msgbox " Welcome " "\ 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" fi # Note: the hostname is set by the base installer (archbaseos-guided-install.sh), # so this script intentionally does not prompt for or change it. # ── Component selection ─────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then COMPONENTS="$AF_COMPONENTS" else # ui_checklist takes tag/desc/state triplets and echoes the selected tags. # All four components are pre-selected ("on") because they form the expected base. # Entering 'q' returns exit code 1; the '|| { ...; exit 0; }' treats that as a clean abort. COMPONENTS=$(ui_checklist " Select Components " "Optional base components — all enabled by default:" \ "pkg" "pkg Package managers yay · nvm · rust" on \ "core" "core Core packages 100+ base system packages" on \ "svc" "svc Core services NetworkManager · cronie · fail2ban" on \ "shell" "shell Shell setup zsh · nvim · yazi · micro · starship" on) \ || { clear; echo "Aborted."; exit 0; } fi # ── DE selection ────────────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then DE="$AF_DE" else # ui_menu is a single-choice list; it outputs the selected tag. # 'q'/blank returns exit code 1 — '|| DE="none"' defaults to skipping the DE, # preserving whatever DE the user already has installed. DE=$(ui_menu " Desktop Environment " "Select a desktop environment · q / none to skip:" \ "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") || DE="none" fi # ── Apps selection ──────────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then SELECTED_APPS="$AF_APPS" else # BEGIN GENERATED MODULES: module-checklist SELECTED_APPS=$(ui_checklist " Applications " "Optional applications — installed after base components:" \ "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" on \ "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" on \ "sox" "sox command-line audio processing toolkit" on \ "imagemagick" "imagemagick image manipulation suite" on \ "yt-dlp" "yt-dlp YouTube and media downloader" on \ "gimp" "gimp GNU Image Manipulation Program" off \ "inkscape" "inkscape vector graphics editor" on \ "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 \ ) || SELECTED_APPS="" # END GENERATED MODULES: module-checklist fi # ── Shell RC preference ─────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then SHELL_RC="$AF_SHELL_RC" else SHELL_RC=$(ui_menu " Shell Config for New Users " \ "Should new users on this machine inherit the dotfiles' rc files?\n (Controls what gets copied to /etc/skel)" \ "dotfiles" "Use the_miro's .zshrc / .bashrc / .vimrc from dotfiles" \ "defaults" "Skip — use system defaults for new users") || 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="" [[ "$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" [[ "$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 ui_yesno " Confirm Installation " " Components to install:\n\n${SUMMARY}\n Log: $LOG" \ || { 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" # 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 — deselecting 'plymouth-custom'" SELECTED_APPS=" $SELECTED_APPS " SELECTED_APPS="${SELECTED_APPS/ plymouth-custom / }" SELECTED_APPS="${SELECTED_APPS# }" SELECTED_APPS="${SELECTED_APPS% }" fi if [[ " $SELECTED_APPS " == *" plymouth-custom "* && " $SELECTED_APPS " == *" plymouth "* ]]; then warn "'plymouth-custom' and 'plymouth' are mutually exclusive — deselecting 'plymouth'" SELECTED_APPS=" $SELECTED_APPS " SELECTED_APPS="${SELECTED_APPS/ plymouth / }" SELECTED_APPS="${SELECTED_APPS# }" SELECTED_APPS="${SELECTED_APPS% }" 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: prompt for each color field in turn. # ui_form emits one value per line (blank → default), captured via $(). # Pressing Enter on every field leaves COLORWAY_RAW at the defaults, and the # comparison below then skips the colorway step entirely. COLORWAY_RAW=$(ui_form " Colorway (optional) " \ " Customize theme colors — bare 6-digit hex, no #.\n Leave each field unchanged to skip colorway setup." \ "COLOR_TEXT" "$DEF_TEXT" \ "COLOR_BG" "$DEF_BG" \ "COLOR_HIGHLIGHT" "$DEF_HIGHLIGHT" \ "COLOR_DARK" "$DEF_DARK" \ "COLOR_RED" "$DEF_RED") || 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 ui_msgbox " Done " " All selected components installed.\n\n Log: $LOG\n\n A reboot may be required for all changes to take effect." clear printf "\n Done. Log: %s\n\n" "$LOG" fi