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}