From 9d9e67a14967fe756c357b0ece3d9f339f8afa17 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 22 Oct 2026 17:25:15 +0200 Subject: 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 `, 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 --- AGENTS.md | 5 +- desktop/modules/appearance/README.md | 19 +++- desktop/modules/appearance/Udt.qml | 55 ++++++++++ desktop/modules/appearance/UdtModule.qml | 44 ++++++++ desktop/modules/appearance/UdtTab.qml | 166 +++++++++++++++++++++++++++++++ desktop/shell.qml | 1 + 6 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 desktop/modules/appearance/UdtModule.qml create mode 100644 desktop/modules/appearance/UdtTab.qml 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 +`, 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_` 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_ function each, + // so adding one in unified-desktop-theme needs no edit here. + property list 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. +// +// 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. +// +// 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 {}, -- cgit v1.2.3