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