Iced port of status area applet
This is based on the GTK version of the status area applet that was previously in this repository. This exposes app indicators found over dbus. As used in applications like nm-applet and steam.
This commit is contained in:
parent
29a2dea760
commit
6a64486163
16 changed files with 1313 additions and 1 deletions
2
cosmic-applet-status-area/src/subscriptions/mod.rs
Normal file
2
cosmic-applet-status-area/src/subscriptions/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod status_notifier_item;
|
||||
pub mod status_notifier_watcher;
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
use cosmic::iced;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use zbus::zvariant::{self, OwnedValue};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StatusNotifierItem {
|
||||
name: String,
|
||||
icon_name: String,
|
||||
_item_proxy: StatusNotifierItemProxy<'static>,
|
||||
menu_proxy: DBusMenuProxy<'static>,
|
||||
}
|
||||
|
||||
impl StatusNotifierItem {
|
||||
pub async fn new(connection: &zbus::Connection, name: String) -> zbus::Result<Self> {
|
||||
let (dest, path) = if let Some(idx) = name.find('/') {
|
||||
(&name[..idx], &name[idx..])
|
||||
} else {
|
||||
(name.as_str(), "/StatusNotifierItem")
|
||||
};
|
||||
|
||||
let item_proxy = StatusNotifierItemProxy::builder(&connection)
|
||||
.destination(dest.to_string())?
|
||||
.path(path.to_string())?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let icon_name = item_proxy.icon_name().await?;
|
||||
|
||||
let menu_path = item_proxy.menu().await?;
|
||||
let menu_proxy = DBusMenuProxy::builder(&connection)
|
||||
.destination(dest.to_string())?
|
||||
.path(menu_path)?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
icon_name,
|
||||
_item_proxy: item_proxy,
|
||||
menu_proxy,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn icon_name(&self) -> &str {
|
||||
&self.icon_name
|
||||
}
|
||||
|
||||
// TODO: Only fetch changed part of layout, if that's any faster
|
||||
pub fn layout_subscription(&self) -> iced::Subscription<Result<Layout, String>> {
|
||||
let menu_proxy = self.menu_proxy.clone();
|
||||
iced::subscription::run_with_id(
|
||||
format!("status-notifier-item-{}", &self.name),
|
||||
async move {
|
||||
let initial = futures::stream::once(get_layout(menu_proxy.clone()));
|
||||
let layout_updated_stream = menu_proxy.receive_layout_updated().await.unwrap();
|
||||
let updates = layout_updated_stream.then(move |_| get_layout(menu_proxy.clone()));
|
||||
initial.chain(updates)
|
||||
}
|
||||
.flatten_stream(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn menu_proxy(&self) -> &DBusMenuProxy<'static> {
|
||||
&self.menu_proxy
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_layout(menu_proxy: DBusMenuProxy<'static>) -> Result<Layout, String> {
|
||||
match menu_proxy.get_layout(0, -1, &[]).await {
|
||||
Ok((_, layout)) => Ok(layout),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus::dbus_proxy(interface = "org.kde.StatusNotifierItem")]
|
||||
trait StatusNotifierItem {
|
||||
#[dbus_proxy(property)]
|
||||
fn icon_name(&self) -> zbus::Result<String>;
|
||||
|
||||
#[dbus_proxy(property)]
|
||||
fn menu(&self) -> zbus::Result<zvariant::OwnedObjectPath>;
|
||||
}
|
||||
|
||||
#[derive(Clone, 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)?;
|
||||
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(Clone, Debug, zvariant::DeserializeDict)]
|
||||
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>>,
|
||||
#[zvariant(rename = "icon-name")]
|
||||
icon_name: Option<String>,
|
||||
disposition: Option<String>,
|
||||
shortcut: Option<String>,
|
||||
}
|
||||
|
||||
impl zvariant::Type for LayoutProps {
|
||||
fn signature() -> zvariant::Signature<'static> {
|
||||
zvariant::Signature::try_from("a{sv}").unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Layout {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn children(&self) -> &[Self] {
|
||||
&self.2
|
||||
}
|
||||
|
||||
pub fn accessible_desc(&self) -> Option<&str> {
|
||||
self.1.accessible_desc.as_deref()
|
||||
}
|
||||
|
||||
pub fn children_display(&self) -> Option<&str> {
|
||||
self.1.children_display.as_deref()
|
||||
}
|
||||
|
||||
pub fn label(&self) -> Option<&str> {
|
||||
self.1.label.as_deref()
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.1.enabled.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
self.1.visible.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn type_(&self) -> Option<&str> {
|
||||
self.1.type_.as_deref()
|
||||
}
|
||||
|
||||
pub fn toggle_type(&self) -> Option<&str> {
|
||||
self.1.toggle_type.as_deref()
|
||||
}
|
||||
|
||||
pub fn toggle_state(&self) -> Option<i32> {
|
||||
self.1.toggle_state
|
||||
}
|
||||
|
||||
pub fn icon_data(&self) -> Option<&[u8]> {
|
||||
self.1.icon_data.as_deref()
|
||||
}
|
||||
|
||||
pub fn icon_name(&self) -> Option<&str> {
|
||||
self.1.icon_name.as_deref()
|
||||
}
|
||||
|
||||
pub fn disposition(&self) -> Option<&str> {
|
||||
self.1.disposition.as_deref()
|
||||
}
|
||||
|
||||
pub fn shortcut(&self) -> Option<&str> {
|
||||
self.1.shortcut.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus::dbus_proxy(interface = "com.canonical.dbusmenu")]
|
||||
trait DBusMenu {
|
||||
fn get_layout(
|
||||
&self,
|
||||
parent_id: i32,
|
||||
recursion_depth: i32,
|
||||
property_names: &[&str],
|
||||
) -> zbus::Result<(u32, Layout)>;
|
||||
|
||||
fn event(&self, id: i32, event_id: &str, data: &OwnedValue, timestamp: u32)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
#[dbus_proxy(signal)]
|
||||
fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
use futures::{Stream, StreamExt};
|
||||
use std::pin::Pin;
|
||||
|
||||
use super::Event;
|
||||
use crate::subscriptions::status_notifier_item::StatusNotifierItem;
|
||||
|
||||
// TODO: Don't use trait object
|
||||
pub type EventStream = Pin<Box<dyn Stream<Item = Event> + Send>>;
|
||||
|
||||
#[zbus::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<()>;
|
||||
|
||||
#[dbus_proxy(property)]
|
||||
fn registered_status_notifier_items(&self) -> zbus::Result<Vec<String>>;
|
||||
|
||||
#[dbus_proxy(signal)]
|
||||
fn status_notifier_item_registered(&self, name: &str) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_proxy(signal)]
|
||||
fn status_notifier_item_unregistered(&self, name: &str) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
pub async fn watch(connection: &zbus::Connection) -> zbus::Result<EventStream> {
|
||||
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);
|
||||
}
|
||||
|
||||
let connection_clone = connection.clone();
|
||||
let registered_stream = watcher
|
||||
.receive_status_notifier_item_registered()
|
||||
.await?
|
||||
.then(move |evt| Box::pin(item_registered(connection_clone.clone(), evt)));
|
||||
let unregistered_stream = watcher
|
||||
.receive_status_notifier_item_unregistered()
|
||||
.await?
|
||||
.map(|evt| match evt.args() {
|
||||
Ok(args) => Event::Unregistered(args.name.to_string()),
|
||||
Err(err) => Event::Error(err.to_string()),
|
||||
});
|
||||
|
||||
let items = watcher.registered_status_notifier_items().await?;
|
||||
let connection = connection.clone();
|
||||
let items_stream = futures::stream::iter(items.into_iter())
|
||||
.then(move |name| status_notifier_item(connection.clone(), name));
|
||||
|
||||
Ok(Box::pin(items_stream.chain(futures::stream_select!(
|
||||
registered_stream,
|
||||
unregistered_stream
|
||||
))))
|
||||
}
|
||||
|
||||
async fn item_registered(connection: zbus::Connection, evt: StatusNotifierItemRegistered) -> Event {
|
||||
match evt.args() {
|
||||
Ok(args) => status_notifier_item(connection, args.name.to_string()).await,
|
||||
Err(err) => Event::Error(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn status_notifier_item(connection: zbus::Connection, name: String) -> Event {
|
||||
match StatusNotifierItem::new(&connection, name).await {
|
||||
Ok(item) => Event::Registered(item),
|
||||
Err(err) => Event::Error(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// TODO: Both this and server proxy could emit same events, have way to generate stream from either?
|
||||
|
||||
use cosmic::iced;
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::subscriptions::status_notifier_item::StatusNotifierItem;
|
||||
|
||||
mod client;
|
||||
mod server;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
Connected(zbus::Connection),
|
||||
Registered(StatusNotifierItem),
|
||||
Unregistered(String),
|
||||
Error(String), // XXX
|
||||
}
|
||||
|
||||
enum State {
|
||||
NotConnected,
|
||||
Connected(client::EventStream),
|
||||
Failed,
|
||||
}
|
||||
|
||||
pub fn subscription() -> iced::Subscription<Event> {
|
||||
iced::subscription::unfold(
|
||||
"status-notifier-watcher",
|
||||
State::NotConnected,
|
||||
|state| async move {
|
||||
match state {
|
||||
State::NotConnected => match connect().await {
|
||||
Ok((connection, stream)) => {
|
||||
(Event::Connected(connection), State::Connected(stream))
|
||||
}
|
||||
Err(err) => (Event::Error(err.to_string()), State::Failed),
|
||||
},
|
||||
State::Connected(mut stream) => match stream.next().await {
|
||||
Some(event) => (event, State::Connected(stream)),
|
||||
None => iced::futures::future::pending().await,
|
||||
},
|
||||
State::Failed => iced::futures::future::pending().await,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn connect() -> zbus::Result<(zbus::Connection, client::EventStream)> {
|
||||
// Connect to session dbus socket
|
||||
let connection = zbus::Connection::session().await?;
|
||||
|
||||
// Start `StatusNotifierWatcher` service, if there isn't one running already
|
||||
server::create_service(&connection).await?;
|
||||
|
||||
// Connect client and listen for registered/unregistered
|
||||
let stream = client::watch(&connection).await?;
|
||||
|
||||
Ok((connection, stream))
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
// TODO: `g_bus_own_name` like abstraction in zbus
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use futures::prelude::*;
|
||||
use zbus::{
|
||||
dbus_interface,
|
||||
fdo::{DBusProxy, RequestNameFlags, RequestNameReply},
|
||||
names::{BusName, UniqueName, WellKnownName},
|
||||
MessageHeader, Result, SignalContext,
|
||||
};
|
||||
|
||||
const NAME: WellKnownName =
|
||||
WellKnownName::from_static_str_unchecked("org.kde.StatusNotifierWatcher");
|
||||
const OBJECT_PATH: &str = "/StatusNotifierWatcher";
|
||||
|
||||
#[derive(Default)]
|
||||
struct StatusNotifierWatcher {
|
||||
items: Vec<(UniqueName<'static>, String)>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.kde.StatusNotifierWatcher")]
|
||||
impl StatusNotifierWatcher {
|
||||
async fn register_status_notifier_item(
|
||||
&mut self,
|
||||
service: &str,
|
||||
#[zbus(header)] hdr: MessageHeader<'_>,
|
||||
#[zbus(signal_context)] ctxt: SignalContext<'_>,
|
||||
) {
|
||||
let sender = hdr.sender().unwrap().unwrap();
|
||||
let service = if service.starts_with('/') {
|
||||
format!("{}{}", sender, service)
|
||||
} else {
|
||||
service.to_string()
|
||||
};
|
||||
Self::status_notifier_item_registered(&ctxt, &service)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.items.push((sender.to_owned(), service));
|
||||
}
|
||||
|
||||
fn register_status_notifier_host(&self, _service: &str) {
|
||||
// XXX emit registed/unregistered
|
||||
}
|
||||
|
||||
#[dbus_interface(property)]
|
||||
fn registered_status_notifier_items(&self) -> Vec<String> {
|
||||
self.items.iter().map(|(_, x)| x.clone()).collect()
|
||||
}
|
||||
|
||||
#[dbus_interface(property)]
|
||||
fn is_status_notifier_host_registered(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[dbus_interface(property)]
|
||||
fn protocol_version(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn status_notifier_item_registered(ctxt: &SignalContext<'_>, service: &str)
|
||||
-> Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn status_notifier_item_unregistered(
|
||||
ctxt: &SignalContext<'_>,
|
||||
service: &str,
|
||||
) -> Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn status_notifier_host_registered(ctxt: &SignalContext<'_>) -> Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn status_notifier_host_unregistered(ctxt: &SignalContext<'_>) -> Result<()>;
|
||||
}
|
||||
|
||||
pub async fn create_service(connection: &zbus::Connection) -> zbus::Result<()> {
|
||||
connection
|
||||
.object_server()
|
||||
.at(OBJECT_PATH, StatusNotifierWatcher::default())
|
||||
.await?;
|
||||
let interface = connection
|
||||
.object_server()
|
||||
.interface::<_, StatusNotifierWatcher>(OBJECT_PATH)
|
||||
.await
|
||||
.unwrap();
|
||||
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(NAME.as_ref(), flags).await? {
|
||||
RequestNameReply::InQueue => {
|
||||
eprintln!("Bus name '{}' already owned", NAME);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let connection = connection.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut have_bus_name = 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() == NAME {
|
||||
if args.new_owner.as_ref() == unique_name.as_ref() {
|
||||
eprintln!("Acquired bus name: {}", NAME);
|
||||
have_bus_name = true;
|
||||
} else if have_bus_name {
|
||||
eprintln!("Lost bus name: {}", NAME);
|
||||
have_bus_name = false;
|
||||
}
|
||||
} else if let BusName::Unique(name) = &args.name {
|
||||
let mut interface = interface.get_mut().await;
|
||||
if let Some(idx) = interface
|
||||
.items
|
||||
.iter()
|
||||
.position(|(unique_name, _)| unique_name == name)
|
||||
{
|
||||
let ctxt = zbus::SignalContext::new(&connection, OBJECT_PATH).unwrap();
|
||||
let service = interface.items.remove(idx).1;
|
||||
StatusNotifierWatcher::status_notifier_item_unregistered(&ctxt, &service)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue