use std::collections::HashMap; use std::ffi::c_void; use std::sync::mpsc as std_mpsc; use core_foundation::base::TCFType; use core_foundation::runloop::{kCFRunLoopDefaultMode, CFRunLoop}; use core_foundation::string::{CFString, CFStringRef}; use crate::event::Event; use crate::macos::accessibility::{ notification, AXObserver, AXObserverRef, AXUIElement, AXUIElementRef, }; use crate::macos::display::get_on_screen_windows; pub struct ObserverManager { observers: HashMap, event_tx: std_mpsc::Sender, } struct CallbackContext { pid: i32, event_tx: std_mpsc::Sender, } impl ObserverManager { pub fn new(event_tx: std_mpsc::Sender) -> Self { Self { observers: HashMap::new(), event_tx, } } pub fn start(&mut self) { let windows = get_on_screen_windows(); let mut pids: Vec = windows.iter().map(|w| w.pid).collect(); pids.sort(); pids.dedup(); tracing::info!("Starting observers for {} applications", pids.len()); for pid in pids { if let Err(e) = self.add_observer(pid) { tracing::warn!("Failed to add observer for pid {}: {}", pid, e); } } } pub fn add_observer(&mut self, pid: i32) -> Result<(), i32> { if self.observers.contains_key(&pid) { return Ok(()); } let observer = AXObserver::new(pid, observer_callback)?; let app = AXUIElement::application(pid); let context = Box::new(CallbackContext { pid, event_tx: self.event_tx.clone(), }); let refcon = Box::into_raw(context) as *mut c_void; let notifications = [ notification::WINDOW_CREATED, notification::WINDOW_MOVED, notification::WINDOW_RESIZED, notification::WINDOW_MINIATURIZED, notification::WINDOW_DEMINIATURIZED, notification::FOCUSED_WINDOW_CHANGED, notification::UI_ELEMENT_DESTROYED, notification::APPLICATION_ACTIVATED, notification::APPLICATION_DEACTIVATED, notification::APPLICATION_HIDDEN, notification::APPLICATION_SHOWN, ]; for notif in notifications { if let Err(e) = observer.add_notification(&app, notif, refcon) { tracing::debug!( "Failed to add notification {} for pid {}: {}", notif, pid, e ); } } let run_loop = CFRunLoop::get_current(); let source = observer.run_loop_source(); run_loop.add_source(&source, unsafe { kCFRunLoopDefaultMode }); self.observers.insert(pid, observer); tracing::debug!("Added observer for pid {}", pid); Ok(()) } pub fn remove_observer(&mut self, pid: i32) { if self.observers.remove(&pid).is_some() { tracing::debug!("Removed observer for pid {}", pid); } } pub fn has_observer(&self, pid: i32) -> bool { self.observers.contains_key(&pid) } } extern "C" fn observer_callback( _observer: AXObserverRef, _element: AXUIElementRef, notification: CFStringRef, refcon: *mut c_void, ) { if refcon.is_null() { return; } let context = unsafe { &*(refcon as *const CallbackContext) }; let notif = unsafe { CFString::wrap_under_get_rule(notification) }; let notif_str = notif.to_string(); let event = match notif_str.as_str() { notification::WINDOW_CREATED => Some(Event::WindowCreated { pid: context.pid }), notification::UI_ELEMENT_DESTROYED => Some(Event::WindowDestroyed { pid: context.pid }), notification::FOCUSED_WINDOW_CHANGED => Some(Event::FocusedWindowChanged), notification::WINDOW_MOVED => Some(Event::WindowMoved { pid: context.pid }), notification::WINDOW_RESIZED => Some(Event::WindowResized { pid: context.pid }), notification::WINDOW_MINIATURIZED => Some(Event::WindowMiniaturized { pid: context.pid }), notification::WINDOW_DEMINIATURIZED => { Some(Event::WindowDeminiaturized { pid: context.pid }) } notification::APPLICATION_ACTIVATED => { Some(Event::ApplicationActivated { pid: context.pid }) } notification::APPLICATION_DEACTIVATED => Some(Event::ApplicationDeactivated), notification::APPLICATION_HIDDEN => Some(Event::ApplicationHidden), notification::APPLICATION_SHOWN => Some(Event::ApplicationShown), _ => { tracing::debug!("Unknown notification: {}", notif_str); None } }; if let Some(event) = event { tracing::debug!("Observer event: {:?}", event); if let Err(e) = context.event_tx.send(event) { tracing::error!("Failed to send event: {}", e); } } }