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
371 lines
18 KiB
Markdown
371 lines
18 KiB
Markdown
# 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 = "[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**
|
|
|
|
```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.
|