Skip to main content

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}