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}