Cache workspace layouts instead of running a script on every switch (#15028)
* Cache workspace layouts instead of running a script on every switch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the workspace layout cache by running its logic, not matching its source Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the fake layout read running while its exit handler runs Addresses Codex review: clearing running first let an undeferred, broken retry pass the cache tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1b1cc5ecb5
commit
e71a816e90
2 files changed
+109
-5
No files matched your search
@@ -48,14 +48,43 @@ BarWidget {
|
||||
return "\uDB85\uDCFB"
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
// A layout only changes through the layout commands, which ask for a refresh,
|
||||
// or a config reload, so each workspace's mode is read once and cached rather
|
||||
// than running the script on every workspace switch.
|
||||
property var modeCache: ({})
|
||||
property int modeGeneration: 0
|
||||
property int modeProcWorkspace: -1
|
||||
property int modeProcGeneration: 0
|
||||
|
||||
function focusedWorkspaceId() {
|
||||
return Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id : -1
|
||||
}
|
||||
|
||||
function showFocusedMode() {
|
||||
var mode = root.modeCache[root.focusedWorkspaceId()]
|
||||
if (mode !== undefined) root.focusedMode = mode
|
||||
else readMode()
|
||||
}
|
||||
|
||||
function readMode() {
|
||||
if (modeProc.running) {
|
||||
root.modeStale = true
|
||||
return
|
||||
}
|
||||
var id = root.focusedWorkspaceId()
|
||||
if (id === -1) return
|
||||
root.modeProcWorkspace = id
|
||||
root.modeProcGeneration = root.modeGeneration
|
||||
modeProc.command = ["omarchy-hyprland-workspace-layout-current", String(id)]
|
||||
modeProc.running = true
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
root.modeCache = ({})
|
||||
root.modeGeneration++
|
||||
readMode()
|
||||
}
|
||||
|
||||
function cycleLayout() {
|
||||
if (root.bar) root.bar.run("omarchy-hyprland-workspace-layout-toggle")
|
||||
}
|
||||
@@ -64,22 +93,28 @@ BarWidget {
|
||||
|
||||
Connections {
|
||||
target: Hyprland
|
||||
function onFocusedWorkspaceChanged() { root.refresh() }
|
||||
function onFocusedWorkspaceChanged() { root.showFocusedMode() }
|
||||
function onRawEvent(event) {
|
||||
if (event && String(event.name) === "configreloaded") root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: modeProc
|
||||
command: ["omarchy-hyprland-workspace-layout-current"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var mode = text.trim()
|
||||
if (mode) root.focusedMode = mode
|
||||
// A read that started before a refresh may predate the change.
|
||||
if (!mode || root.modeProcGeneration !== root.modeGeneration) return
|
||||
root.modeCache[root.modeProcWorkspace] = mode
|
||||
if (root.modeProcWorkspace === root.focusedWorkspaceId()) root.focusedMode = mode
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (!root.modeStale) return
|
||||
root.modeStale = false
|
||||
root.refresh()
|
||||
// Deferred: the process still reports running while it signals exit.
|
||||
Qt.callLater(root.showFocusedMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -455,3 +455,72 @@ assert(bar and bar.bar_color == "rgb(223344)", "titlebars missed the theme with
|
||||
fail "titlebars follow the theme when XDG_STATE_HOME points elsewhere"
|
||||
fi
|
||||
pass "titlebars follow the theme when XDG_STATE_HOME points elsewhere"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const source = require('fs').readFileSync(path.join(root, 'shell/plugins/bar/widgets/Workspaces.qml'), 'utf8')
|
||||
assert(source.includes('function onFocusedWorkspaceChanged() { root.showFocusedMode() }'), 'a workspace switch shows the cached layout instead of running the script')
|
||||
assert(source.includes('String(event.name) === "configreloaded") root.refresh()'), 'a config reload drops every cached layout')
|
||||
|
||||
// Run the widget's own cache logic against a fake Hyprland and process.
|
||||
function body(pattern) {
|
||||
const match = source.match(pattern)
|
||||
if (!match) fail('Workspaces.qml cache logic can be extracted', String(pattern))
|
||||
return match[1]
|
||||
}
|
||||
// QML resolves bare calls like readMode() on the widget, so it is passed in too.
|
||||
const args = ['root', 'Hyprland', 'modeProc', 'Qt', 'readMode']
|
||||
const fn = name => new Function(...args, body(new RegExp(`\\n function ${name}\\(\\) \\{([\\s\\S]*?)\\n \\}`)))
|
||||
const streamFinished = new Function(...args, 'text', body(/onStreamFinished: \{([\s\S]*?)\n \}/))
|
||||
const exited = new Function(...args, body(/onExited: \{([\s\S]*?)\n \}/))
|
||||
|
||||
const hyprland = { focusedWorkspace: { id: 1 } }
|
||||
const proc = { running: false, command: null, runs: [] }
|
||||
const deferred = []
|
||||
const qt = { callLater: callback => deferred.push(callback) }
|
||||
const widget = { focusedMode: 'dwindle', modeStale: false, modeCache: {}, modeGeneration: 0, modeProcWorkspace: -1, modeProcGeneration: 0 }
|
||||
for (const name of ['focusedWorkspaceId', 'showFocusedMode', 'readMode', 'refresh']) {
|
||||
const call = fn(name)
|
||||
widget[name] = () => call(widget, hyprland, proc, qt, widget.readMode)
|
||||
}
|
||||
const start = () => { if (proc.running) proc.runs.push(proc.command[1]) }
|
||||
// Quickshell still reports the process running while it signals exit, so a
|
||||
// retry has to be deferred until after the handler returns.
|
||||
const finish = output => {
|
||||
streamFinished(widget, hyprland, proc, qt, widget.readMode, output)
|
||||
exited(widget, hyprland, proc, qt, widget.readMode)
|
||||
proc.running = false
|
||||
while (deferred.length) deferred.shift()()
|
||||
}
|
||||
const focus = id => { hyprland.focusedWorkspace = { id }; widget.showFocusedMode(); start() }
|
||||
|
||||
widget.refresh(); start()
|
||||
finish('scrolling\n')
|
||||
assertEqual(widget.focusedMode, 'scrolling', 'the first read shows the focused layout')
|
||||
focus(2); finish('floating\n')
|
||||
focus(1)
|
||||
assertEqual(widget.focusedMode, 'scrolling', 'returning to a read workspace shows its cached layout')
|
||||
assertDeepEqual(proc.runs, ['1', '2'], 'a cached workspace switch runs no script')
|
||||
|
||||
widget.refresh(); start()
|
||||
assert(proc.running && proc.runs.length === 3, 'a refresh reads the focused layout again')
|
||||
assertDeepEqual(widget.modeCache, {}, 'a refresh drops every cached layout')
|
||||
widget.refresh()
|
||||
assert(widget.modeStale, 'a refresh during a read marks it stale')
|
||||
finish('dwindle\n')
|
||||
assert(widget.modeCache[1] === undefined, 'a read that predates a refresh is not cached')
|
||||
start()
|
||||
assertDeepEqual(proc.runs, ['1', '2', '1', '1'], 'a stale read is retried once it exits')
|
||||
finish('floating\n')
|
||||
assertEqual(widget.focusedMode, 'floating', 'the retried read shows the current layout')
|
||||
|
||||
widget.refresh(); start()
|
||||
focus(2)
|
||||
assert(widget.modeStale, 'switching to an uncached workspace during a read marks it pending')
|
||||
finish('scrolling\n')
|
||||
assertEqual(widget.modeCache[1], 'scrolling', 'the in-flight read still caches its own workspace')
|
||||
assertEqual(widget.focusedMode, 'floating', 'a read for a workspace no longer focused leaves the shown layout alone')
|
||||
start()
|
||||
assertEqual(proc.runs[proc.runs.length - 1], '2', 'the pending read follows the newly focused workspace')
|
||||
finish('dwindle\n')
|
||||
assertEqual(widget.focusedMode, 'dwindle', 'the newly focused workspace shows its own layout')
|
||||
JS
|
||||
Reference in new issue
Block a user