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