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}