keyboard.rs

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