Skip to main content

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}