From 0174e8f234cf06354a74055a1629beaa8f1fd0cb Mon Sep 17 00:00:00 2001 From: the_aceix Date: Sun, 26 Jul 2026 12:06:54 +0000 Subject: [PATCH] feat(switcher): add per-workspace window switcher toggle Adds a settings-panel toggle (Keybinds > window_switcher.per_workspace) that restricts the Alt-Tab window switcher to windows on the currently active workspace, instead of always listing windows across every workspace. Filtering lives in the shared buildWindowEntries() path used by every compositor backend via CompositorPlatform::workspaceWindowAssignments(), so it applies wherever a backend reports per-window workspace keys (Hyprland, Sway, niri, labwc, mango, the generic ext-workspace/triad backend). Windows with no workspace assignment (e.g. on backends without workspace-window tracking, such as KDE/KWin and dwl) always pass through unfiltered, so the toggle degrades to a no-op there rather than emptying the switcher. Also fixes WindowSwitcher::show() calling refreshWindows() before m_output was assigned, which made the filter (and any future per-output logic) see a stale/null output on first open. --- assets/translations/en.json | 4 +++ src/config/config_overrides.cpp | 2 ++ src/config/config_types.h | 11 +++++++- src/config/schema/config_schema.cpp | 7 +++++ src/config/schema/config_schema.h | 1 + src/config/schema/config_sections.cpp | 1 + src/shell/settings/settings_registry.cpp | 6 ++++ src/shell/switcher/window_switcher.cpp | 35 ++++++++++++++++++++++-- 8 files changed, 63 insertions(+), 4 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 1f5d1a122e..f25d28c50b 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -2071,6 +2071,10 @@ "validate": { "description": "Confirm selections, submit inputs, or activate the focused item", "label": "Confirm" + }, + "window-switcher-per-workspace": { + "description": "Restrict Alt-Tab window switching to windows on the current workspace only", + "label": "Switch Windows: Current Workspace Only" } }, "lockscreen": { diff --git a/src/config/config_overrides.cpp b/src/config/config_overrides.cpp index a03e38eb41..1ecb856471 100644 --- a/src/config/config_overrides.cpp +++ b/src/config/config_overrides.cpp @@ -348,6 +348,7 @@ namespace { && widgetMapEqual(a.widgets, b.widgets) && desktopWidgetsConfigEqual(a.desktopWidgets, b.desktopWidgets) && a.hotCorners == b.hotCorners + && a.windowSwitcher == b.windowSwitcher && lockscreenWidgetsConfigEqual(a.lockscreenWidgets, b.lockscreenWidgets) && a.wallpaper == b.wallpaper && a.backdrop == b.backdrop @@ -739,6 +740,7 @@ ConfigChangeSet computeConfigChangeSet(const Config& prev, const Config& next) { .hotCorners = !(prev.hotCorners == next.hotCorners), .storage = !(prev.storage == next.storage), .accessibility = !(prev.accessibility == next.accessibility), + .windowSwitcher = !(prev.windowSwitcher == next.windowSwitcher), }; } diff --git a/src/config/config_types.h b/src/config/config_types.h index b6b2388174..463cbb2997 100644 --- a/src/config/config_types.h +++ b/src/config/config_types.h @@ -1524,6 +1524,12 @@ struct HotCornersConfig { bool operator==(const HotCornersConfig&) const = default; }; +struct WindowSwitcherConfig { + bool perWorkspace = false; + + bool operator==(const WindowSwitcherConfig&) const = default; +}; + struct Config { std::vector bars; std::unordered_map widgets; @@ -1535,6 +1541,7 @@ struct Config { DesktopWidgetsConfig desktopWidgets; HotCornersConfig hotCorners; StorageConfig storage; + WindowSwitcherConfig windowSwitcher; ShellConfig shell; OsdConfig osd; NotificationConfig notification; @@ -1586,6 +1593,7 @@ struct ConfigChangeSet { bool hotCorners = true; bool storage = true; bool accessibility = true; + bool windowSwitcher = true; [[nodiscard]] bool any() const noexcept { return bars @@ -1615,7 +1623,8 @@ struct ConfigChangeSet { || plugins || hotCorners || storage - || accessibility; + || accessibility + || windowSwitcher; } }; diff --git a/src/config/schema/config_schema.cpp b/src/config/schema/config_schema.cpp index 40193dff11..be895c26e9 100644 --- a/src/config/schema/config_schema.cpp +++ b/src/config/schema/config_schema.cpp @@ -409,6 +409,13 @@ namespace noctalia::config::schema { return s; } + const Schema& windowSwitcherSchema() { + static const Schema s = { + field(&WindowSwitcherConfig::perWorkspace, "per_workspace"), + }; + return s; + } + namespace { const Schema& brightnessMonitorSchema() { static const Schema s = { diff --git a/src/config/schema/config_schema.h b/src/config/schema/config_schema.h index 90d02b3a56..debf251a76 100644 --- a/src/config/schema/config_schema.h +++ b/src/config/schema/config_schema.h @@ -25,6 +25,7 @@ namespace noctalia::config::schema { const Schema& controlCenterSchema(); const Schema& pluginsSchema(); const Schema& hotCornersSchema(); + const Schema& windowSwitcherSchema(); const Schema& calendarSchema(); const Schema& keybindsSchema(); const Schema& hooksSchema(); diff --git a/src/config/schema/config_sections.cpp b/src/config/schema/config_sections.cpp index 2b8aaeece2..4a28ed8736 100644 --- a/src/config/schema/config_sections.cpp +++ b/src/config/schema/config_sections.cpp @@ -66,6 +66,7 @@ namespace noctalia::config::schema { t.push_back(makeSection("keybinds", &Config::keybinds, keybindsSchema())); t.push_back(makeSection("dock", &Config::dock, dockSchema())); t.push_back(makeSection("hot_corners", &Config::hotCorners, hotCornersSchema())); + t.push_back(makeSection("window_switcher", &Config::windowSwitcher, windowSwitcherSchema())); t.push_back(makeSection("control_center", &Config::controlCenter, controlCenterSchema())); t.push_back(makeSection("plugins", &Config::plugins, pluginsSchema())); t.push_back(makeSection("hooks", &Config::hooks, hooksSchema())); diff --git a/src/shell/settings/settings_registry.cpp b/src/shell/settings/settings_registry.cpp index 85002c215d..1f430671b8 100644 --- a/src/shell/settings/settings_registry.cpp +++ b/src/shell/settings/settings_registry.cpp @@ -2081,6 +2081,12 @@ namespace settings { KeybindListSetting{.items = effectiveKeybindItems(cfg.keybinds.tabNext, KeybindAction::TabNext), .maxItems = 4}, "keybind shortcut hotkey tab focus pane" )); + entries.push_back(makeEntry( + SettingsSection::Keybinds, "window-switcher", + tr("settings.schema.keybinds.window-switcher-per-workspace.label"), + tr("settings.schema.keybinds.window-switcher-per-workspace.description"), {"window_switcher", "per_workspace"}, + ToggleSetting{cfg.windowSwitcher.perWorkspace}, "window switcher alt tab workspace cycle restrict current" + )); // Niri-specific integrations if (env.niriOverviewTypeToLaunchSupported || env.niriBackdropSupported) { diff --git a/src/shell/switcher/window_switcher.cpp b/src/shell/switcher/window_switcher.cpp index 9c96e989a2..f228d22b84 100644 --- a/src/shell/switcher/window_switcher.cpp +++ b/src/shell/switcher/window_switcher.cpp @@ -322,9 +322,22 @@ namespace { } } + // Finds the currently active workspace's key on the given output, using the same + // "workspaces() + .active" idiom as WorkspacesWidget::activeWorkspaceIndex(). + [[nodiscard]] std::optional + activeWorkspaceKeyFor(const CompositorPlatform& platform, wl_output* output) { + for (const auto& workspace : platform.workspaces(output)) { + if (workspace.active) { + return workspace.id; + } + } + return std::nullopt; + } + void buildWindowEntries( const CompositorPlatform& platform, IconResolver& iconResolver, int iconSize, - std::vector& out, const std::optional& focusedId + std::vector& out, const std::optional& focusedId, + const std::optional& activeWorkspaceKey ) { std::unordered_map assignmentById; assignmentById.reserve(32); @@ -386,6 +399,16 @@ namespace { addCandidate(std::move(candidate), key); } + // Windows with no workspace assignment (empty workspaceKey) are always kept: on + // compositors whose backend doesn't populate workspace-window assignments, filtering + // them out would empty the switcher entirely, so per-workspace restriction degrades + // to a no-op there instead of hiding every window. + if (activeWorkspaceKey.has_value()) { + std::erase_if(candidates, [&](const WindowSwitcherCandidate& candidate) { + return !candidate.workspaceKey.empty() && candidate.workspaceKey != *activeWorkspaceKey; + }); + } + std::ranges::stable_sort(candidates, [](const WindowSwitcherCandidate& a, const WindowSwitcherCandidate& b) { if (a.workspaceKey != b.workspaceKey) { return a.workspaceKey < b.workspaceKey; @@ -594,9 +617,9 @@ void WindowSwitcher::show(wl_output* output) { } const bool wasActive = m_active; + m_output = output; refreshWindows(); - m_output = output; if (wasActive) { cycleSelection(1); } else { @@ -641,7 +664,13 @@ void WindowSwitcher::refreshWindows() { IconResolver iconResolver; const int iconSize = 96; - buildWindowEntries(*m_platform, iconResolver, iconSize, m_windows, m_platform->focusedCompositorWindowId()); + const std::optional activeWorkspaceKey = + (m_config != nullptr && m_config->config().windowSwitcher.perWorkspace) + ? activeWorkspaceKeyFor(*m_platform, m_output) + : std::nullopt; + buildWindowEntries( + *m_platform, iconResolver, iconSize, m_windows, m_platform->focusedCompositorWindowId(), activeWorkspaceKey + ); if (selectedKey.has_value()) { for (std::size_t i = 0; i < m_windows.size(); ++i) {