use std::thread::sleep;
use std::time::Duration;
use anyhow::{anyhow, Context, Result};
use evdev::{
uinput::VirtualDevice, AbsInfo, AbsoluteAxisCode, AttributeSet, EventType, InputEvent, KeyCode,
PropType, RelativeAxisCode, UinputAbsSetup,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[must_use = "pointer input may be clamped; inspect requested and emitted coordinates"]
pub(crate) struct PointerLanding {
pub(crate) requested: (i32, i32),
pub(crate) emitted: (i32, i32),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[must_use = "drag endpoints may be clamped; inspect requested and emitted coordinates"]
pub(crate) struct DragLanding {
pub(crate) start: PointerLanding,
pub(crate) end: PointerLanding,
}
struct DeviceCapabilities {
keys: AttributeSet<KeyCode>,
relative_axes: AttributeSet<RelativeAxisCode>,
props: AttributeSet<PropType>,
}
impl DeviceCapabilities {
fn new() -> Self {
Self {
keys: AttributeSet::from_iter([
KeyCode::BTN_LEFT,
KeyCode::BTN_RIGHT,
KeyCode::BTN_MIDDLE,
]),
relative_axes: AttributeSet::from_iter([
RelativeAxisCode::REL_WHEEL,
RelativeAxisCode::REL_HWHEEL,
]),
props: AttributeSet::from_iter([PropType::DIRECT]),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct AbsPointerGeometry {
max_x: i32,
max_y: i32,
}
impl AbsPointerGeometry {
fn from_dimensions(width: i32, height: i32) -> Self {
Self {
max_x: width.max(1).saturating_sub(1),
max_y: height.max(1).saturating_sub(1),
}
}
fn axis_maxima(self) -> (i32, i32) {
(self.max_x, self.max_y)
}
fn matches_dimensions(self, width: i32, height: i32) -> bool {
self == Self::from_dimensions(width, height)
}
fn clamp_coordinates(self, x: i32, y: i32) -> (i32, i32) {
(x.clamp(0, self.max_x), y.clamp(0, self.max_y))
}
fn landing_for(self, x: i32, y: i32) -> PointerLanding {
PointerLanding {
requested: (x, y),
emitted: self.clamp_coordinates(x, y),
}
}
}
pub(crate) trait PointerEvents: Send {
fn emit(&mut self, events: &[InputEvent]) -> std::io::Result<()>;
}
impl PointerEvents for VirtualDevice {
fn emit(&mut self, events: &[InputEvent]) -> std::io::Result<()> {
VirtualDevice::emit(self, events)
}
}
pub struct AbsPointer {
device: Box<dyn PointerEvents>,
geometry: AbsPointerGeometry,
}
impl AbsPointer {
pub fn create(width: i32, height: i32) -> Result<Self> {
if cfg!(test) {
anyhow::bail!("unit tests do not create real uinput devices");
}
let geometry = AbsPointerGeometry::from_dimensions(width, height);
let (max_x, max_y) = geometry.axis_maxima();
let abs_x =
UinputAbsSetup::new(AbsoluteAxisCode::ABS_X, AbsInfo::new(0, 0, max_x, 0, 0, 1));
let abs_y =
UinputAbsSetup::new(AbsoluteAxisCode::ABS_Y, AbsInfo::new(0, 0, max_y, 0, 0, 1));
let DeviceCapabilities {
keys,
relative_axes,
props,
} = DeviceCapabilities::new();
let device = VirtualDevice::builder()
.context("uinput builder (is /dev/uinput writable?)")?
.name("computer-use-linux absolute pointer")
.with_properties(&props)?
.with_absolute_axis(&abs_x)?
.with_absolute_axis(&abs_y)?
.with_relative_axes(&relative_axes)?
.with_keys(&keys)?
.build()
.context("failed to create uinput absolute pointer device")?;
sleep(Duration::from_millis(500));
Ok(Self {
device: Box::new(device),
geometry,
})
}
#[cfg(test)]
pub(crate) fn with_events(
device: impl PointerEvents + 'static,
width: i32,
height: i32,
) -> Self {
Self {
device: Box::new(device),
geometry: AbsPointerGeometry::from_dimensions(width, height),
}
}
pub fn matches_dimensions(&self, width: i32, height: i32) -> bool {
self.geometry.matches_dimensions(width, height)
}
pub fn move_to(&mut self, x: i32, y: i32) -> Result<PointerLanding> {
let landing = self.geometry.landing_for(x, y);
let (emitted_x, emitted_y) = landing.emitted;
self.device
.emit(&[
InputEvent::new_now(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_X.0, emitted_x),
InputEvent::new_now(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_Y.0, emitted_y),
])
.context("failed to emit absolute motion")?;
Ok(landing)
}
pub fn wheel(&mut self, dx: i32, dy: i32) -> Result<()> {
self.device
.emit(&[
InputEvent::new_now(EventType::RELATIVE.0, RelativeAxisCode::REL_HWHEEL.0, dx),
InputEvent::new_now(EventType::RELATIVE.0, RelativeAxisCode::REL_WHEEL.0, dy),
])
.context("failed to emit wheel motion")?;
Ok(())
}
pub fn click(
&mut self,
x: i32,
y: i32,
button: PointerButton,
count: u32,
) -> Result<PointerLanding> {
let landing = self.move_to(x, y)?;
sleep(Duration::from_millis(30));
let code = button.key_code();
for _ in 0..count.max(1) {
if let Err(error) = self.press_button(code) {
return Err(self.release_after_error(code, error));
}
sleep(Duration::from_millis(30));
if let Err(error) = self.release_button(code) {
return Err(self.release_after_error(code, error));
}
sleep(Duration::from_millis(40));
}
Ok(landing)
}
pub fn drag(
&mut self,
start: (i32, i32),
end: (i32, i32),
button: PointerButton,
) -> Result<DragLanding> {
let code = button.key_code();
let start_landing = self.move_to(start.0, start.1)?;
sleep(Duration::from_millis(30));
if let Err(error) = self.press_button(code) {
return Err(self.release_after_error(code, error));
}
sleep(Duration::from_millis(40));
for (x, y) in crate::pointer_path::drag_points(start, end) {
if let Err(error) = self.move_to(x, y) {
return Err(self.release_after_error(code, error));
}
sleep(crate::pointer_path::DRAG_STEP_DELAY);
}
if let Err(error) = self.release_button(code) {
return Err(self.release_after_error(code, error));
}
Ok(DragLanding {
start: start_landing,
end: self.geometry.landing_for(end.0, end.1),
})
}
fn press_button(&mut self, code: u16) -> Result<()> {
self.device
.emit(&[InputEvent::new_now(EventType::KEY.0, code, 1)])
.context("failed to emit absolute pointer button press")
}
fn release_button(&mut self, code: u16) -> Result<()> {
self.device
.emit(&[InputEvent::new_now(EventType::KEY.0, code, 0)])
.context("failed to emit absolute pointer button release")
}
fn release_after_error(&mut self, code: u16, error: anyhow::Error) -> anyhow::Error {
match self.release_button(code) {
Ok(()) => anyhow!("{error:#}; sent a best-effort button release"),
Err(release_error) => {
anyhow!("{error:#}; best-effort button release also failed: {release_error:#}")
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum PointerButton {
Left,
Right,
Middle,
}
impl PointerButton {
pub fn from_name(name: Option<&str>) -> Option<Self> {
match name.unwrap_or("left").to_ascii_lowercase().as_str() {
"left" => Some(Self::Left),
"right" => Some(Self::Right),
"middle" => Some(Self::Middle),
_ => None,
}
}
fn key_code(self) -> u16 {
match self {
Self::Left => KeyCode::BTN_LEFT.0,
Self::Right => KeyCode::BTN_RIGHT.0,
Self::Middle => KeyCode::BTN_MIDDLE.0,
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::{
AbsPointer, AbsPointerGeometry, EventType, InputEvent, KeyCode, PointerButton,
PointerEvents, PointerLanding, RelativeAxisCode,
};
#[test]
fn axis_range_ends_at_last_desktop_pixel() {
let geometry = AbsPointerGeometry::from_dimensions(1920, 1080);
assert_eq!(geometry.axis_maxima(), (1919, 1079));
}
#[test]
fn stale_capture_geometry_is_rejected_before_pointer_reuse() {
let cached = AbsPointerGeometry::from_dimensions(3840, 2160);
assert!(cached.matches_dimensions(3840, 2160));
assert!(!cached.matches_dimensions(2560, 1440));
assert!(!cached.matches_dimensions(3840, 1080));
}
#[test]
fn pointer_landing_preserves_the_request_and_emitted_coordinates() {
let geometry = AbsPointerGeometry::from_dimensions(1920, 1080);
for (requested, emitted) in [
((640, 480), (640, 480)),
((1920, 1080), (1919, 1079)),
((-1, -1), (0, 0)),
((i32::MAX, i32::MAX), (1919, 1079)),
] {
let landing = geometry.landing_for(requested.0, requested.1);
assert_eq!(landing.requested, requested);
assert_eq!(landing.emitted, emitted);
}
}
#[test]
fn unsupported_buttons_fall_through_to_other_backends() {
assert!(matches!(
PointerButton::from_name(None),
Some(PointerButton::Left)
));
assert!(matches!(
PointerButton::from_name(Some("right")),
Some(PointerButton::Right)
));
assert!(matches!(
PointerButton::from_name(Some("middle")),
Some(PointerButton::Middle)
));
for button in ["side", "extra", "forward", "back"] {
assert!(
PointerButton::from_name(Some(button)).is_none(),
"{button} must fall through instead of becoming a left click"
);
}
}
#[derive(Clone, Default)]
pub(crate) struct RecordedEvents {
events: std::sync::Arc<std::sync::Mutex<Vec<(u16, u16, i32)>>>,
calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
fail_calls: Vec<usize>,
}
impl RecordedEvents {
pub(crate) fn failing_on(fail_calls: &[usize]) -> Self {
Self {
fail_calls: fail_calls.to_vec(),
..Self::default()
}
}
fn button_events(&self) -> Vec<(u16, i32)> {
self.events
.lock()
.unwrap()
.iter()
.filter(|(kind, _, _)| *kind == EventType::KEY.0)
.map(|(_, code, value)| (*code, *value))
.collect()
}
pub(crate) fn all_events(&self) -> Vec<(u16, u16, i32)> {
self.events.lock().unwrap().clone()
}
pub(crate) fn wheel_events(&self) -> Vec<(u16, i32)> {
self.events
.lock()
.unwrap()
.iter()
.filter(|(kind, _, _)| *kind == EventType::RELATIVE.0)
.map(|(_, code, value)| (*code, *value))
.collect()
}
}
impl PointerEvents for RecordedEvents {
fn emit(&mut self, events: &[InputEvent]) -> std::io::Result<()> {
let call = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.events.lock().unwrap().extend(
events
.iter()
.map(|event| (event.event_type().0, event.code(), event.value())),
);
if self.fail_calls.contains(&call) {
return Err(std::io::Error::other("uinput write failed"));
}
Ok(())
}
}
#[test]
fn click_releases_the_button_after_a_failed_press() {
let events = RecordedEvents::failing_on(&[1]);
let mut pointer = AbsPointer::with_events(events.clone(), 100, 100);
let error = pointer.click(10, 10, PointerButton::Left, 1).unwrap_err();
let error = format!("{error:#}");
let left = KeyCode::BTN_LEFT.0;
assert_eq!(events.button_events(), [(left, 1), (left, 0)]);
assert!(error.contains("uinput write failed"), "{error}");
assert!(
error.contains("sent a best-effort button release"),
"{error}"
);
}
#[test]
fn click_retries_a_failed_release() {
let events = RecordedEvents::failing_on(&[2, 3]);
let mut pointer = AbsPointer::with_events(events.clone(), 100, 100);
let error = pointer.click(10, 10, PointerButton::Left, 2).unwrap_err();
let error = format!("{error:#}");
let left = KeyCode::BTN_LEFT.0;
assert_eq!(events.button_events(), [(left, 1), (left, 0), (left, 0)]);
assert!(
error.contains("best-effort button release also failed"),
"{error}"
);
}
#[test]
fn drag_reports_where_both_endpoints_landed() {
let events = RecordedEvents::default();
let mut pointer = AbsPointer::with_events(events.clone(), 100, 100);
let landing = pointer
.drag((-5, 10), (150, 60), PointerButton::Left)
.unwrap();
assert_eq!(
landing.start,
PointerLanding {
requested: (-5, 10),
emitted: (0, 10),
}
);
assert_eq!(
landing.end,
PointerLanding {
requested: (150, 60),
emitted: (99, 60),
}
);
let motion: Vec<i32> = events
.events
.lock()
.unwrap()
.iter()
.filter(|(kind, _, _)| *kind == EventType::ABSOLUTE.0)
.map(|(_, _, value)| *value)
.collect();
assert_eq!(motion[..2], [0, 10]);
assert_eq!(motion[motion.len() - 2..], [99, 60]);
}
#[test]
fn device_declares_a_wheel_and_stays_an_absolute_mouse() {
use super::{DeviceCapabilities, PropType};
let capabilities = DeviceCapabilities::new();
for axis in [RelativeAxisCode::REL_WHEEL, RelativeAxisCode::REL_HWHEEL] {
assert!(capabilities.relative_axes.contains(axis), "{axis:?}");
}
for axis in [RelativeAxisCode::REL_X, RelativeAxisCode::REL_Y] {
assert!(!capabilities.relative_axes.contains(axis), "{axis:?}");
}
assert!(capabilities.keys.contains(KeyCode::BTN_LEFT));
for key in [
KeyCode::BTN_0,
KeyCode::BTN_1,
KeyCode::BTN_TOUCH,
KeyCode::BTN_TOOL_PEN,
KeyCode::BTN_TOOL_FINGER,
] {
assert!(!capabilities.keys.contains(key), "{key:?}");
}
assert!(capabilities.props.contains(PropType::DIRECT));
}
#[test]
fn wheel_emits_relative_axis_events_from_the_pointer_device() {
let events = RecordedEvents::default();
let mut pointer = AbsPointer::with_events(events.clone(), 100, 100);
pointer.wheel(2, -3).unwrap();
assert_eq!(
events.wheel_events(),
[
(RelativeAxisCode::REL_HWHEEL.0, 2),
(RelativeAxisCode::REL_WHEEL.0, -3),
]
);
}
#[test]
fn wheel_reports_a_failed_emit() {
let events = RecordedEvents::failing_on(&[0]);
let mut pointer = AbsPointer::with_events(events, 100, 100);
let error = format!("{:#}", pointer.wheel(0, -1).unwrap_err());
assert!(error.contains("failed to emit wheel motion"), "{error}");
assert!(error.contains("uinput write failed"), "{error}");
}
#[test]
fn drag_releases_the_button_after_failed_motion_and_reports_it() {
let events = RecordedEvents::failing_on(&[2]);
let mut pointer = AbsPointer::with_events(events.clone(), 100, 100);
let error = pointer
.drag((10, 10), (50, 50), PointerButton::Left)
.unwrap_err();
let error = format!("{error:#}");
let left = KeyCode::BTN_LEFT.0;
assert_eq!(events.button_events(), [(left, 1), (left, 0)]);
assert!(
error.contains("sent a best-effort button release"),
"{error}"
);
}
}