use accessibility_sys::{
AXIsProcessTrustedWithOptions, AXObserverGetRunLoopSource, AXUIElementRef,
kAXTrustedCheckOptionPrompt,
};
use core::ptr::NonNull;
use log::debug;
use objc2_core_foundation::{
CFArray, CFDictionary, CFNumber, CFNumberType, CFRetained, CFRunLoop, CFRunLoopMode,
CFRunLoopSource, CFString, CFType, Type, kCFBooleanTrue, kCFCopyStringDictionaryKeyCallBacks,
kCFTypeArrayCallBacks, kCFTypeDictionaryValueCallBacks,
};
use std::{
ffi::{CStr, OsStr, c_int, c_void},
io::ErrorKind,
os::unix::ffi::OsStrExt,
path::PathBuf,
ptr::null_mut,
};
use stdext::function_name;
use crate::errors::{Error, Result};
use crate::skylight::AXUIElementCopyAttributeValue;
pub struct Cleanuper {
cleanup: Box<dyn Fn()>,
}
unsafe impl Send for Cleanuper {}
impl Drop for Cleanuper {
fn drop(&mut self) {
(self.cleanup)();
}
}
impl Cleanuper {
pub fn new(cleanup: Box<dyn Fn()>) -> Self {
Cleanuper { cleanup }
}
}
#[derive(Debug)]
pub struct AXUIWrapper;
unsafe impl objc2_core_foundation::Type for AXUIWrapper {}
impl AXUIWrapper {
pub fn as_ptr<T>(&self) -> *mut T {
NonNull::from(self).cast::<T>().as_ptr()
}
pub fn from_ptr<T>(ptr: *mut T) -> Result<NonNull<Self>> {
NonNull::new(ptr)
.map(std::ptr::NonNull::cast)
.ok_or(Error::new(
ErrorKind::InvalidInput,
format!("{}: nullptr passed.", function_name!()),
))
}
pub fn from_retained<T>(ptr: *mut T) -> Result<CFRetained<Self>> {
let ptr = Self::from_ptr(ptr)?;
Ok(unsafe { CFRetained::from_raw(ptr) })
}
pub fn retain<T>(ptr: *mut T) -> Result<CFRetained<Self>> {
let ptr = Self::from_ptr(ptr)?;
Ok(unsafe { ptr.as_ref() }.retain())
}
}
impl<T> std::convert::AsRef<T> for AXUIWrapper {
fn as_ref(&self) -> &T {
let ptr = NonNull::from(self).cast();
unsafe { ptr.as_ref() }
}
}
impl std::fmt::Display for AXUIWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.as_ptr::<AXUIElementRef>())
}
}
pub fn get_attribute<T: Type>(
element_ref: &CFRetained<AXUIWrapper>,
name: &CFRetained<CFString>,
) -> Result<CFRetained<T>> {
let mut attribute: *mut CFType = null_mut();
if 0 == unsafe { AXUIElementCopyAttributeValue(element_ref.as_ptr(), name, &mut attribute) } {
NonNull::new(attribute)
.map(|ptr| unsafe { CFRetained::from_raw(ptr.cast()) })
.ok_or(Error::new(
ErrorKind::InvalidData,
format!(
"{}: nullptr while getting attribute {name}.",
function_name!()
),
))
} else {
Err(Error::new(
ErrorKind::NotFound,
format!("{}: failed getting attribute {name}.", function_name!()),
))
}
}
pub fn get_cfdict_value<T>(dict: &CFDictionary, key: &CFString) -> Result<NonNull<T>> {
let ptr = unsafe { CFDictionary::value(dict, NonNull::from(key).as_ptr().cast()) };
NonNull::new(ptr.cast_mut())
.map(std::ptr::NonNull::cast::<T>)
.ok_or(Error::new(
ErrorKind::InvalidData,
format!("{}: can not get data for key {key}", function_name!(),),
))
}
pub fn get_array_values<T>(array: &CFArray) -> impl Iterator<Item = NonNull<T>> + use<'_, T> {
let count = CFArray::count(array);
(0..count).filter_map(move |idx| {
NonNull::new(unsafe { CFArray::value_at_index(array, idx).cast_mut() })
.map(std::ptr::NonNull::cast::<T>)
})
}
pub fn create_array<T>(values: &[T], cftype: CFNumberType) -> Result<CFRetained<CFArray>> {
let numbers = values
.iter()
.filter_map(|value: &T| unsafe {
CFNumber::new(None, cftype, NonNull::from(value).as_ptr().cast())
})
.collect::<Vec<_>>();
let mut ptrs = numbers
.iter()
.map(|num| NonNull::from(&**num).as_ptr() as *const c_void)
.collect::<Vec<_>>();
unsafe {
CFArray::new(
None,
ptrs.as_mut_ptr(),
numbers.len().try_into().unwrap(),
&raw const kCFTypeArrayCallBacks,
)
}
.ok_or(Error::new(
ErrorKind::InvalidData,
format!("{}: can not create an array.", function_name!()),
))
}
fn run_loop_source(observer: &AXUIWrapper) -> Option<&CFRunLoopSource> {
let ptr = NonNull::new(unsafe { AXObserverGetRunLoopSource(observer.as_ptr()) })?;
Some(unsafe { ptr.cast::<CFRunLoopSource>().as_ref() })
}
pub fn add_run_loop(observer: &AXUIWrapper, mode: Option<&CFRunLoopMode>) -> Result<()> {
let run_loop = run_loop_source(observer);
match CFRunLoop::main() {
Some(main_loop) if run_loop.is_some() => {
debug!(
"{}: add runloop: {run_loop:?} observer {:?}",
function_name!(),
observer.as_ptr::<CFRunLoopSource>(),
);
CFRunLoop::add_source(&main_loop, run_loop, mode);
Ok(())
}
_ => Err(Error::new(
ErrorKind::PermissionDenied,
format!(
"{}: Unable to register run loop source for observer {:?} ",
function_name!(),
observer.as_ptr::<CFRunLoopSource>(),
),
)),
}
}
pub fn remove_run_loop(observer: &AXUIWrapper) {
if let Some(run_loop_source) = run_loop_source(observer) {
debug!(
"{}: removing runloop: {run_loop_source:?} observer {:?}",
function_name!(),
observer.as_ptr::<CFRunLoopSource>(),
);
CFRunLoopSource::invalidate(run_loop_source);
}
}
#[must_use]
pub fn exe_path() -> Option<PathBuf> {
#[link(name = "Foundation", kind = "framework")]
unsafe extern "C" {
fn _NSGetExecutablePath(buf: *mut u8, buf_size: *mut u32) -> c_int;
}
let mut path_buf = [0_u8; 4096];
let mut path_buf_size = u32::try_from(path_buf.len()).unwrap();
let path = unsafe { _NSGetExecutablePath(path_buf.as_mut_ptr(), &raw mut path_buf_size) == 0 }
.then(|| CStr::from_bytes_until_nul(&path_buf).ok())??;
Some(OsStr::from_bytes(path.to_bytes()).into())
}
#[allow(dead_code)]
pub fn check_ax_privilege() -> bool {
unsafe {
let mut keys = vec![kAXTrustedCheckOptionPrompt.cast::<c_void>()];
let mut values = vec![NonNull::from(kCFBooleanTrue.unwrap()).as_ptr() as *const c_void];
CFDictionary::new(
None,
keys.as_mut_ptr(),
values.as_mut_ptr(),
isize::try_from(keys.len()).unwrap(),
&raw const kCFCopyStringDictionaryKeyCallBacks,
&raw const kCFTypeDictionaryValueCallBacks,
)
.map(|options| NonNull::from_ref(&*options).as_ptr())
.map(|options| AXIsProcessTrustedWithOptions(options.cast()))
.is_some_and(|supported| supported)
}
}