1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-04-19 19:34:24 +02:00

refactor: major client code changes

This does away with `lazy_static` singletons for all the clients, instead putting them all inside a `Clients` struct on the `Ironbar` struct.

Client code has been refactored in places to accommodate this, and module code has been updated to get the clients the new way.

The Wayland client has been re-written from the ground up to remove a lot of the needless complications, provide a nicer interface and reduce some duplicate data.

The MPD music client has been overhauled to use the `mpd_utils` crate, which simplifies the code within Ironbar and should offer more robustness and better recovery when connection is lost to the server.

The launcher module in particular has been affected by the refactor.
This commit is contained in:
Jake Stanger 2024-01-07 23:50:10 +00:00
parent 57b57ed002
commit c702f6fffa
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
33 changed files with 1081 additions and 1063 deletions

158
Cargo.lock generated
View file

@ -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,8 +235,8 @@ 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]]
@ -478,8 +478,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 +662,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 +680,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 +703,7 @@ dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote 1.0.32",
"quote 1.0.35",
"strsim",
"syn 1.0.109",
]
@ -715,7 +715,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 +737,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 +760,7 @@ checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
"convert_case",
"proc-macro2",
"quote 1.0.32",
"quote 1.0.35",
"rustc_version",
"syn 1.0.109",
]
@ -839,7 +839,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 +860,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 +980,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 +1017,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 +1049,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 +1067,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 +1251,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 +1352,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 +1523,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]]
@ -1650,15 +1650,14 @@ dependencies = [
"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 +1878,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 +2072,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 +2217,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 +2314,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 +2326,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 +2362,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 +2678,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 +2700,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 +2886,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 +2934,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 +3049,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 +3146,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 +3266,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 +3478,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 +3533,7 @@ dependencies = [
"log",
"once_cell",
"proc-macro2",
"quote 1.0.32",
"quote 1.0.35",
"syn 1.0.109",
"wasm-bindgen-shared",
]
@ -3544,7 +3556,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 +3567,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 +3660,7 @@ checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c"
dependencies = [
"proc-macro2",
"quick-xml",
"quote 1.0.32",
"quote 1.0.35",
]
[[package]]
@ -4019,7 +4031,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 +4070,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 +4082,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",
]

View file

@ -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"]
@ -115,7 +115,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 +126,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

View file

@ -27,6 +27,8 @@ pub struct Bar {
monitor_name: String,
position: BarPosition,
ironbar: Rc<Ironbar>,
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<Ironbar>,
) -> 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: &gtk::Box,
modules: Vec<ModuleConfig>,
info: &ModuleInfo,
ironbar: &Rc<Ironbar>,
popup: &Rc<RefCell<Popup>>,
) -> 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<Ironbar>,
) -> Result<Bar> {
let bar = Bar::new(app, monitor_name, config);
let bar = Bar::new(app, monitor_name, config, ironbar);
bar.init(monitor)
}

View file

