From 8b421a4d4bf04d9a5170c4462b063198ea3ddb26 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Wed, 1 Oct 2025 16:09:34 +0200 Subject: [PATCH] Use the user's keyboard layout from the x11 system This change uses xkbcommon to decode the key utf8 value based on the user's current layout. The implementation is adapted from the x11rb's xkbcommon-example. --- Cargo.toml | 1 + src/x11/event_loop.rs | 21 ++++++- src/x11/keyboard.rs | 131 ++++++++++++++++++++++++------------------ 3 files changed, 96 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d65e460..f88215a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ raw-window-handle = "0.5" [target.'cfg(target_os="linux")'.dependencies] x11rb = { version = "0.13.0", features = ["cursor", "resource_manager", "allow-unsafe-code"] } +xkbcommon = { version = "0.7", features = ["x11"]} x11 = { version = "2.21", features = ["xlib", "xlib_xcb"] } nix = "0.22.0" diff --git a/src/x11/event_loop.rs b/src/x11/event_loop.rs index 6b6ecd30..be201586 100644 --- a/src/x11/event_loop.rs +++ b/src/x11/event_loop.rs @@ -9,6 +9,7 @@ use std::os::fd::AsRawFd; use std::time::{Duration, Instant}; use x11rb::connection::Connection; use x11rb::protocol::Event as XEvent; +use xkbcommon::xkb as xkbc; pub(super) struct EventLoop { handler: Box, @@ -18,6 +19,7 @@ pub(super) struct EventLoop { new_physical_size: Option, frame_interval: Duration, event_loop_running: bool, + xkb_state: xkbc::State, } impl EventLoop { @@ -25,6 +27,20 @@ impl EventLoop { window: WindowInner, handler: impl WindowHandler + 'static, parent_handle: Option, ) -> Self { + // Setup the xkb state + let xkb_state = { + let context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS); + let conn = &window.xcb_connection.conn; + let device_id = xkbc::x11::get_core_keyboard_device_id(conn); + assert!(device_id >= 0); + let keymap = xkbc::x11::keymap_new_from_device( + &context, + conn, + device_id, + xkbc::KEYMAP_COMPILE_NO_FLAGS, + ); + xkbc::x11::state_new_from_device(&keymap, &conn, device_id) + }; Self { window, handler: Box::new(handler), @@ -32,6 +48,7 @@ impl EventLoop { frame_interval: Duration::from_millis(15), event_loop_running: false, new_physical_size: None, + xkb_state, } } @@ -261,14 +278,14 @@ impl EventLoop { XEvent::KeyPress(event) => { self.handler.on_event( &mut crate::Window::new(Window { inner: &self.window }), - Event::Keyboard(convert_key_press_event(&event)), + Event::Keyboard(convert_key_press_event(&event, &mut self.xkb_state)), ); } XEvent::KeyRelease(event) => { self.handler.on_event( &mut crate::Window::new(Window { inner: &self.window }), - Event::Keyboard(convert_key_release_event(&event)), + Event::Keyboard(convert_key_release_event(&event, &mut self.xkb_state)), ); } diff --git a/src/x11/keyboard.rs b/src/x11/keyboard.rs index 4985e641..ec1b6e3e 100644 --- a/src/x11/keyboard.rs +++ b/src/x11/keyboard.rs @@ -19,16 +19,14 @@ //! X11 keyboard handling use x11rb::protocol::xproto::{KeyButMask, KeyPressEvent, KeyReleaseEvent}; +use xkbcommon::xkb as xkbc; use keyboard_types::*; use crate::keyboard::code_to_location; /// Convert a hardware scan code to a key. -/// -/// Note: this is a hardcoded layout. We need to detect the user's -/// layout from the system and apply it. -fn code_to_key(code: Code, m: Modifiers) -> Key { +fn code_to_key(code: Code, m: Modifiers, hw_code: xkbc::Keycode, xkb_state: &xkbc::State) -> Key { fn a(s: &str) -> Key { Key::Character(s.into()) } @@ -39,6 +37,15 @@ fn code_to_key(code: Code, m: Modifiers) -> Key { Key::Character(base.into()) } } + fn k(mods: Modifiers, base: &str, code: xkbc::Keycode, state: &xkbc::State) -> Key { + if mods.contains(Modifiers::CONTROL) { + // When ctrl is set, then state.key_get_utf8 return control sequence like \x1e. + // TODO: handle this better? + Key::Character(base.into()) + } else { + Key::Character(state.key_get_utf8(code)) + } + } fn n(mods: Modifiers, base: Key, num: &str) -> Key { if mods.contains(Modifiers::NUM_LOCK) != mods.contains(Modifiers::SHIFT) { Key::Character(num.into()) @@ -47,55 +54,55 @@ fn code_to_key(code: Code, m: Modifiers) -> Key { } } match code { - Code::KeyA => s(m, "a", "A"), - Code::KeyB => s(m, "b", "B"), - Code::KeyC => s(m, "c", "C"), - Code::KeyD => s(m, "d", "D"), - Code::KeyE => s(m, "e", "E"), - Code::KeyF => s(m, "f", "F"), - Code::KeyG => s(m, "g", "G"), - Code::KeyH => s(m, "h", "H"), - Code::KeyI => s(m, "i", "I"), - Code::KeyJ => s(m, "j", "J"), - Code::KeyK => s(m, "k", "K"), - Code::KeyL => s(m, "l", "L"), - Code::KeyM => s(m, "m", "M"), - Code::KeyN => s(m, "n", "N"), - Code::KeyO => s(m, "o", "O"), - Code::KeyP => s(m, "p", "P"), - Code::KeyQ => s(m, "q", "Q"), - Code::KeyR => s(m, "r", "R"), - Code::KeyS => s(m, "s", "S"), - Code::KeyT => s(m, "t", "T"), - Code::KeyU => s(m, "u", "U"), - Code::KeyV => s(m, "v", "V"), - Code::KeyW => s(m, "w", "W"), - Code::KeyX => s(m, "x", "X"), - Code::KeyY => s(m, "y", "Y"), - Code::KeyZ => s(m, "z", "Z"), + Code::KeyA => k(m, "a", hw_code, xkb_state), + Code::KeyB => k(m, "b", hw_code, xkb_state), + Code::KeyC => k(m, "c", hw_code, xkb_state), + Code::KeyD => k(m, "d", hw_code, xkb_state), + Code::KeyE => k(m, "e", hw_code, xkb_state), + Code::KeyF => k(m, "f", hw_code, xkb_state), + Code::KeyG => k(m, "g", hw_code, xkb_state), + Code::KeyH => k(m, "h", hw_code, xkb_state), + Code::KeyI => k(m, "i", hw_code, xkb_state), + Code::KeyJ => k(m, "j", hw_code, xkb_state), + Code::KeyK => k(m, "k", hw_code, xkb_state), + Code::KeyL => k(m, "l", hw_code, xkb_state), + Code::KeyM => k(m, "m", hw_code, xkb_state), + Code::KeyN => k(m, "n", hw_code, xkb_state), + Code::KeyO => k(m, "o", hw_code, xkb_state), + Code::KeyP => k(m, "p", hw_code, xkb_state), + Code::KeyQ => k(m, "q", hw_code, xkb_state), + Code::KeyR => k(m, "r", hw_code, xkb_state), + Code::KeyS => k(m, "s", hw_code, xkb_state), + Code::KeyT => k(m, "t", hw_code, xkb_state), + Code::KeyU => k(m, "u", hw_code, xkb_state), + Code::KeyV => k(m, "v", hw_code, xkb_state), + Code::KeyW => k(m, "w", hw_code, xkb_state), + Code::KeyX => k(m, "x", hw_code, xkb_state), + Code::KeyY => k(m, "y", hw_code, xkb_state), + Code::KeyZ => k(m, "z", hw_code, xkb_state), - Code::Digit0 => s(m, "0", ")"), - Code::Digit1 => s(m, "1", "!"), - Code::Digit2 => s(m, "2", "@"), - Code::Digit3 => s(m, "3", "#"), - Code::Digit4 => s(m, "4", "$"), - Code::Digit5 => s(m, "5", "%"), - Code::Digit6 => s(m, "6", "^"), - Code::Digit7 => s(m, "7", "&"), - Code::Digit8 => s(m, "8", "*"), - Code::Digit9 => s(m, "9", "("), + Code::Digit0 => k(m, "0", hw_code, xkb_state), + Code::Digit1 => k(m, "1", hw_code, xkb_state), + Code::Digit2 => k(m, "2", hw_code, xkb_state), + Code::Digit3 => k(m, "3", hw_code, xkb_state), + Code::Digit4 => k(m, "4", hw_code, xkb_state), + Code::Digit5 => k(m, "5", hw_code, xkb_state), + Code::Digit6 => k(m, "6", hw_code, xkb_state), + Code::Digit7 => k(m, "7", hw_code, xkb_state), + Code::Digit8 => k(m, "8", hw_code, xkb_state), + Code::Digit9 => k(m, "9", hw_code, xkb_state), - Code::Backquote => s(m, "`", "~"), - Code::Minus => s(m, "-", "_"), - Code::Equal => s(m, "=", "+"), - Code::BracketLeft => s(m, "[", "{"), - Code::BracketRight => s(m, "]", "}"), - Code::Backslash => s(m, "\\", "|"), - Code::Semicolon => s(m, ";", ":"), - Code::Quote => s(m, "'", "\""), - Code::Comma => s(m, ",", "<"), - Code::Period => s(m, ".", ">"), - Code::Slash => s(m, "/", "?"), + Code::Backquote => k(m, "`", hw_code, xkb_state), + Code::Minus => k(m, "-", hw_code, xkb_state), + Code::Equal => k(m, "=", hw_code, xkb_state), + Code::BracketLeft => k(m, "[", hw_code, xkb_state), + Code::BracketRight => k(m, "]", hw_code, xkb_state), + Code::Backslash => k(m, "\\", hw_code, xkb_state), + Code::Semicolon => k(m, ";", hw_code, xkb_state), + Code::Quote => k(m, "'", hw_code, xkb_state), + Code::Comma => k(m, ",", hw_code, xkb_state), + Code::Period => k(m, ".", hw_code, xkb_state), + Code::Slash => k(m, "/", hw_code, xkb_state), Code::Space => a(" "), @@ -383,22 +390,36 @@ pub(super) fn key_mods(mods: KeyButMask) -> Modifiers { ret } -pub(super) fn convert_key_press_event(key_press: &KeyPressEvent) -> KeyboardEvent { +pub(super) fn convert_key_press_event( + key_press: &KeyPressEvent, state: &mut xkbc::State, +) -> KeyboardEvent { let hw_keycode = key_press.detail; + + // Update the xkbc state + let hw_code = hw_keycode.into(); + state.update_key(hw_code, xkbc::KeyDirection::Down); + let code = hardware_keycode_to_code(hw_keycode.into()); let modifiers = key_mods(key_press.state); - let key = code_to_key(code, modifiers); + let key = code_to_key(code, modifiers, hw_code, state); let location = code_to_location(code); let state = KeyState::Down; KeyboardEvent { code, key, modifiers, location, state, repeat: false, is_composing: false } } -pub(super) fn convert_key_release_event(key_release: &KeyReleaseEvent) -> KeyboardEvent { +pub(super) fn convert_key_release_event( + key_release: &KeyReleaseEvent, state: &mut xkbc::State, +) -> KeyboardEvent { let hw_keycode = key_release.detail; + + // Update the xkbc state + let hw_code = hw_keycode.into(); + state.update_key(hw_code, xkbc::KeyDirection::Up); + let code = hardware_keycode_to_code(hw_keycode.into()); let modifiers = key_mods(key_release.state); - let key = code_to_key(code, modifiers); + let key = code_to_key(code, modifiers, hw_code, state); let location = code_to_location(code); let state = KeyState::Up;