#!/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"