use focus_tracker_core::{FocusTrackerError, FocusTrackerResult, FocusedWindow, IconConfig};
use objc2::AnyThread;
use objc2::rc::autoreleasepool;
use objc2::runtime::AnyObject;
use objc2_app_kit::{
NSBitmapImageFileType, NSBitmapImageRep, NSCalibratedRGBColorSpace, NSGraphicsContext, NSImage,
NSRunningApplication, NSWorkspace,
};
use objc2_foundation::{NSDictionary, NSPoint, NSRect, NSSize, NSString, ns_string};
use std::ffi::c_void;
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGWindowListCopyWindowInfo(option: u32, relative_to_window: u32) -> *const c_void;
}
const K_CG_WINDOW_LIST_OPTION_ON_SCREEN_ONLY: u32 = 1;
const K_CG_WINDOW_LIST_EXCLUDE_DESKTOP_ELEMENTS: u32 = 1 << 4;
const K_CG_NULL_WINDOW_ID: u32 = 0;
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFRelease(cf: *const c_void);
fn CFArrayGetCount(the_array: *const c_void) -> isize;
fn CFArrayGetValueAtIndex(the_array: *const c_void, idx: isize) -> *const c_void;
fn CFDictionaryGetValue(the_dict: *const c_void, key: *const c_void) -> *const c_void;
fn CFNumberGetValue(number: *const c_void, the_type: i32, value_ptr: *mut c_void) -> bool;
fn CFStringGetLength(the_string: *const c_void) -> isize;
fn CFStringGetCString(
the_string: *const c_void,
buffer: *mut i8,
buffer_size: isize,
encoding: u32,
) -> bool;
}
const K_CF_NUMBER_SINT32_TYPE: i32 = 3;
const K_CF_STRING_ENCODING_UTF8: u32 = 0x0800_0100;
#[link(name = "ApplicationServices", kind = "framework")]
unsafe extern "C" {
fn AXUIElementCreateApplication(pid: i32) -> *mut c_void;
fn AXUIElementCopyAttributeValue(
element: *const c_void,
attribute: *const c_void,
value: *mut *mut c_void,
) -> i32;
}
const K_AX_ERROR_SUCCESS: i32 = 0;
const K_AX_ERROR_API_DISABLED: i32 = -25211;
pub fn get_frontmost_window_basic_info() -> FocusTrackerResult<FocusedWindow> {
autoreleasepool(|_pool| {
let pid = get_frontmost_window_pid()?;
let app = NSRunningApplication::runningApplicationWithProcessIdentifier(pid);
let process_name = app
.and_then(|a| a.localizedName().map(|n| n.to_string()))
.ok_or_else(|| {
FocusTrackerError::platform(format!("failed to get process name for pid {pid}"))
})?;
let window_title = get_window_title_via_accessibility(pid)?;
Ok(FocusedWindow {
process_id: u32::try_from(pid).unwrap_or(0),
window_title,
process_name,
icon: None,
})
})
}
pub fn fetch_icon_for_pid(
pid: i32,
icon_config: &IconConfig,
) -> FocusTrackerResult<Option<image::RgbaImage>> {
autoreleasepool(|_pool| {
let app = NSRunningApplication::runningApplicationWithProcessIdentifier(pid);
match app {
Some(app) => get_app_icon(&app, icon_config),
None => Ok(None),
}
})
}
fn get_frontmost_window_pid() -> FocusTrackerResult<i32> {
unsafe {
let options =
K_CG_WINDOW_LIST_OPTION_ON_SCREEN_ONLY | K_CG_WINDOW_LIST_EXCLUDE_DESKTOP_ELEMENTS;
let window_list = CGWindowListCopyWindowInfo(options, K_CG_NULL_WINDOW_ID);
if window_list.is_null() {
return Err(FocusTrackerError::platform("failed to get window list"));
}
let count = CFArrayGetCount(window_list);
if count <= 0 {
CFRelease(window_list);
return Err(FocusTrackerError::platform("no windows found"));
}
let layer_key: *const c_void =
std::ptr::from_ref::<NSString>(ns_string!("kCGWindowLayer")).cast();
let pid_key: *const c_void =
std::ptr::from_ref::<NSString>(ns_string!("kCGWindowOwnerPID")).cast();
for i in 0..count {
let dict = CFArrayGetValueAtIndex(window_list, i);
if dict.is_null() {
continue;
}
let layer_val = CFDictionaryGetValue(dict, layer_key);
if !layer_val.is_null() {
let mut layer: i32 = 0;
let ok = CFNumberGetValue(
layer_val,
K_CF_NUMBER_SINT32_TYPE,
std::ptr::from_mut(&mut layer).cast(),
);
if ok && layer != 0 {
continue;
}
}
let pid_val = CFDictionaryGetValue(dict, pid_key);
if pid_val.is_null() {
continue;
}
let mut pid: i32 = 0;
if !CFNumberGetValue(
pid_val,
K_CF_NUMBER_SINT32_TYPE,
std::ptr::from_mut(&mut pid).cast(),
) {
continue;
}
CFRelease(window_list);
return Ok(pid);
}
CFRelease(window_list);
Err(FocusTrackerError::platform(
"no normal application window found",
))
}
}
fn get_window_title_via_accessibility(pid: i32) -> FocusTrackerResult<Option<String>> {
let Some(focused_window) = copy_focused_window(pid)? else {
return Ok(None);
};
let title = unsafe { copy_string_attribute(focused_window, ns_string!("AXTitle")) };
unsafe { CFRelease(focused_window) };
Ok(title)
}
pub fn focused_document_url(pid: u32) -> FocusTrackerResult<Option<String>> {
let pid = i32::try_from(pid).map_err(|_| {
FocusTrackerError::platform(format!("pid {pid} does not fit into a macOS pid_t"))
})?;
let Some(focused_window) = copy_focused_window(pid)? else {
return Ok(None);
};
let url = unsafe { copy_string_attribute(focused_window, ns_string!("AXDocument")) };
unsafe { CFRelease(focused_window) };
Ok(url)
}
fn copy_focused_window(pid: i32) -> FocusTrackerResult<Option<*mut c_void>> {
let app_element = unsafe { AXUIElementCreateApplication(pid) };
if app_element.is_null() {
return Ok(None);
}
let focused_window_attr = ns_string!("AXFocusedWindow");
let mut focused_window: *mut c_void = std::ptr::null_mut();
let result = unsafe {
AXUIElementCopyAttributeValue(
app_element,
std::ptr::from_ref::<NSString>(focused_window_attr).cast::<c_void>(),
&raw mut focused_window,
)
};
unsafe { CFRelease(app_element) };
if result == K_AX_ERROR_API_DISABLED {
return Err(FocusTrackerError::PermissionDenied {
context: "macOS accessibility API denied (AXUIElement)".into(),
});
}
if result != K_AX_ERROR_SUCCESS || focused_window.is_null() {
return Ok(None);
}
Ok(Some(focused_window))
}
unsafe fn copy_string_attribute(element: *mut c_void, attribute: &NSString) -> Option<String> {
let mut value: *mut c_void = std::ptr::null_mut();
let result = unsafe {
AXUIElementCopyAttributeValue(
element,
std::ptr::from_ref::<NSString>(attribute).cast::<c_void>(),
&raw mut value,
)
};
if result != K_AX_ERROR_SUCCESS || value.is_null() {
return None;
}
let s = unsafe { cfstring_to_string(value) };
unsafe { CFRelease(value) };
s
}
unsafe fn cfstring_to_string(cf_string: *const c_void) -> Option<String> {
if cf_string.is_null() {
return None;
}
let length = unsafe { CFStringGetLength(cf_string) };
if length <= 0 {
return Some(String::new());
}
let buffer_size = (length * 4 + 1).cast_unsigned();
let mut buffer: Vec<i8> = vec![0; buffer_size];
let success = unsafe {
CFStringGetCString(
cf_string,
buffer.as_mut_ptr(),
buffer_size.cast_signed(),
K_CF_STRING_ENCODING_UTF8,
)
};
if success {
let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
c_str.to_str().ok().map(std::string::ToString::to_string)
} else {
None
}
}
fn get_app_icon(
app: &NSRunningApplication,
icon_config: &IconConfig,
) -> FocusTrackerResult<Option<image::RgbaImage>> {
let Some(bundle_url) = app.bundleURL() else {
return Ok(None);
};
let Some(path) = bundle_url.path() else {
return Ok(None);
};
let workspace = NSWorkspace::sharedWorkspace();
let ns_image = workspace.iconForFile(&path);
nsimage_to_rgba(&ns_image, icon_config)
}
fn nsimage_to_rgba(
ns_image: &NSImage,
icon_config: &IconConfig,
) -> FocusTrackerResult<Option<image::RgbaImage>> {
let icon_size = icon_config.get_size_or_default();
let png_bytes = render_nsimage_to_png(ns_image, icon_size)?;
let dynamic_image = image::load_from_memory(&png_bytes).map_err(|e| {
FocusTrackerError::platform_with_source("failed to decode icon image data", e)
})?;
Ok(Some(dynamic_image.to_rgba8()))
}
fn render_nsimage_to_png(ns_image: &NSImage, size: u32) -> FocusTrackerResult<Vec<u8>> {
let size_i = size as isize;
let size_f = size as f64;
let bitmap_rep = unsafe {
NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
std::ptr::null_mut(), size_i, size_i, 8, 4, true, false, NSCalibratedRGBColorSpace,
0, 0, )
}
.ok_or_else(|| FocusTrackerError::platform("failed to create target NSBitmapImageRep"))?;
let context =
NSGraphicsContext::graphicsContextWithBitmapImageRep(&bitmap_rep).ok_or_else(|| {
FocusTrackerError::platform("failed to create NSGraphicsContext for icon rendering")
})?;
NSGraphicsContext::saveGraphicsState_class();
NSGraphicsContext::setCurrentContext(Some(&context));
let target_rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(size_f, size_f));
ns_image.drawInRect(target_rect);
NSGraphicsContext::restoreGraphicsState_class();
let empty_props = NSDictionary::<NSString, AnyObject>::new();
let png_data = unsafe {
bitmap_rep.representationUsingType_properties(NSBitmapImageFileType::PNG, &empty_props)
}
.ok_or_else(|| FocusTrackerError::platform("failed to encode rendered icon as PNG"))?;
Ok(png_data.to_vec())
}