diff options
Diffstat (limited to 'appearance/Icons.qml')
| -rw-r--r-- | appearance/Icons.qml | 334 |
1 files changed, 0 insertions, 334 deletions
diff --git a/appearance/Icons.qml b/appearance/Icons.qml deleted file mode 100644 index a6eaec5..0000000 --- a/appearance/Icons.qml +++ /dev/null @@ -1,334 +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 icon and cursor themes installed on this machine. Previews are resolved -// from each theme, not just the active one: Quickshell.iconPath can only read -// the platform theme or QS_ICON_THEME, both fixed at load. -Singleton { - id: root - - readonly property string home: Quickshell.env("HOME") - readonly property string iconScript: ` -import sys -import gi -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk -SAMPLES = ["folder","text-x-generic","image-x-generic","network-wireless", - "audio-x-generic","video-x-generic","battery-full","printer"] -t = Gtk.IconTheme.new() -for name in sys.argv[1:]: - t.set_custom_theme(name) - for s in SAMPLES: - info = t.lookup_icon(s, 32, 0) - if info: - print("%s\\t%s\\t%s" % (name, s, info.get_filename())) - print("%s\\tEND\\t" % name) -` - // Four shapes per theme, so a card shows the cases that matter (arrow, - // hand, text, horizontal resize) instead of one ambiguous pointer. Each - // shape falls back through common aliases and through every storage form a - // theme may use: a hyprcursor .hlc zip, a shape directory with an SVG, or - // a legacy Xcursor binary that goes through xcur2png. - readonly property string cursorScript: ` -import sys, os, glob, shutil, zipfile, subprocess -outdir = sys.argv[1] -SAMPLES = { - "left_ptr": ["left_ptr", "default", "pointer"], - "hand2": ["hand2", "pointer", "hand"], - "xterm": ["xterm", "text", "ibeam"], - "resize": ["sb_h_double_arrow", "size_hor", "ew-resize", "h_double_arrow"], -} -roots = [os.path.expanduser("~/.icons"), - os.path.expanduser("~/.local/share/icons"), - "/usr/share/icons"] - -def pick(d, names): - for n in names: - for cand in (f"{d}/hyprcursors/{n}.hlc", f"{d}/hyprcursors/{n}/{n}.svg", - f"{d}/{n}.hlc", f"{d}/{n}/{n}.svg", f"{d}/cursors/{n}"): - if os.path.isfile(cand): - return cand - return None - -os.makedirs(outdir, exist_ok=True) -for name in sys.argv[2:]: - d = next((os.path.join(r, name) for r in roots if os.path.isdir(os.path.join(r, name))), None) - if not d: - continue - for shape, aliases in SAMPLES.items(): - c = pick(d, aliases) - if not c: - continue - out = f"{outdir}/{name}-{shape}.png" - try: - if c.endswith(".hlc"): - with zipfile.ZipFile(c) as z: - members = [e for e in z.namelist() if e.endswith(".svg")] - if not members: - members = [e for e in z.namelist() if e.endswith(".png")] - if not members: - continue - with open(out, "wb") as f: - f.write(z.read(members[0])) - elif c.endswith(".svg"): - shutil.copyfile(c, out) - else: - raw = f"{outdir}/raw-{name}-{shape}" - shutil.rmtree(raw, ignore_errors=True) - os.makedirs(raw, exist_ok=True) - subprocess.run(["xcur2png", "-d", raw, "-c", f"{raw}/out.conf", c], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - pngs = sorted(glob.glob(f"{raw}/*_*.png")) - if not pngs: - continue - shutil.copyfile(pngs[-1], out) - print(f"{name}\\t{shape}\\t{out}") - except Exception: - continue -` - - property var iconThemes: [] - property var cursorThemes: [] - property string currentIcon: "" - property string currentCursor: "" - property int cursorSize: 24 - property var iconPreview: ({}) - property var cursorPreview: ({}) - property bool scanning: true - property string notice: "" - - // Pure: classify works on entries shaped { name, index, cursors, manifest } - // so it can be tested without touching the filesystem. - function classify(entries) { - const icons = [], cursors = []; - for (const e of entries) { - if (e.cursors || e.manifest) cursors.push(e.name); - if (e.index && /Directories=\S/.test(e.index)) icons.push(e.name); - } - const uniq = a => a.filter((v, i) => a.indexOf(v) === i).sort(); - return { icons: uniq(icons), cursors: uniq(cursors) }; - } - - function selftest(): string { - const entries = [ - { name: "Material-Black-Plum-Suru", index: "Directories=32x32/apps\n", cursors: false, manifest: false }, - { name: "hypr_bibata-modern-amber", index: "", cursors: false, manifest: true }, - { name: "default", index: "Inherits=Bibata-Modern-Amber\n", cursors: false, manifest: false }, - { name: "breeze_cursors", index: "", cursors: true, manifest: false }, - { name: "Adwaita", index: "Directories=16x16/apps\n", cursors: true, manifest: false }, - ]; - const r = root.classify(entries); - if (r.icons.length !== 2 || r.icons.indexOf("Adwaita") < 0 || - r.icons.indexOf("Material-Black-Plum-Suru") < 0) - return `SELFTEST Icons FAIL: icons ${JSON.stringify(r.icons)}`; - if (r.cursors.length !== 3 || r.cursors.indexOf("hypr_bibata-modern-amber") < 0 || - r.cursors.indexOf("breeze_cursors") < 0 || r.cursors.indexOf("Adwaita") < 0) - return `SELFTEST Icons FAIL: cursors ${JSON.stringify(r.cursors)}`; - return "SELFTEST Icons PASS"; - } - - function refresh() { - scanProc.running = false; - scanProc.running = true; - curProc.running = false; - curProc.running = true; - } - - // One shell pass over every theme root. Format per line: - // name|hasIndex|cursors|manifest|Directories= - Process { - id: scanProc - command: ["sh", "-c", - `for d in ${root.home}/.icons/* ${root.home}/.local/share/icons/* ` + - `/usr/share/icons/*; do [ -d "$d" ] || continue; ` + - `[ -L "$d" ] && continue; ` + - `i=0; c=0; m=0; dirs=""; ` + - `[ -d "$d/cursors" ] && c=1; ` + - `[ -f "$d/manifest.hl" ] && m=1; ` + - `if [ -f "$d/index.theme" ]; then i=1; dirs=$(sed -n 's/^Directories=//p' "$d/index.theme"); fi; ` + - `echo "$(basename "$d")|$i|$c|$m|$dirs"; done`] - stdout: StdioCollector { - onStreamFinished: { - const entries = []; - for (const line of text.trim().split("\n")) { - const p = line.split("|"); - if (p.length < 5) continue; - entries.push({ name: p[0], index: p[1] === "1" ? "Directories=" + (p[4] || "x") : "", - cursors: p[2] === "1", manifest: p[3] === "1" }); - } - const r = root.classify(entries); - root.iconThemes = r.icons; - root.cursorThemes = r.cursors; - root.scanning = false; - if (root.iconThemes.length) root.previewIcons(); - if (root.cursorThemes.length) root.previewCursors(); - } - } - } - - // One python process for every icon theme, so opening the tab costs one - // spawn, not one per theme. - function previewIcons() { - if (!root.iconThemes.length) return; - iconProc.command = ["python3", "-c", root.iconScript].concat(root.iconThemes); - iconProc.running = false; - iconProc.running = true; - } - Process { - id: iconProc - stdout: StdioCollector { - onStreamFinished: { - const next = {}; - for (const line of text.split("\n")) { - const p = line.split("\t"); - if (p.length < 2 || p[1] === "END" || !p[2]) continue; - if (!next[p[0]]) next[p[0]] = {}; - next[p[0]][p[1]] = p[2]; - } - root.iconPreview = next; - } - } - } - - // gsettings for the current values. - Process { - id: curProc - command: ["sh", "-c", - `gsettings get org.gnome.desktop.interface icon-theme; ` + - // The system's cursor theme is often the `default` alias, a symlink - // to the real theme. The scan skips symlinked dirs, so resolve the - // current value to the target name or nothing would read as current. - `ct=$(gsettings get org.gnome.desktop.interface cursor-theme | sed "s/'//g"); ` + - `for r in ${root.home}/.icons ${root.home}/.local/share/icons /usr/share/icons; do ` + - `[ -L "$r/$ct" ] && ct=$(basename "$(readlink -f "$r/$ct")") && break; done; ` + - `printf '%s\\n' "$ct"; ` + - `gsettings get org.gnome.desktop.interface cursor-size`] - stdout: StdioCollector { - onStreamFinished: { - const lines = text.trim().split("\n"); - root.currentIcon = (lines[0] ?? "").replace(/'/g, ""); - root.currentCursor = (lines[1] ?? "").replace(/'/g, ""); - const size = parseInt(lines[2] ?? "", 10); - if (!isNaN(size)) root.cursorSize = size; - } - } - } - - // One python pass extracts four shapes for EVERY cursor theme and prints - // name<TAB>shape<TAB>path, so the whole row paints on open instead of - // waiting for a hover per card. A theme missing a shape simply shows fewer - // images rather than a broken one. - function previewCursors() { - if (!root.cursorThemes.length) return; - cursorProc.command = ["python3", "-c", root.cursorScript, - Quickshell.cachePath("cursors")].concat(root.cursorThemes); - cursorProc.running = false; - cursorProc.running = true; - } - Process { - id: cursorProc - stdout: StdioCollector { - onStreamFinished: { - const next = {}; - for (const line of text.split("\n")) { - const p = line.split("\t"); - if (p.length < 3 || !p[0] || !p[1] || !p[2]) continue; - if (!next[p[0]]) next[p[0]] = {}; - next[p[0]][p[1]] = p[2]; - } - root.cursorPreview = next; - } - } - } - - // Writes go through FileView so no shell quoting is involved. They are - // preloaded because setText on an unloaded FileView would write empty. - FileView { id: qt6File; path: `${root.home}/.config/qt6ct/qt6ct.conf`; blockLoading: true } - FileView { id: qt5File; path: `${root.home}/.config/qt5ct/qt5ct.conf`; blockLoading: true } - // The cursor lives in its own file, not in hypr-theme's environment.lua: - // which cursor a machine runs is local state, and editing a tracked file - // left hypr-theme dirty after every switch. environment.lua dofile()s it. - // Written whole, so it needs no preload. - FileView { id: cursorFile; path: `${root.home}/.config/udt/cursor.lua` } - function writeCursor() { - cursorFile.setText(`return { theme = ${JSON.stringify(root.currentCursor)}, size = ${root.cursorSize} }\n`); - } - // GTK3 and GTK4 read their own settings.ini, with the theme names written - // out; gsettings alone does not switch them. - FileView { id: gtk3File; path: `${root.home}/.config/gtk-3.0/settings.ini`; blockLoading: true } - FileView { id: gtk4File; path: `${root.home}/.config/gtk-4.0/settings.ini`; blockLoading: true } - - // Replaces a `key=value` line in both GTK settings files. A key the file - // does not carry is left alone, the same as the Qt configs. - function rewriteGtk(key, value) { - const files = [gtk3File, gtk4File]; - const re = new RegExp(`^${key}=.*`, "m"); - for (let i = 0; i < files.length; i++) { - const text = files[i].text(); - if (text !== "") files[i].setText(text.replace(re, `${key}=${value}`)); - } - } - - function gsettingsSet(key, value) { - gsetProc.command = ["gsettings", "set", "org.gnome.desktop.interface", key, value]; - gsetProc.running = false; - gsetProc.running = true; - } - Process { id: gsetProc } - - // The SDDM power icons resolve icon names from gsettings when udt-accent - // generates them, not when they run, so they are regenerated here, - // chained after the gsettings write they read. The island resolves its - // icons live and needs nothing. - readonly property string udtRepo: `${root.home}/Programming/GIT/unified-desktop-theme` - - function applyIcon(name) { - Quickshell.execDetached(["sh", "-c", - `gsettings set org.gnome.desktop.interface icon-theme "$1" || exit; ` + - `[ -e ~/.cache/udt/wpaper ] && ${JSON.stringify(root.udtRepo)}/bin/udt-accent ~/.cache/udt/wpaper`, - "sh", name]); - const files = [qt6File, qt5File]; - for (let i = 0; i < files.length; i++) { - const text = files[i].text(); - if (text !== "") files[i].setText(text.replace(/^icon_theme=.*/m, `icon_theme=${name}`)); - } - root.rewriteGtk("gtk-icon-theme-name", name); - root.currentIcon = name; - root.notice = `${name} set. Waybar and the SDDM icons follow; restart other apps to see it.`; - } - - function applyCursor(name) { - // Live switch first, then persistence. - Quickshell.execDetached(["hyprctl", "setcursor", name, String(root.cursorSize)]); - root.gsettingsSet("cursor-theme", name); - root.currentCursor = name; - root.writeCursor(); - root.rewriteGtk("gtk-cursor-theme-name", name); - root.rewriteGtk("gtk-cursor-theme-size", root.cursorSize); - root.notice = `${name} set live at ${root.cursorSize}px. cursor.lua updated for next login.`; - } - - function setCursorSize(size) { - root.cursorSize = size; - // Live on the current theme, then GTK and the next login's env. - Quickshell.execDetached(["hyprctl", "setcursor", root.currentCursor, String(size)]); - root.gsettingsSet("cursor-size", String(size)); - root.writeCursor(); - root.rewriteGtk("gtk-cursor-theme-size", size); - root.notice = `cursor size ${size}px. cursor.lua updated for next login.`; - } -} |
