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}