1use std::{
2 collections::HashMap,
3 sync::{Arc, Mutex, atomic::AtomicU32},
4};
5
6use chrono::{DateTime, Utc};
7use tokio::sync::broadcast;
8use tokio_util::sync::CancellationToken;
9use tracing::{error, info};
10use wayle_core::Property;
11use wayle_traits::ServiceMonitoring;
12use zbus::{Connection, object_server::Interface};
13
14use crate::{
15 core::{notification::Notification, types::NotificationProps},
16 daemon::NotificationDaemon,
17 error::Error,
18 events::NotificationEvent,
19 persistence::{NotificationStore, StoredNotification},
20 popup_timer::PopupTimerManager,
21 service::NotificationService,
22 types::dbus::{SERVICE_NAME, SERVICE_PATH, WAYLE_SERVICE_NAME, WAYLE_SERVICE_PATH},
23 wayle_daemon::WayleDaemon,
24};
25
26const EVENT_CHANNEL_CAPACITY: usize = 10_000;
27
28#[derive(Debug)]
33pub struct NotificationServiceBuilder {
34 popup_duration: Property<u32>,
35 dnd: Property<bool>,
36 remove_expired: Property<bool>,
37 blocklist: Property<Vec<String>>,
38 register_wayle_daemon: bool,
39}
40
41impl Default for NotificationServiceBuilder {
42 fn default() -> Self {
43 Self {
44 popup_duration: Property::new(5000),
45 dnd: Property::new(false),
46 remove_expired: Property::new(true),
47 blocklist: Property::new(vec![]),
48 register_wayle_daemon: false,
49 }
50 }
51}
52
53impl NotificationServiceBuilder {
54 pub fn new() -> Self {
56 Self::default()
57 }
58 pub fn popup_duration(self, duration: u32) -> Self {
60 self.popup_duration.set(duration);
61 self
62 }
63
64 pub fn dnd(self, dnd: bool) -> Self {
69 self.dnd.set(dnd);
70 self
71 }
72
73 pub fn remove_expired(self, remove: bool) -> Self {
75 self.remove_expired.set(remove);
76 self
77 }
78
79 pub fn blocklist(self, patterns: Property<Vec<String>>) -> Self {
84 Self {
85 blocklist: patterns,
86 ..self
87 }
88 }
89
90 pub fn with_daemon(mut self) -> Self {
95 self.register_wayle_daemon = true;
96 self
97 }
98
99 pub async fn build(self) -> Result<Arc<NotificationService>, Error> {
108 let connection = Connection::session().await.map_err(|err| {
109 Error::ServiceInitializationFailed(format!("D-Bus connection failed: {err}"))
110 })?;
111 let (notif_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
112 let cancellation_token = CancellationToken::new();
113
114 let store = init_store();
115 let stored_notifications =
116 load_stored_notifications(&store, self.remove_expired.get(), &connection, ¬if_tx);
117 let max_id = stored_notifications
118 .iter()
119 .map(|notif| notif.id)
120 .max()
121 .unwrap_or(0);
122
123 let mut initial_owners = HashMap::new();
124 for notification in &stored_notifications {
125 if let Some(app_name) = notification.app_name.get() {
126 initial_owners.insert(notification.id, app_name);
127 }
128 }
129
130 let freedesktop_daemon = NotificationDaemon {
131 counter: AtomicU32::new(max_id + 1),
132 zbus_connection: connection.clone(),
133 notif_tx: notif_tx.clone(),
134 blocklist: self.blocklist.clone(),
135 id_owners: Mutex::new(initial_owners),
136 };
137
138 register_dbus_object(&connection, SERVICE_PATH, freedesktop_daemon).await?;
139 register_dbus_name(&connection, SERVICE_NAME).await?;
140 info!("Notification daemon registered at {SERVICE_NAME}");
141
142 let popups = Property::new(vec![]);
143 let popup_timers = Arc::new(PopupTimerManager::new(popups.clone()));
144
145 let service = Arc::new(NotificationService {
146 cancellation_token,
147 notif_tx,
148 store,
149 connection: connection.clone(),
150 notifications: Property::new(stored_notifications),
151 popups,
152 popup_duration: self.popup_duration,
153 dnd: self.dnd,
154 remove_expired: self.remove_expired,
155 blocklist: self.blocklist,
156 popup_timers,
157 });
158
159 service.start_monitoring().await?;
160
161 if self.register_wayle_daemon {
162 let wayle_daemon = WayleDaemon {
163 service: Arc::clone(&service),
164 };
165 register_dbus_object(&connection, WAYLE_SERVICE_PATH, wayle_daemon).await?;
166 register_dbus_name(&connection, WAYLE_SERVICE_NAME).await?;
167 info!("Wayle notification extensions registered at {WAYLE_SERVICE_NAME}");
168 }
169
170 Ok(service)
171 }
172}
173
174fn init_store() -> Option<NotificationStore> {
175 match NotificationStore::new() {
176 Ok(store) => {
177 info!("Notification persistence enabled");
178 Some(store)
179 }
180 Err(e) => {
181 error!(error = %e, "cannot initialize notification store");
182 error!("notifications will not persist across restarts");
183 None
184 }
185 }
186}
187
188fn load_stored_notifications(
189 store: &Option<NotificationStore>,
190 remove_expired: bool,
191 connection: &Connection,
192 notif_tx: &broadcast::Sender<NotificationEvent>,
193) -> Vec<Arc<Notification>> {
194 store
195 .as_ref()
196 .and_then(|store| store.load_all(remove_expired).ok())
197 .map(|stored| {
198 stored
199 .into_iter()
200 .map(|notification| {
201 stored_to_notification(notification, connection.clone(), notif_tx.clone())
202 })
203 .collect()
204 })
205 .unwrap_or_default()
206}
207
208fn stored_to_notification(
209 stored: StoredNotification,
210 connection: Connection,
211 notif_tx: broadcast::Sender<NotificationEvent>,
212) -> Arc<Notification> {
213 Arc::new(Notification::new(
214 NotificationProps {
215 id: stored.id,
216 app_name: stored.app_name.unwrap_or_default(),
217 replaces_id: stored.replaces_id.unwrap_or(0),
218 app_icon: stored.app_icon.unwrap_or_default(),
219 summary: stored.summary,
220 body: stored.body.unwrap_or_default(),
221 actions: stored.actions,
222 hints: stored.hints,
223 expire_timeout: stored.expire_timeout.unwrap_or(0) as i32,
224 timestamp: DateTime::<Utc>::from_timestamp_millis(stored.timestamp)
225 .unwrap_or_else(Utc::now),
226 },
227 connection,
228 notif_tx,
229 ))
230}
231
232async fn register_dbus_object<T: Interface>(
233 connection: &Connection,
234 path: &str,
235 object: T,
236) -> Result<(), Error> {
237 connection
238 .object_server()
239 .at(path, object)
240 .await
241 .map_err(|err| {
242 Error::ServiceInitializationFailed(format!(
243 "cannot register D-Bus object at '{path}': {err}"
244 ))
245 })?;
246 Ok(())
247}
248
249async fn register_dbus_name(connection: &Connection, name: &str) -> Result<(), Error> {
250 connection.request_name(name).await.map_err(|err| {
251 Error::ServiceInitializationFailed(format!("cannot acquire D-Bus name '{name}': {err}"))
252 })
253}