feat(hytte-plugin-niri-layouts): golden picks 75/25 or the golden cut by the screen's logical width (#1052) - #1056
Conversation
… by the screen's logical width (#1052) Annika: "A golden ratio cut rather than the 25% cut would be better for smaller screens" / "Can we make this adaptive?" (#1052, 2026-09-10). One breakpoint on the target output's logical width, GOLDEN_BREAKPOINT = 2560 px: at or above it, the wide pair from #1019 (75 % / 25 %, her ultrawide's number); below it, the golden ratio itself (61.8 % / 38.2 %), so a narrow column stays wide enough to use on a laptop or 1080p/1440p screen. The pure width -> pair decision lives in layout::golden_pair, unit-tested against the exact decision table (2559 -> golden cut, 2560 -> wide pair, unresolvable width -> wide pair, matching pre-#1052 behaviour). niri.rs's apply() adds one Request::Outputs alongside the existing Windows/Workspaces/FocusedOutput fetch (always, even for equal/split, so the four-request shape never depends on which button was pressed), resolves the focused workspace's own output name via the new layout::target_output_name (the same targeting rule plan() uses internally, so the two can never disagree), looks up that output's logical width, and logs one debug line when it falls back. Both wire pairs are pinned as literal bytes in niri.rs's tests, matching the existing 75.0/25.0 pinning style. The CLI --help and the chip tooltip document the rule (both pairs and the breakpoint) rather than a live per-screen answer, since neither actually dials niri to render that text (--help never connects; the tooltip is a pure, synchronous string with no IPC round trip). docs/live-verify.md's niri-layouts section gets a new #1052 checklist item for the two-output live verification this can't get without a real niri session. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157rpXf1ks2hZEfdB47UJ6v
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157rpXf1ks2hZEfdB47UJ6v
7aaef8e to
d7e690b
Compare
Adversarial review — PR #1056 @ d7e690bVerdict: MERGE — no behavioural defect found. The rule shipped is the rule Annika confirmed ("sounds fine I guess?" on the #1052 triage table), the wire bytes are right for both pairs, the boundary is pinned on both sides, and all three claimed mutations reproduce exactly. Findings below are one doc line that contradicts the shipped behaviour at exactly the boundary case, two unpinned mechanisms (both proven with mutants that pass 101/101), and nits. None block; MED-1 wants one word from @annikahannig either way. Read-only worktree at MED-1 — the doc contradicts the code for a 2560-wide screen, and the triage's own reading column put 1440p on the other side
Those two clauses contradict each other in consecutive lines, and the second one describes behaviour the code does not have: This is not just a comment slip, because the same off-by-one sits in the triage table Annika approved: its Failure scenario: Annika plugs in a 2560×1440 monitor, clicks Fix: one word from @annikahannig — does a 2560-wide screen want the golden cut ( MED-2 —
|
| # | mutation | file:line | result |
|---|---|---|---|
| M1 | flip the breakpoint comparison (< → >=) |
layout.rs:79 | 5 red — matches the PR body |
| M2 | swap the wide and narrow pairs | layout.rs:33-59 | 8 red — matches the PR body |
| M3 | drop the Outputs request (outputs() returns empty) |
niri.rs:133 | 4 red — matches the PR body |
| M4a | GOLDEN_BREAKPOINT 2560 → 2561 |
layout.rs:19 | 2 red |
| M4b | boundary inclusive (< → <=) |
layout.rs:79 | 1 red |
| M5 | logical_width_of reads logical.height |
layout.rs:274 | 2 red |
| M6 | delete the missing-width diagnostic | niri.rs:74-81 | 0 red → LOW-1 |
| M7 | resolve the width off the focused-output name, not target_output_name |
niri.rs:72 | 0 red → MED-2 |
| M8 | logical_width_of ignores name, takes any output |
layout.rs:269 | 2 red (unit only; 0 at apply) |
| M9 | send an Action between the Workspaces and Outputs fetches |
niri.rs:69 | 0 red → NIT-1 |
| — | MED-2's supplied test, on clean d7e690b |
niri.rs (added) | 103 passed / 0 failed |
| — | MED-2's supplied test, against M7 | niri.rs (added) | 102 passed / 1 failed (kills it) |
Gates (fresh, foreground, in the devShell, RUSTC_WRAPPER unset)
| gate | exit |
|---|---|
cargo clippy --workspace --all-targets --features system-tests -- -D warnings |
0 (0 warnings, 0 errors) |
cargo test -p hytte-plugin-niri-layouts |
0 — 101 passed, 0 failed |
cargo run -p hytte-plugin-niri-layouts -- --help |
0 |
Both re-run on the restored baseline after the mutation campaign; md5sum of all six crate sources identical to the pre-mutation recording.
…s adaptive golden MED-1: layout.rs's GOLDEN_NARROW_MAJOR doc contradicted the code it documents (claimed a 2560-wide screen gets the golden cut; the code's `< GOLDEN_BREAKPOINT` gives it 75/25). Rewrites the doc to say exactly what `golden_pair` does and names the one known ambiguity — a screen reporting exactly 2560 logical px (1440p at 1x, or 4K at 1.5x) reads as "wide" today — without moving GOLDEN_BREAKPOINT or its comparison, which are Annika's call. Mirrored into docs/live-verify.md's #1052 checklist item so a live-verify pass surfaces the same open question. MED-2: pins the "apply resolves Golden's width through the same targeting rule plan() uses" invariant at the apply() level, which was previously unpinned — the obvious wrong refactor (`output.as_deref()` instead of `layout::target_output_name(...)`) passed the whole suite. Adds the reviewer's supplied killing test plus a two-output sibling that only became deterministic once NIT-2 landed. LOW-1: the Golden missing-width fallback diagnostic was untested and hardcoded "(75 % / 25 %)" rather than deriving it from GOLDEN_WIDE_MAJOR/MINOR. Extracts `missing_width_diagnostic`, adds a `Transport::log` seam (mirroring watch.rs's `Backend::log`) so a test can assert the exact line without touching real stderr, and pins it. LOW-2: the five hand-typed "2560" strings (USAGE, tooltip, two module docs, live-verify) aren't derivable at compile time without a const_format-shaped dependency this workspace doesn't carry. Adds one test that checks cli::USAGE, Layout::Golden::tooltip() and main.rs's module docs against GOLDEN_BREAKPOINT directly, so a bump to the constant that misses one of them fails there instead of shipping a stale doc. NIT-1: queries_all_four_snapshots_before_acting now asserts the first four entries of the raw seen log rather than the action-filtered queries() view, so an action sent between two snapshots is visible to the "before acting" half of its name. NIT-2: outputs() and logical_width_of() keep niri's Outputs reply as the HashMap<String, Output> it already is, keyed by connector name, instead of flattening to a Vec and linear-searching by Output.name. Removes the assumption that the map key and that field never diverge, and makes multi-output apply tests deterministic. NIT-3: equal_and_split_ignore_the_output_width now also asserts against the literal [(_, 50.0), (_, 50.0)] rather than only comparing narrow against wide, which could pass with both sides wrong the same way. Refs #1052 #1019 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157rpXf1ks2hZEfdB47UJ6v
…ling test's doc comment The two-output sibling test's determinism against a wrong-*name* mutant (M7-shaped) doesn't actually depend on NIT-2's HashMap-vs-Vec choice — a keyed lookup by the correct name is order-independent either way. What NIT-2 actually buys is narrower: it removes the map-key/Output.name divergence assumption and the O(n) scan, not determinism against a mutant that drops the name comparison entirely (that class stays only probabilistically caught, inheriting whatever order the map's own randomised hasher produces). Caught this while verifying the claim empirically before shipping it. Refs #1052 #1019 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157rpXf1ks2hZEfdB47UJ6v
Fold-in — PR #1056 @ 8baa851Folded the review's findings (#1056 (comment)) into two commits: d2c9a13 (the fixes) and 8baa851 (a correction to a doc comment I shipped in the first commit and then couldn't stand behind — see the NIT-2 row below). Findings
Mutation table (fresh,
|
| # | mutation | before this fold-in | after |
|---|---|---|---|
| M1 | flip the breakpoint comparison (< → >=) |
5 red | 7 red (2 new MED-2 tests also hit it) |
| M2 | swap the wide/narrow pairs | 8 red | 10 red (same reason) |
| M3 | drop the Outputs request (outputs() returns empty) |
4 red | 4 red (same tests, different names) |
| M4a | GOLDEN_BREAKPOINT 2560 → 2561 |
2 red | 3 red — hardcoded_breakpoint_strings_track_the_constant now also fires (LOW-2 fixed) |
| M4b | boundary inclusive (< → <=) |
1 red | 1 red (unchanged) |
| M5 | logical_width_of reads logical.height |
2 red | 2 red (unchanged) |
| M6 | delete the missing-width diagnostic block | 0 red | 1 red — golden_logs_the_fallback_pair_from_the_constants_when_width_is_unknown (LOW-1 fixed) |
| M7 | apply resolves width off output.as_deref() instead of target_output_name(...) |
0 red | 1 red — golden_resolves_the_width_through_the_workspace_when_no_output_is_focused (MED-2 fixed) |
| M8 | logical_width_of ignores name, takes any output |
2 red (unit only) | 2 red (unit only — see the NIT-2 caveat above, this class stays probabilistic at the apply level by design) |
| M9 | send an Action between the Workspaces and Outputs fetches |
0 red | 1 red (+5 collateral in wire-byte tests, expected) — queries_all_four_snapshots_before_acting (NIT-1 fixed) |
Gates (foreground, in the devShell, RUSTC_WRAPPER unset)
| gate | exit |
|---|---|
nix fmt -- --no-cache (first pass, reformatted 1 file; second pass, 0 changed) |
0 |
cargo clippy -p hytte-plugin-niri-layouts --all-targets -- -D warnings |
0 |
cargo clippy --workspace --all-targets --features system-tests -- -D warnings |
0 |
cargo test -p hytte-plugin-niri-layouts, run ×6 for the NIT-2 determinism check |
0 every run — 105 passed, 0 failed each time (up from 101: +4 new tests) |
🤖 Generated with Claude Code
What / why
Annika (#1052, 2026-09-10): "A golden ratio cut rather than the 25% cut would be better for smaller screens" / "Can we make this adaptive?"
golden's 75 % / 25 % split (#1019) was tuned for her ultrawide; on a laptop or 1080p/1440p screen a 25 % narrow column is too cramped to use.The rule
One breakpoint, on the logical width of the output the focused workspace lives on (
layout::GOLDEN_BREAKPOINT = 2560):Logical, not physical — a HiDPI output at 2x scale counts by its post-scale (usable) width, which is what niri's own
LogicalOutput::widthalready reports. Missing output / no logical geometry (headless) falls back to the wide pair (the exact behaviour every build before this had) and logs one debug line explaining why.The pure width → pair decision is
layout::golden_pair, unit-tested directly against the decision table (2559 → golden cut, 2560 → wide pair, unresolvable width → wide pair).niri::applyadds oneRequest::Outputsalongside the existingWindows/Workspaces/FocusedOutputfetch — always, even forequal/split, so the shape of what's asked for never depends on which button was pressed — resolves the focused workspace's own output name via the newlayout::target_output_name(the same targeting ruleplan()uses internally, so the two can never pick different workspaces), looks up that output's logical width, and hands the resolved pair intoplan().Both pairs are pinned at the wire as literal bytes in
niri.rs's tests ({"SetProportion":75.0}/25.0and{"SetProportion":61.8}/38.2), matching the existing pinning style from #1019/#1026's review (never derived from the Rust constants, so a rounding regression would still be caught).The CLI
--helpand the chip tooltip name both pairs and the breakpoint rather than a live per-screen answer — neither one actually dials niri to render that text (--helpnever connects to the socket at all; the tooltip is a pure, synchronous string with no IPC round trip), so they document the rule instead of a snapshot of one screen.Lane
crates/hytte-plugin-niri-layouts/src/{layout.rs,niri.rs,cli.rs,main.rs}+docs/live-verify.md's niri-layouts section.plugin.rsneeded no changes (the chip already rendersLayout::tooltip()verbatim).watch.rsandflake.nix/nix/untouched, per the #1053 lane split.Mutation table (falsified after committing, md5 before/after matched on clean revert)
<→>=)golden_pair_picks_by_the_breakpoint,golden_pair_is_consistent_well_away_from_the_breakpoint, both niri.rs pair tests, the narrow wire-bytes test)Outputsrequestgolden_asks_outputs_exactly_once_per_apply,queries_all_four_snapshots_before_acting, both narrow-pair niri.rs tests, sinceoutputsthen resolves to nothing)Gates (all foreground, in the devShell)
nix fmt -- --no-cache— reformatted 2 files initially (own new code + a markdown wrap inlive-verify.md), clean on the final pass — exit 0cargo clippy -p hytte-plugin-niri-layouts --all-targets -- -D warnings— exit 0 (the crate doesn't define thesystem-testsfeature, so that flag from the usual recipe doesn't apply here — confirmed viacargo package does not contain this featurebefore dropping it)cargo clippy --workspace --all-targets --features system-tests -- -D warnings— exit 0cargo test -p hytte-plugin-niri-layouts— 101 passed, 0 failedcargo run -p hytte-plugin-niri-layouts -- --help— reads correctly, names both pairs and the breakpointLive-verify caveat
Nothing here has drawn a pixel — this environment has no niri session.
docs/live-verify.mdgets a new(#1052)checklist item under the niri-layouts section: it needs two outputs of different logical widths straddling 2560 px (an external monitor plus a laptop panel, orniri msg output <name> scale <n>to push a HiDPI panel's logical width across the line without new hardware), confirmsgoldenpicks 75/25 on the wide one and 61.8/38.2 on the narrow one, that the pick follows the focused workspace's own output rather than whichever monitor the shell started on, that unplugging the external monitor falls back to 75/25 with one debug line in the journal, and that the CLI hat'sapply goldenmatches the chip's split on each output. Your glass is the acceptance, same as #1019/#1038.Fold-in — review round 1 (@ 8baa851)
The adversarial review (#1056 (comment)) returned MERGE with no behavioural defect and a handful of doc/test-strength findings, all folded in — full per-finding table and refreshed mutation/gate results in #1056 (comment). Summary:
layout.rs'sGOLDEN_NARROW_MAJORdoc contradicted the code (claimed a 2560-wide screen gets the golden cut; it doesn't —< GOLDEN_BREAKPOINTgives it 75/25). Doc rewritten to match the code exactly;GOLDEN_BREAKPOINTand its comparison are unchanged, pending your read on which side of exactly-2560 you want — flagged in the doc and indocs/live-verify.md's checklist item.applyresolves Golden's width through the same targeting ruleplan()uses" invariant, previously untested at theapplylevel (the obvious wrong refactor passed the whole suite). No production code change — the line was already correct, only unpinned.Transport::logseam mirroringwatch.rs's existing capture pattern.cli::USAGE, the tooltip,main.rs's docs) againstGOLDEN_BREAKPOINTdirectly, so a future bump that misses one of them fails there.HashMap-keyed output lookup instead of a flattenedVec+ linear scan, a literal assertion alongside a self-comparison).Refs #1052 #1019
🤖 Generated with Claude Code
https://claude.ai/code/session_0157rpXf1ks2hZEfdB47UJ6v