diff --git a/Cargo.lock b/Cargo.lock index fb114924..10f87f06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,27 @@ dependencies = [ "term", ] +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener", + "futures-core", +] + +[[package]] +name = "async-recursion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.10", +] + [[package]] name = "async-trait" version = "0.1.64" @@ -134,6 +155,21 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.2.1" @@ -331,6 +367,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "cpufeatures" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-channel" version = "0.5.6" @@ -380,6 +425,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "ctor" version = "0.1.26" @@ -425,6 +480,70 @@ dependencies = [ "syn 1.0.103", ] +[[package]] +name = "dbusmenu-glib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1629dccc5775e0668eb9f07e78d1b88392a63efc34033a18c87ea44318b894" +dependencies = [ + "dbusmenu-glib-sys", + "glib", + "libc", +] + +[[package]] +name = "dbusmenu-glib-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff9ed40330718c94342b953c997ac19d840db07a7710fe35b45a5d3a3a1d6eb" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "dbusmenu-gtk3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15e02bf53eed790cee4d6111643529bf2fdb82c9e61242407d254612f98436e" +dependencies = [ + "atk", + "dbusmenu-glib", + "dbusmenu-gtk3-sys", + "glib", + "gtk", + "libc", +] + +[[package]] +name = "dbusmenu-gtk3-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f30ba5f8aec0e38a84c579bc8ee3db6f6417b201e729fdd96a23d1f61cb6eca" +dependencies = [ + "dbusmenu-glib-sys", + "gdk-pixbuf-sys", + "gdk-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "libc", + "system-deps", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -444,6 +563,25 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -454,6 +592,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -492,6 +641,27 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "enumflags2" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0044ebdf7fbb2a772e0c0233a9d3173c5cd8af8ae7078d4c5188af44ffffaa4b" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2c772ccdbdfd1967b4f5d79d17c98ebf92009fdcc838db7aa434462f600c26" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.10", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -516,6 +686,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -526,6 +707,12 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "eww" version = "0.4.0" @@ -553,6 +740,7 @@ dependencies = [ "log", "maplit", "nix 0.26.2", + "notifier_host", "notify", "once_cell", "pretty_env_logger", @@ -568,6 +756,7 @@ dependencies = [ "wait-timeout", "x11rb", "yuck", + "zbus", ] [[package]] @@ -590,6 +779,15 @@ dependencies = [ "syn 2.0.10", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "field-offset" version = "0.3.4" @@ -608,7 +806,7 @@ checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "windows-sys 0.42.0", ] @@ -804,6 +1002,16 @@ dependencies = [ "x11", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.2.3" @@ -1058,6 +1266,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "humantime" version = "1.3.0" @@ -1143,7 +1357,7 @@ checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", - "rustix", + "rustix 0.36.11", "windows-sys 0.45.0", ] @@ -1283,6 +1497,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" +[[package]] +name = "linux-raw-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" + [[package]] name = "lock_api" version = "0.4.9" @@ -1377,6 +1597,18 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "notifier_host" +version = "0.1.0" +dependencies = [ + "dbusmenu-gtk3", + "gtk", + "log", + "thiserror", + "tokio", + "zbus", +] + [[package]] name = "notify" version = "5.1.0" @@ -1420,6 +1652,16 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_str_bytes" version = "6.3.0" @@ -1478,7 +1720,7 @@ checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.42.0", ] @@ -1733,6 +1975,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -1740,7 +1991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] @@ -1806,10 +2057,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" dependencies = [ "bitflags", - "errno", + "errno 0.2.8", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.3", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustix" +version = "0.37.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" +dependencies = [ + "bitflags", + "errno 0.3.0", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.1", "windows-sys 0.45.0", ] @@ -1895,6 +2160,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.10", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2081,6 +2368,19 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix 0.37.7", + "windows-sys 0.45.0", +] + [[package]] name = "term" version = "0.7.0" @@ -2157,6 +2457,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.45.0", ] @@ -2193,12 +2494,60 @@ dependencies = [ "serde", ] +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "ucd-trie" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + [[package]] name = "unescape" version = "0.1.0" @@ -2452,3 +2801,102 @@ dependencies = [ "strum", "thiserror", ] + +[[package]] +name = "zbus" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc29e76f558b2cb94190e8605ecfe77dd40f5df8c072951714b4b71a97f5848" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "lazy_static", + "nix 0.26.2", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62a80fd82c011cd08459eaaf1fd83d3090c1b61e6d5284360074a7475af3a85d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.103", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zvariant" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe4914a985446d6fd287019b5fceccce38303d71407d9e6e711d44954a05d8" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34c20260af4b28b3275d6676c7e2a6be0d4332e8e0aba4616d34007fd84e462a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.103", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b22993dbc4d128a17a3b6c92f1c63872dd67198537ee728d8b5d7c40640a8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] diff --git a/crates/eww/Cargo.toml b/crates/eww/Cargo.toml index 25d26f35..5300df97 100644 --- a/crates/eww/Cargo.toml +++ b/crates/eww/Cargo.toml @@ -47,6 +47,7 @@ once_cell = "1.14" nix = "0.26.2" simple-signal = "1.1" unescape = "0.1" +zbus = { version = "3.7.0", default-features = false, features = ["tokio"] } tokio = { version = "1.26.0", features = ["full"] } futures-core = "0.3.27" @@ -64,3 +65,4 @@ codespan-reporting = "0.11" simplexpr = { version = "0.1.0", path = "../simplexpr" } eww_shared_util = { version = "0.1.0", path = "../eww_shared_util" } yuck = { version = "0.1.0", path = "../yuck", default-features = false} +notifier_host = { version = "0.1.0", path = "../notifier_host" } diff --git a/crates/eww/src/main.rs b/crates/eww/src/main.rs index 1e61c3bf..07daa526 100644 --- a/crates/eww/src/main.rs +++ b/crates/eww/src/main.rs @@ -45,7 +45,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(); } #[allow(unused)] diff --git a/crates/eww/src/server.rs b/crates/eww/src/server.rs index 5b557fa4..4ad611b7 100644 --- a/crates/eww/src/server.rs +++ b/crates/eww/src/server.rs @@ -98,7 +98,7 @@ pub fn initialize_server( } // 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. @@ -119,22 +119,26 @@ pub fn initialize_server( } }); + // 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) { +fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender) -> 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(); @@ -166,6 +170,8 @@ fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender }) }) .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. diff --git a/crates/eww/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs index e5e25b7c..33ad4181 100644 --- a/crates/eww/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -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; diff --git a/crates/eww/src/widgets/systray.rs b/crates/eww/src/widgets/systray.rs new file mode 100644 index 00000000..76be2fa5 --- /dev/null +++ b/crates/eww/src/widgets/systray.rs @@ -0,0 +1,100 @@ +#![allow(unused)] + +use gtk::prelude::*; +use notifier_host; + +struct Host { + menubar: gtk::MenuBar, + items: std::collections::HashMap, +} + +async fn watch_foreach(mut rx: tokio::sync::watch::Receiver, mut f: impl FnMut(&T)) { + f(&rx.borrow()); + while rx.changed().await.is_ok() { + f(&rx.borrow()); + } +} + +impl notifier_host::Host for Host { + fn add_item(&mut self, id: &str, item: notifier_host::Item) { + let mi = gtk::MenuItem::new(); + self.menubar.add(&mi); + if let Some(old_mi) = self.items.insert(id.to_string(), mi.clone()) { + self.menubar.remove(&old_mi); + } + + // maintain title + glib::MainContext::default().spawn_local({ + let mi = mi.clone(); + watch_foreach(item.title(), move |title| { + mi.set_tooltip_text(Some(title)); + }) + }); + + let icon = gtk::Image::new(); + mi.add(&icon); + + // other initialisation + glib::MainContext::default().spawn_local({ + let mi = mi.clone(); + async move { + let img = item.icon(24).await.unwrap(); + icon.set_from_pixbuf(Some(&img)); + + let menu = item.menu().await.unwrap(); + mi.set_submenu(Some(&menu)); + } + }); + mi.show_all(); + } + fn remove_item(&mut self, id: &str) { + if let Some(mi) = self.items.get(id) { + self.menubar.remove(mi); + } else { + log::warn!("Tried to remove nonexistent item {:?} from systray", id); + } + } +} + +struct DBusGlobalState { + con: zbus::Connection, + name: zbus::names::WellKnownName<'static>, +} + +async fn dbus_state() -> std::sync::Arc { + use tokio::sync::Mutex; + use std::sync::{Weak, Arc}; + use once_cell::sync::Lazy; + static DBUS_STATE: Lazy>> = Lazy::new(Default::default); + + let mut dbus_state = DBUS_STATE.lock().await; + if let Some(state) = dbus_state.upgrade() { + state + } else { + // TODO error handling? + let con = zbus::Connection::session().await.unwrap(); + notifier_host::watcher_on(&con).await.unwrap(); + + let name = notifier_host::attach_new_wellknown_name(&con).await.unwrap(); + + let arc = Arc::new(DBusGlobalState { + con, + name, + }); + *dbus_state = Arc::downgrade(&arc); + + arc + } +} + +pub fn maintain_menubar(menubar: gtk::MenuBar) { + menubar.show_all(); + glib::MainContext::default().spawn_local(async move { + let mut host = Host { + menubar, + items: std::collections::HashMap::new(), + }; + let s = &dbus_state().await; + notifier_host::run_host_forever(&mut host, &s.con, &s.name).await.unwrap(); + }); +} diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index 2cdc9071..795ac42b 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -3,7 +3,7 @@ use super::{build_widget::BuilderArgs, circular_progressbar::*, run_command, tra use crate::{ def_widget, enum_parse, error_handling_ctx, util::{list_difference, unindent}, - widgets::build_widget::build_gtk_widget, + widgets::{build_widget::build_gtk_widget, systray}, }; use anyhow::{anyhow, Context, Result}; use codespan_reporting::diagnostic::Severity; @@ -80,6 +80,7 @@ pub const BUILTIN_WIDGET_NAMES: &[&str] = &[ WIDGET_NAME_REVEALER, WIDGET_NAME_SCROLL, WIDGET_NAME_OVERLAY, + WIDGET_NAME_SYSTRAY, ]; //// widget definitions @@ -107,6 +108,7 @@ pub(super) fn widget_use_to_gtk_widget(bargs: &mut BuilderArgs) -> Result build_gtk_revealer(bargs)?.upcast(), WIDGET_NAME_SCROLL => build_gtk_scrolledwindow(bargs)?.upcast(), WIDGET_NAME_OVERLAY => build_gtk_overlay(bargs)?.upcast(), + WIDGET_NAME_SYSTRAY => build_systray(bargs)?.upcast(), _ => { return Err(DiagError(gen_diagnostic! { msg = format!("referenced unknown widget `{}`", bargs.widget_use.name), @@ -1035,6 +1037,21 @@ fn build_graph(bargs: &mut BuilderArgs) -> Result { Ok(w) } +const WIDGET_NAME_SYSTRAY: &str = "systray"; +/// @widget systray +/// @desc Tray for system notifier icons +fn build_systray(bargs: &mut BuilderArgs) -> Result { + let gtk_widget = gtk::MenuBar::new(); + + def_widget!(bargs, _g, gtk_widget, { + // @prop pack-direction - how to arrange tray items + prop(pack_direction: as_string) { gtk_widget.set_pack_direction(parse_packdirection(&pack_direction)?); }, + }); + + systray::maintain_menubar(gtk_widget.clone()); + Ok(gtk_widget) +} + /// @var orientation - "vertical", "v", "horizontal", "h" fn parse_orientation(o: &str) -> Result { enum_parse! { "orientation", o, @@ -1089,6 +1106,16 @@ fn parse_justification(j: &str) -> Result { } } +/// @var packdirection - "right", "ltr", "left", "rtl", "down", "ttb", "up", "btt" +fn parse_packdirection(o: &str) -> Result { + 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, F: Fn(&W) + 'static>(widget: &W, func: F) { let signal_handler_id = std::rc::Rc::new(std::cell::RefCell::new(None)); diff --git a/crates/notifier_host/Cargo.toml b/crates/notifier_host/Cargo.toml new file mode 100644 index 00000000..c69b3871 --- /dev/null +++ b/crates/notifier_host/Cargo.toml @@ -0,0 +1,17 @@ +[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 = { version = "0.15", features = [ "v3_22" ] } +log = "0.4" +thiserror = "1.0" +tokio = { version = "^1.18", features = ["full"] } +zbus = { version = "3.7.0", default-features = false, features = ["tokio"] } +dbusmenu-gtk3 = "0.1.0" diff --git a/crates/notifier_host/src/dbus/dbus_menu.xml b/crates/notifier_host/src/dbus/dbus_menu.xml new file mode 100644 index 00000000..ae5d7906 --- /dev/null +++ b/crates/notifier_host/src/dbus/dbus_menu.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/notifier_host/src/dbus/dbus_status_notifier_item.rs b/crates/notifier_host/src/dbus/dbus_status_notifier_item.rs new file mode 100644 index 00000000..e20e32d6 --- /dev/null +++ b/crates/notifier_host/src/dbus/dbus_status_notifier_item.rs @@ -0,0 +1,111 @@ +//! # 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. + +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; + + /// AttentionIconPixmap property + #[dbus_proxy(property)] + fn attention_icon_pixmap(&self) -> zbus::Result)>>; + + /// AttentionMovieName property + #[dbus_proxy(property)] + fn attention_movie_name(&self) -> zbus::Result; + + /// Category property + #[dbus_proxy(property)] + fn category(&self) -> zbus::Result; + + /// IconName property + #[dbus_proxy(property)] + fn icon_name(&self) -> zbus::Result; + + /// IconPixmap property + #[dbus_proxy(property)] + fn icon_pixmap(&self) -> zbus::Result)>>; + + /// IconThemePath property + #[dbus_proxy(property)] + fn icon_theme_path(&self) -> zbus::Result; + + /// Id property + #[dbus_proxy(property)] + fn id(&self) -> zbus::Result; + + /// ItemIsMenu property + #[dbus_proxy(property)] + fn item_is_menu(&self) -> zbus::Result; + + /// Menu property + #[dbus_proxy(property)] + fn menu(&self) -> zbus::Result; + + /// OverlayIconName property + #[dbus_proxy(property)] + fn overlay_icon_name(&self) -> zbus::Result; + + /// OverlayIconPixmap property + #[dbus_proxy(property)] + fn overlay_icon_pixmap(&self) -> zbus::Result)>>; + + /// Status property + #[dbus_proxy(property)] + fn status(&self) -> zbus::Result; + + /// Title property + #[dbus_proxy(property)] + fn title(&self) -> zbus::Result; + + /// ToolTip property + #[dbus_proxy(property)] + fn tool_tip(&self) -> zbus::Result<(String, Vec<(i32, i32, Vec)>)>; +} diff --git a/crates/notifier_host/src/dbus/dbus_status_notifier_item.xml b/crates/notifier_host/src/dbus/dbus_status_notifier_item.xml new file mode 100644 index 00000000..c33cd846 --- /dev/null +++ b/crates/notifier_host/src/dbus/dbus_status_notifier_item.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/notifier_host/src/dbus/dbus_status_notifier_watcher.rs b/crates/notifier_host/src/dbus/dbus_status_notifier_watcher.rs new file mode 100644 index 00000000..8c352793 --- /dev/null +++ b/crates/notifier_host/src/dbus/dbus_status_notifier_watcher.rs @@ -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; + + /// ProtocolVersion property + #[dbus_proxy(property)] + fn protocol_version(&self) -> zbus::Result; + + /// RegisteredStatusNotifierItems property + #[dbus_proxy(property)] + fn registered_status_notifier_items(&self) -> zbus::Result>; +} diff --git a/crates/notifier_host/src/dbus/dbus_status_notifier_watcher.xml b/crates/notifier_host/src/dbus/dbus_status_notifier_watcher.xml new file mode 100644 index 00000000..6370e4dd --- /dev/null +++ b/crates/notifier_host/src/dbus/dbus_status_notifier_watcher.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/notifier_host/src/dbus/mod.rs b/crates/notifier_host/src/dbus/mod.rs new file mode 100644 index 00000000..2852ce8f --- /dev/null +++ b/crates/notifier_host/src/dbus/mod.rs @@ -0,0 +1,11 @@ +//! # DBus interface proxies +//! +//! The interface XML files are taken from +//! [Waybar](https://github.com/Alexays/Waybar/tree/master/protocol), and the proxies generated +//! with [zbus-gen](https://docs.rs/crate/zbus_xmlgen/latest). + +mod dbus_status_notifier_item; +pub use dbus_status_notifier_item::*; + +mod dbus_status_notifier_watcher; +pub use dbus_status_notifier_watcher::*; diff --git a/crates/notifier_host/src/host.rs b/crates/notifier_host/src/host.rs new file mode 100644 index 00000000..e16eb2e2 --- /dev/null +++ b/crates/notifier_host/src/host.rs @@ -0,0 +1,95 @@ +use crate::*; + +use zbus::export::ordered_stream::{self, OrderedStreamExt}; + +pub trait Host { + fn add_item(&mut self, id: &str, item: Item); + fn remove_item(&mut self, id: &str); +} + +/// Add a new well-known name of format `org.freedesktop.StatusNotifierHost-{pid}-{nr}` for this connection. +pub async fn attach_new_wellknown_name(con: &zbus::Connection) -> zbus::Result> { + 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"), + }; + }; + Ok(wellknown) +} + +pub async fn run_host_forever(host: &mut dyn Host, con: &zbus::Connection, name: &zbus::names::WellKnownName<'_>) -> zbus::Result<()> { + // register ourself to StatusNotifierWatcher + let snw = dbus::StatusNotifierWatcherProxy::new(&con).await?; + snw.register_status_notifier_host(&name).await?; + + enum ItemEvent { + NewItem(dbus::StatusNotifierItemRegistered), + GoneItem(dbus::StatusNotifierItemUnregistered), + } + + // start listening to these streams + let new_items = snw.receive_status_notifier_item_registered().await?; + let gone_items = snw.receive_status_notifier_item_unregistered().await?; + + let mut item_names = std::collections::HashSet::new(); + + // initial items first + for svc in 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 = 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 = sig.args()?.service; + if item_names.remove(svc) { + host.remove_item(svc); + } + }, + } + } + + // TODO handle running out of events? why could this happen? + + Ok(()) +} diff --git a/crates/notifier_host/src/item.rs b/crates/notifier_host/src/item.rs new file mode 100644 index 00000000..d9f72a63 --- /dev/null +++ b/crates/notifier_host/src/item.rs @@ -0,0 +1,256 @@ +use crate::*; + +use log; +use gtk::{self, prelude::*}; +use zbus::export::ordered_stream::OrderedStreamExt; +use tokio::sync::watch; + +/// Recognised values of org.freedesktop.StatusNotifierItem.Status +/// +/// See +/// +/// for details. +#[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, +} + +impl std::str::FromStr for Status { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + match s { + "Passive" => Ok(Status::Passive), + "Active" => Ok(Status::Active), + "NeedsAttention" => Ok(Status::NeedsAttention), + _ => Err(()), + } + } +} + +#[derive(Clone, Debug)] +pub struct Item { + pub sni: dbus::StatusNotifierItemProxy<'static>, + status_rx: watch::Receiver, + title_rx: watch::Receiver, +} + +/// Split a sevice name e.g. `:1.50:/org/ayatana/NotificationItem/nm_applet` into the address and +/// path. +/// +/// Original logic from +fn split_service_name(service: &str) -> zbus::Result<(String, String)> { + if let Some((addr, path)) = service.split_once('/') { + Ok((addr.to_owned(), format!("/{}", path))) + } else if service.contains(':') { + let addr = service.split(':').skip(1).next(); + // Some StatusNotifierItems will not return an object path, in that case we fallback + // to the default path. + if let Some(addr) = addr { + Ok((addr.to_owned(), "/StatusNotifierItem".to_owned())) + } else { + Err(zbus::Error::Address(service.to_owned())) + } + } else { + Err(zbus::Error::Address(service.to_owned())) + } +} + +impl Item { + pub async fn from_address(con: &zbus::Connection, addr: &str) -> zbus::Result { + let (addr, path) = split_service_name(addr)?; + let sni = dbus::StatusNotifierItemProxy::builder(con) + .destination(addr)? + .path(path)? + .build() + .await?; + + let (status_tx, status_rx) = watch::channel(sni.status().await?.parse().unwrap()); + tokio::spawn({ + let sni = sni.clone(); + async move { + let mut new_status_stream = sni.receive_new_status().await.unwrap(); + while let Some(sig) = new_status_stream.next().await { + let args = sig.args().unwrap(); + let status: Status = args.status.parse().unwrap(); + status_tx.send_replace(status); + } + } + }); + + let (title_tx, title_rx) = watch::channel(sni.title().await?); + tokio::spawn({ + let sni = sni.clone(); + async move { + let mut new_title_stream = sni.receive_new_title().await.unwrap(); + while let Some(_) = new_title_stream.next().await { + let title = sni.title().await.unwrap(); + title_tx.send_replace(title); + } + } + }); + + Ok(Item { + sni, + status_rx, + title_rx, + }) + } + + pub fn status(&self) -> watch::Receiver { + self.status_rx.clone() + } + + pub fn title(&self) -> watch::Receiver { + self.title_rx.clone() + } +} + +#[derive(thiserror::Error, Debug)] +pub enum IconError { + #[error("Dbus error")] + DbusError(#[from] zbus::Error), + #[error("Failed to load icon {icon_name:?} from theme {theme_path:?}")] + LoadIconFromTheme { + icon_name: String, + theme_path: String, + source: gtk::glib::Error, + }, + #[error("Failed to load icon {icon_name:?} from default theme")] + LoadIconFromDefaultTheme { + icon_name: String, + source: gtk::glib::Error, + }, +} + +impl Item { + pub fn load_pixbuf(width: i32, height: i32, mut data: Vec) -> gtk::gdk_pixbuf::Pixbuf { + // We need to convert data from ARGB32 to RGBA32 + for chunk in data.chunks_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; + } + + let pixbuf = gtk::gdk_pixbuf::Pixbuf::from_bytes( + >k::glib::Bytes::from_owned(data), + gtk::gdk_pixbuf::Colorspace::Rgb, + true, + 8, + width, + height, + width * 4, + ); + + pixbuf + } + + pub fn load_pixmap(width: i32, height: i32, mut data: Vec) -> gtk::Image { + // We need to convert data from ARGB32 to RGBA32 + for chunk in data.chunks_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; + } + + let pixmap = gtk::gdk_pixbuf::Pixbuf::from_bytes( + >k::glib::Bytes::from_owned(data), + gtk::gdk_pixbuf::Colorspace::Rgb, + true, + 8, + width, + height, + width * 4, + ); + gtk::Image::from_pixbuf(Some(&pixmap)) + } + + pub async fn icon(&self, size: i32) -> std::result::Result { + let icon_name = match self.sni.icon_name().await { + Ok(icon_name) => icon_name, + Err(e) => { + log::warn!("Failed to get icon name: {}", e); + String::from("") + } + }; + + let icon_theme_path = match self.sni.icon_theme_path().await { + Ok(icon_theme_path) => icon_theme_path, + Err(e) => { + log::warn!("Failed to get icon theme path: {}", e); + String::from("") + } + }; + + if icon_theme_path != "" { + // icon supplied a theme path, so only look there + let theme = gtk::IconTheme::new(); + theme.prepend_search_path(&icon_theme_path); + + return match theme.load_icon(&icon_name, size, gtk::IconLookupFlags::FORCE_SIZE) { + Err(e) => Err(IconError::LoadIconFromTheme { + icon_name, + theme_path: icon_theme_path, + source: e, + }), + Ok(pb) => return Ok(pb.unwrap()), + } + } + + // fallback to default theme + let theme = gtk::IconTheme::default().expect("Could not get default gtk theme"); + match theme.load_icon(&icon_name, size, gtk::IconLookupFlags::FORCE_SIZE) { + // TODO specifically match on icon missing here + Err(e) => log::warn!("Could not find icon {:?} in default theme: {}", &icon_name, e), + Ok(pb) => return Ok(pb.unwrap()), + } + + // "Visualizations are encouraged to prefer icon names over icon pixmaps if both are available." + if let Ok(icon_data) = self.sni.icon_pixmap().await { + for pixmap_data in icon_data { + let width = pixmap_data.0; + let height = pixmap_data.1; + + if width != 24 { + continue; + } + + return Ok(Self::load_pixbuf(width, height, pixmap_data.2)) + } + }; + + // fallback to default icon + let theme = gtk::IconTheme::default().expect("Could not get default gtk theme"); + return match theme.load_icon("image-missing", size, gtk::IconLookupFlags::FORCE_SIZE) { + Err(e) => Err(IconError::LoadIconFromDefaultTheme { + icon_name: "image-missing".to_owned(), + source: e, + }), + Ok(pb) => return Ok(pb.unwrap()), + } + } + + pub async fn menu(&self) -> zbus::Result { + // TODO better handling if menu() method doesn't exist + let menu = dbusmenu_gtk3::Menu::new(self.sni.destination(), &self.sni.menu().await?); + Ok(menu.upcast()) + } +} diff --git a/crates/notifier_host/src/lib.rs b/crates/notifier_host/src/lib.rs new file mode 100644 index 00000000..3ccc94b3 --- /dev/null +++ b/crates/notifier_host/src/lib.rs @@ -0,0 +1,10 @@ +pub mod dbus; + +mod host; +pub use host::*; + +mod item; +pub use item::*; + +mod watcher; +pub use watcher::*; diff --git a/crates/notifier_host/src/watcher.rs b/crates/notifier_host/src/watcher.rs new file mode 100644 index 00000000..31238efe --- /dev/null +++ b/crates/notifier_host/src/watcher.rs @@ -0,0 +1,275 @@ +use zbus::dbus_interface; +use zbus::Interface; +use zbus::export::ordered_stream::OrderedStreamExt; + +pub const WATCHER_BUS_NAME: &'static str = "org.kde.StatusNotifierWatcher"; +pub const WATCHER_OBJECT_NAME: &'static str = "/StatusNotifierWatcher"; + +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 { + 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(), "/StatusNotifierItem")) + } else { + // unwrap: we should always be able to access the dbus interface + let dbus = zbus::fdo::DBusProxy::new(&con).await.unwrap(); + match dbus.get_name_owner(busname).await { + Ok(owner) => Ok((owner.into_inner(), "/StatusNotifierItem")), + Err(e) => { + log::warn!("failed to get owner of {:?}: {}", service, e); + Err(e) + } + } + } + } +} + +/// Wait for a DBus service to exit +async fn wait_for_service_exit( + connection: zbus::Connection, + service: zbus::names::BusName<'_>, +) -> zbus::fdo::Result<()> { + let dbus = zbus::fdo::DBusProxy::new(&connection).await?; + let mut owner_changes = dbus + .receive_name_owner_changed_with_args(&[(0, &service)]) + .await?; + + if !dbus.name_has_owner(service.as_ref()).await? { + return Ok(()) + } + + while let Some(sig) = owner_changes.next().await { + let args = sig.args()?; + if args.new_owner().is_none() { + break + } + } + + Ok(()) +} + +#[derive(Debug, Default)] +pub struct Watcher { + tasks: tokio::task::JoinSet<()>, + hosts: std::sync::Arc>>, + items: std::sync::Arc>>, +} + +#[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<()> { + let (service, _) = parse_service(service, hdr, con).await?; + log::info!("new host: {}", service); + + let added_first = { + let mut hosts = self.hosts.lock().unwrap(); + 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 { + wait_for_service_exit(con.clone(), service.as_ref().into()).await.unwrap(); + log::info!("lost host: {}", service); + + let removed_last = { + let mut hosts = hosts.lock().unwrap(); + let did_remove = hosts.remove(service.as_str()); + did_remove && hosts.is_empty() + }; + + if removed_last { + Watcher::is_status_notifier_host_registered_refresh(&ctxt).await.unwrap(); + } + Watcher::status_notifier_host_unregistered(&ctxt).await.unwrap(); + } + }); + + 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(); + !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(); + 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 { + wait_for_service_exit(con.clone(), service.as_ref()).await.unwrap(); + println!("gone item: {}", &item); + + { + let mut items = items.lock().unwrap(); + items.remove(&item); + } + + Watcher::registered_status_notifier_items_refresh(&ctxt).await.unwrap(); + Watcher::status_notifier_item_unregistered(&ctxt, item.as_ref()).await.unwrap(); + } + }); + + 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 { + let items = self.items.lock().unwrap(); + 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 the Watcher to a connection. + pub async fn run_on(self, con: &zbus::Connection) -> zbus::Result<()> { + if !con.object_server().at(WATCHER_OBJECT_NAME, self).await? { + return Err(zbus::Error::Failure("Interface already exists at this path".into())) + } + + // no ReplaceExisting, no AllowReplacement, no DoNotQueue + con.request_name_with_flags(WATCHER_BUS_NAME, Default::default()).await?; + + Ok(()) + } + + // Based on 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 + } + + // Based on 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 + } +} + +/// Start a StatusNotifierWatcher on this connection. +pub async fn watcher_on(con: &zbus::Connection) -> zbus::Result<()> { + if !con.object_server().at(WATCHER_OBJECT_NAME, Watcher::new()).await? { + // There's already something at this object + // TODO better handling? + return Err(zbus::Error::Failure(format!("Interface already exists at object {}", WATCHER_OBJECT_NAME))) + } + + use zbus::fdo::*; + match con.request_name_with_flags(WATCHER_BUS_NAME, [RequestNameFlags::DoNotQueue].into_iter().collect()).await? { + RequestNameReply::PrimaryOwner => return Ok(()), + RequestNameReply::Exists => {}, + RequestNameReply::AlreadyOwner => {}, // TODO should this return + RequestNameReply::InQueue => unreachable!("request_name_with_flags returned InQueue even though we specified DoNotQueue"), + } + + // TODO should we queue? + + Ok(()) +} diff --git a/flake.nix b/flake.nix index d0973838..456564e9 100644 --- a/flake.nix +++ b/flake.nix @@ -61,6 +61,10 @@ rust rust-analyzer-unwrapped gcc + glib + gdk-pixbuf + librsvg + libdbusmenu-gtk3 gtk3 gtk-layer-shell pkg-config