//! Windows implementation of computer use actions using the Win32 SendInput //! API for input and GDI for screenshots. mod dpi; mod keyboard; mod mouse; mod screenshot; use async_trait::async_trait; use galaxyui::r#async::Timer; use windows::Win32::System::StationsAndDesktops::{ CloseDesktop, DESKTOP_ACCESS_FLAGS, DESKTOP_CONTROL_FLAGS, HDESK, OpenInputDesktop, }; use crate::{Action, ActionResult, Options}; /// Returns whether computer_use can drive input on this machine right now. /// /// Reports `false` when there is no accessible input desktop (e.g., the process is running under /// Session 0 as a Windows service, the workstation is locked, or the user has switched to a /// different secure desktop). In those cases `SendInput` silently no-ops and GDI desktop capture /// fails, so we'd rather fail fast here than surface the error mid-action. pub fn is_supported_on_current_platform() -> bool { probe_input_desktop_available() } /// Shared probe used by both [`is_supported_on_current_platform`] and [`Actor::new`] so the /// "can we drive input right now?" logic lives in one place. This still runs the probe on each /// call (it's a cheap `OpenInputDesktop` / `CloseDesktop` round-trip) — we don't cache it because /// availability can change at runtime (workstation lock, secure desktop swap, Remote Desktop /// reconnect). fn probe_input_desktop_available() -> bool { InputDesktop::acquire().is_some() } /// RAII wrapper for an `HDESK` returned by `OpenInputDesktop`. Guarantees the handle is closed /// (or at least that a close attempt is made and logged on failure) even if the caller returns /// early. Modeled after the GDI handle guards in `screenshot.rs`. struct InputDesktop(HDESK); impl InputDesktop { fn acquire() -> Option { // SAFETY: `OpenInputDesktop` has no preconditions. We pass `false` for inheritance and // request no specific access (just probing for existence). let handle = unsafe { OpenInputDesktop(DESKTOP_CONTROL_FLAGS(0), false, DESKTOP_ACCESS_FLAGS(0)) }; handle.ok().map(Self) } } impl Drop for InputDesktop { fn drop(&mut self) { // SAFETY: `self.0` is a valid HDESK returned by `OpenInputDesktop` and has not been // closed yet. unsafe { if let Err(e) = CloseDesktop(self.0) { log::warn!("CloseDesktop failed in InputDesktop::drop: {e}"); } } } } /// Actor holds Keyboard/Mouse state unconditionally — both are cheap to construct and have no /// side effects — so a `perform_actions` call can recover as soon as an input desktop is /// reachable again, even if `Actor::new` ran while the desktop was temporarily inaccessible /// (workstation locked at startup, RDP disconnect, etc.). Supportability is decided per call by /// [`probe_input_desktop_available`] rather than being cached in the actor's shape. pub struct Actor { keyboard: keyboard::Keyboard, mouse: mouse::Mouse, } impl Actor { pub fn new() -> Self { Self { keyboard: keyboard::Keyboard::new(), mouse: mouse::Mouse::new(), } } } impl Default for Actor { fn default() -> Self { Self::new() } } /// Error returned by `perform_actions` when the input desktop is inaccessible at call time /// (workstation lock, secure desktop swap, RDP reconnect, Session 0 service, …). const NO_INPUT_DESKTOP_ERROR: &str = "Computer use is not available: no accessible input desktop"; #[async_trait] impl super::Actor for Actor { fn platform(&self) -> Option { // Live probe so callers can use `platform().is_some()` as a current "can drive input" // signal. Matches the Linux `Unsupported`-returns-None convention. if probe_input_desktop_available() { Some(super::Platform::Windows) } else { None } } async fn perform_actions( &mut self, actions: &[Action], options: Options, ) -> Result { // Probe at the top of every call so transient loss of the input desktop (workstation // lock, secure desktop swap, RDP reconnect) surfaces as a descriptive error instead of // letting `SendInput` silently no-op. Cheap `OpenInputDesktop`/`CloseDesktop` round-trip. if !probe_input_desktop_available() { return Err(NO_INPUT_DESKTOP_ERROR.to_string()); } let keyboard = &mut self.keyboard; let mouse = &mut self.mouse; for action in actions { match action { Action::Wait(duration) => { Timer::after(*duration).await; } Action::MouseDown { button, at } => { mouse.move_to(*at)?; mouse.button_down(button)?; } Action::MouseUp { button } => mouse.button_up(button)?, Action::MouseMove { to } => mouse.move_to(*to)?, Action::MouseWheel { at, direction, distance, } => { mouse.move_to(*at)?; mouse.scroll(direction, distance)?; } Action::TypeText { text } => { keyboard.type_text(text)?; } Action::KeyDown { key } => { keyboard.key_down(key)?; } Action::KeyUp { key } => { keyboard.key_up(key)?; } } } let screenshot = if let Some(params) = options.screenshot_params { Some(screenshot::take(params)?) } else { None }; Ok(ActionResult { screenshot, cursor_position: Some(mouse.current_position()?), }) } }