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