1#[cfg(any(feature = "wayland", feature = "x11"))]
2use collections::{HashMap, HashSet};
3#[cfg(any(feature = "wayland", feature = "x11"))]
4use strum::{EnumIter, IntoEnumIterator as _};
5#[cfg(any(feature = "wayland", feature = "x11"))]
6use xkbcommon::xkb::{Keycode, Keysym};
7
8use crate::{PlatformKeyboardLayout, SharedString};
9
10#[derive(Clone)]
11pub(crate) struct LinuxKeyboardLayout {
12 name: SharedString,
13}
14
15impl PlatformKeyboardLayout for LinuxKeyboardLayout {
16 fn id(&self) -> &str {
17 &self.name
18 }
19
20 fn name(&self) -> &str {
21 &self.name
22 }
23}
24
25impl LinuxKeyboardLayout {
26 pub(crate) fn new(name: SharedString) -> Self {
27 Self { name }
28 }
29}
30
31#[cfg(any(feature = "wayland", feature = "x11"))]
32pub(crate) struct LinuxKeyboardMapper {
33 letters: HashMap<Keycode, String>,
34 code_to_key: HashMap<Keycode, String>,
35 code_to_shifted_key: HashMap<Keycode, String>,
36}
37
38#[cfg(any(feature = "wayland", feature = "x11"))]
39impl LinuxKeyboardMapper {
40 pub(crate) fn new(xkb_state: &xkbcommon::xkb::State) -> Self {
41 let mut letters = HashMap::default();
42 let mut code_to_key = HashMap::default();
43 let mut code_to_shifted_key = HashMap::default();
44 let mut inserted_letters = HashSet::default();
45
46 let keymap = xkb_state.get_keymap();
47 let mut shifted_state = xkbcommon::xkb::State::new(&keymap);
48 let shift_mod = keymap.mod_get_index(xkbcommon::xkb::MOD_NAME_SHIFT);
49 let shift_mask = 1 << shift_mod;
50 shifted_state.update_mask(shift_mask, 0, 0, 0, 0, 0);
51
52 for scan_code in LinuxScanCodes::iter() {
53 let keycode = Keycode::new(scan_code as u32);
54
55 let key = xkb_state.key_get_utf8(keycode);
56 if !key.is_empty() {
57 if key_is_a_letter(&key) {
58 letters.insert(keycode, key.clone());
59 } else {
60 code_to_key.insert(keycode, key.clone());
61 }
62 inserted_letters.insert(key);
63 } else {
64 // keycode might be a dead key
65 let keysym = xkb_state.key_get_one_sym(keycode);
66 if let Some(key) = underlying_dead_key(keysym) {
67 code_to_key.insert(keycode, key.clone());
68 inserted_letters.insert(key);
69 }
70 }
71
72 let shifted_key = shifted_state.key_get_utf8(keycode);
73 if !shifted_key.is_empty() {
74 code_to_shifted_key.insert(keycode, shifted_key);
75 } else {
76 // keycode might be a dead key
77 let shifted_keysym = shifted_state.key_get_one_sym(keycode);
78 if let Some(shifted_key) = underlying_dead_key(shifted_keysym) {
79 code_to_shifted_key.insert(keycode, shifted_key);
80 }
81 }
82 }
83 insert_letters_if_missing(&inserted_letters, &mut letters);
84
85 Self {
86 letters,
87 code_to_key,
88 code_to_shifted_key,
89 }
90 }
91
92 pub(crate) fn get_key(
93 &self,
94 keycode: Keycode,
95 modifiers: &mut crate::Modifiers,
96 ) -> Option<String> {
97 if let Some(key) = self.letters.get(&keycode) {
98 return Some(key.clone());
99 }
100 if modifiers.shift {
101 modifiers.shift = false;
102 self.code_to_shifted_key.get(&keycode).cloned()
103 } else {
104 self.code_to_key.get(&keycode).cloned()
105 }
106 }
107}
108
109#[cfg(any(feature = "wayland", feature = "x11"))]
110fn key_is_a_letter(key: &str) -> bool {
111 matches!(
112 key,
113 "a" | "b"
114 | "c"
115 | "d"
116 | "e"
117 | "f"
118 | "g"
119 | "h"
120 | "i"
121 | "j"
122 | "k"
123 | "l"
124 | "m"
125 | "n"
126 | "o"
127 | "p"
128 | "q"
129 | "r"
130 | "s"
131 | "t"
132 | "u"
133 | "v"
134 | "w"
135 | "x"
136 | "y"
137 | "z"
138 )
139}
140
141#[cfg(any(feature = "wayland", feature = "x11"))]
142fn is_alphabetic_key(keycode: Keycode) -> bool {
143 matches!(
144 keycode.raw(),
145 0x0026 // a
146 | 0x0038 // b
147 | 0x0036 // c
148 | 0x0028 // d
149 | 0x001a // e
150 | 0x0029 // f
151 | 0x002a // g
152 | 0x002b // h
153 | 0x001f // i
154 | 0x002c // j
155 | 0x002d // k
156 | 0x002e // l
157 | 0x003a // m
158 | 0x0039 // n
159 | 0x0020 // o
160 | 0x0021 // p
161 | 0x0018 // q
162 | 0x001b // r
163 | 0x0027 // s
164 | 0x001c // t
165 | 0x001e // u
166 | 0x0037 // v
167 | 0x0019 // w
168 | 0x0035 // x
169 | 0x001d // y
170 | 0x0034 // z
171 )
172}
173
174/**
175 * Returns which symbol the dead key represents
176 * <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
177 */
178#[cfg(any(feature = "wayland", feature = "x11"))]
179pub(crate) fn underlying_dead_key(keysym: Keysym) -> Option<String> {
180 match keysym {
181 Keysym::dead_grave => Some("`".to_owned()),
182 Keysym::dead_acute => Some("´".to_owned()),
183 Keysym::dead_circumflex => Some("^".to_owned()),
184 Keysym::dead_tilde => Some("~".to_owned()),
185 Keysym::dead_macron => Some("¯".to_owned()),
186 Keysym::dead_breve => Some("˘".to_owned()),
187 Keysym::dead_abovedot => Some("˙".to_owned()),
188 Keysym::dead_diaeresis => Some("¨".to_owned()),
189 Keysym::dead_abovering => Some("˚".to_owned()),
190 Keysym::dead_doubleacute => Some("˝".to_owned()),
191 Keysym::dead_caron => Some("ˇ".to_owned()),
192 Keysym::dead_cedilla => Some("¸".to_owned()),
193 Keysym::dead_ogonek => Some("˛".to_owned()),
194 Keysym::dead_iota => Some("ͅ".to_owned()),
195 Keysym::dead_voiced_sound => Some("゙".to_owned()),
196 Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
197 Keysym::dead_belowdot => Some("̣̣".to_owned()),
198 Keysym::dead_hook => Some("̡".to_owned()),
199 Keysym::dead_horn => Some("̛".to_owned()),
200 Keysym::dead_stroke => Some("̶̶".to_owned()),
201 Keysym::dead_abovecomma => Some("̓̓".to_owned()),
202 Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
203 Keysym::dead_doublegrave => Some("̏".to_owned()),
204 Keysym::dead_belowring => Some("˳".to_owned()),
205 Keysym::dead_belowmacron => Some("̱".to_owned()),
206 Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
207 Keysym::dead_belowtilde => Some("̰".to_owned()),
208 Keysym::dead_belowbreve => Some("̮".to_owned()),
209 Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
210 Keysym::dead_invertedbreve => Some("̯".to_owned()),
211 Keysym::dead_belowcomma => Some("̦".to_owned()),
212 Keysym::dead_currency => None,
213 Keysym::dead_lowline => None,
214 Keysym::dead_aboveverticalline => None,
215 Keysym::dead_belowverticalline => None,
216 Keysym::dead_longsolidusoverlay => None,
217 Keysym::dead_a => None,
218 Keysym::dead_A => None,
219 Keysym::dead_e => None,
220 Keysym::dead_E => None,
221 Keysym::dead_i => None,
222 Keysym::dead_I => None,
223 Keysym::dead_o => None,
224 Keysym::dead_O => None,
225 Keysym::dead_u => None,
226 Keysym::dead_U => None,
227 Keysym::dead_small_schwa => Some("ə".to_owned()),
228 Keysym::dead_capital_schwa => Some("Ə".to_owned()),
229 Keysym::dead_greek => None,
230 _ => None,
231 }
232}
233
234#[cfg(any(feature = "wayland", feature = "x11"))]
235fn insert_letters_if_missing(inserted: &HashSet<String>, letters: &mut HashMap<Keycode, String>) {
236 for scan_code in LinuxScanCodes::LETTERS.iter() {
237 let keycode = Keycode::new(*scan_code as u32);
238 let key = scan_code.to_str();
239 if !inserted.contains(key) {
240 letters.insert(keycode, key.to_owned());
241 }
242 }
243}
244
245#[cfg(any(feature = "wayland", feature = "x11"))]
246#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
247enum LinuxScanCodes {
248 A = 0x0026,
249 B = 0x0038,
250 C = 0x0036,
251 D = 0x0028,
252 E = 0x001a,
253 F = 0x0029,
254 G = 0x002a,
255 H = 0x002b,
256 I = 0x001f,
257 J = 0x002c,
258 K = 0x002d,
259 L = 0x002e,
260 M = 0x003a,
261 N = 0x0039,
262 O = 0x0020,
263 P = 0x0021,
264 Q = 0x0018,
265 R = 0x001b,
266 S = 0x0027,
267 T = 0x001c,
268 U = 0x001e,
269 V = 0x0037,
270 W = 0x0019,
271 X = 0x0035,
272 Y = 0x001d,
273 Z = 0x0034,
274 Digit0 = 0x0013,
275 Digit1 = 0x000a,
276 Digit2 = 0x000b,
277 Digit3 = 0x000c,
278 Digit4 = 0x000d,
279 Digit5 = 0x000e,
280 Digit6 = 0x000f,
281 Digit7 = 0x0010,
282 Digit8 = 0x0011,
283 Digit9 = 0x0012,
284 Backquote = 0x0031,
285 Minus = 0x0014,
286 Equal = 0x0015,
287 LeftBracket = 0x0022,
288 RightBracket = 0x0023,
289 Backslash = 0x0033,
290 Semicolon = 0x002f,
291 Quote = 0x0030,
292 Comma = 0x003b,
293 Period = 0x003c,
294 Slash = 0x003d,
295 // This key is typically located near LeftShift key, varies on international keyboards: Dan: <> Dutch: ][ Ger: <> UK: \|
296 IntlBackslash = 0x005e,
297 // Used for Brazilian /? and Japanese _ 'ro'.
298 IntlRo = 0x0061,
299}
300
301#[cfg(any(feature = "wayland", feature = "x11"))]
302impl LinuxScanCodes {
303 const LETTERS: &'static [LinuxScanCodes] = &[
304 LinuxScanCodes::A,
305 LinuxScanCodes::B,
306 LinuxScanCodes::C,
307 LinuxScanCodes::D,
308 LinuxScanCodes::E,
309 LinuxScanCodes::F,
310 LinuxScanCodes::G,
311 LinuxScanCodes::H,
312 LinuxScanCodes::I,
313 LinuxScanCodes::J,
314 LinuxScanCodes::K,
315 LinuxScanCodes::L,
316 LinuxScanCodes::M,
317 LinuxScanCodes::N,
318 LinuxScanCodes::O,
319 LinuxScanCodes::P,
320 LinuxScanCodes::Q,
321 LinuxScanCodes::R,
322 LinuxScanCodes::S,
323 LinuxScanCodes::T,
324 LinuxScanCodes::U,
325 LinuxScanCodes::V,
326 LinuxScanCodes::W,
327 LinuxScanCodes::X,
328 LinuxScanCodes::Y,
329 LinuxScanCodes::Z,
330 ];
331
332 fn to_str(&self) -> &str {
333 match self {
334 LinuxScanCodes::A => "a",
335 LinuxScanCodes::B => "b",
336 LinuxScanCodes::C => "c",
337 LinuxScanCodes::D => "d",
338 LinuxScanCodes::E => "e",
339 LinuxScanCodes::F => "f",
340 LinuxScanCodes::G => "g",
341 LinuxScanCodes::H => "h",
342 LinuxScanCodes::I => "i",
343 LinuxScanCodes::J => "j",
344 LinuxScanCodes::K => "k",
345 LinuxScanCodes::L => "l",
346 LinuxScanCodes::M => "m",
347 LinuxScanCodes::N => "n",
348 LinuxScanCodes::O => "o",
349 LinuxScanCodes::P => "p",
350 LinuxScanCodes::Q => "q",
351 LinuxScanCodes::R => "r",
352 LinuxScanCodes::S => "s",
353 LinuxScanCodes::T => "t",
354 LinuxScanCodes::U => "u",
355 LinuxScanCodes::V => "v",
356 LinuxScanCodes::W => "w",
357 LinuxScanCodes::X => "x",
358 LinuxScanCodes::Y => "y",
359 LinuxScanCodes::Z => "z",
360 LinuxScanCodes::Digit0 => "0",
361 LinuxScanCodes::Digit1 => "1",
362 LinuxScanCodes::Digit2 => "2",
363 LinuxScanCodes::Digit3 => "3",
364 LinuxScanCodes::Digit4 => "4",
365 LinuxScanCodes::Digit5 => "5",
366 LinuxScanCodes::Digit6 => "6",
367 LinuxScanCodes::Digit7 => "7",
368 LinuxScanCodes::Digit8 => "8",
369 LinuxScanCodes::Digit9 => "9",
370 LinuxScanCodes::Backquote => "`",
371 LinuxScanCodes::Minus => "-",
372 LinuxScanCodes::Equal => "=",
373 LinuxScanCodes::LeftBracket => "[",
374 LinuxScanCodes::RightBracket => "]",
375 LinuxScanCodes::Backslash => "\\",
376 LinuxScanCodes::Semicolon => ";",
377 LinuxScanCodes::Quote => "'",
378 LinuxScanCodes::Comma => ",",
379 LinuxScanCodes::Period => ".",
380 LinuxScanCodes::Slash => "/",
381 LinuxScanCodes::IntlBackslash => "unknown",
382 LinuxScanCodes::IntlRo => "unknown",
383 }
384 }
385
386 #[cfg(test)]
387 fn to_shifted(&self) -> &str {
388 match self {
389 LinuxScanCodes::A => "a",
390 LinuxScanCodes::B => "b",
391 LinuxScanCodes::C => "c",
392 LinuxScanCodes::D => "d",
393 LinuxScanCodes::E => "e",
394 LinuxScanCodes::F => "f",
395 LinuxScanCodes::G => "g",
396 LinuxScanCodes::H => "h",
397 LinuxScanCodes::I => "i",
398 LinuxScanCodes::J => "j",
399 LinuxScanCodes::K => "k",
400 LinuxScanCodes::L => "l",
401 LinuxScanCodes::M => "m",
402 LinuxScanCodes::N => "n",
403 LinuxScanCodes::O => "o",
404 LinuxScanCodes::P => "p",
405 LinuxScanCodes::Q => "q",
406 LinuxScanCodes::R => "r",
407 LinuxScanCodes::S => "s",
408 LinuxScanCodes::T => "t",
409 LinuxScanCodes::U => "u",
410 LinuxScanCodes::V => "v",
411 LinuxScanCodes::W => "w",
412 LinuxScanCodes::X => "x",
413 LinuxScanCodes::Y => "y",
414 LinuxScanCodes::Z => "z",
415 LinuxScanCodes::Digit0 => ")",
416 LinuxScanCodes::Digit1 => "!",
417 LinuxScanCodes::Digit2 => "@",
418 LinuxScanCodes::Digit3 => "#",
419 LinuxScanCodes::Digit4 => "$",
420 LinuxScanCodes::Digit5 => "%",
421 LinuxScanCodes::Digit6 => "^",
422 LinuxScanCodes::Digit7 => "&",
423 LinuxScanCodes::Digit8 => "*",
424 LinuxScanCodes::Digit9 => "(",
425 LinuxScanCodes::Backquote => "~",
426 LinuxScanCodes::Minus => "_",
427 LinuxScanCodes::Equal => "+",
428 LinuxScanCodes::LeftBracket => "{",
429 LinuxScanCodes::RightBracket => "}",
430 LinuxScanCodes::Backslash => "|",
431 LinuxScanCodes::Semicolon => ":",
432 LinuxScanCodes::Quote => "\"",
433 LinuxScanCodes::Comma => "<",
434 LinuxScanCodes::Period => ">",
435 LinuxScanCodes::Slash => "?",
436 LinuxScanCodes::IntlBackslash => "unknown",
437 LinuxScanCodes::IntlRo => "unknown",
438 }
439 }
440}
441
442#[cfg(all(test, any(feature = "wayland", feature = "x11")))]
443mod tests {
444 use std::sync::LazyLock;
445
446 use strum::IntoEnumIterator;
447 use x11rb::{protocol::xkb::ConnectionExt as _, xcb_ffi::XCBConnection};
448 use xkbcommon::xkb::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
449
450 use crate::platform::linux::keyboard::LinuxScanCodes;
451
452 use super::LinuxKeyboardMapper;
453
454 static XCB_CONNECTION: LazyLock<XCBConnection> =
455 LazyLock::new(|| XCBConnection::connect(None).unwrap().0);
456
457 fn create_test_mapper() -> LinuxKeyboardMapper {
458 let _ = XCB_CONNECTION
459 .xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
460 .unwrap()
461 .reply()
462 .unwrap();
463 let xkb_context = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
464 let xkb_device_id = xkbcommon::xkb::x11::get_core_keyboard_device_id(&*XCB_CONNECTION);
465 let xkb_state = {
466 let xkb_keymap = xkbcommon::xkb::x11::keymap_new_from_device(
467 &xkb_context,
468 &*XCB_CONNECTION,
469 xkb_device_id,
470 xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
471 );
472 xkbcommon::xkb::x11::state_new_from_device(&xkb_keymap, &*XCB_CONNECTION, xkb_device_id)
473 };
474 LinuxKeyboardMapper::new(&xkb_state)
475 }
476
477 #[test]
478 fn test_us_layout_mapper() {
479 let mapper = create_test_mapper();
480 for scan_code in super::LinuxScanCodes::iter() {
481 if scan_code == LinuxScanCodes::IntlBackslash || scan_code == LinuxScanCodes::IntlRo {
482 continue;
483 }
484 let keycode = xkbcommon::xkb::Keycode::new(scan_code as u32);
485 let key = mapper
486 .get_key(keycode, &mut crate::Modifiers::default())
487 .unwrap();
488 assert_eq!(key.as_str(), scan_code.to_str());
489
490 let shifted_key = mapper
491 .get_key(
492 keycode,
493 &mut crate::Modifiers {
494 shift: true,
495 ..Default::default()
496 },
497 )
498 .unwrap();
499 assert_eq!(shifted_key.as_str(), scan_code.to_shifted());
500 }
501 }
502}