@ -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<ClipboardItem>),
Add(ClipboardItem),
Remove(usize),
Activate(usize),
}
@ -18,13 +17,16 @@ type EventSender = mpsc::Sender<ClipboardEvent>;
/// Clipboard client singleton,
/// to ensure bars don't duplicate requests to the compositor.
pub struct ClipboardClient {
#[derive(Debug)]
pub struct Client {
wayland: Arc<wayland::Client>,
senders: Arc<Mutex<Vec<(EventSender, usize)>>>,
cache: Arc<Mutex<ClipboardCache>>,
}
impl ClipboardClient {
fn new() -> Self {
impl Client {
pub(crate) fn new(wl: Arc<wayland::Client>) -> 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<ClipboardEvent> {
@ -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, (Arc<ClipboardItem>, usize)>,
cache: IndexMap<usize, (ClipboardItem, usize)>,
}
impl ClipboardCache {
@ -162,12 +164,12 @@ impl ClipboardCache {
}
/// Gets the entry with key `id` from the cache.
fn get(&self, id: usize) -> Option<Arc<ClipboardItem>> {
fn get(&self, id: usize) -> Option<ClipboardItem> {
self.cache.get(&id).map(|(item, _)| item).cloned()
}
/// Inserts an entry with `ref_count` initial references.
fn insert(&mut self, item: Arc<ClipboardItem>, ref_count: usize) -> Option<Arc<ClipboardItem>> {
fn insert(&mut self, item: ClipboardItem, ref_count: usize) -> Option<ClipboardItem> {
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<Arc<ClipboardItem>> {
fn remove(&mut self, id: usize) -> Option<ClipboardItem> {
self.cache.shift_remove(&id).map(|(item, _)| item)
}
@ -224,15 +226,9 @@ impl ClipboardCache {
self.cache.len()
}
fn iter(&self) -> Iter<'_, usize, (Arc<ClipboardItem>, 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);

View file

@ -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<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>,
}
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::<i32>() {
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,

View file

@ -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<Arc<dyn WorkspaceClient + Send + Sync>> {
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<dyn WorkspaceClient + Send + Sync>),
#[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<WorkspaceUpdate>;
}
register_client!(dyn WorkspaceClient, workspaces);

View file

@ -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<Mutex<Connection>>,
workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>,
}
impl SwayEventClient {
fn new() -> Self {
impl Client {
pub(crate) async fn new() -> Result<Self> {
// 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);
{
let workspace_tx = workspace_tx.clone();
spawn(async move {
// create 2nd client as subscription takes ownership
let client = Connection::new().await?;
info!("Sway IPC subscription client connected");
let workspace_tx = workspace_tx.clone();
spawn(async move {
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<Arc<Mutex<Connection>>> = 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<Mutex<Connection>> {
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<Node> for Workspace {
fn from(node: Node) -> Self {
let visibility = Visibility::from(&node);

View file

@ -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<Arc<wayland::Client>>,
#[cfg(feature = "workspaces")]
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "tray")]
tray: Option<Arc<system_tray::TrayEventReceiver>>,
#[cfg(feature = "upower")]
upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>,
}
impl Clients {
pub(crate) fn new() -> Self {
Self::default()
}
pub fn wayland(&mut self) -> Arc<wayland::Client> {
self.wayland
.get_or_insert_with(|| Arc::new(wayland::Client::new()))
.clone()
}
#[cfg(feature = "clipboard")]
pub fn clipboard(&mut self) -> Arc<clipboard::Client> {
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<dyn compositor::WorkspaceClient> {
// 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<dyn music::MusicClient> {
self.music
.entry(client_type.clone())
.or_insert_with(|| music::create_client(client_type))
.clone()
}
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> Arc<system_tray::TrayEventReceiver> {
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<zbus::fdo::PropertiesProxy<'static>> {
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<T: ?Sized> {
/// Returns a singleton client instance of type `T`.
fn provide(&self) -> Arc<T>;
}
/// 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<TSend, TReceive> $crate::clients::ProvidesClient<$ty>
for $crate::modules::WidgetContext<TSend, TReceive>
where
TSend: Clone,
{
fn provide(&self) -> Arc<$ty> {
self.ironbar.clients.borrow_mut().$method()
}
}
};
}

View file

@ -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<Duration>,
}
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<PlayerUpdate>;
}
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<Arc<dyn MusicClient>> {
pub fn create_client(client_type: ClientType) -> Arc<dyn MusicClient> {
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()),
}
}

View file

@ -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<Mutex<HashMap<String, Arc<MpdClient>>>> =
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<PersistentClient>,
music_dir: PathBuf,
tx: broadcast::Sender<PlayerUpdate>,
_rx: broadcast::Receiver<PlayerUpdate>,
}
#[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<Self, MpdConnectionError> {
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<Track>, 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<PlayerUpdate>,
music_dir: &Path,
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
@ -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<PlayerUpdate>) {
async fn send_tick_update(client: &PersistentClient, tx: &broadcast::Sender<PlayerUpdate>) {
let status = client.command(commands::Status).await;
if let Ok(status) = status {
@ -136,18 +117,45 @@ impl MpdClient {
}
}
}
}
fn is_connected(&self) -> bool {
!self.client.is_connection_closed()
impl MusicClient for Client {
fn play(&self) -> Result<()> {
command!(self, commands::SetPause(false))
}
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
info!("Connection to MPD server lost");
self.tx.send(PlayerUpdate::Disconnect)?;
Ok(())
fn pause(&self) -> Result<()> {
command!(self, commands::SetPause(true))
}
fn convert_song(song: &Song, music_dir: &Path) -> Track {
fn next(&self) -> Result<()> {
command!(self, commands::Next)
}
fn prev(&self) -> Result<()> {
command!(self, commands::Previous)
}
fn set_volume_percent(&self, vol: u8) -> Result<()> {
command!(self, commands::SetVolume(vol))
}
fn seek(&self, duration: Duration) -> Result<()> {
command!(self, commands::Seek(SeekMode::Absolute(duration)))
}
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
let rx = self.tx.subscribe();
await_sync(async move {
Self::send_update(&self.client, &self.tx, &self.music_dir)
.await
.expect("to be able to send update");
});
rx
}
}
fn convert_song(song: &Song, music_dir: &Path) -> Track {
let (track, disc) = song.number();
let cover_path = music_dir
@ -162,155 +170,15 @@ impl MpdClient {
.ok();
Track {
title: song.title().map(std::string::ToString::to_string),
album: song.album().map(std::string::ToString::to_string),
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(std::string::ToString::to_string),
genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string),
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,
}
}
}
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 {
fn play(&self) -> Result<()> {
async_command!(self.client, commands::SetPause(false));
Ok(())
}
fn pause(&self) -> Result<()> {
async_command!(self.client, commands::SetPause(true));
Ok(())
}
fn next(&self) -> Result<()> {
async_command!(self.client, commands::Next);
Ok(())
}
fn prev(&self) -> Result<()> {
async_command!(self.client, commands::Previous);
Ok(())
}
fn set_volume_percent(&self, vol: u8) -> Result<()> {
async_command!(self.client, commands::SetVolume(vol));
Ok(())
}
fn seek(&self, duration: Duration) -> Result<()> {
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
Ok(())
}
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
let rx = self.tx.subscribe();
await_sync(async {
Self::send_update(&self.client, &self.tx, &self.music_dir)
.await
.expect("Failed to send player update");
});
rx
}
}
pub async fn get_client(
host: &str,
music_dir: PathBuf,
) -> Result<Arc<MpdClient>, 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");
let client = MpdClient::new(host, music_dir).await?;
let client = Arc::new(client);
connections.insert(host.to_string(), Arc::clone(&client));
Ok(client)
}
}
}
}
async fn wait_for_connection(
host: &str,
interval: Duration,
max_retries: Option<usize>,
) -> Result<Connection, MpdConnectionError> {
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<Connection, MpdProtocolError> {
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<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host).await?;
Client::connect(connection).await
}
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host).await?;
Client::connect(connection).await
}
/// Attempts to read the first value for a tag

View file

@ -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<Client> = Arc::new(Client::new());
}
#[derive(Debug)]
pub struct Client {
current_player: Arc<Mutex<Option<String>>>,
tx: broadcast::Sender<PlayerUpdate>,
@ -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> {
CLIENT.clone()
}
impl From<Metadata> for Track {
fn from(value: Metadata) -> Self {
const KEY_DATE: &str = "xesam:contentCreated";
@ -301,11 +293,11 @@ impl From<Metadata> 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<Metadata> 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),
}
}
}

View file

@ -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<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
#[derive(Debug)]
pub struct TrayEventReceiver {
tx: mpsc::Sender<NotifierItemCommand>,
b_tx: broadcast::Sender<NotifierItemMessage>,
@ -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,8 +93,9 @@ impl TrayEventReceiver {
}
}
lazy_static! {
static ref CLIENT: AsyncOnce<TrayEventReceiver> = AsyncOnce::new(async {
/// 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
@ -108,7 +108,12 @@ lazy_static! {
match tray {
Ok(tray) => break Some(tray),
Err(err) => error!("{:?}", Report::new(err).wrap_err(format!("Failed to create StatusNotifierWatcher (attempt {retries})")))
Err(err) => error!(
"{:?}",
Report::new(err).wrap_err(format!(
"Failed to create StatusNotifierWatcher (attempt {retries})"
))
),
}
if retries == MAX_RETRIES {
@ -117,9 +122,6 @@ lazy_static! {
};
value.expect("Failed to create StatusNotifierWatcher")
});
}
pub async fn get_tray_event_client() -> &'static TrayEventReceiver {
CLIENT.get().await
}
register_client!(TrayEventReceiver, tray);

View file

@ -1,11 +1,9 @@
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<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
pub async fn create_display_proxy() -> Arc<PropertiesProxy<'static>> {
let dbus = Box::pin(zbus::Connection::system())
.await
.expect("failed to create connection to system bus");
@ -32,9 +30,6 @@ lazy_static! {
.expect("failed to build proxy");
Arc::new(proxy)
});
}
pub async fn get_display_proxy() -> &'static PropertiesProxy<'static> {
DISPLAY_PROXY.get().await
}
register_client!(PropertiesProxy<'static>, upower);

View file

@ -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<ClipboardItem>),
/// Forces a dispatch, flushing any currently queued events
Roundtrip,
}
pub struct WaylandClient {
// External channels
toplevel_tx: broadcast::Sender<ToplevelEvent>,
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
#[cfg(feature = "clipboard")]
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
#[cfg(feature = "clipboard")]
_clipboard_rx: broadcast::Receiver<Arc<ClipboardItem>>,
// Internal channels
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
output_rx: mpsc::Receiver<Vec<OutputInfo>>,
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
#[cfg(feature = "clipboard")]
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
request_tx: Sender<Request>,
}
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::<Request>();
// `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::<Environment>::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<ToplevelEvent>,
HashMap<usize, ToplevelHandle>,
) {
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<Arc<ClipboardItem>>,
Option<Arc<ClipboardItem>>,
) {
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<OutputInfo> {
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<WlSeat> {
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<ClipboardItem>) {
send!(self.request_tx, Request::CopyToClipboard(item));
}
}

View file

@ -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<WlSeat>,
#[cfg(feature = "clipboard")]
pub data_control_devices: Vec<DataControlDeviceEntry>,
#[cfg(feature = "clipboard")]
pub selection_offers: Vec<SelectionOfferItem>,
#[cfg(feature = "clipboard")]
pub copy_paste_sources: Vec<CopyPasteSource>,
pub handles: HashMap<usize, ToplevelHandle>,
#[cfg(feature = "clipboard")]
clipboard: Arc<Mutex<Option<Arc<ClipboardItem>>>>,
toplevel_tx: broadcast::Sender<ToplevelEvent>,
#[cfg(feature = "clipboard")]
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
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<OutputInfo>),
OutputInfoAll(Vec<OutputInfo>),
ToplevelInfo(Option<ToplevelInfo>),
ToplevelInfoAll(Vec<ToplevelInfo>),
#[cfg(feature = "clipboard")]
ClipboardItem(Option<ClipboardItem>),
Seat(WlSeat),
}
#[derive(Debug)]
struct BroadcastChannel<T>(broadcast::Sender<T>, Arc<Mutex<broadcast::Receiver<T>>>);
impl<T> From<(broadcast::Sender<T>, broadcast::Receiver<T>)> for BroadcastChannel<T> {
fn from(value: (broadcast::Sender<T>, broadcast::Receiver<T>)) -> Self {
BroadcastChannel(value.0, arc_mut!(value.1))
}
}
#[derive(Debug)]
pub struct Client {
tx: calloop_channel::Sender<Request>,
rx: Arc<Mutex<std::sync::mpsc::Receiver<Response>>>,
output_channel: BroadcastChannel<OutputEvent>,
toplevel_channel: BroadcastChannel<ToplevelEvent>,
#[cfg(feature = "clipboard")]
clipboard_channel: BroadcastChannel<ClipboardItem>,
}
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<Self>,
loop_handle: LoopHandle<'static, Self>,
event_tx: mpsc::Sender<Event>,
response_tx: std::sync::mpsc::Sender<Response>,
// local state
handles: Vec<ToplevelHandle>,
// -- clipboard --
#[cfg(feature = "clipboard")]
data_control_device_manager_state: DataControlDeviceManagerState,
#[cfg(feature = "clipboard")]
data_control_devices: Vec<DataControlDeviceEntry>,
#[cfg(feature = "clipboard")]
copy_paste_sources: Vec<CopyPasteSource>,
#[cfg(feature = "clipboard")]
selection_offers: Vec<SelectionOfferItem>,
// local state
#[cfg(feature = "clipboard")]
clipboard: Arc<Mutex<Option<ClipboardItem>>>,
}
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<Event>,
request_rx: calloop_channel::Channel<Request>,
response_tx: std::sync::mpsc::Sender<Response>,
) {
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::<Self>::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<Request>, _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<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new());
}
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
CLIENT.clone()
}
register_client!(Client, wayland);

