1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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}`));
}
}
}
|