Skip to main content

computer_use/
computer.rs

1use std::collections::HashMap;
2use std::thread;
3use std::time::{Duration, SystemTime};
4
5use crate::app::{self, AppInfo};
6use crate::backend::{Backend, CaptureRequest, Delivery, InputTarget, MouseButton, Permissions, ScrollDirection};
7use crate::element::{Element, build_tree};
8use crate::error::{Error, Result};
9use crate::geometry::{Point, Rect};
10use crate::keys::KeyChord;
11use crate::screenshot::Screenshot;
12use crate::state::{AppState, CaptureOptions, ElementQuery, WindowInfo, WindowSelector};
13use crate::text::TextSelection;
14
15/// What to click.
16#[derive(Debug, Clone, Copy, PartialEq)]
17#[non_exhaustive]
18pub enum ClickTarget {
19    /// An element index from the latest [`AppState`].
20    Element(usize),
21    /// A pixel position in the latest screenshot.
22    Pixel {
23        /// Horizontal pixel.
24        x: f64,
25        /// Vertical pixel.
26        y: f64,
27    },
28    /// A point in screen coordinates (top-left origin). Needs no earlier capture.
29    Screen {
30        /// Horizontal screen point.
31        x: f64,
32        /// Vertical screen point.
33        y: f64,
34    },
35}
36
37/// How a click is performed.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
39#[non_exhaustive]
40pub enum ClickMethod {
41    /// Use the element's accessibility action when that is equivalent, otherwise synthesize
42    /// pointer events with the computer's default [`Delivery`].
43    #[default]
44    Auto,
45    /// Only use accessibility actions; requires an element target.
46    Accessibility,
47    /// Synthesize pointer events delivered to the app in the background.
48    Background,
49    /// Like `Background`, with the target window given synthetic focus first
50    /// (see [`Delivery::FocusedBackground`]).
51    FocusedBackground,
52    /// Synthesize pointer events through the global input stream.
53    Global,
54}
55
56impl std::str::FromStr for ClickMethod {
57    type Err = Error;
58
59    fn from_str(s: &str) -> Result<Self> {
60        match s.trim().to_ascii_lowercase().as_str() {
61            "auto" => Ok(Self::Auto),
62            "accessibility" | "ax" => Ok(Self::Accessibility),
63            "background" => Ok(Self::Background),
64            "focused_background" | "focused-background" => Ok(Self::FocusedBackground),
65            "global" => Ok(Self::Global),
66            other => Err(Error::invalid(format!("unknown click method {other:?}"))),
67        }
68    }
69}
70
71/// Options for [`Computer::click`].
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct ClickOptions {
74    /// Button to press.
75    pub button: MouseButton,
76    /// Number of clicks (2 for a double click).
77    pub count: u32,
78    /// Strategy.
79    pub method: ClickMethod,
80}
81
82impl Default for ClickOptions {
83    fn default() -> Self {
84        Self { button: MouseButton::Left, count: 1, method: ClickMethod::Auto }
85    }
86}
87
88impl ClickOptions {
89    /// Sets the button.
90    #[must_use]
91    pub const fn button(mut self, button: MouseButton) -> Self {
92        self.button = button;
93        self
94    }
95
96    /// Sets the click count.
97    #[must_use]
98    pub const fn count(mut self, count: u32) -> Self {
99        self.count = count;
100        self
101    }
102
103    /// Sets the method.
104    #[must_use]
105    pub const fn method(mut self, method: ClickMethod) -> Self {
106        self.method = method;
107        self
108    }
109}
110
111/// How a click was ultimately delivered.
112#[derive(Debug, Clone, PartialEq, Eq)]
113#[non_exhaustive]
114pub enum ClickOutcome {
115    /// An accessibility action was invoked.
116    Action(String),
117    /// Pointer events were synthesized.
118    Pointer(Delivery),
119}
120
121struct Session<H> {
122    state: AppState,
123    handles: Vec<H>,
124    /// The window explicitly requested for this state, if any.
125    window: Option<u64>,
126}
127
128/// A computer-use session over a [`Backend`].
129///
130/// Observe an app with [`get_app_state`](Self::get_app_state), then act on it by element index or by
131/// screenshot pixel. It remembers the latest state per app so indexes stay valid until the
132/// next capture of that app.
133///
134/// ```no_run
135/// # #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
136/// # fn main() -> computer_use::Result<()> {
137/// use computer_use::{CaptureOptions, ClickOptions, ClickTarget, Computer};
138///
139/// let mut computer = Computer::new()?;
140/// let state = computer.get_app_state("TextEdit", &CaptureOptions::default())?;
141/// println!("{}", state.render());
142///
143/// computer.click("TextEdit", ClickTarget::Element(3), ClickOptions::default())?;
144/// computer.type_text("TextEdit", "Hello from Rust")?;
145/// computer.press_key("TextEdit", "super+s")?;
146/// # Ok(())
147/// # }
148/// # #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
149/// # fn main() {}
150/// ```
151pub struct Computer<B: Backend> {
152    backend: B,
153    sessions: HashMap<String, Session<B::Handle>>,
154    aliases: HashMap<String, String>,
155    delivery: Delivery,
156    settle_delay: Duration,
157}
158
159impl<B: Backend> std::fmt::Debug for Computer<B> {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("Computer")
162            .field("sessions", &self.sessions.keys().collect::<Vec<_>>())
163            .field("delivery", &self.delivery)
164            .field("settle_delay", &self.settle_delay)
165            .finish_non_exhaustive()
166    }
167}
168
169#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
170impl Computer<crate::NativeBackend> {
171    /// Creates a session over this platform's native backend.
172    ///
173    /// # Errors
174    ///
175    /// Fails if the platform's accessibility stack cannot be initialized.
176    pub fn new() -> Result<Self> {
177        Ok(Self::with_backend(crate::NativeBackend::new()?))
178    }
179}
180
181impl<B: Backend> Computer<B> {
182    /// Creates a session over a custom backend.
183    #[must_use]
184    pub fn with_backend(backend: B) -> Self {
185        Self {
186            backend,
187            sessions: HashMap::new(),
188            aliases: HashMap::new(),
189            delivery: Delivery::Background,
190            settle_delay: Duration::ZERO,
191        }
192    }
193
194    /// Sets the default input delivery for synthesized events.
195    #[must_use]
196    pub const fn delivery(mut self, delivery: Delivery) -> Self {
197        self.delivery = delivery;
198        self
199    }
200
201    /// Sleeps this long after every action so the UI can settle before the next capture.
202    #[must_use]
203    pub const fn settle_delay(mut self, delay: Duration) -> Self {
204        self.settle_delay = delay;
205        self
206    }
207
208    /// The underlying backend, for platform-specific calls.
209    pub const fn backend(&self) -> &B {
210        &self.backend
211    }
212
213    /// Permissions currently held by this process.
214    pub fn permissions(&self) -> Permissions {
215        self.backend.permissions()
216    }
217
218    /// Prompts for missing permissions where the OS supports it.
219    pub fn request_permissions(&self) -> Permissions {
220        self.backend.request_permissions()
221    }
222
223    /// Lists running apps first (frontmost first), then installed apps, by name.
224    ///
225    /// # Errors
226    ///
227    /// Fails if the platform cannot enumerate apps.
228    pub fn list_apps(&self) -> Result<Vec<AppInfo>> {
229        let mut apps = self.backend.list_apps()?;
230        apps.sort_by(|a, b| {
231            (!a.is_running, !a.is_active, a.name.to_lowercase()).cmp(&(
232                !b.is_running,
233                !b.is_active,
234                b.name.to_lowercase(),
235            ))
236        });
237        Ok(apps)
238    }
239
240    /// Resolves an app query (name, bundle ID, path, or `pid:<n>`), preferring running apps.
241    ///
242    /// # Errors
243    ///
244    /// Returns [`Error::AppNotFound`] when nothing matches.
245    pub fn resolve_app(&self, query: &str) -> Result<AppInfo> {
246        let apps = self.backend.list_apps()?;
247        app::best_match(&apps, query).cloned().ok_or_else(|| Error::AppNotFound(query.to_owned()))
248    }
249
250    /// Captures the app's current window, replacing any earlier state (and element indexes)
251    /// for that app.
252    ///
253    /// # Errors
254    ///
255    /// Fails if the app cannot be found or launched, has no window, or permissions are missing.
256    pub fn get_app_state(&mut self, app: &str, options: &CaptureOptions) -> Result<&AppState> {
257        let info = self.running_app(app, options.launch_if_needed)?;
258        let window = match &options.window {
259            None => None,
260            Some(WindowSelector::Id(id)) => Some(*id),
261            Some(WindowSelector::Title(title)) => Some(self.window_by_title(&info, title)?),
262        };
263        let request =
264            CaptureRequest::new(options.include_tree, options.include_screenshot, options.max_nodes, options.max_depth)
265                .with_window(window)
266                .with_menu_bar(options.include_menu_bar);
267        let capture = self.backend.capture(&info, &request)?;
268        let (elements, handles, raw_to_element) = build_tree(capture.nodes, options.text_limit);
269        let focused = capture.focused.and_then(|raw| raw_to_element.get(raw).copied().flatten());
270        let screenshot = capture
271            .screenshot
272            .map(|image| Screenshot::encode(image, capture.window.frame, options.max_screenshot_dimension))
273            .transpose()?;
274        let state = AppState {
275            app: info,
276            window: capture.window,
277            elements,
278            truncated: capture.truncated,
279            tree_included: options.include_tree,
280            focused,
281            selected_text: capture.selected_text.map(|s| crate::element::sanitize(&s, options.text_limit)),
282            screenshot,
283            captured_at: SystemTime::now(),
284            notes: capture.notes,
285        };
286        let key = self.remember(app, &state.app);
287        let session = Session { state, handles, window };
288        Ok(&self.sessions.entry(key).insert_entry(session).into_mut().state)
289    }
290
291    /// The app's windows, front to back where the platform knows the order. Pass one to
292    /// [`CaptureOptions::window`] to capture it instead of the focused or main window.
293    ///
294    /// # Errors
295    ///
296    /// Fails if the app is not running or the backend cannot list windows.
297    pub fn list_windows(&self, app: &str) -> Result<Vec<WindowInfo>> {
298        let info = self.running_app(app, false)?;
299        self.backend.list_windows(&info)
300    }
301
302    /// The latest captured state of an app, if any.
303    #[must_use]
304    pub fn state(&self, app: &str) -> Option<&AppState> {
305        self.session_for(app).ok().map(|s| &s.state)
306    }
307
308    /// Discards the remembered state of one app.
309    pub fn forget(&mut self, app: &str) {
310        if let Some(key) = self.aliases.remove(&alias_key(app)) {
311            self.sessions.remove(&key);
312            self.aliases.retain(|_, v| *v != key);
313        }
314    }
315
316    /// Discards all remembered state.
317    pub fn clear(&mut self) {
318        self.sessions.clear();
319        self.aliases.clear();
320    }
321
322    /// Finds elements in the app's current window without taking a screenshot.
323    ///
324    /// The fresh tree replaces the app's remembered state, so the returned indexes can be used
325    /// with element actions. A previous screenshot is kept when the window has not moved.
326    ///
327    /// # Errors
328    ///
329    /// Fails like [`get_app_state`](Self::get_app_state) or if the query is empty.
330    pub fn query(&mut self, app: &str, query: &ElementQuery, max_nodes: usize) -> Result<Vec<Element>> {
331        if query.text.as_deref().is_none_or(str::is_empty) && query.role.as_deref().is_none_or(str::is_empty) {
332            return Err(Error::invalid("query requires text and/or role"));
333        }
334        let previous = self.session_for(app).ok().map(|s| (s.state.window.frame, s.state.screenshot.clone()));
335        let mut options =
336            CaptureOptions::default().include_screenshot(false).max_nodes(max_nodes).launch_if_needed(false);
337        // Stay on the window the last capture chose, if it was one other than the default.
338        options.window = self.session_for(app).ok().and_then(|s| s.window.map(WindowSelector::Id));
339        let state = self.get_app_state(app, &options)?;
340        let matches: Vec<Element> = state.find(query).cloned().collect();
341        let frame = state.window.frame;
342        if let Some((prev_frame, Some(shot))) = previous
343            && prev_frame == frame
344            && let Ok(session) = self.session_for_mut(app)
345        {
346            session.state.screenshot = Some(shot);
347        }
348        Ok(matches)
349    }
350
351    /// Clicks an element, a screenshot pixel, or a screen point.
352    ///
353    /// # Errors
354    ///
355    /// Fails if there is no state for the app, the target is invalid, or delivery fails.
356    pub fn click(&mut self, app: &str, target: ClickTarget, options: ClickOptions) -> Result<ClickOutcome> {
357        if options.count == 0 {
358            return Err(Error::invalid("click count must be at least 1"));
359        }
360        let delivery = match options.method {
361            ClickMethod::Global => Delivery::Global,
362            ClickMethod::Background => Delivery::Background,
363            ClickMethod::FocusedBackground => Delivery::FocusedBackground,
364            ClickMethod::Auto | ClickMethod::Accessibility => self.delivery,
365        };
366        let index = match target {
367            ClickTarget::Element(index) => index,
368            ClickTarget::Pixel { x, y } | ClickTarget::Screen { x, y } => {
369                if options.method == ClickMethod::Accessibility {
370                    return Err(Error::invalid("accessibility clicks require an element target"));
371                }
372                let screen = matches!(target, ClickTarget::Screen { .. });
373                self.with_pointer(app, (x, y), screen, delivery, |backend, input, point| {
374                    backend.click(input, point, options.button, options.count)
375                })?;
376                return Ok(ClickOutcome::Pointer(delivery));
377            }
378        };
379        let session = self.session_for(app)?;
380        let element = element(session, app, index)?;
381        if matches!(options.method, ClickMethod::Auto | ClickMethod::Accessibility)
382            && let Some(action) = equivalent_action(element, options)
383        {
384            let name = action.name.clone();
385            self.backend.perform_action(&session.handles[index], &name)?;
386            self.settle();
387            return Ok(ClickOutcome::Action(name));
388        }
389        if options.method == ClickMethod::Accessibility {
390            return Err(Error::action(format!(
391                "element {index} has no accessibility action for a {:?} click",
392                options.button
393            )));
394        }
395        let point = element
396            .frame
397            .filter(|f| !f.is_empty())
398            .map(|f| f.center())
399            .ok_or_else(|| Error::StateUnavailable(format!("element {index} has no on-screen frame")))?;
400        let input = input_target(&session.state, delivery);
401        self.backend.click(input, point, options.button, options.count)?;
402        self.settle();
403        Ok(ClickOutcome::Pointer(delivery))
404    }
405
406    /// Invokes a named accessibility action on an element (e.g. `Show Menu`, `AXRaise`).
407    ///
408    /// # Errors
409    ///
410    /// Fails if the element does not expose the action.
411    pub fn perform_secondary_action(&mut self, app: &str, index: usize, action: &str) -> Result<()> {
412        let session = self.session_for(app)?;
413        let element = element(session, app, index)?;
414        let found = element.action(action).ok_or_else(|| {
415            let available: Vec<&str> = element.actions.iter().map(|a| a.label.as_str()).collect();
416            Error::invalid(format!(
417                "{action:?} is not a valid action for element {index}; available: [{}]",
418                available.join(", ")
419            ))
420        })?;
421        self.backend.perform_action(&session.handles[index], &found.name)?;
422        self.settle();
423        Ok(())
424    }
425
426    /// Scrolls an element by a number of pages (fractions allowed).
427    ///
428    /// Whole pages use the element's own scroll-by-page action when it has one; otherwise
429    /// wheel events are posted at the element's center.
430    ///
431    /// # Errors
432    ///
433    /// Fails if `pages` is not positive or the element has no frame.
434    pub fn scroll(&mut self, app: &str, index: usize, direction: ScrollDirection, pages: f64) -> Result<()> {
435        if !pages.is_finite() || pages <= 0.0 {
436            return Err(Error::invalid("pages must be a positive number"));
437        }
438        let session = self.session_for(app)?;
439        let element = element(session, app, index)?;
440        let by_page = format!("Scroll {} By Page", direction.as_str());
441        if pages.fract() == 0.0
442            && pages <= 100.0
443            && let Some(action) = element.action(&by_page)
444        {
445            for _ in 0..whole_pages(pages) {
446                self.backend.perform_action(&session.handles[index], &action.name)?;
447            }
448            self.settle();
449            return Ok(());
450        }
451        let frame = element
452            .frame
453            .filter(|f| !f.is_empty())
454            .ok_or_else(|| Error::StateUnavailable(format!("element {index} has no scrollable frame")))?;
455        let (dx, dy) = wheel_delta(frame, direction, pages);
456        let input = input_target(&session.state, self.delivery);
457        self.backend.scroll(input, frame.center(), dx, dy)?;
458        self.settle();
459        Ok(())
460    }
461
462    /// Scrolls at an element, a screenshot pixel, or a screen point, by a number of pages
463    /// (fractions allowed).
464    ///
465    /// An element target behaves like [`scroll`](Self::scroll). For a point, wheel events are
466    /// posted there; a page is 80% of the scroll area under the point when the latest state
467    /// has one, else of the captured window, else 400 points.
468    ///
469    /// # Errors
470    ///
471    /// Fails if `pages` is not positive, a pixel lies outside the screenshot, or delivery fails.
472    pub fn scroll_at(&mut self, app: &str, target: ClickTarget, direction: ScrollDirection, pages: f64) -> Result<()> {
473        let (x, y, screen) = match target {
474            ClickTarget::Element(index) => return self.scroll(app, index, direction, pages),
475            ClickTarget::Pixel { x, y } => (x, y, false),
476            ClickTarget::Screen { x, y } => (x, y, true),
477        };
478        if !pages.is_finite() || pages <= 0.0 {
479            return Err(Error::invalid("pages must be a positive number"));
480        }
481        let state = self.session_for(app).ok().map(|s| &s.state);
482        let page_frame = |point: Point| {
483            state.and_then(|s| scroll_area_at(s, point).or(Some(s.window.frame))).filter(|f| !f.is_empty())
484        };
485        let delivery = self.delivery;
486        self.with_pointer(app, (x, y), screen, delivery, |backend, input, point| {
487            let frame = page_frame(point).unwrap_or(Rect::new(0.0, 0.0, FALLBACK_PAGE, FALLBACK_PAGE));
488            let (dx, dy) = wheel_delta(frame, direction, pages);
489            backend.scroll(input, point, dx, dy)
490        })
491    }
492
493    /// Drags between two screenshot pixels.
494    ///
495    /// # Errors
496    ///
497    /// Fails if there is no screenshot or a point lies outside it.
498    pub fn drag(&mut self, app: &str, from: (f64, f64), to: (f64, f64)) -> Result<()> {
499        let session = self.session_for(app)?;
500        let start = screenshot_point(&session.state, from.0, from.1)?;
501        let end = screenshot_point(&session.state, to.0, to.1)?;
502        let input = input_target(&session.state, self.delivery);
503        self.backend.drag(input, start, end)?;
504        self.settle();
505        Ok(())
506    }
507
508    /// Drags between two screen points. Needs no earlier capture.
509    ///
510    /// # Errors
511    ///
512    /// Fails if the app is not running or delivery fails.
513    pub fn drag_screen(&mut self, app: &str, from: Point, to: Point) -> Result<()> {
514        let delivery = self.delivery;
515        self.with_pointer(app, (from.x, from.y), true, delivery, |backend, input, start| backend.drag(input, start, to))
516    }
517
518    /// Types literal text into the app's focused element.
519    ///
520    /// With background delivery, text is inserted through accessibility when the focused
521    /// element supports it, and synthesized as keystrokes otherwise.
522    ///
523    /// # Errors
524    ///
525    /// Fails if the app is not running or delivery fails.
526    pub fn type_text(&mut self, app: &str, text: &str) -> Result<()> {
527        if text.is_empty() {
528            return Ok(());
529        }
530        let (info, window) = self.input_context(app)?;
531        if self.delivery != Delivery::Global && self.backend.insert_text(&info, text)? {
532            self.settle();
533            return Ok(());
534        }
535        let target = InputTarget { app: &info, window: window.as_ref(), delivery: self.delivery };
536        self.backend.type_text(target, text)?;
537        self.settle();
538        Ok(())
539    }
540
541    /// Presses a key chord in xdotool syntax, e.g. `Return`, `super+c`, `KP_0`.
542    ///
543    /// # Errors
544    ///
545    /// Fails if the chord cannot be parsed or delivered.
546    pub fn press_key(&mut self, app: &str, key: &str) -> Result<()> {
547        let chord: KeyChord = key.parse()?;
548        let (info, window) = self.input_context(app)?;
549        let target = InputTarget { app: &info, window: window.as_ref(), delivery: self.delivery };
550        self.backend.press_key(target, &chord)?;
551        self.settle();
552        Ok(())
553    }
554
555    /// Sets the value of a settable element.
556    ///
557    /// # Errors
558    ///
559    /// Fails if the element's value is read-only.
560    pub fn set_value(&mut self, app: &str, index: usize, value: &str) -> Result<()> {
561        let session = self.session_for(app)?;
562        element(session, app, index)?;
563        self.backend.set_value(&session.handles[index], value)?;
564        self.settle();
565        Ok(())
566    }
567
568    /// Selects text inside a text element, or places the caret before or after it.
569    ///
570    /// # Errors
571    ///
572    /// Fails if the text is missing or ambiguous, or the element does not support selection.
573    pub fn select_text(&mut self, app: &str, index: usize, selection: &TextSelection) -> Result<()> {
574        let session = self.session_for(app)?;
575        let cached = element(session, app, index)?;
576        let handle = &session.handles[index];
577        let value = match self.backend.value(handle) {
578            Ok(Some(value)) => value,
579            Ok(None) => return Err(Error::StateUnavailable(format!("element {index} has no text value"))),
580            Err(Error::Unsupported(_)) => cached
581                .value
582                .clone()
583                .ok_or_else(|| Error::StateUnavailable(format!("element {index} has no text value")))?,
584            Err(e) => return Err(e),
585        };
586        let range = selection.resolve(&value)?;
587        self.backend.select_text(handle, &value, range)?;
588        self.settle();
589        Ok(())
590    }
591
592    /// Resolves a screenshot pixel (or a screen point when `screen`) and runs a pointer action
593    /// there, then settles. Screen points use the remembered window when there is one, and
594    /// need no capture otherwise.
595    fn with_pointer(
596        &self,
597        app: &str,
598        (x, y): (f64, f64),
599        screen: bool,
600        delivery: Delivery,
601        act: impl FnOnce(&B, InputTarget<'_>, Point) -> Result<()>,
602    ) -> Result<()> {
603        if screen {
604            if !x.is_finite() || !y.is_finite() {
605                return Err(Error::invalid(format!("({x}, {y}) is not a screen point")));
606            }
607            let (info, window) = self.input_context(app)?;
608            act(&self.backend, InputTarget { app: &info, window: window.as_ref(), delivery }, Point::new(x, y))?;
609        } else {
610            let session = self.session_for(app)?;
611            let point = screenshot_point(&session.state, x, y)?;
612            act(&self.backend, input_target(&session.state, delivery), point)?;
613        }
614        self.settle();
615        Ok(())
616    }
617
618    fn window_by_title(&self, app: &AppInfo, title: &str) -> Result<u64> {
619        let wanted = title.trim().to_lowercase();
620        let windows = self.backend.list_windows(app)?;
621        let titled = |w: &&WindowInfo| w.id.is_some() && w.title.is_some();
622        let lower = |w: &WindowInfo| w.title.as_deref().unwrap_or_default().to_lowercase();
623        windows
624            .iter()
625            .filter(titled)
626            .find(|w| lower(w) == wanted)
627            .or_else(|| windows.iter().filter(titled).find(|w| lower(w).contains(&wanted)))
628            .and_then(|w| w.id)
629            .ok_or_else(|| Error::WindowNotFound(format!("{} (no window titled {title:?})", app.name)))
630    }
631
632    fn running_app(&self, query: &str, launch: bool) -> Result<AppInfo> {
633        let info = self.resolve_app(query)?;
634        if info.is_running {
635            Ok(info)
636        } else if launch {
637            self.backend.launch_app(&info)
638        } else {
639            Err(Error::AppNotFound(format!("{query} is not running")))
640        }
641    }
642
643    fn input_context(&self, app: &str) -> Result<(AppInfo, Option<WindowInfo>)> {
644        if let Ok(session) = self.session_for(app) {
645            return Ok((session.state.app.clone(), Some(session.state.window.clone())));
646        }
647        Ok((self.running_app(app, false)?, None))
648    }
649
650    fn remember(&mut self, query: &str, info: &AppInfo) -> String {
651        let key = info.pid.map_or_else(|| info.reference().to_lowercase(), |pid| format!("pid:{pid}"));
652        self.aliases.insert(alias_key(query), key.clone());
653        self.aliases.insert(alias_key(info.reference()), key.clone());
654        self.aliases.insert(alias_key(&info.name), key.clone());
655        if let Some(pid) = info.pid {
656            self.aliases.insert(format!("pid:{pid}"), key.clone());
657        }
658        key
659    }
660
661    fn session_for(&self, app: &str) -> Result<&Session<B::Handle>> {
662        self.aliases
663            .get(&alias_key(app))
664            .and_then(|key| self.sessions.get(key))
665            .ok_or_else(|| Error::NoAppState(app.to_owned()))
666    }
667
668    fn session_for_mut(&mut self, app: &str) -> Result<&mut Session<B::Handle>> {
669        self.aliases
670            .get(&alias_key(app))
671            .and_then(|key| self.sessions.get_mut(key))
672            .ok_or_else(|| Error::NoAppState(app.to_owned()))
673    }
674
675    fn settle(&self) {
676        if !self.settle_delay.is_zero() {
677            thread::sleep(self.settle_delay);
678        }
679    }
680}
681
682fn alias_key(query: &str) -> String {
683    query.trim().to_lowercase()
684}
685
686fn element<'a, H>(session: &'a Session<H>, app: &str, index: usize) -> Result<&'a Element> {
687    session.state.element(index).ok_or_else(|| Error::ElementNotFound { app: app.to_owned(), index })
688}
689
690fn equivalent_action(element: &Element, options: ClickOptions) -> Option<&crate::element::Action> {
691    match (options.button, options.count) {
692        (MouseButton::Left, 1) => element.primary_action(),
693        (MouseButton::Right, 1) => element.action("Show Menu"),
694        _ => None,
695    }
696}
697
698fn screenshot_point(state: &AppState, x: f64, y: f64) -> Result<Point> {
699    let shot = state.screenshot.as_ref().ok_or_else(|| {
700        Error::StateUnavailable("no screenshot in the latest state; capture one before coordinate actions".into())
701    })?;
702    if !shot.contains_pixel(x, y) {
703        return Err(Error::invalid(format!("({x}, {y}) is outside the {}x{} screenshot", shot.width(), shot.height())));
704    }
705    Ok(shot.to_screen_point(x, y))
706}
707
708/// Points per page when nothing tells how large the scrolled area is.
709const FALLBACK_PAGE: f64 = 500.0;
710
711/// The wheel delta for scrolling `frame` by `pages`: a page is 80% of the frame (at least 40).
712fn wheel_delta(frame: Rect, direction: ScrollDirection, pages: f64) -> (f64, f64) {
713    let page_x = (frame.size.width * 0.8).max(40.0);
714    let page_y = (frame.size.height * 0.8).max(40.0);
715    match direction {
716        ScrollDirection::Up => (0.0, -page_y * pages),
717        ScrollDirection::Down => (0.0, page_y * pages),
718        ScrollDirection::Left => (-page_x * pages, 0.0),
719        ScrollDirection::Right => (page_x * pages, 0.0),
720    }
721}
722
723/// The frame of the innermost scroll container under `point`.
724fn scroll_area_at(state: &AppState, point: Point) -> Option<Rect> {
725    state
726        .elements
727        .iter()
728        .filter(|e| e.role.contains("scroll") && !e.role.contains("bar"))
729        .filter_map(|e| e.frame)
730        .filter(|f| !f.is_empty() && f.contains(point))
731        .min_by(|a, b| a.area().total_cmp(&b.area()))
732}
733
734const fn input_target(state: &AppState, delivery: Delivery) -> InputTarget<'_> {
735    InputTarget { app: &state.app, window: Some(&state.window), delivery }
736}
737
738#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
739const fn whole_pages(pages: f64) -> u32 {
740    pages as u32
741}