Skip to main content

wayle_notification/
monitoring.rs

1use std::{sync::Arc, time::Duration};
2
3use chrono::Utc;
4use tokio::sync::broadcast;
5use tracing::{debug, info, instrument, warn};
6use wayle_core::Property;
7use wayle_traits::ServiceMonitoring;
8use zbus::Connection;
9
10use crate::{
11    core::notification::Notification,
12    error::Error,
13    events::NotificationEvent,
14    persistence::NotificationStore,
15    popup_timer::PopupTimerManager,
16    service::NotificationService,
17    types::{
18        ClosedReason, Signal,
19        dbus::{SERVICE_INTERFACE, SERVICE_PATH},
20    },
21};
22
23impl ServiceMonitoring for NotificationService {
24    type Error = Error;
25    #[instrument(skip_all, err)]
26    async fn start_monitoring(&self) -> Result<(), Self::Error> {
27        handle_notifications(self).await?;
28        Ok(())
29    }
30}
31
32#[instrument(skip_all)]
33async fn handle_notifications(service: &NotificationService) -> Result<(), Error> {
34    let mut event_receiver = service.notif_tx.subscribe();
35    let notification_list = service.notifications.clone();
36    let popup_list = service.popups.clone();
37    let popup_dur = service.popup_duration.clone();
38    let dnd = service.dnd.clone();
39    let store = service.store.clone();
40    let cancellation_token = service.cancellation_token.clone();
41    let remove_expired = service.remove_expired.clone();
42    let connection = service.connection.clone();
43    let notif_tx = service.notif_tx.clone();
44    let popup_timers = service.popup_timers.clone();
45
46    tokio::spawn(async move {
47        loop {
48            tokio::select! {
49                _ = cancellation_token.cancelled() => {
50                    info!("Notification monitoring cancelled, stopping");
51                    return;
52                }
53                Ok(event) = event_receiver.recv() => {
54                    match event {
55                        NotificationEvent::Add(notif) => {
56                            handle_notification_added(
57                                &notif,
58                                &notification_list,
59                                &store,
60                                &remove_expired,
61                                &notif_tx
62                            );
63                            handle_popup_added(
64                                &notif,
65                                &popup_list,
66                                &popup_dur,
67                                dnd.clone(),
68                                &popup_timers,
69                            );
70                        }
71                        NotificationEvent::Remove(id, reason) => {
72                            handle_notification_removed(
73                                id,
74                                reason,
75                                &notification_list,
76                                &popup_list,
77                                &store,
78                                &connection,
79                                &popup_timers,
80                            ).await;
81                        }
82                    }
83                }
84            }
85        }
86    });
87
88    Ok(())
89}
90
91fn handle_popup_added(
92    incoming_popup: &Notification,
93    popups: &Property<Vec<Arc<Notification>>>,
94    popup_duration: &Property<u32>,
95    dnd: Property<bool>,
96    popup_timers: &Arc<PopupTimerManager>,
97) {
98    if dnd.get() {
99        return;
100    }
101
102    let incoming_popup = Arc::new(incoming_popup.clone());
103    let mut list = popups.get();
104    list.retain(|popup| popup != &incoming_popup);
105    list.insert(0, incoming_popup.clone());
106    popups.replace(list);
107
108    let default_duration = Duration::from_millis(popup_duration.get() as u64);
109
110    match incoming_popup.expire_timeout.get() {
111        Some(0) => {}
112        Some(ttl) => {
113            let expire = Duration::from_millis(ttl as u64);
114            popup_timers.start(incoming_popup.id, default_duration.min(expire));
115        }
116        None => {
117            popup_timers.start(incoming_popup.id, default_duration);
118        }
119    }
120}
121
122fn handle_notification_added(
123    incoming_notif: &Notification,
124    notifications: &Property<Vec<Arc<Notification>>>,
125    store: &Option<NotificationStore>,
126    remove_expired: &Property<bool>,
127    notif_tx: &broadcast::Sender<NotificationEvent>,
128) {
129    if incoming_notif.is_transient.get() {
130        return;
131    }
132
133    let notif_arc = Arc::new(incoming_notif.clone());
134    let mut list = notifications.get();
135
136    let replaced = list
137        .iter()
138        .find(|notif| notif.id == notif_arc.id)
139        .map(|notif| (notif.id, notif.app_name.get()));
140    if let Some((replaced_id, replaced_app)) = &replaced {
141        debug!(
142            incoming_id = notif_arc.id,
143            incoming_app = ?notif_arc.app_name.get(),
144            replaced_id,
145            replaced_app = ?replaced_app,
146            "replacing existing notification"
147        );
148    } else {
149        debug!(
150            id = notif_arc.id,
151            app = ?notif_arc.app_name.get(),
152            summary = %notif_arc.summary.get(),
153            list_size = list.len(),
154            "adding new notification"
155        );
156    }
157
158    list.retain(|notif| notif.id != notif_arc.id);
159    list.insert(0, notif_arc.clone());
160
161    notifications.replace(list);
162
163    if let Some(store) = store.as_ref() {
164        let _ = store.add(incoming_notif);
165    };
166
167    if !remove_expired.get() {
168        return;
169    }
170
171    let Some(ttl) = notif_arc.expire_timeout.get() else {
172        return;
173    };
174
175    let expiration_time = notif_arc.timestamp.get() + Duration::from_millis(ttl as u64);
176    let now = Utc::now();
177
178    if expiration_time <= now {
179        let mut list = notifications.get();
180        list.retain(|notif| notif.id != notif_arc.id);
181        notifications.set(list);
182        return;
183    }
184
185    let time_until_expiration = (expiration_time - now).to_std().unwrap_or(Duration::ZERO);
186    let id = notif_arc.id;
187    let tx = notif_tx.clone();
188
189    tokio::spawn(async move {
190        tokio::time::sleep(time_until_expiration).await;
191        let _ = tx.send(NotificationEvent::Remove(id, ClosedReason::Expired));
192    });
193}
194
195async fn handle_notification_removed(
196    id: u32,
197    reason: ClosedReason,
198    notifications: &Property<Vec<Arc<Notification>>>,
199    popups: &Property<Vec<Arc<Notification>>>,
200    store: &Option<NotificationStore>,
201    connection: &Connection,
202    popup_timers: &Arc<PopupTimerManager>,
203) {
204    if !matches!(reason, ClosedReason::Expired) {
205        popup_timers.cancel(id);
206
207        let mut popup_list = popups.get();
208        popup_list.retain(|popup| popup.id != id);
209        popups.set(popup_list);
210    }
211
212    let mut notif_list = notifications.get();
213    let prev_len = notif_list.len();
214    notif_list.retain(|notif| notif.id != id);
215
216    if notif_list.len() == prev_len {
217        return;
218    }
219
220    notifications.set(notif_list);
221
222    if let Some(store) = store.as_ref() {
223        let _ = store.remove(id);
224    };
225
226    debug!(id = id, ?reason, "emitting NotificationClosed");
227    if let Err(err) = connection
228        .emit_signal(
229            None::<()>,
230            SERVICE_PATH,
231            SERVICE_INTERFACE,
232            Signal::NotificationClosed.as_str(),
233            &(id, reason as u32),
234        )
235        .await
236    {
237        warn!(id = id, error = %err, "cannot emit NotificationClosed signal");
238    }
239}