Skip to main content

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}