From 1f10c78c6a2580a3ee0a8d9bbec9911daad3a09c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 1 Aug 2026 15:29:54 -0500 Subject: [PATCH] Patch settings-only bar config changes in place instead of rebuilding every widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A shell.json write used to reassign the whole layout, and the module Repeaters recreate every delegate when their array model changes — so toggling an inline widget setting (battery percentage, clock format, tray pinning) tore down and rebuilt every widget on every monitor, closing any open panel along the way. When the layout structure is unchanged, hand the new settings to the running widgets instead. Co-Authored-By: Claude Fable 5 --- shell/plugins/bar/Bar.qml | 27 ++++++++++++++++++++- shell/plugins/bar/BarModel.js | 37 +++++++++++++++++++++++++++++ test/shell.d/bar-test.sh | 44 +++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 867119c7..c5be1ff6 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -350,10 +350,35 @@ Item { position = normalizePosition(config.position) setRequestedTransparency(config.transparent === true) centerAnchor = Util.canonicalWidgetId(config.centerAnchor || "") - layoutConfig = normalizeLayout(config.layout) + + // layoutEntries feeds plain JS arrays to the module Repeaters, and QML + // cannot diff those: reassigning layoutConfig rebuilds every widget on + // every monitor. When a shell.json write only changed inline widget + // settings, patch the live layout and running widgets in place instead. + var next = normalizeLayout(config.layout) + var delta = BarModel.inlineSettingsDelta(layoutConfig, next) + if (delta) { + applySettingsDelta(delta) + return + } + layoutConfig = next barConfigSerial++ } + function applySettingsDelta(delta) { + for (var i = 0; i < delta.length; i++) { + var change = delta[i] + layoutConfig[change.region][change.index] = change.entry + var settings = entrySettings(change.entry) + for (var s = 0; s < moduleSlots.length; s++) { + var slot = moduleSlots[s] + if (!slot || slot.region !== change.region || slot.moduleName !== entryId(change.entry)) continue + var item = slot.activeItem + if (item && "settings" in item) item.settings = settings + } + } + } + onBarConfigChanged: applyBarConfig() function layoutEntries(region) { diff --git a/shell/plugins/bar/BarModel.js b/shell/plugins/bar/BarModel.js index e511f20a..00d0ea6a 100644 --- a/shell/plugins/bar/BarModel.js +++ b/shell/plugins/bar/BarModel.js @@ -65,6 +65,42 @@ function entriesAfter(entries, name) { return index === -1 ? [] : entries.slice(index + 1) } +// A shell.json write that only changes inline widget settings (the battery +// percentage toggle, a clock format change) must not rebuild the bar. +// Compare two normalized layouts: when the structure is unchanged — same +// entry ids in the same order per region — return the settings-only changes +// as {region, index, entry}. Return null when the change is structural, or +// touches an entry a live settings push cannot safely reach: custom modules +// read their entry directly rather than an injected settings property, and +// a duplicated id makes the push ambiguous. +function inlineSettingsDelta(current, next) { + if (!isPlainObject(current) || !isPlainObject(next)) return null + var regions = ["left", "center", "right"] + var counts = {} + for (var r = 0; r < regions.length; r++) { + var entries = Array.isArray(next[regions[r]]) ? next[regions[r]] : [] + for (var i = 0; i < entries.length; i++) { + var id = entryId(entries[i]) + counts[id] = (counts[id] || 0) + 1 + } + } + var changes = [] + for (var s = 0; s < regions.length; s++) { + var region = regions[s] + var a = Array.isArray(current[region]) ? current[region] : [] + var b = Array.isArray(next[region]) ? next[region] : [] + if (a.length !== b.length) return null + for (var j = 0; j < a.length; j++) { + if (entryId(a[j]) !== entryId(b[j])) return null + if (JSON.stringify(a[j]) === JSON.stringify(b[j])) continue + if (customModuleType(a[j]) || customModuleType(b[j])) return null + if (counts[entryId(b[j])] > 1) return null + changes.push({ region: region, index: j, entry: b[j] }) + } + } + return changes +} + function expandPath(value, home) { var path = String(value || "") if (path === "") return "" @@ -162,6 +198,7 @@ if (typeof module !== "undefined") { entryIndex: entryIndex, entriesBefore: entriesBefore, entriesAfter: entriesAfter, + inlineSettingsDelta: inlineSettingsDelta, expandPath: expandPath, customModuleSafeName: customModuleSafeName, customModuleType: customModuleType, diff --git a/test/shell.d/bar-test.sh b/test/shell.d/bar-test.sh index 6819f7d5..6a7da3a1 100644 --- a/test/shell.d/bar-test.sh +++ b/test/shell.d/bar-test.sh @@ -122,6 +122,50 @@ const entries = [{ id: 'a' }, { id: 'omarchy.tray' }, { id: 'b' }] assertDeepEqual(bar.pinTrayToInner(entries, 'left').map(bar.entryId), ['a', 'b', 'omarchy.tray'], 'bar pins tray to left inner edge') assertDeepEqual(bar.pinTrayToInner(entries, 'right').map(bar.entryId), ['omarchy.tray', 'a', 'b'], 'bar pins tray to right inner edge') +// A settings-only shell.json write must patch the live bar, not rebuild it: +// the module Repeaters recreate every widget when their array model changes. +const settingsLayout = { left: [{ id: 'omarchy.power' }], center: [{ id: 'omarchy.clock', format: 'HH:mm' }], right: [] } +assertDeepEqual( + bar.inlineSettingsDelta(settingsLayout, { left: [{ id: 'omarchy.power', showPercentage: true }], center: [{ id: 'omarchy.clock', format: 'HH:mm' }], right: [] }), + [{ region: 'left', index: 0, entry: { id: 'omarchy.power', showPercentage: true } }], + 'bar reports a settings-only change as an inline delta' +) +assertDeepEqual( + bar.inlineSettingsDelta(settingsLayout, JSON.parse(JSON.stringify(settingsLayout))), + [], + 'bar reports an unchanged layout as an empty delta' +) +assertEqual( + bar.inlineSettingsDelta(settingsLayout, { left: [{ id: 'omarchy.clock', format: 'HH:mm' }], center: [{ id: 'omarchy.power' }], right: [] }), + null, + 'bar treats reordered entries as structural' +) +assertEqual( + bar.inlineSettingsDelta(settingsLayout, { left: [{ id: 'omarchy.power' }, { id: 'omarchy.battery' }], center: settingsLayout.center, right: [] }), + null, + 'bar treats added entries as structural' +) +assertEqual( + bar.inlineSettingsDelta( + { left: [{ id: 'local.status', exec: 'date' }], center: [], right: [] }, + { left: [{ id: 'local.status', exec: 'uptime' }], center: [], right: [] } + ), + null, + 'bar rebuilds for custom modules, which read their entry directly' +) +assertEqual( + bar.inlineSettingsDelta( + { left: [{ id: 'x' }], center: [], right: [{ id: 'x' }] }, + { left: [{ id: 'x', a: 1 }], center: [], right: [{ id: 'x' }] } + ), + null, + 'bar rebuilds when a changed id appears more than once in the layout' +) +assert( + /BarModel\.inlineSettingsDelta\(layoutConfig, next\)/.test(barSource), + 'bar consults the inline settings delta before rebuilding the layout' +) + assertEqual(bar.moduleString({ id: 'custom', label: 42 }, 'label', 'fallback'), '42', 'bar stringifies module settings') assertEqual(bar.entryIndex(entries, 'b'), 2, 'bar finds entry indexes') assertDeepEqual(bar.entriesBefore(entries, 'b').map(bar.entryId), ['a', 'omarchy.tray'], 'bar returns entries before target')