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 to let the kernel register it.
28const DEVICE_SETTLE_DELAY: Duration = Duration::from_millis(200);
29
30/// Virtual keyboard that injects keystrokes via uinput.
31pub struct Keyboard {
32    device: VirtualDevice,
33    keymap: XkbKeymap,
34    clipboard: Box<dyn ClipboardBackend>,
35    key_delay: Duration,
36}
37
38impl Keyboard {
39    /// Create a new virtual keyboard with auto-detected layout and clipboard.
40    ///
41    /// This is the most ergonomic constructor: it picks up the active
42    /// XKB layout from the compositor (Hyprland / Sway / env vars), picks
43    /// the right clipboard backend for the current display server, and
44    /// builds the uinput device.
45    ///
46    /// `key_delay` is the inter-event delay. Raise it for TUIs that drop
47    /// characters in raw mode (e.g. Node/Ink-based apps like Claude Code).
48    ///
49    /// Requires write access to `/dev/uinput`.
50    ///
51    /// For full control over keymap/clipboard, see
52    /// [`Keyboard::with_components`]; to force a specific XKB layout, see
53    /// [`Keyboard::with_layout`].
54    pub fn new(key_delay: Duration) -> anyhow::Result<Self> {
55        let layout = KeyboardLayout::detect();
56        let keymap = XkbKeymap::from_layout(&layout)?;
57        Self::with_components(keymap, default_clipboard(), key_delay)
58    }
59
60    /// Create a virtual keyboard for a specific XKB layout (and optional
61    /// variant), with the auto-detected clipboard backend.
62    ///
63    /// Useful when the application already knows the user's layout and
64    /// wants to skip the compositor probe (e.g. it's stored in a config
65    /// file).
66    pub fn with_layout(
67        layout: &str,
68        variant: Option<&str>,
69        key_delay: Duration,
70    ) -> anyhow::Result<Self> {
71        let detected = KeyboardLayout {
72            layout: layout.to_string(),
73            variant: variant.unwrap_or("").to_string(),
74        };
75        let keymap = XkbKeymap::from_layout(&detected)?;
76        Self::with_components(keymap, default_clipboard(), key_delay)
77    }
78
79    /// Create a virtual keyboard from explicit components (keymap +
80    /// clipboard backend).
81    ///
82    /// Use this when you need to inject a custom [`XkbKeymap`] (e.g. for
83    /// tests) or a non-default clipboard backend
84    /// ([`crate::NoopClipboard`] / [`crate::WaylandClipboard`] /
85    /// [`crate::X11Clipboard`]).
86    ///
87    /// Requires write access to `/dev/uinput` (user must be in the `input`
88    /// group or have the appropriate udev rule installed).
89    ///
90    /// `key_delay` is the inter-event delay. Raise it for TUIs that drop
91    /// characters in raw mode (e.g. Node/Ink-based apps like Claude Code).
92    pub fn with_components(
93        keymap: XkbKeymap,
94        clipboard: Box<dyn ClipboardBackend>,
95        key_delay: Duration,
96    ) -> anyhow::Result<Self> {
97        // Register all key codes we might need (1..=247 covers standard
98        // keys; KEY_MAX on Linux is ~767 but codes beyond 247 are rare).
99        let mut keys = AttributeSet::<Key>::new();
100        for code in 1..=247 {
101            keys.insert(Key::new(code));
102        }
103
104        let device = evdev::uinput::VirtualDeviceBuilder::new()
105            .context("failed to create VirtualDeviceBuilder")?
106            .name("whisrs virtual keyboard")
107            .with_keys(&keys)
108            .context("failed to register key events")?
109            .build()
110            .context("failed to build uinput virtual device — check /dev/uinput permissions")?;
111
112        // Give the kernel time to register the new device.
113        thread::sleep(DEVICE_SETTLE_DELAY);
114
115        debug!("uinput virtual keyboard created");
116
117        Ok(Self {
118            device,
119            keymap,
120            clipboard,
121            key_delay,
122        })
123    }
124
125    // -----------------------------------------------------------------------
126    // Public API
127    // -----------------------------------------------------------------------
128
129    /// Type `text` by injecting keystrokes through the virtual device.
130    ///
131    /// Characters found in the keymap are typed via direct key events
132    /// (with Shift/AltGr modifiers as needed, including dead-key sequences).
133    /// Characters *not* found in the keymap are accumulated and pasted as
134    /// a batch via the clipboard (Ctrl+V), with clipboard save/restore
135    /// so the user's clipboard is not permanently overwritten.
136    pub fn type_text(&mut self, text: &str) -> anyhow::Result<()> {
137        self.release_all_modifiers()?;
138
139        let mut paste_buf = String::new();
140
141        for ch in text.chars() {
142            // Copy the lookup result to release the immutable borrow on
143            // self.keymap before calling mutable methods below.
144            let mapping = self.keymap.lookup(ch).copied();
145            if let Some(mapping) = mapping {
146                // Flush any pending paste buffer first.
147                if !paste_buf.is_empty() {
148                    self.paste_text(&paste_buf)?;
149                    paste_buf.clear();
150                }
151                self.tap_key(&mapping.main)?;
152                if let Some(follow) = mapping.follow.as_ref() {
153                    self.tap_key(follow)?;
154                }
155            } else {
156                // Character not in keymap — accumulate for clipboard paste.
157                paste_buf.push(ch);
158            }
159        }
160
161        // Flush remaining paste buffer.
162        if !paste_buf.is_empty() {
163            self.paste_text(&paste_buf)?;
164        }
165
166        Ok(())
167    }
168
169    /// Emit Backspace `count` times.
170    pub fn backspace(&mut self, count: usize) -> anyhow::Result<()> {
171        self.release_all_modifiers()?;
172
173        for _ in 0..count {
174            self.tap_key(&KeyTap {
175                keycode: Key::KEY_BACKSPACE.code(),
176                shift: false,
177                altgr: false,
178            })?;
179        }
180
181        Ok(())
182    }
183
184    /// Press all keys in `keys`, then release them in reverse order.
185    ///
186    /// Each press and release is separated by `self.key_delay`.
187    pub fn send_combo(&mut self, keys: &[Key]) -> anyhow::Result<()> {
188        // Press all keys in order.
189        for key in keys {
190            self.device
191                .emit(&[InputEvent::new(EventType::KEY, key.code(), 1)])?;
192            thread::sleep(self.key_delay);
193        }
194
195        // Release in reverse order.
196        for key in keys.iter().rev() {
197            self.device
198                .emit(&[InputEvent::new(EventType::KEY, key.code(), 0)])?;
199            thread::sleep(self.key_delay);
200        }
201
202        Ok(())
203    }
204
205    // -----------------------------------------------------------------------
206    // Internal helpers
207    // -----------------------------------------------------------------------
208
209    /// Press or release a modifier key.
210    fn set_modifier(&mut self, modifier: Key, pressed: bool) -> anyhow::Result<()> {
211        let value = if pressed { 1 } else { 0 };
212        self.device
213            .emit(&[InputEvent::new(EventType::KEY, modifier.code(), value)])?;
214        thread::sleep(self.key_delay);
215        Ok(())
216    }
217
218    /// Press and release a single key, with Shift and/or AltGr held as needed.
219    ///
220    /// AltGr is `KEY_RIGHTALT`. Both modifiers can be held together for
221    /// level-3 chars on layouts like `us:intl` (e.g. Shift+AltGr+something
222    /// for less common accented forms).
223    fn tap_key(&mut self, tap: &KeyTap) -> anyhow::Result<()> {
224        if tap.shift {
225            self.set_modifier(Key::KEY_LEFTSHIFT, true)?;
226        }
227        if tap.altgr {
228            self.set_modifier(Key::KEY_RIGHTALT, true)?;
229        }
230
231        self.device
232            .emit(&[InputEvent::new(EventType::KEY, tap.keycode, 1)])?;
233        thread::sleep(self.key_delay);
234        self.device
235            .emit(&[InputEvent::new(EventType::KEY, tap.keycode, 0)])?;
236        thread::sleep(self.key_delay);
237
238        if tap.altgr {
239            self.set_modifier(Key::KEY_RIGHTALT, false)?;
240        }
241        if tap.shift {
242            self.set_modifier(Key::KEY_LEFTSHIFT, false)?;
243        }
244
245        Ok(())
246    }
247
248    /// Release all modifier keys to prevent interference with injected text.
249    fn release_all_modifiers(&mut self) -> anyhow::Result<()> {
250        let modifiers = [
251            Key::KEY_LEFTSHIFT,
252            Key::KEY_RIGHTSHIFT,
253            Key::KEY_LEFTCTRL,
254            Key::KEY_RIGHTCTRL,
255            Key::KEY_LEFTALT,
256            Key::KEY_RIGHTALT,
257            Key::KEY_LEFTMETA,
258            Key::KEY_RIGHTMETA,
259        ];
260
261        for modifier in &modifiers {
262            self.device
263                .emit(&[InputEvent::new(EventType::KEY, modifier.code(), 0)])?;
264        }
265        thread::sleep(self.key_delay);
266
267        Ok(())
268    }
269
270    /// Inject Ctrl+V to paste from clipboard.
271    fn inject_ctrl_v(&mut self) -> anyhow::Result<()> {
272        self.set_modifier(Key::KEY_LEFTCTRL, true)?;
273        self.device
274            .emit(&[InputEvent::new(EventType::KEY, Key::KEY_V.code(), 1)])?;
275        thread::sleep(self.key_delay);
276        self.device
277            .emit(&[InputEvent::new(EventType::KEY, Key::KEY_V.code(), 0)])?;
278        thread::sleep(self.key_delay);
279        self.set_modifier(Key::KEY_LEFTCTRL, false)?;
280        Ok(())
281    }
282
283    /// Set clipboard to `text`, inject Ctrl+V, then restore previous content.
284    fn paste_text(&mut self, text: &str) -> anyhow::Result<()> {
285        // Save current clipboard.
286        let saved = self.clipboard.get_text().ok();
287
288        // Set new clipboard content.
289        self.clipboard
290            .set_text(text)
291            .context("failed to set clipboard for paste")?;
292
293        // Small delay to ensure clipboard is ready.
294        thread::sleep(Duration::from_millis(10));
295
296        // Inject Ctrl+V.
297        self.release_all_modifiers()?;
298        self.inject_ctrl_v()?;
299
300        // Small delay before restoring clipboard.
301        thread::sleep(Duration::from_millis(50));
302
303        // Restore previous clipboard content.
304        if let Some(previous) = saved {
305            if let Err(_e) = self.clipboard.set_text(&previous) {
306                warn!("failed to restore clipboard: {_e}");
307            }
308        }
309
310        Ok(())
311    }
312}