diff options
Diffstat (limited to 'appearance/Wallpapers.qml')
| -rw-r--r-- | appearance/Wallpapers.qml | 243 |
1 files changed, 0 insertions, 243 deletions
diff --git a/appearance/Wallpapers.qml b/appearance/Wallpapers.qml deleted file mode 100644 index 9f43675..0000000 --- a/appearance/Wallpapers.qml +++ /dev/null @@ -1,243 +0,0 @@ -// 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 Quickshell.Io -import QtQuick - -// The wallpaper side. Setting one is `wallp`'s job: it knows about the two -// outputs, the saved state and the accent run, and none of that is worth -// reimplementing here. -Singleton { - id: root - - readonly property string home: Quickshell.env("HOME") - readonly property string dir: `${home}/Pictures/wallpapers` - // The distribution's wallpapers: loose images, and Plasma packages that - // each carry one picture at several resolutions under contents/images/, - // some with a dark version under contents/images_dark/. - readonly property string sysDir: "/usr/share/wallpapers" - - // Which screen a pick applies to. wallp calls them H and V. - property string target: "H" - - // Staged picks, empty until something is chosen. Selecting does not set - // anything: both screens can be composed and applied together, which is - // also a single wallp call rather than two. - property string pendingH: "" - property string pendingV: "" - readonly property bool dirty: pendingH !== "" || pendingV !== "" - - function pick(path) { - if (target === "V") pendingV = path; - else pendingH = path; - } - - function clearPending() { pendingH = ""; pendingV = ""; } - - // What a screen should show: the staged pick if there is one, else what - // is actually set. - function shown(which) { - if (which === "V") return pendingV !== "" ? pendingV : currentV; - return pendingH !== "" ? pendingH : currentH; - } - - property list<string> files: [] - property bool scanning: true - property bool applying: false - signal applied(bool ok, string message) - - // Sorted by most recent first: the wallpaper wanted now is usually one - // added recently. -maxdepth keeps a stray git checkout from being walked. - function refresh() { - scanning = true; - scanProc.running = false; - scanProc.running = true; - } - - Process { - id: scanProc - running: true - // img DIR "DEPTH" [ACTIONS...]: the image files under DIR. DEPTH is - // left unquoted on purpose, so "-maxdepth 2" splits into two words - // and "" vanishes. - command: ["sh", "-c", - `img() { d=$1; o=$2; shift 2; find "$d" $o -type f ` + - `\\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' \\) "$@" 2>/dev/null; }; ` + - `img ${JSON.stringify(root.dir)} "-maxdepth 2" -not -path '*/.*' -printf '%T@ %p\\n' | sort -rn | cut -d' ' -f2-; ` + - `img ${JSON.stringify(root.sysDir)} "-maxdepth 1" | sort; ` + - `img ${JSON.stringify(root.sysDir)} "" -path '*/contents/images*/*'`] - stdout: StdioCollector { - onStreamFinished: { - root.files = text.trim().split("\n").filter(s => s.length); - root.scanning = false; - sizeProc.running = false; - sizeProc.command = ["sh", "-c", 'printf "%s\\0" "$@" | xargs -0 -r file -N -0', "sh"].concat(root.files); - sizeProc.running = true; - } - } - } - - // Each file's pixel size, by path, from `file`, which reads only the - // header: about half a second for a thousand. -0 ends the name with a - // NUL, since a name can hold the ": " that would otherwise end it. The - // size is the last WxH in the description, because a JPEG's comes after - // its "density 72x72". A file `file` gives no size for has no entry and - // gets no quality colour. - property var sizes: ({}) - Process { - id: sizeProc - stdout: StdioCollector { - onStreamFinished: { - const out = {}; - for (const line of text.split("\n")) { - const nul = line.indexOf("\0"); - if (nul < 0) continue; - const re = /(\d+) ?x ?(\d+)/g; - let m, last = null; - while ((m = re.exec(line.slice(nul + 1))) !== null) last = m; - if (last) out[line.slice(0, nul)] = { w: +last[1], h: +last[2] }; - } - root.sizes = out; - } - } - } - - // The screens wallp sets, as the mock draws them: DP-1 wide, DP-3 turned - // upright. - // ponytail: fixed, like the mock; read Quickshell.screens if a monitor - // changes. - readonly property var panels: ({ H: { w: 2560, h: 1080 }, V: { w: 1080, h: 1920 } }) - - // How far swaybg's `-m fill` has to enlarge an image to cover a screen, - // 0 when the size is unknown. At or under 1 it is downscaled and sharp; - // to 1.33 the softening is slight; past that it shows pixels. - function upscale(path, which) { - const s = sizes[path]; - const p = panels[which]; - return s && s.w > 0 && s.h > 0 ? Math.max(p.w / s.w, p.h / s.h) : 0; - } - function qualityColor(path, which) { - const u = upscale(path, which); - return u === 0 ? "transparent" : u <= 1 ? Theme.green : u <= 1.33 ? Theme.yellow : Theme.red; - } - // Why an image that fits will still look bad, or "" when it will not. - function qualityNote(path, which) { - const u = upscale(path, which); - if (u <= 1) return ""; - const s = sizes[path], p = panels[which]; - return `${s.w}×${s.h} is enlarged ${u.toFixed(2)}× to cover the ${p.w}×${p.h} screen: ` - + (u <= 1.33 ? "each image pixel is spread over more than one screen pixel, so it will look slightly soft." - : "each image pixel becomes a visible block, so it will look pixelated."); - } - - function basename(path) { return path.slice(path.lastIndexOf("/") + 1); } - - // The files by the subfolder they sit in, folders by name, each keeping - // the newest-first order of `files`. Images loose in `dir` itself form a - // group named after it, which sorts first. A folder with no images at - // the scanned depth has no group. The system ones follow: its loose - // images as "system", and the Plasma packages as "Plasma", one image - // each: the dark version where the package has one, else the light, and - // of those the largest by the WxH its file is named after. `key` is what - // `expanded` is indexed by, so a local folder named "system" stays apart. - readonly property var groups: { - const local = {}, system = [], plasma = {}; - for (const f of files) { - if (f.startsWith(dir + "/")) { - const rel = f.slice(dir.length + 1); - const key = rel.includes("/") ? rel.slice(0, rel.indexOf("/")) : ""; - if (!local[key]) local[key] = []; - local[key].push(f); - } else if (f.includes("/contents/images")) { - const pkg = f.slice(sysDir.length + 1).split("/")[0]; - if (!plasma[pkg] || rank(f) > rank(plasma[pkg])) plasma[pkg] = f; - } else { - system.push(f); - } - } - const byName = (a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }); - return Object.keys(local).sort(byName) - .map(k => ({ key: "local/" + k, name: k || basename(dir), files: local[k] })) - .concat([ - { key: "system", name: "system", files: system }, - { key: "plasma", name: "Plasma", files: Object.keys(plasma).sort(byName).map(k => plasma[k]) }, - ]) - .filter(g => g.files.length > 0); - } - - // Dark outranks any light, then size decides. - function rank(path) { - const m = /(\d+)x(\d+)\.\w+$/.exec(path); - return (path.includes("/contents/images_dark/") ? 1e12 : 0) + (m ? m[1] * m[2] : 0); - } - - // What a thumbnail is called: a Plasma package's file is named after its - // resolution, so it goes by the package instead. - function label(path) { - const i = path.indexOf("/contents/images"); - return i < 0 ? basename(path) : basename(path.slice(0, i)); - } - - // Which groups are open, by key. Here rather than in the panel, which is - // destroyed on close, so a folder left open is open next time. Reassigned - // whole, because bindings do not see a key change inside a var. - property var expanded: ({}) - function toggle(key) { - const e = Object.assign({}, expanded); - e[key] = !e[key]; - expanded = e; - } - - // What is on each screen now. wallp writes these on every set, so - // watching them keeps the mock honest even when set from a terminal. - property string currentH: "" - property string currentV: "" - - FileView { - path: `${root.home}/.config/wallp/wall_h` - watchChanges: true - onFileChanged: reload() - onLoaded: root.currentH = text().trim() - } - - FileView { - path: `${root.home}/.config/wallp/wall_v` - watchChanges: true - onFileChanged: reload() - onLoaded: root.currentV = text().trim() - } - - // wallp does the work, including running udt-accent, so the accent follows - // the new wallpaper exactly as it does from the command line. Both screens - // go in one call: wallp accepts H= and V= together. - function apply() { - if (applying || !dirty) return; - applying = true; - const args = []; - if (pendingH !== "") args.push(`H=${JSON.stringify(pendingH)}`); - if (pendingV !== "") args.push(`V=${JSON.stringify(pendingV)}`); - applyProc.command = ["sh", "-c", `wallp --set ${args.join(" ")} >/dev/null 2>&1`]; - applyProc.running = true; - } - - Process { - id: applyProc - stderr: StdioCollector { id: applyErr } - onExited: code => { - root.applying = false; - if (code === 0) root.clearPending(); - root.applied(code === 0, code === 0 ? "" : (applyErr.text.trim() || `wallp exited ${code}`)); - } - } -} |
