//! Input injected on Linux through the kernel's user-level input device (WAI remote-desktop //! §5.4). allow-name: OS API names throughout this file //! //! One virtual device with an absolute pointer the size of the captured frame, its buttons and //! wheels, and the keyboard keys of the HID usages this crate maps. The kernel takes no text, so //! composed text is not injected, nor touch; clipboard text goes through the display's clipboard //! tool when one is installed. The screen statement's `accepts` says which (§3.3). use crate::inject::{Injector, Mapping}; use std::os::fd::{AsRawFd, OwnedFd}; use wai::remote_desktop::{accepts_bit, action_type, Action}; const EV_SYN: u16 = 0; const EV_KEY: u16 = 1; const EV_REL: u16 = 2; const EV_ABS: u16 = 3; const SYN_REPORT: u16 = 0; const REL_HWHEEL: u16 = 6; const REL_WHEEL: u16 = 8; const ABS_X: u16 = 0; const ABS_Y: u16 = 1; const BTN: [u16; 5] = [0x110, 0x112, 0x111, 0x113, 0x114]; // primary, auxiliary, secondary, fourth, fifth const UI_SET_EVBIT: libc::c_ulong = 0x4004_5564; const UI_SET_KEYBIT: libc::c_ulong = 0x4004_5565; const UI_SET_RELBIT: libc::c_ulong = 0x4004_5566; const UI_SET_ABSBIT: libc::c_ulong = 0x4004_5567; const UI_DEV_CREATE: libc::c_ulong = 0x5501; const UI_DEV_DESTROY: libc::c_ulong = 0x5502; /// The kernel's key code of each HID usage of the keyboard page. const KEYS: &[(u16, u16)] = &[ (0x04, 30), (0x05, 48), (0x06, 46), (0x07, 32), (0x08, 18), (0x09, 33), (0x0A, 34), (0x0B, 35), (0x0C, 23), (0x0D, 36), (0x0E, 37), (0x0F, 38), (0x10, 50), (0x11, 49), (0x12, 24), (0x13, 25), (0x14, 16), (0x15, 19), (0x16, 31), (0x17, 20), (0x18, 22), (0x19, 47), (0x1A, 17), (0x1B, 45), (0x1C, 21), (0x1D, 44), (0x1E, 2), (0x1F, 3), (0x20, 4), (0x21, 5), (0x22, 6), (0x23, 7), (0x24, 8), (0x25, 9), (0x26, 10), (0x27, 11), (0x28, 28), (0x29, 1), (0x2A, 14), (0x2B, 15), (0x2C, 57), (0x2D, 12), (0x2E, 13), (0x2F, 26), (0x30, 27), (0x31, 43), (0x33, 39), (0x34, 40), (0x35, 41), (0x36, 51), (0x37, 52), (0x38, 53), (0x39, 58), (0x3A, 59), (0x3B, 60), (0x3C, 61), (0x3D, 62), (0x3E, 63), (0x3F, 64), (0x40, 65), (0x41, 66), (0x42, 67), (0x43, 68), (0x44, 87), (0x45, 88), (0x49, 110), (0x4A, 102), (0x4B, 104), (0x4C, 111), (0x4D, 107), (0x4E, 109), (0x4F, 106), (0x50, 105), (0x51, 108), (0x52, 103), (0xE0, 29), (0xE1, 42), (0xE2, 56), (0xE3, 125), (0xE4, 97), (0xE5, 54), (0xE6, 100), (0xE7, 126), ]; #[repr(C)] struct InputId { bustype: u16, vendor: u16, product: u16, version: u16, } #[repr(C)] struct UserDev { name: [u8; 80], id: InputId, ff_effects_max: u32, absmax: [i32; 64], absmin: [i32; 64], absfuzz: [i32; 64], absflat: [i32; 64], } #[repr(C)] struct InputEvent { sec: libc::time_t, usec: libc::suseconds_t, kind: u16, code: u16, value: i32, } pub struct UinputInjector { fd: OwnedFd, keys_down: Vec, buttons_down: Vec, clipboard: Option<&'static str>, } fn clipboard_tool() -> Option<&'static str> { ["/usr/bin/wl-copy", "/usr/bin/xclip"].into_iter().find(|p| std::path::Path::new(p).exists()) } impl UinputInjector { /// A device whose pointer spans `width` × `height`. pub fn new(width: u32, height: u32) -> Result { let path = c"/dev/uinput"; let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_NONBLOCK) }; if fd < 0 { return Err(format!("/dev/uinput could not be opened: {} (the host needs write access to it)", std::io::Error::last_os_error())); } let fd = unsafe { ::from_raw_fd(fd) }; let raw = fd.as_raw_fd(); let io = |req: libc::c_ulong, v: libc::c_int| -> Result<(), String> { if unsafe { libc::ioctl(raw, req as _, v) } < 0 { Err(format!("an input device ioctl: {}", std::io::Error::last_os_error())) } else { Ok(()) } }; for ev in [EV_KEY, EV_REL, EV_ABS, EV_SYN] { io(UI_SET_EVBIT, ev as i32)?; } for b in BTN { io(UI_SET_KEYBIT, b as i32)?; } for (_, k) in KEYS { io(UI_SET_KEYBIT, *k as i32)?; } io(UI_SET_RELBIT, REL_WHEEL as i32)?; io(UI_SET_RELBIT, REL_HWHEEL as i32)?; io(UI_SET_ABSBIT, ABS_X as i32)?; io(UI_SET_ABSBIT, ABS_Y as i32)?; let mut dev: UserDev = unsafe { std::mem::zeroed() }; dev.name[..15].copy_from_slice(b"wai-remote-host"); dev.id = InputId { bustype: 0x06, vendor: 0, product: 0, version: 1 }; dev.absmax[ABS_X as usize] = width.saturating_sub(1) as i32; dev.absmax[ABS_Y as usize] = height.saturating_sub(1) as i32; let b = unsafe { std::slice::from_raw_parts(&dev as *const UserDev as *const u8, std::mem::size_of::()) }; if unsafe { libc::write(raw, b.as_ptr() as *const libc::c_void, b.len()) } != b.len() as isize { return Err("the input device could not be described".into()); } if unsafe { libc::ioctl(raw, UI_DEV_CREATE as _) } < 0 { return Err(format!("the input device could not be made: {}", std::io::Error::last_os_error())); } Ok(UinputInjector { fd, keys_down: Vec::new(), buttons_down: Vec::new(), clipboard: clipboard_tool() }) } fn emit(&self, kind: u16, code: u16, value: i32) -> Result<(), String> { let e = InputEvent { sec: 0, usec: 0, kind, code, value }; let n = std::mem::size_of::(); if unsafe { libc::write(self.fd.as_raw_fd(), &e as *const InputEvent as *const libc::c_void, n) } != n as isize { return Err(format!("an input event: {}", std::io::Error::last_os_error())); } Ok(()) } fn sync(&self) -> Result<(), String> { self.emit(EV_SYN, SYN_REPORT, 0) } } impl Drop for UinputInjector { fn drop(&mut self) { unsafe { libc::ioctl(self.fd.as_raw_fd(), UI_DEV_DESTROY as _); } } } impl Injector for UinputInjector { fn accepts(&self) -> u8 { let mut a = accepts_bit(action_type::MOVE) | accepts_bit(action_type::BUTTON) | accepts_bit(action_type::WHEEL) | accepts_bit(action_type::KEY); if self.clipboard.is_some() { a |= accepts_bit(action_type::CLIPBOARD); } a } fn inject(&mut self, a: &Action, m: &Mapping) -> Result<(), String> { if let Some((x, y)) = a.at() { let (tx, ty) = m.map(x, y); self.emit(EV_ABS, ABS_X, tx)?; self.emit(EV_ABS, ABS_Y, ty)?; } match a { Action::Move { .. } => {} Action::Button { button, down, .. } => { let code = BTN[*button as usize]; self.emit(EV_KEY, code, *down as i32)?; if *down { self.buttons_down.push(code); } else { self.buttons_down.retain(|c| *c != code); } } Action::Wheel { dx, dy, .. } => { // One detent each 120 pixels, the unit a wheel event in pixels is counted in. if *dy != 0 { self.emit(EV_REL, REL_WHEEL, -(*dy as i32) / 120)?; } if *dx != 0 { self.emit(EV_REL, REL_HWHEEL, *dx as i32 / 120)?; } } Action::Key { usage, down, .. } => { let Some(&(_, k)) = KEYS.iter().find(|(u, _)| u == usage) else { return Ok(()) }; self.emit(EV_KEY, k, *down as i32)?; if *down { self.keys_down.push(k); } else { self.keys_down.retain(|c| *c != k); } } Action::Clipboard(s) => { use std::io::Write; let tool = self.clipboard.ok_or("no clipboard tool")?; let mut c = std::process::Command::new(tool); if tool.ends_with("xclip") { c.args(["-selection", "clipboard"]); } let mut c = c.stdin(std::process::Stdio::piped()).spawn().map_err(|e| format!("the clipboard tool: {e}"))?; c.stdin.as_mut().ok_or("no pipe")?.write_all(s.as_bytes()).map_err(|e| e.to_string())?; drop(c.stdin.take()); return c.wait().map_err(|e| e.to_string()).and_then(|st| if st.success() { Ok(()) } else { Err(format!("the clipboard tool exited {st}")) }); } Action::Text(_) | Action::Touch { .. } => return Err("not injected on this device".into()), // The host opens a sealed action before it injects it (WAI remote-desktop §5.5). Action::Sealed { .. } => return Err("a sealed action, not opened".into()), } self.sync() } fn release_all(&mut self) -> Result<(), String> { for c in std::mem::take(&mut self.buttons_down).into_iter().chain(std::mem::take(&mut self.keys_down)) { self.emit(EV_KEY, c, 0)?; } self.sync() } }