use accessibility_sys::{
AXObserverRef, AXUIElementCreateApplication, AXUIElementRef, kAXErrorSuccess,
};
use bevy::ecs::component::Component;
use core::ptr::NonNull;
use derive_more::{DerefMut, with_trait::Deref};
use objc2_core_foundation::{CFRetained, CFString, kCFRunLoopCommonModes};
use std::ffi::c_void;
use std::pin::Pin;
use std::ptr::null_mut;
use std::sync::LazyLock;
use stdext::function_name;
use tracing::{debug, error};
use super::skylight::_SLPSGetFrontProcess;
use super::{ProcessApi, Window, WindowOS, ax_window_id};
use crate::errors::{Error, Result};
use crate::events::{Event, EventSender};
use crate::platform::{
AXObserverAddNotification, AXObserverCreate, AXObserverRemoveNotification, CFStringRef, ConnID,
Pid, ProcessSerialNumber, WinID,
};
use crate::util::{AXUIAttributes, AXUIWrapper, MacResult, add_run_loop, remove_run_loop};
pub static AX_NOTIFICATIONS: LazyLock<Vec<&str>> = LazyLock::new(|| {
vec![
accessibility_sys::kAXCreatedNotification,
accessibility_sys::kAXFocusedWindowChangedNotification,
accessibility_sys::kAXFocusedUIElementChangedNotification,
accessibility_sys::kAXWindowMovedNotification,
accessibility_sys::kAXWindowResizedNotification,
accessibility_sys::kAXTitleChangedNotification,
accessibility_sys::kAXMenuOpenedNotification,
accessibility_sys::kAXMenuClosedNotification,
]
});
pub static AX_WINDOW_NOTIFICATIONS: LazyLock<Vec<&str>> = LazyLock::new(|| {
vec![
accessibility_sys::kAXUIElementDestroyedNotification,
accessibility_sys::kAXWindowMiniaturizedNotification,
accessibility_sys::kAXWindowDeminiaturizedNotification,
]
});
pub trait ApplicationApi: Send + Sync {
fn pid(&self) -> Pid;
fn psn(&self) -> ProcessSerialNumber;
fn connection(&self) -> Option<ConnID>;
fn focused_window_id(&self) -> Result<WinID>;
fn window_list(&self) -> Vec<Window>;
fn observe(&mut self) -> Result<bool>;
fn observe_window(&mut self, window: &Window) -> Result<bool>;
fn unobserve_window(&mut self, window: &Window);
fn is_frontmost(&self) -> bool;
fn bundle_id(&self) -> Option<&str>;
}
#[derive(Component, Deref, DerefMut)]
pub struct Application(Box<dyn ApplicationApi>);
impl std::fmt::Display for Application {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "app (pid {})", self.pid())
}
}
impl Application {
pub fn new(app: Box<dyn ApplicationApi>) -> Self {
Application(app)
}
}
pub struct ApplicationOS {
element: CFRetained<AXUIWrapper>,
psn: ProcessSerialNumber,
pid: Pid,
connection: Option<ConnID>,
handler: AxObserverHandler,
bundle_id: Option<String>,
name: String,
}
impl Drop for ApplicationOS {
fn drop(&mut self) {
self.handler
.remove_observer(&ObserverType::Application, &self.element, &AX_NOTIFICATIONS);
}
}
impl std::fmt::Display for ApplicationOS {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "app '{}' (pid {})", self.name, self.pid)
}
}
impl ApplicationOS {
pub fn new(
connection: Option<ConnID>,
process: &dyn ProcessApi,
events: &EventSender,
) -> Result<Self> {
let refer = unsafe {
let ptr = AXUIElementCreateApplication(process.pid());
AXUIWrapper::retain(ptr)?
};
let bundle_id = process
.application()
.as_ref()
.and_then(|app| app.bundleIdentifier())
.map(|id| id.to_string());
Ok(Self {
element: refer,
psn: process.psn(),
pid: process.pid(),
connection,
handler: AxObserverHandler::new(process.pid(), events.clone())?,
bundle_id,
name: process.name().to_string(),
})
}
}
impl ApplicationApi for ApplicationOS {
fn pid(&self) -> Pid {
self.pid
}
fn psn(&self) -> ProcessSerialNumber {
self.psn
}
fn connection(&self) -> Option<ConnID> {
self.connection
}
fn focused_window_id(&self) -> Result<WinID> {
self.element.focused_window_id()
}
fn window_list(&self) -> Vec<Window> {
self.element
.windows()
.map(|windows| {
windows
.into_iter()
.flat_map(|element| {
WindowOS::new(&element).map(|window| Window::new(Box::new(window)))
})
.collect()
})
.unwrap_or_default()
}
fn observe(&mut self) -> Result<bool> {
self.handler
.add_observer(&self.element, &AX_NOTIFICATIONS, ObserverType::Application)
.map(|retry| retry.is_empty())
}
fn observe_window(&mut self, window: &Window) -> Result<bool> {
if let Some(element) = window.element() {
self.handler
.add_observer(
&element,
&AX_WINDOW_NOTIFICATIONS,
ObserverType::Window(window.id()),
)
.map(|retry| retry.is_empty())
} else {
Err(Error::InvalidWindow)
}
}
fn unobserve_window(&mut self, window: &Window) {
if let Some(element) = window.element() {
self.handler.remove_observer(
&ObserverType::Window(window.id()),
&element,
&AX_WINDOW_NOTIFICATIONS,
);
}
}
fn is_frontmost(&self) -> bool {
let mut psn = ProcessSerialNumber::default();
unsafe { _SLPSGetFrontProcess(&mut psn) }
.to_result(function_name!())
.is_ok()
&& self.psn == psn
}
fn bundle_id(&self) -> Option<&str> {
self.bundle_id.as_deref()
}
}
enum ObserverType {
Application,
Window(WinID),
}
struct ObserverContext {
events: EventSender,
which: ObserverType,
}
impl ObserverContext {
fn notify(&self, notification: &str, element: AXUIElementRef) {
match self.which {
ObserverType::Application => self.notify_app(notification, element),
ObserverType::Window(id) => self.notify_window(notification, id),
}
}
fn notify_app(&self, notification: &str, element: AXUIElementRef) {
match notification {
accessibility_sys::kAXTitleChangedNotification => {
return;
}
accessibility_sys::kAXCreatedNotification => {
let Ok(element) = AXUIWrapper::retain(element).inspect_err(|err| {
error!("invalid element {element:?}: {err}");
}) else {
return;
};
_ = self.events.send(Event::WindowCreated { element });
return;
}
_ => (),
}
let Ok(window_id) =
ax_window_id(element).inspect_err(|err| debug!("notification {notification}: {err}"))
else {
return;
};
let event = match notification {
accessibility_sys::kAXFocusedWindowChangedNotification
| accessibility_sys::kAXFocusedUIElementChangedNotification => {
Event::WindowFocused { window_id }
}
accessibility_sys::kAXWindowMovedNotification => Event::WindowMoved { window_id },
accessibility_sys::kAXWindowResizedNotification => Event::WindowResized { window_id },
accessibility_sys::kAXMenuOpenedNotification => Event::MenuOpened { window_id },
accessibility_sys::kAXMenuClosedNotification => Event::MenuClosed { window_id },
_ => {
error!("unhandled application notification: {notification:?}");
return;
}
};
_ = self.events.send(event);
}
fn notify_window(&self, notification: &str, window_id: WinID) {
let event = match notification {
accessibility_sys::kAXWindowMiniaturizedNotification => {
Event::WindowMinimized { window_id }
}
accessibility_sys::kAXWindowDeminiaturizedNotification => {
Event::WindowDeminimized { window_id }
}
accessibility_sys::kAXUIElementDestroyedNotification => {
Event::WindowDestroyed { window_id }
}
_ => {
error!("unhandled window notification: {notification:?}");
return;
}
};
_ = self.events.send(event);
}
}
struct AxObserverHandler {
observer: CFRetained<AXUIWrapper>,
events: EventSender,
contexts: Vec<Pin<Box<ObserverContext>>>,
}
impl Drop for AxObserverHandler {
fn drop(&mut self) {
remove_run_loop(&self.observer);
}
}
impl AxObserverHandler {
fn new(pid: Pid, events: EventSender) -> Result<Self> {
let observer = unsafe {
let mut observer_ref: AXObserverRef = null_mut();
if kAXErrorSuccess == AXObserverCreate(pid, Self::callback, &mut observer_ref) {
AXUIWrapper::from_retained(observer_ref)?
} else {
return Err(Error::PermissionDenied(format!(
"{}: error creating observer.",
function_name!()
)));
}
};
unsafe { add_run_loop(&observer, kCFRunLoopCommonModes)? };
Ok(Self {
observer,
events,
contexts: Vec::new(),
})
}
pub fn add_observer(
&mut self,
element: &AXUIWrapper,
notifications: &[&'static str],
which: ObserverType,
) -> Result<Vec<&str>> {
let observer: AXObserverRef = self.observer.as_ptr();
let context = Box::pin(ObserverContext {
events: self.events.clone(),
which,
});
let context_ptr = NonNull::from_ref(&*context).as_ptr();
self.contexts.push(context);
let mut retry = vec![];
let added = notifications
.iter()
.filter_map(|name| {
debug!("adding {name} {element:x?} {observer:?}");
let notification = CFString::from_static_str(name);
match unsafe {
AXObserverAddNotification(
observer,
element.as_ptr(),
¬ification,
context_ptr.cast(),
)
} {
accessibility_sys::kAXErrorSuccess
| accessibility_sys::kAXErrorNotificationAlreadyRegistered => Some(*name),
accessibility_sys::kAXErrorCannotComplete => {
retry.push(*name);
None
}
result => {
error!("error adding {name} {element:x?} {observer:?}: {result}");
None
}
}
})
.collect::<Vec<_>>();
if added.is_empty() {
Err(Error::PermissionDenied(format!(
"{}: unable to register any observers!",
function_name!()
)))
} else {
Ok(retry)
}
}
pub fn remove_observer(
&mut self,
which: &ObserverType,
element: &AXUIWrapper,
notifications: &[&'static str],
) {
for name in notifications {
let observer: AXObserverRef = self.observer.deref().as_ptr();
let notification = CFString::from_static_str(name);
debug!("removing {name} {element:x?} {observer:?}");
let result =
unsafe { AXObserverRemoveNotification(observer, element.as_ptr(), ¬ification) };
if result != kAXErrorSuccess {
debug!("error removing {name} {element:x?} {observer:?}: {result}");
}
}
if let ObserverType::Window(removed) = which {
self.contexts.retain(
|context| !matches!(context.which, ObserverType::Window(window_id) if window_id == *removed),
);
}
}
extern "C" fn callback(
_observer: AXObserverRef,
element: AXUIElementRef,
notification: CFStringRef,
context: *mut c_void,
) {
let notification = NonNull::new(notification.cast_mut())
.map(|ptr| unsafe { ptr.as_ref() })
.map(CFString::to_string);
let context =
NonNull::new(context.cast::<ObserverContext>()).map(|ptr| unsafe { ptr.as_ref() });
let Some((notification, context)) = notification.zip(context) else {
return;
};
context.notify(¬ification, element);
}
}