1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// 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();
}
}
}
|