Skip to main content

mac_notification_sys/
lib.rs

1//! A very thin wrapper around NSNotifications
2#![deny(deref_nullptr)]
3#![deny(invalid_value)]
4#![deny(invalid_from_utf8)]
5#![deny(never_type_fallback_flowing_into_unsafe)]
6#![deny(ptr_to_integer_transmute_in_consts)]
7#![deny(static_mut_refs)]
8#![warn(
9    missing_docs,
10    trivial_casts,
11    trivial_numeric_casts,
12    unused_import_braces,
13    unused_qualifications
14)]
15#![cfg(target_os = "macos")]
16#![allow(improper_ctypes)]
17// The extern "C" callbacks called from ObjC unavoidably take raw pointer arguments.
18// They cannot be marked `unsafe` (Rust forbids unsafe extern "C" fn that are exported),
19// yet they must dereference those pointers — suppress the lint crate-wide for this pattern.
20#![allow(clippy::not_unsafe_ptr_arg_deref)]
21
22use error::{ApplicationError, NotificationError, NotificationResult};
23pub use notification::{MainButton, Notification, NotificationResponse, Sound};
24use objc2_foundation::NSString;
25use std::{
26    ops::Deref,
27    sync::{Arc, Condvar, Mutex, Once, atomic::AtomicBool},
28};
29
30mod bridge;
31pub mod error;
32mod notification;
33mod pending_guard;
34
35mod sys {
36    use objc2_foundation::{NSDictionary, NSString};
37    #[link(name = "notify")]
38    unsafe extern "C" {
39        pub fn sendNotification(
40            title: *const NSString,
41            subtitle: *const NSString,
42            message: *const NSString,
43            options: *const NSDictionary<NSString, NSString>,
44            notification_id: *const u8,
45            should_wait: bool,
46        );
47        pub fn setApplication(newbundleIdentifier: *const NSString) -> bool;
48        pub fn getBundleIdentifier(appName: *const NSString) -> *const NSString;
49        pub fn setupDelegate();
50    }
51}
52
53static INIT_APPLICATION_SET: Once = Once::new();
54
55/// Delivers a new notification
56///
57/// Returns a `NotificationError` if a notification could not be delivered
58///
59/// # Example:
60///
61/// ```no_run
62/// # use mac_notification_sys::*;
63/// // deliver a silent notification
64/// let _ = send_notification("Title", None, "This is the body", None).unwrap();
65/// ```
66pub fn send_notification(
67    title: &str,
68    subtitle: Option<&str>,
69    message: &str,
70    options: Option<&Notification>,
71) -> NotificationResult<NotificationResponse> {
72    if let Some(options) = &options {
73        if let Some(delivery_date) = options.delivery_date {
74            ensure!(
75                delivery_date >= time::OffsetDateTime::now_utc().unix_timestamp() as f64,
76                NotificationError::ScheduleInThePast
77            );
78        }
79    };
80
81    ensure_application_set()?;
82    ensure_delegate_initiated();
83
84    let should_wait = options.map(|o| o.needs_response()).unwrap_or(false);
85    let options_dict = options.unwrap_or(&Notification::new()).to_dictionary();
86
87    let id: [u8; 16] = uuid::Uuid::new_v4().into_bytes();
88
89    let entry = Arc::new(pending_guard::PendingEntry {
90        result: Mutex::new(NotificationResponse::None),
91        done: AtomicBool::new(!should_wait),
92        condvar: Condvar::new(),
93        delivered: Mutex::new(false),
94        delivered_cv: Condvar::new(),
95    });
96    pending_guard::pending()
97        .lock()
98        .unwrap()
99        .insert(id, Arc::clone(&entry));
100    // PendingGuard performs the sole remove — both on the normal path and on panic.
101    // Reading the result from `entry` directly avoids a second remove call.
102    let _guard = pending_guard::PendingGuard { id };
103
104    unsafe {
105        sys::sendNotification(
106            NSString::from_str(title).deref(),
107            NSString::from_str(subtitle.unwrap_or("")).deref(),
108            NSString::from_str(message).deref(),
109            options_dict.deref(),
110            id.as_ptr(),
111            should_wait,
112        );
113    }
114
115    let result = entry.result.lock().unwrap().clone();
116    Ok(result)
117}
118
119/// Search for a possible BundleIdentifier of a given appname.
120/// Defaults to "com.apple.Finder" if no BundleIdentifier is found.
121pub fn get_bundle_identifier_or_default(app_name: &str) -> String {
122    get_bundle_identifier(app_name).unwrap_or_else(|| "com.apple.Finder".to_string())
123}
124
125/// Search for a BundleIdentifier of an given appname.
126pub fn get_bundle_identifier(app_name: &str) -> Option<String> {
127    unsafe { sys::getBundleIdentifier(NSString::from_str(app_name).deref()).as_ref() }
128        .map(NSString::to_string)
129}
130
131/// Sets the application if not already set
132fn ensure_application_set() -> NotificationResult<()> {
133    if INIT_APPLICATION_SET.is_completed() {
134        return Ok(());
135    };
136    let bundle = get_bundle_identifier_or_default("use_default");
137    set_application(&bundle)
138}
139
140fn ensure_delegate_initiated() {
141    // `sharedDelegate` in ObjC is already guarded by `dispatch_once`; calling it here
142    // is idempotent and thread-safe without an extra Rust-side Once.
143    unsafe { sys::setupDelegate() };
144}
145
146/// Set the application which delivers or schedules a notification
147pub fn set_application(bundle_ident: &str) -> NotificationResult<()> {
148    let mut result = Err(ApplicationError::AlreadySet(bundle_ident.into()).into());
149    INIT_APPLICATION_SET.call_once(|| {
150        let was_set = unsafe { sys::setApplication(NSString::from_str(bundle_ident).deref()) };
151        result = if was_set {
152            Ok(())
153        } else {
154            Err(ApplicationError::CouldNotSet(bundle_ident.into()).into())
155        };
156    });
157    result
158}