use std::sync::OnceLock;
use std::sync::mpsc::{self, Receiver, Sender};
use block2::RcBlock;
use objc2::rc::Retained;
use objc2::runtime::{NSObjectProtocol, ProtocolObject};
use objc2_app_kit::{NSRunningApplication, NSWorkspace};
use objc2_foundation::NSString;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FocusEvent {
Focused {
address: String,
class: String,
},
#[allow(dead_code)]
Closed {
address: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitialFocus {
pub address: String,
pub class: String,
}
#[derive(Debug, thiserror::Error)]
pub enum FocusError {
#[error("could not start NSWorkspace focus tracking: {0}")]
Observer(String),
}
struct SendableObserver(#[allow(dead_code)] Retained<ProtocolObject<dyn NSObjectProtocol>>);
unsafe impl Send for SendableObserver {}
unsafe impl Sync for SendableObserver {}
static OBSERVER: OnceLock<SendableObserver> = OnceLock::new();
fn app_identity(app: &NSRunningApplication) -> String {
app.bundleIdentifier()
.map(|s| s.to_string())
.or_else(|| app.localizedName().map(|s| s.to_string()))
.unwrap_or_else(|| "unknown".to_string())
}
pub fn start() -> Result<(Option<InitialFocus>, Receiver<FocusEvent>), FocusError> {
super::app::run_on_main_sync(
|| -> Result<(Option<InitialFocus>, Receiver<FocusEvent>), FocusError> {
let workspace = NSWorkspace::sharedWorkspace();
let initial = workspace.frontmostApplication().map(|app| {
let id = app_identity(&app);
InitialFocus {
address: id.clone(),
class: id,
}
});
let (tx, rx) = mpsc::channel::<FocusEvent>();
let tx_block: Sender<FocusEvent> = tx;
let block = RcBlock::new(
move |_notif: core::ptr::NonNull<objc2_foundation::NSNotification>| {
let ws = NSWorkspace::sharedWorkspace();
if let Some(app) = ws.frontmostApplication() {
let id = app_identity(&app);
let _ = tx_block.send(FocusEvent::Focused {
address: id.clone(),
class: id,
});
}
},
);
let center = workspace.notificationCenter();
let name = NSString::from_str("NSWorkspaceDidActivateApplicationNotification");
let observer = unsafe {
center.addObserverForName_object_queue_usingBlock(Some(&name), None, None, &block)
};
let _ = OBSERVER.set(SendableObserver(observer));
Ok((initial, rx))
},
)
}