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"
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",

View file

@ -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"]

View file

@ -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());
}
}

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 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());
}

View file

@ -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<gtk4::ListBox>,
dbus: OnceCell<DBus>,
dbus: OnceCell<DBusProxy<'static>>,
players: RefCell<HashMap<String, MprisPlayer>>,
}
@ -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<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 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<gtk4::Button>,
play_pause_button: DerefCell<gtk4::Button>,
forward_button: DerefCell<gtk4::Button>,
player: DerefCell<Player>,
player: DerefCell<PlayerProxy<'static>>,
image: DerefCell<gtk4::Image>,
image_uri: RefCell<Option<String>>,
title_label: DerefCell<gtk4::Label>,
@ -114,14 +117,29 @@ glib::wrapper! {
}
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 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<String, OwnedValue>);
impl Metadata {
fn lookup<T: glib::FromVariant>(&self, key: &str) -> Option<T> {
self.0.lookup_value(key, None)?.get()
fn lookup<'a, T: TryFrom<OwnedValue>>(&self, key: &str) -> Option<T> {
T::try_from(self.0.get(key)?.clone()).ok()
}
fn title(&self) -> Option<String> {
@ -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<HashMap<String, OwnedValue>>;
impl Player {
async fn new(name: &str) -> Result<Self, glib::Error> {
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")?))
}
#[dbus_proxy(property)]
fn playback_status(&self) -> zbus::Result<String>;
}

View file

@ -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(&notification);
})),
notifications.connect_notification_closed(clone!(@weak obj => move |id| {

View file

@ -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(&notification);
})),
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::{
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 = "
<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'>
<arg type='u' name='id' direction='in'/>
</method>
enum Event {
NotificationReceived(NotificationId),
CloseNotification(NotificationId),
}
<method name='GetCapabilities'>
<arg type='as' direction='out'/>
</method>
pub struct NotificationsInterfaceInner {
next_id: Mutex<NotificationId>,
notifications: Mutex<HashMap<NotificationId, Arc<Notification>>>,
sender: mpsc::UnboundedSender<Event>,
}
<method name='GetServerInformation'>
<arg type='s' name='name' direction='out'/>
<arg type='s' name='vendor' direction='out'/>
<arg type='s' name='version' direction='out'/>
<arg type='s' name='spec_version' direction='out'/>
</method>
#[derive(Clone)]
pub struct NotificationsInterface(Arc<NotificationsInterfaceInner>);
<signal name='NotificationClosed'>
<arg type='u' name='id'/>
<arg type='u' name='reason'/>
</signal>
impl NotificationsInterface {
fn new() -> (Self, mpsc::UnboundedReceiver<Event>) {
let (sender, receiver) = mpsc::unbounded();
(
Self(Arc::new(NotificationsInterfaceInner {
next_id: Default::default(),
notifications: Default::default(),
sender,
})),
receiver,
)
}
<signal name='ActionInvoked'>
<arg type='u' name='id'/>
<arg type='s' name='action_key'/>
</signal>
</interface>
</node>
";
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<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)]
pub struct NotificationsInner {
next_id: Cell<NotificationId>,
notifications: RefCell<HashMap<NotificationId, Rc<Notification>>>,
connection: RefCell<Option<gio::DBusConnection>>,
interface: DerefCell<NotificationsInterface>,
connection: OnceCell<zbus::Connection>,
}
#[glib::object_subclass]
@ -99,16 +191,12 @@ glib::wrapper! {
pub struct Notifications(ObjectSubclass<NotificationsInner>);
}
// 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<String, glib::Variant>);
struct Hints(HashMap<String, OwnedValue>);
#[allow(dead_code)]
impl Hints {
fn prop<T: glib::FromVariant>(&self, name: &str) -> Option<T> {
self.0.get(name)?.get()
fn prop<T: TryFrom<OwnedValue>>(&self, name: &str) -> Option<T> {
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::<String>() {
if let Ok(v) = <&str>::try_from(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);
} else if let Some(v) = v.get::<bool>() {
} else if let Ok(v) = bool::try_from(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);
} 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<Self> {
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<NotificationId> for u32 {
}
}
impl ToVariant for NotificationId {
fn to_variant(&self) -> glib::Variant {
self.0.get().to_variant()
}
}
impl NotificationId {
fn new(value: u32) -> Option<Self> {
NonZeroU32::new(value).map(Self)
@ -244,14 +314,30 @@ impl Notifications {
pub fn new() -> Self {
let notifications = glib::Object::new::<Self>(&[]).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<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,
});
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<Arc<Notification>> {
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<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>(
pub fn connect_notification_received<F: Fn(Arc<Notification>) + 'static>(
&self,
cb: F,
) -> SignalHandlerId {

View file

@ -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<gtk4::Box>,
watcher: OnceCell<StatusNotifierWatcher>,
watcher: OnceCell<StatusNotifierWatcherProxy<'static>>,
icons: RefCell<HashMap<String, StatusMenu>>,
}
@ -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<Self, glib::Error> {
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<Vec<String>>;
fn property<T: glib::FromVariant>(&self, prop: &str) -> Option<T> {
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<String> {
self.property::<Vec<String>>("RegisteredStatusNotifierItems")
.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(())
}
#[dbus_proxy(signal)]
fn status_notifier_item_unregistered(&self, name: &str) -> zbus::Result<()>;
}

View file

@ -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<gtk4::ToggleButton>,
popover_container: DerefCell<PopoverContainer>,
vbox: DerefCell<gtk4::Box>,
item: DerefCell<StatusNotifierItem>,
dbus_menu: DerefCell<DBusMenu>,
item: DerefCell<StatusNotifierItemProxy<'static>>,
dbus_menu: DerefCell<DBusMenuProxy<'static>>,
menus: RefCell<HashMap<i32, Menu>>,
}
@ -73,18 +75,38 @@ glib::wrapper! {
}
impl StatusMenu {
pub async fn new(name: &str) -> Result<Self, glib::Error> {
let item = StatusNotifierItem::new(name).await?;
let obj = glib::Object::new::<Self>(&[]).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<Self> {
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::<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;
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<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 {
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::<String>() {
s.field(k, &v);
} else if let Some(v) = v.get::<i32>() {
s.field(k, &v);
} else if let Some(v) = v.get::<bool>() {
s.field(k, &v);
} else if let Some(v) = v.get::<Vec<u8>>() {
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<zvariant::OwnedObjectPath>;
}
#[derive(Debug)]
pub struct Layout(i32, LayoutProps, Vec<Layout>);
impl<'a> serde::Deserialize<'a> for Layout {
fn deserialize<D: serde::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
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<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)]
impl Layout {
fn prop<T: glib::FromVariant>(&self, name: &str) -> Option<T> {
self.1.get(name)?.get()
}
fn id(&self) -> i32 {
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] {
&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<i32> {
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<Self> {
let (id, props, children) = variant.get::<(_, _, Vec<glib::Variant>)>()?;
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(
#[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<F: Fn(u32, i32) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.0
.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()
}
#[dbus_proxy(signal)]
fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
}

View file

@ -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 = "
<node name='/StatusNotifierWatcher'>
<interface name='org.kde.StatusNotifierWatcher'>
<method name='RegisterStatusNotifierItem'>
<arg name='service' type='s' direction='in' />
</method>
use crate::dbus_service;
<method name='RegisterStatusNotifierHost'>
<arg name='service' type='s' direction='in' />
</method>
<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,
);
#[derive(Default)]
struct StatusNotifierWatcher {
items: Arc<Mutex<Vec<String>>>,
}
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::<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,
#[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<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) {}