Skip to main content

drmkit_input/
keyboard.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Turning evdev keycodes into keysyms and text, through xkb.
5
6use std::path::Path;
7
8use xkbcommon::xkb;
9
10use crate::InputError;
11use crate::event::KeyEvent;
12use crate::leds::Leds;
13
14/// xkb numbers keycodes eight higher than evdev does, for X11's sake.
15const XKB_OFFSET: u32 = 8;
16
17/// The lock keys, in evdev codes.
18///
19/// Layout-independent: RMLVO remaps what a key *produces*, never which
20/// physical key carries which code.
21const KEY_CAPS_LOCK: u32 = 58;
22const KEY_NUM_LOCK: u32 = 69;
23const KEY_SCROLL_LOCK: u32 = 70;
24
25/// The names a keymap is compiled from: rules, model, layout, variant,
26/// options.
27///
28/// An empty field means "whatever the system defaults to", which is what
29/// xkb does with a null one.
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
31#[allow(
32    clippy::exhaustive_structs,
33    reason = "built by callers with `..Default::default()`, which a new field does not break"
34)]
35pub struct KeymapNames {
36    /// The rules file that maps the rest onto keymap components.
37    pub rules: String,
38    /// The keyboard model.
39    pub model: String,
40    /// The layout, such as `us` or `de`.
41    pub layout: String,
42    /// The layout variant, such as `dvorak`.
43    pub variant: String,
44    /// Comma-separated options, such as `ctrl:nocaps`.
45    pub options: String,
46}
47
48/// Which modifiers are in effect.
49///
50/// Four independent bools rather than a state enum: every combination of
51/// these is reachable and meaningful, which is the case the
52/// `struct_excessive_bools` lint is not about.
53#[allow(clippy::struct_excessive_bools)]
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
55#[non_exhaustive]
56pub struct Modifiers {
57    /// Shift.
58    pub shift: bool,
59    /// Control.
60    pub ctrl: bool,
61    /// Alt.
62    pub alt: bool,
63    /// The Super, Windows, or Command key.
64    pub logo: bool,
65}
66
67/// A keymap and the state of the keys pressed against it.
68///
69/// One per seat, not one per device: a user typing on two keyboards is
70/// holding Shift on one and pressing a letter on the other, and xkb has to
71/// see both.
72pub struct Keyboard {
73    context: xkb::Context,
74    keymap: xkb::Keymap,
75    state: xkb::State,
76    /// Evdev codes currently down, in press order.
77    ///
78    /// Kept so [`Keyboard::reload`] can replay them onto a fresh state: a
79    /// layout swap in the middle of a keystroke should not strand the key
80    /// that is still held.
81    held: Vec<u32>,
82}
83
84impl Keyboard {
85    /// Compile a keymap from RMLVO names.
86    ///
87    /// # Errors
88    ///
89    /// [`InputError::Keymap`] if the names do not compile -- a layout that is
90    /// not installed, an option that does not exist.
91    pub fn from_names(names: &KeymapNames) -> Result<Self, InputError> {
92        let context = Self::context();
93        let keymap = compile(&context, names)?;
94        Ok(Self::with(context, keymap))
95    }
96
97    /// Compile a keymap from a file.
98    ///
99    /// # Errors
100    ///
101    /// [`InputError::Io`] if the file cannot be read, [`InputError::Keymap`]
102    /// if it does not compile.
103    pub fn from_file(path: &Path) -> Result<Self, InputError> {
104        let text = std::fs::read_to_string(path).map_err(|source| InputError::Io {
105            path: path.to_path_buf(),
106            source,
107        })?;
108        Self::from_string(&text)
109    }
110
111    /// Compile a keymap from text already in hand.
112    ///
113    /// For a keymap that arrived over a socket or through an mmap and never
114    /// touched a filesystem.
115    ///
116    /// # Errors
117    ///
118    /// [`InputError::Keymap`] if it does not compile.
119    pub fn from_string(keymap: &str) -> Result<Self, InputError> {
120        let context = Self::context();
121        let keymap = xkb::Keymap::new_from_string(
122            &context,
123            keymap.to_owned(),
124            xkb::KEYMAP_FORMAT_TEXT_V1,
125            xkb::KEYMAP_COMPILE_NO_FLAGS,
126        )
127        .ok_or(InputError::Keymap)?;
128        Ok(Self::with(context, keymap))
129    }
130
131    /// A context whose diagnostics follow drmkit's log level.
132    ///
133    /// xkb writes its own to stderr through a handler this crate cannot
134    /// replace -- `xkb_context_set_log_fn` is not bound -- so a consumer that
135    /// installed a sink would still find keymap errors going somewhere else.
136    /// The level is the one knob there is.
137    ///
138    /// Only the ends of the range move it: asking drmkit for silence should
139    /// silence xkb too, and asking for debug should get xkb's. In between it
140    /// keeps xkb's own default, because the middle levels there are mostly
141    /// "which include paths I tried", which is noise on the way to a keymap
142    /// that compiled fine.
143    fn context() -> xkb::Context {
144        let mut context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
145        context.set_log_level(match drmkit_log::log_level() {
146            drmkit_log::LogLevel::Silent => xkb::LogLevel::Critical,
147            drmkit_log::LogLevel::Debug => xkb::LogLevel::Debug,
148            _ => xkb::LogLevel::Error,
149        });
150        context
151    }
152
153    fn with(context: xkb::Context, keymap: xkb::Keymap) -> Self {
154        let state = xkb::State::new(&keymap);
155        Self {
156            context,
157            keymap,
158            state,
159            held: Vec::new(),
160        }
161    }
162
163    /// Swap in a keymap compiled from different names.
164    ///
165    /// Keys still held are replayed onto the new state, and the lock latch is
166    /// carried over, so a layout change mid-keystroke does not strand a
167    /// modifier down or turn Caps Lock off underneath the user.
168    ///
169    /// The current keymap is left alone if the new names do not compile.
170    ///
171    /// # Errors
172    ///
173    /// [`InputError::Keymap`], as [`Keyboard::from_names`].
174    pub fn reload(&mut self, names: &KeymapNames) -> Result<(), InputError> {
175        let keymap = compile(&self.context, names)?;
176        let mut state = xkb::State::new(&keymap);
177
178        let leds = self.leds();
179        for key in &self.held {
180            state.update_key(keycode(*key), xkb::KeyDirection::Down);
181        }
182        self.keymap = keymap;
183        self.state = state;
184        // After the replay, not before: replaying a held lock key toggles its
185        // latch, and what the user had is what they should still have.
186        self.set_leds(leds);
187        Ok(())
188    }
189
190    /// Fill in what a key resolves to, without touching the keyboard state.
191    ///
192    /// This is what a repeat wants: the same physical key, re-read against
193    /// whatever modifiers are held *now*, so pressing Shift partway through a
194    /// held key changes what the next repeat types.
195    pub fn resolve(&self, event: &mut KeyEvent) {
196        let code = keycode(event.key);
197        event.sym = self.state.key_get_one_sym(code).raw();
198        event.utf8 = self.state.key_get_utf8(code);
199    }
200
201    /// Resolve a key event and fold it into the keyboard state.
202    ///
203    /// Resolution happens *before* the state update, which is the order
204    /// xkbcommon documents: a key's own press must not change what that press
205    /// produces. With a latching modifier -- a sticky Shift, a dead key -- the
206    /// press being resolved is the one that consumes the latch, so reading
207    /// afterward reads a state the user has already left. The reference
208    /// resolves first too (`keyboard.cpp`); see the module tests.
209    ///
210    /// A repeat -- [`KeyEvent::repeat`], as a key repeater synthesizes it --
211    /// is resolved and nothing more, as the reference treats it: feeding xkb
212    /// another press of a key it already holds is mutating the state from a
213    /// read.
214    pub fn process_key(&mut self, event: &mut KeyEvent) {
215        self.resolve(event);
216        if event.repeat {
217            return;
218        }
219
220        let code = keycode(event.key);
221        let direction = if event.pressed {
222            xkb::KeyDirection::Down
223        } else {
224            xkb::KeyDirection::Up
225        };
226        self.state.update_key(code, direction);
227
228        if event.pressed {
229            if !self.held.contains(&event.key) {
230                self.held.push(event.key);
231            }
232        } else {
233            self.held.retain(|key| *key != event.key);
234        }
235    }
236
237    /// Release everything held, keeping the lock state.
238    ///
239    /// For a session coming back from a VT switch, which is a thing the user
240    /// does *by holding Ctrl and Alt*. Those keys come up while the device is
241    /// revoked, and whether their releases survive the round trip is
242    /// libinput's business rather than something to depend on -- so the state
243    /// is squared away on resume instead. Getting it wrong the other way is a
244    /// session where every keystroke is a Ctrl chord until the user presses
245    /// and releases Ctrl again.
246    ///
247    /// Locks survive: Caps Lock is not something the user let go of.
248    pub fn release_all(&mut self) {
249        let leds = self.leds();
250        for key in std::mem::take(&mut self.held) {
251            self.state.update_key(keycode(key), xkb::KeyDirection::Up);
252        }
253        self.set_leds(leds);
254    }
255
256    /// Whether the keymap says this key auto-repeats.
257    ///
258    /// False for modifiers and lock keys, true for letters, digits, arrows
259    /// and function keys -- and it is the keymap's answer, not a guess.
260    #[must_use]
261    pub fn should_repeat(&self, key: u32) -> bool {
262        self.keymap.key_repeats(keycode(key))
263    }
264
265    /// Which modifiers are in effect.
266    #[must_use]
267    pub fn modifiers(&self) -> Modifiers {
268        Modifiers {
269            shift: self.modifier(xkb::MOD_NAME_SHIFT),
270            ctrl: self.modifier(xkb::MOD_NAME_CTRL),
271            alt: self.modifier(xkb::MOD_NAME_ALT),
272            logo: self.modifier(xkb::MOD_NAME_LOGO),
273        }
274    }
275
276    /// The lock state.
277    ///
278    /// Compare two of these to spot a transition, then push the new one to
279    /// the devices so the physical lights follow.
280    #[must_use]
281    pub fn leds(&self) -> Leds {
282        Leds {
283            caps_lock: self.state.led_name_is_active(xkb::LED_NAME_CAPS),
284            num_lock: self.state.led_name_is_active(xkb::LED_NAME_NUM),
285            scroll_lock: self.state.led_name_is_active(xkb::LED_NAME_SCROLL),
286        }
287    }
288
289    /// Drive the lock state to `desired`.
290    ///
291    /// By synthesizing a press and release of each lock key that is on the
292    /// wrong side, which is the only way to move a latch xkb owns. Produces
293    /// no events and does not disturb the held-key set.
294    ///
295    /// For honoring a lock state from elsewhere -- what the console had, what
296    /// the session manager reports -- and used internally by
297    /// [`Keyboard::reload`].
298    pub fn set_leds(&mut self, desired: Leds) {
299        let current = self.leds();
300        if desired.caps_lock != current.caps_lock {
301            self.tap(KEY_CAPS_LOCK);
302        }
303        if desired.num_lock != current.num_lock {
304            self.tap(KEY_NUM_LOCK);
305        }
306        if desired.scroll_lock != current.scroll_lock {
307            self.tap(KEY_SCROLL_LOCK);
308        }
309    }
310
311    /// The keys currently down, in evdev codes and press order.
312    #[must_use]
313    pub fn held_keys(&self) -> &[u32] {
314        &self.held
315    }
316
317    /// The compiled keymap, for a caller that needs to hand it on.
318    #[must_use]
319    pub const fn keymap(&self) -> &xkb::Keymap {
320        &self.keymap
321    }
322
323    fn modifier(&self, name: &str) -> bool {
324        self.state
325            .mod_name_is_active(name, xkb::STATE_MODS_EFFECTIVE)
326    }
327
328    fn tap(&mut self, key: u32) {
329        let code = keycode(key);
330        self.state.update_key(code, xkb::KeyDirection::Down);
331        self.state.update_key(code, xkb::KeyDirection::Up);
332    }
333}
334
335impl std::fmt::Debug for Keyboard {
336    /// Nothing in xkb's handles is printable, so this reports what is
337    /// actually useful: the layouts compiled in and what is held down.
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        f.debug_struct("Keyboard")
340            .field("layouts", &self.keymap.layouts().collect::<Vec<_>>())
341            .field("held", &self.held)
342            .field("leds", &self.leds())
343            .finish_non_exhaustive()
344    }
345}
346
347/// An evdev code as xkb numbers it.
348fn keycode(key: u32) -> xkb::Keycode {
349    // Saturating rather than wrapping: a code near u32::MAX is not a key, and
350    // wrapping would land it back down among the real ones.
351    xkb::Keycode::new(key.saturating_add(XKB_OFFSET))
352}
353
354fn compile(context: &xkb::Context, names: &KeymapNames) -> Result<xkb::Keymap, InputError> {
355    xkb::Keymap::new_from_names(
356        context,
357        names.rules.as_str(),
358        names.model.as_str(),
359        names.layout.as_str(),
360        names.variant.as_str(),
361        // Absent, not empty, when there are none, as the reference passes them.
362        // xkb fills an empty rules, model, layout or variant from its
363        // defaults, but options only when absent: an empty string is "no
364        // options", and the user's `XKB_DEFAULT_OPTIONS` would be dropped.
365        (!names.options.is_empty()).then(|| names.options.clone()),
366        xkb::KEYMAP_COMPILE_NO_FLAGS,
367    )
368    .ok_or(InputError::Keymap)
369}