Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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()