diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-11 18:30:58 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-11 18:30:58 +0200 |
| commit | 1b252e32d93e1e0207d2a69ee3445e1dbf6505ae (patch) | |
| tree | d4280dae5cab886898e7e5e674508728e2d5150a /appearance/Udt.qml | |
| parent | 43c31deaa313a8e471ecc3b5a55411a3b93b6f08 (diff) | |
| download | quickshell-1b252e32d93e1e0207d2a69ee3445e1dbf6505ae.tar.gz quickshell-1b252e32d93e1e0207d2a69ee3445e1dbf6505ae.zip | |
feat(appearance): wallpaper picker and scheme switcher
One drawer for how the desktop looks, on SUPER+Return, replacing the
qarma file dialog that key used to open. wallp is untouched and still
does the work: the panel calls it with H= and V= and it still runs from
a terminal.
Clicking a thumbnail stages it rather than setting it, so both screens
can be composed before anything changes and Apply is a single wallp
call. The mock screens show the result: the staged pick where there is
one, what is set where there is not, and the hovered thumbnail on the
targeted screen. They are drawn at the real proportions from hyprctl
monitors, DP-3 upright because it has transform=1, centred against each
other as Hyprland has them.
The scheme tab reads the palette pair unified-desktop-theme keeps for
each scheme, so the swatches are that scheme's real colours and the list
needs no edit when a scheme is added. Applying writes roles.conf and
runs install.sh, and reloads nothing, because install.sh reloads
nothing: the panel says what is still showing the old scheme rather than
pretending the switch is done.
Two QML traps are documented in AGENTS.md because neither announces
itself: there is no String.matchAll, which threw inside a try and left
every swatch empty, and assigning running = true to an already-running
Process does nothing, which stopped the scheme loader after the first
file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
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}`)); + } + } +} |
