Skip to content

fix: Support Xwayland exclusive keyboard grabs#244

Merged
paperbenni merged 3 commits intomainfrom
fix/xwayland-keyboard-grab-11526415645579586335
Mar 23, 2026
Merged

fix: Support Xwayland exclusive keyboard grabs#244
paperbenni merged 3 commits intomainfrom
fix/xwayland-keyboard-grab-11526415645579586335

Conversation

@paperbenni
Copy link
Copy Markdown
Member

@paperbenni paperbenni commented Mar 22, 2026

Fixes exclusive keyboard grabs (like those used by rofi or dmenu via Xwayland) on the Wayland backend by properly routing events based on active Smithay keyboard grabs instead of ad-hoc window type classification.


PR created automatically by Jules for task 11526415645579586335 started by @paperbenni

Summary by Sourcery

Route keyboard shortcut handling based on actual Wayland keyboard grabs rather than ad-hoc window classification for Xwayland clients.

Bug Fixes:

  • Prevent WM keyboard shortcuts from interfering with exclusive keyboard grabs used by Xwayland clients such as rofi or dmenu.

Enhancements:

  • Simplify keyboard focus handling by removing window-type-based shortcut suppression logic in favor of grab-aware routing.

Summary by CodeRabbit

  • New Features

    • Added support for the Wayland keyboard-shortcuts inhibitor protocol so apps can explicitly block shortcuts.
  • Bug Fixes

    • Shortcuts are now always suppressed while the keyboard is grabbed, respect inhibitor state when not grabbed, and remain enabled for normal window focus. XWayland keyboard grabs now work for surfaces not mapped as full windows.
  • Tests

    • Added basic tests exercising the inhibitor and XWayland grab behaviors.

…e 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>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 22, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adjusts Wayland keyboard handling to rely on Smithay keyboard grabs instead of window classification for determining when WM shortcuts are allowed, and removes the now-unused shortcut suppression helper.

Sequence diagram for updated Wayland keyboard shortcut handling

sequenceDiagram
    participant InputBackend
    participant handle_keyboard
    participant KeyboardHandle
    participant WaylandState
    participant FocusTarget
    participant LayerSurfaceCachedState

    InputBackend->>handle_keyboard: keyboard_key_event
    handle_keyboard->>KeyboardHandle: is_grabbed()
    alt keyboard is grabbed
        handle_keyboard-->>handle_keyboard: wm_shortcuts_allowed = false
    else keyboard not grabbed
        handle_keyboard->>KeyboardHandle: current_focus()
        KeyboardHandle-->>handle_keyboard: KeyboardFocusTarget
        alt focus is None
            handle_keyboard-->>handle_keyboard: wm_shortcuts_allowed = true
        else focus is Window
            handle_keyboard-->>handle_keyboard: wm_shortcuts_allowed = true
        else focus is WlSurface
            handle_keyboard->>LayerSurfaceCachedState: with_states(surface)
            LayerSurfaceCachedState-->>handle_keyboard: keyboard_interactivity
            alt keyboard_interactivity == Exclusive
                handle_keyboard-->>handle_keyboard: wm_shortcuts_allowed = false
            else keyboard_interactivity != Exclusive
                handle_keyboard-->>handle_keyboard: wm_shortcuts_allowed = true
            end
        else focus is Popup
            handle_keyboard-->>handle_keyboard: wm_shortcuts_allowed = false
        end
    end
    handle_keyboard-->>InputBackend: process key with wm_shortcuts_allowed
Loading

Class diagram for updated Wayland keyboard and window classification

classDiagram
    class WaylandState {
        // classify_window(window: Window) -> WindowType
        // should_suppress_shortcuts_for(window: Window) -> bool (removed)
    }

    class Window {
    }

    class WindowType {
        <<enumeration>>
        Normal
        Overlay
        Launcher
        Unmanaged
        Dying
    }

    WaylandState --> WindowType : classify_window
    WaylandState --> Window : operates_on

    class KeyboardHandle {
        is_grabbed() bool
        current_focus() KeyboardFocusTarget
    }

    class KeyboardFocusTarget {
        <<enumeration>>
        Window
        WlSurface
        Popup
        None
    }

    KeyboardHandle --> KeyboardFocusTarget : current_focus

    class LayerSurfaceCachedState {
        current() LayerSurfaceState
    }

    class LayerSurfaceState {
        keyboard_interactivity KeyboardInteractivity
    }

    class KeyboardInteractivity {
        <<enumeration>>
        Exclusive
        Other
    }

    LayerSurfaceCachedState --> LayerSurfaceState : current
    LayerSurfaceState --> KeyboardInteractivity : uses

    class handle_keyboard {
        handle_keyboard(state: WaylandState, keyboard_handle: KeyboardHandle, event: KeyboardKeyEvent)
    }

    class KeyboardKeyEvent {
        key_code() u32
        state() KeyState
    }

    class KeyState {
    }

    handle_keyboard --> WaylandState : reads_state
    handle_keyboard --> KeyboardHandle : queries_focus_and_grab
    handle_keyboard --> KeyboardKeyEvent : reads_key
    handle_keyboard --> LayerSurfaceCachedState : with_states(surface)
    LayerSurfaceCachedState <.. handle_keyboard : used_for_keyboard_interactivity

    WaylandState ..|> handle_keyboard : no_longer_called_should_suppress_shortcuts_for
