// Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only use cosmic::{iced, widget::icon}; use futures::{FutureExt, StreamExt}; use zbus::zvariant::{self, OwnedValue}; #[derive(Clone, Debug)] pub struct StatusNotifierItem { name: String, icon_name: String, // TODO Handle icon with multiple sizes? icon_pixmap: Option, _item_proxy: StatusNotifierItemProxy<'static>, menu_proxy: DBusMenuProxy<'static>, } #[derive(Clone, Debug, zvariant::Value)] pub struct Icon { width: i32, height: i32, bytes: Vec, } impl StatusNotifierItem { pub async fn new(connection: &zbus::Connection, name: String) -> zbus::Result { 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.unwrap_or_default(); let icon_pixmap = item_proxy .icon_pixmap() .await .unwrap_or_default() .into_iter() .max_by_key(|i| (i.width, i.height)) .map(|mut i| { // Convert ARGB to RGBA for pixel in i.bytes.chunks_exact_mut(4) { pixel.rotate_left(1); } icon::from_raster_pixels(i.width as u32, i.height as u32, i.bytes) }); 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, icon_pixmap, _item_proxy: item_proxy, menu_proxy, }) } pub fn name(&self) -> &str { &self.name } pub fn icon_name(&self) -> &str { &self.icon_name } pub fn icon_pixmap(&self) -> Option<&icon::Handle> { self.icon_pixmap.as_ref() } // TODO: Only fetch changed part of layout, if that's any faster pub fn layout_subscription(&self) -> iced::Subscription> { 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 { match menu_proxy.get_layout(0, -1, &[]).await { Ok((_, layout)) => Ok(layout), Err(err) => Err(err.to_string()), } } #[zbus::proxy(interface = "org.kde.StatusNotifierItem")] trait StatusNotifierItem { #[zbus(property)] fn icon_name(&self) -> zbus::Result; // https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/Icons #[zbus(property)] fn icon_pixmap(&self) -> zbus::Result>; #[zbus(property)] fn menu(&self) -> zbus::Result; } #[derive(Clone, Debug)] pub struct Layout(i32, LayoutProps, Vec); impl<'a> serde::Deserialize<'a> for Layout { fn deserialize>(deserializer: D) -> Result { let (id, props, children) = <(i32, LayoutProps, Vec<(zvariant::Signature<'_>, Self)>)>::deserialize(deserializer)?; 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, #[zvariant(rename = "children-display")] children_display: Option, label: Option, enabled: Option, visible: Option, #[zvariant(rename = "type")] type_: Option, #[zvariant(rename = "toggle-type")] toggle_type: Option, #[zvariant(rename = "toggle-state")] toggle_state: Option, #[zvariant(rename = "icon-data")] icon_data: Option>, #[zvariant(rename = "icon-name")] icon_name: Option, disposition: Option, shortcut: Option, } 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 { 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::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<()>; #[zbus(signal)] fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>; }