mod events;
mod model;
mod server;
mod store;
use std::time::Duration;
use tokio::sync::mpsc;
use events::parse_dismiss;
use model::{CloseReason, Event, Notification};
use server::NotifyServer;
use std::sync::atomic::AtomicU32;
use store::Notifications;
fn emit(notifications: &[Notification]) {
if let Ok(json) = serde_json::to_string(&serde_json::json!({ "notifications": notifications }))
{
println!("{json}");
}
}
async fn notify_closed(conn: &zbus::Connection, id: u32, reason: CloseReason) {
let result = conn
.emit_signal(
None::<&str>,
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"NotificationClosed",
&(id, u32::from(reason)),
)
.await;
if let Err(e) = result {
eprintln!("warning: could not emit NotificationClosed for {id}: {e}");
}
}
fn expire_ms(timeout: i32) -> Option<u64> {
match timeout {
0 => None, -1 => Some(5_000), ms => Some(ms as u64),
}
}
#[tokio::main]
async fn main() {
let (event_tx, mut event_rx) = mpsc::unbounded_channel::<Event>();
{
let event_tx = event_tx.clone();
tokio::task::spawn_blocking(move || {
use std::io::BufRead;
let stdin = std::io::stdin();
let mut lines = stdin.lock().lines();
while let Some(Ok(line)) = lines.next() {
let id = serde_json::from_str::<serde_json::Value>(&line)
.ok()
.and_then(|val| parse_dismiss(&val));
if let Some(id) = id {
let event = Event::Close {
id,
reason: CloseReason::Dismissed,
};
if event_tx.send(event).is_err() {
break;
}
}
}
std::process::exit(0);
});
}
let server = NotifyServer {
tx: event_tx.clone(),
next_id: AtomicU32::new(1),
tags: std::sync::Mutex::new(std::collections::HashMap::new()),
};
let conn = zbus::connection::Builder::session()
.expect("session bus unavailable")
.name("org.freedesktop.Notifications")
.expect("could not claim org.freedesktop.Notifications — is another daemon running?")
.serve_at("/org/freedesktop/Notifications", server)
.expect("serve_at failed")
.build()
.await
.expect("D-Bus connection failed");
emit(&[]);
let mut store = Notifications::new();
while let Some(event) = event_rx.recv().await {
match event {
Event::Add(n, timeout) => {
let id = n.id;
let generation = store.upsert(n);
emit(store.items());
if let Some(ms) = expire_ms(timeout) {
let tx = event_tx.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(ms)).await;
let _ = tx.send(Event::Expire { id, generation });
});
}
}
Event::Expire { id, generation } => {
if store.expire(id, generation) {
emit(store.items());
notify_closed(&conn, id, CloseReason::Expired).await;
}
}
Event::Close { id, reason } => {
if store.remove(id) {
emit(store.items());
notify_closed(&conn, id, reason).await;
}
}
}
}
}