keyboard.rs

  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}