Plan Phase 2 expectation gaps

This commit is contained in:
Gabriel Brown
2026-08-18 15:00:18 -04:00
parent bfa9c58b09
commit e5a2a430e0
5 changed files with 1508 additions and 0 deletions
@@ -0,0 +1,260 @@
# Phase 2 Application Volume 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 a live, non-persistent per-application playback mixer to Panama Settings.
**Architecture:** A small pure JavaScript module groups PipeWire playback nodes and owns aggregate calculations. `AudioDevices.qml` exposes the live groups, while focused QML components render and mutate the real tracked nodes.
**Tech Stack:** Quickshell 0.3, Qt 6 QML/JavaScript, `Quickshell.Services.Pipewire`, Bash contract harnesses
**Spec:** `docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md`
## Global Constraints
- PipeWire remains the source of truth; do not persist stream identity or volume.
- Include only ready `PwNodeType.AudioOutStream` nodes with writable audio state.
- Group by `application.id`, binary, name, then node ID, in that order.
- No polling, shell commands, or logging of stream metadata.
- Preserve the existing output, input, balance, and Fedora device-profile controls.
---
### Task 1: Pure application-stream model
**Files:**
- Create: `config/dot/quickshell/services/AudioStreams.js`
- Create: `config/dot/quickshell/audio-streams-harness.qml`
- Create: `tests/quickshell/application-volume-contract.sh`
**Interfaces:**
- Consumes: PipeWire-shaped nodes with `id`, `ready`, `type`, `properties`, and `audio`.
- Produces: `group(nodes, audioOutStreamFlag)`, `volume(group)`, `muted(group)`, `setVolume(group, value)`, and `setMuted(group, muted)`.
- [ ] **Step 1: Write the failing grouping contract**
Create complete fixture nodes for two Chromium streams, one Spotify stream, an input stream, an unready stream, and a metadata-free stream. Through IPC, assert the literal groups and labels:
```json
[
{"key":"org.chromium.Chromium","label":"Chromium","icon":"chromium","count":2},
{"key":"spotify","label":"Spotify","icon":"audio-x-generic-symbolic","count":1},
{"key":"node:99","label":"Unknown application","icon":"audio-x-generic-symbolic","count":1}
]
```
Also assert Chromium volume `0.6` from fixture values `0.4` and `0.8`, mixed mute reports `false`, setting volume writes both nodes, and setting mute normalizes both nodes.
- [ ] **Step 2: Run the contract and verify RED**
Run: `tests/quickshell/application-volume-contract.sh`
Expected: FAIL because `AudioStreams.js` and the IPC target do not exist.
- [ ] **Step 3: Implement the minimal pure model**
Export these exact functions:
```javascript
function property(node, key) {
const value = node && node.properties ? node.properties[key] : "";
return typeof value === "string" ? value.trim() : "";
}
function groupKey(node) {
return property(node, "application.id")
|| property(node, "application.process.binary")
|| property(node, "application.name")
|| `node:${node.id}`;
}
function label(node) {
return property(node, "application.name")
|| String(node.description || "").trim()
|| property(node, "media.name")
|| "Unknown application";
}
function icon(node) {
return property(node, "application.icon_name")
|| "audio-x-generic-symbolic";
}
function group(nodes, audioOutStreamFlag) {
const groups = [];
const byKey = {};
for (const node of nodes || []) {
if (!node || node.ready !== true || !node.audio
|| (node.type & audioOutStreamFlag) !== audioOutStreamFlag)
continue;
const key = groupKey(node);
if (!byKey[key]) {
byKey[key] = { key, label: label(node), icon: icon(node), nodes: [] };
groups.push(byKey[key]);
}
byKey[key].nodes.push(node);
}
return groups;
}
function audioNodes(application) {
return (application && application.nodes || []).filter(node => node && node.audio);
}
function volume(application) {
const nodes = audioNodes(application);
return nodes.length === 0 ? 0
: nodes.reduce((sum, node) => sum + node.audio.volume, 0) / nodes.length;
}
function muted(application) {
const nodes = audioNodes(application);
return nodes.length > 0 && nodes.every(node => node.audio.muted === true);
}
function setVolume(application, value) {
const next = Math.max(0, Math.min(1, Number(value)));
if (!Number.isFinite(next)) return false;
const nodes = audioNodes(application);
for (const node of nodes) {
node.audio.muted = false;
node.audio.volume = next;
}
return nodes.length > 0;
}
function setMuted(application, mutedValue) {
const nodes = audioNodes(application);
for (const node of nodes) node.audio.muted = mutedValue === true;
return nodes.length > 0;
}
```
Use literal property lookups and `Number.isFinite`; do not import PipeWire into the pure module.
- [ ] **Step 4: Run the contract and verify GREEN**
Run: `tests/quickshell/application-volume-contract.sh`
Expected: PASS for grouping, fallback metadata, aggregate volume, mixed mute, and writes.
- [ ] **Step 5: Commit the model**
```bash
git add config/dot/quickshell/services/AudioStreams.js config/dot/quickshell/audio-streams-harness.qml tests/quickshell/application-volume-contract.sh
git commit -m "Model live application audio streams"
```
### Task 2: Live PipeWire service boundary
**Files:**
- Modify: `config/dot/quickshell/services/AudioDevices.qml`
- Modify: `config/dot/quickshell/audio-streams-harness.qml`
- Modify: `tests/quickshell/application-volume-contract.sh`
**Interfaces:**
- Consumes: `AudioStreams.group(nodes, PwNodeType.AudioOutStream)`.
- Produces: `readonly property var applications` and typed wrappers `applicationVolume`, `applicationMuted`, `setApplicationVolume`, `setApplicationMuted`.
- [ ] **Step 1: Extend the contract for the service API**
Assert the harness can report real `AudioDevices.applications` without starting playback and that every returned group contains only nodes with the AudioOutStream flag. Assert mutator wrappers reject `null` and a group with no live audio nodes.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/application-volume-contract.sh`
Expected: FAIL because `AudioDevices.applications` is undefined.
- [ ] **Step 3: Add the reactive service properties**
Implement:
```qml
readonly property var playbackStreams: Pipewire.nodes.values.filter(node =>
node.ready && node.audio
&& (node.type & PwNodeType.AudioOutStream) === PwNodeType.AudioOutStream)
readonly property var applications: AudioStreams.group(root.playbackStreams, PwNodeType.AudioOutStream)
```
Delegate aggregate reads and writes to the pure model. Preserve `outputs`, `inputs`, `nodes()`, `current()`, and `select()` unchanged.
- [ ] **Step 4: Run and verify GREEN**
Run: `tests/quickshell/application-volume-contract.sh`
Expected: PASS with zero or more real live applications and no mutation of the live streams.
- [ ] **Step 5: Commit the service boundary**
```bash
git add config/dot/quickshell/services/AudioDevices.qml config/dot/quickshell/audio-streams-harness.qml tests/quickshell/application-volume-contract.sh
git commit -m "Expose live application audio groups"
```
### Task 3: Application mixer UI
**Files:**
- Create: `config/dot/quickshell/modules/settings/ApplicationVolumeRow.qml`
- Create: `config/dot/quickshell/modules/settings/ApplicationMixer.qml`
- Modify: `config/dot/quickshell/modules/settings/SoundPage.qml`
- Modify: `tests/quickshell/application-volume-contract.sh`
- Modify: `tests/quickshell/sound-page-contract.sh`
**Interfaces:**
- Consumes: one `AudioDevices.applications` group per row.
- Produces: a real Sound **Applications** card and the empty-state sentence from the spec.
- [ ] **Step 1: Write the failing UI contract**
Construct `SoundPage.qml` in the existing isolated Settings harness. Assert no QML warnings and these rendered states: application rows when groups exist, **Applications playing sound will appear here** when empty, and **PipeWire is unavailable** when the service is not ready. Assert the advanced handoff label is exactly **Device profiles**.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/application-volume-contract.sh && tests/quickshell/sound-page-contract.sh`
Expected: FAIL because the mixer components and Applications card are absent.
- [ ] **Step 3: Implement the row and card**
`ApplicationVolumeRow.qml` must declare a `PwObjectTracker` over `application.nodes`, use `ThemedIcon`, `IconButton`, `ValueSlider`, and a tabular percentage. Its only writes are `AudioDevices.setApplicationMuted(root.application, value)` and `AudioDevices.setApplicationVolume(root.application, value)`. `ApplicationMixer.qml` owns the Repeater and empty/unavailable copy. Put the card after Input and before Sound feedback.
- [ ] **Step 4: Run and verify GREEN**
Run: `tests/quickshell/application-volume-contract.sh && tests/quickshell/sound-page-contract.sh`
Expected: PASS with no QML warnings.
- [ ] **Step 5: Commit the finished application mixer**
```bash
git add config/dot/quickshell/modules/settings/ApplicationVolumeRow.qml config/dot/quickshell/modules/settings/ApplicationMixer.qml config/dot/quickshell/modules/settings/SoundPage.qml tests/quickshell/application-volume-contract.sh tests/quickshell/sound-page-contract.sh
git commit -m "Add application volume mixer"
```
### Task 4: Slice verification
**Files:**
- Modify only if verification exposes a defect in the files above.
- [ ] **Step 1: Run the focused static and runtime contracts once**
Run:
```bash
tests/quickshell/application-volume-contract.sh
tests/quickshell/sound-page-contract.sh
tests/quickshell/settings-pages-contract.sh
```
Expected: all PASS; runtime enumeration may report zero applications without failing.
- [ ] **Step 2: Inspect Quickshell output**
The isolated harness output must contain no `ReferenceError`, `TypeError`, binding loop, failed property assignment, or `PwObjectTracker` warning.
- [ ] **Step 3: Review the diff**
Run: `git diff --check && git status --short`
Expected: clean formatting and no uncommitted changes after the Task 3 commit.
@@ -0,0 +1,370 @@
# 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:
```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.sh`
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.sh`
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.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:
```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.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**
```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.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:
```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.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**
```bash
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**
```bash
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**
```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.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**
```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.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**
```bash
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.
@@ -0,0 +1,266 @@
# Phase 2 Expectation Gaps 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:** Deliver application volume, managed lock appearance, wallpaper modes, and safe multi-monitor arrangement as one coherent Panama Settings phase.
**Architecture:** Phase 2 is four independent subsystems, so each has its own executable plan and focused commits. This master plan fixes their order, cross-slice integration, and the single conservative live verification gate.
**Tech Stack:** Quickshell 0.3, Qt 6 QML/JavaScript, PipeWire 1.6, Hyprland 0.56.2 Lua, hyprlock 0.9.6, hyprpaper 0.8.4, Bash, jq
**Spec:** `docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md`
## Global Constraints
- Implement the approved **A — Continuity** direction; do not introduce an inspector or extra navigation level.
- Use schema/service boundaries already established by Panama Settings.
- Write and observe a focused failing test before every production behavior.
- Keep live desktop mutations out of development loops.
- Do not start playback, acquire the live lock screen, rotate the live wallpaper through fixtures, or emulate a second physical display.
- Run changed-page QML harnesses and the live display contract once at completion, not after every edit.
- Each subsystem must be independently green and committed before the next begins.
## Plan set
1. `docs/superpowers/plans/2026-08-18-phase2-application-volume.md`
2. `docs/superpowers/plans/2026-08-18-phase2-lock-screen.md`
3. `docs/superpowers/plans/2026-08-18-phase2-wallpaper-modes.md`
4. `docs/superpowers/plans/2026-08-18-phase2-display-arrangement.md`
## Spec coverage map
| Approved requirement | Owning plan/tasks |
|---|---|
| PipeWire application grouping, controls, empty/error states | Application Volume Tasks 14 |
| Generated lock config, safe fallback, preview, search, restore, health | Lock Screen Tasks 15 |
| Single/slideshow/per-monitor policy, verification, timer, UI, restore | Wallpaper Modes Tasks 16 |
| Position, primary role, transaction/revert, canvas, identify, restore | Display Arrangement Tasks 17 |
| Shared ownership, search vocabulary, deterministic restore ordering | Master Task 5 |
| Conservative runtime/config/visual verification and publication | Master Tasks 67 |
---
### Task 1: Execute the application-volume plan
**Files:** Defined in `2026-08-18-phase2-application-volume.md`.
**Interfaces:**
- Produces: `AudioDevices.applications` and the Sound Applications card.
- Independent of: lock, wallpaper, and display state.
- [ ] **Step 1: Read the spec and application-volume plan completely**
Run: `sed -n '1,520p' docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md && sed -n '1,420p' docs/superpowers/plans/2026-08-18-phase2-application-volume.md`
- [ ] **Step 2: Execute every unchecked application-volume task in order**
Use strict red-green-refactor cycles and the exact commit boundaries in that plan.
- [ ] **Step 3: Confirm the slice head**
Run: `tests/quickshell/application-volume-contract.sh && tests/quickshell/sound-page-contract.sh && git status --short`
Expected: both PASS and no uncommitted files.
### Task 2: Execute the lock-screen plan
**Files:** Defined in `2026-08-18-phase2-lock-screen.md`.
**Interfaces:**
- Consumes: existing schema, wallpaper fallback values, and SettingsBackup settle sequencing.
- Produces: `panama-lock`, `LockScreen.qml`, generated config, Appearance card, and health/restore integration.
- [ ] **Step 1: Read the lock-screen plan completely**
Run: `sed -n '1,520p' docs/superpowers/plans/2026-08-18-phase2-lock-screen.md`
- [ ] **Step 2: Execute every unchecked lock-screen task in order**
Never invoke `panama-lock run` outside its fake-hyprlock fixture.
- [ ] **Step 3: Confirm the slice head**
Run: `tests/quickshell/lock-screen-helper-contract.sh && tests/quickshell/lock-screen-service-contract.sh && tests/quickshell/lock-screen-settings-contract.sh && git status --short`
Expected: all PASS, no live `hyprlock` test process, and no uncommitted files.
### Task 3: Execute the wallpaper-modes plan
**Files:** Defined in `2026-08-18-phase2-wallpaper-modes.md`.
**Interfaces:**
- Consumes: schema and connected output names.
- Produces: policy calculation, verified hyprpaper orchestration, runtime slideshow, controls, and restore integration.
- [ ] **Step 1: Read the wallpaper plan completely**
Run: `sed -n '1,620p' docs/superpowers/plans/2026-08-18-phase2-wallpaper-modes.md`
- [ ] **Step 2: Execute every unchecked wallpaper task in order**
All apply tests use fake hyprpaper IPC and temporary image files.
- [ ] **Step 3: Confirm the slice head**
Run: `tests/quickshell/wallpaper-policy-contract.sh && tests/quickshell/wallpaper-service-contract.sh && tests/quickshell/wallpaper-settings-contract.sh && git status --short`
Expected: all PASS and no uncommitted files.
### Task 4: Execute the display-arrangement plan
**Files:** Defined in `2026-08-18-phase2-display-arrangement.md`.
**Interfaces:**
- Consumes: existing Displays apply/revert contract and SettingsBackup display protection.
- Produces: pure geometry, extended persistence, whole-layout transactions, arrangement UI, identify overlays, and search/restore integration.
- [ ] **Step 1: Read the display plan completely**
Run: `sed -n '1,720p' docs/superpowers/plans/2026-08-18-phase2-display-arrangement.md`
- [ ] **Step 2: Execute every unchecked display task except the live display test**
Use fixture monitor JSON for all multi-monitor cases.
- [ ] **Step 3: Confirm the static slice head**
Run: `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 && git status --short`
Expected: all PASS and no uncommitted files.
### Task 5: Cross-slice Settings integration audit
**Files:**
- Modify: `config/dot/quickshell/services/SettingsBackup.qml`
- Modify: `config/dot/quickshell/services/SettingsSearch.qml`
- Modify: `config/dot/quickshell/modules/settings/AppearancePage.qml`
- Modify: `config/dot/quickshell/modules/settings/README.md`
- Modify: relevant existing contracts only when the combined behavior requires it.
**Interfaces:**
- Consumes: `LockScreen.regenerate()`, `Wallpaper.applyCurrentPolicy(false)`, and complete display-layout restore.
- Verifies: one deterministic restore sequence and one ownership/search vocabulary.
- [ ] **Step 1: Expand the combined restore fixture before the final audit**
The slice plans already extend `tests/quickshell/settings-backup-live-contract.sh` test-first. Confirm its final fixture now contains lock, slideshow, and two-output layout values and asserts this exact order: preferences reload, display apply/verify, idle apply, lock generate, wallpaper apply/verify, keybind/compositor settle, shell reload. It must also assert bounded failure remains in service state and shell reload waits for the settle cap.
- [ ] **Step 2: Run the combined audit**
Run: `tests/quickshell/settings-backup-live-contract.sh`
Expected: PASS. A failure means the independently green hooks conflict and must be debugged before any consolidation.
- [ ] **Step 3: Consolidate only if the audit exposes duplication**
Keep `SettingsBackup.qml` as the established restore coordinator and its injected wrapper pattern for Displays, Wallpaper, LockScreen, Keybinds, and SystemSettings. If two slices added equivalent settle state, collapse them under the existing timer without changing observable order. Do not create another restore service.
- [ ] **Step 4: Reconcile search and ownership once**
Run the ownership contract against final pages. Manual search entries may exist only for internal JSON controls with no schema row. Remove duplicate terms that the schema already indexes.
- [ ] **Step 5: Run and verify GREEN**
Run: `tests/quickshell/settings-backup-live-contract.sh && tests/quickshell/settings-search-contract.sh && tests/quickshell/settings-ownership-contract.sh`
Expected: all PASS.
- [ ] **Step 6: Commit only audit-driven corrections**
If Step 2 or Step 5 required corrections, stage only those files and commit them as `Integrate Phase 2 desktop settings`. If the audit is already green and no consolidation is needed, leave the branch unchanged; an empty ceremony commit is forbidden.
### Task 6: Consolidated completion gate
**Files:**
- Modify only if verification finds a defect.
- [ ] **Step 1: Run new contracts**
```bash
tests/quickshell/application-volume-contract.sh
tests/quickshell/lock-screen-helper-contract.sh
tests/quickshell/lock-screen-service-contract.sh
tests/quickshell/lock-screen-settings-contract.sh
tests/quickshell/wallpaper-policy-contract.sh
tests/quickshell/wallpaper-service-contract.sh
tests/quickshell/wallpaper-settings-contract.sh
tests/quickshell/display-layout-contract.sh
tests/quickshell/display-transaction-contract.sh
tests/quickshell/display-arrangement-contract.sh
```
Expected: all PASS.
- [ ] **Step 2: Run affected existing contracts**
```bash
tests/quickshell/sound-page-contract.sh
tests/quickshell/settings-pages-contract.sh
tests/quickshell/settings-search-contract.sh
tests/quickshell/settings-ownership-contract.sh
tests/quickshell/settings-preferences-contract.sh
tests/quickshell/settings-commit-reset-contract.sh
tests/quickshell/settings-backup-live-contract.sh
tests/quickshell/migrations-contract.sh
tests/quickshell/panama-doctor-contract.sh
tests/quickshell/schema-hypr-shape-contract.sh
```
Expected: all PASS.
- [ ] **Step 3: Run the one state-changing display contract**
Run: `tests/quickshell/displays-contract.sh`
Expected: PASS and cleanup proves the physical monitor returns to its captured original mode, scale, transform, x, and y. Stop immediately if the preflight reports a dirty display baseline.
- [ ] **Step 4: Run config and source checks**
```bash
bash -n config/dot/quickshell/scripts/panama-lock
bash -n config/dot/quickshell/scripts/panama-idle
git diff --check
Hyprland --verify-config
```
Expected: shell syntax exits 0, diff check is empty, and Hyprland reports `config ok`.
- [ ] **Step 5: Construct changed QML once**
Run the isolated harness collection once and inspect its complete log for `ERROR`, `WARN`, `ReferenceError`, `TypeError`, binding loops, invalid anchors, failed property assignments, and focus-grab failures. Expected: none.
- [ ] **Step 6: Review requirement coverage and branch state**
Compare the final diff with every heading in the approved spec. Run `git status --short --branch` and `git log --oneline origin/main..HEAD`. Expected: only intentional commits and a clean tree.
### Task 7: Merge, activate, and publish
**Files:** None unless final activation exposes a defect.
- [ ] **Step 1: Push the feature branch**
Run: `git push -u origin feat/phase2-expectation-gaps`
Expected: remote branch points to the verified head.
- [ ] **Step 2: Fast-forward clean main**
In `/home/gib/.local/share/Panama`, fetch, prove `main` is clean and not behind an unexpected remote commit, then run `git merge --ff-only feat/phase2-expectation-gaps`.
- [ ] **Step 3: Push main**
Run: `git push origin main`
Expected: `origin/main` equals local `main`.
- [ ] **Step 4: Restart Quickshell once**
Use Panama's verified graceful shell restart action. Do not open repeated harness windows. Confirm `qs list --all` reports one live production instance and inspect only the fresh startup log.
- [ ] **Step 5: Perform one visual review**
Open Settings once and inspect Sound Applications, Appearance Lock screen and Wallpaper, and Displays Arrangement. Confirm narrow tiled layout, empty states, focus order, and preview/canvas geometry. Locking, wallpaper rotation, and adding a monitor remain user-driven real-world follow-ups unless a safe no-op state already exercises them.
- [ ] **Step 6: Report exact delivery evidence**
Provide commit range, pushed branch/main state, contract counts, the live display restoration result, Quickshell instance state, and any capability that could not be exercised without external hardware or acquiring the live lock screen.
@@ -0,0 +1,285 @@
# Phase 2 Lock Screen 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:** Make lock-screen appearance configurable from Appearance without editing the tracked hyprlock file or risking an unlocked session.
**Architecture:** Schema-backed values feed an atomic `panama-lock` generator under `$XDG_STATE_HOME`. Hypridle invokes the helper, a small Quickshell service proactively regenerates on relevant changes, and an ordinary QML preview mirrors the selected roles without launching the real locker.
**Tech Stack:** Bash, jq, hyprlock 0.9.6 hyprlang, Quickshell 0.3, Qt 6 QML
**Spec:** `docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md`
## Global Constraints
- Never rewrite `config/dot/hypr/hyprlock.conf` at runtime.
- Authentication, PAM, arbitrary commands, markup, fonts, and paths are not configurable.
- Generation is atomic; failure preserves the last valid generated config.
- `run` falls back to the tracked config if generation fails.
- Tests never acquire the live session lock.
- Appearance owns visual controls; Power and Privacy retain their established timing controls.
---
### Task 1: Schema and deterministic generator
**Files:**
- Modify: `config/dot/quickshell/config/PreferenceSchema.qml`
- Create: `config/dot/quickshell/scripts/panama-lock`
- Create: `tests/quickshell/lock-screen-helper-contract.sh`
**Interfaces:**
- Consumes: `lockBackgroundMode`, `lockBlurLevel`, `lockShowClock`, `lockShowDate`, `lockShowUser`, `lockFadeOnEmpty`, `use24Hour`, `colorScheme`, and wallpaper policy from `settings.json`.
- Produces: `panama-lock generate`, `panama-lock status`, and `panama-lock run`.
- [ ] **Step 1: Write the failing helper contract**
Use temporary `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `HOME`, fake `hyprctl`, and fake `hyprlock`. Cover literal dark defaults and every background mode. Parse the generated file and assert:
```text
background.path = screenshot
background.blur_passes = 3
background.blur_size = 8
clock command = date +"%-I:%M"
date label present = true
user label present = true
input-field.fade_on_empty = false
```
For light solid mode assert `rgba(245, 246, 250, 1.0)`. For wallpaper mode with two fake outputs assert one background block per output and the per-monitor path fallback. Prove invalid enum, blur, boolean, JSON, and path values return to shipped defaults.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/lock-screen-helper-contract.sh`
Expected: FAIL because `panama-lock` does not exist.
- [ ] **Step 3: Add the six schema entries**
Use exact types and defaults:
```qml
{
key: "lockBackgroundMode", type: "enum", def: "screenshot", group: "lockAppearance",
label: "Background", detail: "What appears behind the lock screen",
options: [
{ value: "screenshot", label: "Blurred desktop" },
{ value: "wallpaper", label: "Current wallpaper" },
{ value: "solid", label: "Solid color" }
]
},
{
key: "lockBlurLevel", type: "int", def: 3, min: 0, max: 5, step: 1,
group: "lockAppearance", label: "Background blur", detail: "Softens what is behind the password field"
},
{ key: "lockShowClock", type: "bool", def: true, group: "lockAppearance", label: "Show clock", detail: "Use the desktop's 12 or 24-hour format" },
{ key: "lockShowDate", type: "bool", def: true, group: "lockAppearance", label: "Show date", detail: "Show the weekday and full date" },
{ key: "lockShowUser", type: "bool", def: true, group: "lockAppearance", label: "Show user name", detail: "Identify the signed-in account" },
{ key: "lockFadeOnEmpty", type: "bool", def: false, group: "lockAppearance", label: "Hide password field until typing", detail: "Keep the empty field out of the way" }
```
Insert these entries using the repository's existing schema object shape; do not add a new schema feature.
- [ ] **Step 4: Implement `panama-lock`**
Commands and results:
```text
panama-lock generate -> atomically writes state/panama/hyprlock.conf
panama-lock status -> {"generated":true,"path":"...","fallback":false,"error":""}
panama-lock run -> exec hyprlock -c generated; fallback to config/hypr/hyprlock.conf
```
Read values with `jq`, validate again in Bash, map blur levels exactly as:
```text
0 -> passes 0, size 1
1 -> passes 1, size 3
2 -> passes 2, size 5
3 -> passes 3, size 8
4 -> passes 4, size 10
5 -> passes 5, size 12
```
Write `generated.tmp`, validate that it is non-empty and contains `auth`, `background`, and `input-field` blocks, then `mv` it into place. On failure remove only the temporary file.
- [ ] **Step 5: Extend the contract for atomicity and fallback**
Seed a valid generated file, force generation failure through an unwritable fixture target, and prove its checksum is unchanged. Make fake `hyprlock` record argv and prove `run` uses the generated path after success and the tracked fallback after forced failure.
- [ ] **Step 6: Run and verify GREEN**
Run: `tests/quickshell/lock-screen-helper-contract.sh`
Expected: PASS for modes, visibility, clock format, validation, atomicity, and fallback.
- [ ] **Step 7: Commit the generator**
```bash
git add config/dot/quickshell/config/PreferenceSchema.qml config/dot/quickshell/scripts/panama-lock tests/quickshell/lock-screen-helper-contract.sh
git commit -m "Generate managed lock screen configuration"
```
### Task 2: Make every lock path use the generator
**Files:**
- Modify: `config/dot/hypr/hypridle.conf`
- Modify: `config/dot/quickshell/scripts/panama-idle`
- Create: `config/dot/quickshell/services/LockScreen.qml`
- Create: `config/dot/quickshell/lock-screen-harness.qml`
- Create: `tests/quickshell/lock-screen-service-contract.sh`
- Modify: `tests/quickshell/lock-screen-helper-contract.sh`
**Interfaces:**
- Consumes: `panama-lock generate/status` and `DesktopPreferences.revision`.
- Produces: `LockScreen.generated`, `LockScreen.path`, `LockScreen.lastError`, `LockScreen.busy`, `LockScreen.regenerate()`, and `LockScreen.refresh()`.
- [ ] **Step 1: Write the failing service and idle-path contract**
Assert both hypridle sources emit exactly:
```text
lock_cmd = pidof hyprlock || ~/.config/quickshell/scripts/panama-lock run
```
Through the QML harness, change one lock preference three times inside 250 ms and prove exactly one helper generation starts. Return malformed status JSON and prove the previous valid path remains while `lastError` becomes **The lock-screen configuration could not be read.**
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/lock-screen-service-contract.sh`
Expected: FAIL because the idle paths still invoke `hyprlock` directly and the service is absent.
- [ ] **Step 3: Update shipped and generated hypridle commands**
Change only `lock_cmd`; preserve blanking, sleep, and lock timing behavior. Keep both shipped/generated idle-command assertions in `tests/quickshell/lock-screen-service-contract.sh`; this repository has no separate idle-lock contract.
- [ ] **Step 4: Implement `LockScreen.qml`**
Use one `Process` for generation and one for status. Coalesce `DesktopPreferences.revision` through a 250 ms single-shot Timer. On generation exit, call status; accept only JSON with boolean `generated` and string `path`. Keep the last valid status on malformed output.
- [ ] **Step 5: Run and verify GREEN**
Run: `tests/quickshell/lock-screen-helper-contract.sh && tests/quickshell/lock-screen-service-contract.sh`
Expected: PASS with one coalesced generation and no live locker process.
- [ ] **Step 6: Commit lock invocation and service state**
```bash
git add config/dot/hypr/hypridle.conf config/dot/quickshell/scripts/panama-idle config/dot/quickshell/services/LockScreen.qml config/dot/quickshell/lock-screen-harness.qml tests/quickshell/lock-screen-helper-contract.sh tests/quickshell/lock-screen-service-contract.sh
git commit -m "Route session locking through Panama"
```
### Task 3: Appearance card and representative preview
**Files:**
- Create: `config/dot/quickshell/modules/settings/LockScreenPreview.qml`
- Modify: `config/dot/quickshell/modules/settings/AppearancePage.qml`
- Modify: `config/dot/quickshell/services/SettingsSearch.qml`
- Create: `tests/quickshell/lock-screen-settings-contract.sh`
- Modify: `tests/quickshell/settings-search-contract.sh`
- Modify: `config/dot/quickshell/modules/settings/README.md`
**Interfaces:**
- Consumes: schema values, `Wallpaper` effective preview path, `Theme`, and `LockScreen.lastError`.
- Produces: Appearance **Lock screen** card and search routes for background, blur, clock, date, user name, and password-field behavior.
- [ ] **Step 1: Write the failing UI and routing contract**
Construct the real Appearance page in an isolated QML harness for screenshot, wallpaper, and solid modes. Assert the preview changes background source and visibility without starting `hyprlock`. Assert the six settings route to `appearance`, and no new duplicated setting row appears on Power or Privacy.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/lock-screen-settings-contract.sh && tests/quickshell/settings-search-contract.sh && tests/quickshell/settings-ownership-contract.sh`
Expected: FAIL because the preview, card, and group route are absent.
- [ ] **Step 3: Implement the preview**
Use a clipped `Rectangle` with one asynchronous, decode-bounded `Image` only in wallpaper mode. Screenshot mode uses a static themed approximation with the configured blur level; solid uses `Theme.bg`. Render clock/date/user/password-field elements from the same preferences. Do not use ShaderEffect, live screencopy, pulse, shimmer, or a repeating animation.
- [ ] **Step 4: Add the Appearance controls**
Place the card after Wallpaper and before Typography. Use `ChoiceRow` for background, `SliderRow` with `zeroLabel: "Off"` for blur, and `ToggleRow` for the four booleans. Show `LockScreen.lastError` in the card only when non-empty.
- [ ] **Step 5: Add search and ownership documentation**
Map `lockAppearance` to `appearance`; add manual terms **lock screen background** and **password field** only if the schema labels do not already find them. Document that lock visuals belong to Appearance while lock timing belongs to Power and the existing Privacy mirror.
- [ ] **Step 6: Run and verify GREEN**
Run: `tests/quickshell/lock-screen-settings-contract.sh && tests/quickshell/settings-search-contract.sh && tests/quickshell/settings-ownership-contract.sh`
Expected: PASS with zero QML warnings and no additional mirrors.
- [ ] **Step 7: Commit the lock-screen Settings experience**
```bash
git add config/dot/quickshell/modules/settings/LockScreenPreview.qml config/dot/quickshell/modules/settings/AppearancePage.qml config/dot/quickshell/services/SettingsSearch.qml config/dot/quickshell/modules/settings/README.md tests/quickshell/lock-screen-settings-contract.sh tests/quickshell/settings-search-contract.sh
git commit -m "Add lock screen appearance settings"
```
### Task 4: Backup, reset, and diagnostics integration
**Files:**
- Modify: `config/dot/quickshell/services/SettingsBackup.qml`
- Modify: `config/dot/quickshell/scripts/panama-doctor`
- Modify: `tests/quickshell/settings-backup-live-contract.sh`
- Modify: `tests/quickshell/settings-commit-reset-contract.sh`
- Modify: `tests/quickshell/panama-doctor-contract.sh`
- Modify: `tests/quickshell/lock-screen-service-contract.sh`
**Interfaces:**
- Consumes: `LockScreen.regenerate()` and `panama-lock status`.
- Produces: lock regeneration during restore and a redacted `desktop.hyprlock` health check.
- [ ] **Step 1: Write failing restore and health assertions**
Restore a fixture snapshot with non-default lock values and assert regeneration happens after preferences reload and before shell reload. Reset and assert screenshot background, blur level 3, clock/date/user visible, and the empty password field visible, followed by one regeneration. For doctor fixtures, assert `desktop.hyprlock` is `ok` for a valid generated file, `warning` when fallback is in use, and `error` only when neither generated nor tracked config can be used. Assert no wallpaper path appears in copied diagnostics.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/settings-backup-live-contract.sh && tests/quickshell/settings-commit-reset-contract.sh && tests/quickshell/panama-doctor-contract.sh`
Expected: FAIL because restore and Health do not know the lock generator.
- [ ] **Step 3: Add restore ordering and health probe**
Inject `regenerateLock` into `SettingsBackup.qml`, start it after preference reload and idle regeneration, and include its bounded busy state in the existing settle timer. Add one authored doctor check whose parsed status includes no config contents or paths.
- [ ] **Step 4: Run and verify GREEN**
Run: `tests/quickshell/settings-backup-live-contract.sh && tests/quickshell/settings-commit-reset-contract.sh && tests/quickshell/panama-doctor-contract.sh && tests/quickshell/lock-screen-service-contract.sh`
Expected: PASS with restore ordering and redacted status.
- [ ] **Step 5: Commit integration**
```bash
git add config/dot/quickshell/services/SettingsBackup.qml config/dot/quickshell/scripts/panama-doctor tests/quickshell/settings-backup-live-contract.sh tests/quickshell/settings-commit-reset-contract.sh tests/quickshell/panama-doctor-contract.sh tests/quickshell/lock-screen-service-contract.sh
git commit -m "Integrate managed lock screen recovery"
```
### Task 5: Slice verification
- [ ] **Step 1: Run all lock contracts once**
```bash
tests/quickshell/lock-screen-helper-contract.sh
tests/quickshell/lock-screen-service-contract.sh
tests/quickshell/lock-screen-settings-contract.sh
tests/quickshell/settings-search-contract.sh
tests/quickshell/settings-ownership-contract.sh
tests/quickshell/settings-backup-live-contract.sh
tests/quickshell/settings-commit-reset-contract.sh
tests/quickshell/panama-doctor-contract.sh
```
Expected: all PASS and no process named `hyprlock` is started by the tests.
- [ ] **Step 2: Validate formatting and process safety**
Run: `bash -n config/dot/quickshell/scripts/panama-lock && bash -n config/dot/quickshell/scripts/panama-idle && git diff --check`
Expected: exit 0.
@@ -0,0 +1,327 @@
# Phase 2 Wallpaper Modes 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:** Extend Panama's thumbnail-first wallpaper picker with verified single, slideshow, and per-monitor policies.
**Architecture:** A pure policy module validates collections, calculates output maps, and owns deterministic shuffle bags. `Wallpaper.qml` remains the sole hyprpaper process boundary and persists manual changes only after `listactive` verifies every output.
**Tech Stack:** Quickshell 0.3, Qt 6 QML/JavaScript, hyprpaper 0.8.4 IPC, Bash fixture contracts
**Spec:** `docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md`
## Global Constraints
- Preserve `wallpaperPath` as the single-image and migration fallback.
- Reject non-absolute paths, commas, newlines, and UI selections outside scanned candidates.
- Automatic slideshow changes never rewrite durable policy or spin on failure.
- No filesystem watcher, animated wallpaper, per-workspace mode, or continuous repaint.
- A manual policy mutation persists only after full output-map readback succeeds.
---
### Task 1: Schema and pure wallpaper policy
**Files:**
- Modify: `config/dot/quickshell/config/PreferenceSchema.qml`
- Create: `config/dot/quickshell/services/WallpaperPolicy.js`
- Create: `config/dot/quickshell/wallpaper-policy-harness.qml`
- Create: `tests/quickshell/wallpaper-policy-contract.sh`
**Interfaces:**
- Consumes: mode, global path, collection, per-monitor map, connected outputs, valid candidates, and shuffle bag.
- Produces: `validPath`, `validCollection`, `validAssignments`, `effectiveMap`, `orderedNext`, `shuffledBag`, and `shuffledNext`.
- [ ] **Step 1: Write the failing policy contract**
Use literal paths `/images/a.jpg`, `/images/b.jpg`, and `/images/c.jpg` with connected outputs `DP-2` and `HDMI-A-1`. Assert:
```json
single -> {"DP-2":"/images/a.jpg","HDMI-A-1":"/images/a.jpg"}
per-monitor -> {"DP-2":"/images/b.jpg","HDMI-A-1":"/images/a.jpg"}
slideshow item c -> {"DP-2":"/images/c.jpg","HDMI-A-1":"/images/c.jpg"}
```
Assert missing assignments fall back to the global path, invalid collection entries are skipped, ordered rotation wraps `a -> b -> c -> a`, and a seeded shuffle emits all three literal paths exactly once before any repeat.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/wallpaper-policy-contract.sh`
Expected: FAIL because the policy module and harness are missing.
- [ ] **Step 3: Add schema entries**
Add exact keys and defaults:
```qml
{
key: "wallpaperMode", type: "enum", def: "single", group: "wallpaper",
label: "Wallpaper mode", detail: "Use one image, rotate a collection, or choose per display",
options: [
{ value: "single", label: "Single" },
{ value: "slideshow", label: "Slideshow" },
{ value: "per-monitor", label: "Per display" }
]
},
{
key: "wallpaperSlideshowPaths", type: "json", def: ([]), group: "wallpaper", internal: true,
label: "Slideshow collection", detail: "Backgrounds selected for rotation"
},
{
key: "wallpaperIntervalMinutes", type: "int", def: 30, min: 5, max: 1440, step: 5,
unit: "min", group: "wallpaper", label: "Change background every", detail: "Time between slideshow images"
},
{
key: "wallpaperShuffle", type: "bool", def: true, group: "wallpaper",
label: "Shuffle", detail: "Show every selected image before repeating"
},
{
key: "wallpaperPerMonitor", type: "json", def: ({}), group: "wallpaper", internal: true,
label: "Per-display backgrounds", detail: "Background assigned to each connected display"
}
```
Use `group: "wallpaper"` for every key. Keep the existing `wallpaperPath` pattern unchanged.
- [ ] **Step 4: Implement the pure policy API**
`validPath` accepts only strings matching `^/[^,\n]+$` that occur in the supplied candidate set. `validCollection` de-duplicates while preserving first appearance. `validAssignments` keeps only connector keys matching `^[A-Za-z0-9_.-]+$` and valid paths. `effectiveMap` returns a plain output-keyed object. Shuffle accepts an injected `random()` function so tests use the literal sequence `0.8, 0.1, 0.6` without mocking global state.
- [ ] **Step 5: Run and verify GREEN**
Run: `tests/quickshell/wallpaper-policy-contract.sh`
Expected: PASS for validation, fallback maps, ordered wrap, and shuffle-without-repeat.
- [ ] **Step 6: Commit policy model**
```bash
git add config/dot/quickshell/config/PreferenceSchema.qml config/dot/quickshell/services/WallpaperPolicy.js config/dot/quickshell/wallpaper-policy-harness.qml tests/quickshell/wallpaper-policy-contract.sh
git commit -m "Model wallpaper display policies"
```
### Task 2: Verified multi-output hyprpaper transaction
**Files:**
- Modify: `config/dot/quickshell/services/Wallpaper.qml`
- Create: `config/dot/quickshell/wallpaper-service-harness.qml`
- Create: `tests/quickshell/wallpaper-service-contract.sh`
- Modify: `tests/quickshell/settings-pages-contract.sh`
**Interfaces:**
- Consumes: `WallpaperPolicy.effectiveMap(...)` and hyprpaper `listactive` output.
- Produces: `activeByOutput`, `applyPolicy(candidatePolicy, persist)`, `applyCurrentPolicy(persist)`, `setSingle(path)`, `setAssignment(output, path)`, `toggleSlideshowPath(path)`, and `setMode(mode)`.
- [ ] **Step 1: Write the failing IPC transaction contract**
Put fake `hyprctl` first on PATH. Make it record each argv and return fixture `listactive` maps. Assert a two-output request calls exactly:
```text
hyprctl hyprpaper wallpaper DP-2,/images/a.jpg
hyprctl hyprpaper wallpaper HDMI-A-1,/images/b.jpg
hyprctl hyprpaper listactive
```
Assert preferences are written only after exact readback, wrong readback leaves the old policy untouched, and a second output failure stops the transaction and reports **Hyprpaper did not apply that background.**
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/wallpaper-service-contract.sh`
Expected: FAIL because `Wallpaper.qml` only tracks one active path and persists after exit status.
- [ ] **Step 3: Parse complete active state**
Replace `active` as the primary state with `property var activeByOutput: ({})`; keep a compatibility `active` property bound to the first current screen. Parse every `OUTPUT: /absolute/path` line. Reject malformed lines instead of accepting partial state as verification.
- [ ] **Step 4: Implement the transaction queue**
The operation record contains:
```qml
{
expected: { "DP-2": "/images/a.jpg", "HDMI-A-1": "/images/b.jpg" },
remaining: ["DP-2", "HDMI-A-1"],
candidatePolicy: { mode: "per-monitor", assignments: { "HDMI-A-1": "/images/b.jpg" } },
persist: true,
automatic: false
}
```
Apply outputs sequentially, then call `listactive`. Compare every expected key and path. Only then write the candidate schema values through `DesktopPreferences.set`. Keep the last verified map visible during work.
- [ ] **Step 5: Preserve the legacy API safely**
Keep `set(path)` as `return root.setSingle(path)` so SettingsBackup and existing IPC remain compatible until their dedicated integration task changes them. Keep title formatting and scan behavior unchanged.
- [ ] **Step 6: Run and verify GREEN**
Run: `tests/quickshell/wallpaper-service-contract.sh && tests/quickshell/settings-pages-contract.sh`
Expected: PASS for exact argv, readback gating, old API compatibility, and failure copy.
- [ ] **Step 7: Commit verified application**
```bash
git add config/dot/quickshell/services/Wallpaper.qml config/dot/quickshell/wallpaper-service-harness.qml tests/quickshell/wallpaper-service-contract.sh tests/quickshell/settings-pages-contract.sh
git commit -m "Verify wallpaper policy application"
```
### Task 3: Event-driven slideshow and hotplug settle
**Files:**
- Modify: `config/dot/quickshell/services/Wallpaper.qml`
- Modify: `config/dot/quickshell/services/WallpaperPolicy.js`
- Modify: `config/dot/quickshell/wallpaper-service-harness.qml`
- Modify: `tests/quickshell/wallpaper-policy-contract.sh`
- Modify: `tests/quickshell/wallpaper-service-contract.sh`
**Interfaces:**
- Consumes: validated slideshow collection, interval, shuffle flag, and `Quickshell.screens`.
- Produces: `advanceSlideshow()`, runtime `slideshowIndex`, `shuffleBag`, and one interval Timer active only for a collection of at least two valid images.
- [ ] **Step 1: Write failing timer and failure tests**
Use a harness-adjustable interval measured in milliseconds while production derives `minutes * 60000`. Assert: empty and one-item collections do not repeat; two items advance once per trigger; automatic application uses `persist: false`; failure keeps the same policy and schedules only the next normal interval; three rapid screen-set changes coalesce into one reapply.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/wallpaper-policy-contract.sh && tests/quickshell/wallpaper-service-contract.sh`
Expected: FAIL because no slideshow state or screen-set coalescing exists.
- [ ] **Step 3: Implement runtime rotation**
Add one repeating Timer whose `running` expression requires slideshow mode, at least two valid paths, and no active transaction. `advanceSlideshow()` chooses ordered or shuffled next state through `WallpaperPolicy`, updates runtime state only after verified apply, and never sets a preference.
- [ ] **Step 4: Implement connected-screen coalescing**
Bind a sorted screen-name signature and restart a 350 ms single-shot Timer when it changes. Reapply the current policy with `persist: false`. If a transaction is active, set one boolean follow-up flag rather than creating another queue.
- [ ] **Step 5: Run and verify GREEN**
Run: `tests/quickshell/wallpaper-policy-contract.sh && tests/quickshell/wallpaper-service-contract.sh`
Expected: PASS without rapid retry or durable writes during rotation.
- [ ] **Step 6: Commit runtime policy**
```bash
git add config/dot/quickshell/services/Wallpaper.qml config/dot/quickshell/services/WallpaperPolicy.js config/dot/quickshell/wallpaper-service-harness.qml tests/quickshell/wallpaper-policy-contract.sh tests/quickshell/wallpaper-service-contract.sh
git commit -m "Add event-driven wallpaper rotation"
```
### Task 4: Continuity wallpaper controls
**Files:**
- Create: `config/dot/quickshell/modules/settings/WallpaperControls.qml`
- Modify: `config/dot/quickshell/modules/settings/WallpaperPicker.qml`
- Modify: `config/dot/quickshell/modules/settings/AppearancePage.qml`
- Modify: `config/dot/quickshell/services/SettingsSearch.qml`
- Create: `tests/quickshell/wallpaper-settings-contract.sh`
- Modify: `tests/quickshell/settings-search-contract.sh`
**Interfaces:**
- Consumes: Wallpaper modes, connected displays, collection membership, assignments, and transaction state.
- Produces: compact mode controls above the existing grid and mode-aware tile actions/badges.
- [ ] **Step 1: Write the failing UI contract**
Render Appearance in all three modes. Assert Single tile activation calls `setSingle`; Slideshow activation toggles membership and exposes interval/shuffle; Per monitor exposes a display selector and calls `setAssignment` for the selected output. Assert the active image Prism and collection-member check are separate states.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/wallpaper-settings-contract.sh && tests/quickshell/settings-search-contract.sh`
Expected: FAIL because only single-image tile activation exists.
- [ ] **Step 3: Implement `WallpaperControls.qml`**
Use `ChoiceGrid` for mode, `ChoiceGrid` for output only in per-monitor mode, `SliderRow`-equivalent layout for interval only in slideshow mode, and `SettingsToggle` for shuffle. Controls call Wallpaper transactional methods rather than writing preferences directly.
- [ ] **Step 4: Make the picker mode-aware**
Add `selectedOutput`, `mode`, `selected(path)`, and `activate(path)` properties/functions. Keep thumbnail decode bounds and event-driven fade. In slideshow mode, draw a quiet check in the upper-right for membership; preserve the Prism border exclusively for the image actually active on the selected/current output.
- [ ] **Step 5: Integrate and route search**
Place `WallpaperControls` in the existing wallpaper card above `WallpaperPicker`. Add search terms for slideshow, shuffle interval, and per-monitor assignment without creating a new Settings page.
- [ ] **Step 6: Run and verify GREEN**
Run: `tests/quickshell/wallpaper-settings-contract.sh && tests/quickshell/settings-search-contract.sh && tests/quickshell/settings-ownership-contract.sh`
Expected: PASS with no new mirrors or QML warnings.
- [ ] **Step 7: Commit the wallpaper UI**
```bash
git add config/dot/quickshell/modules/settings/WallpaperControls.qml config/dot/quickshell/modules/settings/WallpaperPicker.qml config/dot/quickshell/modules/settings/AppearancePage.qml config/dot/quickshell/services/SettingsSearch.qml tests/quickshell/wallpaper-settings-contract.sh tests/quickshell/settings-search-contract.sh
git commit -m "Add wallpaper mode controls"
```
### Task 5: Restore and reset integration
**Files:**
- Modify: `config/dot/quickshell/services/SettingsBackup.qml`
- Modify: `tests/quickshell/settings-backup-live-contract.sh`
- Modify: `tests/quickshell/settings-commit-reset-contract.sh`
**Interfaces:**
- Consumes: `Wallpaper.applyCurrentPolicy(false)`.
- Produces: policy-aware restore and shipped single-wallpaper reset.
- [ ] **Step 1: Write failing restore assertions**
Restore slideshow and per-monitor fixture snapshots. Assert the restored policy applies after preferences reload, uses `persist: false`, and shell reload waits for the bounded wallpaper transaction. Reset must yield mode `single`, empty collection/assignments, interval 30, shuffle true, and shipped `wallpaperPath` fallback.
- [ ] **Step 2: Run and verify RED**
Run: `tests/quickshell/settings-backup-live-contract.sh && tests/quickshell/settings-commit-reset-contract.sh`
Expected: FAIL because restore calls `Wallpaper.set(path)` only.
- [ ] **Step 3: Replace path-only restore**
Inject `applyWallpaperPolicy: function() { return Wallpaper.applyCurrentPolicy(false); }`, include `Wallpaper.busy` in the existing bounded settle condition, and remove the path parameter from the restore callback. Do not create a second wallpaper snapshot format; all policy keys are already in Settings JSON.
- [ ] **Step 4: Run and verify GREEN**
Run: `tests/quickshell/settings-backup-live-contract.sh && tests/quickshell/settings-commit-reset-contract.sh && tests/quickshell/wallpaper-service-contract.sh`
Expected: PASS for both policy modes and shipped reset.
- [ ] **Step 5: Commit restore integration**
```bash
git add config/dot/quickshell/services/SettingsBackup.qml tests/quickshell/settings-backup-live-contract.sh tests/quickshell/settings-commit-reset-contract.sh
git commit -m "Restore complete wallpaper policies"
```
### Task 6: Slice verification
- [ ] **Step 1: Run all wallpaper contracts once**
```bash
tests/quickshell/wallpaper-policy-contract.sh
tests/quickshell/wallpaper-service-contract.sh
tests/quickshell/wallpaper-settings-contract.sh
tests/quickshell/settings-backup-live-contract.sh
tests/quickshell/settings-commit-reset-contract.sh
tests/quickshell/settings-search-contract.sh
```
Expected: all PASS. The live desktop wallpaper is never changed by fixture tests.
- [ ] **Step 2: Perform one read-only live comparison**
Run: `hyprctl hyprpaper listactive`
Expected: every connected output reports an absolute path; compare it with `Wallpaper.activeByOutput` through the existing IPC/harness without applying an image.
- [ ] **Step 3: Review formatting and shell state**
Run: `git diff --check && git status --short`
Expected: clean after the Task 5 commit.