Phase 6, the last of the fresh-install spec. 159 scripts lose their .sh: 110 contracts, 47 Vicinae commands, 2 compositor contracts. A shebang and the executable bit already select the interpreter. The extension only ever added something that had to stay in sync, and the rename proved the point twice over in the space of an hour. The spec's stated risk was Vicinae's script discovery. One script was renamed and reloaded on its own before the other 46 followed; it came back as scripts:panama.capture and all 47 resolve. What the probe turned up instead is that the extension was never only a filename: Vicinae's command IDs embed it, so every ID changed. Nothing in this repository refers to them, so nothing breaks. The only trace is Vicinae's metadata.json, whose visited map had two Panama entries that are now orphaned -- two commands lost their usage ranking and will earn it back. Worth knowing before anyone renames these again on a machine that has a keybind pointing at one. Rewriting the references by exact filename missed two things it structurally could not see: a name built from a variable, settings-$page.sh, and a glob, -name '*.sh'. Both were in the contract that counts the generated commands, which promptly reported 47 expected and 0 found. The mechanical part of a rename is the part that looks finished. The three subcommands. panama doctor fronts a health check that already existed and already ran at the end of every install but could not be reached from a terminal. panama upgrade re-runs the installer from anywhere. panama test runs the suite, which had no entry point at all -- 121 files that were the main safety net in this repository and were invisible in it. Writing that runner found three tests nothing was running. calendar_agenda_bridge_test, home_assistant_bridge_test and kdeconnect_bridge_test are unittest suites without the executable bit, so no contract invoked them and the first draft of the runner skipped them silently. All three pass, and have passed unobserved for weeks. The runner collects *_test.py as well now, because a runner with a blind spot is worse than no runner for the same reason a dependency checker with one is: it reports PASS. Six worktrees pruned. Each was re-checked rather than trusted to the spec's list, and two needed it: panama-commands is not on feat/panama-commands but on feat/gnome-tweaks-parity, and fix/panama-displays-review reads [ahead 3] -- ahead of its remote, not of main, with every commit patch-equivalent to landed work. roadmap-completion stays; it has five commits that are genuinely unlanded. The branches are left alone: pruning a worktree costs nothing, deleting a branch is a decision. 121 contracts pass. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
18 KiB
Phase 2 Display Arrangement Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add safe drag-and-snap multi-monitor positioning and a Panama primary-display role without weakening the existing 15-second rollback contract.
Architecture: A pure layout module validates, normalizes, scales, and snaps logical monitor rectangles. Displays.qml upgrades its pending operation to a complete-layout transaction, and a focused canvas component edits drafts before invoking that transaction.
Tech Stack: Quickshell 0.3, Qt 6 QML/JavaScript, Hyprland 0.56.2 Lua evaluation, Lua startup config, Bash/QML contracts
Spec: docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md
Global Constraints
- One connected output is primary; it anchors persisted coordinates at logical
0,0. - Primary does not promise where third-party Wayland applications open.
- Every layout operation captures, applies, verifies, confirms, reverts, and verifies the complete connected layout.
- Keep is disabled until every output matches mode, scale, transform, x, and y.
- Stored disconnected outputs remain untouched and do not enter a live transaction.
- Development uses static fixtures; run the existing live display contract once at final completion.
Task 1: Pure display-layout geometry
Files:
- Create:
config/dot/quickshell/services/DisplayLayout.js - Create:
config/dot/quickshell/display-layout-harness.qml - Create:
tests/quickshell/display-layout-contract
Interfaces:
-
Consumes: records
{name,width,height,scale,transform,x,y,primary}. -
Produces:
logicalSize,validate,normalize,snap,bounds, andcanvasRects. -
Step 1: Write the failing geometry contract
Use literal fixtures:
[
{"name":"DP-2","width":4500,"height":3000,"scale":1.5,"transform":0,"x":140,"y":80,"primary":true},
{"name":"HDMI-A-1","width":2560,"height":1440,"scale":1,"transform":1,"x":3140,"y":80,"primary":false}
]
Assert transformed logical sizes, normalized DP-2 position 0,0, normalized HDMI position 3000,0, exactly one primary, and canvas rectangles preserving the complete desktop aspect ratio. Move HDMI within 16 logical pixels of DP-2's right edge and assert it snaps to x 3000; move it 17 pixels away and assert no snap.
- Step 2: Run and verify RED
Run: tests/quickshell/display-layout-contract
Expected: FAIL because the geometry module and harness do not exist.
- Step 3: Implement geometry functions
Rules:
transform 0 or 2 -> logical width = width/scale, height = height/scale
transform 1 or 3 -> logical width = height/scale, height = width/scale
coordinates -> finite integers between -100000 and 100000
snap threshold -> 16 logical pixels
canvas padding -> caller supplied; return data only, never QML objects
normalize subtracts the primary x/y from every record and returns new objects. snap compares the moving rectangle's four edges with every stationary rectangle's opposite and same-axis edges; choose the smallest eligible delta, then stable output-name order on ties.
- Step 4: Add invalid-layout cases
Assert rejection of duplicate outputs, zero or two primaries, fractional coordinates, non-positive scale, unsupported transform, non-finite values, and rectangles with zero logical size.
- Step 5: Run and verify GREEN
Run: tests/quickshell/display-layout-contract
Expected: PASS for geometry, deterministic snapping, normalization, and invalid cases.
- Step 6: Commit layout geometry
git add config/dot/quickshell/services/DisplayLayout.js config/dot/quickshell/display-layout-harness.qml tests/quickshell/display-layout-contract
git commit -m "Model multi-monitor layout geometry"
Task 2: Parse and persist extended monitor records
Files:
- Modify:
config/dot/quickshell/services/Displays.qml - Modify:
config/dot/quickshell/displays-harness.qml - Modify:
config/dot/hypr/monitors.lua - Modify:
config/dot/quickshell/config/PreferenceSchema.qml - Modify:
tests/quickshell/displays-contract - Modify:
tests/quickshell/settings-preferences-contract
Interfaces:
-
Consumes:
hyprctl -j monitorsx/y values and backward-compatible persisted entries. -
Produces: monitor records with
x,y,primary; Lua validation for optionalx,y,primary. -
Step 1: Extend the static contract and Lua fixture
Assert parsed monitors retain literal x/y. Feed Lua old, valid new, and malformed records. Expected startup calls:
DP-2 position = "0x0"
HDMI-A-1 position = "3000x0"
old valid entry position = "auto"
malformed x/y/primary entry = ignored in favor of shipped/auto behavior
Assert only one valid persisted primary is honored and DP-2's 10-bit/color policy remains unchanged.
- Step 2: Run and verify RED
Run: PANAMA_DISPLAYS_STATIC_ONLY=1 tests/quickshell/displays-contract
Expected: FAIL because x/y/primary are neither parsed nor replayed.
- Step 3: Extend monitor parsing
Read integer monitor.x and monitor.y. Derive the live primary from the connected persisted primary when valid, otherwise the output at 0,0, otherwise the first connected monitor. Include the boolean only in the service model; Hyprland receives position, not a nonexistent primary flag.
- Step 4: Extend Lua validation
Add valid_position(entry) and valid_primary(entry). An entry is extended only when all three new fields are present and valid; an entry with none remains legacy and uses position = "auto"; a partially extended entry is invalid. Render position with string.format("%dx%d", entry.x, entry.y).
- Step 5: Update schema documentation
Change the internal displays detail to Resolution, scale, rotation, position, and primary display. Do not add a second preference.
- Step 6: Run and verify GREEN
Run: PANAMA_DISPLAYS_STATIC_ONLY=1 tests/quickshell/displays-contract && tests/quickshell/settings-preferences-contract
Expected: PASS for old and new records.
- Step 7: Commit extended persistence
git add config/dot/quickshell/services/Displays.qml config/dot/quickshell/displays-harness.qml config/dot/hypr/monitors.lua config/dot/quickshell/config/PreferenceSchema.qml tests/quickshell/displays-contract tests/quickshell/settings-preferences-contract
git commit -m "Persist complete monitor layouts"
Task 3: Whole-layout transaction and rollback
Files:
- Modify:
config/dot/quickshell/services/Displays.qml - Modify:
config/dot/quickshell/displays-harness.qml - Modify:
tests/quickshell/displays-contract - Create:
tests/quickshell/display-transaction-contract
Interfaces:
-
Consumes:
DisplayLayout.validate/normalizeand complete current monitor records. -
Produces: public
Displays.currentLayout(),Displays.applyLayout(layout),Displays.makePrimary(output); internalmatchesLayout(monitors, layout); and backward-compatibleapply(output, mode, scale, transform). -
Step 1: Write the failing fixture transaction contract
Fake hyprctl monitor JSON and eval. Request a two-output layout and assert the evaluator receives both literal monitor calls in one argv payload. Assert canConfirm remains false when one output has wrong y, becomes true only when both match, and confirm persists both records with one primary.
- Step 2: Add rollback and generation cases
Assert timeout, explicit revert, non-zero apply exit, wrong readback, and a disconnect generation each restore all still-connected outputs. Return a stale pre-operation query after a newer operation starts and prove it cannot confirm or clear the newer transaction. Force wrong revert readback and assert the existing manual-restoration error.
- Step 3: Run and verify RED
Run: tests/quickshell/display-transaction-contract
Expected: FAIL because Displays.qml tracks only one output per operation.
- Step 4: Replace pending records with complete layouts
Use:
property var pendingPreviousLayout: null
property var pendingRequestedLayout: null
property var revertExpectedLayout: null
readonly property bool awaitingConfirmation: root.pendingRequestedLayout !== null
Retain the existing generation counters and timers. matchesLayout requires equal connected output-name sets and exact x/y/transform, with the existing tolerances for refresh and scale.
- Step 5: Implement one validated evaluation payload
Build each call only from compositor-reported output names and validated numeric/mode fields:
hl.monitor({ output = "DP-2", mode = "[email protected]", position = "0x0", scale = 1.5, transform = 0 }); hl.monitor({ output = "HDMI-A-1", mode = "[email protected]", position = "3000x0", scale = 1, transform = 0 })
Reject quotes or non-connector characters in output names before generation. Preserve sequential Process/readback timing around the single eval.
- Step 6: Preserve one-field callers
Keep apply(output, mode, scale, transform) by cloning currentLayout(), replacing one output's four existing fields, retaining every position and primary flag, then calling applyLayout. This keeps DisplayModePicker, scale, rotation, and the existing live contract working.
- Step 7: Run and verify GREEN
Run: tests/quickshell/display-transaction-contract && PANAMA_DISPLAYS_STATIC_ONLY=1 tests/quickshell/displays-contract
Expected: PASS for apply, confirm, timeout, all revert paths, disconnect, and stale generations.
- Step 8: Commit safe transactions
git add config/dot/quickshell/services/Displays.qml config/dot/quickshell/displays-harness.qml tests/quickshell/displays-contract tests/quickshell/display-transaction-contract
git commit -m "Apply monitor layouts transactionally"
Task 4: Arrangement canvas
Files:
- Create:
config/dot/quickshell/modules/settings/DisplayArrangement.qml - Modify:
config/dot/quickshell/modules/settings/DisplaysPage.qml - Create:
tests/quickshell/display-arrangement-contract - Modify:
tests/quickshell/settings-pages-contract
Interfaces:
-
Consumes:
Displays.currentLayout(),Displays.applyLayout(),Displays.makePrimary(), andDisplayLayout.canvasRects/snap. -
Produces: selected output, pointer/keyboard draft positioning, Make primary, and narrow-layout textual selection.
-
Step 1: Write the failing visual-behavior contract
Render two literal monitors at wide and 500 px content widths. Through harness IPC, drag HDMI next to DP-2, release, and assert one applyLayout call with snapped logical x. Focus HDMI and send Left plus Shift+Left; assert 10 and 100 logical-pixel draft steps. Activate Make primary and assert normalized DP-2 coordinates become negative while HDMI becomes 0,0.
- Step 2: Run and verify RED
Run: tests/quickshell/display-arrangement-contract
Expected: FAIL because no arrangement component exists.
- Step 3: Implement responsive canvas data flow
Keep draftLayout as copied plain records. Recalculate canvas rectangles from DisplayLayout.canvasRects when width or monitors change, but never mutate live service records. Use DragHandler for the selected tile and call DisplayLayout.snap before mapping canvas movement back to logical coordinates.
- Step 4: Add keyboard and primary actions
Each tile is focusable and exposes accessible name Move DISPLAY_NAME. Arrow keys modify the draft by 10 logical pixels; Shift uses 100. Enter applies the draft. Escape discards it. Make primary changes one boolean, normalizes through the pure module, then applies through the same transaction.
- Step 5: Integrate Displays page
Show the card only for two or more monitors. Keep the existing connected-display ChoiceGrid below/inside the selected-display area for narrow tiled widths. Bind arrangement selection and root.selectedOutput both ways without loops.
- Step 6: Run and verify GREEN
Run: tests/quickshell/display-arrangement-contract && PANAMA_DISPLAYS_STATIC_ONLY=1 tests/quickshell/displays-contract && tests/quickshell/settings-pages-contract
Expected: PASS at both widths with no QML warnings.
- Step 7: Commit the canvas
git add config/dot/quickshell/modules/settings/DisplayArrangement.qml config/dot/quickshell/modules/settings/DisplaysPage.qml tests/quickshell/display-arrangement-contract tests/quickshell/settings-pages-contract
git commit -m "Add monitor arrangement canvas"
Task 5: Static display identification overlays
Files:
- Create:
config/dot/quickshell/modules/settings/DisplayIdentify.qml - Modify:
config/dot/quickshell/modules/settings/DisplayArrangement.qml - Modify:
config/dot/quickshell/shell.qml - Modify:
tests/quickshell/display-arrangement-contract
Interfaces:
-
Consumes:
Displays.identifyingand eachQuickshell.screensentry. -
Produces:
Displays.identify()and one non-interactive three-second numbered overlay per screen. -
Step 1: Write the failing overlay contract
Invoke identify through the fixture harness. Assert screen 1 renders connector/name and number 1, screen 2 renders number 2, overlays accept no keyboard focus or pointer input, and all become invisible after one 3,000 ms single-shot timer. Assert repeated invocation restarts that timer without creating more windows.
- Step 2: Run and verify RED
Run: tests/quickshell/display-arrangement-contract
Expected: FAIL because identify state and overlays do not exist.
- Step 3: Add service state and shell-owned windows
Displays.identify() sets one boolean and restarts one Timer. DisplayIdentify.qml uses a Variants model over Quickshell.screens, one transparent non-focusable PanelWindow per screen, centered static number card, and no animations. Instantiate it once from shell.qml; the Settings button only calls the service.
- Step 4: Run and verify GREEN
Run: tests/quickshell/display-arrangement-contract
Expected: PASS with a fixed window count and no focus-grab warnings.
- Step 5: Commit identification
git add config/dot/quickshell/modules/settings/DisplayIdentify.qml config/dot/quickshell/modules/settings/DisplayArrangement.qml config/dot/quickshell/shell.qml tests/quickshell/display-arrangement-contract
git commit -m "Add display identification overlays"
Task 6: Search, backup, and ownership integration
Files:
- Modify:
config/dot/quickshell/services/SettingsSearch.qml - Modify:
config/dot/quickshell/services/SettingsBackup.qml - Modify:
config/dot/quickshell/modules/settings/README.md - Modify:
tests/quickshell/settings-search-contract - Modify:
tests/quickshell/settings-backup-live-contract - Modify:
tests/quickshell/settings-commit-reset-contract - Modify:
tests/quickshell/settings-ownership-contract
Interfaces:
-
Consumes: the existing internal
displayspreference and complete-layout transaction. -
Produces: search terms for arrangement/primary and protected whole-layout restore.
-
Step 1: Write failing integration assertions
Assert arrange displays, monitor position, and primary display route to Displays. Restore a two-output snapshot while a current layout is protected; assert complete apply/verify occurs before shell reload and any failed restore retains/proves the original layout. Reset must clear confirmed arrangement fields so startup returns to shipped DP-2 plus automatic placement for other outputs.
- Step 2: Run and verify RED
Run: tests/quickshell/settings-search-contract && tests/quickshell/settings-backup-live-contract && tests/quickshell/settings-commit-reset-contract
Expected: FAIL for missing terms and one-output restore assumptions.
- Step 3: Update search and restore
Add three manual search entries because displays is internal. Replace any single-output assumptions in SettingsBackup with cloned complete layout objects and wait on the existing Displays.busy || Displays.awaitingConfirmation boundary.
- Step 4: Update ownership documentation
Document Displays as the sole owner of mode, scale, rotation, arrangement, and primary role. No mirror entry is added.
- Step 5: Run and verify GREEN
Run: tests/quickshell/settings-search-contract && tests/quickshell/settings-backup-live-contract && tests/quickshell/settings-commit-reset-contract && tests/quickshell/settings-ownership-contract
Expected: PASS with no new duplicated controls.
- Step 6: Commit integration
git add config/dot/quickshell/services/SettingsSearch.qml config/dot/quickshell/services/SettingsBackup.qml config/dot/quickshell/modules/settings/README.md tests/quickshell/settings-search-contract tests/quickshell/settings-backup-live-contract tests/quickshell/settings-commit-reset-contract tests/quickshell/settings-ownership-contract
git commit -m "Integrate complete display layouts"
Task 7: Slice verification
- Step 1: Run all static display contracts
tests/quickshell/display-layout-contract
tests/quickshell/display-transaction-contract
tests/quickshell/display-arrangement-contract
PANAMA_DISPLAYS_STATIC_ONLY=1 tests/quickshell/displays-contract
tests/quickshell/settings-search-contract
tests/quickshell/settings-backup-live-contract
Expected: all PASS without touching the physical display.
- Step 2: Validate startup configuration once
Run: Hyprland --verify-config
Expected: config ok.
- Step 3: Defer the live display test
Do not run the state-changing portion of tests/quickshell/displays-contract here. The master Phase 2 completion gate runs it once after every slice is stable and restores the observed mode, scale, transform, and position.
- Step 4: Review branch state
Run: git diff --check && git status --short
Expected: clean after the Task 6 commit.