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

src/server/keyboard_group.rs (29.2K)

  1 // SPDX-FileCopyrightText: © 2026 The River Developers
  2 // SPDX-License-Identifier: GPL-3.0-only
  3 
  4 use crate::ffi;
  5 use crate::seat::Seat;
  6 use crate::keyboard::KeyboardConfig;
  7 use crate::xkb_bindings::XkbBinding;
  8 use crate::server::wl_listener_remove;
  9 use std::collections::HashMap;
 10 
 11 #[derive(Clone, Debug, PartialEq, Eq)]
 12 pub enum KeyConsumer {
 13     Builtin,
 14     Binding(*mut XkbBinding),
 15     CceBinding(crate::config::Keybind),
 16     /// A chord bound through the GlobalShortcuts portal backend (see
 17     /// `global_shortcuts`): the press and the release are both reported on
 18     /// the status socket's `shortcuts` topic and neither reaches the client.
 19     PortalShortcut { session: String, id: String },
 20     EnsureEaten,
 21     ImGrab,
 22     Focus,
 23 }
 24 
 25 pub struct Press {
 26     pub consumer: KeyConsumer,
 27     pub count: u32,
 28 }
 29 
 30 pub struct KeyboardGroup {
 31     pub ref_count: u32,
 32     pub seat: *mut Seat,
 33     pub link: ffi::wl_list, // Seat.keyboard_groups
 34     pub virtual_device: bool,
 35     pub config: KeyboardConfig,
 36     pub wlr_keyboard: ffi::wlr_keyboard,
 37     pub modifiers_old: u32,
 38     pub pressed: HashMap<u32, Press>,
 39     pub key_listener: ffi::wl_listener,
 40     pub modifiers_listener: ffi::wl_listener,
 41     pub keyboards: ffi::wl_list, // list of keyboards in this group
 42 }
 43 
 44 impl KeyboardGroup {
 45     pub unsafe fn create(
 46         seat: *mut Seat,
 47         config: KeyboardConfig,
 48         virtual_device: bool,
 49     ) -> Result<*mut Self, &'static str> {
 50         let mut group = Box::new(Self {
 51             ref_count: 1,
 52             seat,
 53             link: std::mem::zeroed(),
 54             virtual_device,
 55             config,
 56             wlr_keyboard: std::mem::zeroed(),
 57             modifiers_old: 0,
 58             pressed: HashMap::new(),
 59             key_listener: std::mem::zeroed(),
 60             modifiers_listener: std::mem::zeroed(),
 61             keyboards: std::mem::zeroed(),
 62         });
 63 
 64         ffi::wl_list_init(&mut group.keyboards);
 65         ffi::wl_list_init(&mut group.link);
 66 
 67         // Add to seat.keyboard_groups
 68         let seat_groups_head = &mut (*seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
 69         crate::server::wl_list_insert((*seat_groups_head).prev, &mut group.link as *mut ffi::wl_list as *mut crate::server::WlList);
 70 
 71         let group_ptr = Box::into_raw(group);
 72 
 73         ffi::river_wlr_keyboard_init(
 74             &mut (*group_ptr).wlr_keyboard,
 75             Some(led_update),
 76             b"river.KeyboardGroup\0".as_ptr() as *const _,
 77         );
 78 
 79         ffi::river_wlr_keyboard_set_data(&mut (*group_ptr).wlr_keyboard, group_ptr as *mut _);
 80 
 81         if !config.keymap.is_null() {
 82             ffi::wlr_keyboard_set_keymap(&mut (*group_ptr).wlr_keyboard, config.keymap);
 83         }
 84         ffi::wlr_keyboard_set_repeat_info(&mut (*group_ptr).wlr_keyboard, config.repeat_rate, config.repeat_delay);
 85 
 86         let key_listener_ptr = &mut (*group_ptr).key_listener as *mut ffi::wl_listener as *mut crate::server::WlListener;
 87         (*key_listener_ptr).notify = Some(handle_group_key);
 88         let key_signal = ffi::river_wlr_keyboard_get_key_signal(&mut (*group_ptr).wlr_keyboard);
 89         crate::server::wl_signal_add(key_signal, &mut (*group_ptr).key_listener);
 90 
 91         let modifiers_listener_ptr = &mut (*group_ptr).modifiers_listener as *mut ffi::wl_listener as *mut crate::server::WlListener;
 92         (*modifiers_listener_ptr).notify = Some(handle_group_modifiers);
 93         let modifiers_signal = ffi::river_wlr_keyboard_get_modifiers_signal(&mut (*group_ptr).wlr_keyboard);
 94         crate::server::wl_signal_add(modifiers_signal, &mut (*group_ptr).modifiers_listener);
 95 
 96         if !config.keymap.is_null() {
 97             ffi::xkb_keymap_ref(config.keymap);
 98         }
 99 
100         Ok(group_ptr)
101     }
102 
103     pub unsafe fn ref_group(&mut self) -> *mut Self {
104         self.ref_count += 1;
105         self
106     }
107 
108     pub unsafe fn unref(&mut self, to_release: &[u32]) {
109         for &keycode in to_release {
110             let mut event = ffi::wlr_keyboard_key_event {
111                 time_msec: crate::util::msec_timestamp(),
112                 keycode,
113                 update_state: true,
114                 state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED,
115             };
116             self.process_key(&mut event);
117         }
118 
119         self.ref_count -= 1;
120         if self.ref_count > 0 {
121             return;
122         }
123 
124         crate::server::wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut crate::server::WlList);
125         wl_listener_remove(&mut self.key_listener);
126         wl_listener_remove(&mut self.modifiers_listener);
127 
128         // If the currently active keyboard of a seat is destroyed, we need to set a new active keyboard.
129         let active_wlr_kbd = ffi::river_wlr_seat_get_keyboard((*self.seat).wlr_seat);
130         if active_wlr_kbd == &mut self.wlr_keyboard {
131             let seat_groups_head = &mut (*self.seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
132             let first_node = (*seat_groups_head).next;
133             if first_node != seat_groups_head {
134                 let other_group = crate::container_of!(first_node, KeyboardGroup, link);
135                 ffi::wlr_seat_set_keyboard((*self.seat).wlr_seat, &mut (*other_group).wlr_keyboard);
136             } else {
137                 ffi::wlr_seat_set_keyboard((*self.seat).wlr_seat, std::ptr::null_mut());
138             }
139         }
140 
141         ffi::wlr_keyboard_finish(&mut self.wlr_keyboard);
142 
143         if !self.config.keymap.is_null() {
144             ffi::xkb_keymap_unref(self.config.keymap);
145         }
146 
147         let _boxed = Box::from_raw(self);
148     }
149 
150     pub unsafe fn match_config(&self, config: *mut KeyboardConfig) -> bool {
151         if self.config.repeat_rate != (*config).repeat_rate {
152             return false;
153         }
154         if self.config.repeat_delay != (*config).repeat_delay {
155             return false;
156         }
157         if self.config.keymap == (*config).keymap {
158             return true;
159         }
160         if self.config.keymap.is_null() || (*config).keymap.is_null() {
161             return false;
162         }
163 
164         let a_string_ptr = ffi::xkb_keymap_get_as_string(self.config.keymap, ffi::xkb_keymap_format_XKB_KEYMAP_FORMAT_TEXT_V1);
165         if a_string_ptr.is_null() {
166             return false;
167         }
168         let b_string_ptr = ffi::xkb_keymap_get_as_string((*config).keymap, ffi::xkb_keymap_format_XKB_KEYMAP_FORMAT_TEXT_V1);
169         if b_string_ptr.is_null() {
170             libc::free(a_string_ptr as *mut _);
171             return false;
172         }
173 
174         let a_str = std::ffi::CStr::from_ptr(a_string_ptr);
175         let b_str = std::ffi::CStr::from_ptr(b_string_ptr);
176         let matched = a_str == b_str;
177 
178         libc::free(a_string_ptr as *mut _);
179         libc::free(b_string_ptr as *mut _);
180 
181         if matched {
182             ffi::xkb_keymap_unref((*config).keymap);
183             (*config).keymap = self.config.keymap;
184             ffi::xkb_keymap_ref((*config).keymap);
185         }
186 
187         matched
188     }
189 
190     pub unsafe fn process_key(&mut self, event: *const ffi::wlr_keyboard_key_event) {
191         if let Some(key) = self.pressed.get_mut(&(*event).keycode) {
192             assert!(key.count > 0);
193             if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
194                 key.count += 1;
195             } else {
196                 key.count -= 1;
197                 if key.count == 0 {
198                     let mut key_event = ffi::wlr_keyboard_key_event {
199                         time_msec: (*event).time_msec,
200                         keycode: (*event).keycode,
201                         update_state: true,
202                         state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED,
203                     };
204                     ffi::wlr_keyboard_notify_key(&mut self.wlr_keyboard, &mut key_event);
205                 }
206             }
207         } else if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
208             if self.pressed.len() < 32 {
209                 let mut key_event = ffi::wlr_keyboard_key_event {
210                     time_msec: (*event).time_msec,
211                     keycode: (*event).keycode,
212                     update_state: true,
213                     state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED,
214                 };
215                 ffi::wlr_keyboard_notify_key(&mut self.wlr_keyboard, &mut key_event);
216             }
217         }
218     }
219 
220     pub unsafe fn process_modifiers(&mut self, modifiers: ffi::wlr_keyboard_modifiers) {
221         ffi::wlr_keyboard_notify_modifiers(
222             &mut self.wlr_keyboard,
223             modifiers.depressed,
224             modifiers.latched,
225             modifiers.locked,
226             modifiers.group,
227         );
228     }
229 
230     pub unsafe fn process_keymap(&mut self, keymap: *mut ffi::xkb_keymap) {
231         ffi::wlr_keyboard_set_keymap(&mut self.wlr_keyboard, keymap);
232     }
233 
234     pub unsafe fn get_input_method_grab(&self) -> *mut ffi::wlr_input_method_keyboard_grab_v2 {
235         if self.virtual_device {
236             return std::ptr::null_mut();
237         }
238         let input_method = (*self.seat).relay.input_method;
239         if !input_method.is_null() {
240             return (*input_method).keyboard_grab;
241         }
242         std::ptr::null_mut()
243     }
244 
245     pub unsafe fn send_state(&mut self) {
246         let keymap = self.config.keymap;
247         if keymap.is_null() {
248             return;
249         }
250         let layout_index = self.wlr_keyboard.modifiers.group;
251         let layout_name = ffi::xkb_keymap_layout_get_name(keymap, layout_index);
252         let caps_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Caps Lock\0".as_ptr() as *const _);
253         let capslock = if caps_idx != ffi::XKB_MOD_INVALID {
254             let caps_mask = 1 << caps_idx;
255             (self.wlr_keyboard.modifiers.locked & caps_mask) != 0
256         } else {
257             false
258         };
259         let num_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Num Lock\0".as_ptr() as *const _);
260         let numlock = if num_idx != ffi::XKB_MOD_INVALID {
261             let num_mask = 1 << num_idx;
262             (self.wlr_keyboard.modifiers.locked & num_mask) != 0
263         } else {
264             false
265         };
266 
267         let server = (*self.seat).server;
268         let keyboards_head = &mut (*server).xkb_config.keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
269         let mut curr = (*keyboards_head).next;
270         while curr != keyboards_head {
271             let next = (*curr).next;
272             let xkb_kbd = crate::container_of!(curr, crate::xkb_keyboard::XkbKeyboard, link);
273             let parent_dev = (*xkb_kbd).parent_device;
274             let kbd = (*parent_dev).destroy_data as *mut crate::keyboard::Keyboard;
275             if !kbd.is_null() && (*kbd).group == self as *mut KeyboardGroup {
276                 (*xkb_kbd).send_state(layout_index, layout_name, capslock, numlock);
277             }
278             curr = next;
279         }
280     }
281 }
282 
283 fn is_vt_switch(keysym: u32) -> bool {
284     (ffi::XKB_KEY_XF86Switch_VT_1..=ffi::XKB_KEY_XF86Switch_VT_12).contains(&keysym)
285 }
286 
287 /// The keys that still act while the session is locked. Everything else —
288 /// every config keybind (close window, spawn, reload), portal shortcut,
289 /// client-registered xkb binding and input-method grab — goes to the lock
290 /// screen instead. Until 2026-10-01 nothing here asked about the lock, so at
291 /// the lock screen super+q closed the focused window behind it and any
292 /// `spawn` bind ran. What stays: switching VTs (a builtin), and a config
293 /// keybind on a volume, brightness or media key, the controls a locked
294 /// laptop still needs — the same set other compositors bind `--locked`.
295 fn allowed_while_locked(keysym: u32) -> bool {
296     is_vt_switch(keysym)
297         || matches!(
298             keysym,
299             ffi::XKB_KEY_XF86AudioRaiseVolume
300                 | ffi::XKB_KEY_XF86AudioLowerVolume
301                 | ffi::XKB_KEY_XF86AudioMute
302                 | ffi::XKB_KEY_XF86AudioMicMute
303                 | ffi::XKB_KEY_XF86AudioPlay
304                 | ffi::XKB_KEY_XF86AudioPause
305                 | ffi::XKB_KEY_XF86AudioStop
306                 | ffi::XKB_KEY_XF86AudioNext
307                 | ffi::XKB_KEY_XF86AudioPrev
308                 | ffi::XKB_KEY_XF86MonBrightnessUp
309                 | ffi::XKB_KEY_XF86MonBrightnessDown
310                 | ffi::XKB_KEY_XF86KbdBrightnessUp
311                 | ffi::XKB_KEY_XF86KbdBrightnessDown
312         )
313 }
314 
315 unsafe fn handle_builtin_binding(seat: *mut Seat, keysym: u32, modifiers: u32) -> bool {
316     match keysym {
317         ffi::XKB_KEY_XF86Switch_VT_1..=ffi::XKB_KEY_XF86Switch_VT_12 => {
318             log::debug!("switch VT keysym received");
319             let server = (*seat).server;
320             let session = (*server).session;
321             if !session.is_null() {
322                 let vt = keysym - ffi::XKB_KEY_XF86Switch_VT_1 + 1;
323                 log::info!("switching to VT {}", vt);
324                 ffi::wlr_session_change_vt(session, vt);
325             }
326             true
327         }
328         // Plain Escape closes any open in-surface status menu — the keyboard
329         // twin of the click-away dismiss in cursor.rs, consumed the same way
330         // a builtin is (the release is eaten with the press via the consumer
331         // map), so the focused window never sees it. Gated on a menu
332         // actually being open and on NO modifiers: a chorded Escape stays a
333         // bindable/forwardable key, and with nothing expanded this arm never
334         // fires at all.
335         ffi::XKB_KEY_Escape if modifiers == 0 => {
336             let server = (*seat).server;
337             if !(*server).wm.any_expanded_status_segment(std::ptr::null_mut()) {
338                 return false;
339             }
340             log::debug!("Escape dismisses the open status menu");
341             if let Some(ref sender) = (*server).wm.status_sender {
342                 sender.send_menu_dismiss("-");
343             }
344             true
345         }
346         _ => false,
347     }
348 }
349 
350 unsafe extern "C" fn led_update(wlr_keyboard: *mut ffi::wlr_keyboard, leds: u32) {
351     let group = ffi::river_wlr_keyboard_get_data(wlr_keyboard) as *mut KeyboardGroup;
352     if group.is_null() {
353         return;
354     }
355 
356     let keyboards_head = &mut (*group).keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
357     let mut curr = (*keyboards_head).next;
358     while curr != keyboards_head {
359         let next = (*curr).next;
360         let keyboard = crate::container_of!(curr, crate::keyboard::Keyboard, group_link);
361         ffi::wlr_keyboard_led_update((*keyboard).wlr_keyboard, leds);
362         curr = next;
363     }
364 }
365 
366 unsafe extern "C" fn handle_group_key(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
367     let group = &mut *crate::container_of!(listener, KeyboardGroup, key_listener);
368     let event = data as *mut ffi::wlr_keyboard_key_event;
369 
370     let xkb_state = group.wlr_keyboard.xkb_state;
371     if xkb_state.is_null() {
372         log::error!("no xkb_state available");
373         return;
374     }
375 
376     // Keys are activity for the idle timeouts (and the idle-notify clients)
377     // exactly as pointer events are; before 2026-09-16 only tablet, touch
378     // and gestures counted, so idle-notify clients never saw a key.
379     if !group.seat.is_null() {
380         (*group.seat).handle_activity();
381     }
382 
383     // A real key press ends an emulated view drag (see `cursor::ViewDrag`):
384     // the drag holds Space and a pointer button down on the client's behalf,
385     // and a key pressed on top of that reaches the app as a chord nobody
386     // asked for. The drag's own synthetic Space goes straight to the seat and
387     // never passes through here.
388     if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
389         && !group.seat.is_null()
390         && (*group.seat).cursor.view_drag.is_some()
391     {
392         (*group.seat).cursor.end_view_drag("key");
393     }
394     if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
395         && !group.seat.is_null()
396         && (*group.seat).cursor.popup_wheel.is_some()
397     {
398         (*group.seat).cursor.end_popup_wheel("key");
399     }
400     if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
401         && !group.seat.is_null()
402         && (*group.seat).cursor.hscroll_shift.is_some()
403     {
404         (*group.seat).cursor.end_hscroll_shift("key");
405     }
406 
407     // Cancel active binding repeats
408     let seat_groups_head = &mut (*group.seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
409     let mut curr_g = (*seat_groups_head).next;
410     while curr_g != seat_groups_head {
411         let next_g = (*curr_g).next;
412         let g = crate::container_of!(curr_g, KeyboardGroup, link);
413         for press in (*g).pressed.values() {
414             if let KeyConsumer::Binding(binding) = press.consumer {
415                 if !binding.is_null() {
416                     (*binding).stop_repeat();
417                 }
418             }
419         }
420         curr_g = next_g;
421     }
422 
423     let consumer: KeyConsumer = if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED {
424         if let Some(kv) = group.pressed.remove(&(*event).keycode) {
425             assert!(kv.count == 0);
426             kv.consumer
427         } else {
428             KeyConsumer::Focus
429         }
430     } else {
431         let xkb_keycode = (*event).keycode + 8;
432         let modifiers = ffi::wlr_keyboard_get_modifiers(&mut group.wlr_keyboard);
433         // While the session is locked (or locking) a key is the lock
434         // screen's: see `allowed_while_locked` for the few that still act.
435         let locked = (*(*group.seat).server).lock_manager.state != crate::lock_manager::LockState::Unlocked;
436         
437         let mut matched_builtin = false;
438         let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
439         let num_syms = ffi::xkb_state_key_get_syms(xkb_state, xkb_keycode, &mut syms_ptr);
440         log::debug!("handle_group_key keycode={}, xkb_keycode={}, num_syms={}", (*event).keycode, xkb_keycode, num_syms);
441         if num_syms > 0 && !syms_ptr.is_null() {
442             let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
443             for &sym in syms {
444                 log::debug!("  keysym={:#x}", sym);
445                 if locked && !is_vt_switch(sym) {
446                     continue;
447                 }
448                 if handle_builtin_binding(group.seat, sym, modifiers) {
449                     matched_builtin = true;
450                     break;
451                 }
452             }
453         }
454 
455         if matched_builtin {
456             KeyConsumer::Builtin
457         } else if let Some(kb) = match_cce_keybind(&(*(*group.seat).server).wm, xkb_keycode, modifiers, xkb_state)
458             .filter(|kb| !locked || allowed_while_locked(kb.keysym))
459         {
460             log::debug!("matched CCE monolithic keybind: {:?}", kb);
461             KeyConsumer::CceBinding(kb)
462         } else if let Some((session, id)) = (!locked)
463             .then(|| match_portal_shortcut(&(*(*group.seat).server).wm, xkb_keycode, modifiers, xkb_state))
464             .flatten()
465         {
466             log::debug!("matched portal shortcut {} {}", session, id);
467             KeyConsumer::PortalShortcut { session, id }
468         } else if let Some(binding) = (!locked)
469             .then(|| (*group.seat).match_xkb_binding(xkb_keycode, &mut group.wlr_keyboard))
470             .flatten()
471         {
472             log::debug!("matched xkb binding");
473             (*group.seat).xkb_bindings_seat.ensure_next_key_eaten = false;
474             KeyConsumer::Binding(if (*binding).sent_pressed {
475                 std::ptr::null_mut()
476             } else {
477                 binding
478             })
479         } else if (*group.seat).xkb_bindings_seat.ensure_next_key_eaten {
480             let mut has_non_modifier = false;
481             let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
482             let num_syms = ffi::xkb_state_key_get_syms(xkb_state, xkb_keycode, &mut syms_ptr);
483             if num_syms > 0 && !syms_ptr.is_null() {
484                 let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
485                 for &sym in syms {
486                     if !crate::keyboard::keysym_is_modifier(sym) {
487                         has_non_modifier = true;
488                         break;
489                     }
490                 }
491             }
492             if has_non_modifier {
493                 (*group.seat).xkb_bindings_seat.ensure_next_key_eaten = false;
494                 KeyConsumer::EnsureEaten
495             } else {
496                 KeyConsumer::Focus
497             }
498         } else if !locked && !group.get_input_method_grab().is_null() {
499             // Never while locked: an input method would read the password.
500             KeyConsumer::ImGrab
501         } else {
502             KeyConsumer::Focus
503         }
504     };
505 
506     if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
507         group.pressed.insert(
508             (*event).keycode,
509             Press {
510                 consumer: consumer.clone(),
511                 count: 1,
512             },
513         );
514     }
515 
516     match consumer {
517         KeyConsumer::Builtin => {}
518         KeyConsumer::CceBinding(kb) => {
519             if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
520                 let wm = &mut (*(*group.seat).server).wm;
521                 // A key carries no pointer position: the overview toggle's
522                 // exit must land on the focused window, not on whatever the
523                 // pointer was left hovering (or the empty desktop under it).
524                 // Same substitution the control socket makes.
525                 let action = if kb.action == crate::config::Action::Overview {
526                     wm.overview_action_pointerless()
527                 } else {
528                     kb.action
529                 };
530                 log::info!("executing CCE monolithic action: {:?}", action);
531                 wm.execute_action(&action, kb.command.as_deref());
532             }
533         }
534         KeyConsumer::PortalShortcut { session, id } => {
535             // Both edges go out: the portal has a Deactivated signal, and
536             // the release comes back here through the consumer map with the
537             // same variant the press recorded.
538             let pressed = (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED;
539             if let Some(ref sender) = (*(*group.seat).server).wm.status_sender {
540                 sender.send_shortcut_event(&format!(
541                     "{} {} {} {}",
542                     if pressed { "activated" } else { "deactivated" },
543                     session,
544                     id,
545                     (*event).time_msec
546                 ));
547             }
548         }
549         KeyConsumer::Binding(binding) => {
550             if !binding.is_null() {
551                 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
552                     (*binding).pressed();
553                 } else {
554                     (*binding).released();
555                 }
556             }
557         }
558         KeyConsumer::EnsureEaten => {
559             if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
560                 (*group.seat).xkb_bindings_seat.scheduled_ate_unbound_key = true;
561                 (*(*group.seat).server).wm.dirty_windowing();
562             }
563         }
564         KeyConsumer::ImGrab => {
565             let grab = group.get_input_method_grab();
566             if !grab.is_null() {
567                 ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(grab, &mut group.wlr_keyboard);
568                 ffi::wlr_input_method_keyboard_grab_v2_send_key(grab, (*event).time_msec, (*event).keycode, (*event).state);
569             }
570         }
571         KeyConsumer::Focus => {
572             // Overlay AND Popup: desktop chrome (docks, the cce-cloud
573             // launcher) stays keyboard-interactive in overview — only world
574             // windows (spatial thumbnails) have their presses eaten.
575             let is_overlay_mode = (*group.seat).focus_is_chrome();
576 
577             if (*(*group.seat).server).wm.mode != crate::window_manager::WindowManagerMode::Overview
578                 || is_overlay_mode
579                 || (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED
580             {
581                 ffi::wlr_seat_set_keyboard((*group.seat).wlr_seat, &mut group.wlr_keyboard);
582                 ffi::wlr_seat_keyboard_notify_key((*group.seat).wlr_seat, (*event).time_msec, (*event).keycode, (*event).state);
583             }
584         }
585     }
586 
587     group.send_state();
588 }
589 
590 pub unsafe fn match_cce_keybind(
591     wm: &crate::window_manager::WindowManager,
592     keycode: u32,
593     modifiers: u32,
594     xkb_state: *mut ffi::xkb_state,
595 ) -> Option<crate::config::Keybind> {
596     match_chord(keycode, modifiers, xkb_state, |mods, sym| {
597         wm.keybinds.iter().find(|kb| kb.mods == mods && kb.keysym == sym).cloned()
598     })
599 }
600 
601 /// The portal-bound chords (`global_shortcuts`), matched exactly like the
602 /// config keybinds but consulted after them. Returns `(session, id)`.
603 pub unsafe fn match_portal_shortcut(
604     wm: &crate::window_manager::WindowManager,
605     keycode: u32,
606     modifiers: u32,
607     xkb_state: *mut ffi::xkb_state,
608 ) -> Option<(String, String)> {
609     if wm.portal_shortcuts.is_empty() {
610         return None;
611     }
612     match_chord(keycode, modifiers, xkb_state, |mods, sym| {
613         wm.portal_shortcuts
614             .iter()
615             .find(|s| s.mods == mods && s.keysym == sym)
616             .map(|s| (s.session.clone(), s.id.clone()))
617     })
618 }
619 
620 /// Chord lookup shared by every (mods, keysym) table. `probe` is asked
621 /// twice over: first with the keycode's level-0 keysyms against the raw
622 /// modifier mask (so `super+shift+h` matches on `h`, not `H`), then with the
623 /// keysyms of the level the modifiers actually select against the mask with
624 /// the consumed modifiers removed (so a bind on a shifted symbol like
625 /// `plus` still fires). The first hit wins.
626 unsafe fn match_chord<T>(
627     keycode: u32,
628     modifiers: u32,
629     xkb_state: *mut ffi::xkb_state,
630     probe: impl Fn(u32, u32) -> Option<T>,
631 ) -> Option<T> {
632     if xkb_state.is_null() {
633         return None;
634     }
635     let keymap = ffi::xkb_state_get_keymap(xkb_state);
636     if keymap.is_null() {
637         return None;
638     }
639     let layout = ffi::xkb_state_key_get_layout(xkb_state, keycode);
640 
641     let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
642     let num_syms = ffi::xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, 0, &mut syms_ptr);
643     if num_syms > 0 && !syms_ptr.is_null() {
644         let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
645         for &sym in syms {
646             if let Some(hit) = probe(modifiers, sym) {
647                 return Some(hit);
648             }
649         }
650     }
651 
652     let level = ffi::xkb_state_key_get_level(xkb_state, keycode, layout);
653     let mut syms_ptr_level: *const ffi::xkb_keysym_t = std::ptr::null();
654     let num_syms_level = ffi::xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, level, &mut syms_ptr_level);
655     if num_syms_level > 0 && !syms_ptr_level.is_null() {
656         let syms = std::slice::from_raw_parts(syms_ptr_level, num_syms_level as usize);
657         let consumed = ffi::xkb_state_key_get_consumed_mods2(xkb_state, keycode, ffi::xkb_consumed_mode_XKB_CONSUMED_MODE_XKB);
658         let modifiers_translated = modifiers & !consumed;
659         for &sym in syms {
660             if let Some(hit) = probe(modifiers_translated, sym) {
661                 return Some(hit);
662             }
663         }
664     }
665 
666     None
667 }
668 
669 unsafe extern "C" fn handle_group_modifiers(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
670     let group = &mut *crate::container_of!(listener, KeyboardGroup, modifiers_listener);
671 
672     let old = group.modifiers_old;
673     let new = ffi::wlr_keyboard_get_modifiers(&mut group.wlr_keyboard);
674     let watched = (*group.seat).xkb_bindings_seat.requested_mods_watched;
675     if (old & watched) != (new & watched) {
676         (*group.seat).xkb_bindings_seat.scheduled_mods_update = Some(crate::xkb_bindings::XkbBindingsSeatModsUpdate {
677             old,
678             new,
679         });
680         (*(*group.seat).server).wm.dirty_windowing();
681     }
682     group.modifiers_old = new;
683 
684     let grab = group.get_input_method_grab();
685     if !grab.is_null() {
686         ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(grab, &mut group.wlr_keyboard);
687         ffi::wlr_input_method_keyboard_grab_v2_send_modifiers(grab, &mut group.wlr_keyboard.modifiers);
688     } else {
689         ffi::wlr_seat_set_keyboard((*group.seat).wlr_seat, &mut group.wlr_keyboard);
690         ffi::wlr_seat_keyboard_notify_modifiers((*group.seat).wlr_seat, &mut group.wlr_keyboard.modifiers);
691     }
692     // Window-adjust mode (Super held) reads the seat keyboard's mask, which
693     // is this group's — the DEVICE keyboard has no keymap on the DRM
694     // backend (see keyboard::should_set_keymap), so its own modifiers
695     // signal never fires there; this one does for every real key.
696     (*(*group.seat).server).wm.refresh_adjust_held();
697 
698     group.send_state();
699 }
700 
701 #[cfg(test)]
702 mod tests {
703     use super::*;
704 
705     #[test]
706     fn a_locked_session_keeps_only_vt_switching_and_the_media_keys() {
707         for sym in [
708             ffi::XKB_KEY_XF86Switch_VT_1,
709             ffi::XKB_KEY_XF86Switch_VT_12,
710             ffi::XKB_KEY_XF86AudioRaiseVolume,
711             ffi::XKB_KEY_XF86AudioMute,
712             ffi::XKB_KEY_XF86MonBrightnessDown,
713         ] {
714             assert!(allowed_while_locked(sym), "{sym:#x} should act while locked");
715         }
716         // The keys a bind like super+q or super+d is made of, and media-range
717         // keys that launch things.
718         for sym in [
719             ffi::XKB_KEY_q,
720             ffi::XKB_KEY_d,
721             ffi::XKB_KEY_Return,
722             ffi::XKB_KEY_Escape,
723             ffi::XKB_KEY_XF86Calculator,
724             ffi::XKB_KEY_XF86WWW,
725             ffi::XKB_KEY_XF86PowerOff,
726         ] {
727             assert!(!allowed_while_locked(sym), "{sym:#x} must go to the lock screen");
728         }
729     }
730 }