From d2e1587ceb3de4a7b7dd9837259355fbbaefec4e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 24 Jul 2026 14:06:17 -0700 Subject: [PATCH] Refresh bar widgets on every monitor, not just one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bar surface is built per monitor, so a widget in the layout is live once per screen — but an IPC target only ever routes to the handler that registered first. `omarchy.indicators refresh` therefore reached a single bar, and since indicators only re-read their state on that signal, the other screens kept showing a stale reminder count, tmux alert, or DND state until the next reload. Clock and system-update refreshes had the same reach. Let the bar resolve every live instance of a widget id and relay the call to all of them. Co-Authored-By: Claude Opus 5 (1M context) --- shell/Ui/BarWidget.qml | 12 ++++++++++++ shell/plugins/bar/Bar.qml | 18 ++++++++++++++++-- shell/plugins/bar/widgets/Clock.qml | 2 +- shell/plugins/bar/widgets/Indicators.qml | 4 +++- shell/plugins/bar/widgets/SystemUpdate.qml | 4 ++-- shell/shell.qml | 5 ++--- 6 files changed, 36 insertions(+), 9 deletions(-) diff --git a/shell/Ui/BarWidget.qml b/shell/Ui/BarWidget.qml index 10e9c324..a0c9584d 100644 --- a/shell/Ui/BarWidget.qml +++ b/shell/Ui/BarWidget.qml @@ -22,6 +22,18 @@ Item { readonly property bool vertical: bar ? bar.vertical : false readonly property int barSize: bar ? bar.barSize : Style.bar.sizeHorizontal + // Run `method` on every live instance of this widget. An IPC target only + // ever routes to one handler, but a bar surface exists per monitor, so the + // instance that owns the target relays the call to its peers — otherwise a + // refresh would land on a single screen and leave the others stale. + function broadcast(method) { + var items = bar && typeof bar.moduleWidgets === "function" + ? bar.moduleWidgets(moduleName) : [root] + for (var i = 0; i < items.length; i++) { + if (items[i] && typeof items[i][method] === "function") items[i][method]() + } + } + // Read a single value from this widget's inline shell.json entry, with a // fallback for missing/null values. Every widget that takes user-tunable // settings needs this; defining it once on the base keeps the wiring diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index a7a9c4be..af40f78b 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -384,11 +384,25 @@ Item { return true } + // Every live instance of a widget id. A bar surface is built per monitor, so + // a widget that appears once in the layout is still live once per screen. + function moduleWidgets(pluginId) { + var id = String(pluginId || "") + var items = [] + if (!id) return items + for (var i = 0; i < moduleSlots.length; i++) { + var slot = moduleSlots[i] + if (!slot || !slot.activeItem || slot.moduleName !== id) continue + items.push(slot.activeItem) + } + return items + } + // Resolve the live bar-widget instance for a plugin id (e.g. "omarchy.bluetooth"). // Only widgets that expose popup open/close methods count; plain indicators // (clock, workspaces, tray) return null. Used by shell.summon/toggle so - // panel hotkeys route through the bar instead of a per-target IpcHandler - // that goes stale when the bar reloads its widget instances. + // panel hotkeys route through the bar instead of a per-target IPC handler + // that only reaches whichever per-monitor instance claimed the target. function findPanelWidget(pluginId) { var id = String(pluginId || "") if (!id) return null diff --git a/shell/plugins/bar/widgets/Clock.qml b/shell/plugins/bar/widgets/Clock.qml index 2b466ed6..60eb6749 100644 --- a/shell/plugins/bar/widgets/Clock.qml +++ b/shell/plugins/bar/widgets/Clock.qml @@ -49,7 +49,7 @@ BarWidget { IpcHandler { target: "omarchy.clock" - function refresh(): void { root.refresh() } + function refresh(): void { root.broadcast("refresh") } } WidgetButton { diff --git a/shell/plugins/bar/widgets/Indicators.qml b/shell/plugins/bar/widgets/Indicators.qml index c07d53f3..9e15c129 100644 --- a/shell/plugins/bar/widgets/Indicators.qml +++ b/shell/plugins/bar/widgets/Indicators.qml @@ -152,6 +152,8 @@ BarWidget { syncActiveIndicatorModel() } + function refresh() { root.refreshRequested() } + onIndicatorEntriesChanged: syncActiveIndicatorOrder() implicitWidth: root.vertical @@ -165,7 +167,7 @@ BarWidget { target: "omarchy.indicators" function refresh(): void { - root.refreshRequested() + root.broadcast("refresh") } } diff --git a/shell/plugins/bar/widgets/SystemUpdate.qml b/shell/plugins/bar/widgets/SystemUpdate.qml index 11642c35..a4f9020f 100644 --- a/shell/plugins/bar/widgets/SystemUpdate.qml +++ b/shell/plugins/bar/widgets/SystemUpdate.qml @@ -28,11 +28,11 @@ BarWidget { target: "omarchy.system-update" function refresh(): void { - root.refresh() + root.broadcast("refresh") } function clear(): void { - root.clear() + root.broadcast("clear") } } diff --git a/shell/shell.qml b/shell/shell.qml index 0d5c20b0..f8b04fb0 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -423,9 +423,8 @@ ShellRoot { // 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). + // plugin/bar reloads: the bar re-creates the widget, while a fixed IPC + // target only ever routes to one of the per-monitor instances. function isBarWidgetPanelPlugin(pluginId) { var plugins = shell.pluginRegistry.installedPlugins var m = plugins[String(pluginId || "")]