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}