diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-22 17:25:15 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-22 17:25:15 +0200 |
| commit | 9d9e67a14967fe756c357b0ece3d9f339f8afa17 (patch) | |
| tree | cc443f3a54979f2ba0a0cf050917ec4876997c67 /desktop/modules/appearance | |
| parent | 38b64dda38c76fa11424cba8ed012cd94a56362e (diff) | |
| download | quickshell-9d9e67a14967fe756c357b0ece3d9f339f8afa17.tar.gz quickshell-9d9e67a14967fe756c357b0ece3d9f339f8afa17.zip | |
settings: a Theme status page for unified-desktop-theme
Shows the live scheme, accent and both wallpapers, the scheme and
wallpapers linking to their own pages, then every udt consumer in a
software and a hardware list, each with a Re-apply button running
`install.sh <name>`, and Re-apply all in the header. The list comes from
`install.sh --list`, so a consumer added in unified-desktop-theme shows up
here with no edit. One run at a time, and none during a scheme switch.
QML's JS engine has no Object.fromEntries either: it threw a TypeError
that only reached the log and left the page without its list. Noted in
AGENTS.md beside String.matchAll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/appearance')
| -rw-r--r-- | desktop/modules/appearance/README.md | 19 | ||||
| -rw-r--r-- | desktop/modules/appearance/Udt.qml | 55 | ||||
| -rw-r--r-- | desktop/modules/appearance/UdtModule.qml | 44 | ||||
| -rw-r--r-- | desktop/modules/appearance/UdtTab.qml | 166 |
4 files changed, 280 insertions, 4 deletions
diff --git a/desktop/modules/appearance/README.md b/desktop/modules/appearance/README.md index b80b564..50da8fb 100644 --- a/desktop/modules/appearance/README.md +++ b/desktop/modules/appearance/README.md @@ -1,7 +1,7 @@ # appearance -Five pages of the settings window: Wallpaper, Theme, Icons & cursor, -HyprSunset and HyprIdle. Until October 2026 they were the tabs of a separate `appearance/` +Six pages of the settings window: Wallpaper, Theme, Theme status, Icons & +cursor, HyprSunset and HyprIdle. Until October 2026 they were the tabs of a separate `appearance/` shell with an overlay of its own; they moved into the desktop process when the settings window gave them the room that shell existed to provide. @@ -21,8 +21,19 @@ that page; `steps` pokes the steps window; `selftest` runs the parsers' own checks. Write the path out in full in the Hyprland config: `exec_cmd` has no shell to expand `~`. -Search finds a scheme by name and an icon or cursor theme by name, and -scrolls to it. +Search finds a scheme, a theme consumer, and an icon or cursor theme by +name, and scrolls to it. + +## Theme status + +The live scheme, accent and wallpaper, and one row per unified-desktop-theme +consumer, with what it themes, and a Re-apply button running `install.sh +<name>`, split into software and hardware lists; Re-apply all in the header +runs `install.sh` with no argument. The list is `install.sh --list`, which +reads the script's own `c_<name>` functions and their line comments +(`hardware:` marks a device), so a consumer added there appears here with no +edit. One run at a time, and not +during a scheme switch. ## Sunset diff --git a/desktop/modules/appearance/Udt.qml b/desktop/modules/appearance/Udt.qml index a4bf296..98fbba7 100644 --- a/desktop/modules/appearance/Udt.qml +++ b/desktop/modules/appearance/Udt.qml @@ -150,4 +150,59 @@ Singleton { code === 0 ? "" : `install.sh exited ${code}, its output is in the steps window`); } } + + // The consumers come from install.sh itself, one c_<name> function each, + // so adding one in unified-desktop-theme needs no edit here. + property list<string> consumers: [] + // name -> what it themes, the comment on its function line. + property var consumerInfo: ({}) + // name -> "software" or "hardware", for the page's two lists. + property var consumerKind: ({}) + function refreshStatus() { + statusProc.running = false; + statusProc.running = true; + } + + Process { + id: statusProc + command: [`${root.repo}/install.sh`, "--list"] + stdout: StdioCollector { + onStreamFinished: { + const rows = text.trim().split("\n").filter(s => s.length).map(l => l.split("\t")); + // QML's JS engine has no Object.fromEntries. + const info = {}, kind = {}; + for (const [n, k, d] of rows) { kind[n] = k; info[n] = d ?? ""; } + root.consumerInfo = info; + root.consumerKind = kind; + root.consumers = rows.map(r => r[0]); + } + } + } + + // One run at a time, so two never race over the generated files. + // "*" is every consumer at once, install.sh with no argument. + // install.sh hands a failure's output to the steps window itself. + property string reapplying: "" + // name -> true (ok) / false (failed), for the run this session. + property var reapplied: ({}) + function reapply(name) { + if (applying || reapplying !== "") return; + reapplying = name; + reapplyProc.command = [`${root.repo}/install.sh`].concat(name === "*" ? [] : [name]); + reapplyProc.running = true; + } + function isReapplying(name) { return reapplying === "*" || reapplying === name; } + + Process { + id: reapplyProc + onExited: code => { + const next = Object.assign({}, root.reapplied); + // ponytail: a full run marks every row by the one exit code, since + // install.sh stops at the first failure without naming it. + for (const n of root.reapplying === "*" ? root.consumers : [root.reapplying]) + next[n] = code === 0; + root.reapplied = next; + root.reapplying = ""; + } + } } diff --git a/desktop/modules/appearance/UdtModule.qml b/desktop/modules/appearance/UdtModule.qml new file mode 100644 index 0000000..3b15c77 --- /dev/null +++ b/desktop/modules/appearance/UdtModule.qml @@ -0,0 +1,44 @@ +// 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. + +import QtQuick +import "../.." + +// What unified-desktop-theme has set, and re-applying it to one consumer. +Module { + name: "udt" + label: "Theme status" + // nf-fa-sliders, present in Inconsolata Nerd Font. + icon: "" + settings: true + group: "Appearance" + keywords: ["consumer", "reapply", "install", "udt", "accent", "wallpaper"] + searchItems: Array.from(Udt.consumers).map(n => ({ text: `${n} ${Udt.consumerInfo[n] ?? ""}`, key: n })) + + function refresh() { Udt.refreshStatus(); } + + page: Component { + Page { + id: page + title: "Theme status" + actions: Component { + Button { + // nf-fa-refresh + icon: "\uf021" + text: Udt.reapplying === "*" ? "Applying…" : "Re-apply all" + enabled: Udt.reapplying === "" && !Udt.applying + onClicked: Udt.reapply("*") + } + } + UdtTab { width: parent.width; onGo: name => page.go(name) } + } + } +} diff --git a/desktop/modules/appearance/UdtTab.qml b/desktop/modules/appearance/UdtTab.qml new file mode 100644 index 0000000..b4d14b7 --- /dev/null +++ b/desktop/modules/appearance/UdtTab.qml @@ -0,0 +1,166 @@ +// 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. + +import QtQuick +import "../.." + +// The live scheme, accent and both wallpapers, then every consumer install.sh +// knows, each with a button that re-applies the theme to it alone. +Column { + id: tab + spacing: 10 + + // A value set on another settings page links to it. + signal go(string name) + + Component.onCompleted: Udt.refreshStatus() + + Repeater { + model: [ + { label: "Theme", value: Udt.current, page: "theme" }, + { label: "Accent", value: String(Theme.accent), swatch: true }, + { label: "Wallpaper H", value: Wallpapers.basename(Wallpapers.currentH), page: "wallpaper" }, + { label: "Wallpaper V", value: Wallpapers.basename(Wallpapers.currentV), page: "wallpaper" }, + ] + + Row { + required property var modelData + spacing: 8 + + Text { + width: 100 + text: modelData.label + color: Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Rectangle { + anchors.verticalCenter: parent.verticalCenter + visible: modelData.swatch ?? false + width: 16; height: 16; radius: 4 + color: Theme.accent + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.1) + } + Text { + id: value + width: Math.min(implicitWidth, tab.width - 140) + elide: Text.ElideMiddle + text: modelData.value || "-" + color: modelData.page && link.containsMouse ? Theme.accent : Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3; bold: !!modelData.page } + + MouseArea { + id: link + anchors.fill: parent + enabled: !!modelData.page + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: tab.go(modelData.page) + } + } + Text { + anchors.verticalCenter: parent.verticalCenter + visible: !!modelData.page + // nf-fa-chevron_right + text: "\uf054" + color: link.containsMouse ? Theme.accent : Theme.overlay + font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 6 } + } + } + } + + Text { + width: tab.width + wrapMode: Text.Wrap + text: "Re-applying runs install.sh for that consumer alone; Re-apply all in the header runs it for every one. A failure's output lands in the steps window." + color: Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + + // Apps and devices, in install.sh's order. + Repeater { + model: [{ kind: "software", label: "Software" }, { kind: "hardware", label: "Hardware" }] + + Column { + id: section + required property var modelData + readonly property string kind: modelData.kind + width: tab.width + spacing: 6 + + Text { + topPadding: 6 + text: section.modelData.label + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + } + + Repeater { + model: Array.from(Udt.consumers).filter(n => Udt.consumerKind[n] === section.kind) + + Rectangle { + required property string modelData + // What a search hit scrolls to (UdtModule's searchItems key). + objectName: modelData + readonly property var result: Udt.reapplied[modelData] + + width: section.width + implicitHeight: 46 + radius: 8 + color: Qt.alpha(Theme.surface, 0.4) + + Column { + anchors.verticalCenter: parent.verticalCenter + x: 12 + Text { + text: modelData + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + } + Text { + text: Udt.consumerInfo[modelData] ?? "" + visible: text !== "" + color: Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } + } + } + + Row { + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 8 + spacing: 10 + + Text { + anchors.verticalCenter: parent.verticalCenter + visible: Udt.isReapplying(modelData) || result !== undefined + text: Udt.isReapplying(modelData) ? "applying…" + : result ? "" : "" + color: Udt.isReapplying(modelData) ? Theme.overlay + : result ? Theme.green : Theme.red + font { + family: Udt.isReapplying(modelData) ? Theme.fontFamily : Theme.iconFamily + pixelSize: Theme.fontSize - 3 + } + } + Button { + // nf-fa-refresh + icon: "" + text: "Re-apply" + enabled: Udt.reapplying === "" && !Udt.applying + onClicked: Udt.reapply(modelData) + } + } + } + } + } + } +} |
