use std::sync::Once;
use std::time::Duration;
use mac_notification_sys::{MainButton, Notification, NotificationResponse};
use tracing::{info, warn};
use crate::PromptResponse;
static INIT: Once = Once::new();
pub fn init() {
INIT.call_once(|| {
let bundle = mac_notification_sys::get_bundle_identifier_or_default("Terminal");
if let Err(e) = mac_notification_sys::set_application(&bundle) {
warn!(error = %e, "Failed to set notification bundle identifier");
}
});
}
pub fn notify(title: &str, body: &str) {
init();
info!(title, body, "Sending desktop notification (macOS)");
let result = Notification::new()
.title(title)
.message(body)
.asynchronous(true)
.send();
if let Err(e) = result {
warn!(error = %e, "Failed to send desktop notification");
}
}
pub fn prompt(title: &str, body: &str, timeout: Duration) -> PromptResponse {
init();
info!(
title,
body,
timeout_secs = timeout.as_secs(),
"Sending interactive notification (macOS)"
);
let timeout_dur = timeout;
std::thread::spawn(move || {
std::thread::sleep(timeout_dur);
warn!("Interactive notification watchdog triggered, force-exiting");
std::process::exit(1);
});
let result = Notification::new()
.title(title)
.message(body)
.main_button(MainButton::SingleAction("Approve"))
.close_button("Deny")
.send();
match result {
Ok(response) => map_response(response),
Err(e) => {
warn!(error = %e, "Notification send failed");
PromptResponse::TimedOut
}
}
}
fn map_response(response: NotificationResponse) -> PromptResponse {
match response {
NotificationResponse::ActionButton(_) => PromptResponse::Approved,
NotificationResponse::Click => PromptResponse::Approved,
NotificationResponse::CloseButton(_) => PromptResponse::Denied,
NotificationResponse::None => PromptResponse::TimedOut,
NotificationResponse::Reply(_) => PromptResponse::TimedOut,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_map_response_action_button() {
assert_eq!(
map_response(NotificationResponse::ActionButton("Approve".into())),
PromptResponse::Approved,
);
}
#[test]
fn test_map_response_click() {
assert_eq!(
map_response(NotificationResponse::Click),
PromptResponse::Approved,
);
}
#[test]
fn test_map_response_close_button() {
assert_eq!(
map_response(NotificationResponse::CloseButton("Deny".into())),
PromptResponse::Denied,
);
}
#[test]
fn test_map_response_none() {
assert_eq!(
map_response(NotificationResponse::None),
PromptResponse::TimedOut,
);
}
#[test]
fn test_map_response_reply() {
assert_eq!(
map_response(NotificationResponse::Reply("something".into())),
PromptResponse::TimedOut,
);
}
}