notify_rust/notification.rs
1#[cfg(all(unix, not(target_os = "macos")))]
2use crate::{
3 hints::{CustomHintType, Hint},
4 urgency::Urgency,
5 xdg,
6};
7
8#[cfg(all(unix, not(target_os = "macos"), feature = "images"))]
9use crate::image::Image;
10
11#[cfg(all(unix, target_os = "macos"))]
12use crate::macos;
13#[cfg(target_os = "windows")]
14use crate::windows;
15
16use crate::{error::*, timeout::Timeout};
17
18#[cfg(all(unix, not(target_os = "macos")))]
19use std::collections::{HashMap, HashSet};
20
21// Returns the name of the current executable, used as a default for `Notification.appname`.
22fn exe_name() -> String {
23 std::env::current_exe()
24 .unwrap()
25 .file_name()
26 .unwrap()
27 .to_str()
28 .unwrap()
29 .to_owned()
30}
31
32/// Desktop notification.
33///
34/// A desktop notification is configured via builder pattern, before it is launched with `show()`.
35///
36/// # Example
37/// ``` no_run
38/// # use notify_rust::*;
39/// # fn _doc() -> Result<(), Box<dyn std::error::Error>> {
40/// Notification::new()
41/// .summary("☝️ A notification")
42/// .show()?;
43/// # Ok(())
44/// # }
45/// ```
46#[derive(Debug, Clone)]
47#[non_exhaustive]
48pub struct Notification {
49 /// Filled by default with executable name.
50 pub appname: String,
51
52 /// Single line to summarize the content.
53 pub summary: String,
54
55 /// Subtitle for macOS
56 pub subtitle: Option<String>,
57
58 /// Multiple lines possible, may support simple markup,
59 /// check out `get_capabilities()` -> `body-markup` and `body-hyperlinks`.
60 pub body: String,
61
62 /// Use a file:// URI or a name in an icon theme, must be compliant freedesktop.org.
63 pub icon: String,
64
65 /// Check out `Hint`
66 ///
67 /// # warning
68 /// this does not hold all hints, [`Hint::Custom`] and [`Hint::CustomInt`] are held elsewhere,
69 // /// please access hints via [`Notification::get_hints`].
70 #[cfg(all(unix, not(target_os = "macos")))]
71 pub hints: HashSet<Hint>,
72
73 #[cfg(all(unix, not(target_os = "macos")))]
74 pub(crate) hints_unique: HashMap<(String, CustomHintType), Hint>,
75
76 /// See `Notification::actions()` and `Notification::action()`
77 pub actions: Vec<String>,
78
79 #[cfg(target_os = "macos")]
80 pub(crate) sound_name: Option<String>,
81
82 #[cfg(target_os = "windows")]
83 pub(crate) sound_name: Option<String>,
84
85 #[cfg(target_os = "windows")]
86 pub(crate) path_to_image: Option<String>,
87
88 #[cfg(target_os = "windows")]
89 pub(crate) app_id: Option<String>,
90
91 #[cfg(all(unix, not(target_os = "macos")))]
92 pub(crate) bus: xdg::NotificationBus,
93
94 /// Lifetime of the Notification in ms. Often not respected by server, sorry.
95 pub timeout: Timeout, // both gnome and galago want allow for -1
96
97 /// Only to be used on the receive end. Use Notification hand for updating.
98 pub(crate) id: Option<u32>,
99}
100
101impl Notification {
102 /// Constructs a new Notification.
103 ///
104 /// Most fields are empty by default, only `appname` is initialized with the name of the current
105 /// executable.
106 /// The appname is used by some desktop environments to group notifications.
107 pub fn new() -> Notification {
108 Notification::default()
109 }
110
111 /// This is for testing purposes only and will not work with actual implementations.
112 #[cfg(all(unix, not(target_os = "macos")))]
113 #[doc(hidden)]
114 #[deprecated(note = "this is a test only feature")]
115 pub fn at_bus(sub_bus: &str) -> Notification {
116 let bus = xdg::NotificationBus::custom(sub_bus)
117 .ok_or("invalid subpath")
118 .unwrap();
119 Notification {
120 bus,
121 ..Notification::default()
122 }
123 }
124
125 /// Overwrite the appname field used for Notification.
126 ///
127 /// # Platform Support
128 /// Please note that this method has no effect on macOS. Here you can only set the application via [`set_application()`](fn.set_application.html)
129 pub fn appname(&mut self, appname: &str) -> &mut Notification {
130 appname.clone_into(&mut self.appname);
131 self
132 }
133
134 /// Set the `summary`.
135 ///
136 /// Often acts as title of the notification. For more elaborate content use the `body` field.
137 pub fn summary(&mut self, summary: &str) -> &mut Notification {
138 summary.clone_into(&mut self.summary);
139 self
140 }
141
142 /// Set the `subtitle`.
143 ///
144 /// This is only useful on macOS, it's not part of the XDG specification and will therefore be eaten by gremlins under your CPU 😈🤘.
145 pub fn subtitle(&mut self, subtitle: &str) -> &mut Notification {
146 self.subtitle = Some(subtitle.to_owned());
147 self
148 }
149
150 /// Manual wrapper for `Hint::ImageData`
151 #[cfg(all(feature = "images", unix, not(target_os = "macos")))]
152 pub fn image_data(&mut self, image: Image) -> &mut Notification {
153 self.hint(Hint::ImageData(image));
154 self
155 }
156
157 /// Wrapper for `Hint::ImagePath`
158 #[cfg(all(unix, not(target_os = "macos")))]
159 pub fn image_path(&mut self, path: &str) -> &mut Notification {
160 self.hint(Hint::ImagePath(path.to_string()));
161 self
162 }
163
164 /// Wrapper for `NotificationHint::ImagePath`
165 #[cfg(target_os = "windows")]
166 pub fn image_path(&mut self, path: &str) -> &mut Notification {
167 self.path_to_image = Some(path.to_string());
168 self
169 }
170
171 /// app's System.AppUserModel.ID
172 #[cfg(target_os = "windows")]
173 pub fn app_id(&mut self, app_id: &str) -> &mut Notification {
174 self.app_id = Some(app_id.to_string());
175 self
176 }
177
178 /// Wrapper for `Hint::ImageData`
179 #[cfg(all(feature = "images", unix, not(target_os = "macos")))]
180 pub fn image<T: AsRef<std::path::Path> + Sized>(
181 &mut self,
182 path: T,
183 ) -> Result<&mut Notification> {
184 let img = Image::open(&path)?;
185 self.hint(Hint::ImageData(img));
186 Ok(self)
187 }
188
189 /// Wrapper for `Hint::SoundName`
190 #[cfg(all(unix, not(target_os = "macos")))]
191 pub fn sound_name(&mut self, name: &str) -> &mut Notification {
192 self.hint(Hint::SoundName(name.to_owned()));
193 self
194 }
195
196 /// Set the `sound_name` for the `NSUserNotification`
197 #[cfg(any(target_os = "macos", target_os = "windows"))]
198 pub fn sound_name(&mut self, name: &str) -> &mut Notification {
199 self.sound_name = Some(name.to_owned());
200 self
201 }
202
203 /// Set the content of the `body` field.
204 ///
205 /// Multiline textual content of the notification.
206 /// Each line should be treated as a paragraph.
207 /// Simple html markup should be supported, depending on the server implementation.
208 pub fn body(&mut self, body: &str) -> &mut Notification {
209 body.clone_into(&mut self.body);
210 self
211 }
212
213 /// Set the `icon` field.
214 ///
215 /// You can use common icon names here, usually those in `/usr/share/icons`
216 /// can all be used.
217 /// You can also use an absolute path to file.
218 ///
219 /// # Platform support
220 /// macOS does not have support manually setting the icon. However you can pretend to be another app using [`set_application()`](fn.set_application.html)
221 pub fn icon(&mut self, icon: &str) -> &mut Notification {
222 icon.clone_into(&mut self.icon);
223 self
224 }
225
226 /// Set the `icon` field automatically.
227 ///
228 /// This looks at your binary's name and uses it to set the icon.
229 ///
230 /// # Platform support
231 /// macOS does not support manually setting the icon. However you can pretend to be another app using [`set_application()`](fn.set_application.html)
232 pub fn auto_icon(&mut self) -> &mut Notification {
233 self.icon = exe_name();
234 self
235 }
236
237 /// Adds a hint.
238 ///
239 /// This method will add a hint to the internal hint [`HashSet`].
240 /// Hints must be of type [`Hint`].
241 ///
242 /// Many of these are again wrapped by more convenient functions such as:
243 ///
244 /// * `sound_name(...)`
245 /// * `urgency(...)`
246 /// * [`image(...)`](#method.image) or
247 /// * [`image_data(...)`](#method.image_data)
248 /// * [`image_path(...)`](#method.image_path)
249 ///
250 /// ```no_run
251 /// # use notify_rust::Notification;
252 /// # use notify_rust::Hint;
253 /// Notification::new().summary("Category:email")
254 /// .body("This should not go away until you acknowledge it.")
255 /// .icon("thunderbird")
256 /// .appname("thunderbird")
257 /// .hint(Hint::Category("email".to_owned()))
258 /// .hint(Hint::Resident(true))
259 /// .show();
260 /// ```
261 ///
262 /// # Platform support
263 /// Most of these hints don't even have an effect on the big XDG Desktops, they are completely tossed on macOS.
264 #[cfg(all(unix, not(target_os = "macos")))]
265 pub fn hint(&mut self, hint: Hint) -> &mut Notification {
266 match hint {
267 Hint::CustomInt(k, v) => {
268 self.hints_unique
269 .insert((k.clone(), CustomHintType::Int), Hint::CustomInt(k, v));
270 }
271 Hint::Custom(k, v) => {
272 self.hints_unique
273 .insert((k.clone(), CustomHintType::String), Hint::Custom(k, v));
274 }
275 _ => {
276 self.hints.insert(hint);
277 }
278 }
279 self
280 }
281
282 #[cfg(all(unix, not(target_os = "macos")))]
283 pub(crate) fn get_hints(&self) -> impl Iterator<Item = &Hint> {
284 self.hints.iter().chain(self.hints_unique.values())
285 }
286
287 /// Set the `timeout`.
288 ///
289 /// Accepts multiple types that implement `Into<Timeout>`.
290 ///
291 /// ## `i31`
292 ///
293 /// This sets the time (in milliseconds) from the time the notification is displayed until it is
294 /// closed again by the Notification Server.
295 /// According to [specification](https://developer.gnome.org/notification-spec/)
296 /// -1 will leave the timeout to be set by the server and
297 /// 0 will cause the notification never to expire.
298 /// ## [Duration](`std::time::Duration`)
299 ///
300 /// When passing a [`Duration`](`std::time::Duration`) we will try convert it into milliseconds.
301 ///
302 ///
303 /// ```
304 /// # use std::time::Duration;
305 /// # use notify_rust::Timeout;
306 /// assert_eq!(Timeout::from(Duration::from_millis(2000)), Timeout::Milliseconds(2000));
307 /// ```
308 /// ### Caveats!
309 ///
310 /// 1. If the duration is zero milliseconds then the original behavior will apply and the notification will **Never** timeout.
311 /// 2. Should the number of milliseconds not fit within an [`i32`] then we will fall back to the default timeout.
312 /// ```
313 /// # use std::time::Duration;
314 /// # use notify_rust::Timeout;
315 /// assert_eq!(Timeout::from(Duration::from_millis(0)), Timeout::Never);
316 /// assert_eq!(Timeout::from(Duration::from_millis(u64::MAX)), Timeout::Default);
317 /// ```
318 ///
319 /// # Platform support
320 /// This only works on XDG Desktops, macOS does not support manually setting the timeout.
321 pub fn timeout<T: Into<Timeout>>(&mut self, timeout: T) -> &mut Notification {
322 self.timeout = timeout.into();
323 self
324 }
325
326 /// Set the `urgency`.
327 ///
328 /// Pick between Medium, Low and High.
329 ///
330 /// # Platform support
331 /// Most Desktops on linux and bsd are far too relaxed to pay any attention to this.
332 /// In macOS this does not exist
333 #[cfg(all(unix, not(target_os = "macos")))]
334 pub fn urgency(&mut self, urgency: Urgency) -> &mut Notification {
335 self.hint(Hint::Urgency(urgency)); // TODO impl as T where T: Into<Urgency>
336 self
337 }
338
339 /// Set `actions`.
340 ///
341 /// To quote <http://www.galago-project.org/specs/notification/0.9/x408.html#command-notify>
342 ///
343 /// > Actions are sent over as a list of pairs.
344 /// > Each even element in the list (starting at index 0) represents the identifier for the action.
345 /// > Each odd element in the list is the localized string that will be displayed to the user.y
346 ///
347 /// There is nothing fancy going on here yet.
348 /// **Careful! This replaces the internal list of actions!**
349 ///
350 /// (xdg only)
351 #[deprecated(note = "please use .action() only")]
352 pub fn actions(&mut self, actions: Vec<String>) -> &mut Notification {
353 self.actions = actions;
354 self
355 }
356
357 /// Add an action.
358 ///
359 /// This adds a single action to the internal list of actions.
360 ///
361 /// (xdg only)
362 pub fn action(&mut self, identifier: &str, label: &str) -> &mut Notification {
363 self.actions.push(identifier.to_owned());
364 self.actions.push(label.to_owned());
365 self
366 }
367
368 /// Set an Id ahead of time
369 ///
370 /// Setting the id ahead of time allows overriding a known other notification.
371 /// Though if you want to update a notification, it is easier to use the `update()` method of
372 /// the `NotificationHandle` object that `show()` returns.
373 ///
374 /// (xdg only)
375 pub fn id(&mut self, id: u32) -> &mut Notification {
376 self.id = Some(id);
377 self
378 }
379
380 /// Finalizes a Notification.
381 ///
382 /// Part of the builder pattern, returns a complete copy of the built notification.
383 pub fn finalize(&self) -> Notification {
384 self.clone()
385 }
386
387 /// Schedules a Notification
388 ///
389 /// Sends a Notification at the specified date.
390 #[cfg(all(target_os = "macos", feature = "chrono"))]
391 pub fn schedule<T: chrono::TimeZone>(
392 &self,
393 delivery_date: chrono::DateTime<T>,
394 ) -> Result<macos::NotificationHandle> {
395 macos::schedule_notification(self, delivery_date.timestamp() as f64)
396 }
397
398 /// Schedules a Notification
399 ///
400 /// Sends a Notification at the specified timestamp.
401 /// This is a raw `f64`, if that is a bit too raw for you please activate the feature `"chrono"`,
402 /// then you can use `Notification::schedule()` instead, which accepts a `chrono::DateTime<T>`.
403 #[cfg(target_os = "macos")]
404 pub fn schedule_raw(&self, timestamp: f64) -> Result<macos::NotificationHandle> {
405 macos::schedule_notification(self, timestamp)
406 }
407
408 /// Sends Notification to D-Bus.
409 ///
410 /// Returns a handle to a notification
411 #[cfg(all(unix, not(target_os = "macos")))]
412 pub fn show(&self) -> Result<xdg::NotificationHandle> {
413 xdg::show_notification(self)
414 }
415
416 /// Sends Notification to D-Bus.
417 ///
418 /// Returns a handle to a notification
419 #[cfg(all(unix, not(target_os = "macos")))]
420 #[cfg(all(feature = "async", feature = "zbus"))]
421 pub async fn show_async(&self) -> Result<xdg::NotificationHandle> {
422 xdg::show_notification_async(self).await
423 }
424
425 /// Sends Notification to D-Bus.
426 ///
427 /// Returns a handle to a notification
428 #[cfg(all(unix, not(target_os = "macos")))]
429 #[cfg(feature = "async")]
430 // #[cfg(test)]
431 pub async fn show_async_at_bus(&self, sub_bus: &str) -> Result<xdg::NotificationHandle> {
432 let bus = xdg::NotificationBus::custom(sub_bus).ok_or("invalid subpath")?;
433 xdg::show_notification_async_at_bus(self, bus).await
434 }
435
436 /// Sends Notification to `NSUserNotificationCenter`.
437 ///
438 /// Returns an `Ok` no matter what, since there is currently no way of telling the success of
439 /// the notification.
440 #[cfg(target_os = "macos")]
441 pub fn show(&self) -> Result<macos::NotificationHandle> {
442 macos::show_notification(self)
443 }
444
445 /// Sends Notification to `NSUserNotificationCenter`.
446 ///
447 /// Returns an `Ok` no matter what, since there is currently no way of telling the success of
448 /// the notification.
449 #[cfg(target_os = "windows")]
450 pub fn show(&self) -> Result<()> {
451 windows::show_notification(self)
452 }
453
454 /// Wraps [`Notification::show()`] but prints notification to stdout.
455 #[cfg(all(unix, not(target_os = "macos")))]
456 #[deprecated = "this was never meant to be public API"]
457 pub fn show_debug(&mut self) -> Result<xdg::NotificationHandle> {
458 println!(
459 "Notification:\n{appname}: ({icon}) {summary:?} {body:?}\nhints: [{hints:?}]\n",
460 appname = self.appname,
461 summary = self.summary,
462 body = self.body,
463 hints = self.hints,
464 icon = self.icon,
465 );
466 self.show()
467 }
468}
469
470impl Default for Notification {
471 #[cfg(all(unix, not(target_os = "macos")))]
472 fn default() -> Notification {
473 Notification {
474 appname: exe_name(),
475 summary: String::new(),
476 subtitle: None,
477 body: String::new(),
478 icon: String::new(),
479 hints: HashSet::new(),
480 hints_unique: HashMap::new(),
481 actions: Vec::new(),
482 timeout: Timeout::Default,
483 bus: Default::default(),
484 id: None,
485 }
486 }
487
488 #[cfg(target_os = "macos")]
489 fn default() -> Notification {
490 Notification {
491 appname: exe_name(),
492 summary: String::new(),
493 subtitle: None,
494 body: String::new(),
495 icon: String::new(),
496 actions: Vec::new(),
497 timeout: Timeout::Default,
498 sound_name: Default::default(),
499 id: None,
500 }
501 }
502
503 #[cfg(target_os = "windows")]
504 fn default() -> Notification {
505 Notification {
506 appname: exe_name(),
507 summary: String::new(),
508 subtitle: None,
509 body: String::new(),
510 icon: String::new(),
511 actions: Vec::new(),
512 timeout: Timeout::Default,
513 sound_name: Default::default(),
514 id: None,
515 path_to_image: None,
516 app_id: None,
517 }
518 }
519}