mac_notification_sys/
lib.rs1#![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#![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
55pub 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 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
119pub 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
125pub 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
131fn 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 unsafe { sys::setupDelegate() };
144}
145
146pub 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}