View file

@ -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<OutputInfo> {
match self.send_request(Request::OutputInfoAll) {
Response::OutputInfoAll(info) => info,
_ => unreachable!(),
}
}
/// Subscribes to events from outputs.
pub fn subscribe_outputs(&self) -> broadcast::Receiver<OutputEvent> {
self.output_channel.0.subscribe()
}
}
impl Environment {
pub fn output_info(&mut self) -> Vec<OutputInfo> {
pub fn output_info_all(&mut self) -> Vec<OutputInfo> {
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<Self>,
_output: wl_output::WlOutput,
) {
fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, 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<Self>,
_output: wl_output::WlOutput,
) {
fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, 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<Self>,
_output: wl_output::WlOutput,
) {
fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, 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!");
}
}
}

View file

@ -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<Self>, seat: wl_seat::WlSeat) {
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _seat: WlSeat) {
debug!("Handler received new seat");
self.seats.push(seat);
}
fn new_capability(
&mut self,
_: &Connection,
qh: &QueueHandle<Self>,
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<Self>,
_: 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<Self>, seat: wl_seat::WlSeat) {
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _seat: WlSeat) {
debug!("Handler received seat removal");
self.seats.retain(|s| s != &seat);
}
}

View file

@ -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,
}

View file

@ -14,6 +14,7 @@ use wayland_protocols_wlr::data_control::v1::client::{
zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
};
#[derive(Debug)]
pub struct DataControlDeviceManagerState<V = DataControlOfferData> {
manager: ZwlrDataControlManagerV1,
_phantom: PhantomData<V>,

View file

@ -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<RegistrationToken>,
}
/// 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<ClipboardValue>,
pub mime_type: Arc<str>,
}
impl PartialEq<Self> for ClipboardItem {
@ -108,24 +114,60 @@ impl MimeType {
}
}
impl Client {
/// Gets the current clipboard item,
/// if this exists and Ironbar has record of it.
pub fn clipboard_item(&self) -> Option<ClipboardItem> {
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<ClipboardItem> {
self.clipboard_channel.0.subscribe()
}
}
impl Environment {
pub fn copy_to_clipboard(&mut self, item: Arc<ClipboardItem>, qh: &QueueHandle<Self>) {
/// 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:?}");
// TODO: Proper device tracking
let device = self.data_control_devices.first();
if let Some(device) = device {
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(qh, [INTERNAL_MIME_TYPE, item.mime_type.as_str()]);
.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<ClipboardItem> {
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")
),
};

View file

@ -10,6 +10,7 @@ use wayland_protocols_wlr::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
};
#[derive(Debug)]
pub struct ToplevelManagerState<V = ToplevelHandleData> {
manager: ZwlrForeignToplevelManagerV1,
_phantom: PhantomData<V>,
@ -31,12 +32,7 @@ impl ToplevelManagerState {
pub trait ToplevelManagerHandler: Sized {
/// Advertises a new toplevel.
fn toplevel(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
manager: ToplevelManagerState,
);
fn toplevel(&mut self, conn: &Connection, qh: &QueueHandle<Self>);
}
impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> 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.");

View file

@ -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<ToplevelInfo> {
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<ToplevelEvent> {
self.toplevel_channel.0.subscribe()
}
}
impl ToplevelManagerHandler for Environment {
fn toplevel(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_manager: ToplevelManagerState,
) {
fn toplevel(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>) {
debug!("Manager received new handle");
}
}
impl ToplevelHandleHandler for Environment {
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, 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)));
}
}
}

