Skip to main content

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}