wayle_notification/
monitoring.rs1use 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 ¬if,
58 ¬ification_list,
59 &store,
60 &remove_expired,
61 ¬if_tx
62 );
63 handle_popup_added(
64 ¬if,
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 ¬ification_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}