1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct WindowTarget {
31 pub window_id: u32,
33 pub pid: i32,
35 pub bounds: WindowBounds,
37}
38
39pub(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#[derive(Debug, Clone, PartialEq, Serialize)]
54pub struct MenuNode {
55 pub title: String,
57 pub enabled: bool,
60 pub children: Vec<MenuNode>,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize)]
66pub struct MenuListReport {
67 pub app: String,
69 pub sections: Vec<MenuNode>,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize)]
76pub struct MenuClickReport {
77 pub pressed: bool,
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize)]
84pub struct AppleEventReport {
85 pub script_bytes: usize,
87 pub script_hash: String,
90 pub stdout: String,
93 pub stderr: String,
95 pub exit_status: i32,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize)]
101pub struct DockItem {
102 pub title: String,
104}
105
106#[derive(Debug, Clone, PartialEq, Serialize)]
108pub struct DockListReport {
109 pub items: Vec<DockItem>,
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize)]
115pub struct DockClickReport {
116 pub pressed: bool,
118}
119
120pub trait DesktopBackend: Send + Sync {
124 fn screenshot(&self, region: Option<Region>) -> Result<Frame, ComputerError>;
126
127 fn capture_screen(&self, region: Option<Region>) -> Result<RawCapture, ComputerError> {
129 capture::capture_screen(region)
130 }
131
132 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 fn move_mouse(&self, x: i32, y: i32) -> Result<(), ComputerError>;
153
154 fn simulate_click(&self, x: i32, y: i32, button: MouseButton) -> Result<(), ComputerError>;
156
157 fn simulate_type(&self, text: &str) -> Result<(), ComputerError>;
159
160 fn simulate_key(&self, modifiers: &[Modifier], key: &str) -> Result<(), ComputerError>;
164
165 fn simulate_scroll(&self, dx: i32, dy: i32) -> Result<(), ComputerError>;
167
168 fn cursor_position(&self) -> Result<(i32, i32), ComputerError>;
170
171 fn list_displays(&self) -> Result<Vec<DisplayInfo>, ComputerError>;
173
174 fn frontmost(&self) -> FrontmostInfo;
176
177 fn inspect_accessibility(&self, app_name: Option<&str>) -> Result<String, ComputerError>;
179
180 fn list_apps(&self, _all: bool) -> Result<Vec<AppInfo>, ComputerError> {
182 Err(unsupported("list_apps"))
183 }
184
185 fn list_windows(&self) -> Result<WindowList, ComputerError> {
187 Err(unsupported("list_windows"))
188 }
189
190 fn launch(&self, _app: &str) -> Result<LaunchResult, ComputerError> {
192 Err(unsupported("launch"))
193 }
194
195 fn activate(&self, _app: &str, _window: Option<&str>) -> Result<LaunchResult, ComputerError> {
197 Err(unsupported("activate"))
198 }
199
200 fn quit(&self, _app: &str, _force: bool) -> Result<QuitResult, ComputerError> {
202 Err(unsupported("quit"))
203 }
204
205 fn menu_list(&self, _app: &str, _depth: u32) -> Result<MenuListReport, ComputerError> {
210 Err(unsupported("menu_list"))
211 }
212
213 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 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 fn dock_list(&self) -> Result<DockListReport, ComputerError> {
237 Err(unsupported("dock_list"))
238 }
239
240 fn dock_click(&self, _title: &str) -> Result<DockClickReport, ComputerError> {
242 Err(unsupported("dock_click"))
243 }
244
245 fn spaces(&self) -> Result<SpacesReport, ComputerError> {
249 Err(unsupported("spaces"))
250 }
251
252 fn doctor(
256 &self,
257 _options: crate::doctor::DoctorOptions,
258 ) -> Result<DoctorReport, ComputerError> {
259 Err(unsupported("doctor"))
260 }
261
262 fn tree(&self, _query: &TreeQuery) -> Result<TreeReport, ComputerError> {
264 Err(unsupported("tree"))
265 }
266
267 fn end_session(&self, _session: Option<&str>) -> Result<Vec<i32>, ComputerError> {
272 Ok(Vec::new())
273 }
274
275 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 fn display_block(&self) -> Option<ComputerError> {
293 None
294 }
295
296 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 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 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 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 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 crate::tier::Capabilities { event: true }
377 }
378 #[cfg(all(target_os = "linux", not(feature = "portal")))]
379 {
380 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 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 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 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 fn clipboard_get(&self) -> Result<clipboard::ClipboardContents, ComputerError> {
437 clipboard::clipboard_get()
438 }
439
440 fn clipboard_set_text(&self, text: &str) -> Result<(), ComputerError> {
442 clipboard::clipboard_set_text(text)
443 }
444
445 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 fn release_held(&self) -> Result<(), ComputerError> {
459 held::release_all()
460 }
461
462 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 off_platform_input(self, job)
478 }
479 }
480}
481
482#[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
550pub(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
568pub fn shutdown_sessions() {
570 #[cfg(target_os = "macos")]
571 macos::shutdown_sessions();
572}
573
574pub use DesktopBackend as Backend;
576
577pub 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 #[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 #[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 #[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 #[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 #[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}