// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. pragma Singleton import Quickshell import QtQuick import "../.." import "Shuffle.js" as Shuffle // The wallpaper engine: both wallpapers on a timer, from a list per screen // picked with the heart in the picker. Ticks go through Wallpapers.run like // Apply, so the burn plays, but as the light run: udt-accent leaves GRUB, the // homepage and SDDM alone. Each screen has a home, the wallpaper last chosen // for it by hand, and switching off goes back to it with a full run, so those // three match the desktop again. See the spec, // docs/superpowers/specs/2026-10-29-wallpaper-engine-design.md. Singleton { id: root readonly property var cfg: Prefs.wallEngine readonly property bool on: cfg.on function list(which) { return which === "V" ? cfg.v : cfg.h; } function listed(path, which) { return list(which).indexOf(path) >= 0; } // A gallery folder by its Wallpapers.groups key, "" for the hearted list. function folder(which) { return which === "V" ? cfg.folderV : cfg.folderH; } function setFolder(which, key) { Prefs.setWallEngine(which === "V" ? { folderV: key } : { folderH: key }); } // What a tick draws from. A folder that is no longer in the gallery // gives nothing, so that screen is left alone rather than falling back // to the list behind the user's back. function pool(which) { const key = folder(which); if (key === "") return list(which).filter(present); const g = Wallpapers.groups.find(g => g.key === key); return g ? g.files : []; } // ponytail: present means in the gallery's last scan, so a file deleted // since stays drawable until a rescan, and wallp then skips it. function present(path) { return Wallpapers.files.indexOf(path) >= 0; } function toggle(path, which) { const l = list(which).slice(); const i = l.indexOf(path); if (i >= 0) l.splice(i, 1); else l.push(path); Prefs.setWallEngine(which === "V" ? { v: l } : { h: l }); } function setOn(v) { if (v === on) return; if (v) { Prefs.setWallEngine({ on: true, homeH: Wallpapers.currentH, homeV: Wallpapers.currentV }); } else { Prefs.setWallEngine({ on: false }); goHome(); } } function setMinutes(m) { if (Number.isInteger(m) && m >= 1 && m !== cfg.minutes) Prefs.setWallEngine({ minutes: m }); } // ponytail: in memory, so a restart starts a new round. property var bagH: [] property var bagV: [] // A screen whose list is empty, or whose pick is already up, is left out. function tick() { if (Wallpapers.applying) return; const h = Shuffle.draw(pool("H"), bagH, Wallpapers.currentH); const v = Shuffle.draw(pool("V"), bagV, Wallpapers.currentV); const ph = h.pick !== Wallpapers.currentH ? h.pick : ""; const pv = v.pick !== Wallpapers.currentV ? v.pick : ""; if (Wallpapers.run(ph, pv, "tick")) { bagH = h.bag; bagV = v.bag; } } function next() { tick(); timer.restart(); } // A switch-off during a run cannot start its own; it goes when that run // finishes. property bool homeWaiting: false function goHome() { const h = cfg.homeH !== Wallpapers.currentH ? cfg.homeH : ""; const v = cfg.homeV !== Wallpapers.currentV ? cfg.homeV : ""; if (h === "" && v === "") return; homeWaiting = !Wallpapers.run(h, v, "home"); } Timer { id: timer interval: root.cfg.minutes * 60000 repeat: true running: root.on onTriggered: root.tick() } Connections { target: Wallpapers // A pick by hand is the new home, and stays up a full interval. function onManualApplied(h, v) { if (!root.on) return; Prefs.setWallEngine({ homeH: h !== "" ? h : root.cfg.homeH, homeV: v !== "" ? v : root.cfg.homeV }); timer.restart(); } function onFinished() { if (!root.homeWaiting || root.on) return; root.homeWaiting = false; root.goHome(); } } }