1use crate::data::ServiceData;
2use crate::dbus::{DBusService, DBusServiceSignals};
3use crate::private_prelude::*;
4use ignis_events::Event;
5use std::sync::OnceLock;
6use zbus::Connection;
7use zbus::connection::Builder;
8use zbus::object_server::InterfaceRef;
9
10pub(crate) struct NotificationServiceInner {
11 pub(crate) data: ServiceData,
12 pub(crate) connection: OnceLock<Option<Connection>>,
13 pub(crate) cache_dir: Option<PathBuf>,
14 pub(crate) settings: Settings,
15 pub(crate) on_notified: Event<(u32, NotificationHandle, bool)>,
16 pub(crate) on_notification_closed: Event<(u32, CloseReason)>,
17 pub(crate) on_notify_notifications: Event<()>,
18 pub(crate) on_notifications_cleared: Event<()>,
19}
20
21#[derive(Clone)]
26pub struct NotificationService {
27 pub(crate) inner: Arc<NotificationServiceInner>,
28}
29
30impl NotificationService {
31 fn new_with_data(data: ServiceData, cache_dir: Option<PathBuf>) -> Self {
32 let on_notified = Event::<(u32, NotificationHandle, bool)>::new();
33 let on_notification_closed = Event::<(u32, CloseReason)>::new();
34 let on_notify_notifications = Event::<()>::new();
35 let on_notifications_cleared = Event::<()>::new();
36
37 let on_notify_notifications_clone = on_notify_notifications.clone();
38 on_notified.connect(move |_| on_notify_notifications_clone.emit(&()));
39
40 let on_notify_notifications_clone = on_notify_notifications.clone();
41 on_notification_closed.connect(move |_| on_notify_notifications_clone.emit(&()));
42
43 let on_notify_notifications_clone = on_notify_notifications.clone();
44 on_notifications_cleared.connect(move |_| on_notify_notifications_clone.emit(&()));
45
46 Self {
47 inner: Arc::new(NotificationServiceInner {
48 data,
49 connection: OnceLock::new(),
50 cache_dir,
51 settings: Settings::default(),
52 on_notified,
53 on_notification_closed,
54 on_notify_notifications,
55 on_notifications_cleared,
56 }),
57 }
58 }
59
60 pub fn new(cache_dir: Option<PathBuf>) -> Result<Self> {
67 Ok(Self::new_with_data(
68 ServiceData::new(cache_dir.clone())?,
69 cache_dir,
70 ))
71 }
72
73 pub fn new_in_memory() -> Self {
78 Self::new_with_data(ServiceData::new_in_memory(), None)
79 }
80
81 pub fn settings(&self) -> Settings {
83 self.inner.settings.clone()
84 }
85
86 pub async fn run(&self) -> Result<()> {
99 let service = DBusService::new(self.clone())?;
100
101 let connection = Builder::session()?
102 .name("org.freedesktop.Notifications")?
103 .serve_at("/org/freedesktop/Notifications", service)?
104 .build()
105 .await?;
106
107 self.inner
108 .connection
109 .set(Some(connection))
110 .map_err(|_| Error::ConnectionInitializedTwice)?;
111
112 Ok(())
113 }
114
115 pub(crate) fn get_connection(&self) -> Result<Connection> {
116 self.inner
117 .connection
118 .get()
119 .ok_or(Error::NoConnection)?
120 .to_owned()
121 .ok_or(Error::NoConnection)
122 }
123
124 pub(crate) async fn get_dbus_interface(&self) -> Result<InterfaceRef<DBusService>> {
125 Ok(self
126 .get_connection()?
127 .object_server()
128 .interface("/org/freedesktop/Notifications")
129 .await?)
130 }
131
132 pub async fn dismiss_notification(&self, id: u32) -> Result<()> {
143 self.get_dbus_interface()
144 .await?
145 .notification_closed(id, CloseReason::Dismissed.into())
146 .await?;
147
148 self.inner.data.remove_notification(id)?;
149
150 self.inner
151 .on_notification_closed
152 .emit(&(id, CloseReason::Dismissed));
153
154 Ok(())
155 }
156
157 pub async fn invoke_action(&self, notification_id: u32, action_key: &str) -> Result<()> {
162 self.get_dbus_interface()
163 .await?
164 .action_invoked(notification_id, action_key)
165 .await?;
166
167 Ok(())
168 }
169
170 pub fn get_notifications(&self) -> Vec<NotificationHandle> {
172 self.inner
173 .data
174 .get_notifications()
175 .values()
176 .map(|n| NotificationHandle {
177 inner: Arc::clone(n),
178 service: self.clone(),
179 })
180 .collect()
181 }
182
183 pub fn get_notification_by_id(&self, id: u32) -> Option<NotificationHandle> {
185 self.inner
186 .data
187 .get_notifications()
188 .get(&id)
189 .map(|n| NotificationHandle {
190 inner: n.clone(),
191 service: self.clone(),
192 })
193 }
194
195 pub async fn clear_notifications(&self) -> Result<()> {
202 for id in self.inner.data.get_notifications().keys() {
203 self.get_dbus_interface()
204 .await?
205 .notification_closed(id.to_owned(), CloseReason::Dismissed.into())
206 .await?;
207 }
208
209 let res = self.inner.data.clear();
210 self.inner.on_notifications_cleared.emit(&());
211 res
212 }
213
214 pub fn on_notified<F>(&self, callback: F) -> usize
221 where
222 F: Fn(&(u32, NotificationHandle, bool)) + Send + Sync + 'static,
223 {
224 self.inner.on_notified.connect(callback)
225 }
226
227 pub fn on_notification_closed<F>(&self, callback: F) -> usize
233 where
234 F: Fn(&(u32, CloseReason)) + Send + Sync + 'static,
235 {
236 self.inner.on_notification_closed.connect(callback)
237 }
238
239 pub fn on_notify_notifications<F>(&self, callback: F) -> usize
243 where
244 F: Fn() + Send + Sync + 'static,
245 {
246 self.inner
247 .on_notify_notifications
248 .connect(move |_| callback())
249 }
250
251 pub fn on_notifications_cleared<F>(&self, callback: F) -> usize
253 where
254 F: Fn() + Send + Sync + 'static,
255 {
256 self.inner
257 .on_notifications_cleared
258 .connect(move |_| callback())
259 }
260}
261
262#[cfg(test)]
265mod tests {
266
267 use super::*;
268 use std::{collections::HashMap, time::Duration};
269
270 use crate::CloseReason;
271 use crate::Urgency;
272
273 use fake::Fake;
274 use fake::faker::lorem::en::Sentence;
275 use notify_rust::Urgency as ClientUrgency;
276 use notify_rust::{
277 CloseReason as ClientCloseReason, Notification, NotificationHandle, NotificationResponse,
278 };
279 use rand::seq::IndexedRandom;
280 use tempfile::TempDir;
281 use tokio::sync::oneshot;
282
283 impl From<Urgency> for ClientUrgency {
284 fn from(value: Urgency) -> Self {
285 match value {
286 Urgency::Low => ClientUrgency::Low,
287 Urgency::Normal => ClientUrgency::Normal,
288 Urgency::Critical => ClientUrgency::Critical,
289 }
290 }
291 }
292
293 struct TestContext {
294 _temp_dir: TempDir,
295 service: NotificationService,
296 }
297
298 fn no_tmp_cleanup() -> bool {
299 std::env::var_os("NO_TMP_CLEANUP")
300 .map(|v| v == "1")
301 .unwrap_or(false)
302 }
303
304 fn create_random_notification() -> Notification {
305 let summary: String = Sentence(3..6).fake();
306 let body: String = Sentence(6..12).fake();
307 let app_name: String = Sentence(1..3).fake();
308 let icon: String = String::from("cat-sleeping-symbolic");
309
310 let mut notification = Notification::new();
311
312 notification
313 .appname(&app_name)
314 .summary(&summary)
315 .body(&body)
316 .icon(&icon);
317
318 notification
319 }
320
321 async fn send_multiple_random_notifications(quantity: u32) -> HashMap<u32, NotificationHandle> {
322 let mut map: HashMap<u32, NotificationHandle> = HashMap::new();
323
324 for _ in 0..quantity {
325 let handle = create_random_notification().show_async().await.unwrap();
326 let id = handle.id();
327 map.insert(id, handle);
328 }
329
330 map
331 }
332
333 async fn setup_with_details(temp_dir: Option<TempDir>) -> TestContext {
334 let mut temp_dir = temp_dir.unwrap_or_else(|| TempDir::new().unwrap());
335
336 if no_tmp_cleanup() {
337 temp_dir.disable_cleanup(true);
338 }
339
340 let service = NotificationService::new(Some(temp_dir.path().to_path_buf())).unwrap();
341 service.run().await.unwrap();
342
343 TestContext {
344 _temp_dir: temp_dir,
345 service,
346 }
347 }
348
349 async fn setup() -> TestContext {
350 setup_with_details(None).await
351 }
352
353 #[tokio::test]
354 async fn test_single_notification() {
355 let ctx = setup().await;
356
357 let client_urgency_levels = [
358 ClientUrgency::Low,
359 ClientUrgency::Normal,
360 ClientUrgency::Critical,
361 ];
362
363 let client_urgency: ClientUrgency = client_urgency_levels
364 .choose(&mut rand::rng())
365 .unwrap()
366 .to_owned();
367
368 let handle = create_random_notification()
369 .urgency(client_urgency)
370 .show_async()
371 .await
372 .unwrap();
373
374 let notification = ctx.service.get_notification_by_id(handle.id()).unwrap();
375
376 assert_eq!(handle.appname, notification.app_name());
377 assert_eq!(handle.icon, notification.icon().unwrap());
378 assert_eq!(handle.summary, notification.summary());
379 assert_eq!(handle.body, notification.body());
380 assert_eq!(client_urgency, notification.urgency().into());
381 assert_eq!(i32::from(handle.timeout), notification.timeout());
382 }
383
384 #[tokio::test]
385 async fn test_multiple_notifications() {
386 let ctx = setup().await;
387 send_multiple_random_notifications(50).await;
388
389 assert!(ctx.service.get_notifications().is_sorted_by_key(|x| x.id()));
390 assert_eq!(ctx.service.get_notifications().len(), 50);
391 }
392
393 #[tokio::test]
394 async fn test_dismiss_notification() {
395 let ctx = setup().await;
396 let handle = create_random_notification().show_async().await.unwrap();
397 let id = handle.id();
398
399 let (tx, rx) = oneshot::channel();
400
401 tokio::spawn(async move {
402 handle
403 .wait_for_action_async(|response| {
404 match response {
405 NotificationResponse::Closed(reason) => tx.send(reason.to_owned()).unwrap(),
406 _ => unimplemented!(),
407 };
408 })
409 .await;
410 });
411 tokio::time::sleep(Duration::from_millis(500)).await;
415
416 ctx.service.dismiss_notification(id).await.unwrap();
417
418 let close_reason = rx.await.unwrap();
419 assert_eq!(close_reason, ClientCloseReason::Dismissed);
420 }
421
422 #[tokio::test]
423 async fn test_invoke_action() {
424 let ctx = setup().await;
425 let handle = Notification::new()
426 .summary("i am waiting")
427 .action("default", "default")
428 .action("asked", "no one asked")
429 .show_async()
430 .await
431 .unwrap();
432 let id = handle.id();
433
434 let (tx, rx) = oneshot::channel();
435
436 tokio::spawn(async move {
437 handle
438 .wait_for_action_async(|response| {
439 match response {
440 NotificationResponse::Action(action) => {
441 tx.send(action.clone()).unwrap();
442 }
443 _ => unimplemented!(),
444 };
445 })
446 .await;
447 });
448
449 tokio::time::sleep(Duration::from_millis(500)).await;
451
452 let n = ctx.service.get_notification_by_id(id).unwrap();
453 assert_eq!(n.actions().len(), 2);
454
455 for action in n.actions() {
456 if action.action_key() == "asked" {
457 action.invoke().await.unwrap();
458 }
459 }
460
461 let action_key = rx.await.unwrap();
462 assert_eq!(action_key, "asked")
463 }
464
465 #[tokio::test]
466 async fn test_clear_notifications() {
467 let ctx = setup().await;
468
469 send_multiple_random_notifications(10).await;
470
471 assert_eq!(ctx.service.get_notifications().len(), 10);
472 ctx.service.clear_notifications().await.unwrap();
473
474 assert_eq!(ctx.service.get_notifications().len(), 0);
475 }
476
477 async fn check_timeout(ms: i32) {
512 let ctx = setup().await;
513 ctx.service.settings().set_expire_by_default(true);
514
515 create_random_notification()
516 .timeout(ms)
517 .show_async()
518 .await
519 .unwrap();
520
521 ctx.service.on_notification_closed(|(_, reason)| {
522 assert_eq!(reason, &CloseReason::Expired);
523 });
524
525 tokio::time::sleep(Duration::from_secs(2)).await;
526 }
527
528 #[tokio::test]
529 async fn test_default_timeout() {
530 check_timeout(-1).await;
531 }
532
533 #[tokio::test]
534 async fn test_requested_timeout() {
535 check_timeout(1000).await;
536 }
537
538 #[tokio::test]
539 async fn test_on_closed() {
540 let ctx = setup().await;
541
542 let id = create_random_notification()
543 .show_async()
544 .await
545 .unwrap()
546 .id();
547
548 let n = ctx.service.get_notification_by_id(id).unwrap();
549 n.on_closed(|reason| assert_eq!(reason, &CloseReason::Dismissed));
550
551 n.dismiss().await.unwrap();
552
553 tokio::time::sleep(Duration::from_secs(2)).await;
554 }
555
556 #[tokio::test]
557 async fn test_on_notify_notifications() {
558 let ctx = setup().await;
559
560 let dismissed_flag = Arc::new(std::sync::Mutex::new(false));
561 let notified_flag = Arc::new(std::sync::Mutex::new(false));
562 let clear_flag = Arc::new(std::sync::Mutex::new(false));
563
564 let dismissed_flag_clone = dismissed_flag.clone();
565 let notified_flag_clone = notified_flag.clone();
566 let clear_flag_clone = clear_flag.clone();
567
568 ctx.service
569 .on_notify_notifications(move || *notified_flag_clone.lock().unwrap() = true);
570
571 let id = create_random_notification()
572 .show_async()
573 .await
574 .unwrap()
575 .id();
576
577 tokio::time::sleep(Duration::from_secs(1)).await;
578 assert_eq!(*notified_flag.lock().unwrap(), true);
579
580 ctx.service
581 .on_notify_notifications(move || *dismissed_flag_clone.lock().unwrap() = true);
582
583 let n = ctx.service.get_notification_by_id(id).unwrap();
584 n.dismiss().await.unwrap();
585
586 tokio::time::sleep(Duration::from_secs(1)).await;
587 assert_eq!(*dismissed_flag.lock().unwrap(), true);
588
589 ctx.service
590 .on_notify_notifications(move || *clear_flag_clone.lock().unwrap() = true);
591
592 ctx.service.clear_notifications().await.unwrap();
593
594 tokio::time::sleep(Duration::from_secs(1)).await;
595 assert_eq!(*clear_flag.lock().unwrap(), true);
596 }
597}