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