mac_notification_sys/
notification.rs

1//! Custom structs and enums for mac-notification-sys.
2
3use objc2_foundation::{NSDictionary, NSString};
4use std::default::Default;
5use std::ops::Deref;
6use objc2::rc::Retained;
7use crate::error::{NotificationError, NotificationResult};
8use crate::{ensure, ensure_application_set, sys};
9
10/// Possible actions accessible through the main button of the notification
11#[derive(Clone, Debug)]
12pub enum MainButton<'a> {
13    /// Display a single action with the given name
14    ///
15    /// # Example:
16    ///
17    /// ```no_run
18    /// # use mac_notification_sys::*;
19    /// let _ = MainButton::SingleAction("Action name");
20    /// ```
21    SingleAction(&'a str),
22
23    /// Display a dropdown with the given title, with a list of actions with given names
24    ///
25    /// # Example:
26    ///
27    /// ```no_run
28    /// # use mac_notification_sys::*;
29    /// let _ = MainButton::DropdownActions("Dropdown name", &["Action 1", "Action 2"]);
30    /// ```
31    DropdownActions(&'a str, &'a [&'a str]),
32
33    /// Display a text input field with the given placeholder
34    ///
35    /// # Example:
36    ///
37    /// ```no_run
38    /// # use mac_notification_sys::*;
39    /// let _ = MainButton::Response("Enter some text...");
40    /// ```
41    Response(&'a str),
42}
43
44/// Helper to determine whether you want to play the default sound or custom one
45#[derive(Clone)]
46pub enum Sound {
47    /// notification plays the sound [`NSUserNotificationDefaultSoundName`](https://developer.apple.com/documentation/foundation/nsusernotification/nsusernotificationdefaultsoundname)
48    Default,
49    /// notification plays your custom sound
50    Custom(String),
51}
52
53impl<I> From<I> for Sound
54where
55    I: ToString,
56{
57    fn from(value: I) -> Self {
58        Sound::Custom(value.to_string())
59    }
60}
61
62/// Options to further customize the notification
63#[derive(Clone, Default)]
64pub struct Notification<'a> {
65    pub(crate) title: &'a str,
66    pub(crate) subtitle: Option<&'a str>,
67    pub(crate) message: &'a str,
68    pub(crate) main_button: Option<MainButton<'a>>,
69    pub(crate) close_button: Option<&'a str>,
70    pub(crate) app_icon: Option<&'a str>,
71    pub(crate) content_image: Option<&'a str>,
72    pub(crate) delivery_date: Option<f64>,
73    pub(crate) sound: Option<Sound>,
74    pub(crate) asynchronous: Option<bool>,
75}
76
77impl<'a> Notification<'a> {
78    /// Create a Notification to further customize the notification
79    pub fn new() -> Self {
80        Default::default()
81    }
82
83    /// Set `title` field
84    pub fn title(&mut self, title: &'a str) -> &mut Self {
85        self.title = title;
86        self
87    }
88
89    /// Set `subtitle` field
90    pub fn subtitle(&mut self, subtitle: &'a str) -> &mut Self {
91        self.subtitle = Some(subtitle);
92        self
93    }
94
95    /// Set `subtitle` field
96    pub fn maybe_subtitle(&mut self, subtitle: Option<&'a str>) -> &mut Self {
97        self.subtitle = subtitle;
98        self
99    }
100
101    /// Set `message` field
102    pub fn message(&mut self, message: &'a str) -> &mut Self {
103        self.message = message;
104        self
105    }
106
107    /// Allow actions through a main button
108    ///
109    /// # Example:
110    ///
111    /// ```no_run
112    /// # use mac_notification_sys::*;
113    /// let _ = Notification::new().main_button(MainButton::SingleAction("Main button"));
114    /// ```
115    pub fn main_button(&mut self, main_button: MainButton<'a>) -> &mut Self {
116        self.main_button = Some(main_button);
117        self
118    }
119
120    /// Display a close button with the given name
121    ///
122    /// # Example:
123    ///
124    /// ```no_run
125    /// # use mac_notification_sys::*;
126    /// let _ = Notification::new().close_button("Close");
127    /// ```
128    pub fn close_button(&mut self, close_button: &'a str) -> &mut Self {
129        self.close_button = Some(close_button);
130        self
131    }
132
133    /// Display an icon on the left side of the notification
134    ///
135    /// NOTE: The icon of the app associated to the bundle will be displayed next to the notification title
136    ///
137    /// # Example:
138    ///
139    /// ```no_run
140    /// # use mac_notification_sys::*;
141    /// let _ = Notification::new().app_icon("/path/to/icon.icns");
142    /// ```
143    pub fn app_icon(&mut self, app_icon: &'a str) -> &mut Self {
144        self.app_icon = Some(app_icon);
145        self
146    }
147
148    /// Display an image on the right side of the notification
149    ///
150    /// # Example:
151    ///
152    /// ```no_run
153    /// # use mac_notification_sys::*;
154    /// let _ = Notification::new().content_image("/path/to/image.png");
155    /// ```
156    pub fn content_image(&mut self, content_image: &'a str) -> &mut Self {
157        self.content_image = Some(content_image);
158        self
159    }
160
161    /// Schedule the notification to be delivered at a later time
162    ///
163    /// # Example:
164    ///
165    /// ```no_run
166    /// # use mac_notification_sys::*;
167    /// let stamp = time::OffsetDateTime::now_utc().unix_timestamp() as f64 + 5.;
168    /// let _ = Notification::new().delivery_date(stamp);
169    /// ```
170    pub fn delivery_date(&mut self, delivery_date: f64) -> &mut Self {
171        self.delivery_date = Some(delivery_date);
172        self
173    }
174
175    /// Play the default sound `"NSUserNotificationDefaultSoundName"` system sound when the notification is delivered.
176    /// # Example:
177    ///
178    /// ```no_run
179    /// # use mac_notification_sys::*;
180    /// let _ = Notification::new().default_sound();
181    /// ```
182    pub fn default_sound(&mut self) -> &mut Self {
183        self.sound = Some(Sound::Default);
184        self
185    }
186
187    /// Play a system sound when the notification is delivered. Use [`Sound::Default`] to play the default sound.
188    /// # Example:
189    ///
190    /// ```no_run
191    /// # use mac_notification_sys::*;
192    /// let _ = Notification::new().sound("Blow");
193    /// ```
194    pub fn sound<S>(&mut self, sound: S) -> &mut Self
195    where
196        S: Into<Sound>,
197    {
198        self.sound = Some(sound.into());
199        self
200    }
201
202    /// Play a system sound when the notification is delivered. Use [`Sound::Default`] to play the default sound.
203    ///
204    /// # Example:
205    ///
206    /// ```no_run
207    /// # use mac_notification_sys::*;
208    /// let _ = Notification::new().sound("Blow");
209    /// ```
210    pub fn maybe_sound<S>(&mut self, sound: Option<S>) -> &mut Self
211    where
212        S: Into<Sound>,
213    {
214        self.sound = sound.map(Into::into);
215        self
216    }
217
218    /// Deliver the notification asynchronously (without waiting for an interaction).
219    ///
220    /// Note: Setting this to true is equivalent to a fire-and-forget.
221    ///
222    /// # Example:
223    ///
224    /// ```no_run
225    /// # use mac_notification_sys::*;
226    /// let _ = Notification::new().asynchronous(true);
227    /// ```
228    pub fn asynchronous(&mut self, asynchronous: bool) -> &mut Self {
229        self.asynchronous = Some(asynchronous);
230        self
231    }
232
233    /// Convert the Notification to an Objective C NSDictionary
234    pub(crate) fn to_dictionary(&self) -> Retained<NSDictionary<NSString, NSString>> {
235        // TODO: If possible, find a way to simplify this so I don't have to manually convert struct to NSDictionary
236        let keys = &[
237            &*NSString::from_str("mainButtonLabel"),
238            &*NSString::from_str("actions"),
239            &*NSString::from_str("closeButtonLabel"),
240            &*NSString::from_str("appIcon"),
241            &*NSString::from_str("contentImage"),
242            &*NSString::from_str("response"),
243            &*NSString::from_str("deliveryDate"),
244            &*NSString::from_str("asynchronous"),
245            &*NSString::from_str("sound"),
246        ];
247        let (main_button_label, actions, is_response): (&str, &[&str], bool) =
248            match &self.main_button {
249                Some(main_button) => match main_button {
250                    MainButton::SingleAction(main_button_label) => (main_button_label, &[], false),
251                    MainButton::DropdownActions(main_button_label, actions) => {
252                        (main_button_label, actions, false)
253                    }
254                    MainButton::Response(response) => (response, &[], true),
255                },
256                None => ("", &[], false),
257            };
258
259        let sound = match self.sound {
260            Some(Sound::Custom(ref name)) => name.as_str(),
261            Some(Sound::Default) => "NSUserNotificationDefaultSoundName",
262            None => "",
263        };
264
265        let vals = vec![
266            NSString::from_str(main_button_label),
267            // TODO: Find a way to support NSArray as a NSDictionary Value rather than JUST NSString so I don't have to convert array to string and back
268            NSString::from_str(&actions.join(",")),
269            NSString::from_str(self.close_button.unwrap_or("")),
270            NSString::from_str(self.app_icon.unwrap_or("")),
271            NSString::from_str(self.content_image.unwrap_or("")),
272            // TODO: Same as above, if NSDictionary could support multiple types, this could be a boolean
273            NSString::from_str(if is_response { "yes" } else { "" }),
274            NSString::from_str(&match self.delivery_date {
275                Some(delivery_date) => delivery_date.to_string(),
276                _ => String::new(),
277            }),
278            // TODO: Same as above, if NSDictionary could support multiple types, this could be a boolean
279            NSString::from_str(match self.asynchronous {
280                Some(true) => "yes",
281                _ => "no",
282            }),
283            NSString::from_str(sound),
284        ];
285        NSDictionary::from_retained_objects(keys, &vals)
286    }
287
288    /// Delivers a new notification
289    ///
290    /// Returns a `NotificationError` if a notification could not be delivered
291    ///
292    pub fn send(&self) -> NotificationResult<NotificationResponse> {
293        if let Some(delivery_date) = self.delivery_date {
294            ensure!(
295                delivery_date >= time::OffsetDateTime::now_utc().unix_timestamp() as f64,
296                NotificationError::ScheduleInThePast
297            );
298        };
299
300        let options = self.to_dictionary();
301
302        ensure_application_set()?;
303
304        let dictionary_response = unsafe {
305            sys::sendNotification(
306                NSString::from_str(self.title).deref(),
307                NSString::from_str(self.subtitle.unwrap_or("")).deref(),
308                NSString::from_str(self.message).deref(),
309                options.deref(),
310            )
311        };
312        ensure!(
313            dictionary_response
314                .objectForKey(NSString::from_str("error").deref())
315                .is_none(),
316            NotificationError::UnableToDeliver
317        );
318
319        let response = NotificationResponse::from_dictionary(dictionary_response);
320
321        Ok(response)
322    }
323}
324
325/// Response from the Notification
326#[derive(Debug)]
327pub enum NotificationResponse {
328    /// No interaction has occured
329    None,
330    /// User clicked on an action button with the given name
331    ActionButton(String),
332    /// User clicked on the close button with the given name
333    CloseButton(String),
334    /// User clicked the notification directly
335    Click,
336    /// User submitted text to the input text field
337    Reply(String),
338}
339
340impl NotificationResponse {
341    /// Create a NotificationResponse from the given Objective C NSDictionary
342    pub(crate) fn from_dictionary(dictionary: Retained<NSDictionary<NSString, NSString>>) -> Self {
343        let activation_type = dictionary
344            .objectForKey(NSString::from_str("activationType").deref())
345            .map(|str| str.to_string());
346
347        match activation_type.as_deref() {
348            Some("actionClicked") => NotificationResponse::ActionButton(
349                match dictionary.objectForKey(NSString::from_str("activationValue").deref()) {
350                    Some(str) => str.to_string(),
351                    None => String::from(""),
352                },
353            ),
354            Some("closeClicked") => NotificationResponse::CloseButton(
355                match dictionary.objectForKey(NSString::from_str("activationValue").deref()) {
356                    Some(str) => str.to_string(),
357                    None => String::from(""),
358                },
359            ),
360            Some("replied") => NotificationResponse::Reply(
361                match dictionary.objectForKey(NSString::from_str("activationValue").deref()) {
362                    Some(str) => str.to_string(),
363                    None => String::from(""),
364                },
365            ),
366            Some("contentsClicked") => NotificationResponse::Click,
367            _ => NotificationResponse::None,
368        }
369    }
370}