#![allow(
dead_code,
reason = "unused when the mock-loader feature selects the DLL backend instead"
)]
use objc2_application_services::{AXError, AXUIElement, AXValue, AXValueType};
use objc2_core_foundation::{CFArray, CFRetained, CFString, CFType, CGPoint, CGSize};
use std::ptr::NonNull;
pub(crate) const ATTR_WINDOWS: &str = "AXWindows";
pub(crate) const ATTR_TITLE: &str = "AXTitle";
pub(crate) const ATTR_POSITION: &str = "AXPosition";
pub(crate) const ATTR_SIZE: &str = "AXSize";
pub(crate) const ATTR_ROLE: &str = "AXRole";
pub(crate) const ATTR_SUBROLE: &str = "AXSubrole";
pub(crate) const ATTR_FOCUSED_WINDOW: &str = "AXFocusedWindow";
pub(crate) const ATTR_FOCUSED: &str = "AXFocused";
pub(crate) const ATTR_MAIN: &str = "AXMain";
pub(crate) const ATTR_MINIMIZED: &str = "AXMinimized";
pub(crate) const ATTR_CHILDREN: &str = "AXChildren";
pub(crate) const ATTR_VALUE: &str = "AXValue";
pub(crate) const ATTR_FRONTMOST: &str = "AXFrontmost";
pub(crate) const ACTION_PRESS: &str = "AXPress";
pub(crate) const ACTION_RAISE: &str = "AXRaise";
pub(crate) fn app_element(pid: i32) -> CFRetained<AXUIElement> {
unsafe { AXUIElement::new_application(pid) }
}
pub(crate) fn attribute(element: &AXUIElement, name: &str) -> Option<CFRetained<CFType>> {
let key = CFString::from_str(name);
let mut raw: *const CFType = std::ptr::null();
let err = unsafe {
element.copy_attribute_value(&key, NonNull::from(&mut raw).cast::<*const CFType>())
};
if err != AXError::Success || raw.is_null() {
return None;
}
Some(unsafe { CFRetained::from_raw(NonNull::new(raw.cast_mut())?) })
}
pub(crate) fn string_attribute(element: &AXUIElement, name: &str) -> Option<String> {
let value = attribute(element, name)?;
value.downcast_ref::<CFString>().map(|s| s.to_string())
}
pub(crate) fn bool_attribute(element: &AXUIElement, name: &str) -> Option<bool> {
let value = attribute(element, name)?;
value
.downcast_ref::<objc2_core_foundation::CFBoolean>()
.map(|b| b.as_bool())
}
pub(crate) fn element_array(element: &AXUIElement, name: &str) -> Vec<CFRetained<AXUIElement>> {
let Some(value) = attribute(element, name) else {
return Vec::new();
};
let Some(array) = value.downcast_ref::<CFArray>() else {
return Vec::new();
};
let mut out = Vec::new();
for i in 0..array.count() {
let raw = unsafe { array.value_at_index(i) };
if raw.is_null() {
continue;
}
let item = unsafe {
CFRetained::retain(
NonNull::new(raw.cast_mut().cast::<AXUIElement>()).expect("checked non-null above"),
)
};
out.push(item);
}
out
}
pub(crate) fn point_attribute(element: &AXUIElement, name: &str) -> Option<CGPoint> {
let value = attribute(element, name)?;
let ax = value.downcast_ref::<AXValue>()?;
let mut point = CGPoint { x: 0.0, y: 0.0 };
let ok = unsafe {
ax.value(
AXValueType::CGPoint,
NonNull::from(&mut point).cast::<std::ffi::c_void>(),
)
};
ok.then_some(point)
}
pub(crate) fn size_attribute(element: &AXUIElement, name: &str) -> Option<CGSize> {
let value = attribute(element, name)?;
let ax = value.downcast_ref::<AXValue>()?;
let mut size = CGSize {
width: 0.0,
height: 0.0,
};
let ok = unsafe {
ax.value(
AXValueType::CGSize,
NonNull::from(&mut size).cast::<std::ffi::c_void>(),
)
};
ok.then_some(size)
}
pub(crate) fn set_point(element: &AXUIElement, name: &str, mut p: CGPoint) -> bool {
let Some(value) = (unsafe {
AXValue::new(
AXValueType::CGPoint,
NonNull::from(&mut p).cast::<std::ffi::c_void>(),
)
}) else {
return false;
};
let key = CFString::from_str(name);
unsafe { element.set_attribute_value(&key, &value) == AXError::Success }
}
pub(crate) fn set_size(element: &AXUIElement, name: &str, mut s: CGSize) -> bool {
let Some(value) = (unsafe {
AXValue::new(
AXValueType::CGSize,
NonNull::from(&mut s).cast::<std::ffi::c_void>(),
)
}) else {
return false;
};
let key = CFString::from_str(name);
unsafe { element.set_attribute_value(&key, &value) == AXError::Success }
}
pub(crate) fn set_bool(element: &AXUIElement, name: &str, v: bool) -> bool {
let key = CFString::from_str(name);
let value = objc2_core_foundation::CFBoolean::new(v);
unsafe { element.set_attribute_value(&key, value) == AXError::Success }
}
pub(crate) fn perform(element: &AXUIElement, action: &str) -> bool {
let key = CFString::from_str(action);
unsafe { element.perform_action(&key) == AXError::Success }
}
pub(crate) fn set_timeout(element: &AXUIElement, seconds: f32) {
unsafe {
let _ = element.set_messaging_timeout(seconds);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dead_process_yields_an_element_whose_queries_fail_quietly() {
let element = app_element(999_999);
assert!(string_attribute(&element, ATTR_TITLE).is_none());
assert!(element_array(&element, ATTR_WINDOWS).is_empty());
assert!(point_attribute(&element, ATTR_POSITION).is_none());
}
#[test]
fn an_unknown_attribute_is_absent_rather_than_an_error() {
let element = app_element(std::process::id() as i32);
assert!(attribute(&element, "AXDefinitelyNotAnAttribute").is_none());
}
}