328 lines
16 KiB
Markdown
328 lines
16 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.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.
|