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

@@ -11,14 +11,15 @@ silent regression:
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.
Also re-checks the password ≥8 interactive floors (V0-complete) and, when
given the patcher + template, the BACKLOG #91 no-keyboard-variant path.
Pure — no VM, no ISO.
Also re-checks the password ≥8 interactive floors (V0-complete), the
BACKLOG #92 catalog-only keyboard boundary, and, when given the patcher +
template, the BACKLOG #91 no-keyboard-variant path. Pure — no VM, no ISO.
"""
from __future__ import annotations
import json
import os
import re
import shutil
import subprocess
@@ -199,6 +200,95 @@ if len(sys.argv) == 4:
"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 = {
"hostname": "keyboard-test",
"username": "nomarchy",