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')