Route panel hotkeys through the shell IPC target so they survive reloads

Bar-widget panels (audio, bluetooth, network, power, monitor) used to be
toggled via their own per-plugin IpcHandler targets. Quickshell resolves
duplicate targets first-handler-wins, so after a plugin or bar reload the
stale handler of the destroyed widget instance kept claiming the target
and the hotkeys went dead.

The shell root's IpcHandler lives outside the reload cycle, so summon,
hide, and toggle now go through `omarchy-shell shell toggle <plugin>`
and the shell routes to the live widget instance via the bar's slot
registry. Panel plugins that are also panel/overlay/menu kinds keep
using the panel loader path. Failures to find a live widget are logged
so a widget missing from the bar layout stays diagnosable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-07 09:17:54 -07:00
co-authored by Claude Fable 5
parent 6bb3cb666b
commit e1cdc9005e
5 changed files with 83 additions and 7 deletions
+38
View File
@@ -419,6 +419,26 @@ ShellRoot {
// the second clobbering the first.
property var pendingPayloads: ({})
// Bar-widget panels (audio, bluetooth, network, power, monitor, etc.)
// are mounted inside the bar, not via the panel loader below. Route
// summon/hide/toggle to the live bar instance so panel hotkeys survive
// plugin/bar reloads: the bar re-creates the widget, while a fixed
// IpcHandler target would go stale ("first handler wins" leaves a
// destroyed instance's handler active and the new one rejected).
function isBarWidgetPanelPlugin(pluginId) {
var plugins = shell.pluginRegistry.installedPlugins
var m = plugins[String(pluginId || "")]
if (!m || !Array.isArray(m.kinds)) return false
if (m.kinds.indexOf("bar-widget") === -1) return false
// Plugins that are also panel/overlay/menu kinds are owned by the
// panel loader (e.g. omarchy.menu); let that path handle them.
var loaderKinds = ["panel", "overlay", "menu"]
for (var i = 0; i < loaderKinds.length; i++) {
if (m.kinds.indexOf(loaderKinds[i]) !== -1) return false
}
return true
}
function summon(pluginId, payloadJson) {
var id = String(pluginId || "")
if (!id) return false
@@ -434,6 +454,13 @@ ShellRoot {
console.warn("summon: plugin not enabled, not summoning:", id)
return false
}
// Bar widgets take no payload; payloadJson is dropped on this path.
if (shell.isBarWidgetPanelPlugin(id)) {
var summoned = shell.bar && typeof shell.bar.summonBarWidget === "function"
&& shell.bar.summonBarWidget(id)
if (!summoned) console.warn("summon: no live bar widget for:", id)
return summoned === true
}
var next = ({})
for (var k in openPanelIds) next[k] = openPanelIds[k]
next[id] = true
@@ -455,6 +482,12 @@ ShellRoot {
function hide(pluginId) {
var id = String(pluginId || "")
if (!id) return false
if (shell.isBarWidgetPanelPlugin(id)) {
var hidden = shell.bar && typeof shell.bar.hideBarWidget === "function"
&& shell.bar.hideBarWidget(id)
if (!hidden) console.warn("hide: no live bar widget for:", id)
return hidden === true
}
invokeIfLoaded(id, "close", null)
if (!openPanelIds[id]) return true
var next = ({})
@@ -465,6 +498,11 @@ ShellRoot {
function isPluginOpen(pluginId) {
var id = String(pluginId || "")
if (shell.isBarWidgetPanelPlugin(id)) {
return shell.bar && typeof shell.bar.isBarWidgetOpen === "function"
? shell.bar.isBarWidgetOpen(id)
: false
}
var loader = panelLoaders[id]
if (loader && loader.item && loader.item.opened !== undefined)
return loader.item.opened === true