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}