use std::cell::RefCell;
use objc2::MainThreadMarker;
use objc2::rc::Retained;
use objc2::runtime::{AnyClass, AnyObject, Imp, Sel};
use objc2::{msg_send, sel};
use objc2_app_kit::{NSApplication, NSApplicationTerminateReply};
use objc2_foundation::{NSAppleEventDescriptor, NSAppleEventManager};
use super::{OwedReplies, TerminateRequest};
type Post = Box<dyn Fn(TerminateRequest)>;
thread_local! {
static POST: RefCell<Option<Post>> = const { RefCell::new(None) };
}
static OWED_REPLIES: OwedReplies = OwedReplies::new();
const fn four_cc(code: &[u8; 4]) -> u32 {
u32::from_be_bytes(*code)
}
const QUIT_REASON: u32 = four_cc(b"why?");
const SESSION_END_REASONS: [u32; 6] = [
four_cc(b"logo"),
four_cc(b"rlgo"),
four_cc(b"rrst"),
four_cc(b"rsdn"),
four_cc(b"rest"),
four_cc(b"shut"),
];
pub fn install_terminate_hook(post: impl Fn(TerminateRequest) + 'static) -> bool {
let Some(mtm) = MainThreadMarker::new() else {
return false;
};
let app = NSApplication::sharedApplication(mtm);
let Some(delegate) = app.delegate() else {
return false;
};
POST.with(|slot| *slot.borrow_mut() = Some(Box::new(post)));
let object: &AnyObject = delegate.as_ref();
let class: &AnyClass = object.class();
let selector = sel!(applicationShouldTerminate:);
if class.instance_method(selector).is_some() {
return class_has_ours(class, selector);
}
let method: extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> NSApplicationTerminateReply =
application_should_terminate;
let added = unsafe {
let imp: Imp = std::mem::transmute::<
extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> NSApplicationTerminateReply,
Imp,
>(method);
objc2::ffi::class_addMethod(
class as *const AnyClass as *mut AnyClass,
selector,
imp,
c"Q@:@".as_ptr(),
)
};
if !added.as_bool() {
return false;
}
unsafe {
let _: () = msg_send![&*app, setDelegate: object];
}
true
}
fn class_has_ours(class: &AnyClass, selector: Sel) -> bool {
let ours: extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> NSApplicationTerminateReply =
application_should_terminate;
class
.instance_method(selector)
.is_some_and(|method| method.implementation() as usize == ours as usize)
}
extern "C-unwind" fn application_should_terminate(
_this: &AnyObject,
_cmd: Sel,
_sender: &AnyObject,
) -> NSApplicationTerminateReply {
let session_end = quitting_for_session_end();
let posted = POST.with(|slot| match slot.try_borrow() {
Ok(slot) => match slot.as_ref() {
Some(post) => {
OWED_REPLIES.owe();
post(TerminateRequest::new(session_end, reply_on_main_queue));
true
}
None => false,
},
Err(_) => false,
});
if posted {
NSApplicationTerminateReply::TerminateLater
} else {
NSApplicationTerminateReply::TerminateNow
}
}
fn reply_on_main_queue(terminate: bool) {
dispatch2::DispatchQueue::main().exec_async(move || {
if !OWED_REPLIES.take() {
return;
}
if let Some(mtm) = MainThreadMarker::new() {
NSApplication::sharedApplication(mtm).replyToApplicationShouldTerminate(terminate);
}
});
}
fn quitting_for_session_end() -> bool {
let manager = NSAppleEventManager::sharedAppleEventManager();
let Some(event) = manager.currentAppleEvent() else {
return false;
};
let reason: Option<Retained<NSAppleEventDescriptor>> =
unsafe { msg_send![&*event, attributeDescriptorForKeyword: QUIT_REASON] };
let Some(reason) = reason else {
return false;
};
let code: u32 = unsafe { msg_send![&*reason, enumCodeValue] };
SESSION_END_REASONS.contains(&code)
}