From 8abfb296e17e0b1a2be283bcd87a16a065648356 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:41:09 +0000 Subject: [PATCH 1/3] feat: Use keyboard grab state to suppress WM shortcuts\n\nProperly use Smithay's `keyboard_handle.is_grabbed()` in the keyboard input handler to delegate input to active keyboard grabs (e.g., from `zwp_xwayland_keyboard_grab_v1` used by rofi or dmenu). This replaces the fragile, manual suppression of WM shortcuts based on window types (Launcher/Overlay), ensuring full protocol compatibility for exclusive keyboard access. Co-authored-by: paperbenni <15818888+paperbenni@users.noreply.github.com> --- .../wayland/compositor/window/classify.rs | 11 ------- src/wayland/input/keyboard/mod.rs | 30 +++++++++++-------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/backend/wayland/compositor/window/classify.rs b/src/backend/wayland/compositor/window/classify.rs index 8c50fe60..f8963fc4 100644 --- a/src/backend/wayland/compositor/window/classify.rs +++ b/src/backend/wayland/compositor/window/classify.rs @@ -73,17 +73,6 @@ impl WaylandState { WindowType::Normal } - /// Check if a window should suppress WM keyboard shortcuts when focused. - /// - /// Returns true for overlay windows (dmenu, popups, menus) where - /// keyboard input should go to the window without triggering keybindings. - pub fn should_suppress_shortcuts_for(&self, window: &Window) -> bool { - match self.classify_window(window) { - WindowType::Overlay | WindowType::Launcher | WindowType::Unmanaged => true, - WindowType::Normal | WindowType::Dying => false, - } - } - /// Iterator over windows in z-order (top-to-bottom), along with their type. /// /// This follows the render-order defined in `assemble_scene_elements!`: diff --git a/src/wayland/input/keyboard/mod.rs b/src/wayland/input/keyboard/mod.rs index d1872eb0..bda7ea28 100644 --- a/src/wayland/input/keyboard/mod.rs +++ b/src/wayland/input/keyboard/mod.rs @@ -17,20 +17,24 @@ pub fn handle_keyboard( event: impl KeyboardKeyEvent, ) { let serial = SERIAL_COUNTER.next_serial(); - let wm_shortcuts_allowed = match keyboard_handle.current_focus() { - None => true, - Some(KeyboardFocusTarget::Window(ref w)) => !state.should_suppress_shortcuts_for(w), - Some(KeyboardFocusTarget::WlSurface(ref s)) => { - let interactivity = with_states(s, |states| { - states - .cached_state - .get::() - .current() - .keyboard_interactivity - }); - interactivity != KeyboardInteractivity::Exclusive + let wm_shortcuts_allowed = if keyboard_handle.is_grabbed() { + false + } else { + match keyboard_handle.current_focus() { + None => true, + Some(KeyboardFocusTarget::Window(_)) => true, + Some(KeyboardFocusTarget::WlSurface(ref s)) => { + let interactivity = with_states(s, |states| { + states + .cached_state + .get::() + .current() + .keyboard_interactivity + }); + interactivity != KeyboardInteractivity::Exclusive + } + Some(KeyboardFocusTarget::Popup(_)) => false, } - Some(KeyboardFocusTarget::Popup(_)) => false, }; let key_code = event.key_code(); let key_state = event.state(); From aeffae1ab3abbee6198cf75140ca17f262e4daa1 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:25:20 +0000 Subject: [PATCH 2/3] feat: properly implement Wayland/XWayland keyboard grabbing protocols\n\n- Removed ad-hoc manual WM shortcut suppression based on window type heuristics.\n- Integrated Smithay's `zwp_keyboard_shortcuts_inhibit_v1` protocol to respect native Wayland shortcut inhibitors.\n- Updated `XWaylandKeyboardGrabHandler` to correctly fallback to the raw `WlSurface` for unmanaged windows or dummy surfaces, preventing `dmenu` or `rofi` grabs from being silently dropped.\n- Updated `handle_keyboard` to natively disable WM shortcuts globally when `keyboard_handle.is_grabbed()` is true or when `seat.keyboard_shortcuts_inhibited()` is true. Co-authored-by: paperbenni <15818888+paperbenni@users.noreply.github.com> --- src/backend/wayland/compositor/handlers.rs | 24 +++++++++++++++-- src/backend/wayland/compositor/mod.rs | 9 ++++--- src/backend/wayland/compositor/state.rs | 4 +++ src/wayland/input/keyboard/mod.rs | 31 +++++++++++++--------- test_inhibitor.rs | 2 ++ test_xwayland_grab.rs | 2 ++ 6 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 test_inhibitor.rs create mode 100644 test_xwayland_grab.rs diff --git a/src/backend/wayland/compositor/handlers.rs b/src/backend/wayland/compositor/handlers.rs index edbd2904..62852a4c 100644 --- a/src/backend/wayland/compositor/handlers.rs +++ b/src/backend/wayland/compositor/handlers.rs @@ -7,6 +7,10 @@ use smithay::{ buffer::BufferHandler, compositor::{CompositorHandler, get_parent, is_sync_subsurface}, dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}, + keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, + KeyboardShortcutsInhibitor, + }, output::OutputHandler, seat::WaylandFocus, selection::data_device::{ClientDndGrabHandler, ServerDndGrabHandler}, @@ -210,8 +214,24 @@ impl XWaylandKeyboardGrabHandler for WaylandState { } } // For unmanaged X11 surfaces (like dmenu), search in the space - self.window_for_surface(surface) - .map(KeyboardFocusTarget::Window) + if let Some(window) = self.window_for_surface(surface) { + return Some(KeyboardFocusTarget::Window(window)); + } + + // Fallback: If XWayland requests a grab for a surface that isn't mapped + // as a full window (e.g., grabbing the root window or a dummy surface), + // we must still allow the grab by returning the raw WlSurface. + Some(KeyboardFocusTarget::WlSurface(surface.clone())) + } +} + +impl KeyboardShortcutsInhibitHandler for WaylandState { + fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState { + &mut self.keyboard_shortcuts_inhibit_state + } + + fn new_inhibitor(&mut self, _inhibitor: KeyboardShortcutsInhibitor) { + // We handle the inhibitor implicitly via KeyboardShortcutsInhibitState::keyboard_shortcuts_inhibited } } diff --git a/src/backend/wayland/compositor/mod.rs b/src/backend/wayland/compositor/mod.rs index 19aa1507..bbbc9182 100644 --- a/src/backend/wayland/compositor/mod.rs +++ b/src/backend/wayland/compositor/mod.rs @@ -42,10 +42,10 @@ pub use state::{WaylandClientState, WaylandState, WindowIdMarker}; use smithay::{ delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_idle_inhibit, - delegate_layer_shell, delegate_output, delegate_pointer_gestures, delegate_relative_pointer, - delegate_seat, delegate_session_lock, delegate_shm, delegate_viewporter, - delegate_xdg_activation, delegate_xdg_decoration, delegate_xdg_shell, - delegate_xwayland_keyboard_grab, delegate_xwayland_shell, + delegate_keyboard_shortcuts_inhibit, delegate_layer_shell, delegate_output, + delegate_pointer_gestures, delegate_relative_pointer, delegate_seat, delegate_session_lock, + delegate_shm, delegate_viewporter, delegate_xdg_activation, delegate_xdg_decoration, + delegate_xdg_shell, delegate_xwayland_keyboard_grab, delegate_xwayland_shell, }; // --------------------------------------------------------------------------- @@ -67,5 +67,6 @@ delegate_xdg_activation!(WaylandState); delegate_xdg_decoration!(WaylandState); delegate_xdg_shell!(WaylandState); delegate_session_lock!(WaylandState); +delegate_keyboard_shortcuts_inhibit!(WaylandState); delegate_xwayland_keyboard_grab!(WaylandState); delegate_xwayland_shell!(WaylandState); diff --git a/src/backend/wayland/compositor/state.rs b/src/backend/wayland/compositor/state.rs index a89cde6d..c8ec3b46 100644 --- a/src/backend/wayland/compositor/state.rs +++ b/src/backend/wayland/compositor/state.rs @@ -25,6 +25,7 @@ use smithay::{ dmabuf::{DmabufFeedbackBuilder, DmabufGlobal, DmabufState}, foreign_toplevel_list::{ForeignToplevelHandle, ForeignToplevelListState}, idle_inhibit::IdleInhibitManagerState, + keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitState, output::OutputManagerState, pointer_gestures::PointerGesturesState, relative_pointer::RelativePointerManagerState, @@ -114,6 +115,7 @@ pub struct WaylandState { pub session_lock_manager_state: SessionLockManagerState, pub lock_state: SessionLockState, pub lock_surfaces: HashMap, + pub keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState, pub idle_inhibiting_surfaces: HashSet, pub(super) render_node: Option, @@ -224,6 +226,7 @@ impl WaylandState { let viewporter_state = ViewporterState::new::(&dh); let idle_inhibit_manager_state = IdleInhibitManagerState::new::(&dh); let session_lock_manager_state = SessionLockManagerState::new::(&dh, |_| true); + let keyboard_shortcuts_inhibit_state = KeyboardShortcutsInhibitState::new::(&dh); // -- Seat (input devices) -- let cursor_config = wm.g.cfg.cursor.clone(); @@ -257,6 +260,7 @@ impl WaylandState { viewporter_state, idle_inhibit_manager_state, session_lock_manager_state, + keyboard_shortcuts_inhibit_state, lock_state: SessionLockState::Unlocked, lock_surfaces: HashMap::new(), idle_inhibiting_surfaces: HashSet::new(), diff --git a/src/wayland/input/keyboard/mod.rs b/src/wayland/input/keyboard/mod.rs index bda7ea28..207c6865 100644 --- a/src/wayland/input/keyboard/mod.rs +++ b/src/wayland/input/keyboard/mod.rs @@ -9,6 +9,7 @@ use crate::backend::wayland::compositor::{KeyboardFocusTarget, WaylandState}; use crate::wayland::common::modifiers_to_x11_mask; use smithay::utils::SERIAL_COUNTER; +use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat; /// Handle keyboard events. pub fn handle_keyboard( @@ -20,20 +21,24 @@ pub fn handle_keyboard( let wm_shortcuts_allowed = if keyboard_handle.is_grabbed() { false } else { - match keyboard_handle.current_focus() { - None => true, - Some(KeyboardFocusTarget::Window(_)) => true, - Some(KeyboardFocusTarget::WlSurface(ref s)) => { - let interactivity = with_states(s, |states| { - states - .cached_state - .get::() - .current() - .keyboard_interactivity - }); - interactivity != KeyboardInteractivity::Exclusive + if state.seat.keyboard_shortcuts_inhibited() { + false + } else { + match keyboard_handle.current_focus() { + None => true, + Some(KeyboardFocusTarget::Window(_)) => true, + Some(KeyboardFocusTarget::WlSurface(ref s)) => { + let interactivity = with_states(s, |states| { + states + .cached_state + .get::() + .current() + .keyboard_interactivity + }); + interactivity != KeyboardInteractivity::Exclusive + } + Some(KeyboardFocusTarget::Popup(_)) => false, } - Some(KeyboardFocusTarget::Popup(_)) => false, } }; let key_code = event.key_code(); diff --git a/test_inhibitor.rs b/test_inhibitor.rs new file mode 100644 index 00000000..aaa8f6eb --- /dev/null +++ b/test_inhibitor.rs @@ -0,0 +1,2 @@ +use smithay::wayland::keyboard_shortcuts_inhibit::{KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitHandler}; +fn main() {} diff --git a/test_xwayland_grab.rs b/test_xwayland_grab.rs new file mode 100644 index 00000000..7ce49842 --- /dev/null +++ b/test_xwayland_grab.rs @@ -0,0 +1,2 @@ +use smithay::input::keyboard::{KeyboardHandle, GrabStatus}; +fn main() {} From d1c461240fac47e6ad05ea9dab4963e5cb654803 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:53:04 +0000 Subject: [PATCH 3/3] fix: safely unwrap layer shell state and support native wayland inhibitors\n\n- Fixes a panic in `handle_keyboard` where `LayerSurfaceCachedState` was retrieved blindly from any `WlSurface` focus target. Now uses `.has::()` safely.\n- Fully implement `zwp_keyboard_shortcuts_inhibit_v1` using `KeyboardShortcutsInhibitState`, allowing native Wayland applications like `fuzzel` or native `rofi` to explicitly grab the keyboard. Co-authored-by: paperbenni <15818888+paperbenni@users.noreply.github.com> --- src/wayland/input/keyboard/mod.rs | 19 ++++++++++++------- test_inhibitor.rs | 2 -- test_xwayland_grab.rs | 2 -- 3 files changed, 12 insertions(+), 11 deletions(-) delete mode 100644 test_inhibitor.rs delete mode 100644 test_xwayland_grab.rs diff --git a/src/wayland/input/keyboard/mod.rs b/src/wayland/input/keyboard/mod.rs index 207c6865..94c2263b 100644 --- a/src/wayland/input/keyboard/mod.rs +++ b/src/wayland/input/keyboard/mod.rs @@ -28,14 +28,19 @@ pub fn handle_keyboard( None => true, Some(KeyboardFocusTarget::Window(_)) => true, Some(KeyboardFocusTarget::WlSurface(ref s)) => { - let interactivity = with_states(s, |states| { - states - .cached_state - .get::() - .current() - .keyboard_interactivity + let is_exclusive = with_states(s, |states| { + if states.cached_state.has::() { + states + .cached_state + .get::() + .current() + .keyboard_interactivity + == KeyboardInteractivity::Exclusive + } else { + false + } }); - interactivity != KeyboardInteractivity::Exclusive + !is_exclusive } Some(KeyboardFocusTarget::Popup(_)) => false, } diff --git a/test_inhibitor.rs b/test_inhibitor.rs deleted file mode 100644 index aaa8f6eb..00000000 --- a/test_inhibitor.rs +++ /dev/null @@ -1,2 +0,0 @@ -use smithay::wayland::keyboard_shortcuts_inhibit::{KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitHandler}; -fn main() {} diff --git a/test_xwayland_grab.rs b/test_xwayland_grab.rs deleted file mode 100644 index 7ce49842..00000000 --- a/test_xwayland_grab.rs +++ /dev/null @@ -1,2 +0,0 @@ -use smithay::input::keyboard::{KeyboardHandle, GrabStatus}; -fn main() {}