Loading

File-Level Changes

Change Details Files
Route WM shortcut allowance based on active Smithay keyboard grabs and precise focus target instead of ad‑hoc window classification.
  • Introduce an early check using keyboard_handle.is_grabbed() to fully disable WM shortcuts during active keyboard grabs.
  • Simplify handling for focused Wayland windows so WM shortcuts remain allowed whenever a normal window has focus.
  • Preserve layer-surface keyboard interactivity checks, only allowing WM shortcuts when interactivity is not Exclusive.
  • Explicitly disallow WM shortcuts when a popup has keyboard focus.
src/wayland/input/keyboard/mod.rs
Remove obsolete window-classification-based shortcut suppression helper.
  • Delete should_suppress_shortcuts_for helper that used WindowType classification to decide when to suppress shortcuts.
  • Rely solely on keyboard grab state and focus target logic for shortcut suppression, eliminating this classification path.
src/backend/wayland/compositor/window/classify.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

Warning

Rate limit exceeded

@paperbenni has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5bf8324-94bd-4bb5-a40a-a272ac81061e

📥 Commits

Reviewing files that changed from the base of the PR and between aeffae1 and d1c4612.

📒 Files selected for processing (1)
  • src/wayland/input/keyboard/mod.rs
📝 Walkthrough

Walkthrough

Removed window-classification-based shortcut suppression and integrated Smithay's keyboard-shortcuts-inhibit state; keyboard handling now decides shortcut allowance from grab state, inhibitor state, and focus target type, and XWayland grab focus fallback was adjusted to allow surface-level grabs.

Changes

Cohort / File(s) Summary
Window Classification
src/backend/wayland/compositor/window/classify.rs
Removed WaylandState::should_suppress_shortcuts_for, leaving classify_window and windows_in_z_order unchanged.
Keyboard Input Handling
src/wayland/input/keyboard/mod.rs
Changed shortcut decision logic: if keyboard is grabbed -> disallow shortcuts; otherwise consult keyboard_shortcuts_inhibited() and focus target type (Window now allows shortcuts unconditionally; WlSurface respects interactivity; Popup disallows).
Compositor Handlers
src/backend/wayland/compositor/handlers.rs
Added KeyboardShortcutsInhibitHandler impl for WaylandState; adjusted XWaylandKeyboardGrabHandler::keyboard_focus_for_xsurface to return WlSurface fallback when no mapped window exists.
Compositor Module Wiring
src/backend/wayland/compositor/mod.rs
Hooked Smithay delegate_keyboard_shortcuts_inhibit! into module-level delegates.
Compositor State
src/backend/wayland/compositor/state.rs
Added public field keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState and initialized it in WaylandState::new.
Tests (new)
test_inhibitor.rs, test_xwayland_grab.rs
Added minimal test scaffolds importing inhibitor/keyboard-grab types (empty mains).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Compositor as WaylandState
  participant Inhibit as KeyboardShortcutsInhibitState
  participant SurfaceOrWin as FocusTarget

  Client->>Compositor: Key event (press)
  Compositor->>Compositor: check keyboard_handle.is_grabbed()
  alt grabbed
    Compositor-->>Client: deliver (shortcuts disabled)
  else not grabbed
    Compositor->>Inhibit: keyboard_shortcuts_inhibited()?
    alt inhibited
      Compositor-->>Client: deliver (shortcuts disabled)
    else not inhibited
      Compositor->>SurfaceOrWin: determine focus target (Window/WlSurface/Popup)
      alt Window
        Compositor-->>Client: deliver (shortcuts allowed)
      else WlSurface (non-exclusive)
        Compositor-->>Client: deliver (possible shortcuts)
      else Popup
        Compositor-->>Client: deliver (shortcuts disabled)
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through handlers, soft and spry,

