fix(display): #142 — dock mode is an intent, and survives a rebuild
All checks were successful
Check / eval (push) Successful in 3m48s

Every rebuild while docked re-lit the panel and threw a workspace onto
it. Not forgetfulness: a rebuild PERFORMS an undock. Dock mode lived only
in the compositor (`keyword monitor <internal>, disable`, nothing written
down), the config's only monitor line is the catch-all
`monitor=,preferred,auto,1`, and rewriting the config makes Hyprland
reload it — re-enabling every output. Our own undock path already uses
`hyprctl reload` as exactly that hammer (hardware-proven 2026-07-14).

The watcher could not rescue it: reconcile diffs `monitors all`, which
lists disabled outputs, so the connected set is identical across the flip
— structurally blind, the blind spot of rounds 4-5's "the output set is
the truth".

So the intent is recorded where every other setting lives —
settings.display.dockMode, in-flake, menu-writable — written at the one
choke point both menu and watcher pass through (transition: dock → true,
undock → false, `enable` of an internal → false, because "Screen on" IS
leaving dock mode and must not be undone a second later). The watcher
re-asserts it as a tick invariant rather than an event reaction, which is
what makes the reload case work; auto_dock was already idempotent, so it
costs one `hyprctl monitors` per tick.

The trap avoided: baking the disable into the config would survive a
reload and hand a user with no external a zero-output session at login —
the #100/#127 brick. The config cannot know what is plugged in, so it
keeps defaulting to "everything on" and the re-assert is gated on an
external being present.

Verified: 9/9 gates unit-tested by driving the REAL generated function
against stubs (incl. no-external → never re-dock, profile active → yield,
already-docked → no churn), plus the state write/read round-trip on a
throwaway checkout so the live flake was untouched. V3 queued — needs a
relogin, the watcher is exec-once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 11:51:52 +01:00
parent 02ec0f10cd
commit 30e632ff65
4 changed files with 111 additions and 47 deletions

View File

@@ -253,53 +253,6 @@ Recommend (a) + (b) together if the patch stays small: file it upstream, carry
it until it lands, delete on the release that fixes it. `[human]` for whether
a source-built rofi is a price worth paying.
### 142. A rebuild while docked undoes dock mode (the panel comes back, a workspace jumps to it)
Bernardo 2026-07-16: every rebuild while docked lands in the undocked state —
laptop panel back on, a workspace moved onto it — and he re-picks **Dock mode**
in the Display menu by hand. Diagnosed statically on the dev box (no session
disruption needed; our own code already proves each step):
1. **Dock mode is runtime-only.** `nomarchy-display-transition dock` ends with
`hyprctl keyword monitor <internal>, disable` (`hyprland.nix:205`) — a live
compositor command. Nothing writes it down: his `state.json` has no display
key at all, so dock mode is the one display setting outside the in-flake
state, which is exactly why it cannot survive anything.
2. **Any config reload re-enables everything.** The generated config's only
monitor line is the catch-all `monitor=,preferred,auto,1` (live
hyprland.conf:175 — he has no `displayProfiles`), so a reload turns every
output back on, `eDP-1` included, and Hyprland puts a workspace on the
output that just appeared. We do not have to guess that a reload does this:
the **undock** path uses `hyprctl reload` deliberately, as the hammer that
re-enables an inert disabled panel, hardware-proven 2026-07-14
(`hyprland.nix:225`). A rebuild reloads the config and therefore *performs
an undock*, precisely and by construction.
3. **The watcher cannot see it happen.** `reconcile` diffs `outputs()` =
`hyprctl monitors all` (`:505`), which lists **disabled** monitors too. So
the connected set is byte-identical before and after the flip, `cur !=
known_outputs` is false, and reconcile returns having done nothing. The
watcher tracks connect/disconnect and is structurally blind to
enable/disable — the blind spot in round 4/5's "the output set, not the
event, is the truth" (ROADMAP § Docking rounds 45).
**Shape, and the trap in the obvious fix.** The tempting move is to bake the
disable into the config (state → `monitor=eDP-1,disable`) so a reload preserves
it. **Do not** without a guard: a persisted disable with no external attached
is a zero-output session at login — the black brick #100 and #127 are both
about. The config is evaluated with no knowledge of what is plugged in, so it
must keep defaulting to "everything on". Better: persist the *intent*
(`settings.display.dockMode`, in-flake, menu-writable — the philosophy this
currently violates) and let the **watcher** re-assert it, since only it knows
what is live. That needs reconcile to also track the *enabled* set, so
"internal came back on while an external is present and the state says docked"
becomes an event it can act on — which fixes the reload case and any other
path that silently re-enables the panel. Gate re-assertion on an external
actually being present, never on the state key alone.
Pass = docked, `nomarchy-home` (and `nomarchy-rebuild`) → panel stays off, no
workspace moves, no menu trip; undocked rebuild unchanged; unplug while the
state says docked still restores the panel.
### 120. A netinstall ISO, next to the fat offline one
Bernardo 2026-07-14, after seeing the measured size: **keep the current ISO

View File

@@ -509,6 +509,22 @@ Everything else below stays open; order is convenience, not a gate.
active and PPD switching governors; opt-ins: ROCm (`rocminfo`, a GPU
PyTorch/Ollama smoke) and the XDNA NPU driver loading.
- [ ] **Fingerprint**`fprintd-enroll` + (opt-in PAM) login/sudo.
- [ ] **#142 dock mode survives a rebuild** — **relogin first** (the watcher is
`exec-once`; `nomarchy-home` alone leaves the old one running and you
would be testing nothing — round 6's lesson). Then, docked: run
`nomarchy-home`. Pass = the laptop panel stays off, no workspace lands on
it, no menu trip; `journalctl --user -t nomarchy-display-watch` shows
`dock-intent=true panel=eDP-1 state=re-enabled action=re-dock` (a rebuild
re-lights the panel and the watcher takes it back within ~1s, so a brief
flicker is expected — the panel staying *up* is the failure).
Then the three gates that must NOT fire, since this code can disable a
panel: (a) Menu ▸ Display ▸ **Screen on** → the panel stays on (intent
cleared; if it snaps off again within a second, that is the bug);
(b) **undock** → panel returns and stays; (c) boot/relogin **undocked**
with `settings.display.dockMode` still `true` in state.json → panel stays
on (no external ⇒ never re-dock). (c) is the one that matters: it is the
#127 brick if it is wrong. Gates are unit-tested against stubs, but only
hardware proves the wiring.
- [ ] **#138 dock audio does not break a running browser** — the plug event
itself is the only thing V2 could not do (the tool was driven by hand;
Hyprland calls the same entry point). **Relogin first** — the watcher is