xkb_type/lib.rs
1//! Virtual keyboard typing via evdev/uinput or X11 XTEST with XKB layout-aware
2//! key mapping.
3//!
4//! This crate provides:
5//! - [`Keyboard`] — a virtual uinput device that injects keystrokes (or,
6//! with the `xtest` feature, an X11 XTEST connection).
7//! - [`XkbKeymap`] — reverse char→keycode lookup from the active XKB layout.
8//! - [`ClipboardBackend`] — trait + auto-detected clipboard implementations.
9//!
10//! # Example (auto-detect layout and clipboard)
11//!
12//! ```no_run
13//! use xkb_type::{Keyboard, Key};
14//! use std::time::Duration;
15//!
16//! let mut kb = Keyboard::new(Duration::from_millis(2))?;
17//! kb.type_text("Hello — こんにちは — €100 — 😀")?;
18//! kb.backspace(5)?;
19//! kb.send_combo(&[Key::KEY_LEFTCTRL, Key::KEY_C])?;
20//! # Ok::<(), anyhow::Error>(())
21//! ```
22//!
23//! # Example (explicit layout)
24//!
25//! ```no_run
26//! use xkb_type::Keyboard;
27//! use std::time::Duration;
28//!
29//! let mut kb = Keyboard::with_layout("de", None, Duration::from_millis(5))?;
30//! kb.type_text("Schöne Grüße")?;
31//! # Ok::<(), anyhow::Error>(())
32//! ```
33//!
34//! # Example (pause or stop between keys)
35//!
36//! [`KeyInjector::type_text`] asks a gate before every key, with the byte
37//! offset of the next character. The gate may block (to wait for something)
38//! and can stop the typing; the result says where it stopped.
39//!
40//! ```no_run
41//! use std::ops::ControlFlow;
42//! use std::time::Duration;
43//! use xkb_type::{KeyInjector, Keyboard, Typed};
44//!
45//! let mut kb: Box<dyn KeyInjector> = Box::new(Keyboard::new(Duration::from_millis(2))?);
46//! let text = "hello world";
47//! // Stop before the space.
48//! let mut gate = |at: usize| {
49//! if at < 5 {
50//! ControlFlow::Continue(())
51//! } else {
52//! ControlFlow::Break(())
53//! }
54//! };
55//! match kb.type_text(text, &mut gate)? {
56//! Typed::All => println!("typed everything"),
57//! Typed::StoppedAt(at) => println!("not typed: {:?}", &text[at..]),
58//! }
59//! # Ok::<(), anyhow::Error>(())
60//! ```
61
62pub mod clipboard;
63pub mod keyboard;
64pub mod keymap;
65#[cfg(feature = "wayland-vk")]
66pub mod wayland_vk;
67#[cfg(feature = "xtest")]
68mod xtest;
69
70use std::ops::ControlFlow;
71
72// ---------------------------------------------------------------------------
73// Core types
74// ---------------------------------------------------------------------------
75
76/// A single keypress with optional Shift and/or AltGr modifiers.
77#[derive(Debug, Clone, Copy)]
78pub struct KeyTap {
79 pub keycode: u16,
80 pub shift: bool,
81 pub altgr: bool,
82}
83
84/// How far a [`KeyInjector::type_text`] call got.
85#[must_use]
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum Typed {
88 /// Every character of the text was sent.
89 All,
90 /// The gate stopped the typing before the character at this byte offset
91 /// of the text: `text[..offset]` was sent, `text[offset..]` was not.
92 /// Always a char boundary of the text.
93 StoppedAt(usize),
94}
95
96/// Send `steps` in order, asking `gate` with each step's byte offset first.
97/// Stops before the first step the gate breaks on and returns
98/// [`Typed::StoppedAt`] its offset; an error from `send` ends the run at once.
99/// Shared by the backends so every one gates the same way.
100pub(crate) fn run_gated<S>(
101 steps: impl IntoIterator<Item = (usize, S)>,
102 gate: &mut dyn FnMut(usize) -> ControlFlow<()>,
103 mut send: impl FnMut(S) -> anyhow::Result<()>,
104) -> anyhow::Result<Typed> {
105 for (at, step) in steps {
106 if gate(at).is_break() {
107 return Ok(Typed::StoppedAt(at));
108 }
109 send(step)?;
110 }
111 Ok(Typed::All)
112}
113
114/// Information needed to produce a character at the cursor.
115///
116/// For most characters this is a single [`KeyTap`]. For characters that
117/// XKB only exposes as a dead-key combination (e.g. `ã` = `dead_tilde + a`
118/// on `us:intl`, or `'` = `dead_acute + space`), a `follow` tap is
119/// recorded so the typer emits the dead-key keypress followed by the
120/// base-letter (or space) keypress in sequence.
121#[derive(Debug, Clone, Copy)]
122pub struct KeyMapping {
123 pub main: KeyTap,
124 pub follow: Option<KeyTap>,
125}
126
127// ---------------------------------------------------------------------------
128// KeyInjector trait
129// ---------------------------------------------------------------------------
130
131/// High-level text-injection abstraction.
132///
133/// This unifies the available keystroke backends behind a single trait so a
134/// caller can pick an implementation at runtime (e.g. the layout-independent
135/// Wayland [`wayland_vk::WaylandVkKeyboard`] when the compositor supports
136/// `zwp_virtual_keyboard_v1`, falling back to the uinput [`Keyboard`]
137/// otherwise) without changing the typing code.
138///
139/// The semantics mirror [`Keyboard`]'s inherent methods, except that
140/// [`type_text`](Self::type_text) takes a gate.
141pub trait KeyInjector: Send {
142 /// Type `text` by injecting the corresponding keystrokes, asking `gate`
143 /// before each one.
144 ///
145 /// `gate` gets the byte offset in `text` of the next character to send.
146 /// It is asked before every key tap, and once before each clipboard
147 /// paste of a run of characters the keymap lacks (with the offset of the
148 /// run's first character). A character typed as two taps (a dead key and
149 /// its base letter) is asked about once and never split. A character the
150 /// backend cannot type at all is skipped without asking.
151 ///
152 /// [`ControlFlow::Continue`] sends the character. [`ControlFlow::Break`]
153 /// stops before it: what was already sent is flushed, and the result is
154 /// [`Typed::StoppedAt`] that offset. The gate may block, for example
155 /// until the user lets go of a modifier key. Pass
156 /// `&mut |_| ControlFlow::Continue(())` to type everything.
157 fn type_text(
158 &mut self,
159 text: &str,
160 gate: &mut dyn FnMut(usize) -> ControlFlow<()>,
161 ) -> anyhow::Result<Typed>;
162
163 /// Emit Backspace `count` times.
164 fn backspace(&mut self, count: usize) -> anyhow::Result<()>;
165
166 /// Press all keys in `keys`, then release them in reverse order.
167 fn send_combo(&mut self, keys: &[evdev::Key]) -> anyhow::Result<()>;
168
169 /// Set the inter-event delay used between injected key events.
170 fn set_key_delay(&mut self, delay: std::time::Duration);
171}
172
173impl KeyInjector for Keyboard {
174 fn type_text(
175 &mut self,
176 text: &str,
177 gate: &mut dyn FnMut(usize) -> ControlFlow<()>,
178 ) -> anyhow::Result<Typed> {
179 Keyboard::type_text_gated(self, text, gate)
180 }
181
182 fn backspace(&mut self, count: usize) -> anyhow::Result<()> {
183 Keyboard::backspace(self, count)
184 }
185
186 fn send_combo(&mut self, keys: &[evdev::Key]) -> anyhow::Result<()> {
187 Keyboard::send_combo(self, keys)
188 }
189
190 fn set_key_delay(&mut self, delay: std::time::Duration) {
191 Keyboard::set_key_delay(self, delay)
192 }
193}
194
195// ---------------------------------------------------------------------------
196// ClipboardBackend trait
197// ---------------------------------------------------------------------------
198
199/// Trait for clipboard get/set operations.
200pub trait ClipboardBackend: Send + Sync {
201 /// Read the current clipboard text content.
202 ///
203 /// Returns `Ok("")` when the clipboard is empty (nothing owns it) and
204 /// `Err` when it holds content that cannot be read as text, so a caller
205 /// that saves and restores the clipboard never mistakes non-text content
206 /// for empty and overwrites it with `""`.
207 fn get_text(&self) -> anyhow::Result<String>;
208
209 /// Set the clipboard to the given text.
210 fn set_text(&self, text: &str) -> anyhow::Result<()>;
211
212 /// Read the primary selection (highlighted text, no Ctrl+C needed).
213 fn get_primary_selection(&self) -> anyhow::Result<String>;
214}
215
216// ---------------------------------------------------------------------------
217// Re-exports
218// ---------------------------------------------------------------------------
219
220pub use clipboard::{default_clipboard, NoopClipboard, WaylandClipboard, X11Clipboard};
221pub use keyboard::Keyboard;
222pub use keymap::{KeyboardLayout, XkbKeymap};
223
224// Re-export evdev's `Key` enum (and the full `evdev` module) so callers
225// of [`Keyboard::send_combo`] don't need a direct `evdev` dependency.
226pub use evdev;
227pub use evdev::Key;
228
229/// Convenience: build a [`Keyboard`] from the detected layout with a
230/// sensible default key delay (5 ms).
231///
232/// Returns an error if the XKB keymap cannot be built (missing locale data)
233/// or if `/dev/uinput` is not writable.
234///
235/// Equivalent to `Keyboard::new(Duration::from_millis(5))`.
236pub fn keyboard_from_detected_layout() -> anyhow::Result<Keyboard> {
237 Keyboard::new(std::time::Duration::from_millis(5))
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 /// Run `steps` (offsets) through [`run_gated`] with a gate that breaks
245 /// at `break_at` (an offset), returning the result, the offsets the gate
246 /// saw and the steps sent.
247 fn run(steps: &[usize], break_at: Option<usize>) -> (Typed, Vec<usize>, Vec<usize>) {
248 let mut asked = Vec::new();
249 let mut sent = Vec::new();
250 let mut gate = |at: usize| {
251 asked.push(at);
252 if Some(at) == break_at {
253 ControlFlow::Break(())
254 } else {
255 ControlFlow::Continue(())
256 }
257 };
258 let typed = run_gated(steps.iter().map(|&at| (at, at)), &mut gate, |step| {
259 sent.push(step);
260 Ok(())
261 })
262 .unwrap();
263 (typed, asked, sent)
264 }
265
266 #[test]
267 fn run_gated_asks_before_every_step_and_sends_them_all() {
268 let (typed, asked, sent) = run(&[0, 1, 3, 7], None);
269 assert_eq!(typed, Typed::All);
270 assert_eq!(asked, vec![0, 1, 3, 7]);
271 assert_eq!(sent, vec![0, 1, 3, 7]);
272 }
273
274 #[test]
275 fn run_gated_stops_before_the_step_the_gate_breaks_on() {
276 let (typed, asked, sent) = run(&[0, 1, 3, 7], Some(3));
277 assert_eq!(typed, Typed::StoppedAt(3));
278 assert_eq!(asked, vec![0, 1, 3], "nothing is asked after the break");
279 assert_eq!(sent, vec![0, 1], "the step it broke on is not sent");
280
281 let (typed, _, sent) = run(&[0, 1], Some(0));
282 assert_eq!(typed, Typed::StoppedAt(0));
283 assert!(sent.is_empty());
284 }
285
286 #[test]
287 fn run_gated_with_no_steps_is_all_without_asking() {
288 let (typed, asked, sent) = run(&[], Some(0));
289 assert_eq!(typed, Typed::All);
290 assert!(asked.is_empty() && sent.is_empty());
291 }
292
293 #[test]
294 fn run_gated_ends_at_a_send_error() {
295 let mut asked = 0;
296 let mut gate = |_: usize| {
297 asked += 1;
298 ControlFlow::Continue(())
299 };
300 let result = run_gated([(0, ()), (1, ()), (2, ())], &mut gate, |()| {
301 anyhow::bail!("device gone")
302 });
303 assert!(result.is_err());
304 assert_eq!(asked, 1, "no step is asked after a failed send");
305 }
306}