From b3ea28c5c4ab5ba44113ebd27768da9a1b36bd6a Mon Sep 17 00:00:00 2001 From: Kamil Chmielewski Date: Fri, 23 Jan 2026 19:46:34 +0100 Subject: [PATCH] feat: add workspace rename dialog (#1429) * feat: add workspace rename dialog - Adds a modal dialog to rename the current workspace - Supports both Niri (via IPC socket) and Hyprland (via hyprctl dispatch) - Default keybinding: Ctrl+Shift+R to open the dialog - Pre-fills with current workspace name - Allows setting empty name to reset to default * refactor: wrap WorkspaceRenameModal in LazyLoader Reduces memory footprint when the modal is not in use. --- core/internal/config/embedded/hypr-binds.conf | 3 + core/internal/config/embedded/niri-binds.kdl | 5 + quickshell/Common/KeybindActions.js | 3 +- quickshell/DMSShell.qml | 12 + quickshell/DMSShellIPC.qml | 37 +++ quickshell/Modals/WorkspaceRenameModal.qml | 232 ++++++++++++++++++ quickshell/Services/DMSService.qml | 6 + quickshell/Services/HyprlandService.qml | 14 ++ quickshell/Services/NiriService.qml | 10 + 9 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 quickshell/Modals/WorkspaceRenameModal.qml diff --git a/core/internal/config/embedded/hypr-binds.conf b/core/internal/config/embedded/hypr-binds.conf index 00858d06..74107cf7 100644 --- a/core/internal/config/embedded/hypr-binds.conf +++ b/core/internal/config/embedded/hypr-binds.conf @@ -91,6 +91,9 @@ bind = SUPER CTRL, up, movetoworkspace, e-1 bind = SUPER CTRL, U, movetoworkspace, e+1 bind = SUPER CTRL, I, movetoworkspace, e-1 +# === Workspace Management === +bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open + # === Move Workspaces === bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1 bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1 diff --git a/core/internal/config/embedded/niri-binds.kdl b/core/internal/config/embedded/niri-binds.kdl index 22b936fc..f3cb65c7 100644 --- a/core/internal/config/embedded/niri-binds.kdl +++ b/core/internal/config/embedded/niri-binds.kdl @@ -133,6 +133,11 @@ binds { Mod+Ctrl+U { move-column-to-workspace-down; } Mod+Ctrl+I { move-column-to-workspace-up; } + // === Workspace Management === + Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" { + spawn "dms" "ipc" "call" "workspace-rename" "open"; + } + // === Move Workspaces === Mod+Shift+Page_Down { move-workspace-down; } Mod+Shift+Page_Up { move-workspace-up; } diff --git a/quickshell/Common/KeybindActions.js b/quickshell/Common/KeybindActions.js index 95914a3b..bab0dfa3 100644 --- a/quickshell/Common/KeybindActions.js +++ b/quickshell/Common/KeybindActions.js @@ -100,7 +100,8 @@ const DMS_ACTIONS = [ { id: "spawn dms ipc call hypr openOverview", label: "Hyprland: Open Overview", compositor: "hyprland" }, { id: "spawn dms ipc call hypr closeOverview", label: "Hyprland: Close Overview", compositor: "hyprland" }, { id: "spawn dms ipc call wallpaper next", label: "Wallpaper: Next" }, - { id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" } + { id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" }, + { id: "spawn dms ipc call workspace-rename open", label: "Workspace: Rename" } ]; const NIRI_ACTIONS = { diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index b21b5bd8..ca881e0c 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -644,6 +644,18 @@ Item { } } + LazyLoader { + id: workspaceRenameModalLoader + + active: false + + Component.onCompleted: PopoutService.workspaceRenameModalLoader = workspaceRenameModalLoader + + WorkspaceRenameModal { + id: workspaceRenameModal + } + } + LazyLoader { id: processListModalLoader diff --git a/quickshell/DMSShellIPC.qml b/quickshell/DMSShellIPC.qml index fc97a0c6..abfc82c5 100644 --- a/quickshell/DMSShellIPC.qml +++ b/quickshell/DMSShellIPC.qml @@ -1292,4 +1292,41 @@ Item { target: "desktopWidget" } + + IpcHandler { + function open(): string { + if (!workspaceRenameModalLoader || !workspaceRenameModalLoader.item) { + return "WORKSPACE_RENAME_MODAL_NOT_FOUND"; + } + workspaceRenameModalLoader.active = true; + const ws = NiriService.workspaces[NiriService.focusedWorkspaceId]; + workspaceRenameModalLoader.item.show(ws?.name || ""); + return "WORKSPACE_RENAME_MODAL_OPENED"; + } + + function close(): string { + if (!workspaceRenameModalLoader || !workspaceRenameModalLoader.item) { + return "WORKSPACE_RENAME_MODAL_NOT_FOUND"; + } + workspaceRenameModalLoader.item.hide(); + return "WORKSPACE_RENAME_MODAL_CLOSED"; + } + + function toggle(): string { + if (!workspaceRenameModalLoader || !workspaceRenameModalLoader.item) { + return "WORKSPACE_RENAME_MODAL_NOT_FOUND"; + } + if (workspaceRenameModalLoader.item.shouldBeVisible) { + workspaceRenameModalLoader.item.hide(); + return "WORKSPACE_RENAME_MODAL_CLOSED"; + } else { + workspaceRenameModalLoader.active = true; + const ws = NiriService.workspaces[NiriService.focusedWorkspaceId]; + workspaceRenameModalLoader.item.show(ws?.name || ""); + return "WORKSPACE_RENAME_MODAL_OPENED"; + } + } + + target: "workspace-rename" + } } diff --git a/quickshell/Modals/WorkspaceRenameModal.qml b/quickshell/Modals/WorkspaceRenameModal.qml new file mode 100644 index 00000000..7b1301de --- /dev/null +++ b/quickshell/Modals/WorkspaceRenameModal.qml @@ -0,0 +1,232 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +FloatingWindow { + id: root + + readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 + + objectName: "workspaceRenameModal" + title: I18n.tr("Rename Workspace") + minimumSize: Qt.size(400, 180) + maximumSize: Qt.size(400, 180) + color: Theme.surfaceContainer + visible: false + + function show(name) { + nameInput.text = name; + visible = true; + Qt.callLater(() => nameInput.forceActiveFocus()); + } + + function hide() { + visible = false; + } + + function submitAndClose() { + renameWorkspace(nameInput.text); + hide(); + } + + function renameWorkspace(name) { + if (CompositorService.isNiri) { + NiriService.renameWorkspace(name); + } else if (CompositorService.isHyprland) { + HyprlandService.renameWorkspace(name); + } else { + console.warn("WorkspaceRenameModal: rename not supported for this compositor"); + } + } + + onVisibleChanged: { + if (visible) { + Qt.callLater(() => nameInput.forceActiveFocus()); + } + } + + FocusScope { + id: contentFocusScope + + anchors.fill: parent + focus: true + + Keys.onEscapePressed: { + hide(); + event.accepted = true; + } + + Keys.onReturnPressed: { + submitAndClose(); + event.accepted = true; + } + + Column { + id: contentCol + anchors.centerIn: parent + width: parent.width - Theme.spacingL * 2 + spacing: Theme.spacingM + + Item { + width: parent.width + height: Math.max(headerCol.height, buttonRow.height) + + MouseArea { + anchors.fill: parent + onPressed: windowControls.tryStartMove() + onDoubleClicked: windowControls.tryToggleMaximize() + + Column { + id: headerCol + width: parent.width + + StyledText { + text: I18n.tr("Enter a new name for this workspace") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + width: parent.width + } + } + } + + Row { + id: buttonRow + anchors.right: parent.right + spacing: Theme.spacingXS + + DankActionButton { + visible: windowControls.supported && windowControls.canMaximize + iconName: root.maximized ? "fullscreen_exit" : "fullscreen" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: windowControls.tryToggleMaximize() + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: hide() + } + } + } + + Rectangle { + width: parent.width + height: inputFieldHeight + radius: Theme.cornerRadius + color: Theme.surfaceHover + border.color: nameInput.activeFocus ? Theme.primary : Theme.outlineStrong + border.width: nameInput.activeFocus ? 2 : 1 + + MouseArea { + anchors.fill: parent + onClicked: nameInput.forceActiveFocus() + } + + DankTextField { + id: nameInput + + anchors.fill: parent + font.pixelSize: Theme.fontSizeMedium + textColor: Theme.surfaceText + placeholderText: I18n.tr("Workspace name") + backgroundColor: "transparent" + enabled: root.visible + onAccepted: submitAndClose() + } + } + + Item { + width: parent.width + height: 40 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Rectangle { + width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent" + border.color: Theme.surfaceVariantAlpha + border.width: 1 + + StyledText { + id: cancelText + anchors.centerIn: parent + text: I18n.tr("Cancel") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + MouseArea { + id: cancelArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: hide() + } + } + + Rectangle { + width: Math.max(80, renameText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: renameArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary + + StyledText { + id: renameText + anchors.centerIn: parent + text: I18n.tr("Rename") + font.pixelSize: Theme.fontSizeMedium + color: Theme.background + font.weight: Font.Medium + } + + MouseArea { + id: renameArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: submitAndClose() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + } + + FloatingWindowControls { + id: windowControls + targetWindow: root + } + + IpcHandler { + target: "workspace-rename" + + function open(): string { + const ws = NiriService.workspaces[NiriService.focusedWorkspaceId]; + show(ws?.name || ""); + return "WORKSPACE_RENAME_MODAL_OPENED"; + } + + function close(): string { + hide(); + return "WORKSPACE_RENAME_MODAL_CLOSED"; + } + } +} diff --git a/quickshell/Services/DMSService.qml b/quickshell/Services/DMSService.qml index b3dabe89..ee416e75 100644 --- a/quickshell/Services/DMSService.qml +++ b/quickshell/Services/DMSService.qml @@ -737,4 +737,10 @@ Singleton { if (callback) callback(response); }); } + + function renameWorkspace(name, callback) { + sendRequest("extworkspace.renameWorkspace", { + "name": name + }, callback); + } } diff --git a/quickshell/Services/HyprlandService.qml b/quickshell/Services/HyprlandService.qml index 0db4dd8e..ff4020a6 100644 --- a/quickshell/Services/HyprlandService.qml +++ b/quickshell/Services/HyprlandService.qml @@ -308,4 +308,18 @@ decoration { reloadConfig(); }); } + + function renameWorkspace(newName) { + if (!Hyprland.focusedWorkspace) + return; + const wsId = Hyprland.focusedWorkspace.id; + if (!wsId) + return; + const fullName = wsId + " " + newName; + Proc.runCommand("hyprland-rename-ws", ["hyprctl", "dispatch", "renameworkspace", String(wsId), fullName], (output, exitCode) => { + if (exitCode !== 0) { + console.warn("HyprlandService: Failed to rename workspace:", output); + } + }); + } } diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index 76900efa..2d53ebf5 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -1422,6 +1422,16 @@ Singleton { return block; } + function renameWorkspace(name) { + return send({ + "Action": { + "SetWorkspaceName": { + "name": name + } + } + }); + } + IpcHandler { function screenshot(): string { if (!CompositorService.isNiri) {