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

@@ -449,6 +449,36 @@ Design/decision records and a running log of shipped work (items marked
(#131). Those windows therefore carry float/center rules and deliberately no
`size` rule. Measured after: calendar 1536×936 = exactly 60%×65%, doctor
1408×1008 = 55%×70%, both centred in the Waybar work area.
- ✓ **Dock mode survives a rebuild (#142, 2026-07-16):** Bernardo: every rebuild
while docked re-lit the laptop panel and threw a workspace onto it, and he
re-picked Dock mode by hand. It was not forgetfulness — **a rebuild performed
an undock, by construction**. Dock mode existed only inside the compositor
(`keyword monitor <internal>, disable`, nothing written down), the generated
config's only monitor line is the catch-all `monitor=,preferred,auto,1`, and a
rewritten config makes Hyprland reload — which re-enables every output. We did
not have to theorise that: the **undock** path already uses `hyprctl reload`
as precisely that hammer, hardware-proven 2026-07-14. And the watcher could
not rescue it, because `reconcile` diffs `monitors all`, which lists disabled
outputs too — so the connected set is identical across the flip and the
watcher is structurally blind to it (the blind spot of rounds 45's "the
output set, not the event, is the truth").
Fix: dock mode is an **intent** now, recorded where every other setting lives
— `settings.display.dockMode`, in-flake and menu-writable — written at the one
choke point both the menu and the watcher pass through
(`nomarchy-display-transition`: dock → true, undock → false, and `enable` of
an internal → false, since "Screen on" *is* leaving dock mode and must not be
undone a second later). The watcher then re-asserts it as a **tick invariant**
rather than an event reaction, which is what makes the reload case work at
all; `auto_dock` was already idempotent (it returns early unless the panel is
up), so the cost is one `hyprctl monitors` per tick.
**The trap avoided** was baking the disable into the config (state →
`monitor=eDP-1,disable`), which would survive a reload beautifully and, with
no external attached, hand the user a zero-output session at login — the black
brick of #100/#127. The config cannot know what is plugged in, so it keeps
defaulting to "everything on" and the re-assert is gated on an external
actually being present. Verified by unit-testing the real generated function
against stubs — 9/9, including *no external → never re-dock* — plus the state
write/read round-trip on a throwaway checkout.
- ✓ **MIGRATION.md: Mozilla `profiles.ini` (#144, 2026-07-16):** a real
migration ate Bernardo's `~/.thunderbird/profiles.ini`, so Thunderbird did
what it does on any new machine — made an empty profile and opened it —