Dotfiles/setup/simple-install.sh

882 lines
50 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/bin/bash
# simple-install.sh — TUI installer for the_miro's Arch dotfiles
# -u: error on unset variables; -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 it was invoked from.
# BASH_SOURCE[0] remains correct even when the script is sourced.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Dotfiles root is one level above 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 temporary files (e.g. a transient colors.conf).
# mktemp -d creates a unique directory visible only to this process.
TMP_D="$(mktemp -d)"
# Cleanup trap: remove scratch dir and restore terminal state on any exit signal.
# tput reset issues a full terminal reset; stty sane is the bare-console fallback.
# '|| true' ensures the trap never masks the real process exit code.
trap 'rm -rf "$TMP_D"; tput reset 2>/dev/null || stty sane 2>/dev/null || true' EXIT INT TERM HUP
# Allow callers to inject an alternative answerfile via the ANSWERFILE env var.
# /answerfile.json is a conventional location for CI/PXE unattended installs.
ANSWERFILE="${ANSWERFILE:-/answerfile.json}"
ANSWERFILE_MODE=false
# Activate unattended mode only if the answerfile is present on disk.
[[ -f "$ANSWERFILE" ]] && ANSWERFILE_MODE=true
TITLE="the_miro's Arch Dotfiles"
# ── Terminal dimensions ───────────────────────────────────────────────────────
# tput may fail on a bare TTY with no TERM set; fall back to safe 80×24 defaults.
TERM_H=$(tput lines 2>/dev/null || echo 24)
TERM_W=$(tput cols 2>/dev/null || echo 80)
# Cap the TUI width at 72 columns but shrink to fit narrow terminals.
# Leaving a 2-column margin (TERM_W - 2) prevents line wrapping on small screens.
TUI_W=$(( TERM_W < 74 ? TERM_W - 2 : 72 ))
# Build a horizontal rule string of exactly TUI_W '─' characters.
# printf -v assigns the result to TUI_BAR without printing it.
# '%.0s─' prints zero characters from the argument then appends '─', repeated
# once per number from seq, producing TUI_W copies of the character.
printf -v TUI_BAR '%.0s─' $(seq 1 "$TUI_W")
# ── ANSI color codes ──────────────────────────────────────────────────────────
# Short variable names to keep printf format strings readable.
# R=reset, B=bold, D=dim, M=magenta+bold, CY=cyan+bold.
# $'\e[...' is the ANSI CSI escape sequence; 0m resets all attributes.
R=$'\e[0m'; B=$'\e[1m'; D=$'\e[2m'; M=$'\e[1;35m'; CY=$'\e[1;36m'
# ── TUI primitives ────────────────────────────────────────────────────────────
# All display goes to /dev/tty so functions work correctly inside $() captures.
# If output went to stdout, the calling $() subshell would swallow the UI text
# along with the return value, making the screen appear blank.
_hdr() {
# \e[2J clears the screen; \e[H moves the cursor to the top-left.
# The box is drawn using TUI_BAR (pre-built ─ string) and box-drawing chars.
# %-*s with width (TUI_W - 4) left-pads the title to fill the box exactly.
printf '\e[2J\e[H' >/dev/tty
printf "${M} ┌%s┐${R}\n ${M}${B}%-*s${R} ${M}${R}\n ${M}└%s┘${R}\n\n" \
"$TUI_BAR" $(( TUI_W - 4 )) "$TITLE" "$TUI_BAR" >/dev/tty
}
_sep() { printf " ${D}─────────────────────────────────────────────${R}\n\n" >/dev/tty; }
# tui_msg TITLE MSG
tui_msg() {
_hdr
printf " ${M}${B}%s${R}\n" "$1" >/dev/tty; _sep
# %b interprets escape sequences in MSG so callers can embed \n for newlines.
printf "%b\n\n" "$2" >/dev/tty
# Blocks until Enter; 'read -r </dev/tty' reads from the terminal even when
# the function is called inside a $() subshell that has stdin redirected.
printf " ${D}Press Enter to continue...${R}" >/dev/tty; read -r </dev/tty
}
# tui_yesno TITLE MSG → 0=yes 1=no
tui_yesno() {
_hdr
printf " ${M}${B}%s${R}\n" "$1" >/dev/tty; _sep
printf "%b\n\n" "$2" >/dev/tty
local a
while true; do
printf " ${CY}[Y/n]${R} > " >/dev/tty; read -r a </dev/tty
# ${a,,} lowercases the input; empty input (bare Enter) defaults to "yes"
# to follow the conventional [Y/n] prompt behaviour.
case "${a,,}" in
y|yes|"") return 0 ;;
n|no) return 1 ;;
*) printf " Enter Y or N.\n" >/dev/tty ;;
esac
done
}
# tui_input TITLE PROMPT [DEFAULT] → prints value to stdout
tui_input() {
_hdr
printf " ${M}${B}%s${R}\n" "$1" >/dev/tty; _sep
printf " %b\n" "$2" >/dev/tty
[[ -n "${3:-}" ]] && printf " ${D}(default: %s)${R}\n" "$3" >/dev/tty
printf "\n > " >/dev/tty; local v; read -r v </dev/tty
# If the user pressed Enter without typing, fall back to the DEFAULT argument.
# The nested :- handles both an empty input and a missing DEFAULT gracefully.
printf '%s' "${v:-${3:-}}"
}
# tui_checklist TITLE PROMPT tag desc state tag desc state ...
# state: "on" | "off" | "header" (header = section label, tag is ignored)
# Prints space-separated selected tags to stdout.
# Arrow keys navigate · Space toggles · a selects all · Enter/n confirms
tui_checklist() {
local title="$1" prompt="$2"; shift 2
# Parse triplets (tag, description, state) into three parallel arrays.
# Using parallel arrays avoids nested data structures (unavailable in bash 4).
local -a _T _D _S
while [[ $# -ge 3 ]]; do _T+=("$1"); _D+=("$2"); _S+=("$3"); shift 3; done
local n=${#_T[@]}
local _tv _vp _cur _scr _VIS _k _k2 _k3 _k4 _i _j _f _l _chk _res _toti _posi i
# _vp = number of visible list rows; reserve 11 rows for header, prompt, and status bar.
_tv=$(tput lines 2>/dev/null || echo 24)
_vp=$(( _tv - 11 ))
(( _vp < 4 )) && _vp=4
# Pre-count selectable (non-header) items to display the [N/total] counter.
_toti=0
for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" != "header" ]] && (( _toti++ )); done
# Initialise cursor to the first non-header item so the cursor is never
# parked on an unselectable section label.
_cur=0
for (( i=0; i<n; i++ )); do
[[ "${_S[$i]}" != "header" ]] && { _cur=$i; break; }
done
_scr=0; _VIS=0
# Count visual lines from index _s up to (not including) _e; result in _VIS.
# Headers take 1 line when at the top of the viewport, 2 lines otherwise
# (a blank separator line is printed before each non-first header).
__cl_vis() {
local _s=$1 _e=$2; _VIS=0
for (( i=_s; i<_e && i<n; i++ )); do
if [[ "${_S[$i]}" == "header" ]]; then
(( i == _s )) && (( _VIS++ )) || (( _VIS+=2 ))
else
(( _VIS++ ))
fi
done
}
# Ensure _cur is within the visible viewport; adjust _scr (scroll offset) if needed.
# Scrolls _scr forward one step at a time until _cur fits within _vp visual lines.
__cl_sync() {
(( _cur < _scr )) && _scr=$_cur
while true; do
__cl_vis $_scr $(( _cur + 1 ))
(( _VIS <= _vp )) && break
(( _scr < n-1 )) || break
(( _scr++ ))
done
}
__cl_draw() {
_hdr
printf " ${M}${B}%s${R}\n" "$title" >/dev/tty
_sep
printf " ${D}%s${R}\n\n" "$prompt" >/dev/tty
_l=0
for (( i=_scr; i<n; i++ )); do
if [[ "${_S[$i]}" == "header" ]]; then
if (( i == _scr )); then
# First visible item is a header: print it without a leading blank line.
(( _l + 1 > _vp )) && break
printf " ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty
(( _l++ ))
else
# Non-first header: add a blank separator line before it (costs 2 rows).
(( _l + 2 > _vp )) && break
printf "\n ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty
(( _l+=2 ))
fi
else
(( _l + 1 > _vp )) && break
# _chk is the checkbox fill: '*' when selected, ' ' when not.
_chk=" "; [[ "${_S[$i]}" == "on" ]] && _chk="*"
if (( i == _cur )); then
# Highlight the cursor row with cyan arrow and colour.
printf " ${CY}▶ [%s] %-22s${R} %s\n" "$_chk" "${_T[$i]}" "${_D[$i]}" >/dev/tty
else
printf " [%s] %-22s %s\n" "$_chk" "${_T[$i]}" "${_D[$i]}" >/dev/tty
fi
(( _l++ ))
fi
done
# Compute the 1-based position of the cursor among selectable items for
# the status bar counter, e.g. "[7/42]".
_posi=0
for (( i=0; i<_cur; i++ )); do [[ "${_S[$i]}" != "header" ]] && (( _posi++ )); done
(( _posi++ ))
printf "\n ${D}↑↓ navigate Space toggle a all Enter/n confirm [%d/%d]${R}\n" \
"$_posi" "$_toti" >/dev/tty
}
while true; do
__cl_sync
__cl_draw
# Read a keypress: -r disables backslash interpretation, -s suppresses echo,
# -n1 reads exactly one byte at a time. IFS= prevents whitespace trimming.
IFS= read -rsn1 _k </dev/tty
if [[ "$_k" == $'\e' ]]; then
# Escape is the lead byte of multi-byte arrow/page key sequences (ESC [ X).
# Read up to three more bytes with a short timeout (-t 0.05 s) so a bare
# Escape key press doesn't hang waiting for bytes that will never arrive.
IFS= read -rsn1 -t 0.05 _k2 </dev/tty
if [[ "$_k2" == '[' ]]; then
IFS= read -rsn1 -t 0.05 _k3 </dev/tty
if [[ "$_k3" =~ [0-9] ]]; then
# Four-byte sequence (e.g. Page Up ESC[5~, Page Down ESC[6~).
IFS= read -rsn1 -t 0.05 _k4 </dev/tty
_k="${_k}${_k2}${_k3}${_k4}"
else
# Three-byte sequence (e.g. Up ESC[A, Down ESC[B, Home ESC[H, End ESC[F).
_k="${_k}${_k2}${_k3}"
fi
else
_k="${_k}${_k2}"
fi
fi
case "$_k" in
$'\e[A') # Up arrow
for (( _i=_cur-1; _i>=0; _i-- )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
done ;;
$'\e[B') # Down arrow
for (( _i=_cur+1; _i<n; _i++ )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
done ;;
$'\e[5~') # Page Up — jump half a viewport upward
for (( _j=0; _j < _vp/2; _j++ )); do
_f=0
for (( _i=_cur-1; _i>=0; _i-- )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; _f=1; break; }
done
# _f=0 means we hit the top; stop the outer loop early.
(( _f == 0 )) && break
done ;;
$'\e[6~') # Page Down — jump half a viewport downward
for (( _j=0; _j < _vp/2; _j++ )); do
_f=0
for (( _i=_cur+1; _i<n; _i++ )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; _f=1; break; }
done
(( _f == 0 )) && break
done ;;
$'\e[H') # Home — jump to first selectable item; also reset scroll
for (( _i=0; _i<n; _i++ )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
done; _scr=0 ;;
$'\e[F') # End — jump to last selectable item
for (( _i=n-1; _i>=0; _i-- )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
done ;;
' ') # Space — toggle current item between on/off
[[ "${_S[$_cur]}" == "on" ]] && _S[$_cur]="off" || _S[$_cur]="on" ;;
'a') # Select all selectable items at once
for (( _i=0; _i<n; _i++ )); do
[[ "${_S[$_i]}" != "header" ]] && _S[$_i]="on"
done ;;
''|'n') break ;; # Enter or n — confirm and exit the loop
esac
done
# Collect tags of all "on" items into a space-separated string.
# "${_res% }" trims the trailing space added by the loop.
_res=""
for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" == "on" ]] && _res+="${_T[$i]} "; done
printf '%s' "${_res% }"
}
# tui_menu TITLE PROMPT tag desc tag desc ...
# Prints selected tag to stdout; loops until a valid number is entered.
tui_menu() {
local title="$1" prompt="$2"; shift 2
local -a _T _D
while [[ $# -ge 2 ]]; do _T+=("$1"); _D+=("$2"); shift 2; done
local n=${#_T[@]}
while true; do
_hdr
printf " ${M}${B}%s${R}\n" "$title" >/dev/tty; _sep
printf " ${D}%s${R}\n\n" "$prompt" >/dev/tty
local i
for (( i=0; i<n; i++ )); do
printf " %3d) %-22s %s\n" $(( i+1 )) "${_T[$i]}" "${_D[$i]}" >/dev/tty
done
printf "\n ${D}Enter number:${R}\n > " >/dev/tty
local inp; read -r inp </dev/tty
if [[ "$inp" =~ ^[0-9]+$ ]] && (( inp >= 1 && inp <= n )); then
printf '%s' "${_T[$(( inp-1 ))]}"; return 0
fi
printf " Invalid selection.\n" >/dev/tty; sleep 0.4
done
}
# ── State ─────────────────────────────────────────────────────────────────────
STEP=0
TOTAL=0
# ── Helpers ───────────────────────────────────────────────────────────────────
require_jq() {
command -v jq &>/dev/null && return
echo "jq not found — installing..."
sudo pacman -S --noconfirm jq || { echo "Failed to install jq."; exit 1; }
}
die() {
clear
printf "\n Error: %s\n\n" "$1" >&2
exit 1
}
log_sep() {
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
printf "\n${M} [$STEP/$TOTAL] %s${R}\n" "$label"
printf "${D} ─────────────────────────────────────────────${R}\n\n"
local rc=0
bash "$script" 2>&1 | tee -a "$LOG" || rc=${PIPESTATUS[0]}
if [[ $rc -ne 0 ]]; then
if [[ $ANSWERFILE_MODE == true ]]; then
printf "\n Warning: %s exited with code %d — continuing.\n" "$label" "$rc" | tee -a "$LOG"
else
tui_yesno " Module Failed " \
" $label exited with code $rc.\n\n Continue anyway?" \
|| { clear; exit 1; }
fi
fi
}
count_steps() {
local c="$1" de="$2" a="${3:-}"
TOTAL=0
[[ "$c" == *"pkg"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$c" == *"core"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$c" == *"svc"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$c" == *"shell"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$de" != "none" ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$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" == *"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" == *"yt-dlp"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"sox"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"imagemagick"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"ffmpeg"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"localtunnel"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"butter"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"tlp"* ]] && 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" == *"localsend"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"croc"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"onlyoffice"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"wireshark"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"k8s"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"docker"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"freeipa-client"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"python"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"zfs"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"wprs"* ]] && 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" == *"xournal"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"gimp"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"inkscape"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"krita"* ]] && 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" == *"kdenlive"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"openshot"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"shotcut"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"anti-malware"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"timeshift"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"mail-notmuch"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"caldav-sync"* ]] && TOTAL=$(( TOTAL + 1 ))
}
# ── Answerfile ────────────────────────────────────────────────────────────────
AF_HOSTNAME=""
AF_COMPONENTS=""
AF_DE="none"
AF_APPS=""
AF_COLOR_TEXT=""
AF_COLOR_BG=""
AF_COLOR_HIGHLIGHT=""
AF_COLOR_DARK=""
AF_COLOR_RED=""
load_answerfile() {
require_jq
AF_HOSTNAME=$(jq -r '.hostname // ""' "$ANSWERFILE")
AF_COMPONENTS=$(jq -r '(.components // []) | join(" ")' "$ANSWERFILE")
AF_DE=$(jq -r '.desktop_environment // "none"' "$ANSWERFILE")
AF_APPS=$(jq -r '(.apps // []) | join(" ")' "$ANSWERFILE")
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() {
local mac
mac=$(ip link show 2>/dev/null \
| awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}')
printf '%s' "${mac//:/}"
}
# ── Preflight ─────────────────────────────────────────────────────────────────
if [[ $EUID -eq 0 ]]; then
# Root context (e.g. archiso chroot): shim sudo as a passthrough
mkdir -p "$TMP_D/bin"
printf '#!/bin/bash\nexec "$@"\n' > "$TMP_D/bin/sudo"
chmod +x "$TMP_D/bin/sudo"
export PATH="$TMP_D/bin:$PATH"
fi
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 ─────────────────────────────────────────────────────────────
if ! ping -c1 -W3 archlinux.org &>/dev/null; then
if $ANSWERFILE_MODE; then
printf "Warning: no internet connection detected.\n" | tee -a "$LOG"
else
tui_msg " No Network Detected " \
" No internet connection found.\n\n Wired: ensure the cable is plugged in.\n WiFi: switch to another TTY (Alt+F2) and run: iwctl\n\n Press Enter here once connected."
if ! ping -c1 -W3 archlinux.org &>/dev/null; then
tui_yesno " Still Offline " \
" Still no internet connection.\n\n Packages cannot be downloaded without network access.\n\n Continue anyway?" \
|| { clear; echo "Aborted — no network."; exit 1; }
fi
fi
fi
> "$LOG"
printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >> "$LOG"
# ── Welcome ───────────────────────────────────────────────────────────────────
if ! $ANSWERFILE_MODE; then
tui_msg " 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
# ── Hostname ──────────────────────────────────────────────────────────────────
HOSTNAME_SET=""
if $ANSWERFILE_MODE; then
if [[ -n "$AF_HOSTNAME" ]]; then
MAC=$(get_mac_suffix)
HOSTNAME_SET="${AF_HOSTNAME}-${MAC}"
printf "Hostname (from answerfile + MAC): %s\n" "$HOSTNAME_SET" | tee -a "$LOG"
fi
else
HOSTNAME_SET=$(tui_input " Hostname " \
"Hostname for this machine (leave blank to keep default)." "")
fi
if [[ -n "$HOSTNAME_SET" ]]; then
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
COMPONENTS=$(tui_checklist " Select Components " \
"Select system components to install" \
"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)
fi
# ── DE selection ──────────────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then
DE="$AF_DE"
else
DE=$(tui_menu " Desktop Environment " \
"Select a desktop environment:" \
"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")
fi
# ── Apps selection ────────────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then
SELECTED_APPS="$AF_APPS"
else
SELECTED_APPS=$(tui_checklist " Applications " \
"Select optional applications to install" \
\
"" "AI / LLM" header \
"ollama" "Ollama local LLM runner + API server" off \
"llama-cpp" "llama.cpp standalone inference CLI + server" off \
"open-webui" "Open WebUI browser UI for Ollama / LLM backends" off \
"claude" "Claude Code Anthropic CLI via npm" off \
\
"" "Networking & Disk" header \
"networking-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \
"disk-recovery" "Disk Recovery ddrescue · f3" off \
\
"" "CLI Tools" header \
"himalaya" "Himalaya terminal email client (AUR)" off \
"mail-notmuch" "Mail (notmuch) isync · msmtp · notmuch · alot stack" off \
"caldav-sync" "CalDAV Sync vdirsyncer · khal calendar sync" off \
"gnuplot" "Gnuplot scientific plotting" off \
"blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \
"toot" "toot Mastodon CLI client (AUR)" off \
"db-clients" "DB Clients pgcli · mycli" off \
"mysql" "MySQL / MariaDB mariadb server + setup" off \
"productivity" "Productivity taskwarrior · watson · jrnl" off \
"yt-dlp" "yt-dlp YouTube / media downloader" off \
"sox" "SoX audio processing toolkit" off \
"imagemagick" "ImageMagick image manipulation" off \
"ffmpeg" "FFmpeg extras thumbnailer · GStreamer codecs" off \
"localtunnel" "LocalTunnel expose localhost via tunnel" off \
"butter" "butter btrfs snapshot backup (AUR)" off \
"tlp" "TLP laptop power management" off \
\
"" "Gaming" header \
"steam" "Steam gaming platform" off \
"vesktop" "Vesktop Discord + Vencord theme" off \
"spotify" "Spotify launcher + Spicetify theming" off \
"prism" "PrismLauncher Minecraft launcher (Flatpak)" off \
"vintagestory" "Vintage Story survival game (AUR)" off \
"openarena" "OpenArena open-source Quake III Arena" off \
"tetris" "Tetris CLI bastet · vitetris" off \
"doom" "Doom Chocolate Doom + Freedoom data" off \
"sauerbraten" "Sauerbraten open-source FPS (Cube 2)" off \
"stuntrally" "Stunt Rally rally racing game (Flatpak)" off \
\
"" "File Transfer & Office" header \
"localsend" "LocalSend LAN file transfer (AUR)" off \
"croc" "croc cross-platform file transfer" off \
"onlyoffice" "OnlyOffice office suite (AUR)" off \
\
"" "Graphics & Design" header \
"gimp" "GIMP GNU image manipulation program" off \
"inkscape" "Inkscape vector graphics editor" off \
"krita" "Krita digital painting application" off \
"xournal" "Xournal++ note-taking & PDF annotator" off \
\
"" "Audio & Music" header \
"ardour" "Ardour professional DAW" off \
"audacity" "Audacity multi-track audio editor" off \
"lmms" "LMMS Linux MultiMedia Studio DAW" off \
"mixxx" "Mixxx DJ mixing software" off \
"cecilia" "Cecilia audio signal processing (AUR)" off \
\
"" "Video Editing" header \
"kdenlive" "Kdenlive KDE non-linear video editor" off \
"openshot" "OpenShot easy video editor" off \
"shotcut" "Shotcut cross-platform video editor" off \
\
"" "Security" header \
"anti-malware" "Anti-Malware ClamAV · rkhunter · chkrootkit" off \
"timeshift" "Timeshift system snapshot / backup + autosnap" off \
"wireshark" "Wireshark network packet analyser (GUI)" off \
\
"" "Containers & Server" header \
"k8s" "Kubernetes tools kubectl · podman-desktop" off \
"docker" "Docker docker · docker-compose" off \
"podman" "Podman rootless containers · buildah" off \
"cockpit" "Cockpit web UI · machines · podman" off \
"ssh-server" "SSH server openssh · key-auth · enabled" off \
"freeipa-client" "FreeIPA Client sssd + ipa-client-install + enrollment" off \
"freeipa-server" "FreeIPA Server interactive server setup + client gen" off \
"freeipa-image" "FreeIPA Image OCI/LXC/Proxmox LXC builder + Keycloak" off \
\
"" "Dev & System" header \
"python" "Python tools pyright · pipx · pynvim" off \
"zfs" "ZFS zfs-dkms kernel module" off \
"wprs" "WPRS wprs-git (AUR)" off \
\
"" "Browsers" header \
"chromium" "Chromium open-source browser (official)" off \
"firefox-browser" "Firefox Mozilla browser (official)" off \
"zen-browser" "Zen Browser Firefox-based privacy browser (AUR)" off \
"nyxt" "Nyxt keyboard-driven browser (AUR)" off \
"librewolf" "LibreWolf hardened Firefox fork (AUR)" off \
"min-browser" "Min minimal Electron browser (AUR)" off \
\
"" "IDEs & Editors" header \
"vscodium" "VSCodium telemetry-free VS Code (AUR)" off \
"zed-ide" "Zed high-performance Rust IDE (official)" off \
"geany" "Geany lightweight IDE + plugins (official)" off \
"codeblocks" "Code::Blocks C/C++ IDE (official)" off \
"kate" "Kate KDE advanced text editor (official)" off \
\
"" "Remote & Virtualization" header \
"rdp-client" "RDP Client Remmina + FreeRDP + VNC plugins" off \
"lamco-rdp-server" "Lamco RDP Srv native Wayland RDP server (AUR, Rust)" off \
"qemu" "QEMU/KVM full virt stack + virt-manager GUI" off)
fi
# ── Confirmation (interactive mode only) ──────────────────────────────────────
if ! $ANSWERFILE_MODE; then
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"
[[ "$DE" != "none" ]] && SUMMARY+=" ✦ Desktop environment: $DE\n"
if [[ -n "$SELECTED_APPS" ]]; then
SUMMARY+="\n Applications:\n"
for _app in ollama llama-cpp open-webui claude networking-cli disk-recovery \
himalaya gnuplot blender-povray toot db-clients mysql productivity \
yt-dlp sox imagemagick ffmpeg localtunnel butter tlp steam vesktop \
spotify prism vintagestory openarena tetris doom sauerbraten stuntrally localsend croc onlyoffice \
gimp inkscape krita xournal ardour audacity lmms mixxx cecilia \
kdenlive openshot shotcut \
anti-malware timeshift wireshark k8s \
docker podman cockpit ssh-server freeipa-client freeipa-server \
freeipa-image python zfs wprs chromium firefox-browser zen-browser \
nyxt librewolf min-browser vscodium zed-ide geany codeblocks kate \
rdp-client lamco-rdp-server qemu; do
[[ "$SELECTED_APPS" == *"$_app"* ]] && SUMMARY+="$_app\n"
done
fi
tui_yesno " Confirm Installation " \
" Components to install:\n\n${SUMMARY}\n Log: $LOG\n\n Proceed?" \
|| { clear; echo "Aborted."; exit 0; }
fi
count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS"
# ── Installation: base components ─────────────────────────────────────────────
[[ "$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"
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 ────────────────────────────────────────────────
[[ "$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 Code" "$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" == *"gnuplot"* ]] && run_module "Gnuplot" "$APPS/gnuplot.sh"
[[ "$SELECTED_APPS" == *"blender-povray"* ]] && run_module "Blender + POV-Ray" "$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 / MariaDB" "$APPS/mysql.sh"
[[ "$SELECTED_APPS" == *"productivity"* ]] && run_module "Productivity" "$APPS/productivity.sh"
[[ "$SELECTED_APPS" == *"yt-dlp"* ]] && run_module "yt-dlp" "$APPS/yt-dlp.sh"
[[ "$SELECTED_APPS" == *"sox"* ]] && run_module "SoX" "$APPS/sox.sh"
[[ "$SELECTED_APPS" == *"imagemagick"* ]] && run_module "ImageMagick" "$APPS/imagemagick.sh"
[[ "$SELECTED_APPS" == *"ffmpeg"* ]] && run_module "FFmpeg extras" "$APPS/ffmpeg.sh"
[[ "$SELECTED_APPS" == *"localtunnel"* ]] && run_module "LocalTunnel" "$APPS/localtunnel.sh"
[[ "$SELECTED_APPS" == *"butter"* ]] && run_module "butter" "$APPS/butter.sh"
[[ "$SELECTED_APPS" == *"tlp"* ]] && run_module "TLP" "$APPS/tlp.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 "PrismLauncher" "$APPS/prismlauncher.sh"
[[ "$SELECTED_APPS" == *"vintagestory"* ]] && run_module "Vintage Story" "$APPS/vintagestory.sh"
[[ "$SELECTED_APPS" == *"openarena"* ]] && run_module "OpenArena" "$APPS/openarena.sh"
[[ "$SELECTED_APPS" == *"tetris"* ]] && run_module "Tetris CLI" "$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 "Stunt Rally" "$APPS/stuntrally.sh"
[[ "$SELECTED_APPS" == *"localsend"* ]] && run_module "LocalSend" "$APPS/localsend.sh"
[[ "$SELECTED_APPS" == *"croc"* ]] && run_module "croc" "$APPS/croc.sh"
[[ "$SELECTED_APPS" == *"onlyoffice"* ]] && run_module "OnlyOffice" "$APPS/onlyoffice.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" == *"xournal"* ]] && run_module "Xournal++" "$APPS/xournal.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" == *"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" == *"anti-malware"* ]] && run_module "Anti-Malware" "$APPS/anti-malware.sh"
[[ "$SELECTED_APPS" == *"timeshift"* ]] && run_module "Timeshift" "$APPS/timeshift.sh"
[[ "$SELECTED_APPS" == *"wireshark"* ]] && run_module "Wireshark" "$APPS/wireshark.sh"
[[ "$SELECTED_APPS" == *"k8s"* ]] && run_module "Kubernetes Tools" "$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" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.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"
[[ "$SELECTED_APPS" == *"freeipa-image"* ]] && run_module "FreeIPA Image" "$APPS/freeipa-image-builder.sh"
[[ "$SELECTED_APPS" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh"
[[ "$SELECTED_APPS" == *"zfs"* ]] && run_module "ZFS" "$MODULES/optional-Modules/zfs.sh"
[[ "$SELECTED_APPS" == *"wprs"* ]] && run_module "WPRS" "$MODULES/optional-Modules/wprs.sh"
[[ "$SELECTED_APPS" == *"chromium"* ]] && run_module "Chromium" "$APPS/chromium.sh"
[[ "$SELECTED_APPS" == *"firefox-browser"* ]] && run_module "Firefox" "$APPS/firefox.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.sh"
[[ "$SELECTED_APPS" == *"geany"* ]] && run_module "Geany" "$APPS/geany.sh"
[[ "$SELECTED_APPS" == *"codeblocks"* ]] && run_module "Code::Blocks" "$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/KVM" "$APPS/qemu.sh"
# ── Colorway (final step) ─────────────────────────────────────────────────────
declare -A _cdef
if [[ -f "$DOTFILES_DIR/colors.conf" ]]; then
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
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() {
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
if [[ -n "$AF_COLOR_TEXT$AF_COLOR_BG$AF_COLOR_HIGHLIGHT$AF_COLOR_DARK$AF_COLOR_RED" ]]; then
TMP_COLORS="$TMP_D/colors.conf"
_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"
bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true
fi
else
_hdr
printf " ${M}${B} Colorway (optional) ${R}\n" >/dev/tty; _sep
printf " Customize theme colors — bare 6-digit hex, no #.\n Press Enter to keep each default.\n\n" >/dev/tty
printf " COLOR_TEXT ${D}[%s]${R} > " "$DEF_TEXT" >/dev/tty; read -r N_TEXT </dev/tty; N_TEXT="${N_TEXT:-$DEF_TEXT}"
printf " COLOR_BG ${D}[%s]${R} > " "$DEF_BG" >/dev/tty; read -r N_BG </dev/tty; N_BG="${N_BG:-$DEF_BG}"
printf " COLOR_HIGHLIGHT ${D}[%s]${R} > " "$DEF_HIGHLIGHT" >/dev/tty; read -r N_HIGHLIGHT </dev/tty; N_HIGHLIGHT="${N_HIGHLIGHT:-$DEF_HIGHLIGHT}"
printf " COLOR_DARK ${D}[%s]${R} > " "$DEF_DARK" >/dev/tty; read -r N_DARK </dev/tty; N_DARK="${N_DARK:-$DEF_DARK}"
printf " COLOR_RED ${D}[%s]${R} > " "$DEF_RED" >/dev/tty; read -r N_RED </dev/tty; N_RED="${N_RED:-$DEF_RED}"
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${M} Applying colorway...${R}\n\n"
bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true
fi
fi
# ── Sync user config to /etc/skel ─────────────────────────────────────────────
# Captures everything installed by modules so future users start with the same setup.
if [[ -d "$HOME/.config" ]]; then
printf "\n Syncing ~/.config to /etc/skel...\n"
sudo mkdir -p /etc/skel/.config
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/; }
[[ -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
# ── Done ──────────────────────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then
printf "\nDone. Log: %s\n" "$LOG"
else
tui_msg " 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