fix(installer): implement robust step-based navigation and fix multi-line sed error

- Implement a step-based state machine in main loop to support 'Back' navigation via Esc.
- Refactor all prompts to use safe exit-code capture (rc -eq 130/1) and handle 'not submitted' output.
- Add input flushing after Esc events to prevent cascading backtrack signals.
- Add short-circuit checks to every wizard stage for reliable skip-forward behavior.
- Fix sed error when generating multi-disk configurations by escaping newlines in additional_disks.
- Add explicit 'Set a hostname' message to the hostname prompt.
- Convert unsafe short-circuit lists to safe if statements to prevent set -e crashes.
This commit is contained in:
Bernardo Magri
2026-04-26 22:17:00 +01:00
parent 55f0653e59
commit 3aadc36bff

View File

@@ -115,9 +115,28 @@ load_state() {
# Helper to run commands via nix run # Helper to run commands via nix run
nrun() { nrun() {
local pkg="$1" local cmd="$1"
shift shift
nix run --extra-experimental-features "nix-command flakes" "nixpkgs#$pkg" -- "$@" if command -v "$cmd" >/dev/null 2>&1; then
"$cmd" "$@"
else
nix run --extra-experimental-features "nix-command flakes" "nixpkgs#$cmd" -- "$@"
fi
}
clear_step_state() {
case "$1" in
select_disk) TARGET_DRIVE="" ;;
get_luks_passphrase) LUKS_PASSWORD="" ;;
configure_user) USERNAME=""; HOSTNAME=""; USER_PASSWORD="" ;;
select_keymap_locale) KEYMAP_LAYOUT=""; KEYMAP_VARIANT=""; LOCALE="" ;;
select_timezone) TIMEZONE="" ;;
select_hardware) HARDWARE_MODULES=""; NOMARCHY_HW_OPTS="" ;;
confirm_form_factor) FORM_FACTOR="" ;;
configure_impermanence) ENABLE_IMPERMANENCE="" ;;
select_profiles) SELECTED_PROFILES="" ;;
select_nomarchy_rev) NOMARCHY_REV="" ;;
esac
} }
header() { header() {
@@ -201,7 +220,7 @@ check_environment() {
while ! ping -c 1 -W 2 1.1.1.1 &>/dev/null; do while ! ping -c 1 -W 2 1.1.1.1 &>/dev/null; do
error "No internet connection" error "No internet connection"
local choice local choice
choice=$(gum choose "Open Network Manager (nmtui)" "Retry" "Exit") choice=$(gum choose "Open Network Manager (nmtui)" "Retry" "Exit") || exit 1
case "$choice" in case "$choice" in
*nmtui*) nmtui ;; *nmtui*) nmtui ;;
*Exit*) exit 1 ;; *Exit*) exit 1 ;;
@@ -218,8 +237,8 @@ select_disk() {
section "Disk Selection" section "Disk Selection"
if [[ -n "$TARGET_DRIVE" ]]; then if [[ -n "$TARGET_DRIVE" ]]; then
success "Resumed: $TARGET_DRIVE" success "Selected: $TARGET_DRIVE"
return return 0
fi fi
# Build a richer drive table than the bare `NAME SIZE` lsblk default. # Build a richer drive table than the bare `NAME SIZE` lsblk default.
@@ -230,7 +249,7 @@ select_disk() {
| grep -vE '^(/dev/(loop|ram|zram|sr))') | grep -vE '^(/dev/(loop|ram|zram|sr))')
while IFS= read -r line; do while IFS= read -r line; do
[[ -z "$line" ]] && continue if [[ -z "$line" ]]; then continue; fi
# NAME and SIZE are reliably whitespace-free; ROTA/TRAN are short # NAME and SIZE are reliably whitespace-free; ROTA/TRAN are short
# tokens; VENDOR/MODEL/SERIAL can carry internal spaces. Pull the # tokens; VENDOR/MODEL/SERIAL can carry internal spaces. Pull the
# first four fields off the front, treat the rest as the # first four fields off the front, treat the rest as the
@@ -254,9 +273,9 @@ select_disk() {
vendor=$(lsblk -d -n -o VENDOR "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//') vendor=$(lsblk -d -n -o VENDOR "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')
model=$(lsblk -d -n -o MODEL "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//') model=$(lsblk -d -n -o MODEL "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')
serial=$(lsblk -d -n -o SERIAL "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//') serial=$(lsblk -d -n -o SERIAL "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')
[[ -z "$vendor" ]] && vendor="--" if [[ -z "$vendor" ]]; then vendor="--"; fi
[[ -z "$model" ]] && model="--" if [[ -z "$model" ]]; then model="--"; fi
[[ -z "$serial" ]] && serial="--" if [[ -z "$serial" ]]; then serial="--"; fi
# Tab-separated for column -t -s, then collapse internal whitespace # Tab-separated for column -t -s, then collapse internal whitespace
# in MODEL so multi-space brand strings don't break alignment. # in MODEL so multi-space brand strings don't break alignment.
@@ -281,20 +300,25 @@ select_disk() {
# gum choose gets the same aligned rows so the picker reads like the table. # gum choose gets the same aligned rows so the picker reads like the table.
local picker local picker
picker=$(printf '%s' "$rows" | column -t -s $'\t') picker=$(printf '%s' "$rows" | column -t -s $'\t')
local choice local choice rc=0
choice=$(printf '%s\n' "$picker" | gum choose --no-limit --header "Select target drive(s) - Use Space to select multiple for BTRFS RAID/Single") choice=$(printf '%s\n' "$picker" | nrun gum choose --no-limit --header "Select target drive(s) - Use Space to select multiple for BTRFS RAID/Single") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$choice" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
TARGET_DRIVE=$(awk '{print $1}' <<<"$choice" | xargs) TARGET_DRIVE=$(awk '{print $1}' <<<"$choice" | xargs)
if [[ -z "$TARGET_DRIVE" ]]; then if [[ -z "$TARGET_DRIVE" ]]; then
error "No drive selected" error "No drive selected"
exit 1 return 130
fi fi
if [[ "$DRY_RUN" != "true" ]]; then if [[ "$DRY_RUN" != "true" ]]; then
echo "" echo ""
gum style --foreground 9 --bold "⚠ WARNING: All data on $TARGET_DRIVE will be DESTROYED!" nrun gum style --foreground 9 --bold "⚠ WARNING: All data on $TARGET_DRIVE will be DESTROYED!"
echo "" echo ""
if ! gum confirm "Are you sure you want to use $TARGET_DRIVE?"; then rc=0
nrun gum confirm "Are you sure you want to use $TARGET_DRIVE?" || rc=$?
if [[ $rc -ne 0 ]]; then
if [[ $rc -eq 130 || $rc -eq 1 ]]; then return 130; fi
error "Aborted" error "Aborted"
exit 1 exit 1
fi fi
@@ -318,7 +342,7 @@ get_luks_passphrase() {
# Already set this session (review-edit loop iterated). Don't re-prompt; # Already set this session (review-edit loop iterated). Don't re-prompt;
# password isn't persisted across runs but is held in memory until # password isn't persisted across runs but is held in memory until
# execute_installation unsets it. # execute_installation unsets it.
[[ -n "${LUKS_PASSWORD:-}" ]] && return if [[ -n "${LUKS_PASSWORD:-}" ]]; then return; fi
section "Disk Encryption" section "Disk Encryption"
@@ -326,12 +350,19 @@ get_luks_passphrase() {
info "Enter a strong passphrase (you'll need this at every boot)." info "Enter a strong passphrase (you'll need this at every boot)."
echo "" echo ""
local pass1 pass2 local pass1 pass2 rc=0
while true; do while true; do
pass1=$(gum input --password --placeholder "Enter LUKS passphrase") rc=0
[[ -z "$pass1" ]] && continue pass1=$(nrun gum input --password --placeholder "Enter LUKS passphrase") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$pass1" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
pass2=$(gum input --password --placeholder "Confirm passphrase") if [[ -z "$pass1" ]]; then continue; fi
rc=0
pass2=$(nrun gum input --password --placeholder "Confirm passphrase") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$pass2" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ "$pass1" == "$pass2" ]]; then if [[ "$pass1" == "$pass2" ]]; then
LUKS_PASSWORD="$pass1" LUKS_PASSWORD="$pass1"
@@ -351,9 +382,24 @@ get_luks_passphrase() {
configure_user() { configure_user() {
section "User Configuration" section "User Configuration"
if [[ -n "$USERNAME" && -n "$HOSTNAME" ]]; then
# Password check skipped in dry run or if already set
if [[ "$DRY_RUN" == "true" ]] || [[ -n "$USER_PASSWORD" ]]; then
success "User $USERNAME @ $HOSTNAME configured"
return 0
fi
fi
local rc=0
if [[ -z "$USERNAME" ]]; then if [[ -z "$USERNAME" ]]; then
USERNAME=$(nrun gum input --placeholder "Enter username (lowercase, no spaces)") rc=0
USERNAME=$(nrun gum input --placeholder "Enter username (lowercase, no spaces)") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$USERNAME" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ -z "$USERNAME" ]] || [[ ! "$USERNAME" =~ ^[a-z][a-z0-9_-]*$ ]]; then if [[ -z "$USERNAME" ]] || [[ ! "$USERNAME" =~ ^[a-z][a-z0-9_-]*$ ]]; then
# If they just hit Enter/Esc and we got here, they might want to go back
if [[ -z "$USERNAME" ]]; then return 130; fi
error "Invalid username" error "Invalid username"
exit 1 exit 1
fi fi
@@ -361,8 +407,14 @@ configure_user() {
success "Username: $USERNAME" success "Username: $USERNAME"
if [[ -z "$HOSTNAME" ]]; then if [[ -z "$HOSTNAME" ]]; then
HOSTNAME=$(nrun gum input --value "nomarchy" --placeholder "Hostname for this machine") info "Set a hostname for this machine"
rc=0
HOSTNAME=$(nrun gum input --value "nomarchy" --placeholder "Hostname for this machine") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$HOSTNAME" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ -z "$HOSTNAME" ]] || [[ ! "$HOSTNAME" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then if [[ -z "$HOSTNAME" ]] || [[ ! "$HOSTNAME" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
if [[ -z "$HOSTNAME" ]]; then return 130; fi
error "Invalid hostname (use lowercase letters, digits, and hyphens only)" error "Invalid hostname (use lowercase letters, digits, and hyphens only)"
exit 1 exit 1
fi fi
@@ -380,10 +432,17 @@ configure_user() {
info "Set a password for your user account" info "Set a password for your user account"
local pass1 pass2 local pass1 pass2
while true; do while true; do
pass1=$(nrun gum input --password --placeholder "Enter user password") rc=0
[[ -z "$pass1" ]] && continue pass1=$(nrun gum input --password --placeholder "Enter user password") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$pass1" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
pass2=$(nrun gum input --password --placeholder "Confirm user password") if [[ -z "$pass1" ]]; then continue; fi
rc=0
pass2=$(nrun gum input --password --placeholder "Confirm user password") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$pass2" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ "$pass1" == "$pass2" ]]; then if [[ "$pass1" == "$pass2" ]]; then
USER_PASSWORD="$pass1" USER_PASSWORD="$pass1"
@@ -407,17 +466,28 @@ configure_user() {
select_keymap_locale() { select_keymap_locale() {
section "Keyboard & Language" section "Keyboard & Language"
if [[ -n "$KEYMAP_LAYOUT" && -n "$LOCALE" ]]; then
success "Keymap ($KEYMAP_LAYOUT) and Locale ($LOCALE) set"
return 0
fi
local choice rc=0
if [[ -z "$KEYMAP_LAYOUT" ]]; then if [[ -z "$KEYMAP_LAYOUT" ]]; then
local choice rc=0
choice=$(printf '%s\n' "${COMMON_KEYMAPS[@]}" "Other…" \ choice=$(printf '%s\n' "${COMMON_KEYMAPS[@]}" "Other…" \
| nrun gum choose --header "Keyboard layout") | nrun gum choose --header "Keyboard layout") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$choice" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ "$choice" == "Other…" ]]; then if [[ "$choice" == "Other…" ]]; then
rc=0
KEYMAP_LAYOUT=$(localectl list-x11-keymap-layouts 2>/dev/null \ KEYMAP_LAYOUT=$(localectl list-x11-keymap-layouts 2>/dev/null \
| nrun gum filter --placeholder "Search keyboard layout…") | nrun gum filter --placeholder "Search keyboard layout…") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$KEYMAP_LAYOUT" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
else else
KEYMAP_LAYOUT="$choice" KEYMAP_LAYOUT="$choice"
fi fi
[[ -z "$KEYMAP_LAYOUT" ]] && KEYMAP_LAYOUT="us" if [[ -z "$KEYMAP_LAYOUT" ]]; then KEYMAP_LAYOUT="us"; fi
fi fi
success "Keyboard layout: $KEYMAP_LAYOUT" success "Keyboard layout: $KEYMAP_LAYOUT"
@@ -427,12 +497,20 @@ select_keymap_locale() {
variants=$(localectl list-x11-keymap-variants "$KEYMAP_LAYOUT" 2>/dev/null || true) variants=$(localectl list-x11-keymap-variants "$KEYMAP_LAYOUT" 2>/dev/null || true)
if [[ -n "$variants" ]]; then if [[ -n "$variants" ]]; then
local v local v
rc=0
v=$(printf '(none)\n%s\n' "$variants" \ v=$(printf '(none)\n%s\n' "$variants" \
| nrun gum filter --placeholder "Variant (optional)" --value "(none)") | nrun gum filter --placeholder "Variant (optional)" --value "(none)") || rc=$?
[[ "$v" == "(none)" || -z "$v" ]] || KEYMAP_VARIANT="$v" if [[ $rc -eq 130 || $rc -eq 1 || "$v" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ -n "$v" && "$v" != "(none)" ]]; then KEYMAP_VARIANT="$v"; fi
fi fi
fi fi
[[ -n "$KEYMAP_VARIANT" ]] && success "Variant: $KEYMAP_VARIANT" || success "Variant: (default)"
if [[ -n "$KEYMAP_VARIANT" ]]; then
success "Variant: $KEYMAP_VARIANT"
else
success "Variant: (default)"
fi
# Apply to the live session, best-effort. # Apply to the live session, best-effort.
loadkeys "$KEYMAP_LAYOUT" 2>/dev/null || true loadkeys "$KEYMAP_LAYOUT" 2>/dev/null || true
@@ -442,16 +520,22 @@ select_keymap_locale() {
fi fi
if [[ -z "$LOCALE" ]]; then if [[ -z "$LOCALE" ]]; then
local choice local choice rc=0
rc=0
choice=$(printf '%s\n' "${COMMON_LOCALES[@]}" "Other…" \ choice=$(printf '%s\n' "${COMMON_LOCALES[@]}" "Other…" \
| nrun gum choose --header "Language / locale") | nrun gum choose --header "Language / locale") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$choice" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ "$choice" == "Other…" ]]; then if [[ "$choice" == "Other…" ]]; then
rc=0
LOCALE=$(localectl list-locales 2>/dev/null \ LOCALE=$(localectl list-locales 2>/dev/null \
| nrun gum filter --placeholder "Search locale…") | nrun gum filter --placeholder "Search locale…") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$LOCALE" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
else else
LOCALE="$choice" LOCALE="$choice"
fi fi
[[ -z "$LOCALE" ]] && LOCALE="en_US.UTF-8" if [[ -z "$LOCALE" ]]; then LOCALE="en_US.UTF-8"; fi
fi fi
success "Locale: $LOCALE" success "Locale: $LOCALE"
@@ -465,16 +549,18 @@ select_keymap_locale() {
select_timezone() { select_timezone() {
section "Timezone" section "Timezone"
if [[ -n "$TIMEZONE" ]] && [[ "$TIMEZONE" != "UTC" ]]; then if [[ -n "$TIMEZONE" ]]; then
success "Resumed: $TIMEZONE" success "Timezone set to $TIMEZONE"
return return 0
fi fi
local timezones local timezones rc=0
timezones=$(timedatectl list-timezones 2>/dev/null || echo "UTC") timezones=$(timedatectl list-timezones 2>/dev/null || echo "UTC")
TIMEZONE=$(echo "$timezones" | gum filter --placeholder "Search timezone...") rc=0
TIMEZONE=$(echo "$timezones" | gum filter --placeholder "Search timezone...") || rc=$?
[[ -z "$TIMEZONE" ]] && TIMEZONE="UTC" if [[ $rc -eq 130 || $rc -eq 1 || "$TIMEZONE" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ -z "$TIMEZONE" ]]; then TIMEZONE="UTC"; fi
success "Timezone: $TIMEZONE" success "Timezone: $TIMEZONE"
save_state save_state
} }
@@ -487,8 +573,8 @@ select_hardware() {
section "Hardware Configuration" section "Hardware Configuration"
if [[ -n "$HARDWARE_MODULES" ]]; then if [[ -n "$HARDWARE_MODULES" ]]; then
success "Resumed hardware modules" success "Hardware configured"
return return 0
fi fi
local dmi_vendor dmi_product detect_output local dmi_vendor dmi_product detect_output
@@ -514,24 +600,39 @@ select_hardware() {
done <<< "$detect_output" done <<< "$detect_output"
# Let the user accept, extend, or replace the detection. # Let the user accept, extend, or replace the detection.
local choice local choice rc=0
while true; do
rc=0
choice=$(nrun gum choose --header "Hardware configuration" \ choice=$(nrun gum choose --header "Hardware configuration" \
"Accept detected modules" \ "Accept detected modules" \
"Add an extra nixos-hardware module" \ "Add an extra nixos-hardware module" \
"Pick from the manual list (override)") "Pick from the manual list (override)") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$choice" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
case "$choice" in case "$choice" in
"Add an extra nixos-hardware module") "Add an extra nixos-hardware module")
local extra local extra extra_rc=0
extra=$(nrun gum input --placeholder "e.g. asus-zephyrus-ga401 (no 'nixos-hardware.' prefix)") extra=$(nrun gum input --placeholder "e.g. asus-zephyrus-ga401 (no 'nixos-hardware.' prefix)") || extra_rc=$?
[[ -n "$extra" ]] && modules+=("$extra") if [[ $extra_rc -eq 130 || $extra_rc -eq 1 || "$extra" == "not submitted" ]]; then continue; fi
if [[ $extra_rc -ne 0 ]]; then exit $extra_rc; fi
if [[ -n "$extra" ]]; then modules+=("$extra"); fi
break
;; ;;
"Pick from the manual list (override)") "Pick from the manual list (override)")
modules=() modules=()
hw_opts=() hw_opts=()
_select_hardware_manual modules hw_opts local manual_rc=0
_select_hardware_manual modules hw_opts || manual_rc=$?
if [[ $manual_rc -eq 130 ]]; then continue; fi
if [[ $manual_rc -ne 0 ]]; then exit $manual_rc; fi
break
;;
"Accept detected modules")
break
;; ;;
esac esac
done
# De-duplicate while preserving order. # De-duplicate while preserving order.
local seen="" uniq_mods=() m local seen="" uniq_mods=() m
@@ -571,66 +672,76 @@ _select_hardware_manual() {
local -n _mods_ref="$1" local -n _mods_ref="$1"
local -n _opts_ref="$2" local -n _opts_ref="$2"
local vendor local vendor rc=0
rc=0
vendor=$(nrun gum choose --header "Pick vendor" \ vendor=$(nrun gum choose --header "Pick vendor" \
"Framework" "Dell" "Lenovo" "Apple (T2 Mac)" "Microsoft Surface" "ASUS" "System76" "Other...") "Framework" "Dell" "Lenovo" "Apple (T2 Mac)" "Microsoft Surface" "ASUS" "System76" "Other...") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$vendor" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
case "$vendor" in case "$vendor" in
Framework) Framework)
local model local model model_rc=0
model=$(nrun gum choose \ model=$(nrun gum choose \
"framework-16-7040-amd" \ "framework-16-7040-amd" \
"framework-13-amd-ai-300-series" \ "framework-13-amd-ai-300-series" \
"framework-13-7040-amd" \ "framework-13-7040-amd" \
"framework-13-13th-gen-intel" \ "framework-13-13th-gen-intel" \
"framework-13-12th-gen-intel" \ "framework-13-12th-gen-intel" \
"framework-13-11th-gen-intel") "framework-13-11th-gen-intel") || model_rc=$?
if [[ $model_rc -eq 130 || $model_rc -eq 1 || "$model" == "not submitted" ]]; then return 130; fi
if [[ $model_rc -ne 0 ]]; then exit $model_rc; fi
_mods_ref+=("$model") _mods_ref+=("$model")
_opts_ref+=("isFramework=true") _opts_ref+=("isFramework=true")
;; ;;
Dell) Dell)
local model local model model_rc=0
model=$(nrun gum choose \ model=$(nrun gum choose \
"dell-xps-15-9500" "dell-xps-15-9510" "dell-xps-15-9520" \ "dell-xps-15-9500" "dell-xps-15-9510" "dell-xps-15-9520" \
"dell-xps-13-9310" "dell-xps-13-9370" "dell-xps-13-9380" \ "dell-xps-13-9310" "dell-xps-13-9370" "dell-xps-13-9380" \
"dell-precision-5530" "dell-latitude-7480") "dell-precision-5530" "dell-latitude-7480") || model_rc=$?
if [[ $model_rc -eq 130 || $model_rc -eq 1 || "$model" == "not submitted" ]]; then return 130; fi
if [[ $model_rc -ne 0 ]]; then exit $model_rc; fi
_mods_ref+=("$model") _mods_ref+=("$model")
[[ "$model" == *xps* ]] && _opts_ref+=("isXPS=true") if [[ "$model" == *xps* ]]; then _opts_ref+=("isXPS=true"); fi
;; ;;
Lenovo) Lenovo)
local model local model model_rc=0
model=$(nrun gum choose \ model=$(nrun gum choose \
"lenovo-thinkpad-x1-carbon-gen11" "lenovo-thinkpad-x1-carbon-gen10" \ "lenovo-thinkpad-x1-carbon-gen11" "lenovo-thinkpad-x1-carbon-gen10" \
"lenovo-thinkpad-x1-carbon-gen9" "lenovo-thinkpad-x1-extreme" \ "lenovo-thinkpad-x1-carbon-gen9" "lenovo-thinkpad-x1-extreme" \
"lenovo-thinkpad-t14-amd-gen3" "lenovo-thinkpad-t14-amd-gen2" \ "lenovo-thinkpad-t14-amd-gen3" "lenovo-thinkpad-t14-amd-gen2" \
"lenovo-thinkpad-t480" "lenovo-thinkpad-l13") "lenovo-thinkpad-t480" "lenovo-thinkpad-l13") || model_rc=$?
if [[ $model_rc -eq 130 || $model_rc -eq 1 || "$model" == "not submitted" ]]; then return 130; fi
if [[ $model_rc -ne 0 ]]; then exit $model_rc; fi
_mods_ref+=("$model") _mods_ref+=("$model")
;; ;;
"Apple (T2 Mac)")
_mods_ref+=("apple-t2")
_opts_ref+=("isT2Mac=true")
;;
"Microsoft Surface") "Microsoft Surface")
local model local model model_rc=0
model=$(nrun gum choose \ model=$(nrun gum choose \
"microsoft-surface-pro-9" "microsoft-surface-pro-8" "microsoft-surface-pro-7" \ "microsoft-surface-pro-9" "microsoft-surface-pro-8" "microsoft-surface-pro-7" \
"microsoft-surface-laptop-5" "microsoft-surface-laptop-4" "microsoft-surface-laptop-3") "microsoft-surface-laptop-5" "microsoft-surface-laptop-4" "microsoft-surface-laptop-3") || model_rc=$?
if [[ $model_rc -eq 130 || $model_rc -eq 1 || "$model" == "not submitted" ]]; then return 130; fi
if [[ $model_rc -ne 0 ]]; then exit $model_rc; fi
_mods_ref+=("$model") _mods_ref+=("$model")
;; ;;
ASUS) ASUS)
local model local model model_rc=0
model=$(nrun gum choose \ model=$(nrun gum choose \
"asus-zephyrus-ga403" "asus-zephyrus-ga402" "asus-zephyrus-ga401" \ "asus-zephyrus-ga403" "asus-zephyrus-ga402" "asus-zephyrus-ga401" \
"asus-zephyrus-ga503" "asus-rog-strix-g513" "asus-zenbook-ux") "asus-zephyrus-ga503" "asus-rog-strix-g513" "asus-zenbook-ux") || model_rc=$?
if [[ $model_rc -eq 130 || $model_rc -eq 1 || "$model" == "not submitted" ]]; then return 130; fi
if [[ $model_rc -ne 0 ]]; then exit $model_rc; fi
_mods_ref+=("$model") _mods_ref+=("$model")
;; ;;
System76) System76)
_mods_ref+=("system76") _mods_ref+=("system76")
;; ;;
"Other...") "Other...")
local custom local custom custom_rc=0
custom=$(nrun gum input --placeholder "e.g. asus-zephyrus-ga401 (no 'nixos-hardware.' prefix)") custom=$(nrun gum input --placeholder "e.g. asus-zephyrus-ga401 (no 'nixos-hardware.' prefix)") || custom_rc=$?
[[ -n "$custom" ]] && _mods_ref+=("$custom") if [[ $custom_rc -eq 130 || $custom_rc -eq 1 || "$custom" == "not submitted" ]]; then return 130; fi
if [[ $custom_rc -ne 0 ]]; then exit $custom_rc; fi
if [[ -n "$custom" ]]; then _mods_ref+=("$custom"); fi
;; ;;
esac esac
} }
@@ -656,9 +767,12 @@ confirm_form_factor() {
fi fi
info "Auto-detected: $default" info "Auto-detected: $default"
if nrun gum confirm "Treat this machine as a $default?"; then rc=0
nrun gum confirm "Treat this machine as a $default?" || rc=$?
if [[ $rc -eq 0 ]]; then
FORM_FACTOR="$default" FORM_FACTOR="$default"
else else
if [[ $rc -eq 130 || $rc -eq 1 ]]; then return 130; fi
FORM_FACTOR=$([[ "$default" == "laptop" ]] && echo desktop || echo laptop) FORM_FACTOR=$([[ "$default" == "laptop" ]] && echo desktop || echo laptop)
fi fi
success "Form factor: $FORM_FACTOR" success "Form factor: $FORM_FACTOR"
@@ -672,9 +786,13 @@ confirm_form_factor() {
configure_impermanence() { configure_impermanence() {
section "Impermanence (Optional)" section "Impermanence (Optional)"
if [[ "$RESUME" == "true" ]] && [[ "$ENABLE_IMPERMANENCE" != "" ]]; then if [[ -n "$ENABLE_IMPERMANENCE" ]]; then
success "Resumed: impermanence = $ENABLE_IMPERMANENCE" if [[ "$ENABLE_IMPERMANENCE" == "true" ]]; then
return success "Impermanence enabled"
else
info "Impermanence disabled"
fi
return 0
fi fi
info "Impermanence erases your root filesystem on every boot." info "Impermanence erases your root filesystem on every boot."
@@ -682,10 +800,13 @@ configure_impermanence() {
info "This provides a clean, reproducible system." info "This provides a clean, reproducible system."
echo "" echo ""
if gum confirm "Enable Impermanence?"; then rc=0
gum confirm "Enable Impermanence?" || rc=$?
if [[ $rc -eq 0 ]]; then
ENABLE_IMPERMANENCE="true" ENABLE_IMPERMANENCE="true"
success "Impermanence enabled" success "Impermanence enabled"
else else
if [[ $rc -eq 130 || $rc -eq 1 ]]; then return 130; fi
info "Impermanence disabled (traditional persistent root)" info "Impermanence disabled (traditional persistent root)"
fi fi
save_state save_state
@@ -699,21 +820,23 @@ select_profiles() {
section "Software Profiles" section "Software Profiles"
if [[ -n "$SELECTED_PROFILES" ]]; then if [[ -n "$SELECTED_PROFILES" ]]; then
success "Resumed selected profiles" success "Software profiles selected"
return return 0
fi fi
info "Pick optional software profiles to include in your configuration." info "Pick optional software profiles to include in your configuration."
info "Multiple selection allowed (Space to select, Enter to confirm)." info "Multiple selection allowed (Space to select, Enter to confirm)."
echo "" echo ""
rc=0
SELECTED_PROFILES=$(nrun gum choose --no-limit --header "Select Profiles" \ SELECTED_PROFILES=$(nrun gum choose --no-limit --header "Select Profiles" \
"Dev (VSCode, Git, Lazygit, Zed, Docker)" \ "Dev (VSCode, Git, Lazygit, Zed, Docker)" \
"Gaming (Steam, Gamemode, Lutris, Heroic)" \ "Gaming (Steam, Gamemode, Lutris, Heroic)" \
"Office (LibreOffice, Thunderbird, Obsidian, Zotero)" \ "Office (LibreOffice, Thunderbird, Obsidian, Zotero)" \
"Media (VLC, OBS Studio, GIMP, Inkscape, Spotify)" \ "Media (VLC, OBS Studio, GIMP, Inkscape, Spotify)" \
"CLI Utils (ripgrep, fd, bat, eza, zoxide, fzf)") "CLI Utils (ripgrep, fd, bat, eza, zoxide, fzf)") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$SELECTED_PROFILES" == "not submitted" || "$action" == "not submitted" || "$choices" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ -z "$SELECTED_PROFILES" ]]; then if [[ -z "$SELECTED_PROFILES" ]]; then
info "No profiles selected (minimal install)" info "No profiles selected (minimal install)"
else else
@@ -752,11 +875,14 @@ review_configuration() {
nrun gum style --foreground 9 "This will DESTROY all data on $TARGET_DRIVE" nrun gum style --foreground 9 "This will DESTROY all data on $TARGET_DRIVE"
echo "" echo ""
local action local action rc=0
rc=0
action=$(nrun gum choose --header "Choose:" \ action=$(nrun gum choose --header "Choose:" \
"Continue with installation" \ "Continue with installation" \
"Edit a field" \ "Edit a field" \
"Abort") "Abort") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$action" == "not submitted" ]]; then return 130; fi
if [[ $rc -ne 0 ]]; then exit $rc; fi
case "$action" in case "$action" in
"Continue with installation") return 0 ;; "Continue with installation") return 0 ;;
"Edit a field") edit_fields; return 2 ;; "Edit a field") edit_fields; return 2 ;;
@@ -773,7 +899,8 @@ review_configuration() {
edit_fields() { edit_fields() {
section "Edit Fields" section "Edit Fields"
local choices local choices rc=0
rc=0
choices=$(nrun gum choose --no-limit --header "Pick fields to re-enter (space to select, enter to confirm):" \ choices=$(nrun gum choose --no-limit --header "Pick fields to re-enter (space to select, enter to confirm):" \
"Drive ($TARGET_DRIVE)" \ "Drive ($TARGET_DRIVE)" \
"User and host ($USERNAME @ $HOSTNAME)" \ "User and host ($USERNAME @ $HOSTNAME)" \
@@ -783,9 +910,10 @@ edit_fields() {
"Form factor ($FORM_FACTOR)" \ "Form factor ($FORM_FACTOR)" \
"Impermanence ($ENABLE_IMPERMANENCE)" \ "Impermanence ($ENABLE_IMPERMANENCE)" \
"Profiles (${SELECTED_PROFILES:-none})" \ "Profiles (${SELECTED_PROFILES:-none})" \
"Nomarchy rev (${NOMARCHY_REV:-main})") "Nomarchy rev (${NOMARCHY_REV:-main})") || rc=$?
if [[ $rc -eq 130 || $rc -eq 1 || "$choices" == "not submitted" ]]; then return 130; fi
[[ -z "$choices" ]] && return if [[ $rc -ne 0 ]]; then exit $rc; fi
if [[ -z "$choices" ]]; then return 0; fi
while IFS= read -r f; do while IFS= read -r f; do
case "$f" in case "$f" in
@@ -906,7 +1034,10 @@ execute_installation() {
" "
done done
sed "s|@MAIN_DRIVE@|${main_drive}|g; s|@BTRFS_DEVICES@|${btrfs_devs}|g; s|@ADDITIONAL_DISKS@|${additional_disks}|g" "$disko_file" > "$tmp_disko" # Escape newlines for sed
local escaped_disks
escaped_disks=$(printf '%s\n' "$additional_disks" | sed ':a;N;$!ba;s/\n/\\n/g')
sed "s|@MAIN_DRIVE@|${main_drive}|g; s|@BTRFS_DEVICES@|${btrfs_devs}|g; s|@ADDITIONAL_DISKS@|${escaped_disks}|g" "$disko_file" > "$tmp_disko"
else else
disko_file="$NOMARCHY_REPO/installer/disko-golden.nix" disko_file="$NOMARCHY_REPO/installer/disko-golden.nix"
sed "s|@TARGET_DRIVE@|${TARGET_DRIVE}|g" "$disko_file" > "$tmp_disko" sed "s|@TARGET_DRIVE@|${TARGET_DRIVE}|g" "$disko_file" > "$tmp_disko"
@@ -1071,13 +1202,15 @@ EOF
generate_flake_config() { generate_flake_config() {
local impermanence_opt="" local impermanence_opt=""
[[ "$ENABLE_IMPERMANENCE" == "true" ]] && impermanence_opt="nomarchy.system.impermanence.enable = true;" if [[ "$ENABLE_IMPERMANENCE" == "true" ]]; then
impermanence_opt="nomarchy.system.impermanence.enable = true;"
fi
local PROFILE_SYSTEM_OPTS="" local PROFILE_SYSTEM_OPTS=""
local PROFILE_HOME_PACKAGES="" local PROFILE_HOME_PACKAGES=""
while IFS= read -r profile; do while IFS= read -r profile; do
[[ -z "$profile" ]] && continue if [[ -z "$profile" ]]; then continue; fi
case "$profile" in case "$profile" in
"Dev "*) "Dev "*)
PROFILE_SYSTEM_OPTS+=$'\n # Dev profile\n nomarchy.system.virtualization.docker.enable = true;' PROFILE_SYSTEM_OPTS+=$'\n # Dev profile\n nomarchy.system.virtualization.docker.enable = true;'
@@ -1431,11 +1564,7 @@ main() {
check_environment check_environment
# Loop: prompts (each skips when its var is set) → review → Continue|Edit. local steps=(
# On Edit, edit_fields() clears the chosen vars and the next iteration
# re-prompts only those. Continue breaks. --resume lands here with vars
# already loaded, so the first pass goes straight to review.
while true; do
select_disk select_disk
get_luks_passphrase get_luks_passphrase
configure_user configure_user
@@ -1445,7 +1574,65 @@ main() {
confirm_form_factor confirm_form_factor
configure_impermanence configure_impermanence
select_profiles select_profiles
if review_configuration; then break; fi )
local current_step=0
while true; do
if (( current_step < 0 )); then
error "Installation aborted by user."
exit 1
fi
if (( current_step < ${#steps[@]} )); then
local step_func="${steps[current_step]}"
# Run the step. Each step returns 130 if the user presses Esc.
local rc=0
"$step_func" || rc=$?
if (( rc != 0 )); then
if (( rc == 130 )); then
# Clear the current step so it re-prompts if we return to it
clear_step_state "$step_func"
# Flush terminal input buffer to prevent one Esc from cascading
# through multiple consecutive prompts.
sleep 0.1
read -t 0.1 -n 10000 discard 2>/dev/null || true
# Go back one step
current_step=$(( current_step - 1 ))
# If we aren't at the very beginning, clear the TARGET
# step's variables too, so the user is forced to re-enter.
if (( current_step >= 0 )); then
clear_step_state "${steps[current_step]}"
fi
continue
fi
exit "$rc"
fi
current_step=$(( current_step + 1 ))
else
# All steps done, review the configuration.
local review_rc=0
review_configuration || review_rc=$?
if [[ "$review_rc" == "0" ]]; then
break
elif [[ "$review_rc" == "2" ]]; then
# User chose "Edit a field". Restart from step 0; functions
# with still-set variables will return 0 and skip ahead.
current_step=0
elif [[ "$review_rc" == "130" ]]; then
# Go back to the last step.
current_step=$(( ${#steps[@]} - 1 ))
clear_step_state "${steps[current_step]}"
continue
else
exit "$review_rc"
fi
fi
done done
execute_installation execute_installation