use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::NonNull;
use std::sync::atomic::{AtomicI32, Ordering};
use block2::RcBlock;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, ProtocolObject};
use objc2_app_kit::{
NSRunningApplication, NSWorkspace, NSWorkspaceApplicationKey,
NSWorkspaceDidActivateApplicationNotification,
};
use objc2_foundation::{NSNotification, NSNotificationCenter, NSObjectProtocol};
use tracing::error;
use crate::ForegroundApp;
#[must_use]
pub struct ForegroundApplicationObserver {
center: Retained<NSNotificationCenter>,
token: Retained<ProtocolObject<dyn NSObjectProtocol>>,
}
const SAFARI_BUNDLE_ID: &str = "com.apple.Safari";
const NO_SAFARI_PROCESS: i32 = 0;
static FRONTMOST_SAFARI_PID: AtomicI32 = AtomicI32::new(NO_SAFARI_PROCESS);
fn safari_process_id(bundle_id: &str, pid: i32) -> Option<i32> {
(bundle_id == SAFARI_BUNDLE_ID && pid > 0).then_some(pid)
}
pub(super) fn observe_frontmost_application(
app: Option<&NSRunningApplication>,
pool: objc2::rc::AutoreleasePool<'_>,
) -> Option<ForegroundApp> {
let foreground = app.and_then(|app| foreground_app_from_running_application(app, pool));
let safari_pid = app
.zip(foreground.as_ref())
.and_then(|(app, foreground)| safari_process_id(&foreground.id, app.processIdentifier()))
.unwrap_or(NO_SAFARI_PROCESS);
FRONTMOST_SAFARI_PID.store(safari_pid, Ordering::Release);
foreground
}
pub(crate) fn frontmost_safari_pid() -> Option<i32> {
let pid = FRONTMOST_SAFARI_PID.load(Ordering::Acquire);
(pid > 0).then_some(pid)
}
impl Drop for ForegroundApplicationObserver {
fn drop(&mut self) {
objc2::rc::autoreleasepool(|_| {
unsafe { self.center.removeObserver(self.token.as_ref()) };
});
}
}
pub(crate) fn watch_frontmost_application_activations(
on_activation: impl Fn(Option<ForegroundApp>) + Send + Sync + 'static,
) -> ForegroundApplicationObserver {
objc2::rc::autoreleasepool(|_| {
let workspace = NSWorkspace::sharedWorkspace();
let center = workspace.notificationCenter();
let block: RcBlock<dyn Fn(NonNull<NSNotification>)> =
RcBlock::new(move |notification: NonNull<NSNotification>| {
let result = catch_unwind(AssertUnwindSafe(|| {
let activation = objc2::rc::autoreleasepool(|pool| {
let notification = unsafe { notification.as_ref() };
let app = notification.userInfo().and_then(|info| {
info.objectForKey(unsafe { NSWorkspaceApplicationKey } as &AnyObject)?
.downcast::<NSRunningApplication>()
.ok()
});
observe_frontmost_application(app.as_deref(), pool)
});
on_activation(activation);
}));
if result.is_err() {
error!("foreground-application activation callback panicked");
}
});
let token = unsafe {
center.addObserverForName_object_queue_usingBlock(
Some(NSWorkspaceDidActivateApplicationNotification),
Some(&workspace),
None,
&block,
)
};
ForegroundApplicationObserver { center, token }
})
}
pub(super) fn foreground_app_from_running_application(
app: &NSRunningApplication,
pool: objc2::rc::AutoreleasePool<'_>,
) -> Option<ForegroundApp> {
let bundle_id = app.bundleIdentifier()?;
let name = app.localizedName();
let (id, name) = unsafe {
(
bundle_id.to_str(pool).to_owned(),
name.as_ref().map(|name| name.to_str(pool).to_owned()),
)
};
let display_name = name.unwrap_or_else(|| id.clone());
Some(ForegroundApp { id, display_name })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn safari_snapshot_accepts_only_safari_with_a_positive_pid() {
assert_eq!(safari_process_id(SAFARI_BUNDLE_ID, 417), Some(417));
assert_eq!(safari_process_id("com.apple.finder", 417), None);
assert_eq!(safari_process_id(SAFARI_BUNDLE_ID, 0), None);
}
}