Skip to main content

ignis_notifications/
lib.rs

1use pyo3::prelude::*;
2
3/// `ignis_notifications` provides a notification daemon which receives and manages notifications sent by
4/// applications on GNU/Linux desktops that follow XDG Desktop Notifications Specification.
5///
6/// # Example
7/// ```python
8/// from ignis_notifications import NotificationService
9///
10/// SERVICE = None
11///
12/// async fn run(service: NotificationService) -> None:
13///     await service.run()
14///
15///
16/// fn setup_service() -> NotificationService:
17///     if SERVICE:
18///         return SERVICE
19///     else:
20///         service = NotificationService()
21///         # Run in the background
22///         asyncio.create_task(run())
23///         return service
24///
25/// service = setup_service()
26///
27/// # Access notifications
28/// print(service.notifications)
29/// ```
30#[pymodule]
31mod ignis_notifications {
32    use notifications::CloseReason as RCloseReason;
33    use notifications::NotificationService as RNotificationService;
34    use notifications::Settings as RSettings;
35    use notifications::Urgency as RUrgency;
36    use notifications::{ActionHandle, NotificationHandle};
37    use pyo3::{
38        exceptions::{PyIOError, PyKeyError, PyOSError, PyRuntimeError, PyValueError},
39        prelude::*,
40    };
41
42    use pyo3_async_runtimes;
43
44    fn to_py_err(e: notifications::Error) -> PyErr {
45        let msg = e.to_string();
46        match e {
47            notifications::Error::DBusError(_) => PyOSError::new_err(msg),
48            notifications::Error::NoConnection => PyOSError::new_err(msg),
49            notifications::Error::IOError(_) => PyIOError::new_err(msg),
50            notifications::Error::JSONError(_) => PyValueError::new_err(msg),
51            notifications::Error::NotificationNotFound(_) => PyKeyError::new_err(msg),
52            notifications::Error::ConnectionInitializedTwice => PyRuntimeError::new_err(msg),
53        }
54    }
55
56    /// The urgency level of the notification.
57    ///
58    /// Represents how important is the notification and may affect how it's displayed in the graphical
59    /// interface.
60    #[pyclass(from_py_object)]
61    #[derive(Clone, Copy, PartialEq, Eq)]
62    pub enum Urgency {
63        /// A low level of urgency.
64        ///
65        /// Notification does not require immediate user attention.
66        Low,
67
68        /// A normal level of urgency.
69        ///
70        /// For example, a notification about new message from a chat app.
71        Normal,
72
73        /// A critical level of urgency.
74        ///
75        /// The notification requires user attention and should stand out from the rest of
76        /// notifications.
77        Critical,
78    }
79
80    /// A reason why the notification is closed.
81    #[pyclass(from_py_object)]
82    #[derive(Clone, Copy, PartialEq, Eq)]
83    pub enum CloseReason {
84        /// Expired timeout. The notification was closed automatically upon expiration of the timeout.
85        Expired,
86
87        /// Dismissed by the user.
88        ///
89        /// Generated by a call to [`NotificationService.dismiss_notification`][]
90        /// and [`Notification.dismiss`][]
91        Dismissed,
92
93        /// The application requested to close the notification.
94        DBusCall,
95
96        /// Undefined/reserved reasons.
97        Other,
98    }
99
100    impl From<&RUrgency> for Urgency {
101        fn from(value: &RUrgency) -> Self {
102            match value {
103                RUrgency::Low => Urgency::Low,
104                RUrgency::Normal => Urgency::Normal,
105                RUrgency::Critical => Urgency::Critical,
106            }
107        }
108    }
109
110    impl From<&RCloseReason> for CloseReason {
111        fn from(value: &RCloseReason) -> Self {
112            match value {
113                RCloseReason::Expired => Self::Expired,
114                RCloseReason::Dismissed => Self::Dismissed,
115                RCloseReason::DBusCall => Self::DBusCall,
116                RCloseReason::Other => Self::Other,
117            }
118        }
119    }
120
121    /// A notification action.
122    ///
123    /// Notification actions are typically presented as buttons in UI that allow user to
124    /// interact with the application which sent the notification.
125    #[pyclass]
126    struct Action {
127        inner: ActionHandle,
128    }
129
130    #[pymethods]
131    impl Action {
132        /// The ID of the notification this action belongs to.
133        #[getter]
134        fn notification_id(&self) -> u32 {
135            self.inner.notification_id()
136        }
137
138        /// The localized string which should be displayed to the user.
139        #[getter]
140        fn label(&self) -> String {
141            self.inner.label()
142        }
143
144        /// The identifier of the action.
145        ///
146        /// `"default"` means that the action is default.
147        #[getter]
148        fn action_key(&self) -> String {
149            self.inner.action_key()
150        }
151
152        /// Invoke the action.
153        fn invoke<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
154            let inner = self.inner.clone();
155            pyo3_async_runtimes::tokio::future_into_py(py, async move {
156                inner.invoke().await.map_err(to_py_err)
157            })
158        }
159    }
160
161    /// A desktop notification.
162    #[pyclass]
163    struct Notification {
164        inner: NotificationHandle,
165    }
166
167    #[pymethods]
168    impl Notification {
169        /// The unique non-zero identifer of the notification.
170        #[getter]
171        fn id(&self) -> u32 {
172            self.inner.id()
173        }
174
175        /// The name of the application that sent the notification. Can be blank.
176        #[getter]
177        fn app_name(&self) -> String {
178            self.inner.app_name()
179        }
180
181        /// The optional icon of the notification.
182        ///
183        /// It is either file path or icon name.
184        #[getter]
185        fn icon(&self) -> Option<String> {
186            self.inner.icon()
187        }
188
189        /// The summary text briefly describing the notification.
190        #[getter]
191        fn summary(&self) -> String {
192            self.inner.summary()
193        }
194
195        /// The optional detailed body text. Can be empty.
196        #[getter]
197        fn body(&self) -> String {
198            self.inner.body()
199        }
200
201        /// List of actions for this notification. Can be empty.
202        #[getter]
203        fn actions(&self) -> Vec<Action> {
204            self.inner
205                .actions()
206                .into_iter()
207                .map(|inner| Action { inner })
208                .collect()
209        }
210
211        /// The urgency level of the notification.
212        #[getter]
213        fn urgency(&self) -> Urgency {
214            (&self.inner.urgency()).into()
215        }
216
217        /// The expire timeout of the notification.
218        #[getter]
219        fn timeout(&self) -> i32 {
220            self.inner.timeout()
221        }
222
223        /// Dismisses this notification.
224        fn dismiss<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
225            let inner = self.inner.clone();
226            pyo3_async_runtimes::tokio::future_into_py(py, async move {
227                inner.dismiss().await.map_err(to_py_err)
228            })
229        }
230
231        /// Invokes a callback when this notification is closed.
232        ///
233        /// The following arguments are passed to the callback:
234        /// reason ([`CloseReason`][]) - The reason why this notification was closed.
235        fn on_closed(&self, callback: Py<PyAny>) {
236            self.inner.on_closed(move |reason| {
237                Python::attach(|py| {
238                    if let Err(e) = callback.call1(py, (CloseReason::from(reason),)) {
239                        e.print(py)
240                    }
241                })
242            });
243        }
244    }
245
246    /// Settings for [`NotificationService`][].
247    ///
248    /// This struct provides methods to change behavior of some parts of the service.
249    #[pyclass]
250    struct Settings {
251        inner: RSettings,
252    }
253
254    #[pymethods]
255    impl Settings {
256        /// Returns whether to respect XDG Specification for timeout.
257        ///
258        /// If set to `false`, notifications never expire despite the value of [`Notification.timeout`][].
259        /// Otherwise, behavior is based on notification's timeout:
260        /// * `-1` - timeout value is taken from
261        /// [`default_timeout`][].
262        /// * `0` - the notification never expire
263        /// * `>=0` - this timeout is used to expire the notification
264        ///
265        /// Default: `True`.
266        #[getter]
267        fn follow_xdg_timeout(&self) -> bool {
268            self.inner.follow_xdg_timeout()
269        }
270
271        /// Returns the default timeout which is used when a notification doesn't specify timeout (-1).
272        ///
273        /// Has effect only if [`follow_xdg_timeout`][] and [`expire_by_default`][] are both `True`.
274        ///
275        /// Default: `3000`.
276        #[getter]
277        fn default_timeout(&self) -> u32 {
278            self.inner.default_timeout()
279        }
280
281        /// Returns whether to expire notifications if the timeout is not specified (when timeout is -1).
282        ///
283        /// If `true`, notifications expire after the timeout defined in [`default_timeout`][].
284        ///
285        /// Default: `false`.
286        #[getter]
287        fn expire_by_default(&self) -> bool {
288            self.inner.expire_by_default()
289        }
290
291        #[setter]
292        fn set_follow_xdg_timeout(&self, value: bool) {
293            self.inner.set_follow_xdg_timeout(value)
294        }
295
296        #[setter]
297        fn set_default_timeout(&self, value: u32) {
298            self.inner.set_default_timeout(value)
299        }
300
301        #[setter]
302        fn set_expire_by_default(&self, value: bool) {
303            self.inner.set_expire_by_default(value)
304        }
305    }
306
307    /// A notification daemon that follows XDG Desktop Notifications Specification.
308    #[pyclass]
309    struct NotificationService {
310        inner: RNotificationService,
311    }
312
313    #[pymethods]
314    impl NotificationService {
315        #[new]
316        fn new() -> PyResult<Self> {
317            Ok(Self {
318                inner: RNotificationService::new(None).map_err(to_py_err)?,
319            })
320        }
321
322        /// Creates a new instance of the service without any I/O operations.
323        ///
324        /// It doesn't load the notification history from file and doesn't save it consequently.
325        /// This method can not fail and is guaranteed to return the instance.
326        #[staticmethod]
327        fn new_in_memory() -> Self {
328            Self {
329                inner: RNotificationService::new_in_memory(),
330            }
331        }
332
333        /// Returns an instance of settings that affect behavior of the service.
334        fn settings(&self) -> Settings {
335            Settings {
336                inner: self.inner.settings(),
337            }
338        }
339
340        /// Runs the service.
341        ///
342        /// You have to call this this method in order to receive notifications and perform operations
343        /// on them, such as dismissing or invoking actions.
344        /// It creates D-Bus connection and registers D-Bus interface on the session bus.
345        /// Must be called only once.
346        fn run<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
347            let inner = self.inner.clone();
348
349            pyo3_async_runtimes::tokio::future_into_py(py, async move {
350                inner.run().await.map_err(to_py_err)
351            })
352        }
353
354        /// Dismiss a notification by its ID.
355        ///
356        /// The notification is removed from the history and application that sent the notification is notified through D-Bus.
357        fn dismiss_notification<'py>(
358            &self,
359            py: Python<'py>,
360            id: u32,
361        ) -> PyResult<Bound<'py, PyAny>> {
362            let inner = self.inner.clone();
363
364            pyo3_async_runtimes::tokio::future_into_py(py, async move {
365                inner.dismiss_notification(id).await.map_err(to_py_err)
366            })
367        }
368
369        /// Invokes an action by its action key and notification ID it belongs to.
370        fn invoke_action<'py>(
371            &self,
372            py: Python<'py>,
373            notification_id: u32,
374            action_key: String,
375        ) -> PyResult<Bound<'py, PyAny>> {
376            let inner = self.inner.clone();
377
378            pyo3_async_runtimes::tokio::future_into_py(py, async move {
379                inner
380                    .invoke_action(notification_id, &action_key)
381                    .await
382                    .map_err(to_py_err)
383            })
384        }
385
386        /// A list of notification handles.
387        #[getter]
388        fn notifications(&self) -> Vec<Notification> {
389            self.inner
390                .get_notifications()
391                .into_iter()
392                .map(|inner| Notification { inner })
393                .collect()
394        }
395
396        /// Returns a notification handle by notification ID.
397        fn get_notification_by_id(&self, id: u32) -> Option<Notification> {
398            Some(Notification {
399                inner: self.inner.get_notification_by_id(id)?,
400            })
401        }
402
403        /// Clears the notification history.
404        ///
405        /// It dismisses each notification and notifies applications.
406        ///
407        /// # Warning
408        /// It does **NOT** emit
409        /// [`on_notification_closed`][] event for each notification.
410        fn clear_notifications<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
411            let inner = self.inner.clone();
412
413            pyo3_async_runtimes::tokio::future_into_py(py, async move {
414                inner.clear_notifications().await.map_err(to_py_err)
415            })
416        }
417
418        /// Connect a callback to invoke when a new notification is received.    
419        ///
420        /// Callback arguments:
421        /// id (int) - The ID of the notification
422        /// notification ([`Notification`][]) - The notification object.
423        /// replace (bool) - whether this notification replaces an old one with the same ID.
424        fn on_notified(&self, callback: Py<PyAny>) {
425            self.inner.on_notified(move |(id, handle, replace)| {
426                Python::attach(|py| {
427                    if let Err(e) = callback.call1(
428                        py,
429                        (
430                            id,
431                            Notification {
432                                inner: handle.clone(),
433                            },
434                            replace,
435                        ),
436                    ) {
437                        e.print(py)
438                    }
439                });
440            });
441        }
442
443        /// Connect a callback to invoke when a notification is closed.
444        ///
445        /// Callback arguments:
446        /// id (int) - The ID of the notification
447        /// reason ([`CloseReason`][]) - The reason why this notification was
448        /// closed.
449        fn on_notification_closed(&self, callback: Py<PyAny>) {
450            self.inner.on_notification_closed(move |(id, reason)| {
451                Python::attach(|py| {
452                    if let Err(e) = callback.call1(py, (id, CloseReason::from(reason))) {
453                        e.print(py)
454                    }
455                })
456            });
457        }
458
459        /// Invokes a callback when value of [`notifications`][] changes.
460        ///
461        /// It includes arriving of new notifications, closing and clearing notifications.
462        fn on_notify_notifications(&self, callback: Py<PyAny>) {
463            self.inner.on_notify_notifications(move || {
464                Python::attach(|py| {
465                    if let Err(e) = callback.call0(py) {
466                        e.print(py)
467                    }
468                })
469            });
470        }
471
472        /// Invokes a callback when notifications are cleared by a call to [`clear_notifications`][].
473        fn on_notifications_cleared(&self, callback: Py<PyAny>) {
474            self.inner.on_notifications_cleared(move || {
475                Python::attach(|py| {
476                    if let Err(e) = callback.call0(py) {
477                        e.print(py)
478                    }
479                })
480            });
481        }
482    }
483}