From 70bbc970c75221c902c515efd124c47fd702e3b7 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 17 Oct 2026 10:16:40 +0200 Subject: 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 --- desktop/modules/appearance/Udt.qml | 153 +++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 desktop/modules/appearance/Udt.qml (limited to 'desktop/modules/appearance/Udt.qml') diff --git a/desktop/modules/appearance/Udt.qml b/desktop/modules/appearance/Udt.qml new file mode 100644 index 0000000..a4bf296 --- /dev/null +++ b/desktop/modules/appearance/Udt.qml @@ -0,0 +1,153 @@ +// Copyright (C) 2026 Danilo M. +// +// 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 +import "../.." + +// The unified-desktop-theme side: which schemes exist, what they look like, +// and switching between them. +Singleton { + id: root + + readonly property string home: Quickshell.env("HOME") + readonly property string repo: `${home}/Programming/GIT/unified-desktop-theme` + readonly property string selector: `${home}/.config/udt/roles.conf` + + property string current: "" + // name -> { bg, surface, text, accent, red, green, yellow } + property var swatches: ({}) + property list names: [] + + property bool applying: false + signal applied(string scheme, bool ok, string message) + + // A scheme is two files: .conf holds colours under the scheme's own + // names, roles-.conf says what each colour is for. Resolving a role + // means looking its value up as a key in the first file. + function parseSwatch(rolesText, paletteText) { + // QML's JS engine has no String.matchAll, so this is an exec loop. + const colours = {}; + const re = /^(\w+)\s*=\s*(#[0-9a-fA-F]{6})/gm; + let m; + while ((m = re.exec(paletteText)) !== null) colours[m[1]] = m[2]; + + const role = key => { + const m = rolesText.match(new RegExp(`^${key}\\s*=\\s*(\\S+)`, "m")); + // A role can carry a /alpha or *shade suffix; the base colour is + // what a swatch shows. + return m ? colours[m[1].split(/[/*]/)[0]] ?? "" : ""; + }; + + return { + bg: role("bg"), surface: role("surface"), text: role("fg"), + accent: role("accent"), red: role("critical"), + green: role("success"), yellow: role("warning"), + }; + } + + // Which scheme is live. Watched, so switching it from an editor moves the + // panel's marker too. + FileView { + path: root.selector + watchChanges: true + onFileChanged: reload() + onLoaded: { + const m = text().match(/^\s*scheme\s*=\s*(\S+)/m); + if (m) root.current = m[1]; + } + } + + // The scheme list comes from the palette directory rather than the comment + // in roles.conf: adding a scheme is adding two files, and this then needs + // no edit at all. + // Rescanned whenever the panel opens: adding a scheme is adding two + // files, and a shell that has been running since before they appeared + // would otherwise keep showing the old list. + function refresh() { + listProc.running = false; + listProc.running = true; + } + + Process { + id: listProc + running: true + command: ["sh", "-c", + `ls ${root.repo}/palette/roles-*.conf 2>/dev/null | ` + + `sed 's|.*/roles-||; s|\\.conf$||' | sort`] + stdout: StdioCollector { + onStreamFinished: { + root.names = text.trim().split("\n").filter(s => s.length); + loadProc.next = 0; + loadProc.loadNext(); + } + } + } + + Process { + id: loadProc + property int next: 0 + property string scheme: "" + + function loadNext() { + if (next >= root.names.length) return; + scheme = root.names[next]; + next++; + // Both files at once, split on a marker: one process per scheme + // rather than two, and the pair is always consistent. + command = ["sh", "-c", + `cat ${root.repo}/palette/roles-${scheme}.conf; ` + + `echo '===SPLIT==='; cat ${root.repo}/palette/${scheme}.conf`]; + // Assigning true to an already-true `running` does nothing, and + // this Process is reused for every scheme in turn. + running = false; + running = true; + } + + stdout: StdioCollector { + onStreamFinished: { + const [rolesText, paletteText] = text.split("===SPLIT==="); + if (rolesText && paletteText) { + const next = Object.assign({}, root.swatches); + next[loadProc.scheme] = root.parseSwatch(rolesText, paletteText); + root.swatches = next; + } + loadProc.loadNext(); + } + } + } + + // Switching writes the scheme line and regenerates every config; + // install.sh also reloads what is running, and hands its whole output to + // the steps window's `install` tab itself. + function apply(scheme) { + if (applying || scheme === current) return; + applying = true; + applyProc.scheme = scheme; + applyProc.command = ["sh", "-c", + `sed -i 's|^scheme *=.*|scheme = ${scheme}|' ${JSON.stringify(root.selector)} && ` + + `${JSON.stringify(root.repo)}/install.sh >/dev/null 2>&1`]; + applyProc.running = true; + } + + Process { + id: applyProc + property string scheme: "" + onExited: code => { + root.applying = false; + root.applied(applyProc.scheme, code === 0, + code === 0 ? "" : `install.sh exited ${code}, its output is in the steps window`); + } + } +} -- cgit v1.2.3