diff --git a/Cargo.lock b/Cargo.lock index 2a193e8..482f924 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,8 +218,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -235,16 +235,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] -[[package]] -name = "async_once" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce4f10ea3abcd6617873bae9f91d1c5332b4a778bd9ce34d0cd517474c1de82" - [[package]] name = "atk" version = "0.18.0" @@ -478,8 +472,8 @@ checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck 0.4.1", "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -662,9 +656,9 @@ dependencies = [ "codespan-reporting", "once_cell", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "scratch", - "syn 2.0.28", + "syn 2.0.48", ] [[package]] @@ -680,8 +674,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -703,7 +697,7 @@ dependencies = [ "fnv", "ident_case", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "strsim", "syn 1.0.109", ] @@ -715,7 +709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", ] @@ -737,7 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", ] @@ -760,7 +754,7 @@ checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "rustc_version", "syn 1.0.109", ] @@ -839,7 +833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" dependencies = [ "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", ] @@ -860,8 +854,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -980,15 +974,15 @@ checksum = "55a5e644a80e6d96b2b4910fa7993301d7b7926c045b475b62202b20a36ce69e" dependencies = [ "darling", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1017,9 +1011,9 @@ checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1049,9 +1043,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" dependencies = [ "fastrand 2.0.1", "futures-core", @@ -1067,8 +1061,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -1251,8 +1245,8 @@ dependencies = [ "proc-macro-crate 2.0.1", "proc-macro-error", "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -1352,8 +1346,8 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -1523,8 +1517,8 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c941d3d52e979612af8cb94e8de49000c7fada2014a7791d173ab41339f4e4eb" dependencies = [ - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -1643,22 +1637,20 @@ checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" name = "ironbar" version = "0.14.0-pre" dependencies = [ - "async_once", "cfg-if", "chrono", "clap", "color-eyre", "ctrlc", "dirs", - "futures-lite 2.1.0", + "futures-lite 2.2.0", "futures-util", "glib", "gtk", "gtk-layer-shell", "hyprland", "indexmap 2.1.0", - "lazy_static", - "mpd_client", + "mpd-utils", "mpris", "nix 0.27.1", "notify", @@ -1879,6 +1871,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mpd-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7081a86d39a604868a671b0166febc1f31a4c4801d9436ab733f2664baabf8a4" +dependencies = [ + "futures", + "mpd_client", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "mpd_client" version = "1.3.0" @@ -2060,8 +2065,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -2205,8 +2210,8 @@ dependencies = [ "pest", "pest_meta", "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -2302,7 +2307,7 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", "version_check", ] @@ -2314,15 +2319,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -2350,9 +2355,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.32" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2666,8 +2671,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -2688,8 +2693,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -2874,9 +2879,9 @@ checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" dependencies = [ "heck 0.4.1", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "rustversion", - "syn 2.0.28", + "syn 2.0.48", ] [[package]] @@ -2922,18 +2927,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.28" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "unicode-ident", ] @@ -3037,22 +3042,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -3134,8 +3139,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -3254,8 +3259,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", - "quote 1.0.32", - "syn 2.0.28", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -3466,7 +3471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" dependencies = [ "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", ] [[package]] @@ -3521,7 +3526,7 @@ dependencies = [ "log", "once_cell", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", "wasm-bindgen-shared", ] @@ -3544,7 +3549,7 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ - "quote 1.0.32", + "quote 1.0.35", "wasm-bindgen-macro-support", ] @@ -3555,7 +3560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -3648,7 +3653,7 @@ checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c" dependencies = [ "proc-macro2", "quick-xml", - "quote 1.0.32", + "quote 1.0.35", ] [[package]] @@ -4019,7 +4024,7 @@ checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "regex", "syn 1.0.109", "zvariant_utils", @@ -4058,7 +4063,7 @@ checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", "zvariant_utils", ] @@ -4070,6 +4075,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ "proc-macro2", - "quote 1.0.32", + "quote 1.0.35", "syn 1.0.109", ] diff --git a/Cargo.toml b/Cargo.toml index 60d54c5..c111620 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ clock = ["chrono"] music = ["regex"] "music+all" = ["music", "music+mpris", "music+mpd"] "music+mpris" = ["music", "mpris"] -"music+mpd" = ["music", "mpd_client"] +"music+mpd" = ["music", "mpd-utils"] sys_info = ["sysinfo", "regex"] @@ -94,9 +94,6 @@ smithay-client-toolkit = { version = "0.18.0", default-features = false, feature ] } universal-config = { version = "0.4.3", default_features = false } ctrlc = "3.4.2" - -lazy_static = "1.4.0" -async_once = "0.2.6" cfg-if = "1.0.0" # cli @@ -115,7 +112,7 @@ nix = { version = "0.27.1", optional = true, features = ["event"] } chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] } # music -mpd_client = { version = "1.3.0", optional = true } +mpd-utils = { version = "0.2.0", optional = true } mpris = { version = "2.0.1", optional = true } # sys_info @@ -126,7 +123,7 @@ system-tray = { version = "0.1.4", optional = true } # upower upower_dbus = { version = "0.3.2", optional = true } -futures-lite = { version = "2.1.0", optional = true } +futures-lite = { version = "2.2.0", optional = true } zbus = { version = "3.14.1", optional = true } # workspaces diff --git a/src/bar.rs b/src/bar.rs index 76beed3..2efa218 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -27,6 +27,8 @@ pub struct Bar { monitor_name: String, position: BarPosition, + ironbar: Rc, + window: ApplicationWindow, content: gtk::Box, @@ -39,7 +41,12 @@ pub struct Bar { } impl Bar { - pub fn new(app: &Application, monitor_name: String, config: Config) -> Self { + pub fn new( + app: &Application, + monitor_name: String, + config: Config, + ironbar: Rc, + ) -> Self { let window = ApplicationWindow::builder() .application(app) .type_(WindowType::Toplevel) @@ -90,6 +97,7 @@ impl Bar { name, monitor_name, position, + ironbar, window, content, start, @@ -263,17 +271,17 @@ impl Bar { if let Some(modules) = config.start { let info = info!(ModuleLocation::Left); - add_modules(&self.start, modules, &info, &popup)?; + add_modules(&self.start, modules, &info, &self.ironbar, &popup)?; } if let Some(modules) = config.center { let info = info!(ModuleLocation::Center); - add_modules(&self.center, modules, &info, &popup)?; + add_modules(&self.center, modules, &info, &self.ironbar, &popup)?; } if let Some(modules) = config.end { let info = info!(ModuleLocation::Right); - add_modules(&self.end, modules, &info, &popup)?; + add_modules(&self.end, modules, &info, &self.ironbar, &popup)?; } let result = BarLoadResult { popup }; @@ -333,6 +341,7 @@ fn add_modules( content: >k::Box, modules: Vec, info: &ModuleInfo, + ironbar: &Rc, popup: &Rc>, ) -> Result<()> { let orientation = info.bar_position.get_orientation(); @@ -343,6 +352,7 @@ fn add_modules( let widget_parts = create_module( *$module, $id, + ironbar.clone(), common.name.clone(), &info, &Rc::clone(&popup), @@ -387,7 +397,8 @@ pub fn create_bar( monitor: &Monitor, monitor_name: String, config: Config, + ironbar: Rc, ) -> Result { - let bar = Bar::new(app, monitor_name, config); + let bar = Bar::new(app, monitor_name, config, ironbar); bar.init(monitor) } diff --git a/src/clients/clipboard.rs b/src/clients/clipboard.rs index 614c7e3..1752486 100644 --- a/src/clients/clipboard.rs +++ b/src/clients/clipboard.rs @@ -1,15 +1,14 @@ use super::wayland::{self, ClipboardItem}; -use crate::{arc_mut, lock, spawn, try_send}; +use crate::{arc_mut, lock, register_client, spawn, try_send}; use indexmap::map::Iter; use indexmap::IndexMap; -use lazy_static::lazy_static; use std::sync::{Arc, Mutex}; use tokio::sync::mpsc; use tracing::{debug, trace}; #[derive(Debug)] pub enum ClipboardEvent { - Add(Arc), + Add(ClipboardItem), Remove(usize), Activate(usize), } @@ -18,13 +17,16 @@ type EventSender = mpsc::Sender; /// Clipboard client singleton, /// to ensure bars don't duplicate requests to the compositor. -pub struct ClipboardClient { +#[derive(Debug)] +pub struct Client { + wayland: Arc, + senders: Arc>>, cache: Arc>, } -impl ClipboardClient { - fn new() -> Self { +impl Client { + pub(crate) fn new(wl: Arc) -> Self { trace!("Initializing clipboard client"); let senders = arc_mut!(Vec::<(EventSender, usize)>::new()); @@ -34,13 +36,11 @@ impl ClipboardClient { { let senders = senders.clone(); let cache = cache.clone(); + let wl = wl.clone(); spawn(async move { - let (mut rx, item) = { - let wl = wayland::get_client(); - let wl = lock!(wl); - wl.subscribe_clipboard() - }; + let item = wl.clipboard_item(); + let mut rx = wl.subscribe_clipboard(); if let Some(item) = item { let senders = lock!(senders); @@ -91,7 +91,11 @@ impl ClipboardClient { }); } - Self { senders, cache } + Self { + wayland: wl, + senders, + cache, + } } pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver { @@ -120,9 +124,7 @@ impl ClipboardClient { }; if let Some(item) = item { - let wl = wayland::get_client(); - let wl = lock!(wl); - wl.copy_to_clipboard(item); + self.wayland.copy_to_clipboard(item); } let senders = lock!(self.senders); @@ -150,7 +152,7 @@ impl ClipboardClient { /// at different times. #[derive(Debug)] struct ClipboardCache { - cache: IndexMap, usize)>, + cache: IndexMap, } impl ClipboardCache { @@ -162,12 +164,12 @@ impl ClipboardCache { } /// Gets the entry with key `id` from the cache. - fn get(&self, id: usize) -> Option> { + fn get(&self, id: usize) -> Option { self.cache.get(&id).map(|(item, _)| item).cloned() } /// Inserts an entry with `ref_count` initial references. - fn insert(&mut self, item: Arc, ref_count: usize) -> Option> { + fn insert(&mut self, item: ClipboardItem, ref_count: usize) -> Option { self.cache .insert(item.id, (item, ref_count)) .map(|(item, _)| item) @@ -175,7 +177,7 @@ impl ClipboardCache { /// Removes the entry with key `id`. /// This ignores references. - fn remove(&mut self, id: usize) -> Option> { + fn remove(&mut self, id: usize) -> Option { self.cache.shift_remove(&id).map(|(item, _)| item) } @@ -224,15 +226,9 @@ impl ClipboardCache { self.cache.len() } - fn iter(&self) -> Iter<'_, usize, (Arc, usize)> { + fn iter(&self) -> Iter<'_, usize, (ClipboardItem, usize)> { self.cache.iter() } } -lazy_static! { - static ref CLIENT: ClipboardClient = ClipboardClient::new(); -} - -pub fn get_client() -> &'static ClipboardClient { - &CLIENT -} +register_client!(Client, clipboard); diff --git a/src/clients/compositor/hyprland.rs b/src/clients/compositor/hyprland.rs index 74f0d85..2e945de 100644 --- a/src/clients/compositor/hyprland.rs +++ b/src/clients/compositor/hyprland.rs @@ -6,23 +6,26 @@ use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial} use hyprland::event_listener::EventListener; use hyprland::prelude::*; use hyprland::shared::{HyprDataVec, WorkspaceType}; -use lazy_static::lazy_static; use tokio::sync::broadcast::{channel, Receiver, Sender}; use tracing::{debug, error, info}; -pub struct EventClient { +#[derive(Debug)] +pub struct Client { workspace_tx: Sender, _workspace_rx: Receiver, } -impl EventClient { - fn new() -> Self { +impl Client { + pub(crate) fn new() -> Self { let (workspace_tx, workspace_rx) = channel(16); - Self { + let instance = Self { workspace_tx, _workspace_rx: workspace_rx, - } + }; + + instance.listen_workspace_events(); + instance } fn listen_workspace_events(&self) { @@ -203,7 +206,7 @@ impl EventClient { } } -impl WorkspaceClient for EventClient { +impl WorkspaceClient for Client { fn focus(&self, id: String) -> Result<()> { let identifier = match id.parse::() { Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum), @@ -239,18 +242,6 @@ impl WorkspaceClient for EventClient { } } -lazy_static! { - static ref CLIENT: EventClient = { - let client = EventClient::new(); - client.listen_workspace_events(); - client - }; -} - -pub fn get_client() -> &'static EventClient { - &CLIENT -} - fn get_workspace_name(name: WorkspaceType) -> String { match name { WorkspaceType::Regular(name) => name, diff --git a/src/clients/compositor/mod.rs b/src/clients/compositor/mod.rs index c703ff5..ce8d749 100644 --- a/src/clients/compositor/mod.rs +++ b/src/clients/compositor/mod.rs @@ -1,6 +1,8 @@ +use crate::{await_sync, register_client}; use cfg_if::cfg_if; use color_eyre::{Help, Report, Result}; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; +use std::sync::Arc; use tokio::sync::broadcast; use tracing::debug; @@ -44,7 +46,7 @@ impl Compositor { } } else if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() { cfg_if! { - if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland} + if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland } else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported } } } else { @@ -52,15 +54,17 @@ impl Compositor { } } - /// Gets the workspace client for the current compositor - pub fn get_workspace_client() -> Result<&'static (dyn WorkspaceClient + Send)> { + /// Creates a new instance of + /// the workspace client for the current compositor. + pub fn create_workspace_client() -> Result> { let current = Self::get_current(); debug!("Getting workspace client for: {current}"); match current { #[cfg(feature = "workspaces+sway")] - Self::Sway => Ok(sway::get_sub_client()), + Self::Sway => await_sync(async { sway::Client::new().await }) + .map(|client| Arc::new(client) as Arc), #[cfg(feature = "workspaces+hyprland")] - Self::Hyprland => Ok(hyprland::get_client()), + Self::Hyprland => Ok(Arc::new(hyprland::Client::new())), Self::Unsupported => Err(Report::msg("Unsupported compositor") .note("Currently workspaces are only supported by Sway and Hyprland")), } @@ -129,10 +133,12 @@ pub enum WorkspaceUpdate { Unknown, } -pub trait WorkspaceClient { +pub trait WorkspaceClient: Debug + Send + Sync { /// Requests the workspace with this name is focused. fn focus(&self, name: String) -> Result<()>; /// Creates a new to workspace event receiver. fn subscribe_workspace_change(&self) -> broadcast::Receiver; } + +register_client!(dyn WorkspaceClient, workspaces); diff --git a/src/clients/compositor/sway.rs b/src/clients/compositor/sway.rs index 7906ab1..695ac5c 100644 --- a/src/clients/compositor/sway.rs +++ b/src/clients/compositor/sway.rs @@ -1,32 +1,35 @@ use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate}; use crate::{await_sync, send, spawn}; -use async_once::AsyncOnce; -use color_eyre::Report; +use color_eyre::{Report, Result}; use futures_util::StreamExt; -use lazy_static::lazy_static; use std::sync::Arc; use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent}; use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::Mutex; use tracing::{info, trace}; -pub struct SwayEventClient { +#[derive(Debug)] +pub struct Client { + client: Arc>, workspace_tx: Sender, _workspace_rx: Receiver, } -impl SwayEventClient { - fn new() -> Self { +impl Client { + pub(crate) async fn new() -> Result { + // Avoid using `arc_mut!` here because we need tokio Mutex. + let client = Arc::new(Mutex::new(Connection::new().await?)); + info!("Sway IPC subscription client connected"); + let (workspace_tx, workspace_rx) = channel(16); { + // create 2nd client as subscription takes ownership + let client = Connection::new().await?; let workspace_tx = workspace_tx.clone(); + spawn(async move { - let client = Connection::new().await?; - info!("Sway IPC subscription client connected"); - let event_types = [EventType::Workspace]; - let mut events = client.subscribe(event_types).await?; while let Some(event) = events.next().await { @@ -43,18 +46,18 @@ impl SwayEventClient { }); } - Self { + Ok(Self { + client, workspace_tx, _workspace_rx: workspace_rx, - } + }) } } -impl WorkspaceClient for SwayEventClient { - fn focus(&self, id: String) -> color_eyre::Result<()> { +impl WorkspaceClient for Client { + fn focus(&self, id: String) -> Result<()> { await_sync(async move { - let client = get_client().await; - let mut client = client.lock().await; + let mut client = self.client.lock().await; client.run_command(format!("workspace {id}")).await })?; Ok(()) @@ -65,14 +68,12 @@ impl WorkspaceClient for SwayEventClient { { let tx = self.workspace_tx.clone(); - await_sync(async { - let client = get_client().await; - let mut client = client.lock().await; + let client = self.client.clone(); + + await_sync(async { + let mut client = client.lock().await; + let workspaces = client.get_workspaces().await.expect("to get workspaces"); - let workspaces = client - .get_workspaces() - .await - .expect("Failed to get workspaces"); let event = WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect()); @@ -84,27 +85,6 @@ impl WorkspaceClient for SwayEventClient { } } -lazy_static! { - static ref CLIENT: AsyncOnce>> = AsyncOnce::new(async { - let client = Connection::new() - .await - .expect("Failed to connect to Sway socket"); - Arc::new(Mutex::new(client)) - }); - static ref SUB_CLIENT: SwayEventClient = SwayEventClient::new(); -} - -/// Gets the sway IPC client -async fn get_client() -> Arc> { - let client = CLIENT.get().await; - Arc::clone(client) -} - -/// Gets the sway IPC event subscription client -pub fn get_sub_client() -> &'static SwayEventClient { - &SUB_CLIENT -} - impl From for Workspace { fn from(node: Node) -> Self { let visibility = Visibility::from(&node); diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 0d53701..f1433bc 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + #[cfg(feature = "clipboard")] pub mod clipboard; #[cfg(feature = "workspaces")] @@ -9,3 +11,109 @@ pub mod system_tray; #[cfg(feature = "upower")] pub mod upower; pub mod wayland; + +/// Singleton wrapper consisting of +/// all the singleton client types used by modules. +#[derive(Debug, Default)] +pub struct Clients { + wayland: Option>, + #[cfg(feature = "workspaces")] + workspaces: Option>, + #[cfg(feature = "clipboard")] + clipboard: Option>, + #[cfg(feature = "music")] + music: std::collections::HashMap>, + #[cfg(feature = "tray")] + tray: Option>, + #[cfg(feature = "upower")] + upower: Option>>, +} + +impl Clients { + pub(crate) fn new() -> Self { + Self::default() + } + + pub fn wayland(&mut self) -> Arc { + self.wayland + .get_or_insert_with(|| Arc::new(wayland::Client::new())) + .clone() + } + + #[cfg(feature = "clipboard")] + pub fn clipboard(&mut self) -> Arc { + let wayland = self.wayland(); + + self.clipboard + .get_or_insert_with(|| Arc::new(clipboard::Client::new(wayland))) + .clone() + } + + #[cfg(feature = "workspaces")] + pub fn workspaces(&mut self) -> Arc { + // TODO: Error handling here isn't great - should throw a user-friendly error & exit + self.workspaces + .get_or_insert_with(|| { + compositor::Compositor::create_workspace_client().expect("to be valid compositor") + }) + .clone() + } + + #[cfg(feature = "music")] + pub fn music(&mut self, client_type: music::ClientType) -> Arc { + self.music + .entry(client_type.clone()) + .or_insert_with(|| music::create_client(client_type)) + .clone() + } + + #[cfg(feature = "tray")] + pub fn tray(&mut self) -> Arc { + self.tray + .get_or_insert_with(|| { + Arc::new(crate::await_sync(async { + system_tray::create_client().await + })) + }) + .clone() + } + + #[cfg(feature = "upower")] + pub fn upower(&mut self) -> Arc> { + self.upower + .get_or_insert_with(|| { + crate::await_sync(async { upower::create_display_proxy().await }) + }) + .clone() + } +} + +/// Types implementing this trait +/// indicate that they provide a singleton client instance of type `T`. +pub trait ProvidesClient { + /// Returns a singleton client instance of type `T`. + fn provide(&self) -> Arc; +} + +/// Generates a `ProvidesClient` impl block on `WidgetContext` +/// for the provided `$ty` (first argument) client type. +/// +/// The implementation calls `$method` (second argument) +/// on the `Clients` struct to obtain the client instance. +/// +/// # Example +/// `register_client!(Client, clipboard);` +#[macro_export] +macro_rules! register_client { + ($ty:ty, $method:ident) => { + impl $crate::clients::ProvidesClient<$ty> + for $crate::modules::WidgetContext + where + TSend: Clone, + { + fn provide(&self) -> Arc<$ty> { + self.ironbar.clients.borrow_mut().$method() + } + } + }; +} diff --git a/src/clients/music/mod.rs b/src/clients/music/mod.rs index c2161d5..4269f81 100644 --- a/src/clients/music/mod.rs +++ b/src/clients/music/mod.rs @@ -1,4 +1,5 @@ use color_eyre::Result; +use std::fmt::Debug; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -19,8 +20,6 @@ pub enum PlayerUpdate { /// Triggered at regular intervals while a track is playing. /// Used to keep track of the progress through the current track. ProgressTick(ProgressTick), - /// Triggered when the client disconnects from the player. - Disconnect, } #[derive(Clone, Debug)] @@ -56,7 +55,7 @@ pub struct ProgressTick { pub elapsed: Option, } -pub trait MusicClient { +pub trait MusicClient: Debug + Send + Sync { fn play(&self) -> Result<()>; fn pause(&self) -> Result<()>; fn next(&self) -> Result<()>; @@ -68,18 +67,15 @@ pub trait MusicClient { fn subscribe_change(&self) -> broadcast::Receiver; } -pub enum ClientType<'a> { - Mpd { host: &'a str, music_dir: PathBuf }, +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum ClientType { + Mpd { host: String, music_dir: PathBuf }, Mpris, } -pub async fn get_client(client_type: ClientType<'_>) -> Box> { +pub fn create_client(client_type: ClientType) -> Arc { match client_type { - ClientType::Mpd { host, music_dir } => Box::new( - mpd::get_client(host, music_dir) - .await - .expect("Failed to connect to MPD client"), - ), - ClientType::Mpris => Box::new(mpris::get_client()), + ClientType::Mpd { host, music_dir } => Arc::new(mpd::Client::new(host, music_dir)), + ClientType::Mpris => Arc::new(mpris::Client::new()), } } diff --git a/src/clients/music/mpd.rs b/src/clients/music/mpd.rs index 2d2ffaa..f1239c9 100644 --- a/src/clients/music/mpd.rs +++ b/src/clients/music/mpd.rs @@ -1,91 +1,72 @@ use super::{ MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS, }; -use crate::{await_sync, send, spawn}; +use crate::{await_sync, send, spawn, Ironbar}; +use color_eyre::Report; use color_eyre::Result; -use lazy_static::lazy_static; -use mpd_client::client::{Connection, ConnectionEvent, Subsystem}; -use mpd_client::commands::SeekMode; -use mpd_client::protocol::MpdProtocolError; +use mpd_client::client::{ConnectionEvent, Subsystem}; +use mpd_client::commands::{self, SeekMode}; use mpd_client::responses::{PlayState, Song}; use mpd_client::tag::Tag; -use mpd_client::{commands, Client}; -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::os::unix::fs::FileTypeExt; +use mpd_utils::{mpd_client, PersistentClient}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; -use tokio::net::{TcpStream, UnixStream}; use tokio::sync::broadcast; -use tokio::sync::Mutex; use tokio::time::sleep; -use tracing::{debug, error, info}; +use tracing::debug; -lazy_static! { - static ref CONNECTIONS: Arc>>> = - Arc::new(Mutex::new(HashMap::new())); +macro_rules! command { + ($self:ident, $command:expr) => { + await_sync(async move { $self.client.command($command).await.map_err(Report::new) }) + }; } -pub struct MpdClient { - client: Client, +#[derive(Debug)] +pub struct Client { + client: Arc, music_dir: PathBuf, tx: broadcast::Sender, _rx: broadcast::Receiver, } -#[derive(Debug)] -pub enum MpdConnectionError { - MaxRetries, - ProtocolError(MpdProtocolError), -} +impl Client { + pub fn new(host: String, music_dir: PathBuf) -> Self { + let client = Arc::new(PersistentClient::new(host, Duration::from_secs(5))); + let mut client_rx = client.subscribe(); -impl Display for MpdConnectionError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::MaxRetries => write!(f, "Reached max retries"), - Self::ProtocolError(e) => write!(f, "{e:?}"), - } - } -} + let (tx, rx) = broadcast::channel(32); -impl std::error::Error for MpdConnectionError {} - -impl MpdClient { - async fn new(host: &str, music_dir: PathBuf) -> Result { - debug!("Creating new MPD connection to {}", host); - - let (client, mut state_changes) = - wait_for_connection(host, Duration::from_secs(5), None).await?; - - let (tx, rx) = broadcast::channel(16); + let _guard = Ironbar::runtime().enter(); + client.init(); { - let music_dir = music_dir.clone(); let tx = tx.clone(); let client = client.clone(); + let music_dir = music_dir.clone(); spawn(async move { - while let Some(change) = state_changes.next().await { - debug!("Received state change: {:?}", change); + Self::send_update(&client, &tx, &music_dir) + .await + .expect("Failed to send update"); + while let Ok(change) = client_rx.recv().await { + debug!("Received state change: {change:?}"); if let ConnectionEvent::SubsystemChange( Subsystem::Player | Subsystem::Queue | Subsystem::Mixer, - ) = change + ) = *change { Self::send_update(&client, &tx, &music_dir) .await .expect("Failed to send update"); } } - - Ok::<(), broadcast::error::SendError<(Option, Status)>>(()) }); } { - let client = client.clone(); let tx = tx.clone(); + let client = client.clone(); spawn(async move { loop { @@ -95,16 +76,16 @@ impl MpdClient { }); } - Ok(Self { + Self { client, - music_dir, tx, + music_dir, _rx: rx, - }) + } } async fn send_update( - client: &Client, + client: &PersistentClient, tx: &broadcast::Sender, music_dir: &Path, ) -> Result<(), broadcast::error::SendError> { @@ -112,7 +93,7 @@ impl MpdClient { let status = client.command(commands::Status).await; if let (Ok(current_song), Ok(status)) = (current_song, status) { - let track = current_song.map(|s| Self::convert_song(&s.song, music_dir)); + let track = current_song.map(|s| convert_song(&s.song, music_dir)); let status = Status::from(status); let update = PlayerUpdate::Update(Box::new(track), status); @@ -122,7 +103,7 @@ impl MpdClient { Ok(()) } - async fn send_tick_update(client: &Client, tx: &broadcast::Sender) { + async fn send_tick_update(client: &PersistentClient, tx: &broadcast::Sender) { let status = client.command(commands::Status).await; if let Ok(status) = status { @@ -136,183 +117,70 @@ impl MpdClient { } } } - - fn is_connected(&self) -> bool { - !self.client.is_connection_closed() - } - - fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError> { - info!("Connection to MPD server lost"); - self.tx.send(PlayerUpdate::Disconnect)?; - Ok(()) - } - - fn convert_song(song: &Song, music_dir: &Path) -> Track { - let (track, disc) = song.number(); - - let cover_path = music_dir - .join( - song.file_path() - .parent() - .expect("Song path should not be root") - .join("cover.jpg"), - ) - .into_os_string() - .into_string() - .ok(); - - Track { - title: song.title().map(std::string::ToString::to_string), - album: song.album().map(std::string::ToString::to_string), - artist: Some(song.artists().join(", ")), - date: try_get_first_tag(song, &Tag::Date).map(std::string::ToString::to_string), - genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string), - disc: Some(disc), - track: Some(track), - cover_path, - } - } } -macro_rules! async_command { - ($client:expr, $command:expr) => { - await_sync(async { - $client - .command($command) - .await - .unwrap_or_else(|err| error!("Failed to send command: {err:?}")) - }) - }; -} - -impl MusicClient for MpdClient { +impl MusicClient for Client { fn play(&self) -> Result<()> { - async_command!(self.client, commands::SetPause(false)); - Ok(()) + command!(self, commands::SetPause(false)) } fn pause(&self) -> Result<()> { - async_command!(self.client, commands::SetPause(true)); - Ok(()) + command!(self, commands::SetPause(true)) } fn next(&self) -> Result<()> { - async_command!(self.client, commands::Next); - Ok(()) + command!(self, commands::Next) } fn prev(&self) -> Result<()> { - async_command!(self.client, commands::Previous); - Ok(()) + command!(self, commands::Previous) } fn set_volume_percent(&self, vol: u8) -> Result<()> { - async_command!(self.client, commands::SetVolume(vol)); - Ok(()) + command!(self, commands::SetVolume(vol)) } fn seek(&self, duration: Duration) -> Result<()> { - async_command!(self.client, commands::Seek(SeekMode::Absolute(duration))); - Ok(()) + command!(self, commands::Seek(SeekMode::Absolute(duration))) } fn subscribe_change(&self) -> broadcast::Receiver { let rx = self.tx.subscribe(); - await_sync(async { + await_sync(async move { Self::send_update(&self.client, &self.tx, &self.music_dir) .await - .expect("Failed to send player update"); + .expect("to be able to send update"); }); rx } } -pub async fn get_client( - host: &str, - music_dir: PathBuf, -) -> Result, MpdConnectionError> { - let mut connections = CONNECTIONS.lock().await; - match connections.get(host) { - None => { - let client = MpdClient::new(host, music_dir).await?; - let client = Arc::new(client); - connections.insert(host.to_string(), Arc::clone(&client)); - Ok(client) - } - Some(client) => { - if client.is_connected() { - Ok(Arc::clone(client)) - } else { - client - .send_disconnect_update() - .expect("Failed to send disconnect update"); +fn convert_song(song: &Song, music_dir: &Path) -> Track { + let (track, disc) = song.number(); - let client = MpdClient::new(host, music_dir).await?; - let client = Arc::new(client); - connections.insert(host.to_string(), Arc::clone(&client)); - Ok(client) - } - } + let cover_path = music_dir + .join( + song.file_path() + .parent() + .expect("Song path should not be root") + .join("cover.jpg"), + ) + .into_os_string() + .into_string() + .ok(); + + Track { + title: song.title().map(ToString::to_string), + album: song.album().map(ToString::to_string), + artist: Some(song.artists().join(", ")), + date: try_get_first_tag(song, &Tag::Date).map(ToString::to_string), + genre: try_get_first_tag(song, &Tag::Genre).map(ToString::to_string), + disc: Some(disc), + track: Some(track), + cover_path, } } -async fn wait_for_connection( - host: &str, - interval: Duration, - max_retries: Option, -) -> Result { - let mut retries = 0; - let max_retries = max_retries.unwrap_or(usize::MAX); - - loop { - if retries == max_retries { - break Err(MpdConnectionError::MaxRetries); - } - - retries += 1; - - match try_get_mpd_conn(host).await { - Ok(conn) => break Ok(conn), - Err(err) => { - if retries == max_retries { - break Err(MpdConnectionError::ProtocolError(err)); - } - } - } - - sleep(interval).await; - } -} - -/// Cycles through each MPD host and -/// returns the first one which connects, -/// or none if there are none -async fn try_get_mpd_conn(host: &str) -> Result { - if is_unix_socket(host) { - connect_unix(host).await - } else { - connect_tcp(host).await - } -} - -fn is_unix_socket(host: &str) -> bool { - let path = PathBuf::from(host); - path.exists() - && path - .metadata() - .map_or(false, |metadata| metadata.file_type().is_socket()) -} - -async fn connect_unix(host: &str) -> Result { - let connection = UnixStream::connect(host).await?; - Client::connect(connection).await -} - -async fn connect_tcp(host: &str) -> Result { - let connection = TcpStream::connect(host).await?; - Client::connect(connection).await -} - /// Attempts to read the first value for a tag /// (since the MPD client returns a vector of tags, or None) pub fn try_get_first_tag<'a>(song: &'a Song, tag: &'a Tag) -> Option<&'a str> { diff --git a/src/clients/music/mpris.rs b/src/clients/music/mpris.rs index a520f59..f6eae4c 100644 --- a/src/clients/music/mpris.rs +++ b/src/clients/music/mpris.rs @@ -2,20 +2,16 @@ use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL use crate::clients::music::ProgressTick; use crate::{arc_mut, lock, send, spawn_blocking}; use color_eyre::Result; -use lazy_static::lazy_static; use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder}; +use std::cmp; use std::collections::HashSet; use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::time::Duration; -use std::{cmp, string}; use tokio::sync::broadcast; use tracing::{debug, error, trace}; -lazy_static! { - static ref CLIENT: Arc = Arc::new(Client::new()); -} - +#[derive(Debug)] pub struct Client { current_player: Arc>>, tx: broadcast::Sender, @@ -23,7 +19,7 @@ pub struct Client { } impl Client { - fn new() -> Self { + pub(crate) fn new() -> Self { let (tx, rx) = broadcast::channel(32); let current_player = arc_mut!(None); @@ -289,10 +285,6 @@ impl MusicClient for Client { } } -pub fn get_client() -> Arc { - CLIENT.clone() -} - impl From for Track { fn from(value: Metadata) -> Self { const KEY_DATE: &str = "xesam:contentCreated"; @@ -301,11 +293,11 @@ impl From for Track { Self { title: value .title() - .map(std::string::ToString::to_string) + .map(ToString::to_string) .and_then(replace_empty_none), album: value .album_name() - .map(std::string::ToString::to_string) + .map(ToString::to_string) .and_then(replace_empty_none), artist: value .artists() @@ -314,14 +306,14 @@ impl From for Track { date: value .get(KEY_DATE) .and_then(mpris::MetadataValue::as_string) - .map(std::string::ToString::to_string), + .map(ToString::to_string), disc: value.disc_number().map(|disc| disc as u64), genre: value .get(KEY_GENRE) .and_then(mpris::MetadataValue::as_str_array) .and_then(|arr| arr.first().map(|val| (*val).to_string())), track: value.track_number().map(|track| track as u64), - cover_path: value.art_url().map(string::ToString::to_string), + cover_path: value.art_url().map(ToString::to_string), } } } diff --git a/src/clients/system_tray.rs b/src/clients/system_tray.rs index 0c700c0..60c8b12 100644 --- a/src/clients/system_tray.rs +++ b/src/clients/system_tray.rs @@ -1,7 +1,5 @@ -use crate::{arc_mut, lock, send, spawn, Ironbar}; -use async_once::AsyncOnce; +use crate::{arc_mut, lock, register_client, send, spawn, Ironbar}; use color_eyre::Report; -use lazy_static::lazy_static; use std::collections::BTreeMap; use std::sync::{Arc, Mutex}; use system_tray::message::menu::TrayMenu; @@ -13,6 +11,7 @@ use tracing::{debug, error, trace}; type Tray = BTreeMap, Option)>; +#[derive(Debug)] pub struct TrayEventReceiver { tx: mpsc::Sender, b_tx: broadcast::Sender, @@ -39,7 +38,7 @@ impl TrayEventReceiver { spawn(async move { while let Ok(message) = host.recv().await { - trace!("Received message: {message:?} "); + trace!("Received message: {message:?}"); send!(b_tx, message.clone()); let mut tray = lock!(tray); @@ -94,32 +93,35 @@ impl TrayEventReceiver { } } -lazy_static! { - static ref CLIENT: AsyncOnce = AsyncOnce::new(async { - const MAX_RETRIES: i32 = 10; +/// Attempts to create a new `TrayEventReceiver` instance, +/// retrying a maximum of 10 times before panicking the thread. +pub async fn create_client() -> TrayEventReceiver { + const MAX_RETRIES: i32 = 10; - // sometimes this can fail - let mut retries = 0; + // sometimes this can fail + let mut retries = 0; - let value = loop { - retries += 1; + let value = loop { + retries += 1; - let tray = Box::pin(TrayEventReceiver::new()).await; + let tray = Box::pin(TrayEventReceiver::new()).await; - match tray { - Ok(tray) => break Some(tray), - Err(err) => error!("{:?}", Report::new(err).wrap_err(format!("Failed to create StatusNotifierWatcher (attempt {retries})"))) - } + match tray { + Ok(tray) => break Some(tray), + Err(err) => error!( + "{:?}", + Report::new(err).wrap_err(format!( + "Failed to create StatusNotifierWatcher (attempt {retries})" + )) + ), + } - if retries == MAX_RETRIES { - break None; - } - }; + if retries == MAX_RETRIES { + break None; + } + }; - value.expect("Failed to create StatusNotifierWatcher") - }); + value.expect("Failed to create StatusNotifierWatcher") } -pub async fn get_tray_event_client() -> &'static TrayEventReceiver { - CLIENT.get().await -} +register_client!(TrayEventReceiver, tray); diff --git a/src/clients/upower.rs b/src/clients/upower.rs index 895b9ce..f3cade1 100644 --- a/src/clients/upower.rs +++ b/src/clients/upower.rs @@ -1,40 +1,35 @@ -use async_once::AsyncOnce; -use lazy_static::lazy_static; +use crate::register_client; use std::sync::Arc; use upower_dbus::UPowerProxy; use zbus::fdo::PropertiesProxy; -lazy_static! { - static ref DISPLAY_PROXY: AsyncOnce>> = AsyncOnce::new(async { - let dbus = Box::pin(zbus::Connection::system()) - .await - .expect("failed to create connection to system bus"); +pub async fn create_display_proxy() -> Arc> { + let dbus = Box::pin(zbus::Connection::system()) + .await + .expect("failed to create connection to system bus"); - let device_proxy = UPowerProxy::new(&dbus) - .await - .expect("failed to create upower proxy"); + let device_proxy = UPowerProxy::new(&dbus) + .await + .expect("failed to create upower proxy"); - let display_device = device_proxy - .get_display_device() - .await - .unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}")); + let display_device = device_proxy + .get_display_device() + .await + .unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}")); - let path = display_device.path().to_owned(); + let path = display_device.path().to_owned(); - let proxy = PropertiesProxy::builder(&dbus) - .destination("org.freedesktop.UPower") - .expect("failed to set proxy destination address") - .path(path) - .expect("failed to set proxy path") - .cache_properties(zbus::CacheProperties::No) - .build() - .await - .expect("failed to build proxy"); + let proxy = PropertiesProxy::builder(&dbus) + .destination("org.freedesktop.UPower") + .expect("failed to set proxy destination address") + .path(path) + .expect("failed to set proxy path") + .cache_properties(zbus::CacheProperties::No) + .build() + .await + .expect("failed to build proxy"); - Arc::new(proxy) - }); + Arc::new(proxy) } -pub async fn get_display_proxy() -> &'static PropertiesProxy<'static> { - DISPLAY_PROXY.get().await -} +register_client!(PropertiesProxy<'static>, upower); diff --git a/src/clients/wayland/client.rs b/src/clients/wayland/client.rs deleted file mode 100644 index c4ddb62..0000000 --- a/src/clients/wayland/client.rs +++ /dev/null @@ -1,269 +0,0 @@ -use super::wlr_foreign_toplevel::handle::ToplevelHandle; -use super::wlr_foreign_toplevel::manager::ToplevelManagerState; -use super::wlr_foreign_toplevel::ToplevelEvent; -use super::Environment; -use crate::error::ERR_CHANNEL_RECV; -use crate::{send, spawn_blocking}; -use cfg_if::cfg_if; -use color_eyre::Report; -use smithay_client_toolkit::output::{OutputInfo, OutputState}; -use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender}; -use smithay_client_toolkit::reexports::calloop::EventLoop; -use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource; -use smithay_client_toolkit::registry::RegistryState; -use smithay_client_toolkit::seat::SeatState; -use std::collections::HashMap; -use std::sync::mpsc; -use tokio::sync::broadcast; -use tracing::{debug, error, trace}; -use wayland_client::globals::registry_queue_init; -use wayland_client::protocol::wl_seat::WlSeat; -use wayland_client::Connection; - -cfg_if! { - if #[cfg(feature = "clipboard")] { - use super::ClipboardItem; - use super::wlr_data_control::manager::DataControlDeviceManagerState; - use crate::lock; - use std::sync::Arc; - } -} - -#[derive(Debug)] -pub enum Request { - /// Sends a request for all the outputs. - /// These are then sent on the `output` channel. - Outputs, - /// Sends a request for all the seats. - /// These are then sent ont the `seat` channel. - Seats, - /// Sends a request for all the toplevels. - /// These are then sent on the `toplevel_init` channel. - Toplevels, - /// Sends a request for the current clipboard item. - /// This is then sent on the `clipboard_init` channel. - #[cfg(feature = "clipboard")] - Clipboard, - /// Copies the value to the clipboard - #[cfg(feature = "clipboard")] - CopyToClipboard(Arc), - /// Forces a dispatch, flushing any currently queued events - Roundtrip, -} - -pub struct WaylandClient { - // External channels - toplevel_tx: broadcast::Sender, - _toplevel_rx: broadcast::Receiver, - #[cfg(feature = "clipboard")] - clipboard_tx: broadcast::Sender>, - #[cfg(feature = "clipboard")] - _clipboard_rx: broadcast::Receiver>, - - // Internal channels - toplevel_init_rx: mpsc::Receiver>, - output_rx: mpsc::Receiver>, - seat_rx: mpsc::Receiver>, - #[cfg(feature = "clipboard")] - clipboard_init_rx: mpsc::Receiver>>, - - request_tx: Sender, -} - -impl WaylandClient { - pub(super) fn new() -> Self { - let (toplevel_tx, toplevel_rx) = broadcast::channel(32); - - let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel(); - #[cfg(feature = "clipboard")] - let (clipboard_init_tx, clipboard_init_rx) = mpsc::channel(); - let (output_tx, output_rx) = mpsc::channel(); - let (seat_tx, seat_rx) = mpsc::channel(); - - let toplevel_tx2 = toplevel_tx.clone(); - - cfg_if! { - if #[cfg(feature = "clipboard")] { - let (clipboard_tx, clipboard_rx) = broadcast::channel(32); - let clipboard_tx2 = clipboard_tx.clone(); - } - } - - let (ev_tx, ev_rx) = channel::(); - - // `queue` is not `Send` so we need to handle everything inside the task - spawn_blocking(move || { - let toplevel_tx = toplevel_tx2; - #[cfg(feature = "clipboard")] - let clipboard_tx = clipboard_tx2; - - let conn = - Connection::connect_to_env().expect("Failed to connect to Wayland compositor"); - let (globals, queue) = - registry_queue_init(&conn).expect("Failed to retrieve Wayland globals"); - - let qh = queue.handle(); - let mut event_loop = - EventLoop::::try_new().expect("Failed to create new event loop"); - - WaylandSource::new(conn, queue) - .insert(event_loop.handle()) - .expect("Failed to insert Wayland event queue into event loop"); - - let loop_handle = event_loop.handle(); - - // Initialize the registry handling - // so other parts of Smithay's client toolkit may bind globals. - let registry_state = RegistryState::new(&globals); - - let output_delegate = OutputState::new(&globals, &qh); - let seat_delegate = SeatState::new(&globals, &qh); - - #[cfg(feature = "clipboard")] - let data_control_device_manager_delegate = - DataControlDeviceManagerState::bind(&globals, &qh) - .expect("data device manager is not available"); - - let foreign_toplevel_manager_delegate = ToplevelManagerState::bind(&globals, &qh) - .expect("foreign toplevel manager is not available"); - - let mut env = Environment { - registry_state, - output_state: output_delegate, - seat_state: seat_delegate, - #[cfg(feature = "clipboard")] - data_control_device_manager_state: data_control_device_manager_delegate, - foreign_toplevel_manager_state: foreign_toplevel_manager_delegate, - seats: vec![], - handles: HashMap::new(), - #[cfg(feature = "clipboard")] - clipboard: crate::arc_mut!(None), - toplevel_tx, - #[cfg(feature = "clipboard")] - clipboard_tx, - #[cfg(feature = "clipboard")] - data_control_devices: vec![], - #[cfg(feature = "clipboard")] - selection_offers: vec![], - #[cfg(feature = "clipboard")] - copy_paste_sources: vec![], - loop_handle: event_loop.handle(), - }; - - loop_handle - .insert_source(ev_rx, move |event, _metadata, env| { - trace!("{event:?}"); - match event { - Event::Msg(Request::Roundtrip) => debug!("Received refresh event"), - Event::Msg(Request::Outputs) => { - trace!("Received get outputs request"); - - send!(output_tx, env.output_info()); - } - Event::Msg(Request::Seats) => { - trace!("Receive get seats request"); - send!(seat_tx, env.seats.clone()); - } - Event::Msg(Request::Toplevels) => { - trace!("Receive get toplevels request"); - send!(toplevel_init_tx, env.handles.clone()); - } - #[cfg(feature = "clipboard")] - Event::Msg(Request::Clipboard) => { - trace!("Receive get clipboard requests"); - let clipboard = lock!(env.clipboard).clone(); - send!(clipboard_init_tx, clipboard); - } - #[cfg(feature = "clipboard")] - Event::Msg(Request::CopyToClipboard(value)) => { - env.copy_to_clipboard(value, &qh); - } - Event::Closed => panic!("Channel unexpectedly closed"), - } - }) - .expect("Failed to insert channel into event queue"); - - loop { - trace!("Dispatching event loop"); - if let Err(err) = event_loop.dispatch(None, &mut env) { - error!( - "{:?}", - Report::new(err).wrap_err("Failed to dispatch pending wayland events") - ); - } - } - }); - - Self { - toplevel_tx, - _toplevel_rx: toplevel_rx, - toplevel_init_rx, - #[cfg(feature = "clipboard")] - clipboard_init_rx, - output_rx, - seat_rx, - #[cfg(feature = "clipboard")] - clipboard_tx, - #[cfg(feature = "clipboard")] - _clipboard_rx: clipboard_rx, - request_tx: ev_tx, - } - } - - pub fn subscribe_toplevels( - &self, - ) -> ( - broadcast::Receiver, - HashMap, - ) { - let rx = self.toplevel_tx.subscribe(); - - let receiver = &self.toplevel_init_rx; - send!(self.request_tx, Request::Toplevels); - let data = receiver.recv().expect(ERR_CHANNEL_RECV); - - (rx, data) - } - - #[cfg(feature = "clipboard")] - pub fn subscribe_clipboard( - &self, - ) -> ( - broadcast::Receiver>, - Option>, - ) { - let rx = self.clipboard_tx.subscribe(); - - let receiver = &self.clipboard_init_rx; - send!(self.request_tx, Request::Clipboard); - let data = receiver.recv().expect(ERR_CHANNEL_RECV); - - (rx, data) - } - - /// Force a roundtrip on the wayland connection, - /// flushing any queued events and immediately receiving any new ones. - pub fn roundtrip(&self) { - trace!("Sending roundtrip request"); - send!(self.request_tx, Request::Roundtrip); - } - - pub fn get_outputs(&self) -> Vec { - trace!("Sending get outputs request"); - - send!(self.request_tx, Request::Outputs); - self.output_rx.recv().expect(ERR_CHANNEL_RECV) - } - - pub fn get_seats(&self) -> Vec { - trace!("Sending get seats request"); - - send!(self.request_tx, Request::Seats); - self.seat_rx.recv().expect(ERR_CHANNEL_RECV) - } - - #[cfg(feature = "clipboard")] - pub fn copy_to_clipboard(&self, item: Arc) { - send!(self.request_tx, Request::CopyToClipboard(item)); - } -} diff --git a/src/clients/wayland/mod.rs b/src/clients/wayland/mod.rs index 014c786..86746e3 100644 --- a/src/clients/wayland/mod.rs +++ b/src/clients/wayland/mod.rs @@ -1,28 +1,37 @@ -mod client; - mod macros; mod wl_output; mod wl_seat; mod wlr_foreign_toplevel; -use self::wlr_foreign_toplevel::manager::ToplevelManagerState; -use crate::{arc_mut, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager}; +use crate::error::ERR_CHANNEL_RECV; +use crate::{ + arc_mut, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager, lock, + register_client, send, Ironbar, +}; +use std::sync::{Arc, Mutex}; + +use calloop_channel::Event::Msg; use cfg_if::cfg_if; -use lazy_static::lazy_static; -use smithay_client_toolkit::output::OutputState; -use smithay_client_toolkit::reexports::calloop::LoopHandle; +use color_eyre::Report; +use smithay_client_toolkit::output::{OutputInfo, OutputState}; +use smithay_client_toolkit::reexports::calloop::channel as calloop_channel; +use smithay_client_toolkit::reexports::calloop::{EventLoop, LoopHandle}; +use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource; use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState}; use smithay_client_toolkit::seat::SeatState; use smithay_client_toolkit::{ delegate_output, delegate_registry, delegate_seat, registry_handlers, }; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use tokio::sync::broadcast; +use tokio::sync::{broadcast, mpsc}; +use tracing::{debug, error, trace}; +use wayland_client::globals::registry_queue_init; use wayland_client::protocol::wl_seat::WlSeat; +use wayland_client::{Connection, QueueHandle}; -pub use self::client::WaylandClient; -pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo}; +use wlr_foreign_toplevel::manager::ToplevelManagerState; + +pub use wl_output::OutputEvent; +pub use wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo}; cfg_if! { if #[cfg(feature = "clipboard")] { @@ -36,6 +45,7 @@ cfg_if! { pub use wlr_data_control::{ClipboardItem, ClipboardValue}; + #[derive(Debug)] pub struct DataControlDeviceEntry { seat: WlSeat, device: DataControlDevice, @@ -43,35 +53,162 @@ cfg_if! { } } -pub struct Environment { - pub registry_state: RegistryState, - pub output_state: OutputState, - pub seat_state: SeatState, - pub foreign_toplevel_manager_state: ToplevelManagerState, +#[derive(Debug)] +pub enum Event { + Output(OutputEvent), + Toplevel(ToplevelEvent), #[cfg(feature = "clipboard")] - pub data_control_device_manager_state: DataControlDeviceManagerState, - pub loop_handle: LoopHandle<'static, Self>, - - pub seats: Vec, - - #[cfg(feature = "clipboard")] - pub data_control_devices: Vec, - #[cfg(feature = "clipboard")] - pub selection_offers: Vec, - #[cfg(feature = "clipboard")] - pub copy_paste_sources: Vec, - - pub handles: HashMap, - #[cfg(feature = "clipboard")] - clipboard: Arc>>>, - - toplevel_tx: broadcast::Sender, - #[cfg(feature = "clipboard")] - clipboard_tx: broadcast::Sender>, + Clipboard(ClipboardItem), } -// Now we need to say we are delegating the responsibility of output related events for our application data -// type to the requisite delegate. +#[derive(Debug)] +pub enum Request { + Roundtrip, + + OutputInfoAll, + + ToplevelInfoAll, + ToplevelFocus(usize), + + #[cfg(feature = "clipboard")] + CopyToClipboard(ClipboardItem), + #[cfg(feature = "clipboard")] + ClipboardItem, +} + +#[derive(Debug)] +pub enum Response { + /// An empty success response + Ok, + + OutputInfo(Option), + OutputInfoAll(Vec), + + ToplevelInfo(Option), + ToplevelInfoAll(Vec), + + #[cfg(feature = "clipboard")] + ClipboardItem(Option), + + Seat(WlSeat), +} + +#[derive(Debug)] +struct BroadcastChannel(broadcast::Sender, Arc>>); + +impl From<(broadcast::Sender, broadcast::Receiver)> for BroadcastChannel { + fn from(value: (broadcast::Sender, broadcast::Receiver)) -> Self { + BroadcastChannel(value.0, arc_mut!(value.1)) + } +} + +#[derive(Debug)] +pub struct Client { + tx: calloop_channel::Sender, + rx: Arc>>, + + output_channel: BroadcastChannel, + toplevel_channel: BroadcastChannel, + #[cfg(feature = "clipboard")] + clipboard_channel: BroadcastChannel, +} + +impl Client { + pub(crate) fn new() -> Self { + let (event_tx, mut event_rx) = mpsc::channel(32); + + let (request_tx, request_rx) = calloop_channel::channel(); + let (response_tx, response_rx) = std::sync::mpsc::channel(); + + let output_channel = broadcast::channel(32); + let toplevel_channel = broadcast::channel(32); + + #[cfg(feature = "clipboard")] + let clipboard_channel = broadcast::channel(32); + + Ironbar::runtime().spawn_blocking(move || { + Environment::spawn(event_tx, request_rx, response_tx); + }); + + // listen to events + { + let output_tx = output_channel.0.clone(); + let toplevel_tx = toplevel_channel.0.clone(); + + #[cfg(feature = "clipboard")] + let clipboard_tx = clipboard_channel.0.clone(); + + let rt = Ironbar::runtime(); + rt.spawn(async move { + while let Some(event) = event_rx.recv().await { + match event { + Event::Output(event) => send!(output_tx, event), + Event::Toplevel(event) => send!(toplevel_tx, event), + #[cfg(feature = "clipboard")] + Event::Clipboard(item) => send!(clipboard_tx, item), + }; + } + }); + } + + Self { + tx: request_tx, + rx: arc_mut!(response_rx), + + output_channel: output_channel.into(), + toplevel_channel: toplevel_channel.into(), + #[cfg(feature = "clipboard")] + clipboard_channel: clipboard_channel.into(), + } + } + + /// Sends a request to the environment event loop, + /// and returns the response. + fn send_request(&self, request: Request) -> Response { + send!(self.tx, request); + lock!(self.rx).recv().expect(ERR_CHANNEL_RECV) + } + + /// Sends a round-trip request to the client, + /// forcing it to send/receive any events in the queue. + pub(crate) fn roundtrip(&self) -> Response { + self.send_request(Request::Roundtrip) + } +} + +#[derive(Debug)] +pub struct Environment { + registry_state: RegistryState, + output_state: OutputState, + seat_state: SeatState, + + queue_handle: QueueHandle, + loop_handle: LoopHandle<'static, Self>, + + event_tx: mpsc::Sender, + response_tx: std::sync::mpsc::Sender, + + // local state + handles: Vec, + + // -- clipboard -- + #[cfg(feature = "clipboard")] + data_control_device_manager_state: DataControlDeviceManagerState, + + #[cfg(feature = "clipboard")] + data_control_devices: Vec, + #[cfg(feature = "clipboard")] + copy_paste_sources: Vec, + #[cfg(feature = "clipboard")] + selection_offers: Vec, + + // local state + #[cfg(feature = "clipboard")] + clipboard: Arc>>, +} + +delegate_registry!(Environment); + delegate_output!(Environment); delegate_seat!(Environment); @@ -82,21 +219,128 @@ cfg_if! { if #[cfg(feature = "clipboard")] { delegate_data_control_device_manager!(Environment); delegate_data_control_device!(Environment); - delegate_data_control_source!(Environment); delegate_data_control_offer!(Environment); + delegate_data_control_source!(Environment); } } -// In order for our delegate to know of the existence of globals, we need to implement registry -// handling for the program. This trait will forward events to the RegistryHandler trait -// implementations. -delegate_registry!(Environment); +impl Environment { + pub fn spawn( + event_tx: mpsc::Sender, + request_rx: calloop_channel::Channel, + response_tx: std::sync::mpsc::Sender, + ) { + let conn = Connection::connect_to_env().expect("Failed to connect to Wayland compositor"); + let (globals, queue) = + registry_queue_init(&conn).expect("Failed to retrieve Wayland globals"); + + let qh = queue.handle(); + let mut event_loop = EventLoop::::try_new().expect("Failed to create new event loop"); + + WaylandSource::new(conn, queue) + .insert(event_loop.handle()) + .expect("Failed to insert Wayland event queue into event loop"); + + let loop_handle = event_loop.handle(); + + // Initialize the registry handling + // so other parts of Smithay's client toolkit may bind globals. + let registry_state = RegistryState::new(&globals); + + let output_state = OutputState::new(&globals, &qh); + let seat_state = SeatState::new(&globals, &qh); + ToplevelManagerState::bind(&globals, &qh) + .expect("to bind to wlr_foreign_toplevel_manager global"); + + #[cfg(feature = "clipboard")] + let data_control_device_manager_state = DataControlDeviceManagerState::bind(&globals, &qh) + .expect("to bind to wlr_data_control_device_manager global"); + + let mut env = Self { + registry_state, + output_state, + seat_state, + #[cfg(feature = "clipboard")] + data_control_device_manager_state, + queue_handle: qh, + loop_handle: loop_handle.clone(), + event_tx, + response_tx, + handles: vec![], + + #[cfg(feature = "clipboard")] + data_control_devices: vec![], + #[cfg(feature = "clipboard")] + copy_paste_sources: vec![], + #[cfg(feature = "clipboard")] + selection_offers: vec![], + #[cfg(feature = "clipboard")] + clipboard: arc_mut!(None), + }; + + loop_handle + .insert_source(request_rx, Self::on_request) + .expect("to be able to insert source"); + + loop { + trace!("Dispatching event loop"); + if let Err(err) = event_loop.dispatch(None, &mut env) { + error!( + "{:?}", + Report::new(err).wrap_err("Failed to dispatch pending wayland events") + ); + } + } + } + + /// Processes a request from the client + /// and sends the response. + fn on_request(event: calloop_channel::Event, _metadata: &mut (), env: &mut Self) { + trace!("Request: {event:?}"); + + match event { + Msg(Request::Roundtrip) => { + debug!("received roundtrip request"); + send!(env.response_tx, Response::Ok); + } + Msg(Request::OutputInfoAll) => { + let infos = env.output_info_all(); + send!(env.response_tx, Response::OutputInfoAll(infos)); + } + Msg(Request::ToplevelInfoAll) => { + let infos = env + .handles + .iter() + .filter_map(ToplevelHandle::info) + .collect(); + send!(env.response_tx, Response::ToplevelInfoAll(infos)); + } + Msg(Request::ToplevelFocus(id)) => { + let handle = env + .handles + .iter() + .find(|handle| handle.info().map_or(false, |info| info.id == id)); + if let Some(handle) = handle { + let seat = env.default_seat(); + handle.focus(&seat); + } + send!(env.response_tx, Response::Ok); + } + #[cfg(feature = "clipboard")] + Msg(Request::CopyToClipboard(item)) => { + env.copy_to_clipboard(item); + send!(env.response_tx, Response::Ok); + } + #[cfg(feature = "clipboard")] + Msg(Request::ClipboardItem) => { + let item = lock!(env.clipboard).clone(); + send!(env.response_tx, Response::ClipboardItem(item)); + } + calloop_channel::Event::Closed => error!("request channel unexpectedly closed"), + } + } +} -// In order for delegate_registry to work, our application data type needs to provide a way for the -// implementation to access the registry state. -// -// We also need to indicate which delegates will get told about globals being created. We specify -// the types of the delegates inside the array. impl ProvidesRegistryState for Environment { fn registry(&mut self) -> &mut RegistryState { &mut self.registry_state @@ -104,10 +348,4 @@ impl ProvidesRegistryState for Environment { registry_handlers![OutputState, SeatState]; } -lazy_static! { - static ref CLIENT: Arc> = arc_mut!(WaylandClient::new()); -} - -pub fn get_client() -> Arc> { - CLIENT.clone() -} +register_client!(Client, wayland); diff --git a/src/clients/wayland/wl_output.rs b/src/clients/wayland/wl_output.rs index 1b3af8d..b162b6d 100644 --- a/src/clients/wayland/wl_output.rs +++ b/src/clients/wayland/wl_output.rs @@ -1,11 +1,41 @@ -use super::Environment; +use super::{Client, Environment, Event, Request, Response}; +use crate::try_send; use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState}; -use tracing::debug; -use wayland_client::protocol::wl_output; +use tokio::sync::broadcast; +use tracing::{debug, error}; +use wayland_client::protocol::wl_output::WlOutput; use wayland_client::{Connection, QueueHandle}; +#[derive(Debug, Clone)] +pub struct OutputEvent { + output: OutputInfo, + event_type: OutputEventType, +} + +#[derive(Debug, Clone, Copy)] +pub enum OutputEventType { + New, + Update, + Destroyed, +} + +impl Client { + /// Gets the information for all outputs. + pub fn output_info_all(&self) -> Vec { + match self.send_request(Request::OutputInfoAll) { + Response::OutputInfoAll(info) => info, + _ => unreachable!(), + } + } + + /// Subscribes to events from outputs. + pub fn subscribe_outputs(&self) -> broadcast::Receiver { + self.output_channel.0.subscribe() + } +} + impl Environment { - pub fn output_info(&mut self) -> Vec { + pub fn output_info_all(&mut self) -> Vec { self.output_state .outputs() .filter_map(|output| self.output_state.info(&output)) @@ -27,29 +57,48 @@ impl OutputHandler for Environment { // Then there exist these functions that indicate the lifecycle of an output. // These will be called as appropriate by the delegate implementation. - fn new_output( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _output: wl_output::WlOutput, - ) { + fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle, output: WlOutput) { debug!("Handler received new output"); + if let Some(info) = self.output_state.info(&output) { + try_send!( + self.event_tx, + Event::Output(OutputEvent { + output: info, + event_type: OutputEventType::New + }) + ); + } else { + error!("Output is missing information!"); + } } - fn update_output( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _output: wl_output::WlOutput, - ) { + fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle, output: WlOutput) { + debug!("Handle received output update"); + if let Some(info) = self.output_state.info(&output) { + try_send!( + self.event_tx, + Event::Output(OutputEvent { + output: info, + event_type: OutputEventType::Update + }) + ); + } else { + error!("Output is missing information!"); + } } - fn output_destroyed( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _output: wl_output::WlOutput, - ) { + fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle, output: WlOutput) { debug!("Handle received output destruction"); + if let Some(info) = self.output_state.info(&output) { + try_send!( + self.event_tx, + Event::Output(OutputEvent { + output: info, + event_type: OutputEventType::Destroyed + }) + ); + } else { + error!("Output is missing information!"); + } } } diff --git a/src/clients/wayland/wl_seat.rs b/src/clients/wayland/wl_seat.rs index ee36892..abd401d 100644 --- a/src/clients/wayland/wl_seat.rs +++ b/src/clients/wayland/wl_seat.rs @@ -1,24 +1,30 @@ use super::Environment; use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; use tracing::debug; -use wayland_client::protocol::wl_seat; +use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::{Connection, QueueHandle}; +impl Environment { + /// Gets the default seat. + pub(crate) fn default_seat(&self) -> WlSeat { + self.seat_state.seats().next().expect("one seat to exist") + } +} + impl SeatHandler for Environment { fn seat_state(&mut self) -> &mut SeatState { &mut self.seat_state } - fn new_seat(&mut self, _: &Connection, _: &QueueHandle, seat: wl_seat::WlSeat) { + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _seat: WlSeat) { debug!("Handler received new seat"); - self.seats.push(seat); } fn new_capability( &mut self, _: &Connection, qh: &QueueHandle, - seat: wl_seat::WlSeat, + seat: WlSeat, _: Capability, ) { debug!("Handler received new capability"); @@ -39,25 +45,22 @@ impl SeatHandler for Environment { device: data_control_device, }); } - - if !self.seats.iter().any(|s| s == &seat) { - self.seats.push(seat); - } } fn remove_capability( &mut self, _: &Connection, _: &QueueHandle, - _: wl_seat::WlSeat, + seat: WlSeat, _: Capability, ) { debug!("Handler received capability removal"); - // Not applicable + + #[cfg(feature = "clipboard")] + self.data_control_devices.retain(|entry| entry.seat != seat); } - fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, seat: wl_seat::WlSeat) { + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _seat: WlSeat) { debug!("Handler received seat removal"); - self.seats.retain(|s| s != &seat); } } diff --git a/src/clients/wayland/wlr_data_control/device.rs b/src/clients/wayland/wlr_data_control/device.rs index 8135a86..08e90c8 100644 --- a/src/clients/wayland/wlr_data_control/device.rs +++ b/src/clients/wayland/wlr_data_control/device.rs @@ -12,6 +12,7 @@ use wayland_protocols_wlr::data_control::v1::client::{ zwlr_data_control_offer_v1::ZwlrDataControlOfferV1, }; +#[derive(Debug)] pub struct DataControlDevice { pub device: ZwlrDataControlDeviceV1, } diff --git a/src/clients/wayland/wlr_data_control/manager.rs b/src/clients/wayland/wlr_data_control/manager.rs index a2a2ac4..323b71b 100644 --- a/src/clients/wayland/wlr_data_control/manager.rs +++ b/src/clients/wayland/wlr_data_control/manager.rs @@ -14,6 +14,7 @@ use wayland_protocols_wlr::data_control::v1::client::{ zwlr_data_control_source_v1::ZwlrDataControlSourceV1, }; +#[derive(Debug)] pub struct DataControlDeviceManagerState { manager: ZwlrDataControlManagerV1, _phantom: PhantomData, diff --git a/src/clients/wayland/wlr_data_control/mod.rs b/src/clients/wayland/wlr_data_control/mod.rs index dc958b6..8c40a84 100644 --- a/src/clients/wayland/wlr_data_control/mod.rs +++ b/src/clients/wayland/wlr_data_control/mod.rs @@ -6,8 +6,8 @@ pub mod source; use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler}; use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer}; use self::source::DataControlSourceHandler; -use crate::clients::wayland::Environment; -use crate::{lock, send, Ironbar}; +use super::{Client, Environment, Event, Request, Response}; +use crate::{lock, try_send, Ironbar}; use device::DataControlDevice; use glib::Bytes; use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ}; @@ -21,22 +21,28 @@ use std::io::{ErrorKind, Read, Write}; use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::sync::Arc; use std::{fs, io}; +use tokio::sync::broadcast; use tracing::{debug, error, trace}; use wayland_client::{Connection, QueueHandle}; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal"; +#[derive(Debug)] pub struct SelectionOfferItem { offer: SelectionOffer, token: Option, } +/// Represents a value which can be read/written +/// to/from the system clipboard and surrounding metadata. +/// +/// Can be cheaply cloned. #[derive(Debug, Clone, Eq)] pub struct ClipboardItem { pub id: usize, - pub value: ClipboardValue, - pub mime_type: String, + pub value: Arc, + pub mime_type: Arc, } impl PartialEq for ClipboardItem { @@ -108,24 +114,60 @@ impl MimeType { } } -impl Environment { - pub fn copy_to_clipboard(&mut self, item: Arc, qh: &QueueHandle) { - debug!("Copying item to clipboard: {item:?}"); - - // TODO: Proper device tracking - let device = self.data_control_devices.first(); - if let Some(device) = device { - let source = self - .data_control_device_manager_state - .create_copy_paste_source(qh, [INTERNAL_MIME_TYPE, item.mime_type.as_str()]); - - source.set_selection(&device.device); - self.copy_paste_sources.push(source); - - lock!(self.clipboard).replace(item); +impl Client { + /// Gets the current clipboard item, + /// if this exists and Ironbar has record of it. + pub fn clipboard_item(&self) -> Option { + match self.send_request(Request::ClipboardItem) { + Response::ClipboardItem(item) => item, + _ => unreachable!(), } } + /// Copies the provided value to the system clipboard. + pub fn copy_to_clipboard(&self, item: ClipboardItem) { + match self.send_request(Request::CopyToClipboard(item)) { + Response::Ok => (), + _ => unreachable!(), + } + } + + /// Subscribes to the system clipboard, + /// receiving all new copied items. + pub fn subscribe_clipboard(&self) -> broadcast::Receiver { + self.clipboard_channel.0.subscribe() + } +} + +impl Environment { + /// Creates a new copy/paste source on the + /// seat's data control device. + /// + /// This provides it as an offer, + /// which the compositor will then treat as the current copied value. + pub fn copy_to_clipboard(&mut self, item: ClipboardItem) { + debug!("Copying item to clipboard: {item:?}"); + + let seat = self.default_seat(); + let Some(device) = self + .data_control_devices + .iter() + .find(|entry| entry.seat == seat) + else { + return; + }; + + let source = self + .data_control_device_manager_state + .create_copy_paste_source(&self.queue_handle, [INTERNAL_MIME_TYPE, &item.mime_type]); + + source.set_selection(&device.device); + self.copy_paste_sources.push(source); + + lock!(self.clipboard).replace(item); + } + + /// Reads an offer file handle into a new `ClipboardItem`. fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result { let value = match mime_type.category { MimeTypeCategory::Text => { @@ -145,13 +187,15 @@ impl Environment { Ok(ClipboardItem { id: Ironbar::unique_id(), - value, - mime_type: mime_type.value.clone(), + value: Arc::new(value), + mime_type: mime_type.value.clone().into(), }) } } impl DataControlDeviceHandler for Environment { + /// Called when an offer for a new value is received + /// (ie something has copied to the clipboard) fn selection( &mut self, _conn: &Connection, @@ -175,15 +219,16 @@ impl DataControlDeviceHandler for Environment { .last_mut() .expect("Failed to get current offer"); + // clear prev let Some(mime_type) = MimeType::parse_multiple(&mime_types) else { lock!(self.clipboard).take(); // send an event so the clipboard module is aware it's changed - send!( - self.clipboard_tx, - Arc::new(ClipboardItem { + try_send!( + self.event_tx, + Event::Clipboard(ClipboardItem { id: usize::MAX, - mime_type: String::new(), - value: ClipboardValue::Other + mime_type: String::new().into(), + value: Arc::new(ClipboardValue::Other) }) ); return; @@ -192,7 +237,7 @@ impl DataControlDeviceHandler for Environment { if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) { let offer_clone = cur_offer.offer.clone(); - let tx = self.clipboard_tx.clone(); + let tx = self.event_tx.clone(); let clipboard = self.clipboard.clone(); let token = @@ -207,9 +252,8 @@ impl DataControlDeviceHandler for Environment { match Self::read_file(&mime_type, file.get_mut()) { Ok(item) => { - let item = Arc::new(item); lock!(clipboard).replace(item.clone()); - send!(tx, item); + try_send!(tx, Event::Clipboard(item)); } Err(err) => error!("{err:?}"), } @@ -255,6 +299,8 @@ impl DataControlSourceHandler for Environment { debug!("Accepted mime type: {mime:?}"); } + /// Writes the current clipboard item to 'paste' it + /// upon request from a compositor client. fn send_request( &mut self, _conn: &Connection, @@ -274,12 +320,12 @@ impl DataControlSourceHandler for Environment { { trace!("Source found, writing to file"); - let mut bytes = match &item.value { + let mut bytes = match item.value.as_ref() { ClipboardValue::Text(text) => text.as_bytes(), ClipboardValue::Image(bytes) => bytes.as_ref(), ClipboardValue::Other => panic!( "{:?}", - io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type",) + io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type") ), }; diff --git a/src/clients/wayland/wlr_foreign_toplevel/manager.rs b/src/clients/wayland/wlr_foreign_toplevel/manager.rs index ed3d56a..416d0ac 100644 --- a/src/clients/wayland/wlr_foreign_toplevel/manager.rs +++ b/src/clients/wayland/wlr_foreign_toplevel/manager.rs @@ -10,6 +10,7 @@ use wayland_protocols_wlr::foreign_toplevel::v1::client::{ zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1}, }; +#[derive(Debug)] pub struct ToplevelManagerState { manager: ZwlrForeignToplevelManagerV1, _phantom: PhantomData, @@ -31,12 +32,7 @@ impl ToplevelManagerState { pub trait ToplevelManagerHandler: Sized { /// Advertises a new toplevel. - fn toplevel( - &mut self, - conn: &Connection, - qh: &QueueHandle, - manager: ToplevelManagerState, - ); + fn toplevel(&mut self, conn: &Connection, qh: &QueueHandle); } impl ProvidesBoundGlobal for ToplevelManagerState { @@ -60,7 +56,7 @@ where fn event( state: &mut D, - toplevel_manager: &ZwlrForeignToplevelManagerV1, + _toplevel_manager: &ZwlrForeignToplevelManagerV1, event: Event, _data: &GlobalData, conn: &Connection, @@ -68,14 +64,7 @@ where ) { match event { Event::Toplevel { toplevel: _ } => { - state.toplevel( - conn, - qhandle, - ToplevelManagerState { - manager: toplevel_manager.clone(), - _phantom: PhantomData, - }, - ); + state.toplevel(conn, qhandle); } Event::Finished => { warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues."); diff --git a/src/clients/wayland/wlr_foreign_toplevel/mod.rs b/src/clients/wayland/wlr_foreign_toplevel/mod.rs index f87dd76..a66df66 100644 --- a/src/clients/wayland/wlr_foreign_toplevel/mod.rs +++ b/src/clients/wayland/wlr_foreign_toplevel/mod.rs @@ -2,41 +2,62 @@ pub mod handle; pub mod manager; use self::handle::ToplevelHandleHandler; -use self::manager::{ToplevelManagerHandler, ToplevelManagerState}; -use crate::clients::wayland::Environment; +use self::manager::ToplevelManagerHandler; +use super::{Client, Environment, Event, Request, Response}; +use crate::try_send; +use tokio::sync::broadcast; use tracing::{debug, error, trace}; use wayland_client::{Connection, QueueHandle}; -use crate::send; pub use handle::{ToplevelHandle, ToplevelInfo}; #[derive(Debug, Clone)] pub enum ToplevelEvent { - New(ToplevelHandle), - Update(ToplevelHandle), - Remove(ToplevelHandle), + New(ToplevelInfo), + Update(ToplevelInfo), + Remove(ToplevelInfo), +} + +impl Client { + /// Gets the information for all currently open toplevels (windows) + pub fn toplevel_info_all(&self) -> Vec { + match self.send_request(Request::ToplevelInfoAll) { + Response::ToplevelInfoAll(infos) => infos, + _ => unreachable!(), + } + } + + /// Focuses the toplevel with the provided ID. + pub fn toplevel_focus(&self, handle_id: usize) { + match self.send_request(Request::ToplevelFocus(handle_id)) { + Response::Ok => (), + _ => unreachable!(), + } + } + + /// Subscribes to events from toplevels. + pub fn subscribe_toplevels(&self) -> broadcast::Receiver { + self.toplevel_channel.0.subscribe() + } } impl ToplevelManagerHandler for Environment { - fn toplevel( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _manager: ToplevelManagerState, - ) { + fn toplevel(&mut self, _conn: &Connection, _qh: &QueueHandle) { debug!("Manager received new handle"); } } impl ToplevelHandleHandler for Environment { fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle, handle: ToplevelHandle) { - trace!("Handler received new handle"); + debug!("Handler received new handle"); match handle.info() { Some(info) => { trace!("Adding new handle: {info:?}"); - self.handles.insert(info.id, handle.clone()); - send!(self.toplevel_tx, ToplevelEvent::New(handle)); + self.handles.push(handle.clone()); + if let Some(info) = handle.info() { + try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::New(info))); + } } None => { error!("Handle is missing information!"); @@ -55,8 +76,10 @@ impl ToplevelHandleHandler for Environment { match handle.info() { Some(info) => { trace!("Updating handle: {info:?}"); - self.handles.insert(info.id, handle.clone()); - send!(self.toplevel_tx, ToplevelEvent::Update(handle)); + self.handles.push(handle.clone()); + if let Some(info) = handle.info() { + try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Update(info))); + } } None => { error!("Handle is missing information!"); @@ -71,14 +94,10 @@ impl ToplevelHandleHandler for Environment { handle: ToplevelHandle, ) { debug!("Handler received handle close"); - match handle.info() { - Some(info) => { - self.handles.remove(&info.id); - send!(self.toplevel_tx, ToplevelEvent::Remove(handle)); - } - None => { - error!("Handle is missing information!"); - } + + self.handles.retain(|h| h != &handle); + if let Some(info) = handle.info() { + try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Remove(info))); } } } diff --git a/src/desktop_file.rs b/src/desktop_file.rs index 75dfcf0..2d10bd0 100644 --- a/src/desktop_file.rs +++ b/src/desktop_file.rs @@ -1,9 +1,8 @@ -use lazy_static::lazy_static; use std::collections::{HashMap, HashSet}; use std::env; use std::fs; use std::path::{Path, PathBuf}; -use std::sync::Mutex; +use std::sync::{Mutex, OnceLock}; use tracing::warn; use walkdir::{DirEntry, WalkDir}; @@ -11,13 +10,15 @@ use crate::lock; type DesktopFile = HashMap>; -lazy_static! { - static ref DESKTOP_FILES: Mutex> = - Mutex::new(HashMap::new()); +fn desktop_files() -> &'static Mutex> { + static DESKTOP_FILES: OnceLock>> = OnceLock::new(); + DESKTOP_FILES.get_or_init(|| Mutex::new(HashMap::new())) +} - /// These are the keys that in the cache - static ref DESKTOP_FILES_LOOK_OUT_KEYS: HashSet<&'static str> = - HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]); +fn desktop_files_look_out_keys() -> &'static HashSet<&'static str> { + static DESKTOP_FILES_LOOK_OUT_KEYS: OnceLock> = OnceLock::new(); + DESKTOP_FILES_LOOK_OUT_KEYS + .get_or_init(|| HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"])) } /// Finds directories that should contain `.desktop` files @@ -104,7 +105,7 @@ fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option Option { let app_id = &app_id.to_lowercase(); - let mut desktop_files_cache = lock!(DESKTOP_FILES); + let mut desktop_files_cache = lock!(desktop_files()); let files = files .iter() @@ -171,7 +172,7 @@ fn parse_desktop_file(path: &Path) -> Option { let key = key.trim(); let value = value.trim(); - if DESKTOP_FILES_LOOK_OUT_KEYS.contains(key) { + if desktop_files_look_out_keys().contains(key) { Some((key, value)) } else { None @@ -193,7 +194,7 @@ pub fn get_desktop_icon_name(app_id: &str) -> Option { return None; }; - let mut desktop_files_cache = lock!(DESKTOP_FILES); + let mut desktop_files_cache = lock!(desktop_files()); let desktop_file = match desktop_files_cache.get(&path) { Some(desktop_file) => desktop_file, diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 9bf7379..40f0c48 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -104,7 +104,11 @@ impl Ipc { /// Takes an input command, runs it and returns with the appropriate response. /// /// This runs on the main thread, allowing commands to interact with GTK. - fn handle_command(command: Command, application: &Application, ironbar: &Ironbar) -> Response { + fn handle_command( + command: Command, + application: &Application, + ironbar: &Rc, + ) -> Response { match command { Command::Inspect => { gtk::Window::set_interactive_debugging(true); @@ -117,7 +121,7 @@ impl Ipc { window.close(); } - *ironbar.bars.borrow_mut() = crate::load_interface(application); + *ironbar.bars.borrow_mut() = crate::load_interface(application, ironbar.clone()); Response::Ok } diff --git a/src/main.rs b/src/main.rs index 7dcb8b7..1c6a690 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::rc::Rc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; #[cfg(feature = "ipc")] use std::sync::RwLock; -use std::sync::{mpsc, Arc}; +use std::sync::{mpsc, Arc, OnceLock}; use cfg_if::cfg_if; #[cfg(feature = "cli")] @@ -26,9 +26,8 @@ use tokio::task::{block_in_place, JoinHandle}; use tracing::{debug, error, info, warn}; use universal_config::ConfigLoader; -use clients::wayland; - use crate::bar::{create_bar, Bar}; +use crate::clients::Clients; use crate::config::{Config, MonitorConfig}; use crate::error::ExitCode; #[cfg(feature = "ipc")] @@ -90,26 +89,17 @@ fn run_with_args() { } } -static COUNTER: AtomicUsize = AtomicUsize::new(1); - -lazy_static::lazy_static! { - static ref RUNTIME: Arc = Arc::new(create_runtime()); -} - -#[cfg(feature = "ipc")] -lazy_static::lazy_static! { - static ref VARIABLE_MANAGER: Arc> = arc_rw!(VariableManager::new()); -} - #[derive(Debug)] pub struct Ironbar { bars: Rc>>, + clients: Rc>, } impl Ironbar { fn new() -> Self { Self { bars: Rc::new(RefCell::new(vec![])), + clients: Rc::new(RefCell::new(Clients::new())), } } @@ -124,8 +114,8 @@ impl Ironbar { let instance = Rc::new(self); // force start wayland client ahead of ui - let wl = wayland::get_client(); - lock!(wl).roundtrip(); + let wl = instance.clients.borrow_mut().wayland(); + wl.roundtrip(); app.connect_activate(move |app| { if running.load(Ordering::Relaxed) { @@ -142,7 +132,7 @@ impl Ironbar { } } - *instance.bars.borrow_mut() = load_interface(app); + *instance.bars.borrow_mut() = load_interface(app, instance.clone()); let style_path = env::var("IRONBAR_CSS").ok().map_or_else( || { @@ -192,12 +182,14 @@ impl Ironbar { /// Gets the current Tokio runtime. #[must_use] pub fn runtime() -> Arc { - RUNTIME.clone() + static RUNTIME: OnceLock> = OnceLock::new(); + RUNTIME.get_or_init(|| Arc::new(create_runtime())).clone() } /// Gets a `usize` ID value that is unique to the entire Ironbar instance. /// This is just a static `AtomicUsize` that increments every time this function is called. pub fn unique_id() -> usize { + static COUNTER: AtomicUsize = AtomicUsize::new(1); COUNTER.fetch_add(1, Ordering::Relaxed) } @@ -205,7 +197,10 @@ impl Ironbar { #[cfg(feature = "ipc")] #[must_use] pub fn variable_manager() -> Arc> { - VARIABLE_MANAGER.clone() + static VARIABLE_MANAGER: OnceLock>> = OnceLock::new(); + VARIABLE_MANAGER + .get_or_init(|| arc_rw!(VariableManager::new())) + .clone() } /// Gets a clone of a bar by its unique name. @@ -228,7 +223,7 @@ fn start_ironbar() { } /// Loads the Ironbar config and interface. -pub fn load_interface(app: &Application) -> Vec { +pub fn load_interface(app: &Application, ironbar: Rc) -> Vec { let display = Display::default().map_or_else( || { let report = Report::msg("Failed to get default GTK display"); @@ -264,7 +259,7 @@ pub fn load_interface(app: &Application) -> Vec { } } - match create_bars(app, &display, &config) { + match create_bars(app, &display, &config, &ironbar) { Ok(bars) => { debug!("Created {} bars", bars.len()); bars @@ -277,9 +272,14 @@ pub fn load_interface(app: &Application) -> Vec { } /// Creates each of the bars across each of the (configured) outputs. -fn create_bars(app: &Application, display: &Display, config: &Config) -> Result> { - let wl = wayland::get_client(); - let outputs = lock!(wl).get_outputs(); +fn create_bars( + app: &Application, + display: &Display, + config: &Config, + ironbar: &Rc, +) -> Result> { + let wl = ironbar.clients.borrow_mut().wayland(); + let outputs = wl.output_info_all(); debug!("Received {} outputs from Wayland", outputs.len()); debug!("Outputs: {:?}", outputs); @@ -313,17 +313,27 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result< &monitor, monitor_name.to_string(), config.clone(), + ironbar.clone(), )?] } Some(MonitorConfig::Multiple(configs)) => configs .iter() - .map(|config| create_bar(app, &monitor, monitor_name.to_string(), config.clone())) + .map(|config| { + create_bar( + app, + &monitor, + monitor_name.to_string(), + config.clone(), + ironbar.clone(), + ) + }) .collect::>()?, None if show_default_bar => vec![create_bar( app, &monitor, monitor_name.to_string(), config.clone(), + ironbar.clone(), )?], None => vec![], }; @@ -364,11 +374,8 @@ where /// This is not an `async` operation /// so can be used outside of an async function. /// -/// Do note it must be called from within a Tokio runtime still. -/// -/// Use sparingly! Prefer async functions wherever possible. -/// -/// TODO: remove all instances of this once async trait funcs are stable +/// Use sparingly, as this risks blocking the UI thread! +/// Prefer async functions wherever possible. pub fn await_sync(f: F) -> F::Output { block_in_place(|| Ironbar::runtime().block_on(f)) } diff --git a/src/modules/clipboard.rs b/src/modules/clipboard.rs index c7ef2bf..9749df6 100644 --- a/src/modules/clipboard.rs +++ b/src/modules/clipboard.rs @@ -49,7 +49,7 @@ const fn default_max_items() -> usize { #[derive(Debug, Clone)] pub enum ControllerEvent { - Add(usize, Arc), + Add(usize, ClipboardItem), Remove(usize), Activate(usize), Deactivate, @@ -72,22 +72,22 @@ impl Module