* 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>
167 lines
4.8 KiB
QML
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|