diff --git a/Workspaces.qml b/Workspaces.qml index ec0482a..fbe7000 100644 --- a/Workspaces.qml +++ b/Workspaces.qml @@ -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: Style.space(1) + readonly property real groupGap: itemSpacing * 4 implicitWidth: grid.implicitWidth + trailingGap implicitHeight: grid.implicitHeight @@ -44,28 +109,58 @@ 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 + // Stock rendered 10 as "0" to keep every label one character. With + // 11-20 alongside it, two-digit labels are the norm, so 10 is shown + // as itself. + text: focused ? "󱓻" : String(wsId) + opacity: occupied || focused ? 1 : 0.5 + horizontalMargin: 6 + verticalPadding: 6 + fixedWidth: root.vertical ? root.barSize : Style.space(22) + fixedHeight: root.barSize + onPressed: function() { root.focusWorkspace(wsId) } + } + } } } }