# 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`, and `canvasRects`.

- [ ] **Step 1: Write the failing geometry contract**

Use literal fixtures:

```json
[
  {"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:

```text
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**

```bash
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 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:

```lua
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**

```bash
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/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`

Expected: FAIL because `Displays.qml` tracks only one output per operation.

- [ ] **Step 4: Replace pending records with complete layouts**

Use:

```qml
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:

```text
hl.monitor({ output = "DP-2", mode = "4500x3000@60.00", position = "0x0", scale = 1.5, transform = 0 }); hl.monitor({ output = "HDMI-A-1", mode = "2560x1440@60.00", 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**

```bash
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()`, 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`

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**

```bash
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.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`

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**

```bash
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 `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 && 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**

```bash
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**

```bash
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.
