Skip to main content

mac_notification_sys/
notification.rs

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