View file

@ -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<Ironbar>,
) -> 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
}

View file

@ -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")]
@ -104,12 +103,14 @@ lazy_static::lazy_static! {
#[derive(Debug)]
pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>,
clients: Rc<RefCell<Clients>>,
}
impl Ironbar {
fn new() -> Self {
Self {
bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())),
}
}
@ -124,8 +125,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 +143,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(
|| {
@ -228,7 +229,7 @@ fn start_ironbar() {
}
/// Loads the Ironbar config and interface.
pub fn load_interface(app: &Application) -> Vec<Bar> {
pub fn load_interface(app: &Application, ironbar: Rc<Ironbar>) -> Vec<Bar> {
let display = Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
@ -264,7 +265,7 @@ pub fn load_interface(app: &Application) -> Vec<Bar> {
}
}
match create_bars(app, &display, &config) {
match create_bars(app, &display, &config, &ironbar) {
Ok(bars) => {
debug!("Created {} bars", bars.len());
bars
@ -277,9 +278,14 @@ pub fn load_interface(app: &Application) -> Vec<Bar> {
}
/// Creates each of the bars across each of the (configured) outputs.
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<Vec<Bar>> {
let wl = wayland::get_client();
let outputs = lock!(wl).get_outputs();
fn create_bars(
app: &Application,
display: &Display,
config: &Config,
ironbar: &Rc<Ironbar>,
) -> Result<Vec<Bar>> {
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 +319,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::<Result<_>>()?,
None if show_default_bar => vec![create_bar(
app,
&monitor,
monitor_name.to_string(),
config.clone(),
ironbar.clone(),
)?],
None => vec![],
};
@ -364,11 +380,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: Future>(f: F) -> F::Output {
block_in_place(|| Ironbar::runtime().block_on(f))
}

View file

@ -49,7 +49,7 @@ const fn default_max_items() -> usize {
#[derive(Debug, Clone)]
pub enum ControllerEvent {
Add(usize, Arc<ClipboardItem>),
Add(usize, ClipboardItem),
Remove(usize),
Activate(usize),
Deactivate,
@ -78,17 +78,16 @@ impl Module<Button> for ClipboardModule {
let max_items = self.max_items;
let tx = context.tx.clone();
let client: Arc<clipboard::Client> = context.client();
// listen to clipboard events
spawn(async move {
let mut rx = {
let client = clipboard::get_client();
client.subscribe(max_items)
};
let mut rx = client.subscribe(max_items);
while let Some(event) = rx.recv().await {
match event {
ClipboardEvent::Add(item) => {
let msg = match &item.value {
let msg = match item.value.as_ref() {
ClipboardValue::Other => {
ModuleUpdateEvent::Update(ControllerEvent::Deactivate)
}
@ -108,10 +107,11 @@ impl Module<Button> for ClipboardModule {
error!("Clipboard client unexpectedly closed");
});
let client = context.client::<clipboard::Client>();
// listen to ui events
spawn(async move {
while let Some(event) = rx.recv().await {
let client = clipboard::get_client();
match event {
UIEvent::Copy(id) => client.copy(id),
UIEvent::Remove(id) => client.remove(id),
@ -172,7 +172,7 @@ impl Module<Button> for ClipboardModule {
let row = gtk::Box::new(Orientation::Horizontal, 0);
row.style_context().add_class("item");
let button = match &item.value {
let button = match item.value.as_ref() {
ClipboardValue::Text(value) => {
let button = RadioButton::from_widget(&hidden_option);

View file

@ -3,12 +3,12 @@ use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, lock, send_async, spawn, try_send};
use crate::{glib_recv, send_async, spawn, try_send};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::mpsc;
use tracing::debug;
#[derive(Debug, Deserialize, Clone)]
@ -58,21 +58,16 @@ impl Module<gtk::Box> for FocusedModule {
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: Receiver<Self::ReceiveMessage>,
_rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let tx = context.tx.clone();
spawn(async move {
let (mut wlrx, handles) = {
let wl = wayland::get_client();
let wl = lock!(wl);
wl.subscribe_toplevels()
};
let wl = context.client::<wayland::Client>();
let focused = handles.values().find_map(|handle| {
handle
.info()
.and_then(|info| if info.focused { Some(info) } else { None })
});
spawn(async move {
let mut wlrx = wl.subscribe_toplevels();
let handles = wl.toplevel_info_all();
let focused = handles.into_iter().find(|info| info.focused);
if let Some(focused) = focused {
try_send!(
@ -83,9 +78,7 @@ impl Module<gtk::Box> for FocusedModule {
while let Ok(event) = wlrx.recv().await {
match event {
ToplevelEvent::Update(handle) => {
let info = handle.info().unwrap_or_default();
ToplevelEvent::Update(info) => {
if info.focused {
debug!("Changing focus");
send_async!(
@ -99,8 +92,7 @@ impl Module<gtk::Box> for FocusedModule {
send_async!(tx, ModuleUpdateEvent::Update(None));
}
}
ToplevelEvent::Remove(handle) => {
let info = handle.info().unwrap_or_default();
ToplevelEvent::Remove(info) => {
if info.focused {
debug!("Clearing focus");
send_async!(tx, ModuleUpdateEvent::Update(None));

View file

@ -1,12 +1,11 @@
use super::open_state::OpenState;
use crate::clients::wayland::ToplevelHandle;
use crate::clients::wayland::ToplevelInfo;
use crate::config::BarPosition;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent;
use crate::{read_lock, try_send};
use color_eyre::{Report, Result};
use glib::Propagation;
use gtk::prelude::*;
use gtk::{Button, IconTheme};
@ -15,7 +14,6 @@ use std::rc::Rc;
use std::sync::RwLock;
use tokio::sync::mpsc::Sender;
use tracing::error;
use wayland_client::protocol::wl_seat::WlSeat;
#[derive(Debug, Clone)]
pub struct Item {
@ -38,31 +36,25 @@ impl Item {
}
/// Merges the provided node into this launcher item
pub fn merge_toplevel(&mut self, handle: ToplevelHandle) -> Result<Window> {
let info = handle
.info()
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
pub fn merge_toplevel(&mut self, info: ToplevelInfo) -> Window {
let id = info.id;
if self.windows.is_empty() {
self.name = info.title;
self.name = info.title.clone();
}
let window = Window::try_from(handle)?;
let window = Window::from(info);
self.windows.insert(id, window.clone());
self.recalculate_open_state();
Ok(window)
window
}
pub fn unmerge_toplevel(&mut self, handle: &ToplevelHandle) {
if let Some(info) = handle.info() {
pub fn unmerge_toplevel(&mut self, info: &ToplevelInfo) {
self.windows.remove(&info.id);
self.recalculate_open_state();
}
}
pub fn set_window_name(&mut self, window_id: usize, name: String) {
if let Some(window) = self.windows.get_mut(&window_id) {
@ -97,29 +89,24 @@ impl Item {
}
}
impl TryFrom<ToplevelHandle> for Item {
type Error = Report;
fn try_from(handle: ToplevelHandle) -> std::result::Result<Self, Self::Error> {
let info = handle
.info()
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
impl From<ToplevelInfo> for Item {
fn from(info: ToplevelInfo) -> Self {
let id = info.id;
let name = info.title.clone();
let app_id = info.app_id.clone();
let open_state = OpenState::from(&info);
let mut windows = IndexMap::new();
let window = Window::try_from(handle)?;
windows.insert(info.id, window);
let window = Window::from(info);
windows.insert(id, window);
Ok(Self {
Self {
app_id,
favorite: false,
open_state,
windows,
name,
})
}
}
}
@ -128,30 +115,17 @@ pub struct Window {
pub id: usize,
pub name: String,
pub open_state: OpenState,
handle: ToplevelHandle,
}
impl TryFrom<ToplevelHandle> for Window {
type Error = Report;
fn try_from(handle: ToplevelHandle) -> Result<Self, Self::Error> {
let info = handle
.info()
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
impl From<ToplevelInfo> for Window {
fn from(info: ToplevelInfo) -> Self {
let open_state = OpenState::from(&info);
Ok(Self {
Self {
id: info.id,
name: info.title,
open_state,
handle,
})
}
}
impl Window {
pub fn focus(&self, seat: &WlSeat) {
self.handle.focus(seat);
}
}

View file

@ -1,15 +1,12 @@
mod item;
mod open_state;
use self::item::{Item, ItemButton, Window};
use self::item::{AppearanceOptions, Item, ItemButton, Window};
use self::open_state::OpenState;
use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext};
use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file;
use crate::modules::launcher::item::AppearanceOptions;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{arc_mut, glib_recv, lock, send_async, spawn, try_send, write_lock};
use color_eyre::{Help, Report};
use gtk::prelude::*;
@ -112,27 +109,26 @@ impl Module<gtk::Box> for LauncherModule {
let items2 = Arc::clone(&items);
let tx = context.tx.clone();
let tx2 = context.tx.clone();
let wl = context.client::<wayland::Client>();
spawn(async move {
let items = items2;
let tx = tx2;
let (mut wlrx, handles) = {
let wl = wayland::get_client();
let wl = lock!(wl);
wl.subscribe_toplevels()
};
for handle in handles.values() {
let Some(info) = handle.info() else { continue };
let mut wlrx = wl.subscribe_toplevels();
let handles = wl.toplevel_info_all();
for info in handles {
let mut items = lock!(items);
let item = items.get_mut(&info.app_id);
match item {
Some(item) => {
item.merge_toplevel(handle.clone())?;
item.merge_toplevel(info.clone());
}
None => {
items.insert(info.app_id.clone(), Item::try_from(handle.clone())?);
items.insert(info.app_id.clone(), Item::from(info.clone()));
}
}
}
@ -154,22 +150,22 @@ impl Module<gtk::Box> for LauncherModule {
trace!("event: {:?}", event);
match event {
ToplevelEvent::New(handle) => {
let Some(info) = handle.info() else { continue };
ToplevelEvent::New(info) => {
let app_id = info.app_id.clone();
let new_item = {
let mut items = lock!(items);
let item = items.get_mut(&info.app_id);
match item {
None => {
let item: Item = handle.try_into()?;
let item: Item = info.into();
items.insert(info.app_id.clone(), item.clone());
items.insert(app_id.clone(), item.clone());
ItemOrWindow::Item(item)
}
Some(item) => {
let window = item.merge_toplevel(handle)?;
let window = item.merge_toplevel(info);
ItemOrWindow::Window(window)
}
}
@ -180,14 +176,11 @@ impl Module<gtk::Box> for LauncherModule {
send_update(LauncherUpdate::AddItem(item)).await
}
ItemOrWindow::Window(window) => {
send_update(LauncherUpdate::AddWindow(info.app_id.clone(), window))
.await
send_update(LauncherUpdate::AddWindow(app_id, window)).await
}
}?;
}
ToplevelEvent::Update(handle) => {
let Some(info) = handle.info() else { continue };
ToplevelEvent::Update(info) => {
if let Some(item) = lock!(items).get_mut(&info.app_id) {
item.set_window_focused(info.id, info.focused);
item.set_window_name(info.id, info.title.clone());
@ -202,15 +195,13 @@ impl Module<gtk::Box> for LauncherModule {
))
.await?;
}
ToplevelEvent::Remove(handle) => {
let Some(info) = handle.info() else { continue };
ToplevelEvent::Remove(info) => {
let remove_item = {
let mut items = lock!(items);
let item = items.get_mut(&info.app_id);
match item {
Some(item) => {
item.unmerge_toplevel(&handle);
item.unmerge_toplevel(&info);
if item.windows.is_empty() {
items.remove(&info.app_id);
@ -245,6 +236,7 @@ impl Module<gtk::Box> for LauncherModule {
});
// listen to ui events
let wl = context.client::<wayland::Client>();
spawn(async move {
while let Some(event) = rx.recv().await {
if let ItemEvent::OpenItem(app_id) = event {
@ -272,37 +264,29 @@ impl Module<gtk::Box> for LauncherModule {
} else {
send_async!(tx, ModuleUpdateEvent::ClosePopup);
let wl = wayland::get_client();
let items = lock!(items);
let id = match event {
ItemEvent::FocusItem(app_id) => items.get(&app_id).and_then(|item| {
ItemEvent::FocusItem(app_id) => {
lock!(items).get(&app_id).and_then(|item| {
item.windows
.iter()
.find(|(_, win)| !win.open_state.is_focused())
.or_else(|| item.windows.first())
.map(|(_, win)| win.id)
}),
})
}
ItemEvent::FocusWindow(id) => Some(id),
ItemEvent::OpenItem(_) => unreachable!(),
};
if let Some(id) = id {
if let Some(window) =
items.iter().find_map(|(_, item)| item.windows.get(&id))
if let Some(window) = lock!(items)
.iter()
.find_map(|(_, item)| item.windows.get(&id))
{
debug!("Focusing window {id}: {}", window.name);
let seat = lock!(wl)
.get_seats()
.pop()
.expect("Failed to get Wayland seat");
window.focus(&seat);
wl.toplevel_focus(window.id);
}
}
// roundtrip to immediately send activate event
lock!(wl).roundtrip();
}
}
});

View file

@ -1,6 +1,7 @@
use std::cell::RefCell;
use std::fmt::Debug;
use std::rc::Rc;
use std::sync::Arc;
use color_eyre::Result;
use glib::IsA;
@ -10,10 +11,11 @@ use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widge
use tokio::sync::{broadcast, mpsc};
use tracing::debug;
use crate::clients::ProvidesClient;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{glib_recv_mpsc, send};
use crate::{glib_recv_mpsc, send, Ironbar};
#[cfg(feature = "clipboard")]
pub mod clipboard;
@ -76,6 +78,7 @@ where
TSend: Clone,
{
pub id: usize,
pub ironbar: Rc<Ironbar>,
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
pub update_tx: broadcast::Sender<TSend>,
pub controller_tx: mpsc::Sender<TReceive>,
@ -87,6 +90,18 @@ impl<TSend, TReceive> WidgetContext<TSend, TReceive>
where
TSend: Clone,
{
/// Gets client `T` from the context.
///
/// This is a shorthand to avoid needing to go through
/// `context.ironbar.clients`.
pub fn client<T: ?Sized>(&self) -> Arc<T>
where
WidgetContext<TSend, TReceive>: ProvidesClient<T>,
{
ProvidesClient::provide(self)
}
/// Subscribes to events sent from this widget.
pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
self.update_tx.subscribe()
}
@ -194,6 +209,7 @@ where
pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule,
id: usize,
ironbar: Rc<Ironbar>,
name: Option<String>,
info: &ModuleInfo,
popup: &Rc<RefCell<Popup>>,
@ -210,6 +226,7 @@ where
let context = WidgetContext {
id,
ironbar,
tx: ui_tx,
update_tx: tx.clone(),
controller_tx,

View file

@ -1,3 +1,4 @@
use std::cell::RefMut;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@ -14,6 +15,7 @@ use tracing::error;
use crate::clients::music::{
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
};
use crate::clients::Clients;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::PopupButton;
@ -67,16 +69,18 @@ pub struct SongUpdate {
display_string: String,
}
async fn get_client(
fn get_client(
mut clients: RefMut<'_, Clients>,
player_type: PlayerType,
host: &str,
host: String,
music_dir: PathBuf,
) -> Box<Arc<dyn MusicClient>> {
match player_type {
PlayerType::Mpd => music::get_client(music::ClientType::Mpd { host, music_dir }),
PlayerType::Mpris => music::get_client(music::ClientType::Mpris {}),
}
.await
) -> Arc<dyn MusicClient> {
let client_type = match player_type {
PlayerType::Mpd => music::ClientType::Mpd { host, music_dir },
PlayerType::Mpris => music::ClientType::Mpris,
};
clients.music(client_type)
}
impl Module<Button> for MusicModule {
@ -98,19 +102,21 @@ impl Module<Button> for MusicModule {
let re = Regex::new(r"\{([\w-]+)}")?;
let tokens = get_tokens(&re, self.format.as_str());
let client = get_client(
context.ironbar.clients.borrow_mut(),
self.player_type,
self.host.clone(),
self.music_dir.clone(),
);
// receive player updates
{
let player_type = self.player_type;
let host = self.host.clone();
let music_dir = self.music_dir.clone();
let tx = context.tx.clone();
let client = client.clone();
spawn(async move {
loop {
let mut rx = {
let client = get_client(player_type, &host, music_dir.clone()).await;
client.subscribe_change()
};
let mut rx = client.subscribe_change();
while let Ok(update) = rx.recv().await {
match update {
@ -143,7 +149,6 @@ impl Module<Button> for MusicModule {
progress_tick
))
),
PlayerUpdate::Disconnect => break,
}
}
}
@ -152,13 +157,8 @@ impl Module<Button> for MusicModule {
// listen to ui events
{
let player_type = self.player_type;
let host = self.host.clone();
let music_dir = self.music_dir.clone();
spawn(async move {
while let Some(event) = rx.recv().await {
let client = get_client(player_type, &host, music_dir.clone()).await;
let res = match event {
PlayerCommand::Previous => client.prev(),
PlayerCommand::Play => client.play(),

View file

@ -1,7 +1,7 @@
use crate::clients::system_tray::get_tray_event_client;
use crate::clients::system_tray::TrayEventReceiver;
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{await_sync, glib_recv, spawn, try_send};
use crate::{glib_recv, spawn, try_send};
use color_eyre::Result;
use glib::ffi::g_strfreev;
use glib::translate::ToGlibPtr;
@ -171,8 +171,10 @@ impl Module<MenuBar> for TrayModule {
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let client = await_sync(async { get_tray_event_client().await });
let tx = context.tx.clone();
let client = context.client::<TrayEventReceiver>();
let (tray_tx, mut tray_rx) = client.subscribe();
// listen to tray updates

View file

@ -6,8 +6,8 @@ use serde::Deserialize;
use tokio::sync::{broadcast, mpsc};
use upower_dbus::BatteryState;
use zbus;
use zbus::fdo::PropertiesProxy;
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
@ -15,7 +15,7 @@ use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{await_sync, error, glib_recv, send_async, spawn, try_send};
use crate::{error, glib_recv, send_async, spawn, try_send};
const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60;
@ -65,9 +65,10 @@ impl Module<gtk::Button> for UpowerModule {
_rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let tx = context.tx.clone();
let display_proxy = context.client::<PropertiesProxy>();
spawn(async move {
// await_sync due to strange "higher-ranked lifetime error"
let display_proxy = await_sync(async move { get_display_proxy().await });
let mut prop_changed_stream = display_proxy.receive_properties_changed().await?;
let device_interface_name =

View file

@ -1,4 +1,4 @@
use crate::clients::compositor::{Compositor, Visibility, Workspace, WorkspaceUpdate};
use crate::clients::compositor::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::config::CommonConfig;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
@ -98,7 +98,7 @@ fn create_button(
}
if !visibility.is_visible() {
style_context.add_class("inactive")
style_context.add_class("inactive");
}
{
@ -155,13 +155,10 @@ impl Module<gtk::Box> for WorkspacesModule {
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let tx = context.tx.clone();
let client = context.ironbar.clients.borrow_mut().workspaces();
// Subscribe & send events
spawn(async move {
let mut srx = {
let client =
Compositor::get_workspace_client().expect("Failed to get workspace client");
client.subscribe_workspace_change()
};
let mut srx = client.subscribe_workspace_change();
trace!("Set up workspace subscription");
@ -171,13 +168,13 @@ impl Module<gtk::Box> for WorkspacesModule {
}
});
let client = context.client::<dyn WorkspaceClient>();
// Change workspace focus
spawn(async move {
trace!("Setting up UI event handler");
while let Some(name) = rx.recv().await {
let client =
Compositor::get_workspace_client().expect("Failed to get workspace client");
client.focus(name)?;
}