test(docking): prove the keyboard-restore path; the reload clobber is real
All checks were successful
Check / eval (push) Successful in 3m10s

The last unproven half of bec826b, and it needed a keyboard that stays behind
when the dock leaves. The dev box's by-tech hangs off the BenQ's own hub, so
it always walked away with the cable — a Logitech K400 with its receiver in
the laptop is the real case, and Bernardo happened to plug one in.

Both halves measured on hardware (dev box, 2026-07-14):

  - PRIMARY: across a real unplug + the fixed transition, the K400 kept its
    remembered `us`. Panel back, layout intact.
  - CONTROL: a bare `hyprctl reload` with no restore took the same board from
    `us` to the session's `gb`.

The control matters more than the pass. That reload clobbers runtime
`device[<name>]:kb_layout` was an inference from the mechanism when
restore_keyboards was written — reasonable, but unmeasured, and this session
has already shown what unmeasured reasoning about this watcher is worth.
It holds: without that call the undock's escalation would have silently
broken the layout of every keyboard that outlives the cable, trading a black
panel for a keyboard that types the wrong characters at the password prompt.

Verified V2 again after the comment/doc edits: nix flake check --no-build,
checks.docking-ux, shellcheck clean.

Round 8 now asks only for what is still open: relogin (the watcher calls the
transition by baked store path, so exec-once means it runs the old build
until then) and repetition — 5+ unplugs, one with the lid shut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 12:35:23 +01:00
parent bec826baf0
commit e4404e0162
3 changed files with 28 additions and 20 deletions

View File

@@ -49,19 +49,21 @@ the **T14s** (webcam case).
The transition now escalates to `reload` when the keyword proves inert, The transition now escalates to `reload` when the keyword proves inert,
re-asserts the rule, and restores per-device keyboard layouts (a reload re-asserts the rule, and restores per-device keyboard layouts (a reload
drops runtime `device[…]:kb_layout` keywords). drops runtime `device[…]:kb_layout` keywords).
**Already proven on hardware:** one real unplug, invoking the fixed **Already proven on hardware** (2026-07-14, two real unplugs invoking
`nomarchy-display-transition undock eDP-1` directly with the watcher the fixed `nomarchy-display-transition undock eDP-1` directly with the
paused panel on and workspaces 13 home in 1.8s, journal watcher paused): panel on and workspaces 13 home in 1.8s, journal
`keyword=inert escalate=reload``enable=via-reload``result=ok`. `keyword=inert escalate=reload``enable=via-reload``result=ok`; and
the keyboard-restore path with a Logitech K400 whose receiver is in the
laptop — it kept its remembered `us` across the undock, while a control
`hyprctl reload` with no restore dropped it to the session's `gb`
(so `restore_keyboards` is load-bearing, not insurance).
**What is NOT proven, and is this round:** the *watcher-driven* path **What is NOT proven, and is this round:** the *watcher-driven* path
(the running watcher calls the transition by baked store path, so it (the running watcher calls the transition by baked store path, so it
only picks the fix up at relogin — the round-5 trap), and repetition. only picks the fix up at relogin — the round-5 trap), and repetition.
**Re-check:** relogin (mandatory — see below), dock, then unplug the **Re-check:** relogin (mandatory — see below), dock, then unplug the
cable **at least five times**, plus once with the lid shut and once cable **at least five times**, plus once with the lid shut.
while an external keyboard is plugged into the *laptop* (not the dock's **Pass:** the panel comes back every time with workspaces intact, and
hub) — that last one is the only check on the keyboard-restore path. `journalctl --user
**Pass:** the panel comes back every time with workspaces intact, the
external keyboard keeps its remembered layout, and `journalctl --user
-t nomarchy-display-transition -t nomarchy-display-watch --since -t nomarchy-display-transition -t nomarchy-display-watch --since
'-10 min'` shows `result=ok` for each undock with **no** '-10 min'` shows `result=ok` for each undock with **no**
`result=enable-failed`. `keyword=inert escalate=reload` on every undock `result=enable-failed`. `keyword=inert escalate=reload` on every undock

View File

@@ -41,16 +41,19 @@ Template:
around it will keep confirming whatever story you bring. The probe cost 10 around it will keep confirming whatever story you bring. The probe cost 10
minutes and killed two rounds of theory. minutes and killed two rounds of theory.
- **Verified:** V3 (partial) — the fixed `nomarchy-display-transition undock - **Verified:** V3 (partial) — the fixed `nomarchy-display-transition undock
eDP-1` driven through a real unplug on the dev box: panel on, workspaces eDP-1` driven through two real unplugs on the dev box: panel on, workspaces
13 home, 1.8s, `keyword=inert escalate=reload` → `enable=via-reload` → 13 home, 1.8s, `keyword=inert escalate=reload` → `enable=via-reload` →
`result=ok`. Plus V2: `nix flake check --no-build` all pass; `result=ok`. Keyboard-restore proven too, on a K400 whose receiver is in the
`checks.docking-ux` + `checks.dock-audio` pass; shellcheck clean on laptop (so it survives the cable): kept its remembered `us`, while a control
bare `hyprctl reload` dropped it to the session `gb` — the clobber was an
inference until measured, and it makes `restore_keyboards` load-bearing.
Plus V2: `nix flake check --no-build` all pass; `checks.docking-ux` +
`checks.dock-audio` + `monitor-fallback.nix` pass; shellcheck clean on
display-transition (profile-watch's lone SC2125 is pre-existing). display-transition (profile-watch's lone SC2125 is pre-existing).
- **Pending:** HARDWARE-QUEUE round 8 — the *watcher-driven* path needs a - **Pending:** HARDWARE-QUEUE round 8 — the *watcher-driven* path needs a
relogin (exec-once, baked store path) and repetition; the keyboard-restore relogin (exec-once, baked store path) and repetition (5+ unplugs, one with
path is entirely untested (dev box's keyboard hangs off the dock hub, so it the lid shut). New BACKLOG #114 (PROPOSED): tuigreet ignores per-device
leaves with the cable). New BACKLOG #114 (PROPOSED): tuigreet ignores layouts.
per-device layouts.
- **Next suggestion:** #114, or the dock direction's unverified panel-off - **Next suggestion:** #114, or the dock direction's unverified panel-off
(it disables the panel trusting hyprctl's exit code — same class, latent). (it disables the panel trusting hyprctl's exit code — same class, latent).

View File

@@ -170,10 +170,13 @@ let
} }
# `hyprctl reload` re-reads the config, and every runtime keyword dies # `hyprctl reload` re-reads the config, and every runtime keyword dies
# with it including the `device[<name>]:kb_layout` the keyboard watcher # with it including the `device[<name>]:kb_layout` the keyboard watcher
# applies to an external board. The remembered choices live in the # applies to an external board. Measured on hardware 2026-07-14: a bare
# in-flake state, so `restore` is authoritative and cheap; without this a # reload took a keyboard remembered as `us` straight back to the session's
# reload would silently drop an external keyboard back to the session # `gb`, so without this the undock's escalation would silently break the
# layout mid-undock. # layout of any keyboard that stays behind when the dock leaves (a
# wireless receiver in the laptop; only boards on the dock's own hub are
# unaffected, and only because they leave with it). The remembered
# choices live in the in-flake state, so `restore` is authoritative.
restore_keyboards() { restore_keyboards() {
hyprctl devices -j 2>/dev/null | jq -r '.keyboards[].name' 2>/dev/null \ hyprctl devices -j 2>/dev/null | jq -r '.keyboards[].name' 2>/dev/null \
| while IFS= read -r kb; do | while IFS= read -r kb; do