Skip to main content

mac_notification_sys/
notification.rs

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