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, 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}