Files
guru-connect/agent/src/input/mouse.rs
Mike Swanson 1c5c1e78e7
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
style: cargo fmt --all — make codebase rustfmt-clean
First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced
pre-existing formatting drift across the agent and server crates. Apply rustfmt
across the workspace so the codebase meets its own CI gate. Pure formatting; no
logic changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:02:12 +00:00

221 lines
6.5 KiB
Rust

//! Mouse input simulation using Windows SendInput API
use super::MouseButton;
use anyhow::Result;
#[cfg(windows)]
use windows::Win32::UI::Input::KeyboardAndMouse::{
SendInput, INPUT, INPUT_0, INPUT_MOUSE, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_HWHEEL,
MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP,
MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_VIRTUALDESK,
MOUSEEVENTF_WHEEL, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP, MOUSEINPUT,
};
// X button constants (not exported in windows crate 0.58+)
#[cfg(windows)]
const XBUTTON1: u32 = 0x0001;
#[cfg(windows)]
const XBUTTON2: u32 = 0x0002;
#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
};
/// Mouse input controller
pub struct MouseController {
/// Virtual screen dimensions for coordinate translation
#[cfg(windows)]
virtual_screen: VirtualScreen,
}
#[cfg(windows)]
struct VirtualScreen {
x: i32,
y: i32,
width: i32,
height: i32,
}
impl MouseController {
/// Create a new mouse controller
pub fn new() -> Result<Self> {
#[cfg(windows)]
{
let virtual_screen = unsafe {
VirtualScreen {
x: GetSystemMetrics(SM_XVIRTUALSCREEN),
y: GetSystemMetrics(SM_YVIRTUALSCREEN),
width: GetSystemMetrics(SM_CXVIRTUALSCREEN),
height: GetSystemMetrics(SM_CYVIRTUALSCREEN),
}
};
Ok(Self { virtual_screen })
}
#[cfg(not(windows))]
{
anyhow::bail!("Mouse input only supported on Windows")
}
}
/// Move mouse to absolute screen coordinates
#[cfg(windows)]
pub fn move_to(&mut self, x: i32, y: i32) -> Result<()> {
// Convert screen coordinates to normalized absolute coordinates (0-65535)
let norm_x = ((x - self.virtual_screen.x) * 65535) / self.virtual_screen.width;
let norm_y = ((y - self.virtual_screen.y) * 65535) / self.virtual_screen.height;
let input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: norm_x,
dy: norm_y,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
time: 0,
dwExtraInfo: 0,
},
},
};
self.send_input(&[input])
}
/// Press mouse button down
#[cfg(windows)]
pub fn button_down(&mut self, button: MouseButton) -> Result<()> {
let (flags, data) = match button {
MouseButton::Left => (MOUSEEVENTF_LEFTDOWN, 0),
MouseButton::Right => (MOUSEEVENTF_RIGHTDOWN, 0),
MouseButton::Middle => (MOUSEEVENTF_MIDDLEDOWN, 0),
MouseButton::X1 => (MOUSEEVENTF_XDOWN, XBUTTON1),
MouseButton::X2 => (MOUSEEVENTF_XDOWN, XBUTTON2),
};
let input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: data,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
};
self.send_input(&[input])
}
/// Release mouse button
#[cfg(windows)]
pub fn button_up(&mut self, button: MouseButton) -> Result<()> {
let (flags, data) = match button {
MouseButton::Left => (MOUSEEVENTF_LEFTUP, 0),
MouseButton::Right => (MOUSEEVENTF_RIGHTUP, 0),
MouseButton::Middle => (MOUSEEVENTF_MIDDLEUP, 0),
MouseButton::X1 => (MOUSEEVENTF_XUP, XBUTTON1),
MouseButton::X2 => (MOUSEEVENTF_XUP, XBUTTON2),
};
let input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: data,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
};
self.send_input(&[input])
}
/// Scroll mouse wheel
#[cfg(windows)]
pub fn scroll(&mut self, delta_x: i32, delta_y: i32) -> Result<()> {
let mut inputs = Vec::new();
// Vertical scroll
if delta_y != 0 {
inputs.push(INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: delta_y as u32,
dwFlags: MOUSEEVENTF_WHEEL,
time: 0,
dwExtraInfo: 0,
},
},
});
}
// Horizontal scroll
if delta_x != 0 {
inputs.push(INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: delta_x as u32,
dwFlags: MOUSEEVENTF_HWHEEL,
time: 0,
dwExtraInfo: 0,
},
},
});
}
if !inputs.is_empty() {
self.send_input(&inputs)?;
}
Ok(())
}
/// Send input events
#[cfg(windows)]
fn send_input(&self, inputs: &[INPUT]) -> Result<()> {
let sent = unsafe { SendInput(inputs, std::mem::size_of::<INPUT>() as i32) };
if sent as usize != inputs.len() {
anyhow::bail!("SendInput failed: sent {} of {} inputs", sent, inputs.len());
}
Ok(())
}
#[cfg(not(windows))]
pub fn move_to(&mut self, _x: i32, _y: i32) -> Result<()> {
anyhow::bail!("Mouse input only supported on Windows")
}
#[cfg(not(windows))]
pub fn button_down(&mut self, _button: MouseButton) -> Result<()> {
anyhow::bail!("Mouse input only supported on Windows")
}
#[cfg(not(windows))]
pub fn button_up(&mut self, _button: MouseButton) -> Result<()> {
anyhow::bail!("Mouse input only supported on Windows")
}
#[cfg(not(windows))]
pub fn scroll(&mut self, _delta_x: i32, _delta_y: i32) -> Result<()> {
anyhow::bail!("Mouse input only supported on Windows")
}
}