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