use std::sync::OnceLock;
use accessibility_sys::{AXError, AXObserverRef, AXUIElementRef};
use objc2::MainThreadMarker;
use objc2::rc::{Retained, autoreleasepool};
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEventMask};
use objc2_core_foundation::CFString;
use objc2_foundation::{NSDate, NSDefaultRunLoopMode, NSProcessInfo};
use std::ffi::c_void;
use std::pin::Pin;
use tracing::error;
use crate::config::{CONFIGURATION_FILE, Config};
use crate::errors::{Error, Result};
use crate::events::{Event, EventSender};
use crate::manager::{check_ax_privilege, check_separate_spaces};
use crate::platform::display::PinnedDisplayHandler;
use crate::platform::input::PinnedInputHandler;
use crate::platform::process::PinnedProcessHandler;
use display::DisplayHandler;
use input::InputHandler;
use mission_control::MissionControlHandler;
use process::ProcessHandler;
pub use process::ProcessSerialNumber;
pub use workspace::WorkspaceObserver;
mod display;
pub(crate) mod input;
mod mission_control;
mod process;
pub mod service;
mod workspace;
pub type OSStatus = i32;
pub type WinID = i32;
pub type ConnID = i64;
pub type Pid = i32;
pub type CFStringRef = *const CFString;
pub type WorkspaceId = u64;
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Modifiers: u8 {
const ALT = 1 << 0;
const SHIFT = 1 << 1;
const CMD = 1 << 2;
const CTRL = 1 << 3;
}
}
type AXObserverCallback = unsafe extern "C" fn(
observer: AXObserverRef,
element: AXUIElementRef,
notification: CFStringRef,
refcon: *mut c_void,
);
unsafe extern "C" {
pub fn AXObserverCreate(
application: Pid,
callback: AXObserverCallback,
out_observer: &mut AXObserverRef,
) -> AXError;
pub fn AXObserverAddNotification(
observer: AXObserverRef,
element: AXUIElementRef,
notification: &CFString,
refcon: *mut c_void,
) -> AXError;
pub fn AXObserverRemoveNotification(
observer: AXObserverRef,
element: AXUIElementRef,
notification: &CFString,
) -> AXError;
}
pub struct PlatformCallbacks {
pub main_thread_marker: MainThreadMarker,
cocoa_app: Retained<NSApplication>,
events: EventSender,
process_handler: Option<PinnedProcessHandler>,
event_handler: Option<PinnedInputHandler>,
workspace_observer: Retained<WorkspaceObserver>,
mission_control_observer: MissionControlHandler,
display_handler: Option<PinnedDisplayHandler>,
}
impl PlatformCallbacks {
pub fn new(events: EventSender) -> Pin<Box<Self>> {
let main_thread_marker = MainThreadMarker::new().unwrap();
let cocoa_app = NSApplication::sharedApplication(main_thread_marker);
cocoa_app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
cocoa_app.finishLaunching();
NSApplication::load();
let workspace_observer = WorkspaceObserver::new(events.clone());
Box::pin(PlatformCallbacks {
main_thread_marker,
cocoa_app,
process_handler: None,
event_handler: None,
workspace_observer,
mission_control_observer: MissionControlHandler::new(events.clone()),
display_handler: None,
events,
})
}
pub fn setup_handlers(&mut self) -> Result<()> {
if !check_ax_privilege() {
return Err(Error::PermissionDenied(
"Accessibility permissions are required. Please enable them in System Preferences -> Security & Privacy -> Privacy -> Accessibility.".to_string(),
));
}
if !check_separate_spaces() {
error!("Option 'display has separate spaces' disabled.");
return Err(Error::InvalidConfig(
"Option 'display has separate spaces' disabled.".to_string(),
));
}
let config = Config::new(CONFIGURATION_FILE.as_path())?;
self.events.send(Event::InitialConfig(config.clone()))?;
self.event_handler = Some(InputHandler::new(self.events.clone(), config).start()?);
self.display_handler = Some(DisplayHandler::new(self.events.clone()).start()?);
self.process_handler = Some(
ProcessHandler::new(self.events.clone(), self.workspace_observer.clone()).start()?,
);
self.mission_control_observer.observe()?;
self.workspace_observer.start();
self.events.send(Event::ProcessesLoaded)
}
pub fn pump_cocoa_event_loop(&mut self, timeout: f64) {
autoreleasepool(|_| {
let until_date = NSDate::dateWithTimeIntervalSinceNow(timeout);
while let Some(event) = unsafe {
self.cocoa_app
.nextEventMatchingMask_untilDate_inMode_dequeue(
NSEventMask::Any,
Some(&until_date),
NSDefaultRunLoopMode,
true, )
} {
self.cocoa_app.sendEvent(&event);
}
self.cocoa_app.updateWindows();
});
}
}
pub fn macos_major_version() -> u32 {
static VERSION: OnceLock<u32> = OnceLock::new();
*VERSION.get_or_init(|| {
let version = NSProcessInfo::processInfo().operatingSystemVersion();
u32::try_from(version.majorVersion).unwrap_or(16)
})
}
#[cfg(test)]
mod tests {
#[test]
fn macos_major_version_returns_valid() {
let v = super::macos_major_version();
assert!(v >= 13, "expected macOS 13+, got {v}");
}
}