| 1 | pragma Singleton |
| 2 | import QtQuick |
| 3 | import Quickshell |
| 4 | import Quickshell.Io |
| 5 | |
| 6 | // Wallpapers: folder listing, cached thumbnails, and applying them. aurora owns the wallpaper: |
| 7 | // apply() writes hyprpaper.conf, switches hyprpaper live, runs matugen (all colours, terminal included; Theme then |
| 8 | // recolours itself). recolor() re-runs just matugen when the scheme/contrast setting changes. |
| 9 | // ~/.local/bin/setwall is only a CLI front-end for the IPC calls (wallrandom / wall / wallset). |
| 10 | Singleton { |
| 11 | id: root |
| 12 | |
| 13 | readonly property string home: Quickshell.env("HOME") |
| 14 | property string rootDir: home + "/pix/wallpapers" |
| 15 | property string folder: Settings.wallFolder // default dusky-dark |
| 16 | readonly property string conf: home + "/.config/hypr/hyprpaper.conf" |
| 17 | readonly property string thumbDir: home + "/.cache/aurora/thumbs" |
| 18 | |
| 19 | property var folders: [] // subfolder names; "" = everything under rootDir |
| 20 | property var files: [] // absolute paths of the current folder |
| 21 | property var ready: ({}) // md5(path) -> true once its thumbnail exists |
| 22 | property string current: "" // wallpaper hyprpaper is showing |
| 23 | property bool loading: false |
| 24 | property bool pendingRandom: false // random() asked before the folder was listed |
| 25 | property bool checkedColours: false // matugen output checked once at startup |
| 26 | |
| 27 | function dirOf(name) { return name === "" ? rootDir : rootDir + "/" + name } |
| 28 | function thumbOf(path) { return thumbDir + "/" + Qt.md5(path) + ".jpg" } |
| 29 | |
| 30 | function refresh() { |
| 31 | dirsProc.running = true |
| 32 | listFolder(folder) |
| 33 | } |
| 34 | function listFolder(name) { |
| 35 | folder = name |
| 36 | Settings.wallFolder = name |
| 37 | thumbProc.running = false |
| 38 | listProc.running = false |
| 39 | loading = true |
| 40 | listProc.command = ["find", dirOf(name), "-type", "f", "-not", "-path", "*/.*", |
| 41 | "-regextype", "posix-extended", "-iregex", ".*\\.(png|jpe?g|webp)"] |
| 42 | listProc.running = true |
| 43 | } |
| 44 | // hyprpaper.conf is the source of truth for the current wallpaper (hyprpaper reads it on start). |
| 45 | // If hyprpaper isn't running, let Hyprland start it so it doesn't live in aurora's service cgroup. |
| 46 | readonly property string applyJob: |
| 47 | 'p="$1"; c="$2"; ' + |
| 48 | 'printf "splash = false\\nwallpaper {\\n monitor =\\n path = %s\\n}\\n" "$p" > "$c"; ' + |
| 49 | 'if pgrep -x hyprpaper >/dev/null; then hyprctl hyprpaper wallpaper ",$p"; ' + |
| 50 | 'else hyprctl dispatch \'hl.dsp.exec_cmd("hyprpaper")\'; fi; ' + |
| 51 | 'matugen image "$p" -m dark -t "$3" --contrast="$4" --prefer saturation -q' |
| 52 | function apply(path) { |
| 53 | if (!path) return |
| 54 | current = path |
| 55 | Quickshell.execDetached(["sh", "-c", applyJob, "sh", path, conf, Settings.matuScheme, String(Settings.matuContrast)]) |
| 56 | } |
| 57 | |
| 58 | // matugen only. --prefer: v4 otherwise asks on stdin when the image has several source colours. |
| 59 | // With "missing", only if there is no output yet (fresh install). |
| 60 | readonly property string matuJob: |
| 61 | '[ "$4" = missing ] && [ -f "$HOME/.cache/matugen/aurora.json" ] && exit 0; ' + |
| 62 | 'matugen image "$1" -m dark -t "$2" --contrast="$3" --prefer saturation -q' |
| 63 | function recolor(onlyIfMissing) { |
| 64 | if (!current) return |
| 65 | Quickshell.execDetached(["sh", "-c", matuJob, "sh", current, Settings.matuScheme, |
| 66 | String(Settings.matuContrast), onlyIfMissing ? "missing" : ""]) |
| 67 | } |
| 68 | Timer { id: recolorLater; interval: 400; onTriggered: root.recolor(false) } |
| 69 | Connections { |
| 70 | target: Settings |
| 71 | function onMatuSchemeChanged() { recolorLater.restart() } |
| 72 | function onMatuContrastChanged() { recolorLater.restart() } |
| 73 | } |
| 74 | // random from the picker's folder (Settings.wallFolder), never the one already shown |
| 75 | function random() { |
| 76 | if (!files.length) { |
| 77 | pendingRandom = true |
| 78 | if (!loading) listFolder(folder) |
| 79 | return |
| 80 | } |
| 81 | var pick = files.filter(f => f !== current) |
| 82 | if (!pick.length) pick = files |
| 83 | apply(pick[Math.floor(Math.random() * pick.length)]) |
| 84 | } |
| 85 | |
| 86 | Process { |
| 87 | id: dirsProc |
| 88 | command: ["find", root.rootDir, "-mindepth", "1", "-maxdepth", "1", "-type", "d", "-not", "-name", ".*"] |
| 89 | stdout: StdioCollector { |
| 90 | onStreamFinished: { |
| 91 | var out = [], l = text.split("\n") |
| 92 | for (var i = 0; i < l.length; i++) { |
| 93 | var s = l[i].trim() |
| 94 | if (s !== "") out.push(s.substring(s.lastIndexOf("/") + 1)) |
| 95 | } |
| 96 | out.sort() |
| 97 | root.folders = out |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | Process { |
| 103 | id: listProc |
| 104 | stdout: StdioCollector { |
| 105 | onStreamFinished: { |
| 106 | var out = [], l = text.split("\n") |
| 107 | for (var i = 0; i < l.length; i++) if (l[i] !== "") out.push(l[i]) |
| 108 | out.sort() |
| 109 | root.files = out |
| 110 | root.loading = false |
| 111 | if (root.pendingRandom) { root.pendingRandom = false; if (out.length) root.random() } |
| 112 | root.makeThumbs() |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | // ---- thumbnails ---- |
| 118 | // one background job per folder: 4 parallel `magick` runs, prints each hash when its thumb exists |
| 119 | readonly property string thumbJob: |
| 120 | 'f="$1"; h=$(printf %s "$f" | md5sum | cut -c1-32); t="$0/$h.jpg"; ' + |
| 121 | 'if [ ! -f "$t" ]; then ' + |
| 122 | ' magick "$f[0]" -thumbnail 320x180^ -gravity center -extent 320x180 -quality 82 "$t.part.jpg" 2>/dev/null ' + |
| 123 | ' && mv "$t.part.jpg" "$t"; ' + |
| 124 | 'fi; [ -f "$t" ] && echo "$h"' |
| 125 | function makeThumbs() { |
| 126 | if (!files.length) return |
| 127 | thumbProc.command = ["sh", "-c", |
| 128 | 'j="$1"; d="$2"; shift 2; mkdir -p "$d"; printf "%s\\0" "$@" | xargs -0 -n1 -P4 sh -c "$j" "$d"', |
| 129 | "sh", thumbJob, thumbDir].concat(files) |
| 130 | thumbProc.running = true |
| 131 | } |
| 132 | property var pendingReady: ({}) |
| 133 | Timer { |
| 134 | id: flush |
| 135 | interval: 250 |
| 136 | onTriggered: { |
| 137 | var r = root.ready |
| 138 | for (var k in root.pendingReady) r[k] = true |
| 139 | root.pendingReady = {} |
| 140 | root.ready = r |
| 141 | root.readyChanged() |
| 142 | } |
| 143 | } |
| 144 | Process { |
| 145 | id: thumbProc |
| 146 | stdout: SplitParser { |
| 147 | onRead: line => { |
| 148 | root.pendingReady[line.trim()] = true |
| 149 | if (!flush.running) flush.start() |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | // current wallpaper, straight from hyprpaper.conf. None yet (fresh install): pick a random one. |
| 155 | FileView { |
| 156 | path: root.conf |
| 157 | watchChanges: true |
| 158 | onFileChanged: reload() |
| 159 | onLoadFailed: root.random() |
| 160 | onLoaded: { |
| 161 | var m = text().match(/^\s*path\s*=\s*(.+)$/m) |
| 162 | if (m) root.current = m[1].trim() |
| 163 | if (!root.checkedColours) { root.checkedColours = true; root.recolor(true) } |
| 164 | } |
| 165 | } |
| 166 | } |