pragma Singleton // The desktop background. // // hyprpaper owns the actual painting; this owns choosing. Two things are worth // knowing about hyprpaper 0.8: // // * Its IPC is much smaller than the documentation for older versions // suggests. `wallpaper ,` and `listactive` work; `preload`, // `listloaded`, `unload`, and `reload` all answer "invalid hyprpaper // request". So there is no preload step -- setting is a single call. // * hyprpaper.conf lives in the Panama repo via the ~/.config/hypr symlink, // so it cannot be rewritten at runtime without dirtying a tracked file. // The chosen wallpaper therefore lives in the shared settings store like // every other preference, and is re-applied when the shell starts. // // The argument is ",", so a path containing a comma would be // parsed as a different request. The schema's pattern rejects those, and the // value is passed as a single argv element rather than through a shell. import Quickshell import Quickshell.Io import QtQuick import qs.config Singleton { id: root // Absolute paths of candidate images, newest first. property var available: [] property string active: "" property string lastError: "" property bool scanning: false readonly property string configured: DesktopPreferences.get("wallpaperPath") // Directories searched for wallpapers, in order. Screenshots are // deliberately excluded: a folder of 300 screenshots is not a wallpaper // picker, and including it made the grid useless on this machine. 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 // -print0 would be safer against odd filenames, but the schema already // rejects paths containing commas or newlines, and this list is only // ever offered as candidates -- the value that gets stored is validated // again on the way in. 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: { const paths = this.text.split("\n").map(line => line.trim()).filter(line => line.length > 0); root.available = paths; root.scanning = false; } } } Process { id: activeQuery command: ["hyprctl", "hyprpaper", "listactive"] stdout: StdioCollector { onStreamFinished: { // "DP-2: /path/to/image.jpg", one line per output. const first = this.text.split("\n").find(line => line.indexOf(":") > 0); root.active = first ? first.slice(first.indexOf(":") + 1).trim() : ""; } } } // hyprpaper requires an explicit output name: the "," form that // older versions accepted as "all outputs" is silently ignored by 0.8, so a // wallpaper set that way appears to succeed and never changes. Outputs are // therefore walked one at a time. Process { id: apply property string requested: "" property var remaining: [] onExited: (exitCode, exitStatus) => { if (exitCode !== 0) { root.lastError = "hyprpaper could not load that image."; apply.remaining = []; return; } if (apply.remaining.length > 0) { const next = apply.remaining[0]; apply.remaining = apply.remaining.slice(1); apply.exec(["hyprctl", "hyprpaper", "wallpaper", `${next},${apply.requested}`]); return; } root.lastError = ""; DesktopPreferences.set("wallpaperPath", apply.requested); root.refreshActive(); } } Component.onCompleted: { root.rescan(); root.refreshActive(); restore.restart(); } // hyprpaper is started by the compositor's autostart, so it may not be // listening yet when the shell comes up. Re-applying the stored choice // after a short delay makes the wallpaper survive a reboot without needing // hyprpaper.conf to know about it. Timer { id: restore interval: 1500 onTriggered: { const stored = root.configured; if (stored !== "" && stored !== root.active) root.set(stored); } } function rescan(): void { if (scan.running) return; root.scanning = true; scan.running = true; } function refreshActive(): void { if (!activeQuery.running) activeQuery.running = true; } // Applies to every connected output. Returns false when the path is not one // the schema will accept, so a caller can report the refusal. function set(path: string): bool { if (PreferenceSchema.coerce("wallpaperPath", path) === undefined) { root.lastError = "That file path cannot be used as a wallpaper."; return false; } if (apply.running) return false; apply.requested = path; // "" clears the preference without touching what is on screen. if (path === "") { DesktopPreferences.set("wallpaperPath", ""); return true; } const outputs = Quickshell.screens.map(screen => screen.name).filter(name => !!name); if (outputs.length === 0) { root.lastError = "No display to set a wallpaper on."; return false; } apply.remaining = outputs.slice(1); apply.exec(["hyprctl", "hyprpaper", "wallpaper", `${outputs[0]},${path}`]); return true; } // The display name for a path: the file's own name, without extension, // with separators turned into spaces. function titleFor(path: string): string { const file = String(path).split("/").pop(); return file.replace(/\.[^.]+$/, "").replace(/[_-]+/g, " "); } }