16 KiB
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
wallpaperPathas 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, andshuffledNext. -
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:
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:
{
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
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 hyprpaperlistactiveoutput. -
Produces:
activeByOutput,applyPolicy(candidatePolicy, persist),applyCurrentPolicy(persist),setSingle(path),setAssignment(output, path),toggleSlideshowPath(path), andsetMode(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:
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:
{
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
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(), runtimeslideshowIndex,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
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
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
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
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.