Skip to main content

wayle_notification/
builder.rs

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/// Builder for configuring and creating a NotificationService instance.
29///
30/// Allows customization of popup duration, do-not-disturb mode, and
31/// automatic removal of expired notifications.
32#[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    /// Creates a new NotificationServiceBuilder with default values.
55    pub fn new() -> Self {
56        Self::default()
57    }
58    /// Sets the duration in milliseconds for how long popups should be displayed.
59    pub fn popup_duration(self, duration: u32) -> Self {
60        self.popup_duration.set(duration);
61        self
62    }
63
64    /// Configures the Do Not Disturb mode.
65    ///
66    /// When enabled, new notifications won't appear as popups but will still
67    /// be added to the notification list.
68    pub fn dnd(self, dnd: bool) -> Self {
69        self.dnd.set(dnd);
70        self
71    }
72
73    /// Sets whether to automatically remove expired notifications.
74    pub fn remove_expired(self, remove: bool) -> Self {
75        self.remove_expired.set(remove);
76        self
77    }
78
79    /// Sets glob patterns for blocking notifications by app name.
80    ///
81    /// Notifications from matching apps are silently dropped.
82    /// Patterns support `*` and `?` wildcards.
83    pub fn blocklist(self, patterns: Property<Vec<String>>) -> Self {
84        Self {
85            blocklist: patterns,
86            ..self
87        }
88    }
89
90    /// Enables the Wayle D-Bus daemon for CLI control.
91    ///
92    /// When enabled, the service registers at `com.wayle.Notifications1`,
93    /// allowing CLI tools to control notifications (dismiss, toggle DND, etc.).
94    pub fn with_daemon(mut self) -> Self {
95        self.register_wayle_daemon = true;
96        self
97    }
98
99    /// Builds and initializes the NotificationService.
100    ///
101    /// Establishes a D-Bus connection, registers the notification daemon,
102    /// restores persisted notifications, and starts monitoring for events.
103    ///
104    /// # Errors
105    /// Returns error if D-Bus connection fails, service registration fails,
106    /// or monitoring cannot be started.
107    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, &notif_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}