feat(client): implement Windows client (Phase 2)
- 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
This commit is contained in:
parent
c2ff818506
commit
04527ae1bf
6 changed files with 793 additions and 7 deletions
154
crates/client/src/input.rs
Normal file
154
crates/client/src/input.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/// 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())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue