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}