Files
omarchy/shell/plugins/bar/widgets/Workspaces.qml
T
David Heinemeier HanssonandClaude Opus 5.5 e71a816e90 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>
2026-10-11 23:03:30 +09:00

167 lines
4.8 KiB
QML

import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Commons
import qs.Ui
BarWidget {
id: root
moduleName: "omarchy.workspaces"
function workspaceById(id) {
var values = Hyprland.workspaces.values
for (var i = 0; i < values.length; i++) {
if (values[i].id === id) return values[i]
}
return null
}
function workspaceIds() {
var ids = [1, 2, 3, 4, 5]
var values = Hyprland.workspaces.values
for (var i = 0; i < values.length; i++) {
var id = values[i].id
if (id > 0 && id <= 10 && ids.indexOf(id) === -1) ids.push(id)
}
ids.sort(function(left, right) { return left - right })
return ids
}
function focusWorkspace(id) {
if (!root.bar) return
root.bar.run("hyprctl dispatch " + Util.shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })"))
}
// The focused workspace shows its layout: a square tiles, a circle scrolls,
// and a triangle floats. Clicking it cycles to the next one, as Super + L does.
property string focusedMode: "dwindle"
property bool modeStale: false
function modeGlyph(mode) {
if (mode === "scrolling") return "\uDB81\uDF65"
if (mode === "floating") return "\uDB81\uDD36"
return "\uDB85\uDCFB"
}
// 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")
}
Component.onCompleted: refresh()
Connections {
target: Hyprland
function onFocusedWorkspaceChanged() { root.showFocusedMode() }
function onRawEvent(event) {
if (event && String(event.name) === "configreloaded") root.refresh()
}
}
Process {
id: modeProc
stdout: StdioCollector {
onStreamFinished: {
var mode = text.trim()
// 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
// Deferred: the process still reports running while it signals exit.
Qt.callLater(root.showFocusedMode)
}
}
// Super + L and the default layout change the mode without a focus change,
// so they ask every bar to read it again.
ShellIpc {
target: "omarchy.workspaces"
function refresh(): void { root.broadcast("refresh") }
}
readonly property real trailingGap: root.vertical ? 0 : Style.spaceReal(1.5)
implicitWidth: grid.implicitWidth + trailingGap
implicitHeight: grid.implicitHeight
GridLayout {
id: grid
anchors.fill: parent
anchors.rightMargin: root.trailingGap
columns: root.vertical ? 1 : root.workspaceIds().length
columnSpacing: root.vertical ? 0 : Style.space(1)
rowSpacing: root.vertical ? Style.space(2) : 0
Repeater {
model: root.workspaceIds()
WidgetButton {
required property int modelData
readonly property var workspace: root.workspaceById(modelData)
readonly property bool occupied: workspace !== null && workspace.toplevels.values.length > 0
readonly property bool focused: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === modelData
bar: root.bar
text: focused ? root.modeGlyph(root.focusedMode) : (modelData === 10 ? "0" : String(modelData))
opacity: occupied || focused ? 1 : 0.5
horizontalMargin: 6
verticalPadding: 6
fixedWidth: root.vertical ? root.barSize : Style.space(20)
fixedHeight: root.barSize
onPressed: function() {
if (focused) root.cycleLayout()
else root.focusWorkspace(modelData)
}
}
}
}
}