System Tray (#743)

* Allow tokio on gtk thread

* Basic notifier host implementation

* Implement systray widget

* Use dbusmenu-gtk3

* Update flake.nix

* US spelling of license

* Fix possible TOCTOU

* Change how hosts are started

* Add watcher

* Bunch of refactor

* Handle errors better

* Refactor service parsing

* Avoid duplicate dbus connections

* Fix watcher producing bad items

* Handle zbus::Error::NameTaken

* Refactor icon loading & don't panic on zoom

* Implement pixbuf icons

Co-authored-by: Bojan Nemčić <bnemcic@gmail.com>

* Don't panic on icon/menu error

* Improve icon error handling to make discord work

* Update comments

* Big refactor into actor model

* Reword error messages

* Remove redundant watcher_on function

* Big icon handling refactor

* Don't unnecessarily wrap StatusNotifierItem

* cargo fmt

* Documentation

* Avoid registering to StatusNotifierWatcher multiple times

* None theme means default theme

* Add dbus logging

* Add libdbusmenu-gtk3 dependency to docs

* Some code tidying

* Make Item more clearer

* Make clippy happy

* Systray widget improvements

* Remove unwraps from dbus state

* Temporarily add libdbusmenu-gtk3 to flake buildInputs

* Fix blurry tray icon for HiDPI display

* feat: dynamic icons

* fix: don't cache IconPixmap property

this fixes dynamic icons for some icons, e.g. syncthingtray

* fixup! feat: dynamic icons

* Fix unused borrow warning

* Add some documentation to notifier_host

* Rename notifier_host::dbus to more descriptive notifier_host::proxy

* fixup! Rename notifier_host::dbus to more descriptive notifier_host::proxy

* fixup! Merge remote-tracking branch 'upstream/master' into tray-3

* fixup! Merge remote-tracking branch 'upstream/master' into tray-3

* Remove commented out fields of DBusSession

* Refactor host

* Remove git conflict marker

* Various improvements

* Icon documentation

* cargo fmt

* Add dependency to CI

---------

Co-authored-by: Bojan Nemčić <bnemcic@gmail.com>
Co-authored-by: MoetaYuko <loli@yuko.moe>
Co-authored-by: hylo <hylo@posteo.de>
This commit is contained in:
Temmie 2024-03-30 20:55:01 +11:00 committed by GitHub
parent f1ec00a1c9
commit 1b819fb646
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2585 additions and 334 deletions

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
run: sudo apt-get update && sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libdbusmenu-gtk3-dev
- uses: actions/checkout@v4

View File

@ -9,6 +9,9 @@ All notable changes to eww will be listed here, starting at changes since versio
- Fix nix flake
- Fix `jq` (By: w-lfchen)
### Features
- Add `systray` widget (By: ralismark)
## [0.5.0] (17.02.2024)
### BREAKING CHANGES

1467
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ resolver = "2"
simplexpr = { version = "0.1.0", path = "crates/simplexpr" }
eww_shared_util = { version = "0.1.0", path = "crates/eww_shared_util" }
yuck = { version = "0.1.0", path = "crates/yuck", default-features = false}
notifier_host = { version = "0.1.0", path = "crates/notifier_host" }
anyhow = "1.0.79"
bincode = "1.3.3"

View File

@ -19,6 +19,7 @@ wayland = ["gtk-layer-shell"]
simplexpr.workspace = true
eww_shared_util.workspace = true
yuck.workspace = true
notifier_host.workspace = true
gtk = "0.17.1"
gdk = "0.17.1"
@ -34,6 +35,9 @@ gtk-layer-shell = { version = "0.6.1", optional = true }
gdkx11 = { version = "0.17", optional = true }
x11rb = { version = "0.11.1", features = ["randr"], optional = true }
zbus = { version = "3.7.0", default-features = false, features = ["tokio"] }
ordered-stream = "0.2.0"
anyhow.workspace = true
bincode.workspace = true
chrono.workspace = true

View File

@ -42,7 +42,10 @@ fn main() {
if std::env::var("RUST_LOG").is_ok() {
pretty_env_logger::init_timed();
} else {
pretty_env_logger::formatted_timed_builder().filter(Some("eww"), log_level_filter).init();
pretty_env_logger::formatted_timed_builder()
.filter(Some("eww"), log_level_filter)
.filter(Some("notifier_host"), log_level_filter)
.init();
}
if let opts::Action::ShellCompletions { shell } = opts.action {

View File

@ -103,7 +103,7 @@ pub fn initialize_server<B: DisplayBackend>(
}
// initialize all the handlers and tasks running asyncronously
init_async_part(app.paths.clone(), ui_send);
let tokio_handle = init_async_part(app.paths.clone(), ui_send);
glib::MainContext::default().spawn_local(async move {
// if an action was given to the daemon initially, execute it first.
@ -124,22 +124,26 @@ pub fn initialize_server<B: DisplayBackend>(
}
});
// allow the GTK main thread to do tokio things
let _g = tokio_handle.enter();
gtk::main();
log::info!("main application thread finished");
Ok(ForkResult::Child)
}
fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>) {
fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>) -> tokio::runtime::Handle {
let rt = tokio::runtime::Builder::new_multi_thread()
.thread_name("main-async-runtime")
.enable_all()
.build()
.expect("Failed to initialize tokio runtime");
let handle = rt.handle().clone();
std::thread::Builder::new()
.name("outer-main-async-runtime".to_string())
.spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.thread_name("main-async-runtime")
.enable_all()
.build()
.expect("Failed to initialize tokio runtime");
rt.block_on(async {
let filewatch_join_handle = {
let ui_send = ui_send.clone();
@ -171,6 +175,8 @@ fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>
})
})
.expect("Failed to start outer-main-async-runtime thread");
handle
}
/// Watch configuration files for changes, sending reload events to the eww app when the files change.

View File

