use crate::{
vault::{
BlockingNotification, DbusSignalEvent, Hint, NOTIFICATION_EVENT, Notification,
NotificationManager, Timeout, Urgency,
},
wayland_adapter::SpellWin,
};
use smithay_client_toolkit::reexports::calloop::channel::{self, Sender};
use std::{cmp::Ordering, collections::HashMap};
use tracing::{info, warn};
use zbus::{fdo::Error as BusError, interface, object_server::SignalEmitter, zvariant::Value};
pub fn set_notification(win: &SpellWin, ui: Box<dyn NotificationManager>) {
let (sender, rx) = channel::channel::<NotifyEvent>();
let (tx, dbus_rx) = tokio::sync::mpsc::unbounded_channel::<DbusSignalEvent>();
let layer_name = win.layer_name.clone();
let sender_cl = sender.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let _ = notification_service_enter(sender_cl, layer_name, dbus_rx).await;
});
});
let _ = NOTIFICATION_EVENT.set(BlockingNotification::new(tx));
let _ = win
.get_handler()
.0
.insert_source(rx, move |event, _, _| match event {
channel::Event::Msg(msg) => match msg {
NotifyEvent::Noti(notification) => {
if let Err(err) = ui.new_notification(notification) {
warn!("{:?}", err);
}
}
NotifyEvent::NotificationClosed(id) => {
if let Err(err) = ui.close_notification(id) {
warn!(" Error closing notification with id {} : {:?}", id, err);
}
}
},
channel::Event::Closed => info!("Notification Channel to async thread is closed!"),
});
}
pub(crate) enum NotifyEvent {
Noti(Notification),
NotificationClosed(u32),
}
async fn notification_service_enter(
sender: Sender<NotifyEvent>,
layer_name: String,
mut rx: tokio::sync::mpsc::UnboundedReceiver<DbusSignalEvent>,
) -> zbus::fdo::Result<()> {
let conn = zbus::Connection::session().await?;
conn.object_server()
.at(
"/org/freedesktop/Notifications",
NotificationHandler {
sender: sender.clone(),
layer_name,
next_id: 1,
notifications: Vec::new(),
},
)
.await?;
info!("Object server is setup");
if let Err(err) = conn.request_name("org.freedesktop.Notifications").await {
warn!("Error When creating notification crate {:?}", err);
}
info!("Notification service is live with the provided name");
while let Some(event) = rx.recv().await {
if let Ok(iface_ref) = conn
.object_server()
.interface::<_, NotificationHandler>("/org/freedesktop/Notifications")
.await
{
let emitter = iface_ref.signal_emitter();
match event {
DbusSignalEvent::ActionInvoked { id, action_key } => {
let _ = NotificationHandler::action_invoked(emitter, id, &action_key).await;
}
DbusSignalEvent::NotificationClosed { id, reason } => {
let _ = NotificationHandler::notification_closed(emitter, id, reason).await;
}
}
}
}
Ok(())
}
pub(crate) struct NotificationHandler {
pub(crate) sender: Sender<NotifyEvent>,
pub(crate) layer_name: String,
pub(crate) next_id: u32,
pub(crate) notifications: Vec<Notification>,
}
#[interface(name = "org.freedesktop.Notifications", proxy(gen_blocking = false,))]
impl NotificationHandler {
async fn get_capabilities(&self) -> Result<Vec<String>, BusError> {
info!("capabilities called");
Ok([
"actions",
"body",
"body-images",
"icon-static",
"persistence",
]
.iter()
.map(|x| x.to_string())
.collect())
}
#[allow(clippy::too_many_arguments)]
async fn notify(
&mut self,
app_name: String,
replaces_id: u32,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
hints: HashMap<String, zbus::zvariant::Value<'_>>,
expire_timeout: i32,
) -> Result<u32, BusError> {
info!("Notifcation event received");
let notification = Notification {
id: replaces_id,
appname: app_name,
summary,
subtitle: None,
body,
icon: app_icon,
hints: hints
.into_iter()
.map(|(key, value)| {
let val: Hint = match key.as_str() {
"action-icons" => {
if let Value::Bool(x) = value {
Hint::ActionIcons(x)
} else {
Hint::Invalid
}
}
"category" => {
if let Value::Str(x) = value {
Hint::Category(x.to_string())
} else {
Hint::Invalid
}
}
"desktop-entry" => {
if let Value::Str(x) = value {
Hint::DesktopEntry(x.to_string())
} else {
Hint::Invalid
}
}
"image-data" => Hint::Invalid,
"image_data" => Hint::Invalid,
"image-path" => {
if let Value::Str(x) = value {
Hint::ImagePath(x.to_string())
} else {
Hint::Invalid
}
}
"image_path" => {
if let Value::Str(x) = value {
Hint::ImagePath(x.to_string())
} else {
Hint::Invalid
}
}
"icon_data" => Hint::Invalid,
"resident" => {
if let Value::Bool(x) = value {
Hint::Resident(x)
} else {
Hint::Invalid
}
}
"sound-file" => {
if let Value::Str(x) = value {
Hint::SoundFile(x.to_string())
} else {
Hint::Invalid
}
}
"sound-name" => {
if let Value::Str(x) = value {
Hint::SoundName(x.to_string())
} else {
Hint::Invalid
}
}
"suppress-sound" => {
if let Value::Bool(x) = value {
Hint::SuppressSound(x)
} else {
Hint::Invalid
}
}
"transient" => {
if let Value::Bool(x) = value {
Hint::Transient(x)
} else {
Hint::Invalid
}
}
"x" => {
if let Value::I32(x) = value {
Hint::X(x)
} else {
Hint::Invalid
}
}
"y" => {
if let Value::I32(x) = value {
Hint::Y(x)
} else {
Hint::Invalid
}
}
"urgency" => {
if let Value::U8(x) = value {
Hint::Urgency(match x {
0 => Urgency::Low,
1 => Urgency::Normal,
2 => Urgency::Critical,
_ => Urgency::Normal,
})
} else {
Hint::Invalid
}
}
err => {
warn!("Invalid hint passed with key: {}", err);
Hint::Invalid
}
};
val
})
.collect(),
actions,
timeout: match expire_timeout.cmp(&0) {
Ordering::Equal => Timeout::Never,
Ordering::Greater => Timeout::Milliseconds(expire_timeout),
Ordering::Less => Timeout::Default,
},
};
let _ = self
.sender
.clone()
.send(NotifyEvent::Noti(notification.clone()));
self.notifications.push(notification);
if replaces_id == 0 {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
if self.next_id == 0 {
self.next_id = 1;
}
Ok(id)
} else {
Ok(replaces_id)
}
}
async fn close_notification(
&self,
#[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
id: u32,
) -> Result<(), BusError> {
emitter.notification_closed(id, 4).await?;
if let Err(err) = self
.sender
.clone()
.send(NotifyEvent::NotificationClosed(id))
{
warn!("Error calling CloseNotification: {err}")
}
Ok(())
}
async fn get_server_information(&self) -> Result<(String, String, String, String), BusError> {
Ok((
"SpellNC-".to_string() + self.layer_name.as_str(),
"VimYoung".to_string(),
"0.0.1".to_string(),
"1.3".to_string(),
))
}
#[zbus(signal)]
async fn notification_closed(
emitter: &SignalEmitter<'_>,
id: u32,
reason: u32,
) -> zbus::Result<()>;
#[zbus(signal)]
async fn action_invoked(
emitter: &SignalEmitter<'_>,
id: u32,
action_key: &str,
) -> zbus::Result<()>;
}