Files
Panama/docs/superpowers/plans/2026-08-18-phase2-wallpaper-modes.md
T
Gabriel Brown e1faaf7a76 Drop the extension, and give the test suite a front door
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
2026-08-20 21:55:55 -04:00

328 lines
15 KiB
Markdown

# 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`
**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`
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`
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
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`
- Modify: `tests/quickshell/settings-pages-contract`
**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`
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 && tests/quickshell/settings-pages-contract`
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 tests/quickshell/settings-pages-contract
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`
- Modify: `tests/quickshell/wallpaper-service-contract`
**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 && tests/quickshell/wallpaper-service-contract`
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 && tests/quickshell/wallpaper-service-contract`
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 tests/quickshell/wallpaper-service-contract
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`
- Modify: `tests/quickshell/settings-search-contract`
**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 && tests/quickshell/settings-search-contract`
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 && tests/quickshell/settings-search-contract && tests/quickshell/settings-ownership-contract`
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 tests/quickshell/settings-search-contract
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`
- Modify: `tests/quickshell/settings-commit-reset-contract`
**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 && tests/quickshell/settings-commit-reset-contract`
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 && tests/quickshell/settings-commit-reset-contract && tests/quickshell/wallpaper-service-contract`
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 tests/quickshell/settings-commit-reset-contract
git commit -m "Restore complete wallpaper policies"
```
### Task 6: Slice verification
- [ ] **Step 1: Run all wallpaper contracts once**
```bash
tests/quickshell/wallpaper-policy-contract
tests/quickshell/wallpaper-service-contract
tests/quickshell/wallpaper-settings-contract
tests/quickshell/settings-backup-live-contract
tests/quickshell/settings-commit-reset-contract
tests/quickshell/settings-search-contract
```
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.