Skip to main content

computer_use/backend/
mod.rs

1//! Platform desktop backends.
2//!
3//! Screenshot capture uses `xcap`. Pointer and keyboard input use `enigo`.
4//! Accessibility and the frontmost window are platform-specific and best-effort.
5
6mod capture;
7pub(crate) mod clipboard;
8pub(crate) mod command;
9pub(crate) mod held;
10mod input;
11
12#[cfg(target_os = "linux")]
13mod linux;
14#[cfg(target_os = "macos")]
15mod macos;
16#[cfg(target_os = "windows")]
17mod windows;
18
19use crate::element::{ElementError, ElementOutcome, ElementRequest, HitInfo};
20use crate::error::ComputerError;
21use crate::protocol::AppleEventLanguage;
22use crate::target::{AppInfo, DoctorReport, LaunchResult, QuitResult, WindowBounds, WindowList};
23use crate::tree::{TreeQuery, TreeReport};
24use crate::types::{DisplayInfo, Frame, FrontmostInfo, Modifier, MouseButton, RawCapture, Region};
25use crate::window_groups::SpacesReport;
26use serde::Serialize;
27
28/// One window selected for capture.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct WindowTarget {
31    /// Platform window id. On macOS this is the CG window id.
32    pub window_id: u32,
33    /// Owning process id.
34    pub pid: i32,
35    /// Screen bounds in logical points.
36    pub bounds: WindowBounds,
37}
38
39/// Error for an operation this operating system does not implement.
40pub(crate) fn unsupported(operation: &str) -> ComputerError {
41    ComputerError::Unsupported(format!(
42        "{operation} is not available on {}",
43        std::env::consts::OS
44    ))
45}
46
47pub(crate) use command::run_with_stdin;
48pub use input::validate_key;
49
50/// One entry under a menu bar item or submenu. The macOS backend fills `title`
51/// from `AXTitle`, `enabled` from `AXEnabled`, and recurses into `AXMenu`
52/// children up to `depth` levels.
53#[derive(Debug, Clone, PartialEq, Serialize)]
54pub struct MenuNode {
55    /// Item title from `AXTitle`. Empty string when the menu hides it.
56    pub title: String,
57    /// `AXEnabled`. Items are still listed when `false`; the macOS backend
58    /// refuses to press them.
59    pub enabled: bool,
60    /// Submenu children. Empty for leaf items.
61    pub children: Vec<MenuNode>,
62}
63
64/// Full menu report for one running app.
65#[derive(Debug, Clone, PartialEq, Serialize)]
66pub struct MenuListReport {
67    /// App name as resolved by the platform.
68    pub app: String,
69    /// Sections under the menu bar. Each one is a top-level menu (File, Edit,
70    /// ...) with the items inside it.
71    pub sections: Vec<MenuNode>,
72}
73
74/// What `menu_click` did.
75#[derive(Debug, Clone, PartialEq, Serialize)]
76pub struct MenuClickReport {
77    /// `true` when the click was pressed. `false` is reserved for a deliberate
78    /// no-op (the path resolved but the trailing segment was a separator).
79    pub pressed: bool,
80}
81
82/// Outcome of an Apple event script.
83#[derive(Debug, Clone, PartialEq, Serialize)]
84pub struct AppleEventReport {
85    /// Number of bytes in the original script. The body is never logged.
86    pub script_bytes: usize,
87    /// Lowercase hex hash of the script body. Audit lines include this so a
88    /// script can be identified without leaking its content.
89    pub script_hash: String,
90    /// Standard output, trimmed to one line. Empty when the script did not
91    /// write anything.
92    pub stdout: String,
93    /// Standard error, trimmed. Empty when `osascript` was silent.
94    pub stderr: String,
95    /// Exit status of `osascript`.
96    pub exit_status: i32,
97}
98
99/// One Dock item.
100#[derive(Debug, Clone, PartialEq, Serialize)]
101pub struct DockItem {
102    /// Item title as the Dock exposes it.
103    pub title: String,
104}
105
106/// Full Dock listing.
107#[derive(Debug, Clone, PartialEq, Serialize)]
108pub struct DockListReport {
109    /// Items in Dock order.
110    pub items: Vec<DockItem>,
111}
112
113/// What `dock_click` did.
114#[derive(Debug, Clone, PartialEq, Serialize)]
115pub struct DockClickReport {
116    /// `true` when the click was pressed.
117    pub pressed: bool,
118}
119
120/// Desktop automation surface used by [`crate::dispatch`].
121///
122/// Also exported as [`Backend`].
123pub trait DesktopBackend: Send + Sync {
124    /// Capture the desktop, or a logical region of it, as RGBA pixels.
125    fn screenshot(&self, region: Option<Region>) -> Result<Frame, ComputerError>;
126
127    /// Desktop or region capture plus the map inputs (origin and logical size).
128    fn capture_screen(&self, region: Option<Region>) -> Result<RawCapture, ComputerError> {
129        capture::capture_screen(region)
130    }
131
132    /// Capture one window. macOS uses ScreenCaptureKit. Other platforms crop
133    /// the desktop image when the bounds are known.
134    fn capture_window(&self, target: &WindowTarget) -> Result<RawCapture, ComputerError> {
135        #[cfg(target_os = "macos")]
136        {
137            macos::capture_window(target)
138        }
139        #[cfg(target_os = "linux")]
140        {
141            linux::capture_window(target)
142        }
143        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
144        {
145            let _ = target.pid;
146            let _ = target.window_id;
147            capture::crop_window(target.bounds)
148        }
149    }
150
151    /// Move the pointer without clicking.
152    fn move_mouse(&self, x: i32, y: i32) -> Result<(), ComputerError>;
153
154    /// Move the pointer to `(x, y)` and click once.
155    fn simulate_click(&self, x: i32, y: i32, button: MouseButton) -> Result<(), ComputerError>;
156
157    /// Type Unicode text into the focused control.
158    fn simulate_type(&self, text: &str) -> Result<(), ComputerError>;
159
160    /// Press modifiers, tap `key`, then release the modifiers.
161    ///
162    /// `key` is a canonical name from [`crate::keys::canonical_key`].
163    fn simulate_key(&self, modifiers: &[Modifier], key: &str) -> Result<(), ComputerError>;
164
165    /// Scroll by wheel steps. Positive `dy` scrolls down. Positive `dx` scrolls right.
166    fn simulate_scroll(&self, dx: i32, dy: i32) -> Result<(), ComputerError>;
167
168    /// Current pointer position in logical pixels.
169    fn cursor_position(&self) -> Result<(i32, i32), ComputerError>;
170
171    /// Connected displays. Does not capture pixels.
172    fn list_displays(&self) -> Result<Vec<DisplayInfo>, ComputerError>;
173
174    /// Frontmost application and window. Never panics; missing data is `None`.
175    fn frontmost(&self) -> FrontmostInfo;
176
177    /// Accessibility tree as a JSON string, when the platform can produce one.
178    fn inspect_accessibility(&self, app_name: Option<&str>) -> Result<String, ComputerError>;
179
180    /// Running applications. `all` includes accessory and prohibited apps.
181    fn list_apps(&self, _all: bool) -> Result<Vec<AppInfo>, ComputerError> {
182        Err(unsupported("list_apps"))
183    }
184
185    /// On-screen windows in z-order. Index 0 is front.
186    fn list_windows(&self) -> Result<WindowList, ComputerError> {
187        Err(unsupported("list_windows"))
188    }
189
190    /// Launch an application by name, bundle id, or path.
191    fn launch(&self, _app: &str) -> Result<LaunchResult, ComputerError> {
192        Err(unsupported("launch"))
193    }
194
195    /// Bring an application forward. `window` selects a window inside it.
196    fn activate(&self, _app: &str, _window: Option<&str>) -> Result<LaunchResult, ComputerError> {
197        Err(unsupported("activate"))
198    }
199
200    /// Ask an application to quit. `force` uses a forced terminate where the OS has one.
201    fn quit(&self, _app: &str, _force: bool) -> Result<QuitResult, ComputerError> {
202        Err(unsupported("quit"))
203    }
204
205    /// Walk `app`'s menu bar. The macOS backend uses
206    /// `AXMenuBar -> AXMenuBarItem -> AXMenu`. Other platforms return
207    /// `unsupported`. `depth` caps recursion; out-of-depth submenus are
208    /// reported as a single placeholder child.
209    fn menu_list(&self, _app: &str, _depth: u32) -> Result<MenuListReport, ComputerError> {
210        Err(unsupported("menu_list"))
211    }
212
213    /// Press one menu item by `">"`-separated titles. A miss is
214    /// `element_not_found`. A disabled item is `execution_failed`.
215    fn menu_click(
216        &self,
217        _app: &str,
218        _path: &crate::menu_path::MenuPath,
219    ) -> Result<MenuClickReport, ComputerError> {
220        Err(unsupported("menu_click"))
221    }
222
223    /// Run an Apple event via `osascript`. macOS only. `script` is the body
224    /// passed with `-e`; it never goes through a shell. The script body is
225    /// never logged verbatim; only its byte count and SHA-256 are recorded.
226    fn apple_event(
227        &self,
228        _app: &str,
229        _script: &str,
230        _language: AppleEventLanguage,
231    ) -> Result<AppleEventReport, ComputerError> {
232        Err(unsupported("apple_event"))
233    }
234
235    /// List Dock items. macOS only.
236    fn dock_list(&self) -> Result<DockListReport, ComputerError> {
237        Err(unsupported("dock_list"))
238    }
239
240    /// Click a Dock item by title. macOS only. A miss is `element_not_found`.
241    fn dock_click(&self, _title: &str) -> Result<DockClickReport, ComputerError> {
242        Err(unsupported("dock_click"))
243    }
244
245    /// Group on-screen windows by the screen that contains each window's
246    /// center. Mission Control moves are not attempted; the skylight feature,
247    /// when active, may opt into best-effort space moves.
248    fn spaces(&self) -> Result<SpacesReport, ComputerError> {
249        Err(unsupported("spaces"))
250    }
251
252    /// Permission probe. `options.prompt` may show a system prompt on macOS.
253    ///
254    /// The default does not prompt and does not open Settings.
255    fn doctor(
256        &self,
257        _options: crate::doctor::DoctorOptions,
258    ) -> Result<DoctorReport, ComputerError> {
259        Err(unsupported("doctor"))
260    }
261
262    /// Numbered accessibility tree for the key window.
263    fn tree(&self, _query: &TreeQuery) -> Result<TreeReport, ComputerError> {
264        Err(unsupported("tree"))
265    }
266
267    /// Restore accessibility flags recorded for `session`.
268    ///
269    /// The returned pids are the processes whose flags were written back.
270    /// An empty list means there was nothing to restore.
271    fn end_session(&self, _session: Option<&str>) -> Result<Vec<i32>, ComputerError> {
272        Ok(Vec::new())
273    }
274
275    /// Press, focus, set a value, select text, perform an action, scroll, or type
276    /// into one cached element. The element is revalidated before the action.
277    fn element_action(&self, request: &ElementRequest) -> Result<ElementOutcome, ElementError> {
278        #[cfg(target_os = "macos")]
279        {
280            macos::element_action(request)
281        }
282        #[cfg(not(target_os = "macos"))]
283        {
284            Err(ElementError::new(unsupported(request.op.action_name())))
285        }
286    }
287
288    /// Locked, asleep, or no display. `None` when capture may proceed.
289    ///
290    /// The default is `None` so the stub does not read the live session.
291    /// The macOS backend overrides it.
292    fn display_block(&self) -> Option<ComputerError> {
293        None
294    }
295
296    /// Page URL for a browser process. `Ok(None)` when the platform has no URL.
297    ///
298    /// A [`ComputerError::PermissionRequired`] fails closed when a block list
299    /// caused the call. Other platforms return `Ok(None)`.
300    fn browser_url(&self, pid: i32) -> Result<Option<String>, ComputerError> {
301        #[cfg(target_os = "macos")]
302        {
303            macos::browser_url(pid)
304        }
305        #[cfg(not(target_os = "macos"))]
306        {
307            let _ = pid;
308            Ok(None)
309        }
310    }
311
312    /// Title and secure bit of a cached element. `Ok(None)` when it is not cached.
313    ///
314    /// The default on macOS reads the session cache. The stub reads its fixture.
315    fn peek_element(
316        &self,
317        app: Option<&str>,
318        window: Option<&str>,
319        session: Option<&str>,
320        index: u32,
321    ) -> Result<Option<crate::element::ElementPeek>, ComputerError> {
322        #[cfg(target_os = "macos")]
323        {
324            macos::peek_element(app, window, session, index)
325        }
326        #[cfg(not(target_os = "macos"))]
327        {
328            let _ = (app, window, session, index);
329            Ok(None)
330        }
331    }
332
333    /// Install the listen-only user-input tap when this process should pause.
334    ///
335    /// `None` when the tap is installed or was not requested. The warning string
336    /// is `user-input pause unavailable`. Tests never create a real event tap.
337    fn user_input_pause_warning(&self) -> Option<&'static str> {
338        #[cfg(test)]
339        {
340            None
341        }
342        #[cfg(all(not(test), target_os = "macos"))]
343        {
344            macos::input::ensure_installed()
345        }
346        #[cfg(all(not(test), not(target_os = "macos")))]
347        {
348            Some("user-input pause unavailable")
349        }
350    }
351
352    /// Hit-test a screen point. macOS reports the element under the point.
353    /// Other platforms return `Ok(None)` and the coordinate click still runs.
354    fn hit_test(&self, x: i32, y: i32) -> Result<Option<HitInfo>, ComputerError> {
355        #[cfg(target_os = "macos")]
356        {
357            macos::hit_test(x, y)
358        }
359        #[cfg(not(target_os = "macos"))]
360        {
361            let _ = (x, y);
362            Ok(None)
363        }
364    }
365
366    /// Whether background event posting exists. The stub overrides this.
367    fn input_support(&self) -> crate::tier::Capabilities {
368        #[cfg(target_os = "macos")]
369        {
370            crate::tier::Capabilities { event: true }
371        }
372        #[cfg(all(target_os = "linux", feature = "portal"))]
373        {
374            // With the portal compiled in, a Wayland session has a route to
375            // background pointer and keyboard input through RemoteDesktop.
376            crate::tier::Capabilities { event: true }
377        }
378        #[cfg(all(target_os = "linux", not(feature = "portal")))]
379        {
380            // Without the portal there is still XTest on a plain X11 session.
381            crate::tier::Capabilities {
382                event: linux::x11_event_capable(false),
383            }
384        }
385        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
386        {
387            crate::tier::Capabilities { event: false }
388        }
389    }
390
391    /// Walk the latest tree and return rectangles in image pixels plus the
392    /// label to draw (`n` for tree index). The screenshot command uses
393    /// these for the `--marks` overlay.
394    ///
395    /// Empty results are not an error; the dispatcher decides whether to
396    /// fall back to OCR or to leave the image unmarked. Non-macOS platforms
397    /// always return `Ok(Vec::new())`; OCR for those platforms is `Unsupported`.
398    fn marks_for_tree(
399        &self,
400        _query: &TreeQuery,
401        _map: &crate::image_util::ScreenshotMap,
402    ) -> Result<Vec<crate::vision::MarkBox>, ComputerError> {
403        Ok(Vec::new())
404    }
405
406    /// Run OCR on one captured image and return the redacted, screen-remapped
407    /// blocks. `image` is the prepared shot's pixels.
408    fn recognize_text(
409        &self,
410        _image: &image::RgbaImage,
411        _map: &crate::image_util::ScreenshotMap,
412    ) -> Result<crate::vision::OcrResult, ComputerError> {
413        Err(unsupported("ocr"))
414    }
415
416    /// Image-pixel rectangles of every secure element in the latest tree.
417    /// The OCR dispatcher drops recognized text whose block falls inside
418    /// any of these rectangles. The default returns an empty list, which
419    /// matches every non-macOS platform and the mock backend.
420    ///
421    /// `map` is the screenshot map of the capture the OCR ran on. Platform
422    /// frames are screen points, so redaction is only correct when the
423    /// backend converts through it — P12b wires that up on Windows and
424    /// finishes the macOS placeholder it replaces.
425    fn secure_ocr_frames(
426        &self,
427        _query: &TreeQuery,
428        _map: &crate::image_util::ScreenshotMap,
429    ) -> Result<Vec<crate::vision::ocr::ImageRect>, ComputerError> {
430        Ok(Vec::new())
431    }
432
433    /// Read the clipboard. The default uses the system pasteboard outside tests.
434    ///
435    /// The stub overrides this and never touches the real pasteboard.
436    fn clipboard_get(&self) -> Result<clipboard::ClipboardContents, ComputerError> {
437        clipboard::clipboard_get()
438    }
439
440    /// Write clipboard text. Empty text is refused before this is called.
441    fn clipboard_set_text(&self, text: &str) -> Result<(), ComputerError> {
442        clipboard::clipboard_set_text(text)
443    }
444
445    /// Write an RGBA image to the clipboard.
446    fn clipboard_set_image(
447        &self,
448        width: u32,
449        height: u32,
450        rgba: &[u8],
451    ) -> Result<(), ComputerError> {
452        clipboard::clipboard_set_image(width, height, rgba)
453    }
454
455    /// Release mouse buttons and keys this process still holds.
456    ///
457    /// The real registry is process-global. The stub overrides this method.
458    fn release_held(&self) -> Result<(), ComputerError> {
459        held::release_all()
460    }
461
462    /// Run one input tier. The default posts nothing.
463    ///
464    /// `event` and `synthetic` are `unsupported` off macOS. `activate` uses the
465    /// existing foreground enigo path and does not restore focus.
466    fn perform_input(
467        &self,
468        job: &crate::tier::InputJob,
469    ) -> Result<crate::tier::InputReport, ComputerError> {
470        #[cfg(target_os = "macos")]
471        {
472            macos::perform_input(job)
473        }
474        #[cfg(not(target_os = "macos"))]
475        {
476            // `Self` is unsized when this default runs through `dyn DesktopBackend`.
477            off_platform_input(self, job)
478        }
479    }
480}
481
482/// Foreground enigo path used when event posting is not available.
483///
484/// `B: ?Sized` so the default method can pass `self` from a trait object.
485#[cfg(not(target_os = "macos"))]
486fn off_platform_input<B: DesktopBackend + ?Sized>(
487    backend: &B,
488    job: &crate::tier::InputJob,
489) -> Result<crate::tier::InputReport, ComputerError> {
490    use crate::tier::{InputKind, InputReport, TierName};
491    match job.tier {
492        TierName::Event => {
493            return Err(unsupported("tier event"));
494        }
495        TierName::Synthetic => {
496            return Err(unsupported("tier synthetic"));
497        }
498        TierName::Ax => {
499            return Err(unsupported("tier ax for coordinate and keyboard input"));
500        }
501        TierName::Activate => {}
502    }
503    if job.require_background {
504        return Err(ComputerError::ExecutionFailed(
505            "--require-background refuses tier activate because it uses the foreground cursor"
506                .into(),
507        ));
508    }
509    match &job.kind {
510        InputKind::Click {
511            x,
512            y,
513            button,
514            count,
515        } => {
516            for index in 0..*count {
517                if index > 0 {
518                    std::thread::sleep(std::time::Duration::from_millis(50));
519                }
520                backend.simulate_click(*x, *y, *button)?;
521            }
522        }
523        InputKind::Move { x, y } => backend.move_mouse(*x, *y)?,
524        InputKind::Scroll { x, y, dx, dy } => {
525            if let (Some(x), Some(y)) = (x, y) {
526                backend.move_mouse(*x, *y)?;
527            }
528            backend.simulate_scroll(*dx, *dy)?;
529        }
530        InputKind::Type { text } => backend.simulate_type(text)?,
531        InputKind::Key { strokes } => {
532            for stroke in strokes {
533                backend.simulate_key(&stroke.modifiers, &stroke.key)?;
534            }
535        }
536        InputKind::Drag { points, button } => input::drag(points, *button)?,
537        InputKind::MouseDown { x, y, button } => input::button_down(*x, *y, *button)?,
538        InputKind::MouseUp { x, y, button } => input::button_up(*x, *y, *button)?,
539        InputKind::HoldKey { modifiers, key, ms } => input::hold_key(modifiers, key, *ms)?,
540    }
541    Ok(InputReport {
542        tier: "activate",
543        cursor_moved: true,
544        focus_restored: None,
545        warnings: Vec::new(),
546        scroll: None,
547    })
548}
549
550/// Overlay helper. macOS opens the click-through window. Other platforms return `unsupported`.
551pub(crate) fn serve_overlay() -> Result<(), ComputerError> {
552    #[cfg(all(target_os = "macos", not(test)))]
553    {
554        macos::overlay::serve()
555    }
556    #[cfg(all(target_os = "macos", test))]
557    {
558        Err(ComputerError::Unsupported(
559            "overlay serve does not open a window in tests".into(),
560        ))
561    }
562    #[cfg(not(target_os = "macos"))]
563    {
564        Err(unsupported("overlay serve"))
565    }
566}
567
568/// Restore Electron flags and stop observers. No-op off macOS.
569pub fn shutdown_sessions() {
570    #[cfg(target_os = "macos")]
571    macos::shutdown_sessions();
572}
573
574/// Alias for [`DesktopBackend`].
575pub use DesktopBackend as Backend;
576
577/// Create the backend for the current operating system.
578pub fn create_desktop_backend() -> Box<dyn DesktopBackend> {
579    #[cfg(target_os = "macos")]
580    {
581        Box::new(macos::MacOsDesktopBackend)
582    }
583    #[cfg(target_os = "linux")]
584    {
585        Box::new(linux::LinuxDesktopBackend)
586    }
587    #[cfg(target_os = "windows")]
588    {
589        Box::new(windows::WindowsDesktopBackend)
590    }
591    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
592    {
593        compile_error!("computer-use supports macOS, Linux, and Windows");
594    }
595}
596
597macro_rules! impl_platform_backend {
598    ($ty:ty, $inspect:path, $front:path, $doctor:path, $list_apps:path, $list_windows:path, $launch:path, $activate:path, $quit:path, $tree:path, $end_session:path) => {
599        impl DesktopBackend for $ty {
600            fn screenshot(&self, region: Option<Region>) -> Result<Frame, ComputerError> {
601                capture::capture(region)
602            }
603
604            fn move_mouse(&self, x: i32, y: i32) -> Result<(), ComputerError> {
605                input::move_mouse(x, y)
606            }
607
608            fn simulate_click(
609                &self,
610                x: i32,
611                y: i32,
612                button: MouseButton,
613            ) -> Result<(), ComputerError> {
614                input::click(x, y, button)
615            }
616
617            fn simulate_type(&self, text: &str) -> Result<(), ComputerError> {
618                input::type_text(text)
619            }
620
621            fn simulate_key(&self, modifiers: &[Modifier], key: &str) -> Result<(), ComputerError> {
622                input::press_key(modifiers, key)
623            }
624
625            fn simulate_scroll(&self, dx: i32, dy: i32) -> Result<(), ComputerError> {
626                input::scroll(dx, dy)
627            }
628
629            fn cursor_position(&self) -> Result<(i32, i32), ComputerError> {
630                input::cursor_position()
631            }
632
633            fn list_displays(&self) -> Result<Vec<DisplayInfo>, ComputerError> {
634                capture::list_displays()
635            }
636
637            fn display_block(&self) -> Option<ComputerError> {
638                #[cfg(target_os = "macos")]
639                {
640                    macos::session_display_block()
641                }
642                #[cfg(not(target_os = "macos"))]
643                {
644                    None
645                }
646            }
647
648            fn frontmost(&self) -> FrontmostInfo {
649                $front()
650            }
651
652            fn inspect_accessibility(
653                &self,
654                app_name: Option<&str>,
655            ) -> Result<String, ComputerError> {
656                $inspect(app_name)
657            }
658
659            fn list_apps(&self, all: bool) -> Result<Vec<AppInfo>, ComputerError> {
660                $list_apps(all)
661            }
662
663            fn list_windows(&self) -> Result<WindowList, ComputerError> {
664                $list_windows()
665            }
666
667            fn launch(&self, app: &str) -> Result<LaunchResult, ComputerError> {
668                $launch(app)
669            }
670
671            fn activate(
672                &self,
673                app: &str,
674                window: Option<&str>,
675            ) -> Result<LaunchResult, ComputerError> {
676                $activate(app, window)
677            }
678
679            fn quit(&self, app: &str, force: bool) -> Result<QuitResult, ComputerError> {
680                $quit(app, force)
681            }
682
683            fn doctor(
684                &self,
685                options: crate::doctor::DoctorOptions,
686            ) -> Result<DoctorReport, ComputerError> {
687                $doctor(options)
688            }
689
690            fn tree(&self, query: &TreeQuery) -> Result<TreeReport, ComputerError> {
691                $tree(query)
692            }
693
694            fn end_session(&self, session: Option<&str>) -> Result<Vec<i32>, ComputerError> {
695                $end_session(session)
696            }
697
698            // P9: menu bar, Apple events, Dock, and Spaces are macOS-only.
699            // Linux and Windows keep the `unsupported` default from the trait.
700            #[cfg(target_os = "macos")]
701            fn menu_list(&self, app: &str, depth: u32) -> Result<MenuListReport, ComputerError> {
702                macos::menu::list(app, depth)
703            }
704
705            #[cfg(target_os = "macos")]
706            fn menu_click(
707                &self,
708                app: &str,
709                path: &crate::menu_path::MenuPath,
710            ) -> Result<MenuClickReport, ComputerError> {
711                macos::menu::click(app, path)
712            }
713
714            #[cfg(target_os = "macos")]
715            fn apple_event(
716                &self,
717                app: &str,
718                script: &str,
719                language: AppleEventLanguage,
720            ) -> Result<AppleEventReport, ComputerError> {
721                let _ = app;
722                let outcome = macos::apple::run(script, language)?;
723                Ok(AppleEventReport {
724                    script_bytes: outcome.script_bytes,
725                    script_hash: outcome.script_hash,
726                    stdout: outcome.stdout,
727                    stderr: outcome.stderr,
728                    exit_status: outcome.exit_status,
729                })
730            }
731
732            #[cfg(target_os = "macos")]
733            fn dock_list(&self) -> Result<DockListReport, ComputerError> {
734                macos::dock::list()
735            }
736
737            #[cfg(target_os = "macos")]
738            fn dock_click(&self, title: &str) -> Result<DockClickReport, ComputerError> {
739                macos::dock::click(title)
740            }
741
742            #[cfg(target_os = "macos")]
743            fn spaces(&self) -> Result<crate::window_groups::SpacesReport, ComputerError> {
744                Ok(macos::spaces::report())
745            }
746
747            // P11: marks overlay reuses the latest tree; OCR runs Apple
748            // Vision. macOS overrides the trait defaults; Linux and Windows
749            // keep the empty/unsupported ones.
750            #[cfg(target_os = "macos")]
751            fn marks_for_tree(
752                &self,
753                query: &TreeQuery,
754                map: &crate::image_util::ScreenshotMap,
755            ) -> Result<Vec<crate::vision::MarkBox>, ComputerError> {
756                macos::marks_for_tree(query, map)
757            }
758
759            #[cfg(target_os = "macos")]
760            fn recognize_text(
761                &self,
762                image: &image::RgbaImage,
763                map: &crate::image_util::ScreenshotMap,
764            ) -> Result<crate::vision::OcrResult, ComputerError> {
765                macos::recognize_text(image, map)
766            }
767
768            #[cfg(target_os = "macos")]
769            fn secure_ocr_frames(
770                &self,
771                query: &TreeQuery,
772                map: &crate::image_util::ScreenshotMap,
773            ) -> Result<Vec<crate::vision::ocr::ImageRect>, ComputerError> {
774                macos::secure_ocr_frames(query, map)
775            }
776
777            #[cfg(target_os = "windows")]
778            fn element_action(
779                &self,
780                request: &ElementRequest,
781            ) -> Result<ElementOutcome, ElementError> {
782                windows::element_action(request)
783            }
784
785            // P13a: Linux element actions route through AT-SPI once the
786            // `portal` feature lands in part B. For now the cached
787            // fingerprint is revalidated and the live dispatch path stays
788            // `unsupported`, mirroring the macOS / Windows shape.
789            #[cfg(target_os = "linux")]
790            fn element_action(
791                &self,
792                request: &ElementRequest,
793            ) -> Result<ElementOutcome, ElementError> {
794                linux::element_action(request)
795            }
796
797            // P13a: Linux `perform_input` overrides the default so the
798            // `event` tier can use XTest `fake_input` on plain X11.
799            #[cfg(target_os = "linux")]
800            fn perform_input(
801                &self,
802                job: &crate::tier::InputJob,
803            ) -> Result<crate::tier::InputReport, ComputerError> {
804                linux::perform_input(job)
805            }
806
807            // P12b: Windows captures through `windows-capture` and recognizes
808            // text with `Windows.Media.Ocr`. The secure-frame walk is the
809            // same `UiNode` recursion as macOS, converted through the map.
810            #[cfg(target_os = "windows")]
811            fn capture_window(&self, target: &WindowTarget) -> Result<RawCapture, ComputerError> {
812                windows::capture_window(target)
813            }
814
815            #[cfg(target_os = "windows")]
816            fn marks_for_tree(
817                &self,
818                query: &TreeQuery,
819                map: &crate::image_util::ScreenshotMap,
820            ) -> Result<Vec<crate::vision::MarkBox>, ComputerError> {
821                windows::marks_for_tree(query, map)
822            }
823
824            #[cfg(target_os = "windows")]
825            fn recognize_text(
826                &self,
827                image: &image::RgbaImage,
828                map: &crate::image_util::ScreenshotMap,
829            ) -> Result<crate::vision::OcrResult, ComputerError> {
830                windows::recognize_text(image, map)
831            }
832
833            #[cfg(target_os = "windows")]
834            fn secure_ocr_frames(
835                &self,
836                query: &TreeQuery,
837                map: &crate::image_util::ScreenshotMap,
838            ) -> Result<Vec<crate::vision::ocr::ImageRect>, ComputerError> {
839                windows::secure_ocr_frames(query, map)
840            }
841        }
842    };
843}
844
845#[cfg(target_os = "macos")]
846impl_platform_backend!(
847    macos::MacOsDesktopBackend,
848    macos::inspect_accessibility,
849    macos::frontmost,
850    macos::doctor,
851    macos::list_apps,
852    macos::list_windows,
853    macos::launch,
854    macos::activate,
855    macos::quit,
856    macos::tree,
857    macos::end_session
858);
859#[cfg(target_os = "linux")]
860impl_platform_backend!(
861    linux::LinuxDesktopBackend,
862    linux::inspect_accessibility,
863    linux::frontmost,
864    linux::doctor,
865    linux::list_apps,
866    linux::list_windows,
867    linux::launch,
868    linux::activate,
869    linux::quit,
870    linux::tree,
871    linux::end_session
872);
873#[cfg(target_os = "windows")]
874impl_platform_backend!(
875    windows::WindowsDesktopBackend,
876    windows::inspect_accessibility,
877    windows::frontmost,
878    windows::doctor,
879    windows::list_apps,
880    windows::list_windows,
881    windows::launch,
882    windows::activate,
883    windows::quit,
884    windows::tree,
885    windows::end_session
886);
887
888#[cfg(target_os = "linux")]
889pub use linux::LinuxDesktopBackend;
890#[cfg(target_os = "macos")]
891pub use macos::MacOsDesktopBackend;
892#[cfg(target_os = "windows")]
893pub use windows::WindowsDesktopBackend;
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898
899    #[test]
900    fn create_desktop_backend_is_a_trait_object() {
901        let backend = create_desktop_backend();
902        let _backend: &dyn DesktopBackend = backend.as_ref();
903    }
904}