fix(display): #142 — dock mode is an intent, and survives a rebuild
All checks were successful
Check / eval (push) Successful in 3m48s
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:
@@ -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 4–5).
|
||||
|
||||
**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
|
||||
|
||||
Reference in New Issue
Block a user