1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-04 12:11:03 +02:00

Merge pull request #396 from JakeStanger/refactor/clients

Massive Client Refactor
This commit is contained in:
Jake Stanger 2024-01-09 23:39:30 +00:00 committed by GitHub
commit b55fbb3001
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1131 additions and 1118 deletions

165
Cargo.lock generated
View file

@ -218,8 +218,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -235,16 +235,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]]
name = "async_once"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ce4f10ea3abcd6617873bae9f91d1c5332b4a778bd9ce34d0cd517474c1de82"
[[package]] [[package]]
name = "atk" name = "atk"
version = "0.18.0" version = "0.18.0"
@ -478,8 +472,8 @@ checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [ dependencies = [
"heck 0.4.1", "heck 0.4.1",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -662,9 +656,9 @@ dependencies = [
"codespan-reporting", "codespan-reporting",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"scratch", "scratch",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -680,8 +674,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -703,7 +697,7 @@ dependencies = [
"fnv", "fnv",
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"strsim", "strsim",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -715,7 +709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -737,7 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -760,7 +754,7 @@ checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"rustc_version", "rustc_version",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -839,7 +833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -860,8 +854,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -980,15 +974,15 @@ checksum = "55a5e644a80e6d96b2b4910fa7993301d7b7926c045b475b62202b20a36ce69e"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -1017,9 +1011,9 @@ checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -1049,9 +1043,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
version = "2.1.0" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
dependencies = [ dependencies = [
"fastrand 2.0.1", "fastrand 2.0.1",
"futures-core", "futures-core",
@ -1067,8 +1061,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1251,8 +1245,8 @@ dependencies = [
"proc-macro-crate 2.0.1", "proc-macro-crate 2.0.1",
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1352,8 +1346,8 @@ dependencies = [
"proc-macro-crate 1.3.1", "proc-macro-crate 1.3.1",
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1523,8 +1517,8 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c941d3d52e979612af8cb94e8de49000c7fada2014a7791d173ab41339f4e4eb" checksum = "c941d3d52e979612af8cb94e8de49000c7fada2014a7791d173ab41339f4e4eb"
dependencies = [ dependencies = [
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1643,22 +1637,20 @@ checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
name = "ironbar" name = "ironbar"
version = "0.14.0-pre" version = "0.14.0-pre"
dependencies = [ dependencies = [
"async_once",
"cfg-if", "cfg-if",
"chrono", "chrono",
"clap", "clap",
"color-eyre", "color-eyre",
"ctrlc", "ctrlc",
"dirs", "dirs",
"futures-lite 2.1.0", "futures-lite 2.2.0",
"futures-util", "futures-util",
"glib", "glib",
"gtk", "gtk",
"gtk-layer-shell", "gtk-layer-shell",
"hyprland", "hyprland",
"indexmap 2.1.0", "indexmap 2.1.0",
"lazy_static", "mpd-utils",
"mpd_client",
"mpris", "mpris",
"nix 0.27.1", "nix 0.27.1",
"notify", "notify",
@ -1879,6 +1871,19 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "mpd_client" name = "mpd_client"
version = "1.3.0" version = "1.3.0"
@ -2060,8 +2065,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2205,8 +2210,8 @@ dependencies = [
"pest", "pest",
"pest_meta", "pest_meta",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2302,7 +2307,7 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [ dependencies = [
"proc-macro-error-attr", "proc-macro-error-attr",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
"version_check", "version_check",
] ]
@ -2314,15 +2319,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"version_check", "version_check",
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.66" version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2350,9 +2355,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.32" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -2666,8 +2671,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2688,8 +2693,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2874,9 +2879,9 @@ checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [ dependencies = [
"heck 0.4.1", "heck 0.4.1",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"rustversion", "rustversion",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2922,18 +2927,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.28" version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"unicode-ident", "unicode-ident",
] ]
@ -3037,22 +3042,22 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.40" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.40" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3134,8 +3139,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3254,8 +3259,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 2.0.28", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3466,7 +3471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
] ]
[[package]] [[package]]
@ -3521,7 +3526,7 @@ dependencies = [
"log", "log",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3544,7 +3549,7 @@ version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
dependencies = [ dependencies = [
"quote 1.0.32", "quote 1.0.35",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
] ]
@ -3555,7 +3560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
@ -3648,7 +3653,7 @@ checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml", "quick-xml",
"quote 1.0.32", "quote 1.0.35",
] ]
[[package]] [[package]]
@ -4019,7 +4024,7 @@ checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d"
dependencies = [ dependencies = [
"proc-macro-crate 1.3.1", "proc-macro-crate 1.3.1",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"regex", "regex",
"syn 1.0.109", "syn 1.0.109",
"zvariant_utils", "zvariant_utils",
@ -4058,7 +4063,7 @@ checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd"
dependencies = [ dependencies = [
"proc-macro-crate 1.3.1", "proc-macro-crate 1.3.1",
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
"zvariant_utils", "zvariant_utils",
] ]
@ -4070,6 +4075,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.32", "quote 1.0.35",
"syn 1.0.109", "syn 1.0.109",
] ]

View file

@ -48,7 +48,7 @@ clock = ["chrono"]
music = ["regex"] music = ["regex"]
"music+all" = ["music", "music+mpris", "music+mpd"] "music+all" = ["music", "music+mpris", "music+mpd"]
"music+mpris" = ["music", "mpris"] "music+mpris" = ["music", "mpris"]
"music+mpd" = ["music", "mpd_client"] "music+mpd" = ["music", "mpd-utils"]
sys_info = ["sysinfo", "regex"] sys_info = ["sysinfo", "regex"]
@ -94,9 +94,6 @@ smithay-client-toolkit = { version = "0.18.0", default-features = false, feature
] } ] }
universal-config = { version = "0.4.3", default_features = false } universal-config = { version = "0.4.3", default_features = false }
ctrlc = "3.4.2" ctrlc = "3.4.2"
lazy_static = "1.4.0"
async_once = "0.2.6"
cfg-if = "1.0.0" cfg-if = "1.0.0"
# cli # cli
@ -115,7 +112,7 @@ nix = { version = "0.27.1", optional = true, features = ["event"] }
chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] } chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] }
# music # music
mpd_client = { version = "1.3.0", optional = true } mpd-utils = { version = "0.2.0", optional = true }
mpris = { version = "2.0.1", optional = true } mpris = { version = "2.0.1", optional = true }
# sys_info # sys_info
@ -126,7 +123,7 @@ system-tray = { version = "0.1.4", optional = true }
# upower # upower
upower_dbus = { version = "0.3.2", optional = true } 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 } zbus = { version = "3.14.1", optional = true }
# workspaces # workspaces

View file

@ -27,6 +27,8 @@ pub struct Bar {
monitor_name: String, monitor_name: String,
position: BarPosition, position: BarPosition,
ironbar: Rc<Ironbar>,
window: ApplicationWindow, window: ApplicationWindow,
content: gtk::Box, content: gtk::Box,
@ -39,7 +41,12 @@ pub struct Bar {
} }
impl 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() let window = ApplicationWindow::builder()
.application(app) .application(app)
.type_(WindowType::Toplevel) .type_(WindowType::Toplevel)
@ -90,6 +97,7 @@ impl Bar {
name, name,
monitor_name, monitor_name,
position, position,
ironbar,
window, window,
content, content,
start, start,
@ -263,17 +271,17 @@ impl Bar {
if let Some(modules) = config.start { if let Some(modules) = config.start {
let info = info!(ModuleLocation::Left); 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 { if let Some(modules) = config.center {
let info = info!(ModuleLocation::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 { if let Some(modules) = config.end {
let info = info!(ModuleLocation::Right); 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 }; let result = BarLoadResult { popup };
@ -333,6 +341,7 @@ fn add_modules(
content: &gtk::Box, content: &gtk::Box,
modules: Vec<ModuleConfig>, modules: Vec<ModuleConfig>,
info: &ModuleInfo, info: &ModuleInfo,
ironbar: &Rc<Ironbar>,
popup: &Rc<RefCell<Popup>>, popup: &Rc<RefCell<Popup>>,
) -> Result<()> { ) -> Result<()> {
let orientation = info.bar_position.get_orientation(); let orientation = info.bar_position.get_orientation();
@ -343,6 +352,7 @@ fn add_modules(
let widget_parts = create_module( let widget_parts = create_module(
*$module, *$module,
$id, $id,
ironbar.clone(),
common.name.clone(), common.name.clone(),
&info, &info,
&Rc::clone(&popup), &Rc::clone(&popup),
@ -387,7 +397,8 @@ pub fn create_bar(
monitor: &Monitor, monitor: &Monitor,
monitor_name: String, monitor_name: String,
config: Config, config: Config,
ironbar: Rc<Ironbar>,
) -> Result<Bar> { ) -> Result<Bar> {
let bar = Bar::new(app, monitor_name, config); let bar = Bar::new(app, monitor_name, config, ironbar);
bar.init(monitor) bar.init(monitor)
} }

View file

@ -1,15 +1,14 @@
use super::wayland::{self, ClipboardItem}; 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::map::Iter;
use indexmap::IndexMap; use indexmap::IndexMap;
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, trace}; use tracing::{debug, trace};
#[derive(Debug)] #[derive(Debug)]
pub enum ClipboardEvent { pub enum ClipboardEvent {
Add(Arc<ClipboardItem>), Add(ClipboardItem),
Remove(usize), Remove(usize),
Activate(usize), Activate(usize),
} }
@ -18,13 +17,16 @@ type EventSender = mpsc::Sender<ClipboardEvent>;
/// Clipboard client singleton, /// Clipboard client singleton,
/// to ensure bars don't duplicate requests to the compositor. /// 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)>>>, senders: Arc<Mutex<Vec<(EventSender, usize)>>>,
cache: Arc<Mutex<ClipboardCache>>, cache: Arc<Mutex<ClipboardCache>>,
} }
impl ClipboardClient { impl Client {
fn new() -> Self { pub(crate) fn new(wl: Arc<wayland::Client>) -> Self {
trace!("Initializing clipboard client"); trace!("Initializing clipboard client");
let senders = arc_mut!(Vec::<(EventSender, usize)>::new()); let senders = arc_mut!(Vec::<(EventSender, usize)>::new());
@ -34,13 +36,11 @@ impl ClipboardClient {
{ {
let senders = senders.clone(); let senders = senders.clone();
let cache = cache.clone(); let cache = cache.clone();
let wl = wl.clone();
spawn(async move { spawn(async move {
let (mut rx, item) = { let item = wl.clipboard_item();
let wl = wayland::get_client(); let mut rx = wl.subscribe_clipboard();
let wl = lock!(wl);
wl.subscribe_clipboard()
};
if let Some(item) = item { if let Some(item) = item {
let senders = lock!(senders); 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> { pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
@ -120,9 +124,7 @@ impl ClipboardClient {
}; };
if let Some(item) = item { if let Some(item) = item {
let wl = wayland::get_client(); self.wayland.copy_to_clipboard(item);
let wl = lock!(wl);
wl.copy_to_clipboard(item);
} }
let senders = lock!(self.senders); let senders = lock!(self.senders);
@ -150,7 +152,7 @@ impl ClipboardClient {
/// at different times. /// at different times.
#[derive(Debug)] #[derive(Debug)]
struct ClipboardCache { struct ClipboardCache {
cache: IndexMap<usize, (Arc<ClipboardItem>, usize)>, cache: IndexMap<usize, (ClipboardItem, usize)>,
} }
impl ClipboardCache { impl ClipboardCache {
@ -162,12 +164,12 @@ impl ClipboardCache {
} }
/// Gets the entry with key `id` from the cache. /// 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() self.cache.get(&id).map(|(item, _)| item).cloned()
} }
/// Inserts an entry with `ref_count` initial references. /// 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 self.cache
.insert(item.id, (item, ref_count)) .insert(item.id, (item, ref_count))
.map(|(item, _)| item) .map(|(item, _)| item)
@ -175,7 +177,7 @@ impl ClipboardCache {
/// Removes the entry with key `id`. /// Removes the entry with key `id`.
/// This ignores references. /// 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) self.cache.shift_remove(&id).map(|(item, _)| item)
} }
@ -224,15 +226,9 @@ impl ClipboardCache {
self.cache.len() self.cache.len()
} }
fn iter(&self) -> Iter<'_, usize, (Arc<ClipboardItem>, usize)> { fn iter(&self) -> Iter<'_, usize, (ClipboardItem, usize)> {
self.cache.iter() self.cache.iter()
} }
} }
lazy_static! { register_client!(Client, clipboard);
static ref CLIENT: ClipboardClient = ClipboardClient::new();
}
pub fn get_client() -> &'static ClipboardClient {
&CLIENT
}

View file

@ -6,23 +6,26 @@ use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial}
use hyprland::event_listener::EventListener; use hyprland::event_listener::EventListener;
use hyprland::prelude::*; use hyprland::prelude::*;
use hyprland::shared::{HyprDataVec, WorkspaceType}; use hyprland::shared::{HyprDataVec, WorkspaceType};
use lazy_static::lazy_static;
use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::broadcast::{channel, Receiver, Sender};
use tracing::{debug, error, info}; use tracing::{debug, error, info};
pub struct EventClient { #[derive(Debug)]
pub struct Client {
workspace_tx: Sender<WorkspaceUpdate>, workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>, _workspace_rx: Receiver<WorkspaceUpdate>,
} }
impl EventClient { impl Client {
fn new() -> Self { pub(crate) fn new() -> Self {
let (workspace_tx, workspace_rx) = channel(16); let (workspace_tx, workspace_rx) = channel(16);
Self { let instance = Self {
workspace_tx, workspace_tx,
_workspace_rx: workspace_rx, _workspace_rx: workspace_rx,
} };
instance.listen_workspace_events();
instance
} }
fn listen_workspace_events(&self) { 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<()> { fn focus(&self, id: String) -> Result<()> {
let identifier = match id.parse::<i32>() { let identifier = match id.parse::<i32>() {
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum), 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 { fn get_workspace_name(name: WorkspaceType) -> String {
match name { match name {
WorkspaceType::Regular(name) => name, WorkspaceType::Regular(name) => name,

View file

@ -1,6 +1,8 @@
use crate::{await_sync, register_client};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result}; 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 tokio::sync::broadcast;
use tracing::debug; use tracing::debug;
@ -52,15 +54,17 @@ impl Compositor {
} }
} }
/// Gets the workspace client for the current compositor /// Creates a new instance of
pub fn get_workspace_client() -> Result<&'static (dyn WorkspaceClient + Send)> { /// the workspace client for the current compositor.
pub fn create_workspace_client() -> Result<Arc<dyn WorkspaceClient + Send + Sync>> {
let current = Self::get_current(); let current = Self::get_current();
debug!("Getting workspace client for: {current}"); debug!("Getting workspace client for: {current}");
match current { match current {
#[cfg(feature = "workspaces+sway")] #[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")] #[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") Self::Unsupported => Err(Report::msg("Unsupported compositor")
.note("Currently workspaces are only supported by Sway and Hyprland")), .note("Currently workspaces are only supported by Sway and Hyprland")),
} }
@ -129,10 +133,12 @@ pub enum WorkspaceUpdate {
Unknown, Unknown,
} }
pub trait WorkspaceClient { pub trait WorkspaceClient: Debug + Send + Sync {
/// Requests the workspace with this name is focused. /// Requests the workspace with this name is focused.
fn focus(&self, name: String) -> Result<()>; fn focus(&self, name: String) -> Result<()>;
/// Creates a new to workspace event receiver. /// Creates a new to workspace event receiver.
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>; 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 super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send, spawn}; use crate::{await_sync, send, spawn};
use async_once::AsyncOnce; use color_eyre::{Report, Result};
use color_eyre::Report;
use futures_util::StreamExt; use futures_util::StreamExt;
use lazy_static::lazy_static;
use std::sync::Arc; use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent}; use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{info, trace}; use tracing::{info, trace};
pub struct SwayEventClient { #[derive(Debug)]
pub struct Client {
client: Arc<Mutex<Connection>>,
workspace_tx: Sender<WorkspaceUpdate>, workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>, _workspace_rx: Receiver<WorkspaceUpdate>,
} }
impl SwayEventClient { impl Client {
fn new() -> Self { 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_rx) = channel(16);
{ {
let workspace_tx = workspace_tx.clone(); // create 2nd client as subscription takes ownership
spawn(async move {
let client = Connection::new().await?; 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 event_types = [EventType::Workspace];
let mut events = client.subscribe(event_types).await?; let mut events = client.subscribe(event_types).await?;
while let Some(event) = events.next().await { while let Some(event) = events.next().await {
@ -43,18 +46,18 @@ impl SwayEventClient {
}); });
} }
Self { Ok(Self {
client,
workspace_tx, workspace_tx,
_workspace_rx: workspace_rx, _workspace_rx: workspace_rx,
} })
} }
} }
impl WorkspaceClient for SwayEventClient { impl WorkspaceClient for Client {
fn focus(&self, id: String) -> color_eyre::Result<()> { fn focus(&self, id: String) -> Result<()> {
await_sync(async move { await_sync(async move {
let client = get_client().await; let mut client = self.client.lock().await;
let mut client = client.lock().await;
client.run_command(format!("workspace {id}")).await client.run_command(format!("workspace {id}")).await
})?; })?;
Ok(()) Ok(())
@ -65,14 +68,12 @@ impl WorkspaceClient for SwayEventClient {
{ {
let tx = self.workspace_tx.clone(); let tx = self.workspace_tx.clone();
await_sync(async { let client = self.client.clone();
let client = get_client().await;
let mut client = client.lock().await; 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 = let event =
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect()); 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 { impl From<Node> for Workspace {
fn from(node: Node) -> Self { fn from(node: Node) -> Self {
let visibility = Visibility::from(&node); let visibility = Visibility::from(&node);

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub mod clipboard; pub mod clipboard;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
@ -9,3 +11,109 @@ pub mod system_tray;
#[cfg(feature = "upower")] #[cfg(feature = "upower")]
pub mod upower; pub mod upower;
pub mod wayland; 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 color_eyre::Result;
use std::fmt::Debug;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -19,8 +20,6 @@ pub enum PlayerUpdate {
/// Triggered at regular intervals while a track is playing. /// Triggered at regular intervals while a track is playing.
/// Used to keep track of the progress through the current track. /// Used to keep track of the progress through the current track.
ProgressTick(ProgressTick), ProgressTick(ProgressTick),
/// Triggered when the client disconnects from the player.
Disconnect,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -56,7 +55,7 @@ pub struct ProgressTick {
pub elapsed: Option<Duration>, pub elapsed: Option<Duration>,
} }
pub trait MusicClient { pub trait MusicClient: Debug + Send + Sync {
fn play(&self) -> Result<()>; fn play(&self) -> Result<()>;
fn pause(&self) -> Result<()>; fn pause(&self) -> Result<()>;
fn next(&self) -> Result<()>; fn next(&self) -> Result<()>;
@ -68,18 +67,15 @@ pub trait MusicClient {
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>; fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
} }
pub enum ClientType<'a> { #[derive(Debug, Clone, Eq, PartialEq, Hash)]
Mpd { host: &'a str, music_dir: PathBuf }, pub enum ClientType {
Mpd { host: String, music_dir: PathBuf },
Mpris, 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 { match client_type {
ClientType::Mpd { host, music_dir } => Box::new( ClientType::Mpd { host, music_dir } => Arc::new(mpd::Client::new(host, music_dir)),
mpd::get_client(host, music_dir) ClientType::Mpris => Arc::new(mpris::Client::new()),
.await
.expect("Failed to connect to MPD client"),
),
ClientType::Mpris => Box::new(mpris::get_client()),
} }
} }

View file

@ -1,91 +1,72 @@
use super::{ use super::{
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS, 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 color_eyre::Result;
use lazy_static::lazy_static; use mpd_client::client::{ConnectionEvent, Subsystem};
use mpd_client::client::{Connection, ConnectionEvent, Subsystem}; use mpd_client::commands::{self, SeekMode};
use mpd_client::commands::SeekMode;
use mpd_client::protocol::MpdProtocolError;
use mpd_client::responses::{PlayState, Song}; use mpd_client::responses::{PlayState, Song};
use mpd_client::tag::Tag; use mpd_client::tag::Tag;
use mpd_client::{commands, Client}; use mpd_utils::{mpd_client, PersistentClient};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::net::{TcpStream, UnixStream};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::Mutex;
use tokio::time::sleep; use tokio::time::sleep;
use tracing::{debug, error, info}; use tracing::debug;
lazy_static! { macro_rules! command {
static ref CONNECTIONS: Arc<Mutex<HashMap<String, Arc<MpdClient>>>> = ($self:ident, $command:expr) => {
Arc::new(Mutex::new(HashMap::new())); await_sync(async move { $self.client.command($command).await.map_err(Report::new) })
};
} }
pub struct MpdClient { #[derive(Debug)]
client: Client, pub struct Client {
client: Arc<PersistentClient>,
music_dir: PathBuf, music_dir: PathBuf,
tx: broadcast::Sender<PlayerUpdate>, tx: broadcast::Sender<PlayerUpdate>,
_rx: broadcast::Receiver<PlayerUpdate>, _rx: broadcast::Receiver<PlayerUpdate>,
} }
#[derive(Debug)] impl Client {
pub enum MpdConnectionError { pub fn new(host: String, music_dir: PathBuf) -> Self {
MaxRetries, let client = Arc::new(PersistentClient::new(host, Duration::from_secs(5)));
ProtocolError(MpdProtocolError), let mut client_rx = client.subscribe();
}
impl Display for MpdConnectionError { let (tx, rx) = broadcast::channel(32);
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::MaxRetries => write!(f, "Reached max retries"),
Self::ProtocolError(e) => write!(f, "{e:?}"),
}
}
}
impl std::error::Error for MpdConnectionError {} let _guard = Ironbar::runtime().enter();
client.init();
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 music_dir = music_dir.clone();
let tx = tx.clone(); let tx = tx.clone();
let client = client.clone(); let client = client.clone();
let music_dir = music_dir.clone();
spawn(async move { spawn(async move {
while let Some(change) = state_changes.next().await { Self::send_update(&client, &tx, &music_dir)
debug!("Received state change: {:?}", change); .await
.expect("Failed to send update");
while let Ok(change) = client_rx.recv().await {
debug!("Received state change: {change:?}");
if let ConnectionEvent::SubsystemChange( if let ConnectionEvent::SubsystemChange(
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer, Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
) = change ) = *change
{ {
Self::send_update(&client, &tx, &music_dir) Self::send_update(&client, &tx, &music_dir)
.await .await
.expect("Failed to send update"); .expect("Failed to send update");
} }
} }
Ok::<(), broadcast::error::SendError<(Option<Track>, Status)>>(())
}); });
} }
{ {
let client = client.clone();
let tx = tx.clone(); let tx = tx.clone();
let client = client.clone();
spawn(async move { spawn(async move {
loop { loop {
@ -95,16 +76,16 @@ impl MpdClient {
}); });
} }
Ok(Self { Self {
client, client,
music_dir,
tx, tx,
music_dir,
_rx: rx, _rx: rx,
}) }
} }
async fn send_update( async fn send_update(
client: &Client, client: &PersistentClient,
tx: &broadcast::Sender<PlayerUpdate>, tx: &broadcast::Sender<PlayerUpdate>,
music_dir: &Path, music_dir: &Path,
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> { ) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
@ -112,7 +93,7 @@ impl MpdClient {
let status = client.command(commands::Status).await; let status = client.command(commands::Status).await;
if let (Ok(current_song), Ok(status)) = (current_song, status) { 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 status = Status::from(status);
let update = PlayerUpdate::Update(Box::new(track), status); let update = PlayerUpdate::Update(Box::new(track), status);
@ -122,7 +103,7 @@ impl MpdClient {
Ok(()) 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; let status = client.command(commands::Status).await;
if let Ok(status) = status { if let Ok(status) = status {
@ -136,15 +117,42 @@ impl MpdClient {
} }
} }
} }
fn is_connected(&self) -> bool {
!self.client.is_connection_closed()
} }
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> { impl MusicClient for Client {
info!("Connection to MPD server lost"); fn play(&self) -> Result<()> {
self.tx.send(PlayerUpdate::Disconnect)?; command!(self, commands::SetPause(false))
Ok(()) }
fn pause(&self) -> Result<()> {
command!(self, commands::SetPause(true))
}
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 { fn convert_song(song: &Song, music_dir: &Path) -> Track {
@ -162,156 +170,16 @@ impl MpdClient {
.ok(); .ok();
Track { Track {
title: song.title().map(std::string::ToString::to_string), title: song.title().map(ToString::to_string),
album: song.album().map(std::string::ToString::to_string), album: song.album().map(ToString::to_string),
artist: Some(song.artists().join(", ")), artist: Some(song.artists().join(", ")),
date: try_get_first_tag(song, &Tag::Date).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(std::string::ToString::to_string), genre: try_get_first_tag(song, &Tag::Genre).map(ToString::to_string),
disc: Some(disc), disc: Some(disc),
track: Some(track), track: Some(track),
cover_path, 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 /// Attempts to read the first value for a tag
/// (since the MPD client returns a vector of tags, or None) /// (since the MPD client returns a vector of tags, or None)

View file

@ -2,20 +2,16 @@ use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL
use crate::clients::music::ProgressTick; use crate::clients::music::ProgressTick;
use crate::{arc_mut, lock, send, spawn_blocking}; use crate::{arc_mut, lock, send, spawn_blocking};
use color_eyre::Result; use color_eyre::Result;
use lazy_static::lazy_static;
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder}; use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
use std::cmp;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
use std::{cmp, string};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
lazy_static! { #[derive(Debug)]
static ref CLIENT: Arc<Client> = Arc::new(Client::new());
}
pub struct Client { pub struct Client {
current_player: Arc<Mutex<Option<String>>>, current_player: Arc<Mutex<Option<String>>>,
tx: broadcast::Sender<PlayerUpdate>, tx: broadcast::Sender<PlayerUpdate>,
@ -23,7 +19,7 @@ pub struct Client {
} }
impl Client { impl Client {
fn new() -> Self { pub(crate) fn new() -> Self {
let (tx, rx) = broadcast::channel(32); let (tx, rx) = broadcast::channel(32);
let current_player = arc_mut!(None); 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 { impl From<Metadata> for Track {
fn from(value: Metadata) -> Self { fn from(value: Metadata) -> Self {
const KEY_DATE: &str = "xesam:contentCreated"; const KEY_DATE: &str = "xesam:contentCreated";
@ -301,11 +293,11 @@ impl From<Metadata> for Track {
Self { Self {
title: value title: value
.title() .title()
.map(std::string::ToString::to_string) .map(ToString::to_string)
.and_then(replace_empty_none), .and_then(replace_empty_none),
album: value album: value
.album_name() .album_name()
.map(std::string::ToString::to_string) .map(ToString::to_string)
.and_then(replace_empty_none), .and_then(replace_empty_none),
artist: value artist: value
.artists() .artists()
@ -314,14 +306,14 @@ impl From<Metadata> for Track {
date: value date: value
.get(KEY_DATE) .get(KEY_DATE)
.and_then(mpris::MetadataValue::as_string) .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), disc: value.disc_number().map(|disc| disc as u64),
genre: value genre: value
.get(KEY_GENRE) .get(KEY_GENRE)
.and_then(mpris::MetadataValue::as_str_array) .and_then(mpris::MetadataValue::as_str_array)
.and_then(|arr| arr.first().map(|val| (*val).to_string())), .and_then(|arr| arr.first().map(|val| (*val).to_string())),
track: value.track_number().map(|track| track as u64), 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 crate::{arc_mut, lock, register_client, send, spawn, Ironbar};
use async_once::AsyncOnce;
use color_eyre::Report; use color_eyre::Report;
use lazy_static::lazy_static;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use system_tray::message::menu::TrayMenu; use system_tray::message::menu::TrayMenu;
@ -13,6 +11,7 @@ use tracing::{debug, error, trace};
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>; type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
#[derive(Debug)]
pub struct TrayEventReceiver { pub struct TrayEventReceiver {
tx: mpsc::Sender<NotifierItemCommand>, tx: mpsc::Sender<NotifierItemCommand>,
b_tx: broadcast::Sender<NotifierItemMessage>, b_tx: broadcast::Sender<NotifierItemMessage>,
@ -94,8 +93,9 @@ impl TrayEventReceiver {
} }
} }
lazy_static! { /// Attempts to create a new `TrayEventReceiver` instance,
static ref CLIENT: AsyncOnce<TrayEventReceiver> = AsyncOnce::new(async { /// retrying a maximum of 10 times before panicking the thread.
pub async fn create_client() -> TrayEventReceiver {
const MAX_RETRIES: i32 = 10; const MAX_RETRIES: i32 = 10;
// sometimes this can fail // sometimes this can fail
@ -108,7 +108,12 @@ lazy_static! {
match tray { match tray {
Ok(tray) => break Some(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 { if retries == MAX_RETRIES {
@ -117,9 +122,6 @@ lazy_static! {
}; };
value.expect("Failed to create StatusNotifierWatcher") value.expect("Failed to create StatusNotifierWatcher")
});
} }
pub async fn get_tray_event_client() -> &'static TrayEventReceiver { register_client!(TrayEventReceiver, tray);
CLIENT.get().await
}

View file

@ -1,11 +1,9 @@
use async_once::AsyncOnce; use crate::register_client;
use lazy_static::lazy_static;
use std::sync::Arc; use std::sync::Arc;
use upower_dbus::UPowerProxy; use upower_dbus::UPowerProxy;
use zbus::fdo::PropertiesProxy; use zbus::fdo::PropertiesProxy;
lazy_static! { pub async fn create_display_proxy() -> Arc<PropertiesProxy<'static>> {
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
let dbus = Box::pin(zbus::Connection::system()) let dbus = Box::pin(zbus::Connection::system())
.await .await
.expect("failed to create connection to system bus"); .expect("failed to create connection to system bus");
@ -32,9 +30,6 @@ lazy_static! {
.expect("failed to build proxy"); .expect("failed to build proxy");
Arc::new(proxy) Arc::new(proxy)
});
} }
pub async fn get_display_proxy() -> &'static PropertiesProxy<'static> { register_client!(PropertiesProxy<'static>, upower);
DISPLAY_PROXY.get().await
}

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 macros;
mod wl_output; mod wl_output;
mod wl_seat; mod wl_seat;
mod wlr_foreign_toplevel; mod wlr_foreign_toplevel;
use self::wlr_foreign_toplevel::manager::ToplevelManagerState; use crate::error::ERR_CHANNEL_RECV;
use crate::{arc_mut, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager}; 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 cfg_if::cfg_if;
use lazy_static::lazy_static; use color_eyre::Report;
use smithay_client_toolkit::output::OutputState; use smithay_client_toolkit::output::{OutputInfo, OutputState};
use smithay_client_toolkit::reexports::calloop::LoopHandle; 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::registry::{ProvidesRegistryState, RegistryState};
use smithay_client_toolkit::seat::SeatState; use smithay_client_toolkit::seat::SeatState;
use smithay_client_toolkit::{ use smithay_client_toolkit::{
delegate_output, delegate_registry, delegate_seat, registry_handlers, delegate_output, delegate_registry, delegate_seat, registry_handlers,
}; };
use std::collections::HashMap; use tokio::sync::{broadcast, mpsc};
use std::sync::{Arc, Mutex}; use tracing::{debug, error, trace};
use tokio::sync::broadcast; use wayland_client::globals::registry_queue_init;
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, QueueHandle};
pub use self::client::WaylandClient; use wlr_foreign_toplevel::manager::ToplevelManagerState;
pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
pub use wl_output::OutputEvent;
pub use wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
@ -36,6 +45,7 @@ cfg_if! {
pub use wlr_data_control::{ClipboardItem, ClipboardValue}; pub use wlr_data_control::{ClipboardItem, ClipboardValue};
#[derive(Debug)]
pub struct DataControlDeviceEntry { pub struct DataControlDeviceEntry {
seat: WlSeat, seat: WlSeat,
device: DataControlDevice, device: DataControlDevice,
@ -43,35 +53,162 @@ cfg_if! {
} }
} }
pub struct Environment { #[derive(Debug)]
pub registry_state: RegistryState, pub enum Event {
pub output_state: OutputState, Output(OutputEvent),
pub seat_state: SeatState, Toplevel(ToplevelEvent),
pub foreign_toplevel_manager_state: ToplevelManagerState,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub data_control_device_manager_state: DataControlDeviceManagerState, Clipboard(ClipboardItem),
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>>,
} }
// Now we need to say we are delegating the responsibility of output related events for our application data #[derive(Debug)]
// type to the requisite delegate. 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_output!(Environment);
delegate_seat!(Environment); delegate_seat!(Environment);
@ -82,21 +219,128 @@ cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
delegate_data_control_device_manager!(Environment); delegate_data_control_device_manager!(Environment);
delegate_data_control_device!(Environment); delegate_data_control_device!(Environment);
delegate_data_control_source!(Environment);
delegate_data_control_offer!(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 impl Environment {
// handling for the program. This trait will forward events to the RegistryHandler trait pub fn spawn(
// implementations. event_tx: mpsc::Sender<Event>,
delegate_registry!(Environment); 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 { impl ProvidesRegistryState for Environment {
fn registry(&mut self) -> &mut RegistryState { fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state &mut self.registry_state
@ -104,10 +348,4 @@ impl ProvidesRegistryState for Environment {
registry_handlers![OutputState, SeatState]; registry_handlers![OutputState, SeatState];
} }
lazy_static! { register_client!(Client, wayland);
static ref CLIENT: Arc<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new());
}
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
CLIENT.clone()
}

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 smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
use tracing::debug; use tokio::sync::broadcast;
use wayland_client::protocol::wl_output; use tracing::{debug, error};
use wayland_client::protocol::wl_output::WlOutput;
use wayland_client::{Connection, QueueHandle}; 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 { impl Environment {
pub fn output_info(&mut self) -> Vec<OutputInfo> { pub fn output_info_all(&mut self) -> Vec<OutputInfo> {
self.output_state self.output_state
.outputs() .outputs()
.filter_map(|output| self.output_state.info(&output)) .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. // Then there exist these functions that indicate the lifecycle of an output.
// These will be called as appropriate by the delegate implementation. // These will be called as appropriate by the delegate implementation.
fn new_output( fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
debug!("Handler received new output"); 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( fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
&mut self, debug!("Handle received output update");
_conn: &Connection, if let Some(info) = self.output_state.info(&output) {
_qh: &QueueHandle<Self>, try_send!(
_output: wl_output::WlOutput, self.event_tx,
) { Event::Output(OutputEvent {
output: info,
event_type: OutputEventType::Update
})
);
} else {
error!("Output is missing information!");
}
} }
fn output_destroyed( fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
debug!("Handle received output destruction"); 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 super::Environment;
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
use tracing::debug; use tracing::debug;
use wayland_client::protocol::wl_seat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, QueueHandle}; 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 { impl SeatHandler for Environment {
fn seat_state(&mut self) -> &mut SeatState { fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state &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"); debug!("Handler received new seat");
self.seats.push(seat);
} }
fn new_capability( fn new_capability(
&mut self, &mut self,
_: &Connection, _: &Connection,
qh: &QueueHandle<Self>, qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat, seat: WlSeat,
_: Capability, _: Capability,
) { ) {
debug!("Handler received new capability"); debug!("Handler received new capability");
@ -39,25 +45,22 @@ impl SeatHandler for Environment {
device: data_control_device, device: data_control_device,
}); });
} }
if !self.seats.iter().any(|s| s == &seat) {
self.seats.push(seat);
}
} }
fn remove_capability( fn remove_capability(
&mut self, &mut self,
_: &Connection, _: &Connection,
_: &QueueHandle<Self>, _: &QueueHandle<Self>,
_: wl_seat::WlSeat, seat: WlSeat,
_: Capability, _: Capability,
) { ) {
debug!("Handler received capability removal"); 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"); 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, zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
}; };
#[derive(Debug)]
pub struct DataControlDevice { pub struct DataControlDevice {
pub device: ZwlrDataControlDeviceV1, pub device: ZwlrDataControlDeviceV1,
} }

View file

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

View file

@ -6,8 +6,8 @@ pub mod source;
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler}; use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer}; use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
use self::source::DataControlSourceHandler; use self::source::DataControlSourceHandler;
use crate::clients::wayland::Environment; use super::{Client, Environment, Event, Request, Response};
use crate::{lock, send, Ironbar}; use crate::{lock, try_send, Ironbar};
use device::DataControlDevice; use device::DataControlDevice;
use glib::Bytes; use glib::Bytes;
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ}; 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::os::fd::{AsRawFd, OwnedFd, RawFd};
use std::sync::Arc; use std::sync::Arc;
use std::{fs, io}; use std::{fs, io};
use tokio::sync::broadcast;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use wayland_client::{Connection, QueueHandle}; use wayland_client::{Connection, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal"; const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
#[derive(Debug)]
pub struct SelectionOfferItem { pub struct SelectionOfferItem {
offer: SelectionOffer, offer: SelectionOffer,
token: Option<RegistrationToken>, 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)] #[derive(Debug, Clone, Eq)]
pub struct ClipboardItem { pub struct ClipboardItem {
pub id: usize, pub id: usize,
pub value: ClipboardValue, pub value: Arc<ClipboardValue>,
pub mime_type: String, pub mime_type: Arc<str>,
} }
impl PartialEq<Self> for ClipboardItem { 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 { 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:?}"); debug!("Copying item to clipboard: {item:?}");
// TODO: Proper device tracking let seat = self.default_seat();
let device = self.data_control_devices.first(); let Some(device) = self
if let Some(device) = device { .data_control_devices
.iter()
.find(|entry| entry.seat == seat)
else {
return;
};
let source = self let source = self
.data_control_device_manager_state .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); source.set_selection(&device.device);
self.copy_paste_sources.push(source); self.copy_paste_sources.push(source);
lock!(self.clipboard).replace(item); 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> { fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
let value = match mime_type.category { let value = match mime_type.category {
MimeTypeCategory::Text => { MimeTypeCategory::Text => {
@ -145,13 +187,15 @@ impl Environment {
Ok(ClipboardItem { Ok(ClipboardItem {
id: Ironbar::unique_id(), id: Ironbar::unique_id(),
value, value: Arc::new(value),
mime_type: mime_type.value.clone(), mime_type: mime_type.value.clone().into(),
}) })
} }
} }
impl DataControlDeviceHandler for Environment { impl DataControlDeviceHandler for Environment {
/// Called when an offer for a new value is received
/// (ie something has copied to the clipboard)
fn selection( fn selection(
&mut self, &mut self,
_conn: &Connection, _conn: &Connection,
@ -175,15 +219,16 @@ impl DataControlDeviceHandler for Environment {
.last_mut() .last_mut()
.expect("Failed to get current offer"); .expect("Failed to get current offer");
// clear prev
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else { let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
lock!(self.clipboard).take(); lock!(self.clipboard).take();
// send an event so the clipboard module is aware it's changed // send an event so the clipboard module is aware it's changed
send!( try_send!(
self.clipboard_tx, self.event_tx,
Arc::new(ClipboardItem { Event::Clipboard(ClipboardItem {
id: usize::MAX, id: usize::MAX,
mime_type: String::new(), mime_type: String::new().into(),
value: ClipboardValue::Other value: Arc::new(ClipboardValue::Other)
}) })
); );
return; return;
@ -192,7 +237,7 @@ impl DataControlDeviceHandler for Environment {
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) { if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
let offer_clone = cur_offer.offer.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 clipboard = self.clipboard.clone();
let token = let token =
@ -207,9 +252,8 @@ impl DataControlDeviceHandler for Environment {
match Self::read_file(&mime_type, file.get_mut()) { match Self::read_file(&mime_type, file.get_mut()) {
Ok(item) => { Ok(item) => {
let item = Arc::new(item);
lock!(clipboard).replace(item.clone()); lock!(clipboard).replace(item.clone());
send!(tx, item); try_send!(tx, Event::Clipboard(item));
} }
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
} }
@ -255,6 +299,8 @@ impl DataControlSourceHandler for Environment {
debug!("Accepted mime type: {mime:?}"); debug!("Accepted mime type: {mime:?}");
} }
/// Writes the current clipboard item to 'paste' it
/// upon request from a compositor client.
fn send_request( fn send_request(
&mut self, &mut self,
_conn: &Connection, _conn: &Connection,
@ -274,12 +320,12 @@ impl DataControlSourceHandler for Environment {
{ {
trace!("Source found, writing to file"); 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::Text(text) => text.as_bytes(),
ClipboardValue::Image(bytes) => bytes.as_ref(), ClipboardValue::Image(bytes) => bytes.as_ref(),
ClipboardValue::Other => panic!( 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}, zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
}; };
#[derive(Debug)]
pub struct ToplevelManagerState<V = ToplevelHandleData> { pub struct ToplevelManagerState<V = ToplevelHandleData> {
manager: ZwlrForeignToplevelManagerV1, manager: ZwlrForeignToplevelManagerV1,
_phantom: PhantomData<V>, _phantom: PhantomData<V>,
@ -31,12 +32,7 @@ impl ToplevelManagerState {
pub trait ToplevelManagerHandler: Sized { pub trait ToplevelManagerHandler: Sized {
/// Advertises a new toplevel. /// Advertises a new toplevel.
fn toplevel( fn toplevel(&mut self, conn: &Connection, qh: &QueueHandle<Self>);
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
manager: ToplevelManagerState,
);
} }
impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState { impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState {
@ -60,7 +56,7 @@ where
fn event( fn event(
state: &mut D, state: &mut D,
toplevel_manager: &ZwlrForeignToplevelManagerV1, _toplevel_manager: &ZwlrForeignToplevelManagerV1,
event: Event, event: Event,
_data: &GlobalData, _data: &GlobalData,
conn: &Connection, conn: &Connection,
@ -68,14 +64,7 @@ where
) { ) {
match event { match event {
Event::Toplevel { toplevel: _ } => { Event::Toplevel { toplevel: _ } => {
state.toplevel( state.toplevel(conn, qhandle);
conn,
qhandle,
ToplevelManagerState {
manager: toplevel_manager.clone(),
_phantom: PhantomData,
},
);
} }
Event::Finished => { Event::Finished => {
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues."); 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; pub mod manager;
use self::handle::ToplevelHandleHandler; use self::handle::ToplevelHandleHandler;
use self::manager::{ToplevelManagerHandler, ToplevelManagerState}; use self::manager::ToplevelManagerHandler;
use crate::clients::wayland::Environment; use super::{Client, Environment, Event, Request, Response};
use crate::try_send;
use tokio::sync::broadcast;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use wayland_client::{Connection, QueueHandle}; use wayland_client::{Connection, QueueHandle};
use crate::send;
pub use handle::{ToplevelHandle, ToplevelInfo}; pub use handle::{ToplevelHandle, ToplevelInfo};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ToplevelEvent { pub enum ToplevelEvent {
New(ToplevelHandle), New(ToplevelInfo),
Update(ToplevelHandle), Update(ToplevelInfo),
Remove(ToplevelHandle), 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 { impl ToplevelManagerHandler for Environment {
fn toplevel( fn toplevel(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>) {
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_manager: ToplevelManagerState,
) {
debug!("Manager received new handle"); debug!("Manager received new handle");
} }
} }
impl ToplevelHandleHandler for Environment { impl ToplevelHandleHandler for Environment {
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) { 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() { match handle.info() {
Some(info) => { Some(info) => {
trace!("Adding new handle: {info:?}"); trace!("Adding new handle: {info:?}");
self.handles.insert(info.id, handle.clone()); self.handles.push(handle.clone());
send!(self.toplevel_tx, ToplevelEvent::New(handle)); if let Some(info) = handle.info() {
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::New(info)));
}
} }
None => { None => {
error!("Handle is missing information!"); error!("Handle is missing information!");
@ -55,8 +76,10 @@ impl ToplevelHandleHandler for Environment {
match handle.info() { match handle.info() {
Some(info) => { Some(info) => {
trace!("Updating handle: {info:?}"); trace!("Updating handle: {info:?}");
self.handles.insert(info.id, handle.clone()); self.handles.push(handle.clone());
send!(self.toplevel_tx, ToplevelEvent::Update(handle)); if let Some(info) = handle.info() {
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Update(info)));
}
} }
None => { None => {
error!("Handle is missing information!"); error!("Handle is missing information!");
@ -71,14 +94,10 @@ impl ToplevelHandleHandler for Environment {
handle: ToplevelHandle, handle: ToplevelHandle,
) { ) {
debug!("Handler received handle close"); debug!("Handler received handle close");
match handle.info() {
Some(info) => { self.handles.retain(|h| h != &handle);
self.handles.remove(&info.id); if let Some(info) = handle.info() {
send!(self.toplevel_tx, ToplevelEvent::Remove(handle)); try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Remove(info)));
}
None => {
error!("Handle is missing information!");
}
} }
} }
} }

View file

@ -1,9 +1,8 @@
use lazy_static::lazy_static;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env; use std::env;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Mutex; use std::sync::{Mutex, OnceLock};
use tracing::warn; use tracing::warn;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
@ -11,13 +10,15 @@ use crate::lock;
type DesktopFile = HashMap<String, Vec<String>>; type DesktopFile = HashMap<String, Vec<String>>;
lazy_static! { fn desktop_files() -> &'static Mutex<HashMap<PathBuf, DesktopFile>> {
static ref DESKTOP_FILES: Mutex<HashMap<PathBuf, DesktopFile>> = static DESKTOP_FILES: OnceLock<Mutex<HashMap<PathBuf, DesktopFile>>> = OnceLock::new();
Mutex::new(HashMap::new()); DESKTOP_FILES.get_or_init(|| Mutex::new(HashMap::new()))
}
/// These are the keys that in the cache fn desktop_files_look_out_keys() -> &'static HashSet<&'static str> {
static ref DESKTOP_FILES_LOOK_OUT_KEYS: HashSet<&'static str> = static DESKTOP_FILES_LOOK_OUT_KEYS: OnceLock<HashSet<&'static str>> = OnceLock::new();
HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]); DESKTOP_FILES_LOOK_OUT_KEYS
.get_or_init(|| HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]))
} }
/// Finds directories that should contain `.desktop` files /// Finds directories that should contain `.desktop` files
@ -104,7 +105,7 @@ fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<Path
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS` /// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> { fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
let app_id = &app_id.to_lowercase(); let app_id = &app_id.to_lowercase();
let mut desktop_files_cache = lock!(DESKTOP_FILES); let mut desktop_files_cache = lock!(desktop_files());
let files = files let files = files
.iter() .iter()
@ -171,7 +172,7 @@ fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
let key = key.trim(); let key = key.trim();
let value = value.trim(); let value = value.trim();
if DESKTOP_FILES_LOOK_OUT_KEYS.contains(key) { if desktop_files_look_out_keys().contains(key) {
Some((key, value)) Some((key, value))
} else { } else {
None None
@ -193,7 +194,7 @@ pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
return None; return None;
}; };
let mut desktop_files_cache = lock!(DESKTOP_FILES); let mut desktop_files_cache = lock!(desktop_files());
let desktop_file = match desktop_files_cache.get(&path) { let desktop_file = match desktop_files_cache.get(&path) {
Some(desktop_file) => desktop_file, Some(desktop_file) => desktop_file,

View file

@ -104,7 +104,11 @@ impl Ipc {
/// Takes an input command, runs it and returns with the appropriate response. /// Takes an input command, runs it and returns with the appropriate response.
/// ///
/// This runs on the main thread, allowing commands to interact with GTK. /// 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 { match command {
Command::Inspect => { Command::Inspect => {
gtk::Window::set_interactive_debugging(true); gtk::Window::set_interactive_debugging(true);
@ -117,7 +121,7 @@ impl Ipc {
window.close(); window.close();
} }
*ironbar.bars.borrow_mut() = crate::load_interface(application); *ironbar.bars.borrow_mut() = crate::load_interface(application, ironbar.clone());
Response::Ok Response::Ok
} }

View file

@ -9,7 +9,7 @@ use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
use std::sync::RwLock; use std::sync::RwLock;
use std::sync::{mpsc, Arc}; use std::sync::{mpsc, Arc, OnceLock};
use cfg_if::cfg_if; use cfg_if::cfg_if;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
@ -26,9 +26,8 @@ use tokio::task::{block_in_place, JoinHandle};
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use universal_config::ConfigLoader; use universal_config::ConfigLoader;
use clients::wayland;
use crate::bar::{create_bar, Bar}; use crate::bar::{create_bar, Bar};
use crate::clients::Clients;
use crate::config::{Config, MonitorConfig}; use crate::config::{Config, MonitorConfig};
use crate::error::ExitCode; use crate::error::ExitCode;
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
@ -90,26 +89,17 @@ fn run_with_args() {
} }
} }
static COUNTER: AtomicUsize = AtomicUsize::new(1);
lazy_static::lazy_static! {
static ref RUNTIME: Arc<Runtime> = Arc::new(create_runtime());
}
#[cfg(feature = "ipc")]
lazy_static::lazy_static! {
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
}
#[derive(Debug)] #[derive(Debug)]
pub struct Ironbar { pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>, bars: Rc<RefCell<Vec<Bar>>>,
clients: Rc<RefCell<Clients>>,
} }
impl Ironbar { impl Ironbar {
fn new() -> Self { fn new() -> Self {
Self { Self {
bars: Rc::new(RefCell::new(vec![])), bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())),
} }
} }
@ -124,8 +114,8 @@ impl Ironbar {
let instance = Rc::new(self); let instance = Rc::new(self);
// force start wayland client ahead of ui // force start wayland client ahead of ui
let wl = wayland::get_client(); let wl = instance.clients.borrow_mut().wayland();
lock!(wl).roundtrip(); wl.roundtrip();
app.connect_activate(move |app| { app.connect_activate(move |app| {
if running.load(Ordering::Relaxed) { if running.load(Ordering::Relaxed) {
@ -142,7 +132,7 @@ impl Ironbar {
} }
} }
*instance.bars.borrow_mut() = load_interface(app); *instance.bars.borrow_mut() = load_interface(app, instance.clone());
let style_path = env::var("IRONBAR_CSS").ok().map_or_else( let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|| { || {
@ -192,12 +182,14 @@ impl Ironbar {
/// Gets the current Tokio runtime. /// Gets the current Tokio runtime.
#[must_use] #[must_use]
pub fn runtime() -> Arc<Runtime> { pub fn runtime() -> Arc<Runtime> {
RUNTIME.clone() static RUNTIME: OnceLock<Arc<Runtime>> = OnceLock::new();
RUNTIME.get_or_init(|| Arc::new(create_runtime())).clone()
} }
/// Gets a `usize` ID value that is unique to the entire Ironbar instance. /// Gets a `usize` ID value that is unique to the entire Ironbar instance.
/// This is just a static `AtomicUsize` that increments every time this function is called. /// This is just a static `AtomicUsize` that increments every time this function is called.
pub fn unique_id() -> usize { pub fn unique_id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed) COUNTER.fetch_add(1, Ordering::Relaxed)
} }
@ -205,7 +197,10 @@ impl Ironbar {
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
#[must_use] #[must_use]
pub fn variable_manager() -> Arc<RwLock<VariableManager>> { pub fn variable_manager() -> Arc<RwLock<VariableManager>> {
VARIABLE_MANAGER.clone() static VARIABLE_MANAGER: OnceLock<Arc<RwLock<VariableManager>>> = OnceLock::new();
VARIABLE_MANAGER
.get_or_init(|| arc_rw!(VariableManager::new()))
.clone()
} }
/// Gets a clone of a bar by its unique name. /// Gets a clone of a bar by its unique name.
@ -228,7 +223,7 @@ fn start_ironbar() {
} }
/// Loads the Ironbar config and interface. /// 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 display = Display::default().map_or_else(
|| { || {
let report = Report::msg("Failed to get default GTK display"); let report = Report::msg("Failed to get default GTK display");
@ -264,7 +259,7 @@ pub fn load_interface(app: &Application) -> Vec<Bar> {
} }
} }
match create_bars(app, &display, &config) { match create_bars(app, &display, &config, &ironbar) {
Ok(bars) => { Ok(bars) => {
debug!("Created {} bars", bars.len()); debug!("Created {} bars", bars.len());
bars bars
@ -277,9 +272,14 @@ pub fn load_interface(app: &Application) -> Vec<Bar> {
} }
/// Creates each of the bars across each of the (configured) outputs. /// Creates each of the bars across each of the (configured) outputs.
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<Vec<Bar>> { fn create_bars(
let wl = wayland::get_client(); app: &Application,
let outputs = lock!(wl).get_outputs(); 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!("Received {} outputs from Wayland", outputs.len());
debug!("Outputs: {:?}", outputs); debug!("Outputs: {:?}", outputs);
@ -313,17 +313,27 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
&monitor, &monitor,
monitor_name.to_string(), monitor_name.to_string(),
config.clone(), config.clone(),
ironbar.clone(),
)?] )?]
} }
Some(MonitorConfig::Multiple(configs)) => configs Some(MonitorConfig::Multiple(configs)) => configs
.iter() .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<_>>()?, .collect::<Result<_>>()?,
None if show_default_bar => vec![create_bar( None if show_default_bar => vec![create_bar(
app, app,
&monitor, &monitor,
monitor_name.to_string(), monitor_name.to_string(),
config.clone(), config.clone(),
ironbar.clone(),
)?], )?],
None => vec![], None => vec![],
}; };
@ -364,11 +374,8 @@ where
/// This is not an `async` operation /// This is not an `async` operation
/// so can be used outside of an async function. /// so can be used outside of an async function.
/// ///
/// Do note it must be called from within a Tokio runtime still. /// Use sparingly, as this risks blocking the UI thread!
/// /// Prefer async functions wherever possible.
/// Use sparingly! Prefer async functions wherever possible.
///
/// TODO: remove all instances of this once async trait funcs are stable
pub fn await_sync<F: Future>(f: F) -> F::Output { pub fn await_sync<F: Future>(f: F) -> F::Output {
block_in_place(|| Ironbar::runtime().block_on(f)) block_in_place(|| Ironbar::runtime().block_on(f))
} }

View file

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

View file

@ -78,9 +78,10 @@ impl Module<Button> for ClockModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: mpsc::Receiver<Self::ReceiveMessage>, _rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let tx = context.tx.clone();
spawn(async move { spawn(async move {
loop { loop {
let date = Local::now(); let date = Local::now();

View file

@ -158,9 +158,10 @@ impl Module<gtk::Box> for CustomModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>, mut rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let tx = context.tx.clone();
spawn(async move { spawn(async move {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
if event.cmd.starts_with('!') { if event.cmd.starts_with('!') {

View file

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

View file

@ -36,9 +36,10 @@ impl Module<Label> for LabelModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: mpsc::Receiver<Self::ReceiveMessage>, _rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let tx = context.tx.clone();
dynamic_string(&self.label, move |string| { dynamic_string(&self.label, move |string| {
try_send!(tx, ModuleUpdateEvent::Update(string)); try_send!(tx, ModuleUpdateEvent::Update(string));
}); });

View file

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

View file

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

View file

@ -1,6 +1,7 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt::Debug; use std::fmt::Debug;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use color_eyre::Result; use color_eyre::Result;
use glib::IsA; use glib::IsA;
@ -10,10 +11,11 @@ use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widge
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use tracing::debug; use tracing::debug;
use crate::clients::ProvidesClient;
use crate::config::{BarPosition, CommonConfig, TransitionType}; use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry}; use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup; use crate::popup::Popup;
use crate::{glib_recv_mpsc, send}; use crate::{glib_recv_mpsc, send, Ironbar};
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub mod clipboard; pub mod clipboard;
@ -76,6 +78,7 @@ where
TSend: Clone, TSend: Clone,
{ {
pub id: usize, pub id: usize,
pub ironbar: Rc<Ironbar>,
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>, pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
pub update_tx: broadcast::Sender<TSend>, pub update_tx: broadcast::Sender<TSend>,
pub controller_tx: mpsc::Sender<TReceive>, pub controller_tx: mpsc::Sender<TReceive>,
@ -87,6 +90,18 @@ impl<TSend, TReceive> WidgetContext<TSend, TReceive>
where where
TSend: Clone, 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> { pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
self.update_tx.subscribe() self.update_tx.subscribe()
} }
@ -162,7 +177,7 @@ where
fn spawn_controller( fn spawn_controller(
&self, &self,
info: &ModuleInfo, info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
rx: mpsc::Receiver<Self::ReceiveMessage>, rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> ) -> Result<()>
where where
@ -194,6 +209,7 @@ where
pub fn create_module<TModule, TWidget, TSend, TRec>( pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule, module: TModule,
id: usize, id: usize,
ironbar: Rc<Ironbar>,
name: Option<String>, name: Option<String>,
info: &ModuleInfo, info: &ModuleInfo,
popup: &Rc<RefCell<Popup>>, popup: &Rc<RefCell<Popup>>,
@ -208,16 +224,17 @@ where
let (tx, rx) = broadcast::channel(64); let (tx, rx) = broadcast::channel(64);
module.spawn_controller(info, ui_tx.clone(), controller_rx)?;
let context = WidgetContext { let context = WidgetContext {
id, id,
ironbar,
tx: ui_tx, tx: ui_tx,
update_tx: tx.clone(), update_tx: tx.clone(),
controller_tx, controller_tx,
_update_rx: rx, _update_rx: rx,
}; };
module.spawn_controller(info, &context, controller_rx)?;
let module_name = TModule::name(); let module_name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string()); let instance_name = name.unwrap_or_else(|| module_name.to_string());

View file

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

View file

@ -6,7 +6,7 @@ use color_eyre::{Help, Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc;
use tracing::error; use tracing::error;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@ -55,11 +55,12 @@ impl Module<Label> for ScriptModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: Receiver<Self::ReceiveMessage>, _rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let script: Script = self.into(); let script: Script = self.into();
let tx = context.tx.clone();
spawn(async move { spawn(async move {
script.run(None, move |out, _| match out { script.run(None, move |out, _| match out {
OutputStream::Stdout(stdout) => { OutputStream::Stdout(stdout) => {

View file

@ -11,7 +11,6 @@ use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt}; use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::time::sleep; use tokio::time::sleep;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@ -124,8 +123,8 @@ impl Module<gtk::Box> for SysInfoModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: Receiver<Self::ReceiveMessage>, _rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let interval = self.interval; let interval = self.interval;
@ -159,6 +158,7 @@ impl Module<gtk::Box> for SysInfoModule {
spawn_refresh!(RefreshType::Network, networks); spawn_refresh!(RefreshType::Network, networks);
spawn_refresh!(RefreshType::System, system); spawn_refresh!(RefreshType::System, system);
let tx = context.tx.clone();
spawn(async move { spawn(async move {
let mut format_info = HashMap::new(); let mut format_info = HashMap::new();

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::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; 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 color_eyre::Result;
use glib::ffi::g_strfreev; use glib::ffi::g_strfreev;
use glib::translate::ToGlibPtr; use glib::translate::ToGlibPtr;
@ -168,10 +168,13 @@ impl Module<MenuBar> for TrayModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: Receiver<Self::ReceiveMessage>, mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> 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(); let (tray_tx, mut tray_rx) = client.subscribe();
// listen to tray updates // listen to tray updates

View file

@ -6,8 +6,8 @@ use serde::Deserialize;
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use upower_dbus::BatteryState; use upower_dbus::BatteryState;
use zbus; use zbus;
use zbus::fdo::PropertiesProxy;
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider; use crate::image::ImageProvider;
@ -15,7 +15,7 @@ use crate::modules::PopupButton;
use crate::modules::{ use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext, 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 DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60; const HOUR: i64 = 60 * 60;
@ -61,12 +61,14 @@ impl Module<gtk::Button> for UpowerModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: mpsc::Receiver<Self::ReceiveMessage>, _rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let tx = context.tx.clone();
let display_proxy = context.client::<PropertiesProxy>();
spawn(async move { 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 mut prop_changed_stream = display_proxy.receive_properties_changed().await?;
let device_interface_name = 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::config::CommonConfig;
use crate::image::new_icon_button; use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
@ -98,7 +98,7 @@ fn create_button(
} }
if !visibility.is_visible() { if !visibility.is_visible() {
style_context.add_class("inactive") style_context.add_class("inactive");
} }
{ {
@ -151,16 +151,14 @@ impl Module<gtk::Box> for WorkspacesModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>, context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: Receiver<Self::ReceiveMessage>, mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let tx = context.tx.clone();
let client = context.ironbar.clients.borrow_mut().workspaces();
// Subscribe & send events // Subscribe & send events
spawn(async move { spawn(async move {
let mut srx = { let mut srx = client.subscribe_workspace_change();
let client =
Compositor::get_workspace_client().expect("Failed to get workspace client");
client.subscribe_workspace_change()
};
trace!("Set up workspace subscription"); trace!("Set up workspace subscription");
@ -170,13 +168,13 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
}); });
let client = context.client::<dyn WorkspaceClient>();
// Change workspace focus // Change workspace focus
spawn(async move { spawn(async move {
trace!("Setting up UI event handler"); trace!("Setting up UI event handler");
while let Some(name) = rx.recv().await { while let Some(name) = rx.recv().await {
let client =
Compositor::get_workspace_client().expect("Failed to get workspace client");
client.focus(name)?; client.focus(name)?;
} }