2024-05-06 15:39:04 +02:00
|
|
|
// Copyright 2023 System76 <info@system76.com>
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
2026-03-31 16:34:59 -04:00
|
|
|
use std::hash::Hash;
|
2026-05-18 13:53:50 -06:00
|
|
|
use std::path::PathBuf;
|
2026-03-31 16:34:59 -04:00
|
|
|
|
2025-08-12 21:38:51 +02:00
|
|
|
use cosmic::iced::{self, Subscription};
|
2023-01-03 14:36:56 -08:00
|
|
|
use futures::{FutureExt, StreamExt};
|
2025-10-22 02:33:30 +00:00
|
|
|
use rustc_hash::FxHashMap;
|
2023-01-03 14:36:56 -08:00
|
|
|
use zbus::zvariant::{self, OwnedValue};
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct StatusNotifierItem {
|
|
|
|
|
name: String,
|
2026-01-28 18:44:05 -08:00
|
|
|
is_menu: bool,
|
2025-03-17 13:51:56 -07:00
|
|
|
item_proxy: StatusNotifierItemProxy<'static>,
|
2025-11-06 09:32:54 -05:00
|
|
|
menu_proxy: Option<DBusMenuProxy<'static>>,
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|
|
|
|
|
|
2024-03-27 13:33:37 -07:00
|
|
|
#[derive(Clone, Debug, zvariant::Value)]
|
|
|
|
|
pub struct Icon {
|
2025-03-17 13:51:56 -07:00
|
|
|
pub width: i32,
|
|
|
|
|
pub height: i32,
|
|
|
|
|
pub bytes: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
2025-11-04 10:25:57 -08:00
|
|
|
pub struct IconUpdate {
|
|
|
|
|
pub name: Option<String>,
|
|
|
|
|
pub pixmap: Option<Vec<Icon>>,
|
2026-05-18 13:53:50 -06:00
|
|
|
pub theme_path: Option<PathBuf>,
|
2024-03-27 13:33:37 -07:00
|
|
|
}
|
|
|
|
|
|
2023-01-03 14:36:56 -08:00
|
|
|
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")
|
|
|
|
|
};
|
|
|
|
|
|
2023-11-16 17:27:47 +00:00
|
|
|
let item_proxy = StatusNotifierItemProxy::builder(connection)
|
2025-03-17 13:51:56 -07:00
|
|
|
// Status icons don't seem to report property changes the normal way...
|
|
|
|
|
.cache_properties(zbus::proxy::CacheProperties::No)
|
2023-01-03 14:36:56 -08:00
|
|
|
.destination(dest.to_string())?
|
|
|
|
|
.path(path.to_string())?
|
|
|
|
|
.build()
|
|
|
|
|
.await?;
|
|
|
|
|
|
2026-01-28 18:44:05 -08:00
|
|
|
let is_menu = item_proxy.item_is_menu().await.unwrap_or(false);
|
2025-11-06 09:32:54 -05:00
|
|
|
|
2026-01-28 18:44:05 -08:00
|
|
|
let menu_proxy = if let Ok(menu_path) = item_proxy.menu().await {
|
|
|
|
|
Some(
|
|
|
|
|
DBusMenuProxy::builder(connection)
|
|
|
|
|
.destination(dest.to_string())?
|
|
|
|
|
.path(menu_path)?
|
|
|
|
|
.build()
|
|
|
|
|
.await?,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2023-01-03 14:36:56 -08:00
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
name,
|
2026-01-28 18:44:05 -08:00
|
|
|
is_menu,
|
2025-03-17 13:51:56 -07:00
|
|
|
item_proxy,
|
2026-01-28 18:44:05 -08:00
|
|
|
menu_proxy,
|
2023-01-03 14:36:56 -08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn name(&self) -> &str {
|
|
|
|
|
&self.name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Only fetch changed part of layout, if that's any faster
|
|
|
|
|
pub fn layout_subscription(&self) -> iced::Subscription<Result<Layout, String>> {
|
2025-11-06 09:32:54 -05:00
|
|
|
let Some(menu_proxy) = self.menu_proxy.clone() else {
|
|
|
|
|
return Subscription::none();
|
|
|
|
|
};
|
2026-03-31 16:34:59 -04:00
|
|
|
struct Wrapper {
|
|
|
|
|
menu_proxy: DBusMenuProxy<'static>,
|
|
|
|
|
name: String,
|
|
|
|
|
}
|
|
|
|
|
impl Hash for Wrapper {
|
|
|
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
|
|
|
self.name.hash(state);
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|
2026-03-31 16:34:59 -04:00
|
|
|
}
|
|
|
|
|
Subscription::run_with(
|
|
|
|
|
Wrapper {
|
|
|
|
|
menu_proxy,
|
|
|
|
|
name: format!("status-notifier-item-layout-{}", &self.name),
|
|
|
|
|
},
|
|
|
|
|
|Wrapper { menu_proxy, .. }| {
|
|
|
|
|
let menu_proxy = menu_proxy.clone();
|
|
|
|
|
async move {
|
|
|
|
|
let initial = futures::stream::once(get_layout(menu_proxy.clone()));
|
|
|
|
|
|
|
|
|
|
let layout_updated = menu_proxy.receive_layout_updated().await.unwrap();
|
|
|
|
|
let props_updated =
|
|
|
|
|
menu_proxy.receive_items_properties_updated().await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Merge both streams - any update triggers a layout refetch
|
|
|
|
|
let updates = futures::stream_select!(
|
|
|
|
|
layout_updated.map(|_| ()),
|
|
|
|
|
props_updated.map(|_| ())
|
|
|
|
|
)
|
|
|
|
|
.then(move |()| get_layout(menu_proxy.clone()));
|
|
|
|
|
|
|
|
|
|
initial.chain(updates)
|
|
|
|
|
}
|
|
|
|
|
.flatten_stream()
|
|
|
|
|
},
|
2023-01-03 14:36:56 -08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-17 13:51:56 -07:00
|
|
|
pub fn icon_subscription(&self) -> iced::Subscription<IconUpdate> {
|
2025-11-04 10:25:57 -08:00
|
|
|
async fn icon_events(item_proxy: StatusNotifierItemProxy<'static>) -> IconUpdate {
|
|
|
|
|
let icon_name = item_proxy.icon_name().await;
|
|
|
|
|
let icon_pixmap = item_proxy.icon_pixmap().await;
|
2026-05-18 13:53:50 -06:00
|
|
|
let icon_theme_path = item_proxy.icon_theme_path().await.map(PathBuf::from);
|
2025-11-04 10:25:57 -08:00
|
|
|
IconUpdate {
|
|
|
|
|
name: icon_name.ok(),
|
|
|
|
|
pixmap: icon_pixmap.ok(),
|
2026-05-18 13:53:50 -06:00
|
|
|
theme_path: icon_theme_path.ok().filter(|x| !x.as_os_str().is_empty()),
|
2025-03-17 13:51:56 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let item_proxy = self.item_proxy.clone();
|
2026-03-31 16:34:59 -04:00
|
|
|
struct Wrapper {
|
|
|
|
|
item_proxy: StatusNotifierItemProxy<'static>,
|
|
|
|
|
name: String,
|
|
|
|
|
}
|
|
|
|
|
impl Hash for Wrapper {
|
|
|
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
|
|
|
self.name.hash(state);
|
2025-03-17 13:51:56 -07:00
|
|
|
}
|
2026-03-31 16:34:59 -04:00
|
|
|
}
|
|
|
|
|
Subscription::run_with(
|
|
|
|
|
Wrapper {
|
|
|
|
|
item_proxy,
|
|
|
|
|
name: format!("status-notifier-item-icon-{}", &self.name),
|
|
|
|
|
},
|
|
|
|
|
|Wrapper { item_proxy, .. }| {
|
|
|
|
|
let item_proxy = item_proxy.clone();
|
|
|
|
|
async move {
|
|
|
|
|
let new_icon_stream = item_proxy.receive_new_icon().await.unwrap();
|
|
|
|
|
futures::stream::once(async {})
|
|
|
|
|
.chain(new_icon_stream.map(|_| ()))
|
|
|
|
|
.then(move |()| icon_events(item_proxy.clone()))
|
|
|
|
|
}
|
|
|
|
|
.flatten_stream()
|
|
|
|
|
},
|
2025-03-17 13:51:56 -07:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 18:44:05 -08:00
|
|
|
/// Item is only a menu, with no `Activate` action
|
|
|
|
|
pub fn is_menu(&self) -> bool {
|
|
|
|
|
self.is_menu
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 09:32:54 -05:00
|
|
|
pub fn menu_proxy(&self) -> Option<&DBusMenuProxy<'static>> {
|
|
|
|
|
self.menu_proxy.as_ref()
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|
2025-08-05 12:14:53 -04:00
|
|
|
|
|
|
|
|
pub fn item_proxy(&self) -> &StatusNotifierItemProxy<'static> {
|
|
|
|
|
&self.item_proxy
|
|
|
|
|
}
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-16 21:29:28 -04:00
|
|
|
#[zbus::proxy(interface = "org.kde.StatusNotifierItem")]
|
2024-11-05 11:56:50 -08:00
|
|
|
pub trait StatusNotifierItem {
|
2024-05-16 21:29:28 -04:00
|
|
|
#[zbus(property)]
|
2023-01-03 14:36:56 -08:00
|
|
|
fn icon_name(&self) -> zbus::Result<String>;
|
|
|
|
|
|
2025-11-04 10:29:58 -08:00
|
|
|
#[zbus(property)]
|
|
|
|
|
fn icon_theme_path(&self) -> zbus::Result<String>;
|
|
|
|
|
|
2024-03-27 13:33:37 -07:00
|
|
|
// https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/Icons
|
2024-05-16 21:29:28 -04:00
|
|
|
#[zbus(property)]
|
2024-03-27 13:33:37 -07:00
|
|
|
fn icon_pixmap(&self) -> zbus::Result<Vec<Icon>>;
|
|
|
|
|
|
2024-05-16 21:29:28 -04:00
|
|
|
#[zbus(property)]
|
2023-01-03 14:36:56 -08:00
|
|
|
fn menu(&self) -> zbus::Result<zvariant::OwnedObjectPath>;
|
2025-03-17 13:51:56 -07:00
|
|
|
|
2025-11-06 09:32:54 -05:00
|
|
|
#[zbus(property)]
|
|
|
|
|
fn item_is_menu(&self) -> zbus::Result<bool>;
|
|
|
|
|
|
2025-03-17 13:51:56 -07:00
|
|
|
#[zbus(signal)]
|
|
|
|
|
fn new_icon(&self) -> zbus::Result<()>;
|
2025-08-05 12:14:53 -04:00
|
|
|
|
|
|
|
|
fn provide_xdg_activation_token(&self, token: String) -> zbus::Result<()>;
|
2025-11-06 09:32:54 -05:00
|
|
|
|
|
|
|
|
fn activate(&self, x: i32, y: i32) -> zbus::Result<()>;
|
|
|
|
|
|
|
|
|
|
fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>;
|
2026-07-02 17:02:40 +01:00
|
|
|
|
|
|
|
|
fn scroll(&self, delta: i32, orientation: &str) -> zbus::Result<()>;
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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) =
|
2024-11-05 11:56:50 -08:00
|
|
|
<(i32, LayoutProps, Vec<(zvariant::Signature, Self)>)>::deserialize(deserializer)?;
|
2023-01-03 14:36:56 -08:00
|
|
|
Ok(Self(id, props, children.into_iter().map(|x| x.1).collect()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl zvariant::Type for Layout {
|
2025-10-22 02:33:30 +00:00
|
|
|
const SIGNATURE: &zvariant::Signature = <(
|
|
|
|
|
i32,
|
|
|
|
|
FxHashMap<String, zvariant::Value>,
|
|
|
|
|
Vec<zvariant::Value>,
|
|
|
|
|
)>::SIGNATURE;
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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>,
|
2024-06-22 19:24:18 -07:00
|
|
|
// If this field has a different type, this causes the whole type to fail
|
|
|
|
|
// to parse, due to a zvariant bug.
|
|
|
|
|
// https://github.com/dbus2/zbus/issues/856
|
|
|
|
|
// shortcut: Option<String>,
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl zvariant::Type for LayoutProps {
|
2025-10-22 02:33:30 +00:00
|
|
|
const SIGNATURE: &zvariant::Signature = <FxHashMap<String, zvariant::Value>>::SIGNATURE;
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-16 21:29:28 -04:00
|
|
|
#[zbus::proxy(interface = "com.canonical.dbusmenu")]
|
2024-11-05 11:56:50 -08:00
|
|
|
pub trait DBusMenu {
|
2023-01-03 14:36:56 -08:00
|
|
|
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)
|
2025-08-12 21:38:51 +02:00
|
|
|
-> zbus::Result<()>;
|
2023-01-03 14:36:56 -08:00
|
|
|
|
2024-06-22 19:40:25 -07:00
|
|
|
fn about_to_show(&self, id: i32) -> zbus::Result<bool>;
|
|
|
|
|
|
2024-05-16 21:29:28 -04:00
|
|
|
#[zbus(signal)]
|
2023-01-03 14:36:56 -08:00
|
|
|
fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
|
2026-01-08 17:06:33 -07:00
|
|
|
|
|
|
|
|
#[zbus(signal)]
|
|
|
|
|
fn items_properties_updated(
|
|
|
|
|
&self,
|
|
|
|
|
updated_props: Vec<(i32, std::collections::HashMap<String, zvariant::OwnedValue>)>,
|
|
|
|
|
removed_props: Vec<(i32, Vec<String>)>,
|
|
|
|
|
) -> zbus::Result<()>;
|
2023-01-03 14:36:56 -08:00
|
|
|
}
|