use accessibility_sys::{AXIsProcessTrustedWithOptions, kAXTrustedCheckOptionPrompt};
use bevy::ecs::resource::Resource;
use bevy::math::{IRect, IVec2};
use core::ptr::NonNull;
use derive_more::{DerefMut, with_trait::Deref};
use notify::{RecursiveMode, Watcher};
use objc2_core_foundation::{
CFArray, CFDictionary, CFMutableData, CFNumber, CFNumberType, CFRetained, CFString, CFType,
CGPoint, CGRect, CGSize, kCFBooleanTrue,
};
use objc2_core_graphics::{
CGDirectDisplayID, CGDisplayBounds, CGError, CGGetActiveDisplayList, CGWarpMouseCursorPosition,
};
use std::path::Path;
use std::ptr::null_mut;
use std::slice::from_raw_parts_mut;
use std::time::Duration;
use stdext::function_name;
use tracing::{Level, debug, error, instrument, trace, warn};
use crate::errors::{Error, Result};
use crate::events::{Event, EventSender};
use crate::manager::skylight::SLSSetWindowListBrightness;
use crate::platform::{ConnID, Pid, ProcessSerialNumber, WinID, WorkspaceId};
use crate::util::{AXUIWrapper, MacResult, create_array, symlink_target};
use app::ApplicationOS;
pub use app::{Application, ApplicationApi};
pub use display::Display;
pub use process::{Process, ProcessApi};
pub use skylight::AXUIElementCopyAttributeValue;
use skylight::{
_AXUIElementCreateWithRemoteToken, SLSCopyActiveMenuBarDisplayIdentifier,
SLSCopyAssociatedWindows, SLSCopyManagedDisplaySpaces, SLSCopyWindowsWithOptionsAndTags,
SLSFindWindowAndOwner, SLSGetConnectionIDForPSN, SLSGetCurrentCursorLocation,
SLSGetDisplayMenubarHeight, SLSGetSpaceManagementMode, SLSMainConnectionID,
SLSManagedDisplayGetCurrentSpace, SLSSpaceGetType, SLSWindowIteratorAdvance,
SLSWindowIteratorGetAttributes, SLSWindowIteratorGetParentID, SLSWindowIteratorGetTags,
SLSWindowIteratorGetWindowID, SLSWindowQueryResultCopyWindows, SLSWindowQueryWindows,
};
pub use windows::{Window, WindowApi, WindowOS, WindowPadding, ax_window_id};
pub(crate) mod app;
mod display;
mod process;
mod skylight;
mod windows;
pub type Origin = IVec2;
pub type Size = IVec2;
pub fn origin_from(point: CGPoint) -> Origin {
Origin::new(point.x as i32, point.y as i32)
}
pub fn origin_to(point: Origin) -> CGPoint {
CGPoint::new(point.x.into(), point.y.into())
}
pub fn size_from(size: CGSize) -> Size {
Size::new(size.width as i32, size.height as i32)
}
pub fn irect_from(rect: CGRect) -> IRect {
let mid = rect.mid();
IRect::from_center_size(origin_from(mid), size_from(rect.size))
}
pub trait WindowManagerApi: Send + Sync {
fn new_application(&self, process: &dyn ProcessApi) -> Result<Application>;
fn get_associated_windows(&self, window_id: WinID) -> Vec<WinID>;
fn present_displays(&self) -> Vec<(Display, Vec<WorkspaceId>)>;
fn active_display_id(&self) -> Result<u32>;
fn active_display_space(&self, display_id: CGDirectDisplayID) -> Result<WorkspaceId>;
fn is_fullscreen_space(&self, display_id: CGDirectDisplayID) -> bool;
fn warp_mouse(&self, origin: Origin);
fn find_existing_application_windows(
&self,
app: &mut Application,
spaces: &[WorkspaceId],
) -> Result<(Vec<Window>, Vec<WinID>)>;
fn find_window_at_point(&self, point: &CGPoint) -> Result<WinID>;
fn windows_in_workspace(&self, space_id: WorkspaceId) -> Result<Vec<WinID>>;
fn quit(&self) -> Result<()>;
fn setup_config_watcher(&self, path: &Path) -> Result<Box<dyn Watcher>>;
fn cursor_position(&self) -> Option<CGPoint>;
fn dim_windows(&self, windows: &[WinID], level: f32);
}
#[derive(Deref, DerefMut, Resource)]
pub struct WindowManager(pub Box<dyn WindowManagerApi>);
pub struct WindowManagerOS {
main_cid: ConnID,
event_sender: EventSender,
}
impl WindowManagerOS {
pub fn new(event_sender: EventSender) -> Self {
let main_cid = unsafe { SLSMainConnectionID() };
debug!("My connection id: {main_cid}");
Self {
main_cid,
event_sender,
}
}
fn display_space_list(&self, uuid: &CFString) -> Result<Vec<WorkspaceId>> {
let display_spaces = NonNull::new(unsafe { SLSCopyManagedDisplaySpaces(self.main_cid) })
.map(|ptr| unsafe { CFRetained::from_raw(ptr) })
.ok_or(Error::PermissionDenied(format!(
"can not copy managed display spaces for {}.",
self.main_cid
)))?;
let uuid = uuid.to_string();
let display = display_spaces.iter().find(|display| {
let identifier = display
.get(&CFString::from_static_str("Display Identifier"))
.map(|name| name.to_string());
identifier.is_some_and(|identifier| {
identifier == "Main" || identifier == uuid
})
});
let Some(display) = display else {
return Err(Error::PermissionDenied(format!(
"could not get any displays for {}",
self.main_cid
)));
};
debug!("found display with uuid '{uuid}'");
let display = unsafe {
display.cast_unchecked::<CFString, CFArray<CFDictionary<CFString, CFNumber>>>()
};
let Some(spaces) = display.get(&CFString::from_static_str("Spaces")) else {
return Err(Error::PermissionDenied(format!(
"could not get any spaces for dislay '{uuid}'",
)));
};
let spaces = spaces
.iter()
.filter_map(|space| {
space
.get(&CFString::from_static_str("id64"))
.and_then(|id| id.as_i64().and_then(|value| u64::try_from(value).ok()))
})
.collect::<Vec<WorkspaceId>>();
debug!(
"spaces [{}]",
spaces
.iter()
.map(|id| format!("{id}"))
.collect::<Vec<_>>()
.join(", ")
);
Ok(spaces)
}
fn active_display_uuid(&self) -> Result<CFRetained<CFString>> {
unsafe {
let ptr = SLSCopyActiveMenuBarDisplayIdentifier(self.main_cid);
let ptr = NonNull::new(ptr.cast_mut()).ok_or(Error::NotFound(format!(
"can not find active display for connection {}.",
self.main_cid
)))?;
Ok(CFRetained::from_raw(ptr))
}
}
fn connection_for_process(&self, psn: ProcessSerialNumber) -> Option<ConnID> {
let mut connection: ConnID = 0;
unsafe { SLSGetConnectionIDForPSN(self.main_cid, &psn, &mut connection) };
(connection != 0).then_some(connection)
}
}
impl WindowManagerApi for WindowManagerOS {
fn new_application(&self, process: &dyn ProcessApi) -> Result<Application> {
let connection = self.connection_for_process(process.psn());
ApplicationOS::new(connection, process, &self.event_sender)
.map(|app| Application::new(Box::new(app)))
}
#[instrument(level = Level::TRACE, skip(self), ret)]
fn get_associated_windows(&self, window_id: WinID) -> Vec<WinID> {
trace!("for window {window_id}");
let windows =
unsafe { CFRetained::retain(SLSCopyAssociatedWindows(self.main_cid, window_id)) };
windows.into_iter().filter_map(|id| id.as_i32()).collect()
}
#[instrument(level = Level::DEBUG, skip_all, ret)]
fn present_displays(&self) -> Vec<(Display, Vec<WorkspaceId>)> {
let mut count = 0u32;
unsafe {
CGGetActiveDisplayList(0, null_mut(), &raw mut count);
}
if count < 1 {
return vec![];
}
let mut displays = Vec::with_capacity(count.try_into().unwrap());
unsafe {
CGGetActiveDisplayList(count, displays.as_mut_ptr(), &raw mut count);
displays.set_len(count.try_into().unwrap());
}
displays
.into_iter()
.filter_map(|id| {
let bounds = CGDisplayBounds(id);
let mut menubar_height: u32 = 0;
unsafe { SLSGetDisplayMenubarHeight(id, &raw mut menubar_height) };
debug!("menubar height: {menubar_height}");
let workspaces = Display::uuid_from_id(id)
.and_then(|uuid| self.display_space_list(uuid.as_ref()))
.ok()?;
Some((
Display::new(id, irect_from(bounds), menubar_height.cast_signed()),
workspaces,
))
})
.collect()
}
#[instrument(level = Level::TRACE, skip_all, ret)]
fn active_display_id(&self) -> Result<u32> {
let uuid = self.active_display_uuid()?;
Display::id_from_uuid(&uuid)
}
fn active_display_space(&self, display_id: CGDirectDisplayID) -> Result<WorkspaceId> {
Display::uuid_from_id(display_id).map(|uuid| unsafe {
SLSManagedDisplayGetCurrentSpace(self.main_cid, &raw const *uuid)
})
}
fn is_fullscreen_space(&self, display_id: CGDirectDisplayID) -> bool {
self.active_display_space(display_id)
.map(|space_id| unsafe { SLSSpaceGetType(self.main_cid, space_id) } == 4)
.unwrap_or(false)
}
#[instrument(level = Level::DEBUG, skip_all, fields(window))]
fn warp_mouse(&self, origin: Origin) {
CGWarpMouseCursorPosition(origin_to(origin));
}
fn find_existing_application_windows(
&self,
app: &mut Application,
spaces: &[WorkspaceId],
) -> Result<(Vec<Window>, Vec<WinID>)> {
let global_window_list = existing_application_window_list(self.main_cid, app, spaces)?;
if global_window_list.is_empty() {
return Err(Error::InvalidInput(format!("No windows found for {app}")));
}
debug!("{app} has global windows: {global_window_list:?}");
let found_windows = app.window_list();
if found_windows.len() == global_window_list.len() {
debug!("All windows for {:?} are now resolved", app.psn());
return Ok((found_windows, vec![]));
}
let find_window = |window_id| found_windows.iter().find(|window| window.id() == window_id);
let offscreen_windows = global_window_list
.into_iter()
.filter(|&window_id| find_window(window_id).is_none())
.collect::<Vec<_>>();
debug!(
"{:?} has {} windows that are not yet resolved",
app.psn(),
offscreen_windows.len()
);
Ok((found_windows, offscreen_windows))
}
fn find_window_at_point(&self, point: &CGPoint) -> Result<WinID> {
let mut window_id: WinID = 0;
let mut window_conn_id: ConnID = 0;
let mut window_point = CGPoint { x: 0f64, y: 0f64 };
unsafe {
SLSFindWindowAndOwner(
self.main_cid,
0, 1,
0,
point,
&mut window_point,
&mut window_id,
&mut window_conn_id,
)
}
.to_result(function_name!())?;
if self.main_cid == window_conn_id {
unsafe {
SLSFindWindowAndOwner(
self.main_cid,
window_id,
-1,
0,
point,
&mut window_point,
&mut window_id,
&mut window_conn_id,
)
}
.to_result(function_name!())?;
}
if window_id == 0 {
Err(Error::invalid_window(&format!(
"could not find a window at {point:?}",
)))
} else {
Ok(window_id)
}
}
fn windows_in_workspace(&self, space_id: WorkspaceId) -> Result<Vec<WinID>> {
space_window_list_for_connection(self.main_cid, &[space_id], None, true)
}
fn quit(&self) -> Result<()> {
self.event_sender.send(Event::Exit)
}
fn cursor_position(&self) -> Option<CGPoint> {
let mut cursor = CGPoint::default();
if unsafe { CGError::Success != SLSGetCurrentCursorLocation(self.main_cid, &mut cursor) } {
return None;
}
Some(cursor)
}
fn setup_config_watcher(&self, path: &Path) -> Result<Box<dyn Watcher>> {
let setup = notify::Config::default()
.with_poll_interval(Duration::from_secs(3))
.with_follow_symlinks(false);
let config_handler = ConfigHandler(self.event_sender.clone());
let symlink = symlink_target(path);
let mut watcher = if let Some(symlink) = symlink {
setup.with_follow_symlinks(true);
let mut watcher = notify::PollWatcher::new(config_handler, setup)?;
debug!("watching symlink target {} for changes.", symlink.display());
watcher.watch(&symlink, RecursiveMode::NonRecursive)?;
Ok::<Box<dyn Watcher>, Error>(Box::new(watcher))
} else {
Ok::<Box<dyn Watcher>, Error>(Box::new(notify::RecommendedWatcher::new(
config_handler,
setup,
)?))
}?;
debug!("watching config file {} for changes.", path.display());
watcher.watch(path, RecursiveMode::NonRecursive)?;
Ok(watcher)
}
fn dim_windows(&self, windows: &[WinID], level: f32) {
let Ok(count) = isize::try_from(windows.len()) else {
return;
};
let levels = vec![level; windows.len()];
_ = unsafe {
SLSSetWindowListBrightness(self.main_cid, windows.as_ptr(), levels.as_ptr(), count)
}
.to_result(function_name!())
.inspect_err(|err| debug!("{err}"));
}
}
fn space_window_list_for_connection(
main_cid: ConnID,
spaces: &[WorkspaceId],
cid: Option<ConnID>,
also_minimized: bool,
) -> Result<Vec<WinID>> {
let iterator = window_iterator_for_connection(main_cid, spaces, cid, also_minimized)?;
let count = spaces.len();
let mut window_list = Vec::with_capacity(count);
while unsafe { SLSWindowIteratorAdvance(&raw const *iterator) } {
let tags = unsafe { SLSWindowIteratorGetTags(&raw const *iterator) };
let attributes = unsafe { SLSWindowIteratorGetAttributes(&raw const *iterator) };
let parent_wid: WinID = unsafe { SLSWindowIteratorGetParentID(&raw const *iterator) };
let window_id: WinID = unsafe { SLSWindowIteratorGetWindowID(&raw const *iterator) };
trace!(
"id: {window_id} parent: {parent_wid} tags: 0x{tags:x} attributes: 0x{attributes:x}",
);
if found_valid_window(parent_wid, attributes, tags) {
window_list.push(window_id);
}
}
Ok(window_list)
}
fn window_iterator_for_connection(
main_cid: ConnID,
spaces: &[WorkspaceId],
cid: Option<ConnID>,
also_minimized: bool,
) -> Result<CFRetained<CFType>> {
let space_list_ref = create_array(spaces, CFNumberType::SInt64Type)?;
let mut set_tags = 0i64;
let mut clear_tags = 0i64;
let options = if also_minimized { 0x7 } else { 0x2 };
let ptr = NonNull::new(unsafe {
SLSCopyWindowsWithOptionsAndTags(
main_cid,
cid.unwrap_or(0),
&raw const *space_list_ref,
options,
&mut set_tags,
&mut clear_tags,
)
})
.ok_or(Error::InvalidInput(format!(
"{}: nullptr returned from SLSCopyWindowsWithOptionsAndTags.",
function_name!()
)))?;
let window_list_ref = unsafe { CFRetained::from_raw(ptr) };
let count = window_list_ref.count();
if count == 0 {
return Err(Error::NotFound(format!(
"{}: zero windows returned",
function_name!()
)));
}
let query = unsafe {
CFRetained::from_raw(SLSWindowQueryWindows(
main_cid,
&raw const *window_list_ref,
count,
))
};
Ok(unsafe { CFRetained::from_raw(SLSWindowQueryResultCopyWindows(query.deref().into())) })
}
pub fn window_iterator_for_id(window_id: WinID) -> Option<CFRetained<CFType>> {
let cid = unsafe { SLSMainConnectionID() };
let windows = create_array(&[window_id], CFNumberType::SInt32Type).ok()?;
let query = unsafe { CFRetained::from_raw(SLSWindowQueryWindows(cid, &raw const *windows, 1)) };
Some(unsafe { CFRetained::from_raw(SLSWindowQueryResultCopyWindows(query.deref().into())) })
}
fn found_valid_window(parent_wid: WinID, attributes: i64, tags: i64) -> bool {
parent_wid == 0
&& ((0 != (attributes & 0x2) || 0 != (tags & 0x0400_0000_0000_0000))
&& (0 != (tags & 0x1) || (0 != (tags & 0x2) && 0 != (tags & 0x8000_0000))))
|| ((attributes == 0x0 || attributes == 0x1)
&& (0 != (tags & 0x1000_0000_0000_0000) || 0 != (tags & 0x0300_0000_0000_0000))
&& (0 != (tags & 0x1) || (0 != (tags & 0x2) && 0 != (tags & 0x8000_0000))))
}
fn existing_application_window_list(
cid: ConnID,
app: &Application,
spaces: &[WorkspaceId],
) -> Result<Vec<WinID>> {
if spaces.is_empty() {
return Err(Error::NotFound(format!(
"{}: no spaces returned",
function_name!()
)));
}
space_window_list_for_connection(cid, spaces, app.connection(), true)
}
pub fn bruteforce_windows(pid: Pid, mut window_list: Vec<WinID>) -> Vec<Window> {
const MAGIC: u32 = 0x636f_636f;
const BUFSIZE: isize = 0x14;
let mut found_windows = Vec::new();
debug!("{pid} has unresolved window on other desktops, bruteforcing them.");
let Some(data_ref) = CFMutableData::new(None, BUFSIZE) else {
error!("error creating mutable data");
return found_windows;
};
CFMutableData::increase_length(data_ref.deref().into(), BUFSIZE);
let data = unsafe {
from_raw_parts_mut(
CFMutableData::mutable_byte_ptr(data_ref.deref().into()),
BUFSIZE as usize,
)
};
let bytes = pid.to_ne_bytes();
data[0x0..bytes.len()].copy_from_slice(&bytes);
let bytes = MAGIC.to_ne_bytes();
data[0x8..0x8 + bytes.len()].copy_from_slice(&bytes);
for element_id in 0..0x7fffu64 {
let bytes = element_id.to_ne_bytes();
data[0xc..0xc + bytes.len()].copy_from_slice(&bytes);
let Ok(element_ref) =
AXUIWrapper::retain(unsafe { _AXUIElementCreateWithRemoteToken(data_ref.as_ref()) })
else {
continue;
};
let Ok(window_id) = ax_window_id(element_ref.as_ptr()) else {
continue;
};
if let Some(index) = window_list.iter().position(|&id| id == window_id) {
window_list.remove(index);
debug!("Found window {window_id:?}");
if let Ok(window) = WindowOS::new(&element_ref).inspect_err(|err| warn!("{err}")) {
found_windows.push(Window::new(Box::new(window)));
}
}
}
found_windows
}
pub fn check_ax_privilege() -> bool {
unsafe {
let keys = [kAXTrustedCheckOptionPrompt
.cast::<CFString>()
.as_ref()
.unwrap()];
let values = [kCFBooleanTrue.unwrap()];
let opts = CFDictionary::from_slices(&keys, &values);
AXIsProcessTrustedWithOptions((&raw const *opts).cast())
}
}
pub fn check_separate_spaces() -> bool {
unsafe {
let cid = SLSMainConnectionID();
SLSGetSpaceManagementMode(cid) == 1
}
}
struct ConfigHandler(EventSender);
impl notify::EventHandler for ConfigHandler {
fn handle_event(&mut self, event: notify::Result<notify::Event>) {
if let Ok(event) = event {
_ = self.0.send(Event::ConfigRefresh(event)).inspect_err(|err| {
warn!("error sending config refresh: {err}");
});
}
}
}