Skip to main content

computer_use/
backend.rs

1use std::ops::Range;
2
3use crate::app::AppInfo;
4use crate::element::RawNode;
5use crate::error::{Error, Result};
6use crate::geometry::Point;
7use crate::keys::KeyChord;
8use crate::screenshot::RawImage;
9use crate::state::WindowInfo;
10
11/// A mouse button.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13#[non_exhaustive]
14pub enum MouseButton {
15    /// Primary button.
16    #[default]
17    Left,
18    /// Secondary button.
19    Right,
20    /// Middle button / wheel click.
21    Middle,
22}
23
24impl std::str::FromStr for MouseButton {
25    type Err = Error;
26
27    fn from_str(s: &str) -> Result<Self> {
28        match s.trim().to_ascii_lowercase().as_str() {
29            "left" | "primary" => Ok(Self::Left),
30            "right" | "secondary" => Ok(Self::Right),
31            "middle" | "center" => Ok(Self::Middle),
32            other => Err(Error::invalid(format!("unknown mouse button {other:?}"))),
33        }
34    }
35}
36
37/// A scroll direction.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39#[non_exhaustive]
40pub enum ScrollDirection {
41    /// Reveal content above.
42    Up,
43    /// Reveal content below.
44    Down,
45    /// Reveal content to the left.
46    Left,
47    /// Reveal content to the right.
48    Right,
49}
50
51impl ScrollDirection {
52    /// Lowercase name.
53    #[must_use]
54    pub const fn as_str(self) -> &'static str {
55        match self {
56            Self::Up => "up",
57            Self::Down => "down",
58            Self::Left => "left",
59            Self::Right => "right",
60        }
61    }
62}
63
64impl std::str::FromStr for ScrollDirection {
65    type Err = Error;
66
67    fn from_str(s: &str) -> Result<Self> {
68        match s.trim().to_ascii_lowercase().as_str() {
69            "up" => Ok(Self::Up),
70            "down" => Ok(Self::Down),
71            "left" => Ok(Self::Left),
72            "right" => Ok(Self::Right),
73            other => Err(Error::invalid(format!("unknown scroll direction {other:?}"))),
74        }
75    }
76}
77
78/// How synthetic input reaches the app.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
80#[non_exhaustive]
81pub enum Delivery {
82    /// Post events directly to the target app without moving the user's pointer or
83    /// changing focus, where the platform allows it. Backends that cannot target an app
84    /// fall back to [`Delivery::Global`].
85    #[default]
86    Background,
87    /// Like [`Delivery::Background`], but the target window is also made active and key
88    /// *inside its own process* for the duration of the input, without changing the real
89    /// frontmost app, raising the window, or moving the pointer.
90    ///
91    /// On macOS this uses private `SkyLight` window-server calls and is what Chromium/Electron
92    /// apps need to accept background clicks and typing. Command-key shortcuts use it automatically.
93    /// Other platforms treat it as [`Delivery::Background`].
94    FocusedBackground,
95    /// Post events through the system input stream. May move the pointer and activate the app.
96    Global,
97}
98
99/// Where synthetic input should be delivered.
100#[derive(Debug, Clone, Copy)]
101pub struct InputTarget<'a> {
102    /// The receiving app (must be running).
103    pub app: &'a AppInfo,
104    /// The receiving window, when known.
105    pub window: Option<&'a WindowInfo>,
106    /// Delivery path.
107    pub delivery: Delivery,
108}
109
110/// Which OS permissions the current process holds.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112#[non_exhaustive]
113pub struct Permissions {
114    /// Accessibility / UI automation access.
115    pub accessibility: bool,
116    /// Screen capture access.
117    pub screen_recording: bool,
118}
119
120impl Permissions {
121    /// Every permission granted.
122    pub const GRANTED: Self = Self { accessibility: true, screen_recording: true };
123
124    /// Creates a permission report.
125    #[must_use]
126    pub const fn new(accessibility: bool, screen_recording: bool) -> Self {
127        Self { accessibility, screen_recording }
128    }
129
130    /// True when nothing is missing.
131    #[must_use]
132    pub const fn all_granted(&self) -> bool {
133        self.accessibility && self.screen_recording
134    }
135}
136
137/// Parameters for [`Backend::capture`].
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[non_exhaustive]
140pub struct CaptureRequest {
141    /// Walk the accessibility tree.
142    pub include_tree: bool,
143    /// Capture window pixels.
144    pub include_screenshot: bool,
145    /// Maximum raw nodes to visit.
146    pub max_nodes: usize,
147    /// Maximum depth to visit.
148    pub max_depth: usize,
149    /// Capture the window with this [`WindowInfo::id`] instead of the focused or main window.
150    /// Backends that cannot honor it must fail rather than capture another window.
151    pub window: Option<u64>,
152    /// Also walk the app's menu bar when it lives outside the window (macOS). Its nodes follow
153    /// the window's, with `parent: None` on the menu bar itself. Counts toward `max_nodes`.
154    pub include_menu_bar: bool,
155}
156
157impl CaptureRequest {
158    /// Creates a request.
159    #[must_use]
160    pub const fn new(include_tree: bool, include_screenshot: bool, max_nodes: usize, max_depth: usize) -> Self {
161        Self { include_tree, include_screenshot, max_nodes, max_depth, window: None, include_menu_bar: false }
162    }
163
164    /// Sets [`window`](Self::window).
165    #[must_use]
166    pub const fn with_window(mut self, window: Option<u64>) -> Self {
167        self.window = window;
168        self
169    }
170
171    /// Sets [`include_menu_bar`](Self::include_menu_bar).
172    #[must_use]
173    pub const fn with_menu_bar(mut self, yes: bool) -> Self {
174        self.include_menu_bar = yes;
175        self
176    }
177}
178
179/// What a backend returns from [`Backend::capture`].
180#[derive(Debug, Clone)]
181#[non_exhaustive]
182pub struct Capture<H> {
183    /// The captured window.
184    pub window: WindowInfo,
185    /// Accessibility nodes in depth-first pre-order, rooted at the window.
186    pub nodes: Vec<RawNode<H>>,
187    /// Whether limits cut the walk short.
188    pub truncated: bool,
189    /// Index into `nodes` of the focused element.
190    pub focused: Option<usize>,
191    /// Selected text in the focused element.
192    pub selected_text: Option<String>,
193    /// Window pixels; the image must cover exactly `window.frame`.
194    pub screenshot: Option<RawImage>,
195    /// Non-fatal problems worth surfacing, e.g. why a screenshot is missing.
196    pub notes: Vec<String>,
197}
198
199impl<H> Capture<H> {
200    /// A capture of `window` with nothing else filled in.
201    #[must_use]
202    pub const fn new(window: WindowInfo) -> Self {
203        Self {
204            window,
205            nodes: Vec::new(),
206            truncated: false,
207            focused: None,
208            selected_text: None,
209            screenshot: None,
210            notes: Vec::new(),
211        }
212    }
213}
214
215/// OS primitives for observing and driving apps.
216///
217/// All coordinates are screen points with a top-left origin. Methods with default
218/// implementations return [`Error::Unsupported`].
219pub trait Backend {
220    /// Handle to a live accessibility element (e.g. `AXUIElement`, `IUIAutomationElement`).
221    type Handle: Clone;
222
223    /// Running apps plus installed apps the user can launch.
224    ///
225    /// # Errors
226    ///
227    /// Fails if the platform's process or app registry cannot be read.
228    fn list_apps(&self) -> Result<Vec<AppInfo>>;
229
230    /// Starts `app` without bringing it to the foreground and returns its running record.
231    ///
232    /// # Errors
233    ///
234    /// Fails if the app cannot be started or does not appear in time.
235    fn launch_app(&self, app: &AppInfo) -> Result<AppInfo> {
236        Err(Error::Unsupported(format!("launching {} is not supported by this backend", app.name)))
237    }
238
239    /// Permissions currently held by this process.
240    fn permissions(&self) -> Permissions {
241        Permissions::GRANTED
242    }
243
244    /// Asks the OS to prompt the user for missing permissions, then reports the current state.
245    fn request_permissions(&self) -> Permissions {
246        self.permissions()
247    }
248
249    /// The app's windows, front to back where the platform knows the order. Each one with an
250    /// [`id`](WindowInfo::id) can be passed back as [`CaptureRequest::window`].
251    ///
252    /// Unsupported by default.
253    ///
254    /// # Errors
255    ///
256    /// Fails if the app is not running or permissions are missing.
257    fn list_windows(&self, app: &AppInfo) -> Result<Vec<WindowInfo>> {
258        let _ = app;
259        Err(Error::Unsupported("listing windows".into()))
260    }
261
262    /// Captures the app's current window, or the one [`CaptureRequest::window`] names.
263    ///
264    /// # Errors
265    ///
266    /// Fails if the app has no usable window (or not the requested one) or permissions are
267    /// missing.
268    fn capture(&self, app: &AppInfo, request: &CaptureRequest) -> Result<Capture<Self::Handle>>;
269
270    /// Invokes a named accessibility action (as listed in [`Action::name`](crate::Action::name)).
271    ///
272    /// # Errors
273    ///
274    /// Fails if the element is gone or rejects the action.
275    fn perform_action(&self, element: &Self::Handle, action: &str) -> Result<()>;
276
277    /// Writes an element's value.
278    ///
279    /// # Errors
280    ///
281    /// Fails if the value is not settable.
282    fn set_value(&self, element: &Self::Handle, value: &str) -> Result<()>;
283
284    /// Reads an element's full, untruncated value.
285    ///
286    /// # Errors
287    ///
288    /// Fails if the element is gone.
289    fn value(&self, element: &Self::Handle) -> Result<Option<String>> {
290        let _ = element;
291        Err(Error::Unsupported("reading element values".into()))
292    }
293
294    /// Selects `range` (byte offsets into `value`, the element's current value).
295    /// An empty range places the caret.
296    ///
297    /// # Errors
298    ///
299    /// Fails if the element does not support text selection.
300    fn select_text(&self, element: &Self::Handle, value: &str, range: Range<usize>) -> Result<()> {
301        let _ = (element, value, range);
302        Err(Error::Unsupported("text selection".into()))
303    }
304
305    /// Inserts text at the caret of the focused element without synthesizing keystrokes.
306    /// Returns `Ok(false)` when the focused element does not support it, so the caller can
307    /// fall back to [`Backend::type_text`].
308    ///
309    /// # Errors
310    ///
311    /// Fails if insertion was attempted and rejected.
312    fn insert_text(&self, app: &AppInfo, text: &str) -> Result<bool> {
313        let _ = (app, text);
314        Ok(false)
315    }
316
317    /// Clicks `count` times at a screen point.
318    ///
319    /// # Errors
320    ///
321    /// Fails if the events cannot be posted.
322    fn click(&self, target: InputTarget<'_>, point: Point, button: MouseButton, count: u32) -> Result<()>;
323
324    /// Scrolls at a screen point by `(dx, dy)` points; positive `dy` reveals content below.
325    ///
326    /// # Errors
327    ///
328    /// Fails if the events cannot be posted.
329    fn scroll(&self, target: InputTarget<'_>, point: Point, dx: f64, dy: f64) -> Result<()>;
330
331    /// Presses the primary button at `from`, moves to `to`, and releases.
332    ///
333    /// # Errors
334    ///
335    /// Fails if the events cannot be posted.
336    fn drag(&self, target: InputTarget<'_>, from: Point, to: Point) -> Result<()>;
337
338    /// Types literal text.
339    ///
340    /// # Errors
341    ///
342    /// Fails if the events cannot be posted.
343    fn type_text(&self, target: InputTarget<'_>, text: &str) -> Result<()>;
344
345    /// Presses and releases a key chord.
346    ///
347    /// # Errors
348    ///
349    /// Fails if the key has no mapping on this platform or events cannot be posted.
350    fn press_key(&self, target: InputTarget<'_>, chord: &KeyChord) -> Result<()>;
351}