use std::collections::{BTreeMap, HashMap, VecDeque};
use std::sync::{Mutex, PoisonError};
use zbus::blocking::{Connection, Proxy};
use zbus::zvariant::Value;
use super::{Click, Prepared, app_identity, deliver};
use crate::{NativeError, Result};
const DEST: &str = "org.freedesktop.Notifications";
const PATH: &str = "/org/freedesktop/Notifications";
const KEEP: usize = 256;
static CONN: Mutex<Option<Connection>> = Mutex::new(None);
static LIVE: Mutex<VecDeque<(u32, String, BTreeMap<String, String>)>> = Mutex::new(VecDeque::new());
static LISTENING: Mutex<bool> = Mutex::new(false);
fn os(e: zbus::Error) -> NativeError {
NativeError::Os(format!("no notification service answered ({})", crate::os_text(e)))
}
fn conn() -> Result<Connection> {
let mut slot = CONN.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(c) = slot.as_ref() {
return Ok(c.clone());
}
let c = Connection::session().map_err(os)?;
*slot = Some(c.clone());
Ok(c)
}
fn server_id(id: &str) -> Option<u32> {
LIVE.lock().unwrap_or_else(PoisonError::into_inner).iter().find(|(_, ours, _)| ours == id).map(|(n, _, _)| *n)
}
pub fn show(p: &Prepared) -> Result<()> {
let conn = conn()?;
let (app_id, _) = app_identity();
let app = std::env::current_exe().ok().and_then(|p| p.file_stem().map(|n| n.to_string_lossy().into_owned())).unwrap_or_default();
let mut hints: HashMap<&str, Value<'_>> = HashMap::new();
hints.insert("desktop-entry", Value::from(app_id.as_str()));
if p.silent {
hints.insert("suppress-sound", Value::from(true));
}
let actions = vec!["default", "Open"];
let replaces = server_id(&p.id).unwrap_or(0);
let reply = conn
.call_method(Some(DEST), PATH, Some(DEST), "Notify", &(app.as_str(), replaces, "", p.title.as_str(), p.body.as_str(), actions, hints, -1i32))
.map_err(os)?;
let nid: u32 = reply.body().deserialize().map_err(os)?;
let mut live = LIVE.lock().unwrap_or_else(PoisonError::into_inner);
live.retain(|(n, ours, _)| *ours != p.id && *n != nid);
live.push_back((nid, p.id.clone(), p.info.clone()));
while live.len() > KEEP {
live.pop_front();
}
Ok(())
}
pub fn remove(ids: &[String]) -> Result<()> {
let found: Vec<u32> = {
let mut live = LIVE.lock().unwrap_or_else(PoisonError::into_inner);
let found = live.iter().filter(|(_, ours, _)| ids.contains(ours)).map(|(n, _, _)| *n).collect();
live.retain(|(_, ours, _)| !ids.contains(ours));
found
};
if found.is_empty() {
return Ok(());
}
let conn = conn()?;
for nid in found {
conn.call_method(Some(DEST), PATH, Some(DEST), "CloseNotification", &(nid,)).map_err(os)?;
}
Ok(())
}
pub fn listen() -> Result<()> {
let mut listening = LISTENING.lock().unwrap_or_else(PoisonError::into_inner);
if *listening {
return Ok(());
}
let conn = conn()?;
let proxy = Proxy::new(&conn, DEST, PATH, DEST).map_err(os)?;
let signals = proxy.receive_signal("ActionInvoked").map_err(os)?;
std::thread::Builder::new()
.name("ashlar-notify".into())
.spawn(move || {
let _proxy = proxy;
for msg in signals {
let Ok((nid, action)) = msg.body().deserialize::<(u32, String)>() else { continue };
if action != "default" {
continue;
}
let hit = LIVE
.lock()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.find(|(n, _, _)| *n == nid)
.map(|(_, id, info)| Click { id: id.clone(), info: info.clone() });
if let Some(click) = hit {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| deliver(click)));
}
}
})
.map_err(|e| NativeError::Os(format!("could not start the notification listener ({})", crate::os_text(e))))?;
*listening = true;
Ok(())
}