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

src/server/keyboard_group.rs (26.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 unsafe fn handle_builtin_binding(seat: *mut Seat, keysym: u32, modifiers: u32) -> bool {
284     match keysym {
285         ffi::XKB_KEY_XF86Switch_VT_1..=ffi::XKB_KEY_XF86Switch_VT_12 => {
286             log::debug!("switch VT keysym received");
287             let server = (*seat).server;
288             let session = (*server).session;
289             if !session.is_null() {
290                 let vt = keysym - ffi::XKB_KEY_XF86Switch_VT_1 + 1;
291                 log::info!("switching to VT {}", vt);
292                 ffi::wlr_session_change_vt(session, vt);
293             }
294             true
295         }
296         // Plain Escape closes any open in-surface status menu — the keyboard
297         // twin of the click-away dismiss in cursor.rs, consumed the same way
298         // a builtin is (the release is eaten with the press via the consumer
299         // map), so the focused window never sees it. Gated on a menu
300         // actually being open and on NO modifiers: a chorded Escape stays a
301         // bindable/forwardable key, and with nothing expanded this arm never
302         // fires at all.
303         ffi::XKB_KEY_Escape if modifiers == 0 => {
304             let server = (*seat).server;
305             if !(*server).wm.any_expanded_status_segment(std::ptr::null_mut()) {
306                 return false;
307             }
308             log::debug!("Escape dismisses the open status menu");
309             if let Some(ref sender) = (*server).wm.status_sender {
310                 sender.send_menu_dismiss("-");
311             }
312             true
313         }
314         _ => false,
315     }
316 }
317 
318 unsafe extern "C" fn led_update(wlr_keyboard: *mut ffi::wlr_keyboard, leds: u32) {
319     let group = ffi::river_wlr_keyboard_get_data(wlr_keyboard) as *mut KeyboardGroup;
320     if group.is_null() {
321         return;
322     }
323 
324     let keyboards_head = &mut (*group).keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
325     let mut curr = (*keyboards_head).next;
326     while curr != keyboards_head {
327         let next = (*curr).next;
328         let keyboard = crate::container_of!(curr, crate::keyboard::Keyboard, group_link);
329         ffi::wlr_keyboard_led_update((*keyboard).wlr_keyboard, leds);
330         curr = next;
331     }
332 }
333 
334 unsafe extern "C" fn handle_group_key(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
335     let group = &mut *crate::container_of!(listener, KeyboardGroup, key_listener);
336     let event = data as *mut ffi::wlr_keyboard_key_event;
337 
338     let xkb_state = group.wlr_keyboard.xkb_state;
339     if xkb_state.is_null() {
340         log::error!("no xkb_state available");
341         return;
342     }
343 
344     // Keys are activity for the idle timeouts (and the idle-notify clients)
345     // exactly as pointer events are; before 2026-09-16 only tablet, touch
346     // and gestures counted, so idle-notify clients never saw a key.
347     if !group.seat.is_null() {
348         (*group.seat).handle_activity();
349     }
350 
351     // A real key press ends an emulated view drag (see `cursor::ViewDrag`):
352     // the drag holds Space and a pointer button down on the client's behalf,
353     // and a key pressed on top of that reaches the app as a chord nobody
354     // asked for. The drag's own synthetic Space goes straight to the seat and
355     // never passes through here.
356     if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
357         && !group.seat.is_null()
358         && (*group.seat).cursor.view_drag.is_some()
359     {
360         (*group.seat).cursor.end_view_drag("key");
361     }
362     if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
363         && !group.seat.is_null()
364         && (*group.seat).cursor.popup_wheel.is_some()
365     {
366         (*group.seat).cursor.end_popup_wheel("key");
367     }
368     if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
369         && !group.seat.is_null()
370         && (*group.seat).cursor.hscroll_shift.is_some()
371     {
372         (*group.seat).cursor.end_hscroll_shift("key");
373     }
374 
375     // Cancel active binding repeats
376     let seat_groups_head = &mut (*group.seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
377     let mut curr_g = (*seat_groups_head).next;
378     while curr_g != seat_groups_head {
379         let next_g = (*curr_g).next;
380         let g = crate::container_of!(curr_g, KeyboardGroup, link);
381         for press in (*g).pressed.values() {
382             if let KeyConsumer::Binding(binding) = press.consumer {
383                 if !binding.is_null() {
384                     (*binding).stop_repeat();
385                 }
386             }
387         }
388         curr_g = next_g;
389     }
390 
391     let consumer: KeyConsumer = if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED {
392         if let Some(kv) = group.pressed.remove(&(*event).keycode) {
393             assert!(kv.count == 0);
394             kv.consumer
395         } else {
396             KeyConsumer::Focus
397         }
398     } else {
399         let xkb_keycode = (*event).keycode + 8;
400         let modifiers = ffi::wlr_keyboard_get_modifiers(&mut group.wlr_keyboard);
401         
402         let mut matched_builtin = false;
403         let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
404         let num_syms = ffi::xkb_state_key_get_syms(xkb_state, xkb_keycode, &mut syms_ptr);
405         log::debug!("handle_group_key keycode={}, xkb_keycode={}, num_syms={}", (*event).keycode, xkb_keycode, num_syms);
406         if num_syms > 0 && !syms_ptr.is_null() {
407             let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
408             for &sym in syms {
409                 log::debug!("  keysym={:#x}", sym);
410                 if handle_builtin_binding(group.seat, sym, modifiers) {
411                     matched_builtin = true;
412                     break;
413                 }
414             }
415         }
416 
417         if matched_builtin {
418             KeyConsumer::Builtin
419         } else if let Some(kb) = match_cce_keybind(&(*(*group.seat).server).wm, xkb_keycode, modifiers, xkb_state) {
420             log::debug!("matched CCE monolithic keybind: {:?}", kb);
421             KeyConsumer::CceBinding(kb)
422         } else if let Some((session, id)) = match_portal_shortcut(&(*(*group.seat).server).wm, xkb_keycode, modifiers, xkb_state) {
423             log::debug!("matched portal shortcut {} {}", session, id);
424             KeyConsumer::PortalShortcut { session, id }
425         } else if let Some(binding) = (*group.seat).match_xkb_binding(xkb_keycode, &mut group.wlr_keyboard) {
426             log::debug!("matched xkb binding");
427             (*group.seat).xkb_bindings_seat.ensure_next_key_eaten = false;
428             KeyConsumer::Binding(if (*binding).sent_pressed {
429                 std::ptr::null_mut()
430             } else {
431                 binding
432             })
433         } else if (*group.seat).xkb_bindings_seat.ensure_next_key_eaten {
434             let mut has_non_modifier = false;
435             let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
436             let num_syms = ffi::xkb_state_key_get_syms(xkb_state, xkb_keycode, &mut syms_ptr);
437             if num_syms > 0 && !syms_ptr.is_null() {
438                 let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
439                 for &sym in syms {
440                     if !crate::keyboard::keysym_is_modifier(sym) {
441                         has_non_modifier = true;
442                         break;
443                     }
444                 }
445             }
446             if has_non_modifier {
447                 (*group.seat).xkb_bindings_seat.ensure_next_key_eaten = false;
448                 KeyConsumer::EnsureEaten
449             } else {
450                 KeyConsumer::Focus
451             }
452         } else if !group.get_input_method_grab().is_null() {
453             KeyConsumer::ImGrab
454         } else {
455             KeyConsumer::Focus
456         }
457     };
458 
459     if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
460         group.pressed.insert(
461             (*event).keycode,
462             Press {
463                 consumer: consumer.clone(),
464                 count: 1,
465             },
466         );
467     }
468 
469     match consumer {
470         KeyConsumer::Builtin => {}
471         KeyConsumer::CceBinding(kb) => {
472             if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
473                 let wm = &mut (*(*group.seat).server).wm;
474                 // A key carries no pointer position: the overview toggle's
475                 // exit must land on the focused window, not on whatever the
476                 // pointer was left hovering (or the empty desktop under it).
477                 // Same substitution the control socket makes.
478                 let action = if kb.action == crate::config::Action::Overview {
479                     wm.overview_action_pointerless()
480                 } else {
481                     kb.action
482                 };
483                 log::info!("executing CCE monolithic action: {:?}", action);
484                 wm.execute_action(&action, kb.command.as_deref());
485             }
486         }
487         KeyConsumer::PortalShortcut { session, id } => {
488             // Both edges go out: the portal has a Deactivated signal, and
489             // the release comes back here through the consumer map with the
490             // same variant the press recorded.
491             let pressed = (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED;
492             if let Some(ref sender) = (*(*group.seat).server).wm.status_sender {
493                 sender.send_shortcut_event(&format!(
494                     "{} {} {} {}",
495                     if pressed { "activated" } else { "deactivated" },
496                     session,
497                     id,
498                     (*event).time_msec
499                 ));
500             }
501         }
502         KeyConsumer::Binding(binding) => {
503             if !binding.is_null() {
504                 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
505                     (*binding).pressed();
506                 } else {
507                     (*binding).released();
508                 }
509             }
510         }
511         KeyConsumer::EnsureEaten => {
512             if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
513                 (*group.seat).xkb_bindings_seat.scheduled_ate_unbound_key = true;
514                 (*(*group.seat).server).wm.dirty_windowing();
515             }
516         }
517         KeyConsumer::ImGrab => {
518             let grab = group.get_input_method_grab();
519             if !grab.is_null() {
520                 ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(grab, &mut group.wlr_keyboard);
521                 ffi::wlr_input_method_keyboard_grab_v2_send_key(grab, (*event).time_msec, (*event).keycode, (*event).state);
522             }
523         }
524         KeyConsumer::Focus => {
525             // Overlay AND Popup: desktop chrome (docks, the cce-cloud
526             // launcher) stays keyboard-interactive in overview — only world
527             // windows (spatial thumbnails) have their presses eaten.
528             let is_overlay_mode = (*group.seat).focus_is_chrome();
529 
530             if (*(*group.seat).server).wm.mode != crate::window_manager::WindowManagerMode::Overview
531                 || is_overlay_mode
532                 || (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED
533             {
534                 ffi::wlr_seat_set_keyboard((*group.seat).wlr_seat, &mut group.wlr_keyboard);
535                 ffi::wlr_seat_keyboard_notify_key((*group.seat).wlr_seat, (*event).time_msec, (*event).keycode, (*event).state);
536             }
537         }
538     }
539 
540     group.send_state();
541 }
542 
543 pub unsafe fn match_cce_keybind(
544     wm: &crate::window_manager::WindowManager,
545     keycode: u32,
546     modifiers: u32,
547     xkb_state: *mut ffi::xkb_state,
548 ) -> Option<crate::config::Keybind> {
549     match_chord(keycode, modifiers, xkb_state, |mods, sym| {
550         wm.keybinds.iter().find(|kb| kb.mods == mods && kb.keysym == sym).cloned()
551     })
552 }
553 
554 /// The portal-bound chords (`global_shortcuts`), matched exactly like the
555 /// config keybinds but consulted after them. Returns `(session, id)`.
556 pub unsafe fn match_portal_shortcut(
557     wm: &crate::window_manager::WindowManager,
558     keycode: u32,
559     modifiers: u32,
560     xkb_state: *mut ffi::xkb_state,
561 ) -> Option<(String, String)> {
562     if wm.portal_shortcuts.is_empty() {
563         return None;
564     }
565     match_chord(keycode, modifiers, xkb_state, |mods, sym| {
566         wm.portal_shortcuts
567             .iter()
568             .find(|s| s.mods == mods && s.keysym == sym)
569             .map(|s| (s.session.clone(), s.id.clone()))
570     })
571 }
572 
573 /// Chord lookup shared by every (mods, keysym) table. `probe` is asked
574 /// twice over: first with the keycode's level-0 keysyms against the raw
575 /// modifier mask (so `super+shift+h` matches on `h`, not `H`), then with the
576 /// keysyms of the level the modifiers actually select against the mask with
577 /// the consumed modifiers removed (so a bind on a shifted symbol like
578 /// `plus` still fires). The first hit wins.
579 unsafe fn match_chord<T>(
580     keycode: u32,
581     modifiers: u32,
582     xkb_state: *mut ffi::xkb_state,
583     probe: impl Fn(u32, u32) -> Option<T>,
584 ) -> Option<T> {
585     if xkb_state.is_null() {
586         return None;
587     }
588     let keymap = ffi::xkb_state_get_keymap(xkb_state);
589     if keymap.is_null() {
590         return None;
591     }
592     let layout = ffi::xkb_state_key_get_layout(xkb_state, keycode);
593 
594     let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
595     let num_syms = ffi::xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, 0, &mut syms_ptr);
596     if num_syms > 0 && !syms_ptr.is_null() {
597         let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
598         for &sym in syms {
599             if let Some(hit) = probe(modifiers, sym) {
600                 return Some(hit);
601             }
602         }
603     }
604 
605     let level = ffi::xkb_state_key_get_level(xkb_state, keycode, layout);
606     let mut syms_ptr_level: *const ffi::xkb_keysym_t = std::ptr::null();
607     let num_syms_level = ffi::xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, level, &mut syms_ptr_level);
608     if num_syms_level > 0 && !syms_ptr_level.is_null() {
609         let syms = std::slice::from_raw_parts(syms_ptr_level, num_syms_level as usize);
610         let consumed = ffi::xkb_state_key_get_consumed_mods2(xkb_state, keycode, ffi::xkb_consumed_mode_XKB_CONSUMED_MODE_XKB);
611         let modifiers_translated = modifiers & !consumed;
612         for &sym in syms {
613             if let Some(hit) = probe(modifiers_translated, sym) {
614                 return Some(hit);
615             }
616         }
617     }
618 
619     None
620 }
621 
622 unsafe extern "C" fn handle_group_modifiers(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
623     let group = &mut *crate::container_of!(listener, KeyboardGroup, modifiers_listener);
624 
625     let old = group.modifiers_old;
626     let new = ffi::wlr_keyboard_get_modifiers(&mut group.wlr_keyboard);
627     let watched = (*group.seat).xkb_bindings_seat.requested_mods_watched;
628     if (old & watched) != (new & watched) {
629         (*group.seat).xkb_bindings_seat.scheduled_mods_update = Some(crate::xkb_bindings::XkbBindingsSeatModsUpdate {
630             old,
631             new,
632         });
633         (*(*group.seat).server).wm.dirty_windowing();
634     }
635     group.modifiers_old = new;
636 
637     let grab = group.get_input_method_grab();
638     if !grab.is_null() {
639         ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(grab, &mut group.wlr_keyboard);
640         ffi::wlr_input_method_keyboard_grab_v2_send_modifiers(grab, &mut group.wlr_keyboard.modifiers);
641     } else {
642         ffi::wlr_seat_set_keyboard((*group.seat).wlr_seat, &mut group.wlr_keyboard);
643         ffi::wlr_seat_keyboard_notify_modifiers((*group.seat).wlr_seat, &mut group.wlr_keyboard.modifiers);
644     }
645     // Window-adjust mode (Super held) reads the seat keyboard's mask, which
646     // is this group's — the DEVICE keyboard has no keymap on the DRM
647     // backend (see keyboard::should_set_keymap), so its own modifiers
648     // signal never fires there; this one does for every real key.
649     (*(*group.seat).server).wm.refresh_adjust_held();
650 
651     group.send_state();
652 }