aboutsummaryrefslogtreecommitdiffstats
path: root/appearance/Icons.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-17 10:16:40 +0200
committerDanilo M. <danix@danix.xyz>2026-10-17 10:16:40 +0200
commit70bbc970c75221c902c515efd124c47fd702e3b7 (patch)
treed35f6b1ffbb506b65430af7fc1eca2c432879a22 /appearance/Icons.qml
parente5399f3e8d8572f1db7c4391a7c571d4845500f1 (diff)
downloadquickshell-70bbc970c75221c902c515efd124c47fd702e3b7.tar.gz
quickshell-70bbc970c75221c902c515efd124c47fd702e3b7.zip
appearance moves into the settings window
The appearance shell existed because a wallpaper picker needed more room than the 600px drawer, so it ran as its own process with a full-screen overlay. The settings window now gives it that room, and its five tabs become five settings pages in the desktop process: Wallpaper, Theme, Icons & cursor, Sunset and Idle. appearance/ is gone, and with it a process, a keepalive window and a layer blur rule. The files move to desktop/modules/appearance/ unchanged in what they do. Each tab gets a small *Module.qml; the window's refresh-on-open calls the singletons' refresh(), which the panel's show() used to. The two tabs that were inline Components in AppearancePanel become ThemeTab and WallpaperTab, owning the notices the panel printed. Sunset, Idle and Icons drop their own Flickable so the page scrolls them and search can reveal a row; the wallpaper list virtualises its rows and scrolls itself, so it takes the page's new viewHeight instead. Search finds a colour scheme and an icon or cursor theme by name. The appearance IPC target moves to desktop/shell.qml with the same verbs, so udt-steps and any keybind change only the path. Git now follows the appearance pages and autostart in the sidebar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.`;
- }
-}