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}
150
151impl CaptureRequest {
152 /// Creates a request.
153 #[must_use]
154 pub const fn new(include_tree: bool, include_screenshot: bool, max_nodes: usize, max_depth: usize) -> Self {
155 Self { include_tree, include_screenshot, max_nodes, max_depth }
156 }
157}
158
159/// What a backend returns from [`Backend::capture`].
160#[derive(Debug, Clone)]
161#[non_exhaustive]
162pub struct Capture<H> {
163 /// The captured window.
164 pub window: WindowInfo,
165 /// Accessibility nodes in depth-first pre-order, rooted at the window.
166 pub nodes: Vec<RawNode<H>>,
167 /// Whether limits cut the walk short.
168 pub truncated: bool,
169 /// Index into `nodes` of the focused element.
170 pub focused: Option<usize>,
171 /// Selected text in the focused element.
172 pub selected_text: Option<String>,
173 /// Window pixels; the image must cover exactly `window.frame`.
174 pub screenshot: Option<RawImage>,
175 /// Non-fatal problems worth surfacing, e.g. why a screenshot is missing.
176 pub notes: Vec<String>,
177}
178
179impl<H> Capture<H> {
180 /// A capture of `window` with nothing else filled in.
181 #[must_use]
182 pub const fn new(window: WindowInfo) -> Self {
183 Self {
184 window,
185 nodes: Vec::new(),
186 truncated: false,
187 focused: None,
188 selected_text: None,
189 screenshot: None,
190 notes: Vec::new(),
191 }
192 }
193}
194
195/// OS primitives for observing and driving apps.
196///
197/// All coordinates are screen points with a top-left origin. Methods with default
198/// implementations return [`Error::Unsupported`].
199pub trait Backend {
200 /// Handle to a live accessibility element (e.g. `AXUIElement`, `IUIAutomationElement`).
201 type Handle: Clone;
202
203 /// Running apps plus installed apps the user can launch.
204 ///
205 /// # Errors
206 ///
207 /// Fails if the platform's process or app registry cannot be read.
208 fn list_apps(&self) -> Result<Vec<AppInfo>>;
209
210 /// Starts `app` without bringing it to the foreground and returns its running record.
211 ///
212 /// # Errors
213 ///
214 /// Fails if the app cannot be started or does not appear in time.
215 fn launch_app(&self, app: &AppInfo) -> Result<AppInfo> {
216 Err(Error::Unsupported(format!("launching {} is not supported by this backend", app.name)))
217 }
218
219 /// Permissions currently held by this process.
220 fn permissions(&self) -> Permissions {
221 Permissions::GRANTED
222 }
223
224 /// Asks the OS to prompt the user for missing permissions, then reports the current state.
225 fn request_permissions(&self) -> Permissions {
226 self.permissions()
227 }
228
229 /// Captures the app's current window.
230 ///
231 /// # Errors
232 ///
233 /// Fails if the app has no usable window or permissions are missing.
234 fn capture(&self, app: &AppInfo, request: &CaptureRequest) -> Result<Capture<Self::Handle>>;
235
236 /// Invokes a named accessibility action (as listed in [`Action::name`](crate::Action::name)).
237 ///
238 /// # Errors
239 ///
240 /// Fails if the element is gone or rejects the action.
241 fn perform_action(&self, element: &Self::Handle, action: &str) -> Result<()>;
242
243 /// Writes an element's value.
244 ///
245 /// # Errors
246 ///
247 /// Fails if the value is not settable.
248 fn set_value(&self, element: &Self::Handle, value: &str) -> Result<()>;
249
250 /// Reads an element's full, untruncated value.
251 ///
252 /// # Errors
253 ///
254 /// Fails if the element is gone.
255 fn value(&self, element: &Self::Handle) -> Result<Option<String>> {
256 let _ = element;
257 Err(Error::Unsupported("reading element values".into()))
258 }
259
260 /// Selects `range` (byte offsets into `value`, the element's current value).
261 /// An empty range places the caret.
262 ///
263 /// # Errors
264 ///
265 /// Fails if the element does not support text selection.
266 fn select_text(&self, element: &Self::Handle, value: &str, range: Range<usize>) -> Result<()> {
267 let _ = (element, value, range);
268 Err(Error::Unsupported("text selection".into()))
269 }
270
271 /// Inserts text at the caret of the focused element without synthesizing keystrokes.
272 /// Returns `Ok(false)` when the focused element does not support it, so the caller can
273 /// fall back to [`Backend::type_text`].
274 ///
275 /// # Errors
276 ///
277 /// Fails if insertion was attempted and rejected.
278 fn insert_text(&self, app: &AppInfo, text: &str) -> Result<bool> {
279 let _ = (app, text);
280 Ok(false)
281 }
282
283 /// Clicks `count` times at a screen point.
284 ///
285 /// # Errors
286 ///
287 /// Fails if the events cannot be posted.
288 fn click(&self, target: InputTarget<'_>, point: Point, button: MouseButton, count: u32) -> Result<()>;
289
290 /// Scrolls at a screen point by `(dx, dy)` points; positive `dy` reveals content below.
291 ///
292 /// # Errors
293 ///
294 /// Fails if the events cannot be posted.
295 fn scroll(&self, target: InputTarget<'_>, point: Point, dx: f64, dy: f64) -> Result<()>;
296
297 /// Presses the primary button at `from`, moves to `to`, and releases.
298 ///
299 /// # Errors
300 ///
301 /// Fails if the events cannot be posted.
302 fn drag(&self, target: InputTarget<'_>, from: Point, to: Point) -> Result<()>;
303
304 /// Types literal text.
305 ///
306 /// # Errors
307 ///
308 /// Fails if the events cannot be posted.
309 fn type_text(&self, target: InputTarget<'_>, text: &str) -> Result<()>;
310
311 /// Presses and releases a key chord.
312 ///
313 /// # Errors
314 ///
315 /// Fails if the key has no mapping on this platform or events cannot be posted.
316 fn press_key(&self, target: InputTarget<'_>, chord: &KeyChord) -> Result<()>;
317}