From 31b649c9bd0a0710b054d83fd936db910994f305 Mon Sep 17 00:00:00 2001 From: Yocraft-2000 <304616174+Yocraft-2000@users.noreply.github.com> Date: Sun, 23 Aug 2026 01:05:47 +0200 Subject: [PATCH 1/6] feat: add workspace-move-down and up binds --- src/config/keybind_parse.cpp | 2 ++ src/config/keybind_parse.h | 2 ++ src/server/actions.cpp | 11 +++++++++++ src/workspace/workspace.cpp | 27 +++++++++++++++++++++++++++ src/workspace/workspace.h | 1 + 5 files changed, 43 insertions(+) diff --git a/src/config/keybind_parse.cpp b/src/config/keybind_parse.cpp index a89f8e0d..a491bf0e 100644 --- a/src/config/keybind_parse.cpp +++ b/src/config/keybind_parse.cpp @@ -207,10 +207,12 @@ namespace umbriel { {"window-toggle-maximize", "", KeybindAction::ToggleMaximize}, {"window-toggle-maximize-to-edges", "", KeybindAction::ToggleMaximizeToEdges}, {"window-toggle-pinned", "", KeybindAction::TogglePinned}, + {"workspace-move-down", "", KeybindAction::WorkspaceMoveDown}, {"workspace-move-to-output-down", "", KeybindAction::WorkspaceMoveToOutputDown}, {"workspace-move-to-output-left", "", KeybindAction::WorkspaceMoveToOutputLeft}, {"workspace-move-to-output-right", "", KeybindAction::WorkspaceMoveToOutputRight}, {"workspace-move-to-output-up", "", KeybindAction::WorkspaceMoveToOutputUp}, + {"workspace-move-up", "", KeybindAction::WorkspaceMoveUp}, {"workspace-next", "", KeybindAction::WorkspaceNext}, {"workspace-previous", "", KeybindAction::WorkspacePrevious}, {"workspace-set-layout", "", KeybindAction::WorkspaceSetLayout, diff --git a/src/config/keybind_parse.h b/src/config/keybind_parse.h index 16113026..76f24964 100644 --- a/src/config/keybind_parse.h +++ b/src/config/keybind_parse.h @@ -88,6 +88,8 @@ namespace umbriel { WorkspaceSetLayout, DpmsOff, DpmsOn, + WorkspaceMoveDown, + WorkspaceMoveUp, Count, }; diff --git a/src/server/actions.cpp b/src/server/actions.cpp index b40cd7a2..26fd7cb4 100644 --- a/src/server/actions.cpp +++ b/src/server/actions.cpp @@ -628,6 +628,15 @@ namespace umbriel { return true; } + template bool actionWorkspaceMove(Server& server, const Keybind& /*bind*/, std::string* /*error*/) { + Workspace* workspace = activeWorkspace(server); + if (workspace == nullptr || workspace->group() == nullptr) { + return true; + } + workspace->group()->moveActiveWorkspace(Direction); + return true; + } + template bool actionLayoutScroll(Server& server, const Keybind& bind, std::string* /*error*/) { const int multiplier = bind.wheel != WheelDirection::None && tiledDragActive(server) ? 2 : 1; scrollActiveLayout(server, multiplier); @@ -927,6 +936,8 @@ namespace umbriel { &actionWorkspaceSetLayout, &actionDpms, &actionDpms, + &actionWorkspaceMove<1>, + &actionWorkspaceMove<-1>, }; consteval bool everyActionHasHandler() { diff --git a/src/workspace/workspace.cpp b/src/workspace/workspace.cpp index 3660395c..016cbc0d 100644 --- a/src/workspace/workspace.cpp +++ b/src/workspace/workspace.cpp @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include #include "wlr.h" // clang-format on @@ -949,6 +951,31 @@ namespace umbriel { return result; } + bool WorkspaceGroup::moveActiveWorkspace(int direction) { + if (m_active == nullptr || m_workspaces.size() < 2 || direction == 0 || m_output == nullptr) { + return false; + } + const size_t index = m_active->index(); + const auto target = static_cast(index) + direction; + if (target < 0 || target >= static_cast(m_workspaces.size())) { + return false; + } + slideFinish(); + std::swap(m_workspaces[index], m_workspaces[static_cast(target)]); + if (m_dynamic) { + refreshDynamicWorkspaceMetadata(); + } else { + for (const size_t slot : {index, static_cast(target)}) { + Workspace* moved = m_workspaces[slot].get(); + moved->rename(moved->name(), slot); + } + } + if (Overview* overview = m_server->overview(); overview != nullptr && overview->active()) { + overview->onWorkspaceInventoryChanged(this); + } + return true; + } + void WorkspaceGroup::reconcileInventory() { slideFinish(); const char* outputName = m_output->wlr()->name != nullptr ? m_output->wlr()->name : "output"; diff --git a/src/workspace/workspace.h b/src/workspace/workspace.h index 9ff76416..4ab6710a 100644 --- a/src/workspace/workspace.h +++ b/src/workspace/workspace.h @@ -179,6 +179,7 @@ namespace umbriel { // Insert an empty numbered workspace into a dynamic group and renumber the following workspaces. Static configured // groups cannot be extended this way and return null. Workspace* insertDynamicWorkspace(size_t index); + bool moveActiveWorkspace(int direction); void reconcileInventory(); void refreshLayouts(); void reconcileDynamic(); From d50ab7d57b3a07953b76ec7abfd6909f3e6602ef Mon Sep 17 00:00:00 2001 From: Yocraft-2000 <304616174+Yocraft-2000@users.noreply.github.com> Date: Sun, 23 Aug 2026 01:19:37 +0200 Subject: [PATCH 2/6] docs: document `workspace-move-down` and `workspace-move-up` binds --- docs/user/keybinds.md | 3 +++ examples/config.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/docs/user/keybinds.md b/docs/user/keybinds.md index cf141582..09952180 100644 --- a/docs/user/keybinds.md +++ b/docs/user/keybinds.md @@ -127,6 +127,9 @@ focused output, by index. They do not wrap around: `workspace-previous` on the first workspace is a silent no-op. On a dynamic output, `workspace-next` reaches the trailing empty workspace, which becomes active as usual. +`workspace-move-down` and `workspace-move-up` move the focused workspace up or down +on the focused output. They do not wrap around either. + The matching window actions can be bound independently: ```toml diff --git a/examples/config.toml b/examples/config.toml index 7c0cdda2..46858359 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -210,6 +210,8 @@ follows_mouse = false # "Mod+Period" = "workspace-next" # "Mod+Shift+Comma" = "window-move-to-workspace-previous" # "Mod+Shift+Period" = "window-move-to-workspace-next" +# "Mod+Ctrl+Comma" = "workspace-move-up" +# "Mod+Ctrl+Period" = "workspace-move-down" # "Mod+Ctrl+H" = "output-focus-left" # "Mod+Shift+O" = "dpms-off" # input activity powers displays back on # "Mod+Ctrl+O" = "dpms-off:DP-1" # target one connector From 63aa04cba7897146b3b53cf27dbfe5fa63a33657 Mon Sep 17 00:00:00 2001 From: Yocraft-2000 <304616174+Yocraft-2000@users.noreply.github.com> Date: Sun, 23 Aug 2026 01:32:08 +0200 Subject: [PATCH 3/6] fix: prevent moving empty trailing workspace with dynamic workspaces --- src/workspace/workspace.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/workspace/workspace.cpp b/src/workspace/workspace.cpp index 016cbc0d..4cf000d4 100644 --- a/src/workspace/workspace.cpp +++ b/src/workspace/workspace.cpp @@ -960,6 +960,14 @@ namespace umbriel { if (target < 0 || target >= static_cast(m_workspaces.size())) { return false; } + if (m_dynamic) { + const bool activeIsTrailingEmpty = index == m_workspaces.size() - 1 && !m_active->hasViews(); + const bool targetIsTrailingEmpty = static_cast(target) == m_workspaces.size() - 1.0 + && !m_workspaces[static_cast(target)]->hasViews(); + if (activeIsTrailingEmpty || (direction > 0 && targetIsTrailingEmpty)) { + return false; + } + } slideFinish(); std::swap(m_workspaces[index], m_workspaces[static_cast(target)]); if (m_dynamic) { From 36f82a14d6ba5a6d63b1eede5051b69feef5d04a Mon Sep 17 00:00:00 2001 From: Yocraft-2000 <304616174+Yocraft-2000@users.noreply.github.com> Date: Sun, 23 Aug 2026 01:50:23 +0200 Subject: [PATCH 4/6] feat: allow moving from the trailing workspace but not to it --- src/workspace/workspace.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/workspace/workspace.cpp b/src/workspace/workspace.cpp index 4cf000d4..025c08a9 100644 --- a/src/workspace/workspace.cpp +++ b/src/workspace/workspace.cpp @@ -960,11 +960,10 @@ namespace umbriel { if (target < 0 || target >= static_cast(m_workspaces.size())) { return false; } - if (m_dynamic) { - const bool activeIsTrailingEmpty = index == m_workspaces.size() - 1 && !m_active->hasViews(); - const bool targetIsTrailingEmpty = static_cast(target) == m_workspaces.size() - 1.0 + if (m_dynamic && direction > 0) { + const bool targetIsTrailingEmpty = static_cast(target) == m_workspaces.size() - 1 && !m_workspaces[static_cast(target)]->hasViews(); - if (activeIsTrailingEmpty || (direction > 0 && targetIsTrailingEmpty)) { + if (targetIsTrailingEmpty) { return false; } } From 26c82d87916fc05e71cc30cc8c5f50cad78e7148 Mon Sep 17 00:00:00 2001 From: Yocraft-2000 <304616174+Yocraft-2000@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:48:53 +0200 Subject: [PATCH 5/6] fix: add actions to cheatsheet group --- src/scene/cheatsheet_rows.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scene/cheatsheet_rows.cpp b/src/scene/cheatsheet_rows.cpp index fbbcacb3..ec38f421 100644 --- a/src/scene/cheatsheet_rows.cpp +++ b/src/scene/cheatsheet_rows.cpp @@ -306,6 +306,8 @@ namespace { case A::WindowMoveToWorkspacePrevious: case A::WorkspaceNext: case A::WorkspacePrevious: + case A::WorkspaceMoveDown: + case A::WorkspaceMoveUp: case A::WorkspaceSetLayout: case A::WorkspaceMoveToOutputLeft: case A::WorkspaceMoveToOutputRight: From 0d9022eab401c70f7e19db861a06f1518d2636f2 Mon Sep 17 00:00:00 2001 From: Yocraft-2000 <304616174+Yocraft-2000@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:22:59 +0200 Subject: [PATCH 6/6] fix: always keep an empty workspace at the end in dynamic --- src/workspace/workspace.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/workspace/workspace.cpp b/src/workspace/workspace.cpp index 025c08a9..a01735bb 100644 --- a/src/workspace/workspace.cpp +++ b/src/workspace/workspace.cpp @@ -971,6 +971,9 @@ namespace umbriel { std::swap(m_workspaces[index], m_workspaces[static_cast(target)]); if (m_dynamic) { refreshDynamicWorkspaceMetadata(); + if (m_workspaces.back()->hasViews()) { + appendDynamicWorkspace(); + } } else { for (const size_t slot : {index, static_cast(target)}) { Workspace* moved = m_workspaces[slot].get();