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 }