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}