refactor(tui-install): replace dialog with plain-bash CLI prompts

Drop the dialog dependency entirely so the installer runs on a bare
console with only bash + coreutils. Reimplement the needed widgets
(msgbox, yesno, input, menu, checklist, form) as ui_* helpers using
read, preserving the cyberqueer magenta/cyan palette via ANSI codes
and the stdout/stderr fd convention so existing capture sites work
unchanged. Update generate-modules.sh to emit the ui_checklist form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-06-26 16:44:48 +02:00
parent 0ab535f772
commit 1354dc4fd1
2 changed files with 209 additions and 142 deletions

View File

@ -60,19 +60,19 @@ for i in "${ACTIVE_IDS[@]}"; do
done
# -- module-checklist (tui-install.sh and generate-answerfile.sh) -------------
# Builds the full SELECTED_APPS=$(dialog ...) call including open/close.
# Builds the full SELECTED_APPS=$(ui_checklist ...) call including open/close.
# ui_checklist (defined in tui-install.sh) is a plain-bash replacement for the
# `dialog --checklist` widget; it takes the same tag/desc/state triplets.
build_checklist_tui() {
local out
out=' SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \\\n'
out+=' --title " Applications " \\\n'
out+=' --checklist "Optional applications — installed after base components:" "$_APP_H" 76 "$_APP_LIST_H" \\\n'
out=' SELECTED_APPS=$(ui_checklist " Applications " "Optional applications — installed after base components:" \\\n'
for i in "${ACTIVE_IDS[@]}"; do
local id="${M_IDS[$i]}" desc="${M_DESCS[$i]}" def="${M_DEFAULTS[$i]}"
local padded
padded=$(printf '%-20s' "$id")
out+=" \"${id}\" \"${padded} ${desc}\" ${def} \\\\\n"
done
out+=' 3>&1 1>&2 2>&3) || SELECTED_APPS=""\n'
out+=' ) || SELECTED_APPS=""\n'
printf '%s' "$out"
}

View File

