From d7d08d1b17ddd6fd3c4bac689b26cb4f99353567 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 6 Jun 2026 15:48:56 +0200 Subject: [PATCH 1/7] Improve Tailscale panel controls --- shell/plugins/panels/tailscale/Panel.qml | 54 ++++++++++++++-------- shell/plugins/panels/tailscale/Service.qml | 8 ++-- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/shell/plugins/panels/tailscale/Panel.qml b/shell/plugins/panels/tailscale/Panel.qml index 69e8d5fb..9626f801 100644 --- a/shell/plugins/panels/tailscale/Panel.qml +++ b/shell/plugins/panels/tailscale/Panel.qml @@ -238,7 +238,7 @@ Panel { if (focusSection === "header") { tailscale.toggleTailscale() } else if (focusSection === "auth") { - tailscale.authorizeProfileSwitching() + tailscale.authorizeTailscaleOperator() } else if (focusSection === "accounts") { var account = selectedAccount() if (account) tailscale.switchAccount(account.id) @@ -748,7 +748,7 @@ Panel { cursorShape: tailscale.busy ? Qt.ArrowCursor : Qt.PointingHandCursor enabled: !tailscale.busy onEntered: root.setAuthCursor() - onClicked: tailscale.authorizeProfileSwitching() + onClicked: tailscale.authorizeTailscaleOperator() } RowLayout { @@ -774,7 +774,7 @@ Panel { Text { Layout.fillWidth: true - text: "Authorize switching" + text: "Authorize Tailscale operator" color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.body @@ -783,7 +783,7 @@ Panel { Text { Layout.fillWidth: true - text: "Allow this user to see and switch Tailscale connections" + text: "Allow this user to operate this Tailscale profile" color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -791,14 +791,6 @@ Panel { } } - PanelActionButton { - iconText: "󰄬" - foreground: root.foreground - fontFamily: root.fontFamily - enabled: !tailscale.busy - Layout.alignment: Qt.AlignVCenter - onClicked: tailscale.authorizeProfileSwitching() - } } } @@ -994,14 +986,7 @@ Panel { modal: false focus: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - onOpenedChanged: { - root.copyMenuOpen = opened - if (opened) { - peerRow.clampCopyIndex() - forceActiveFocus() - } - } - Keys.onPressed: function(event) { + function handleKey(event) { if (event.key === Qt.Key_Escape) { close() event.accepted = true @@ -1022,6 +1007,15 @@ Panel { event.accepted = true } } + onOpenedChanged: { + root.copyMenuOpen = opened + if (opened) { + peerRow.clampCopyIndex() + Qt.callLater(function() { copyPopupContent.forceActiveFocus() }) + } else if (root.opened) { + Qt.callLater(function() { keyCatcher.forceActiveFocus() }) + } + } background: BorderSurface { color: Color.background borderSpec: Border.flat(root.dim, 1) @@ -1029,7 +1023,11 @@ Panel { } contentItem: Column { + id: copyPopupContent width: parent.width + focus: true + Keys.priority: Keys.BeforeItem + Keys.onPressed: function(event) { copyPopup.handleKey(event) } Repeater { model: peerRow.copyOptions @@ -1102,6 +1100,7 @@ Panel { readonly property bool activeExitNode: peer && peer.ExitNode === true readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "") readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || "Unknown") : "Unknown" + readonly property string actionTooltip: addMullvad ? "" : (activeExitNode ? "Disconnect" : "Connect") hasCursor: root.cursorActive && root.focusSection === "exitNodes" && root.exitNodeIndex === rowIndex current: activeExitNode || settingExitNode || (addMullvad && root.mullvadPickerOpen) @@ -1154,12 +1153,19 @@ Panel { } MouseArea { + id: exitNodeMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: root.setExitNodeCursor(exitNodeRow.rowIndex) onClicked: root.chooseExitNode(exitNodeRow.peer) } + + PanelToolTip { + visible: exitNodeRow.actionTooltip !== "" && exitNodeMouse.containsMouse + text: exitNodeRow.actionTooltip + fontFamily: root.fontFamily + } } component MullvadRegionRow: CursorSurface { @@ -1172,6 +1178,7 @@ Panel { readonly property bool activeExitNode: peer && peer.ExitNode === true readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "") readonly property bool selectedRegion: root.mullvadPickerOpen && root.mullvadRegionIndex === rowIndex + readonly property string actionTooltip: activeExitNode ? "Disconnect" : "Connect" foreground: root.foreground fill: root.hoverFill @@ -1226,11 +1233,18 @@ Panel { } MouseArea { + id: regionMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: root.mullvadRegionIndex = regionRow.rowIndex onClicked: root.chooseExitNode(regionRow.peer) } + + PanelToolTip { + visible: regionMouse.containsMouse + text: regionRow.actionTooltip + fontFamily: root.fontFamily + } } } diff --git a/shell/plugins/panels/tailscale/Service.qml b/shell/plugins/panels/tailscale/Service.qml index 8a2a33eb..820d3b48 100644 --- a/shell/plugins/panels/tailscale/Service.qml +++ b/shell/plugins/panels/tailscale/Service.qml @@ -291,11 +291,11 @@ Item { exitNodeProcess.running = true } - function authorizeProfileSwitching() { + function authorizeTailscaleOperator() { if (!installed || operatorProcess.running || userName === "") return _operatorOutput = "" _operatorError = "" - actionStatus = "Authorizing Tailscale profiles…" + actionStatus = "Authorizing Tailscale operator..." operatorProcess.command = ["pkexec", "tailscale", "set", "--operator=" + userName] operatorProcess.running = true } @@ -414,7 +414,7 @@ Item { root.parseAccounts("") if (/profiles access denied/i.test(stderr) || /profiles access denied/i.test(stdout)) { root.accountsAccessDenied = true - root.lastError = "Authorize Tailscale profiles to show connections" + root.lastError = "Authorize Tailscale operator to show connections" } else { root.lastError = elideStatus(stderr || stdout || "Could not list Tailscale connections") } @@ -534,7 +534,7 @@ Item { } else { root.accountsAccessDenied = false root.lastError = "" - root.actionStatus = "Tailscale profiles authorized" + root.actionStatus = "Tailscale operator authorized" actionStatusTimer.restart() root._lastAccountsRefreshMs = 0 } From 166b395203388c716d1d58e28b4107caccf6fd45 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 6 Jun 2026 16:43:00 +0200 Subject: [PATCH 2/7] Polish bar icon spacing --- shell/plugins/bar/Bar.qml | 14 ++++++++++++-- shell/plugins/bar/widgets/Workspaces.qml | 4 ++-- shell/plugins/panels/audio/Panel.qml | 2 ++ shell/plugins/panels/bluetooth/Panel.qml | 2 ++ shell/plugins/panels/dropbox/Panel.qml | 2 +- shell/plugins/panels/monitor/Panel.qml | 3 +++ shell/plugins/panels/network/Panel.qml | 5 +++-- shell/plugins/panels/power/Panel.qml | 4 ++-- shell/plugins/panels/tailscale/Panel.qml | 4 +++- shell/plugins/panels/weather/BarWidget.qml | 2 +- 10 files changed, 31 insertions(+), 11 deletions(-) diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 0f99803d..2d0dbee1 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -1257,6 +1257,12 @@ Item { readonly property bool hovered: moduleHover.hovered readonly property bool dragSource: root.barDragSource === slot readonly property bool panelOpen: root.activePopout === slot.activeItem + readonly property real openIndicatorInlineOffset: { + var item = slot.activeItem + if (!item || !("openIndicatorInlineOffset" in item)) return 0 + var offset = Number(item.openIndicatorInlineOffset) + return isFinite(offset) ? offset : 0 + } implicitWidth: activeItem && activeItem.visible ? (root.vertical ? root.barSize : activeItem.implicitWidth) : 0 implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0 @@ -1329,8 +1335,12 @@ Item { radius: Math.min(width, height) / 2 width: root.vertical ? Style.space(2) : Math.max(Style.space(10), Math.round(parent.width * 0.55)) height: root.vertical ? Math.max(Style.space(10), Math.round(parent.height * 0.55)) : Style.space(2) - x: root.vertical ? (root.position === "left" ? parent.width - width - inset : inset) : Math.round((parent.width - width) / 2) - y: root.vertical ? Math.round((parent.height - height) / 2) : (root.position === "top" ? parent.height - height - inset : inset) + x: root.vertical + ? (root.position === "left" ? parent.width - width - inset : inset) + : (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.width - width) / 2) : (parent.width - width) / 2 + slot.openIndicatorInlineOffset) + y: root.vertical + ? (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.height - height) / 2) : (parent.height - height) / 2 + slot.openIndicatorInlineOffset) + : (root.position === "top" ? parent.height - height - inset : inset) z: 50 Behavior on opacity { diff --git a/shell/plugins/bar/widgets/Workspaces.qml b/shell/plugins/bar/widgets/Workspaces.qml index a47e2d53..5a829fc0 100644 --- a/shell/plugins/bar/widgets/Workspaces.qml +++ b/shell/plugins/bar/widgets/Workspaces.qml @@ -42,7 +42,7 @@ BarWidget { id: grid anchors.fill: parent columns: root.vertical ? 1 : root.workspaceIds().length - columnSpacing: root.vertical ? 0 : Style.space(2) + columnSpacing: root.vertical ? 0 : Style.space(1) rowSpacing: root.vertical ? Style.space(2) : 0 Repeater { @@ -60,7 +60,7 @@ BarWidget { opacity: occupied || focused ? 1 : 0.5 horizontalMargin: 6 verticalPadding: 6 - fixedWidth: root.vertical ? root.barSize : Style.space(22) + fixedWidth: root.vertical ? root.barSize : Style.space(20) fixedHeight: root.barSize onPressed: function() { root.focusWorkspace(modelData) } } diff --git a/shell/plugins/panels/audio/Panel.qml b/shell/plugins/panels/audio/Panel.qml index 19a56e09..788b7a7e 100644 --- a/shell/plugins/panels/audio/Panel.qml +++ b/shell/plugins/panels/audio/Panel.qml @@ -528,6 +528,8 @@ Panel { bar: root.bar text: root.outputIcon() fontSize: Style.font.body + fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27) + fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1 onPressed: function(b) { if (b === Qt.RightButton) root.toggleOutputMute() else root.toggle() diff --git a/shell/plugins/panels/bluetooth/Panel.qml b/shell/plugins/panels/bluetooth/Panel.qml index 9a3be5d0..589cc0a9 100644 --- a/shell/plugins/panels/bluetooth/Panel.qml +++ b/shell/plugins/panels/bluetooth/Panel.qml @@ -406,6 +406,8 @@ Panel { anchors.fill: parent bar: root.bar text: root.icon + fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27) + fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1 onPressed: function(b) { if (b === Qt.RightButton) root.toggleBluetooth() else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth") diff --git a/shell/plugins/panels/dropbox/Panel.qml b/shell/plugins/panels/dropbox/Panel.qml index e5debe14..bb1b23a6 100644 --- a/shell/plugins/panels/dropbox/Panel.qml +++ b/shell/plugins/panels/dropbox/Panel.qml @@ -141,7 +141,7 @@ Panel { Item { id: button anchors.fill: parent - implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(32) + implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(27) implicitHeight: root.bar && root.bar.vertical ? Style.space(26) : (root.bar ? root.bar.barSize : Style.space(26)) property var registeredBar: null diff --git a/shell/plugins/panels/monitor/Panel.qml b/shell/plugins/panels/monitor/Panel.qml index ad23160e..0a1f5972 100644 --- a/shell/plugins/panels/monitor/Panel.qml +++ b/shell/plugins/panels/monitor/Panel.qml @@ -343,6 +343,9 @@ Panel { bar: root.bar text: root.displays.length > 1 ? "󰍺" : "󰍹" fontSize: Style.font.subtitle + fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27) + fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1 + rightExtraMargin: 4 onPressed: function(b) { root.toggle() } onWheelMoved: function(delta) { if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5)) diff --git a/shell/plugins/panels/network/Panel.qml b/shell/plugins/panels/network/Panel.qml index 4def7c1c..3a2050a2 100644 --- a/shell/plugins/panels/network/Panel.qml +++ b/shell/plugins/panels/network/Panel.qml @@ -584,8 +584,9 @@ Panel { anchors.fill: parent bar: root.bar text: root.icon - horizontalMargin: 8.5 - rightExtraMargin: 2 + fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27) + fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1 + rightExtraMargin: 5.5 onPressed: function(b) { if (root.opened) root.close() diff --git a/shell/plugins/panels/power/Panel.qml b/shell/plugins/panels/power/Panel.qml index 08f41ab5..5d8869fb 100644 --- a/shell/plugins/panels/power/Panel.qml +++ b/shell/plugins/panels/power/Panel.qml @@ -252,8 +252,8 @@ Panel { anchors.fill: parent bar: root.bar text: root.batteryIcon() - horizontalMargin: 8.5 - rightExtraMargin: 2 + fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27) + fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1 active: root.batteryPresent && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery tooltipText: "" onPressed: function(b) { if (root.batteryPresent) root.toggle() } diff --git a/shell/plugins/panels/tailscale/Panel.qml b/shell/plugins/panels/tailscale/Panel.qml index 9626f801..9d3ef11a 100644 --- a/shell/plugins/panels/tailscale/Panel.qml +++ b/shell/plugins/panels/tailscale/Panel.qml @@ -12,6 +12,7 @@ Panel { ipcTarget: "omarchy.tailscale" manageIpc: false + readonly property real openIndicatorInlineOffset: bar && bar.vertical ? 0 : Style.spaceReal(1.5) property string focusSection: "header" property int headerIndex: 0 property int accountIndex: 0 @@ -362,7 +363,7 @@ Panel { Item { id: button anchors.fill: parent - implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(32) + implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(27) implicitHeight: root.bar && root.bar.vertical ? Style.space(26) : (root.bar ? root.bar.barSize : Style.space(26)) property var registeredBar: null @@ -389,6 +390,7 @@ Panel { TailscaleIcon { anchors.centerIn: parent + anchors.horizontalCenterOffset: root.openIndicatorInlineOffset anchors.verticalCenterOffset: -Style.space(1) iconSize: Style.space(12) * 0.85 color: root.barIconColor diff --git a/shell/plugins/panels/weather/BarWidget.qml b/shell/plugins/panels/weather/BarWidget.qml index e02b1925..c50b739c 100644 --- a/shell/plugins/panels/weather/BarWidget.qml +++ b/shell/plugins/panels/weather/BarWidget.qml @@ -49,7 +49,7 @@ BarWidget { bar: root.bar text: panelLoader.item ? panelLoader.item.label : "" active: panelLoader.item && panelLoader.item.klass === "active" - horizontalMargin: 5 + horizontalMargin: 2.5 // Tooltip suppressed because the panel is the detail view. tooltipText: "" From 43e758ed3264557d1b4a94b1a4a52d152c2c502b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 6 Jun 2026 17:08:02 +0200 Subject: [PATCH 3/7] Align network panel details --- shell/plugins/panels/network/Panel.qml | 107 +++++++++++-------------- 1 file changed, 46 insertions(+), 61 deletions(-) diff --git a/shell/plugins/panels/network/Panel.qml b/shell/plugins/panels/network/Panel.qml index 3a2050a2..95676d19 100644 --- a/shell/plugins/panels/network/Panel.qml +++ b/shell/plugins/panels/network/Panel.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Networking @@ -795,48 +796,41 @@ Panel { width: parent.width spacing: Style.spacing.labelGap - Row { - visible: root.info.rx_bytes !== undefined + GridLayout { width: parent.width - spacing: Style.space(20) + columns: 4 + columnSpacing: Style.space(20) + rowSpacing: Style.spacing.labelGap - Column { - width: (parent.width - parent.spacing) / 2 - spacing: Style.spacing.labelGap - InfoPair { label: "Receiving"; value: root.formatRate(root.downloadRate) } - InfoPair { label: "Downloaded"; value: root.formatBytes(parseFloat(root.info.rx_bytes || "0")) } - } + InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Receiving" } + DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatRate(root.downloadRate) } + InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Sending" } + DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatRate(root.uploadRate) } - Column { - width: (parent.width - parent.spacing) / 2 - spacing: Style.spacing.labelGap - InfoPair { label: "Sending"; value: root.formatRate(root.uploadRate) } - InfoPair { label: "Uploaded"; value: root.formatBytes(parseFloat(root.info.tx_bytes || "0")) } - } - } + InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Downloaded" } + DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatBytes(parseFloat(root.info.rx_bytes || "0")) } + InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Uploaded" } + DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatBytes(parseFloat(root.info.tx_bytes || "0")) } - Row { - width: parent.width - spacing: Style.space(20) - Column { - width: (parent.width - parent.spacing) / 2 - InfoPair { - visible: !!root.info.ip - label: "IP Address" - value: root.info.ip || "" - copyable: true - tooltipText: "Copy IP" - } + InfoLabel { + visible: !!root.info.ip || !!root.info.gateway + text: root.info.ip ? "IP Address" : "" } - Column { - width: (parent.width - parent.spacing) / 2 - InfoPair { - visible: !!root.info.gateway - label: "Gateway" - value: root.info.gateway || "" - copyable: true - tooltipText: "Copy gateway" - } + DetailValue { + visible: !!root.info.ip || !!root.info.gateway + text: root.info.ip || "" + copyable: !!root.info.ip + tooltipText: "Copy IP" + } + InfoLabel { + visible: !!root.info.ip || !!root.info.gateway + text: root.info.gateway ? "Gateway" : "" + } + DetailValue { + visible: !!root.info.ip || !!root.info.gateway + text: root.info.gateway || "" + copyable: !!root.info.gateway + tooltipText: "Copy gateway" } } } @@ -1314,35 +1308,26 @@ Panel { onTriggered: if (!networkProc.running) networkProc.running = true } - component InfoPair: Row { - property string label: "" - property string value: "" + component DetailValue: InfoValue { property bool copyable: false property string tooltipText: "Copy to clipboard" - width: parent.width - spacing: Style.space(8) + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight - InfoLabel { text: label } - Item { width: Math.max(0, parent.width - parent.children[0].implicitWidth - valueText.implicitWidth - parent.spacing * 2); height: 1 } - InfoValue { - id: valueText - text: value + MouseArea { + id: valueMouse + anchors.fill: parent + enabled: copyable && parent.text !== "" + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: root.copyToClipboard(parent.text) + } - MouseArea { - id: valueMouse - anchors.fill: parent - enabled: copyable && valueText.text !== "" - hoverEnabled: enabled - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: root.copyToClipboard(valueText.text) - } - - PanelToolTip { - visible: valueMouse.enabled && valueMouse.containsMouse - text: tooltipText - fontFamily: root.bar.fontFamily - } + PanelToolTip { + visible: valueMouse.enabled && valueMouse.containsMouse + text: tooltipText + fontFamily: root.bar.fontFamily } } From b83505d7380bbe0525f56f5dc556c103848d34d5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 6 Jun 2026 17:21:09 +0200 Subject: [PATCH 4/7] More small adjustments --- shell/plugins/bar/widgets/Workspaces.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shell/plugins/bar/widgets/Workspaces.qml b/shell/plugins/bar/widgets/Workspaces.qml index 5a829fc0..ec0482a5 100644 --- a/shell/plugins/bar/widgets/Workspaces.qml +++ b/shell/plugins/bar/widgets/Workspaces.qml @@ -35,12 +35,15 @@ BarWidget { root.bar.run("hyprctl dispatch " + Util.shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })")) } - implicitWidth: grid.implicitWidth + readonly property real trailingGap: root.vertical ? 0 : Style.spaceReal(1.5) + + implicitWidth: grid.implicitWidth + trailingGap implicitHeight: grid.implicitHeight GridLayout { id: grid anchors.fill: parent + anchors.rightMargin: root.trailingGap columns: root.vertical ? 1 : root.workspaceIds().length columnSpacing: root.vertical ? 0 : Style.space(1) rowSpacing: root.vertical ? Style.space(2) : 0 From 740f4e2c829e46b929f274dae7ab999411600a22 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 6 Jun 2026 17:53:07 +0200 Subject: [PATCH 5/7] Fix power panel discharge state --- bin/omarchy-battery-status | 2 +- shell/plugins/panels/power/Model.js | 6 ++++-- shell/plugins/panels/power/Panel.qml | 29 ++++++++++++++++------------ test/shell.d/power-test.sh | 2 ++ 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/bin/omarchy-battery-status b/bin/omarchy-battery-status index c2eaf249..ac81e9d6 100755 --- a/bin/omarchy-battery-status +++ b/bin/omarchy-battery-status @@ -58,7 +58,7 @@ fi charge_holding=false if [[ $ac_online == "true" && -n $threshold_end ]]; then - if [[ $state == "discharging" || $state == "pending-charge" ]]; then + if [[ $state == "pending-charge" ]]; then charge_holding=true elif [[ $state == "fully-charged" ]] && (( percentage < 99 )); then charge_holding=true diff --git a/shell/plugins/panels/power/Model.js b/shell/plugins/panels/power/Model.js index 11901fe8..553d9aef 100644 --- a/shell/plugins/panels/power/Model.js +++ b/shell/plugins/panels/power/Model.js @@ -55,7 +55,8 @@ function chargeThresholdActive(device, onBattery, states) { if (!(d && d.isPresent && !onBattery)) return false var fraction = batteryFraction(d) - if (d.state === s.Discharging || d.state === s.PendingCharge) return true + if (d.state === s.Discharging) return false + if (d.state === s.PendingCharge) return true if (d.state === s.FullyCharged && fraction < 0.99) return true if (d.state !== s.Charging || fraction >= 0.99) return false @@ -74,6 +75,7 @@ function batteryIcon(device, onBattery, states) { if (threshold) return defaultIcons[index] if (d.state === states.FullyCharged) return "󰂅" if (d.state === states.Charging) return chargingIcons[index] + if (d.state === states.Discharging) return defaultIcons[index] if (!onBattery) return "" return defaultIcons[index] } @@ -85,7 +87,7 @@ function modeLabel(device, onBattery, states) { var percentage = d.isPresent ? d.percentage : 0 if (chargeThresholdActive(d, onBattery, states)) return "Threshold" if (!onBattery && percentage >= 1) return "Fully charged" - if (onBattery) return "On battery" + if (onBattery || d.state === states.Discharging) return "On battery" return "Charging" } diff --git a/shell/plugins/panels/power/Panel.qml b/shell/plugins/panels/power/Panel.qml index 5d8869fb..3062eb5e 100644 --- a/shell/plugins/panels/power/Panel.qml +++ b/shell/plugins/panels/power/Panel.qml @@ -41,12 +41,12 @@ Panel { function batteryIcon() { var device = UPower.displayDevice - return Model.batteryIcon(device, UPower.onBattery, upowerStates()) + return Model.batteryIcon(device, root.discharging, upowerStates()) } function modeLabel() { var device = UPower.displayDevice - return Model.modeLabel(device, UPower.onBattery, upowerStates()) + return Model.modeLabel(device, root.discharging, upowerStates()) } function profileIcon(name) { @@ -57,11 +57,15 @@ Panel { var device = UPower.displayDevice return device && device.isPresent && device.state === UPowerDeviceState.FullyCharged && !root.chargeThresholdActive } + readonly property bool discharging: { + var device = UPower.displayDevice + return !!(device && device.isPresent && (UPower.onBattery || device.state === UPowerDeviceState.Discharging)) + } readonly property bool chargeThresholdActive: { var device = UPower.displayDevice - return Model.chargeThresholdActive(device, UPower.onBattery, upowerStates()) + return Model.chargeThresholdActive(device, root.discharging, upowerStates()) } - readonly property bool batteryFull: fullyCharged || (!UPower.onBattery && batteryFraction >= 1) + readonly property bool batteryFull: fullyCharged || (!root.discharging && batteryFraction >= 1) readonly property bool batteryFlowIdle: batteryFull || chargeThresholdActive // 0..1 charge level, used by the visual progress bar. @@ -70,14 +74,12 @@ Panel { return Model.batteryFraction(d) } - readonly property bool batteryLow: UPower.onBattery && batteryFraction > 0 && batteryFraction <= 0.2 readonly property bool charging: { var d = UPower.displayDevice return d && d.isPresent && d.state === UPowerDeviceState.Charging && !root.chargeThresholdActive } readonly property color batteryFillColor: { - if (batteryLow) return Color.urgent return root.bar ? root.bar.foreground : Color.foreground } @@ -111,7 +113,7 @@ Panel { readonly property var activePhrases: { if (fullyCharged) return [] if (charging) return chargingPhrases - if (UPower.onBattery || chargeThresholdActive) return onBatteryPhrases + if (discharging) return onBatteryPhrases return [] } readonly property bool rotatingPhrases: activePhrases.length > 0 @@ -254,7 +256,6 @@ Panel { text: root.batteryIcon() fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27) fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1 - active: root.batteryPresent && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery tooltipText: "" onPressed: function(b) { if (root.batteryPresent) root.toggle() } } @@ -296,7 +297,7 @@ Panel { Text { id: heroIcon text: root.batteryIcon() - color: root.batteryLow ? Color.urgent : root.bar.foreground + color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.display anchors.left: parent.left @@ -340,7 +341,7 @@ Panel { Text { id: heroPercent text: root.batteryInfo.percentage || "—" - color: root.batteryLow ? Color.urgent : root.bar.foreground + color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.displayLarge font.bold: true @@ -408,17 +409,21 @@ Panel { width: (parent.width - parent.spacing) / 2 spacing: Style.spacing.labelGap InfoPair { - label: root.chargeThresholdActive ? "Charge limit" : (UPower.onBattery ? "Time left" : "Time to full") + label: root.chargeThresholdActive ? "Charge limit" : (root.discharging ? "Time left" : "Time to full") value: root.chargeThresholdActive ? (root.batteryInfo.threshold || "-") : (root.batteryFlowIdle ? "-" : (root.batteryInfo.time || "—")) } InfoPair { - label: root.chargeThresholdActive ? "Battery state" : (UPower.onBattery ? "Discharging" : "Charging") + label: root.chargeThresholdActive ? "Battery state" : (root.discharging ? "Discharging" : "Charging") value: root.chargeThresholdActive ? "Holding" : (root.batteryFull ? "-" : (root.batteryInfo.rate || "")) } } } // ---------- Power profile picker ---------- + PanelSeparator { + foreground: root.bar.foreground + } + Column { width: parent.width spacing: Style.space(10) diff --git a/test/shell.d/power-test.sh b/test/shell.d/power-test.sh index 3b26fe2f..66ec5166 100644 --- a/test/shell.d/power-test.sh +++ b/test/shell.d/power-test.sh @@ -24,7 +24,9 @@ assertEqual(power.batteryFraction({ isPresent: true, percentage: 1.5 }), 1, 'pow assert(power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.PendingCharge }, false, states), 'power detects threshold by pending charge state') assert(power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.Charging, changeRate: 0.1, timeToFull: 120 }, false, states), 'power detects threshold by stalled charging') assert(!power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.Charging, changeRate: 1.0, timeToFull: 120 }, false, states), 'power does not flag active charging as threshold') +assert(!power.chargeThresholdActive({ isPresent: true, percentage: 0.5, state: states.Discharging }, false, states), 'power does not flag discharging as threshold') assertEqual(power.modeLabel({ isPresent: true, percentage: 1, state: states.FullyCharged }, false, states), 'Fully charged', 'power labels full battery') assertEqual(power.modeLabel({ isPresent: true, percentage: 0.5, state: states.Discharging }, true, states), 'On battery', 'power labels battery mode') +assertEqual(power.modeLabel({ isPresent: true, percentage: 0.5, state: states.Discharging }, false, states), 'On battery', 'power labels discharging battery mode') assert(power.batteryIcon({ isPresent: true, percentage: 0.4, state: states.Charging }, false, states).length > 0, 'power maps battery icons') JS From 42bdf374dd4eb074f5308d76e567c9a0e2c84caf Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sat, 6 Jun 2026 02:49:40 -0400 Subject: [PATCH 6/7] Use Omarchy log upload endpoint --- bin/omarchy-debug | 4 ++-- bin/omarchy-upload-log | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/omarchy-debug b/bin/omarchy-debug index 830caa1b..7868acf5 100755 --- a/bin/omarchy-debug +++ b/bin/omarchy-debug @@ -74,8 +74,8 @@ ACTION=$(gum choose "${OPTIONS[@]}") case "$ACTION" in "Upload log") - echo "Uploading debug log to 0x0.st..." - URL=$(curl -sF "file=@$LOG_FILE" -Fexpires=24 https://0x0.st) + echo "Uploading debug log to logs.omarchy.org..." + URL=$(curl -sF "file=@$LOG_FILE" -Fexpires=24 https://logs.omarchy.org) if (( $? == 0 )) && [[ -n $URL ]]; then echo "✓ Log uploaded successfully!" echo "Share this URL:" diff --git a/bin/omarchy-upload-log b/bin/omarchy-upload-log index 7d06ac3b..7cf81ed3 100755 --- a/bin/omarchy-upload-log +++ b/bin/omarchy-upload-log @@ -1,6 +1,6 @@ #!/bin/bash -# omarchy:summary=Upload logs to 0x0.st +# omarchy:summary=Upload logs to logs.omarchy.org # omarchy:args= # omarchy:hidden=true @@ -53,7 +53,7 @@ install) exit 1 fi - echo "Uploading installation log to 0x0.st..." + echo "Uploading installation log to logs.omarchy.org..." ;; this-boot) @@ -66,7 +66,7 @@ this-boot) exit 1 fi - echo "Uploading current boot logs to 0x0.st..." + echo "Uploading current boot logs to logs.omarchy.org..." ;; last-boot) @@ -79,7 +79,7 @@ last-boot) exit 1 fi - echo "Uploading previous boot logs to 0x0.st..." + echo "Uploading previous boot logs to logs.omarchy.org..." ;; installed|system-info) @@ -98,7 +98,7 @@ installed|system-info) exit 1 fi - echo "Uploading system information to 0x0.st..." + echo "Uploading system information to logs.omarchy.org..." ;; *) @@ -113,7 +113,7 @@ esac echo "" -URL=$(curl -sF "file=@$TEMP_LOG" -Fexpires=24 https://0x0.st) +URL=$(curl -sF "file=@$TEMP_LOG" -Fexpires=24 https://logs.omarchy.org) if (( $? == 0 )) && [[ -n $URL ]]; then echo "✓ Log uploaded successfully!" From 92246a50fbfef71dfb1ec7e1d4276b3a927fc49d Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sat, 6 Jun 2026 13:25:53 -0400 Subject: [PATCH 7/7] Enable plugin hot reloading for add / remove --- bin/omarchy-plugin | 24 +++++++- bin/omarchy-plugin-add | 19 +++++- bin/omarchy-plugin-remove | 3 +- bin/omarchy-plugin-update | 3 +- default/omarchy-skill/SKILL.md | 2 +- docs/omarchy-shell.md | 4 +- shell/README.md | 4 +- shell/plugins/panels/tailscale/README.md | 2 +- shell/services/PluginRegistry.qml | 9 ++- shell/shell.qml | 77 +++++++++++++++++++++--- 10 files changed, 126 insertions(+), 21 deletions(-) diff --git a/bin/omarchy-plugin b/bin/omarchy-plugin index bb411ef2..99c2b46a 100755 --- a/bin/omarchy-plugin +++ b/bin/omarchy-plugin @@ -94,6 +94,21 @@ require_command() { command -v "$command_name" >/dev/null 2>&1 || fail "$command_name is required" } +plugin_discovered() { + local id="$1" plugins + plugins=$(omarchy-shell shell listPlugins 2>/dev/null) || return 1 + jq -e --arg id "$id" 'any(.[]; .id == $id)' <<<"$plugins" >/dev/null 2>&1 +} + +wait_for_plugin_discovery() { + local id="$1" attempt + for (( attempt = 0; attempt < 40; attempt++ )); do + plugin_discovered "$id" && return 0 + sleep 0.05 + done + return 1 +} + section_valid() { [[ $1 =~ ^(left|center|right)$ ]] } @@ -181,7 +196,14 @@ plugin_enabled() { id=$(canonical_widget_id "$id") omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true - omarchy-shell shell setPluginEnabled "$id" "$enabled" || fail "could not update plugin; is omarchy-shell running?" + if [[ $enabled == "true" ]]; then + require_command jq + wait_for_plugin_discovery "$id" || fail "plugin '$id' was not discovered; is omarchy-shell running?" + fi + + local result + result=$(omarchy-shell shell setPluginEnabled "$id" "$enabled") || fail "could not update plugin; is omarchy-shell running?" + [[ $result == "ok" ]] || fail "plugin '$id' is not known; run: omarchy plugin rescan" if [[ $enabled == "true" && $# -gt 0 ]]; then mutate_bar move --id "$id" "$@" diff --git a/bin/omarchy-plugin-add b/bin/omarchy-plugin-add index 0b1048bf..a3324080 100755 --- a/bin/omarchy-plugin-add +++ b/bin/omarchy-plugin-add @@ -29,6 +29,21 @@ interactive() { [[ -t 0 && -t 1 ]] } +plugin_discovered() { + local id="$1" plugins + plugins=$(omarchy-shell shell listPlugins 2>/dev/null) || return 1 + jq -e --arg id "$id" 'any(.[]; .id == $id)' <<<"$plugins" >/dev/null 2>&1 +} + +wait_for_plugin_discovery() { + local id="$1" attempt + for (( attempt = 0; attempt < 40; attempt++ )); do + plugin_discovered "$id" && return 0 + sleep 0.05 + done + return 1 +} + PLUGIN_ID="" FROM_SOURCE="" ENABLE_AFTER="" # "", "true", or "false" @@ -280,7 +295,9 @@ if [[ -z $ENABLE_AFTER ]]; then fi if [[ $ENABLE_AFTER == true ]]; then - if omarchy-shell shell setPluginEnabled "$m_id" true >/dev/null 2>&1; then + if ! wait_for_plugin_discovery "$m_id"; then + echo "Could not enable $m_id because omarchy-shell did not discover it yet. Enable later with: omarchy plugin enable $m_id" >&2 + elif result=$(omarchy-shell shell setPluginEnabled "$m_id" true) && [[ $result == "ok" ]]; then echo "Enabled $m_id." if [[ $m_kinds == *bar-widget* ]]; then echo "It was added to the bar; arrange it with: omarchy plugin bar move $m_id --section " diff --git a/bin/omarchy-plugin-remove b/bin/omarchy-plugin-remove index 26a2e77c..0fe78584 100755 --- a/bin/omarchy-plugin-remove +++ b/bin/omarchy-plugin-remove @@ -102,6 +102,5 @@ fi omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true if [[ $was_enabled == "true" ]]; then - echo "Plugin was enabled; restart the shell to fully unload it:" - echo " omarchy restart shell" + echo "Plugin was enabled and was unloaded from omarchy-shell." fi diff --git a/bin/omarchy-plugin-update b/bin/omarchy-plugin-update index a8bb43b0..ee326231 100755 --- a/bin/omarchy-plugin-update +++ b/bin/omarchy-plugin-update @@ -208,8 +208,7 @@ done if ((any_enabled)); then echo - echo "Updated plugins that were enabled may need a shell restart to fully reload:" - echo " omarchy restart shell" + echo "Updated enabled plugins were hot-reloaded by omarchy-shell." fi exit $rc diff --git a/default/omarchy-skill/SKILL.md b/default/omarchy-skill/SKILL.md index bb35d3e9..a7293aae 100644 --- a/default/omarchy-skill/SKILL.md +++ b/default/omarchy-skill/SKILL.md @@ -200,7 +200,7 @@ cp ~/.config/hypr/bindings.conf ~/.config/hypr/bindings.conf.bak.$(date +%s) # 4. Apply changes # - Hyprland: auto-reloads on save, but MUST validate with `hyprctl reload` and `hyprctl configerrors` -# - Omarchy shell: shell.json hot-reloads; use `omarchy-restart-shell` for plugin/widget code changes +# - Omarchy shell: shell.json hot-reloads; use `omarchy plugin rescan` for plugin/widget code changes # - Launcher: restart with `omarchy restart shell` # - Terminals: MUST restart with `omarchy restart terminal` ``` diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index 157da7c2..87c50af4 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -78,9 +78,9 @@ individual plugins (`bar`, `image-selector`, …). | `hide ` | close a previously-summoned | | `toggle ` | summon if closed, hide if open | | `call ` | call an already-loaded plugin | -| `rescanPlugins` | re-walk plugin dirs | +| `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code | | `reloadConfig` | reload shell.json | -| `setPluginEnabled <"true"\|…>` | flip enabled bit | +| `setPluginEnabled <"true"\|…>` | flip enabled bit (`ok` / `unknown`) | | `listPlugins` | JSON of every discovered plugin | `setPluginEnabled` takes a string; only literal `"true"` enables. diff --git a/shell/README.md b/shell/README.md index 3e886d9d..c5adfdc0 100644 --- a/shell/README.md +++ b/shell/README.md @@ -172,9 +172,9 @@ running a separate Quickshell instance. | `hide ` | — | close a previously-summoned plugin | | `toggle ` | — | summon if closed, hide if open | | `call ` | string | call a method on an already-loaded plugin | -| `rescanPlugins` | — | re-walk plugin dirs and pick up new/changed manifests | +| `rescanPlugins` | — | re-walk plugin dirs and hot-reload plugin code | | `reloadConfig` | `ok` | reload `~/.config/omarchy/shell.json` | -| `setPluginEnabled ` | — | flip the persisted enabled bit (see note) | +| `setPluginEnabled ` | `ok` / `unknown` | flip the persisted enabled bit (see note) | | `listPlugins` | JSON | every discovered plugin (id, name, kinds, enabled) | Direct invocation: diff --git a/shell/plugins/panels/tailscale/README.md b/shell/plugins/panels/tailscale/README.md index e4bf8a03..955759e7 100644 --- a/shell/plugins/panels/tailscale/README.md +++ b/shell/plugins/panels/tailscale/README.md @@ -35,4 +35,4 @@ Renders the Tailscale mark natively as a theme-colored 3×3 dot grid, matching t ## Add to the bar -This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy plugin bar add omarchy.tailscale`, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json` and restart the shell. +This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy plugin bar add omarchy.tailscale`, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json`; the shell reloads `shell.json` automatically. diff --git a/shell/services/PluginRegistry.qml b/shell/services/PluginRegistry.qml index b7637248..f4ca0378 100644 --- a/shell/services/PluginRegistry.qml +++ b/shell/services/PluginRegistry.qml @@ -26,6 +26,7 @@ QtObject { property bool scanning: false signal pluginsChanged() + signal scanFinished() signal pluginLoadFailed(string id, string error) // ---------------------------------------------------------------- helpers @@ -157,9 +158,13 @@ QtObject { var key = Util.canonicalWidgetId(String(id)) if (!shellConfigMutator) { console.warn("PluginRegistry.setEnabled called before shellConfigMutator wired") - return + return false } var manifest = installedPlugins[key] + if (value && !manifest) { + console.warn("PluginRegistry.setEnabled: unknown plugin " + key) + return false + } var isBarOption = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1 var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1 shellConfigMutator(function(config) { @@ -196,6 +201,7 @@ QtObject { }) registryRevision++ pluginsChanged() + return true } // ---------------------------------------------------------------- scanning @@ -269,6 +275,7 @@ QtObject { registryRevision++ scanning = false pluginsChanged() + scanFinished() } property Process scanProcess: Process { diff --git a/shell/shell.qml b/shell/shell.qml index 095f86be..2bac9598 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -54,6 +54,8 @@ ShellRoot { property var defaultsConfig: builtinShellConfig property var shellConfig: builtinShellConfig property bool suppressUserReload: false + property bool pluginReloading: false + property bool pluginReloadPending: false onShellConfigChanged: { if (failedBarId !== "") failedBarId = "" @@ -245,11 +247,11 @@ ShellRoot { Loader { id: pluginBarLoader - active: shell.activeBarId !== shell.defaultBarId && shell.activeBarSourceUrl !== "" + active: !shell.pluginReloading && shell.activeBarId !== shell.defaultBarId && shell.activeBarSourceUrl !== "" source: shell.activeBarId !== shell.defaultBarId ? shell.activeBarSourceUrl : "" asynchronous: true onLoaded: shell.configureBar(item, shell.activeBarManifest) - onActiveChanged: if (!active && shell.activeBarId === shell.defaultBarId) shell.bar = null + onActiveChanged: if (!active) shell.bar = null onStatusChanged: { if (status === Loader.Error) { var detail = errorString && errorString() ? errorString() : "" @@ -344,9 +346,17 @@ ShellRoot { } } + function unloadPluginServices() { + for (var existingId in _services) { + var inst = _services[existingId] + if (inst && typeof inst.destroy === "function") inst.destroy() + } + _services = ({}) + } + Connections { target: shell.pluginRegistry - function onPluginsChanged() { shell._syncServices() } + function onPluginsChanged() { if (!shell.pluginReloading) shell._syncServices() } } // Writes inline settings to a bar layout entry or top-level plugin entry in @@ -484,6 +494,14 @@ ShellRoot { panelLoaders = next } + function unloadPanels() { + for (var id in panelLoaders) hide(id) + panelEntries = [] + panelLoaders = ({}) + pendingPayloads = ({}) + openPanelIds = ({}) + } + function deliverIfLoaded(pluginId) { var loader = panelLoaders[pluginId] if (!loader || !loader.item) return @@ -549,7 +567,7 @@ ShellRoot { Connections { target: shell.pluginRegistry - function onPluginsChanged() { shell.panelEntries = shell.computePanelEntries() } + function onPluginsChanged() { if (!shell.pluginReloading) shell.panelEntries = shell.computePanelEntries() } } Instantiator { @@ -606,7 +624,7 @@ ShellRoot { // widgets use the same first-party manifest contract as third-party widgets. Connections { target: shell.pluginRegistry - function onPluginsChanged() { shell.syncPluginWidgets() } + function onPluginsChanged() { if (!shell.pluginReloading) shell.syncPluginWidgets() } } property var pluginWidgetComponents: ({}) @@ -670,6 +688,49 @@ ShellRoot { } } + function unloadPluginWidgets() { + for (var id in pluginWidgetComponents) shell.barWidgetRegistry.unregister(id) + pluginWidgetComponents = ({}) + } + + function reloadPlugins() { + if (shell.pluginReloading || shell.pluginRegistry.scanning) { + shell.pluginReloadPending = true + return + } + shell.pluginReloading = true + shell.unloadPanels() + shell.unloadPluginServices() + shell.unloadPluginWidgets() + Qt.callLater(shell.finishPluginReload) + } + + function finishPluginReload() { + if (!shell.pluginReloading) return + if (shell.pluginRegistry.scanning) { + shell.pluginReloadPending = true + return + } + if (typeof Qt.clearComponentCache === "function") Qt.clearComponentCache() + shell.pluginRegistry.rescan() + } + + Connections { + target: shell.pluginRegistry + function onScanFinished() { + if (shell.pluginReloadPending) { + shell.pluginReloadPending = false + shell.pluginReloading = false + Qt.callLater(shell.reloadPlugins) + return + } + shell.pluginReloading = false + shell._syncServices() + shell.panelEntries = shell.computePanelEntries() + shell.syncPluginWidgets() + } + } + function loadPluginWidget(registryKey, url, meta) { var comp = Qt.createComponent(url, Component.Asynchronous) function finalize() { @@ -712,7 +773,7 @@ ShellRoot { } function rescanPlugins(): void { - shell.pluginRegistry.rescan() + shell.reloadPlugins() } function reloadConfig(): string { @@ -720,8 +781,8 @@ ShellRoot { return "ok" } - function setPluginEnabled(id: string, enabled: string): void { - shell.pluginRegistry.setEnabled(id, enabled === "true") + function setPluginEnabled(id: string, enabled: string): string { + return shell.pluginRegistry.setEnabled(id, enabled === "true") ? "ok" : "unknown" } function listPlugins(): string {