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

commit114ef89b027d3b968a98f8dfad12b63c170e22da
parent881c2b1eec
authorLucas Galante <[email protected]>
date2026-07-13 15:11
feat(ipc): injected key-down/key-up now carries modifier state

wlr_seat_keyboard_notify_key delivers keys but never touches modifier
state — that lives on the keyboard device, which injection bypasses —
so clients saw ctrl/shift/alt/super keycodes arrive with their xkb state
stuck at zero and no combo could ever land. Modifier keycodes (ctrl
29/97, shift 42/54, alt 56/100, super 125/126) now also update an
injected xkb mask (resolved per-keymap via xkb_keymap_mod_get_index)
and push wlr_seat_keyboard_notify_modifiers with the mask OR'd over the
device's live state, so a real keyboard keeps working mid-injection.
Compositor keybindings still do not run on injected keys.

Verified on the nested rig with WAYLAND_DEBUG: key(29) →
modifiers(depressed=4) → key(36) reaches the client exactly like
hardware, and settings' ctrl+j/k/i/u section nav now drives end-to-end.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fb2AGNKjfQZxfbxP43fVbC

 src/cce_ctl.rs               |  5 +++--
 src/server/window_manager.rs | 43 ++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 43 insertions(+), 5 deletions(-)

diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index d258233..e1835fd 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -76,8 +76,9 @@ fn usage(name: &str, to_stderr: bool) {
     print("  pointer-press [button]             (held until pointer-release — drives drags)");
     print("  pointer-release [button]");
     print("  keypress <keycode>                 (evdev code; press+release to the focused client)");
-    print("  key-down <keycode>");
-    print("  key-up <keycode>");
+    print("  key-down <keycode>                 (modifier codes — ctrl 29/97, shift 42/54,");
+    print("  key-up <keycode>                    alt 56/100, super 125/126 — update client");
+    print("                                      xkb state, so e.g. 29+36 lands as ctrl+j)");
 }
  
 pub fn run_cce_ctl(args: Vec<String>) {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index b23fd17..821b24f 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -110,6 +110,10 @@ pub struct WindowManager {
     pub last_status_update: std::cell::RefCell<Option<crate::status_server::StatusUpdate>>,
     pub status_hide_mode: bool,
     pub adjust_position_mode: bool,
+    /// xkb modifier mask currently held via injected `key-down` (see the ipc handler):
+    /// OR'd over the device state on every synthetic modifiers notify so clients see
+    /// ctrl/shift/alt/super combos from injection like they would from hardware.
+    pub injected_key_mods: u32,
     pub restore_queue: Vec<SavedWindowState>,
     pub last_window_states: Vec<SavedWindowState>,
     pub shutting_down: bool,
@@ -197,6 +201,7 @@ impl WindowManager {
         self.last_status_update = std::cell::RefCell::new(None);
         self.status_hide_mode = false;
         self.adjust_position_mode = false;
+        self.injected_key_mods = 0;
         let _ = std::fs::remove_file("/tmp/cce-status-interface-adjust-mode");
 
         ffi::wl_list_init(&mut self.sent.outputs);
@@ -3443,22 +3448,54 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
                 reply
             }
             // One-direction key events (held modifiers/keys); `keycode` is the evdev
-            // code. Like `keypress`, this notifies the focused client directly — it does
-            // not run compositor keybindings or update xkb modifier state.
+            // code. Like `keypress`, this notifies the focused client directly and does
+            // not run compositor keybindings. Modifier keycodes additionally update an
+            // injected xkb mask and push a modifiers event, so the focused client's xkb
+            // state tracks ctrl/shift/alt/super combos exactly as it would from
+            // hardware (`wlr_seat_keyboard_notify_key` alone never changes modifier
+            // state — that lives on the keyboard device, which injection bypasses).
             "key-down" | "key-up" => {
                 if parts.len() < 2 { return "error: usage: key-down|key-up <keycode>\n".to_string(); }
                 if let Ok(keycode) = parts[1].parse::<u32>() {
-                    let state = if action == "key-down" {
+                    let pressed = action == "key-down";
+                    let state = if pressed {
                         ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
                     } else {
                         ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED
                     };
+                    // evdev → real xkb modifier name (left/right pairs).
+                    let mod_name: Option<&[u8]> = match keycode {
+                        42 | 54 => Some(b"Shift\0"),
+                        29 | 97 => Some(b"Control\0"),
+                        56 | 100 => Some(b"Mod1\0"),
+                        125 | 126 => Some(b"Mod4\0"),
+                        _ => None,
+                    };
                     let seats_list = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
                     let mut curr_seat = (*seats_list).next;
                     while curr_seat != seats_list {
                         let next_seat = (*curr_seat).next;
                         let seat = crate::container_of!(curr_seat, crate::seat::Seat, link);
                         ffi::wlr_seat_keyboard_notify_key((*seat).wlr_seat, crate::util::msec_timestamp(), keycode, state);
+                        if let Some(name) = mod_name {
+                            let kb = ffi::river_wlr_seat_get_keyboard((*seat).wlr_seat);
+                            if !kb.is_null() && !(*kb).keymap.is_null() {
+                                let idx = ffi::xkb_keymap_mod_get_index((*kb).keymap, name.as_ptr() as *const _);
+                                if idx != ffi::XKB_MOD_INVALID {
+                                    let mask = 1u32 << idx;
+                                    if pressed {
+                                        self.injected_key_mods |= mask;
+                                    } else {
+                                        self.injected_key_mods &= !mask;
+                                    }
+                                    // Injected mask OR'd over the device's live state, so a
+                                    // real keyboard keeps working mid-injection.
+                                    let mut mods = (*kb).modifiers;
+                                    mods.depressed |= self.injected_key_mods;
+                                    ffi::wlr_seat_keyboard_notify_modifiers((*seat).wlr_seat, &mut mods);
+                                }
+                            }
+                        }
                         curr_seat = next_seat;
                     }
                     "ok\n".to_string()