Rebuild Displays around the canvas, and let the transaction keep color

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 10:44:11 -04:00
parent 1f694b00b6
commit 9bc68ba358
29 changed files with 3065 additions and 388 deletions
@@ -187,4 +187,65 @@ phase-3 flake above: that failure only appeared under a storming full-suite run.
- 2026-08-24 full-suite run: 166 contracts, all green except `displays-contract`
and `switcher-contract`, which are live interactive tests that cannot run
behind hyprlock (both passed in the same day's unlocked run; neither
subsystem changed in phase 4). Re-verify after unlock.
subsystem changed in phase 4). Re-verify after unlock. **Still pending**
and `displays-contract` has since changed (phase 5), so this run is now the
first one that exercises the new material as well.
## Phase 5 (Displays) — append below
Spec: `2026-08-24-displays-redesign.md`. The Displays page became canvas-first,
the per-monitor record grew VRR / colour profile / bit depth / SDR trim /
mirroring, and `pushLayout` stopped clobbering `monitors.lua`'s colour values.
**Nothing in this wave was run.** The test window was closed while it was
written: these are live harnesses that drive the real compositor through
display transactions, and three agents were editing the tree concurrently. What
*was* verified is listed as static below — evaluated directly against the
implementation files without a harness, by loading `monitors.lua` under a stub
`prefs` and by evaluating `DisplayLayout.js` in node. Everything else is
deferred to the sweep.
### Contracts changed (4)
| Contract | What it now pins | Verified |
|---|---|---|
| `quickshell/display-transaction-contract` | The extended record end to end: `applyRecord` merging one field at a time; `vrr` **omitted** when `vrrMode === -1` and emitted when it is not; neutral `sdrsaturation` omitted rather than written; a mirrored rule asking for `position = "auto"`; the mirror x/y carve-out in `matchesLayout`, and that it does not leak to an unmirrored record; a framebuffer format with no 8/10 mapping skipping the bit-depth assertion instead of blocking Keep; `confirm()` persisting the whole record; old-shape stored blobs still validating and an impossible `vrrMode` not; and seven refusals (self-mirror, primary mirroring, absent target, out-of-range vrr/profile/depth/SDR). | Bash and Python syntax; every new static grep checked against the landed `Displays.qml` (including the `vrrMode >= 0` omission branch in `monitorRule`) and against the extended harness; the fake compositor's rule parser dry-run on a real `monitorRule` payload, covering the `position = "auto"` and `mirror =` branches and the `mirrorOf: "none"` readback spelling. Every IPC assertion is **deferred**. |
| `quickshell/display-arrangement-contract` | **Flipped**: the canvas is no longer hidden below two displays. The `visible: … monitors.length` gate is now asserted *absent* from the `DisplayArrangement` element (by AST-free block scan, so the selector chips and Workspaces card may keep theirs), with a solo hint string, a `draggable` flag, and an `enabled:` binding on the `DragHandler`. Plus the mirror badge: `Mirrors ` in the component, `mirrorOf` read off the rects, and two new harness fixtures — solo renders one rect with `draggable == false`, mirrored stacks rect 1 on rect 0 and flags it. | **Statically verified** against the rebuilt `DisplayArrangement.qml`: every grep hits, the `DragHandler`'s `enabled:` is found by the brace-matching helper, and the flip check was mutation-tested both ways — it fails on the pre-redesign page (where the gate sat on the enclosing card, not on the canvas) and passes once the gate is gone. The two new harness fixtures are **deferred**. |
| `quickshell/display-layout-contract` | Mirror geometry in `DisplayLayout.js`: a valid mirror validates; the mirrored record keeps its stored coordinates through `normalize` (the primary's anchor does not apply to a position nothing reads back); it contributes nothing to `bounds`; `canvasRects` stacks its rect on its target's and carries `mirrorOf`/`mirrored`. Five refusals: self, absent target, mirroring primary, a two-hop chain, and a non-string. | **Statically verified** in node against the real `DisplayLayout.js` — every expected value in the two new `jq` filters came from that run, including the 3140/80 the mirrored record keeps. Harness plumbing deferred. |
| `quickshell/displays-contract` | The Lua consumer half: `color_profile` / `bitdepth_value` / `vrr_value` / `sdr_value` / `mirror_value` present, `cm`/`bitdepth`/`sdrbrightness`/`vrr`/`mirror` emitted under Hyprland's own key names, a mirrored entry's `position` forced to `auto`, neutral SDR saturation and `vrrMode = -1` written as absence rather than as a value, and an entry with every new field impossible surviving with its geometry while each bad field drops. Plus the extended-record greps on `Displays.qml`. | **Statically verified**: the whole `LUA` block was run against `config/dot/hypr/monitors.lua` with a stub `prefs` and passes. The `Displays.qml` greps were checked by hand and all hit. The live compositor half is deferred. |
### Cross-agent shapes these contracts now pin
Written from the spec's pinned API while agents A and B worked in parallel, and
re-checked against their files as those landed:
- `DisplayArrangement.canvasSnapshot()` exposes `draggable` alongside
`rects`/`scale`, and passes `canvasRects`' `mirrorOf`/`mirrored` through — it
returns `canvasData.rects`, not the solo-shrunk `tiles`, which is what the
new fixtures assert against. Confirmed in the landed component.
- The solo hint is pinned as the prefix `One display connected` rather than the
full sentence, so the em dash cannot break the grep.
- The mirror badge is pinned as `Mirrors ` in `DisplayArrangement.qml`.
- `soloFixture`/`mirrorFixture` mutate `fixtureService.monitors` and call
`resetDraft()`, which is what the component's own `onMonitorsChanged` does.
Whether that ordering settles before `canvasSnapshot()` reads back is the one
thing only a run can answer.
### Still open before the run
- ~~`PreferenceSchema.qml`'s stale `displays` detail string~~ — resolved: the
detail now names color, VRR override, and mirroring, and the matching grep in
`displays-contract` was updated in the same commit.
- `displays-contract` also greps `DisplaysPage.qml` for `selectedOutput`,
`scalesForMode(`, `primaryFirstMonitors.map(` and `enabled: Displays.canConfirm`.
All four still hit, but the page was still the pre-redesign one when this was
written — re-check them once the rebuilt page lands, particularly
`primaryFirstMonitors.map(`, since the spec replaces the "Connected display"
picker card with selector chips.
- No contract file was added or removed, so the README count line stays at
**166** and `setup/readme-contract` needs nothing.
- Run order for the sweep: `display-layout-contract` first (pure geometry, no
compositor), then `display-transaction-contract` (fake compositor on `PATH`),
then `display-arrangement-contract`, and `displays-contract` last — it is the
only one that drives the physical display, and it refuses to start from a
scale that does not match what `monitors.lua` ships.