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