@ -53,6 +53,8 @@ macro_rules! def_widget {
// values is a map of all the variables that are required to evaluate the
// attributes expression.
// allow $gtk_widget to never be used, by creating a reference that gets immediately discarded
{let _ = &$gtk_widget;};
// We first initialize all the local variables for all the expected attributes in scope
$(

View File

@ -4,6 +4,7 @@ pub mod build_widget;
pub mod circular_progressbar;
pub mod def_widget_macro;
pub mod graph;
mod systray;
pub mod transform;
pub mod widget_definitions;

View File

@ -0,0 +1,203 @@
use futures::StreamExt;
use gtk::{cairo::Surface, gdk::ffi::gdk_cairo_surface_create_from_pixbuf, prelude::*};
use notifier_host;
// DBus state shared between systray instances, to avoid creating too many connections etc.
struct DBusSession {
snw: notifier_host::proxy::StatusNotifierWatcherProxy<'static>,
}
async fn dbus_session() -> zbus::Result<&'static DBusSession> {
// TODO make DBusSession reference counted so it's dropped when not in use?
static DBUS_STATE: tokio::sync::OnceCell<DBusSession> = tokio::sync::OnceCell::const_new();
DBUS_STATE
.get_or_try_init(|| async {
let con = zbus::Connection::session().await?;
notifier_host::Watcher::new().attach_to(&con).await?;
let (_, snw) = notifier_host::register_as_host(&con).await?;
Ok(DBusSession { snw })
})
.await
}
pub struct Props {
icon_size_tx: tokio::sync::watch::Sender<i32>,
}
impl Props {
pub fn new() -> Self {
let (icon_size_tx, _) = tokio::sync::watch::channel(24);
Self { icon_size_tx }
}
pub fn icon_size(&self, value: i32) {
let _ = self.icon_size_tx.send_if_modified(|x| {
if *x == value {
false
} else {
*x = value;
true
}
});
}
}
struct Tray {
menubar: gtk::MenuBar,
items: std::collections::HashMap<String, Item>,
icon_size: tokio::sync::watch::Receiver<i32>,
}
pub fn spawn_systray(menubar: &gtk::MenuBar, props: &Props) {
let mut systray = Tray { menubar: menubar.clone(), items: Default::default(), icon_size: props.icon_size_tx.subscribe() };
let task = glib::MainContext::default().spawn_local(async move {
let s = match dbus_session().await {
Ok(x) => x,
Err(e) => {
log::error!("could not initialise dbus connection for tray: {}", e);
return;
}
};
systray.menubar.show();
let e = notifier_host::run_host(&mut systray, &s.snw).await;
log::error!("notifier host error: {}", e);
});
// stop the task when the widget is dropped
menubar.connect_destroy(move |_| {
task.abort();
});
}
impl notifier_host::Host for Tray {
fn add_item(&mut self, id: &str, item: notifier_host::Item) {
let item = Item::new(id.to_owned(), item, self.icon_size.clone());
self.menubar.add(&item.widget);
if let Some(old_item) = self.items.insert(id.to_string(), item) {
self.menubar.remove(&old_item.widget);
}
}
fn remove_item(&mut self, id: &str) {
if let Some(item) = self.items.get(id) {
self.menubar.remove(&item.widget);
} else {
log::warn!("Tried to remove nonexistent item {:?} from systray", id);
}
}
}
/// Item represents a single icon being shown in the system tray.
struct Item {
/// Main widget representing this tray item.
widget: gtk::MenuItem,
/// Async task to stop when this item gets removed.
task: Option<glib::JoinHandle<()>>,
}
impl Drop for Item {
fn drop(&mut self) {
if let Some(task) = &self.task {
task.abort();
}
}
}
impl Item {
fn new(id: String, item: notifier_host::Item, icon_size: tokio::sync::watch::Receiver<i32>) -> Self {
let widget = gtk::MenuItem::new();
let out_widget = widget.clone(); // copy so we can return it
let task = glib::MainContext::default().spawn_local(async move {
if let Err(e) = Item::maintain(widget.clone(), item, icon_size).await {
log::error!("error for systray item {}: {}", id, e);
}
});
Self { widget: out_widget, task: Some(task) }
}
async fn maintain(
widget: gtk::MenuItem,
item: notifier_host::Item,
mut icon_size: tokio::sync::watch::Receiver<i32>,
) -> zbus::Result<()> {
// init icon
let icon = gtk::Image::new();
widget.add(&icon);
icon.show();
// init menu
match item.menu().await {
Ok(m) => widget.set_submenu(Some(&m)),
Err(e) => log::warn!("failed to get menu: {}", e),
}
// TODO this is a lot of code duplication unfortunately, i'm not really sure how to
// refactor without making the borrow checker angry
// set status
match item.status().await? {
notifier_host::Status::Passive => widget.hide(),
notifier_host::Status::Active | notifier_host::Status::NeedsAttention => widget.show(),
}
// set title
widget.set_tooltip_text(Some(&item.sni.title().await?));
// set icon
let scale = icon.scale_factor();
load_icon_for_item(&icon, &item, *icon_size.borrow_and_update(), scale).await;
// updates
let mut status_updates = item.sni.receive_new_status().await?;
let mut title_updates = item.sni.receive_new_status().await?;
let mut icon_updates = item.sni.receive_new_icon().await?;
loop {
tokio::select! {
Some(_) = status_updates.next() => {
// set status
match item.status().await? {
notifier_host::Status::Passive => widget.hide(),
notifier_host::Status::Active | notifier_host::Status::NeedsAttention => widget.show(),
}
}
Ok(_) = icon_size.changed() => {
// set icon
load_icon_for_item(&icon, &item, *icon_size.borrow_and_update(), scale).await;
}
Some(_) = title_updates.next() => {
// set title
widget.set_tooltip_text(Some(&item.sni.title().await?));
}
Some(_) = icon_updates.next() => {
// set icon
load_icon_for_item(&icon, &item, *icon_size.borrow_and_update(), scale).await;
}
}
}
}
}
async fn load_icon_for_item(icon: &gtk::Image, item: &notifier_host::Item, size: i32, scale: i32) {
if let Some(pixbuf) = item.icon(size, scale).await {
let surface = unsafe {
// gtk::cairo::Surface will destroy the underlying surface on drop
let ptr = gdk_cairo_surface_create_from_pixbuf(
pixbuf.as_ptr(),
scale,
icon.window().map_or(std::ptr::null_mut(), |v| v.as_ptr()),
);
Surface::from_raw_full(ptr)
};
icon.set_from_surface(surface.ok().as_ref());
}
}

View File

@ -3,7 +3,7 @@ use super::{build_widget::BuilderArgs, circular_progressbar::*, run_command, tra
use crate::{
def_widget, enum_parse, error_handling_ctx,
util::{self, list_difference},
widgets::build_widget::build_gtk_widget,
widgets::{build_widget::build_gtk_widget, systray},
};
use anyhow::{anyhow, Context, Result};
use codespan_reporting::diagnostic::Severity;
@ -82,6 +82,7 @@ pub const BUILTIN_WIDGET_NAMES: &[&str] = &[
WIDGET_NAME_SCROLL,
WIDGET_NAME_OVERLAY,
WIDGET_NAME_STACK,
WIDGET_NAME_SYSTRAY,
];
/// widget definitions
@ -111,6 +112,7 @@ pub(super) fn widget_use_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<gtk::W
WIDGET_NAME_SCROLL => build_gtk_scrolledwindow(bargs)?.upcast(),
WIDGET_NAME_OVERLAY => build_gtk_overlay(bargs)?.upcast(),
WIDGET_NAME_STACK => build_gtk_stack(bargs)?.upcast(),
WIDGET_NAME_SYSTRAY => build_systray(bargs)?.upcast(),
_ => {
return Err(DiagError(gen_diagnostic! {
msg = format!("referenced unknown widget `{}`", bargs.widget_use.name),
@ -1133,6 +1135,33 @@ fn build_graph(bargs: &mut BuilderArgs) -> Result<super::graph::Graph> {
Ok(w)
}
const WIDGET_NAME_SYSTRAY: &str = "systray";
/// @widget systray
/// @desc Tray for system notifier icons
fn build_systray(bargs: &mut BuilderArgs) -> Result<gtk::MenuBar> {
let gtk_widget = gtk::MenuBar::new();
let props = Rc::new(systray::Props::new());
let props_clone = props.clone();
// copies for def_widget
def_widget!(bargs, _g, gtk_widget, {
// @prop icon-size - size of icons in the tray
prop(icon_size: as_i32) {
if icon_size <= 0 {
log::warn!("Icon size is not a positive number");
} else {
props.icon_size(icon_size);
}
},
// @prop pack-direction - how to arrange tray items
prop(pack_direction: as_string) { gtk_widget.set_pack_direction(parse_packdirection(&pack_direction)?); },
});
systray::spawn_systray(&gtk_widget, &props_clone);
Ok(gtk_widget)
}
/// @var orientation - "vertical", "v", "horizontal", "h"
fn parse_orientation(o: &str) -> Result<gtk::Orientation> {
enum_parse! { "orientation", o,
@ -1210,6 +1239,16 @@ fn parse_gravity(g: &str) -> Result<gtk::pango::Gravity> {
}
}
/// @var pack-direction - "right", "ltr", "left", "rtl", "down", "ttb", "up", "btt"
fn parse_packdirection(o: &str) -> Result<gtk::PackDirection> {
enum_parse! { "packdirection", o,
"right" | "ltr" => gtk::PackDirection::Ltr,
"left" | "rtl" => gtk::PackDirection::Rtl,
"down" | "ttb" => gtk::PackDirection::Ttb,
"up" | "btt" => gtk::PackDirection::Btt,
}
}
/// Connect a function to the first map event of a widget. After that first map, the handler will get disconnected.
fn connect_first_map<W: IsA<gtk::Widget>, F: Fn(&W) + 'static>(widget: &W, func: F) {
let signal_handler_id = std::rc::Rc::new(std::cell::RefCell::new(None));

View File

@ -0,0 +1,18 @@
[package]
name = "notifier_host"
version = "0.1.0"
authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"]
edition = "2021"
license = "MIT"
description = "SystemNotifierHost implementation"
repository = "https://github.com/elkowar/eww"
homepage = "https://github.com/elkowar/eww"
[dependencies]
gtk = "0.17.1"
zbus = { version = "3.7.0", default-features = false, features = ["tokio"] }
dbusmenu-gtk3 = "0.1.0"
log.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["full"] }

View File

@ -0,0 +1,135 @@
use crate::*;
use zbus::export::ordered_stream::{self, OrderedStreamExt};
/// Trait for system tray implementations, to be notified of changes to what items are in the tray.
pub trait Host {
/// Called when an item is added to the tray. This is also called for all existing items when
/// starting [`run_host`].
fn add_item(&mut self, id: &str, item: Item);
/// Called when an item is removed from the tray.
fn remove_item(&mut self, id: &str);
}
// TODO We aren't really thinking about what happens when we shut down a host. Currently, we don't
// provide a way to unregister as a host.
//
// It would also be good to combine `register_as_host` and `run_host`, so that we're only
// registered while we're running.
/// Register this DBus connection as a StatusNotifierHost (i.e. system tray).
///
/// This associates with the DBus connection new name of the format
/// `org.freedesktop.StatusNotifierHost-{pid}-{nr}`, and registers it to active
/// StatusNotifierWatcher. The name and the StatusNotifierWatcher proxy are returned.
///
/// You still need to call [`run_host`] to have the instance of [`Host`] be notified of new and
/// removed items.
pub async fn register_as_host(
con: &zbus::Connection,
) -> zbus::Result<(zbus::names::WellKnownName<'static>, proxy::StatusNotifierWatcherProxy<'static>)> {
let snw = proxy::StatusNotifierWatcherProxy::new(con).await?;
// get a well-known name
let pid = std::process::id();
let mut i = 0;
let wellknown = loop {
use zbus::fdo::RequestNameReply::*;
i += 1;
let wellknown = format!("org.freedesktop.StatusNotifierHost-{}-{}", pid, i);
let wellknown: zbus::names::WellKnownName = wellknown.try_into().expect("generated well-known name is invalid");
let flags = [zbus::fdo::RequestNameFlags::DoNotQueue];
match con.request_name_with_flags(&wellknown, flags.into_iter().collect()).await? {
PrimaryOwner => break wellknown,
Exists => {}
AlreadyOwner => {}
InQueue => unreachable!("request_name_with_flags returned InQueue even though we specified DoNotQueue"),
};
};
// register it to the StatusNotifierWatcher, so that they know there is a systray on the system
snw.register_status_notifier_host(&wellknown).await?;
Ok((wellknown, snw))
}
/// Run the Host forever, calling its methods as signals are received from the StatusNotifierWatcher.
///
/// Before calling this, you should have called [`register_as_host`] (which returns an instance of
/// [`proxy::StatusNotifierWatcherProxy`]).
///
/// This async function runs forever, and only returns if it gets an error! As such, it is
/// recommended to call this via something like `tokio::spawn` that runs this in the
/// background.
pub async fn run_host(host: &mut dyn Host, snw: &proxy::StatusNotifierWatcherProxy<'static>) -> zbus::Error {
// Replacement for ? operator since we're not returning a Result.
macro_rules! try_ {
($e:expr) => {
match $e {
Ok(x) => x,
Err(e) => return e,
}
};
}
enum ItemEvent {
NewItem(proxy::StatusNotifierItemRegistered),
GoneItem(proxy::StatusNotifierItemUnregistered),
}
// start listening to these streams
let new_items = try_!(snw.receive_status_notifier_item_registered().await);
let gone_items = try_!(snw.receive_status_notifier_item_unregistered().await);
let mut item_names = std::collections::HashSet::new();
// initial items first
for svc in try_!(snw.registered_status_notifier_items().await) {
match Item::from_address(snw.connection(), &svc).await {
Ok(item) => {
item_names.insert(svc.to_owned());
host.add_item(&svc, item);
}
Err(e) => {
log::warn!("Could not create StatusNotifierItem from address {:?}: {:?}", svc, e);
}
}
}
let mut ev_stream = ordered_stream::join(
OrderedStreamExt::map(new_items, ItemEvent::NewItem),
OrderedStreamExt::map(gone_items, ItemEvent::GoneItem),
);
while let Some(ev) = ev_stream.next().await {
match ev {
ItemEvent::NewItem(sig) => {
let svc = try_!(sig.args()).service;
if item_names.contains(svc) {
log::info!("Got duplicate new item: {:?}", svc);
} else {
match Item::from_address(snw.connection(), svc).await {
Ok(item) => {
item_names.insert(svc.to_owned());
host.add_item(svc, item);
}
Err(e) => {
log::warn!("Could not create StatusNotifierItem from address {:?}: {:?}", svc, e);
}
}
}
}
ItemEvent::GoneItem(sig) => {
let svc = try_!(sig.args()).service;
if item_names.remove(svc) {
host.remove_item(svc);
}
}
}
}
// I do not know whether this is possible to reach or not.
unreachable!("StatusNotifierWatcher stopped producing events")
}

View File

@ -0,0 +1,207 @@
use crate::*;
use gtk::{self, prelude::*};
#[derive(thiserror::Error, Debug)]
enum IconError {
#[error("while fetching icon name: {0}")]
DBusIconName(#[source] zbus::Error),
#[error("while fetching icon theme path: {0}")]
DBusTheme(#[source] zbus::Error),
#[error("while fetching pixmap: {0}")]
DBusPixmap(#[source] zbus::Error),
#[error("loading icon from file {path:?}")]
LoadIconFromFile {
path: String,
#[source]
source: gtk::glib::Error,
},
#[error("loading icon {icon_name:?} from theme {}", .theme_path.as_ref().unwrap_or(&"(default)".to_owned()))]
LoadIconFromTheme {
icon_name: String,
theme_path: Option<String>,
#[source]
source: gtk::glib::Error,
},
#[error("no icon available")]
NotAvailable,
}
/// Get the fallback GTK icon, as a final fallback if the tray item has no icon.
async fn fallback_icon(size: i32, scale: i32) -> Option<gtk::gdk_pixbuf::Pixbuf> {
let theme = gtk::IconTheme::default().expect("Could not get default gtk theme");
match theme.load_icon_for_scale("image-missing", size, scale, gtk::IconLookupFlags::FORCE_SIZE) {
Ok(pb) => pb,
Err(e) => {
log::error!("failed to load \"image-missing\" from default theme: {}", e);
None
}
}
}
/// Load a pixbuf from StatusNotifierItem's [Icon format].
///
/// [Icon format]: https://freedesktop.org/wiki/Specifications/StatusNotifierItem/Icons/
fn icon_from_pixmap(width: i32, height: i32, mut data: Vec<u8>) -> gtk::gdk_pixbuf::Pixbuf {
// We need to convert data from ARGB32 to RGBA32, since that's the only one that gdk-pixbuf
// understands.
for chunk in data.chunks_exact_mut(4) {
let a = chunk[0];
let r = chunk[1];
let g = chunk[2];
let b = chunk[3];
chunk[0] = r;
chunk[1] = g;
chunk[2] = b;
chunk[3] = a;
}
gtk::gdk_pixbuf::Pixbuf::from_bytes(
&gtk::glib::Bytes::from_owned(data),
gtk::gdk_pixbuf::Colorspace::Rgb,
true,
8,
width,
height,
width * 4,
)
}
/// From a list of pixmaps, create an icon from the most appropriately sized one.
///
/// This function returns None if and only if no pixmaps are provided.
fn icon_from_pixmaps(pixmaps: Vec<(i32, i32, Vec<u8>)>, size: i32) -> Option<gtk::gdk_pixbuf::Pixbuf> {
pixmaps
.into_iter()
.max_by(|(w1, h1, _), (w2, h2, _)| {
// take smallest one bigger than requested size, otherwise take biggest
let a = size * size;
let a1 = w1 * h1;
let a2 = w2 * h2;
match (a1 >= a, a2 >= a) {
(true, true) => a2.cmp(&a1),
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
(false, false) => a1.cmp(&a2),
}
})
.and_then(|(w, h, d)| {
let pixbuf = icon_from_pixmap(w, h, d);
if w != size || h != size {
pixbuf.scale_simple(size, size, gtk::gdk_pixbuf::InterpType::Bilinear)
} else {
Some(pixbuf)
}
})
}
/// Load an icon with a given name from either the default (if `theme_path` is `None`), or from the
/// theme at a path.
fn icon_from_name(
icon_name: &str,
theme_path: Option<&str>,
size: i32,
scale: i32,
) -> std::result::Result<gtk::gdk_pixbuf::Pixbuf, IconError> {
let theme = if let Some(path) = theme_path {
let theme = gtk::IconTheme::new();
theme.prepend_search_path(&path);
theme
} else {
gtk::IconTheme::default().expect("Could not get default gtk theme")
};
match theme.load_icon_for_scale(icon_name, size, scale, gtk::IconLookupFlags::FORCE_SIZE) {
Ok(pb) => Ok(pb.expect("no pixbuf from theme.load_icon despite no error")),
Err(e) => Err(IconError::LoadIconFromTheme {
icon_name: icon_name.to_owned(),
theme_path: theme_path.map(str::to_owned),
source: e,
}),
}
}
pub async fn load_icon_from_sni(
sni: &proxy::StatusNotifierItemProxy<'_>,
size: i32,
scale: i32,
) -> Option<gtk::gdk_pixbuf::Pixbuf> {
// "Visualizations are encouraged to prefer icon names over icon pixmaps if both are
// available."
let scaled_size = size * scale;
// First, see if we can get an icon from the name they provide, using either the theme they
// specify or the default.
let icon_from_name: std::result::Result<gtk::gdk_pixbuf::Pixbuf, IconError> = (async {
// fetch icon name
let icon_name = sni.icon_name().await;
log::debug!("dbus: {} icon_name -> {:?}", sni.destination(), icon_name);
let icon_name = match icon_name {
Ok(s) if s.is_empty() => return Err(IconError::NotAvailable),
Ok(s) => s,
Err(e) => return Err(IconError::DBusIconName(e)),
};
// interpret it as an absolute path if we can
let icon_path = std::path::Path::new(&icon_name);
if icon_path.is_absolute() && icon_path.is_file() {
return gtk::gdk_pixbuf::Pixbuf::from_file_at_size(icon_path, scaled_size, scaled_size)
.map_err(|e| IconError::LoadIconFromFile { path: icon_name, source: e });
}
// otherwise, fetch icon theme and lookup using icon_from_name
let icon_theme_path = sni.icon_theme_path().await;
log::debug!("dbus: {} icon_theme_path -> {:?}", sni.destination(), icon_theme_path);
let icon_theme_path = match icon_theme_path {
Ok(p) if p.is_empty() => None,
Ok(p) => Some(p),
// treat property not existing as the same as it being empty i.e. to use the default
// system theme
Err(zbus::Error::FDO(e)) => match *e {
zbus::fdo::Error::UnknownProperty(_) | zbus::fdo::Error::InvalidArgs(_) => None,
// this error is reported by discord, blueman-applet
zbus::fdo::Error::Failed(msg) if msg == "error occurred in Get" => None,
_ => return Err(IconError::DBusTheme(zbus::Error::FDO(e))),
},
Err(e) => return Err(IconError::DBusTheme(e)),
};
let icon_theme_path: Option<&str> = match &icon_theme_path {
// this looks weird but this converts &String to &str
Some(s) => Some(s),
None => None,
};
icon_from_name(&icon_name, icon_theme_path, size, scale)
})
.await;
match icon_from_name {
Ok(p) => return Some(p), // got an icon!
Err(IconError::NotAvailable) => {} // this error is expected, don't log
Err(e) => log::warn!("failed to get icon by name for {}: {}", sni.destination(), e),
};
// Can't get it from name + theme, try the pixmap
let icon_from_pixmaps = match sni.icon_pixmap().await {
Ok(ps) => match icon_from_pixmaps(ps, scaled_size) {
Some(p) => Ok(p),
None => Err(IconError::NotAvailable),
},
Err(zbus::Error::FDO(e)) => match *e {
// property not existing is an expected error
zbus::fdo::Error::UnknownProperty(_) | zbus::fdo::Error::InvalidArgs(_) => Err(IconError::NotAvailable),
_ => Err(IconError::DBusPixmap(zbus::Error::FDO(e))),
},
Err(e) => Err(IconError::DBusPixmap(e)),
};
match icon_from_pixmaps {
Ok(p) => return Some(p),
Err(IconError::NotAvailable) => {}
Err(e) => log::warn!("failed to get icon pixmap for {}: {}", sni.destination(), e),
};
// Tray didn't provide a valid icon so use the default fallback one.
fallback_icon(size, scale).await
}

View File

@ -0,0 +1,97 @@
use crate::*;
use gtk::{self, prelude::*};
/// Recognised values of [`org.freedesktop.StatusNotifierItem.Status`].
///
/// [`org.freedesktop.StatusNotifierItem.Status`]: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/#org.freedesktop.statusnotifieritem.status
#[derive(Debug, Clone, Copy)]
pub enum Status {
/// The item doesn't convey important information to the user, it can be considered an "idle"
/// status and is likely that visualizations will chose to hide it.
Passive,
/// The item is active, is more important that the item will be shown in some way to the user.
Active,
/// The item carries really important information for the user, such as battery charge running
/// out and is wants to incentive the direct user intervention. Visualizations should emphasize
/// in some way the items with NeedsAttention status.
NeedsAttention,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ParseStatusError;
impl std::str::FromStr for Status {
type Err = ParseStatusError;
fn from_str(s: &str) -> std::result::Result<Self, ParseStatusError> {
match s {
"Passive" => Ok(Status::Passive),
"Active" => Ok(Status::Active),
"NeedsAttention" => Ok(Status::NeedsAttention),
_ => Err(ParseStatusError),
}
}
}
/// A StatusNotifierItem (SNI).
///
/// At the moment, this does not wrap much of the SNI's properties and methods. As such, you should
/// directly access the `sni` member as needed for functionalty that is not provided.
pub struct Item {
/// The StatusNotifierItem that is wrapped by this instance.
pub sni: proxy::StatusNotifierItemProxy<'static>,
}
impl Item {
/// Create an instance from the service's address.
///
/// The format of `addr` is `{bus}{object_path}` (e.g.
/// `:1.50/org/ayatana/NotificationItem/nm_applet`), which is the format that is used for
/// StatusNotifierWatcher's [RegisteredStatusNotifierItems property][rsni]).
///
/// [rsni]: https://freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierWatcher/#registeredstatusnotifieritems
pub async fn from_address(con: &zbus::Connection, service: &str) -> zbus::Result<Self> {
let (addr, path) = {
// Based on <https://github.com/oknozor/stray/blob/main/stray/src/notifier_watcher/notifier_address.rs>
//
// TODO is the service name format actually documented anywhere?
if let Some((addr, path)) = service.split_once('/') {
(addr.to_owned(), format!("/{}", path))
} else if service.starts_with(':') {
(service[0..6].to_owned(), names::ITEM_OBJECT.to_owned())
} else {
return Err(zbus::Error::Address(service.to_owned()));
}
};
let sni = proxy::StatusNotifierItemProxy::builder(con).destination(addr)?.path(path)?.build().await?;
Ok(Item { sni })
}
/// Get the current status of the item.
pub async fn status(&self) -> zbus::Result<Status> {
let status = self.sni.status().await?;
match status.parse() {
Ok(s) => Ok(s),
Err(_) => Err(zbus::Error::Failure(format!("Invalid status {:?}", status))),
}
}
/// Get the menu of this item.
pub async fn menu(&self) -> zbus::Result<gtk::Menu> {
// TODO document what this returns if there is no menu.
let menu = dbusmenu_gtk3::Menu::new(self.sni.destination(), &self.sni.menu().await?);
Ok(menu.upcast())
}
/// Get the current icon.
pub async fn icon(&self, size: i32, scale: i32) -> Option<gtk::gdk_pixbuf::Pixbuf> {
// TODO explain what size and scale mean here
// see icon.rs
load_icon_from_sni(&self.sni, size, scale).await
}
}

View File

@ -0,0 +1,52 @@
//! The system tray side of the [notifier host DBus
//! protocols](https://freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierHost/),
//! implementing most of the relevant DBus protocol logic so system tray implementations (e.g. eww)
//! don't need to care about them.
//!
//! This crate does not implement the tray icon side of the protocol. For that, see, for example,
//! the [ksni](https://crates.io/crates/ksni) crate.
//!
//! # Overview / Notes for Contributors
//!
//! This crate makes extensive use of the `zbus` library to interact with DBus. You should read
//! through the [zbus tutorial](https://dbus2.github.io/zbus/) if you aren't familiar with DBus or
//! `zbus`.
//!
//! There are two separate services that are required for the tray side of the protocol:
//!
//! - `StatusNotifierWatcher`, a service which tracks what items and trays there are but doesn't do
//! any rendering. This is implemented by [`Watcher`] (see that for further details), and
//! should always be started alongside the `StatusNotifierHost`.
//!
//! - `StatusNotifierHost`, the actual tray, which registers itself to the StatusNotifierHost and
//! subscribes to its signals to know what items exist. This DBus service has a completely
//! empty interface, but is mainly by StatusNotifierWatcher to know when trays disappear. This
//! is represented by the [`Host`] trait.
//!
//! The actual tray implements the [`Host`] trait to be notified of when items (called
//! `StatusNotifierItem` in the spec and represented by [`Item`]) appear and disappear, then calls
//! [`run_host`] to run the DBus side of the protocol.
//!
//! If there are multiple trays running on the system, there can be multiple `StatusNotifierHost`s,
//! but only one `StatusNotifierWatcher` (usually from whatever tray was started first).
pub mod proxy;
mod host;
pub use host::*;
mod icon;
pub use icon::*;
mod item;
pub use item::*;
mod watcher;
pub use watcher::*;
pub(crate) mod names {
pub const WATCHER_BUS: &str = "org.kde.StatusNotifierWatcher";
pub const WATCHER_OBJECT: &str = "/StatusNotifierWatcher";
pub const ITEM_OBJECT: &str = "/StatusNotifierItem";
}

View File

@ -0,0 +1,69 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="com.canonical.dbusmenu">
<!-- Properties -->
<property name="Version" type="u" access="read" />
<property name="TextDirection" type="s" access="read" />
<property name="Status" type="s" access="read" />
<property name="IconThemePath" type="as" access="read" />
<!-- Functions -->
<method name="GetLayout">
<arg type="i" name="parentId" direction="in" />
<arg type="i" name="recursionDepth" direction="in" />
<arg type="as" name="propertyNames" direction="in" />
<arg type="u" name="revision" direction="out" />
<arg type="(ia{sv}av)" name="layout" direction="out" />
</method>
<method name="GetGroupProperties">
<arg type="ai" name="ids" direction="in" />
<arg type="as" name="propertyNames" direction="in" />
<arg type="a(ia{sv})" name="properties" direction="out" />
</method>
<method name="GetProperty">
<arg type="i" name="id" direction="in" />
<arg type="s" name="name" direction="in" />
<arg type="v" name="value" direction="out" />
</method>
<method name="Event">
<arg type="i" name="id" direction="in" />
<arg type="s" name="eventId" direction="in" />
<arg type="v" name="data" direction="in" />
<arg type="u" name="timestamp" direction="in" />
</method>
<method name="EventGroup">
<arg type="a(isvu)" name="events" direction="in" />
<arg type="ai" name="idErrors" direction="out" />
</method>
<method name="AboutToShow">
<arg type="i" name="id" direction="in" />
<arg type="b" name="needUpdate" direction="out" />
</method>
<method name="AboutToShowGroup">
<arg type="ai" name="ids" direction="in" />
<arg type="ai" name="updatesNeeded" direction="out" />
<arg type="ai" name="idErrors" direction="out" />
</method>
<!-- Signals -->
<signal name="ItemsPropertiesUpdated">
<arg type="a(ia{sv})" name="updatedProps" direction="out" />
<arg type="a(ias)" name="removedProps" direction="out" />
</signal>
<signal name="LayoutUpdated">
<arg type="u" name="revision" direction="out" />
<arg type="i" name="parent" direction="out" />
</signal>
<signal name="ItemActivationRequested">
<arg type="i" name="id" direction="out" />
<arg type="u" name="timestamp" direction="out" />
</signal>
</interface>
</node>

View File

@ -0,0 +1,114 @@
//! # DBus interface proxy for: `org.kde.StatusNotifierItem`
//!
//! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.
//! Source: `dbus-status-notifier-item.xml`.
//!
//! You may prefer to adapt it, instead of using it verbatim.
//!
//! More information can be found in the
//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
//! section of the zbus documentation.
// suppress warning from generated code
#![allow(clippy::type_complexity)]
use zbus::dbus_proxy;
#[dbus_proxy(interface = "org.kde.StatusNotifierItem", assume_defaults = true)]
trait StatusNotifierItem {
/// Activate method
fn activate(&self, x: i32, y: i32) -> zbus::Result<()>;
/// ContextMenu method
fn context_menu(&self, x: i32, y: i32) -> zbus::Result<()>;
/// Scroll method
fn scroll(&self, delta: i32, orientation: &str) -> zbus::Result<()>;
/// SecondaryActivate method
fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>;
/// NewAttentionIcon signal
#[dbus_proxy(signal)]
fn new_attention_icon(&self) -> zbus::Result<()>;
/// NewIcon signal
#[dbus_proxy(signal)]
fn new_icon(&self) -> zbus::Result<()>;
/// NewOverlayIcon signal
#[dbus_proxy(signal)]
fn new_overlay_icon(&self) -> zbus::Result<()>;
/// NewStatus signal
#[dbus_proxy(signal)]
fn new_status(&self, status: &str) -> zbus::Result<()>;
/// NewTitle signal
#[dbus_proxy(signal)]
fn new_title(&self) -> zbus::Result<()>;
/// NewToolTip signal
#[dbus_proxy(signal)]
fn new_tool_tip(&self) -> zbus::Result<()>;
/// AttentionIconName property
#[dbus_proxy(property)]
fn attention_icon_name(&self) -> zbus::Result<String>;
/// AttentionIconPixmap property
#[dbus_proxy(property)]
fn attention_icon_pixmap(&self) -> zbus::Result<Vec<(i32, i32, Vec<u8>)>>;
/// AttentionMovieName property
#[dbus_proxy(property)]
fn attention_movie_name(&self) -> zbus::Result<String>;
/// Category property
#[dbus_proxy(property)]
fn category(&self) -> zbus::Result<String>;
/// IconName property
#[dbus_proxy(property(emits_changed_signal = "false"))]
fn icon_name(&self) -> zbus::Result<String>;
/// IconPixmap property
#[dbus_proxy(property(emits_changed_signal = "false"))]
fn icon_pixmap(&self) -> zbus::Result<Vec<(i32, i32, Vec<u8>)>>;
/// IconThemePath property
#[dbus_proxy(property)]
fn icon_theme_path(&self) -> zbus::Result<String>;
/// Id property
#[dbus_proxy(property)]
fn id(&self) -> zbus::Result<String>;
/// ItemIsMenu property
#[dbus_proxy(property)]
fn item_is_menu(&self) -> zbus::Result<bool>;
/// Menu property
#[dbus_proxy(property)]
fn menu(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>;
/// OverlayIconName property
#[dbus_proxy(property)]
fn overlay_icon_name(&self) -> zbus::Result<String>;
/// OverlayIconPixmap property
#[dbus_proxy(property)]
fn overlay_icon_pixmap(&self) -> zbus::Result<Vec<(i32, i32, Vec<u8>)>>;
/// Status property
#[dbus_proxy(property)]
fn status(&self) -> zbus::Result<String>;
/// Title property
#[dbus_proxy(property)]
fn title(&self) -> zbus::Result<String>;
/// ToolTip property
#[dbus_proxy(property)]
fn tool_tip(&self) -> zbus::Result<(String, Vec<(i32, i32, Vec<u8>)>)>;
}

View File

@ -0,0 +1,49 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name='org.kde.StatusNotifierItem'>
<annotation name="org.gtk.GDBus.C.Name" value="Item" />
<method name='ContextMenu'>
<arg type='i' direction='in' name='x'/>
<arg type='i' direction='in' name='y'/>
</method>
<method name='Activate'>
<arg type='i' direction='in' name='x'/>
<arg type='i' direction='in' name='y'/>
</method>
<method name='SecondaryActivate'>
<arg type='i' direction='in' name='x'/>
<arg type='i' direction='in' name='y'/>
</method>
<method name='Scroll'>
<arg type='i' direction='in' name='delta'/>
<arg type='s' direction='in' name='orientation'/>
</method>
<signal name='NewTitle'/>
<signal name='NewIcon'/>
<signal name='NewAttentionIcon'/>
<signal name='NewOverlayIcon'/>
<signal name='NewToolTip'/>
<signal name='NewStatus'>
<arg type='s' name='status'/>
</signal>
<property name='Category' type='s' access='read'/>
<property name='Id' type='s' access='read'/>
<property name='Title' type='s' access='read'/>
<property name='Status' type='s' access='read'/>
<!-- See discussion on pull #536
<property name='WindowId' type='u' access='read'/>
-->
<property name='IconThemePath' type='s' access='read'/>
<property name='IconName' type='s' access='read'/>
<property name='IconPixmap' type='a(iiay)' access='read'/>
<property name='OverlayIconName' type='s' access='read'/>
<property name='OverlayIconPixmap' type='a(iiay)' access='read'/>
<property name='AttentionIconName' type='s' access='read'/>
<property name='AttentionIconPixmap' type='a(iiay)' access='read'/>
<property name='AttentionMovieName' type='s' access='read'/>
<property name='ToolTip' type='(sa(iiay)ss)' access='read'/>
<property name='Menu' type='o' access='read'/>
<property name='ItemIsMenu' type='b' access='read'/>
</interface>
</node>

View File

@ -0,0 +1,53 @@
//! # DBus interface proxy for: `org.kde.StatusNotifierWatcher`
//!
//! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.
//! Source: `dbus-status-notifier-watcher.xml`.
//!
//! You may prefer to adapt it, instead of using it verbatim.
//!
//! More information can be found in the
//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
//! section of the zbus documentation.
use zbus::dbus_proxy;
#[dbus_proxy(
default_service = "org.kde.StatusNotifierWatcher",
interface = "org.kde.StatusNotifierWatcher",
default_path = "/StatusNotifierWatcher"
)]
trait StatusNotifierWatcher {
/// RegisterStatusNotifierHost method
fn register_status_notifier_host(&self, service: &str) -> zbus::Result<()>;
/// RegisterStatusNotifierItem method
fn register_status_notifier_item(&self, service: &str) -> zbus::Result<()>;
/// StatusNotifierHostRegistered signal
#[dbus_proxy(signal)]
fn status_notifier_host_registered(&self) -> zbus::Result<()>;
/// StatusNotifierHostUnregistered signal
#[dbus_proxy(signal)]
fn status_notifier_host_unregistered(&self) -> zbus::Result<()>;
/// StatusNotifierItemRegistered signal
#[dbus_proxy(signal)]
fn status_notifier_item_registered(&self, service: &str) -> zbus::Result<()>;
/// StatusNotifierItemUnregistered signal
#[dbus_proxy(signal)]
fn status_notifier_item_unregistered(&self, service: &str) -> zbus::Result<()>;
/// IsStatusNotifierHostRegistered property
#[dbus_proxy(property)]
fn is_status_notifier_host_registered(&self) -> zbus::Result<bool>;
/// ProtocolVersion property
#[dbus_proxy(property)]
fn protocol_version(&self) -> zbus::Result<i32>;
/// RegisteredStatusNotifierItems property
#[dbus_proxy(property)]
fn registered_status_notifier_items(&self) -> zbus::Result<Vec<String>>;
}

View File

@ -0,0 +1,52 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.StatusNotifierWatcher">
<annotation name="org.gtk.GDBus.C.Name" value="Watcher" />
<!-- methods -->
<method name="RegisterStatusNotifierItem">
<annotation name="org.gtk.GDBus.C.Name" value="RegisterItem" />
<arg name="service" type="s" direction="in"/>
</method>
<method name="RegisterStatusNotifierHost">
<annotation name="org.gtk.GDBus.C.Name" value="RegisterHost" />
<arg name="service" type="s" direction="in"/>
</method>
<!-- properties -->
<property name="RegisteredStatusNotifierItems" type="as" access="read">
<annotation name="org.gtk.GDBus.C.Name" value="RegisteredItems" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/>
</property>
<property name="IsStatusNotifierHostRegistered" type="b" access="read">
<annotation name="org.gtk.GDBus.C.Name" value="IsHostRegistered" />
</property>
<property name="ProtocolVersion" type="i" access="read"/>
<!-- signals -->
<signal name="StatusNotifierItemRegistered">
<annotation name="org.gtk.GDBus.C.Name" value="ItemRegistered" />
<arg type="s" direction="out" name="service" />
</signal>
<signal name="StatusNotifierItemUnregistered">
<annotation name="org.gtk.GDBus.C.Name" value="ItemUnregistered" />
<arg type="s" direction="out" name="service" />
</signal>
<signal name="StatusNotifierHostRegistered">
<annotation name="org.gtk.GDBus.C.Name" value="HostRegistered" />
</signal>
<signal name="StatusNotifierHostUnregistered">
<annotation name="org.gtk.GDBus.C.Name" value="HostUnregistered" />
</signal>
</interface>
</node>

View File

@ -0,0 +1,16 @@
//! Proxies for DBus services, so we can call them.
//!
//! The interface XML files were taken from
//! [Waybar](https://github.com/Alexays/Waybar/tree/master/protocol), and the proxies were
//! generated with [zbus-xmlgen](https://docs.rs/crate/zbus_xmlgen/latest) by running `zbus-xmlgen
//! dbus_status_notifier_item.xml` and `zbus-xmlgen dbus_status_notifier_watcher.xml`. At the
//! moment, `dbus_menu.xml` isn't used.
//!
//! For more information, see ["Writing a client proxy" in the zbus
//! tutorial](https://dbus2.github.io/zbus/).
mod dbus_status_notifier_item;
pub use dbus_status_notifier_item::*;
mod dbus_status_notifier_watcher;
pub use dbus_status_notifier_watcher::*;

View File

@ -0,0 +1,299 @@
use crate::names;
use zbus::{dbus_interface, export::ordered_stream::OrderedStreamExt, Interface};
/// An instance of [`org.kde.StatusNotifierWatcher`]. It only tracks what tray items and trays
/// exist, and doesn't have any logic for displaying items (for that, see [`Host`][`crate::Host`]).
///
/// While this is usually run alongside the tray, it can also be used standalone.
///
/// [`org.kde.StatusNotifierWatcher`]: https://freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierWatcher/
#[derive(Debug, Default)]
pub struct Watcher {
tasks: tokio::task::JoinSet<()>,
// Intentionally using std::sync::Mutex instead of tokio's async mutex, since we don't need to
// hold the mutex across an await.
//
// See <https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use>
hosts: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
items: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
}
/// Implementation of the `StatusNotifierWatcher` service.
///
/// Methods and properties correspond to methods and properties on the DBus service that can be
/// used by others, while signals are events that we generate that other services listen to.
#[dbus_interface(name = "org.kde.StatusNotifierWatcher")]
impl Watcher {
/// RegisterStatusNotifierHost method
async fn register_status_notifier_host(
&mut self,
service: &str,
#[zbus(header)] hdr: zbus::MessageHeader<'_>,
#[zbus(connection)] con: &zbus::Connection,
#[zbus(signal_context)] ctxt: zbus::SignalContext<'_>,
) -> zbus::fdo::Result<()> {
// TODO right now, we convert everything to the unique bus name (something like :1.234).
// However, it might make more sense to listen to the actual name they give us, so that if
// the connection dissociates itself from the org.kde.StatusNotifierHost-{pid}-{nr} name
// but still remains around, we drop them as a host.
//
// (This also applies to RegisterStatusNotifierItem)
let (service, _) = parse_service(service, hdr, con).await?;
log::info!("new host: {}", service);
let added_first = {
// scoped around locking of hosts
let mut hosts = self.hosts.lock().unwrap(); // unwrap: mutex poisoning is okay
if !hosts.insert(service.to_string()) {
// we're already tracking them
return Ok(());
}
hosts.len() == 1
};
if added_first {
self.is_status_notifier_host_registered_changed(&ctxt).await?;
}
Watcher::status_notifier_host_registered(&ctxt).await?;
self.tasks.spawn({
let hosts = self.hosts.clone();
let ctxt = ctxt.to_owned();
let con = con.to_owned();
async move {
if let Err(e) = wait_for_service_exit(&con, service.as_ref().into()).await {
log::error!("failed to wait for service exit: {}", e);
}
log::info!("lost host: {}", service);
let removed_last = {
let mut hosts = hosts.lock().unwrap(); // unwrap: mutex poisoning is okay
let did_remove = hosts.remove(service.as_str());
did_remove && hosts.is_empty()
};
if removed_last {
if let Err(e) = Watcher::is_status_notifier_host_registered_refresh(&ctxt).await {
log::error!("failed to signal Watcher: {}", e);
}
}
if let Err(e) = Watcher::status_notifier_host_unregistered(&ctxt).await {
log::error!("failed to signal Watcher: {}", e);
}
}
});
Ok(())
}
/// StatusNotifierHostRegistered signal.
#[dbus_interface(signal)]
async fn status_notifier_host_registered(ctxt: &zbus::SignalContext<'_>) -> zbus::Result<()>;
/// StatusNotifierHostUnregistered signal
#[dbus_interface(signal)]
async fn status_notifier_host_unregistered(ctxt: &zbus::SignalContext<'_>) -> zbus::Result<()>;
/// IsStatusNotifierHostRegistered property
#[dbus_interface(property)]
async fn is_status_notifier_host_registered(&self) -> bool {
let hosts = self.hosts.lock().unwrap(); // unwrap: mutex poisoning is okay
!hosts.is_empty()
}
// ------------------------------------------------------------------------
/// RegisterStatusNotifierItem method
async fn register_status_notifier_item(
&mut self,
service: &str,
#[zbus(header)] hdr: zbus::MessageHeader<'_>,
#[zbus(connection)] con: &zbus::Connection,
#[zbus(signal_context)] ctxt: zbus::SignalContext<'_>,
) -> zbus::fdo::Result<()> {
let (service, objpath) = parse_service(service, hdr, con).await?;
let service = zbus::names::BusName::Unique(service);
let item = format!("{}{}", service, objpath);
{
let mut items = self.items.lock().unwrap(); // unwrap: mutex poisoning is okay
if !items.insert(item.clone()) {
// we're already tracking them
log::info!("new item: {} (duplicate)", item);
return Ok(());
}
}
log::info!("new item: {}", item);
self.registered_status_notifier_items_changed(&ctxt).await?;
Watcher::status_notifier_item_registered(&ctxt, item.as_ref()).await?;
self.tasks.spawn({
let items = self.items.clone();
let ctxt = ctxt.to_owned();
let con = con.to_owned();
async move {
if let Err(e) = wait_for_service_exit(&con, service.as_ref()).await {
log::error!("failed to wait for service exit: {}", e);
}
println!("gone item: {}", &item);
{
let mut items = items.lock().unwrap(); // unwrap: mutex poisoning is okay
items.remove(&item);
}
if let Err(e) = Watcher::registered_status_notifier_items_refresh(&ctxt).await {
log::error!("failed to signal Watcher: {}", e);
}
if let Err(e) = Watcher::status_notifier_item_unregistered(&ctxt, item.as_ref()).await {
log::error!("failed to signal Watcher: {}", e);
}
}
});
Ok(())
}
/// StatusNotifierItemRegistered signal
#[dbus_interface(signal)]
async fn status_notifier_item_registered(ctxt: &zbus::SignalContext<'_>, service: &str) -> zbus::Result<()>;
/// StatusNotifierItemUnregistered signal
#[dbus_interface(signal)]
async fn status_notifier_item_unregistered(ctxt: &zbus::SignalContext<'_>, service: &str) -> zbus::Result<()>;
/// RegisteredStatusNotifierItems property
#[dbus_interface(property)]
async fn registered_status_notifier_items(&self) -> Vec<String> {
let items = self.items.lock().unwrap(); // unwrap: mutex poisoning is okay
items.iter().cloned().collect()
}
// ------------------------------------------------------------------------
/// ProtocolVersion property
#[dbus_interface(property)]
fn protocol_version(&self) -> i32 {
0
}
}
impl Watcher {
/// Create a new Watcher.
pub fn new() -> Watcher {
Default::default()
}
/// Attach and run the Watcher (in the background) on a connection.
pub async fn attach_to(self, con: &zbus::Connection) -> zbus::Result<()> {
if !con.object_server().at(names::WATCHER_OBJECT, self).await? {
return Err(zbus::Error::Failure(format!(
"Object already exists at {} on this connection -- is StatusNotifierWatcher already running?",
names::WATCHER_OBJECT
)));
}
// not AllowReplacement, not ReplaceExisting, not DoNotQueue
let flags: [zbus::fdo::RequestNameFlags; 0] = [];
match con.request_name_with_flags(names::WATCHER_BUS, flags.into_iter().collect()).await {
Ok(zbus::fdo::RequestNameReply::PrimaryOwner) => Ok(()),
Ok(_) | Err(zbus::Error::NameTaken) => Ok(()), // defer to existing
Err(e) => Err(e),
}
}
/// Equivalent to `is_status_notifier_host_registered_invalidate`, but without requiring
/// `self`.
async fn is_status_notifier_host_registered_refresh(ctxt: &zbus::SignalContext<'_>) -> zbus::Result<()> {
zbus::fdo::Properties::properties_changed(
ctxt,
Self::name(),
&std::collections::HashMap::new(),
&["IsStatusNotifierHostRegistered"],
)
.await
}
/// Equivalent to `registered_status_notifier_items_invalidate`, but without requiring `self`.
async fn registered_status_notifier_items_refresh(ctxt: &zbus::SignalContext<'_>) -> zbus::Result<()> {
zbus::fdo::Properties::properties_changed(
ctxt,
Self::name(),
&std::collections::HashMap::new(),
&["RegisteredStatusNotifierItems"],
)
.await
}
}
/// Decode the service name that others give to us, into the [bus
/// name](https://dbus2.github.io/zbus/concepts.html#bus-name--service-name) and the [object
/// path](https://dbus2.github.io/zbus/concepts.html#objects-and-object-paths) within the
/// connection.
///
/// The freedesktop.org specification has the format of this be just the bus name, however some
/// status items pass non-conforming values. One common one is just the object path.
async fn parse_service<'a>(
service: &'a str,
hdr: zbus::MessageHeader<'_>,
con: &zbus::Connection,
) -> zbus::fdo::Result<(zbus::names::UniqueName<'static>, &'a str)> {
if service.starts_with('/') {
// they sent us just the object path
if let Some(sender) = hdr.sender()? {
Ok((sender.to_owned(), service))
} else {
log::warn!("unknown sender");
Err(zbus::fdo::Error::InvalidArgs("Unknown bus address".into()))
}
} else {
// parse the bus name they gave us
let busname: zbus::names::BusName = match service.try_into() {
Ok(x) => x,
Err(e) => {
log::warn!("received invalid bus name {:?}: {}", service, e);
return Err(zbus::fdo::Error::InvalidArgs(e.to_string()));
}
};
if let zbus::names::BusName::Unique(unique) = busname {
Ok((unique.to_owned(), names::ITEM_OBJECT))
} else {
// they gave us a "well-known name" like org.kde.StatusNotifierHost-81830-0, we need to
// convert this into the actual identifier for their bus (e.g. :1.234), so that even if
// they remove that well-known name it's fine.
let dbus = zbus::fdo::DBusProxy::new(con).await?;
match dbus.get_name_owner(busname).await {
Ok(owner) => Ok((owner.into_inner(), names::ITEM_OBJECT)),
Err(e) => {
log::warn!("failed to get owner of {:?}: {}", service, e);
Err(e)
}
}
}
}
}
/// Wait for a DBus service to disappear
async fn wait_for_service_exit(con: &zbus::Connection, service: zbus::names::BusName<'_>) -> zbus::fdo::Result<()> {
let dbus = zbus::fdo::DBusProxy::new(con).await?;
let mut owner_changes = dbus.receive_name_owner_changed_with_args(&[(0, &service)]).await?;
if !dbus.name_has_owner(service.as_ref()).await? {
// service has already disappeared
return Ok(());
}
while let Some(sig) = owner_changes.next().await {
let args = sig.args()?;
if args.new_owner().is_none() {
break;
}
}
Ok(())
}

View File

@ -29,6 +29,7 @@ The following list of package names should work for arch linux:
- gtk-layer-shell (only on Wayland)
- pango (libpango)
- gdk-pixbuf2 (libgdk_pixbuf-2)
- libdbusmenu-gtk3
- cairo (libcairo, libcairo-gobject)
- glib2 (libgio, libglib-2, libgobject-2)
- gcc-libs (libgcc)

View File

@ -42,6 +42,8 @@
cargoDeps =
rustPlatform.importCargoLock { lockFile = ./Cargo.lock; };
patches = [ ];
# remove this when nixpkgs includes it
buildInputs = old.buildInputs ++ [ final.libdbusmenu-gtk3 ];
});
eww-wayland = final.eww;
@ -63,6 +65,10 @@
rust
rust-analyzer-unwrapped
gcc
glib
gdk-pixbuf
librsvg
libdbusmenu-gtk3
gtk3
gtk-layer-shell
pkg-config