accessibility_ng/
ui_element.rs

1use std::{
2    ffi::c_uchar,
3    hash::{Hash, Hasher},
4    thread,
5    time::{Duration, Instant},
6};
7
8use accessibility_sys_ng::{
9    kAXTrustedCheckOptionPrompt, pid_t, AXIsProcessTrusted, AXIsProcessTrustedWithOptions,
10    AXUIElementCopyActionNames, AXUIElementCopyAttributeNames, AXUIElementCopyAttributeValue,
11    AXUIElementCopyParameterizedAttributeNames, AXUIElementCopyParameterizedAttributeValue,
12    AXUIElementCreateApplication, AXUIElementCreateSystemWide, AXUIElementGetPid,
13    AXUIElementGetTypeID, AXUIElementIsAttributeSettable, AXUIElementPerformAction, AXUIElementRef,
14    AXUIElementSetAttributeValue, AXUIElementSetMessagingTimeout,
15};
16use cocoa::{
17    base::{id, nil},
18    foundation::{NSAutoreleasePool, NSFastEnumeration, NSString},
19};
20use core_foundation::{
21    array::CFArray,
22    base::{CFType, TCFType, TCFTypeRef},
23    boolean::CFBoolean,
24    declare_TCFType,
25    dictionary::CFDictionary,
26    impl_CFTypeDescription, impl_TCFType,
27    string::CFString,
28};
29use objc::{class, msg_send, rc::autoreleasepool, sel, sel_impl};
30
31use crate::{
32    util::{ax_call, ax_call_void},
33    AXAttribute, Error,
34};
35
36declare_TCFType!(AXUIElement, AXUIElementRef);
37impl_TCFType!(AXUIElement, AXUIElementRef, AXUIElementGetTypeID);
38impl_CFTypeDescription!(AXUIElement);
39
40unsafe impl Send for AXUIElement {}
41unsafe impl Sync for AXUIElement {}
42
43impl AXUIElement {
44    pub fn system_wide() -> Self {
45        unsafe { Self::wrap_under_create_rule(AXUIElementCreateSystemWide()) }
46    }
47
48    pub fn application(pid: pid_t) -> Self {
49        unsafe { Self::wrap_under_create_rule(AXUIElementCreateApplication(pid)) }
50    }
51
52    pub fn application_with_bundle(bundle_id: &str) -> Result<Self, Error> {
53        unsafe {
54            autoreleasepool(|| {
55                let bundle_id_str = NSString::alloc(nil).init_str(bundle_id).autorelease();
56                let apps: id = msg_send![
57                    class![NSRunningApplication],
58                    runningApplicationsWithBundleIdentifier: bundle_id_str
59                ];
60
61                if let Some(app) = apps.iter().next() {
62                    let pid: pid_t = msg_send![app, processIdentifier];
63
64                    Ok(Self::wrap_under_create_rule(AXUIElementCreateApplication(
65                        pid,
66                    )))
67                } else {
68                    Err(Error::NotFound)
69                }
70            })
71        }
72    }
73
74    pub fn application_with_bundle_timeout(
75        bundle_id: &str,
76        timeout: Duration,
77    ) -> Result<Self, Error> {
78        let deadline = Instant::now() + timeout;
79
80        loop {
81            match Self::application_with_bundle(bundle_id) {
82                Ok(result) => return Ok(result),
83                Err(e) => {
84                    let now = Instant::now();
85
86                    if now >= deadline {
87                        return Err(e);
88                    } else {
89                        let time_left = deadline.saturating_duration_since(now);
90                        thread::sleep(std::cmp::min(time_left, Duration::from_millis(250)));
91                    }
92                }
93            }
94        }
95    }
96
97    pub fn pid(&self) -> Result<pid_t, Error> {
98        unsafe { Ok(ax_call(|x| AXUIElementGetPid(self.0, x)).map_err(Error::Ax)?) }
99    }
100
101    pub fn attribute_names(&self) -> Result<CFArray<CFString>, Error> {
102        unsafe {
103            Ok(CFArray::wrap_under_create_rule(
104                ax_call(|x| AXUIElementCopyAttributeNames(self.0, x)).map_err(Error::Ax)?,
105            ))
106        }
107    }
108
109    pub fn parameterized_attribute_names(&self) -> Result<CFArray<CFString>, Error> {
110        unsafe {
111            Ok(CFArray::wrap_under_create_rule(
112                ax_call(|x| AXUIElementCopyParameterizedAttributeNames(self.0, x))
113                    .map_err(Error::Ax)?,
114            ))
115        }
116    }
117
118    pub fn attribute<T: TCFType>(&self, attribute: &AXAttribute<T>) -> Result<T, Error> {
119        let res = unsafe {
120            Ok(T::wrap_under_create_rule(T::Ref::from_void_ptr(
121                ax_call(|x| {
122                    AXUIElementCopyAttributeValue(
123                        self.0,
124                        attribute.as_CFString().as_concrete_TypeRef(),
125                        x,
126                    )
127                })
128                .map_err(Error::Ax)?,
129            )))
130        };
131        if let Ok(val) = &res {
132            if T::type_id() != CFType::type_id() && !val.instance_of::<T>() {
133                return Err(Error::UnexpectedType {
134                    expected: T::type_id(),
135                    received: val.type_of(),
136                });
137            }
138        }
139        res
140    }
141
142    pub fn set_attribute<T: TCFType>(
143        &self,
144        attribute: &AXAttribute<T>,
145        value: impl Into<T>,
146    ) -> Result<(), Error> {
147        let value = value.into();
148
149        unsafe {
150            Ok(ax_call_void(|| {
151                AXUIElementSetAttributeValue(
152                    self.0,
153                    attribute.as_CFString().as_concrete_TypeRef(),
154                    value.as_CFTypeRef(),
155                )
156            })
157            .map_err(Error::Ax)?)
158        }
159    }
160
161    pub fn is_settable<T: TCFType>(&self, attribute: &AXAttribute<T>) -> Result<bool, Error> {
162        let settable: c_uchar = unsafe {
163            ax_call(|x| {
164                AXUIElementIsAttributeSettable(
165                    self.0,
166                    attribute.as_CFString().as_concrete_TypeRef(),
167                    x,
168                )
169            })
170            .map_err(Error::Ax)?
171        };
172        Ok(settable != 0)
173    }
174
175    pub fn parameterized_attribute<T: TCFType, U: TCFType>(
176        &self,
177        attribute: &AXAttribute<T>,
178        parameter: &U,
179    ) -> Result<T, Error> {
180        unsafe {
181            Ok(T::wrap_under_create_rule(T::Ref::from_void_ptr(
182                ax_call(|x| {
183                    AXUIElementCopyParameterizedAttributeValue(
184                        self.0,
185                        attribute.as_CFString().as_concrete_TypeRef(),
186                        parameter.as_CFTypeRef(),
187                        x,
188                    )
189                })
190                .map_err(Error::Ax)?,
191            )))
192        }
193    }
194
195    pub fn action_names(&self) -> Result<CFArray<CFString>, Error> {
196        unsafe {
197            Ok(CFArray::wrap_under_create_rule(
198                ax_call(|x| AXUIElementCopyActionNames(self.0, x)).map_err(Error::Ax)?,
199            ))
200        }
201    }
202
203    pub fn perform_action(&self, name: &CFString) -> Result<(), Error> {
204        unsafe {
205            Ok(
206                ax_call_void(|| AXUIElementPerformAction(self.0, name.as_concrete_TypeRef()))
207                    .map_err(Error::Ax)?,
208            )
209        }
210    }
211
212    pub fn set_messaging_timeout(&self, timeout: f32) -> Result<(), Error> {
213        unsafe {
214            Ok(
215                ax_call_void(|| AXUIElementSetMessagingTimeout(self.0, timeout))
216                    .map_err(Error::Ax)?,
217            )
218        }
219    }
220
221    /// Checks whether or not this application is a trusted accessibility client.
222    pub fn application_is_trusted() -> bool {
223        unsafe {
224            return AXIsProcessTrusted();
225        }
226    }
227
228    /// Same as [application_is_trusted], but also shows the user a prompt asking
229    /// them to allow accessibility API access if it hasn't already been given.
230    pub fn application_is_trusted_with_prompt() -> bool {
231        unsafe {
232            let option_prompt = CFString::wrap_under_get_rule(kAXTrustedCheckOptionPrompt);
233            let dict: CFDictionary<CFString, CFBoolean> =
234                CFDictionary::from_CFType_pairs(&[(option_prompt, CFBoolean::true_value())]);
235            return AXIsProcessTrustedWithOptions(dict.as_concrete_TypeRef());
236        }
237    }
238}
239
240impl Hash for AXUIElement {
241    fn hash<H: Hasher>(&self, state: &mut H) {
242        self.0.hash(state);
243    }
244}