From 1ff343428cdc04f7bc08d597fad50a97c87b21e9 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 20 May 2026 17:51:11 +0200 Subject: [PATCH] Extract workspace switcher widget --- shell/plugins/bar/Bar.qml | 58 +------------------ shell/plugins/bar/README.md | 3 +- shell/plugins/bar/widgets/workspaces.qml | 72 ++++++++++++++++++++++++ shell/plugins/settings/SettingsPanel.qml | 1 - 4 files changed, 75 insertions(+), 59 deletions(-) create mode 100644 shell/plugins/bar/widgets/workspaces.qml diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 0716a0f2..5b36398d 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -1,5 +1,4 @@ import Quickshell -import Quickshell.Hyprland import Quickshell.Io import Quickshell.Services.SystemTray import Quickshell.Wayland @@ -260,7 +259,6 @@ Item { function builtinModuleComponent(name) { switch (String(name)) { case "omarchy": return omarchyModuleComponent - case "workspaces": return workspacesModuleComponent case "clock": return clockModuleComponent case "update": return updateModuleComponent case "indicators": return indicatorsModuleComponent @@ -305,6 +303,7 @@ Item { // Each entry maps a module id to its display metadata; the QML source is // loaded asynchronously via Qt.createComponent. readonly property var firstPartyWidgetMetadata: ({ + "workspaces": { displayName: "Workspaces", description: "Workspace number indicators", category: "Compositor", allowMultiple: false }, "media": { displayName: "Media", description: "MPRIS now-playing with playback controls", category: "Media", allowMultiple: false }, "audioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false, sourceDir: "../panels", sourceName: "Audio" }, "monitorPanel": { displayName: "Display", description: "Brightness slider and laptop display controls", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Monitor" }, @@ -441,30 +440,6 @@ Item { indicatorsRefreshRequested() } - 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 trayIconSource(icon) { var value = String(icon || "") var marker = "?path=" @@ -480,10 +455,6 @@ Item { return item.tooltipTitle || item.title || item.id || "" } - function focusWorkspace(id) { - root.run("hyprctl dispatch " + Util.shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })")) - } - function clockEntry() { var serial = barConfigSerial var sections = ["left", "center", "right"] @@ -741,7 +712,6 @@ Item { Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } } Component { id: omarchyModuleComponent; OmarchyModule {} } - Component { id: workspacesModuleComponent; WorkspacesModule {} } Component { id: clockModuleComponent; ClockModule {} } Component { id: updateModuleComponent; UpdateModule {} } Component { id: indicatorsModuleComponent; IndicatorsModule {} } @@ -1482,32 +1452,6 @@ Item { } } - component WorkspacesModule: GridLayout { - columns: root.vertical ? 1 : root.workspaceIds().length - columnSpacing: root.vertical ? 0 : Style.space(2) - rowSpacing: root.vertical ? Style.space(2) : 0 - - Repeater { - model: root.workspaceIds() - - ModuleButton { - required property int modelData - - property var workspace: root.workspaceById(modelData) - property bool occupied: workspace !== null && workspace.toplevels.values.length > 0 - property bool focused: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === modelData - - text: focused ? "󱓻" : (modelData === 10 ? "0" : String(modelData)) - opacity: occupied || focused ? 1 : 0.5 - horizontalMargin: 6 - verticalPadding: 6 - fixedWidth: root.vertical ? root.barSize : 22 - fixedHeight: root.barSize - onPressed: function() { root.focusWorkspace(modelData) } - } - } - } - component ClockModule: ModuleButton { text: root.clockText() horizontalMargin: 8.75 diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index 1261056d..9a1cf6ec 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -54,6 +54,7 @@ Example `shell.json` (bar subtree only shown): | Name | What it does | Interactions | |---|---|---| +| `workspaces` | Hyprland workspace switcher | left = focus workspace | | `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup | | `daytime` | Day/time label with date tooltip | hover = date | | `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND | @@ -73,7 +74,7 @@ Example `shell.json` (bar subtree only shown): ### Built-in base modules (in `Bar.qml`) -`omarchy`, `workspaces`, `clock`, `update`, `indicators`, `tray`. +`omarchy`, `clock`, `update`, `indicators`, `tray`. The `indicators` module loads individual bar indicators from `indicators/`, ordered by its `items` array in `shell.json`. Rich panels such as `powerPanel`, `networkPanel`, and `audioPanel` live in `../panels/` above. diff --git a/shell/plugins/bar/widgets/workspaces.qml b/shell/plugins/bar/widgets/workspaces.qml new file mode 100644 index 00000000..b0d7e304 --- /dev/null +++ b/shell/plugins/bar/widgets/workspaces.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Hyprland +import qs.Commons +import qs.Ui + +BarWidget { + id: root + moduleName: "workspaces" + + readonly property bool vertical: bar ? bar.vertical : false + readonly property int barSize: bar ? bar.barSize : Style.bar.sizeHorizontal + + 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 + "\" })")) + } + + implicitWidth: grid.implicitWidth + implicitHeight: grid.implicitHeight + + GridLayout { + id: grid + anchors.fill: parent + columns: root.vertical ? 1 : root.workspaceIds().length + columnSpacing: root.vertical ? 0 : Style.space(2) + 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 ? "\uDB85\uDCFB" : (modelData === 10 ? "0" : String(modelData)) + opacity: occupied || focused ? 1 : 0.5 + horizontalMargin: 6 + verticalPadding: 6 + fixedWidth: root.vertical ? root.barSize : 22 + fixedHeight: root.barSize + onPressed: function() { root.focusWorkspace(modelData) } + } + } + } +} diff --git a/shell/plugins/settings/SettingsPanel.qml b/shell/plugins/settings/SettingsPanel.qml index d20b5cc6..91e84d16 100644 --- a/shell/plugins/settings/SettingsPanel.qml +++ b/shell/plugins/settings/SettingsPanel.qml @@ -292,7 +292,6 @@ Item { // ---------------- widget catalog ----------------------------------------- readonly property var builtinWidgetMeta: ({ "omarchy": { name: "Omarchy menu", description: "Launches the Omarchy menu", category: "Compositor" }, - "workspaces": { name: "Workspaces", description: "Workspace number indicators", category: "Compositor" }, "clock": { name: "Clock", description: "Date / time text", category: "Time" }, "update": { name: "Updates", description: "Indicates available system updates", category: "System" }, "indicators": { name: "Indicators", description: "Manual state indicators", category: "Status" },