Skip to main content

xkb_type/
keyboard.rs

1//! Virtual keyboard via evdev/uinput.
2//!
3//! Creates a virtual keyboard device via `/dev/uinput` and injects key events
4//! using the reverse XKB keymap to produce the correct characters regardless
5//! of the user's keyboard layout.
6
7use std::thread;
8use std::time::Duration;
9
10use anyhow::Context;
11use evdev::{uinput::VirtualDevice, AttributeSet, EventType, InputEvent, Key};
12// Logging via the `log` crate (only active with `logging` feature).
13#[cfg(feature = "logging")]
14use log::{debug, warn};
15#[cfg(not(feature = "logging"))]
16macro_rules! debug {
17    ($($arg:tt)*) => {};
18}
19#[cfg(not(feature = "logging"))]
20macro_rules! warn {
21    ($($arg:tt)*) => {};
22}
23
24use crate::keymap::{KeyboardLayout, XkbKeymap};
25use crate::{default_clipboard, ClipboardBackend, KeyTap};
26
27/// Delay after creating the virtual device on the runtime path (e.g. when
28/// the daemon re-creates the keyboard after an error). Short, because this
29/// runs on the user's typing path — every error-recovery would otherwise
30/// stall typing.
31const DEVICE_SETTLE_DELAY: Duration = Duration::from_millis(200);
32
33/// Longer delay used at daemon startup (prewarm), so X11 has time to
34/// process MappingNotify and attach the new device's keymap before the
35/// first character is typed. Without this, the very first typed character
36/// after boot can race the X11 keymap refresh and drop accented keys
37/// (notably `<LVL3>` characters like `é` on `us:qwerty-fr`).
38const INITIAL_WARMUP_DELAY: Duration = Duration::from_secs(1);
39
40/// Virtual keyboard that injects keystrokes via uinput.
41pub struct Keyboard {
42    device: VirtualDevice,
43    keymap: XkbKeymap,
44    clipboard: Box<dyn ClipboardBackend>,
45    key_delay: Duration,
46}
47
48impl Keyboard {
49    /// Create a new virtual keyboard with auto-detected layout and clipboard.
50    ///
51    /// This is the most ergonomic constructor: it picks up the active
52    /// XKB layout from the compositor (Hyprland / Sway / env vars), picks
53    /// the right clipboard backend for the current display server, and
54    /// builds the uinput device.
55    ///
56    /// `key_delay` is the inter-event delay. Raise it for TUIs that drop
57    /// characters in raw mode (e.g. Node/Ink-based apps like Claude Code).
58    ///
59    /// Requires write access to `/dev/uinput`.
60    ///
61    /// For full control over keymap/clipboard, see
62    /// [`Keyboard::with_components`]; to force a specific XKB layout, see
63    /// [`Keyboard::with_layout`].
64    pub fn new(key_delay: Duration) -> anyhow::Result<Self> {
65        let layout = KeyboardLayout::detect();
66        let keymap = XkbKeymap::from_layout(&layout)?;
67        Self::with_components(keymap, default_clipboard(), key_delay)
68    }
69
70    /// Like [`Keyboard::new`], but uses a longer post-creation settle
71    /// delay suited for the **first** virtual keyboard created in a
72    /// process. Call this from a startup prewarm so X11 has time to
73    /// process MappingNotify and attach the new device's keymap before
74    /// any character is typed.
75    ///
76    /// Subsequent recreates (e.g. on error recovery) should keep using
77    /// [`Keyboard::new`], which uses a shorter settle so the user's
78    /// typing path is not stalled by 1s on every recovery.
79    pub fn new_prewarm(key_delay: Duration) -> anyhow::Result<Self> {
80        let layout = KeyboardLayout::detect();
81        let keymap = XkbKeymap::from_layout(&layout)?;
82        Self::with_components_settle(keymap, default_clipboard(), key_delay, INITIAL_WARMUP_DELAY)
83    }
84
85    /// Create a virtual keyboard for a specific XKB layout (and optional
86    /// variant), with the auto-detected clipboard backend.
87    ///
88    /// Useful when the application already knows the user's layout and
89    /// wants to skip the compositor probe (e.g. it's stored in a config
90    /// file).
91    pub fn with_layout(
92        layout: &str,
93        variant: Option<&str>,
94        key_delay: Duration,
95    ) -> anyhow::Result<Self> {
96        let detected = KeyboardLayout {
97            layout: layout.to_string(),
98            variant: variant.unwrap_or("").to_string(),
99        };
100        let keymap = XkbKeymap::from_layout(&detected)?;
101        Self::with_components(keymap, default_clipboard(), key_delay)
102    }
103
104    /// Create a virtual keyboard from explicit components (keymap +
105    /// clipboard backend).
106    ///
107    /// Use this when you need to inject a custom [`XkbKeymap`] (e.g. for
108    /// tests) or a non-default clipboard backend
109    /// ([`crate::NoopClipboard`] / [`crate::WaylandClipboard`] /
110    /// [`crate::X11Clipboard`]).
111    ///
112    /// Requires write access to `/dev/uinput` (user must be in the `input`
113    /// group or have the appropriate udev rule installed).
114    ///
115    /// `key_delay` is the inter-event delay. Raise it for TUIs that drop
116    /// characters in raw mode (e.g. Node/Ink-based apps like Claude Code).
117    pub fn with_components(
118        keymap: XkbKeymap,
119        clipboard: Box<dyn ClipboardBackend>,
120        key_delay: Duration,
121    ) -> anyhow::Result<Self> {
122        Self::with_components_settle(keymap, clipboard, key_delay, DEVICE_SETTLE_DELAY)
123    }
124
125    /// Same as [`Keyboard::with_components`], but with an explicit
126    /// post-creation settle delay. Kept private — callers should pick
127    /// between [`Keyboard::new`] (short settle, runtime path) and
128    /// [`Keyboard::new_prewarm`] (long settle, startup path).
129    fn with_components_settle(
130        keymap: XkbKeymap,
131        clipboard: Box<dyn ClipboardBackend>,
132        key_delay: Duration,
133        settle: Duration,
134    ) -> anyhow::Result<Self> {
135        // Register all key codes we might need (1..=247 covers standard
136        // keys; KEY_MAX on Linux is ~767 but codes beyond 247 are rare).
137        let mut keys = AttributeSet::<Key>::new();
138        for code in 1..=247 {
139            keys.insert(Key::new(code));
140        }
141
142        let device = evdev::uinput::VirtualDeviceBuilder::new()
143            .context("failed to create VirtualDeviceBuilder")?
144            .name("whisrs virtual keyboard")
145            .with_keys(&keys)
146            .context("failed to register key events")?
147            .build()
148            .context("failed to build uinput virtual device — check /dev/uinput permissions")?;
149
150        // Give the kernel (and on X11, the server) time to register the
151        // new device and refresh its keymap before any keys are sent.
152        thread::sleep(settle);
153
154        debug!("uinput virtual keyboard created");
155
156        Ok(Self {
157            device,
158            keymap,
159            clipboard,
160            key_delay,
161        })
162    }
163
164    // -----------------------------------------------------------------------
165    // Public API
166    // -----------------------------------------------------------------------
167
168    pub fn set_key_delay(&mut self, key_delay: Duration) {
169        self.key_delay = key_delay;
170    }
171
172    /// Type `text` by injecting keystrokes through the virtual device.
173    ///
174    /// Characters found in the keymap are typed via direct key events
175    /// (with Shift/AltGr modifiers as needed, including dead-key sequences).
176    /// Characters *not* found in the keymap are accumulated and pasted as
177    /// a batch via the clipboard (Ctrl+V), with clipboard save/restore
178    /// so the user's clipboard is not permanently overwritten.
179    pub fn type_text(&mut self, text: &str) -> anyhow::Result<()> {
180        self.release_all_modifiers()?;
181
182        let mut paste_buf = String::new();
183
184        for ch in text.chars() {
185            // Copy the lookup result to release the immutable borrow on
186            // self.keymap before calling mutable methods below.
187            let mapping = self.keymap.lookup(ch).copied();
188            if let Some(mapping) = mapping {
189                // Flush any pending paste buffer first.
190                if !paste_buf.is_empty() {
191                    self.paste_text(&paste_buf)?;
192                    paste_buf.clear();
193                }
194                self.tap_key(&mapping.main)?;
195                if let Some(follow) = mapping.follow.as_ref() {
196                    self.tap_key(follow)?;
197                }
198            } else {
199                // Character not in keymap — accumulate for clipboard paste.
200                paste_buf.push(ch);
201            }
202        }
203
204        // Flush remaining paste buffer.
205        if !paste_buf.is_empty() {
206            self.paste_text(&paste_buf)?;
207        }
208
209        Ok(())
210    }
211
212    /// Emit Backspace `count` times.
213    pub fn backspace(&mut self, count: usize) -> anyhow::Result<()> {
214        self.release_all_modifiers()?;
215
216        for _ in 0..count {
217            self.tap_key(&KeyTap {
218                keycode: Key::KEY_BACKSPACE.code(),
219                shift: false,
220                altgr: false,
221            })?;
222        }
223
224        Ok(())
225    }
226
227    /// Press all keys in `keys`, then release them in reverse order.
228    ///
229    /// Each press and release is separated by `self.key_delay`.
230    pub fn send_combo(&mut self, keys: &[Key]) -> anyhow::Result<()> {
231        // Press all keys in order.
232        for key in keys {
233            self.device
234                .emit(&[InputEvent::new(EventType::KEY, key.code(), 1)])?;
235            thread::sleep(self.key_delay);
236        }
237
238        // Release in reverse order.
239        for key in keys.iter().rev() {
240            self.device
241                .emit(&[InputEvent::new(EventType::KEY, key.code(), 0)])?;
242            thread::sleep(self.key_delay);
243        }
244
245        Ok(())
246    }
247
248    // -----------------------------------------------------------------------
249    // Internal helpers
250    // -----------------------------------------------------------------------
251
252    /// Press or release a modifier key.
253    fn set_modifier(&mut self, modifier: Key, pressed: bool) -> anyhow::Result<()> {
254        let value = if pressed { 1 } else { 0 };
255        self.device
256            .emit(&[InputEvent::new(EventType::KEY, modifier.code(), value)])?;
257        thread::sleep(self.key_delay);
258        Ok(())
259    }
260
261    /// Press and release a single key, with Shift and/or AltGr held as needed.
262    ///
263    /// AltGr is the keycode XKB exposes for `ISO_Level3_Shift`. On some
264    /// X11 layouts that is a dedicated `<LVL3>` keycode rather than
265    /// `KEY_RIGHTALT`; using the XKB keycode keeps third-level characters
266    /// like `é` working with uinput.
267    fn tap_key(&mut self, tap: &KeyTap) -> anyhow::Result<()> {
268        let level3_key = Key::new(self.keymap.level3_keycode());
269
270        if tap.shift {
271            self.set_modifier(Key::KEY_LEFTSHIFT, true)?;
272        }
273        if tap.altgr {
274            self.set_modifier(level3_key, true)?;
275        }
276
277        self.device
278            .emit(&[InputEvent::new(EventType::KEY, tap.keycode, 1)])?;
279        thread::sleep(self.key_delay);
280        self.device
281            .emit(&[InputEvent::new(EventType::KEY, tap.keycode, 0)])?;
282        thread::sleep(self.key_delay);
283
284        if tap.altgr {
285            self.set_modifier(level3_key, false)?;
286        }
287        if tap.shift {
288            self.set_modifier(Key::KEY_LEFTSHIFT, false)?;
289        }
290
291        Ok(())
292    }
293
294    /// Release all modifier keys to prevent interference with injected text.
295    fn release_all_modifiers(&mut self) -> anyhow::Result<()> {
296        let level3_key = Key::new(self.keymap.level3_keycode());
297        let modifiers = [
298            Key::KEY_LEFTSHIFT,
299            Key::KEY_RIGHTSHIFT,
300            Key::KEY_LEFTCTRL,
301            Key::KEY_RIGHTCTRL,
302            Key::KEY_LEFTALT,
303            Key::KEY_RIGHTALT,
304            level3_key,
305            Key::KEY_LEFTMETA,
306            Key::KEY_RIGHTMETA,
307        ];
308
309        for modifier in &modifiers {
310            self.device
311                .emit(&[InputEvent::new(EventType::KEY, modifier.code(), 0)])?;
312        }
313        thread::sleep(self.key_delay);
314
315        Ok(())
316    }
317
318    /// Inject Ctrl+V to paste from clipboard.
319    fn inject_ctrl_v(&mut self) -> anyhow::Result<()> {
320        self.set_modifier(Key::KEY_LEFTCTRL, true)?;
321        self.device
322            .emit(&[InputEvent::new(EventType::KEY, Key::KEY_V.code(), 1)])?;
323        thread::sleep(self.key_delay);
324        self.device
325            .emit(&[InputEvent::new(EventType::KEY, Key::KEY_V.code(), 0)])?;
326        thread::sleep(self.key_delay);
327        self.set_modifier(Key::KEY_LEFTCTRL, false)?;
328        Ok(())
329    }
330
331    /// Set clipboard to `text`, inject Ctrl+V, then restore previous content.
332    fn paste_text(&mut self, text: &str) -> anyhow::Result<()> {
333        // Save current clipboard.
334        let saved = self.clipboard.get_text().ok();
335
336        // Set new clipboard content.
337        self.clipboard
338            .set_text(text)
339            .context("failed to set clipboard for paste")?;
340
341        // Small delay to ensure clipboard is ready.
342        thread::sleep(Duration::from_millis(10));
343
344        // Inject Ctrl+V.
345        self.release_all_modifiers()?;
346        self.inject_ctrl_v()?;
347
348        // Small delay before restoring clipboard.
349        thread::sleep(Duration::from_millis(50));
350
351        // Restore previous clipboard content.
352        if let Some(previous) = saved {
353            if let Err(_e) = self.clipboard.set_text(&previous) {
354                warn!("failed to restore clipboard: {_e}");
355            }
356        }
357
358        Ok(())
359    }
360}