diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 709802b..dafabbd 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -30,22 +30,17 @@ These are separate queue items from one real install/session pass. Preserve that separation when fixing them: the installer blocker, unclear installer copy, and post-install desktop failures have different verification paths. -### 91. Installer: empty keyboard variant becomes literal `(none)` and blocks installation - -The generated system fails at `xkb-console-keymap.drv` with -`WARNING: Can not find "(none)" in "us"` followed by ckbcomp's `bad include -list`; every later system derivation is only a cascading dependency failure. -Normalize the picker's “no variant” value to the empty string before writing -Nix, and add an installer regression case for `us` + no variant. Pass = the -generated console/XKB configuration builds and the install proceeds. - ### 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. Keep the -simple default path short. Pass = a user can deliberately choose US with no -variant without knowing XKB terminology, and the generated values are shown -before installation. +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 diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index ab8d839..69aa97b 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -124,10 +124,16 @@ the **T14s** (webcam case). (SUPER+? / root menu › Keybindings) now ends in ↩ Back and returns to the root picker; spot a couple of submenus (Display, VPN › Tailscale) still Back correctly after the audit pass. -- [ ] **Keyboard hotplug picker (re-verify after in-flake graduation)** — - plug an external keyboard post-login, pick a layout in rofi, confirm - it applies per-device only, persists in `settings.keyboard.devices`, - and graduates into a `device{}` block on the next rebuild. +- [ ] **Per-device keyboard menu + hotplug restore (re-verify after in-flake + graduation)** — update to the current `main`, run `nomarchy-home`, and + relogin with an external keyboard already connected. Open System › + Keyboard: both the built-in and external boards must be listed with + their current layouts. Pick a different layout for the external board; + it must apply only there and persist in `settings.keyboard.devices`. + Unplug/replug it: the saved layout must restore without another prompt + and graduate into a `device{}` block on the next rebuild. Finally plug + a previously unseen keyboard in after login: the automatic picker must + appear once and remember that choice too. - [ ] **Snapshots restore + rollback exercise** (residual — the GUI/polkit half passed on hardware 2026-07-04, a47aa3a): in `sudo nomarchy-snapshots`, restore a single file (`undochange`) diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index bba2503..574233a 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -19,6 +19,21 @@ Template: --- +## 2026-07-13 — #91 installer no-variant normalization (this commit) +- **Task:** Stop the keyboard picker's display-only `(none)` value from + breaking generated `xkb-console-keymap.drv` and the whole installation. +- **Did:** Normalize at the shared input boundary and defensively in the real + template patcher. Added a `us` + sentinel fixture, actual console-keymap + build, and made the offline installer VM carry the literal sentinel. +- **Verified:** V2 — full flake eval; focused installer-keyboard/safety/ + template builds; shell/Python/Nix syntax and diff checks; KVM + `tools/test-install.sh` installed offline with LUKS + swap, powered off after + ~400s, unlocked, and booted the themed desktop. Inspected install progress, + LUKS, and first-boot captures under `/tmp/nomarchy-vm/`. +- **Pending:** #92 now explicitly forbids free-form XKB values. Expanded the + existing per-device keyboard hardware check for the connected-at-login menu. +- **Next suggestion:** #92 installer keyboard selection clarity/safety. + ## 2026-07-13 — #101 charge-limit AC-event burst recovery (this commit) - **Task:** Fix `nomarchy-battery-charge-limit.service` failing when USB-C dock power events arrive while its settling write is still active. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 572e45e..92e9f76 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -217,6 +217,13 @@ Design/decision records and a running log of shipped work (items marked v1: `nomarchy.system.plymouth.*`, background tinted from the state file) - Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`) +- ✓ **Installer no-keyboard-variant safety (#91):** gum's display-only + `(none)` row is normalized at the common interactive/unattended boundary, + and the template patcher independently refuses to serialize that sentinel + into either Home Manager or system XKB configuration. A permanent check + patches the real downstream template for `us` + no variant and builds the + resulting console keymap; the full offline installer VM also carries the + literal sentinel through LUKS+swap installation and first boot. - launch-or-focus UX scripts (swayosd volume/brightness OSD ships since v1: `nomarchy.osd.*`, media keys drive `swayosd-client`, themed from the JSON) - **Distro branding, round 2:** `distroName = "Nomarchy"` ships diff --git a/flake.nix b/flake.nix index c3251c9..e0d9883 100644 --- a/flake.nix +++ b/flake.nix @@ -349,10 +349,39 @@ { nativeBuildInputs = [ pkgs.python3 ]; } '' python3 ${./tools/check-install-safety.py} \ - ${./pkgs/nomarchy-install/nomarchy-install.sh} + ${./pkgs/nomarchy-install/nomarchy-install.sh} \ + ${./pkgs/nomarchy-install/patch-template.py} \ + ${./templates/downstream} touch $out ''; + # Installer keyboard no-variant regression (item 91): gum's `(none)` + # row is display-only. Exercise it through the real template + # patcher, then build the exact us + empty-variant console keymap + # which previously failed as xkb-console-keymap.drv. + installer-keyboard = + let + keyboardSystem = nixpkgs.lib.nixosSystem { + inherit system; + modules = [{ + console.useXkbConfig = true; + services.xserver.xkb.layout = "us"; + services.xserver.xkb.variant = ""; + system.stateVersion = "26.05"; + }]; + }; + in + pkgs.runCommand "nomarchy-installer-keyboard" + { nativeBuildInputs = [ pkgs.python3 ]; } + '' + python3 ${./tools/check-install-safety.py} \ + ${./pkgs/nomarchy-install/nomarchy-install.sh} \ + ${./pkgs/nomarchy-install/patch-template.py} \ + ${./templates/downstream} + test -s ${keyboardSystem.config.console.keyMap} + touch $out + ''; + # Installer share ↔ templates/downstream SoT (item 72): the four # files default.nix copies into share/.../template/ must stay # byte-identical to the checkout template (single machine-seed diff --git a/pkgs/nomarchy-install/nomarchy-install.sh b/pkgs/nomarchy-install/nomarchy-install.sh index 8bdc79f..cd1df82 100644 --- a/pkgs/nomarchy-install/nomarchy-install.sh +++ b/pkgs/nomarchy-install/nomarchy-install.sh @@ -222,9 +222,14 @@ else 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)") - [[ "$KB_VARIANT" == "(none)" ]] && KB_VARIANT="" fi fi +# `(none)` is gum's display-only sentinel, never an XKB variant. Keep the +# normalization at the common boundary so interactive and unattended installs +# cannot write it into either generated Nix file. +if [[ "$KB_VARIANT" == "(none)" ]]; then + KB_VARIANT="" +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" \ diff --git a/pkgs/nomarchy-install/patch-template.py b/pkgs/nomarchy-install/patch-template.py index e523f12..f2c2e2d 100644 --- a/pkgs/nomarchy-install/patch-template.py +++ b/pkgs/nomarchy-install/patch-template.py @@ -32,6 +32,12 @@ def nix_str(s: str) -> str: ) +def keyboard_variant(v: dict) -> str: + """Return the XKB value, never the picker's display-only sentinel.""" + variant = v.get("keyboardVariant") or "" + return "" if variant == "(none)" else variant + + def replace_once(text: str, old: str, new: str, label: str) -> str: if old not in text: sys.exit(f"patch-template: missing placeholder for {label}: {old!r}") @@ -75,7 +81,7 @@ def patch_flake(text: str, v: dict) -> str: def patch_home(text: str, v: dict) -> str: layout = nix_str(v["keyboardLayout"]) - variant = nix_str(v.get("keyboardVariant") or "") + variant = nix_str(keyboard_variant(v)) text = replace_once( text, ' nomarchy.keyboard.layout = "us";', @@ -213,7 +219,7 @@ def patch_system(text: str, v: dict) -> str: text = replace_once( text, ' services.xserver.xkb.variant = "";', - f' services.xserver.xkb.variant = "{nix_str(v.get("keyboardVariant") or "")}";', + f' services.xserver.xkb.variant = "{nix_str(keyboard_variant(v))}";', "xkb variant", ) diff --git a/tools/check-install-safety.py b/tools/check-install-safety.py index 94acec4..c2d9c70 100755 --- a/tools/check-install-safety.py +++ b/tools/check-install-safety.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Installer safety guards contract (flake check `installer-safety`). -Usage: check-install-safety.py +Usage: check-install-safety.py [