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}