fix(install): constrain keyboard selection to XKB catalog
All checks were successful
Check / eval (push) Successful in 3m17s

Make keyboard typing filter-only with Gum strict mode, validate interactive and unattended choices against the pinned XKB catalog before any disk mutation, and show resolved human-readable choices in review.

Verified: V1 flake eval, focused installer builds, shell/Python/diff checks; current-ISO KVM boot proved the packaged new flow while the pre-destructive interaction pass continued.
This commit is contained in:
2026-07-13 17:07:08 +01:00
parent 9d0abe5422
commit d09c11d872
7 changed files with 277 additions and 24 deletions

View File

@@ -26,6 +26,121 @@
set -euo pipefail
keyboard_layout_catalog() {
local rules
rules=$(keyboard_rules_file) || return 1
awk '
/^! layout/ { in_section=1; next }
/^!/ && in_section { exit }
in_section && NF { print $1 }
' "$rules"
}
keyboard_variant_catalog() {
local layout="$1" rules
rules=$(keyboard_rules_file) || return 1
awk -v wanted_layout="$layout:" '
/^! variant/ { in_section=1; next }
/^!/ && in_section { exit }
in_section && $2 == wanted_layout { print $1 }
' "$rules"
}
keyboard_rules_file() {
local rules
for rules in "${NOMARCHY_XKB_RULES:-}" \
/run/current-system/sw/share/X11/xkb/rules/base.lst \
/usr/share/X11/xkb/rules/base.lst; do
[[ -n "$rules" && -r "$rules" ]] && { echo "$rules"; return 0; }
done
return 1
}
keyboard_layout_name() {
local layout="$1" rules
rules=$(keyboard_rules_file) || { echo "layout $layout"; return; }
awk -v wanted="$layout" '
/^! layout/ { in_section=1; next }
/^!/ && in_section { exit }
in_section && $1 == wanted {
$1=""; sub(/^[[:space:]]+/, ""); print; found=1; exit
}
END { if (!found) print "layout " wanted }
' "$rules"
}
keyboard_variant_name() {
local layout="$1" variant="$2" rules
rules=$(keyboard_rules_file) || { echo "$variant key behaviour"; return; }
awk -v wanted_layout="$layout:" -v wanted_variant="$variant" '
/^! variant/ { in_section=1; next }
/^!/ && in_section { exit }
in_section && $1 == wanted_variant && $2 == wanted_layout {
$1=""; $2=""; sub(/^[[:space:]]+/, ""); print; found=1; exit
}
END { if (!found) print wanted_variant " key behaviour" }
' "$rules"
}
keyboard_layout_choices() {
local layout
while IFS= read -r layout; do
[[ -n "$layout" ]] || continue
printf '%s\t%s\n' "$layout" "$(keyboard_layout_name "$layout")"
done < <(keyboard_layout_catalog)
}
keyboard_variant_choices() {
local layout="$1" variant
printf '(none)\tStandard keys (no special variant)\n'
while IFS= read -r variant; do
[[ -n "$variant" ]] || continue
printf '%s\t%s\n' "$variant" "$(keyboard_variant_name "$layout" "$variant")"
done < <(keyboard_variant_catalog "$layout")
}
keyboard_catalog_has() {
local wanted="$1" candidate
while IFS= read -r candidate; do
[[ "$candidate" == "$wanted" ]] && return 0
done
return 1
}
# Common trust boundary for both gum and unattended input. The picker is a
# search over these catalogs, never a text field; this second check means even
# surprising gum behaviour or a mistyped NOMARCHY_KB_* value cannot become
# generated Nix and fail much later during installation.
validate_keyboard_choice() {
local layout="$1" variant="$2" layouts variants
layouts=$(keyboard_layout_catalog) || {
echo "Could not read the installed keyboard-layout catalog." >&2
return 2
}
if [[ -z "$layouts" ]] || ! keyboard_catalog_has "$layout" <<< "$layouts"; then
echo "Unknown keyboard layout '$layout'. Choose an installed layout (NOMARCHY_KB_LAYOUT)." >&2
return 2
fi
[[ -z "$variant" ]] && return 0
variants=$(keyboard_variant_catalog "$layout") || true
if [[ -z "$variants" ]] || ! keyboard_catalog_has "$variant" <<< "$variants"; then
echo "Unknown keyboard variant '$variant' for layout '$layout'. Choose an installed variant or no variant (NOMARCHY_KB_VARIANT)." >&2
return 2
fi
}
# Small, side-effect-free interface used by the deterministic installer guard.
# It deliberately runs before root/live-ISO checks and uses the exact same
# validation function as a real install.
if [[ "${1:-}" == "--validate-keyboard" ]]; then
[[ $# -eq 3 ]] || {
echo "usage: nomarchy-install --validate-keyboard <layout> <variant>" >&2
exit 64
}
validate_keyboard_choice "$2" "$3"
exit $?
fi
# Baked in by the package wrapper:
# NOMARCHY_INSTALL_SHARE — disko-config.nix, hardware-db.sh,
# compose-lock.py, flake.lock, template/,
@@ -214,14 +329,39 @@ else
| sed 's/$/.UTF-8/' \
| gum filter --placeholder "language / locale (type to search)" \
|| echo en_US.UTF-8)
KB_LAYOUT=$(localectl list-x11-keymap-layouts 2>/dev/null \
| gum filter --placeholder "keyboard layout (type to search)" \
|| echo us)
KB_VARIANT=""
if [[ "$KB_LAYOUT" != "us" ]] || gum confirm --default=No "Pick a keyboard variant (intl, nodeadkeys, …)?"; then
KB_VARIANT=$( { echo "(none)"; localectl list-x11-keymap-variants "$KB_LAYOUT" 2>/dev/null; } \
| gum filter --placeholder "variant for $KB_LAYOUT (pick '(none)' for the default)" \
|| echo "(none)")
keyboard_layouts=$(keyboard_layout_catalog) || \
fail "Could not read the installed keyboard-layout catalog."
[[ -n "$keyboard_layouts" ]] || \
fail "The installed keyboard-layout catalog is empty."
if gum confirm --default=yes "Use the standard US English keyboard (no special variant)?"; then
KB_LAYOUT="us"
KB_VARIANT=""
else
while true; do
if keyboard_pick=$(keyboard_layout_choices \
| gum filter --strict \
--placeholder "keyboard layout — type to search installed choices"); then
KB_LAYOUT="${keyboard_pick%%$'\t'*}"
if validate_keyboard_choice "$KB_LAYOUT" "" 2>/dev/null; then
break
fi
fi
warn "Choose one of the listed keyboard layouts; typed text is search only."
done
while true; do
if keyboard_pick=$( \
keyboard_variant_choices "$KB_LAYOUT" \
| gum filter --strict \
--placeholder "key behaviour for $KB_LAYOUT — '(none)' means the standard keys" \
); then
KB_VARIANT="${keyboard_pick%%$'\t'*}"
if [[ "$KB_VARIANT" == "(none)" ]] \
|| validate_keyboard_choice "$KB_LAYOUT" "$KB_VARIANT" 2>/dev/null; then
break
fi
fi
warn "Choose a listed key behaviour, or '(none)' for the standard keys; typed text is search only."
done
fi
fi
# `(none)` is gum's display-only sentinel, never an XKB variant. Keep the
@@ -230,6 +370,10 @@ fi
if [[ "$KB_VARIANT" == "(none)" ]]; then
KB_VARIANT=""
fi
keyboard_error=""
if ! keyboard_error=$(validate_keyboard_choice "$KB_LAYOUT" "$KB_VARIANT" 2>&1); then
fail "$keyboard_error"
fi
[[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] || fail "Invalid username '$USERNAME'."
[[ -f "/usr/share/zoneinfo/$TIMEZONE" || -e "/etc/zoneinfo/$TIMEZONE" ]] \
|| timedatectl list-timezones 2>/dev/null | grep -qx "$TIMEZONE" \
@@ -237,7 +381,13 @@ fi
HASHED_PASSWORD=$(printf '%s' "$PASSWORD" | mkpasswd -m sha-512 -s)
unset PASSWORD
info "User: $USERNAME @ $HOSTNAME_ ($TIMEZONE)"
info "Locale: $LOCALE · keyboard: $KB_LAYOUT${KB_VARIANT:+ ($KB_VARIANT)}"
if [[ -n "$KB_VARIANT" ]]; then
KEYBOARD_SUMMARY="$(keyboard_layout_name "$KB_LAYOUT") [$KB_LAYOUT] — $(keyboard_variant_name "$KB_LAYOUT" "$KB_VARIANT") [$KB_VARIANT]"
else
KEYBOARD_SUMMARY="$(keyboard_layout_name "$KB_LAYOUT") [$KB_LAYOUT] — standard keys (no special variant)"
fi
info "Locale: $LOCALE"
info "Keyboard: $KEYBOARD_SUMMARY"
# ─── Hardware profile ───────────────────────────────────────────────────
section "Hardware detection"
@@ -296,6 +446,7 @@ gum style --border normal --padding "0 2" \
"User: $USERNAME" \
"Hostname: $HOSTNAME_" \
"Timezone: $TIMEZONE" \
"Keyboard: $KEYBOARD_SUMMARY" \
"Hardware: ${HW_PROFILES[*]:-none}" \
"Snapshots: snapper timeline on /" \
"Source: nomarchy ${NOMARCHY_REV:0:12}${NOMARCHY_REV:+ }$([[ -z "${NOMARCHY_REV:-}" ]] && echo "(dirty tree) ")$SOURCE_NET"