mirror of
https://github.com/elkowar/eww.git
synced 2024-10-03 22:48:31 +03:00
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:
parent
f1ec00a1c9
commit
1b819fb646
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -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
|
||||
|
||||
|
@ -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
1467
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
$(
|
||||
|
@ -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;
|
||||
|
||||
|
203
crates/eww/src/widgets/systray.rs
Normal file
203
crates/eww/src/widgets/systray.rs
Normal 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: >k::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: >k::Image, item: ¬ifier_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());
|
||||
}
|
||||
}
|
@ -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(>k_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));
|
||||
|
18
crates/notifier_host/Cargo.toml
Normal file
18
crates/notifier_host/Cargo.toml
Normal 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"] }
|
135
crates/notifier_host/src/host.rs
Normal file
135
crates/notifier_host/src/host.rs
Normal 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")
|
||||
}
|
207
crates/notifier_host/src/icon.rs
Normal file
207
crates/notifier_host/src/icon.rs
Normal 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(
|
||||
>k::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
|
||||
}
|
97
crates/notifier_host/src/item.rs
Normal file
97
crates/notifier_host/src/item.rs
Normal 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
|
||||
}
|
||||
}
|
52
crates/notifier_host/src/lib.rs
Normal file
52
crates/notifier_host/src/lib.rs
Normal 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";
|
||||
}
|
69
crates/notifier_host/src/proxy/dbus_menu.xml
Normal file
69
crates/notifier_host/src/proxy/dbus_menu.xml
Normal 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>
|
114
crates/notifier_host/src/proxy/dbus_status_notifier_item.rs
Normal file
114
crates/notifier_host/src/proxy/dbus_status_notifier_item.rs
Normal 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>)>)>;
|
||||
}
|
49
crates/notifier_host/src/proxy/dbus_status_notifier_item.xml
Normal file
49
crates/notifier_host/src/proxy/dbus_status_notifier_item.xml
Normal 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>
|
@ -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>>;
|
||||
}
|
@ -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>
|
16
crates/notifier_host/src/proxy/mod.rs
Normal file
16
crates/notifier_host/src/proxy/mod.rs
Normal 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::*;
|
299
crates/notifier_host/src/watcher.rs
Normal file
299
crates/notifier_host/src/watcher.rs
Normal 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(())
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user