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 /// afterwards reads a state the user has already left. The reference does
208 /// it the other way round; see the module tests.
209 pub fn process_key(&mut self, event: &mut KeyEvent) {
210 self.resolve(event);
211
212 let code = keycode(event.key);
213 let direction = if event.pressed {
214 xkb::KeyDirection::Down
215 } else {
216 xkb::KeyDirection::Up
217 };
218 self.state.update_key(code, direction);
219
220 if event.pressed {
221 if !self.held.contains(&event.key) {
222 self.held.push(event.key);
223 }
224 } else {
225 self.held.retain(|key| *key != event.key);
226 }
227 }
228
229 /// Release everything held, keeping the lock state.
230 ///
231 /// For a session coming back from a VT switch, which is a thing the user
232 /// does *by holding Ctrl and Alt*. Those keys come up while the device is
233 /// revoked, and whether their releases survive the round trip is
234 /// libinput's business rather than something to depend on -- so the state
235 /// is squared away on resume instead. Getting it wrong the other way is a
236 /// session where every keystroke is a Ctrl chord until the user presses
237 /// and releases Ctrl again.
238 ///
239 /// Locks survive: Caps Lock is not something the user let go of.
240 pub fn release_all(&mut self) {
241 let leds = self.leds();
242 for key in std::mem::take(&mut self.held) {
243 self.state.update_key(keycode(key), xkb::KeyDirection::Up);
244 }
245 self.set_leds(leds);
246 }
247
248 /// Whether the keymap says this key auto-repeats.
249 ///
250 /// False for modifiers and lock keys, true for letters, digits, arrows
251 /// and function keys -- and it is the keymap's answer, not a guess.
252 #[must_use]
253 pub fn should_repeat(&self, key: u32) -> bool {
254 self.keymap.key_repeats(keycode(key))
255 }
256
257 /// Which modifiers are in effect.
258 #[must_use]
259 pub fn modifiers(&self) -> Modifiers {
260 Modifiers {
261 shift: self.modifier(xkb::MOD_NAME_SHIFT),
262 ctrl: self.modifier(xkb::MOD_NAME_CTRL),
263 alt: self.modifier(xkb::MOD_NAME_ALT),
264 logo: self.modifier(xkb::MOD_NAME_LOGO),
265 }
266 }
267
268 /// The lock state.
269 ///
270 /// Compare two of these to spot a transition, then push the new one to
271 /// the devices so the physical lights follow.
272 #[must_use]
273 pub fn leds(&self) -> Leds {
274 Leds {
275 caps_lock: self.state.led_name_is_active(xkb::LED_NAME_CAPS),
276 num_lock: self.state.led_name_is_active(xkb::LED_NAME_NUM),
277 scroll_lock: self.state.led_name_is_active(xkb::LED_NAME_SCROLL),
278 }
279 }
280
281 /// Drive the lock state to `desired`.
282 ///
283 /// By synthesizing a press and release of each lock key that is on the
284 /// wrong side, which is the only way to move a latch xkb owns. Produces
285 /// no events and does not disturb the held-key set.
286 ///
287 /// For honouring a lock state from elsewhere -- what the console had, what
288 /// the session manager reports -- and used internally by
289 /// [`Keyboard::reload`].
290 pub fn set_leds(&mut self, desired: Leds) {
291 let current = self.leds();
292 if desired.caps_lock != current.caps_lock {
293 self.tap(KEY_CAPS_LOCK);
294 }
295 if desired.num_lock != current.num_lock {
296 self.tap(KEY_NUM_LOCK);
297 }
298 if desired.scroll_lock != current.scroll_lock {
299 self.tap(KEY_SCROLL_LOCK);
300 }
301 }
302
303 /// The keys currently down, in evdev codes and press order.
304 #[must_use]
305 pub fn held_keys(&self) -> &[u32] {
306 &self.held
307 }
308
309 /// The compiled keymap, for a caller that needs to hand it on.
310 #[must_use]
311 pub const fn keymap(&self) -> &xkb::Keymap {
312 &self.keymap
313 }
314
315 fn modifier(&self, name: &str) -> bool {
316 self.state
317 .mod_name_is_active(name, xkb::STATE_MODS_EFFECTIVE)
318 }
319
320 fn tap(&mut self, key: u32) {
321 let code = keycode(key);
322 self.state.update_key(code, xkb::KeyDirection::Down);
323 self.state.update_key(code, xkb::KeyDirection::Up);
324 }
325}
326
327impl std::fmt::Debug for Keyboard {
328 /// Nothing in xkb's handles is printable, so this reports what is
329 /// actually useful: the layouts compiled in and what is held down.
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 f.debug_struct("Keyboard")
332 .field("layouts", &self.keymap.layouts().collect::<Vec<_>>())
333 .field("held", &self.held)
334 .field("leds", &self.leds())
335 .finish_non_exhaustive()
336 }
337}
338
339/// An evdev code as xkb numbers it.
340fn keycode(key: u32) -> xkb::Keycode {
341 // Saturating rather than wrapping: a code near u32::MAX is not a key, and
342 // wrapping would land it back down among the real ones.
343 xkb::Keycode::new(key.saturating_add(XKB_OFFSET))
344}
345
346fn compile(context: &xkb::Context, names: &KeymapNames) -> Result<xkb::Keymap, InputError> {
347 xkb::Keymap::new_from_names(
348 context,
349 names.rules.as_str(),
350 names.model.as_str(),
351 names.layout.as_str(),
352 names.variant.as_str(),
353 // The crate takes options as an owned Option because it has to append
354 // its own NUL; an empty string means the same thing as None to xkb.
355 Some(names.options.clone()),
356 xkb::KEYMAP_COMPILE_NO_FLAGS,
357 )
358 .ok_or(InputError::Keymap)
359}