Skip to content

Commit 8721c51

Browse files
thmsnhlClaude Sonnet 4.6
andcommitted
fix: dismiss window switcher on modifier release for custom shortcuts
The window switcher previously only auto-dismissed (activating the selected window) when the Super key was released, which was hardcoded specifically for the default Super+Tab binding. Custom shortcuts using other modifier keys (e.g. Ctrl+Tab) would leave the switcher open after key release, requiring the user to manually click a window or press a number key to confirm the selection. This fix extends the auto-dismiss-on-release behavior to any custom window-switcher shortcut: when the trigger modifier(s) are released, the switcher now activates the highlighted window and closes, matching the behavior of the built-in Super+Tab binding. Fixes #2138 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 253b44f commit 8721c51

3 files changed

Lines changed: 125 additions & 0 deletions

File tree

src/input/actions.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ impl State {
7373
&self.common.config,
7474
self.common.event_loop_handle.clone(),
7575
);
76+
shell.set_window_switcher_binding(None);
7677
}
7778
let pointer = seat.get_pointer().unwrap();
7879
let keyboard = seat.get_keyboard().unwrap();
@@ -1002,6 +1003,34 @@ impl State {
10021003

10031004
// Gets the configured command for a given system action.
10041005
Action::System(system) => {
1006+
// Track the triggering binding for the window switcher so
1007+
// modifier-release can dismiss it even for custom shortcuts.
1008+
if matches!(
1009+
system,
1010+
shortcuts::action::System::WindowSwitcher
1011+
| shortcuts::action::System::WindowSwitcherPrevious
1012+
) {
1013+
let mods = &pattern.modifiers;
1014+
// For custom modifiers (not Alt/Super), cosmic-launcher does
1015+
// not know when to confirm the selection, so cosmic-comp
1016+
// tracks the cycle index itself and focuses the window
1017+
// directly on modifier release.
1018+
// Native Alt/Super bindings are handled entirely by
1019+
// cosmic-launcher, so we do not track state for them.
1020+
if !mods.alt && !mods.logo {
1021+
let forward =
1022+
matches!(system, shortcuts::action::System::WindowSwitcher);
1023+
let output = seat.active_output();
1024+
let mut shell = self.common.shell.write();
1025+
let len = shell
1026+
.active_space(&output)
1027+
.map(|ws| ws.focus_stack.get(seat).iter().count())
1028+
.unwrap_or(1)
1029+
.max(1);
1030+
shell.advance_window_switcher(forward, len);
1031+
shell.set_window_switcher_binding(Some(pattern.clone()));
1032+
}
1033+
}
10051034
if let Some(command) = self.common.config.system_actions.get(&system) {
10061035
self.spawn_command(command.clone());
10071036
}

src/input/mod.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,6 +1654,52 @@ impl State {
16541654
}
16551655
}
16561656

1657+
// Dismiss window switcher when its trigger modifier is released and the
1658+
// window-switcher app does not natively watch for that modifier (i.e.,
1659+
// the binding uses neither Alt nor Super, which cosmic-launcher already
1660+
// intercepts on its own).
1661+
//
1662+
// Only check modifier release — not key release. For Alt+Tab the user
1663+
// holds Alt and presses Tab to cycle; releasing Tab should NOT close
1664+
// the switcher. The same applies to custom bindings like Shift+F: the
1665+
// user holds Shift and can press F multiple times to cycle through
1666+
// windows; only releasing Shift should confirm and close.
1667+
//
1668+
// For custom-modifier bindings, cosmic-launcher does not know which
1669+
// modifier to watch, so it cannot activate the selected window itself.
1670+
// Instead, cosmic-comp tracks a cycle index and focuses the window
1671+
// directly when the modifier is released. Capture the pending target
1672+
// now (before the binding/index is cleared below).
1673+
let (dismiss_window_switcher, pending_switcher_focus) =
1674+
if let Some(binding) = shell.window_switcher_binding().cloned() {
1675+
let mods = &binding.modifiers;
1676+
// cosmic-launcher already handles Alt and Super release internally;
1677+
// only intervene for other modifiers (Ctrl, Shift, …).
1678+
let launcher_handles_natively = mods.alt || mods.logo;
1679+
let dismiss = !launcher_handles_natively
1680+
&& event.state() == KeyState::Released
1681+
&& ((mods.ctrl && !modifiers.ctrl) || (mods.shift && !modifiers.shift));
1682+
1683+
let pending = if dismiss {
1684+
shell.window_switcher_index().and_then(|idx| {
1685+
let output = seat.active_output();
1686+
shell
1687+
.active_space(&output)
1688+
.and_then(|ws| ws.focus_stack.get(seat).iter().nth(idx).cloned())
1689+
.map(KeyboardFocusTarget::from)
1690+
})
1691+
} else {
1692+
None
1693+
};
1694+
1695+
(dismiss, pending)
1696+
} else {
1697+
(false, None)
1698+
};
1699+
if dismiss_window_switcher {
1700+
shell.set_window_switcher_binding(None);
1701+
}
1702+
16571703
// Leave or update resize mode, if modifiers changed or initial key was released
16581704
if let Some(action_pattern) = shell.resize_mode().0.active_binding() {
16591705
if action_pattern.key.is_some()
@@ -1759,6 +1805,18 @@ impl State {
17591805

17601806
std::mem::drop(shell);
17611807

1808+
// Dismiss window switcher for custom modifier bindings (non-Alt/Super).
1809+
// Focus the window that was selected (tracked by the cycle index) so
1810+
// that cosmic-launcher does not need to handle the custom modifier.
1811+
// If the tracked window closed while the switcher was open, skip the
1812+
// focus call rather than clearing all focus via set_focus(None).
1813+
if dismiss_window_switcher {
1814+
if let Some(ref focus) = pending_switcher_focus {
1815+
Shell::set_focus(self, Some(focus), seat, Some(serial), false);
1816+
}
1817+
return FilterResult::Intercept(None);
1818+
}
1819+
17621820
// cancel grabs
17631821
if is_grabbed
17641822
&& handle.modified_sym() == Keysym::Escape

src/shell/mod.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ pub struct Shell {
276276
pub active_hint: bool,
277277
overview_mode: OverviewMode,
278278
swap_indicator: Option<SwapIndicator>,
279+
window_switcher_binding: Option<shortcuts::Binding>,
280+
window_switcher_index: Option<usize>,
279281
resize_mode: ResizeMode,
280282
resize_state: Option<(
281283
KeyboardFocusTarget,
@@ -1592,6 +1594,8 @@ impl Shell {
15921594
active_hint: config.cosmic_conf.active_hint,
15931595
overview_mode: OverviewMode::None,
15941596
swap_indicator: None,
1597+
window_switcher_binding: None,
1598+
window_switcher_index: None,
15951599
resize_mode: ResizeMode::None,
15961600
resize_state: None,
15971601
resize_indicator: None,
@@ -2182,6 +2186,40 @@ impl Shell {
21822186
clients
21832187
}
21842188

2189+
pub fn window_switcher_binding(&self) -> Option<&shortcuts::Binding> {
2190+
self.window_switcher_binding.as_ref()
2191+
}
2192+
2193+
pub fn set_window_switcher_binding(&mut self, binding: Option<shortcuts::Binding>) {
2194+
if binding.is_none() {
2195+
self.window_switcher_index = None;
2196+
}
2197+
// When setting a new (Some) binding the index is intentionally kept:
2198+
// the caller already called advance_window_switcher() just before this,
2199+
// so resetting here would undo that advance.
2200+
self.window_switcher_binding = binding;
2201+
}
2202+
2203+
pub fn window_switcher_index(&self) -> Option<usize> {
2204+
self.window_switcher_index
2205+
}
2206+
2207+
/// Advance the window-switcher cycle index.
2208+
///
2209+
/// `len` is the number of windows available. The index wraps around.
2210+
/// The very first call (when `window_switcher_index` is `None`) starts at
2211+
/// position 1 for forward (the window after the current one) or `len-1`
2212+
/// for backward — matching standard Alt+Tab behaviour.
2213+
pub fn advance_window_switcher(&mut self, forward: bool, len: usize) {
2214+
let len = len.max(1);
2215+
self.window_switcher_index = Some(match self.window_switcher_index {
2216+
None if forward => 1 % len,
2217+
None => len.saturating_sub(1),
2218+
Some(i) if forward => (i + 1) % len,
2219+
Some(i) => i.checked_sub(1).unwrap_or(len - 1),
2220+
});
2221+
}
2222+
21852223
pub fn set_overview_mode(
21862224
&mut self,
21872225
enabled: Option<Trigger>,

0 commit comments

Comments
 (0)