- Persistent shell session (cmd.exe) preserving cd state between commands - Screenshot capture via Windows GDI (BGRA→RGBA→PNG→Base64) - Mouse click via SendInput with absolute screen coordinates - Text input via SendInput with Unicode (UTF-16) key events - Auto-reconnect with exponential backoff (max 30s) - Config stored in %APPDATA%/helios-remote/config.json - All Windows APIs under #[cfg(windows)] for cross-compile safety - CI: add Windows cross-compile job (x86_64-pc-windows-gnu) with artifact upload
154 lines
5.2 KiB
Rust
154 lines
5.2 KiB
Rust
/// Mouse click and keyboard input via Windows SendInput (or stub on non-Windows).
|
|
use helios_common::MouseButton;
|
|
|
|
#[cfg(windows)]
|
|
pub fn click(x: i32, y: i32, button: &MouseButton) -> Result<(), String> {
|
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
|
SendInput, INPUT, INPUT_MOUSE, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN,
|
|
MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE,
|
|
MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEINPUT,
|
|
};
|
|
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};
|
|
|
|
unsafe {
|
|
let screen_w = GetSystemMetrics(SM_CXSCREEN) as i32;
|
|
let screen_h = GetSystemMetrics(SM_CYSCREEN) as i32;
|
|
|
|
if screen_w == 0 || screen_h == 0 {
|
|
return Err(format!(
|
|
"Could not get screen dimensions: {screen_w}x{screen_h}"
|
|
));
|
|
}
|
|
|
|
// Convert pixel coords to absolute 0-65535 range
|
|
let abs_x = ((x * 65535) / screen_w) as i32;
|
|
let abs_y = ((y * 65535) / screen_h) as i32;
|
|
|
|
let (down_flag, up_flag) = match button {
|
|
MouseButton::Left => (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP),
|
|
MouseButton::Right => (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP),
|
|
MouseButton::Middle => (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP),
|
|
};
|
|
|
|
// Move to position
|
|
let move_input = INPUT {
|
|
r#type: INPUT_MOUSE,
|
|
Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 {
|
|
mi: MOUSEINPUT {
|
|
dx: abs_x,
|
|
dy: abs_y,
|
|
mouseData: 0,
|
|
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
};
|
|
|
|
let down_input = INPUT {
|
|
r#type: INPUT_MOUSE,
|
|
Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 {
|
|
mi: MOUSEINPUT {
|
|
dx: abs_x,
|
|
dy: abs_y,
|
|
mouseData: 0,
|
|
dwFlags: down_flag | MOUSEEVENTF_ABSOLUTE,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
};
|
|
|
|
let up_input = INPUT {
|
|
r#type: INPUT_MOUSE,
|
|
Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 {
|
|
mi: MOUSEINPUT {
|
|
dx: abs_x,
|
|
dy: abs_y,
|
|
mouseData: 0,
|
|
dwFlags: up_flag | MOUSEEVENTF_ABSOLUTE,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
};
|
|
|
|
let inputs = [move_input, down_input, up_input];
|
|
let result = SendInput(&inputs, std::mem::size_of::<INPUT>() as i32);
|
|
|
|
if result != inputs.len() as u32 {
|
|
return Err(format!(
|
|
"SendInput for click at ({x},{y}) sent {result}/{} events — some may have been blocked by UIPI",
|
|
inputs.len()
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub fn type_text(text: &str) -> Result<(), String> {
|
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
|
SendInput, INPUT, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_UNICODE,
|
|
};
|
|
|
|
if text.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
unsafe {
|
|
let mut inputs: Vec<INPUT> = Vec::with_capacity(text.len() * 2);
|
|
|
|
for ch in text.encode_utf16() {
|
|
// Key down
|
|
inputs.push(INPUT {
|
|
r#type: INPUT_KEYBOARD,
|
|
Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 {
|
|
ki: KEYBDINPUT {
|
|
wVk: windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY(0),
|
|
wScan: ch,
|
|
dwFlags: KEYEVENTF_UNICODE,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
});
|
|
// Key up
|
|
inputs.push(INPUT {
|
|
r#type: INPUT_KEYBOARD,
|
|
Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 {
|
|
ki: KEYBDINPUT {
|
|
wVk: windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY(0),
|
|
wScan: ch,
|
|
dwFlags: KEYEVENTF_UNICODE
|
|
| windows::Win32::UI::Input::KeyboardAndMouse::KEYEVENTF_KEYUP,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
let result = SendInput(&inputs, std::mem::size_of::<INPUT>() as i32);
|
|
|
|
if result != inputs.len() as u32 {
|
|
return Err(format!(
|
|
"SendInput for type_text sent {result}/{} events — some may have been blocked (UIPI or secure desktop)",
|
|
inputs.len()
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn click(_x: i32, _y: i32, _button: &MouseButton) -> Result<(), String> {
|
|
Err("click() is only supported on Windows".to_string())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn type_text(_text: &str) -> Result<(), String> {
|
|
Err("type_text() is only supported on Windows".to_string())
|
|
}
|