# 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.