diff --git a/Cargo.lock b/Cargo.lock index a0dc4848..e0bfd6ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,37 +452,16 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "enumflags2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" -dependencies = [ - "enumflags2_derive 0.6.4", - "serde", -] - [[package]] name = "enumflags2" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def" dependencies = [ - "enumflags2_derive 0.7.3", + "enumflags2_derive", "serde", ] -[[package]] -name = "enumflags2_derive" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "enumflags2_derive" version = "0.7.3" @@ -847,7 +826,7 @@ checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" dependencies = [ "anyhow", "heck", - "proc-macro-crate 1.1.0", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -962,7 +941,7 @@ dependencies = [ "anyhow", "heck", "itertools", - "proc-macro-crate 1.1.0", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -1080,9 +1059,9 @@ dependencies = [ "x11", "x11rb", "xdg", - "zbus 2.0.0", - "zvariant 3.0.0", - "zvariant_derive 3.0.0", + "zbus", + "zvariant", + "zvariant_derive", ] [[package]] @@ -1119,16 +1098,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "nb-connect" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1bb540dc6ef51cfe1916ec038ce7a620daf3a111e2502d745197cd53d6bca15" -dependencies = [ - "libc", - "socket2", -] - [[package]] name = "nix" version = "0.20.2" @@ -1338,15 +1307,21 @@ dependencies = [ "cascade", "chrono", "derivative", + "enumflags2", + "futures", + "futures-channel", "gdk4-wayland", "gdk4-x11", "gobject-sys", "gtk4", "libcosmic", "once_cell", + "serde", "toml", "x11", - "zbus 1.9.2", + "zbus", + "zbus_names", + "zvariant", ] [[package]] @@ -1415,15 +1390,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - [[package]] name = "proc-macro-crate" version = "1.1.0" @@ -2113,29 +2079,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" -[[package]] -name = "zbus" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5983c3d035549ab80db67c844ec83ed271f7c1f2546fd9577c594d34c1b6c85" -dependencies = [ - "async-io", - "byteorder", - "derivative", - "enumflags2 0.6.4", - "fastrand", - "futures", - "nb-connect", - "nix 0.20.2", - "once_cell", - "polling", - "scoped-tls", - "serde", - "serde_repr", - "zbus_macros 1.9.2", - "zvariant 2.10.0", -] - [[package]] name = "zbus" version = "2.0.0" @@ -2152,7 +2095,7 @@ dependencies = [ "async-trait", "byteorder", "derivative", - "enumflags2 0.7.3", + "enumflags2", "event-listener", "futures-core", "futures-sink", @@ -2166,21 +2109,9 @@ dependencies = [ "serde_repr", "sha1", "static_assertions", - "zbus_macros 2.0.0", + "zbus_macros", "zbus_names", - "zvariant 3.0.0", -] - -[[package]] -name = "zbus_macros" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce54ac7b2150a2fa21ad5842a7470ce2288158d7da1f9bfda8ad455a1c59a97" -dependencies = [ - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote", - "syn", + "zvariant", ] [[package]] @@ -2189,7 +2120,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd2ea67f43e8abd245eabc480e597990340d9870b585d40bf4350d742acb2219" dependencies = [ - "proc-macro-crate 1.1.0", + "proc-macro-crate", "proc-macro2", "quote", "regex", @@ -2204,21 +2135,7 @@ checksum = "ae1f142d242d6854815a8c5c2aea83d9508f72f5757d0a137c21ef4b07bfee66" dependencies = [ "serde", "static_assertions", - "zvariant 3.0.0", -] - -[[package]] -name = "zvariant" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68c7b55f2074489b7e8e07d2d0a6ee6b4f233867a653c664d8020ba53692525" -dependencies = [ - "byteorder", - "enumflags2 0.6.4", - "libc", - "serde", - "static_assertions", - "zvariant_derive 2.10.0", + "zvariant", ] [[package]] @@ -2228,23 +2145,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a946c049b2eac1a253f98e9267a8ce7a3d93be274ea146e6dd7a0965232a911" dependencies = [ "byteorder", - "enumflags2 0.7.3", + "enumflags2", "libc", "serde", "static_assertions", - "zvariant_derive 3.0.0", -] - -[[package]] -name = "zvariant_derive" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ca5e22593eb4212382d60d26350065bf2a02c34b85bc850474a74b589a3de9" -dependencies = [ - "proc-macro-crate 1.1.0", - "proc-macro2", - "quote", - "syn", + "zvariant_derive", ] [[package]] @@ -2253,7 +2158,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28fce5afb8d639bff79b1e8cdb258a3ca22d458f4603b23d794b4cb4e878c990" dependencies = [ - "proc-macro-crate 1.1.0", + "proc-macro-crate", "proc-macro2", "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index ce868ac3..4007838a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pop-cosmic-panel" version = "0.1.0" -edition = "2018" +edition = "2021" license = "LGPL-3.0-or-later" [dependencies] @@ -9,15 +9,21 @@ cascade = "1" chrono = "0.4" byte_string = "1" derivative = "2" +enumflags2 = "0.7" +futures = "0.3" +futures-channel = "0.3" gdk4-x11 = "0.3" gdk4-wayland = { version = "0.3", optional = true } gtk4 = "0.3" gobject-sys = "0.14.0" libcosmic = { git = "https://github.com/pop-os/libcosmic" } once_cell = "1" +serde = "1" toml = "0.5" x11 = { version = "2", features = ["xlib"] } -zbus = "1" +zbus = "2" +zbus_names = "2" +zvariant = "3" [features] layer-shell = ["gdk4-wayland", "libcosmic/layer-shell"] diff --git a/src/application.rs b/src/application.rs index a549cb22..6fe8eeb4 100644 --- a/src/application.rs +++ b/src/application.rs @@ -30,7 +30,7 @@ impl ObjectImpl for PanelAppInner { self.parent_constructed(obj); - status_notifier_watcher::start(); + glib::MainContext::default().spawn_local(status_notifier_watcher::start()); self.notifications.set(Notifications::new()); } } diff --git a/src/dbus_service.rs b/src/dbus_service.rs new file mode 100644 index 00000000..3c00b5bc --- /dev/null +++ b/src/dbus_service.rs @@ -0,0 +1,53 @@ +use futures::prelude::*; +use gtk4::glib::{self, clone}; +use std::cell::Cell; +use zbus::fdo::{DBusProxy, RequestNameFlags, RequestNameReply}; +use zbus_names::WellKnownName; + +pub async fn create< + F: Fn(zbus::ConnectionBuilder<'static>) -> zbus::Result>, +>( + well_known_name: &'static str, + serve_cb: F, +) -> zbus::Result { + let well_known_name = WellKnownName::try_from(well_known_name)?; + + let connection = serve_cb(zbus::ConnectionBuilder::session()?)? + .build() + .await?; + let dbus_proxy = DBusProxy::new(&connection).await?; + let mut name_owner_changed_stream = dbus_proxy.receive_name_owner_changed().await?; + + let flags = RequestNameFlags::AllowReplacement.into(); + match dbus_proxy + .request_name(well_known_name.as_ref(), flags) + .await? + { + RequestNameReply::InQueue => { + eprintln!("Bus name '{}' already owned", well_known_name); + } + _ => {} + } + + glib::MainContext::default().spawn_local(clone!(@strong connection => async move { + let have_bus_name = Cell::new(false); + let unique_name = connection.unique_name().map(|x| x.as_ref()); + while let Some(evt) = name_owner_changed_stream.next().await { + let args = match evt.args() { + Ok(args) => args, + Err(_) => { continue; }, + }; + if args.name.as_ref() == well_known_name { + if args.new_owner.as_ref() == unique_name.as_ref() { + eprintln!("Acquired bus name: {}", well_known_name); + have_bus_name.set(true); + } else if have_bus_name.get() { + eprintln!("Lost bus name: {}", well_known_name); + have_bus_name.set(false); + } + } + } + })); + + Ok(connection) +} diff --git a/src/main.rs b/src/main.rs index 03a40bde..cbf04e26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -use gtk4::prelude::*; +use gtk4::{glib, prelude::*}; mod application; +mod dbus_service; mod deref_cell; mod mpris; mod mpris_player; @@ -18,5 +19,5 @@ mod window; use application::PanelApp; fn main() { - PanelApp::new().run(); + glib::MainContext::default().with_thread_default(|| PanelApp::new().run()); } diff --git a/src/mpris.rs b/src/mpris.rs index 8dbfcbbb..ff3cd228 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -1,12 +1,13 @@ use cascade::cascade; +use futures::stream::StreamExt; use gtk4::{ - gio, glib::{self, clone}, prelude::*, subclass::prelude::*, }; use once_cell::unsync::OnceCell; use std::{cell::RefCell, collections::HashMap}; +use zbus::fdo::DBusProxy; use crate::deref_cell::DerefCell; use crate::mpris_player::MprisPlayer; @@ -14,7 +15,7 @@ use crate::mpris_player::MprisPlayer; #[derive(Default)] pub struct MprisControlsInner { listbox: DerefCell, - dbus: OnceCell, + dbus: OnceCell>, players: RefCell>, } @@ -37,23 +38,32 @@ impl ObjectImpl for MprisControlsInner { }; glib::MainContext::default().spawn_local(clone!(@strong obj => async move { - let dbus = match DBus::new().await { - Ok(dbus) => dbus, + let (dbus, mut name_owner_changed_stream) = match async { + let connection = zbus::Connection::session().await?; + let dbus = DBusProxy::new(&connection).await?; + let stream = dbus.receive_name_owner_changed().await?; + Ok::<_, zbus::Error>((dbus, stream)) + }.await { + Ok(value) => value, Err(err) => { eprintln!("Failed to connect to 'org.freedesktop.DBus': {}", err); return; } }; - dbus.connect_name_owner_changed(clone!(@strong obj => move |name, old, new| { - if name.starts_with("org.mpris.MediaPlayer2.") { - if !old.is_empty() { - obj.player_removed(&name); - } - if !new.is_empty() { - glib::MainContext::default().spawn_local(clone!(@strong obj => async move { - obj.player_added(&name).await; - })); + glib::MainContext::default().spawn_local(clone!(@strong obj => async move { + while let Some(evt) = name_owner_changed_stream.next().await { + let args = match evt.args() { + Ok(args) => args, + Err(_) => { continue; }, + }; + if args.name.starts_with("org.mpris.MediaPlayer2.") { + if !args.old_owner.is_none() { + obj.player_removed(&args.name); + } + if !args.new_owner.is_none() { + obj.player_added(&args.name).await; + } } } })); @@ -122,45 +132,3 @@ impl MprisControls { self.inner().players.borrow_mut().remove(name); } } - -struct DBus(gio::DBusProxy); - -impl DBus { - async fn new() -> Result { - let proxy = gio::DBusProxy::for_bus_future( - gio::BusType::Session, - gio::DBusProxyFlags::NONE, - None, - "org.freedesktop.DBus", - "/org/freedesktop/DBus", - "org.freedesktop.DBus", - ) - .await?; - Ok(Self(proxy)) - } - - async fn list_names(&self) -> Result, glib::Error> { - Ok(self - .0 - .call_future("ListNames", None, gio::DBusCallFlags::NONE, 1000) - .await? - .child_value(0) - .iter() - .filter_map(|x| x.get::())) - } - - fn connect_name_owner_changed( - &self, - f: F, - ) -> glib::SignalHandlerId { - self.0 - .connect_local("g-signal", false, move |args| { - if &args[2].get::().unwrap() == "NameOwnerChanged" { - let (name, old, new) = args[3].get::().unwrap().get().unwrap(); - f(name, old, new); - } - None - }) - .unwrap() - } -} diff --git a/src/mpris_player.rs b/src/mpris_player.rs index 0bdd1003..2fd0ae99 100644 --- a/src/mpris_player.rs +++ b/src/mpris_player.rs @@ -1,4 +1,5 @@ use cascade::cascade; +use futures::StreamExt; use gtk4::{ gdk_pixbuf, gio, glib::{self, clone}, @@ -6,7 +7,9 @@ use gtk4::{ prelude::*, subclass::prelude::*, }; -use std::cell::RefCell; +use std::{cell::RefCell, collections::HashMap}; +use zbus::dbus_proxy; +use zvariant::OwnedValue; use crate::deref_cell::DerefCell; @@ -16,7 +19,7 @@ pub struct MprisPlayerInner { backward_button: DerefCell, play_pause_button: DerefCell, forward_button: DerefCell, - player: DerefCell, + player: DerefCell>, image: DerefCell, image_uri: RefCell>, title_label: DerefCell, @@ -114,14 +117,29 @@ glib::wrapper! { } impl MprisPlayer { - pub async fn new(name: &str) -> Result { + pub async fn new(name: &str) -> zbus::Result { let obj = glib::Object::new::(&[]).unwrap(); - let player = Player::new(name).await?; - player.connect_properties_changed(clone!(@weak obj => move |_player| { - obj.update(); - })); + let connection = zbus::Connection::session().await?; + let player = PlayerProxy::builder(&connection) + .destination(name.to_string())? + .build() + .await?; + + let metadata_stream = player.receive_metadata_changed().await; + let playback_status_stream = player.receive_playback_status_changed().await; + let mut stream = futures::stream_select!( + metadata_stream.map(|_| ()), + playback_status_stream.map(|_| ()) + ); + obj.inner().player.set(player); + + glib::MainContext::default().spawn_local(clone!(@strong obj => async move { + if stream.next().await.is_some() { + obj.update(); + } + })); obj.update(); Ok(obj) @@ -133,7 +151,7 @@ impl MprisPlayer { fn call(&self, method: &'static str) { glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move { - if let Err(err) = self_.inner().player.call(method).await { + if let Err(err) = self_.inner().player.call::<_, _, ()>(method, &()).await { eprintln!("Failed to call '{}': {}", method, err); } })); @@ -164,8 +182,8 @@ impl MprisPlayer { let player = &self.inner().player; // XXX status - let (status, metadata) = match (player.playback_status(), player.metadata()) { - (Some(status), Some(metadata)) => (status, metadata), + let (status, metadata) = match (player.cached_playback_status(), player.cached_metadata()) { + (Ok(Some(status)), Ok(Some(metadata))) => (status, Metadata(metadata)), _ => return, }; @@ -197,11 +215,11 @@ impl MprisPlayer { } } -struct Metadata(glib::VariantDict); +struct Metadata(HashMap); impl Metadata { - fn lookup(&self, key: &str) -> Option { - self.0.lookup_value(key, None)?.get() + fn lookup<'a, T: TryFrom>(&self, key: &str) -> Option { + T::try_from(self.0.get(key)?.clone()).ok() } fn title(&self) -> Option { @@ -221,53 +239,14 @@ impl Metadata { } } -#[derive(Clone)] -struct Player(gio::DBusProxy); +#[dbus_proxy( + interface = "org.mpris.MediaPlayer2.Player", + default_path = "/org/mpris/MediaPlayer2" +)] +trait Player { + #[dbus_proxy(property)] + fn metadata(&self) -> zbus::Result>; -impl Player { - async fn new(name: &str) -> Result { - let proxy = gio::DBusProxy::for_bus_future( - gio::BusType::Session, - gio::DBusProxyFlags::NONE, - None, - name, - "/org/mpris/MediaPlayer2", - "org.mpris.MediaPlayer2.Player", - ) - .await?; - Ok(Self(proxy)) - } - - async fn call(&self, method: &str) -> Result<(), glib::Error> { - self.0 - .call_future(method, None, gio::DBusCallFlags::NONE, 1000) - .await?; - Ok(()) - } - - fn property(&self, prop: &str) -> Option { - self.0.cached_property(prop)?.get() - } - - fn connect_properties_changed(&self, f: F) -> glib::SignalHandlerId { - let proxy = &self.0; - self.0 - .connect_local( - "g-properties-changed", - false, - clone!(@weak proxy => @default-panic, move |_| { - f(Self(proxy)); - None - }), - ) - .unwrap() - } - - fn playback_status(&self) -> Option { - self.property("PlaybackStatus") - } - - fn metadata(&self) -> Option { - Some(Metadata(self.property("Metadata")?)) - } + #[dbus_proxy(property)] + fn playback_status(&self) -> zbus::Result; } diff --git a/src/notification_list.rs b/src/notification_list.rs index 2fbc4051..86980bad 100644 --- a/src/notification_list.rs +++ b/src/notification_list.rs @@ -36,7 +36,10 @@ impl ObjectImpl for NotificationListInner { ..set_parent(obj); ..connect_row_activated(clone!(@weak obj => move |_, row| { if let Some(id) = obj.id_for_row(row) { - obj.inner().notifications.invoke_action(id, "default"); + let notifications = obj.inner().notifications.clone(); + glib::MainContext::default().spawn_local(async move { + notifications.invoke_action(id, "default").await; + }); } })); }; @@ -66,7 +69,7 @@ impl NotificationList { obj.inner().notifications.set(notifications.clone()); *obj.inner().ids.borrow_mut() = vec![ - notifications.connect_notification_recieved(clone!(@weak obj => move |notification| { + notifications.connect_notification_received(clone!(@weak obj => move |notification| { obj.handle_notification(¬ification); })), notifications.connect_notification_closed(clone!(@weak obj => move |id| { diff --git a/src/notification_popover.rs b/src/notification_popover.rs index 7d7d550f..2fee186c 100644 --- a/src/notification_popover.rs +++ b/src/notification_popover.rs @@ -39,7 +39,10 @@ impl ObjectImpl for NotificationPopoverInner { return; } if let Some(id) = obj.id() { - obj.inner().notifications.invoke_action(id, "default"); + let notifications = obj.inner().notifications.clone(); + glib::MainContext::default().spawn_local(async move { + notifications.invoke_action(id, "default").await; + }); } obj.popdown(); })); @@ -83,7 +86,7 @@ impl NotificationPopover { obj.inner().notifications.set(notifications.clone()); *obj.inner().ids.borrow_mut() = vec![ - notifications.connect_notification_recieved(clone!(@weak obj => move |notification| { + notifications.connect_notification_received(clone!(@weak obj => move |notification| { obj.handle_notification(¬ification); })), notifications.connect_notification_closed(clone!(@weak obj => move |id| { diff --git a/src/notifications.rs b/src/notifications.rs index e58a4cea..ed0b3a67 100644 --- a/src/notifications.rs +++ b/src/notifications.rs @@ -1,69 +1,161 @@ +#![allow(non_snake_case)] + +use futures::stream::StreamExt; +use futures_channel::mpsc; use gtk4::{ - gio, glib::{self, clone, subclass::Signal, SignalHandlerId}, prelude::*, subclass::prelude::*, }; use once_cell::sync::Lazy; +use once_cell::unsync::OnceCell; use std::{ - borrow::Cow, - cell::{Cell, RefCell}, collections::HashMap, + convert::TryFrom, fmt, num::NonZeroU32, - rc::Rc, + sync::{Arc, Mutex}, }; +use zbus::{dbus_interface, Result, SignalContext}; +use zvariant::OwnedValue; + +use crate::dbus_service; +use crate::deref_cell::DerefCell; static PATH: &str = "/org/freedesktop/Notifications"; static INTERFACE: &str = "org.freedesktop.Notifications"; -static NOTIFICATIONS_XML: &str = " - - - - - - - - - - - - - - - - +enum Event { + NotificationReceived(NotificationId), + CloseNotification(NotificationId), +} - - - +pub struct NotificationsInterfaceInner { + next_id: Mutex, + notifications: Mutex>>, + sender: mpsc::UnboundedSender, +} - - - - - - +#[derive(Clone)] +pub struct NotificationsInterface(Arc); - - - - +impl NotificationsInterface { + fn new() -> (Self, mpsc::UnboundedReceiver) { + let (sender, receiver) = mpsc::unbounded(); + ( + Self(Arc::new(NotificationsInterfaceInner { + next_id: Default::default(), + notifications: Default::default(), + sender, + })), + receiver, + ) + } - - - - - - -"; + fn next_id(&self) -> NotificationId { + let mut next_id = self.0.next_id.lock().unwrap(); + let id = *next_id; + *next_id = NotificationId::new(u32::from(id).wrapping_add(1)).unwrap_or_default(); + 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, + ) -> NotificationId { + // Ignores `expire-timeout`, like Gnome Shell + + let id = replaces_id.unwrap_or_else(|| self.next_id()); + + let notification = Arc::new(Notification { + id, + app_name, + app_icon, + summary, + body, + actions, + hints, + }); + + self.0 + .notifications + .lock() + .unwrap() + .insert(id, notification); + + self.0 + .sender + .unbounded_send(Event::NotificationReceived(id)) + .unwrap(); + + id + } +} + +// TODO: return value variable names in introspection data? + +#[dbus_interface(name = "org.freedesktop.Notifications")] +impl NotificationsInterface { + fn Notify( + &self, + app_name: String, + replaces_id: u32, + app_icon: String, + summary: String, + body: String, + actions: Vec, + hints: HashMap, + expire_timeout: i32, + ) -> u32 { + u32::from(self.handle_notify( + app_name, + NotificationId::new(replaces_id), + app_icon, + summary, + body, + actions, + Hints(hints), + expire_timeout, + )) + } + + async fn CloseNotification(&self, id: u32) { + if let Some(id) = NotificationId::new(id) { + self.0 + .sender + .unbounded_send(Event::CloseNotification(id)) + .unwrap(); + } + // TODO error? + } + + fn GetCapabilities(&self) -> Vec<&'static str> { + // TODO: body-markup, sound + vec!["actions", "body", "icon-static", "persistence"] + } + + fn GetServerInformation(&self) -> (&'static str, &'static str, &'static str, &'static str) { + ("cosmic-panel", "system76", env!("CARGO_PKG_VERSION"), "1.2") + } + + #[dbus_interface(signal)] + async fn NotificationClosed(ctxt: &SignalContext<'_>, id: u32, reason: u32) -> Result<()>; + + #[dbus_interface(signal)] + async fn ActionInvoked(ctxt: &SignalContext<'_>, id: u32, action_key: &str) -> Result<()>; +} #[derive(Default)] pub struct NotificationsInner { - next_id: Cell, - notifications: RefCell>>, - connection: RefCell>, + interface: DerefCell, + connection: OnceCell, } #[glib::object_subclass] @@ -99,16 +191,12 @@ 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); +struct Hints(HashMap); #[allow(dead_code)] impl Hints { - fn prop(&self, name: &str) -> Option { - self.0.get(name)?.get() + fn prop>(&self, name: &str) -> Option { + T::try_from(self.0.get(name)?.clone()).ok() } fn actions_icon(&self) -> bool { @@ -162,13 +250,13 @@ 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::() { + if let Ok(v) = <&str>::try_from(v) { s.field(k, &v); - } else if let Some(v) = v.get::() { + } else if let Ok(v) = i32::try_from(v) { s.field(k, &v); - } else if let Some(v) = v.get::() { + } else if let Ok(v) = bool::try_from(v) { s.field(k, &v); - } else if let Some(v) = v.get::() { + } else if let Ok(v) = u8::try_from(v) { s.field(k, &v); } else { s.field(k, v); @@ -178,18 +266,6 @@ impl fmt::Debug for Hints { } } -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) - } -} - #[repr(transparent)] #[derive(Debug, Clone, Copy, Hash, glib::GBoxed, PartialEq, Eq)] #[gboxed(type_name = "S76NotificationId")] @@ -207,12 +283,6 @@ impl From for u32 { } } -impl ToVariant for NotificationId { - fn to_variant(&self) -> glib::Variant { - self.0.get().to_variant() - } -} - impl NotificationId { fn new(value: u32) -> Option { NonZeroU32::new(value).map(Self) @@ -244,14 +314,30 @@ impl Notifications { pub fn new() -> Self { let notifications = glib::Object::new::(&[]).unwrap(); - gio::bus_own_name( - gio::BusType::Session, - INTERFACE, - 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)), - ); + let (interface, mut receiver) = NotificationsInterface::new(); + notifications.inner().interface.set(interface); + + glib::MainContext::default().spawn_local(clone!(@strong notifications => async move { + let connection = match dbus_service::create(INTERFACE, |builder| builder.serve_at(PATH, notifications.inner().interface.clone())).await { + Ok(connection) => connection, + Err(err) => { + eprintln!("Failed to start `Notifications` service: {}", err); + return; + } + }; + let _ = notifications.inner().connection.set(connection.clone()); + + if let Some(event) = receiver.next().await { + match event { + Event::NotificationReceived(id) => { + notifications.emit_by_name("notification-received", &[&id]).unwrap(); + } + Event::CloseNotification(id) => { + notifications.close_notification(id, CloseReason::Call).await + } + } + } + })); notifications } @@ -260,155 +346,49 @@ impl Notifications { NotificationsInner::from_instance(self) } - fn next_id(&self) -> NotificationId { - let next_id = &self.inner().next_id; - let id = next_id.get(); - next_id.set(NotificationId::new(u32::from(id).wrapping_add(1)).unwrap_or_default()); - 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, - ) -> NotificationId { - // Ignores `expire-timeout`, like Gnome Shell - - let id = replaces_id.unwrap_or_else(|| self.next_id()); - - let notification = Rc::new(Notification { - id, - app_name, - app_icon, - summary, - body, - actions, - hints, - }); - + async fn close_notification(&self, id: NotificationId, reason: CloseReason) { self.inner() + .interface + .0 .notifications - .borrow_mut() - .insert(id, notification); - - self.emit_by_name("notification-received", &[&id]).unwrap(); - - id - } - - fn close_notification(&self, id: NotificationId, reason: CloseReason) { - self.inner().notifications.borrow_mut().remove(&id); + .lock() + .unwrap() + .remove(&id); self.emit_by_name("notification-closed", &[&id]).unwrap(); - if let Some(connection) = self.inner().connection.borrow().as_ref() { - connection - .emit_signal( - None, - PATH, - INTERFACE, - "CloseNotification", - Some(&(id, &(reason as u32)).to_variant()), - ) - .unwrap(); + if let Some(connection) = self.inner().connection.get() { + let ctxt = SignalContext::new(connection, PATH).unwrap(); // XXX unwrap? + let _ = + NotificationsInterface::NotificationClosed(&ctxt, id.into(), reason as u32).await; } } pub fn dismiss(&self, id: NotificationId) { - self.close_notification(id, CloseReason::Dismiss); + glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move { + self_.close_notification(id, CloseReason::Dismiss).await + })); } - pub fn invoke_action(&self, id: NotificationId, action_key: &str) { - if let Some(connection) = self.inner().connection.borrow().as_ref() { - connection - .emit_signal( - None, - PATH, - INTERFACE, - "ActionInvoked", - Some(&(&(id, action_key),).to_variant()), - ) - .unwrap(); + pub async fn invoke_action(&self, id: NotificationId, action_key: &str) { + if let Some(connection) = self.inner().connection.get() { + let ctxt = SignalContext::new(connection, PATH).unwrap(); // XXX unwrap? + let _ = NotificationsInterface::ActionInvoked(&ctxt, id.into(), action_key).await; } } - fn bus_acquired(&self, connection: gio::DBusConnection, _name: &str) { - *self.inner().connection.borrow_mut() = Some(connection); + pub fn get(&self, id: NotificationId) -> Option> { + self.inner() + .interface + .0 + .notifications + .lock() + .unwrap() + .get(&id) + .cloned() } - 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(INTERFACE).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 = NotificationId::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(); - if let Some(id) = NotificationId::new(id) { - self_.close_notification(id, CloseReason::Call); - } - 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( - PATH, - &interface_info, - method_call, - get_property, - set_property, - ) { - eprintln!("Failed to register object: {}", err); - } - } - - fn name_lost(&self, _connection: Option, _name: &str) {} - - pub fn get(&self, id: NotificationId) -> Option> { - self.inner().notifications.borrow().get(&id).cloned() - } - - pub fn connect_notification_recieved) + 'static>( + pub fn connect_notification_received) + 'static>( &self, cb: F, ) -> SignalHandlerId { diff --git a/src/status_area.rs b/src/status_area.rs index 2784035d..8babbf8e 100644 --- a/src/status_area.rs +++ b/src/status_area.rs @@ -1,12 +1,13 @@ use cascade::cascade; +use futures::stream::StreamExt; use gtk4::{ - gio, glib::{self, clone}, prelude::*, subclass::prelude::*, }; use once_cell::unsync::OnceCell; use std::{cell::RefCell, collections::HashMap}; +use zbus::dbus_proxy; use crate::deref_cell::DerefCell; use crate::status_menu::StatusMenu; @@ -14,7 +15,7 @@ use crate::status_menu::StatusMenu; #[derive(Default)] pub struct StatusAreaInner { box_: DerefCell, - watcher: OnceCell, + watcher: OnceCell>, icons: RefCell>, } @@ -39,35 +40,46 @@ impl ObjectImpl for StatusAreaInner { self.box_.set(box_); glib::MainContext::default().spawn_local(clone!(@strong obj => async move { - let watcher = match StatusNotifierWatcher::new().await { - Ok(watcher) => watcher, - Err(err) => { - eprintln!("Failed to connect to 'org.kde.StatusNotifierWatcher': {}", err); - return; + async { + let connection = zbus::Connection::session().await?; + let watcher = StatusNotifierWatcherProxy::new(&connection).await?; + + let name = connection.unique_name().unwrap().as_str(); + if let Err(err) = watcher.register_status_notifier_host(name).await { + eprintln!("Failed to register status notifier host: {}", err); } - }; - if let Err(err) = watcher.register_host().await { - eprintln!("Failed to register status notifier host: {}", err); - } + let mut registered_stream = watcher.receive_status_notifier_item_registered().await?; + let mut unregistered_stream = watcher.receive_status_notifier_item_unregistered().await?; - for name in watcher.registered_items().await { - glib::MainContext::default().spawn_local(clone!(@strong obj => async move { - obj.item_registered(&name).await; - })); - } - - watcher.connect_item_registered_unregistered(clone!(@strong obj => move |name, registered| { - if registered { + for name in watcher.registered_status_notifier_items().await? { glib::MainContext::default().spawn_local(clone!(@strong obj => async move { obj.item_registered(&name).await; })); - } else { - obj.item_unregistered(&name); } - })); - let _ = obj.inner().watcher.set(watcher); + glib::MainContext::default().spawn_local(clone!(@strong obj => async move { + if let Some(evt) = registered_stream.next().await { + if let Ok(args) = evt.args() { + obj.item_registered(&args.name).await; + } + } + })); + + glib::MainContext::default().spawn_local(clone!(@strong obj => async move { + if let Some(evt) = unregistered_stream.next().await { + if let Ok(args) = evt.args() { + obj.item_unregistered(&args.name); + } + } + })); + + let _ = obj.inner().watcher.set(watcher); + + Ok::<_, zbus::Error>(()) + }.await.unwrap_or_else(|err| { + eprintln!("Failed to connect to 'org.kde.StatusNotifierWatcher': {}", err); + }); })); } @@ -114,62 +126,20 @@ impl StatusArea { } } -struct StatusNotifierWatcher(gio::DBusProxy); +#[dbus_proxy( + interface = "org.kde.StatusNotifierWatcher", + default_service = "org.kde.StatusNotifierWatcher", + default_path = "/StatusNotifierWatcher" +)] +trait StatusNotifierWatcher { + fn register_status_notifier_host(&self, name: &str) -> zbus::Result<()>; -impl StatusNotifierWatcher { - async fn new() -> Result { - let proxy = gio::DBusProxy::for_bus_future( - gio::BusType::Session, - gio::DBusProxyFlags::NONE, - None, - "org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher", - "org.kde.StatusNotifierWatcher", - ) - .await?; - Ok(Self(proxy)) - } + #[dbus_proxy(property)] + fn registered_status_notifier_items(&self) -> zbus::Result>; - fn property(&self, prop: &str) -> Option { - self.0.cached_property(prop)?.get() - } + #[dbus_proxy(signal)] + fn status_notifier_item_registered(&self, name: &str) -> zbus::Result<()>; - async fn registered_items(&self) -> Vec { - self.property::>("RegisteredStatusNotifierItems") - .unwrap_or_default() - } - - fn connect_item_registered_unregistered( - &self, - f: F, - ) -> glib::SignalHandlerId { - self.0 - .connect_local("g-signal", false, move |args| { - let signal_args = args[3].get::().unwrap(); - match args[2].get::().unwrap().as_str() { - "StatusNotifierItemRegistered" => { - f(signal_args.get::<(String,)>().unwrap().0, true); - } - "StatusNotifierItemUnregistered" => { - f(signal_args.get::<(String,)>().unwrap().0, false); - } - _ => {} - } - None - }) - .unwrap() - } - - async fn register_host(&self) -> Result<(), glib::Error> { - let service = self.0.connection().unique_name().unwrap(); - self.0 - .call_future( - "RegisterStatusNotifierHost", - Some(&(service.as_str(),).to_variant()), - gio::DBusCallFlags::NONE, - 1000, - ) - .await?; - Ok(()) - } + #[dbus_proxy(signal)] + fn status_notifier_item_unregistered(&self, name: &str) -> zbus::Result<()>; } diff --git a/src/status_menu.rs b/src/status_menu.rs index 61b1d987..a3bf48bb 100644 --- a/src/status_menu.rs +++ b/src/status_menu.rs @@ -1,12 +1,14 @@ -use byte_string::ByteStr; use cascade::cascade; +use futures::StreamExt; use gtk4::{ - gdk_pixbuf, gio, + gdk_pixbuf, glib::{self, clone}, prelude::*, subclass::prelude::*, }; -use std::{borrow::Cow, cell::RefCell, collections::HashMap, fmt, io}; +use std::{cell::RefCell, collections::HashMap, io}; +use zbus::dbus_proxy; +use zvariant::OwnedValue; use crate::deref_cell::DerefCell; use crate::popover_container::PopoverContainer; @@ -21,8 +23,8 @@ pub struct StatusMenuInner { button: DerefCell, popover_container: DerefCell, vbox: DerefCell, - item: DerefCell, - dbus_menu: DerefCell, + item: DerefCell>, + dbus_menu: DerefCell>, menus: RefCell>, } @@ -73,18 +75,38 @@ glib::wrapper! { } impl StatusMenu { - pub async fn new(name: &str) -> Result { - let item = StatusNotifierItem::new(name).await?; - let obj = glib::Object::new::(&[]).unwrap(); - if let Some(icon_name) = item.icon_name().as_deref() { - obj.inner().button.set_icon_name(&icon_name); - } + pub async fn new(name: &str) -> zbus::Result { + let idx = name.find('/').unwrap(); + let dest = &name[..idx]; + let path = &name[idx..]; - let menu = item.menu().unwrap(); // XXX unwrap? + let connection = zbus::Connection::session().await?; + let item = StatusNotifierItemProxy::builder(&connection) + .destination(dest.to_string())? + .path(path.to_string())? + .build() + .await?; + let obj = glib::Object::new::(&[]).unwrap(); + let icon_name = item.icon_name().await?; + obj.inner().button.set_icon_name(&icon_name); + + let menu = item.menu().await?; + let menu = DBusMenuProxy::builder(&connection) + .destination(dest.to_string())? + .path(menu)? + .build() + .await?; let layout = menu.get_layout(0, -1, &[]).await?.1; - menu.connect_layout_updated(clone!(@weak obj => move |revision, parent| { - obj.layout_updated(revision, parent); + let mut layout_updated_stream = menu.receive_layout_updated().await?; + glib::MainContext::default().spawn_local(clone!(@strong obj => async move { + while let Some(evt) = layout_updated_stream.next().await { + let args = match evt.args() { + Ok(args) => args, + Err(_) => { continue; }, + }; + obj.layout_updated(args.revision, args.parent); + } })); obj.inner().item.set(item); @@ -140,7 +162,8 @@ impl StatusMenu { ..set_visible(i.visible()); }; box_.append(&separator); - } else if let Some(mut label) = i.label() { + } else if let Some(label) = i.label() { + let mut label = label.to_string(); if let Some(toggle_state) = i.toggle_state() { if toggle_state != 0 { label = format!("✓ {}", label); @@ -160,7 +183,7 @@ impl StatusMenu { }; if let Some(icon_data) = i.icon_data() { - let icon_data = io::Cursor::new(icon_data); + let icon_data = io::Cursor::new(icon_data.to_vec()); let pixbuf = gdk_pixbuf::Pixbuf::from_read(icon_data).unwrap(); // XXX unwrap let image = cascade! { gtk4::Image::from_pixbuf(Some(&pixbuf)); @@ -182,7 +205,9 @@ impl StatusMenu { if close_on_click { self_.inner().popover_container.popdown(); } - self_.inner().dbus_menu.event(id, "clicked", &0.to_variant(), 0); + glib::MainContext::default().spawn_local(clone!(@strong self_ => async move { + let _ = self_.inner().dbus_menu.event(id, "clicked", &0.into(), 0).await; + })) })); }; box_.append(&button); @@ -218,197 +243,111 @@ impl StatusMenu { } } -struct Layout(i32, HashMap, Vec); +#[dbus_proxy(interface = "org.kde.StatusNotifierItem")] +trait StatusNotifierItem { + #[dbus_proxy(property)] + fn icon_name(&self) -> zbus::Result; -impl fmt::Debug for Layout { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut s = f.debug_struct("Layout"); - s.field("id", &self.0); - for (k, v) in &self.1 { - 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, &ByteStr::new(&v)); - } else { - s.field(k, v); - } - } - s.field("children", &self.2); - s.finish() + #[dbus_proxy(property)] + fn menu(&self) -> zbus::Result; +} + +#[derive(Debug)] +pub struct Layout(i32, LayoutProps, Vec); + +impl<'a> serde::Deserialize<'a> for Layout { + fn deserialize>(deserializer: D) -> Result { + let (id, props, children) = + <(i32, LayoutProps, Vec<(zvariant::Signature<'_>, Self)>)>::deserialize(deserializer) + .unwrap(); + Ok(Self(id, props, children.into_iter().map(|x| x.1).collect())) } } +impl zvariant::Type for Layout { + fn signature() -> zvariant::Signature<'static> { + zvariant::Signature::try_from("(ia{sv}av)").unwrap() + } +} + +#[derive(Debug, zvariant::DeserializeDict, zvariant::Type)] +pub struct LayoutProps { + #[zvariant(rename = "accessible-desc")] + accessible_desc: Option, + #[zvariant(rename = "children-display")] + children_display: Option, + label: Option, + enabled: Option, + visible: Option, + #[zvariant(rename = "type")] + type_: Option, + #[zvariant(rename = "toggle-type")] + toggle_type: Option, + #[zvariant(rename = "toggle-state")] + toggle_state: Option, + #[zvariant(rename = "icon-data")] + icon_data: Option>, +} + #[allow(dead_code)] impl Layout { - fn prop(&self, name: &str) -> Option { - self.1.get(name)?.get() - } - fn id(&self) -> i32 { self.0 } - fn accessible_desc(&self) -> Option { - self.prop("accessible-desc") - } - - fn children_display(&self) -> Option { - self.prop("children-display") - } - - fn label(&self) -> Option { - self.prop("label") - } - - fn enabled(&self) -> bool { - self.prop("enabled").unwrap_or(true) - } - - fn visible(&self) -> bool { - self.prop("visible").unwrap_or(true) - } - - fn type_(&self) -> Option { - self.prop("type") - } - - fn toggle_type(&self) -> Option { - self.prop("toggle-type") - } - - fn toggle_state(&self) -> Option { - self.prop("toggle-state") - } - - fn icon_data(&self) -> Option> { - self.prop("icon-data") - } - fn children(&self) -> &[Self] { &self.2 } -} -impl glib::StaticVariantType for Layout { - fn static_variant_type() -> Cow<'static, glib::VariantTy> { - glib::VariantTy::new("(ia{sv}av)").unwrap().into() + fn accessible_desc(&self) -> Option<&str> { + self.1.accessible_desc.as_deref() + } + + fn children_display(&self) -> Option<&str> { + self.1.children_display.as_deref() + } + + fn label(&self) -> Option<&str> { + self.1.label.as_deref() + } + + fn enabled(&self) -> bool { + self.1.enabled.unwrap_or(true) + } + + fn visible(&self) -> bool { + self.1.visible.unwrap_or(true) + } + + fn type_(&self) -> Option<&str> { + self.1.type_.as_deref() + } + + fn toggle_type(&self) -> Option<&str> { + self.1.toggle_type.as_deref() + } + + fn toggle_state(&self) -> Option { + self.1.toggle_state + } + + fn icon_data(&self) -> Option<&[u8]> { + self.1.icon_data.as_deref() } } -impl glib::FromVariant for Layout { - fn from_variant(variant: &glib::Variant) -> Option { - let (id, props, children) = variant.get::<(_, _, Vec)>()?; - let children = children.iter().filter_map(Self::from_variant).collect(); - Some(Self(id, props, children)) - } -} - -#[derive(Clone)] -struct DBusMenu(gio::DBusProxy); - -impl DBusMenu { - async fn new(dest: &str, path: &str) -> Result { - let proxy = gio::DBusProxy::for_bus_future( - gio::BusType::Session, - gio::DBusProxyFlags::NONE, - None, - dest, - path, - "com.canonical.dbusmenu", - ) - .await?; - Ok(Self(proxy)) - } - - async fn get_layout( +#[dbus_proxy(interface = "com.canonical.dbusmenu")] +trait DBusMenu { + fn get_layout( &self, - parent: i32, - depth: i32, - properties: &[&str], - ) -> Result<(u32, Layout), glib::Error> { - // XXX unwrap - Ok(self - .0 - .call_future( - "GetLayout", - Some(&(parent, depth, properties).to_variant()), - gio::DBusCallFlags::NONE, - 1000, - ) - .await? - .get() - .unwrap()) - } + parent_id: i32, + recursion_depth: i32, + property_names: &[&str], + ) -> zbus::Result<(u32, Layout)>; - fn event(&self, id: i32, eventid: &str, data: &glib::Variant, timestamp: u32) { - let res = self.0.call_future( - "Event", - Some(&(id, eventid, data, timestamp).to_variant()), - gio::DBusCallFlags::NONE, - 1000, - ); - glib::MainContext::default().spawn_local(async move { - if let Err(err) = res.await { - eprintln!("Failed to call `Event`: {}", err); - } - }); - } + fn event(&self, id: i32, event_id: &str, data: &OwnedValue, timestamp: u32) + -> zbus::Result<()>; - fn connect_layout_updated(&self, f: F) -> glib::SignalHandlerId { - self.0 - .connect_local("g-signal", false, move |args| { - if &args[2].get::().unwrap() == "LayoutUpdated" { - // XXX unwrap - let (revision, parent) = args[3].get::().unwrap().get().unwrap(); - f(revision, parent); - } - None - }) - .unwrap() - } -} - -struct StatusNotifierItem(gio::DBusProxy, Option); - -impl StatusNotifierItem { - async fn new(name: &str) -> Result { - let idx = name.find('/').unwrap(); - let dest = &name[..idx]; - let path = &name[idx..]; - let proxy = gio::DBusProxy::for_bus_future( - gio::BusType::Session, - gio::DBusProxyFlags::NONE, - None, - dest, - path, - "org.kde.StatusNotifierItem", - ) - .await?; - let menu_path = proxy - .cached_property("Menu") - .and_then(|x| x.get::()); - let menu = if let Some(menu_path) = menu_path { - Some(DBusMenu::new(dest, &menu_path).await?) - } else { - None - }; - Ok(Self(proxy, menu)) - } - - fn property(&self, prop: &str) -> Option { - self.0.cached_property(prop)?.get() - } - - fn icon_name(&self) -> Option { - // TODO: IconThemePath? AttentionIconName? - self.property("IconName") - } - - fn menu(&self) -> Option { - self.1.clone() - } + #[dbus_proxy(signal)] + fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>; } diff --git a/src/status_notifier_watcher.rs b/src/status_notifier_watcher.rs index 54dee592..2f012187 100644 --- a/src/status_notifier_watcher.rs +++ b/src/status_notifier_watcher.rs @@ -1,106 +1,70 @@ -use gtk4::{ - gio, - glib::{self, clone}, - prelude::*, -}; +#![allow(non_snake_case)] + use std::sync::{Arc, Mutex}; +use zbus::{dbus_interface, MessageHeader, Result, SignalContext}; -static STATUS_NOTIFIER_XML: &str = " - - - - - +use crate::dbus_service; - - - - - - - - - - - - - - - - - - - - - -"; - -pub fn start() { - gio::bus_own_name( - gio::BusType::Session, - "org.kde.StatusNotifierWatcher", - gio::BusNameOwnerFlags::NONE, - bus_acquired, - name_acquired, - name_lost, - ); +#[derive(Default)] +struct StatusNotifierWatcher { + items: Arc>>, } -fn bus_acquired(_connection: gio::DBusConnection, _name: &str) {} - -fn name_acquired(connection: gio::DBusConnection, _name: &str) { - let introspection_data = gio::DBusNodeInfo::for_xml(STATUS_NOTIFIER_XML).unwrap(); - let interface_info = introspection_data - .lookup_interface("org.kde.StatusNotifierWatcher") - .unwrap(); - let items = Arc::new(Mutex::new(Vec::::new())); - let method_call = clone!(@strong items => move |connection: gio::DBusConnection, - sender: &str, - path: &str, - interface: &str, - method: &str, - args: glib::Variant, - invocation: gio::DBusMethodInvocation| { - match method { - "RegisterStatusNotifierItem" => { - let (service,) = args.get::<(String,)>().unwrap(); - let service = format!("{}{}", sender, service); - connection.emit_signal(None, path, interface, "StatusNotifierItemRegistered", Some(&(&service,).to_variant())).unwrap(); - // XXX emit unreigstered - items.lock().unwrap().push(service); - invocation.return_value(None); - } - "RegisterStatusNotifierHost" => { - let (_service,) = args.get::<(String,)>().unwrap(); - // XXX emit registed/unregistered - invocation.return_value(None); - } - _ => unreachable!() - } - }); - let get_property = clone!(@strong items => move |_: gio::DBusConnection, _sender: &str, _path: &str, _interface: &str, prop: &str| { - match prop { - "RegisteredStatusNotifierItems" => items.lock().unwrap().to_variant(), - "IsStatusNotifierHostRegistered" => true.to_variant(), - "ProtocolVersion" => 0i32.to_variant(), - _ => 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( - "/StatusNotifierWatcher", - &interface_info, - method_call, - get_property, - set_property, +#[dbus_interface(name = "org.kde.StatusNotifierWatcher")] +impl StatusNotifierWatcher { + async fn RegisterStatusNotifierItem( + &self, + service: &str, + #[zbus(header)] hdr: MessageHeader<'_>, + #[zbus(signal_context)] ctxt: SignalContext<'_>, ) { - eprintln!("Failed to register object: {}", err); + let service = format!("{}{}", hdr.sender().unwrap().unwrap(), service); + Self::StatusNotifierItemRegistered(&ctxt, &service) + .await + .unwrap(); + + // XXX emit unreigstered + self.items.lock().unwrap().push(service); + } + + fn RegisterStatusNotifierHost(&self, _service: &str) { + // XXX emit registed/unregistered + } + + #[dbus_interface(property)] + fn RegisteredStatusNotifierItems(&self) -> Vec { + self.items.lock().unwrap().clone() + } + + #[dbus_interface(property)] + fn IsStatusNotifierHostRegistered(&self) -> bool { + true + } + + #[dbus_interface(property)] + fn ProtocolVersion(&self) -> i32 { + 0 + } + + #[dbus_interface(signal)] + async fn StatusNotifierItemRegistered(ctxt: &SignalContext<'_>, service: &str) -> Result<()>; + + #[dbus_interface(signal)] + async fn StatusNotifierItemUnregistered(ctxt: &SignalContext<'_>, service: &str) -> Result<()>; + + #[dbus_interface(signal)] + async fn StatusNotifierHostRegistered(ctxt: &SignalContext<'_>) -> Result<()>; + + #[dbus_interface(signal)] + async fn StatusNotifierHostUnregistered(ctxt: &SignalContext<'_>) -> Result<()>; +} + +pub async fn start() { + if let Err(err) = dbus_service::create("org.kde.StatusNotifierWatcher", |builder| { + builder.serve_at("/StatusNotifierWatcher", StatusNotifierWatcher::default()) + }) + .await + { + eprintln!("Failed to start `StatusNotifierWatcher` service: {}", err); } } - -fn name_lost(_connection: Option, _name: &str) {}