Twenty workspaces plus dividers was ~912px of bar. - Label each workspace with `id % 10`, so 11-15 read "1 2 3 4 5", 1-10 read "1 2 3 4 5 6 7 8 9 0" and 16-20 read "6 7 8 9 0". Beyond the width this buys, it makes the label the key you press: the bindings are SUPER+<n> for 1-10, SUPER+CTRL+1..5 for 11-15 and SUPER+CTRL+6..0 for 16-20, so the digit shown is the digit in that workspace's shortcut. - Narrow the buttons to space(13) now every label is one character, drop the inter-button spacing to 0, and set the font a step below body. The group dividers keep their wider gaps — that separation is what distinguishes the three monitors. Net ~560px, down from ~912px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
176 lines
6.8 KiB
QML
176 lines
6.8 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell.Hyprland
|
|
import qs.Commons
|
|
import qs.Ui
|
|
|
|
BarWidget {
|
|
id: root
|
|
moduleName: "pastilhas.workspaces"
|
|
|
|
// Workspaces grouped in physical monitor order, mirroring the pinning in
|
|
// ~/.config/hypr/workspaces.lua:
|
|
//
|
|
// HDMI-A-1 (left) 11-15
|
|
// DP-1 (centre) 1-10
|
|
// HDMI-A-2 (right) 16-20
|
|
//
|
|
// Stock built its list from occupancy and hard-capped it at 10, so 11-20
|
|
// could never appear no matter how they were assigned. This is a fixed
|
|
// layout instead: every workspace is always shown, in the order the monitors
|
|
// physically sit, so the bar reads as a map of the desk.
|
|
readonly property var workspaceGroups: [
|
|
[11, 12, 13, 14, 15],
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
[16, 17, 18, 19, 20]
|
|
]
|
|
|
|
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
|
|
}
|
|
|
|
// Flattened to a single model with separator markers between the groups, so
|
|
// one Repeater can lay out buttons and dividers in order.
|
|
function entries() {
|
|
var out = []
|
|
for (var g = 0; g < workspaceGroups.length; g++) {
|
|
if (g > 0) out.push({ separator: true, id: -g })
|
|
var group = workspaceGroups[g]
|
|
for (var i = 0; i < group.length; i++) out.push({ separator: false, id: group[i] })
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Which monitor each group is pinned to, parallel to workspaceGroups above.
|
|
readonly property var groupMonitors: ["HDMI-A-1", "DP-1", "HDMI-A-2"]
|
|
|
|
function monitorForWorkspace(id) {
|
|
for (var g = 0; g < workspaceGroups.length; g++) {
|
|
if (workspaceGroups[g].indexOf(id) !== -1) return groupMonitors[g]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Clicking a workspace that lives on another monitor should change what that
|
|
// monitor shows without dragging the pointer across the desk. A plain
|
|
// `focus({ workspace })` can't do that: Omarchy sets
|
|
// cursor.warp_on_change_workspace = 1, so the pointer teleports to whichever
|
|
// screen the workspace is pinned to.
|
|
//
|
|
// So for a cross-monitor click: suppress warping, hop focus to the target
|
|
// monitor, change its workspace, hop back, restore warping. It goes through
|
|
// `hyprctl eval` rather than `dispatch` because dispatch takes exactly one
|
|
// dispatcher object, and because `hyprctl keyword` no longer works under the
|
|
// Lua parser ("keyword can't work with non-legacy parsers. Use eval.").
|
|
//
|
|
// Clicking a workspace on the monitor you're already on keeps stock
|
|
// behaviour — plain focus, cursor unaffected anyway.
|
|
function focusWorkspace(id) {
|
|
if (!root.bar) return
|
|
|
|
var target = monitorForWorkspace(id)
|
|
var monitor = Hyprland.focusedMonitor
|
|
var current = monitor ? String(monitor.name || "") : ""
|
|
|
|
if (target === "" || current === "" || target === current) {
|
|
root.bar.run("hyprctl dispatch " + Util.shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })"))
|
|
return
|
|
}
|
|
|
|
var lua =
|
|
'hl.config({ cursor = { warp_on_change_workspace = 0, no_warps = true } }) ' +
|
|
'hl.dispatch(hl.dsp.focus({ monitor = "' + target + '" })) ' +
|
|
'hl.dispatch(hl.dsp.focus({ workspace = "' + id + '" })) ' +
|
|
'hl.dispatch(hl.dsp.focus({ monitor = "' + current + '" })) ' +
|
|
'hl.config({ cursor = { warp_on_change_workspace = 1, no_warps = false } })'
|
|
|
|
root.bar.run("hyprctl eval " + Util.shellQuote(lua))
|
|
}
|
|
|
|
readonly property real trailingGap: root.vertical ? 0 : Style.spaceReal(1.5)
|
|
readonly property color lineColor: root.bar ? root.bar.foreground : Color.foreground
|
|
|
|
// Spacing between adjacent workspace buttons, and the extra breathing room
|
|
// added on each side of a group divider. The divider sits in the same
|
|
// GridLayout as the buttons, so without this it would inherit the ordinary
|
|
// inter-button spacing and the three groups would read as one run.
|
|
readonly property real itemSpacing: 0
|
|
readonly property real groupGap: Style.space(5)
|
|
|
|
implicitWidth: grid.implicitWidth + trailingGap
|
|
implicitHeight: grid.implicitHeight
|
|
|
|
GridLayout {
|
|
id: grid
|
|
anchors.fill: parent
|
|
anchors.rightMargin: root.trailingGap
|
|
columns: root.vertical ? 1 : root.entries().length
|
|
columnSpacing: root.vertical ? 0 : root.itemSpacing
|
|
rowSpacing: root.vertical ? Style.space(2) : 0
|
|
|
|
Repeater {
|
|
model: root.entries()
|
|
|
|
Loader {
|
|
required property var modelData
|
|
|
|
Layout.alignment: Qt.AlignCenter
|
|
// Widen the gap either side of a divider so the groups read as three
|
|
// distinct blocks rather than one long row.
|
|
Layout.leftMargin: (modelData.separator && !root.vertical) ? root.groupGap : 0
|
|
Layout.rightMargin: (modelData.separator && !root.vertical) ? root.groupGap : 0
|
|
Layout.topMargin: (modelData.separator && root.vertical) ? root.groupGap : 0
|
|
Layout.bottomMargin: (modelData.separator && root.vertical) ? root.groupGap : 0
|
|
sourceComponent: modelData.separator ? separatorComponent : workspaceComponent
|
|
|
|
Component {
|
|
id: separatorComponent
|
|
|
|
Rectangle {
|
|
implicitWidth: root.vertical ? Math.round(root.barSize * 0.45) : 1
|
|
implicitHeight: root.vertical ? 1 : Math.round(root.barSize * 0.45)
|
|
color: root.lineColor
|
|
opacity: 0.3
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: workspaceComponent
|
|
|
|
WidgetButton {
|
|
readonly property int wsId: modelData.id
|
|
readonly property var workspace: root.workspaceById(wsId)
|
|
readonly property bool occupied: workspace !== null && workspace.toplevels.values.length > 0
|
|
readonly property bool focused: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === wsId
|
|
|
|
bar: root.bar
|
|
// Single digit per workspace: id mod 10, so 11-15 read "1 2 3 4 5",
|
|
// 1-10 read "1 2 3 4 5 6 7 8 9 0", and 16-20 read "6 7 8 9 0".
|
|
//
|
|
// That is not just for width — it makes the label the key you press.
|
|
// The bindings in ~/.config/hypr/bindings.lua are SUPER+<n> for 1-10,
|
|
// SUPER+CTRL+1..5 for 11-15 and SUPER+CTRL+6..0 for 16-20, so the
|
|
// digit shown is the digit in the shortcut for that workspace.
|
|
text: focused ? "" : String(wsId % 10)
|
|
opacity: occupied || focused ? 1 : 0.5
|
|
horizontalMargin: 6
|
|
verticalPadding: 6
|
|
// Twenty workspaces plus dividers is a lot of bar. Every label is a
|
|
// single character now, so these can be narrower still than the
|
|
// stock space(20), with a font a step below body.
|
|
fontSize: Style.font.bodySmall
|
|
fixedWidth: root.vertical ? root.barSize : Style.space(13)
|
|
fixedHeight: root.barSize
|
|
onPressed: function() { root.focusWorkspace(wsId) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|