pragma Singleton // Verified wallpaper policy application. hyprpaper 0.8 applies one output per // IPC call, so Panama queues every connected output and persists a policy only // after listactive confirms the complete map. import Quickshell import Quickshell.Io import QtQuick import "WallpaperPolicy.js" as WallpaperPolicy import qs.config Singleton { id: root property var available: [] property var activeByOutput: ({}) property string lastError: "" property bool scanning: false property var transaction: null property bool startupRestoreEnabled: true readonly property string configured: DesktopPreferences.get("wallpaperPath") readonly property string shippedPath: `${Quickshell.env("HOME")}/Pictures/Wallpapers/faroe_islands.jpg` readonly property string active: { const outputs = root.outputNames(); if (outputs.length > 0 && root.activeByOutput[outputs[0]]) return root.activeByOutput[outputs[0]]; const paths = Object.values(root.activeByOutput); return paths.length > 0 ? paths[0] : ""; } readonly property bool busy: root.transaction !== null || applyProcess.running || verifyProcess.running property var outputNames: function() { return Quickshell.screens.map(screen => screen.name).filter(name => !!name); } readonly property var searchRoots: [ `${Quickshell.env("HOME")}/Pictures/Wallpapers`, `${Quickshell.env("HOME")}/Pictures/Backgrounds`, `${Quickshell.env("HOME")}/.local/share/backgrounds`, "/usr/share/backgrounds" ] Process { id: scan command: ["bash", "-lc", "find " + root.searchRoots.map(dir => `'${dir}'`).join(" ") + " -maxdepth 2 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\)" + " -printf '%T@ %p\\n' 2>/dev/null | sort -rn | cut -d' ' -f2- | head -60"] stdout: StdioCollector { onStreamFinished: { root.available = this.text.split("\n") .map(line => line.trim()).filter(line => line.length > 0); root.scanning = false; } } } Process { id: activeQuery command: ["hyprctl", "hyprpaper", "listactive"] stdout: StdioCollector { onStreamFinished: { const parsed = root.parseActive(this.text); if (parsed !== null) root.activeByOutput = parsed; } } } Process { id: applyProcess onExited: (exitCode, exitStatus) => { if (root.transaction === null) return; if (exitCode !== 0) { root.lastError = "Hyprpaper did not apply that background."; root.transaction = null; return; } root.drainTransaction(); } } Process { id: verifyProcess property string outputText: "" onStarted: outputText = "" stdout: StdioCollector { onStreamFinished: verifyProcess.outputText = this.text } onExited: (exitCode, exitStatus) => root.finishVerification(exitCode) } Component.onCompleted: { root.rescan(); root.refreshActive(); restore.restart(); } Timer { id: restore interval: 1500 onTriggered: { if (root.startupRestoreEnabled) root.applyCurrentPolicy(false); } } function rescan(): void { if (scan.running) return; root.scanning = true; scan.running = true; } function refreshActive(): void { if (!activeQuery.running && !root.busy) activeQuery.running = true; } function candidates(extra: var): var { const result = []; for (const path of root.available.concat([root.shippedPath, root.configured]).concat(extra || [])) { if (typeof path === "string" && /^\/[^,\n]+$/.test(path) && !result.includes(path)) result.push(path); } return result; } function currentPolicy(): var { return { mode: DesktopPreferences.get("wallpaperMode") ?? "single", globalPath: root.configured === "" ? root.shippedPath : root.configured, collection: DesktopPreferences.get("wallpaperSlideshowPaths") ?? [], intervalMinutes: DesktopPreferences.get("wallpaperIntervalMinutes") ?? 30, shuffle: DesktopPreferences.get("wallpaperShuffle") !== false, assignments: DesktopPreferences.get("wallpaperPerMonitor") ?? ({}), slideshowPath: root.active }; } function normalisePolicy(policy: var): var { if (!policy || typeof policy !== "object") return null; const mode = ["single", "slideshow", "per-monitor"].includes(policy.mode) ? policy.mode : "single"; const rawGlobal = policy.globalPath === "" ? root.shippedPath : policy.globalPath; const candidatePaths = root.candidates([rawGlobal] .concat(policy.collection || []) .concat(Object.values(policy.assignments || {})) .concat([policy.slideshowPath || ""])); if (!WallpaperPolicy.validPath(rawGlobal, candidatePaths)) return null; return { mode, globalPath: rawGlobal, storedPath: rawGlobal === root.shippedPath ? "" : rawGlobal, collection: WallpaperPolicy.validCollection(policy.collection || [], candidatePaths), intervalMinutes: Math.max(5, Math.min(1440, Number(policy.intervalMinutes) || 30)), shuffle: policy.shuffle !== false, assignments: WallpaperPolicy.validAssignments(policy.assignments || {}, candidatePaths), slideshowPath: WallpaperPolicy.validPath(policy.slideshowPath, candidatePaths) ? policy.slideshowPath : rawGlobal, candidates: candidatePaths }; } function applyPolicy(policy: var, persist: bool, automatic: bool): bool { if (root.busy) return false; const normalised = root.normalisePolicy(policy); if (normalised === null) { root.lastError = "That wallpaper policy is not valid."; return false; } const outputs = root.outputNames(); if (outputs.length === 0) { root.lastError = "No display to set a wallpaper on."; return false; } const expected = WallpaperPolicy.effectiveMap( normalised.mode, normalised.globalPath, normalised.slideshowPath, normalised.assignments, outputs, normalised.candidates); if (Object.keys(expected).length !== outputs.length || Object.values(expected).some(path => path === "")) { root.lastError = "That wallpaper policy is not valid."; return false; } root.lastError = ""; root.transaction = { expected, remaining: outputs.slice(), policy: normalised, persist: persist === true, automatic: automatic === true }; root.drainTransaction(); return true; } function drainTransaction(): void { if (root.transaction === null || applyProcess.running || verifyProcess.running) return; if (root.transaction.remaining.length === 0) { verifyProcess.exec(["hyprctl", "hyprpaper", "listactive"]); return; } const output = root.transaction.remaining[0]; root.transaction.remaining = root.transaction.remaining.slice(1); applyProcess.exec([ "hyprctl", "hyprpaper", "wallpaper", `${output},${root.transaction.expected[output]}` ]); } function parseActive(text: string): var { const result = {}; const lines = String(text).split("\n").map(line => line.trim()).filter(line => line !== ""); for (const line of lines) { const match = line.match(/^([A-Za-z0-9_.-]+): (\/[^,\n]+)$/); if (!match || result[match[1]] !== undefined) return null; result[match[1]] = match[2]; } return result; } function finishVerification(exitCode: int): void { if (root.transaction === null) return; const observed = exitCode === 0 ? root.parseActive(verifyProcess.outputText) : null; const expected = root.transaction.expected; const matches = observed !== null && Object.keys(observed).length === Object.keys(expected).length && Object.keys(expected).every(output => observed[output] === expected[output]); if (!matches) { root.lastError = "Hyprpaper did not confirm that background."; root.transaction = null; return; } const completed = root.transaction; root.activeByOutput = observed; root.transaction = null; root.lastError = ""; if (completed.persist) root.persistPolicy(completed.policy); } function persistPolicy(policy: var): void { DesktopPreferences.set("wallpaperMode", policy.mode); DesktopPreferences.set("wallpaperPath", policy.storedPath); DesktopPreferences.set("wallpaperSlideshowPaths", policy.collection); DesktopPreferences.set("wallpaperIntervalMinutes", policy.intervalMinutes); DesktopPreferences.set("wallpaperShuffle", policy.shuffle); DesktopPreferences.set("wallpaperPerMonitor", policy.assignments); } function applyCurrentPolicy(persist: bool): bool { return root.applyPolicy(root.currentPolicy(), persist === true, false); } function setSingle(path: string): bool { const effectivePath = path === "" ? root.shippedPath : path; const allowed = root.candidates([]); if (!WallpaperPolicy.validPath(effectivePath, allowed)) { root.lastError = "That file path cannot be used as a wallpaper."; return false; } const policy = root.currentPolicy(); policy.mode = "single"; policy.globalPath = effectivePath; policy.slideshowPath = effectivePath; return root.applyPolicy(policy, true, false); } // Compatibility boundary used by backup/reset and the existing picker. function set(path: string): bool { return root.setSingle(path); } function titleFor(path: string): string { const file = String(path).split("/").pop(); return file.replace(/\.[^.]+$/, "").replace(/[_-]+/g, " "); } }