//! X11 输入注入:XTest 扩展(Xorg / XWayland / Xvfb 均支持)。 //! //! **键码映射是本模块的关键**:协议里键盘沿用 Windows 口味的 PS/2 Set-1 扫描码, //! 而 X11 要的是 keycode,两套编号并非处处等价: //! - 主键区:`keycode = scancode + 8`(evdev 偏移),已逐键核对; //! - E0 扩展键:两条链路编号**完全不同**(如方向键 Set-1 是 0x48,evdev 侧 X11 keycode 是 111),必须查表。 //! //! 鼠标沿用虚拟桌面归一化坐标 0..=65535:X11 根窗口天然横跨全部显示器, //! 与 Windows 的 `MOUSEEVENTF_VIRTUALDESK` 语义一致,故映射到根窗口像素即可。 use agent_core::InputInjector; use anyhow::{anyhow, Result}; use common::MouseButton; use std::sync::Mutex; use x11rb::connection::Connection; use x11rb::protocol::xproto::{ConnectionExt as _, Window}; use x11rb::protocol::xtest::ConnectionExt as _; use x11rb::rust_connection::RustConnection; // X11 核心事件码(协议固定值) const KEY_PRESS: u8 = 2; const KEY_RELEASE: u8 = 3; const BUTTON_PRESS: u8 = 4; const BUTTON_RELEASE: u8 = 5; const MOTION_NOTIFY: u8 = 6; // X11 按钮编号:1=左 2=中 3=右 4=滚轮上 5=滚轮下 const BTN_LEFT: u8 = 1; const BTN_MIDDLE: u8 = 2; const BTN_RIGHT: u8 = 3; const BTN_WHEEL_UP: u8 = 4; const BTN_WHEEL_DOWN: u8 = 5; /// Windows 滚轮一格 = 120(协议沿用该语义) const WHEEL_DELTA: i32 = 120; /// 单次滚轮最多合成的格数,防止异常值刷爆事件队列 const WHEEL_MAX_CLICKS: i32 = 20; pub struct X11Injector { /// `Mutex` 只为满足 `InputInjector: Sync`(X11 连接本身非 Sync),无竞争 conn: Mutex, root: Window, width: i32, height: i32, } impl X11Injector { pub fn new() -> Result { let (conn, screen_num) = x11rb::connect(None).map_err(|e| { anyhow!( "连接 X11 失败(DISPLAY={:?}): {e}", std::env::var("DISPLAY").ok() ) })?; // XTest 缺失就没有注入能力,启动即失败好过连上后才报错 let ext = conn .query_extension(b"XTEST") .map_err(|e| anyhow!("查询 XTEST 扩展失败: {e}"))? .reply() .map_err(|e| anyhow!("查询 XTEST 扩展应答失败: {e}"))?; anyhow::ensure!(ext.present, "X server 未提供 XTEST 扩展,无法注入输入"); // 先拷出参数,避免借用 conn 的同时又把它移进结构体 let (root, width, height) = { let screen = &conn.setup().roots[screen_num]; ( screen.root, screen.width_in_pixels as i32, screen.height_in_pixels as i32, ) }; println!("[input] XTEST ready, root {width}x{height}"); Ok(Self { conn: Mutex::new(conn), root, width, height, }) } fn fake(&self, type_: u8, detail: u8, x: i32, y: i32) { let conn = match self.conn.lock() { Ok(c) => c, Err(poisoned) => poisoned.into_inner(), }; if let Err(e) = conn.xtest_fake_input( type_, detail, 0, self.root, x as i16, y as i16, 0, // deviceid:0 = 核心设备 ) { eprintln!("[input] xtest_fake_input 失败: {e}"); return; } let _ = conn.flush(); } fn click(&self, button: u8) { self.fake(BUTTON_PRESS, button, 0, 0); self.fake(BUTTON_RELEASE, button, 0, 0); } } impl InputInjector for X11Injector { fn mouse_move(&self, x: u16, y: u16) { // detail=0 → 绝对坐标 let px = scale(x, self.width); let py = scale(y, self.height); self.fake(MOTION_NOTIFY, 0, px, py); } fn mouse_button(&self, b: MouseButton, down: bool) { let button = match b { MouseButton::Left => BTN_LEFT, MouseButton::Middle => BTN_MIDDLE, MouseButton::Right => BTN_RIGHT, }; self.fake( if down { BUTTON_PRESS } else { BUTTON_RELEASE }, button, 0, 0, ); } fn wheel(&self, dy: i32) { // 协议沿用 Windows 语义:dy > 0 向上滚,一格 = 120 if dy == 0 { return; } let clicks = (dy.abs() / WHEEL_DELTA).clamp(1, WHEEL_MAX_CLICKS); let button = if dy > 0 { BTN_WHEEL_UP } else { BTN_WHEEL_DOWN }; for _ in 0..clicks { self.click(button); } } fn key(&self, scancode: u16, extended: bool, down: bool) { let Some(keycode) = keycode_of(scancode, extended) else { eprintln!("[input] 未映射的扫描码 scancode=0x{scancode:02X} extended={extended}"); return; }; self.fake( if down { KEY_PRESS } else { KEY_RELEASE }, keycode, 0, 0, ); } } /// 虚拟桌面归一化坐标(0..=65535)→ 根窗口像素坐标 fn scale(v: u16, extent: i32) -> i32 { if extent <= 1 { return 0; } ((v as i64 * (extent as i64 - 1)) / 65535) as i32 } /// PS/2 Set-1 扫描码 → X11 keycode。 /// /// 非扩展键恒为 `scancode + 8`(当前 viewer 的 keymap 只发该区间内的键); /// E0 扩展键查表,编号与主键区规则无关。 fn keycode_of(scancode: u16, extended: bool) -> Option { if extended { // 已按 Xorg evdev 标准 keycode 分配核对 let kc = match scancode { 0x1C => 104, // 小键盘 Enter 0x1D => 105, // 右 Ctrl 0x35 => 106, // 小键盘 / 0x37 => 107, // PrintScreen 0x38 => 108, // 右 Alt 0x47 => 110, // Home 0x48 => 111, // ↑ 0x49 => 112, // PageUp 0x4B => 113, // ← 0x4D => 114, // → 0x4F => 115, // End 0x50 => 116, // ↓ 0x51 => 117, // PageDown 0x52 => 118, // Insert 0x53 => 119, // Delete 0x5B => 133, // 左 Super(Win) 0x5C => 134, // 右 Super(Win) 0x5D => 135, // Menu _ => return None, }; Some(kc) } else { // 0x01..=0x53 主键区,以及 0x57/0x58 的 F11/F12;其余为 Set-1 未定义位 match scancode { 0x01..=0x53 | 0x57 | 0x58 => Some((scancode + 8) as u8), _ => None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn main_block_uses_evdev_offset() { assert_eq!(keycode_of(0x1E, false), Some(38)); // A assert_eq!(keycode_of(0x02, false), Some(10)); // 1 assert_eq!(keycode_of(0x39, false), Some(65)); // Space assert_eq!(keycode_of(0x1C, false), Some(36)); // Enter assert_eq!(keycode_of(0x1D, false), Some(37)); // 左 Ctrl assert_eq!(keycode_of(0x2A, false), Some(50)); // 左 Shift assert_eq!(keycode_of(0x58, false), Some(96)); // F12 } #[test] fn extended_keys_are_not_offset() { // 若误用 +8 规则,方向键会落到小键盘区(0x48+8=80=KP_8),这正是要防的坑 assert_eq!(keycode_of(0x48, true), Some(111)); // ↑ assert_ne!(keycode_of(0x48, true), Some(0x48 + 8)); assert_eq!(keycode_of(0x50, true), Some(116)); // ↓ assert_eq!(keycode_of(0x4B, true), Some(113)); // ← assert_eq!(keycode_of(0x4D, true), Some(114)); // → assert_eq!(keycode_of(0x47, true), Some(110)); // Home assert_eq!(keycode_of(0x4F, true), Some(115)); // End assert_eq!(keycode_of(0x49, true), Some(112)); // PageUp assert_eq!(keycode_of(0x51, true), Some(117)); // PageDown assert_eq!(keycode_of(0x53, true), Some(119)); // Delete assert_eq!(keycode_of(0x5B, true), Some(133)); // 左 Super } #[test] fn undefined_scancodes_are_rejected() { assert_eq!(keycode_of(0x54, false), None); assert_eq!(keycode_of(0x7F, false), None); assert_eq!(keycode_of(0x99, true), None); } #[test] fn coordinate_scaling_covers_full_range() { assert_eq!(scale(0, 1920), 0); assert_eq!(scale(65535, 1920), 1919); assert_eq!(scale(32768, 1920), 960); assert_eq!(scale(12345, 0), 0); // 退化尺寸不 panic } }