Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions src/config/config_overrides.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
};
}

Expand Down
11 changes: 10 additions & 1 deletion src/config/config_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<BarConfig> bars;
std::unordered_map<std::string, WidgetConfig> widgets;
Expand All @@ -1535,6 +1541,7 @@ struct Config {
DesktopWidgetsConfig desktopWidgets;
HotCornersConfig hotCorners;
StorageConfig storage;
WindowSwitcherConfig windowSwitcher;
ShellConfig shell;
OsdConfig osd;
NotificationConfig notification;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1615,7 +1623,8 @@ struct ConfigChangeSet {
|| plugins
|| hotCorners
|| storage
|| accessibility;
|| accessibility
|| windowSwitcher;
}
};

Expand Down
7 changes: 7 additions & 0 deletions src/config/schema/config_schema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,13 @@ namespace noctalia::config::schema {
return s;
}

const Schema<WindowSwitcherConfig>& windowSwitcherSchema() {
static const Schema<WindowSwitcherConfig> s = {
field(&WindowSwitcherConfig::perWorkspace, "per_workspace"),
};
return s;
}

namespace {
const Schema<BrightnessMonitorOverride>& brightnessMonitorSchema() {
static const Schema<BrightnessMonitorOverride> s = {
Expand Down
1 change: 1 addition & 0 deletions src/config/schema/config_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace noctalia::config::schema {
const Schema<ControlCenterConfig>& controlCenterSchema();
const Schema<PluginsConfig>& pluginsSchema();
const Schema<HotCornersConfig>& hotCornersSchema();
const Schema<WindowSwitcherConfig>& windowSwitcherSchema();
const Schema<CalendarConfig>& calendarSchema();
const Schema<KeybindsConfig>& keybindsSchema();
const Schema<HooksConfig>& hooksSchema();
Expand Down
1 change: 1 addition & 0 deletions src/config/schema/config_sections.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
6 changes: 6 additions & 0 deletions src/shell/settings/settings_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
35 changes: 32 additions & 3 deletions src/shell/switcher/window_switcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>
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<WindowSwitcherEntry>& out, const std::optional<std::string>& focusedId
std::vector<WindowSwitcherEntry>& out, const std::optional<std::string>& focusedId,
const std::optional<std::string>& activeWorkspaceKey
) {
std::unordered_map<std::string, WorkspaceWindowAssignment> assignmentById;
assignmentById.reserve(32);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<std::string> 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) {
Expand Down