setup: add answerfile system for fully automated installs

tui-install.sh:
  - Reads /answerfile.json if present (ANSWERFILE_MODE)
  - All dialog selections (components, DE, apps) sourced from file
  - Hostname from answerfile gets MAC address suffix appended to
    prevent conflicts when deploying one image to multiple machines
  - Interactive hostname inputbox added to the normal TUI flow
  - Colorway dialog added as final step; skipped if no colors differ
    from defaults and no answerfile colors are set
  - Answerfile mode: runs non-interactively, logs warnings on failure

generate-answerfile.sh (new):
  - Dry-runs the full installer dialog flow (OS + dotfiles)
  - Writes selections to ~/answerfile.json (or a given path)
  - No software is installed; passwords are never written to the file

build.sh:
  - New --preconf [FILE] flag embeds an answerfile into the ISO at
    /answerfile.json; omitting the flag leaves the ISO clean
  - Validates JSON with jq if available before embedding
  - Reworked arg parsing to handle the new flag alongside OUT_DIR

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
The_miro 2026-05-18 15:24:47 +02:00
parent e25dd231cb
commit b5a3b46c79
3 changed files with 777 additions and 173 deletions

View File

@ -1,10 +1,48 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# build.sh — build the M-Archy Arch Linux ISO
#
# Usage:
# bash build.sh [--preconf [FILE]] [OUT_DIR]
#
# --preconf Embed ~/answerfile.json into the ISO at /answerfile.json
# --preconf FILE Embed the specified answerfile instead
# OUT_DIR Output directory (default: ~/m-archy-out, or $OUT_DIR env)
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" DOTFILES_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# ── Argument parsing ───────────────────────────────────────────────────────────
PRECONF_FILE=""
OUT_ARG=""
while [[ $# -gt 0 ]]; do
case "$1" in
--preconf)
# Optional next arg: a file path (doesn't start with -)
if [[ $# -gt 1 && "${2:0:1}" != "-" ]]; then
PRECONF_FILE="$2"; shift
else
PRECONF_FILE="$HOME/answerfile.json"
fi
shift
;;
--preconf=*)
PRECONF_FILE="${1#--preconf=}"
shift
;;
-*)
echo "Unknown flag: $1" >&2; exit 1
;;
*)
OUT_ARG="$1"; shift
;;
esac
done
WORK_DIR="${WORK_DIR:-$HOME/m-archy-build}" WORK_DIR="${WORK_DIR:-$HOME/m-archy-build}"
OUT_DIR="${1:-${OUT_DIR:-$HOME/m-archy-out}}" OUT_DIR="${OUT_ARG:-${OUT_DIR:-$HOME/m-archy-out}}"
PROFILE="$WORK_DIR/profile" PROFILE="$WORK_DIR/profile"
RELENG="/usr/share/archiso/configs/releng" RELENG="/usr/share/archiso/configs/releng"
@ -15,6 +53,16 @@ fi
[[ -d "$RELENG" ]] || { echo "ERROR: $RELENG not found — is archiso installed?"; exit 1; } [[ -d "$RELENG" ]] || { echo "ERROR: $RELENG not found — is archiso installed?"; exit 1; }
# Validate answerfile early if --preconf was given
if [[ -n "$PRECONF_FILE" ]]; then
[[ -f "$PRECONF_FILE" ]] \
|| { echo "ERROR: answerfile not found: $PRECONF_FILE"; exit 1; }
command -v jq &>/dev/null \
&& jq empty "$PRECONF_FILE" \
|| echo "Warning: jq not available — skipping answerfile JSON validation"
echo "Answerfile to embed: $PRECONF_FILE"
fi
rm -rf "$WORK_DIR" rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR" "$OUT_DIR" mkdir -p "$WORK_DIR" "$OUT_DIR"
@ -43,9 +91,19 @@ chmod 755 \
"$PROFILE/airootfs/usr/local/bin/install-arch" \ "$PROFILE/airootfs/usr/local/bin/install-arch" \
"$PROFILE/airootfs/root/installer/"*.sh "$PROFILE/airootfs/root/installer/"*.sh
# ── Embed answerfile (--preconf) ───────────────────────────────────────────────
if [[ -n "$PRECONF_FILE" ]]; then
echo "Embedding answerfile: $PRECONF_FILE → /answerfile.json"
install -m 644 "$PRECONF_FILE" "$PROFILE/airootfs/answerfile.json"
fi
echo "Building ISO (this may take a while)..." echo "Building ISO (this may take a while)..."
sudo mkarchiso -v -w "$WORK_DIR/mkarchiso" -o "$OUT_DIR" "$PROFILE" sudo mkarchiso -v -w "$WORK_DIR/mkarchiso" -o "$OUT_DIR" "$PROFILE"
echo echo
echo "Done." echo "Done."
ls -lh "$OUT_DIR/"*.iso 2>/dev/null || true ls -lh "$OUT_DIR/"*.iso 2>/dev/null || true
if [[ -n "$PRECONF_FILE" ]]; then
echo "Answerfile embedded — automated install will activate on boot."
fi

