use dbus::{ arg::messageitem::{MessageItem, MessageItemArray}, ffidisp::{BusType, Connection, ConnectionItem}, Message, }; use super::{ bus::NotificationBus, ActionResponse, ActionResponseHandler, CloseReason, NOTIFICATION_INTERFACE, }; use super::super::{ error::*, hints::message::HintMessage, notification::Notification, xdg::{ServerInformation, NOTIFICATION_OBJECTPATH}, }; pub mod bus { use super::super::super::xdg::NOTIFICATION_DEFAULT_BUS; fn skip_first_slash(s: &str) -> &str { if let Some('/') = s.chars().next() { &s[1..] } else { s } } use std::path::PathBuf; type BusNameType = dbus::strings::BusName<'static>; #[derive(Clone, Debug)] pub struct NotificationBus(BusNameType); impl Default for NotificationBus { fn default() -> Self { Self(dbus::strings::BusName::from_slice(NOTIFICATION_DEFAULT_BUS).unwrap()) } } impl NotificationBus { fn namespaced_custom(custom_path: &str) -> Option { // abusing path for semantic join skip_first_slash( PathBuf::from("/de/hoodie/Notification") .join(custom_path) .to_str()?, ) .replace('/', ".") .into() } pub fn custom(custom_path: &str) -> Option { let name = dbus::strings::BusName::new(Self::namespaced_custom(custom_path)?).ok()?; Some(Self(name)) } pub fn into_name(self) -> BusNameType { self.0 } } } /// A handle to a shown notification. /// /// This keeps a connection alive to ensure actions work on certain desktops. #[derive(Debug)] pub struct DbusNotificationHandle { pub(crate) id: u32, pub(crate) connection: Connection, pub(crate) notification: Notification, } impl DbusNotificationHandle { pub(crate) fn new( id: u32, connection: Connection, notification: Notification, ) -> DbusNotificationHandle { DbusNotificationHandle { id, connection, notification, } } pub fn wait_for_action(self, invocation_closure: impl ActionResponseHandler) { wait_for_action_signal(&self.connection, self.id, invocation_closure); } pub fn close(self) { let mut message = build_message("CloseNotification", Default::default()); message.append_items(&[self.id.into()]); let _ = self.connection.send(message); // If closing fails there's nothing we could do anyway } pub fn on_close(self, closure: F) where F: FnOnce(CloseReason), { self.wait_for_action(|action: &ActionResponse| { if let ActionResponse::Closed(reason) = action { closure(*reason); } }); } pub fn update(&mut self) { self.id = send_notification_via_connection(&self.notification, self.id, &self.connection) .unwrap(); } } pub fn send_notification_via_connection( notification: &Notification, id: u32, connection: &Connection, ) -> Result { send_notification_via_connection_at_bus(notification, id, connection, Default::default()) } pub fn send_notification_via_connection_at_bus( notification: &Notification, id: u32, connection: &Connection, bus: NotificationBus, ) -> Result { let mut message = build_message("Notify", bus); let timeout: i32 = notification.timeout.into(); message.append_items(&[ notification.appname.to_owned().into(), // appname id.into(), // notification to update notification.icon.to_owned().into(), // icon notification.summary.to_owned().into(), // summary (title) notification.body.to_owned().into(), // body pack_actions(notification), // actions pack_hints(notification)?, // hints timeout.into(), // timeout ]); let reply = connection.send_with_reply_and_block(message, 2000)?; match reply.get_items().first() { Some(MessageItem::UInt32(ref id)) => Ok(*id), _ => Ok(0), } } pub fn connect_and_send_notification( notification: &Notification, ) -> Result { let bus = notification.bus.clone(); connect_and_send_notification_at_bus(notification, bus) } pub fn connect_and_send_notification_at_bus( notification: &Notification, bus: NotificationBus, ) -> Result { let connection = Connection::get_private(BusType::Session)?; let inner_id = notification.id.unwrap_or(0); let id = send_notification_via_connection_at_bus(notification, inner_id, &connection, bus)?; Ok(DbusNotificationHandle::new( id, connection, notification.clone(), )) } fn build_message(method_name: &str, bus: NotificationBus) -> Message { Message::new_method_call( bus.into_name(), NOTIFICATION_OBJECTPATH, NOTIFICATION_INTERFACE, method_name, ) .unwrap_or_else(|_| panic!("Error building message call {:?}.", method_name)) } pub fn pack_hints(notification: &Notification) -> Result { if !notification.hints.is_empty() || !notification.hints_unique.is_empty() { let hints = notification .get_hints() .cloned() .map(HintMessage::wrap_hint) .collect::>(); if let Ok(array) = MessageItem::new_dict(hints) { return Ok(array); } } Ok(MessageItem::Array( MessageItemArray::new(vec![], "a{sv}".into()).unwrap(), )) } pub fn pack_actions(notification: &Notification) -> MessageItem { if !notification.actions.is_empty() { let mut actions = vec![]; for action in ¬ification.actions { actions.push(action.to_owned().into()); } if let Ok(array) = MessageItem::new_array(actions) { return array; } } MessageItem::Array(MessageItemArray::new(vec![], "as".into()).unwrap()) } pub fn get_capabilities() -> Result> { let mut capabilities = vec![]; let message = build_message("GetCapabilities", Default::default()); let connection = Connection::get_private(BusType::Session)?; let reply = connection.send_with_reply_and_block(message, 2000)?; if let Some(MessageItem::Array(items)) = reply.get_items().first() { for item in items.iter() { if let MessageItem::Str(ref cap) = *item { capabilities.push(cap.clone()); } } } Ok(capabilities) } fn unwrap_message_string(item: Option<&MessageItem>) -> String { match item { Some(MessageItem::Str(value)) => value.to_owned(), _ => "".to_owned(), } } #[allow(clippy::get_first)] pub fn get_server_information() -> Result { let message = build_message("GetServerInformation", Default::default()); let connection = Connection::get_private(BusType::Session)?; let reply = connection.send_with_reply_and_block(message, 2000)?; let items = reply.get_items(); Ok(ServerInformation { name: unwrap_message_string(items.get(0)), vendor: unwrap_message_string(items.get(1)), version: unwrap_message_string(items.get(2)), spec_version: unwrap_message_string(items.get(3)), }) } /// Listens for the `ActionInvoked(UInt32, String)` Signal. /// /// No need to use this, check out `Notification::show_and_wait_for_action(FnOnce(action:&str))` pub fn handle_action(id: u32, func: impl ActionResponseHandler) { let connection = Connection::get_private(BusType::Session).unwrap(); wait_for_action_signal(&connection, id, func); } // Listens for the `ActionInvoked(UInt32, String)` signal. fn wait_for_action_signal(connection: &Connection, id: u32, handler: impl ActionResponseHandler) { connection .add_match(&format!( "interface='{}',member='ActionInvoked'", NOTIFICATION_INTERFACE )) .unwrap(); connection .add_match(&format!( "interface='{}',member='NotificationClosed'", NOTIFICATION_INTERFACE )) .unwrap(); for item in connection.iter(1000) { if let ConnectionItem::Signal(message) = item { let items = message.get_items(); let (path, interface, member) = ( message.path().map_or_else(String::new, |p| { p.into_cstring().to_string_lossy().into_owned() }), message.interface().map_or_else(String::new, |p| { p.into_cstring().to_string_lossy().into_owned() }), message.member().map_or_else(String::new, |p| { p.into_cstring().to_string_lossy().into_owned() }), ); match (path.as_str(), interface.as_str(), member.as_str()) { // match (protocol.unwrap(), iface.unwrap(), member.unwrap()) { // Action Invoked (path, interface, "ActionInvoked") if path == NOTIFICATION_OBJECTPATH && interface == NOTIFICATION_INTERFACE => { if let (&MessageItem::UInt32(nid), MessageItem::Str(ref action)) = (&items[0], &items[1]) { if nid == id { handler.call(&ActionResponse::Custom(action)); break; } } } // Notification Closed (path, interface, "NotificationClosed") if path == NOTIFICATION_OBJECTPATH && interface == NOTIFICATION_INTERFACE => { if let (&MessageItem::UInt32(nid), &MessageItem::UInt32(reason)) = (&items[0], &items[1]) { if nid == id { handler.call(&ActionResponse::Closed(reason.into())); break; } } } (..) => (), } } } }