From fd2c6a07847a3a623ffeba79f8c9d314294bc153 Mon Sep 17 00:00:00 2001 From: Kamil Chmielewski Date: Wed, 21 Jan 2026 16:43:55 +0100 Subject: [PATCH] Feat/niri workspace names (#1396) * dankbar: show niri workspace names Keep labels aligned with niri indices and live renames. * dankbar: prefix named workspaces with index Use workspace index toggle to show index: name labels. * workspaces: change size conditions for workspace names --------- Co-authored-by: bbedward --- quickshell/Modules/DankBar/DankBarContent.qml | 29 ++- .../DankBar/Widgets/WorkspaceSwitcher.qml | 180 +++++++++++++----- quickshell/Services/NiriService.qml | 8 +- 3 files changed, 161 insertions(+), 56 deletions(-) diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index 2094f7cd..b3a9670a 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -46,11 +46,24 @@ Item { function getRealWorkspaces() { if (CompositorService.isNiri) { + const fallbackWorkspaces = [ + { + "id": 1, + "idx": 0, + "name": "" + }, + { + "id": 2, + "idx": 1, + "name": "" + } + ]; if (!barWindow.screenName || SettingsData.workspaceFollowFocus) { - return NiriService.getCurrentOutputWorkspaceNumbers(); + const currentWorkspaces = NiriService.getCurrentOutputWorkspaces(); + return currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces; } - const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName).map(ws => ws.idx + 1); - return workspaces.length > 0 ? workspaces : [1, 2]; + const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName); + return workspaces.length > 0 ? workspaces : fallbackWorkspaces; } else if (CompositorService.isHyprland) { const workspaces = Hyprland.workspaces?.values || []; @@ -118,7 +131,7 @@ Item { return NiriService.getCurrentWorkspaceNumber(); } const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active); - return activeWs ? activeWs.idx + 1 : 1; + return activeWs ? activeWs.idx : 1; } else if (CompositorService.isHyprland) { const monitors = Hyprland.monitors?.values || []; const currentMonitor = monitors.find(monitor => monitor.name === barWindow.screenName); @@ -151,12 +164,16 @@ Item { if (CompositorService.isNiri) { const currentWs = getCurrentWorkspace(); - const currentIndex = realWorkspaces.findIndex(ws => ws === currentWs); + const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs); const validIndex = currentIndex === -1 ? 0 : currentIndex; const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); if (nextIndex !== validIndex) { - NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1); + const nextWorkspace = realWorkspaces[nextIndex]; + if (!nextWorkspace || nextWorkspace.idx === undefined) { + return; + } + NiriService.switchToWorkspace(nextWorkspace.idx); } } else if (CompositorService.isHyprland) { const currentWs = getCurrentWorkspace(); diff --git a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml index f6f418c0..a4ccdfdd 100644 --- a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml +++ b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml @@ -199,15 +199,22 @@ Item { let targetWorkspaceId; if (CompositorService.isNiri) { - const wsNumber = typeof ws === "number" ? ws : -1; - if (wsNumber <= 0) { - return []; + if (!ws || typeof ws !== "object") { + const wsNumber = typeof ws === "number" ? ws : -1; + if (wsNumber <= 0) { + return []; + } + const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.effectiveScreenName); + if (!workspace) { + return []; + } + targetWorkspaceId = workspace.id; + } else { + if (ws.id === undefined || ws.id === -1 || ws.idx === -1) { + return []; + } + targetWorkspaceId = ws.id; } - const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.effectiveScreenName); - if (!workspace) { - return []; - } - targetWorkspaceId = workspace.id; } else if (CompositorService.isHyprland) { targetWorkspaceId = ws.id !== undefined ? ws.id : ws; } else if (CompositorService.isDwl) { @@ -300,6 +307,12 @@ Item { "active": false, "hidden": true }; + } else if (CompositorService.isNiri) { + placeholder = { + "id": -1, + "idx": -1, + "name": "" + }; } else if (CompositorService.isHyprland) { placeholder = { "id": -1, @@ -324,28 +337,52 @@ Item { function getNiriWorkspaces() { if (NiriService.allWorkspaces.length === 0) { - return [1, 2]; + return [ + { + "id": 1, + "idx": 0, + "name": "" + }, + { + "id": 2, + "idx": 1, + "name": "" + } + ]; } + const fallbackWorkspaces = [ + { + "id": 1, + "idx": 0, + "name": "" + }, + { + "id": 2, + "idx": 1, + "name": "" + } + ]; + let workspaces; if (!root.screenName || SettingsData.workspaceFollowFocus) { - workspaces = NiriService.getCurrentOutputWorkspaceNumbers(); + const currentWorkspaces = NiriService.getCurrentOutputWorkspaces(); + workspaces = currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces; } else { - const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1); - workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]; + const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName); + workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : fallbackWorkspaces; } + workspaces = workspaces.slice().sort((a, b) => a.idx - b.idx); + if (!SettingsData.showOccupiedWorkspacesOnly) { return workspaces; } - return workspaces.filter(wsNum => { - const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNum && w.output === root.effectiveScreenName); - if (!workspace) - return false; - if (workspace.is_active) + return workspaces.filter(ws => { + if (ws.is_active) return true; - return NiriService.windows?.some(win => win.workspace_id === workspace.id) ?? false; + return NiriService.windows?.some(win => win.workspace_id === ws.id) ?? false; }); } @@ -359,7 +396,7 @@ Item { } const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active); - return activeWs ? activeWs.idx + 1 : 1; + return activeWs ? activeWs.idx : 1; } function getDwlTags() { @@ -471,15 +508,18 @@ Item { function getRealWorkspaces() { return root.workspaceList.filter(ws => { - if (useExtWorkspace) - return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden; - if (CompositorService.isHyprland) - return ws && ws.id !== -1; - if (CompositorService.isDwl) - return ws && ws.tag !== -1; - if (CompositorService.isSway || CompositorService.isScroll) - return ws && ws.num !== -1; - return ws !== -1; + if (useExtWorkspace) + return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden; + if (CompositorService.isNiri) + return ws && ws.idx !== -1; + if (CompositorService.isHyprland) + return ws && ws.id !== -1; + if (CompositorService.isDwl) + return ws && ws.tag !== -1; + if (CompositorService.isSway || CompositorService.isScroll) + return ws && ws.num !== -1; + return ws !== -1; + }); } @@ -506,7 +546,7 @@ Item { return; } - const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace); + const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === root.currentWorkspace); const validIndex = currentIndex === -1 ? 0 : currentIndex; const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); @@ -514,7 +554,11 @@ Item { return; } - NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1); + const nextWorkspace = realWorkspaces[nextIndex]; + if (!nextWorkspace || nextWorkspace.idx === undefined) { + return; + } + NiriService.switchToWorkspace(nextWorkspace.idx); } else if (CompositorService.isHyprland) { const realWorkspaces = getRealWorkspaces(); if (realWorkspaces.length < 2) { @@ -565,10 +609,26 @@ Item { } } + function getWorkspaceIndexFallback(modelData, index) { + if (root.useExtWorkspace) + return index + 1; + if (CompositorService.isNiri) + return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : ""; + if (CompositorService.isHyprland) + return modelData?.id || ""; + if (CompositorService.isDwl) + return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""; + if (CompositorService.isSway || CompositorService.isScroll) + return modelData?.num || ""; + return modelData - 1; + } + function getWorkspaceIndex(modelData, index) { let isPlaceholder; if (root.useExtWorkspace) { isPlaceholder = modelData?.hidden === true; + } else if (CompositorService.isNiri) { + isPlaceholder = modelData?.idx === -1; } else if (CompositorService.isHyprland) { isPlaceholder = modelData?.id === -1; } else if (CompositorService.isDwl) { @@ -582,26 +642,28 @@ Item { if (isPlaceholder) return index + 1; + let workspaceName = ""; if (SettingsData.showWorkspaceName) { - let workspaceName = modelData?.name; + workspaceName = modelData?.name ?? ""; if (workspaceName && workspaceName !== "") { if (root.isVertical) { - return workspaceName.charAt(0); + workspaceName = workspaceName.charAt(0); } - return workspaceName; + } else { + workspaceName = ""; } } - if (root.useExtWorkspace) - return index + 1; - if (CompositorService.isHyprland) - return modelData?.id || ""; - if (CompositorService.isDwl) - return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""; - if (CompositorService.isSway || CompositorService.isScroll) - return modelData?.num || ""; - return modelData - 1; + if (workspaceName) { + if (SettingsData.showWorkspaceIndex) { + const indexLabel = getWorkspaceIndexFallback(modelData, index); + return indexLabel ? `${indexLabel}: ${workspaceName}` : workspaceName; + } + return workspaceName; + } + + return getWorkspaceIndexFallback(modelData, index); } readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll @@ -747,6 +809,8 @@ Item { property bool isActive: { if (root.useExtWorkspace) return (modelData?.id || modelData?.name) === root.currentWorkspace; + if (CompositorService.isNiri) + return !!(modelData && modelData.idx === root.currentWorkspace); if (CompositorService.isHyprland) return !!(modelData && modelData.id === root.currentWorkspace); if (CompositorService.isDwl) @@ -769,6 +833,8 @@ Item { property bool isPlaceholder: { if (root.useExtWorkspace) return !!(modelData && modelData.hidden); + if (CompositorService.isNiri) + return !!(modelData && modelData.idx === -1); if (CompositorService.isHyprland) return !!(modelData && modelData.id === -1); if (CompositorService.isDwl) @@ -800,6 +866,10 @@ Item { readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) + readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== "" + readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && CompositorService.isNiri + readonly property real contentImplicitWidth: (hasWorkspaceName || loadedHasIcon) ? (appIconsLoader.item?.contentWidth ?? 0) : 0 + readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0 readonly property real iconsExtraWidth: { if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) { @@ -816,8 +886,16 @@ Item { return 0; } - readonly property real visualWidth: baseWidth + iconsExtraWidth - readonly property real visualHeight: baseHeight + iconsExtraHeight + readonly property real visualWidth: { + if (contentImplicitWidth <= 0) return baseWidth + iconsExtraWidth; + const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS; + return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding); + } + readonly property real visualHeight: { + if (contentImplicitHeight <= 0) return baseHeight + iconsExtraHeight; + const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS; + return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding); + } readonly property color unfocusedColor: { switch (SettingsData.workspaceUnfocusedColorMode) { @@ -915,8 +993,8 @@ Item { } else if (CompositorService.isNiri) { if (isRightClick) { NiriService.toggleOverview(); - } else { - NiriService.switchToWorkspace(modelData - 1); + } else if (modelData && modelData.idx !== undefined) { + NiriService.switchToWorkspace(modelData.idx); } } else if (CompositorService.isHyprland && modelData?.id) { if (isRightClick && root.hyprlandOverviewLoader?.item) { @@ -958,7 +1036,7 @@ Item { if (root.useExtWorkspace) { wsData = modelData; } else if (CompositorService.isNiri) { - wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName) || null; + wsData = modelData || null; } else if (CompositorService.isHyprland) { wsData = modelData; } else if (CompositorService.isDwl) { @@ -984,6 +1062,8 @@ Item { if (SettingsData.showWorkspaceApps) { if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll) { delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData); + } else if (CompositorService.isNiri) { + delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData); } else { delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData)); } @@ -1096,8 +1176,12 @@ Item { Loader { id: appIconsLoader anchors.fill: parent - active: SettingsData.showWorkspaceApps + active: SettingsData.showWorkspaceApps || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon sourceComponent: Item { + id: contentRoot + readonly property real contentWidth: contentRow.item?.implicitWidth ?? 0 + readonly property real contentHeight: contentRow.item?.implicitHeight ?? 0 + Loader { id: contentRow anchors.centerIn: parent diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index 3b1e9568..76900efa 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -49,7 +49,7 @@ Singleton { function setWorkspaces(newMap) { root.workspaces = newMap; - allWorkspaces = Object.values(newMap).sort((a, b) => a.idx - b.idx); + root.allWorkspaces = Object.values(newMap).sort((a, b) => a.idx - b.idx); } Component.onCompleted: fetchOutputs() @@ -863,9 +863,13 @@ Singleton { return currentOutputWorkspaces.map(w => w.idx + 1); } + function getCurrentOutputWorkspaces() { + return currentOutputWorkspaces.slice(); + } + function getCurrentWorkspaceNumber() { if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) { - return allWorkspaces[focusedWorkspaceIndex].idx + 1; + return allWorkspaces[focusedWorkspaceIndex].idx; } return 1; }