keyboard.rs

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