git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit9198e2f9096ea8efbe3e64e91b43fc2c54ec0788
parent68e1f53e25
authorLucas Galante <[email protected]>
date2026-09-10 12:50
fix(seat): report held keys to a new focus in evdev codes, not xkb

`wl_keyboard.enter`'s key array is the same space as `wl_keyboard.key` --
the client is what adds 8 to reach an xkb keycode -- but
`keyboard_notify_enter` added it first, shifting every held key up by 8.

That array only goes out when an X11 override-redirect window takes focus
(`XwaylandOverrideRedirect::focus_if_desired`), which is exactly when a
popup menu opens under the key that opened it. Holding Tab (evdev 15) to
open Houdini's TAB menu handed Xwayland keycode 23, which it turned into
X keycode 31 -- `i` -- and typed that into the menu's search field. No
release followed, because the real Tab release frees keycode 23, so X
went on autorepeating it.

Reproduced with an X11 client that maps an override-redirect popup on its
first key press while a virtual keyboard holds Tab: the enter carried
[23] and XQueryKeymap reported `i` down; it now carries [15] and reports
Tab.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/server/seat.rs | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/server/seat.rs b/src/server/seat.rs
index ad7aeea..d84dd4e 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -698,13 +698,21 @@ impl Seat {
         if !kbd.is_null() {
             let group_ptr = ffi::river_wlr_keyboard_get_data(kbd) as *mut crate::keyboard_group::KeyboardGroup;
             if !group_ptr.is_null() {
+                // Raw evdev keycodes, NOT xkb ones: `wl_keyboard.enter`'s key
+                // array is the same space as `wl_keyboard.key`, and the client
+                // is the one that adds 8 to reach an xkb keycode. Adding it
+                // here shifted every held key up by 8 on the way out — and an
+                // X11 popup that opens under a held key is exactly when this
+                // array is sent, so opening Houdini's TAB menu (evdev 15)
+                // handed Xwayland keycode 31 and typed an `i` into it, with no
+                // release to follow, so X autorepeated it.
                 let mut buffer = [0u32; 32];
                 let mut count = 0;
                 for &keycode in (*group_ptr).pressed.keys() {
                     if count >= 32 {
                         break;
                     }
-                    buffer[count] = keycode + 8;
+                    buffer[count] = keycode;
                     count += 1;
                 }
                 let modifiers = ffi::river_wlr_keyboard_get_modifiers(kbd);