aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md5
-rw-r--r--desktop/modules/appearance/README.md19
-rw-r--r--desktop/modules/appearance/Udt.qml55
-rw-r--r--desktop/modules/appearance/UdtModule.qml44
-rw-r--r--desktop/modules/appearance/UdtTab.qml166
-rw-r--r--desktop/shell.qml1
6 files changed, 285 insertions, 5 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 1c3f729..3e84b04 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,7 +13,7 @@ modules.
desktop/ the drawer (SUPER alone): user details, sound, mail, VMs, status,
performance, clipboard, updates; and the settings
- window for pages set once: wallpaper, theme, icons,
+ window for pages set once: wallpaper, theme, theme status, icons,
sunset, idle (the former appearance shell), notifications, autostart,
the assistant's daemon and presets, qtmaildir, git
island/ top-centre pills on DP-1: clock, volume, tray, layout, privacy,
@@ -98,6 +98,9 @@ changing that component. The ones that generalise:
one overflowed the panel and pushed a button past its edge.
- **QML's JS engine has no `String.matchAll`.** It throws, and inside a `try`
that looks like a parser quietly returning nothing. Use an `exec` loop.
+- **Nor `Object.fromEntries`.** It throws a `TypeError` that only reaches the
+ log, and the page renders without whatever the handler was building. Build
+ the object with a loop.
- **Assigning `running = true` to a `Process` that is already running does
nothing.** Reusing one `Process` for a sequence of commands needs
`running = false` immediately before each start.
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)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/desktop/shell.qml b/desktop/shell.qml
index a42422a..8579709 100644
--- a/desktop/shell.qml
+++ b/desktop/shell.qml
@@ -81,6 +81,7 @@ ShellRoot {
NotesModule {},
WallpaperModule {},
ThemeModule {},
+ UdtModule {},
IconsModule {},
SunsetModule {},
IdleModule {},