Skip to main content

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, Urgency};
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(any(target_os = "windows", target_os = "macos"))]
86    pub(crate) path_to_image: Option<String>,
87
88    #[cfg(target_os = "windows")]
89    pub(crate) app_id: Option<String>,
90
91    #[cfg(target_os = "windows")]
92    pub(crate) urgency: Option<Urgency>,
93
94    #[cfg(all(unix, not(target_os = "macos")))]
95    pub(crate) bus: xdg::NotificationBus,
96
97    /// Lifetime of the Notification in ms. Often not respected by server, sorry.
98    pub timeout: Timeout, // both gnome and galago want allow for -1
99
100    /// Only to be used on the receive end. Use Notification hand for updating.
101    pub(crate) id: Option<u32>,
102}
103
104impl Notification {
105    /// Constructs a new Notification.
106    ///
107    /// Most fields are empty by default, only `appname` is initialized with the name of the current
108    /// executable.
109    /// The appname is used by some desktop environments to group notifications.
110    pub fn new() -> Notification {
111        Notification::default()
112    }
113
114    /// This is for testing purposes only and will not work with actual implementations.
115    #[cfg(all(unix, not(target_os = "macos")))]
116    #[doc(hidden)]
117    #[deprecated(note = "this is a test only feature")]
118    pub fn at_bus(sub_bus: &str) -> Notification {
119        let bus = xdg::NotificationBus::custom(sub_bus)
120            .ok_or("invalid subpath")
121            .unwrap();
122        Notification {
123            bus,
124            ..Notification::default()
125        }
126    }
127
128    /// Overwrite the appname field used for Notification.
129    ///
130    /// # Platform Support
131    /// Please note that this method has no effect on macOS. Here you can only set the application via [`set_application()`](fn.set_application.html)
132    pub fn appname(&mut self, appname: &str) -> &mut Notification {
133        appname.clone_into(&mut self.appname);
134        self
135    }
136
137    /// Set the `summary`.
138    ///
139    /// Often acts as title of the notification. For more elaborate content use the `body` field.
140    pub fn summary(&mut self, summary: &str) -> &mut Notification {
141        summary.clone_into(&mut self.summary);
142        self
143    }
144
145    /// Set the `subtitle`.
146    ///
147    /// This is only useful on macOS, it's not part of the XDG specification and will therefore be eaten by gremlins under your CPU 😈🤘.
148    pub fn subtitle(&mut self, subtitle: &str) -> &mut Notification {
149        self.subtitle = Some(subtitle.to_owned());
150        self
151    }
152
153    /// Manual wrapper for `Hint::ImageData`
154    #[cfg(all(feature = "images", unix, not(target_os = "macos")))]
155    pub fn image_data(&mut self, image: Image) -> &mut Notification {
156        self.hint(Hint::ImageData(image));
157        self
158    }
159
160    /// Sets the image path for the notification˝.
161    ///
162    /// The path is passed to the platform's native notification API directly — no additional
163    /// dependencies or crate features are required.
164    ///
165    /// Platform behaviour:
166    /// - **Linux/BSD (XDG):** maps to the `image-path` hint in the D-Bus notification spec.
167    /// - **macOS:** maps to `content_image` in `mac-notification-sys`, displayed on the right
168    ///   side of the notification banner.
169    /// - **Windows:** passed directly to `winrt-notification` as the notification image.
170    pub fn image_path(&mut self, path: &str) -> &mut Notification {
171        #[cfg(all(unix, not(target_os = "macos")))]
172        {
173            self.hint(Hint::ImagePath(path.to_string()));
174        }
175        #[cfg(any(target_os = "macos", target_os = "windows"))]
176        {
177            self.path_to_image = Some(path.to_string());
178        }
179        self
180    }
181
182    /// app's System.AppUserModel.ID
183    #[cfg(target_os = "windows")]
184    pub fn app_id(&mut self, app_id: &str) -> &mut Notification {
185        self.app_id = Some(app_id.to_string());
186        self
187    }
188
189    /// Wrapper for `Hint::ImageData`
190    #[cfg(all(feature = "images", unix, not(target_os = "macos")))]
191    pub fn image<T: AsRef<std::path::Path> + Sized>(
192        &mut self,
193        path: T,
194    ) -> Result<&mut Notification> {
195        let img = Image::open(&path)?;
196        self.hint(Hint::ImageData(img));
197        Ok(self)
198    }
199
200    /// Wrapper for `Hint::SoundName`
201    #[cfg(all(unix, not(target_os = "macos")))]
202    pub fn sound_name(&mut self, name: &str) -> &mut Notification {
203        self.hint(Hint::SoundName(name.to_owned()));
204        self
205    }
206
207    /// Set the `sound_name` for the `NSUserNotification`
208    #[cfg(any(target_os = "macos", target_os = "windows"))]
209    pub fn sound_name(&mut self, name: &str) -> &mut Notification {
210        self.sound_name = Some(name.to_owned());
211        self
212    }
213
214    /// Set the content of the `body` field.
215    ///
216    /// Multiline textual content of the notification.
217    /// Each line should be treated as a paragraph.
218    /// Simple html markup should be supported, depending on the server implementation.
219    pub fn body(&mut self, body: &str) -> &mut Notification {
220        body.clone_into(&mut self.body);
221        self
222    }
223
224    /// Set the `icon` field.
225    ///
226    /// You can use common icon names here, usually those in `/usr/share/icons`
227    /// can all be used.
228    /// You can also use an absolute path to file.
229    ///
230    /// # Platform support
231    /// macOS does not have support manually setting the icon. However you can pretend to be another app using [`set_application()`](fn.set_application.html)
232    pub fn icon(&mut self, icon: &str) -> &mut Notification {
233        icon.clone_into(&mut self.icon);
234        self
235    }
236
237    /// Set the `icon` field automatically.
238    ///
239    /// This looks at your binary's name and uses it to set the icon.
240    ///
241    /// # Platform support
242    /// macOS does not support manually setting the icon. However you can pretend to be another app using [`set_application()`](fn.set_application.html)
243    pub fn auto_icon(&mut self) -> &mut Notification {
244        self.icon = exe_name();
245        self
246    }
247
248    /// Adds a hint.
249    ///
250    /// This method will add a hint to the internal hint [`HashSet`].
251    /// Hints must be of type [`Hint`].
252    ///
253    /// Many of these are again wrapped by more convenient functions such as:
254    ///
255    /// * `sound_name(...)`
256    /// * `urgency(...)`
257    /// * [`image(...)`](#method.image) or
258    ///   * [`image_data(...)`](#method.image_data)
259    ///   * [`image_path(...)`](#method.image_path)
260    ///
261    /// ```no_run
262    /// # use notify_rust::Notification;
263    /// # use notify_rust::Hint;
264    /// Notification::new().summary("Category:email")
265    ///                    .body("This should not go away until you acknowledge it.")
266    ///                    .icon("thunderbird")
267    ///                    .appname("thunderbird")
268    ///                    .hint(Hint::Category("email".to_owned()))
269    ///                    .hint(Hint::Resident(true))
270    ///                    .show();
271    /// ```
272    ///
273    /// # Platform support
274    /// Most of these hints don't even have an effect on the big XDG Desktops, they are completely tossed on macOS.
275    #[cfg(all(unix, not(target_os = "macos")))]
276    pub fn hint(&mut self, hint: Hint) -> &mut Notification {
277        match hint {
278            Hint::CustomInt(k, v) => {
279                self.hints_unique
280                    .insert((k.clone(), CustomHintType::Int), Hint::CustomInt(k, v));
281            }
282            Hint::Custom(k, v) => {
283                self.hints_unique
284                    .insert((k.clone(), CustomHintType::String), Hint::Custom(k, v));
285            }
286            _ => {
287                self.hints.insert(hint);
288            }
289        }
290        self
291    }
292
293    #[cfg(all(unix, not(target_os = "macos")))]
294    pub(crate) fn get_hints(&self) -> impl Iterator<Item = &Hint> {
295        self.hints.iter().chain(self.hints_unique.values())
296    }
297
298    /// Set the `timeout`.
299    ///
300    /// Accepts multiple types that implement `Into<Timeout>`.
301    ///
302    /// ## `i31`
303    ///
304    /// This sets the time (in milliseconds) from the time the notification is displayed until it is
305    /// closed again by the Notification Server.
306    /// According to [specification](https://developer.gnome.org/notification-spec/)
307    /// -1 will leave the timeout to be set by the server and
308    /// 0 will cause the notification never to expire.
309    /// ## [Duration](`std::time::Duration`)
310    ///
311    /// When passing a [`Duration`](`std::time::Duration`) we will try convert it into milliseconds.
312    ///
313    ///
314    /// ```
315    /// # use std::time::Duration;
316    /// # use notify_rust::Timeout;
317    /// assert_eq!(Timeout::from(Duration::from_millis(2000)), Timeout::Milliseconds(2000));
318    /// ```
319    /// ### Caveats!
320    ///
321    /// 1. If the duration is zero milliseconds then the original behavior will apply and the notification will **Never** timeout.
322    /// 2. Should the number of milliseconds not fit within an [`i32`] then we will fall back to the default timeout.
323    /// ```
324    /// # use std::time::Duration;
325    /// # use notify_rust::Timeout;
326    /// assert_eq!(Timeout::from(Duration::from_millis(0)), Timeout::Never);
327    /// assert_eq!(Timeout::from(Duration::from_millis(u64::MAX)), Timeout::Default);
328    /// ```
329    ///
330    /// # Platform support
331    /// This only works on XDG Desktops, macOS does not support manually setting the timeout.
332    pub fn timeout<T: Into<Timeout>>(&mut self, timeout: T) -> &mut Notification {
333        self.timeout = timeout.into();
334        self
335    }
336
337    /// Set the `urgency`.
338    ///
339    /// Pick between Low, Normal, and Critical.
340    ///
341    /// # Platform support
342    ///
343    /// ## Linux/BSD (XDG)
344    /// Urgency is sent as a hint to the notification server. Most desktops are fairly relaxed
345    /// about urgency and may not change behavior significantly. Critical notifications are
346    /// intended to not timeout automatically.
347    ///
348    /// ## Windows
349    /// Urgency is mapped to toast scenarios:
350    /// - `Low` and `Normal` → Default scenario (standard toast behavior)
351    /// - `Critical` → Reminder scenario (stays on screen until user dismisses)
352    ///
353    /// ## macOS
354    /// Not currently supported.
355    #[cfg(all(unix, not(target_os = "macos")))]
356    pub fn urgency(&mut self, urgency: Urgency) -> &mut Notification {
357        self.hint(Hint::Urgency(urgency)); // TODO impl as T where T: Into<Urgency>
358        self
359    }
360
361    /// Set the `urgency`.
362    ///
363    /// Pick between Low, Normal, and Critical.
364    ///
365    /// # Platform support
366    ///
367    /// ## Windows
368    /// Urgency is mapped to toast scenarios:
369    /// - `Low` and `Normal` → Default scenario (standard toast behavior)
370    /// - `Critical` → Reminder scenario (stays on screen until user dismisses)
371    ///
372    /// ## Linux/BSD (XDG)
373    /// See the Unix implementation documentation.
374    ///
375    /// ## macOS
376    /// Not currently supported.
377    #[cfg(target_os = "windows")]
378    pub fn urgency(&mut self, urgency: Urgency) -> &mut Notification {
379        self.urgency = Some(urgency);
380        self
381    }
382
383    /// Set `actions`.
384    ///
385    /// To quote <http://www.galago-project.org/specs/notification/0.9/x408.html#command-notify>
386    ///
387    /// >  Actions are sent over as a list of pairs.
388    /// >  Each even element in the list (starting at index 0) represents the identifier for the action.
389    /// >  Each odd element in the list is the localized string that will be displayed to the user.y
390    ///
391    /// There is nothing fancy going on here yet.
392    /// **Careful! This replaces the internal list of actions!**
393    ///
394    /// (xdg only)
395    #[deprecated(note = "please use .action() only")]
396    pub fn actions(&mut self, actions: Vec<String>) -> &mut Notification {
397        self.actions = actions;
398        self
399    }
400
401    /// Add an action.
402    ///
403    /// This adds a single action to the internal list of actions.
404    ///
405    /// (xdg only)
406    pub fn action(&mut self, identifier: &str, label: &str) -> &mut Notification {
407        self.actions.push(identifier.to_owned());
408        self.actions.push(label.to_owned());
409        self
410    }
411
412    /// Set an Id ahead of time
413    ///
414    /// Setting the id ahead of time allows overriding a known other notification.
415    /// Though if you want to update a notification, it is easier to use the `update()` method of
416    /// the `NotificationHandle` object that `show()` returns.
417    ///
418    /// (xdg only)
419    pub fn id(&mut self, id: u32) -> &mut Notification {
420        self.id = Some(id);
421        self
422    }
423
424    /// Finalizes a Notification.
425    ///
426    /// Part of the builder pattern, returns a complete copy of the built notification.
427    pub fn finalize(&self) -> Notification {
428        self.clone()
429    }
430
431    /// Schedules a Notification
432    ///
433    /// Sends a Notification at the specified date.
434    #[cfg(all(target_os = "macos", feature = "chrono"))]
435    pub fn schedule<T: chrono::TimeZone>(
436        &self,
437        delivery_date: chrono::DateTime<T>,
438    ) -> Result<macos::NotificationHandle> {
439        macos::schedule_notification(self, delivery_date.timestamp() as f64)
440    }
441
442    /// Schedules a Notification
443    ///
444    /// Sends a Notification at the specified timestamp.
445    /// This is a raw `f64`, if that is a bit too raw for you please activate the feature `"chrono"`,
446    /// then you can use `Notification::schedule()` instead, which accepts a `chrono::DateTime<T>`.
447    #[cfg(target_os = "macos")]
448    pub fn schedule_raw(&self, timestamp: f64) -> Result<macos::NotificationHandle> {
449        macos::schedule_notification(self, timestamp)
450    }
451
452    /// Sends Notification to D-Bus.
453    ///
454    /// Returns a handle to a notification
455    #[cfg(all(unix, not(target_os = "macos")))]
456    pub fn show(&self) -> Result<xdg::NotificationHandle> {
457        xdg::show_notification(self)
458    }
459
460    /// Sends Notification to D-Bus.
461    ///
462    /// Returns a handle to a notification
463    #[cfg(all(unix, not(target_os = "macos")))]
464    #[cfg(feature = "zbus")]
465    pub async fn show_async(&self) -> Result<xdg::NotificationHandle> {
466        xdg::show_notification_async(self).await
467    }
468
469    /// Sends Notification to D-Bus.
470    ///
471    /// Returns a handle to a notification
472    #[cfg(all(unix, not(target_os = "macos")))]
473    #[cfg(feature = "zbus")]
474    // #[cfg(test)]
475    pub async fn show_async_at_bus(&self, sub_bus: &str) -> Result<xdg::NotificationHandle> {
476        let bus = xdg::NotificationBus::custom(sub_bus).ok_or("invalid subpath")?;
477        xdg::show_notification_async_at_bus(self, bus).await
478    }
479
480    /// Sends Notification to `NSUserNotificationCenter`.
481    ///
482    /// Returns an `Ok` no matter what, since there is currently no way of telling the success of
483    /// the notification.
484    #[cfg(target_os = "macos")]
485    pub fn show(&self) -> Result<macos::NotificationHandle> {
486        macos::show_notification(self)
487    }
488
489    /// Sends Notification to `NSUserNotificationCenter`.
490    ///
491    /// Returns an `Ok` no matter what, since there is currently no way of telling the success of
492    /// the notification.
493    #[cfg(target_os = "windows")]
494    pub fn show(&self) -> Result<()> {
495        windows::show_notification(self)
496    }
497
498    /// Wraps [`Notification::show()`] but prints notification to stdout.
499    #[cfg(all(unix, not(target_os = "macos")))]
500    #[deprecated = "this was never meant to be public API"]
501    pub fn show_debug(&mut self) -> Result<xdg::NotificationHandle> {
502        println!(
503            "Notification:\n{appname}: ({icon}) {summary:?} {body:?}\nhints: [{hints:?}]\n",
504            appname = self.appname,
505            summary = self.summary,
506            body = self.body,
507            hints = self.hints,
508            icon = self.icon,
509        );
510        self.show()
511    }
512}
513
514impl Default for Notification {
515    #[cfg(all(unix, not(target_os = "macos")))]
516    fn default() -> Notification {
517        Notification {
518            appname: exe_name(),
519            summary: String::new(),
520            subtitle: None,
521            body: String::new(),
522            icon: String::new(),
523            hints: HashSet::new(),
524            hints_unique: HashMap::new(),
525            actions: Vec::new(),
526            timeout: Timeout::Default,
527            bus: Default::default(),
528            id: None,
529        }
530    }
531
532    #[cfg(target_os = "macos")]
533    fn default() -> Notification {
534        Notification {
535            appname: exe_name(),
536            summary: String::new(),
537            subtitle: None,
538            body: String::new(),
539            icon: String::new(),
540            actions: Vec::new(),
541            timeout: Timeout::Default,
542            sound_name: Default::default(),
543            path_to_image: None,
544            id: None,
545        }
546    }
547
548    #[cfg(target_os = "windows")]
549    fn default() -> Notification {
550        Notification {
551            appname: exe_name(),
552            summary: String::new(),
553            subtitle: None,
554            body: String::new(),
555            icon: String::new(),
556            actions: Vec::new(),
557            timeout: Timeout::Default,
558            sound_name: Default::default(),
559            id: None,
560            path_to_image: None,
561            app_id: None,
562            urgency: None,
563        }
564    }
565}