use super::{ax, ffi::*};
use std::{collections::HashSet, process::Command, thread, time::Duration};
const LAUNCH_WAIT: Duration = Duration::from_secs(10);
const LAUNCH_STEP: Duration = Duration::from_millis(150);
#[derive(Clone)]
pub struct Window {
pub id: u32,
pub pid: i32,
pub app: String,
pub title: String,
pub rect: ax::Bounds,
}
pub fn list() -> Vec<Window> {
let mut out = Vec::new();
unsafe {
let Some(info) = Owned::new(CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID)) else {
return out;
};
let me = std::process::id() as i32;
for entry in items(info.0) {
let number = |key: CFStringRef| number_of(CFDictionaryGetValue(entry, key));
if number(kCGWindowLayer) != Some(0.0) {
continue;
}
let (Some(id), Some(pid)) = (number(kCGWindowNumber), number(kCGWindowOwnerPID)) else { continue };
if pid as i32 == me {
continue;
}
let mut bounds = CGRect::default();
let dict = CFDictionaryGetValue(entry, kCGWindowBounds);
if dict.is_null() || !CGRectMakeWithDictionaryRepresentation(dict, &mut bounds) || bounds.size.width < 2.0 || bounds.size.height < 2.0 {
continue;
}
out.push(Window {
id: id as u32,
pid: pid as i32,
app: string_of(CFDictionaryGetValue(entry, kCGWindowOwnerName)).unwrap_or_default(),
title: string_of(CFDictionaryGetValue(entry, kCGWindowName)).unwrap_or_default(),
rect: (bounds.origin.x as i32, bounds.origin.y as i32, bounds.size.width as i32, bounds.size.height as i32),
});
}
}
let untitled: HashSet<i32> = out.iter().filter(|w| w.title.is_empty()).map(|w| w.pid).collect();
for pid in untitled {
for (window, number) in ax::windows_of(pid) {
let title = ax::text(&window, "AXTitle").unwrap_or_default();
if let Some(listed) = out.iter_mut().find(|w| w.id == number) {
listed.title = title;
}
}
}
out
}
pub fn find(id: u32) -> Option<Window> {
list().into_iter().find(|w| w.id == id)
}
pub fn element(window: &Window) -> Option<Owned> {
ax::windows_of(window.pid).into_iter().find(|(_, number)| *number == window.id).map(|(element, _)| element)
}
pub fn launch(app: &str, args: &[String], front: bool) -> Result<Option<Window>, String> {
let before: HashSet<u32> = list().iter().map(|w| w.id).collect();
let mut open = Command::new("/usr/bin/open");
if !front {
open.arg("-g");
}
if app.contains('/') {
open.arg(app);
} else {
open.args(["-a", app]);
}
if !args.is_empty() {
open.arg("--args").args(args);
}
let done = open.output().map_err(|e| e.to_string())?;
if !done.status.success() {
return Err(format!("no app named '{app}': {}", String::from_utf8_lossy(&done.stderr).trim()));
}
let started = std::time::Instant::now();
while started.elapsed() < LAUNCH_WAIT {
if let Some(new) = list().into_iter().find(|w| !before.contains(&w.id)) {
return Ok(Some(new));
}
thread::sleep(LAUNCH_STEP);
}
Ok(None)
}
pub fn main_height() -> f64 {
unsafe { CGDisplayBounds(CGMainDisplayID()).size.height }
}
pub fn front_pid() -> Option<i32> {
use objc2::runtime::AnyObject;
use objc2::{class, msg_send};
unsafe {
let workspace: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace];
let app: *mut AnyObject = msg_send![workspace, frontmostApplication];
if app.is_null() {
return None;
}
let pid: i32 = msg_send![app, processIdentifier];
Some(pid)
}
}
pub fn activate(pid: i32) -> bool {
use objc2::runtime::AnyObject;
use objc2::{class, msg_send};
let asked = unsafe {
let app: *mut AnyObject = msg_send![class!(NSRunningApplication), runningApplicationWithProcessIdentifier: pid];
!app.is_null() && {
let done: bool = msg_send![app, activateWithOptions: 2usize];
done
}
};
if !asked {
return false;
}
for _ in 0..20 {
if front_pid() == Some(pid) {
return true;
}
thread::sleep(Duration::from_millis(25));
}
false
}
pub fn work_area() -> ax::Bounds {
use objc2::runtime::AnyObject;
use objc2::{class, msg_send};
unsafe {
let screen: *mut AnyObject = msg_send![class!(NSScreen), mainScreen];
if screen.is_null() {
let all = CGDisplayBounds(CGMainDisplayID());
return (0, 0, all.size.width as i32, all.size.height as i32);
}
let area: CGRect = msg_send![screen, visibleFrame];
let top = main_height() - area.origin.y - area.size.height;
(area.origin.x as i32, top as i32, area.size.width as i32, area.size.height as i32)
}
}
pub fn screens() -> Vec<(ax::Bounds, bool)> {
let mut ids = [0u32; 16];
let mut count = 0u32;
unsafe { CGGetActiveDisplayList(ids.len() as u32, ids.as_mut_ptr(), &mut count) };
let main = unsafe { CGMainDisplayID() };
ids[..count as usize]
.iter()
.map(|id| {
let r = unsafe { CGDisplayBounds(*id) };
((r.origin.x as i32, r.origin.y as i32, r.size.width as i32, r.size.height as i32), *id == main)
})
.collect()
}
const PLAIN_TEXT: &std::ffi::CStr = c"public.utf8-plain-text";
fn pasteboard() -> *mut objc2::runtime::AnyObject {
use objc2::{class, msg_send};
unsafe { msg_send![class!(NSPasteboard), generalPasteboard] }
}
fn ns_string(text: &str) -> *mut objc2::runtime::AnyObject {
use objc2::runtime::AnyObject;
use objc2::{class, msg_send};
let owned = std::ffi::CString::new(text.replace('\0', "")).unwrap_or_default();
unsafe {
let made: *mut AnyObject = msg_send![class!(NSString), stringWithUTF8String: owned.as_ptr()];
made
}
}
pub fn clipboard() -> Option<String> {
use objc2::msg_send;
use objc2::runtime::AnyObject;
objc2::rc::autoreleasepool(|_| unsafe {
let kind = ns_string(PLAIN_TEXT.to_str().unwrap_or_default());
let text: *mut AnyObject = msg_send![pasteboard(), stringForType: kind];
if text.is_null() {
return None;
}
let utf8: *const std::ffi::c_char = msg_send![text, UTF8String];
(!utf8.is_null()).then(|| std::ffi::CStr::from_ptr(utf8).to_string_lossy().into_owned())
})
}
pub fn set_clipboard(text: Option<&str>) -> Result<(), String> {
use objc2::msg_send;
objc2::rc::autoreleasepool(|_| unsafe {
let board = pasteboard();
let _: isize = msg_send![board, clearContents];
let Some(text) = text else { return Ok(()) };
let kind = ns_string(PLAIN_TEXT.to_str().unwrap_or_default());
let done: bool = msg_send![board, setString: ns_string(text), forType: kind];
if done { Ok(()) } else { Err("macOS refused the clipboard text".into()) }
})
}