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}