diff options
Diffstat (limited to 'appearance/Udt.qml')
| -rw-r--r-- | appearance/Udt.qml | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/appearance/Udt.qml b/appearance/Udt.qml new file mode 100644 index 0000000..30ae285 --- /dev/null +++ b/appearance/Udt.qml @@ -0,0 +1,145 @@ +// 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 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<string> names: [] + + property bool applying: false + signal applied(string scheme, bool ok, string message) + + // A scheme is two files: <name>.conf holds colours under the scheme's own + // names, roles-<name>.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. + 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. Nothing + // is reloaded here: install.sh reloads nothing itself, and which apps to + // signal is the panel's message to the user rather than its job. + 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`]; + applyProc.running = true; + } + + Process { + id: applyProc + property string scheme: "" + stderr: StdioCollector { id: applyErr } + onExited: code => { + root.applying = false; + root.applied(applyProc.scheme, code === 0, + code === 0 ? "" : (applyErr.text.trim() || `install.sh exited ${code}`)); + } + } +} |
