Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: wire virtual keyboards into the keyboard stack
wlr_virtual_keyboard_manager_v1 was created but had no new_virtual_keyboard
listener, so virtual keyboards were advertised to clients and then silently
dropped — keys sent through zwp_virtual_keyboard_v1 went nowhere. Wire them
the same way hardware keyboards go in (InputDevice -> Keyboard -> attach_device
-> KeyboardGroup), so compositor keybindings apply to injected keys too, and
honor the seat the client bound rather than assuming the default.
Two consequences of virtual keyboards actually existing, handled here:
- The client's keymap arrives AFTER creation (the zwp keymap request), but the
group's own wlr_keyboard is what processes keys — with no keymap it has no
xkb_state and drops every key. Virtual keyboards get a keymap listener that
rewrites their (private) group's keymap in place when it lands.
- set_group's matching loop could now let a hardware keyboard join a virtual
group, whose keymap changes under it and whose IME grab is deliberately
disabled. Skip virtual groups when matching.
Verified in a shadow instance with a scratch zwp_virtual_keyboard_v1 client:
typed text (with and without held modifiers) reaches the focused terminal, and
client disconnect tears the device down cleanly. This is what lets wtype-style
tools work and lets cce-shadow sessions exercise keyboard paths — e.g. the
Escape-closes-status-menus arm (7db8c03) is now behaviorally testable.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/input_manager.rs | 30 +++++++++++++++++++++++++
src/server/keyboard.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 85 insertions(+)
diff --git a/src/server/input_manager.rs b/src/server/input_manager.rs
index 51bc4c2..c4bd1fb 100644
--- a/src/server/input_manager.rs
+++ b/src/server/input_manager.rs
@@ -27,6 +27,7 @@ pub struct InputManager {
pub new_text_input: ffi::wl_listener,
pub new_input_method: ffi::wl_listener,
pub new_virtual_pointer_listener: ffi::wl_listener,
+ pub new_virtual_keyboard_listener: ffi::wl_listener,
/// Pending deferred pointer-focus re-evaluation (see
/// [`InputManager::schedule_pointer_refresh`]); null when none. One idle
@@ -104,6 +105,11 @@ impl InputManager {
(*new_virtual_pointer_ptr).notify = Some(handle_new_virtual_pointer);
wl_signal_add(&mut (*self.virtual_pointer_manager).events.new_virtual_pointer, &mut self.new_virtual_pointer_listener);
+ // Connect new_virtual_keyboard listener
+ let new_virtual_keyboard_ptr = &mut self.new_virtual_keyboard_listener as *mut ffi::wl_listener as *mut WlListener;
+ (*new_virtual_keyboard_ptr).notify = Some(handle_new_virtual_keyboard);
+ wl_signal_add(&mut (*self.virtual_keyboard_manager).events.new_virtual_keyboard, &mut self.new_virtual_keyboard_listener);
+
Ok(())
}
@@ -188,6 +194,7 @@ impl InputManager {
wl_listener_remove(&mut self.new_text_input);
wl_listener_remove(&mut self.new_input_method);
wl_listener_remove(&mut self.new_virtual_pointer_listener);
+ wl_listener_remove(&mut self.new_virtual_keyboard_listener);
log::info!("[deinit] InputManager::deinit finished");
}
}
@@ -434,6 +441,29 @@ unsafe extern "C" fn handle_new_virtual_pointer(listener: *mut ffi::wl_listener,
(*im.default_seat).attach_device(device);
}
+unsafe extern "C" fn handle_new_virtual_keyboard(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+ let im = &mut *crate::container_of!(listener, InputManager, new_virtual_keyboard_listener);
+ let virtual_keyboard = data as *mut ffi::wlr_virtual_keyboard_v1;
+
+ log::info!("new virtual keyboard device connected");
+
+ let wlr_device = &mut (*virtual_keyboard).keyboard.base as *mut ffi::wlr_input_device;
+
+ // Honor the seat the client bound the virtual keyboard to, like input
+ // methods do; fall back to the default seat if it carries no Seat data.
+ let mut seat = ffi::river_wlr_seat_get_data((*virtual_keyboard).seat) as *mut Seat;
+ if seat.is_null() {
+ seat = im.default_seat;
+ }
+
+ let device = crate::input_device::InputDevice::new(seat, wlr_device, true);
+ crate::keyboard::Keyboard::create(device);
+
+ // Same path a hardware keyboard takes: attach_device puts the keyboard in
+ // a (private, virtual) KeyboardGroup, so compositor keybindings apply.
+ (*seat).attach_device(device);
+}
+
unsafe extern "C" fn handle_pointer_refresh_idle(data: *mut std::ffi::c_void) {
let manager = data as *mut InputManager;
(*manager).pointer_refresh_idle = std::ptr::null_mut();
diff --git a/src/server/keyboard.rs b/src/server/keyboard.rs
index 55ec155..68c79b3 100644
--- a/src/server/keyboard.rs
+++ b/src/server/keyboard.rs
@@ -19,6 +19,10 @@ pub struct Keyboard {
pub wlr_keyboard: *mut ffi::wlr_keyboard,
pub key_listener: ffi::wl_listener,
pub modifiers_listener: ffi::wl_listener,
+ /// Registered for virtual keyboards only: their keymap arrives from the
+ /// client *after* creation (the zwp_virtual_keyboard_v1 keymap request),
+ /// so it must be forwarded to the group once it lands.
+ pub keymap_listener: ffi::wl_listener,
pub pressed: HashSet<u32>,
pub group: *mut KeyboardGroup,
pub group_link: ffi::wl_list,
@@ -50,6 +54,7 @@ impl Keyboard {
wlr_keyboard,
key_listener: std::mem::zeroed(),
modifiers_listener: std::mem::zeroed(),
+ keymap_listener: std::mem::zeroed(),
pressed: HashSet::new(),
group: std::ptr::null_mut(),
group_link: std::mem::zeroed(),
@@ -75,6 +80,13 @@ impl Keyboard {
let modifiers_signal = ffi::river_wlr_keyboard_get_modifiers_signal(wlr_keyboard);
wl_signal_add(modifiers_signal, &mut (*keyboard).modifiers_listener);
+ if virtual_device {
+ let keymap_listener_ptr = &mut (*keyboard).keymap_listener as *mut ffi::wl_listener as *mut WlListener;
+ (*keymap_listener_ptr).notify = Some(handle_keymap);
+ let keymap_signal = ffi::river_wlr_keyboard_get_keymap_signal(wlr_keyboard);
+ wl_signal_add(keymap_signal, &mut (*keyboard).keymap_listener);
+ }
+
if !virtual_device && should_set_keymap((*(*device).seat).server) {
if !keymap.is_null() {
ffi::wlr_keyboard_set_keymap(wlr_keyboard, keymap);
@@ -90,6 +102,10 @@ impl Keyboard {
pub unsafe fn destroy(keyboard: *mut Self) {
wl_listener_remove(&mut (*keyboard).key_listener);
wl_listener_remove(&mut (*keyboard).modifiers_listener);
+ // destroy_fn runs before the InputDevice is freed, so device is valid.
+ if (*(*keyboard).device).virtual_device {
+ wl_listener_remove(&mut (*keyboard).keymap_listener);
+ }
if !(*keyboard).group.is_null() {
let keys: Vec<u32> = (*keyboard).pressed.iter().cloned().collect();
@@ -114,6 +130,13 @@ impl Keyboard {
while curr != seat_groups_head {
let next = (*curr).next;
let group = crate::container_of!(curr, KeyboardGroup, link);
+ // A virtual keyboard's group is private to it (its keymap can
+ // change under it, and its IME grab is disabled) — hardware
+ // keyboards must never join one.
+ if (*group).virtual_device {
+ curr = next;
+ continue;
+ }
let config_ptr = &mut self.config as *mut KeyboardConfig;
if (*group).match_config(config_ptr) {
self.group = (*group).ref_group();
@@ -246,6 +269,38 @@ unsafe extern "C" fn handle_key(listener: *mut ffi::wl_listener, data: *mut std:
}
}
+/// Virtual keyboards only. The client's keymap request lands after the group
+/// already exists, and the group's own wlr_keyboard is what processes keys —
+/// with no keymap it has no xkb_state and every key is dropped. The group is
+/// private to this keyboard (see set_group), so its config can be rewritten
+/// in place instead of regrouping.
+unsafe extern "C" fn handle_keymap(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+ let keyboard = &mut *crate::container_of!(listener, Keyboard, keymap_listener);
+ let keymap = ffi::river_wlr_keyboard_get_keymap(keyboard.wlr_keyboard);
+
+ if !keymap.is_null() {
+ ffi::xkb_keymap_ref(keymap);
+ }
+ if !keyboard.config.keymap.is_null() {
+ ffi::xkb_keymap_unref(keyboard.config.keymap);
+ }
+ keyboard.config.keymap = keymap;
+
+ if !keyboard.group.is_null() {
+ let group = &mut *keyboard.group;
+ if !keymap.is_null() {
+ ffi::xkb_keymap_ref(keymap);
+ }
+ if !group.config.keymap.is_null() {
+ ffi::xkb_keymap_unref(group.config.keymap);
+ }
+ group.config.keymap = keymap;
+ if !keymap.is_null() {
+ group.process_keymap(keymap);
+ }
+ }
+}
+
unsafe extern "C" fn handle_modifiers(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
let keyboard = &mut *crate::container_of!(listener, Keyboard, modifiers_listener);