250 lines
8.3 KiB
Rust
250 lines
8.3 KiB
Rust
|
|
//! 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<RustConnection>,
|
|||
|
|
root: Window,
|
|||
|
|
width: i32,
|
|||
|
|
height: i32,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl X11Injector {
|
|||
|
|
pub fn new() -> Result<Self> {
|
|||
|
|
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<u8> {
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|