Files
Panama/docs/superpowers/plans/2026-08-18-phase2-display-arrangement.md
T

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.sh

Interfaces:

  • Consumes: records {name,width,height,scale,transform,x,y,primary}.

  • Produces: logicalSize, validate, normalize, snap, bounds, and canvasRects.

  • 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.sh

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.sh

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.sh
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.sh
  • Modify: tests/quickshell/settings-preferences-contract.sh

Interfaces:

  • Consumes: hyprctl -j monitors x/y values and backward-compatible persisted entries.

  • Produces: monitor records with x, y, primary; Lua validation for optional x, 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.sh

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.sh && tests/quickshell/settings-preferences-contract.sh

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.sh tests/quickshell/settings-preferences-contract.sh
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.sh
  • Create: tests/quickshell/display-transaction-contract.sh

Interfaces:

  • Consumes: DisplayLayout.validate/normalize and complete current monitor records.

  • Produces: public Displays.currentLayout(), Displays.applyLayout(layout), Displays.makePrimary(output); internal matchesLayout(monitors, layout); and backward-compatible apply(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.sh

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.sh && PANAMA_DISPLAYS_STATIC_ONLY=1 tests/quickshell/displays-contract.sh

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.sh tests/quickshell/display-transaction-contract.sh
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.sh
  • Modify: tests/quickshell/settings-pages-contract.sh

Interfaces:

  • Consumes: Displays.currentLayout(), Displays.applyLayout(), Displays.makePrimary(), and DisplayLayout.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.sh

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.sh && PANAMA_DISPLAYS_STATIC_ONLY=1 tests/quickshell/displays-contract.sh && tests/quickshell/settings-pages-contract.sh

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.sh tests/quickshell/settings-pages-contract.sh
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.sh

Interfaces:

  • Consumes: Displays.identifying and each Quickshell.screens entry.

  • 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.sh

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.sh

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.sh
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.sh
  • Modify: tests/quickshell/settings-backup-live-contract.sh
  • Modify: tests/quickshell/settings-commit-reset-contract.sh
  • Modify: tests/quickshell/settings-ownership-contract.sh

Interfaces:

  • Consumes: the existing internal displays preference 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.sh && tests/quickshell/settings-backup-live-contract.sh && tests/quickshell/settings-commit-reset-contract.sh

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.sh && tests/quickshell/settings-backup-live-contract.sh && tests/quickshell/settings-commit-reset-contract.sh && tests/quickshell/settings-ownership-contract.sh

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.sh tests/quickshell/settings-backup-live-contract.sh tests/quickshell/settings-commit-reset-contract.sh tests/quickshell/settings-ownership-contract.sh
git commit -m "Integrate complete display layouts"

Task 7: Slice verification

  • Step 1: Run all static display contracts
tests/quickshell/display-layout-contract.sh
tests/quickshell/display-transaction-contract.sh
tests/quickshell/display-arrangement-contract.sh
PANAMA_DISPLAYS_STATIC_ONLY=1 tests/quickshell/displays-contract.sh
tests/quickshell/settings-search-contract.sh
tests/quickshell/settings-backup-live-contract.sh

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.sh 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.