xkb_type/keyboard.rs
1//! Virtual keyboard via evdev/uinput, or via XTEST on X11.
2//!
3//! Creates a virtual keyboard device via `/dev/uinput` (or, with the `xtest`
4//! feature, an X server connection) and injects key events using the reverse
5//! XKB keymap to produce the correct characters regardless of the user's
6//! keyboard layout.
7
8use std::ops::{ControlFlow, Range};
9use std::thread;
10use std::time::Duration;
11
12use anyhow::Context;
13use evdev::{uinput::VirtualDevice, AttributeSet, EventType, InputEvent, Key};
14// Logging via the `log` crate (only active with `logging` feature).
15#[cfg(feature = "logging")]
16use log::{debug, warn};
17#[cfg(not(feature = "logging"))]
18macro_rules! debug {
19 ($($arg:tt)*) => {};
20}
21#[cfg(not(feature = "logging"))]
22macro_rules! warn {
23 ($($arg:tt)*) => {};
24}
25
26use crate::keymap::{KeyboardLayout, XkbKeymap};
27use crate::{default_clipboard, run_gated, ClipboardBackend, KeyMapping, KeyTap, Typed};
28
29/// Delay after creating the virtual device on the runtime path (e.g. when
30/// the daemon re-creates the keyboard after an error). Short, because this
31/// runs on the user's typing path — every error-recovery would otherwise
32/// stall typing.
33const DEVICE_SETTLE_DELAY: Duration = Duration::from_millis(200);
34
35/// Longer delay used at daemon startup (prewarm), so X11 has time to
36/// process MappingNotify and attach the new device's keymap before the
37/// first character is typed. Without this, the very first typed character
38/// after boot can race the X11 keymap refresh and drop accented keys
39/// (notably `<LVL3>` characters like `é` on `us:qwerty-fr`).
40const INITIAL_WARMUP_DELAY: Duration = Duration::from_secs(1);
41
42/// See [`Sink::min_delay`].
43const UINPUT_MIN_DELAY: Duration = Duration::from_millis(1);
44
45/// Where a [`Keyboard`]'s key events go.
46enum Sink {
47 /// A uinput device: each event is written to the kernel immediately.
48 Uinput(VirtualDevice),
49 /// The X server via XTEST: events are queued on the X connection.
50 #[cfg(feature = "xtest")]
51 Xtest(Box<crate::xtest::XtestSink>),
52}
53
54impl Sink {
55 /// Press (`pressed`) or release the key with evdev code `code`.
56 fn key(&mut self, code: u16, pressed: bool) -> anyhow::Result<()> {
57 match self {
58 Sink::Uinput(device) => {
59 device.emit(&[InputEvent::new(EventType::KEY, code, i32::from(pressed))])?;
60 }
61 #[cfg(feature = "xtest")]
62 Sink::Xtest(xtest) => xtest.key(code, pressed)?,
63 }
64 Ok(())
65 }
66
67 /// Hand queued events to the X server. uinput writes are immediate.
68 fn flush(&mut self) -> anyhow::Result<()> {
69 match self {
70 Sink::Uinput(_) => Ok(()),
71 #[cfg(feature = "xtest")]
72 Sink::Xtest(xtest) => xtest.flush(),
73 }
74 }
75
76 /// The shortest wait between events. uinput events pass through the
77 /// kernel's evdev queue (about 64 events) to the X server or compositor;
78 /// with no wait a burst overflows it and the reader drops the rest of the
79 /// text, so a zero `key_delay` is raised to 1 ms there.
80 fn min_delay(&self) -> Duration {
81 match self {
82 Sink::Uinput(_) => UINPUT_MIN_DELAY,
83 #[cfg(feature = "xtest")]
84 Sink::Xtest(_) => Duration::ZERO,
85 }
86 }
87
88 /// Return once every event sent so far has been processed (XTEST only).
89 fn sync(&mut self) -> anyhow::Result<()> {
90 match self {
91 Sink::Uinput(_) => Ok(()),
92 #[cfg(feature = "xtest")]
93 Sink::Xtest(xtest) => xtest.sync(),
94 }
95 }
96}
97
98/// One gated step of [`Keyboard::type_text_gated`], from [`plan_steps`].
99#[derive(Debug, Clone)]
100enum Step {
101 /// One character the keymap has: tap `main`, then `follow` (a dead
102 /// key's base letter) when there is one. The pair is one step, so a
103 /// pause can never split it.
104 Key(KeyMapping),
105 /// A run of consecutive characters the keymap lacks, pasted through
106 /// the clipboard in one go: this byte range of the text.
107 Paste(Range<usize>),
108}
109
110/// Split `text` into the steps [`Keyboard::type_text_gated`] sends, in
111/// order, each with the byte offset of its first character (what the gate
112/// is asked with). `lookup` is the keymap lookup. A run of characters it
113/// does not map becomes one [`Step::Paste`] that starts where the run does.
114fn plan_steps(
115 text: &str,
116 mut lookup: impl FnMut(char) -> Option<KeyMapping>,
117) -> Vec<(usize, Step)> {
118 let mut steps = Vec::new();
119 let mut paste_start = None;
120 for (at, ch) in text.char_indices() {
121 match lookup(ch) {
122 Some(mapping) => {
123 if let Some(start) = paste_start.take() {
124 steps.push((start, Step::Paste(start..at)));
125 }
126 steps.push((at, Step::Key(mapping)));
127 }
128 None => {
129 paste_start.get_or_insert(at);
130 }
131 }
132 }
133 if let Some(start) = paste_start {
134 steps.push((start, Step::Paste(start..text.len())));
135 }
136 steps
137}
138
139/// Virtual keyboard that injects keystrokes via uinput, or via XTEST on X11
140/// (see [`Keyboard::new_xtest`]).
141pub struct Keyboard {
142 sink: Sink,
143 keymap: XkbKeymap,
144 clipboard: Box<dyn ClipboardBackend>,
145 key_delay: Duration,
146}
147
148impl Keyboard {
149 /// Create a new virtual keyboard with auto-detected layout and clipboard.
150 ///
151 /// This is the most ergonomic constructor: it picks up the active
152 /// XKB layout from the compositor (Hyprland / Sway / env vars), picks
153 /// the right clipboard backend for the current display server, and
154 /// builds the uinput device.
155 ///
156 /// `key_delay` is the inter-event delay. Raise it for TUIs that drop
157 /// characters in raw mode (e.g. Node/Ink-based apps like Claude Code).
158 ///
159 /// Requires write access to `/dev/uinput`.
160 ///
161 /// For full control over keymap/clipboard, see
162 /// [`Keyboard::with_components`]; to force a specific XKB layout, see
163 /// [`Keyboard::with_layout`].
164 pub fn new(key_delay: Duration) -> anyhow::Result<Self> {
165 let layout = KeyboardLayout::detect();
166 let keymap = XkbKeymap::from_layout(&layout)?;
167 Self::with_components(keymap, default_clipboard(), key_delay)
168 }
169
170 /// Like [`Keyboard::new`], but uses a longer post-creation settle
171 /// delay suited for the **first** virtual keyboard created in a
172 /// process. Call this from a startup prewarm so X11 has time to
173 /// process MappingNotify and attach the new device's keymap before
174 /// any character is typed.
175 ///
176 /// Subsequent recreates (e.g. on error recovery) should keep using
177 /// [`Keyboard::new`], which uses a shorter settle so the user's
178 /// typing path is not stalled by 1s on every recovery.
179 pub fn new_prewarm(key_delay: Duration) -> anyhow::Result<Self> {
180 let layout = KeyboardLayout::detect();
181 let keymap = XkbKeymap::from_layout(&layout)?;
182 Self::with_components_settle(keymap, default_clipboard(), key_delay, INITIAL_WARMUP_DELAY)
183 }
184
185 /// Create a virtual keyboard for a specific XKB layout (and optional
186 /// variant), with the auto-detected clipboard backend.
187 ///
188 /// Useful when the application already knows the user's layout and
189 /// wants to skip the compositor probe (e.g. it's stored in a config
190 /// file).
191 pub fn with_layout(
192 layout: &str,
193 variant: Option<&str>,
194 key_delay: Duration,
195 ) -> anyhow::Result<Self> {
196 let detected = KeyboardLayout {
197 layout: layout.to_string(),
198 variant: variant.unwrap_or("").to_string(),
199 };
200 let keymap = XkbKeymap::from_layout(&detected)?;
201 Self::with_components(keymap, default_clipboard(), key_delay)
202 }
203
204 /// Create a virtual keyboard from explicit components (keymap +
205 /// clipboard backend).
206 ///
207 /// Use this when you need to inject a custom [`XkbKeymap`] (e.g. for
208 /// tests) or a non-default clipboard backend
209 /// ([`crate::NoopClipboard`] / [`crate::WaylandClipboard`] /
210 /// [`crate::X11Clipboard`]).
211 ///
212 /// Requires write access to `/dev/uinput` (user must be in the `input`
213 /// group or have the appropriate udev rule installed).
214 ///
215 /// `key_delay` is the inter-event delay. Raise it for TUIs that drop
216 /// characters in raw mode (e.g. Node/Ink-based apps like Claude Code).
217 pub fn with_components(
218 keymap: XkbKeymap,
219 clipboard: Box<dyn ClipboardBackend>,
220 key_delay: Duration,
221 ) -> anyhow::Result<Self> {
222 Self::with_components_settle(keymap, clipboard, key_delay, DEVICE_SETTLE_DELAY)
223 }
224
225 /// Same as [`Keyboard::with_components`], but with an explicit
226 /// post-creation settle delay. Kept private — callers should pick
227 /// between [`Keyboard::new`] (short settle, runtime path) and
228 /// [`Keyboard::new_prewarm`] (long settle, startup path).
229 fn with_components_settle(
230 keymap: XkbKeymap,
231 clipboard: Box<dyn ClipboardBackend>,
232 key_delay: Duration,
233 settle: Duration,
234 ) -> anyhow::Result<Self> {
235 // Register all key codes we might need (1..=247 covers standard
236 // keys; KEY_MAX on Linux is ~767 but codes beyond 247 are rare).
237 let mut keys = AttributeSet::<Key>::new();
238 for code in 1..=247 {
239 keys.insert(Key::new(code));
240 }
241
242 let device = evdev::uinput::VirtualDeviceBuilder::new()
243 .context("failed to create VirtualDeviceBuilder")?
244 .name("whisrs virtual keyboard")
245 .with_keys(&keys)
246 .context("failed to register key events")?
247 .build()
248 .context("failed to build uinput virtual device — check /dev/uinput permissions")?;
249
250 // Give the kernel (and on X11, the server) time to register the
251 // new device and refresh its keymap before any keys are sent.
252 thread::sleep(settle);
253
254 debug!("uinput virtual keyboard created");
255
256 Ok(Self {
257 sink: Sink::Uinput(device),
258 keymap,
259 clipboard,
260 key_delay,
261 })
262 }
263
264 /// Create a keyboard that injects through the X server's XTEST extension
265 /// instead of uinput, with auto-detected layout and clipboard.
266 ///
267 /// XTEST events go straight to the X server, which processes them in
268 /// order, so nothing is dropped however fast they are sent: `key_delay`
269 /// can be zero. Needs no `/dev/uinput` access and no settle delay.
270 ///
271 /// Fails on Xwayland (keys would only reach X11 clients) and when the
272 /// server's XTEST keyboard has a different layout than the detected one.
273 #[cfg(feature = "xtest")]
274 pub fn new_xtest(key_delay: Duration) -> anyhow::Result<Self> {
275 let layout = KeyboardLayout::detect();
276 let mut keymap = XkbKeymap::from_layout(&layout)?;
277 let sink = Sink::Xtest(Box::new(crate::xtest::XtestSink::connect(&mut keymap)?));
278 debug!("XTEST keyboard connected");
279 Ok(Self {
280 sink,
281 keymap,
282 clipboard: default_clipboard(),
283 key_delay,
284 })
285 }
286
287 // -----------------------------------------------------------------------
288 // Public API
289 // -----------------------------------------------------------------------
290
291 pub fn set_key_delay(&mut self, key_delay: Duration) {
292 self.key_delay = key_delay;
293 }
294
295 /// Type `text` by injecting keystrokes through the virtual device.
296 ///
297 /// Characters found in the keymap are typed via direct key events
298 /// (with Shift/AltGr modifiers as needed, including dead-key sequences).
299 /// Characters *not* found in the keymap are accumulated and pasted as
300 /// a batch via the clipboard (Ctrl+V), with clipboard save/restore
301 /// so the user's clipboard is not permanently overwritten.
302 ///
303 /// Types everything; [`Keyboard::type_text_gated`] (and
304 /// [`crate::KeyInjector::type_text`]) can pause or stop between keys.
305 pub fn type_text(&mut self, text: &str) -> anyhow::Result<()> {
306 self.type_text_gated(text, &mut |_| ControlFlow::Continue(()))
307 .map(|_| ())
308 }
309
310 /// [`Keyboard::type_text`], asking `gate` before every key tap and
311 /// before every clipboard paste, with the byte offset of the next unsent
312 /// character (for a paste, the first character of the run). A dead key
313 /// and its base letter are one character: asked about once, never
314 /// split. On [`ControlFlow::Break`] nothing more is sent, the events
315 /// sent so far are flushed, and the result is [`Typed::StoppedAt`] that
316 /// offset. See [`crate::KeyInjector::type_text`].
317 pub fn type_text_gated(
318 &mut self,
319 text: &str,
320 gate: &mut dyn FnMut(usize) -> ControlFlow<()>,
321 ) -> anyhow::Result<Typed> {
322 self.release_all_modifiers()?;
323
324 let keymap = &self.keymap;
325 let steps = plan_steps(text, |ch| keymap.lookup(ch).copied());
326 let typed = run_gated(steps, gate, |step| match step {
327 Step::Key(mapping) => {
328 self.tap_key(&mapping.main)?;
329 if let Some(follow) = mapping.follow.as_ref() {
330 self.tap_key(follow)?;
331 }
332 Ok(())
333 }
334 Step::Paste(range) => self.paste_text(&text[range]),
335 })?;
336
337 self.sink.sync()?;
338 Ok(typed)
339 }
340
341 /// Emit Backspace `count` times.
342 pub fn backspace(&mut self, count: usize) -> anyhow::Result<()> {
343 self.release_all_modifiers()?;
344
345 for _ in 0..count {
346 self.tap_key(&KeyTap {
347 keycode: Key::KEY_BACKSPACE.code(),
348 shift: false,
349 altgr: false,
350 })?;
351 }
352
353 self.sink.sync()
354 }
355
356 /// Press all keys in `keys`, then release them in reverse order.
357 ///
358 /// Each press and release is separated by `self.key_delay`.
359 pub fn send_combo(&mut self, keys: &[Key]) -> anyhow::Result<()> {
360 self.release_all_modifiers()?;
361
362 // Press all keys in order.
363 for key in keys {
364 self.sink.key(key.code(), true)?;
365 self.pause()?;
366 }
367
368 // Release in reverse order.
369 for key in keys.iter().rev() {
370 self.sink.key(key.code(), false)?;
371 self.pause()?;
372 }
373
374 self.sink.sync()
375 }
376
377 // -----------------------------------------------------------------------
378 // Internal helpers
379 // -----------------------------------------------------------------------
380
381 /// Send queued events, then wait `key_delay` (at least
382 /// [`Sink::min_delay`]).
383 fn pause(&mut self) -> anyhow::Result<()> {
384 self.sink.flush()?;
385 thread::sleep(self.key_delay.max(self.sink.min_delay()));
386 Ok(())
387 }
388
389 /// Press or release a modifier key.
390 fn set_modifier(&mut self, modifier: Key, pressed: bool) -> anyhow::Result<()> {
391 self.sink.key(modifier.code(), pressed)?;
392 self.pause()
393 }
394
395 /// Press and release a single key, with Shift and/or AltGr held as needed.
396 ///
397 /// AltGr is the keycode XKB exposes for `ISO_Level3_Shift`. On some
398 /// X11 layouts that is a dedicated `<LVL3>` keycode rather than
399 /// `KEY_RIGHTALT`; using the XKB keycode keeps third-level characters
400 /// like `é` working with uinput.
401 fn tap_key(&mut self, tap: &KeyTap) -> anyhow::Result<()> {
402 let level3_key = Key::new(self.keymap.level3_keycode());
403
404 if tap.shift {
405 self.set_modifier(Key::KEY_LEFTSHIFT, true)?;
406 }
407 if tap.altgr {
408 self.set_modifier(level3_key, true)?;
409 }
410
411 self.sink.key(tap.keycode, true)?;
412 self.pause()?;
413 self.sink.key(tap.keycode, false)?;
414 self.pause()?;
415
416 if tap.altgr {
417 self.set_modifier(level3_key, false)?;
418 }
419 if tap.shift {
420 self.set_modifier(Key::KEY_LEFTSHIFT, false)?;
421 }
422
423 Ok(())
424 }
425
426 /// Release all modifier keys to prevent interference with injected text.
427 fn release_all_modifiers(&mut self) -> anyhow::Result<()> {
428 let level3_key = Key::new(self.keymap.level3_keycode());
429 let modifiers = [
430 Key::KEY_LEFTSHIFT,
431 Key::KEY_RIGHTSHIFT,
432 Key::KEY_LEFTCTRL,
433 Key::KEY_RIGHTCTRL,
434 Key::KEY_LEFTALT,
435 Key::KEY_RIGHTALT,
436 level3_key,
437 Key::KEY_LEFTMETA,
438 Key::KEY_RIGHTMETA,
439 ];
440
441 for modifier in &modifiers {
442 self.sink.key(modifier.code(), false)?;
443 }
444 self.pause()
445 }
446
447 /// Inject Ctrl+V to paste from clipboard.
448 fn inject_ctrl_v(&mut self) -> anyhow::Result<()> {
449 self.set_modifier(Key::KEY_LEFTCTRL, true)?;
450 self.sink.key(Key::KEY_V.code(), true)?;
451 self.pause()?;
452 self.sink.key(Key::KEY_V.code(), false)?;
453 self.pause()?;
454 self.set_modifier(Key::KEY_LEFTCTRL, false)?;
455 // The paste must reach the server before the clipboard is restored.
456 self.sink.sync()
457 }
458
459 /// Set clipboard to `text`, inject Ctrl+V, then restore previous content.
460 fn paste_text(&mut self, text: &str) -> anyhow::Result<()> {
461 // Save current clipboard.
462 let saved = self.clipboard.get_text().ok();
463
464 // Set new clipboard content.
465 self.clipboard
466 .set_text(text)
467 .context("failed to set clipboard for paste")?;
468
469 // Small delay to ensure clipboard is ready.
470 thread::sleep(Duration::from_millis(10));
471
472 // Inject Ctrl+V.
473 self.release_all_modifiers()?;
474 self.inject_ctrl_v()?;
475
476 // Small delay before restoring clipboard.
477 thread::sleep(Duration::from_millis(50));
478
479 // Restore previous clipboard content.
480 if let Some(previous) = saved {
481 if let Err(_e) = self.clipboard.set_text(&previous) {
482 warn!("failed to restore clipboard: {_e}");
483 }
484 }
485
486 Ok(())
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 fn tap(keycode: u16) -> KeyTap {
495 KeyTap {
496 keycode,
497 shift: false,
498 altgr: false,
499 }
500 }
501
502 /// A stand-in keymap: ASCII letters are one tap (keycode = the byte),
503 /// `ã` is a dead tilde (100) followed by `a`, and nothing else maps
504 /// (so it is pasted).
505 fn lookup(ch: char) -> Option<KeyMapping> {
506 match ch {
507 'a'..='z' | ' ' => Some(KeyMapping {
508 main: tap(ch as u16),
509 follow: None,
510 }),
511 'ã' => Some(KeyMapping {
512 main: tap(100),
513 follow: Some(tap('a' as u16)),
514 }),
515 _ => None,
516 }
517 }
518
519 /// Each step as (offset, what it sends): `key:<main>[+<follow>]` or
520 /// the pasted text.
521 fn plan(text: &str) -> Vec<(usize, String)> {
522 plan_steps(text, lookup)
523 .into_iter()
524 .map(|(at, step)| {
525 let what = match step {
526 Step::Key(m) => match m.follow {
527 Some(f) => format!("key:{}+{}", m.main.keycode, f.keycode),
528 None => format!("key:{}", m.main.keycode),
529 },
530 Step::Paste(range) => {
531 assert_eq!(range.start, at, "a paste is asked about at its start");
532 format!("paste:{}", &text[range])
533 }
534 };
535 (at, what)
536 })
537 .collect()
538 }
539
540 #[test]
541 fn mapped_characters_are_one_step_each_at_their_byte_offset() {
542 assert_eq!(
543 plan("ab c"),
544 vec![
545 (0, "key:97".to_string()),
546 (1, "key:98".to_string()),
547 (2, "key:32".to_string()),
548 (3, "key:99".to_string()),
549 ]
550 );
551 assert!(plan("").is_empty());
552 }
553
554 #[test]
555 fn a_dead_key_pair_is_one_step() {
556 // `ã` is two UTF-8 bytes, so `b` sits at offset 2.
557 assert_eq!(
558 plan("ãb"),
559 vec![(0, "key:100+97".to_string()), (2, "key:98".to_string())]
560 );
561 }
562
563 #[test]
564 fn a_run_of_unmapped_characters_is_one_paste_at_its_start() {
565 // Each emoji is 4 bytes, `€` is 3.
566 assert_eq!(
567 plan("a😀😀b€€"),
568 vec![
569 (0, "key:97".to_string()),
570 (1, "paste:😀😀".to_string()),
571 (9, "key:98".to_string()),
572 (10, "paste:€€".to_string()),
573 ]
574 );
575 assert_eq!(
576 plan("😀a"),
577 vec![(0, "paste:😀".to_string()), (4, "key:97".to_string())]
578 );
579 }
580
581 #[test]
582 fn the_steps_cover_the_text_in_order() {
583 let text = "grüße ã 😀 x€y";
584 let mut covered = String::new();
585 let mut last = None;
586 for (at, step) in plan_steps(text, lookup) {
587 assert!(text.is_char_boundary(at), "offset {at} splits a character");
588 if let Some(last) = last {
589 assert!(last < at, "offsets go up");
590 }
591 last = Some(at);
592 match step {
593 Step::Key(_) => {
594 let ch = text[at..].chars().next().unwrap();
595 covered.push(ch);
596 }
597 Step::Paste(range) => covered.push_str(&text[range]),
598 }
599 }
600 assert_eq!(covered, text);
601 }
602
603 /// The gate is asked once per step, so a break mid-text stops at a step
604 /// boundary: never between a dead key and its letter, never inside a
605 /// paste run. What is left is exactly the untyped rest.
606 #[test]
607 fn a_break_leaves_the_rest_from_a_step_boundary() {
608 let text = "aã😀😀b";
609 let steps = plan_steps(text, lookup);
610 let offsets: Vec<usize> = steps.iter().map(|(at, _)| *at).collect();
611 assert_eq!(offsets, vec![0, 1, 3, 11]);
612
613 for (n, &stop) in offsets.iter().enumerate() {
614 let mut gate = |at: usize| {
615 if at == stop {
616 ControlFlow::Break(())
617 } else {
618 ControlFlow::Continue(())
619 }
620 };
621 let mut sent = 0;
622 let typed = run_gated(plan_steps(text, lookup), &mut gate, |_| {
623 sent += 1;
624 Ok(())
625 })
626 .unwrap();
627 assert_eq!(typed, Typed::StoppedAt(stop));
628 assert_eq!(sent, n, "steps before the break");
629 }
630 }
631}