use std::{
collections::HashMap,
sync::{Arc, Mutex, atomic::AtomicU32},
};
use chrono::{DateTime, Utc};
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use tracing::{error, info};
use wayle_core::Property;
use wayle_traits::ServiceMonitoring;
use zbus::{Connection, object_server::Interface};
use crate::{
core::{notification::Notification, types::NotificationProps},
daemon::NotificationDaemon,
error::Error,
events::NotificationEvent,
persistence::{NotificationStore, StoredNotification},
popup_timer::PopupTimerManager,
service::NotificationService,
types::dbus::{SERVICE_NAME, SERVICE_PATH, WAYLE_SERVICE_NAME, WAYLE_SERVICE_PATH},
wayle_daemon::WayleDaemon,
};
const EVENT_CHANNEL_CAPACITY: usize = 10_000;
#[derive(Debug)]
pub struct NotificationServiceBuilder {
popup_duration: Property<u32>,
dnd: Property<bool>,
remove_expired: Property<bool>,
blocklist: Property<Vec<String>>,
register_wayle_daemon: bool,
}
impl Default for NotificationServiceBuilder {
fn default() -> Self {
Self {
popup_duration: Property::new(5000),
dnd: Property::new(false),
remove_expired: Property::new(true),
blocklist: Property::new(vec![]),
register_wayle_daemon: false,
}
}
}
impl NotificationServiceBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn popup_duration(self, duration: u32) -> Self {
self.popup_duration.set(duration);
self
}
pub fn dnd(self, dnd: bool) -> Self {
self.dnd.set(dnd);
self
}
pub fn remove_expired(self, remove: bool) -> Self {
self.remove_expired.set(remove);
self
}
pub fn blocklist(self, patterns: Property<Vec<String>>) -> Self {
Self {
blocklist: patterns,
..self
}
}
pub fn with_daemon(mut self) -> Self {
self.register_wayle_daemon = true;
self
}
pub async fn build(self) -> Result<Arc<NotificationService>, Error> {
let connection = Connection::session().await.map_err(|err| {
Error::ServiceInitializationFailed(format!("D-Bus connection failed: {err}"))
})?;
let (notif_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
let cancellation_token = CancellationToken::new();
let store = init_store();
let stored_notifications =
load_stored_notifications(&store, self.remove_expired.get(), &connection, ¬if_tx);
let max_id = stored_notifications
.iter()
.map(|notif| notif.id)
.max()
.unwrap_or(0);
let mut initial_owners = HashMap::new();
for notification in &stored_notifications {
if let Some(app_name) = notification.app_name.get() {
initial_owners.insert(notification.id, app_name);
}
}
let freedesktop_daemon = NotificationDaemon {
counter: AtomicU32::new(max_id + 1),
zbus_connection: connection.clone(),
notif_tx: notif_tx.clone(),
blocklist: self.blocklist.clone(),
id_owners: Mutex::new(initial_owners),
};
register_dbus_object(&connection, SERVICE_PATH, freedesktop_daemon).await?;
register_dbus_name(&connection, SERVICE_NAME).await?;
info!("Notification daemon registered at {SERVICE_NAME}");
let popups = Property::new(vec![]);
let popup_timers = Arc::new(PopupTimerManager::new(popups.clone()));
let service = Arc::new(NotificationService {
cancellation_token,
notif_tx,
store,
connection: connection.clone(),
notifications: Property::new(stored_notifications),
popups,
popup_duration: self.popup_duration,
dnd: self.dnd,
remove_expired: self.remove_expired,
blocklist: self.blocklist,
popup_timers,
});
service.start_monitoring().await?;
if self.register_wayle_daemon {
let wayle_daemon = WayleDaemon {
service: Arc::clone(&service),
};
register_dbus_object(&connection, WAYLE_SERVICE_PATH, wayle_daemon).await?;
register_dbus_name(&connection, WAYLE_SERVICE_NAME).await?;
info!("Wayle notification extensions registered at {WAYLE_SERVICE_NAME}");
}
Ok(service)
}
}
fn init_store() -> Option<NotificationStore> {
match NotificationStore::new() {
Ok(store) => {
info!("Notification persistence enabled");
Some(store)
}
Err(e) => {
error!(error = %e, "cannot initialize notification store");
error!("notifications will not persist across restarts");
None
}
}
}
fn load_stored_notifications(
store: &Option<NotificationStore>,
remove_expired: bool,
connection: &Connection,
notif_tx: &broadcast::Sender<NotificationEvent>,
) -> Vec<Arc<Notification>> {
store
.as_ref()
.and_then(|store| store.load_all(remove_expired).ok())
.map(|stored| {
stored
.into_iter()
.map(|notification| {
stored_to_notification(notification, connection.clone(), notif_tx.clone())
})
.collect()
})
.unwrap_or_default()
}
fn stored_to_notification(
stored: StoredNotification,
connection: Connection,
notif_tx: broadcast::Sender<NotificationEvent>,
) -> Arc<Notification> {
Arc::new(Notification::new(
NotificationProps {
id: stored.id,
app_name: stored.app_name.unwrap_or_default(),
replaces_id: stored.replaces_id.unwrap_or(0),
app_icon: stored.app_icon.unwrap_or_default(),
summary: stored.summary,
body: stored.body.unwrap_or_default(),
actions: stored.actions,
hints: stored.hints,
expire_timeout: stored.expire_timeout.unwrap_or(0) as i32,
timestamp: DateTime::<Utc>::from_timestamp_millis(stored.timestamp)
.unwrap_or_else(Utc::now),
},
connection,
notif_tx,
))
}
async fn register_dbus_object<T: Interface>(
connection: &Connection,
path: &str,
object: T,
) -> Result<(), Error> {
connection
.object_server()
.at(path, object)
.await
.map_err(|err| {
Error::ServiceInitializationFailed(format!(
"cannot register D-Bus object at '{path}': {err}"
))
})?;
Ok(())
}
async fn register_dbus_name(connection: &Connection, name: &str) -> Result<(), Error> {
connection.request_name(name).await.map_err(|err| {
Error::ServiceInitializationFailed(format!("cannot acquire D-Bus name '{name}': {err}"))
})
}