aboutsummaryrefslogtreecommitdiffstats
path: root/appearance/Icons.qml
diff options
context:
space:
mode:
Diffstat (limited to 'appearance/Icons.qml')
-rw-r--r--appearance/Icons.qml334
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.`;
- }
-}