libnotify_rs/api/notification.rs
1use crate::api::errors::LibnotifyError;
2
3use super::libnotify::*;
4
5#[derive(Debug, Clone, Copy)]
6#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
7/// Notification urgency level. Refer to [libnotify docs](https://gnome.pages.gitlab.gnome.org/libnotify/enum.Urgency.html) for more info
8pub enum Urgency {
9 /// Low urgency. Used for unimportant notifications
10 Low,
11 /// Normal urgency. Used for most standard notifications
12 Normal,
13 /// Critical urgency. Used for very important notifications
14 Critical,
15}
16
17#[derive(Debug, Clone, Copy)]
18/// Reason why notification was closed. Refer to [libnotify docs](https://gnome.pages.gitlab.gnome.org/libnotify/enum.ClosedReason.html)
19pub enum ClosedReason {
20 /// Notification not closed
21 Unset,
22 /// Timeout has expired
23 Expired,
24 /// It has been dismissed by the user
25 Dismissed,
26 /// It has been closed by a call to `notify_notification_close()`
27 ApiRequest,
28 /// Closed by undefined/reserved reasons
29 Undefined,
30
31 /// This variant is returned only if libnotify returns some weird enum value, which should never
32 /// happen
33 Invalid,
34}
35
36/// Enum representing `[GVariant](https://docs.gtk.org/glib/struct.Variant.html)`
37/// Used in set hint.
38#[derive(Debug, Clone)]
39pub enum HintValue {
40 #[allow(missing_docs)]
41 String(String),
42 #[allow(missing_docs)]
43 Int32(i32),
44 #[allow(missing_docs)]
45 Boolean(bool),
46}
47
48/// Notification timeout, until close
49#[derive(Debug, Clone, Copy)]
50pub enum Timeout {
51 #[allow(missing_docs)]
52 Default,
53 #[allow(missing_docs)]
54 Never,
55 /// Custom timeout in milliseconds
56 Custom(i32),
57}
58
59/// Represents one notification. Wraps most of [libnotify::notifaction](https://gnome.pages.gitlab.gnome.org/libnotify/class.Notification.html) functions.
60pub struct Notification {
61 inner: *mut NotifyNotification,
62}
63
64impl<'a> Notification {
65 /// Create new notification.
66 ///
67 /// # Errors
68 /// This function can return LibnotifyError::{NewNotificationError, NulError}
69 ///
70 /// # Example
71 /// ```
72 ///
73 /// use libnotify_rs::api::{
74 /// functions::{init, uninit},
75 /// notification::Notification,
76 /// };
77 ///
78 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
79 /// init("Test app")?;
80 /// let notification = Notification::new("My notification", "Yap yap", None)?;
81 /// // ...
82 /// uninit();
83 /// Ok(())
84 /// }
85 /// ```
86 pub fn new<T, Y>(summary: &str, body: T, icon: Y) -> Result<Self, LibnotifyError>
87 where
88 T: Into<Option<&'a str>>,
89 Y: Into<Option<&'a str>>,
90 {
91 let summary = std::ffi::CString::new(summary)?;
92
93 let body = body.into().map(std::ffi::CString::new).transpose()?;
94
95 let icon = icon.into().map(std::ffi::CString::new).transpose()?;
96
97 let inner = unsafe {
98 notify_notification_new(
99 summary.as_ptr(),
100 body.as_ref().map_or(std::ptr::null(), |body| body.as_ptr()),
101 icon.as_ref().map_or(std::ptr::null(), |icon| icon.as_ptr()),
102 )
103 };
104
105 if inner.is_null() {
106 return Err(LibnotifyError::NewNotificationError);
107 }
108
109 Ok(Self { inner })
110 }
111
112 /// Add callback to notification.
113 ///
114 /// This function needs a [glib mainloop](https://docs.gtk.org/glib/main-loop.html)
115 ///
116 /// # Args
117 /// `action` – action identifier. `default` will work for most use cases. Other text will be
118 /// displayed as button names.
119 ///
120 /// `label` – label for user
121 ///
122 /// `callback` - any closure that accepts &str as an argument, where &str is action name.
123 ///
124 /// # Errors
125 /// This function can return LibnotifyError::NulError
126 ///
127 /// # Example
128 /// ```no_run(There's no notification deamon on CI and no one to click the notification)
129 /// use glib::MainLoop;
130 /// use libnotify_rs::{
131 /// api::functions::{init, uninit},
132 /// api::notification::Notification,
133 /// };
134 ///
135 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
136 /// init("Coffee reminder")?;
137 /// let mut notify = Notification::new("Drink coffee", "Click me when you're done drinking", None)?;
138 ///
139 /// // We need to have a glib main loop in order to receive callback actions
140 /// let main_loop = MainLoop::new(None, false);
141 /// let loop_clone = main_loop.clone();
142 ///
143 /// notify.add_action("default", "I'm drinking!", move |action_name| {
144 /// println!("User drunk his coffee!");
145 /// println!("Action was named: {}", action_name);
146 /// loop_clone.quit();
147 /// })?;
148 ///
149 /// notify.show()?;
150 /// main_loop.run();
151 ///
152 /// println!("Glib mainloop has ended");
153 ///
154 /// uninit();
155 ///
156 /// Ok(())
157 /// }
158 /// ```
159 pub fn add_action<F>(
160 &mut self,
161 action: &str,
162 label: &str,
163 callback: F,
164 ) -> Result<(), LibnotifyError>
165 where
166 F: Fn(&str) + 'static,
167 {
168 let action = std::ffi::CString::new(action)?;
169 let label = std::ffi::CString::new(label)?;
170
171 let callback: Box<F> = Box::new(callback);
172 let user_data = Box::into_raw(callback) as *mut std::ffi::c_void;
173
174 let free_func = drop_box::<F>;
175
176 unsafe {
177 notify_notification_add_action(
178 self.inner,
179 action.as_ptr(),
180 label.as_ptr(),
181 Some(action_trampoline::<F>),
182 user_data,
183 Some(free_func),
184 );
185 }
186
187 Ok(())
188 }
189
190 /// Clears all actions from the notification
191 ///
192 /// # Example
193 /// ```
194 /// use libnotify_rs::api::{
195 /// functions::{init, uninit},
196 /// notification::Notification,
197 /// };
198 ///
199 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
200 /// init("Test app")?;
201 /// let mut notification = Notification::new("My notification", "Yap yap", None)?;
202 /// notification.add_action("default", "yap", |action| {
203 /// // do stuff
204 /// })?;
205 ///
206 /// notification.add_action("click", "yappers", |action| {
207 /// // do some other stuff
208 /// })?;
209 ///
210 /// notification.clear_actions(); // now there are no callbacks!
211 ///
212 /// uninit();
213 /// Ok(())
214 /// }
215 /// ```
216 ///
217 pub fn clear_actions(&mut self) {
218 unsafe { notify_notification_clear_actions(self.inner) }
219 }
220
221 /// Clears all hints from the notification
222 pub fn clear_hints(&mut self) {
223 unsafe { notify_notification_clear_hints(self.inner) }
224 }
225
226 /// Synchronously tells the notification server to hide the notification on the screen
227 ///
228 /// # Errors
229 /// This function can return LibnotifyError::{AllocationError, GerrorError}
230 ///
231 /// # Example
232 /// ```no_run(Not notification deamon on CI)
233 /// use libnotify_rs::api::{functions::init, notification::Notification};
234 ///
235 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
236 /// init("Test app")?;
237 /// let notification = Notification::new("My notification", "Yap yap", None)?;
238 /// notification.show()?;
239 /// std::thread::sleep(std::time::Duration::from_secs(5)); // To do this you can just set duration,
240 /// // but this is an example
241 /// notification.close()?; // After 5 seconds no notification
242 ///
243 /// Ok(())
244 /// }
245 /// ```
246 pub fn close(&self) -> Result<(), LibnotifyError> {
247 let mut gerror = std::ptr::null_mut() as *mut GError;
248
249 if unsafe { notify_notification_close(self.inner, &mut gerror) } == 0 {
250 if gerror.is_null() {
251 return Err(LibnotifyError::AllocationError);
252 }
253
254 let e = unsafe { *gerror }.message;
255
256 if e.is_null() {
257 unsafe { g_error_free(gerror) };
258 return Err(LibnotifyError::AllocationError);
259 }
260
261 let e_str = unsafe { std::ffi::CStr::from_ptr(e) }
262 .to_string_lossy()
263 .to_string();
264
265 unsafe { g_error_free(gerror) };
266
267 return Err(LibnotifyError::GerrorError(e_str));
268 }
269
270 Ok(())
271 }
272
273 /// Returns the closed reason code for the notification.
274 ///
275 /// This is valid only after the Notification::closed signal is emitted.
276 ///
277 /// This function needs a [glib mainloop](https://docs.gtk.org/glib/main-loop.html)
278 ///
279 /// # Example
280 /// ```no_run(No notificaton daemon on CI)
281 /// use glib::MainLoop;
282 /// use libnotify_rs::{
283 /// api::functions::{init, uninit},
284 /// api::notification::Notification,
285 /// };
286 ///
287 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
288 /// init("Coffee reminder")?;
289 /// let mut notify = Notification::new("Drink coffee", "Click me when you're done drinking", None)?;
290 ///
291 /// // We need to have a glib main loop in order to receive closed_reason
292 /// let main_loop = MainLoop::new(None, false);
293 /// let loop_clone = main_loop.clone();
294 ///
295 /// notify.show()?;
296 /// main_loop.run();
297 ///
298 /// // Thanks to the main loop we can also read why the notification was closed
299 /// println!(
300 /// "Notification closed, because: {:?}",
301 /// notify.get_closed_reason()
302 /// );
303 ///
304 /// Ok(())
305 /// }
306 ///```
307 pub fn get_closed_reason(&self) -> ClosedReason {
308 match unsafe { notify_notification_get_closed_reason(self.inner) } {
309 NotifyClosedReason_NOTIFY_CLOSED_REASON_UNSET => ClosedReason::Unset,
310 NotifyClosedReason_NOTIFY_CLOSED_REASON_EXPIRED => ClosedReason::Expired,
311 NotifyClosedReason_NOTIFY_CLOSED_REASON_DISMISSED => ClosedReason::Dismissed,
312 NotifyClosedReason_NOTIFY_CLOSED_REASON_API_REQUEST => ClosedReason::ApiRequest,
313 NotifyClosedReason_NOTIFY_CLOSED_REASON_UNDEFINED => ClosedReason::Undefined,
314 _ => ClosedReason::Invalid,
315 }
316 }
317
318 /// Sets the application icon for the notification.
319 ///
320 /// Refer to the [documentation](https://specifications.freedesktop.org/icon-naming/latest/) for
321 /// icon names.
322 ///
323 /// If this function is not called, the application icon will be set from the value set via
324 /// `set_app_icon()`. Usage is the same.
325 ///
326 /// Available since: 0.8.4
327 ///
328 /// # Errors
329 /// This function can return LibnotifyError::{NulError}
330 pub fn set_app_icon(&mut self, app_icon: &str) -> Result<(), LibnotifyError> {
331 let app_icon_c = std::ffi::CString::new(app_icon)?;
332 unsafe { notify_notification_set_app_icon(self.inner, app_icon_c.as_ptr()) };
333
334 Ok(())
335 }
336
337 /// Sets the application name for the notification.
338 ///
339 /// If this function is not called, the application name will be set from the value used in init() or overridden with set_app_name(). Usage is the same.
340 ///
341 /// Available since: 0.7.3
342 ///
343 /// # Errors
344 /// This function can return LibnotifyError::{NulError}
345 pub fn set_app_name(&mut self, app_name: &str) -> Result<(), LibnotifyError> {
346 let app_name_c = std::ffi::CString::new(app_name)?;
347 unsafe { notify_notification_set_app_name(self.inner, app_name_c.as_ptr()) };
348
349 Ok(())
350 }
351
352 /// Sets the category of this notification.
353 ///
354 /// This can be used by the notification server to filter or display the data in a certain way
355 ///
356 /// The only reference to what this even is, is [this thread](https://unix.stackexchange.com/questions/251243/what-do-a-notify-send-notification-category-hint-and-version-parameters-mean). If you know how to use this bad boy, have fun :D
357 ///
358 /// # Errors
359 /// This function can return LibnotifyError::{NulError}
360 pub fn set_category(&mut self, category: &str) -> Result<(), LibnotifyError> {
361 let category_c = std::ffi::CString::new(category)?;
362 unsafe { notify_notification_set_category(self.inner, category_c.as_ptr()) };
363
364 Ok(())
365 }
366
367 /// Sets a hint for key with value value
368 ///
369 /// Available since: 0.6
370 ///
371 ///
372 /// The only reference to what this even is, is [this thread](https://unix.stackexchange.com/questions/251243/what-do-a-notify-send-notification-category-hint-and-version-parameters-mean). If you know how to use this bad boy, have fun :D
373 ///
374 /// # Errors
375 /// This function can return LibnotifyError::{NulError, AllocationError}
376 pub fn set_hint(&mut self, key: &str, value: HintValue) -> Result<(), LibnotifyError> {
377 let key_c = std::ffi::CString::new(key)?;
378
379 let value_c = match value {
380 HintValue::String(s) => {
381 let s = std::ffi::CString::new(s)?;
382 unsafe { g_variant_new_string(s.as_ptr()) }
383 }
384
385 HintValue::Int32(i) => unsafe { g_variant_new_int32(i) },
386
387 HintValue::Boolean(b) => unsafe { g_variant_new_boolean(b as i32) },
388 };
389
390 if value_c.is_null() {
391 return Err(LibnotifyError::AllocationError);
392 }
393
394 unsafe { notify_notification_set_hint(self.inner, key_c.as_ptr(), value_c) };
395
396 Ok(())
397 }
398
399 /// Sets the timeout of the notification.
400 ///
401 /// Note that the timeout may be ignored by the server.
402 ///
403 /// # Example
404 /// ```
405 /// use libnotify_rs::api::{
406 /// functions::{init, uninit},
407 /// notification::{self, Notification},
408 /// };
409 ///
410 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
411 /// init("I love coffee")?;
412 /// let mut notify = Notification::new(
413 /// "Remember to drink coffe!",
414 /// "Coffee is very important for your mental health!",
415 /// None,
416 /// )?;
417 ///
418 /// notify.set_timeout(notification::Timeout::Custom(1000 * 10)); // 10 secs
419 /// Ok(())
420 /// }
421 /// ```
422 pub fn set_timeout(&mut self, timeout: Timeout) {
423 let timeout_c = match timeout {
424 Timeout::Default => NOTIFY_EXPIRES_DEFAULT,
425 Timeout::Never => NOTIFY_EXPIRES_NEVER as i32,
426 Timeout::Custom(i) => i,
427 };
428
429 unsafe { notify_notification_set_timeout(self.inner, timeout_c) }
430 }
431
432 /// Sets the urgency level of this notification
433 /// # Example
434 /// ```
435 /// use libnotify_rs::api::{
436 /// functions::{init, uninit},
437 /// notification::{self, Notification},
438 /// };
439 ///
440 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
441 /// init("I love coffee")?;
442 /// let mut notify = Notification::new(
443 /// "Remember to drink coffe!",
444 /// "Coffee is very important for your mental health!",
445 /// None,
446 /// )?;
447 ///
448 /// notify.set_urgency(notification::Urgency::Critical); // coffee is very important
449 /// Ok(())
450 /// }
451 /// ```
452 pub fn set_urgency(&mut self, urgency: Urgency) {
453 let urgency: u32 = match urgency {
454 Urgency::Low => NotifyUrgency_NOTIFY_URGENCY_LOW,
455 Urgency::Normal => NotifyUrgency_NOTIFY_URGENCY_NORMAL,
456 Urgency::Critical => NotifyUrgency_NOTIFY_URGENCY_CRITICAL,
457 };
458
459 unsafe { notify_notification_set_urgency(self.inner, urgency) }
460 }
461
462 /// Tells the notification server to display the notification on the screen
463 ///
464 /// # Errors
465 /// This function can return LibnotifyError::{GerrorError, AllocationError}
466 ///
467 /// # Example
468 /// ```no_run(No notification daemon on CI)
469 /// use libnotify_rs::api::{
470 /// functions::{init, uninit},
471 /// notification::{self, Notification},
472 /// };
473 ///
474 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
475 /// init("I love coffee")?;
476 /// let mut notify = Notification::new(
477 /// "Remember to drink coffe!",
478 /// "Coffee is very important for your mental health!",
479 /// None,
480 /// )?;
481 ///
482 /// notify.show()?;
483 ///
484 /// Ok(())
485 /// }
486 /// ```
487 pub fn show(&self) -> Result<(), LibnotifyError> {
488 let mut gerror = std::ptr::null_mut() as *mut GError;
489
490 if unsafe { notify_notification_show(self.inner, &mut gerror) } == 0 {
491 if gerror.is_null() {
492 return Err(LibnotifyError::AllocationError);
493 }
494
495 let err_msg = unsafe { *gerror }.message;
496
497 if err_msg.is_null() {
498 unsafe { g_error_free(gerror) };
499 return Err(LibnotifyError::AllocationError);
500 }
501
502 let err_msg_str = unsafe { std::ffi::CStr::from_ptr(err_msg) }
503 .to_string_lossy()
504 .to_string();
505
506 unsafe { g_error_free(gerror) };
507 return Err(LibnotifyError::GerrorError(err_msg_str));
508 }
509
510 Ok(())
511 }
512
513 /// Updates the notification text and icon.
514 ///
515 /// This won’t send the update out and display it on the screen. For that, you will need to call `show()`.
516 ///
517 /// # Errors
518 /// This function can return LibnotifyError::{NulError}
519 ///
520 /// # Example
521 /// ```
522 /// use libnotify_rs::api::{functions::init, notification::Notification};
523 ///
524 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
525 /// init("Test app")?;
526 /// let mut notification = Notification::new("My notification", "Yap yap", None)?;
527 /// notification.update("My new summary", "My new body", None)?;
528 ///
529 /// Ok(())
530 /// }
531 /// ```
532 pub fn update<'b, F, I>(
533 &mut self,
534 summary: &str,
535 body: F,
536 icon: I,
537 ) -> Result<(), LibnotifyError>
538 where
539 F: Into<Option<&'b str>>,
540 I: Into<Option<&'b str>>,
541 {
542 let summary = std::ffi::CString::new(summary)?;
543
544 let body = body.into().map(std::ffi::CString::new).transpose()?;
545
546 let icon = icon.into().map(std::ffi::CString::new).transpose()?;
547
548 unsafe {
549 notify_notification_update(
550 self.inner,
551 summary.as_ptr(),
552 body.as_ref().map_or(std::ptr::null(), |body| body.as_ptr()),
553 icon.as_ref().map_or(std::ptr::null(), |icon| icon.as_ptr()),
554 );
555 }
556
557 Ok(())
558 }
559}
560
561impl Drop for Notification {
562 fn drop(&mut self) {
563 if self.inner.is_null() {
564 return; // shouldn't happen, but check just in case
565 }
566
567 unsafe { g_object_unref(self.inner as *mut std::os::raw::c_void) };
568 }
569}
570
571/// This function translates c callback to rust. Used in `add_action`
572unsafe extern "C" fn action_trampoline<F>(
573 _notification: *mut NotifyNotification,
574 action: *mut ::std::os::raw::c_char,
575 user_data: gpointer,
576) where
577 F: Fn(&str) + 'static,
578{
579 let callback = unsafe { &*(user_data as *const F) };
580
581 if action.is_null() {
582 return;
583 }
584
585 let action = unsafe { std::ffi::CStr::from_ptr(action).to_string_lossy() };
586 callback(&action);
587}
588
589/// This function tells c how to free memory of our callback
590unsafe extern "C" fn drop_box<F>(data: gpointer) {
591 unsafe { drop(Box::from_raw(data as *mut F)) };
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn new_test() {
600 assert!(
601 !Notification::new("Test", "Test", "Test")
602 .unwrap()
603 .inner
604 .is_null()
605 );
606 assert!(
607 !Notification::new("Test", None, None)
608 .unwrap()
609 .inner
610 .is_null()
611 );
612 assert!(
613 !Notification::new("Test", None, "Test")
614 .unwrap()
615 .inner
616 .is_null()
617 );
618 assert!(
619 !Notification::new("Test", "Test", None)
620 .unwrap()
621 .inner
622 .is_null()
623 );
624 }
625
626 // I meant to write more tests, but I'm to lazy :3
627 // Doctests should suffice
628}