Port all dbus server/client to to zbus

Seems to generally be working well. May still need a few fixes.
This commit is contained in:
Ian Douglas Scott 2021-12-10 16:32:17 -08:00 committed by Ian Douglas Scott
parent 8b5e1a7f12
commit 8b2a9c6359
13 changed files with 592 additions and 821 deletions

137
Cargo.lock generated
View file

@ -452,37 +452,16 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 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]] [[package]]
name = "enumflags2" name = "enumflags2"
version = "0.7.3" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def" checksum = "a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def"
dependencies = [ dependencies = [
"enumflags2_derive 0.7.3", "enumflags2_derive",
"serde", "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]] [[package]]
name = "enumflags2_derive" name = "enumflags2_derive"
version = "0.7.3" version = "0.7.3"
@ -847,7 +826,7 @@ checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"heck", "heck",
"proc-macro-crate 1.1.0", "proc-macro-crate",
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -962,7 +941,7 @@ dependencies = [
"anyhow", "anyhow",
"heck", "heck",
"itertools", "itertools",
"proc-macro-crate 1.1.0", "proc-macro-crate",
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1080,9 +1059,9 @@ dependencies = [
"x11", "x11",
"x11rb", "x11rb",
"xdg", "xdg",
"zbus 2.0.0", "zbus",
"zvariant 3.0.0", "zvariant",
"zvariant_derive 3.0.0", "zvariant_derive",
] ]
[[package]] [[package]]
@ -1119,16 +1098,6 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "nix" name = "nix"
version = "0.20.2" version = "0.20.2"
@ -1338,15 +1307,21 @@ dependencies = [
"cascade", "cascade",
"chrono", "chrono",
"derivative", "derivative",
"enumflags2",
"futures",
"futures-channel",
"gdk4-wayland", "gdk4-wayland",
"gdk4-x11", "gdk4-x11",
"gobject-sys", "gobject-sys",
"gtk4", "gtk4",
"libcosmic", "libcosmic",
"once_cell", "once_cell",
"serde",
"toml", "toml",
"x11", "x11",
"zbus 1.9.2", "zbus",
"zbus_names",
"zvariant",
] ]
[[package]] [[package]]
@ -1415,15 +1390,6 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 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]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "1.1.0" version = "1.1.0"
@ -2113,29 +2079,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" 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]] [[package]]
name = "zbus" name = "zbus"
version = "2.0.0" version = "2.0.0"
@ -2152,7 +2095,7 @@ dependencies = [
"async-trait", "async-trait",
"byteorder", "byteorder",
"derivative", "derivative",
"enumflags2 0.7.3", "enumflags2",
"event-listener", "event-listener",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -2166,21 +2109,9 @@ dependencies = [
"serde_repr", "serde_repr",
"sha1", "sha1",
"static_assertions", "static_assertions",
"zbus_macros 2.0.0", "zbus_macros",
"zbus_names", "zbus_names",
"zvariant 3.0.0", "zvariant",
]
[[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",
] ]
[[package]] [[package]]
@ -2189,7 +2120,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd2ea67f43e8abd245eabc480e597990340d9870b585d40bf4350d742acb2219" checksum = "fd2ea67f43e8abd245eabc480e597990340d9870b585d40bf4350d742acb2219"
dependencies = [ dependencies = [
"proc-macro-crate 1.1.0", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
@ -2204,21 +2135,7 @@ checksum = "ae1f142d242d6854815a8c5c2aea83d9508f72f5757d0a137c21ef4b07bfee66"
dependencies = [ dependencies = [
"serde", "serde",
"static_assertions", "static_assertions",
"zvariant 3.0.0", "zvariant",
]
[[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",
] ]
[[package]] [[package]]
@ -2228,23 +2145,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a946c049b2eac1a253f98e9267a8ce7a3d93be274ea146e6dd7a0965232a911" checksum = "4a946c049b2eac1a253f98e9267a8ce7a3d93be274ea146e6dd7a0965232a911"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"enumflags2 0.7.3", "enumflags2",
"libc", "libc",
"serde", "serde",
"static_assertions", "static_assertions",
"zvariant_derive 3.0.0", "zvariant_derive",
]
[[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",
] ]
[[package]] [[package]]
@ -2253,7 +2158,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28fce5afb8d639bff79b1e8cdb258a3ca22d458f4603b23d794b4cb4e878c990" checksum = "28fce5afb8d639bff79b1e8cdb258a3ca22d458f4603b23d794b4cb4e878c990"
dependencies = [ dependencies = [
"proc-macro-crate 1.1.0", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",

View file

@ -1,7 +1,7 @@
[package] [package]
name = "pop-cosmic-panel" name = "pop-cosmic-panel"
version = "0.1.0" version = "0.1.0"
edition = "2018" edition = "2021"
license = "LGPL-3.0-or-later" license = "LGPL-3.0-or-later"
[dependencies] [dependencies]
@ -9,15 +9,21 @@ cascade = "1"
chrono = "0.4" chrono = "0.4"
byte_string = "1" byte_string = "1"
derivative = "2" derivative = "2"
enumflags2 = "0.7"
futures = "0.3"
futures-channel = "0.3"
gdk4-x11 = "0.3" gdk4-x11 = "0.3"
gdk4-wayland = { version = "0.3", optional = true } gdk4-wayland = { version = "0.3", optional = true }
gtk4 = "0.3" gtk4 = "0.3"
gobject-sys = "0.14.0" gobject-sys = "0.14.0"
libcosmic = { git = "https://github.com/pop-os/libcosmic" } libcosmic = { git = "https://github.com/pop-os/libcosmic" }
once_cell = "1" once_cell = "1"
serde = "1"
toml = "0.5" toml = "0.5"
x11 = { version = "2", features = ["xlib"] } x11 = { version = "2", features = ["xlib"] }
zbus = "1" zbus = "2"
zbus_names = "2"
zvariant = "3"
[features] [features]
layer-shell = ["gdk4-wayland", "libcosmic/layer-shell"] layer-shell = ["gdk4-wayland", "libcosmic/layer-shell"]

View file

@ -30,7 +30,7 @@ impl ObjectImpl for PanelAppInner {
self.parent_constructed(obj); self.parent_constructed(obj);
status_notifier_watcher::start(); glib::MainContext::default().spawn_local(status_notifier_watcher::start());
self.notifications.set(Notifications::new()); self.notifications.set(Notifications::new());
} }
} }

53
src/dbus_service.rs Normal file
View file

@ -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<zbus::ConnectionBuilder<'static>>,
>(
well_known_name: &'static str,
serve_cb: F,
) -> zbus::Result<zbus::Connection> {
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)
}

View file

@ -1,6 +1,7 @@
use gtk4::prelude::*; use gtk4::{glib, prelude::*};
mod application; mod application;
mod dbus_service;
mod deref_cell; mod deref_cell;
mod mpris; mod mpris;
mod mpris_player; mod mpris_player;
@ -18,5 +19,5 @@ mod window;
use application::PanelApp; use application::PanelApp;
fn main() { fn main() {
PanelApp::new().run(); glib::MainContext::default().with_thread_default(|| PanelApp::new().run());
} }

View file

@ -1,12 +1,13 @@
use cascade::cascade; use cascade::cascade;
use futures::stream::StreamExt;
use gtk4::{ use gtk4::{
gio,
glib::{self, clone}, glib::{self, clone},
prelude::*, prelude::*,
subclass::prelude::*, subclass::prelude::*,
}; };
use once_cell::unsync::OnceCell; use once_cell::unsync::OnceCell;
use std::{cell::RefCell, collections::HashMap}; use std::{cell::RefCell, collections::HashMap};
use zbus::fdo::DBusProxy;
use crate::deref_cell::DerefCell; use crate::deref_cell::DerefCell;
use crate::mpris_player::MprisPlayer; use crate::mpris_player::MprisPlayer;
@ -14,7 +15,7 @@ use crate::mpris_player::MprisPlayer;
#[derive(Default)] #[derive(Default)]
pub struct MprisControlsInner { pub struct MprisControlsInner {
listbox: DerefCell<gtk4::ListBox>, listbox: DerefCell<gtk4::ListBox>,
dbus: OnceCell<DBus>, dbus: OnceCell<DBusProxy<'static>>,
players: RefCell<HashMap<String, MprisPlayer>>, players: RefCell<HashMap<String, MprisPlayer>>,
} }
@ -37,23 +38,32 @@ impl ObjectImpl for MprisControlsInner {
}; };
glib::MainContext::default().spawn_local(clone!(@strong obj => async move { glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
let dbus = match DBus::new().await { let (dbus, mut name_owner_changed_stream) = match async {
Ok(dbus) => dbus, 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) => { Err(err) => {
eprintln!("Failed to connect to 'org.freedesktop.DBus': {}", err); eprintln!("Failed to connect to 'org.freedesktop.DBus': {}", err);
return; return;
} }
}; };
dbus.connect_name_owner_changed(clone!(@strong obj => move |name, old, new| { glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
if name.starts_with("org.mpris.MediaPlayer2.") { while let Some(evt) = name_owner_changed_stream.next().await {
if !old.is_empty() { let args = match evt.args() {
obj.player_removed(&name); Ok(args) => args,
} Err(_) => { continue; },
if !new.is_empty() { };
glib::MainContext::default().spawn_local(clone!(@strong obj => async move { if args.name.starts_with("org.mpris.MediaPlayer2.") {
obj.player_added(&name).await; 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); self.inner().players.borrow_mut().remove(name);
} }
} }
struct DBus(gio::DBusProxy);
impl DBus {
async fn new() -> Result<Self, glib::Error> {
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<impl Iterator<Item = String>, glib::Error> {
Ok(self
.0
.call_future("ListNames", None, gio::DBusCallFlags::NONE, 1000)
.await?
.child_value(0)
.iter()
.filter_map(|x| x.get::<String>()))
}
fn connect_name_owner_changed<F: Fn(String, String, String) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.0
.connect_local("g-signal", false, move |args| {
if &args[2].get::<String>().unwrap() == "NameOwnerChanged" {
let (name, old, new) = args[3].get::<glib::Variant>().unwrap().get().unwrap();
f(name, old, new);
}
None
})
.unwrap()
}
}

View file

@ -1,4 +1,5 @@
use cascade::cascade; use cascade::cascade;
use futures::StreamExt;
use gtk4::{ use gtk4::{
gdk_pixbuf, gio, gdk_pixbuf, gio,
glib::{self, clone}, glib::{self, clone},
@ -6,7 +7,9 @@ use gtk4::{
prelude::*, prelude::*,
subclass::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; use crate::deref_cell::DerefCell;
@ -16,7 +19,7 @@ pub struct MprisPlayerInner {
backward_button: DerefCell<gtk4::Button>, backward_button: DerefCell<gtk4::Button>,
play_pause_button: DerefCell<gtk4::Button>, play_pause_button: DerefCell<gtk4::Button>,
forward_button: DerefCell<gtk4::Button>, forward_button: DerefCell<gtk4::Button>,
player: DerefCell<Player>, player: DerefCell<PlayerProxy<'static>>,
image: DerefCell<gtk4::Image>, image: DerefCell<gtk4::Image>,
image_uri: RefCell<Option<String>>, image_uri: RefCell<Option<String>>,
title_label: DerefCell<gtk4::Label>, title_label: DerefCell<gtk4::Label>,
@ -114,14 +117,29 @@ glib::wrapper! {
} }
impl MprisPlayer { impl MprisPlayer {
pub async fn new(name: &str) -> Result<Self, glib::Error> { pub async fn new(name: &str) -> zbus::Result<Self> {
let obj = glib::Object::new::<Self>(&[]).unwrap(); let obj = glib::Object::new::<Self>(&[]).unwrap();
let player = Player::new(name).await?; let connection = zbus::Connection::session().await?;
player.connect_properties_changed(clone!(@weak obj => move |_player| { let player = PlayerProxy::builder(&connection)
obj.update(); .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); 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(); obj.update();
Ok(obj) Ok(obj)
@ -133,7 +151,7 @@ impl MprisPlayer {
fn call(&self, method: &'static str) { fn call(&self, method: &'static str) {
glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move { 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); eprintln!("Failed to call '{}': {}", method, err);
} }
})); }));
@ -164,8 +182,8 @@ impl MprisPlayer {
let player = &self.inner().player; let player = &self.inner().player;
// XXX status // XXX status
let (status, metadata) = match (player.playback_status(), player.metadata()) { let (status, metadata) = match (player.cached_playback_status(), player.cached_metadata()) {
(Some(status), Some(metadata)) => (status, metadata), (Ok(Some(status)), Ok(Some(metadata))) => (status, Metadata(metadata)),
_ => return, _ => return,
}; };
@ -197,11 +215,11 @@ impl MprisPlayer {
} }
} }
struct Metadata(glib::VariantDict); struct Metadata(HashMap<String, OwnedValue>);
impl Metadata { impl Metadata {
fn lookup<T: glib::FromVariant>(&self, key: &str) -> Option<T> { fn lookup<'a, T: TryFrom<OwnedValue>>(&self, key: &str) -> Option<T> {
self.0.lookup_value(key, None)?.get() T::try_from(self.0.get(key)?.clone()).ok()
} }
fn title(&self) -> Option<String> { fn title(&self) -> Option<String> {
@ -221,53 +239,14 @@ impl Metadata {
} }
} }
#[derive(Clone)] #[dbus_proxy(
struct Player(gio::DBusProxy); interface = "org.mpris.MediaPlayer2.Player",
default_path = "/org/mpris/MediaPlayer2"
)]
trait Player {
#[dbus_proxy(property)]
fn metadata(&self) -> zbus::Result<HashMap<String, OwnedValue>>;
impl Player { #[dbus_proxy(property)]
async fn new(name: &str) -> Result<Self, glib::Error> { fn playback_status(&self) -> zbus::Result<String>;
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<T: glib::FromVariant>(&self, prop: &str) -> Option<T> {
self.0.cached_property(prop)?.get()
}
fn connect_properties_changed<F: Fn(Self) + 'static>(&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<String> {
self.property("PlaybackStatus")
}
fn metadata(&self) -> Option<Metadata> {
Some(Metadata(self.property("Metadata")?))
}
} }

View file

@ -36,7 +36,10 @@ impl ObjectImpl for NotificationListInner {
..set_parent(obj); ..set_parent(obj);
..connect_row_activated(clone!(@weak obj => move |_, row| { ..connect_row_activated(clone!(@weak obj => move |_, row| {
if let Some(id) = obj.id_for_row(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().notifications.set(notifications.clone());
*obj.inner().ids.borrow_mut() = vec![ *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(&notification); obj.handle_notification(&notification);
})), })),
notifications.connect_notification_closed(clone!(@weak obj => move |id| { notifications.connect_notification_closed(clone!(@weak obj => move |id| {

View file

@ -39,7 +39,10 @@ impl ObjectImpl for NotificationPopoverInner {
return; return;
} }
if let Some(id) = obj.id() { 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(); obj.popdown();
})); }));
@ -83,7 +86,7 @@ impl NotificationPopover {
obj.inner().notifications.set(notifications.clone()); obj.inner().notifications.set(notifications.clone());
*obj.inner().ids.borrow_mut() = vec![ *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(&notification); obj.handle_notification(&notification);
})), })),
notifications.connect_notification_closed(clone!(@weak obj => move |id| { notifications.connect_notification_closed(clone!(@weak obj => move |id| {

View file

@ -1,69 +1,161 @@
#![allow(non_snake_case)]
use futures::stream::StreamExt;
use futures_channel::mpsc;
use gtk4::{ use gtk4::{
gio,
glib::{self, clone, subclass::Signal, SignalHandlerId}, glib::{self, clone, subclass::Signal, SignalHandlerId},
prelude::*, prelude::*,
subclass::prelude::*, subclass::prelude::*,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use once_cell::unsync::OnceCell;
use std::{ use std::{
borrow::Cow,
cell::{Cell, RefCell},
collections::HashMap, collections::HashMap,
convert::TryFrom,
fmt, fmt,
num::NonZeroU32, 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 PATH: &str = "/org/freedesktop/Notifications";
static INTERFACE: &str = "org.freedesktop.Notifications"; static INTERFACE: &str = "org.freedesktop.Notifications";
static NOTIFICATIONS_XML: &str = "
<node name='/org/freedesktop/Notifications'>
<interface name='org.freedesktop.Notifications'>
<method name='Notify'>
<arg type='s' name='app_name' direction='in'/>
<arg type='u' name='replaces_id' direction='in'/>
<arg type='s' name='app_icon' direction='in'/>
<arg type='s' name='summary' direction='in'/>
<arg type='s' name='body' direction='in'/>
<arg type='as' name='actions' direction='in'/>
<arg type='a{sv}' name='hints' direction='in'/>
<arg type='i' name='expire_timeout' direction='in'/>
<arg type='u' name='id' direction='out'/>
</method>
<method name='CloseNotification'> enum Event {
<arg type='u' name='id' direction='in'/> NotificationReceived(NotificationId),
</method> CloseNotification(NotificationId),
}
<method name='GetCapabilities'> pub struct NotificationsInterfaceInner {
<arg type='as' direction='out'/> next_id: Mutex<NotificationId>,
</method> notifications: Mutex<HashMap<NotificationId, Arc<Notification>>>,
sender: mpsc::UnboundedSender<Event>,
}
<method name='GetServerInformation'> #[derive(Clone)]
<arg type='s' name='name' direction='out'/> pub struct NotificationsInterface(Arc<NotificationsInterfaceInner>);
<arg type='s' name='vendor' direction='out'/>
<arg type='s' name='version' direction='out'/>
<arg type='s' name='spec_version' direction='out'/>
</method>
<signal name='NotificationClosed'> impl NotificationsInterface {
<arg type='u' name='id'/> fn new() -> (Self, mpsc::UnboundedReceiver<Event>) {
<arg type='u' name='reason'/> let (sender, receiver) = mpsc::unbounded();
</signal> (
Self(Arc::new(NotificationsInterfaceInner {
next_id: Default::default(),
notifications: Default::default(),
sender,
})),
receiver,
)
}
<signal name='ActionInvoked'> fn next_id(&self) -> NotificationId {
<arg type='u' name='id'/> let mut next_id = self.0.next_id.lock().unwrap();
<arg type='s' name='action_key'/> let id = *next_id;
</signal> *next_id = NotificationId::new(u32::from(id).wrapping_add(1)).unwrap_or_default();
</interface> id
</node> }
";
fn handle_notify(
&self,
app_name: String,
replaces_id: Option<NotificationId>,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
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<String>,
hints: HashMap<String, OwnedValue>,
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)] #[derive(Default)]
pub struct NotificationsInner { pub struct NotificationsInner {
next_id: Cell<NotificationId>, interface: DerefCell<NotificationsInterface>,
notifications: RefCell<HashMap<NotificationId, Rc<Notification>>>, connection: OnceCell<zbus::Connection>,
connection: RefCell<Option<gio::DBusConnection>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -99,16 +191,12 @@ glib::wrapper! {
pub struct Notifications(ObjectSubclass<NotificationsInner>); pub struct Notifications(ObjectSubclass<NotificationsInner>);
} }
// XXX hack: https://github.com/gtk-rs/gtk-rs-core/issues/263 struct Hints(HashMap<String, OwnedValue>);
unsafe impl Send for Notifications {}
unsafe impl Sync for Notifications {}
struct Hints(HashMap<String, glib::Variant>);
#[allow(dead_code)] #[allow(dead_code)]
impl Hints { impl Hints {
fn prop<T: glib::FromVariant>(&self, name: &str) -> Option<T> { fn prop<T: TryFrom<OwnedValue>>(&self, name: &str) -> Option<T> {
self.0.get(name)?.get() T::try_from(self.0.get(name)?.clone()).ok()
} }
fn actions_icon(&self) -> bool { fn actions_icon(&self) -> bool {
@ -162,13 +250,13 @@ impl fmt::Debug for Hints {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = f.debug_struct("Hints"); let mut s = f.debug_struct("Hints");
for (k, v) in &self.0 { for (k, v) in &self.0 {
if let Some(v) = v.get::<String>() { if let Ok(v) = <&str>::try_from(v) {
s.field(k, &v); s.field(k, &v);
} else if let Some(v) = v.get::<i32>() { } else if let Ok(v) = i32::try_from(v) {
s.field(k, &v); s.field(k, &v);
} else if let Some(v) = v.get::<bool>() { } else if let Ok(v) = bool::try_from(v) {
s.field(k, &v); s.field(k, &v);
} else if let Some(v) = v.get::<u8>() { } else if let Ok(v) = u8::try_from(v) {
s.field(k, &v); s.field(k, &v);
} else { } else {
s.field(k, v); 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<Self> {
variant.get().map(Self)
}
}
#[repr(transparent)] #[repr(transparent)]
#[derive(Debug, Clone, Copy, Hash, glib::GBoxed, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Hash, glib::GBoxed, PartialEq, Eq)]
#[gboxed(type_name = "S76NotificationId")] #[gboxed(type_name = "S76NotificationId")]
@ -207,12 +283,6 @@ impl From<NotificationId> for u32 {
} }
} }
impl ToVariant for NotificationId {
fn to_variant(&self) -> glib::Variant {
self.0.get().to_variant()
}
}
impl NotificationId { impl NotificationId {
fn new(value: u32) -> Option<Self> { fn new(value: u32) -> Option<Self> {
NonZeroU32::new(value).map(Self) NonZeroU32::new(value).map(Self)
@ -244,14 +314,30 @@ impl Notifications {
pub fn new() -> Self { pub fn new() -> Self {
let notifications = glib::Object::new::<Self>(&[]).unwrap(); let notifications = glib::Object::new::<Self>(&[]).unwrap();
gio::bus_own_name( let (interface, mut receiver) = NotificationsInterface::new();
gio::BusType::Session, notifications.inner().interface.set(interface);
INTERFACE,
gio::BusNameOwnerFlags::NONE, glib::MainContext::default().spawn_local(clone!(@strong notifications => async move {
clone!(@strong notifications => move |connection, name| notifications.bus_acquired(connection, name)), let connection = match dbus_service::create(INTERFACE, |builder| builder.serve_at(PATH, notifications.inner().interface.clone())).await {
clone!(@strong notifications => move |connection, name| notifications.name_acquired(connection, name)), Ok(connection) => connection,
clone!(@strong notifications => move |connection, name| notifications.name_lost(connection, name)), 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 notifications
} }
@ -260,155 +346,49 @@ impl Notifications {
NotificationsInner::from_instance(self) NotificationsInner::from_instance(self)
} }
fn next_id(&self) -> NotificationId { async fn close_notification(&self, id: NotificationId, reason: CloseReason) {
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<NotificationId>,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
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,
});
self.inner() self.inner()
.interface
.0
.notifications .notifications
.borrow_mut() .lock()
.insert(id, notification); .unwrap()
.remove(&id);
self.emit_by_name("notification-received", &[&id]).unwrap();
id
}
fn close_notification(&self, id: NotificationId, reason: CloseReason) {
self.inner().notifications.borrow_mut().remove(&id);
self.emit_by_name("notification-closed", &[&id]).unwrap(); self.emit_by_name("notification-closed", &[&id]).unwrap();
if let Some(connection) = self.inner().connection.borrow().as_ref() { if let Some(connection) = self.inner().connection.get() {
connection let ctxt = SignalContext::new(connection, PATH).unwrap(); // XXX unwrap?
.emit_signal( let _ =
None, NotificationsInterface::NotificationClosed(&ctxt, id.into(), reason as u32).await;
PATH,
INTERFACE,
"CloseNotification",
Some(&(id, &(reason as u32)).to_variant()),
)
.unwrap();
} }
} }
pub fn dismiss(&self, id: NotificationId) { 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) { pub async fn invoke_action(&self, id: NotificationId, action_key: &str) {
if let Some(connection) = self.inner().connection.borrow().as_ref() { if let Some(connection) = self.inner().connection.get() {
connection let ctxt = SignalContext::new(connection, PATH).unwrap(); // XXX unwrap?
.emit_signal( let _ = NotificationsInterface::ActionInvoked(&ctxt, id.into(), action_key).await;
None,
PATH,
INTERFACE,
"ActionInvoked",
Some(&(&(id, action_key),).to_variant()),
)
.unwrap();
} }
} }
fn bus_acquired(&self, connection: gio::DBusConnection, _name: &str) { pub fn get(&self, id: NotificationId) -> Option<Arc<Notification>> {
*self.inner().connection.borrow_mut() = Some(connection); self.inner()
.interface
.0
.notifications
.lock()
.unwrap()
.get(&id)
.cloned()
} }
fn name_acquired(&self, connection: gio::DBusConnection, _name: &str) { pub fn connect_notification_received<F: Fn(Arc<Notification>) + 'static>(
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<gio::DBusConnection>, _name: &str) {}
pub fn get(&self, id: NotificationId) -> Option<Rc<Notification>> {
self.inner().notifications.borrow().get(&id).cloned()
}
pub fn connect_notification_recieved<F: Fn(Rc<Notification>) + 'static>(
&self, &self,
cb: F, cb: F,
) -> SignalHandlerId { ) -> SignalHandlerId {

View file

@ -1,12 +1,13 @@
use cascade::cascade; use cascade::cascade;
use futures::stream::StreamExt;
use gtk4::{ use gtk4::{
gio,
glib::{self, clone}, glib::{self, clone},
prelude::*, prelude::*,
subclass::prelude::*, subclass::prelude::*,
}; };
use once_cell::unsync::OnceCell; use once_cell::unsync::OnceCell;
use std::{cell::RefCell, collections::HashMap}; use std::{cell::RefCell, collections::HashMap};
use zbus::dbus_proxy;
use crate::deref_cell::DerefCell; use crate::deref_cell::DerefCell;
use crate::status_menu::StatusMenu; use crate::status_menu::StatusMenu;
@ -14,7 +15,7 @@ use crate::status_menu::StatusMenu;
#[derive(Default)] #[derive(Default)]
pub struct StatusAreaInner { pub struct StatusAreaInner {
box_: DerefCell<gtk4::Box>, box_: DerefCell<gtk4::Box>,
watcher: OnceCell<StatusNotifierWatcher>, watcher: OnceCell<StatusNotifierWatcherProxy<'static>>,
icons: RefCell<HashMap<String, StatusMenu>>, icons: RefCell<HashMap<String, StatusMenu>>,
} }
@ -39,35 +40,46 @@ impl ObjectImpl for StatusAreaInner {
self.box_.set(box_); self.box_.set(box_);
glib::MainContext::default().spawn_local(clone!(@strong obj => async move { glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
let watcher = match StatusNotifierWatcher::new().await { async {
Ok(watcher) => watcher, let connection = zbus::Connection::session().await?;
Err(err) => { let watcher = StatusNotifierWatcherProxy::new(&connection).await?;
eprintln!("Failed to connect to 'org.kde.StatusNotifierWatcher': {}", err);
return; 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 { let mut registered_stream = watcher.receive_status_notifier_item_registered().await?;
eprintln!("Failed to register status notifier host: {}", err); let mut unregistered_stream = watcher.receive_status_notifier_item_unregistered().await?;
}
for name in watcher.registered_items().await { for name in watcher.registered_status_notifier_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 {
glib::MainContext::default().spawn_local(clone!(@strong obj => async move { glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
obj.item_registered(&name).await; 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 { #[dbus_proxy(property)]
async fn new() -> Result<Self, glib::Error> { fn registered_status_notifier_items(&self) -> zbus::Result<Vec<String>>;
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))
}
fn property<T: glib::FromVariant>(&self, prop: &str) -> Option<T> { #[dbus_proxy(signal)]
self.0.cached_property(prop)?.get() fn status_notifier_item_registered(&self, name: &str) -> zbus::Result<()>;
}
async fn registered_items(&self) -> Vec<String> { #[dbus_proxy(signal)]
self.property::<Vec<String>>("RegisteredStatusNotifierItems") fn status_notifier_item_unregistered(&self, name: &str) -> zbus::Result<()>;
.unwrap_or_default()
}
fn connect_item_registered_unregistered<F: Fn(String, bool) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.0
.connect_local("g-signal", false, move |args| {
let signal_args = args[3].get::<glib::Variant>().unwrap();
match args[2].get::<String>().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(())
}
} }

View file

@ -1,12 +1,14 @@
use byte_string::ByteStr;
use cascade::cascade; use cascade::cascade;
use futures::StreamExt;
use gtk4::{ use gtk4::{
gdk_pixbuf, gio, gdk_pixbuf,
glib::{self, clone}, glib::{self, clone},
prelude::*, prelude::*,
subclass::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::deref_cell::DerefCell;
use crate::popover_container::PopoverContainer; use crate::popover_container::PopoverContainer;
@ -21,8 +23,8 @@ pub struct StatusMenuInner {
button: DerefCell<gtk4::ToggleButton>, button: DerefCell<gtk4::ToggleButton>,
popover_container: DerefCell<PopoverContainer>, popover_container: DerefCell<PopoverContainer>,
vbox: DerefCell<gtk4::Box>, vbox: DerefCell<gtk4::Box>,
item: DerefCell<StatusNotifierItem>, item: DerefCell<StatusNotifierItemProxy<'static>>,
dbus_menu: DerefCell<DBusMenu>, dbus_menu: DerefCell<DBusMenuProxy<'static>>,
menus: RefCell<HashMap<i32, Menu>>, menus: RefCell<HashMap<i32, Menu>>,
} }
@ -73,18 +75,38 @@ glib::wrapper! {
} }
impl StatusMenu { impl StatusMenu {
pub async fn new(name: &str) -> Result<Self, glib::Error> { pub async fn new(name: &str) -> zbus::Result<Self> {
let item = StatusNotifierItem::new(name).await?; let idx = name.find('/').unwrap();
let obj = glib::Object::new::<Self>(&[]).unwrap(); let dest = &name[..idx];
if let Some(icon_name) = item.icon_name().as_deref() { let path = &name[idx..];
obj.inner().button.set_icon_name(&icon_name);
}
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::<Self>(&[]).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; let layout = menu.get_layout(0, -1, &[]).await?.1;
menu.connect_layout_updated(clone!(@weak obj => move |revision, parent| { let mut layout_updated_stream = menu.receive_layout_updated().await?;
obj.layout_updated(revision, parent); 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); obj.inner().item.set(item);
@ -140,7 +162,8 @@ impl StatusMenu {
..set_visible(i.visible()); ..set_visible(i.visible());
}; };
box_.append(&separator); 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 let Some(toggle_state) = i.toggle_state() {
if toggle_state != 0 { if toggle_state != 0 {
label = format!("{}", label); label = format!("{}", label);
@ -160,7 +183,7 @@ impl StatusMenu {
}; };
if let Some(icon_data) = i.icon_data() { 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 pixbuf = gdk_pixbuf::Pixbuf::from_read(icon_data).unwrap(); // XXX unwrap
let image = cascade! { let image = cascade! {
gtk4::Image::from_pixbuf(Some(&pixbuf)); gtk4::Image::from_pixbuf(Some(&pixbuf));
@ -182,7 +205,9 @@ impl StatusMenu {
if close_on_click { if close_on_click {
self_.inner().popover_container.popdown(); 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); box_.append(&button);
@ -218,197 +243,111 @@ impl StatusMenu {
} }
} }
struct Layout(i32, HashMap<String, glib::Variant>, Vec<Layout>); #[dbus_proxy(interface = "org.kde.StatusNotifierItem")]
trait StatusNotifierItem {
#[dbus_proxy(property)]
fn icon_name(&self) -> zbus::Result<String>;
impl fmt::Debug for Layout { #[dbus_proxy(property)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn menu(&self) -> zbus::Result<zvariant::OwnedObjectPath>;
let mut s = f.debug_struct("Layout"); }
s.field("id", &self.0);
for (k, v) in &self.1 { #[derive(Debug)]
if let Some(v) = v.get::<String>() { pub struct Layout(i32, LayoutProps, Vec<Layout>);
s.field(k, &v);
} else if let Some(v) = v.get::<i32>() { impl<'a> serde::Deserialize<'a> for Layout {
s.field(k, &v); fn deserialize<D: serde::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
} else if let Some(v) = v.get::<bool>() { let (id, props, children) =
s.field(k, &v); <(i32, LayoutProps, Vec<(zvariant::Signature<'_>, Self)>)>::deserialize(deserializer)
} else if let Some(v) = v.get::<Vec<u8>>() { .unwrap();
s.field(k, &ByteStr::new(&v)); Ok(Self(id, props, children.into_iter().map(|x| x.1).collect()))
} else {
s.field(k, v);
}
}
s.field("children", &self.2);
s.finish()
} }
} }
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<String>,
#[zvariant(rename = "children-display")]
children_display: Option<String>,
label: Option<String>,
enabled: Option<bool>,
visible: Option<bool>,
#[zvariant(rename = "type")]
type_: Option<String>,
#[zvariant(rename = "toggle-type")]
toggle_type: Option<String>,
#[zvariant(rename = "toggle-state")]
toggle_state: Option<i32>,
#[zvariant(rename = "icon-data")]
icon_data: Option<Vec<u8>>,
}
#[allow(dead_code)] #[allow(dead_code)]
impl Layout { impl Layout {
fn prop<T: glib::FromVariant>(&self, name: &str) -> Option<T> {
self.1.get(name)?.get()
}
fn id(&self) -> i32 { fn id(&self) -> i32 {
self.0 self.0
} }
fn accessible_desc(&self) -> Option<String> {
self.prop("accessible-desc")
}
fn children_display(&self) -> Option<String> {
self.prop("children-display")
}
fn label(&self) -> Option<String> {
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<String> {
self.prop("type")
}
fn toggle_type(&self) -> Option<String> {
self.prop("toggle-type")
}
fn toggle_state(&self) -> Option<i32> {
self.prop("toggle-state")
}
fn icon_data(&self) -> Option<Vec<u8>> {
self.prop("icon-data")
}
fn children(&self) -> &[Self] { fn children(&self) -> &[Self] {
&self.2 &self.2
} }
}
impl glib::StaticVariantType for Layout { fn accessible_desc(&self) -> Option<&str> {
fn static_variant_type() -> Cow<'static, glib::VariantTy> { self.1.accessible_desc.as_deref()
glib::VariantTy::new("(ia{sv}av)").unwrap().into() }
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<i32> {
self.1.toggle_state
}
fn icon_data(&self) -> Option<&[u8]> {
self.1.icon_data.as_deref()
} }
} }
impl glib::FromVariant for Layout { #[dbus_proxy(interface = "com.canonical.dbusmenu")]
fn from_variant(variant: &glib::Variant) -> Option<Self> { trait DBusMenu {
let (id, props, children) = variant.get::<(_, _, Vec<glib::Variant>)>()?; fn get_layout(
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<Self, glib::Error> {
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(
&self, &self,
parent: i32, parent_id: i32,
depth: i32, recursion_depth: i32,
properties: &[&str], property_names: &[&str],
) -> Result<(u32, Layout), glib::Error> { ) -> zbus::Result<(u32, Layout)>;
// XXX unwrap
Ok(self
.0
.call_future(
"GetLayout",
Some(&(parent, depth, properties).to_variant()),
gio::DBusCallFlags::NONE,
1000,
)
.await?
.get()
.unwrap())
}
fn event(&self, id: i32, eventid: &str, data: &glib::Variant, timestamp: u32) { fn event(&self, id: i32, event_id: &str, data: &OwnedValue, timestamp: u32)
let res = self.0.call_future( -> zbus::Result<()>;
"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 connect_layout_updated<F: Fn(u32, i32) + 'static>(&self, f: F) -> glib::SignalHandlerId { #[dbus_proxy(signal)]
self.0 fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
.connect_local("g-signal", false, move |args| {
if &args[2].get::<String>().unwrap() == "LayoutUpdated" {
// XXX unwrap
let (revision, parent) = args[3].get::<glib::Variant>().unwrap().get().unwrap();
f(revision, parent);
}
None
})
.unwrap()
}
}
struct StatusNotifierItem(gio::DBusProxy, Option<DBusMenu>);
impl StatusNotifierItem {
async fn new(name: &str) -> Result<Self, glib::Error> {
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::<String>());
let menu = if let Some(menu_path) = menu_path {
Some(DBusMenu::new(dest, &menu_path).await?)
} else {
None
};
Ok(Self(proxy, menu))
}
fn property<T: glib::FromVariant>(&self, prop: &str) -> Option<T> {
self.0.cached_property(prop)?.get()
}
fn icon_name(&self) -> Option<String> {
// TODO: IconThemePath? AttentionIconName?
self.property("IconName")
}
fn menu(&self) -> Option<DBusMenu> {
self.1.clone()
}
} }

View file

@ -1,106 +1,70 @@
use gtk4::{ #![allow(non_snake_case)]
gio,
glib::{self, clone},
prelude::*,
};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use zbus::{dbus_interface, MessageHeader, Result, SignalContext};
static STATUS_NOTIFIER_XML: &str = " use crate::dbus_service;
<node name='/StatusNotifierWatcher'>
<interface name='org.kde.StatusNotifierWatcher'>
<method name='RegisterStatusNotifierItem'>
<arg name='service' type='s' direction='in' />
</method>
<method name='RegisterStatusNotifierHost'> #[derive(Default)]
<arg name='service' type='s' direction='in' /> struct StatusNotifierWatcher {
</method> items: Arc<Mutex<Vec<String>>>,
<property name='RegisteredStatusNotifierItems' type='as' access='read' />
<property name='IsStatusNotifierHostRegistered' type='b' access='read' />
<property name='ProtocolVersion' type='i' access='read' />
<signal name='StatusNotifierItemRegistered'>
<arg type='s' name='service' direction='out' />
</signal>
<signal name='StatusNotifierItemUnregistered'>
<arg type='s' name='service' direction='out' />
</signal>
<signal name='StatusNotifierHostRegistered' />
<signal name='StatusNotifierHostUnregistered' />
</interface>
</node>
";
pub fn start() {
gio::bus_own_name(
gio::BusType::Session,
"org.kde.StatusNotifierWatcher",
gio::BusNameOwnerFlags::NONE,
bus_acquired,
name_acquired,
name_lost,
);
} }
fn bus_acquired(_connection: gio::DBusConnection, _name: &str) {} #[dbus_interface(name = "org.kde.StatusNotifierWatcher")]
impl StatusNotifierWatcher {
fn name_acquired(connection: gio::DBusConnection, _name: &str) { async fn RegisterStatusNotifierItem(
let introspection_data = gio::DBusNodeInfo::for_xml(STATUS_NOTIFIER_XML).unwrap(); &self,
let interface_info = introspection_data service: &str,
.lookup_interface("org.kde.StatusNotifierWatcher") #[zbus(header)] hdr: MessageHeader<'_>,
.unwrap(); #[zbus(signal_context)] ctxt: SignalContext<'_>,
let items = Arc::new(Mutex::new(Vec::<String>::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,
) { ) {
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<String> {
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<gio::DBusConnection>, _name: &str) {}