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+ 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) } } } } } } }