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
25 changed files with 2585 additions and 334 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- 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
|
- 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 nix flake
|
||||||
- Fix `jq` (By: w-lfchen)
|
- Fix `jq` (By: w-lfchen)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add `systray` widget (By: ralismark)
|
||||||
|
|
||||||
## [0.5.0] (17.02.2024)
|
## [0.5.0] (17.02.2024)
|
||||||
|
|
||||||
### BREAKING CHANGES
|
### 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" }
|
simplexpr = { version = "0.1.0", path = "crates/simplexpr" }
|
||||||
eww_shared_util = { version = "0.1.0", path = "crates/eww_shared_util" }
|
eww_shared_util = { version = "0.1.0", path = "crates/eww_shared_util" }
|
||||||
yuck = { version = "0.1.0", path = "crates/yuck", default-features = false}
|
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"
|
anyhow = "1.0.79"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
|
|
|
@ -19,6 +19,7 @@ wayland = ["gtk-layer-shell"]
|
||||||
simplexpr.workspace = true
|
simplexpr.workspace = true
|
||||||
eww_shared_util.workspace = true
|
eww_shared_util.workspace = true
|
||||||
yuck.workspace = true
|
yuck.workspace = true
|
||||||
|
notifier_host.workspace = true
|
||||||
|
|
||||||
gtk = "0.17.1"
|
gtk = "0.17.1"
|
||||||
gdk = "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 }
|
gdkx11 = { version = "0.17", optional = true }
|
||||||
x11rb = { version = "0.11.1", features = ["randr"], 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
|
anyhow.workspace = true
|
||||||
bincode.workspace = true
|
bincode.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
|
|
@ -42,7 +42,10 @@ fn main() {
|
||||||
if std::env::var("RUST_LOG").is_ok() {
|
if std::env::var("RUST_LOG").is_ok() {
|
||||||
pretty_env_logger::init_timed();
|
pretty_env_logger::init_timed();
|
||||||
} else {
|
} 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 {
|
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
|
// 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 {
|
glib::MainContext::default().spawn_local(async move {
|
||||||
// if an action was given to the daemon initially, execute it first.
|
// 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();
|
gtk::main();
|
||||||
log::info!("main application thread finished");
|
log::info!("main application thread finished");
|
||||||
|
|
||||||
Ok(ForkResult::Child)
|
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 {
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("outer-main-async-runtime".to_string())
|
|
||||||
.spawn(move || {
|
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
.thread_name("main-async-runtime")
|
.thread_name("main-async-runtime")
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to initialize tokio runtime");
|
.expect("Failed to initialize tokio runtime");
|
||||||
|
let handle = rt.handle().clone();
|
||||||
|
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("outer-main-async-runtime".to_string())
|
||||||
|
.spawn(move || {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let filewatch_join_handle = {
|
let filewatch_join_handle = {
|
||||||
let ui_send = ui_send.clone();
|
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");
|
.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.
|
/// 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
|
// values is a map of all the variables that are required to evaluate the
|
||||||
// attributes expression.
|
// 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
|
// 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 circular_progressbar;
|
||||||
pub mod def_widget_macro;
|
pub mod def_widget_macro;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
mod systray;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
pub mod widget_definitions;
|
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::{
|
use crate::{
|
||||||
def_widget, enum_parse, error_handling_ctx,
|
def_widget, enum_parse, error_handling_ctx,
|
||||||
util::{self, list_difference},
|
util::{self, list_difference},
|
||||||
widgets::build_widget::build_gtk_widget,
|
widgets::{build_widget::build_gtk_widget, systray},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use codespan_reporting::diagnostic::Severity;
|
use codespan_reporting::diagnostic::Severity;
|
||||||
|
@ -82,6 +82,7 @@ pub const BUILTIN_WIDGET_NAMES: &[&str] = &[
|
||||||
WIDGET_NAME_SCROLL,
|
WIDGET_NAME_SCROLL,
|
||||||
WIDGET_NAME_OVERLAY,
|
WIDGET_NAME_OVERLAY,
|
||||||
WIDGET_NAME_STACK,
|
WIDGET_NAME_STACK,
|
||||||
|
WIDGET_NAME_SYSTRAY,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// widget definitions
|
/// 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_SCROLL => build_gtk_scrolledwindow(bargs)?.upcast(),
|
||||||
WIDGET_NAME_OVERLAY => build_gtk_overlay(bargs)?.upcast(),
|
WIDGET_NAME_OVERLAY => build_gtk_overlay(bargs)?.upcast(),
|
||||||
WIDGET_NAME_STACK => build_gtk_stack(bargs)?.upcast(),
|
WIDGET_NAME_STACK => build_gtk_stack(bargs)?.upcast(),
|
||||||
|
WIDGET_NAME_SYSTRAY => build_systray(bargs)?.upcast(),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DiagError(gen_diagnostic! {
|
return Err(DiagError(gen_diagnostic! {
|
||||||
msg = format!("referenced unknown widget `{}`", bargs.widget_use.name),
|
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)
|
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"
|
/// @var orientation - "vertical", "v", "horizontal", "h"
|
||||||
fn parse_orientation(o: &str) -> Result<gtk::Orientation> {
|
fn parse_orientation(o: &str) -> Result<gtk::Orientation> {
|
||||||
enum_parse! { "orientation", o,
|
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.
|
/// 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) {
|
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));
|
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)
|
- gtk-layer-shell (only on Wayland)
|
||||||
- pango (libpango)
|
- pango (libpango)
|
||||||
- gdk-pixbuf2 (libgdk_pixbuf-2)
|
- gdk-pixbuf2 (libgdk_pixbuf-2)
|
||||||
|
- libdbusmenu-gtk3
|
||||||
- cairo (libcairo, libcairo-gobject)
|
- cairo (libcairo, libcairo-gobject)
|
||||||
- glib2 (libgio, libglib-2, libgobject-2)
|
- glib2 (libgio, libglib-2, libgobject-2)
|
||||||
- gcc-libs (libgcc)
|
- gcc-libs (libgcc)
|
||||||
|
|
|
@ -42,6 +42,8 @@
|
||||||
cargoDeps =
|
cargoDeps =
|
||||||
rustPlatform.importCargoLock { lockFile = ./Cargo.lock; };
|
rustPlatform.importCargoLock { lockFile = ./Cargo.lock; };
|
||||||
patches = [ ];
|
patches = [ ];
|
||||||
|
# remove this when nixpkgs includes it
|
||||||
|
buildInputs = old.buildInputs ++ [ final.libdbusmenu-gtk3 ];
|
||||||
});
|
});
|
||||||
|
|
||||||
eww-wayland = final.eww;
|
eww-wayland = final.eww;
|
||||||
|
@ -63,6 +65,10 @@
|
||||||
rust
|
rust
|
||||||
rust-analyzer-unwrapped
|
rust-analyzer-unwrapped
|
||||||
gcc
|
gcc
|
||||||
|
glib
|
||||||
|
gdk-pixbuf
|
||||||
|
librsvg
|
||||||
|
libdbusmenu-gtk3
|
||||||
gtk3
|
gtk3
|
||||||
gtk-layer-shell
|
gtk-layer-shell
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
Loading…
Add table
Reference in a new issue