1pub mod action;
2pub mod attribute;
3pub mod observer;
4pub mod ui_element;
5mod util;
6pub mod value;
7
8use accessibility_sys_ng::{error_string, AXError};
9use core_foundation::{
10 array::CFArray,
11 base::CFTypeID,
12 base::{CFCopyTypeIDDescription, TCFType},
13 string::CFString,
14};
15use std::{
16 cell::{Cell, RefCell},
17 thread,
18 time::{Duration, Instant},
19};
20use thiserror::Error as TError;
21
22pub use action::*;
23pub use attribute::*;
24pub use observer::*;
25pub use ui_element::*;
26pub use value::*;
27
28#[non_exhaustive]
29#[derive(Debug, TError)]
30pub enum Error {
31 #[error("element not found")]
32 NotFound,
33 #[error(
34 "expected attribute type {} but got {}",
35 type_name(*expected),
36 type_name(*received),
37 )]
38 UnexpectedType {
39 expected: CFTypeID,
40 received: CFTypeID,
41 },
42 #[error("accessibility error {}", error_string(*.0))]
43 Ax(AXError),
44}
45
46fn type_name(type_id: CFTypeID) -> CFString {
47 unsafe { CFString::wrap_under_create_rule(CFCopyTypeIDDescription(type_id)) }
48}
49
50pub trait TreeVisitor {
51 fn enter_element(&self, element: &AXUIElement) -> TreeWalkerFlow;
52 fn exit_element(&self, element: &AXUIElement);
53}
54
55pub struct TreeWalker {
56 attr_children: AXAttribute<CFArray<AXUIElement>>,
57}
58
59#[derive(Copy, Clone, PartialEq, Eq)]
60pub enum TreeWalkerFlow {
61 Continue,
62 SkipSubtree,
63 Exit,
64}
65
66impl TreeWalker {
67 pub fn new() -> Self {
68 Self {
69 attr_children: AXAttribute::children(),
70 }
71 }
72
73 pub fn walk(&self, root: &AXUIElement, visitor: &dyn TreeVisitor) {
74 let _ = self.walk_one(root, visitor);
75 }
76
77 fn walk_one(&self, root: &AXUIElement, visitor: &dyn TreeVisitor) -> TreeWalkerFlow {
78 let mut flow = visitor.enter_element(root);
79
80 if flow == TreeWalkerFlow::Continue {
81 if let Ok(children) = root.attribute(&self.attr_children) {
82 for child in children.into_iter() {
83 let child_flow = self.walk_one(&*child, visitor);
84
85 if child_flow == TreeWalkerFlow::Exit {
86 flow = child_flow;
87 break;
88 }
89 }
90 }
91 }
92
93 visitor.exit_element(root);
94 flow
95 }
96}
97
98pub struct ElementFinder {
99 root: AXUIElement,
100 implicit_wait: Option<Duration>,
101 predicate: Box<dyn Fn(&AXUIElement) -> bool>,
102 depth: Cell<usize>,
103 cached: RefCell<Option<AXUIElement>>,
104}
105
106impl ElementFinder {
107 pub fn new<F>(root: &AXUIElement, predicate: F, implicit_wait: Option<Duration>) -> Self
108 where
109 F: 'static + Fn(&AXUIElement) -> bool,
110 {
111 Self {
112 root: root.clone(),
113 predicate: Box::new(predicate),
114 implicit_wait,
115 depth: Cell::new(0),
116 cached: RefCell::new(None),
117 }
118 }
119
120 pub fn find(&self) -> Result<AXUIElement, Error> {
121 if let Some(result) = &*self.cached.borrow() {
122 return Ok(result.clone());
123 }
124
125 let mut deadline = Instant::now();
126 let walker = TreeWalker::new();
127
128 if let Some(implicit_wait) = &self.implicit_wait {
129 deadline += *implicit_wait;
130 }
131
132 loop {
133 if let Some(result) = &*self.cached.borrow() {
134 return Ok(result.clone());
135 }
136
137 walker.walk(&self.root, self);
138 let now = Instant::now();
139
140 if now >= deadline {
141 return Err(Error::NotFound);
142 } else {
143 let time_left = deadline.saturating_duration_since(now);
144 thread::sleep(std::cmp::min(time_left, Duration::from_millis(250)));
145 }
146 }
147 }
148
149 pub fn reset(&self) {
150 self.cached.replace(None);
151 }
152
153 pub fn attribute<T: TCFType>(&self, attribute: &AXAttribute<T>) -> Result<T, Error> {
154 self.find()?.attribute(attribute)
155 }
156
157 pub fn set_attribute<T: TCFType>(
158 &self,
159 attribute: &AXAttribute<T>,
160 value: impl Into<T>,
161 ) -> Result<(), Error> {
162 self.find()?.set_attribute(attribute, value)
163 }
164
165 pub fn perform_action(&self, name: &CFString) -> Result<(), Error> {
166 self.find()?.perform_action(name)
167 }
168}
169
170const MAX_DEPTH: usize = 100;
171
172impl TreeVisitor for ElementFinder {
173 fn enter_element(&self, element: &AXUIElement) -> TreeWalkerFlow {
174 self.depth.set(self.depth.get() + 1);
175
176 if (self.predicate)(element) {
177 self.cached.replace(Some(element.clone()));
178 return TreeWalkerFlow::Exit;
179 }
180
181 if self.depth.get() > MAX_DEPTH {
182 TreeWalkerFlow::SkipSubtree
183 } else {
184 TreeWalkerFlow::Continue
185 }
186 }
187
188 fn exit_element(&self, _element: &AXUIElement) {
189 self.depth.set(self.depth.get() - 1)
190 }
191}