View File

@ -0,0 +1,377 @@
#!/bin/bash
# generate-answerfile.sh — dry-run the M-Archy installer dialogs and write
# all selections to an answerfile.json. Nothing is installed.
#
# Usage:
# bash generate-answerfile.sh [OUTPUT_PATH]
# OUTPUT_PATH defaults to ~/answerfile.json
#
# The generated file can be placed at /answerfile.json on the installer USB
# (use build.sh --preconf to embed it automatically) so that the full
# install — base OS + dotfiles — runs without any manual input.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
OUTPUT="${1:-$HOME/answerfile.json}"
TMP_D="$(mktemp -d)"
trap 'rm -rf "$TMP_D"' EXIT
BACKTITLE="M-Archy Answerfile Generator"
# ── Dialog theme ──────────────────────────────────────────────────────────────
export DIALOGRC="$TMP_D/dialogrc"
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
# ── Helpers ───────────────────────────────────────────────────────────────────
require_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() {
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; }
# json_str: emit a properly-quoted JSON string value (handles empty → null)
json_str() {
local v="$1"
if [[ -z "$v" ]]; then printf 'null'; else printf '"%s"' "$v"; fi
}
# json_bool: emit true/false from YES/NO or true/false input
json_bool() {
local v="${1,,}"
[[ "$v" == "yes" || "$v" == "true" ]] && echo "true" || echo "false"
}
# ── Preflight ─────────────────────────────────────────────────────────────────
require_dialog
require_jq
# ── Welcome ───────────────────────────────────────────────────────────────────
dialog --backtitle "$BACKTITLE" \
--title " Answerfile Generator " \
--msgbox "\n\
This tool walks you through all installer dialogs\n\
and saves your choices to an answerfile.\n\
─────────────────────────────────────────────────\n\
\n\
NO software will be installed — this is a dry run.\n\
\n\
Output: $OUTPUT\n\
\n\
To use on a USB: build.sh --preconf $OUTPUT\n" 16 62
# ═══════════════════════════════════════════════════════════════
# PART 1 — Base OS install options
# ═══════════════════════════════════════════════════════════════
# ── Drive ─────────────────────────────────────────────────────────────────────
AVAIL_DRIVES=$(lsblk -dn -o NAME,SIZE,MODEL 2>/dev/null | awk '{printf "%s \"%s %s\" off ", $1, $2, $3}' || true)
AF_DRIVE=$(dialog --backtitle "$BACKTITLE" \
--title " Target Drive " \
--inputbox "\n Enter the install drive (e.g. /dev/sda, /dev/nvme0n1).\n\n Available drives:\n$(lsblk -dn -o NAME,SIZE,MODEL 2>/dev/null | sed 's/^/ /')\n" \
16 64 "/dev/sda" \
3>&1 1>&2 2>&3) || AF_DRIVE=""
# ── Kernel ────────────────────────────────────────────────────────────────────
AF_KERNEL=$(dialog --backtitle "$BACKTITLE" \
--title " Kernel " \
--menu "Select kernel package:" 12 54 3 \
"linux" "Stable kernel" \
"linux-lts" "Long-term support kernel" \
"linux-zen" "Zen performance kernel" \
3>&1 1>&2 2>&3) || AF_KERNEL="linux"
# ── Hostname ──────────────────────────────────────────────────────────────────
AF_HOSTNAME=$(dialog --backtitle "$BACKTITLE" \
--title " Hostname " \
--inputbox "\n Hostname for the new system.\n\n Leave blank to keep default.\n\n Note: a MAC address suffix will be appended\n automatically when the answerfile is applied,\n preventing hostname conflicts across machines.\n" \
14 62 "" \
3>&1 1>&2 2>&3) || AF_HOSTNAME=""
# ── Username ──────────────────────────────────────────────────────────────────
AF_USERNAME=$(dialog --backtitle "$BACKTITLE" \
--title " Username " \
--inputbox "\n Name for the primary user account.\n" \
9 54 "" \
3>&1 1>&2 2>&3) || AF_USERNAME=""
# NOTE: passwords are intentionally NOT stored in the answerfile.
dialog --backtitle "$BACKTITLE" \
--title " Password " \
--msgbox "\n Passwords are NOT stored in the answerfile.\n\n You will be prompted for the user password\n at install time even in automated mode.\n" \
10 56
# ── Disk encryption ───────────────────────────────────────────────────────────
dialog --backtitle "$BACKTITLE" \
--title " Disk Encryption " \
--yesno "\n Enable LUKS2 disk encryption on the root partition?\n\n If yes, a backup LUKS key will be auto-generated\n and placed at /_LUKS_BACKUP_KEY in the new system.\n" \
11 62 && AF_ENCRYPT="true" || AF_ENCRYPT="false"
AF_FIDO2_ROOT="false"
AF_FIDO2_USER="false"
if [[ "$AF_ENCRYPT" == "true" ]]; then
dialog --backtitle "$BACKTITLE" \
--title " FIDO2 Root Unlock " \
--yesno "\n Enable FIDO2 hardware key for LUKS root unlock?\n" \
8 56 && AF_FIDO2_ROOT="true" || AF_FIDO2_ROOT="false"
fi
dialog --backtitle "$BACKTITLE" \
--title " FIDO2 User Login " \
--yesno "\n Enable FIDO2 hardware key for user login (PAM)?\n" \
8 56 && AF_FIDO2_USER="true" || AF_FIDO2_USER="false"
# ── Run TUI installer after base install ──────────────────────────────────────
dialog --backtitle "$BACKTITLE" \
--title " Dotfiles Setup " \
--yesno "\n Automatically run the dotfiles TUI installer\n inside the chroot after base install completes?\n" \
9 58 && AF_RUN_TUI="true" || AF_RUN_TUI="false"
# ═══════════════════════════════════════════════════════════════
# PART 2 — Dotfiles / TUI installer options
# (only shown if run_tui is true)
# ═══════════════════════════════════════════════════════════════
AF_COMPONENTS=""
AF_DE="none"
AF_APPS=""
AF_COLOR_TEXT=""
AF_COLOR_BG=""
AF_COLOR_HIGHLIGHT=""
AF_COLOR_DARK=""
AF_COLOR_RED=""
if [[ "$AF_RUN_TUI" == "true" ]]; then
# ── Components ────────────────────────────────────────────────────────────
AF_COMPONENTS=$(dialog --backtitle "$BACKTITLE" \
--title " Select Components " \
--checklist "Space toggles · Enter confirms · Esc quits" 15 68 4 \
"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 \
3>&1 1>&2 2>&3) || AF_COMPONENTS=""
# ── DE ────────────────────────────────────────────────────────────────────
AF_DE=$(dialog --backtitle "$BACKTITLE" \
--title " Desktop Environment " \
--menu "Select a desktop environment · Esc / none to skip:" 20 70 8 \
"hyprland" "Hyprland — Wayland WM, full setup (primary)" \
"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) || AF_DE="none"
# ── Apps ──────────────────────────────────────────────────────────────────
AF_APPS=$(dialog --backtitle "$BACKTITLE" \
--title " Applications " \
--checklist "Optional applications — installed after base components:" 40 76 32 \
"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-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \
"disk-recovery" "Disk Recovery ddrescue · f3" off \
"himalaya" "Himalaya terminal email client (AUR)" off \
"gnuplot" "Gnuplot scientific plotting" off \
"povray" "POV-Ray ray-tracing renderer" off \
"blender" "Blender 3D creation suite" 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 \
"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 \
"localsend" "LocalSend LAN file transfer (AUR)" off \
"croc" "croc cross-platform file transfer" off \
"onlyoffice" "OnlyOffice office suite (AUR)" off \
"wireshark" "Wireshark network packet analyser (GUI)" off \
"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 \
"python" "Python tools pyright · pipx · pynvim" off \
"zfs" "ZFS zfs-dkms kernel module" off \
"wprs" "WPRS wprs-git (AUR)" off \
"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 \
"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 \
3>&1 1>&2 2>&3) || AF_APPS=""
# ── Colorway ──────────────────────────────────────────────────────────────
# Read defaults from repo colors.conf
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}"
COLORWAY_RAW=$(dialog --backtitle "$BACKTITLE" \
--title " Colorway (optional) " \
--form "\n Customize theme colors — bare 6-digit hex, no #.\n Leave unchanged to omit colors from answerfile.\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 -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 save colors if any differ from defaults
if [[ "${N_TEXT^^}" != "$DEF_TEXT" || \
"${N_BG^^}" != "$DEF_BG" || \
"${N_HIGHLIGHT^^}" != "$DEF_HIGHLIGHT" || \
"${N_DARK^^}" != "$DEF_DARK" || \
"${N_RED^^}" != "$DEF_RED" ]]; then
AF_COLOR_TEXT="${N_TEXT^^}"
AF_COLOR_BG="${N_BG^^}"
AF_COLOR_HIGHLIGHT="${N_HIGHLIGHT^^}"
AF_COLOR_DARK="${N_DARK^^}"
AF_COLOR_RED="${N_RED^^}"
fi
fi
fi
# ── Confirmation ──────────────────────────────────────────────────────────────
SUMMARY=""
[[ -n "$AF_DRIVE" ]] && SUMMARY+=" Drive: $AF_DRIVE\n"
[[ -n "$AF_KERNEL" ]] && SUMMARY+=" Kernel: $AF_KERNEL\n"
[[ -n "$AF_HOSTNAME" ]] && SUMMARY+=" Hostname: $AF_HOSTNAME (+ MAC suffix at deploy)\n"
[[ -n "$AF_USERNAME" ]] && SUMMARY+=" Username: $AF_USERNAME\n"
SUMMARY+=" Encrypt: $AF_ENCRYPT\n"
SUMMARY+=" FIDO2 root: $AF_FIDO2_ROOT / FIDO2 user: $AF_FIDO2_USER\n"
SUMMARY+=" Run TUI: $AF_RUN_TUI\n"
[[ -n "$AF_DE" && "$AF_DE" != "none" ]] && SUMMARY+=" DE: $AF_DE\n"
[[ -n "$AF_COLOR_TEXT" ]] && SUMMARY+=" Colors: custom\n"
dialog --backtitle "$BACKTITLE" \
--title " Confirm " \
--yesno "\n Save answerfile with these settings:\n\n${SUMMARY}\n Output: $OUTPUT\n\n Proceed?" \
22 66 || { clear; echo "Aborted."; exit 0; }
# ── Build JSON arrays from space-separated dialog output ──────────────────────
_words_to_json_array() {
local input="$1"
local first=1
printf '['
for w in $input; do
[[ $first -eq 0 ]] && printf ','
printf '"%s"' "$w"
first=0
done
printf ']'
}
# ── Write answerfile ──────────────────────────────────────────────────────────
mkdir -p "$(dirname "$OUTPUT")"
{
printf '{\n'
printf ' "_generated": "%s",\n' "$(date -Iseconds)"
printf ' "drive": %s,\n' "$(json_str "$AF_DRIVE")"
printf ' "kernel": %s,\n' "$(json_str "$AF_KERNEL")"
printf ' "hostname": %s,\n' "$(json_str "$AF_HOSTNAME")"
printf ' "username": %s,\n' "$(json_str "$AF_USERNAME")"
printf ' "encrypt": %s,\n' "$AF_ENCRYPT"
printf ' "fido2_root": %s,\n' "$AF_FIDO2_ROOT"
printf ' "fido2_user": %s,\n' "$AF_FIDO2_USER"
printf ' "run_tui": %s,\n' "$AF_RUN_TUI"
printf ' "components": %s,\n' "$(_words_to_json_array "$AF_COMPONENTS")"
printf ' "desktop_environment": %s,\n' "$(json_str "$AF_DE")"
printf ' "apps": %s' "$(_words_to_json_array "$AF_APPS")"
if [[ -n "$AF_COLOR_TEXT" ]]; then
printf ',\n "colors": {\n'
printf ' "COLOR_TEXT": "%s",\n' "$AF_COLOR_TEXT"
printf ' "COLOR_BG": "%s",\n' "$AF_COLOR_BG"
printf ' "COLOR_HIGHLIGHT": "%s",\n' "$AF_COLOR_HIGHLIGHT"
printf ' "COLOR_DARK": "%s",\n' "$AF_COLOR_DARK"
printf ' "COLOR_RED": "%s"\n' "$AF_COLOR_RED"
printf ' }'
fi
printf '\n}\n'
} > "$OUTPUT"
clear
printf "\n Answerfile saved to: %s\n\n" "$OUTPUT"
printf " To embed in ISO: bash setup/archiso/build.sh --preconf %s\n\n" "$OUTPUT"

View File

@ -12,6 +12,10 @@ LOG="$HOME/dotfiles-install.log"
TMP_D="$(mktemp -d)" TMP_D="$(mktemp -d)"
trap 'rm -rf "$TMP_D"' EXIT trap 'rm -rf "$TMP_D"' EXIT
ANSWERFILE="${ANSWERFILE:-/answerfile.json}"
ANSWERFILE_MODE=false
[[ -f "$ANSWERFILE" ]] && ANSWERFILE_MODE=true
BACKTITLE="the_miro's Arch Dotfiles" BACKTITLE="the_miro's Arch Dotfiles"
# ── Cyberqueer dialog theme ─────────────────────────────────────────────────── # ── Cyberqueer dialog theme ───────────────────────────────────────────────────
@ -56,6 +60,12 @@ require_dialog() {
sudo pacman -S --noconfirm dialog || { echo "Failed to install dialog."; exit 1; } sudo pacman -S --noconfirm dialog || { echo "Failed to install dialog."; exit 1; }
} }
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() { die() {
clear clear
printf "\n Error: %s\n\n" "$1" >&2 printf "\n Error: %s\n\n" "$1" >&2
@ -79,11 +89,15 @@ run_module() {
bash "$script" 2>&1 | tee -a "$LOG" || rc=${PIPESTATUS[0]} bash "$script" 2>&1 | tee -a "$LOG" || rc=${PIPESTATUS[0]}
if [[ $rc -ne 0 ]]; then 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
dialog --backtitle "$BACKTITLE" \ dialog --backtitle "$BACKTITLE" \
--title " Module Failed " \ --title " Module Failed " \
--yesno "$label exited with code $rc.\n\nContinue anyway?" 8 54 \ --yesno "$label exited with code $rc.\n\nContinue anyway?" 8 54 \
|| { clear; exit 1; } || { clear; exit 1; }
fi fi
fi
} }
count_steps() { count_steps() {
@ -148,13 +162,53 @@ count_steps() {
[[ "$a" == *"kate"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"kate"* ]] && 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 ───────────────────────────────────────────────────────────────── # ── Preflight ─────────────────────────────────────────────────────────────────
[[ $EUID -eq 0 ]] && die "Run as your normal user (not root)." [[ $EUID -eq 0 ]] && die "Run as your normal user (not root)."
command -v pacman &>/dev/null || die "pacman not found — Arch Linux required." command -v pacman &>/dev/null || die "pacman not found — Arch Linux required."
require_dialog require_dialog
if $ANSWERFILE_MODE; then
load_answerfile
printf "Answerfile mode: %s\n" "$ANSWERFILE" | tee -a "$LOG"
fi
if ! ping -c1 -W3 archlinux.org &>/dev/null; then if ! ping -c1 -W3 archlinux.org &>/dev/null; then
if $ANSWERFILE_MODE; then
printf "Warning: no internet connection detected.\n" | tee -a "$LOG"
else
dialog --backtitle "$BACKTITLE" \ dialog --backtitle "$BACKTITLE" \
--title " No Network Detected " \ --title " No Network Detected " \
--msgbox "\n No internet connection found.\n\n nmtui will open so you can configure networking.\n Close nmtui when done to continue the installer.\n" 11 58 --msgbox "\n No internet connection found.\n\n nmtui will open so you can configure networking.\n Close nmtui when done to continue the installer.\n" 11 58
@ -165,13 +219,15 @@ if ! ping -c1 -W3 archlinux.org &>/dev/null; then
--yesno "\n Still no internet connection.\n\n Packages cannot be downloaded without network access.\n\n Continue anyway?" 11 58 \ --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; } || { clear; echo "Aborted — no network."; exit 1; }
fi fi
fi
fi fi
> "$LOG" > "$LOG"
printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >> "$LOG" printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >> "$LOG"
# ── Welcome ─────────────────────────────────────────────────────────────────── # ── Welcome ───────────────────────────────────────────────────────────────────
dialog --backtitle "$BACKTITLE" \ if ! $ANSWERFILE_MODE; then
dialog --backtitle "$BACKTITLE" \
--title " Welcome " \ --title " Welcome " \
--msgbox "\n\ --msgbox "\n\
the_miro's Arch dotfiles installer\n\ the_miro's Arch dotfiles installer\n\
@ -182,9 +238,35 @@ dialog --backtitle "$BACKTITLE" \
\n\ \n\
Source: $DOTFILES_DIR\n\ Source: $DOTFILES_DIR\n\
Log: $LOG\n" 14 62 Log: $LOG\n" 14 62
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_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
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 ─────────────────────────────────────────────────────── # ── Component selection ───────────────────────────────────────────────────────
COMPONENTS=$(dialog --backtitle "$BACKTITLE" \ if $ANSWERFILE_MODE; then
COMPONENTS="$AF_COMPONENTS"
else
COMPONENTS=$(dialog --backtitle "$BACKTITLE" \
--title " Select Components " \ --title " Select Components " \
--checklist "Space toggles · Enter confirms · Esc quits" 15 68 4 \ --checklist "Space toggles · Enter confirms · Esc quits" 15 68 4 \
"pkg" "Package managers yay · nvm · rust" on \ "pkg" "Package managers yay · nvm · rust" on \
@ -192,9 +274,13 @@ COMPONENTS=$(dialog --backtitle "$BACKTITLE" \
"svc" "Core services NetworkManager · cronie · fail2ban" on \ "svc" "Core services NetworkManager · cronie · fail2ban" on \
"shell" "Shell setup zsh · nvim · yazi · micro · starship" on \ "shell" "Shell setup zsh · nvim · yazi · micro · starship" on \
3>&1 1>&2 2>&3) || { clear; echo "Aborted."; exit 0; } 3>&1 1>&2 2>&3) || { clear; echo "Aborted."; exit 0; }
fi
# ── DE selection ────────────────────────────────────────────────────────────── # ── DE selection ──────────────────────────────────────────────────────────────
DE=$(dialog --backtitle "$BACKTITLE" \ if $ANSWERFILE_MODE; then
DE="$AF_DE"
else
DE=$(dialog --backtitle "$BACKTITLE" \
--title " Desktop Environment " \ --title " Desktop Environment " \
--menu "Select a desktop environment · Esc / none to skip:" 20 70 8 \ --menu "Select a desktop environment · Esc / none to skip:" 20 70 8 \
"hyprland" "Hyprland — Wayland WM, full setup (primary)" \ "hyprland" "Hyprland — Wayland WM, full setup (primary)" \
@ -206,9 +292,13 @@ DE=$(dialog --backtitle "$BACKTITLE" \
"lxqt" "LXQt — lightweight Qt X11 DE" \ "lxqt" "LXQt — lightweight Qt X11 DE" \
"none" "Skip DE installation" \ "none" "Skip DE installation" \
3>&1 1>&2 2>&3) || DE="none" 3>&1 1>&2 2>&3) || DE="none"
fi
# ── Apps selection ──────────────────────────────────────────────────────────── # ── Apps selection ────────────────────────────────────────────────────────────
SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \ if $ANSWERFILE_MODE; then
SELECTED_APPS="$AF_APPS"
else
SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \
--title " Applications " \ --title " Applications " \
--checklist "Optional applications — installed after base components:" 40 76 32 \ --checklist "Optional applications — installed after base components:" 40 76 32 \
"ollama" "Ollama local LLM runner + API server" off \ "ollama" "Ollama local LLM runner + API server" off \
@ -266,16 +356,19 @@ SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \
"codeblocks" "Code::Blocks C/C++ IDE (official)" off \ "codeblocks" "Code::Blocks C/C++ IDE (official)" off \
"kate" "Kate KDE advanced text editor (official)" off \ "kate" "Kate KDE advanced text editor (official)" off \
3>&1 1>&2 2>&3) || SELECTED_APPS="" 3>&1 1>&2 2>&3) || SELECTED_APPS=""
fi
# ── Confirmation ────────────────────────────────────────────────────────────── # ── Confirmation (interactive mode only) ──────────────────────────────────────
SUMMARY="" if ! $ANSWERFILE_MODE; then
[[ "$COMPONENTS" == *"pkg"* ]] && SUMMARY+=" ✦ Package managers (yay, nvm, rust)\n" SUMMARY=""
[[ "$COMPONENTS" == *"core"* ]] && SUMMARY+=" ✦ Core packages\n" [[ -n "$HOSTNAME_SET" ]] && SUMMARY+=" ✦ Hostname: $HOSTNAME_SET\n"
[[ "$COMPONENTS" == *"svc"* ]] && SUMMARY+=" ✦ Core services\n" [[ "$COMPONENTS" == *"pkg"* ]] && SUMMARY+=" ✦ Package managers (yay, nvm, rust)\n"
[[ "$COMPONENTS" == *"shell"* ]] && SUMMARY+=" ✦ Shell setup\n" [[ "$COMPONENTS" == *"core"* ]] && SUMMARY+=" ✦ Core packages\n"
[[ "$DE" != "none" ]] && SUMMARY+=" ✦ Desktop environment: $DE\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 if [[ -n "$SELECTED_APPS" ]]; then
SUMMARY+="\n Applications:\n" SUMMARY+="\n Applications:\n"
[[ "$SELECTED_APPS" == *"ollama"* ]] && SUMMARY+=" ✦ Ollama\n" [[ "$SELECTED_APPS" == *"ollama"* ]] && SUMMARY+=" ✦ Ollama\n"
[[ "$SELECTED_APPS" == *"llama-cpp"* ]] && SUMMARY+=" ✦ llama.cpp\n" [[ "$SELECTED_APPS" == *"llama-cpp"* ]] && SUMMARY+=" ✦ llama.cpp\n"
@ -329,12 +422,13 @@ if [[ -n "$SELECTED_APPS" ]]; then
[[ "$SELECTED_APPS" == *"geany"* ]] && SUMMARY+=" ✦ Geany\n" [[ "$SELECTED_APPS" == *"geany"* ]] && SUMMARY+=" ✦ Geany\n"
[[ "$SELECTED_APPS" == *"codeblocks"* ]] && SUMMARY+=" ✦ Code::Blocks\n" [[ "$SELECTED_APPS" == *"codeblocks"* ]] && SUMMARY+=" ✦ Code::Blocks\n"
[[ "$SELECTED_APPS" == *"kate"* ]] && SUMMARY+=" ✦ Kate\n" [[ "$SELECTED_APPS" == *"kate"* ]] && SUMMARY+=" ✦ Kate\n"
fi fi
dialog --backtitle "$BACKTITLE" \ dialog --backtitle "$BACKTITLE" \
--title " Confirm Installation " \ --title " Confirm Installation " \
--yesno "\n Components to install:\n\n${SUMMARY}\n Log: $LOG\n\n Proceed?" \ --yesno "\n Components to install:\n\n${SUMMARY}\n Log: $LOG\n\n Proceed?" \
24 62 || { clear; echo "Aborted."; exit 0; } 24 62 || { clear; echo "Aborted."; exit 0; }
fi
count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS" count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS"
@ -410,10 +504,85 @@ fi
[[ "$SELECTED_APPS" == *"codeblocks"* ]] && run_module "Code::Blocks" "$APPS/codeblocks.sh" [[ "$SELECTED_APPS" == *"codeblocks"* ]] && run_module "Code::Blocks" "$APPS/codeblocks.sh"
[[ "$SELECTED_APPS" == *"kate"* ]] && run_module "Kate" "$APPS/kate.sh" [[ "$SELECTED_APPS" == *"kate"* ]] && run_module "Kate" "$APPS/kate.sh"
# ── Colorway (final step) ─────────────────────────────────────────────────────
# Read defaults from repo colors.conf for pre-population
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
# Apply colors from answerfile if any are set
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
# Interactive: show color form dialog
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 -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}"
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
# ── Done ────────────────────────────────────────────────────────────────────── # ── Done ──────────────────────────────────────────────────────────────────────
dialog --backtitle "$BACKTITLE" \ if $ANSWERFILE_MODE; then
printf "\nDone. Log: %s\n" "$LOG"
else
dialog --backtitle "$BACKTITLE" \
--title " Done " \ --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 --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 clear
printf "\n Done. Log: %s\n\n" "$LOG" printf "\n Done. Log: %s\n\n" "$LOG"
fi