Grabbed keys halt shortcuts — that's no lie,
Inhibitors whispered, "quiet, please,"
Windows open, popups freeze,
A little rabbit taps acclaim — hop, hooray! 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the changeset: adding support for XWayland exclusive keyboard grabs by implementing Smithay keyboard shortcuts inhibit protocol and improving keyboard grab handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/xwayland-keyboard-grab-11526415645579586335

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The new wm_shortcuts_allowed logic now treats any keyboard grab (keyboard_handle.is_grabbed()) as fully suppressing WM shortcuts; if Smithay distinguishes between exclusive and non-exclusive grabs, consider checking specifically for exclusive grabs to avoid over-suppressing shortcuts.
  • With should_suppress_shortcuts_for removed, WindowType::Overlay/Launcher/Unmanaged are no longer used for shortcut routing; if these types still have semantic meaning elsewhere, consider whether any of that behavior should be preserved for non-grabbed windows or if the classifications can be simplified.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `wm_shortcuts_allowed` logic now treats any keyboard grab (`keyboard_handle.is_grabbed()`) as fully suppressing WM shortcuts; if Smithay distinguishes between exclusive and non-exclusive grabs, consider checking specifically for exclusive grabs to avoid over-suppressing shortcuts.
- With `should_suppress_shortcuts_for` removed, `WindowType::Overlay`/`Launcher`/`Unmanaged` are no longer used for shortcut routing; if these types still have semantic meaning elsewhere, consider whether any of that behavior should be preserved for non-grabbed windows or if the classifications can be simplified.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@paperbenni
Copy link
Copy Markdown
Member Author

@jules the problem is still happening, maybe it is somewhere else

@paperbenni
Copy link
Copy Markdown
Member Author

@jules
rofi is not running in xwayland btw, so the problem affects both xwayland and rofi (and likely more)

@google-labs-jules
Copy link
Copy Markdown
Contributor

@jules the problem is still happening, maybe it is somewhere else

Good catch! It turns out XWayland often requests keyboard grabs on dummy surfaces or the root window rather than a fully mapped Wayland Window. Because of this, Smithay's keyboard_focus_for_xsurface was returning None and silently ignoring the grab. I have updated the handler to fallback to returning the raw WlSurface, which ensures the grab is successfully established and correctly suppresses WM shortcuts. I've included the fix in the latest commit.

@paperbenni
Copy link
Copy Markdown
Member Author

@jules this is not just affecting keys which have window manager shortcuts. even keys which have no window manager shortcuts do not get passed to rofi (A NATIVE WAYLAND APPLICATION, NOT RUNNING IN XWAYLAND). Fuzzel straight up closes without doing anything, likely because the key grab fails

…\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>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/wayland/input/keyboard/mod.rs`:
- Around line 27-39: The WlSurface match arm currently assumes every WlSurface
has LayerSurfaceCachedState and unconditionally calls
.get::<LayerSurfaceCachedState>(), which panics for raw grab surfaces; instead,
change the WlSurface branch in the keyboard focus check (the match on
keyboard_handle.current_focus()) to first try to fetch LayerSurfaceCachedState
via with_states(s, |states|
states.cached_state.get::<LayerSurfaceCachedState>()), and if that returns
Some(layer_state) read layer_state.current().keyboard_interactivity and compare
to KeyboardInteractivity::Exclusive, but if it returns None (raw grab surface)
treat it as non-exclusive (i.e., allow shortcuts/return true). This ensures
KeyboardFocusTarget::WlSurface is split into layer-backed vs raw grab handling
without panicking.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2957f186-50f1-42c1-97a2-8b7dd991b426

📥 Commits

Reviewing files that changed from the base of the PR and between 8abfb29 and aeffae1.

📒 Files selected for processing (6)
  • src/backend/wayland/compositor/handlers.rs
  • src/backend/wayland/compositor/mod.rs
  • src/backend/wayland/compositor/state.rs
  • src/wayland/input/keyboard/mod.rs
  • test_inhibitor.rs
  • test_xwayland_grab.rs
✅ Files skipped from review due to trivial changes (2)
  • test_inhibitor.rs
  • test_xwayland_grab.rs

@paperbenni
Copy link
Copy Markdown
Member Author

@jules
Verify each finding against the current code and only fix it if needed.

In @src/wayland/input/keyboard/mod.rs around lines 27 - 39, The WlSurface match
arm currently assumes every WlSurface has LayerSurfaceCachedState and
unconditionally calls .get::(), which panics for raw
grab surfaces; instead, change the WlSurface branch in the keyboard focus check
(the match on keyboard_handle.current_focus()) to first try to fetch
LayerSurfaceCachedState via with_states(s, |states|
states.cached_state.get::()), and if that returns
Some(layer_state) read layer_state.current().keyboard_interactivity and compare
to KeyboardInteractivity::Exclusive, but if it returns None (raw grab surface)
treat it as non-exclusive (i.e., allow shortcuts/return true). This ensures
KeyboardFocusTarget::WlSurface is split into layer-backed vs raw grab handling
without panicking.

…itors\n\n- Fixes a panic in `handle_keyboard` where `LayerSurfaceCachedState` was retrieved blindly from any `WlSurface` focus target. Now uses `.has::<LayerSurfaceCachedState>()` 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>
@paperbenni paperbenni merged commit 02626c2 into main Mar 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant