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