#[cfg(target_os = "macos")]
use std::sync::mpsc::Sender;
use std::sync::{Mutex, OnceLock, mpsc};
pub fn escape_for_applescript(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NotificationUrgency {
Low,
#[default]
Normal,
Critical,
}
pub struct NotificationRequest<'a> {
pub title: &'a str,
pub message: &'a str,
pub timeout_ms: u32,
pub urgency: NotificationUrgency,
pub identity: Option<&'a str>,
pub click_token: Option<u64>,
}
pub fn deliver_desktop_notification(
title: &str,
message: &str,
timeout_ms: u32,
urgency: NotificationUrgency,
) {
deliver_desktop_notification_request(&NotificationRequest {
title,
message,
timeout_ms,
urgency,
identity: None,
click_token: None,
});
}
pub fn deliver_desktop_notification_request(req: &NotificationRequest<'_>) {
#[cfg(all(unix, not(target_os = "macos")))]
{
use notify_rust::{Notification, Timeout, Urgency as RustUrgency};
let notification_title = if !req.title.is_empty() {
req.title
} else {
"Terminal Notification"
};
let timeout = match req.urgency {
NotificationUrgency::Low => Timeout::Milliseconds(1500),
NotificationUrgency::Normal => Timeout::Milliseconds(req.timeout_ms),
NotificationUrgency::Critical => Timeout::Never,
};
let rust_urgency = match req.urgency {
NotificationUrgency::Low => RustUrgency::Low,
NotificationUrgency::Normal => RustUrgency::Normal,
NotificationUrgency::Critical => RustUrgency::Critical,
};
let mut notification = Notification::new();
notification
.summary(notification_title)
.body(req.message)
.timeout(timeout)
.urgency(rust_urgency);
if let Some(identity) = req.identity {
notification.id(fnv1a_u32(identity));
}
if req.click_token.is_some() {
notification.action("default", "Open");
}
match notification.show() {
Ok(handle) => {
if let Some(token) = req.click_token {
let sender = click_sender();
std::thread::spawn(move || {
handle.wait_for_action(move |action: &str| {
if action == "default" || action == "clicked" {
let _ = sender.send(token);
}
});
});
}
}
Err(e) => log::warn!("Failed to send desktop notification: {}", e),
}
}
#[cfg(all(not(unix), not(target_os = "macos")))]
{
use notify_rust::Notification;
let _ = req.urgency;
let _ = req.identity;
let _ = req.click_token;
let notification_title = if !req.title.is_empty() {
req.title
} else {
"Terminal Notification"
};
if let Err(e) = Notification::new()
.summary(notification_title)
.body(req.message)
.timeout(notify_rust::Timeout::Milliseconds(req.timeout_ms))
.show()
{
log::warn!("Failed to send desktop notification: {}", e);
}
}
#[cfg(target_os = "macos")]
{
crate::platform::notify_macos::deliver(req);
}
}
#[cfg(all(unix, not(target_os = "macos")))]
fn fnv1a_u32(s: &str) -> u32 {
let mut hash: u32 = 0x811c_9dc5;
for byte in s.as_bytes() {
hash ^= u32::from(*byte);
hash = hash.wrapping_mul(0x0100_0193);
}
hash
}
struct ClickChannel {
sender: mpsc::Sender<u64>,
receiver: Mutex<mpsc::Receiver<u64>>,
}
static CLICK_CHANNEL: OnceLock<ClickChannel> = OnceLock::new();
fn click_channel() -> &'static ClickChannel {
CLICK_CHANNEL.get_or_init(|| {
let (sender, receiver) = mpsc::channel();
ClickChannel {
sender,
receiver: Mutex::new(receiver),
}
})
}
#[cfg(target_os = "macos")]
pub(crate) fn click_sender() -> Sender<u64> {
click_channel().sender.clone()
}
#[cfg(all(unix, not(target_os = "macos")))]
fn click_sender() -> mpsc::Sender<u64> {
click_channel().sender.clone()
}
pub fn drain_notification_clicks() -> Vec<u64> {
let receiver = match click_channel().receiver.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let mut tokens = Vec::new();
while let Ok(token) = receiver.try_recv() {
tokens.push(token);
}
tokens
}