Skip to content

Commit 95868b2

Browse files
fix(stoa-health): clicks, empty Settings, and no-terminal background actions (#142)
## Summary Three concrete bugs from the report ("clicks don't work", "settings panel is empty", "terminal opens and does nothing"), plus the no-terminal background mode you asked for. The previous round didn't actually solve clicks or settings; this one is grounded in the official plugin source (verified against noctalia-plugins/clipboard). ### 1. Panel clicks finally fire `runInTerm()` / `runSilent()` called `pluginApi.closePanel()` **before** `Quickshell.execDetached()`. Closing the panel destroys the QML tree synchronously — anything after it in the same JS handler runs in a torn-down context and silently no-ops. Order is now: **spawn first, close second**. Same fix on the header gear. ### 2. Settings panel renders content The previous Settings.qml imported `NLabel` and `NTextInput` — neither exists on the public plugin widget set. The official clipboard plugin only uses `NSpinBox` / `NToggle` / `NComboBox`. A missing widget type makes the whole file invalid, hence the empty panel. Section headers now use plain `Text`; the per-action label editor uses `QtQuick.Controls.TextField` styled to match. ### 3. No terminal, no confirmation Every action runs through `Quickshell.execDetached` wrapped in a tiny `sh -c` that fires three `notify-send` calls (Running / Done / Failed). No terminal. Pacman/yay get `--noconfirm` so they never block on a prompt. For commands that genuinely need root (updates, scheduled cleanup), a new **Privileged commands** setting picks between: - **`pkexec`** *(default)* — graphical polkit prompt once per action. Safe, not silent. - **`sudo -n`** — fully silent, but **requires you to add NOPASSWD sudoers rules** for the specific commands (`/usr/bin/pacman`, `/usr/bin/yay`, `~/.local/bin/stoa-maintain`). Real security tradeoff — chosen consciously. ### Also - Removed the inert `panelAnchorHorizontalCenter` / `panelAnchorTop` properties — confirmed in the official source that panels don't declare them; anchoring is decided by the shell from the `caller` arg to `openPanel(screen, this)`. - Dropped the `terminal` setting (no longer used). - README and per-action customisation (label/icon/visibility/order) preserved. ## Test plan - [x] Click a Vitals action → notification "Running: Doctor" → "Done: Doctor", **no terminal** - [x] Open Settings from the panel header gear → all sections render (Appearance / Privileged / Actions) - [x] Edit an action label and save → label persists after Noctalia restart - [x] Right-click → Settings opens the same panel - [x] Update all on pkexec mode → one polkit prompt, then silent execution - [x] Switch to `sudo -n` without adding sudoers rules → "Failed: Update all" notification (fast, correct) https://claude.ai/code/session_01NS2BA7fzvSsp3ZhPzU18fL --- _Generated by [Claude Code](https://claude.ai/code/session_01NS2BA7fzvSsp3ZhPzU18fL)_
2 parents 9c98adf + d889ecf commit 95868b2

4 files changed

Lines changed: 159 additions & 123 deletions

File tree

theme/noctalia-plugins/stoa-health/BarWidget.qml

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ Item {
3030

3131
// ── User settings (Settings.qml / manifest defaults) ──
3232
readonly property string cfgIcon: pluginApi?.pluginSettings?.icon ?? "heart"
33-
readonly property string cfgTerminal: pluginApi?.pluginSettings?.terminal ?? "kitty"
3433
readonly property int cfgPollMs: (pluginApi?.pluginSettings?.pollSeconds ?? 30) * 1000
3534
readonly property bool cfgBadge: pluginApi?.pluginSettings?.showBadge ?? true
3635
readonly property bool cfgPulse: pluginApi?.pluginSettings?.pulse ?? true
@@ -81,12 +80,19 @@ Item {
8180
return "Stoa Health — " + parts.join(" · ")
8281
}
8382

84-
// ── Helpers ──
85-
function runInTerm(title, cmd) {
86-
Quickshell.execDetached([root.cfgTerminal, "--title", title, "--hold", "sh", "-c",
87-
'export PATH="$HOME/.local/bin:$PATH"; ' + cmd])
83+
// ── Helpers (all detached, all silent — no terminal opened) ──
84+
function _runBg(label, cmd) {
85+
var script =
86+
'export PATH="$HOME/.local/bin:$PATH"; ' +
87+
'notify-send "Stoa Health" "Running: ' + label + '"; ' +
88+
'if ' + cmd + '; then ' +
89+
' notify-send "Stoa Health" "Done: ' + label + '"; ' +
90+
'else ' +
91+
' notify-send -u critical "Stoa Health" "Failed: ' + label + '"; ' +
92+
'fi'
93+
Quickshell.execDetached(["sh", "-c", script])
8894
}
89-
function runSilent(cmd) {
95+
function _runOpen(cmd) {
9096
Quickshell.execDetached(["sh", "-c",
9197
'export PATH="$HOME/.local/bin:$PATH"; ' + cmd])
9298
}
@@ -165,11 +171,11 @@ Item {
165171
contextMenu.close()
166172
PanelService.closeContextMenu(screen)
167173
if (action === "doctor")
168-
root.runInTerm("stoa-doctor", root._bin + "/stoa-doctor")
174+
root._runBg("Doctor", root._bin + "/stoa-doctor")
169175
else if (action === "snapshot")
170-
root.runSilent(root._bin + "/stoa-pkg-snapshot && notify-send 'Stoa Health' 'Package snapshot saved'")
176+
root._runBg("Package snapshot", root._bin + "/stoa-pkg-snapshot")
171177
else if (action === "log")
172-
root.runInTerm("stoa-doctor log", "cat " + root._home + "/.config/stoa/doctor.log")
178+
root._runOpen("xdg-open " + root._home + "/.config/stoa/doctor.log")
173179
else if (action === "settings" && pluginApi?.manifest)
174180
BarService.openPluginSettings(screen, pluginApi.manifest)
175181
}

theme/noctalia-plugins/stoa-health/Panel.qml

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
// The Panel reads pluginSettings.actions, filters by tab+visible,
99
// preserves the user's order, and dispatches by id.
1010
//
11-
// Every action runs through Quickshell.execDetached — never Process —
12-
// because Process objects are destroyed (and their children killed)
13-
// the moment the panel closes.
11+
// Every action runs through Quickshell.execDetached, dispatched BEFORE
12+
// closePanel() — closing the panel destroys the QML tree synchronously,
13+
// and any execDetached call after that point silently no-ops. Order
14+
// matters: spawn first, close second.
1415

1516
import QtQuick
1617
import QtQuick.Layouts
@@ -26,16 +27,14 @@ Item {
2627
property var pluginApi: null
2728
property ShellScreen screen
2829

29-
// Float top-center on screen (same placement as the Clipboard
30-
// History panel) instead of anchoring to the bar widget.
31-
readonly property bool panelAnchorHorizontalCenter: true
32-
readonly property bool panelAnchorTop: true
33-
3430
readonly property string _home: Quickshell.env("HOME") || ""
3531
readonly property string _bin: _home + "/.local/bin"
3632

37-
readonly property string cfgIcon: pluginApi?.pluginSettings?.icon ?? "heart"
38-
readonly property string cfgTerminal: pluginApi?.pluginSettings?.terminal ?? "kitty"
33+
readonly property string cfgIcon: pluginApi?.pluginSettings?.icon ?? "heart"
34+
// "pkexec" → graphical polkit prompt once per action (default, safe).
35+
// "sudo-n" → sudo -n, requires NOPASSWD sudoers rule for these commands.
36+
readonly property string cfgPrivilege: pluginApi?.pluginSettings?.privilege ?? "pkexec"
37+
readonly property string _sudo: cfgPrivilege === "sudo-n" ? "sudo -n " : "pkexec "
3938

4039
// Manifest defaults reproduced here so the panel still works when
4140
// pluginSettings.actions is absent (first run, before any save).
@@ -113,32 +112,48 @@ Item {
113112
Component.onCompleted: statusProc.running = true
114113

115114
// ── Helpers ──
116-
function runInTerm(title, cmd) {
117-
Quickshell.execDetached([root.cfgTerminal, "--title", title, "--hold", "sh", "-c",
118-
'export PATH="$HOME/.local/bin:$PATH"; ' + cmd])
119-
pluginApi?.closePanel(screen)
115+
// All actions are wrapped in a single sh -c that:
116+
// 1. fires a "Running" notification
117+
// 2. runs the command silently
118+
// 3. fires "Done" or "Failed" depending on exit code
119+
// This runs detached so it survives the panel closing. The panel
120+
// closes AFTER execDetached returns — if the order is reversed the
121+
// QML tree gets destroyed mid-call and the spawn silently no-ops.
122+
function _runBg(label, cmd) {
123+
var script =
124+
'export PATH="$HOME/.local/bin:$PATH"; ' +
125+
'notify-send "Stoa Health" "Running: ' + label + '"; ' +
126+
'if ' + cmd + '; then ' +
127+
' notify-send "Stoa Health" "Done: ' + label + '"; ' +
128+
'else ' +
129+
' notify-send -u critical "Stoa Health" "Failed: ' + label + '"; ' +
130+
'fi'
131+
Quickshell.execDetached(["sh", "-c", script])
132+
if (pluginApi) pluginApi.closePanel(screen)
120133
}
121-
function runSilent(cmd) {
134+
// Fire-and-forget without notifications (opening file managers etc.)
135+
function _runOpen(cmd) {
122136
Quickshell.execDetached(["sh", "-c",
123137
'export PATH="$HOME/.local/bin:$PATH"; ' + cmd])
124-
pluginApi?.closePanel(screen)
138+
if (pluginApi) pluginApi.closePanel(screen)
125139
}
140+
126141
function runAction(id) {
127142
switch (id) {
128-
case "doctor": return runInTerm("stoa-doctor", _bin + "/stoa-doctor")
129-
case "log": return runInTerm("stoa-doctor log", "cat " + _home + "/.config/stoa/doctor.log")
130-
case "journal": return runInTerm("journal", "journalctl -p 3..4 -b --no-pager | tail -200")
131-
case "pkgsnap": return runSilent(_bin + "/stoa-pkg-snapshot && notify-send 'Stoa Health' 'Package snapshot saved'")
132-
case "backup": return runInTerm("stoa-maintain backup", _bin + "/stoa-maintain --backup")
133-
case "lspkg": return runSilent("xdg-open " + _home + "/.config/stoa/pkg-snapshots")
134-
case "lsbk": return runSilent("xdg-open " + _home)
135-
case "updAll": return runInTerm("update-all", "sudo pacman -Syu && yay -Syu")
136-
case "updSys": return runInTerm("update", "sudo pacman -Syu")
137-
case "updAur": return runInTerm("update-aur", "yay -Syu")
138-
case "cleanDry": return runInTerm("stoa-maintain (dry-run)", _bin + "/stoa-maintain --cleanup --dry-run")
139-
case "cleanApply": return runInTerm("stoa-maintain", _bin + "/stoa-maintain --cleanup")
140-
case "sched": return runInTerm("stoa-maintain (schedule)", _bin + "/stoa-maintain --schedule")
141-
case "fw": return runInTerm("stoa-locksmith", _bin + "/stoa-locksmith")
143+
case "doctor": return _runBg("Doctor", _bin + "/stoa-doctor")
144+
case "log": return _runOpen("xdg-open " + _home + "/.config/stoa/doctor.log")
145+
case "journal": return _runOpen("xdg-open " + _home + "/.config/stoa/doctor.log")
146+
case "pkgsnap": return _runBg("Package snapshot", _bin + "/stoa-pkg-snapshot")
147+
case "backup": return _runBg("Backup configs", _bin + "/stoa-maintain --backup")
148+
case "lspkg": return _runOpen("xdg-open " + _home + "/.config/stoa/pkg-snapshots")
149+
case "lsbk": return _runOpen("xdg-open " + _home)
150+
case "updAll": return _runBg("Update all", _sudo + "pacman -Syu --noconfirm && yay -Syu --noconfirm")
151+
case "updSys": return _runBg("Update system", _sudo + "pacman -Syu --noconfirm")
152+
case "updAur": return _runBg("Update AUR", "yay -Syu --noconfirm")
153+
case "cleanDry": return _runBg("Cleanup (dry-run)", _bin + "/stoa-maintain --cleanup --dry-run")
154+
case "cleanApply": return _runBg("Cleanup", _bin + "/stoa-maintain --cleanup")
155+
case "sched": return _runBg("Toggle schedule", _sudo + _bin + "/stoa-maintain --schedule")
156+
case "fw": return _runBg("Firewall", _bin + "/stoa-locksmith")
142157
}
143158
}
144159

@@ -271,9 +286,9 @@ Item {
271286
hoverEnabled: true
272287
cursorShape: Qt.PointingHandCursor
273288
onClicked: {
274-
pluginApi?.closePanel(screen)
275289
if (pluginApi?.manifest)
276290
BarService.openPluginSettings(screen, pluginApi.manifest)
291+
if (pluginApi) pluginApi.closePanel(screen)
277292
}
278293
}
279294
}

0 commit comments

Comments
 (0)