@ -30,49 +30,165 @@ ANSWERFILE_MODE=false
# Enable unattended mode only when the answerfile is actually present on disk.
[[ -f "$ANSWERFILE" ]] && ANSWERFILE_MODE=true
BACKTITLE="the_miro's Arch Dotfiles"
# ── 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_RULE=$'\033[35m' # magenta — separators
C_ACCENT=$'\033[36m' # cyan — prompts and selected marks
C_BOLD=$'\033[1m'
C_DIM=$'\033[2m'
# ── Terminal dimensions (bare console safe) ───────────────────────────────────
# tput may fail on a bare TTY with no TERM set; fall back to safe 80×24 defaults
# so dialog sizing arithmetic never operates on empty strings.
TERM_H=$(tput lines 2>/dev/null || echo 24)
TERM_W=$(tput cols 2>/dev/null || echo 80)
# ── 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.
# ── Cyberqueer dialog theme ───────────────────────────────────────────────────
# dialog reads its color scheme from $DIALOGRC at startup.
# Writing it to TMP_D (rather than ~/.dialogrc) avoids permanently altering
# the user's existing dialog configuration on the live system.
export DIALOGRC="$TMP_D/dialogrc"
# The heredoc uses single-quoted 'EOF' so no variable expansion happens inside;
# every value is a literal dialog color tuple: (FG, BG, BOLD).
cat > "$DIALOGRC" <<'EOF'
use_shadow = ON
use_colors = ON
screen_color = (BLACK,BLACK,ON)
shadow_color = (BLACK,BLACK,ON)
title_color = (MAGENTA,BLACK,ON)
border_color = (MAGENTA,BLACK,ON)
button_active_color = (BLACK,MAGENTA,ON)
button_inactive_color = (WHITE,BLACK,OFF)
button_key_active_color = (BLACK,CYAN,ON)
button_key_inactive_color = (CYAN,BLACK,ON)
button_label_active_color = (BLACK,MAGENTA,ON)
button_label_inactive_color = (WHITE,BLACK,OFF)
inputbox_color = (WHITE,BLACK,OFF)
inputbox_border_color = (MAGENTA,BLACK,ON)
menubox_color = (WHITE,BLACK,OFF)
menubox_border_color = (MAGENTA,BLACK,ON)
item_color = (WHITE,BLACK,OFF)
item_selected_color = (BLACK,MAGENTA,ON)
tag_color = (CYAN,BLACK,ON)
tag_selected_color = (BLACK,CYAN,ON)
tag_key_color = (CYAN,BLACK,ON)
tag_key_selected_color = (BLACK,CYAN,ON)
check_color = (WHITE,BLACK,OFF)
check_selected_color = (BLACK,MAGENTA,ON)
uarrow_color = (MAGENTA,BLACK,ON)
darrow_color = (MAGENTA,BLACK,ON)
EOF
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 _ </dev/tty || true
printf '\n'
}
ui_yesno() {
# Confirmation prompt. Returns 0 for yes, 1 for no.
# $3 sets the default applied on a bare Enter ("yes" or "no").
local title="$1" body="$2" def="${3:-yes}" ans hint="[Y/n]"
[[ "$def" == no ]] && hint="[y/N]"
ui_header "$title"
printf '%b\n' "$body"
while true; do
printf '\n%s %s %s%s ' "$C_ACCENT" "Confirm?" "$hint" "$C_RESET"
read -r ans </dev/tty || ans=""
ans="${ans,,}"; [[ -z "$ans" ]] && ans="$def"
case "$ans" in
y|yes) return 0 ;;
n|no) return 1 ;;
esac
done
}
ui_input() {
# Single-line text entry. Echoes the entered value (or default) to stdout.
local title="$1" prompt="$2" def="${3:-}" val
{
ui_header "$title"
printf '%b\n' "$prompt"
[[ -n "$def" ]] && printf '%s (default: %s)%s\n' "$C_DIM" "$def" "$C_RESET"
printf '%s > %s' "$C_ACCENT" "$C_RESET"
} >&2
read -r val </dev/tty || val=""
[[ -z "$val" ]] && val="$def"
printf '%s' "$val"
}
ui_menu() {
# Single-choice list. Args: title prompt tag1 desc1 tag2 desc2 …
# Echoes the chosen tag to stdout; returns 1 (no selection) if the user
# enters 'q' or a blank line, so callers can fall back to a default.
local title="$1" prompt="$2"; shift 2
local -a tags=() descs=()
while [[ $# -gt 0 ]]; do tags+=("$1"); descs+=("$2"); shift 2; done
local i choice
while true; do
{
ui_header "$title"
printf '%b\n\n' "$prompt"
for i in "${!tags[@]}"; do
printf ' %s%2d%s) %s%-12s%s %s\n' \
"$C_ACCENT" $((i + 1)) "$C_RESET" "$C_BOLD" "${tags[$i]}" "$C_RESET" "${descs[$i]}"
done
printf '\n%s Enter number (q to skip): %s' "$C_ACCENT" "$C_RESET"
} >&2
read -r choice </dev/tty || choice=""
[[ "$choice" == q || -z "$choice" ]] && return 1
if [[ "$choice" =~ ^[0-9]+$ ]] && (( 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. The list is redrawn each
# round; the user toggles items by entering numbers and/or ranges
# (e.g. "1 3 5-8"), then presses Enter to confirm. Entering 'q' aborts
# (returns 1). The space-separated selected tags are printed 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
while true; do
{
ui_header "$title"
printf '%b\n\n' "$prompt"
for i in "${!tags[@]}"; 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
printf '\n%s Toggle items by number/range (e.g. 1 3 5-8); Enter confirms, q aborts.%s\n' "$C_DIM" "$C_RESET"
printf '%s > %s' "$C_ACCENT" "$C_RESET"
} >&2
read -r line </dev/tty || line=""
[[ "$line" == q ]] && return 1
[[ -z "$line" ]] && break
for tok in $line; do
if [[ "$tok" =~ ^([0-9]+)-([0-9]+)$ ]]; then
lo=${BASH_REMATCH[1]}; hi=${BASH_REMATCH[2]}
elif [[ "$tok" =~ ^[0-9]+$ ]]; then
lo=$tok; hi=$tok
else
continue
fi
for (( j = lo; j <= hi; j++ )); do
(( j >= 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/tty || v=""
[[ -z "$v" ]] && v="$def"
out+="$v"$'\n'
done
printf '%s' "$out"
}
# ── State ─────────────────────────────────────────────────────────────────────
# STEP counts completed modules; TOTAL is pre-computed by count_steps() before
@ -81,24 +197,16 @@ STEP=0
TOTAL=0
# ── Helpers ───────────────────────────────────────────────────────────────────
require_dialog() {
# Short-circuit if dialog is already present; otherwise bootstrap it with pacman.
# This allows the script to be run on a fresh Arch install that hasn't yet installed dialog.
command -v dialog &>/dev/null && return
echo "dialog not found — installing..."
sudo pacman -S --noconfirm dialog || { echo "Failed to install dialog."; exit 1; }
}
require_jq() {
# Same bootstrap pattern as require_dialog: jq is only needed in answerfile mode
# but we install it on demand to avoid a hard dependency for interactive use.
# jq is only needed in answerfile mode, so we install it on demand rather
# than making it a hard dependency for interactive use.
command -v jq &>/dev/null && return
echo "jq not found — installing..."
sudo pacman -S --noconfirm jq || { echo "Failed to install jq."; exit 1; }
}
die() {
# Fatal error helper: clear the dialog overlay before printing so the message
# 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
@ -107,13 +215,11 @@ die() {
warn() {
# Non-fatal notice: in answerfile mode just log it; interactively show a
# dialog msgbox that blocks until the user acknowledges with OK.
# message box that blocks until the user acknowledges with Enter.
if $ANSWERFILE_MODE; then
printf "\n Warning: %s\n" "$1" | tee -a "$LOG"
else
dialog --backtitle "$BACKTITLE" \
--title " Module Conflict " \
--msgbox "\n $1" 8 62
ui_msgbox " Module Conflict " " $1"
fi
}
@ -128,7 +234,7 @@ run_module() {
STEP=$(( STEP + 1 ))
log_sep "[$STEP/$TOTAL] $label"
# Clear the dialog overlay so module output scrolls cleanly on the raw terminal.
# 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"
@ -146,11 +252,9 @@ run_module() {
printf "\n Warning: %s exited with code %d — continuing.\n" "$label" "$rc" | tee -a "$LOG"
else
# Interactive mode: ask the user whether to abort or continue.
# 'dialog --yesno' returns 0 for Yes and 1 for No; the '|| { ...; exit 1; }'
# ui_yesno returns 0 for Yes and 1 for No; the '|| { ...; exit 1; }'
# fires on No, aborting the install.
dialog --backtitle "$BACKTITLE" \
--title " Module Failed " \
--yesno "$label exited with code $rc.\n\nContinue anyway?" 8 54 \
ui_yesno " Module Failed " "$label exited with code $rc.\n\nContinue anyway?" no \
|| { clear; exit 1; }
fi
fi
@ -322,9 +426,6 @@ fi
# the installer cannot function on this OS.
command -v pacman &>/dev/null || die "pacman not found — Arch Linux required."
# Ensure the dialog binary is available before we try to use it.
require_dialog
if $ANSWERFILE_MODE; then
load_answerfile
printf "Answerfile mode: %s\n" "$ANSWERFILE" | tee -a "$LOG"
@ -341,13 +442,9 @@ if ! ping -c1 -W3 archlinux.org &>/dev/null; then
# Give the user a chance to plug in a cable or run iwctl before we
# re-test. A second failed ping offers a soft-abort rather than a
# hard failure, in case the user wants to proceed with a local mirror.
dialog --backtitle "$BACKTITLE" \
--title " No Network Detected " \
--msgbox "\n No internet connection found.\n\n Wired: ensure the cable is plugged in.\n WiFi: switch to another TTY (Alt+F2)\n and run: iwctl\n\n Press OK once connected.\n" 13 58
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
dialog --backtitle "$BACKTITLE" \
--title " Still Offline " \
--yesno "\n Still no internet connection.\n\n Packages cannot be downloaded without network access.\n\n Continue anyway?" 11 58 \
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
@ -360,9 +457,7 @@ printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >
# ── Welcome ───────────────────────────────────────────────────────────────────
if ! $ANSWERFILE_MODE; then
dialog --backtitle "$BACKTITLE" \
--title " Welcome " \
--msgbox "\n\
ui_msgbox " Welcome " "\
the_miro's Arch dotfiles installer\n\
Cyberqueer · Wayland · Hyprland\n\
─────────────────────────────────────────\n\
@ -370,7 +465,7 @@ if ! $ANSWERFILE_MODE; then
Arch Linux — network admin, development & gaming\n\
\n\
Source: $DOTFILES_DIR\n\
Log: $LOG\n" 14 62
Log: $LOG"
fi
# ── Hostname ──────────────────────────────────────────────────────────────────
@ -385,14 +480,10 @@ if $ANSWERFILE_MODE; then
printf "Hostname (from answerfile + MAC): %s\n" "$HOSTNAME_SET" | tee -a "$LOG"
fi
else
# dialog --inputbox: 3>&1 1>&2 2>&3 swaps fd1 and fd2 so the user's typed
# value (written to stdout by dialog) is captured by the $() subshell, while
# dialog's UI (which needs stderr for the terminal) flows to the actual terminal.
# The '|| HOSTNAME_INPUT=""' handles the user pressing Esc (dialog returns 1).
HOSTNAME_INPUT=$(dialog --backtitle "$BACKTITLE" \
--title " Hostname " \
--inputbox "\n Hostname for this machine (leave blank to keep default).\n" 9 54 "" \
3>&1 1>&2 2>&3) || HOSTNAME_INPUT=""
# ui_input prints its prompt to stderr and the typed value to stdout, so the
# $() capture receives only the hostname. A blank line keeps the default.
HOSTNAME_INPUT=$(ui_input " Hostname " \
" Hostname for this machine (leave blank to keep default).") || HOSTNAME_INPUT=""
HOSTNAME_SET="$HOSTNAME_INPUT"
fi
@ -408,30 +499,25 @@ fi
if $ANSWERFILE_MODE; then
COMPONENTS="$AF_COMPONENTS"
else
# dialog --checklist args: height width list-height, then triplets of tag desc state.
# 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.
# The 3>&1 1>&2 2>&3 fd swap captures dialog's output (the selected tags) via $().
# Esc / Cancel returns exit code 1; the '|| { ...; exit 0; }' treats that as a clean abort.
COMPONENTS=$(dialog --backtitle "$BACKTITLE" \
--title " Select Components " \
--checklist "Space toggles · Enter confirms · Esc quits" 16 68 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) || { clear; echo "Aborted."; exit 0; }
# 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
# dialog --menu is a single-choice list; it outputs the selected tag.
# Esc returns exit code 1 — '|| DE="none"' defaults to skipping the DE,
# 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=$(dialog --backtitle "$BACKTITLE" \
--title " Desktop Environment " \
--menu "Select a desktop environment · Esc / none to skip:" 24 72 11 \
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)" \
@ -441,23 +527,15 @@ else
"cosmic" "COSMIC — Rust-built Wayland DE (System76)" \
"xfce" "XFCE — lightweight X11 DE" \
"lxqt" "LXQt — lightweight Qt X11 DE" \
"none" "Skip DE installation" \
3>&1 1>&2 2>&3) || DE="none"
"none" "Skip DE installation") || DE="none"
fi
# ── Apps selection ────────────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then
SELECTED_APPS="$AF_APPS"
else
# Cap the dialog box at 40 rows but shrink to fit smaller terminals.
# The list height is the dialog height minus 8 rows of chrome (title, borders,
# prompt, buttons), with a minimum of 4 to remain usable on tiny screens.
_APP_H=$(( TERM_H - 2 < 40 ? TERM_H - 2 : 40 ))
_APP_LIST_H=$(( _APP_H - 8 < 4 ? 4 : _APP_H - 8 ))
# BEGIN GENERATED MODULES: module-checklist
SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \
--title " Applications " \
--checklist "Optional applications — installed after base components:" "$_APP_H" 76 "$_APP_LIST_H" \
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 \
@ -538,7 +616,7 @@ else
"qemu" "qemu full QEMU/KVM stack with virt-manager" off \
"freeipa-client" "freeipa-client sssd and ipa-client-install with auto-enrollment" off \
"freeipa-server" "freeipa-server interactive FreeIPA server setup with client generator" off \
3>&1 1>&2 2>&3) || SELECTED_APPS=""
) || SELECTED_APPS=""
# END GENERATED MODULES: module-checklist
fi
@ -546,12 +624,10 @@ fi
if $ANSWERFILE_MODE; then
SHELL_RC="$AF_SHELL_RC"
else
SHELL_RC=$(dialog --backtitle "$BACKTITLE" \
--title " Shell Config for New Users " \
--menu "\n Should new users on this machine inherit the dotfiles' rc files?\n (Controls what gets copied to /etc/skel)\n" 13 68 2 \
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" \
3>&1 1>&2 2>&3) || SHELL_RC="defaults"
"defaults" "Skip — use system defaults for new users") || SHELL_RC="defaults"
fi
# ── Confirmation (interactive mode only) ──────────────────────────────────────
@ -654,12 +730,8 @@ if ! $ANSWERFILE_MODE; then
# END GENERATED MODULES: module-summary
fi
# Size the confirmation dialog to the terminal, capped at 24 rows.
_CONF_H=$(( TERM_H - 2 < 24 ? TERM_H - 2 : 24 ))
dialog --backtitle "$BACKTITLE" \
--title " Confirm Installation " \
--yesno "\n Components to install:\n\n${SUMMARY}\n Log: $LOG\n\n Proceed?" \
"$_CONF_H" 62 || { clear; echo "Aborted."; exit 0; }
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
@ -846,20 +918,17 @@ if $ANSWERFILE_MODE; then
bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true
fi
else
# Interactive: show color form dialog
# dialog --form: each field is specified as label row col default frow fcol flen max.
# The fd swap (3>&1 1>&2 2>&3) captures the form output (one value per line) via $().
# Esc / Cancel sets COLORWAY_RAW="" so the colorway step is simply skipped.
COLORWAY_RAW=$(dialog --backtitle "$BACKTITLE" \
--title " Colorway (optional) " \
--form "\n Customize theme colors — bare 6-digit hex, no #.\n Leave unchanged to skip colorway setup.\n" \
16 62 5 \
"COLOR_TEXT " 1 1 "$DEF_TEXT" 1 18 10 7 \
"COLOR_BG " 2 1 "$DEF_BG" 2 18 10 7 \
"COLOR_HIGHLIGHT " 3 1 "$DEF_HIGHLIGHT" 3 18 10 7 \
"COLOR_DARK " 4 1 "$DEF_DARK" 4 18 10 7 \
"COLOR_RED " 5 1 "$DEF_RED" 5 18 10 7 \
3>&1 1>&2 2>&3) || COLORWAY_RAW=""
# 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.
@ -912,9 +981,7 @@ fi
if $ANSWERFILE_MODE; then
printf "\nDone. Log: %s\n" "$LOG"
else
dialog --backtitle "$BACKTITLE" \
--title " Done " \
--msgbox "\n All selected components installed.\n\n Log: $LOG\n\n A reboot may be required for all changes to take effect.\n" 12 58
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"