use gtk4::{ gio, glib::{self, clone, subclass::Signal, SignalHandlerId}, prelude::*, subclass::prelude::*, }; use once_cell::sync::Lazy; use std::{borrow::Cow, cell::Cell, collections::HashMap, fmt, num::NonZeroU32}; static NOTIFICATIONS_XML: &str = " "; pub struct NotificationsInner { next_id: Cell, } #[glib::object_subclass] impl ObjectSubclass for NotificationsInner { const NAME: &'static str = "S76Notifications"; type ParentType = glib::Object; type Type = Notifications; fn new() -> Self { Self { next_id: Cell::new(NonZeroU32::new(1).unwrap()), } } } impl ObjectImpl for NotificationsInner { fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![Signal::builder("notification-received", &[], glib::Type::UNIT.into()).build()] }); SIGNALS.as_ref() } } glib::wrapper! { pub struct Notifications(ObjectSubclass); } // XXX hack: https://github.com/gtk-rs/gtk-rs-core/issues/263 unsafe impl Send for Notifications {} unsafe impl Sync for Notifications {} struct Hints(HashMap); #[allow(dead_code)] impl Hints { fn prop(&self, name: &str) -> Option { self.0.get(name)?.get() } fn actions_icon(&self) -> bool { self.prop("actions-icon").unwrap_or(false) } fn category(&self) -> Option { self.prop("category") } fn desktop_entry(&self) -> Option { self.prop("desktop-entry") } fn image_data(&self) -> Option<(i32, i32, i32, bool, i32, i32, Vec)> { self.prop("image-data") .or_else(|| self.prop("image_data")) .or_else(|| self.prop("icon_data")) } fn image_path(&self) -> Option { self.prop("image-path").or_else(|| self.prop("image_path")) } fn resident(&self) -> bool { self.prop("resident").unwrap_or(false) } fn sound_file(&self) -> Option { self.prop("sound-file") } fn sound_name(&self) -> Option { self.prop("sound-name") } fn transient(&self) -> bool { self.prop("transient").unwrap_or(false) } fn xy(&self) -> Option<(u8, u8)> { Some((self.prop("x")?, self.prop("y")?)) } fn urgency(&self) -> Option { self.prop("urgency") } } impl fmt::Debug for Hints { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut s = f.debug_struct("Hints"); for (k, v) in &self.0 { if let Some(v) = v.get::() { s.field(k, &v); } else if let Some(v) = v.get::() { s.field(k, &v); } else if let Some(v) = v.get::() { s.field(k, &v); } else if let Some(v) = v.get::() { s.field(k, &v); } else { s.field(k, v); }; } s.finish() } } impl glib::StaticVariantType for Hints { fn static_variant_type() -> Cow<'static, glib::VariantTy> { glib::VariantTy::new("a{sv}").unwrap().into() } } impl glib::FromVariant for Hints { fn from_variant(variant: &glib::Variant) -> Option { variant.get().map(Self) } } impl Notifications { pub fn new() -> Self { let notifications = glib::Object::new::(&[]).unwrap(); gio::bus_own_name( gio::BusType::Session, "org.freedesktop.Notifications", gio::BusNameOwnerFlags::NONE, clone!(@strong notifications => move |connection, name| notifications.bus_acquired(connection, name)), clone!(@strong notifications => move |connection, name| notifications.name_acquired(connection, name)), clone!(@strong notifications => move |connection, name| notifications.name_lost(connection, name)), ); notifications } fn inner(&self) -> &NotificationsInner { NotificationsInner::from_instance(self) } fn next_id(&self) -> NonZeroU32 { let next_id = &self.inner().next_id; let id = next_id.get(); next_id.set( NonZeroU32::new(u32::from(id).wrapping_add(1)).unwrap_or(NonZeroU32::new(1).unwrap()), ); id } fn handle_notify( &self, app_name: String, replaces_id: Option, app_icon: String, summary: String, body: String, actions: Vec, hints: Hints, expire_timeout: i32, ) -> NonZeroU32 { let id = replaces_id.unwrap_or_else(|| self.next_id()); println!( "{:?}", ( id, app_name, app_icon, summary, body, actions, hints, expire_timeout ) ); // TODO // XXX self.emit_by_name("notification-received", &[]).unwrap(); id } fn handle_close_notification(&self, _id: u32) {} fn bus_acquired(&self, _connection: gio::DBusConnection, _name: &str) {} fn name_acquired(&self, connection: gio::DBusConnection, _name: &str) { let introspection_data = gio::DBusNodeInfo::for_xml(NOTIFICATIONS_XML).unwrap(); let interface_info = introspection_data .lookup_interface("org.freedesktop.Notifications") .unwrap(); let method_call = clone!(@strong self as self_ => move |_connection: gio::DBusConnection, _sender: &str, _path: &str, _interface: &str, method: &str, args: glib::Variant, invocation: gio::DBusMethodInvocation| { match method { "Notify" => { let (app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout) = args.get().unwrap(); let replaces_id = NonZeroU32::new(replaces_id); let res = self_.handle_notify(app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout); invocation.return_value(Some(&(u32::from(res),).to_variant())); // TODO error? } "CloseNotification" => { let (id,) = args.get::<(u32,)>().unwrap(); self_.handle_close_notification(id); invocation.return_value(None); // TODO error? } "GetCapabilities" => { // TODO: body-markup, sound let capabilities = vec!["actions", "body", "icon-static", "persistence"]; invocation.return_value(Some(&(capabilities,).to_variant())); } "GetServerInformation" => { let information = ("cosmic-panel", "system76", env!("CARGO_PKG_VERSION"), "1.2"); invocation.return_value(Some(&information.to_variant())); } _ => unreachable!() } }); let get_property = |_: gio::DBusConnection, _sender: &str, _path: &str, _interface: &str, _prop: &str| { unreachable!() }; let set_property = |_: gio::DBusConnection, _sender: &str, _path: &str, _interface: &str, _prop: &str, _value: glib::Variant| { unreachable!() }; if let Err(err) = connection.register_object( "/org/freedesktop/Notifications", &interface_info, method_call, get_property, set_property, ) { eprintln!("Failed to register object: {}", err); } } fn name_lost(&self, _connection: Option, _name: &str) {} pub fn connect_notification_recieved(&self, cb: F) -> SignalHandlerId { self.connect_local("notification-received", false, move |_values| { cb(); None }) .unwrap() } }