Files
Panama/config/dot/quickshell/services/Wallpaper.qml
T

174 lines
6.4 KiB
QML

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 <output>,<path>` 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 "<output>,<path>", 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")
readonly property string shippedPath: `${Quickshell.env("HOME")}/Pictures/Wallpapers/faroe_islands.jpg`
// 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 "<empty>,<path>" 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 string storedValue: ""
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.storedValue);
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 {
const effectivePath = path === "" ? root.shippedPath : path;
if (PreferenceSchema.coerce("wallpaperPath", effectivePath) === undefined) {
root.lastError = "That file path cannot be used as a wallpaper.";
return false;
}
if (apply.running)
return false;
apply.requested = effectivePath;
apply.storedValue = path;
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]},${effectivePath}`]);
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, " ");
}
}