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;
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(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    /// Sets the image path for the notification˝.
158    ///
159    /// The path is passed to the platform's native notification API directly — no additional
160    /// dependencies or crate features are required.
161    ///
162    /// Platform behaviour:
163    /// - **Linux/BSD (XDG):** maps to the `image-path` hint in the D-Bus notification spec.
164    /// - **macOS:** maps to `content_image` in `mac-notification-sys`, displayed on the right
165    ///   side of the notification banner.
166    /// - **Windows:** passed directly to `winrt-notification` as the notification image.
167    pub fn image_path(&mut self, path: &str) -> &mut Notification {
168        #[cfg(all(unix, not(target_os = "macos")))]
169        {
170            self.hint(Hint::ImagePath(path.to_string()));
171        }
172        #[cfg(any(target_os = "macos", target_os = "windows"))]
173        {
174            self.path_to_image = Some(path.to_string());
175        }
176        self
177    }
178
179    /// app's System.AppUserModel.ID
180    #[cfg(target_os = "windows")]
181    pub fn app_id(&mut self, app_id: &str) -> &mut Notification {
182        self.app_id = Some(app_id.to_string());
183        self
184    }
185
186    /// Wrapper for `Hint::ImageData`
187    #[cfg(all(feature = "images", unix, not(target_os = "macos")))]
188    pub fn image<T: AsRef<std::path::Path> + Sized>(
189        &mut self,
190        path: T,
191    ) -> Result<&mut Notification> {
192        let img = Image::open(&path)?;
193        self.hint(Hint::ImageData(img));
194        Ok(self)
195    }
196
197    /// Wrapper for `Hint::SoundName`
198    #[cfg(all(unix, not(target_os = "macos")))]
199    pub fn sound_name(&mut self, name: &str) -> &mut Notification {
200        self.hint(Hint::SoundName(name.to_owned()));
201        self
202    }
203
204    /// Set the `sound_name` for the `NSUserNotification`
205    #[cfg(any(target_os = "macos", target_os = "windows"))]
206    pub fn sound_name(&mut self, name: &str) -> &mut Notification {
207        self.sound_name = Some(name.to_owned());
208        self
209    }
210
211    /// Set the content of the `body` field.
212    ///
213    /// Multiline textual content of the notification.
214    /// Each line should be treated as a paragraph.
215    /// Simple html markup should be supported, depending on the server implementation.
216    pub fn body(&mut self, body: &str) -> &mut Notification {
217        body.clone_into(&mut self.body);
218        self
219    }
220
221    /// Set the `icon` field.
222    ///
223    /// You can use common icon names here, usually those in `/usr/share/icons`
224    /// can all be used.
225    /// You can also use an absolute path to file.
226    ///
227    /// # Platform support
228    /// macOS does not have support manually setting the icon. However you can pretend to be another app using [`set_application()`](fn.set_application.html)
229    pub fn icon(&mut self, icon: &str) -> &mut Notification {
230        icon.clone_into(&mut self.icon);
231        self
232    }
233
234    /// Set the `icon` field automatically.
235    ///
236    /// This looks at your binary's name and uses it to set the icon.
237    ///
238    /// # Platform support
239    /// macOS does not support manually setting the icon. However you can pretend to be another app using [`set_application()`](fn.set_application.html)
240    pub fn auto_icon(&mut self) -> &mut Notification {
241        self.icon = exe_name();
242        self
243    }
244
245    /// Adds a hint.
246    ///
247    /// This method will add a hint to the internal hint [`HashSet`].
248    /// Hints must be of type [`Hint`].
249    ///
250    /// Many of these are again wrapped by more convenient functions such as:
251    ///
252    /// * `sound_name(...)`
253    /// * `urgency(...)`
254    /// * [`image(...)`](#method.image) or
255    ///   * [`image_data(...)`](#method.image_data)
256    ///   * [`image_path(...)`](#method.image_path)
257    ///
258    /// ```no_run
259    /// # use notify_rust::Notification;
260    /// # use notify_rust::Hint;
261    /// Notification::new().summary("Category:email")
262    ///                    .body("This should not go away until you acknowledge it.")
263    ///                    .icon("thunderbird")
264    ///                    .appname("thunderbird")
265    ///                    .hint(Hint::Category("email".to_owned()))
266    ///                    .hint(Hint::Resident(true))
267    ///                    .show();
268    /// ```
269    ///
270    /// # Platform support
271    /// Most of these hints don't even have an effect on the big XDG Desktops, they are completely tossed on macOS.
272    #[cfg(all(unix, not(target_os = "macos")))]
273    pub fn hint(&mut self, hint: Hint) -> &mut Notification {
274        match hint {
275            Hint::CustomInt(k, v) => {
276                self.hints_unique
277                    .insert((k.clone(), CustomHintType::Int), Hint::CustomInt(k, v));
278            }
279            Hint::Custom(k, v) => {
280                self.hints_unique
281                    .insert((k.clone(), CustomHintType::String), Hint::Custom(k, v));
282            }
283            _ => {
284                self.hints.insert(hint);
285            }
286        }
287        self
288    }
289
290    #[cfg(all(unix, not(target_os = "macos")))]
291    pub(crate) fn get_hints(&self) -> impl Iterator<Item = &Hint> {
292        self.hints.iter().chain(self.hints_unique.values())
293    }
294
295    /// Set the `timeout`.
296    ///
297    /// Accepts multiple types that implement `Into<Timeout>`.
298    ///
299    /// ## `i31`
300    ///
301    /// This sets the time (in milliseconds) from the time the notification is displayed until it is
302    /// closed again by the Notification Server.
303    /// According to [specification](https://developer.gnome.org/notification-spec/)
304    /// -1 will leave the timeout to be set by the server and
305    /// 0 will cause the notification never to expire.
306    /// ## [Duration](`std::time::Duration`)
307    ///
308    /// When passing a [`Duration`](`std::time::Duration`) we will try convert it into milliseconds.
309    ///
310    ///
311    /// ```
312    /// # use std::time::Duration;
313    /// # use notify_rust::Timeout;
314    /// assert_eq!(Timeout::from(Duration::from_millis(2000)), Timeout::Milliseconds(2000));
315    /// ```
316    /// ### Caveats!
317    ///
318    /// 1. If the duration is zero milliseconds then the original behavior will apply and the notification will **Never** timeout.
319    /// 2. Should the number of milliseconds not fit within an [`i32`] then we will fall back to the default timeout.
320    /// ```
321    /// # use std::time::Duration;
322    /// # use notify_rust::Timeout;
323    /// assert_eq!(Timeout::from(Duration::from_millis(0)), Timeout::Never);
324    /// assert_eq!(Timeout::from(Duration::from_millis(u64::MAX)), Timeout::Default);
325    /// ```
326    ///
327    /// # Platform support
328    /// This only works on XDG Desktops, macOS does not support manually setting the timeout.
329    pub fn timeout<T: Into<Timeout>>(&mut self, timeout: T) -> &mut Notification {
330        self.timeout = timeout.into();
331        self
332    }
333
334    /// Set the `urgency`.
335    ///
336    /// Pick between Medium, Low and High.
337    ///
338    /// # Platform support
339    /// Most Desktops on linux and bsd are far too relaxed to pay any attention to this.
340    /// In macOS this does not exist
341    #[cfg(all(unix, not(target_os = "macos")))]
342    pub fn urgency(&mut self, urgency: Urgency) -> &mut Notification {
343        self.hint(Hint::Urgency(urgency)); // TODO impl as T where T: Into<Urgency>
344        self
345    }
346
347    /// Set `actions`.
348    ///
349    /// To quote <http://www.galago-project.org/specs/notification/0.9/x408.html#command-notify>
350    ///
351    /// >  Actions are sent over as a list of pairs.
352    /// >  Each even element in the list (starting at index 0) represents the identifier for the action.
353    /// >  Each odd element in the list is the localized string that will be displayed to the user.y
354    ///
355    /// There is nothing fancy going on here yet.
356    /// **Careful! This replaces the internal list of actions!**
357    ///
358    /// (xdg only)
359    #[deprecated(note = "please use .action() only")]
360    pub fn actions(&mut self, actions: Vec<String>) -> &mut Notification {
361        self.actions = actions;
362        self
363    }
364
365    /// Add an action.
366    ///
367    /// This adds a single action to the internal list of actions.
368    ///
369    /// (xdg only)
370    pub fn action(&mut self, identifier: &str, label: &str) -> &mut Notification {
371        self.actions.push(identifier.to_owned());
372        self.actions.push(label.to_owned());
373        self
374    }
375
376    /// Set an Id ahead of time
377    ///
378    /// Setting the id ahead of time allows overriding a known other notification.
379    /// Though if you want to update a notification, it is easier to use the `update()` method of
380    /// the `NotificationHandle` object that `show()` returns.
381    ///
382    /// (xdg only)
383    pub fn id(&mut self, id: u32) -> &mut Notification {
384        self.id = Some(id);
385        self
386    }
387
388    /// Finalizes a Notification.
389    ///
390    /// Part of the builder pattern, returns a complete copy of the built notification.
391    pub fn finalize(&self) -> Notification {
392        self.clone()
393    }
394
395    /// Schedules a Notification
396    ///
397    /// Sends a Notification at the specified date.
398    #[cfg(all(target_os = "macos", feature = "chrono"))]
399    pub fn schedule<T: chrono::TimeZone>(
400        &self,
401        delivery_date: chrono::DateTime<T>,
402    ) -> Result<macos::NotificationHandle> {
403        macos::schedule_notification(self, delivery_date.timestamp() as f64)
404    }
405
406    /// Schedules a Notification
407    ///
408    /// Sends a Notification at the specified timestamp.
409    /// This is a raw `f64`, if that is a bit too raw for you please activate the feature `"chrono"`,
410    /// then you can use `Notification::schedule()` instead, which accepts a `chrono::DateTime<T>`.
411    #[cfg(target_os = "macos")]
412    pub fn schedule_raw(&self, timestamp: f64) -> Result<macos::NotificationHandle> {
413        macos::schedule_notification(self, timestamp)
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    pub fn show(&self) -> Result<xdg::NotificationHandle> {
421        xdg::show_notification(self)
422    }
423
424    /// Sends Notification to D-Bus.
425    ///
426    /// Returns a handle to a notification
427    #[cfg(all(unix, not(target_os = "macos")))]
428    #[cfg(all(feature = "async", feature = "zbus"))]
429    pub async fn show_async(&self) -> Result<xdg::NotificationHandle> {
430        xdg::show_notification_async(self).await
431    }
432
433    /// Sends Notification to D-Bus.
434    ///
435    /// Returns a handle to a notification
436    #[cfg(all(unix, not(target_os = "macos")))]
437    #[cfg(feature = "async")]
438    // #[cfg(test)]
439    pub async fn show_async_at_bus(&self, sub_bus: &str) -> Result<xdg::NotificationHandle> {
440        let bus = xdg::NotificationBus::custom(sub_bus).ok_or("invalid subpath")?;
441        xdg::show_notification_async_at_bus(self, bus).await
442    }
443
444    /// Sends Notification to `NSUserNotificationCenter`.
445    ///
446    /// Returns an `Ok` no matter what, since there is currently no way of telling the success of
447    /// the notification.
448    #[cfg(target_os = "macos")]
449    pub fn show(&self) -> Result<macos::NotificationHandle> {
450        macos::show_notification(self)
451    }
452
453    /// Sends Notification to `NSUserNotificationCenter`.
454    ///
455    /// Returns an `Ok` no matter what, since there is currently no way of telling the success of
456    /// the notification.
457    #[cfg(target_os = "windows")]
458    pub fn show(&self) -> Result<()> {
459        windows::show_notification(self)
460    }
461
462    /// Wraps [`Notification::show()`] but prints notification to stdout.
463    #[cfg(all(unix, not(target_os = "macos")))]
464    #[deprecated = "this was never meant to be public API"]
465    pub fn show_debug(&mut self) -> Result<xdg::NotificationHandle> {
466        println!(
467            "Notification:\n{appname}: ({icon}) {summary:?} {body:?}\nhints: [{hints:?}]\n",
468            appname = self.appname,
469            summary = self.summary,
470            body = self.body,
471            hints = self.hints,
472            icon = self.icon,
473        );
474        self.show()
475    }
476}
477
478impl Default for Notification {
479    #[cfg(all(unix, not(target_os = "macos")))]
480    fn default() -> Notification {
481        Notification {
482            appname: exe_name(),
483            summary: String::new(),
484            subtitle: None,
485            body: String::new(),
486            icon: String::new(),
487            hints: HashSet::new(),
488            hints_unique: HashMap::new(),
489            actions: Vec::new(),
490            timeout: Timeout::Default,
491            bus: Default::default(),
492            id: None,
493        }
494    }
495
496    #[cfg(target_os = "macos")]
497    fn default() -> Notification {
498        Notification {
499            appname: exe_name(),
500            summary: String::new(),
501            subtitle: None,
502            body: String::new(),
503            icon: String::new(),
504            actions: Vec::new(),
505            timeout: Timeout::Default,
506            sound_name: Default::default(),
507            path_to_image: None,
508            id: None,
509        }
510    }
511
512    #[cfg(target_os = "windows")]
513    fn default() -> Notification {
514        Notification {
515            appname: exe_name(),
516            summary: String::new(),
517            subtitle: None,
518            body: String::new(),
519            icon: String::new(),
520            actions: Vec::new(),
521            timeout: Timeout::Default,
522            sound_name: Default::default(),
523            id: None,
524            path_to_image: None,
525            app_id: None,
526        }
527    }
528}