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