2 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 a8b49abd4b Shrink the workspace list and label each with its shortcut digit
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>
2026-08-17 17:32:39 +01:00
pastilhasandClaude Opus 5 e05733448a Per-monitor workspace list with cross-monitor switching
Show every workspace grouped by the monitor it is pinned to, in physical
order, and let a click change another monitor without moving the pointer.

- Fixed layout of all 20 workspaces instead of stock's occupancy-derived
  list, which seeded [1..5] and hard-capped at 10 — 11-20 could never
  appear however they were assigned. Grouped as 11-15 | 1-10 | 16-20,
  matching the pinning in ~/.config/hypr/workspaces.lua, with dividers
  and wider gaps between groups. Workspace 10 renders as "10" rather than
  "0" now that two-digit labels are the norm.

- Clicking a workspace that lives on another monitor switches that
  monitor without dragging the cursor across. Omarchy sets
  cursor.warp_on_change_workspace = 1, so a plain focus({ workspace })
  teleports the pointer to whichever screen the workspace is pinned to.
  Instead: suppress warping, hop focus to the target monitor, change its
  workspace, hop back, restore warping.

  This goes through `hyprctl eval` rather than `dispatch` for two
  reasons. Hyprland 0.56 parses `hyprctl dispatch` as Lua, so it takes
  exactly one dispatcher object — `dispatch workspace 12` fails as
  `hl.dispatch(workspace 12)`. And `hyprctl keyword` no longer works at
  all under that parser: "keyword can't work with non-legacy parsers.
  Use eval." eval takes multiple statements, so the whole sequence runs
  as one call instead of five racing ones.

  Clicking a workspace on the monitor you are already on keeps stock
  behaviour.

The monitor names are specific to this machine (HDMI-A-1 / DP-1 /
HDMI-A-2) and live in workspaceGroups + groupMonitors at the top of the
file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 22:45:52 +01:00
+131 -28
View File
@@ -6,7 +6,24 @@ import qs.Ui
BarWidget {
id: root
moduleName: "omarchy.workspaces"
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
@@ -17,25 +34,73 @@ BarWidget {
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)
// 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] })
}
ids.sort(function(left, right) { return left - right })
return ids
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
root.bar.run("hyprctl dispatch " + Util.shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })"))
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
@@ -44,28 +109,66 @@ BarWidget {
id: grid
anchors.fill: parent
anchors.rightMargin: root.trailingGap
columns: root.vertical ? 1 : root.workspaceIds().length
columnSpacing: root.vertical ? 0 : Style.space(1)
columns: root.vertical ? 1 : root.entries().length
columnSpacing: root.vertical ? 0 : root.itemSpacing
rowSpacing: root.vertical ? Style.space(2) : 0
Repeater {
model: root.workspaceIds()
model: root.entries()
WidgetButton {
required property int modelData
Loader {
required property var 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
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
bar: root.bar
text: focused ? "\uDB85\uDCFB" : (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() { root.focusWorkspace(modelData) }
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) }
}
}
}
}
}