fix(install): constrain keyboard selection to XKB catalog
All checks were successful
Check / eval (push) Successful in 3m17s
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:
@@ -30,18 +30,6 @@ These are separate queue items from one real install/session pass. Preserve
|
|||||||
that separation when fixing them: the installer blocker, unclear installer
|
that separation when fixing them: the installer blocker, unclear installer
|
||||||
copy, and post-install desktop failures have different verification paths.
|
copy, and post-install desktop failures have different verification paths.
|
||||||
|
|
||||||
### 92. Installer: keyboard selection is confusing
|
|
||||||
|
|
||||||
Make the layout/variant relationship and the “no variant” choice explicit;
|
|
||||||
show a human-readable preview/current choice before confirmation. Typing may
|
|
||||||
filter the installed XKB catalog, but it must never become a free-form value:
|
|
||||||
only an enumerated layout/variant (or the explicit no-variant choice) can be
|
|
||||||
accepted, and a no-match query must cancel or re-prompt before generation.
|
|
||||||
Keep the simple default path short. Pass = a user can deliberately choose US
|
|
||||||
with no variant without knowing XKB terminology, arbitrary text cannot reach
|
|
||||||
generated Nix, and the validated generated values are shown before
|
|
||||||
installation.
|
|
||||||
|
|
||||||
### 93. Installer: swap-size field lacks a clear prompt/explanation
|
### 93. Installer: swap-size field lacks a clear prompt/explanation
|
||||||
|
|
||||||
The partition value appears prefilled without a caption explaining that it is
|
The partition value appears prefilled without a caption explaining that it is
|
||||||
|
|||||||
@@ -19,6 +19,20 @@ Template:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-13 — #92 catalog-only installer keyboard selection (this commit)
|
||||||
|
- **Task:** Make installer layout/variant choices understandable without
|
||||||
|
allowing arbitrary XKB values to break generated Nix.
|
||||||
|
- **Did:** Added a pinned human-readable XKB catalog, a one-step US/default
|
||||||
|
path, strict searchable layout/variant pickers, shared exact validation for
|
||||||
|
interactive and unattended input, and a resolved review summary.
|
||||||
|
- **Verified:** V1 — full flake eval; installer safety/keyboard/package builds;
|
||||||
|
shell/Python/diff checks; real validator accepts valid layout-specific rows
|
||||||
|
and rejects fabricated/prefix/wrong-layout values before installer setup.
|
||||||
|
KVM boot of the exact current ISO proved the packaged installer contains the
|
||||||
|
new flow; interactive screenshot pass was still running at commit time.
|
||||||
|
- **Pending:** Complete the already-running pre-destructive KVM UI review.
|
||||||
|
- **Next suggestion:** #93 swap-size prompt and explanation.
|
||||||
|
|
||||||
## 2026-07-13 — #91 installer no-variant normalization (this commit)
|
## 2026-07-13 — #91 installer no-variant normalization (this commit)
|
||||||
- **Task:** Stop the keyboard picker's display-only `(none)` value from
|
- **Task:** Stop the keyboard picker's display-only `(none)` value from
|
||||||
breaking generated `xkb-console-keymap.drv` and the whole installation.
|
breaking generated `xkb-console-keymap.drv` and the whole installation.
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ iteration would otherwise rediscover.
|
|||||||
without a hierarchy pass.
|
without a hierarchy pass.
|
||||||
|
|
||||||
## Gotchas (cost a debugging session once)
|
## Gotchas (cost a debugging session once)
|
||||||
|
- Gum `filter` returns unmatched typed text by default; catalog-only pickers
|
||||||
|
require `--strict` plus an independent exact-membership validation boundary.
|
||||||
- Waybar `layer: top` renders above **even real-fullscreen windows** — the
|
- Waybar `layer: top` renders above **even real-fullscreen windows** — the
|
||||||
bar draws over a fullscreen video. `layer: bottom` lets the fullscreen
|
bar draws over a fullscreen video. `layer: bottom` lets the fullscreen
|
||||||
surface cover it while the exclusive zone still reserves the bar's space
|
surface cover it while the exclusive zone still reserves the bar's space
|
||||||
|
|||||||
@@ -224,6 +224,12 @@ Design/decision records and a running log of shipped work (items marked
|
|||||||
patches the real downstream template for `us` + no variant and builds the
|
patches the real downstream template for `us` + no variant and builds the
|
||||||
resulting console keymap; the full offline installer VM also carries the
|
resulting console keymap; the full offline installer VM also carries the
|
||||||
literal sentinel through LUKS+swap installation and first boot.
|
literal sentinel through LUKS+swap installation and first boot.
|
||||||
|
- ✓ **Catalog-only installer keyboard selection (#92):** the short default
|
||||||
|
confirms US English with standard keys; non-default layouts and variants
|
||||||
|
are searchable human-readable rows from the pinned XKB catalog. Gum runs
|
||||||
|
in strict mode and a shared exact-membership boundary validates interactive
|
||||||
|
and unattended values before hardware detection, disk changes, or generated
|
||||||
|
Nix. The review panel shows the resolved layout and key behaviour.
|
||||||
- launch-or-focus UX scripts (swayosd volume/brightness OSD ships since v1:
|
- launch-or-focus UX scripts (swayosd volume/brightness OSD ships since v1:
|
||||||
`nomarchy.osd.*`, media keys drive `swayosd-client`, themed from the JSON)
|
`nomarchy.osd.*`, media keys drive `swayosd-client`, themed from the JSON)
|
||||||
- **Distro branding, round 2:** `distroName = "Nomarchy"` ships
|
- **Distro branding, round 2:** `distroName = "Nomarchy"` ships
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
, pciutils # lspci
|
, pciutils # lspci
|
||||||
, usbutils # lsusb (fingerprint-reader VID probe; sysfs is the fallback)
|
, usbutils # lsusb (fingerprint-reader VID probe; sysfs is the fallback)
|
||||||
, btrfs-progs # inspect-internal map-swapfile (hibernation offset)
|
, btrfs-progs # inspect-internal map-swapfile (hibernation offset)
|
||||||
|
, xkeyboard_config # human-readable installed layout/variant catalog
|
||||||
# Baked metadata — what this installer installs and where it came from.
|
# Baked metadata — what this installer installs and where it came from.
|
||||||
, templateDir # templates/downstream (home.nix, theme-state.json)
|
, templateDir # templates/downstream (home.nix, theme-state.json)
|
||||||
, nomarchyLock # the distro's flake.lock (for offline lock composition)
|
, nomarchyLock # the distro's flake.lock (for offline lock composition)
|
||||||
@@ -65,6 +66,7 @@ stdenvNoCC.mkDerivation {
|
|||||||
util-linux gptfdisk parted cryptsetup lvm2 pciutils usbutils btrfs-progs
|
util-linux gptfdisk parted cryptsetup lvm2 pciutils usbutils btrfs-progs
|
||||||
]} \
|
]} \
|
||||||
--set NOMARCHY_INSTALL_SHARE "$share" \
|
--set NOMARCHY_INSTALL_SHARE "$share" \
|
||||||
|
--set NOMARCHY_XKB_RULES ${xkeyboard_config}/share/X11/xkb/rules/base.lst \
|
||||||
--set NOMARCHY_NIXPKGS ${nixpkgsPath} \
|
--set NOMARCHY_NIXPKGS ${nixpkgsPath} \
|
||||||
--set NOMARCHY_FLAKE_URL ${lib.escapeShellArg flakeUrl} \
|
--set NOMARCHY_FLAKE_URL ${lib.escapeShellArg flakeUrl} \
|
||||||
--set NOMARCHY_REV ${lib.escapeShellArg (toString rev)} \
|
--set NOMARCHY_REV ${lib.escapeShellArg (toString rev)} \
|
||||||
|
|||||||
@@ -26,6 +26,121 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
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:
|
# Baked in by the package wrapper:
|
||||||
# NOMARCHY_INSTALL_SHARE — disko-config.nix, hardware-db.sh,
|
# NOMARCHY_INSTALL_SHARE — disko-config.nix, hardware-db.sh,
|
||||||
# compose-lock.py, flake.lock, template/,
|
# compose-lock.py, flake.lock, template/,
|
||||||
@@ -214,14 +329,39 @@ else
|
|||||||
| sed 's/$/.UTF-8/' \
|
| sed 's/$/.UTF-8/' \
|
||||||
| gum filter --placeholder "language / locale (type to search)" \
|
| gum filter --placeholder "language / locale (type to search)" \
|
||||||
|| echo en_US.UTF-8)
|
|| echo en_US.UTF-8)
|
||||||
KB_LAYOUT=$(localectl list-x11-keymap-layouts 2>/dev/null \
|
keyboard_layouts=$(keyboard_layout_catalog) || \
|
||||||
| gum filter --placeholder "keyboard layout (type to search)" \
|
fail "Could not read the installed keyboard-layout catalog."
|
||||||
|| echo us)
|
[[ -n "$keyboard_layouts" ]] || \
|
||||||
KB_VARIANT=""
|
fail "The installed keyboard-layout catalog is empty."
|
||||||
if [[ "$KB_LAYOUT" != "us" ]] || gum confirm --default=No "Pick a keyboard variant (intl, nodeadkeys, …)?"; then
|
if gum confirm --default=yes "Use the standard US English keyboard (no special variant)?"; then
|
||||||
KB_VARIANT=$( { echo "(none)"; localectl list-x11-keymap-variants "$KB_LAYOUT" 2>/dev/null; } \
|
KB_LAYOUT="us"
|
||||||
| gum filter --placeholder "variant for $KB_LAYOUT (pick '(none)' for the default)" \
|
KB_VARIANT=""
|
||||||
|| echo "(none)")
|
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
|
||||||
fi
|
fi
|
||||||
# `(none)` is gum's display-only sentinel, never an XKB variant. Keep the
|
# `(none)` is gum's display-only sentinel, never an XKB variant. Keep the
|
||||||
@@ -230,6 +370,10 @@ fi
|
|||||||
if [[ "$KB_VARIANT" == "(none)" ]]; then
|
if [[ "$KB_VARIANT" == "(none)" ]]; then
|
||||||
KB_VARIANT=""
|
KB_VARIANT=""
|
||||||
fi
|
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'."
|
[[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] || fail "Invalid username '$USERNAME'."
|
||||||
[[ -f "/usr/share/zoneinfo/$TIMEZONE" || -e "/etc/zoneinfo/$TIMEZONE" ]] \
|
[[ -f "/usr/share/zoneinfo/$TIMEZONE" || -e "/etc/zoneinfo/$TIMEZONE" ]] \
|
||||||
|| timedatectl list-timezones 2>/dev/null | grep -qx "$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)
|
HASHED_PASSWORD=$(printf '%s' "$PASSWORD" | mkpasswd -m sha-512 -s)
|
||||||
unset PASSWORD
|
unset PASSWORD
|
||||||
info "User: $USERNAME @ $HOSTNAME_ ($TIMEZONE)"
|
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 ───────────────────────────────────────────────────
|
# ─── Hardware profile ───────────────────────────────────────────────────
|
||||||
section "Hardware detection"
|
section "Hardware detection"
|
||||||
@@ -296,6 +446,7 @@ gum style --border normal --padding "0 2" \
|
|||||||
"User: $USERNAME" \
|
"User: $USERNAME" \
|
||||||
"Hostname: $HOSTNAME_" \
|
"Hostname: $HOSTNAME_" \
|
||||||
"Timezone: $TIMEZONE" \
|
"Timezone: $TIMEZONE" \
|
||||||
|
"Keyboard: $KEYBOARD_SUMMARY" \
|
||||||
"Hardware: ${HW_PROFILES[*]:-none}" \
|
"Hardware: ${HW_PROFILES[*]:-none}" \
|
||||||
"Snapshots: snapper timeline on /" \
|
"Snapshots: snapper timeline on /" \
|
||||||
"Source: nomarchy ${NOMARCHY_REV:0:12}${NOMARCHY_REV:+ }$([[ -z "${NOMARCHY_REV:-}" ]] && echo "(dirty tree) ")— $SOURCE_NET"
|
"Source: nomarchy ${NOMARCHY_REV:0:12}${NOMARCHY_REV:+ }$([[ -z "${NOMARCHY_REV:-}" ]] && echo "(dirty tree) ")— $SOURCE_NET"
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ silent regression:
|
|||||||
3. HM pre-activate failure drops NOMARCHY-DESKTOP-NOT-THEMED.txt.
|
3. HM pre-activate failure drops NOMARCHY-DESKTOP-NOT-THEMED.txt.
|
||||||
4. Review panel Source line tracks the same OFFLINE probe as the install path.
|
4. Review panel Source line tracks the same OFFLINE probe as the install path.
|
||||||
|
|
||||||
Also re-checks the password ≥8 interactive floors (V0-complete) and, when
|
Also re-checks the password ≥8 interactive floors (V0-complete), the
|
||||||
given the patcher + template, the BACKLOG #91 no-keyboard-variant path.
|
BACKLOG #92 catalog-only keyboard boundary, and, when given the patcher +
|
||||||
Pure — no VM, no ISO.
|
template, the BACKLOG #91 no-keyboard-variant path. Pure — no VM, no ISO.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -199,6 +200,95 @@ if len(sys.argv) == 4:
|
|||||||
"keyboard variant normalization is not on the shared input boundary",
|
"keyboard variant normalization is not on the shared input boundary",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── 6. Catalog-only keyboard input (BACKLOG #92) ──────────────────
|
||||||
|
# gum input is search-only: selections are parsed from rows generated by
|
||||||
|
# the installed layout/variant catalogs, then both interactive and
|
||||||
|
# unattended paths meet the same exact-membership validator.
|
||||||
|
check(
|
||||||
|
"keyboard_pick=$(keyboard_layout_choices" in script
|
||||||
|
and 'gum filter --strict' in script
|
||||||
|
and 'KB_LAYOUT="${keyboard_pick%%$\'\\t\'*}"' in script,
|
||||||
|
"layout picker is not strict/catalog-constrained",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
'keyboard_variant_choices "$KB_LAYOUT"' in script
|
||||||
|
and script.count("gum filter --strict") == 2
|
||||||
|
and 'KB_VARIANT="${keyboard_pick%%$\'\\t\'*}"' in script,
|
||||||
|
"variant picker is not strict/catalog-constrained",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
not re.search(r"KB_(?:LAYOUT|VARIANT)=\$\(gum input", script),
|
||||||
|
"keyboard choice must not come from a free-form gum input",
|
||||||
|
)
|
||||||
|
validation_at = script.find(
|
||||||
|
'keyboard_error=$(validate_keyboard_choice "$KB_LAYOUT" "$KB_VARIANT"'
|
||||||
|
)
|
||||||
|
prewipe_at = script.find('prewipe "$TARGET_DISK"')
|
||||||
|
patch_at = script.find('python3 "$SHARE/patch-template.py"')
|
||||||
|
check(
|
||||||
|
normalize_at < validation_at < prewipe_at < patch_at,
|
||||||
|
"shared keyboard validation must fail before disk changes/generated Nix",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
'"Keyboard: $KEYBOARD_SUMMARY"' in script
|
||||||
|
and "standard keys (no special variant)" in script,
|
||||||
|
"review does not show a plain-language resolved keyboard choice",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Exercise the real shell validator with a tiny deterministic XKB rules
|
||||||
|
# catalog. Exact boundaries matter: a prefix or a made-up unattended
|
||||||
|
# value must fail here, not become an XKB/Nix build error later.
|
||||||
|
with tempfile.TemporaryDirectory(prefix="nomarchy-keyboard-catalog-") as tmp:
|
||||||
|
rules = Path(tmp) / "base.lst"
|
||||||
|
rules.write_text(
|
||||||
|
"""! layout
|
||||||
|
us English (US)
|
||||||
|
gb English (UK)
|
||||||
|
! variant
|
||||||
|
intl us: English (US, intl., with dead keys)
|
||||||
|
dvorak us: English (Dvorak)
|
||||||
|
extd gb: English (UK, extended, with Win keys)
|
||||||
|
! option
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["NOMARCHY_XKB_RULES"] = str(rules)
|
||||||
|
|
||||||
|
def keyboard_validate(layout: str, variant: str) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(
|
||||||
|
["bash", str(sys.argv[1]), "--validate-keyboard", layout, variant],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
for layout, variant in (("us", ""), ("us", "intl"), ("gb", "extd")):
|
||||||
|
result = keyboard_validate(layout, variant)
|
||||||
|
check(
|
||||||
|
result.returncode == 0,
|
||||||
|
f"valid keyboard choice {layout!r}/{variant!r} rejected: {result.stderr.strip()}",
|
||||||
|
)
|
||||||
|
|
||||||
|
invalid = (
|
||||||
|
("u", "", "Unknown keyboard layout 'u'"),
|
||||||
|
("made-up", "", "Unknown keyboard layout 'made-up'"),
|
||||||
|
("us", "int", "Unknown keyboard variant 'int' for layout 'us'"),
|
||||||
|
("gb", "intl", "Unknown keyboard variant 'intl' for layout 'gb'"),
|
||||||
|
)
|
||||||
|
for layout, variant, message in invalid:
|
||||||
|
result = keyboard_validate(layout, variant)
|
||||||
|
check(
|
||||||
|
result.returncode == 2 and message in result.stderr,
|
||||||
|
f"invalid keyboard choice {layout!r}/{variant!r} did not fail friendly/early: "
|
||||||
|
f"rc={result.returncode}, stderr={result.stderr.strip()!r}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
"NOMARCHY_INSTALL_SHARE" not in result.stderr,
|
||||||
|
"invalid keyboard choice reached normal installer setup",
|
||||||
|
)
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
"hostname": "keyboard-test",
|
"hostname": "keyboard-test",
|
||||||
"username": "nomarchy",
|
"username": "nomarchy",
|
||||||
|
|||||||
Reference in New Issue
Block a user