use std::thread;
use auv_driver_common::capture::{Activation, Capture, CaptureOptions, DisplayCapture, RegionCapture};
use auv_driver_common::display::ObservedDisplays;
use auv_driver_common::error::DriverResult;
use auv_driver_common::geometry::{Point, RatioRect, ScreenPoint, WindowPoint};
use auv_driver_common::input::{
Click, ClickOptions, InputActionResult, InputAttempt, InputDeliveryPath, InputPolicy, KeyPressOptions, PasteTextOptions, Scroll,
ScrollDeliveryCandidate, ScrollOptions, TypeTextOptions, WaitOptions, WindowInput,
};
use auv_driver_common::permission::PermissionProbe;
use auv_driver_common::selector::WindowSelector;
use auv_driver_common::vision::{TextRecognition, TextRecognitionOptions};
use auv_driver_common::window::Window;
use crate::accessibility::{AxTreeSnapshot, focus_node, select_node, snapshot_window};
use crate::capture::{capture_display, capture_region, list_displays};
use crate::clipboard::{restore as restore_clipboard, set_text as set_clipboard_text, snapshot};
use crate::driver::LinuxDriverSession;
use crate::error::{invalid_input, not_found};
use crate::input::{click_at, copy, paste, paste_text, press_key, scroll_at, type_text};
use crate::permission::{LinuxPortalProbe, probe_portals};
use crate::vision::{OcrMatches, find_text_in_capture, recognize_text_in_capture};
use crate::window::{capture_window, list_windows, resolve_window};
#[derive(Clone, Copy, Debug)]
pub struct DisplayApi<'a> {
session: &'a LinuxDriverSession,
}
#[derive(Clone, Copy, Debug)]
pub struct WindowApi<'a> {
session: &'a LinuxDriverSession,
}
#[derive(Clone, Copy, Debug)]
pub struct InputApi<'a> {
pub(crate) session: &'a LinuxDriverSession,
}
#[derive(Clone, Copy, Debug)]
pub struct VisionApi<'a> {
session: &'a LinuxDriverSession,
}
#[derive(Clone, Copy, Debug)]
pub struct PermissionApi<'a> {
session: &'a LinuxDriverSession,
}
#[derive(Clone, Copy, Debug)]
pub struct AccessibilityApi<'a> {
session: &'a LinuxDriverSession,
}
#[derive(Clone, Copy, Debug)]
pub struct ClipboardApi<'a> {
session: &'a LinuxDriverSession,
}
impl LinuxDriverSession {
pub fn display(&self) -> DisplayApi<'_> {
DisplayApi { session: self }
}
pub fn window(&self) -> WindowApi<'_> {
WindowApi { session: self }
}
pub fn input(&self) -> InputApi<'_> {
InputApi { session: self }
}
pub fn vision(&self) -> VisionApi<'_> {
VisionApi { session: self }
}
pub fn permission(&self) -> PermissionApi<'_> {
PermissionApi { session: self }
}
pub fn accessibility(&self) -> AccessibilityApi<'_> {
AccessibilityApi { session: self }
}
pub fn clipboard(&self) -> ClipboardApi<'_> {
ClipboardApi { session: self }
}
}
impl PermissionApi<'_> {
pub fn authorize_portals(&self) -> DriverResult<()> {
let mut state = self.session.state.lock().expect("linux driver session state poisoned");
let authorization = if state.input_session.is_none() || state.input_backend == crate::InputBackend::Uinput {
Some(crate::native::portal::PortalInput::open(state.restore_tokens.as_ref(), state.portal_app_id.as_ref())?)
} else {
None
};
let temporary_authorization = if state.input_backend == crate::InputBackend::Portal {
if let Some(session) = authorization {
state.input_session = Some(std::sync::Arc::new(std::sync::Mutex::new(crate::input::InputSession::Portal(session))));
}
None
} else {
authorization
};
if state.screencast_session.is_none() {
state.screencast_session =
Some(crate::native::portal::ScreenCastSession::open_monitor(state.restore_tokens.as_ref(), state.portal_app_id.as_ref())?);
}
drop(temporary_authorization);
Ok(())
}
pub fn probe_linux(&self) -> LinuxPortalProbe {
let _ = self.session;
probe_portals()
}
pub fn probe(&self) -> PermissionProbe {
self.probe_linux().as_permission_probe()
}
}
impl DisplayApi<'_> {
pub fn list(&self) -> DriverResult<ObservedDisplays> {
let _ = self.session;
list_displays()
}
pub fn capture(&self, options: CaptureOptions) -> DriverResult<DisplayCapture> {
let _ = self.session;
if options.window.is_some() || options.region.is_some() {
return Err(invalid_input("display.capture does not accept window or region capture options"));
}
if let Activation::ActivateFirst { .. } = options.activation {
return Err(invalid_input("display.capture cannot activate an application without an application target"));
}
capture_display(&self.session.state, options.display.as_deref())
}
pub fn capture_region(&self, options: CaptureOptions) -> DriverResult<RegionCapture> {
let _ = self.session;
if options.window.is_some() {
return Err(invalid_input("display.capture_region does not accept nested window capture options"));
}
if let Activation::ActivateFirst { .. } = options.activation {
return Err(invalid_input("display.capture_region cannot activate an application without an application target"));
}
let region = options.region.ok_or_else(|| invalid_input("display.capture_region requires CaptureOptions.region"))?;
capture_region(&self.session.state, options.display.as_deref(), region)
}
}
impl WindowApi<'_> {
pub fn list(&self) -> DriverResult<Vec<Window>> {
let _ = self.session;
list_windows()
}
pub fn resolve(&self, selector: WindowSelector) -> DriverResult<Window> {
let _ = self.session;
resolve_window(&selector)
}
pub fn capture(&self, window: &Window) -> DriverResult<Capture> {
self.capture_with(window, CaptureOptions::default())
}
pub fn capture_with(&self, window: &Window, options: CaptureOptions) -> DriverResult<Capture> {
if options.display.is_some() || options.region.is_some() || options.window.is_some() {
return Err(invalid_input("window.capture_with does not accept display, region, or nested window capture options"));
}
if let Activation::ActivateFirst { .. } = options.activation {
return Err(invalid_input("window.capture_with cannot activate Linux Wayland windows in this slice"));
}
capture_window(&self.session.state, window)
}
pub fn find_text(&self, window: &Window, query: &str, region: RatioRect, wait: WaitOptions) -> DriverResult<OcrMatches> {
let started = std::time::Instant::now();
loop {
let capture = self.capture(window)?;
let matches = self.session.vision().find_text_in_capture(&capture, query, region)?;
if !matches.matches.is_empty() || started.elapsed() >= wait.timeout {
return Ok(matches);
}
thread::sleep(wait.poll_interval);
}
}
pub fn wait_text(&self, window: &Window, query: &str, region: RatioRect, wait: WaitOptions) -> DriverResult<OcrMatches> {
let matches = self.find_text(window, query, region, wait)?;
if matches.matches.is_empty() {
Err(not_found(format!("text {query:?} before timeout")))
} else {
Ok(matches)
}
}
pub fn to_screen_point(&self, window: &Window, point: WindowPoint) -> DriverResult<ScreenPoint> {
let _ = self.session;
let point = point.point();
Ok(ScreenPoint::new(window.frame.origin.x + point.x, window.frame.origin.y + point.y))
}
pub fn to_window_point(&self, window: &Window, point: ScreenPoint) -> DriverResult<WindowPoint> {
let _ = self.session;
let point = point.point();
Ok(WindowPoint::new(point.x - window.frame.origin.x, point.y - window.frame.origin.y))
}
fn click_impl(&self, window: &Window, point: WindowPoint, options: ClickOptions) -> DriverResult<InputActionResult> {
if matches!(options.policy, InputPolicy::BackgroundOnly) {
return Err(invalid_input("linux window.click cannot use background_only input policy"));
}
let _ = options.window_strategy;
let focus_attempts = match focus_node(window, "0") {
Ok(result) => result.attempts,
Err(error) => vec![InputAttempt::failure(
InputDeliveryPath::AxFocus,
format!("AT-SPI could not foreground the target window before pointer delivery: {error}"),
)],
};
let screen_point = self.to_screen_point(window, point)?.point();
let mut result = self.session.input().click_at(screen_point, options.button, options.click, options.modifiers)?;
result.attempts.splice(0..0, focus_attempts);
add_foreground_window_fallback_reason(
&mut result,
InputDeliveryPath::WindowTargetedMouse,
"linux window.click used foreground RemoteDesktop portal input; Wayland window-targeted background pointer delivery is not available in this slice",
);
Ok(result)
}
fn scroll_impl(&self, window: &Window, point: WindowPoint, scroll: Scroll, options: ScrollOptions) -> DriverResult<InputActionResult> {
if matches!(options.policy, InputPolicy::BackgroundOnly) {
return Err(invalid_input("linux window.scroll cannot use background_only input policy"));
}
if matches!(options.policy, InputPolicy::BackgroundPreferred)
&& !options.delivery_strategy.candidates.contains(&ScrollDeliveryCandidate::ForegroundHid)
{
return Err(invalid_input(
"linux window.scroll needs ForegroundHid in the delivery strategy because Wayland background window scroll is not available in this slice",
));
}
let screen_point = self.to_screen_point(window, point)?.point();
let mut result = self.session.input().scroll_at(screen_point, scroll, options.settle)?;
add_foreground_window_fallback_reason(
&mut result,
InputDeliveryPath::WindowTargetedWheel,
"linux window.scroll used foreground RemoteDesktop portal input; Wayland window-targeted background wheel delivery is not available in this slice",
);
Ok(result)
}
}
impl WindowInput for WindowApi<'_> {
fn click(&self, window: &Window, point: WindowPoint, options: ClickOptions) -> DriverResult<InputActionResult> {
let _desktop = auv_driver_common::mouse_input::reserve_desktop_input()?;
self.click_impl(window, point, options)
}
fn scroll(&self, window: &Window, point: WindowPoint, scroll: Scroll, options: ScrollOptions) -> DriverResult<InputActionResult> {
let _desktop = auv_driver_common::mouse_input::reserve_desktop_input()?;
self.scroll_impl(window, point, scroll, options)
}
}
fn add_foreground_window_fallback_reason(result: &mut InputActionResult, unavailable_path: InputDeliveryPath, reason: &str) {
if result.fallback_reason().is_none() {
result.attempts.insert(0, InputAttempt::failure(unavailable_path, reason));
}
}
impl VisionApi<'_> {
pub fn recognize_text_in_capture(&self, capture: &Capture, region: RatioRect) -> DriverResult<TextRecognition> {
self.recognize_text_in_capture_with_options(capture, region, TextRecognitionOptions::default())
}
pub fn recognize_text_in_capture_with_options(
&self,
capture: &Capture,
region: RatioRect,
options: TextRecognitionOptions,
) -> DriverResult<TextRecognition> {
let _ = self.session;
recognize_text_in_capture(capture, region, &options)
}
pub fn find_text_in_capture(&self, capture: &Capture, query: &str, region: RatioRect) -> DriverResult<OcrMatches> {
self.find_text_in_capture_with_options(capture, query, region, TextRecognitionOptions::default())
}
pub fn find_text_in_capture_with_options(
&self,
capture: &Capture,
query: &str,
region: RatioRect,
options: TextRecognitionOptions,
) -> DriverResult<OcrMatches> {
let _ = self.session;
find_text_in_capture(capture, query, region, &options)
}
}
impl AccessibilityApi<'_> {
pub fn snapshot_window(&self, window: &Window) -> DriverResult<AxTreeSnapshot> {
let _ = self.session;
snapshot_window(window)
}
pub fn focus_node(&self, window: &Window, node_path: &str) -> DriverResult<InputActionResult> {
let _ = self.session;
focus_node(window, node_path)
}
pub fn select_node(&self, window: &Window, node_path: &str) -> DriverResult<InputActionResult> {
let _ = self.session;
select_node(window, node_path)
}
}
impl InputApi<'_> {
pub fn drag_mouse(
&self,
request: auv_driver_common::MoveMouseRequest,
button: auv_driver_common::MouseButton,
) -> DriverResult<(Point, InputActionResult)> {
let backend = self.pointer_backend(request.target.as_ref())?;
auv_driver_common::mouse_input::mouse_coordinator().motion(request, Some(button), backend, |_| true)
}
fn pointer_backend(
&self,
target: Option<&auv_driver_common::InputTarget>,
) -> DriverResult<std::sync::Arc<dyn auv_driver_common::mouse_input::MouseBackend>> {
match target {
None | Some(auv_driver_common::InputTarget::Foreground) => {
Ok(std::sync::Arc::new(crate::input::MouseBackend::new(self.session.state.clone())))
}
Some(auv_driver_common::InputTarget::Window(window)) => {
let _ = window;
Err(auv_driver_common::DriverError::unsupported("linux window-targeted mouse input"))
}
Some(auv_driver_common::InputTarget::Application { .. }) => {
Err(auv_driver_common::DriverError::unsupported("mouse input requires a window or foreground target"))
}
}
}
pub fn move_mouse(
&self,
request: auv_driver_common::MoveMouseRequest,
notify: impl FnMut(auv_driver_common::mouse_input::MotionEvent) -> bool,
) -> DriverResult<(Point, InputActionResult)> {
let backend = self.pointer_backend(request.target.as_ref())?;
auv_driver_common::mouse_input::mouse_coordinator().motion(request, None, backend, notify)
}
pub fn hold_mouse(
&self,
target: &auv_driver_common::InputTarget,
mouse: u64,
point: Point,
button: auv_driver_common::MouseButton,
duration: std::time::Duration,
) -> DriverResult<InputActionResult> {
auv_driver_common::mouse_input::mouse_coordinator().hold(mouse, point, button, duration, self.pointer_backend(Some(target))?)
}
pub fn create_mouse(&self) -> DriverResult<u64> {
auv_driver_common::mouse_input::mouse_coordinator().create_mouse()
}
pub fn remove_mouse(&self, mouse: u64) -> DriverResult<InputActionResult> {
auv_driver_common::mouse_input::mouse_coordinator().remove_mouse(mouse)
}
pub fn mouse_down(
&self,
target: &auv_driver_common::InputTarget,
mouse: u64,
point: Point,
button: auv_driver_common::MouseButton,
timeout: std::time::Duration,
) -> DriverResult<InputActionResult> {
auv_driver_common::mouse_input::mouse_coordinator().down(mouse, point, button, timeout, self.pointer_backend(Some(target))?)
}
pub fn mouse_up(&self, mouse: u64) -> DriverResult<InputActionResult> {
auv_driver_common::mouse_input::mouse_coordinator().up(mouse)
}
pub fn move_mouse_to(&self, mouse: u64, point: Point) -> DriverResult<InputActionResult> {
auv_driver_common::mouse_input::mouse_coordinator().move_to(
mouse,
point,
std::sync::Arc::new(crate::input::MouseBackend::new(self.session.state.clone())),
)
}
pub fn current_position(&self) -> DriverResult<Point> {
let _ = self.session;
crate::input::current_position()
}
pub fn move_to(&self, point: Point) -> DriverResult<InputActionResult> {
self.move_mouse_to(0, point)
}
pub fn click_at(
&self,
point: Point,
button: auv_driver_common::MouseButton,
click: Click,
modifiers: auv_driver_common::ClickModifiers,
) -> DriverResult<InputActionResult> {
let _desktop = auv_driver_common::mouse_input::reserve_desktop_input()?;
click_at(&self.session.state, point, button, click, modifiers)
}
pub fn scroll_at(&self, point: Point, scroll: Scroll, settle: std::time::Duration) -> DriverResult<InputActionResult> {
let _desktop = auv_driver_common::mouse_input::reserve_desktop_input()?;
scroll_at(&self.session.state, point, scroll, settle)
}
pub fn type_text(&self, text: &str, options: TypeTextOptions) -> DriverResult<InputActionResult> {
type_text(&self.session.state, text, options)
}
pub fn press_key(&self, options: KeyPressOptions) -> DriverResult<InputActionResult> {
press_key(&self.session.state, options)
}
pub fn copy(&self) -> DriverResult<()> {
copy(&self.session.state)
}
pub fn paste(&self) -> DriverResult<()> {
paste(&self.session.state)
}
pub fn paste_text(&self, options: PasteTextOptions) -> DriverResult<InputActionResult> {
paste_text(&self.session.state, options)
}
}
impl ClipboardApi<'_> {
pub fn snapshot(&self) -> DriverResult<String> {
snapshot(&self.session.state)
}
pub fn restore(&self, snapshot: &str) -> DriverResult<()> {
restore_clipboard(&self.session.state, snapshot)
}
pub fn set_text(&self, text: &str) -> DriverResult<()> {
set_clipboard_text(&self.session.state, text)
}
}
#[cfg(test)]
#[path = "session_test.rs"]
mod tests;