Skip to main content

xa11y_linux/
input.rs

1//! Linux input-simulation backend.
2//!
3//! Two implementations live behind a single [`LinuxInputProvider`] facade,
4//! chosen at construction time based on the session environment:
5//!
6//! - **X11** — drives the XTest extension over `x11rb`.
7//!   Zero setup required; matches what existing X11 users already have.
8//! - **Wayland / uinput** (anything else) — opens the kernel's
9//!   `/dev/uinput` device and registers a virtual evdev keyboard+pointer.
10//!   Compositor-agnostic — works on every Linux desktop (GNOME, KDE,
11//!   sway, Hyprland, Cosmic, weston) plus headless setups. Same mechanism
12//!   `xdotool --using-uinput`, `ydotool`, `wtype`, Steam, and Wine use.
13//!   Requires `input` group membership; surfaces an actionable
14//!   `Error::PermissionDenied` if the user isn't in the group. See
15//!   [`crate::wayland_input`].
16//!
17//! Routing recognizes that a Wayland desktop normally exports `DISPLAY` for
18//! XWayland. Native Wayland wins in a mixed session; use
19//! `XA11Y_LINUX_INPUT_BACKEND=x11|wayland` to override it. There is no
20//! compile-time feature flag; both backends are always built on Linux.
21//!
22//! Key mapping on X11 goes keysym → keycode via `GetKeyboardMapping`, queried
23//! once at connect time. `Key::Char` for printable ASCII uses the codepoint
24//! as its keysym; [`Keyboard::type_text`](xa11y_core::input::Keyboard::type_text)
25//! looks up each character in the same keymap table and holds Shift when it
26//! appears in the shifted column. The Wayland backend does the same dance
27//! against an xkb keymap built from system defaults.
28
29use std::sync::{Mutex, RwLock};
30
31use x11rb::connection::Connection;
32use x11rb::protocol::xproto::{
33    ConnectionExt as _, GetKeyboardMappingReply, Keycode, Screen, Window, BUTTON_PRESS_EVENT,
34    BUTTON_RELEASE_EVENT, KEY_PRESS_EVENT, KEY_RELEASE_EVENT, MOTION_NOTIFY_EVENT,
35};
36use x11rb::protocol::xtest::ConnectionExt as _;
37use x11rb::rust_connection::RustConnection;
38
39use xa11y_core::input::{InputProvider, Key, MouseButton, Point, ScrollDelta};
40use xa11y_core::{Error, Result};
41
42use crate::session::{select_backend, DesktopBackend};
43use crate::wayland_input::WaylandInputBackend;
44
45// X11 keysyms — lifted from /usr/include/X11/keysymdef.h. Only the keysyms
46// named by [`Key`] plus the modifiers we need to hold for shifted text.
47// Shared with the Wayland backend via `key_to_keysym`.
48pub(crate) const XK_SHIFT_L: u32 = 0xffe1;
49pub(crate) const XK_CONTROL_L: u32 = 0xffe3;
50pub(crate) const XK_ALT_L: u32 = 0xffe9;
51pub(crate) const XK_SUPER_L: u32 = 0xffeb;
52const XK_RETURN: u32 = 0xff0d;
53const XK_ESCAPE: u32 = 0xff1b;
54const XK_BACKSPACE: u32 = 0xff08;
55const XK_TAB: u32 = 0xff09;
56const XK_DELETE: u32 = 0xffff;
57const XK_INSERT: u32 = 0xff63;
58const XK_UP: u32 = 0xff52;
59const XK_DOWN: u32 = 0xff54;
60const XK_LEFT: u32 = 0xff51;
61const XK_RIGHT: u32 = 0xff53;
62const XK_HOME: u32 = 0xff50;
63const XK_END: u32 = 0xff57;
64const XK_PAGE_UP: u32 = 0xff55;
65const XK_PAGE_DOWN: u32 = 0xff56;
66const XK_F1: u32 = 0xffbe;
67// XK_F24 = 0xffd5; 1..=24 is contiguous starting at XK_F1.
68
69/// Resolve a [`Key`] to its X11 keysym.
70///
71/// Both the X11 and Wayland backends translate `Key` → keysym via this
72/// function. The Wayland path then looks the keysym up in an xkb keymap to
73/// get an evdev keycode; the X11 path looks it up in `GetKeyboardMapping`.
74pub(crate) fn key_to_keysym(key: &Key) -> Result<u32> {
75    let keysym = match key {
76        Key::Shift => XK_SHIFT_L,
77        Key::Ctrl => XK_CONTROL_L,
78        Key::Alt => XK_ALT_L,
79        Key::Meta => XK_SUPER_L,
80        Key::Enter => XK_RETURN,
81        Key::Escape => XK_ESCAPE,
82        Key::Backspace => XK_BACKSPACE,
83        Key::Tab => XK_TAB,
84        Key::Space => 0x0020,
85        Key::Delete => XK_DELETE,
86        Key::Insert => XK_INSERT,
87        Key::ArrowUp => XK_UP,
88        Key::ArrowDown => XK_DOWN,
89        Key::ArrowLeft => XK_LEFT,
90        Key::ArrowRight => XK_RIGHT,
91        Key::Home => XK_HOME,
92        Key::End => XK_END,
93        Key::PageUp => XK_PAGE_UP,
94        Key::PageDown => XK_PAGE_DOWN,
95        Key::F(n) => {
96            if *n < 1 || *n > 24 {
97                return Err(Error::InvalidActionData {
98                    message: format!("F{n} is out of range (1..=24)"),
99                });
100            }
101            XK_F1 + (*n as u32 - 1)
102        }
103        Key::Char(c) => char_keysym(*c),
104    };
105    Ok(keysym)
106}
107
108/// Convert a character to its X11 keysym. ASCII 0x20..=0x7e maps directly;
109/// everything above uses the Unicode plane (`0x01000000 | codepoint`).
110pub(crate) fn char_keysym(c: char) -> u32 {
111    let cp = c as u32;
112    if (0x20..=0x7e).contains(&cp) {
113        cp
114    } else {
115        0x0100_0000 | cp
116    }
117}
118
119/// Keymap snapshot built from `GetKeyboardMapping`. For each keysym we care
120/// about we store the keycode plus whether it lives in the shifted column
121/// (column 1) rather than the unshifted column (column 0).
122struct Keymap {
123    min_keycode: u8,
124    syms_per_code: u8,
125    syms: Vec<u32>,
126}
127
128impl Keymap {
129    fn from_reply(reply: GetKeyboardMappingReply, min_keycode: u8) -> Self {
130        Self {
131            min_keycode,
132            syms_per_code: reply.keysyms_per_keycode,
133            syms: reply.keysyms,
134        }
135    }
136
137    /// Locate a keysym in the map. Returns `(keycode, needs_shift)` — the
138    /// caller must hold Shift iff `needs_shift` is true.
139    fn lookup(&self, keysym: u32) -> Option<(Keycode, bool)> {
140        let per = self.syms_per_code as usize;
141        if per == 0 {
142            return None;
143        }
144        for (code_index, chunk) in self.syms.chunks(per).enumerate() {
145            // Column 0 is the unshifted level, column 1 the shifted level.
146            // Some keys repeat the column-0 keysym into column 1 when there
147            // is no shifted binding; that's fine — either lookup succeeds.
148            if chunk.first() == Some(&keysym) {
149                return Some((self.min_keycode + code_index as u8, false));
150            }
151            if per >= 2 && chunk.get(1) == Some(&keysym) {
152                return Some((self.min_keycode + code_index as u8, true));
153            }
154        }
155        None
156    }
157}
158
159/// XTest-backed backend. Holds the X server connection and a keymap snapshot.
160pub(crate) struct X11InputBackend {
161    /// Connection is `!Sync`; guard all server traffic with the mutex.
162    conn: Mutex<RustConnection>,
163    root: Window,
164    keymap: RwLock<Keymap>,
165}
166
167impl X11InputBackend {
168    fn new() -> Result<Self> {
169        let (conn, screen_num) = RustConnection::connect(None).map_err(platform)?;
170        let setup = conn.setup().clone();
171        let screen: &Screen = setup
172            .roots
173            .get(screen_num)
174            .ok_or_else(|| platform_msg("X server reported no screens"))?;
175        let root = screen.root;
176
177        let min_keycode = setup.min_keycode;
178        let max_keycode = setup.max_keycode;
179        if max_keycode < min_keycode {
180            return Err(platform_msg("X server reported an empty keycode range"));
181        }
182        let count = max_keycode - min_keycode + 1;
183        let reply = conn
184            .get_keyboard_mapping(min_keycode, count)
185            .map_err(platform)?
186            .reply()
187            .map_err(platform)?;
188        let keymap = Keymap::from_reply(reply, min_keycode);
189
190        Ok(Self {
191            conn: Mutex::new(conn),
192            root,
193            keymap: RwLock::new(keymap),
194        })
195    }
196
197    fn with_conn<F, R>(&self, f: F) -> Result<R>
198    where
199        F: FnOnce(&RustConnection) -> Result<R>,
200    {
201        let guard = self.conn.lock().unwrap_or_else(|e| e.into_inner());
202        f(&guard)
203    }
204
205    fn send(&self, type_: u8, detail: u8, x: i16, y: i16) -> Result<()> {
206        self.with_conn(|conn| {
207            conn.xtest_fake_input(type_, detail, 0, self.root, x, y, 0)
208                .map_err(platform)?
209                .check()
210                .map_err(platform)?;
211            conn.flush().map_err(platform)?;
212            Ok(())
213        })
214    }
215
216    fn key_event(&self, keysym: u32, press: bool) -> Result<()> {
217        let (keycode, _shift) = {
218            let map = self.keymap.read().unwrap_or_else(|e| e.into_inner());
219            map.lookup(keysym).ok_or_else(|| Error::Unsupported {
220                feature: format!("keysym 0x{keysym:04x} has no keycode in the current X layout"),
221            })?
222        };
223        let type_ = if press {
224            KEY_PRESS_EVENT
225        } else {
226            KEY_RELEASE_EVENT
227        };
228        self.send(type_, keycode, 0, 0)
229    }
230
231    fn button_event(&self, button: u8, press: bool) -> Result<()> {
232        let type_ = if press {
233            BUTTON_PRESS_EVENT
234        } else {
235            BUTTON_RELEASE_EVENT
236        };
237        self.send(type_, button, 0, 0)
238    }
239
240    fn pointer_move(&self, to: Point) -> Result<()> {
241        // detail=0 on MOTION_NOTIFY means absolute coordinates relative to root.
242        self.send(MOTION_NOTIFY_EVENT, 0, clamp_coord(to.x), clamp_coord(to.y))
243    }
244
245    fn pointer_down(&self, button: MouseButton) -> Result<()> {
246        self.button_event(button_number(button), true)
247    }
248
249    fn pointer_up(&self, button: MouseButton) -> Result<()> {
250        self.button_event(button_number(button), false)
251    }
252
253    fn pointer_click(&self, at: Point, button: MouseButton, count: u32) -> Result<()> {
254        if count == 0 {
255            return Ok(());
256        }
257        self.pointer_move(at)?;
258        let btn = button_number(button);
259        for _ in 0..count {
260            self.button_event(btn, true)?;
261            self.button_event(btn, false)?;
262        }
263        Ok(())
264    }
265
266    fn pointer_scroll(&self, at: Point, delta: ScrollDelta) -> Result<()> {
267        self.pointer_move(at)?;
268        // X11 scroll-wheel convention: button 4 = up, 5 = down, 6 = left, 7 = right.
269        // The input-sim contract: positive dy scrolls content down (viewport
270        // up), which is the "wheel rolled toward the user" direction — that's
271        // button 5 on X11.
272        for _ in 0..delta.dy.abs() {
273            let btn = if delta.dy > 0 { 5 } else { 4 };
274            self.button_event(btn, true)?;
275            self.button_event(btn, false)?;
276        }
277        for _ in 0..delta.dx.abs() {
278            let btn = if delta.dx > 0 { 7 } else { 6 };
279            self.button_event(btn, true)?;
280            self.button_event(btn, false)?;
281        }
282        Ok(())
283    }
284
285    fn key_down(&self, key: &Key) -> Result<()> {
286        let keysym = key_to_keysym(key)?;
287        self.key_event(keysym, true)
288    }
289
290    fn key_up(&self, key: &Key) -> Result<()> {
291        let keysym = key_to_keysym(key)?;
292        self.key_event(keysym, false)
293    }
294
295    fn type_text(&self, text: &str) -> Result<()> {
296        // For each character, look up its keysym; if the keymap has it in the
297        // shifted column, hold Shift for that press. This is the X11 analogue
298        // of the KEYEVENTF_UNICODE path on Windows — both aim to be robust
299        // against the active keyboard layout.
300        for c in text.chars() {
301            let keysym = char_keysym(c);
302            let (keycode, needs_shift) = {
303                let map = self.keymap.read().unwrap_or_else(|e| e.into_inner());
304                match map.lookup(keysym) {
305                    Some(v) => v,
306                    None => {
307                        return Err(Error::Unsupported {
308                            feature: format!(
309                                "character '{c}' (keysym 0x{keysym:04x}) has no keycode \
310                                 in the current X keyboard layout"
311                            ),
312                        });
313                    }
314                }
315            };
316            if needs_shift {
317                self.key_event(XK_SHIFT_L, true)?;
318            }
319            self.send(KEY_PRESS_EVENT, keycode, 0, 0)?;
320            self.send(KEY_RELEASE_EVENT, keycode, 0, 0)?;
321            if needs_shift {
322                self.key_event(XK_SHIFT_L, false)?;
323            }
324        }
325        Ok(())
326    }
327}
328
329/// Public input provider for Linux. Dispatches to the X11 or Wayland backend
330/// chosen at construction.
331pub struct LinuxInputProvider {
332    backend: InputBackend,
333}
334
335enum InputBackend {
336    X11(Box<X11InputBackend>),
337    Wayland(Box<WaylandInputBackend>),
338}
339
340impl LinuxInputProvider {
341    /// Choose a backend based on the session environment.
342    ///
343    /// - X11 session → X11 (XTest).
344    /// - Wayland or mixed session → uinput.
345    /// - `XA11Y_LINUX_INPUT_BACKEND=x11|wayland` overrides auto detection.
346    ///   May return [`Error::PermissionDenied`] if the user isn't in the
347    ///   `input` group, or [`Error::Unsupported`] if the kernel `uinput`
348    ///   module isn't loaded.
349    pub fn new() -> Result<Self> {
350        match select_backend("XA11Y_LINUX_INPUT_BACKEND", "input simulation")? {
351            DesktopBackend::X11 => {
352                let x11 = X11InputBackend::new()?;
353                Ok(Self {
354                    backend: InputBackend::X11(Box::new(x11)),
355                })
356            }
357            DesktopBackend::Wayland => {
358                let wl = WaylandInputBackend::new()?;
359                Ok(Self {
360                    backend: InputBackend::Wayland(Box::new(wl)),
361                })
362            }
363        }
364    }
365}
366
367impl LinuxInputProvider {
368    /// Convert a logical target point (the cross-platform contract, same space
369    /// as `Element::bounds`) to the physical device pixels both backends
370    /// expect: XTest warps to physical root coordinates, and the uinput
371    /// virtual range is mapped against the physical screen size. No-op at
372    /// scale 1.0. See `crate::scale`.
373    fn to_physical(&self, p: Point) -> Point {
374        p.to_physical(crate::scale::coordinate_scale())
375    }
376}
377
378impl InputProvider for LinuxInputProvider {
379    fn pointer_move(&self, to: Point) -> Result<()> {
380        let to = self.to_physical(to);
381        match &self.backend {
382            InputBackend::X11(x) => x.pointer_move(to),
383            InputBackend::Wayland(w) => w.pointer_move(to),
384        }
385    }
386
387    fn pointer_down(&self, button: MouseButton) -> Result<()> {
388        match &self.backend {
389            InputBackend::X11(x) => x.pointer_down(button),
390            InputBackend::Wayland(w) => w.pointer_down(button),
391        }
392    }
393
394    fn pointer_up(&self, button: MouseButton) -> Result<()> {
395        match &self.backend {
396            InputBackend::X11(x) => x.pointer_up(button),
397            InputBackend::Wayland(w) => w.pointer_up(button),
398        }
399    }
400
401    fn pointer_click(&self, at: Point, button: MouseButton, count: u32) -> Result<()> {
402        let at = self.to_physical(at);
403        match &self.backend {
404            InputBackend::X11(x) => x.pointer_click(at, button, count),
405            InputBackend::Wayland(w) => w.pointer_click(at, button, count),
406        }
407    }
408
409    fn pointer_scroll(&self, at: Point, delta: ScrollDelta) -> Result<()> {
410        let at = self.to_physical(at);
411        match &self.backend {
412            InputBackend::X11(x) => x.pointer_scroll(at, delta),
413            InputBackend::Wayland(w) => w.pointer_scroll(at, delta),
414        }
415    }
416
417    fn key_down(&self, key: &Key) -> Result<()> {
418        match &self.backend {
419            InputBackend::X11(x) => x.key_down(key),
420            InputBackend::Wayland(w) => w.key_down(key),
421        }
422    }
423
424    fn key_up(&self, key: &Key) -> Result<()> {
425        match &self.backend {
426            InputBackend::X11(x) => x.key_up(key),
427            InputBackend::Wayland(w) => w.key_up(key),
428        }
429    }
430
431    fn type_text(&self, text: &str) -> Result<()> {
432        match &self.backend {
433            InputBackend::X11(x) => x.type_text(text),
434            InputBackend::Wayland(w) => w.type_text(text),
435        }
436    }
437}
438
439pub(crate) fn platform<E: std::fmt::Display>(e: E) -> Error {
440    Error::Platform {
441        code: -1,
442        message: e.to_string(),
443    }
444}
445
446pub(crate) fn platform_msg(msg: &str) -> Error {
447    Error::Platform {
448        code: -1,
449        message: msg.to_string(),
450    }
451}
452
453/// Clamp an `i32` screen coordinate into the `i16` range that XTest takes.
454/// X11 coordinates are 16-bit; this matches how libX11 itself narrows them.
455fn clamp_coord(v: i32) -> i16 {
456    v.clamp(i16::MIN as i32, i16::MAX as i32) as i16
457}
458
459fn button_number(button: MouseButton) -> u8 {
460    match button {
461        MouseButton::Left => 1,
462        MouseButton::Middle => 2,
463        MouseButton::Right => 3,
464    }
465}