From ab47c01cf72148d572dc3199cde426ceef9cf863 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 18 Jul 2026 11:47:15 -0700 Subject: [PATCH] Fix stuck HUD on fast modifier release --- src/main.rs | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/main.rs b/src/main.rs index 65f67ec..3ae82ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ enum DaemonMessage { } struct Session { + id: u64, groups: Vec, selected: usize, original: usize, @@ -49,6 +50,7 @@ struct Session { struct RuntimeState { history: FocusHistory, session: Option, + next_session_id: u64, window_cycle: Option, ignore_focus_until: Option, } @@ -107,6 +109,7 @@ impl Switcher { state: RefCell::new(RuntimeState { history, session: None, + next_session_id: 0, window_cycle: None, ignore_focus_until: None, }), @@ -149,7 +152,57 @@ impl Switcher { } }); + let switcher = Rc::clone(self); + controller.connect_modifiers(move |_controller, modifiers| { + if !switch_modifier_is_pressed(modifiers) { + switcher.finish(true); + } + Propagation::Proceed + }); + self.window.add_controller(controller); + + let focus_controller = gtk::EventControllerLegacy::new(); + let switcher = Rc::clone(self); + focus_controller.connect_event(move |_controller, event| { + let Some(focus_event) = event.downcast_ref::() else { + return Propagation::Proceed; + }; + if !focus_event.is_in() { + return Propagation::Proceed; + } + + let session_id = switcher + .state + .borrow() + .session + .as_ref() + .map(|session| session.id); + let Some(session_id) = session_id else { + return Propagation::Proceed; + }; + + let switcher = Rc::clone(&switcher); + // The Wayland keyboard-enter and modifier updates can arrive in + // adjacent dispatches. Let GDK apply both before checking whether + // Super was released before the exclusive grab took effect. + glib::timeout_add_local_once(Duration::from_millis(10), move || { + let is_current_session = switcher + .state + .borrow() + .session + .as_ref() + .is_some_and(|session| session.id == session_id); + if is_current_session + && current_keyboard_modifiers() + .is_some_and(|modifiers| !switch_modifier_is_pressed(modifiers)) + { + switcher.finish(true); + } + }); + Propagation::Proceed + }); + self.window.add_controller(focus_controller); } fn handle_message(&self, message: DaemonMessage) { @@ -247,7 +300,10 @@ impl Switcher { (false, Direction::Next) => (original + 1) % groups.len(), (false, Direction::Previous) => (original + groups.len() - 1) % groups.len(), }; + let session_id = state.next_session_id; + state.next_session_id = state.next_session_id.wrapping_add(1); state.session = Some(Session { + id: session_id, groups, selected, original, @@ -503,6 +559,17 @@ impl Switcher { } } +fn switch_modifier_is_pressed(modifiers: gdk::ModifierType) -> bool { + modifiers.intersects(gdk::ModifierType::SUPER_MASK | gdk::ModifierType::META_MASK) +} + +fn current_keyboard_modifiers() -> Option { + gdk::Display::default()? + .default_seat()? + .keyboard() + .map(|keyboard| keyboard.modifier_state()) +} + fn install_css() { let provider = gtk::CssProvider::new(); provider.load_from_data( @@ -657,3 +724,26 @@ fn main() -> Result<()> { Some(command) => anyhow::bail!("unknown command: {command}"), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn switch_modifier_accepts_super_and_meta_mappings() { + assert!(switch_modifier_is_pressed(gdk::ModifierType::SUPER_MASK)); + assert!(switch_modifier_is_pressed(gdk::ModifierType::META_MASK)); + assert!(switch_modifier_is_pressed( + gdk::ModifierType::SHIFT_MASK | gdk::ModifierType::SUPER_MASK + )); + } + + #[test] + fn switch_modifier_rejects_unrelated_modifiers() { + assert!(!switch_modifier_is_pressed(gdk::ModifierType::empty())); + assert!(!switch_modifier_is_pressed(gdk::ModifierType::SHIFT_MASK)); + assert!(!switch_modifier_is_pressed( + gdk::ModifierType::CONTROL_MASK | gdk::ModifierType::ALT_MASK + )); + } +}