mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-04-19 19:34:24 +02:00
Major module refactor (#19)
* refactor: major module restructuring Modules now implement a "controller", which allows for separation of logic from UI code and enforces a tighter structure around how modules should be written. The introduction of this change required major refactoring or even rewriting of all modules. This also better integrates the popup into modules, making it easier for data to be passed around without fetching the same thing twice The refactor also improves some client code, switching from `ksway` to the much more stable `swayipc-async`. Partial multi-monitor for the tray module has been added. BREAKING CHANGE: The `mpd` module config has changed, moving the icons to their own object.
This commit is contained in:
parent
daafa0943e
commit
720ba7bfb0
26 changed files with 2381 additions and 1846 deletions
499
Cargo.lock
generated
499
Cargo.lock
generated
|
@ -94,6 +94,26 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-io"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"concurrent-queue",
|
||||||
|
"futures-lite",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"parking",
|
||||||
|
"polling",
|
||||||
|
"slab",
|
||||||
|
"socket2",
|
||||||
|
"waker-fn",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-lock"
|
name = "async-lock"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
@ -103,15 +123,25 @@ dependencies = [
|
||||||
"event-listener",
|
"event-listener",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-pidfd"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12177058299bb8e3507695941b6d0d7dc0e4e6515b8bc1bf4609d9e32ef51799"
|
||||||
|
dependencies = [
|
||||||
|
"async-io",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-recursion"
|
name = "async-recursion"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
|
checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -126,11 +156,17 @@ version = "0.1.56"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
|
checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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.15.1"
|
version = "0.15.1"
|
||||||
|
@ -180,7 +216,7 @@ checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"addr2line",
|
"addr2line",
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
"object",
|
"object",
|
||||||
|
@ -202,15 +238,6 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bstr"
|
|
||||||
version = "0.2.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
|
@ -268,12 +295,6 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "0.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -318,9 +339,9 @@ checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -391,7 +412,7 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b6981753b68f7642c3737b302cd37dee779189fcdad975a69d6a7bb165f134e"
|
checksum = "0b6981753b68f7642c3737b302cd37dee779189fcdad975a69d6a7bb165f134e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"clap",
|
"clap",
|
||||||
"colored",
|
"colored",
|
||||||
"pest",
|
"pest",
|
||||||
|
@ -411,23 +432,14 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-channel"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils 0.6.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
|
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"crossbeam-utils 0.8.11",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -436,9 +448,9 @@ version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
|
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"crossbeam-epoch",
|
"crossbeam-epoch",
|
||||||
"crossbeam-utils 0.8.11",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -448,30 +460,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1"
|
checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"crossbeam-utils 0.8.11",
|
"crossbeam-utils",
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-utils"
|
|
||||||
version = "0.6.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 0.1.10",
|
|
||||||
"lazy_static",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
|
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -485,29 +487,81 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derivative"
|
name = "derivative"
|
||||||
version = "2.2.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 = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_builder"
|
||||||
version = "0.15.0"
|
version = "0.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a141330240c921ec6d074a3e188a7c7ef95668bb95e7d44fa0e5778ec2a7afe"
|
checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"derive_builder_macro",
|
||||||
"proc-macro2 0.4.30",
|
]
|
||||||
"quote 0.6.13",
|
|
||||||
"regex",
|
[[package]]
|
||||||
"rustc_version 0.2.3",
|
name = "derive_builder_core"
|
||||||
"syn 0.15.44",
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -562,9 +616,9 @@ version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae"
|
checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -599,7 +653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92"
|
checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"rustc_version 0.3.3",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -608,7 +662,7 @@ version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
|
checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
|
@ -682,9 +736,9 @@ version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
|
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -789,7 +843,7 @@ version = "0.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
|
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
]
|
]
|
||||||
|
@ -860,9 +914,9 @@ dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -875,29 +929,6 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "globset"
|
|
||||||
version = "0.4.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"bstr",
|
|
||||||
"fnv",
|
|
||||||
"log",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "globwalk"
|
|
||||||
version = "0.7.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9db17aec586697a93219b19726b5b68307eba92898c34b170857343fe67c99d"
|
|
||||||
dependencies = [
|
|
||||||
"ignore",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.15.10"
|
version = "0.15.10"
|
||||||
|
@ -987,9 +1018,9 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1020,22 +1051,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ignore"
|
name = "ident_case"
|
||||||
version = "0.4.18"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils 0.8.11",
|
|
||||||
"globset",
|
|
||||||
"lazy_static",
|
|
||||||
"log",
|
|
||||||
"memchr",
|
|
||||||
"regex",
|
|
||||||
"same-file",
|
|
||||||
"thread_local",
|
|
||||||
"walkdir",
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indenter"
|
name = "indenter"
|
||||||
|
@ -1079,23 +1098,23 @@ version = "0.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironbar"
|
name = "ironbar"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async_once",
|
||||||
"chrono",
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"cornfig",
|
"cornfig",
|
||||||
"crossbeam-channel 0.5.6",
|
"derive_builder",
|
||||||
"dirs",
|
"dirs",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"glib",
|
"glib",
|
||||||
"gtk",
|
"gtk",
|
||||||
"gtk-layer-shell",
|
"gtk-layer-shell",
|
||||||
"ksway",
|
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"mpd_client",
|
"mpd_client",
|
||||||
"notify",
|
"notify",
|
||||||
|
@ -1105,6 +1124,7 @@ dependencies = [
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"stray",
|
"stray",
|
||||||
"strip-ansi-escapes",
|
"strip-ansi-escapes",
|
||||||
|
"swayipc-async",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
@ -1115,15 +1135,6 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.8.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
@ -1150,23 +1161,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ksway"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db7bda3675f034f78f763af40e633bcb0529c022e4d2ee063944dab9777004c5"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
"crossbeam-channel 0.3.9",
|
|
||||||
"derive_more",
|
|
||||||
"globwalk",
|
|
||||||
"itertools",
|
|
||||||
"num-derive",
|
|
||||||
"num-traits",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -1195,7 +1189,7 @@ version = "0.4.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1282,7 +1276,7 @@ checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"memoffset",
|
"memoffset",
|
||||||
]
|
]
|
||||||
|
@ -1304,7 +1298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a"
|
checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"crossbeam-channel 0.5.6",
|
"crossbeam-channel",
|
||||||
"filetime",
|
"filetime",
|
||||||
"fsevent-sys",
|
"fsevent-sys",
|
||||||
"inotify",
|
"inotify",
|
||||||
|
@ -1324,17 +1318,6 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-derive"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2 0.4.30",
|
|
||||||
"quote 0.6.13",
|
|
||||||
"syn 0.15.44",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.45"
|
version = "0.1.45"
|
||||||
|
@ -1457,7 +1440,7 @@ version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
|
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
@ -1492,9 +1475,9 @@ checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1526,6 +1509,20 @@ version = "0.3.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polling"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"wepoll-ffi",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
@ -1549,9 +1546,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-error-attr",
|
"proc-macro-error-attr",
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1561,20 +1558,11 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "0.4.30"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-xid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.42"
|
version = "1.0.42"
|
||||||
|
@ -1584,22 +1572,13 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "0.6.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2 0.4.30",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.20"
|
version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
|
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1650,9 +1629,9 @@ version = "1.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
|
checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel 0.5.6",
|
"crossbeam-channel",
|
||||||
"crossbeam-deque",
|
"crossbeam-deque",
|
||||||
"crossbeam-utils 0.8.11",
|
"crossbeam-utils",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1717,22 +1696,13 @@ version = "0.1.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc_version"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
|
|
||||||
dependencies = [
|
|
||||||
"semver 0.9.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
|
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"semver 0.11.0",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1756,30 +1726,15 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
|
||||||
dependencies = [
|
|
||||||
"semver-parser 0.7.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"semver-parser 0.10.2",
|
"semver-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver-parser"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver-parser"
|
name = "semver-parser"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
@ -1804,9 +1759,9 @@ version = "1.0.143"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391"
|
checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1826,9 +1781,9 @@ version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed"
|
checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1850,7 +1805,7 @@ version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
|
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
@ -1913,16 +1868,17 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stray"
|
name = "stray"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/JakeStanger/stray.git?branch=fix/tracing#719d921b769f85772caa181c4469e0fedd61df87"
|
||||||
checksum = "e5f063390cf8e8b633159bd4fa682ad45508e56a40d9ac341cb322cfac626d56"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chrono",
|
"chrono",
|
||||||
"event-listener",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"tracing",
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1942,14 +1898,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "swayipc-async"
|
||||||
version = "0.15.44"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/JakeStanger/swayipc-rs.git?branch=feat/derive-clone#6464c41ac8329381f2171dcb38329bff29958baf"
|
||||||
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 0.4.30",
|
"async-io",
|
||||||
"quote 0.6.13",
|
"async-pidfd",
|
||||||
"unicode-xid",
|
"futures-lite",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"swayipc-types",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "swayipc-types"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "git+https://github.com/JakeStanger/swayipc-rs.git?branch=feat/derive-clone#6464c41ac8329381f2171dcb38329bff29958baf"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1958,8 +1926,8 @@ version = "1.0.98"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
|
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1969,7 +1937,7 @@ version = "0.26.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ae2421f3e16b3afd4aa692d23b83d0ba42ee9b0081d5deeb7d21428d7195fb1"
|
checksum = "4ae2421f3e16b3afd4aa692d23b83d0ba42ee9b0081d5deeb7d21428d7195fb1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"ntapi",
|
"ntapi",
|
||||||
|
@ -1997,7 +1965,7 @@ version = "3.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
|
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
|
@ -2035,9 +2003,9 @@ version = "1.0.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
|
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2073,9 +2041,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.20.1"
|
version = "1.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
|
checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -2096,9 +2064,9 @@ version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
|
checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2127,7 +2095,7 @@ version = "0.1.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
|
checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
|
@ -2139,7 +2107,7 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e"
|
checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel 0.5.6",
|
"crossbeam-channel",
|
||||||
"time 0.3.13",
|
"time 0.3.13",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
@ -2150,9 +2118,9 @@ version = "0.1.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
|
checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2232,12 +2200,6 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-xid"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -2285,8 +2247,8 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
|
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2318,6 +2280,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wepoll-ffi"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@ -2439,10 +2410,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f8fb5186d1c87ae88cf234974c240671238b4a679158ad3b94ec465237349a6"
|
checksum = "1f8fb5186d1c87ae88cf234974c240671238b4a679158ad3b94ec465237349a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2477,7 +2448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08e977eaa3af652f63d479ce50d924254ad76722a6289ec1a1eac3231ca30430"
|
checksum = "08e977eaa3af652f63d479ce50d924254ad76722a6289ec1a1eac3231ca30430"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2 1.0.42",
|
"proc-macro2",
|
||||||
"quote 1.0.20",
|
"quote",
|
||||||
"syn 1.0.98",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -8,10 +8,11 @@ description = "Customisable wlroots/sway bar"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
derive_builder = "0.11.2"
|
||||||
gtk = "0.15.5"
|
gtk = "0.15.5"
|
||||||
gtk-layer-shell = "0.4.1"
|
gtk-layer-shell = "0.4.1"
|
||||||
glib = "0.15.12"
|
glib = "0.15.12"
|
||||||
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "time"] }
|
tokio = { version = "1.21.0", features = ["macros", "rt-multi-thread", "time"] }
|
||||||
tracing = "0.1.36"
|
tracing = "0.1.36"
|
||||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
|
@ -26,13 +27,12 @@ serde_yaml = "0.9.4"
|
||||||
toml = "0.5.9"
|
toml = "0.5.9"
|
||||||
cornfig = "0.3.0"
|
cornfig = "0.3.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
async_once = "0.2.6"
|
||||||
regex = "1.6.0"
|
regex = "1.6.0"
|
||||||
stray = "0.1.1"
|
stray = { git = "https://github.com/JakeStanger/stray.git", branch = "fix/tracing" }
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
notify = "5.0.0"
|
notify = "5.0.0"
|
||||||
mpd_client = "1.0.0"
|
mpd_client = "1.0.0"
|
||||||
ksway = "0.1.0"
|
swayipc-async = { git = "https://github.com/JakeStanger/swayipc-rs.git", branch = "feat/derive-clone" }
|
||||||
sysinfo = "0.26.2"
|
sysinfo = "0.26.2"
|
||||||
# required for wrapping ksway
|
|
||||||
crossbeam-channel = "0.5.6"
|
|
177
src/bar.rs
177
src/bar.rs
|
@ -1,10 +1,21 @@
|
||||||
|
use crate::bridge_channel::BridgeChannel;
|
||||||
use crate::config::{BarPosition, ModuleConfig};
|
use crate::config::{BarPosition, ModuleConfig};
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleLocation};
|
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||||
|
use crate::modules::mpd::{PlayerCommand, SongUpdate};
|
||||||
|
use crate::modules::workspaces::WorkspaceUpdate;
|
||||||
|
use crate::modules::{Module, ModuleInfoBuilder, ModuleLocation, ModuleUpdateEvent, WidgetContext};
|
||||||
|
use crate::popup::Popup;
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use gtk::gdk::Monitor;
|
use gtk::gdk::Monitor;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Application, ApplicationWindow, Orientation};
|
use gtk::{Application, ApplicationWindow, Orientation};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use stray::message::NotifierItemCommand;
|
||||||
|
use stray::NotifierItemMessage;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
/// Creates a new window for a bar,
|
/// Creates a new window for a bar,
|
||||||
|
@ -65,40 +76,29 @@ fn load_modules(
|
||||||
monitor: &Monitor,
|
monitor: &Monitor,
|
||||||
output_name: &str,
|
output_name: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(modules) = config.left {
|
let mut info_builder = ModuleInfoBuilder::default();
|
||||||
let info = ModuleInfo {
|
let info_builder = info_builder
|
||||||
app,
|
.app(app)
|
||||||
location: ModuleLocation::Left,
|
.bar_position(&config.position)
|
||||||
bar_position: &config.position,
|
.monitor(monitor)
|
||||||
monitor,
|
.output_name(output_name);
|
||||||
output_name,
|
|
||||||
};
|
|
||||||
|
|
||||||
add_modules(left, modules, &info)?;
|
if let Some(modules) = config.left {
|
||||||
|
let info_builder = info_builder.location(ModuleLocation::Left);
|
||||||
|
|
||||||
|
add_modules(left, modules, info_builder)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(modules) = config.center {
|
if let Some(modules) = config.center {
|
||||||
let info = ModuleInfo {
|
let info_builder = info_builder.location(ModuleLocation::Center);
|
||||||
app,
|
|
||||||
location: ModuleLocation::Center,
|
|
||||||
bar_position: &config.position,
|
|
||||||
monitor,
|
|
||||||
output_name,
|
|
||||||
};
|
|
||||||
|
|
||||||
add_modules(center, modules, &info)?;
|
add_modules(center, modules, info_builder)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(modules) = config.right {
|
if let Some(modules) = config.right {
|
||||||
let info = ModuleInfo {
|
let info_builder = info_builder.location(ModuleLocation::Right);
|
||||||
app,
|
|
||||||
location: ModuleLocation::Right,
|
|
||||||
bar_position: &config.position,
|
|
||||||
monitor,
|
|
||||||
output_name,
|
|
||||||
};
|
|
||||||
|
|
||||||
add_modules(right, modules, &info)?;
|
add_modules(right, modules, info_builder)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -106,33 +106,124 @@ fn load_modules(
|
||||||
|
|
||||||
/// Adds modules into a provided GTK box,
|
/// Adds modules into a provided GTK box,
|
||||||
/// which should be one of its left, center or right containers.
|
/// which should be one of its left, center or right containers.
|
||||||
fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
|
fn add_modules(
|
||||||
|
content: >k::Box,
|
||||||
|
modules: Vec<ModuleConfig>,
|
||||||
|
info_builder: &mut ModuleInfoBuilder,
|
||||||
|
) -> Result<()> {
|
||||||
|
let base_popup_info = info_builder.module_name("").build()?;
|
||||||
|
let popup = Popup::new(&base_popup_info);
|
||||||
|
let popup = Arc::new(RwLock::new(popup));
|
||||||
|
|
||||||
macro_rules! add_module {
|
macro_rules! add_module {
|
||||||
($module:expr, $name:literal) => {{
|
($module:expr, $id:expr, $name:literal, $send_message:ty, $receive_message:ty) => {
|
||||||
let widget = $module.into_widget(&info)?;
|
let info = info_builder.module_name($name).build()?;
|
||||||
widget.set_widget_name($name);
|
|
||||||
content.add(&widget);
|
let (w_tx, w_rx) = glib::MainContext::channel::<$send_message>(glib::PRIORITY_DEFAULT);
|
||||||
debug!("Added module of type {}", $name);
|
let (p_tx, p_rx) = glib::MainContext::channel::<$send_message>(glib::PRIORITY_DEFAULT);
|
||||||
}};
|
|
||||||
|
let channel = BridgeChannel::<ModuleUpdateEvent<$send_message>>::new();
|
||||||
|
let (ui_tx, ui_rx) = mpsc::channel::<$receive_message>(16);
|
||||||
|
|
||||||
|
$module.spawn_controller(&info, channel.create_sender(), ui_rx)?;
|
||||||
|
|
||||||
|
let context = WidgetContext {
|
||||||
|
id: $id,
|
||||||
|
widget_rx: w_rx,
|
||||||
|
popup_rx: p_rx,
|
||||||
|
tx: channel.create_sender(),
|
||||||
|
controller_tx: ui_tx,
|
||||||
|
};
|
||||||
|
|
||||||
|
let widget = $module.into_widget(context, &info)?;
|
||||||
|
|
||||||
|
content.add(&widget.widget);
|
||||||
|
widget.widget.set_widget_name(info.module_name);
|
||||||
|
|
||||||
|
let has_popup = widget.popup.is_some();
|
||||||
|
if let Some(popup_content) = widget.popup {
|
||||||
|
popup
|
||||||
|
.write()
|
||||||
|
.expect("Failed to get write lock on popup")
|
||||||
|
.register_content($id, popup_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
let popup2 = Arc::clone(&popup);
|
||||||
|
channel.recv(move |ev| {
|
||||||
|
let popup = popup2.clone();
|
||||||
|
match ev {
|
||||||
|
ModuleUpdateEvent::Update(update) => {
|
||||||
|
if has_popup {
|
||||||
|
p_tx.send(update.clone())
|
||||||
|
.expect("Failed to send update to popup");
|
||||||
|
}
|
||||||
|
|
||||||
|
w_tx.send(update).expect("Failed to send update to module");
|
||||||
|
}
|
||||||
|
ModuleUpdateEvent::TogglePopup((x, w)) => {
|
||||||
|
debug!("Toggling popup for {} [#{}]", $name, $id);
|
||||||
|
let popup = popup.read().expect("Failed to get read lock on popup");
|
||||||
|
if popup.is_visible() {
|
||||||
|
popup.hide()
|
||||||
|
} else {
|
||||||
|
popup.show_content($id);
|
||||||
|
popup.show(x, w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ModuleUpdateEvent::OpenPopup((x, w)) => {
|
||||||
|
debug!("Opening popup for {} [#{}]", $name, $id);
|
||||||
|
|
||||||
|
let popup = popup.read().expect("Failed to get read lock on popup");
|
||||||
|
popup.hide();
|
||||||
|
popup.show(x, w);
|
||||||
|
popup.show_content($id);
|
||||||
|
}
|
||||||
|
ModuleUpdateEvent::ClosePopup => {
|
||||||
|
debug!("Closing popup for {} [#{}]", $name, $id);
|
||||||
|
|
||||||
|
let popup = popup.read().expect("Failed to get read lock on popup");
|
||||||
|
popup.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for config in modules {
|
for (id, config) in modules.into_iter().enumerate() {
|
||||||
match config {
|
match config {
|
||||||
ModuleConfig::Clock(module) => add_module!(module, "clock"),
|
ModuleConfig::Clock(module) => {
|
||||||
ModuleConfig::Mpd(module) => add_module!(module, "mpd"),
|
add_module!(module, id, "clock", DateTime<Local>, ());
|
||||||
ModuleConfig::Tray(module) => add_module!(module, "tray"),
|
}
|
||||||
ModuleConfig::Workspaces(module) => add_module!(module, "workspaces"),
|
ModuleConfig::Script(module) => {
|
||||||
ModuleConfig::SysInfo(module) => add_module!(module, "sysinfo"),
|
add_module!(module, id, "script", String, ());
|
||||||
ModuleConfig::Launcher(module) => add_module!(module, "launcher"),
|
}
|
||||||
ModuleConfig::Script(module) => add_module!(module, "script"),
|
ModuleConfig::SysInfo(module) => {
|
||||||
ModuleConfig::Focused(module) => add_module!(module, "focused"),
|
add_module!(module, id, "sysinfo", HashMap<String, String>, ());
|
||||||
|
}
|
||||||
|
ModuleConfig::Focused(module) => {
|
||||||
|
add_module!(module, id, "focused", (String, String), ());
|
||||||
|
}
|
||||||
|
ModuleConfig::Workspaces(module) => {
|
||||||
|
add_module!(module, id, "workspaces", WorkspaceUpdate, String);
|
||||||
|
}
|
||||||
|
ModuleConfig::Tray(module) => {
|
||||||
|
add_module!(module, id, "tray", NotifierItemMessage, NotifierItemCommand);
|
||||||
|
}
|
||||||
|
ModuleConfig::Mpd(module) => {
|
||||||
|
add_module!(module, id, "mpd", Option<SongUpdate>, PlayerCommand);
|
||||||
|
}
|
||||||
|
ModuleConfig::Launcher(module) => {
|
||||||
|
add_module!(module, id, "launcher", LauncherUpdate, ItemEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up GTK layer shell for a provided aplication window.
|
/// Sets up GTK layer shell for a provided application window.
|
||||||
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
|
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
|
||||||
gtk_layer_shell::init_for_window(win);
|
gtk_layer_shell::init_for_window(win);
|
||||||
gtk_layer_shell::set_monitor(win, monitor);
|
gtk_layer_shell::set_monitor(win, monitor);
|
||||||
|
|
43
src/bridge_channel.rs
Normal file
43
src/bridge_channel.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
/// MPSC async -> sync channel.
|
||||||
|
/// The sender uses `tokio::sync::mpsc`
|
||||||
|
/// while the receiver uses `glib::MainContext::channel`.
|
||||||
|
///
|
||||||
|
/// This makes it possible to send events asynchronously
|
||||||
|
/// and receive them on the main thread,
|
||||||
|
/// allowing UI updates to be handled on the receiving end.
|
||||||
|
pub struct BridgeChannel<T> {
|
||||||
|
async_tx: mpsc::Sender<T>,
|
||||||
|
sync_rx: glib::Receiver<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + 'static> BridgeChannel<T> {
|
||||||
|
/// Creates a new channel
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (async_tx, mut async_rx) = mpsc::channel(32);
|
||||||
|
let (sync_tx, sync_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(val) = async_rx.recv().await {
|
||||||
|
sync_tx.send(val).expect("Failed to send message");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { async_tx, sync_rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a clone of the sender.
|
||||||
|
pub fn create_sender(&self) -> mpsc::Sender<T> {
|
||||||
|
self.async_tx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches a callback to the receiver.
|
||||||
|
pub fn recv<F>(self, f: F) -> glib::SourceId
|
||||||
|
where
|
||||||
|
F: FnMut(T) -> glib::Continue + 'static,
|
||||||
|
{
|
||||||
|
self.sync_rx.attach(None, f)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::slice::{Iter, IterMut};
|
use std::slice::{Iter, IterMut};
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
/// An ordered map.
|
/// An ordered map.
|
||||||
/// Internally this is just two vectors -
|
/// Internally this is just two vectors -
|
||||||
|
@ -47,6 +48,11 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a value for the given key exists inside the collection
|
||||||
|
pub fn contains(&self, key: &TKey) -> bool {
|
||||||
|
self.keys.contains(key)
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes the key/value from the collection
|
/// Removes the key/value from the collection
|
||||||
/// if it exists
|
/// if it exists
|
||||||
/// and returns the removed value.
|
/// and returns the removed value.
|
||||||
|
@ -144,3 +150,12 @@ impl<TKey: PartialEq, TData> Default for Collection<TKey, TData> {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<TKey: PartialEq, TData> IntoIterator for Collection<TKey, TData> {
|
||||||
|
type Item = TData;
|
||||||
|
type IntoIter = vec::IntoIter<TData>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.values.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
53
src/main.rs
53
src/main.rs
|
@ -1,4 +1,5 @@
|
||||||
mod bar;
|
mod bar;
|
||||||
|
mod bridge_channel;
|
||||||
mod collection;
|
mod collection;
|
||||||
mod config;
|
mod config;
|
||||||
mod icon;
|
mod icon;
|
||||||
|
@ -11,16 +12,18 @@ mod sway;
|
||||||
use crate::bar::create_bar;
|
use crate::bar::create_bar;
|
||||||
use crate::config::{Config, MonitorConfig};
|
use crate::config::{Config, MonitorConfig};
|
||||||
use crate::style::load_css;
|
use crate::style::load_css;
|
||||||
use crate::sway::{get_client, SwayOutput};
|
use crate::sway::get_client;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use dirs::config_dir;
|
use dirs::config_dir;
|
||||||
use gtk::gdk::Display;
|
use gtk::gdk::Display;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Application;
|
use gtk::Application;
|
||||||
use ksway::IpcCommand;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::future::Future;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
use tokio::task::block_in_place;
|
||||||
|
|
||||||
use crate::logging::install_tracing;
|
use crate::logging::install_tracing;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
@ -48,14 +51,14 @@ async fn main() -> Result<()> {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
app.connect_activate(move |app| {
|
||||||
let display = match Display::default() {
|
let display = Display::default().map_or_else(
|
||||||
Some(display) => display,
|
|| {
|
||||||
None => {
|
|
||||||
let report = Report::msg("Failed to get default GTK display");
|
let report = Report::msg("Failed to get default GTK display");
|
||||||
error!("{:?}", report);
|
error!("{:?}", report);
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
},
|
||||||
};
|
|display| display,
|
||||||
|
);
|
||||||
|
|
||||||
let config = match Config::load() {
|
let config = match Config::load() {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
|
@ -66,21 +69,21 @@ async fn main() -> Result<()> {
|
||||||
};
|
};
|
||||||
debug!("Loaded config file");
|
debug!("Loaded config file");
|
||||||
|
|
||||||
if let Err(err) = create_bars(app, &display, &config) {
|
if let Err(err) = await_sync(create_bars(app, &display, &config)) {
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
exit(2);
|
exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Created bars");
|
debug!("Created bars");
|
||||||
|
|
||||||
let style_path = match config_dir() {
|
let style_path = config_dir().map_or_else(
|
||||||
Some(dir) => dir.join("ironbar").join("style.css"),
|
|| {
|
||||||
None => {
|
|
||||||
let report = Report::msg("Failed to locate user config dir");
|
let report = Report::msg("Failed to locate user config dir");
|
||||||
error!("{:?}", report);
|
error!("{:?}", report);
|
||||||
exit(3);
|
exit(3);
|
||||||
}
|
},
|
||||||
};
|
|dir| dir.join("ironbar").join("style.css"),
|
||||||
|
);
|
||||||
|
|
||||||
if style_path.exists() {
|
if style_path.exists() {
|
||||||
load_css(style_path);
|
load_css(style_path);
|
||||||
|
@ -96,12 +99,12 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<()> {
|
async fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
|
||||||
let outputs = {
|
let outputs = {
|
||||||
let sway = get_client();
|
let sway = get_client().await;
|
||||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
let mut sway = sway.lock().await;
|
||||||
|
|
||||||
let outputs = sway.ipc(IpcCommand::GetOutputs);
|
let outputs = sway.get_outputs().await;
|
||||||
|
|
||||||
match outputs {
|
match outputs {
|
||||||
Ok(outputs) => Ok(outputs),
|
Ok(outputs) => Ok(outputs),
|
||||||
|
@ -109,8 +112,6 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
|
||||||
}
|
}
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)?;
|
|
||||||
|
|
||||||
debug!("Received {} outputs from Sway IPC", outputs.len());
|
debug!("Received {} outputs from Sway IPC", outputs.len());
|
||||||
|
|
||||||
let num_monitors = display.n_monitors();
|
let num_monitors = display.n_monitors();
|
||||||
|
@ -121,7 +122,7 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
|
||||||
|
|
||||||
info!("Creating bar on '{}'", monitor_name);
|
info!("Creating bar on '{}'", monitor_name);
|
||||||
|
|
||||||
// TODO: Could we use an Arc<Config> here to avoid cloning?
|
// TODO: Could we use an Arc<Config> or `Cow<Config>` here to avoid cloning?
|
||||||
config.monitors.as_ref().map_or_else(
|
config.monitors.as_ref().map_or_else(
|
||||||
|| create_bar(app, &monitor, monitor_name, config.clone()),
|
|| create_bar(app, &monitor, monitor_name, config.clone()),
|
||||||
|config| {
|
|config| {
|
||||||
|
@ -145,3 +146,15 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Blocks on a `Future` until it resolves.
|
||||||
|
///
|
||||||
|
/// This is not an `async` operation
|
||||||
|
/// so can be used outside of an async function.
|
||||||
|
///
|
||||||
|
/// Do note it must be called from within a Tokio runtime still.
|
||||||
|
///
|
||||||
|
/// Use sparingly! Prefer async functions wherever possible.
|
||||||
|
pub fn await_sync<F: Future>(f: F) -> F::Output {
|
||||||
|
block_in_place(|| Handle::current().block_on(f))
|
||||||
|
}
|
||||||
|
|
114
src/modules/clock.rs
Normal file
114
src/modules/clock.rs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
|
use crate::popup::Popup;
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use color_eyre::Result;
|
||||||
|
use glib::Continue;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{Align, Button, Calendar, Label, Orientation};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ClockModule {
|
||||||
|
/// Date/time format string.
|
||||||
|
/// Default: `%d/%m/%Y %H:%M`
|
||||||
|
///
|
||||||
|
/// Detail on available tokens can be found here:
|
||||||
|
/// <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
|
||||||
|
#[serde(default = "default_format")]
|
||||||
|
pub(crate) format: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_format() -> String {
|
||||||
|
String::from("%d/%m/%Y %H:%M")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module<Button> for ClockModule {
|
||||||
|
type SendMessage = DateTime<Local>;
|
||||||
|
type ReceiveMessage = ();
|
||||||
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
let date = Local::now();
|
||||||
|
tx.send(ModuleUpdateEvent::Update(date))
|
||||||
|
.await
|
||||||
|
.expect("Failed to send date");
|
||||||
|
sleep(tokio::time::Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<Button>> {
|
||||||
|
let button = Button::new();
|
||||||
|
|
||||||
|
button.connect_clicked(move |button| {
|
||||||
|
context
|
||||||
|
.tx
|
||||||
|
.try_send(ModuleUpdateEvent::TogglePopup(Popup::button_pos(button)))
|
||||||
|
.expect("Failed to toggle popup");
|
||||||
|
});
|
||||||
|
|
||||||
|
let format = self.format.clone();
|
||||||
|
{
|
||||||
|
let button = button.clone();
|
||||||
|
context.widget_rx.attach(None, move |date| {
|
||||||
|
let date_string = format!("{}", date.format(&format));
|
||||||
|
button.set_label(&date_string);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let popup = self.into_popup(context.controller_tx, context.popup_rx);
|
||||||
|
|
||||||
|
Ok(ModuleWidget {
|
||||||
|
widget: button,
|
||||||
|
popup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_popup(
|
||||||
|
self,
|
||||||
|
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||||
|
rx: glib::Receiver<Self::SendMessage>,
|
||||||
|
) -> Option<gtk::Box> {
|
||||||
|
let container = gtk::Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.name("popup-clock")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let clock = Label::builder()
|
||||||
|
.name("calendar-clock")
|
||||||
|
.halign(Align::Center)
|
||||||
|
.build();
|
||||||
|
let format = "%H:%M:%S";
|
||||||
|
|
||||||
|
container.add(&clock);
|
||||||
|
|
||||||
|
let calendar = Calendar::builder().name("calendar").build();
|
||||||
|
container.add(&calendar);
|
||||||
|
|
||||||
|
{
|
||||||
|
rx.attach(None, move |date| {
|
||||||
|
let date_string = format!("{}", date.format(format));
|
||||||
|
clock.set_label(&date_string);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(container)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,71 +0,0 @@
|
||||||
mod popup;
|
|
||||||
|
|
||||||
use self::popup::Popup;
|
|
||||||
use crate::modules::{Module, ModuleInfo};
|
|
||||||
use chrono::Local;
|
|
||||||
use color_eyre::Result;
|
|
||||||
use glib::Continue;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::{Button, Orientation};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use tokio::spawn;
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
pub struct ClockModule {
|
|
||||||
/// Date/time format string.
|
|
||||||
/// Default: `%d/%m/%Y %H:%M`
|
|
||||||
///
|
|
||||||
/// Detail on available tokens can be found here:
|
|
||||||
/// <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
|
|
||||||
#[serde(default = "default_format")]
|
|
||||||
pub(crate) format: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_format() -> String {
|
|
||||||
String::from("%d/%m/%Y %H:%M")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module<Button> for ClockModule {
|
|
||||||
fn into_widget(self, info: &ModuleInfo) -> Result<Button> {
|
|
||||||
let button = Button::new();
|
|
||||||
|
|
||||||
let popup = Popup::new(
|
|
||||||
"popup-clock",
|
|
||||||
info.app,
|
|
||||||
info.monitor,
|
|
||||||
Orientation::Vertical,
|
|
||||||
info.bar_position,
|
|
||||||
);
|
|
||||||
popup.add_clock_widgets();
|
|
||||||
|
|
||||||
button.show_all();
|
|
||||||
|
|
||||||
button.connect_clicked(move |button| {
|
|
||||||
popup.show(button);
|
|
||||||
});
|
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
spawn(async move {
|
|
||||||
let format = self.format.as_str();
|
|
||||||
loop {
|
|
||||||
let date = Local::now();
|
|
||||||
let date_string = format!("{}", date.format(format));
|
|
||||||
|
|
||||||
tx.send(date_string).expect("Failed to send date string");
|
|
||||||
|
|
||||||
sleep(tokio::time::Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
let button = button.clone();
|
|
||||||
rx.attach(None, move |s| {
|
|
||||||
button.set_label(s.as_str());
|
|
||||||
Continue(true)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(button)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
pub use crate::popup::Popup;
|
|
||||||
use chrono::Local;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::{Align, Calendar, Label};
|
|
||||||
use tokio::spawn;
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
impl Popup {
|
|
||||||
pub fn add_clock_widgets(&self) {
|
|
||||||
let clock = Label::builder()
|
|
||||||
.name("calendar-clock")
|
|
||||||
.halign(Align::Center)
|
|
||||||
.build();
|
|
||||||
let format = "%H:%M:%S";
|
|
||||||
|
|
||||||
self.container.add(&clock);
|
|
||||||
|
|
||||||
let calendar = Calendar::builder().name("calendar").build();
|
|
||||||
self.container.add(&calendar);
|
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
spawn(async move {
|
|
||||||
loop {
|
|
||||||
let date = Local::now();
|
|
||||||
let date_string = format!("{}", date.format(format));
|
|
||||||
|
|
||||||
tx.send(date_string).expect("Failed to send date string");
|
|
||||||
|
|
||||||
sleep(tokio::time::Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
rx.attach(None, move |s| {
|
|
||||||
clock.set_label(s.as_str());
|
|
||||||
Continue(true)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +1,16 @@
|
||||||
use crate::icon;
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::modules::{Module, ModuleInfo};
|
use crate::sway::node::{get_node_id, get_open_windows};
|
||||||
use crate::sway::get_client;
|
use crate::sway::{get_client, get_sub_client};
|
||||||
|
use crate::{await_sync, icon};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use glib::Continue;
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{IconTheme, Image, Label, Orientation};
|
use gtk::{IconTheme, Image, Label, Orientation};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::task::spawn_blocking;
|
use swayipc_async::WindowChange;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct FocusedModule {
|
pub struct FocusedModule {
|
||||||
|
@ -29,7 +33,73 @@ const fn default_icon_size() -> i32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for FocusedModule {
|
impl Module<gtk::Box> for FocusedModule {
|
||||||
fn into_widget(self, _info: &ModuleInfo) -> Result<gtk::Box> {
|
type SendMessage = (String, String);
|
||||||
|
type ReceiveMessage = ();
|
||||||
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
_rx: Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let focused = await_sync(async {
|
||||||
|
let sway = get_client().await;
|
||||||
|
let mut sway = sway.lock().await;
|
||||||
|
get_open_windows(&mut sway)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get open windows")
|
||||||
|
.into_iter()
|
||||||
|
.find(|node| node.focused)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(node) = focused {
|
||||||
|
let id = get_node_id(&node);
|
||||||
|
let name = node.name.as_deref().unwrap_or(id);
|
||||||
|
|
||||||
|
tx.try_send(ModuleUpdateEvent::Update((
|
||||||
|
name.to_string(),
|
||||||
|
id.to_string(),
|
||||||
|
)))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
let mut srx = {
|
||||||
|
let sway = get_sub_client();
|
||||||
|
sway.subscribe_window()
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!("Set up Sway window subscription");
|
||||||
|
|
||||||
|
while let Ok(payload) = srx.recv().await {
|
||||||
|
let update = match payload.change {
|
||||||
|
WindowChange::Focus => true,
|
||||||
|
WindowChange::Title => payload.container.focused,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if update {
|
||||||
|
let node = payload.container;
|
||||||
|
|
||||||
|
let id = get_node_id(&node);
|
||||||
|
let name = node.name.as_deref().unwrap_or(id);
|
||||||
|
|
||||||
|
tx.try_send(ModuleUpdateEvent::Update((
|
||||||
|
name.to_string(),
|
||||||
|
id.to_string(),
|
||||||
|
)))
|
||||||
|
.expect("Failed to send focus update");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<gtk::Box>> {
|
||||||
let icon_theme = IconTheme::new();
|
let icon_theme = IconTheme::new();
|
||||||
|
|
||||||
if let Some(theme) = self.icon_theme {
|
if let Some(theme) = self.icon_theme {
|
||||||
|
@ -44,59 +114,25 @@ impl Module<gtk::Box> for FocusedModule {
|
||||||
container.add(&icon);
|
container.add(&icon);
|
||||||
container.add(&label);
|
container.add(&label);
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
let focused = {
|
|
||||||
let sway = get_client();
|
|
||||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
|
||||||
sway.get_open_windows()?
|
|
||||||
.into_iter()
|
|
||||||
.find(|node| node.focused)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(focused) = focused {
|
|
||||||
tx.send(focused)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
spawn_blocking(move || {
|
|
||||||
let srx = {
|
|
||||||
let sway = get_client();
|
|
||||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
|
||||||
sway.subscribe_window()
|
|
||||||
};
|
|
||||||
|
|
||||||
while let Ok(payload) = srx.recv() {
|
|
||||||
let update = match payload.change.as_str() {
|
|
||||||
"focus" => true,
|
|
||||||
"title" => payload.container.focused,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if update {
|
|
||||||
tx.send(payload.container)
|
|
||||||
.expect("Failed to sendf focus update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
{
|
||||||
rx.attach(None, move |node| {
|
context.widget_rx.attach(None, move |(name, id)| {
|
||||||
let value = node.name.as_deref().unwrap_or_else(|| node.get_id());
|
let pixbuf = icon::get_icon(&icon_theme, &id, self.icon_size);
|
||||||
|
|
||||||
let pixbuf = icon::get_icon(&icon_theme, node.get_id(), self.icon_size);
|
|
||||||
|
|
||||||
if self.show_icon {
|
if self.show_icon {
|
||||||
icon.set_pixbuf(pixbuf.as_ref());
|
icon.set_pixbuf(pixbuf.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.show_title {
|
if self.show_title {
|
||||||
label.set_label(value);
|
label.set_label(&name);
|
||||||
}
|
}
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(container)
|
Ok(ModuleWidget {
|
||||||
|
widget: container,
|
||||||
|
popup: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,292 +1,299 @@
|
||||||
|
use super::open_state::OpenState;
|
||||||
use crate::collection::Collection;
|
use crate::collection::Collection;
|
||||||
use crate::icon::{find_desktop_file, get_icon};
|
use crate::icon::get_icon;
|
||||||
use crate::modules::launcher::open_state::OpenState;
|
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||||
use crate::modules::launcher::popup::Popup;
|
use crate::modules::ModuleUpdateEvent;
|
||||||
use crate::modules::launcher::FocusEvent;
|
use crate::popup::Popup;
|
||||||
use crate::sway::SwayNode;
|
use crate::sway::node::{get_node_id, is_node_xwayland};
|
||||||
use crate::Report;
|
|
||||||
use color_eyre::Help;
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme, Image};
|
use gtk::{Button, IconTheme, Image};
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::RwLock;
|
||||||
use tokio::spawn;
|
use swayipc_async::Node;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LauncherItem {
|
pub struct Item {
|
||||||
pub app_id: String,
|
pub app_id: String,
|
||||||
pub favorite: bool,
|
pub favorite: bool,
|
||||||
pub windows: Rc<RwLock<Collection<i32, LauncherWindow>>>,
|
pub open_state: OpenState,
|
||||||
pub state: Arc<RwLock<State>>,
|
pub windows: Collection<i64, Window>,
|
||||||
pub button: Button,
|
pub name: Option<String>,
|
||||||
|
pub is_xwayland: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
impl Item {
|
||||||
pub struct LauncherWindow {
|
pub const fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
|
||||||
pub con_id: i32,
|
Self {
|
||||||
|
app_id,
|
||||||
|
favorite,
|
||||||
|
open_state,
|
||||||
|
windows: Collection::new(),
|
||||||
|
name: None,
|
||||||
|
is_xwayland: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges the provided node into this launcher item
|
||||||
|
pub fn merge_node(&mut self, node: Node) -> Window {
|
||||||
|
let id = node.id;
|
||||||
|
|
||||||
|
if self.windows.is_empty() {
|
||||||
|
self.name = node.name.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.is_xwayland = self.is_xwayland || is_node_xwayland(&node);
|
||||||
|
|
||||||
|
let window: Window = node.into();
|
||||||
|
self.windows.insert(id, window.clone());
|
||||||
|
|
||||||
|
self.recalculate_open_state();
|
||||||
|
|
||||||
|
window
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmerge_node(&mut self, node: &Node) {
|
||||||
|
self.windows.remove(&node.id);
|
||||||
|
self.recalculate_open_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_name(&self) -> &str {
|
||||||
|
self.name.as_ref().unwrap_or(&self.app_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_window_name(&mut self, window_id: i64, name: Option<String>) {
|
||||||
|
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||||
|
if let OpenState::Open { focused: true, .. } = window.open_state {
|
||||||
|
self.name = name.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_unfocused(&mut self) {
|
||||||
|
let focused = self
|
||||||
|
.windows
|
||||||
|
.iter_mut()
|
||||||
|
.find(|window| window.open_state.is_focused());
|
||||||
|
|
||||||
|
if let Some(focused) = focused {
|
||||||
|
focused.open_state = OpenState::Open {
|
||||||
|
focused: false,
|
||||||
|
urgent: focused.open_state.is_urgent(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.recalculate_open_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_window_focused(&mut self, window_id: i64, focused: bool) {
|
||||||
|
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||||
|
window.open_state =
|
||||||
|
OpenState::merge_states(&[&window.open_state, &OpenState::focused(focused)]);
|
||||||
|
|
||||||
|
self.recalculate_open_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_window_urgent(&mut self, window_id: i64, urgent: bool) {
|
||||||
|
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||||
|
window.open_state =
|
||||||
|
OpenState::merge_states(&[&window.open_state, &OpenState::urgent(urgent)]);
|
||||||
|
|
||||||
|
self.recalculate_open_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets this item's open state
|
||||||
|
/// to the merged result of its windows' open states
|
||||||
|
fn recalculate_open_state(&mut self) {
|
||||||
|
let new_state = OpenState::merge_states(
|
||||||
|
&self
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.map(|win| &win.open_state)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
self.open_state = new_state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Node> for Item {
|
||||||
|
fn from(node: Node) -> Self {
|
||||||
|
let app_id = get_node_id(&node).to_string();
|
||||||
|
let open_state = OpenState::from_node(&node);
|
||||||
|
let name = node.name.clone();
|
||||||
|
|
||||||
|
let is_xwayland = is_node_xwayland(&node);
|
||||||
|
|
||||||
|
let mut windows = Collection::new();
|
||||||
|
windows.insert(node.id, node.into());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
app_id,
|
||||||
|
favorite: false,
|
||||||
|
open_state,
|
||||||
|
windows,
|
||||||
|
name,
|
||||||
|
is_xwayland,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Window {
|
||||||
|
pub id: i64,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub open_state: OpenState,
|
pub open_state: OpenState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
impl From<Node> for Window {
|
||||||
pub struct State {
|
fn from(node: Node) -> Self {
|
||||||
pub is_xwayland: bool,
|
let open_state = OpenState::from_node(&node);
|
||||||
pub open_state: OpenState,
|
|
||||||
|
Self {
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
open_state,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pub struct MenuState {
|
||||||
pub struct ButtonConfig {
|
pub num_windows: usize,
|
||||||
pub icon_theme: IconTheme,
|
}
|
||||||
|
|
||||||
|
pub struct ItemButton {
|
||||||
|
pub button: Button,
|
||||||
|
pub persistent: bool,
|
||||||
pub show_names: bool,
|
pub show_names: bool,
|
||||||
pub show_icons: bool,
|
pub menu_state: Rc<RwLock<MenuState>>,
|
||||||
pub popup: Popup,
|
|
||||||
pub tx: mpsc::Sender<FocusEvent>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LauncherItem {
|
impl ItemButton {
|
||||||
pub fn new(app_id: String, favorite: bool, config: &ButtonConfig) -> Self {
|
pub fn new(
|
||||||
let button = Button::new();
|
item: &Item,
|
||||||
button.style_context().add_class("item");
|
show_names: bool,
|
||||||
|
show_icons: bool,
|
||||||
|
icon_theme: &IconTheme,
|
||||||
|
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
||||||
|
controller_tx: &Sender<ItemEvent>,
|
||||||
|
) -> Self {
|
||||||
|
let mut button = Button::builder();
|
||||||
|
|
||||||
let state = State {
|
if show_names {
|
||||||
open_state: OpenState::Closed,
|
button = button.label(item.get_name());
|
||||||
is_xwayland: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let item = Self {
|
|
||||||
app_id,
|
|
||||||
favorite,
|
|
||||||
windows: Rc::new(RwLock::new(Collection::new())),
|
|
||||||
state: Arc::new(RwLock::new(state)),
|
|
||||||
button,
|
|
||||||
};
|
|
||||||
|
|
||||||
item.configure_button(config);
|
|
||||||
item
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_node(node: &SwayNode, config: &ButtonConfig) -> Self {
|
|
||||||
let button = Button::new();
|
|
||||||
button.style_context().add_class("item");
|
|
||||||
|
|
||||||
let windows = Collection::from((
|
|
||||||
node.id,
|
|
||||||
LauncherWindow {
|
|
||||||
con_id: node.id,
|
|
||||||
name: node.name.clone(),
|
|
||||||
open_state: OpenState::from_node(node),
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
let state = State {
|
|
||||||
open_state: OpenState::from_node(node),
|
|
||||||
is_xwayland: node.is_xwayland(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let item = Self {
|
|
||||||
app_id: node.get_id().to_string(),
|
|
||||||
favorite: false,
|
|
||||||
windows: Rc::new(RwLock::new(windows)),
|
|
||||||
state: Arc::new(RwLock::new(state)),
|
|
||||||
button,
|
|
||||||
};
|
|
||||||
|
|
||||||
item.configure_button(config);
|
|
||||||
item
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configure_button(&self, config: &ButtonConfig) {
|
|
||||||
let button = &self.button;
|
|
||||||
|
|
||||||
let windows = self
|
|
||||||
.windows
|
|
||||||
.read()
|
|
||||||
.expect("Failed to get read lock on windows");
|
|
||||||
|
|
||||||
let name = if windows.len() == 1 {
|
|
||||||
windows
|
|
||||||
.first()
|
|
||||||
.expect("Failed to get first window")
|
|
||||||
.name
|
|
||||||
.as_ref()
|
|
||||||
} else {
|
|
||||||
Some(&self.app_id)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(name) = name {
|
|
||||||
self.set_title(name, config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.show_icons {
|
if show_icons {
|
||||||
let icon = get_icon(&config.icon_theme, &self.app_id, 32);
|
let icon = get_icon(icon_theme, &item.app_id, 32);
|
||||||
if icon.is_some() {
|
if icon.is_some() {
|
||||||
let image = Image::from_pixbuf(icon.as_ref());
|
let image = Image::from_pixbuf(icon.as_ref());
|
||||||
button.set_image(Some(&image));
|
button = button.image(&image).always_show_image(true);
|
||||||
button.set_always_show_image(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_id = self.app_id.clone();
|
let button = button.build();
|
||||||
let state = Arc::clone(&self.state);
|
|
||||||
let tx_click = config.tx.clone();
|
|
||||||
|
|
||||||
let (focus_tx, mut focus_rx) = mpsc::channel(32);
|
let style_context = button.style_context();
|
||||||
|
style_context.add_class("item");
|
||||||
|
|
||||||
button.connect_clicked(move |_| {
|
if item.favorite {
|
||||||
let state = state.read().expect("Failed to get read lock on state");
|
style_context.add_class("favorite");
|
||||||
if state.open_state.is_open() {
|
}
|
||||||
focus_tx.try_send(()).expect("Failed to send focus event");
|
if item.open_state.is_open() {
|
||||||
} else {
|
style_context.add_class("open");
|
||||||
// attempt to find desktop file and launch
|
}
|
||||||
match find_desktop_file(&app_id) {
|
if item.open_state.is_focused() {
|
||||||
Some(file) => {
|
style_context.add_class("focused");
|
||||||
if let Err(err) = Command::new("gtk-launch")
|
}
|
||||||
.arg(
|
if item.open_state.is_urgent() {
|
||||||
file.file_name()
|
style_context.add_class("urgent");
|
||||||
.expect("File segment missing from path to desktop file"),
|
}
|
||||||
)
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.spawn()
|
|
||||||
{
|
|
||||||
error!(
|
|
||||||
"{:?}",
|
|
||||||
Report::new(err)
|
|
||||||
.wrap_err("Failed to run gtk-launch command.")
|
|
||||||
.suggestion("Perhaps the desktop file is invalid?")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => error!("Could not find desktop file for {}", app_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let app_id = self.app_id.clone();
|
{
|
||||||
let state = Arc::clone(&self.state);
|
let app_id = item.app_id.clone();
|
||||||
|
let tx = controller_tx.clone();
|
||||||
spawn(async move {
|
button.connect_clicked(move |button| {
|
||||||
while focus_rx.recv().await == Some(()) {
|
// lazy check :|
|
||||||
let state = state.read().expect("Failed to get read lock on state");
|
let style_context = button.style_context();
|
||||||
if state.is_xwayland {
|
if style_context.has_class("open") {
|
||||||
tx_click
|
tx.try_send(ItemEvent::FocusItem(app_id.clone()))
|
||||||
.try_send(FocusEvent::Class(app_id.clone()))
|
.expect("Failed to send item focus event");
|
||||||
.expect("Failed to send focus event");
|
|
||||||
} else {
|
} else {
|
||||||
tx_click
|
tx.try_send(ItemEvent::OpenItem(app_id.clone()))
|
||||||
.try_send(FocusEvent::AppId(app_id.clone()))
|
.expect("Failed to send item open event");
|
||||||
.expect("Failed to send focus event");
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
let popup = config.popup.clone();
|
let menu_state = Rc::new(RwLock::new(MenuState {
|
||||||
let popup2 = config.popup.clone();
|
num_windows: item.windows.len(),
|
||||||
let windows = Rc::clone(&self.windows);
|
}));
|
||||||
let tx_hover = config.tx.clone();
|
|
||||||
|
|
||||||
button.connect_enter_notify_event(move |button, _| {
|
{
|
||||||
let windows = windows.read().expect("Failed to get read lock on windows");
|
let app_id = item.app_id.clone();
|
||||||
if windows.len() > 1 {
|
let tx = tx.clone();
|
||||||
popup.set_windows(windows.as_slice(), &tx_hover);
|
let menu_state = menu_state.clone();
|
||||||
popup.show(button);
|
|
||||||
}
|
|
||||||
|
|
||||||
Inhibit(false)
|
button.connect_enter_notify_event(move |button, _| {
|
||||||
});
|
let menu_state = menu_state
|
||||||
|
.read()
|
||||||
|
.expect("Failed to get read lock on item menu state");
|
||||||
|
|
||||||
{}
|
if menu_state.num_windows > 1 {
|
||||||
|
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::Hover(
|
||||||
|
app_id.clone(),
|
||||||
|
)))
|
||||||
|
.expect("Failed to send item open popup event");
|
||||||
|
|
||||||
button.connect_leave_notify_event(move |_, e| {
|
tx.try_send(ModuleUpdateEvent::OpenPopup(Popup::button_pos(button)))
|
||||||
let (_, y) = e.position();
|
.expect("Failed to send item open popup event");
|
||||||
// hover boundary
|
} else {
|
||||||
if y > 2.0 {
|
tx.try_send(ModuleUpdateEvent::ClosePopup)
|
||||||
popup2.hide();
|
.expect("Failed to send item close popup event");
|
||||||
}
|
}
|
||||||
|
|
||||||
Inhibit(false)
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
let style = button.style_context();
|
|
||||||
|
|
||||||
style.add_class("launcher-item");
|
|
||||||
self.update_button_classes(&self.state.read().expect("Failed to get read lock on state"));
|
|
||||||
|
|
||||||
button.show_all();
|
button.show_all();
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_title(&self, title: &str, config: &ButtonConfig) {
|
Self {
|
||||||
if config.show_names {
|
button,
|
||||||
self.button.set_label(title);
|
persistent: item.favorite,
|
||||||
} else {
|
show_names,
|
||||||
self.button.set_tooltip_text(Some(title));
|
menu_state,
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the classnames on the GTK button
|
|
||||||
/// based on its current state.
|
|
||||||
///
|
|
||||||
/// State must be passed as an arg here rather than
|
|
||||||
/// using `self.state` to avoid a weird `RwLock` issue.
|
|
||||||
pub fn update_button_classes(&self, state: &State) {
|
|
||||||
let style = self.button.style_context();
|
|
||||||
|
|
||||||
if self.favorite {
|
|
||||||
style.add_class("favorite");
|
|
||||||
} else {
|
|
||||||
style.remove_class("favorite");
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.open_state.is_open() {
|
|
||||||
style.add_class("open");
|
|
||||||
} else {
|
|
||||||
style.remove_class("open");
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.open_state.is_focused() {
|
|
||||||
style.add_class("focused");
|
|
||||||
} else {
|
|
||||||
style.remove_class("focused");
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.open_state.is_urgent() {
|
|
||||||
style.add_class("urgent");
|
|
||||||
} else {
|
|
||||||
style.remove_class("urgent");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the open state for a specific window on the item
|
pub fn set_open(&self, open: bool) {
|
||||||
/// and updates the item state based on all its windows.
|
self.update_class("open", open);
|
||||||
pub fn set_window_open_state(&self, window_id: i32, new_state: OpenState, state: &mut State) {
|
|
||||||
let mut windows = self
|
|
||||||
.windows
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on windows");
|
|
||||||
|
|
||||||
let window = windows.iter_mut().find(|w| w.con_id == window_id);
|
if !open {
|
||||||
if let Some(window) = window {
|
self.set_focused(false);
|
||||||
window.open_state = new_state;
|
self.set_urgent(false);
|
||||||
|
|
||||||
state.open_state =
|
|
||||||
OpenState::merge_states(windows.iter().map(|w| &w.open_state).collect());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the open state on the item and all its windows.
|
pub fn set_focused(&self, focused: bool) {
|
||||||
/// This overrides the existing open states.
|
self.update_class("focused", focused);
|
||||||
pub fn set_open_state(&self, new_state: OpenState, state: &mut State) {
|
}
|
||||||
state.open_state = new_state;
|
|
||||||
let mut windows = self
|
|
||||||
.windows
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on windows");
|
|
||||||
|
|
||||||
windows
|
pub fn set_urgent(&self, urgent: bool) {
|
||||||
.iter_mut()
|
self.update_class("urgent", urgent);
|
||||||
.for_each(|window| window.open_state = new_state);
|
}
|
||||||
|
|
||||||
|
/// Adds or removes a class to the button based on `toggle`.
|
||||||
|
fn update_class(&self, class: &str, toggle: bool) {
|
||||||
|
let style_context = self.button.style_context();
|
||||||
|
|
||||||
|
if toggle {
|
||||||
|
style_context.add_class(class);
|
||||||
|
} else {
|
||||||
|
style_context.remove_class(class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
mod item;
|
mod item;
|
||||||
mod open_state;
|
mod open_state;
|
||||||
mod popup;
|
|
||||||
|
|
||||||
use crate::collection::Collection;
|
use crate::collection::Collection;
|
||||||
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow};
|
use crate::icon::find_desktop_file;
|
||||||
|
use crate::modules::launcher::item::{Item, ItemButton, Window};
|
||||||
use crate::modules::launcher::open_state::OpenState;
|
use crate::modules::launcher::open_state::OpenState;
|
||||||
use crate::modules::launcher::popup::Popup;
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::modules::{Module, ModuleInfo};
|
use crate::sway::get_sub_client;
|
||||||
use crate::sway::{get_client, SwayNode};
|
use crate::sway::node::{get_node_id, get_open_windows};
|
||||||
use color_eyre::{Report, Result};
|
use crate::{await_sync, get_client};
|
||||||
|
use color_eyre::{Help, Report};
|
||||||
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{IconTheme, Orientation};
|
use gtk::{Button, IconTheme, Orientation};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::rc::Rc;
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use swayipc_async::WindowChange;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::debug;
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct LauncherModule {
|
pub struct LauncherModule {
|
||||||
/// List of app IDs (or classes) to always show regardles of open state,
|
/// List of app IDs (or classes) to always show regardless of open state,
|
||||||
/// in the order specified.
|
/// in the order specified.
|
||||||
favorites: Option<Vec<String>>,
|
favorites: Option<Vec<String>>,
|
||||||
/// Whether to show application names on the bar.
|
/// Whether to show application names on the bar.
|
||||||
|
@ -34,277 +38,501 @@ pub struct LauncherModule {
|
||||||
icon_theme: Option<String>,
|
icon_theme: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LauncherUpdate {
|
||||||
|
/// Adds item
|
||||||
|
AddItem(Item),
|
||||||
|
/// Adds window to item with `app_id`
|
||||||
|
AddWindow(String, Window),
|
||||||
|
/// Removes item with `app_id`
|
||||||
|
RemoveItem(String),
|
||||||
|
/// Removes window from item with `app_id`.
|
||||||
|
RemoveWindow(String, i64),
|
||||||
|
/// Sets title for `app_id`
|
||||||
|
Title(String, i64, Option<String>),
|
||||||
|
/// Focuses first `app_id`, unfocuses second `app_id` (if present)
|
||||||
|
Focus(String, Option<String>),
|
||||||
|
/// Marks the item with `app_id` as urgent or not urgent
|
||||||
|
Urgent(String, bool),
|
||||||
|
/// Declares the item with `app_id` has been hovered over
|
||||||
|
Hover(String),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FocusEvent {
|
pub enum ItemEvent {
|
||||||
AppId(String),
|
FocusItem(String),
|
||||||
Class(String),
|
FocusWindow(i64),
|
||||||
ConId(i32),
|
OpenItem(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppId = String;
|
enum ItemOrWindow {
|
||||||
|
Item(Item),
|
||||||
struct Launcher {
|
Window(Window),
|
||||||
items: Collection<AppId, LauncherItem>,
|
|
||||||
container: gtk::Box,
|
|
||||||
button_config: ButtonConfig,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Launcher {
|
enum ItemOrWindowId {
|
||||||
fn new(favorites: Vec<String>, container: gtk::Box, button_config: ButtonConfig) -> Self {
|
Item,
|
||||||
let items = favorites
|
Window,
|
||||||
.into_iter()
|
|
||||||
.map(|app_id| {
|
|
||||||
(
|
|
||||||
app_id.clone(),
|
|
||||||
LauncherItem::new(app_id, true, &button_config),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Collection<_, _>>();
|
|
||||||
|
|
||||||
for item in &items {
|
|
||||||
container.add(&item.button);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
items,
|
|
||||||
container,
|
|
||||||
button_config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a new window to the launcher.
|
|
||||||
/// This gets added to an existing group
|
|
||||||
/// if an instance of the program is already open.
|
|
||||||
fn add_window(&mut self, node: SwayNode) {
|
|
||||||
let id = node.get_id().to_string();
|
|
||||||
|
|
||||||
debug!("Adding window with ID {}", id);
|
|
||||||
|
|
||||||
if let Some(item) = self.items.get_mut(&id) {
|
|
||||||
let mut state = item
|
|
||||||
.state
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on state");
|
|
||||||
let new_open_state = OpenState::from_node(&node);
|
|
||||||
state.open_state = OpenState::merge_states(vec![&state.open_state, &new_open_state]);
|
|
||||||
state.is_xwayland = node.is_xwayland();
|
|
||||||
|
|
||||||
item.update_button_classes(&state);
|
|
||||||
|
|
||||||
let mut windows = item
|
|
||||||
.windows
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on windows");
|
|
||||||
|
|
||||||
windows.insert(
|
|
||||||
node.id,
|
|
||||||
LauncherWindow {
|
|
||||||
con_id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
open_state: new_open_state,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let item = LauncherItem::from_node(&node, &self.button_config);
|
|
||||||
|
|
||||||
self.container.add(&item.button);
|
|
||||||
self.items.insert(id, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes a window from the launcher.
|
|
||||||
/// This removes it from the group if multiple instances were open.
|
|
||||||
/// The button will remain on the launcher if it is favourited.
|
|
||||||
fn remove_window(&mut self, window: &SwayNode) {
|
|
||||||
let id = window.get_id().to_string();
|
|
||||||
|
|
||||||
debug!("Removing window with ID {}", id);
|
|
||||||
|
|
||||||
let item = self.items.get_mut(&id);
|
|
||||||
|
|
||||||
let remove = if let Some(item) = item {
|
|
||||||
let windows = Rc::clone(&item.windows);
|
|
||||||
let mut windows = windows
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on windows");
|
|
||||||
|
|
||||||
windows.remove(&window.id);
|
|
||||||
|
|
||||||
if windows.is_empty() {
|
|
||||||
let mut state = item.state.write().expect("Failed to get lock on windows");
|
|
||||||
state.open_state = OpenState::Closed;
|
|
||||||
item.update_button_classes(&state);
|
|
||||||
|
|
||||||
if item.favorite {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
self.container.remove(&item.button);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if remove {
|
|
||||||
self.items.remove(&id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unfocuses the currently focused window
|
|
||||||
/// and focuses the newly focused one.
|
|
||||||
fn set_window_focused(&mut self, node: &SwayNode) {
|
|
||||||
let id = node.get_id().to_string();
|
|
||||||
|
|
||||||
debug!("Setting window with ID {} focused", id);
|
|
||||||
|
|
||||||
let prev_focused = self.items.iter_mut().find(|item| {
|
|
||||||
item.state
|
|
||||||
.read()
|
|
||||||
.expect("Failed to get read lock on state")
|
|
||||||
.open_state
|
|
||||||
.is_focused()
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(prev_focused) = prev_focused {
|
|
||||||
let mut state = prev_focused
|
|
||||||
.state
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on state");
|
|
||||||
|
|
||||||
// if a window from the same item took focus,
|
|
||||||
// we don't need to unfocus the item.
|
|
||||||
if prev_focused.app_id != id {
|
|
||||||
prev_focused.set_open_state(OpenState::open(), &mut state);
|
|
||||||
prev_focused.update_button_classes(&state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let item = self.items.get_mut(&id);
|
|
||||||
if let Some(item) = item {
|
|
||||||
let mut state = item
|
|
||||||
.state
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on state");
|
|
||||||
item.set_window_open_state(node.id, OpenState::focused(), &mut state);
|
|
||||||
item.update_button_classes(&state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the window title for the given node.
|
|
||||||
fn set_window_title(&mut self, window: SwayNode) {
|
|
||||||
let id = window.get_id().to_string();
|
|
||||||
let item = self.items.get_mut(&id);
|
|
||||||
|
|
||||||
debug!("Updating title for window with ID {}", id);
|
|
||||||
|
|
||||||
if let (Some(item), Some(name)) = (item, window.name) {
|
|
||||||
let mut windows = item
|
|
||||||
.windows
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on windows");
|
|
||||||
if windows.len() == 1 {
|
|
||||||
item.set_title(&name, &self.button_config);
|
|
||||||
} else if let Some(window) = windows.get_mut(&window.id) {
|
|
||||||
window.name = Some(name);
|
|
||||||
} else {
|
|
||||||
// This should never happen
|
|
||||||
// But makes more sense to wipe title than keep old one in case of error
|
|
||||||
item.set_title("", &self.button_config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the window urgency based on the given node.
|
|
||||||
fn set_window_urgent(&mut self, node: &SwayNode) {
|
|
||||||
let id = node.get_id().to_string();
|
|
||||||
let item = self.items.get_mut(&id);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Setting urgency to {} for window with ID {}",
|
|
||||||
node.urgent, id
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(item) = item {
|
|
||||||
let mut state = item
|
|
||||||
.state
|
|
||||||
.write()
|
|
||||||
.expect("Failed to get write lock on state");
|
|
||||||
|
|
||||||
item.set_window_open_state(node.id, OpenState::urgent(node.urgent), &mut state);
|
|
||||||
item.update_button_classes(&state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for LauncherModule {
|
impl Module<gtk::Box> for LauncherModule {
|
||||||
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
|
type SendMessage = LauncherUpdate;
|
||||||
let icon_theme = IconTheme::new();
|
type ReceiveMessage = ItemEvent;
|
||||||
|
|
||||||
if let Some(theme) = self.icon_theme {
|
fn spawn_controller(
|
||||||
icon_theme.set_custom_theme(Some(&theme));
|
&self,
|
||||||
}
|
_info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
let popup = Popup::new(
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
"popup-launcher",
|
) -> crate::Result<()> {
|
||||||
info.app,
|
let items = match &self.favorites {
|
||||||
info.monitor,
|
Some(favorites) => favorites
|
||||||
Orientation::Vertical,
|
.iter()
|
||||||
info.bar_position,
|
.map(|app_id| {
|
||||||
);
|
(
|
||||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
app_id.to_string(),
|
||||||
|
Item::new(app_id.to_string(), OpenState::Closed, true),
|
||||||
let (ui_tx, mut ui_rx) = mpsc::channel(32);
|
)
|
||||||
|
})
|
||||||
let button_config = ButtonConfig {
|
.collect::<Collection<_, _>>(),
|
||||||
icon_theme,
|
None => Collection::new(),
|
||||||
show_names: self.show_names,
|
|
||||||
show_icons: self.show_icons,
|
|
||||||
popup,
|
|
||||||
tx: ui_tx,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut launcher = Launcher::new(
|
let items = Arc::new(Mutex::new(items));
|
||||||
self.favorites.unwrap_or_default(),
|
|
||||||
container.clone(),
|
|
||||||
button_config,
|
|
||||||
);
|
|
||||||
|
|
||||||
let open_windows = {
|
let open_windows = await_sync(async {
|
||||||
let sway = get_client();
|
let sway = get_client().await;
|
||||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
let mut sway = sway.lock().await;
|
||||||
sway.get_open_windows()
|
get_open_windows(&mut sway).await
|
||||||
}?;
|
})?;
|
||||||
|
|
||||||
for window in open_windows {
|
{
|
||||||
launcher.add_window(window);
|
let mut items = items.lock().expect("Failed to get lock on items");
|
||||||
|
for window in open_windows {
|
||||||
|
let id = get_node_id(&window).to_string();
|
||||||
|
|
||||||
|
let item = items.get_mut(&id);
|
||||||
|
match item {
|
||||||
|
Some(item) => {
|
||||||
|
item.merge_node(window);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
items.insert(id, window.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = items.iter();
|
||||||
|
for item in items {
|
||||||
|
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
|
||||||
|
item.clone(),
|
||||||
|
)))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
let items2 = Arc::clone(&items);
|
||||||
|
spawn(async move {
|
||||||
|
let items = items2;
|
||||||
|
|
||||||
spawn_blocking(move || {
|
let mut srx = {
|
||||||
let srx = {
|
let sway = get_sub_client();
|
||||||
let sway = get_client();
|
|
||||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
|
||||||
sway.subscribe_window()
|
sway.subscribe_window()
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Ok(payload) = srx.recv() {
|
while let Ok(event) = srx.recv().await {
|
||||||
tx.send(payload)
|
trace!("event: {:?}", event);
|
||||||
.expect("Failed to send window event payload");
|
|
||||||
|
let window = event.container;
|
||||||
|
let id = get_node_id(&window).to_string();
|
||||||
|
|
||||||
|
let send_update =
|
||||||
|
|update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update));
|
||||||
|
|
||||||
|
let items = || items.lock().expect("Failed to get lock on items");
|
||||||
|
|
||||||
|
match event.change {
|
||||||
|
WindowChange::New => {
|
||||||
|
let new_item = {
|
||||||
|
let mut items = items();
|
||||||
|
match items.get_mut(&id) {
|
||||||
|
None => {
|
||||||
|
let item: Item = window.into();
|
||||||
|
items.insert(id.clone(), item.clone());
|
||||||
|
|
||||||
|
ItemOrWindow::Item(item)
|
||||||
|
}
|
||||||
|
Some(item) => {
|
||||||
|
let window = item.merge_node(window);
|
||||||
|
ItemOrWindow::Window(window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match new_item {
|
||||||
|
ItemOrWindow::Item(item) => {
|
||||||
|
send_update(LauncherUpdate::AddItem(item)).await
|
||||||
|
}
|
||||||
|
ItemOrWindow::Window(window) => {
|
||||||
|
send_update(LauncherUpdate::AddWindow(id, window)).await
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
WindowChange::Close => {
|
||||||
|
let remove_item = {
|
||||||
|
let mut items = items();
|
||||||
|
match items.get_mut(&id) {
|
||||||
|
Some(item) => {
|
||||||
|
item.unmerge_node(&window);
|
||||||
|
|
||||||
|
if item.windows.is_empty() {
|
||||||
|
items.remove(&id);
|
||||||
|
Some(ItemOrWindowId::Item)
|
||||||
|
} else {
|
||||||
|
Some(ItemOrWindowId::Window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match remove_item {
|
||||||
|
Some(ItemOrWindowId::Item) => {
|
||||||
|
send_update(LauncherUpdate::RemoveItem(id)).await?;
|
||||||
|
}
|
||||||
|
Some(ItemOrWindowId::Window) => {
|
||||||
|
send_update(LauncherUpdate::RemoveWindow(id, window.id)).await?;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
WindowChange::Focus => {
|
||||||
|
let prev_id = {
|
||||||
|
let mut items = items();
|
||||||
|
|
||||||
|
let prev_focused =
|
||||||
|
items.iter_mut().find(|item| item.open_state.is_focused());
|
||||||
|
if let Some(prev_focused) = prev_focused {
|
||||||
|
prev_focused.set_unfocused();
|
||||||
|
Some(prev_focused.app_id.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut update_title = false;
|
||||||
|
if let Some(item) = items().get_mut(&id) {
|
||||||
|
item.set_window_focused(window.id, true);
|
||||||
|
|
||||||
|
// might be switching focus between windows of same app
|
||||||
|
if item.windows.len() > 1 {
|
||||||
|
item.set_window_name(window.id, window.name.clone());
|
||||||
|
update_title = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send_update(LauncherUpdate::Focus(id.clone(), prev_id)).await?;
|
||||||
|
|
||||||
|
if update_title {
|
||||||
|
send_update(LauncherUpdate::Title(id, window.id, window.name)).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowChange::Title => {
|
||||||
|
if let Some(item) = items().get_mut(&id) {
|
||||||
|
item.set_window_name(window.id, window.name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
send_update(LauncherUpdate::Title(id, window.id, window.name)).await?;
|
||||||
|
}
|
||||||
|
WindowChange::Urgent => {
|
||||||
|
if let Some(item) = items().get_mut(&id) {
|
||||||
|
item.set_window_urgent(window.id, window.urgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
send_update(LauncherUpdate::Urgent(id, window.urgent)).await?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<LauncherUpdate>>>(())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// listen to ui events
|
||||||
|
spawn(async move {
|
||||||
|
let sway = get_client().await;
|
||||||
|
|
||||||
|
while let Some(event) = rx.recv().await {
|
||||||
|
trace!("{:?}", event);
|
||||||
|
|
||||||
|
if let ItemEvent::OpenItem(app_id) = event {
|
||||||
|
find_desktop_file(&app_id).map_or_else(
|
||||||
|
|| error!("Could not find desktop file for {}", app_id),
|
||||||
|
|file| {
|
||||||
|
if let Err(err) = Command::new("gtk-launch")
|
||||||
|
.arg(
|
||||||
|
file.file_name()
|
||||||
|
.expect("File segment missing from path to desktop file"),
|
||||||
|
)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
error!(
|
||||||
|
"{:?}",
|
||||||
|
Report::new(err)
|
||||||
|
.wrap_err("Failed to run gtk-launch command.")
|
||||||
|
.suggestion("Perhaps the desktop file is invalid?")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let selector = {
|
||||||
|
let items = items.lock().expect("Failed to get lock on items");
|
||||||
|
|
||||||
|
match event {
|
||||||
|
ItemEvent::FocusItem(app_id) => items.get(&app_id).map(|item| {
|
||||||
|
if item.is_xwayland {
|
||||||
|
format!("[class={}]", app_id)
|
||||||
|
} else {
|
||||||
|
format!("[app_id={}]", app_id)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ItemEvent::FocusWindow(con_id) => Some(format!("[con_id={}]", con_id)),
|
||||||
|
ItemEvent::OpenItem(_) => unreachable!(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(selector) = selector {
|
||||||
|
let mut sway = sway.lock().await;
|
||||||
|
sway.run_command(format!("{} focus", selector)).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), swayipc_async::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> crate::Result<ModuleWidget<gtk::Box>> {
|
||||||
|
let icon_theme = IconTheme::new();
|
||||||
|
if let Some(ref theme) = self.icon_theme {
|
||||||
|
icon_theme.set_custom_theme(Some(theme));
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||||
|
|
||||||
{
|
{
|
||||||
|
let container = container.clone();
|
||||||
|
|
||||||
|
let show_names = self.show_names;
|
||||||
|
let show_icons = self.show_icons;
|
||||||
|
|
||||||
|
let mut buttons = Collection::<String, ItemButton>::new();
|
||||||
|
|
||||||
|
let controller_tx2 = context.controller_tx.clone();
|
||||||
|
context.widget_rx.attach(None, move |event| {
|
||||||
|
match event {
|
||||||
|
LauncherUpdate::AddItem(item) => {
|
||||||
|
debug!("Adding item with id {}", item.app_id);
|
||||||
|
|
||||||
|
if let Some(button) = buttons.get(&item.app_id) {
|
||||||
|
button.set_open(true);
|
||||||
|
} else {
|
||||||
|
let button = ItemButton::new(
|
||||||
|
&item,
|
||||||
|
show_names,
|
||||||
|
show_icons,
|
||||||
|
&icon_theme,
|
||||||
|
&context.tx,
|
||||||
|
&controller_tx2,
|
||||||
|
);
|
||||||
|
|
||||||
|
container.add(&button.button);
|
||||||
|
buttons.insert(item.app_id, button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::AddWindow(app_id, _) => {
|
||||||
|
if let Some(button) = buttons.get(&app_id) {
|
||||||
|
let mut menu_state = button
|
||||||
|
.menu_state
|
||||||
|
.write()
|
||||||
|
.expect("Failed to get write lock on item menu state");
|
||||||
|
menu_state.num_windows += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::RemoveItem(app_id) => {
|
||||||
|
debug!("Removing item with id {}", app_id);
|
||||||
|
|
||||||
|
if let Some(button) = buttons.get(&app_id) {
|
||||||
|
if button.persistent {
|
||||||
|
button.set_open(false);
|
||||||
|
if button.show_names {
|
||||||
|
button.button.set_label(&app_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.remove(&button.button);
|
||||||
|
buttons.remove(&app_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::RemoveWindow(app_id, _) => {
|
||||||
|
if let Some(button) = buttons.get(&app_id) {
|
||||||
|
let mut menu_state = button
|
||||||
|
.menu_state
|
||||||
|
.write()
|
||||||
|
.expect("Failed to get write lock on item menu state");
|
||||||
|
menu_state.num_windows -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::Focus(new, prev) => {
|
||||||
|
debug!(
|
||||||
|
"Changing focus to item with id {} (removing from {:?})",
|
||||||
|
new, prev
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(prev) = prev {
|
||||||
|
if let Some(button) = buttons.get(&prev) {
|
||||||
|
button.set_focused(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(button) = buttons.get(&new) {
|
||||||
|
button.set_focused(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::Title(app_id, _, name) => {
|
||||||
|
debug!("Updating title for item with id {}: {:?}", app_id, name);
|
||||||
|
|
||||||
|
if show_names {
|
||||||
|
if let Some(button) = buttons.get(&app_id) {
|
||||||
|
button.button.set_label(&name.unwrap_or_default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::Urgent(app_id, urgent) => {
|
||||||
|
debug!("Updating urgency for item with id {}: {}", app_id, urgent);
|
||||||
|
|
||||||
|
if let Some(button) = buttons.get(&app_id) {
|
||||||
|
button.set_urgent(urgent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::Hover(_) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let popup = self.into_popup(context.controller_tx, context.popup_rx);
|
||||||
|
Ok(ModuleWidget {
|
||||||
|
widget: container,
|
||||||
|
popup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_popup(
|
||||||
|
self,
|
||||||
|
controller_tx: Sender<Self::ReceiveMessage>,
|
||||||
|
rx: glib::Receiver<Self::SendMessage>,
|
||||||
|
) -> Option<gtk::Box> {
|
||||||
|
let container = gtk::Box::new(Orientation::Vertical, 0);
|
||||||
|
|
||||||
|
let mut buttons = Collection::<String, Collection<i64, Button>>::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let container = container.clone();
|
||||||
rx.attach(None, move |event| {
|
rx.attach(None, move |event| {
|
||||||
match event.change.as_str() {
|
match event {
|
||||||
"new" => launcher.add_window(event.container),
|
LauncherUpdate::AddItem(item) => {
|
||||||
"close" => launcher.remove_window(&event.container),
|
let app_id = item.app_id.clone();
|
||||||
"focus" => launcher.set_window_focused(&event.container),
|
|
||||||
"title" => launcher.set_window_title(event.container),
|
let window_buttons = item
|
||||||
"urgent" => launcher.set_window_urgent(&event.container),
|
.windows
|
||||||
|
.into_iter()
|
||||||
|
.map(|win| {
|
||||||
|
let button = Button::builder()
|
||||||
|
.label(win.name.as_ref().unwrap_or(&String::new()))
|
||||||
|
.height_request(40)
|
||||||
|
.width_request(100)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = controller_tx.clone();
|
||||||
|
button.connect_clicked(move |button| {
|
||||||
|
tx.try_send(ItemEvent::FocusWindow(win.id))
|
||||||
|
.expect("Failed to send window click event");
|
||||||
|
|
||||||
|
if let Some(win) = button.window() {
|
||||||
|
win.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(win.id, button)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
buttons.insert(app_id, window_buttons);
|
||||||
|
}
|
||||||
|
LauncherUpdate::AddWindow(app_id, win) => {
|
||||||
|
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||||
|
let button = Button::builder()
|
||||||
|
.label(win.name.as_ref().unwrap_or(&String::new()))
|
||||||
|
.height_request(40)
|
||||||
|
.width_request(100)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = controller_tx.clone();
|
||||||
|
button.connect_clicked(move |button| {
|
||||||
|
tx.try_send(ItemEvent::FocusWindow(win.id))
|
||||||
|
.expect("Failed to send window click event");
|
||||||
|
|
||||||
|
if let Some(win) = button.window() {
|
||||||
|
win.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.insert(win.id, button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::RemoveWindow(app_id, win_id) => {
|
||||||
|
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||||
|
buttons.remove(&win_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::Title(app_id, win_id, title) => {
|
||||||
|
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||||
|
if let Some(button) = buttons.get(&win_id) {
|
||||||
|
if let Some(title) = title {
|
||||||
|
button.set_label(&title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherUpdate::Hover(app_id) => {
|
||||||
|
// empty current buttons
|
||||||
|
for child in container.children() {
|
||||||
|
container.remove(&child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add app's buttons
|
||||||
|
if let Some(buttons) = buttons.get(&app_id) {
|
||||||
|
for button in buttons {
|
||||||
|
container.add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.show_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,22 +540,6 @@ impl Module<gtk::Box> for LauncherModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn(async move {
|
Some(container)
|
||||||
let sway = get_client();
|
|
||||||
|
|
||||||
while let Some(event) = ui_rx.recv().await {
|
|
||||||
let selector = match event {
|
|
||||||
FocusEvent::AppId(app_id) => format!("[app_id={}]", app_id),
|
|
||||||
FocusEvent::Class(class) => format!("[class={}]", class),
|
|
||||||
FocusEvent::ConId(id) => format!("[con_id={}]", id),
|
|
||||||
};
|
|
||||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
|
||||||
sway.run(format!("{} focus", selector))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), Report>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(container)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::sway::SwayNode;
|
use swayipc_async::Node;
|
||||||
|
|
||||||
/// Open state for a launcher item, or item window.
|
/// Open state for a launcher item, or item window.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
|
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
|
||||||
|
@ -9,25 +9,17 @@ pub enum OpenState {
|
||||||
|
|
||||||
impl OpenState {
|
impl OpenState {
|
||||||
/// Creates from `SwayNode`
|
/// Creates from `SwayNode`
|
||||||
pub const fn from_node(node: &SwayNode) -> Self {
|
pub const fn from_node(node: &Node) -> Self {
|
||||||
Self::Open {
|
Self::Open {
|
||||||
focused: node.focused,
|
focused: node.focused,
|
||||||
urgent: node.urgent,
|
urgent: node.urgent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates open without focused/urgent
|
|
||||||
pub const fn open() -> Self {
|
|
||||||
Self::Open {
|
|
||||||
focused: false,
|
|
||||||
urgent: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates open with focused
|
/// Creates open with focused
|
||||||
pub const fn focused() -> Self {
|
pub const fn focused(focused: bool) -> Self {
|
||||||
Self::Open {
|
Self::Open {
|
||||||
focused: true,
|
focused,
|
||||||
urgent: false,
|
urgent: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +51,7 @@ impl OpenState {
|
||||||
/// This is effectively an OR operation,
|
/// This is effectively an OR operation,
|
||||||
/// so sets state to open and flags to true if any state is open
|
/// so sets state to open and flags to true if any state is open
|
||||||
/// or any instance of the flag is true.
|
/// or any instance of the flag is true.
|
||||||
pub fn merge_states(states: Vec<&Self>) -> Self {
|
pub fn merge_states(states: &[&Self]) -> Self {
|
||||||
states.iter().fold(Self::Closed, |merged, current| {
|
states.iter().fold(Self::Closed, |merged, current| {
|
||||||
if merged.is_open() || current.is_open() {
|
if merged.is_open() || current.is_open() {
|
||||||
Self::Open {
|
Self::Open {
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
use crate::modules::launcher::item::LauncherWindow;
|
|
||||||
use crate::modules::launcher::FocusEvent;
|
|
||||||
pub use crate::popup::Popup;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::Button;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
impl Popup {
|
|
||||||
pub fn set_windows(&self, windows: &[LauncherWindow], tx: &mpsc::Sender<FocusEvent>) {
|
|
||||||
// clear
|
|
||||||
for child in self.container.children() {
|
|
||||||
self.container.remove(&child);
|
|
||||||
}
|
|
||||||
|
|
||||||
for window in windows {
|
|
||||||
let mut button_builder = Button::builder().height_request(40);
|
|
||||||
|
|
||||||
if let Some(name) = &window.name {
|
|
||||||
button_builder = button_builder.label(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let button = button_builder.build();
|
|
||||||
|
|
||||||
let con_id = window.con_id;
|
|
||||||
let window = self.window.clone();
|
|
||||||
let tx = tx.clone();
|
|
||||||
button.connect_clicked(move |_| {
|
|
||||||
tx.try_send(FocusEvent::ConId(con_id))
|
|
||||||
.expect("Failed to send focus event");
|
|
||||||
window.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.container.add(&button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,11 +15,11 @@ pub mod workspaces;
|
||||||
|
|
||||||
use crate::config::BarPosition;
|
use crate::config::BarPosition;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
/// Shamelessly stolen from here:
|
use derive_builder::Builder;
|
||||||
/// <https://github.com/zeroeightysix/rustbar/blob/master/src/modules/module.rs>
|
|
||||||
use glib::IsA;
|
use glib::IsA;
|
||||||
use gtk::gdk::Monitor;
|
use gtk::gdk::Monitor;
|
||||||
use gtk::{Application, Widget};
|
use gtk::{Application, Widget};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ModuleLocation {
|
pub enum ModuleLocation {
|
||||||
|
@ -28,19 +28,71 @@ pub enum ModuleLocation {
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Builder)]
|
||||||
pub struct ModuleInfo<'a> {
|
pub struct ModuleInfo<'a> {
|
||||||
pub app: &'a Application,
|
pub app: &'a Application,
|
||||||
pub location: ModuleLocation,
|
pub location: ModuleLocation,
|
||||||
pub bar_position: &'a BarPosition,
|
pub bar_position: &'a BarPosition,
|
||||||
pub monitor: &'a Monitor,
|
pub monitor: &'a Monitor,
|
||||||
pub output_name: &'a str,
|
pub output_name: &'a str,
|
||||||
|
pub module_name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ModuleUpdateEvent<T> {
|
||||||
|
/// Sends an update to the module UI
|
||||||
|
Update(T),
|
||||||
|
/// Toggles the open state of the popup.
|
||||||
|
/// Takes the button X position and width.
|
||||||
|
TogglePopup((i32, i32)),
|
||||||
|
/// Force sets the popup open.
|
||||||
|
/// Takes the button X position and width.
|
||||||
|
OpenPopup((i32, i32)),
|
||||||
|
/// Force sets the popup closed.
|
||||||
|
ClosePopup,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WidgetContext<TSend, TReceive> {
|
||||||
|
pub id: usize,
|
||||||
|
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
|
||||||
|
pub controller_tx: mpsc::Sender<TReceive>,
|
||||||
|
pub widget_rx: glib::Receiver<TSend>,
|
||||||
|
pub popup_rx: glib::Receiver<TSend>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModuleWidget<W: IsA<Widget>> {
|
||||||
|
pub widget: W,
|
||||||
|
pub popup: Option<gtk::Box>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Module<W>
|
pub trait Module<W>
|
||||||
where
|
where
|
||||||
W: IsA<Widget>,
|
W: IsA<Widget>,
|
||||||
{
|
{
|
||||||
/// Consumes the module config
|
type SendMessage;
|
||||||
/// and produces a GTK widget of type `W`
|
type ReceiveMessage;
|
||||||
fn into_widget(self, info: &ModuleInfo) -> Result<W>;
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<W>>;
|
||||||
|
|
||||||
|
fn into_popup(
|
||||||
|
self,
|
||||||
|
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||||
|
_rx: glib::Receiver<Self::SendMessage>,
|
||||||
|
) -> Option<gtk::Box>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +1,126 @@
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use mpd_client::client::Connection;
|
use mpd_client::client::{CommandError, Connection, ConnectionEvent, Subsystem};
|
||||||
|
use mpd_client::commands::Command;
|
||||||
use mpd_client::protocol::MpdProtocolError;
|
use mpd_client::protocol::MpdProtocolError;
|
||||||
use mpd_client::responses::Status;
|
use mpd_client::responses::Status;
|
||||||
use mpd_client::Client;
|
use mpd_client::Client;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
use std::path::PathBuf;
|
use std::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::net::{TcpStream, UnixStream};
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CLIENTS: Arc<Mutex<HashMap<String, Arc<Client>>>> =
|
static ref CONNECTIONS: Arc<Mutex<HashMap<String, Arc<MpdClient>>>> =
|
||||||
Arc::new(Mutex::new(HashMap::new()));
|
Arc::new(Mutex::new(HashMap::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_connection(host: &str) -> Option<Arc<Client>> {
|
pub struct MpdClient {
|
||||||
let mut clients = CLIENTS.lock().await;
|
client: Client,
|
||||||
|
tx: Sender<()>,
|
||||||
|
_rx: Receiver<()>,
|
||||||
|
}
|
||||||
|
|
||||||
match clients.get(host) {
|
#[derive(Debug)]
|
||||||
Some(client) => Some(Arc::clone(client)),
|
pub enum MpdConnectionError {
|
||||||
None => {
|
MaxRetries,
|
||||||
let client = wait_for_connection(host, Duration::from_secs(5), None).await?;
|
ProtocolError(MpdProtocolError),
|
||||||
let client = Arc::new(client);
|
}
|
||||||
clients.insert(host.to_string(), Arc::clone(&client));
|
|
||||||
Some(client)
|
impl Display for MpdConnectionError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::MaxRetries => write!(f, "Reached max retries"),
|
||||||
|
Self::ProtocolError(e) => write!(f, "{:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for MpdConnectionError {}
|
||||||
|
|
||||||
|
impl MpdClient {
|
||||||
|
async fn new(host: &str) -> 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) = channel(16);
|
||||||
|
let tx2 = tx.clone();
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(change) = state_changes.next().await {
|
||||||
|
debug!("Received state change: {:?}", change);
|
||||||
|
|
||||||
|
if let ConnectionEvent::SubsystemChange(Subsystem::Player | Subsystem::Queue) =
|
||||||
|
change
|
||||||
|
{
|
||||||
|
tx2.send(())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), SendError<()>>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
tx,
|
||||||
|
_rx: rx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self) -> Receiver<()> {
|
||||||
|
self.tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn command<C: Command>(&self, command: C) -> Result<C::Response, CommandError> {
|
||||||
|
self.client.command(command).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_client(host: &str) -> Result<Arc<MpdClient>, MpdConnectionError> {
|
||||||
|
let mut connections = CONNECTIONS.lock().await;
|
||||||
|
match connections.get(host) {
|
||||||
|
None => {
|
||||||
|
let client = MpdClient::new(host).await?;
|
||||||
|
let client = Arc::new(client);
|
||||||
|
connections.insert(host.to_string(), Arc::clone(&client));
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
Some(client) => Ok(Arc::clone(client)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn wait_for_connection(
|
async fn wait_for_connection(
|
||||||
host: &str,
|
host: &str,
|
||||||
interval: Duration,
|
interval: Duration,
|
||||||
max_retries: Option<usize>,
|
max_retries: Option<usize>,
|
||||||
) -> Option<Client> {
|
) -> Result<Connection, MpdConnectionError> {
|
||||||
let mut retries = 0;
|
let mut retries = 0;
|
||||||
let max_retries = max_retries.unwrap_or(usize::MAX);
|
let max_retries = max_retries.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if retries == max_retries {
|
if retries == max_retries {
|
||||||
break None;
|
break Err(MpdConnectionError::MaxRetries);
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(conn) = try_get_mpd_conn(host).await {
|
|
||||||
break Some(conn.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
retries += 1;
|
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;
|
sleep(interval).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,14 +128,12 @@ async fn wait_for_connection(
|
||||||
/// Cycles through each MPD host and
|
/// Cycles through each MPD host and
|
||||||
/// returns the first one which connects,
|
/// returns the first one which connects,
|
||||||
/// or none if there are none
|
/// or none if there are none
|
||||||
async fn try_get_mpd_conn(host: &str) -> Option<Connection> {
|
async fn try_get_mpd_conn(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||||
let connection = if is_unix_socket(host) {
|
if is_unix_socket(host) {
|
||||||
connect_unix(host).await
|
connect_unix(host).await
|
||||||
} else {
|
} else {
|
||||||
connect_tcp(host).await
|
connect_tcp(host).await
|
||||||
};
|
}
|
||||||
|
|
||||||
connection.ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_unix_socket(host: &str) -> bool {
|
fn is_unix_socket(host: &str) -> bool {
|
||||||
|
|
|
@ -1,27 +1,52 @@
|
||||||
mod client;
|
mod client;
|
||||||
mod popup;
|
|
||||||
|
|
||||||
use self::popup::Popup;
|
use crate::modules::mpd::client::MpdConnectionError;
|
||||||
use crate::modules::mpd::client::{get_connection, get_duration, get_elapsed};
|
use crate::modules::mpd::client::{get_client, get_duration, get_elapsed};
|
||||||
use crate::modules::mpd::popup::{MpdPopup, PopupEvent};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::modules::{Module, ModuleInfo};
|
use crate::popup::Popup;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use dirs::{audio_dir, home_dir};
|
use dirs::{audio_dir, home_dir};
|
||||||
use glib::Continue;
|
use glib::Continue;
|
||||||
|
use gtk::gdk_pixbuf::Pixbuf;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, Orientation};
|
use gtk::{Button, Image, Label, Orientation};
|
||||||
use mpd_client::commands;
|
use mpd_client::commands;
|
||||||
use mpd_client::responses::{PlayState, Song, Status};
|
use mpd_client::responses::{PlayState, Song, Status};
|
||||||
use mpd_client::tag::Tag;
|
use mpd_client::tag::Tag;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::time::sleep;
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PlayerCommand {
|
||||||
|
Previous,
|
||||||
|
Toggle,
|
||||||
|
Next,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct Icons {
|
||||||
|
/// Icon to display when playing.
|
||||||
|
#[serde(default = "default_icon_play")]
|
||||||
|
play: Option<String>,
|
||||||
|
/// Icon to display when paused.
|
||||||
|
#[serde(default = "default_icon_pause")]
|
||||||
|
pause: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Icons {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pause: default_icon_pause(),
|
||||||
|
play: default_icon_play(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct MpdModule {
|
pub struct MpdModule {
|
||||||
/// TCP or Unix socket address.
|
/// TCP or Unix socket address.
|
||||||
|
@ -30,12 +55,10 @@ pub struct MpdModule {
|
||||||
/// Format of current song info to display on the bar.
|
/// Format of current song info to display on the bar.
|
||||||
#[serde(default = "default_format")]
|
#[serde(default = "default_format")]
|
||||||
format: String,
|
format: String,
|
||||||
/// Icon to display when playing.
|
|
||||||
#[serde(default = "default_icon_play")]
|
/// Player state icons
|
||||||
icon_play: Option<String>,
|
#[serde(default)]
|
||||||
/// Icon to display when paused.
|
icons: Icons,
|
||||||
#[serde(default = "default_icon_pause")]
|
|
||||||
icon_pause: Option<String>,
|
|
||||||
|
|
||||||
/// Path to root of music directory.
|
/// Path to root of music directory.
|
||||||
#[serde(default = "default_music_dir")]
|
#[serde(default = "default_music_dir")]
|
||||||
|
@ -89,76 +112,72 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Event {
|
#[derive(Clone, Debug)]
|
||||||
Open,
|
pub struct SongUpdate {
|
||||||
Update(Box<Option<(Song, Status, String)>>),
|
song: Song,
|
||||||
|
status: Status,
|
||||||
|
display_string: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<Button> for MpdModule {
|
impl Module<Button> for MpdModule {
|
||||||
fn into_widget(self, info: &ModuleInfo) -> Result<Button> {
|
type SendMessage = Option<SongUpdate>;
|
||||||
|
type ReceiveMessage = PlayerCommand;
|
||||||
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let host1 = self.host.clone();
|
||||||
|
let host2 = self.host.clone();
|
||||||
|
let format = self.format.clone();
|
||||||
|
let icons = self.icons.clone();
|
||||||
|
|
||||||
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 button = Button::new();
|
// poll mpd server
|
||||||
|
|
||||||
let (ui_tx, mut ui_rx) = mpsc::channel(32);
|
|
||||||
|
|
||||||
let popup = Popup::new(
|
|
||||||
"popup-mpd",
|
|
||||||
info.app,
|
|
||||||
info.monitor,
|
|
||||||
Orientation::Horizontal,
|
|
||||||
info.bar_position,
|
|
||||||
);
|
|
||||||
let mpd_popup = MpdPopup::new(popup, ui_tx);
|
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
let click_tx = tx.clone();
|
|
||||||
|
|
||||||
let music_dir = self.music_dir.clone();
|
|
||||||
|
|
||||||
button.connect_clicked(move |_| {
|
|
||||||
click_tx
|
|
||||||
.send(Event::Open)
|
|
||||||
.expect("Failed to send popup open event");
|
|
||||||
});
|
|
||||||
|
|
||||||
let host = self.host.clone();
|
|
||||||
let host2 = self.host.clone();
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let client = get_connection(&host)
|
let client = get_client(&host1).await.expect("Failed to connect to MPD");
|
||||||
.await
|
let mut mpd_rx = client.subscribe();
|
||||||
.expect("Unexpected error when trying to connect to MPD server");
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let current_song = client.command(commands::CurrentSong).await;
|
let current_song = client.command(commands::CurrentSong).await;
|
||||||
let status = client.command(commands::Status).await;
|
let status = client.command(commands::Status).await;
|
||||||
|
|
||||||
if let (Ok(Some(song)), Ok(status)) = (current_song, status) {
|
if let (Ok(Some(song)), Ok(status)) = (current_song, status) {
|
||||||
let string = self
|
let display_string =
|
||||||
.replace_tokens(self.format.as_str(), &tokens, &song.song, &status)
|
replace_tokens(format.as_str(), &tokens, &song.song, &status, &icons);
|
||||||
.await;
|
|
||||||
|
|
||||||
tx.send(Event::Update(Box::new(Some((song.song, status, string)))))
|
let update = SongUpdate {
|
||||||
.expect("Failed to send update event");
|
song: song.song,
|
||||||
|
status,
|
||||||
|
display_string,
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.send(ModuleUpdateEvent::Update(Some(update))).await?;
|
||||||
} else {
|
} else {
|
||||||
tx.send(Event::Update(Box::new(None)))
|
tx.send(ModuleUpdateEvent::Update(None)).await?;
|
||||||
.expect("Failed to send update event");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sleep(Duration::from_secs(1)).await;
|
// wait for player state change
|
||||||
|
if mpd_rx.recv().await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// listen to ui events
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let client = get_connection(&host2)
|
let client = get_client(&host2).await?;
|
||||||
.await
|
|
||||||
.expect("Unexpected error when trying to connect to MPD server");
|
|
||||||
|
|
||||||
while let Some(event) = ui_rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
let res = match event {
|
let res = match event {
|
||||||
PopupEvent::Previous => client.command(commands::Previous).await,
|
PlayerCommand::Previous => client.command(commands::Previous).await,
|
||||||
PopupEvent::Toggle => match client.command(commands::Status).await {
|
PlayerCommand::Toggle => match client.command(commands::Status).await {
|
||||||
Ok(status) => match status.state {
|
Ok(status) => match status.state {
|
||||||
PlayState::Playing => client.command(commands::SetPause(true)).await,
|
PlayState::Playing => client.command(commands::SetPause(true)).await,
|
||||||
PlayState::Paused => client.command(commands::SetPause(false)).await,
|
PlayState::Paused => client.command(commands::SetPause(false)).await,
|
||||||
|
@ -166,86 +185,258 @@ impl Module<Button> for MpdModule {
|
||||||
},
|
},
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
PopupEvent::Next => client.command(commands::Next).await,
|
PlayerCommand::Next => client.command(commands::Next).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("Failed to send command to MPD server: {:?}", err);
|
error!("Failed to send command to MPD server: {:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok::<(), MpdConnectionError>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<Button>> {
|
||||||
|
let button = Button::new();
|
||||||
|
|
||||||
|
button.connect_clicked(move |button| {
|
||||||
|
context
|
||||||
|
.tx
|
||||||
|
.try_send(ModuleUpdateEvent::TogglePopup(Popup::button_pos(button)))
|
||||||
|
.expect("Failed to send MPD popup open event");
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let button = button.clone();
|
let button = button.clone();
|
||||||
|
|
||||||
rx.attach(None, move |event| {
|
context.widget_rx.attach(None, move |mut event| {
|
||||||
match event {
|
if let Some(event) = event.take() {
|
||||||
Event::Open => {
|
button.set_label(&event.display_string);
|
||||||
mpd_popup.popup.show(&button);
|
button.show();
|
||||||
}
|
} else {
|
||||||
Event::Update(mut msg) => {
|
button.hide();
|
||||||
if let Some((song, status, string)) = msg.take() {
|
|
||||||
mpd_popup.update(&song, &status, music_dir.as_path());
|
|
||||||
|
|
||||||
button.set_label(&string);
|
|
||||||
button.show();
|
|
||||||
} else {
|
|
||||||
button.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(button)
|
let popup = self.into_popup(context.controller_tx, context.popup_rx);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MpdModule {
|
Ok(ModuleWidget {
|
||||||
/// Replaces each of the formatting tokens in the formatting string
|
widget: button,
|
||||||
/// with actual data pulled from MPD
|
popup,
|
||||||
async fn replace_tokens(
|
})
|
||||||
&self,
|
}
|
||||||
format_string: &str,
|
|
||||||
tokens: &Vec<String>,
|
fn into_popup(
|
||||||
song: &Song,
|
self,
|
||||||
status: &Status,
|
tx: Sender<Self::ReceiveMessage>,
|
||||||
) -> String {
|
rx: glib::Receiver<Self::SendMessage>,
|
||||||
let mut compiled_string = format_string.to_string();
|
) -> Option<gtk::Box> {
|
||||||
for token in tokens {
|
let container = gtk::Box::builder()
|
||||||
let value = self.get_token_value(song, status, token).await;
|
.orientation(Orientation::Horizontal)
|
||||||
compiled_string =
|
.spacing(10)
|
||||||
compiled_string.replace(format!("{{{}}}", token).as_str(), value.as_str());
|
.name("popup-mpd")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let album_image = Image::builder()
|
||||||
|
.width_request(128)
|
||||||
|
.height_request(128)
|
||||||
|
.name("album-art")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let info_box = gtk::Box::new(Orientation::Vertical, 10);
|
||||||
|
let title_label = IconLabel::new("\u{f886}", None);
|
||||||
|
let album_label = IconLabel::new("\u{f524}", None);
|
||||||
|
let artist_label = IconLabel::new("\u{fd01}", None);
|
||||||
|
|
||||||
|
title_label.container.set_widget_name("title");
|
||||||
|
album_label.container.set_widget_name("album");
|
||||||
|
artist_label.container.set_widget_name("label");
|
||||||
|
|
||||||
|
info_box.add(&title_label.container);
|
||||||
|
info_box.add(&album_label.container);
|
||||||
|
info_box.add(&artist_label.container);
|
||||||
|
|
||||||
|
let controls_box = gtk::Box::builder().name("controls").build();
|
||||||
|
let btn_prev = Button::builder().label("\u{f9ad}").name("btn-prev").build();
|
||||||
|
let btn_play_pause = Button::builder().label("").name("btn-play-pause").build();
|
||||||
|
let btn_next = Button::builder().label("\u{f9ac}").name("btn-next").build();
|
||||||
|
|
||||||
|
controls_box.add(&btn_prev);
|
||||||
|
controls_box.add(&btn_play_pause);
|
||||||
|
controls_box.add(&btn_next);
|
||||||
|
|
||||||
|
info_box.add(&controls_box);
|
||||||
|
|
||||||
|
container.add(&album_image);
|
||||||
|
container.add(&info_box);
|
||||||
|
|
||||||
|
let tx_prev = tx.clone();
|
||||||
|
btn_prev.connect_clicked(move |_| {
|
||||||
|
tx_prev
|
||||||
|
.try_send(PlayerCommand::Previous)
|
||||||
|
.expect("Failed to send prev track message");
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx_toggle = tx.clone();
|
||||||
|
btn_play_pause.connect_clicked(move |_| {
|
||||||
|
tx_toggle
|
||||||
|
.try_send(PlayerCommand::Toggle)
|
||||||
|
.expect("Failed to send play/pause track message");
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx_next = tx;
|
||||||
|
btn_next.connect_clicked(move |_| {
|
||||||
|
tx_next
|
||||||
|
.try_send(PlayerCommand::Next)
|
||||||
|
.expect("Failed to send next track message");
|
||||||
|
});
|
||||||
|
|
||||||
|
container.show_all();
|
||||||
|
|
||||||
|
{
|
||||||
|
let music_dir = self.music_dir;
|
||||||
|
|
||||||
|
rx.attach(None, move |update| {
|
||||||
|
if let Some(update) = update {
|
||||||
|
let prev_album = album_label.label.text();
|
||||||
|
let curr_album = update.song.album().unwrap_or_default();
|
||||||
|
|
||||||
|
// only update art when album changes
|
||||||
|
if prev_album != curr_album {
|
||||||
|
let cover_path = music_dir.join(
|
||||||
|
update
|
||||||
|
.song
|
||||||
|
.file_path()
|
||||||
|
.parent()
|
||||||
|
.expect("Song path should not be root")
|
||||||
|
.join("cover.jpg"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(pixbuf) = Pixbuf::from_file_at_scale(cover_path, 128, 128, true) {
|
||||||
|
album_image.set_from_pixbuf(Some(&pixbuf));
|
||||||
|
} else {
|
||||||
|
album_image.set_from_pixbuf(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title_label
|
||||||
|
.label
|
||||||
|
.set_text(update.song.title().unwrap_or_default());
|
||||||
|
album_label.label.set_text(curr_album);
|
||||||
|
artist_label
|
||||||
|
.label
|
||||||
|
.set_text(update.song.artists().first().unwrap_or(&String::new()));
|
||||||
|
|
||||||
|
match update.status.state {
|
||||||
|
PlayState::Stopped => {
|
||||||
|
btn_play_pause.set_sensitive(false);
|
||||||
|
}
|
||||||
|
PlayState::Playing => {
|
||||||
|
btn_play_pause.set_sensitive(true);
|
||||||
|
btn_play_pause.set_label("");
|
||||||
|
}
|
||||||
|
PlayState::Paused => {
|
||||||
|
btn_play_pause.set_sensitive(true);
|
||||||
|
btn_play_pause.set_label("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let enable_prev = match update.status.current_song {
|
||||||
|
Some((pos, _)) => pos.0 > 0,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let enable_next = match update.status.current_song {
|
||||||
|
Some((pos, _)) => pos.0 < update.status.playlist_length,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
btn_prev.set_sensitive(enable_prev);
|
||||||
|
btn_next.set_sensitive(enable_next);
|
||||||
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
compiled_string
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a string format token value
|
Some(container)
|
||||||
/// into its respective MPD value.
|
}
|
||||||
pub async fn get_token_value(&self, song: &Song, status: &Status, token: &str) -> String {
|
}
|
||||||
let s = match token {
|
|
||||||
"icon" => {
|
/// Replaces each of the formatting tokens in the formatting string
|
||||||
let icon = match status.state {
|
/// with actual data pulled from MPD
|
||||||
PlayState::Stopped => None,
|
fn replace_tokens(
|
||||||
PlayState::Playing => self.icon_play.as_ref(),
|
format_string: &str,
|
||||||
PlayState::Paused => self.icon_pause.as_ref(),
|
tokens: &Vec<String>,
|
||||||
};
|
song: &Song,
|
||||||
icon.map(String::as_str)
|
status: &Status,
|
||||||
}
|
icons: &Icons,
|
||||||
"title" => song.title(),
|
) -> String {
|
||||||
"album" => try_get_first_tag(song.tags.get(&Tag::Album)),
|
let mut compiled_string = format_string.to_string();
|
||||||
"artist" => try_get_first_tag(song.tags.get(&Tag::Artist)),
|
for token in tokens {
|
||||||
"date" => try_get_first_tag(song.tags.get(&Tag::Date)),
|
let value = get_token_value(song, status, icons, token);
|
||||||
"disc" => try_get_first_tag(song.tags.get(&Tag::Disc)),
|
compiled_string =
|
||||||
"genre" => try_get_first_tag(song.tags.get(&Tag::Genre)),
|
compiled_string.replace(format!("{{{}}}", token).as_str(), value.as_str());
|
||||||
"track" => try_get_first_tag(song.tags.get(&Tag::Track)),
|
}
|
||||||
"duration" => return get_duration(status).map(format_time).unwrap_or_default(),
|
compiled_string
|
||||||
|
}
|
||||||
"elapsed" => return get_elapsed(status).map(format_time).unwrap_or_default(),
|
|
||||||
_ => Some(token),
|
/// Converts a string format token value
|
||||||
};
|
/// into its respective MPD value.
|
||||||
s.unwrap_or_default().to_string()
|
fn get_token_value(song: &Song, status: &Status, icons: &Icons, token: &str) -> String {
|
||||||
|
let s = match token {
|
||||||
|
"icon" => {
|
||||||
|
let icon = match status.state {
|
||||||
|
PlayState::Stopped => None,
|
||||||
|
PlayState::Playing => icons.play.as_ref(),
|
||||||
|
PlayState::Paused => icons.pause.as_ref(),
|
||||||
|
};
|
||||||
|
icon.map(String::as_str)
|
||||||
|
}
|
||||||
|
"title" => song.title(),
|
||||||
|
"album" => try_get_first_tag(song.tags.get(&Tag::Album)),
|
||||||
|
"artist" => try_get_first_tag(song.tags.get(&Tag::Artist)),
|
||||||
|
"date" => try_get_first_tag(song.tags.get(&Tag::Date)),
|
||||||
|
"disc" => try_get_first_tag(song.tags.get(&Tag::Disc)),
|
||||||
|
"genre" => try_get_first_tag(song.tags.get(&Tag::Genre)),
|
||||||
|
"track" => try_get_first_tag(song.tags.get(&Tag::Track)),
|
||||||
|
"duration" => return get_duration(status).map(format_time).unwrap_or_default(),
|
||||||
|
|
||||||
|
"elapsed" => return get_elapsed(status).map(format_time).unwrap_or_default(),
|
||||||
|
_ => Some(token),
|
||||||
|
};
|
||||||
|
s.unwrap_or_default().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct IconLabel {
|
||||||
|
label: Label,
|
||||||
|
container: gtk::Box,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconLabel {
|
||||||
|
fn new(icon: &str, label: Option<&str>) -> Self {
|
||||||
|
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||||
|
|
||||||
|
let icon = Label::new(Some(icon));
|
||||||
|
let label = Label::new(label);
|
||||||
|
|
||||||
|
icon.style_context().add_class("icon");
|
||||||
|
label.style_context().add_class("label");
|
||||||
|
|
||||||
|
container.add(&icon);
|
||||||
|
container.add(&label);
|
||||||
|
|
||||||
|
Self { label, container }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,175 +0,0 @@
|
||||||
pub use crate::popup::Popup;
|
|
||||||
use gtk::gdk_pixbuf::Pixbuf;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::{Button, Image, Label, Orientation};
|
|
||||||
use mpd_client::responses::{PlayState, Song, Status};
|
|
||||||
use std::path::Path;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct IconLabel {
|
|
||||||
label: Label,
|
|
||||||
container: gtk::Box,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IconLabel {
|
|
||||||
fn new(icon: &str, label: Option<&str>) -> Self {
|
|
||||||
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
|
||||||
|
|
||||||
let icon = Label::new(Some(icon));
|
|
||||||
let label = Label::new(label);
|
|
||||||
|
|
||||||
icon.style_context().add_class("icon");
|
|
||||||
label.style_context().add_class("label");
|
|
||||||
|
|
||||||
container.add(&icon);
|
|
||||||
container.add(&label);
|
|
||||||
|
|
||||||
Self { label, container }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MpdPopup {
|
|
||||||
pub popup: Popup,
|
|
||||||
|
|
||||||
cover: Image,
|
|
||||||
|
|
||||||
title: IconLabel,
|
|
||||||
album: IconLabel,
|
|
||||||
artist: IconLabel,
|
|
||||||
|
|
||||||
btn_prev: Button,
|
|
||||||
btn_play_pause: Button,
|
|
||||||
btn_next: Button,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum PopupEvent {
|
|
||||||
Previous,
|
|
||||||
Toggle,
|
|
||||||
Next,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MpdPopup {
|
|
||||||
pub fn new(popup: Popup, tx: mpsc::Sender<PopupEvent>) -> Self {
|
|
||||||
let album_image = Image::builder()
|
|
||||||
.width_request(128)
|
|
||||||
.height_request(128)
|
|
||||||
.name("album-art")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let info_box = gtk::Box::new(Orientation::Vertical, 10);
|
|
||||||
|
|
||||||
let title_label = IconLabel::new("\u{f886}", None);
|
|
||||||
let album_label = IconLabel::new("\u{f524}", None);
|
|
||||||
let artist_label = IconLabel::new("\u{fd01}", None);
|
|
||||||
|
|
||||||
title_label.container.set_widget_name("title");
|
|
||||||
album_label.container.set_widget_name("album");
|
|
||||||
artist_label.container.set_widget_name("label");
|
|
||||||
|
|
||||||
info_box.add(&title_label.container);
|
|
||||||
info_box.add(&album_label.container);
|
|
||||||
info_box.add(&artist_label.container);
|
|
||||||
|
|
||||||
let controls_box = gtk::Box::builder().name("controls").build();
|
|
||||||
|
|
||||||
let btn_prev = Button::builder().label("\u{f9ad}").name("btn-prev").build();
|
|
||||||
let btn_play_pause = Button::builder().label("").name("btn-play-pause").build();
|
|
||||||
let btn_next = Button::builder().label("\u{f9ac}").name("btn-next").build();
|
|
||||||
|
|
||||||
controls_box.add(&btn_prev);
|
|
||||||
controls_box.add(&btn_play_pause);
|
|
||||||
controls_box.add(&btn_next);
|
|
||||||
|
|
||||||
info_box.add(&controls_box);
|
|
||||||
|
|
||||||
popup.container.add(&album_image);
|
|
||||||
popup.container.add(&info_box);
|
|
||||||
|
|
||||||
let tx_prev = tx.clone();
|
|
||||||
btn_prev.connect_clicked(move |_| {
|
|
||||||
tx_prev
|
|
||||||
.try_send(PopupEvent::Previous)
|
|
||||||
.expect("Failed to send prev track message");
|
|
||||||
});
|
|
||||||
|
|
||||||
let tx_toggle = tx.clone();
|
|
||||||
btn_play_pause.connect_clicked(move |_| {
|
|
||||||
tx_toggle
|
|
||||||
.try_send(PopupEvent::Toggle)
|
|
||||||
.expect("Failed to send play/pause track message");
|
|
||||||
});
|
|
||||||
|
|
||||||
let tx_next = tx;
|
|
||||||
btn_next.connect_clicked(move |_| {
|
|
||||||
tx_next
|
|
||||||
.try_send(PopupEvent::Next)
|
|
||||||
.expect("Failed to send next track message");
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
popup,
|
|
||||||
cover: album_image,
|
|
||||||
artist: artist_label,
|
|
||||||
album: album_label,
|
|
||||||
title: title_label,
|
|
||||||
btn_prev,
|
|
||||||
btn_play_pause,
|
|
||||||
btn_next,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&self, song: &Song, status: &Status, path: &Path) {
|
|
||||||
let prev_album = self.album.label.text();
|
|
||||||
let curr_album = song.album().unwrap_or_default();
|
|
||||||
|
|
||||||
// only update art when album changes
|
|
||||||
if prev_album != curr_album {
|
|
||||||
let cover_path = path.join(
|
|
||||||
song.file_path()
|
|
||||||
.parent()
|
|
||||||
.expect("Song path should not be root")
|
|
||||||
.join("cover.jpg"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Ok(pixbuf) = Pixbuf::from_file_at_scale(cover_path, 128, 128, true) {
|
|
||||||
self.cover.set_from_pixbuf(Some(&pixbuf));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.title.label.set_text(song.title().unwrap_or_default());
|
|
||||||
self.album.label.set_text(song.album().unwrap_or_default());
|
|
||||||
self.artist
|
|
||||||
.label
|
|
||||||
.set_text(song.artists().first().unwrap_or(&String::new()));
|
|
||||||
|
|
||||||
match status.state {
|
|
||||||
PlayState::Stopped => {
|
|
||||||
self.btn_play_pause.set_sensitive(false);
|
|
||||||
}
|
|
||||||
PlayState::Playing => {
|
|
||||||
self.btn_play_pause.set_sensitive(true);
|
|
||||||
self.btn_play_pause.set_label("");
|
|
||||||
}
|
|
||||||
PlayState::Paused => {
|
|
||||||
self.btn_play_pause.set_sensitive(true);
|
|
||||||
self.btn_play_pause.set_label("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let enable_prev = match status.current_song {
|
|
||||||
Some((pos, _)) => pos.0 > 0,
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let enable_next = match status.current_song {
|
|
||||||
Some((pos, _)) => pos.0 < status.playlist_length,
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.btn_prev.set_sensitive(enable_prev);
|
|
||||||
self.btn_next.set_sensitive(enable_next);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,11 @@
|
||||||
use crate::modules::{Module, ModuleInfo};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr, Section};
|
use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr, Section};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Label;
|
use gtk::Label;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
|
@ -23,57 +24,78 @@ const fn default_interval() -> u64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<Label> for ScriptModule {
|
impl Module<Label> for ScriptModule {
|
||||||
fn into_widget(self, _info: &ModuleInfo) -> Result<Label> {
|
type SendMessage = String;
|
||||||
let label = Label::builder().use_markup(true).build();
|
type ReceiveMessage = ();
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
_rx: Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let interval = self.interval;
|
||||||
|
let path = self.path.clone();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
match self.run_script() {
|
match run_script(&path) {
|
||||||
Ok(stdout) => tx.send(stdout).expect("Failed to send stdout"),
|
Ok(stdout) => tx
|
||||||
|
.send(ModuleUpdateEvent::Update(stdout))
|
||||||
|
.await
|
||||||
|
.expect("Failed to send stdout"),
|
||||||
Err(err) => error!("{:?}", err),
|
Err(err) => error!("{:?}", err),
|
||||||
}
|
}
|
||||||
|
|
||||||
sleep(tokio::time::Duration::from_millis(self.interval)).await;
|
sleep(tokio::time::Duration::from_millis(interval)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<Label>> {
|
||||||
|
let label = Label::builder().use_markup(true).build();
|
||||||
|
|
||||||
{
|
{
|
||||||
let label = label.clone();
|
let label = label.clone();
|
||||||
rx.attach(None, move |s| {
|
context.widget_rx.attach(None, move |s| {
|
||||||
label.set_label(s.as_str());
|
label.set_label(s.as_str());
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(label)
|
Ok(ModuleWidget {
|
||||||
|
widget: label,
|
||||||
|
popup: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptModule {
|
#[instrument]
|
||||||
#[instrument]
|
fn run_script(path: &str) -> Result<String> {
|
||||||
fn run_script(&self) -> Result<String> {
|
let output = Command::new("sh")
|
||||||
let output = Command::new("sh")
|
.arg("-c")
|
||||||
.arg("-c")
|
.arg(path)
|
||||||
.arg(&self.path)
|
.output()
|
||||||
.output()
|
.wrap_err("Failed to get script output")?;
|
||||||
.wrap_err("Failed to get script output")?;
|
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let stdout = String::from_utf8(output.stdout)
|
let stdout = String::from_utf8(output.stdout)
|
||||||
.map(|output| output.trim().to_string())
|
.map(|output| output.trim().to_string())
|
||||||
.wrap_err("Script stdout not valid UTF-8")?;
|
.wrap_err("Script stdout not valid UTF-8")?;
|
||||||
|
|
||||||
Ok(stdout)
|
Ok(stdout)
|
||||||
} else {
|
} else {
|
||||||
let stderr = String::from_utf8(output.stderr)
|
let stderr = String::from_utf8(output.stderr)
|
||||||
.map(|output| output.trim().to_string())
|
.map(|output| output.trim().to_string())
|
||||||
.wrap_err("Script stderr not valid UTF-8")?;
|
.wrap_err("Script stderr not valid UTF-8")?;
|
||||||
|
|
||||||
Err(Report::msg(stderr)
|
Err(Report::msg(stderr)
|
||||||
.wrap_err("Script returned non-zero error code")
|
.wrap_err("Script returned non-zero error code")
|
||||||
.suggestion("Check the path to your script")
|
.suggestion("Check the path to your script")
|
||||||
.suggestion("Check the script for errors"))
|
.suggestion("Check the script for errors"))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::modules::{Module, ModuleInfo};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Label, Orientation};
|
use gtk::{Label, Orientation};
|
||||||
|
@ -7,6 +7,7 @@ use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use sysinfo::{CpuExt, System, SystemExt};
|
use sysinfo::{CpuExt, System, SystemExt};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
@ -16,20 +17,15 @@ pub struct SysInfoModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for SysInfoModule {
|
impl Module<gtk::Box> for SysInfoModule {
|
||||||
fn into_widget(self, _info: &ModuleInfo) -> Result<gtk::Box> {
|
type SendMessage = HashMap<String, String>;
|
||||||
let re = Regex::new(r"\{([\w-]+)}")?;
|
type ReceiveMessage = ();
|
||||||
|
|
||||||
let container = gtk::Box::new(Orientation::Horizontal, 10);
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
let mut labels = Vec::new();
|
_info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
for format in &self.format {
|
_rx: Receiver<Self::ReceiveMessage>,
|
||||||
let label = Label::builder().label(format).name("item").build();
|
) -> Result<()> {
|
||||||
container.add(&label);
|
|
||||||
labels.push(label);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut sys = System::new_all();
|
let mut sys = System::new_all();
|
||||||
|
|
||||||
|
@ -45,19 +41,46 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||||
|
|
||||||
// TODO: Add remaining format info
|
// TODO: Add remaining format info
|
||||||
|
|
||||||
format_info.insert("memory-percent", format!("{:0>2.0}", memory_percent));
|
format_info.insert(
|
||||||
format_info.insert("cpu-percent", format!("{:0>2.0}", cpu_percent));
|
String::from("memory-percent"),
|
||||||
|
format!("{:0>2.0}", memory_percent),
|
||||||
|
);
|
||||||
|
format_info.insert(
|
||||||
|
String::from("cpu-percent"),
|
||||||
|
format!("{:0>2.0}", cpu_percent),
|
||||||
|
);
|
||||||
|
|
||||||
tx.send(format_info)
|
tx.send(ModuleUpdateEvent::Update(format_info))
|
||||||
|
.await
|
||||||
.expect("Failed to send system info map");
|
.expect("Failed to send system info map");
|
||||||
|
|
||||||
sleep(tokio::time::Duration::from_secs(1)).await;
|
sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<gtk::Box>> {
|
||||||
|
let re = Regex::new(r"\{([\w-]+)}")?;
|
||||||
|
|
||||||
|
let container = gtk::Box::new(Orientation::Horizontal, 10);
|
||||||
|
|
||||||
|
let mut labels = Vec::new();
|
||||||
|
|
||||||
|
for format in &self.format {
|
||||||
|
let label = Label::builder().label(format).name("item").build();
|
||||||
|
container.add(&label);
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let formats = self.format;
|
let formats = self.format;
|
||||||
rx.attach(None, move |info| {
|
context.widget_rx.attach(None, move |info| {
|
||||||
for (format, label) in formats.iter().zip(labels.clone()) {
|
for (format, label) in formats.iter().zip(labels.clone()) {
|
||||||
let format_compiled = re.replace(format, |caps: &Captures| {
|
let format_compiled = re.replace(format, |caps: &Captures| {
|
||||||
info.get(&caps[1])
|
info.get(&caps[1])
|
||||||
|
@ -72,6 +95,9 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(container)
|
Ok(ModuleWidget {
|
||||||
|
widget: container,
|
||||||
|
popup: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
74
src/modules/tray/client.rs
Normal file
74
src/modules/tray/client.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use async_once::AsyncOnce;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||||
|
use stray::StatusNotifierWatcher;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::{broadcast, mpsc};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
pub struct TrayEventReceiver {
|
||||||
|
tx: mpsc::Sender<NotifierItemCommand>,
|
||||||
|
b_tx: broadcast::Sender<NotifierItemMessage>,
|
||||||
|
_b_rx: broadcast::Receiver<NotifierItemMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrayEventReceiver {
|
||||||
|
async fn new() -> stray::error::Result<Self> {
|
||||||
|
let (tx, rx) = mpsc::channel(16);
|
||||||
|
let (b_tx, b_rx) = broadcast::channel(16);
|
||||||
|
|
||||||
|
let tray = StatusNotifierWatcher::new(rx).await?;
|
||||||
|
let mut host = tray.create_notifier_host("ironbar").await?;
|
||||||
|
|
||||||
|
let b_tx2 = b_tx.clone();
|
||||||
|
spawn(async move {
|
||||||
|
while let Ok(message) = host.recv().await {
|
||||||
|
b_tx2.send(message)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tx,
|
||||||
|
b_tx,
|
||||||
|
_b_rx: b_rx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
mpsc::Sender<NotifierItemCommand>,
|
||||||
|
broadcast::Receiver<NotifierItemMessage>,
|
||||||
|
) {
|
||||||
|
(self.tx.clone(), self.b_tx.subscribe())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CLIENT: AsyncOnce<TrayEventReceiver> = AsyncOnce::new(async {
|
||||||
|
const MAX_RETRIES: i32 = 10;
|
||||||
|
|
||||||
|
// sometimes this can fail
|
||||||
|
let mut retries = 0;
|
||||||
|
|
||||||
|
let value = loop {
|
||||||
|
retries += 1;
|
||||||
|
|
||||||
|
let tray = TrayEventReceiver::new().await;
|
||||||
|
|
||||||
|
if tray.is_ok() || retries == MAX_RETRIES {
|
||||||
|
break tray;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Failed to create StatusNotifierWatcher (attempt {retries})");
|
||||||
|
};
|
||||||
|
|
||||||
|
value.expect("Failed to create StatusNotifierWatcher")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_tray_event_client() -> &'static TrayEventReceiver {
|
||||||
|
CLIENT.get().await
|
||||||
|
}
|
|
@ -1,26 +1,23 @@
|
||||||
use crate::modules::{Module, ModuleInfo};
|
mod client;
|
||||||
|
|
||||||
|
use crate::await_sync;
|
||||||
|
use crate::modules::tray::client::get_tray_event_client;
|
||||||
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use futures_util::StreamExt;
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{IconLookupFlags, IconTheme, Image, Menu, MenuBar, MenuItem, SeparatorMenuItem};
|
use gtk::{IconLookupFlags, IconTheme, Image, Menu, MenuBar, MenuItem, SeparatorMenuItem};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use stray::message::menu::{MenuItem as MenuItemInfo, MenuType, TrayMenu};
|
use stray::message::menu::{MenuItem as MenuItemInfo, MenuType};
|
||||||
use stray::message::tray::StatusNotifierItem;
|
use stray::message::tray::StatusNotifierItem;
|
||||||
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||||
use stray::SystemTray;
|
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct TrayModule;
|
pub struct TrayModule;
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum TrayUpdate {
|
|
||||||
Update(String, Box<StatusNotifierItem>, Option<TrayMenu>),
|
|
||||||
Remove(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a GTK `Image` component
|
/// Gets a GTK `Image` component
|
||||||
/// for the status notifier item's icon.
|
/// for the status notifier item's icon.
|
||||||
fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
|
fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
|
||||||
|
@ -39,7 +36,7 @@ fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
|
||||||
/// for the provided submenu array.
|
/// for the provided submenu array.
|
||||||
fn get_menu_items(
|
fn get_menu_items(
|
||||||
menu: &[MenuItemInfo],
|
menu: &[MenuItemInfo],
|
||||||
tx: &mpsc::Sender<NotifierItemCommand>,
|
tx: &Sender<NotifierItemCommand>,
|
||||||
id: &str,
|
id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Vec<MenuItem> {
|
) -> Vec<MenuItem> {
|
||||||
|
@ -90,72 +87,88 @@ fn get_menu_items(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<MenuBar> for TrayModule {
|
impl Module<MenuBar> for TrayModule {
|
||||||
fn into_widget(self, _info: &ModuleInfo) -> Result<MenuBar> {
|
type SendMessage = NotifierItemMessage;
|
||||||
let container = MenuBar::new();
|
type ReceiveMessage = NotifierItemCommand;
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
fn spawn_controller(
|
||||||
let (ui_tx, ui_rx) = mpsc::channel(32);
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let client = await_sync(async { get_tray_event_client().await });
|
||||||
|
let (tray_tx, mut tray_rx) = client.subscribe();
|
||||||
|
|
||||||
|
// listen to tray updates
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
// FIXME: Can only spawn one of these at a time - means cannot have tray on multiple bars
|
while let Ok(message) = tray_rx.recv().await {
|
||||||
let mut tray = SystemTray::new(ui_rx).await;
|
tx.send(ModuleUpdateEvent::Update(message)).await?;
|
||||||
|
|
||||||
// listen for tray updates & send message to update UI
|
|
||||||
while let Some(message) = tray.next().await {
|
|
||||||
match message {
|
|
||||||
NotifierItemMessage::Update {
|
|
||||||
address: id,
|
|
||||||
item,
|
|
||||||
menu,
|
|
||||||
} => {
|
|
||||||
tx.send(TrayUpdate::Update(id, Box::new(item), menu))
|
|
||||||
.expect("Failed to send tray update event");
|
|
||||||
}
|
|
||||||
NotifierItemMessage::Remove { address: id } => {
|
|
||||||
tx.send(TrayUpdate::Remove(id))
|
|
||||||
.expect("Failed to send tray remove event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// send tray commands
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(cmd) = rx.recv().await {
|
||||||
|
tray_tx.send(cmd).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), mpsc::error::SendError<NotifierItemCommand>>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<MenuBar>> {
|
||||||
|
let container = MenuBar::new();
|
||||||
|
|
||||||
{
|
{
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
let mut widgets = HashMap::new();
|
let mut widgets = HashMap::new();
|
||||||
|
|
||||||
// listen for UI updates
|
// listen for UI updates
|
||||||
rx.attach(None, move |update| {
|
context.widget_rx.attach(None, move |update| {
|
||||||
match update {
|
match update {
|
||||||
TrayUpdate::Update(id, item, menu) => {
|
NotifierItemMessage::Update {
|
||||||
let menu_item = widgets.remove(id.as_str()).unwrap_or_else(|| {
|
item,
|
||||||
|
address,
|
||||||
|
menu,
|
||||||
|
} => {
|
||||||
|
let menu_item = widgets.remove(address.as_str()).unwrap_or_else(|| {
|
||||||
let menu_item = MenuItem::new();
|
let menu_item = MenuItem::new();
|
||||||
menu_item.style_context().add_class("item");
|
menu_item.style_context().add_class("item");
|
||||||
if let Some(image) = get_icon(&item) {
|
if let Some(image) = get_icon(&item) {
|
||||||
image.set_widget_name(id.as_str());
|
image.set_widget_name(address.as_str());
|
||||||
menu_item.add(&image);
|
menu_item.add(&image);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.add(&menu_item);
|
container.add(&menu_item);
|
||||||
menu_item.show_all();
|
menu_item.show_all();
|
||||||
|
|
||||||
menu_item
|
menu_item
|
||||||
});
|
});
|
||||||
|
|
||||||
if let (Some(menu_opts), Some(menu_path)) = (menu, item.menu) {
|
if let (Some(menu_opts), Some(menu_path)) = (menu, item.menu) {
|
||||||
let submenus = menu_opts.submenus;
|
let submenus = menu_opts.submenus;
|
||||||
if !submenus.is_empty() {
|
if !submenus.is_empty() {
|
||||||
let menu = Menu::new();
|
let menu = Menu::new();
|
||||||
get_menu_items(&submenus, &ui_tx.clone(), &id, &menu_path)
|
get_menu_items(
|
||||||
.iter()
|
&submenus,
|
||||||
.for_each(|item| menu.add(item));
|
&context.controller_tx.clone(),
|
||||||
|
&address,
|
||||||
|
&menu_path,
|
||||||
|
)
|
||||||
|
.iter()
|
||||||
|
.for_each(|item| menu.add(item));
|
||||||
menu_item.set_submenu(Some(&menu));
|
menu_item.set_submenu(Some(&menu));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
widgets.insert(address, menu_item);
|
||||||
widgets.insert(id, menu_item);
|
|
||||||
}
|
}
|
||||||
TrayUpdate::Remove(id) => {
|
NotifierItemMessage::Remove { address } => {
|
||||||
if let Some(widget) = widgets.get(&id) {
|
if let Some(widget) = widgets.get(&address) {
|
||||||
container.remove(widget);
|
container.remove(widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,6 +178,9 @@ impl Module<MenuBar> for TrayModule {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(container)
|
Ok(ModuleWidget {
|
||||||
|
widget: container,
|
||||||
|
popup: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,62 +1,82 @@
|
||||||
use crate::modules::{Module, ModuleInfo};
|
use crate::await_sync;
|
||||||
use crate::sway::{get_client, Workspace};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
|
use crate::sway::{get_client, get_sub_client};
|
||||||
use color_eyre::{Report, Result};
|
use color_eyre::{Report, Result};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, Orientation};
|
use gtk::{Button, Orientation};
|
||||||
use ksway::IpcCommand;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use swayipc_async::{Workspace, WorkspaceChange, WorkspaceEvent};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::task::spawn_blocking;
|
use tracing::trace;
|
||||||
use tracing::{debug, trace};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct WorkspacesModule {
|
pub struct WorkspacesModule {
|
||||||
/// Map of actual workspace names to custom names.
|
/// Map of actual workspace names to custom names.
|
||||||
name_map: Option<HashMap<String, String>>,
|
name_map: Option<HashMap<String, String>>,
|
||||||
|
|
||||||
/// Whether to display icons for all monitors.
|
/// Whether to display buttons for all monitors.
|
||||||
#[serde(default = "crate::config::default_false")]
|
#[serde(default = "crate::config::default_false")]
|
||||||
all_monitors: bool,
|
all_monitors: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
#[derive(Clone, Debug)]
|
||||||
fn as_button(&self, name_map: &HashMap<String, String>, tx: &mpsc::Sender<String>) -> Button {
|
pub enum WorkspaceUpdate {
|
||||||
let button = Button::builder()
|
Init(Vec<Workspace>),
|
||||||
.label(name_map.get(self.name.as_str()).unwrap_or(&self.name))
|
Update(Box<WorkspaceEvent>),
|
||||||
.build();
|
}
|
||||||
|
|
||||||
let style_context = button.style_context();
|
/// Creates a button from a workspace
|
||||||
style_context.add_class("item");
|
fn create_button(
|
||||||
|
workspace: &Workspace,
|
||||||
|
name_map: &HashMap<String, String>,
|
||||||
|
tx: &Sender<String>,
|
||||||
|
) -> Button {
|
||||||
|
let button = Button::builder()
|
||||||
|
.label(
|
||||||
|
name_map
|
||||||
|
.get(workspace.name.as_str())
|
||||||
|
.unwrap_or(&workspace.name),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
if self.focused {
|
let style_context = button.style_context();
|
||||||
style_context.add_class("focused");
|
style_context.add_class("item");
|
||||||
}
|
|
||||||
|
|
||||||
{
|
if workspace.focused {
|
||||||
let tx = tx.clone();
|
style_context.add_class("focused");
|
||||||
let name = self.name.clone();
|
|
||||||
button.connect_clicked(move |_item| {
|
|
||||||
tx.try_send(name.clone())
|
|
||||||
.expect("Failed to send workspace click event");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
button
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
let name = workspace.name.clone();
|
||||||
|
button.connect_clicked(move |_item| {
|
||||||
|
tx.try_send(name.clone())
|
||||||
|
.expect("Failed to send workspace click event");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
button
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for WorkspacesModule {
|
impl Module<gtk::Box> for WorkspacesModule {
|
||||||
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
|
type SendMessage = WorkspaceUpdate;
|
||||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
type ReceiveMessage = String;
|
||||||
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
let workspaces = {
|
let workspaces = {
|
||||||
trace!("Getting current workspaces");
|
trace!("Getting current workspaces");
|
||||||
let sway = get_client();
|
let workspaces = await_sync(async {
|
||||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
let sway = get_client().await;
|
||||||
let raw = sway.ipc(IpcCommand::GetWorkspaces)?;
|
let mut sway = sway.lock().await;
|
||||||
let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw)?;
|
sway.get_workspaces().await
|
||||||
|
})?;
|
||||||
|
|
||||||
if self.all_monitors {
|
if self.all_monitors {
|
||||||
workspaces
|
workspaces
|
||||||
|
@ -69,43 +89,67 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tx.try_send(ModuleUpdateEvent::Update(WorkspaceUpdate::Init(workspaces)))
|
||||||
|
.expect("Failed to send initial workspace list");
|
||||||
|
|
||||||
|
// Subscribe & send events
|
||||||
|
spawn(async move {
|
||||||
|
let mut srx = {
|
||||||
|
let sway = get_sub_client();
|
||||||
|
sway.subscribe_workspace()
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!("Set up Sway workspace subscription");
|
||||||
|
|
||||||
|
while let Ok(payload) = srx.recv().await {
|
||||||
|
tx.send(ModuleUpdateEvent::Update(WorkspaceUpdate::Update(payload)))
|
||||||
|
.await
|
||||||
|
.expect("Failed to send workspace update");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change workspace focus
|
||||||
|
spawn(async move {
|
||||||
|
trace!("Setting up UI event handler");
|
||||||
|
let sway = get_client().await;
|
||||||
|
while let Some(name) = rx.recv().await {
|
||||||
|
let mut sway = sway.lock().await;
|
||||||
|
sway.run_command(format!("workspace {}", name)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), Report>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<gtk::Box>> {
|
||||||
|
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||||
|
|
||||||
let name_map = self.name_map.unwrap_or_default();
|
let name_map = self.name_map.unwrap_or_default();
|
||||||
|
|
||||||
let mut button_map: HashMap<String, Button> = HashMap::new();
|
let mut button_map: HashMap<String, Button> = HashMap::new();
|
||||||
|
|
||||||
let (ui_tx, mut ui_rx) = mpsc::channel(32);
|
|
||||||
|
|
||||||
trace!("Creating workspace buttons");
|
|
||||||
for workspace in workspaces {
|
|
||||||
let item = workspace.as_button(&name_map, &ui_tx);
|
|
||||||
container.add(&item);
|
|
||||||
button_map.insert(workspace.name, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
spawn_blocking(move || {
|
|
||||||
trace!("Starting workspace event listener task");
|
|
||||||
let srx = {
|
|
||||||
let sway = get_client();
|
|
||||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
|
||||||
|
|
||||||
sway.subscribe_workspace()
|
|
||||||
};
|
|
||||||
|
|
||||||
while let Ok(payload) = srx.recv() {
|
|
||||||
tx.send(payload).expect("Failed to send workspace event");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
{
|
||||||
trace!("Setting up sway event handler");
|
let container = container.clone();
|
||||||
let menubar = container.clone();
|
|
||||||
let output_name = info.output_name.to_string();
|
let output_name = info.output_name.to_string();
|
||||||
rx.attach(None, move |event| {
|
|
||||||
debug!("Received workspace event {:?}", event);
|
context.widget_rx.attach(None, move |event| {
|
||||||
match event.change.as_str() {
|
match event {
|
||||||
"focus" => {
|
WorkspaceUpdate::Init(workspaces) => {
|
||||||
|
trace!("Creating workspace buttons");
|
||||||
|
for workspace in workspaces {
|
||||||
|
let item = create_button(&workspace, &name_map, &context.controller_tx);
|
||||||
|
container.add(&item);
|
||||||
|
button_map.insert(workspace.name, item);
|
||||||
|
}
|
||||||
|
container.show_all();
|
||||||
|
}
|
||||||
|
WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Focus => {
|
||||||
let old = event.old.and_then(|old| button_map.get(&old.name));
|
let old = event.old.and_then(|old| button_map.get(&old.name));
|
||||||
if let Some(old) = old {
|
if let Some(old) = old {
|
||||||
old.style_context().remove_class("focused");
|
old.style_context().remove_class("focused");
|
||||||
|
@ -115,62 +159,55 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
if let Some(new) = new {
|
if let Some(new) = new {
|
||||||
new.style_context().add_class("focused");
|
new.style_context().add_class("focused");
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("{:?} {:?}", old, new);
|
|
||||||
}
|
}
|
||||||
"init" => {
|
WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Init => {
|
||||||
if let Some(workspace) = event.current {
|
if let Some(workspace) = event.current {
|
||||||
if self.all_monitors || workspace.output == output_name {
|
if self.all_monitors || workspace.output == output_name {
|
||||||
let item = workspace.as_button(&name_map, &ui_tx);
|
let item =
|
||||||
|
create_button(&workspace, &name_map, &context.controller_tx);
|
||||||
|
|
||||||
item.show();
|
item.show();
|
||||||
menubar.add(&item);
|
container.add(&item);
|
||||||
button_map.insert(workspace.name, item);
|
button_map.insert(workspace.name, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"move" => {
|
WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Move => {
|
||||||
if let Some(workspace) = event.current {
|
if let Some(workspace) = event.current {
|
||||||
if !self.all_monitors {
|
if !self.all_monitors {
|
||||||
if workspace.output == output_name {
|
if workspace.output == output_name {
|
||||||
let item = workspace.as_button(&name_map, &ui_tx);
|
let item = create_button(
|
||||||
|
&workspace,
|
||||||
|
&name_map,
|
||||||
|
&context.controller_tx,
|
||||||
|
);
|
||||||
|
|
||||||
item.show();
|
item.show();
|
||||||
menubar.add(&item);
|
container.add(&item);
|
||||||
button_map.insert(workspace.name, item);
|
button_map.insert(workspace.name, item);
|
||||||
} else if let Some(item) = button_map.get(&workspace.name) {
|
} else if let Some(item) = button_map.get(&workspace.name) {
|
||||||
menubar.remove(item);
|
container.remove(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"empty" => {
|
WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Empty => {
|
||||||
if let Some(workspace) = event.current {
|
if let Some(workspace) = event.current {
|
||||||
if let Some(item) = button_map.get(&workspace.name) {
|
if let Some(item) = button_map.get(&workspace.name) {
|
||||||
menubar.remove(item);
|
container.remove(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
WorkspaceUpdate::Update(_) => {}
|
||||||
}
|
};
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn(async move {
|
Ok(ModuleWidget {
|
||||||
trace!("Setting up UI event handler");
|
widget: container,
|
||||||
let sway = get_client();
|
popup: None,
|
||||||
while let Some(name) = ui_rx.recv().await {
|
})
|
||||||
let mut sway = sway
|
|
||||||
.lock()
|
|
||||||
.expect("Failed to get write lock on Sway IPC client");
|
|
||||||
sway.run(format!("workspace {}", name))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), Report>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(container)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
109
src/popup.rs
109
src/popup.rs
|
@ -1,12 +1,16 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::config::BarPosition;
|
use crate::config::BarPosition;
|
||||||
|
use crate::modules::ModuleInfo;
|
||||||
use gtk::gdk::Monitor;
|
use gtk::gdk::Monitor;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Application, ApplicationWindow, Button, Orientation};
|
use gtk::{ApplicationWindow, Button};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Popup {
|
pub struct Popup {
|
||||||
pub window: ApplicationWindow,
|
pub window: ApplicationWindow,
|
||||||
pub container: gtk::Box,
|
pub cache: HashMap<usize, gtk::Box>,
|
||||||
monitor: Monitor,
|
monitor: Monitor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,14 +18,11 @@ impl Popup {
|
||||||
/// Creates a new popup window.
|
/// Creates a new popup window.
|
||||||
/// This includes setting up gtk-layer-shell
|
/// This includes setting up gtk-layer-shell
|
||||||
/// and an empty `gtk::Box` container.
|
/// and an empty `gtk::Box` container.
|
||||||
pub fn new(
|
pub fn new(module_info: &ModuleInfo) -> Self {
|
||||||
name: &str,
|
let pos = module_info.bar_position;
|
||||||
app: &Application,
|
let win = ApplicationWindow::builder()
|
||||||
monitor: &Monitor,
|
.application(module_info.app)
|
||||||
orientation: Orientation,
|
.build();
|
||||||
bar_position: &BarPosition,
|
|
||||||
) -> Self {
|
|
||||||
let win = ApplicationWindow::builder().application(app).build();
|
|
||||||
|
|
||||||
gtk_layer_shell::init_for_window(&win);
|
gtk_layer_shell::init_for_window(&win);
|
||||||
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
|
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
|
||||||
|
@ -29,48 +30,25 @@ impl Popup {
|
||||||
gtk_layer_shell::set_margin(
|
gtk_layer_shell::set_margin(
|
||||||
&win,
|
&win,
|
||||||
gtk_layer_shell::Edge::Top,
|
gtk_layer_shell::Edge::Top,
|
||||||
if bar_position == &BarPosition::Top {
|
if pos == &BarPosition::Top { 5 } else { 0 },
|
||||||
5
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
gtk_layer_shell::set_margin(
|
gtk_layer_shell::set_margin(
|
||||||
&win,
|
&win,
|
||||||
gtk_layer_shell::Edge::Bottom,
|
gtk_layer_shell::Edge::Bottom,
|
||||||
if bar_position == &BarPosition::Bottom {
|
if pos == &BarPosition::Bottom { 5 } else { 0 },
|
||||||
5
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Left, 0);
|
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Left, 0);
|
||||||
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Right, 0);
|
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Right, 0);
|
||||||
|
|
||||||
gtk_layer_shell::set_anchor(
|
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Top, pos == &BarPosition::Top);
|
||||||
&win,
|
|
||||||
gtk_layer_shell::Edge::Top,
|
|
||||||
bar_position == &BarPosition::Top,
|
|
||||||
);
|
|
||||||
gtk_layer_shell::set_anchor(
|
gtk_layer_shell::set_anchor(
|
||||||
&win,
|
&win,
|
||||||
gtk_layer_shell::Edge::Bottom,
|
gtk_layer_shell::Edge::Bottom,
|
||||||
bar_position == &BarPosition::Bottom,
|
pos == &BarPosition::Bottom,
|
||||||
);
|
);
|
||||||
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Left, true);
|
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Left, true);
|
||||||
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Right, false);
|
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Right, false);
|
||||||
|
|
||||||
let content = gtk::Box::builder()
|
|
||||||
.orientation(orientation)
|
|
||||||
.spacing(0)
|
|
||||||
.hexpand(false)
|
|
||||||
.name(name)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
content.style_context().add_class("popup");
|
|
||||||
|
|
||||||
win.add(&content);
|
|
||||||
|
|
||||||
win.connect_leave_notify_event(|win, ev| {
|
win.connect_leave_notify_event(|win, ev| {
|
||||||
const THRESHOLD: f64 = 3.0;
|
const THRESHOLD: f64 = 3.0;
|
||||||
|
|
||||||
|
@ -88,15 +66,36 @@ impl Popup {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
window: win,
|
window: win,
|
||||||
container: content,
|
cache: HashMap::new(),
|
||||||
monitor: monitor.clone(),
|
monitor: module_info.monitor.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_content(&mut self, key: usize, content: gtk::Box) {
|
||||||
|
debug!("Registered popup content for #{}", key);
|
||||||
|
self.cache.insert(key, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_content(&self, key: usize) {
|
||||||
|
self.clear_window();
|
||||||
|
|
||||||
|
if let Some(content) = self.cache.get(&key) {
|
||||||
|
content.style_context().add_class("popup");
|
||||||
|
self.window.add(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_window(&self) {
|
||||||
|
let children = self.window.children();
|
||||||
|
for child in children {
|
||||||
|
self.window.remove(&child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the popover
|
/// Shows the popover
|
||||||
pub fn show(&self, button: &Button) {
|
pub fn show(&self, button_x: i32, button_width: i32) {
|
||||||
self.window.show_all();
|
self.window.show_all();
|
||||||
self.set_pos(button);
|
self.set_pos(button_x, button_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hides the popover
|
/// Hides the popover
|
||||||
|
@ -104,18 +103,17 @@ impl Popup {
|
||||||
self.window.hide();
|
self.window.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the popup is currently visible
|
||||||
|
pub fn is_visible(&self) -> bool {
|
||||||
|
self.window.is_visible()
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the popover's X position relative to the left border of the screen
|
/// Sets the popover's X position relative to the left border of the screen
|
||||||
fn set_pos(&self, button: &Button) {
|
fn set_pos(&self, button_x: i32, button_width: i32) {
|
||||||
let widget_width = button.allocation().width();
|
|
||||||
let screen_width = self.monitor.workarea().width();
|
let screen_width = self.monitor.workarea().width();
|
||||||
let popup_width = self.window.allocated_width();
|
let popup_width = self.window.allocated_width();
|
||||||
|
|
||||||
let top_level = button.toplevel().expect("Failed to get top-level widget");
|
let widget_center = f64::from(button_x) + f64::from(button_width) / 2.0;
|
||||||
let (widget_x, _) = button
|
|
||||||
.translate_coordinates(&top_level, 0, 0)
|
|
||||||
.unwrap_or((0, 0));
|
|
||||||
|
|
||||||
let widget_center = f64::from(widget_x) + f64::from(widget_width) / 2.0;
|
|
||||||
|
|
||||||
let mut offset = (widget_center - (f64::from(popup_width) / 2.0)).round();
|
let mut offset = (widget_center - (f64::from(popup_width) / 2.0)).round();
|
||||||
|
|
||||||
|
@ -127,4 +125,17 @@ impl Popup {
|
||||||
|
|
||||||
gtk_layer_shell::set_margin(&self.window, gtk_layer_shell::Edge::Left, offset as i32);
|
gtk_layer_shell::set_margin(&self.window, gtk_layer_shell::Edge::Left, offset as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the absolute X position of the button
|
||||||
|
/// and its width.
|
||||||
|
pub fn button_pos(button: &Button) -> (i32, i32) {
|
||||||
|
let button_width = button.allocation().width();
|
||||||
|
|
||||||
|
let top_level = button.toplevel().expect("Failed to get top-level widget");
|
||||||
|
let (button_x, _) = button
|
||||||
|
.translate_coordinates(&top_level, 0, 0)
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
|
||||||
|
(button_x, button_width)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
256
src/sway/mod.rs
256
src/sway/mod.rs
|
@ -1,224 +1,94 @@
|
||||||
use color_eyre::{Report, Result};
|
use async_once::AsyncOnce;
|
||||||
use crossbeam_channel::Receiver;
|
use color_eyre::Report;
|
||||||
use ksway::{Error, IpcCommand, IpcEvent};
|
use futures_util::StreamExt;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::Deserialize;
|
use std::sync::Arc;
|
||||||
use std::sync::{Arc, Mutex};
|
use swayipc_async::{Connection, Event, EventType, WindowEvent, WorkspaceEvent};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tracing::{debug, info, trace};
|
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::{info, trace};
|
||||||
|
|
||||||
pub mod node;
|
pub mod node;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
pub struct SwayEventClient {
|
||||||
pub struct WorkspaceEvent {
|
workspace_tx: Sender<Box<WorkspaceEvent>>,
|
||||||
pub change: String,
|
_workspace_rx: Receiver<Box<WorkspaceEvent>>,
|
||||||
pub old: Option<Workspace>,
|
window_tx: Sender<Box<WindowEvent>>,
|
||||||
pub current: Option<Workspace>,
|
_window_rx: Receiver<Box<WindowEvent>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
impl SwayEventClient {
|
||||||
pub struct Workspace {
|
fn new() -> Self {
|
||||||
pub name: String,
|
let (workspace_tx, workspace_rx) = channel(16);
|
||||||
pub focused: bool,
|
let (window_tx, window_rx) = channel(16);
|
||||||
// pub num: i32,
|
|
||||||
pub output: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
let workspace_tx2 = workspace_tx.clone();
|
||||||
pub struct WindowEvent {
|
let window_tx2 = window_tx.clone();
|
||||||
pub change: String,
|
|
||||||
pub container: SwayNode,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
pub struct SwayNode {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub node_type: String,
|
|
||||||
pub id: i32,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub app_id: Option<String>,
|
|
||||||
pub focused: bool,
|
|
||||||
pub urgent: bool,
|
|
||||||
pub nodes: Vec<SwayNode>,
|
|
||||||
pub floating_nodes: Vec<SwayNode>,
|
|
||||||
pub shell: Option<String>,
|
|
||||||
pub window_properties: Option<WindowProperties>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
pub struct WindowProperties {
|
|
||||||
pub class: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct SwayOutput {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Broadcaster<T> = Arc<Mutex<UnboundedBroadcast<T>>>;
|
|
||||||
|
|
||||||
pub struct SwayClient {
|
|
||||||
client: ksway::Client,
|
|
||||||
|
|
||||||
workspace_bc: Broadcaster<WorkspaceEvent>,
|
|
||||||
window_bc: Broadcaster<WindowEvent>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SwayClient {
|
|
||||||
fn connect() -> Result<Self> {
|
|
||||||
let client = match ksway::Client::connect() {
|
|
||||||
Ok(client) => Ok(client),
|
|
||||||
Err(err) => Err(get_client_error(err)),
|
|
||||||
}?;
|
|
||||||
info!("Sway IPC client connected");
|
|
||||||
|
|
||||||
let workspace_bc = Arc::new(Mutex::new(UnboundedBroadcast::new()));
|
|
||||||
let window_bc = Arc::new(Mutex::new(UnboundedBroadcast::new()));
|
|
||||||
|
|
||||||
let workspace_bc2 = workspace_bc.clone();
|
|
||||||
let window_bc2 = window_bc.clone();
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut sub_client = match ksway::Client::connect() {
|
let workspace_tx = workspace_tx2;
|
||||||
Ok(client) => Ok(client),
|
let window_tx = window_tx2;
|
||||||
Err(err) => Err(get_client_error(err)),
|
|
||||||
}
|
let client = Connection::new().await?;
|
||||||
.expect("Failed to connect to Sway IPC server");
|
|
||||||
info!("Sway IPC subscription client connected");
|
info!("Sway IPC subscription client connected");
|
||||||
|
|
||||||
let event_types = vec![IpcEvent::Window, IpcEvent::Workspace];
|
let event_types = [EventType::Window, EventType::Workspace];
|
||||||
let rx = match sub_client.subscribe(event_types) {
|
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(err) => Err(get_client_error(err)),
|
|
||||||
}
|
|
||||||
.expect("Failed to subscribe to Sway IPC server");
|
|
||||||
|
|
||||||
loop {
|
let mut events = client.subscribe(event_types).await?;
|
||||||
while let Ok((ev_type, payload)) = rx.try_recv() {
|
|
||||||
debug!("Received sway event {:?}", ev_type);
|
while let Some(event) = events.next().await {
|
||||||
match ev_type {
|
trace!("event: {:?}", event);
|
||||||
IpcEvent::Workspace => {
|
match event? {
|
||||||
let json = serde_json::from_slice::<WorkspaceEvent>(&payload).expect(
|
Event::Workspace(ev) => {
|
||||||
"Received invalid workspace event payload from Sway IPC server",
|
workspace_tx.send(ev)?;
|
||||||
);
|
|
||||||
workspace_bc
|
|
||||||
.lock()
|
|
||||||
.expect("Failed to get lock on workspace event bus")
|
|
||||||
.send(json)
|
|
||||||
.expect("Failed to broadcast workspace event");
|
|
||||||
}
|
|
||||||
IpcEvent::Window => {
|
|
||||||
let json = serde_json::from_slice::<WindowEvent>(&payload).expect(
|
|
||||||
"Received invalid window event payload from Sway IPC server",
|
|
||||||
);
|
|
||||||
window_bc
|
|
||||||
.lock()
|
|
||||||
.expect("Failed to get lock on window event bus")
|
|
||||||
.send(json)
|
|
||||||
.expect("Failed to broadcast window event");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
Event::Window(ev) => {
|
||||||
match sub_client.poll() {
|
window_tx.send(ev)?;
|
||||||
Ok(()) => Ok(()),
|
}
|
||||||
Err(err) => Err(get_client_error(err)),
|
_ => {}
|
||||||
}
|
};
|
||||||
.expect("Failed to poll Sway IPC client");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok::<(), Report>(())
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Self {
|
Self {
|
||||||
client,
|
workspace_tx,
|
||||||
workspace_bc: workspace_bc2,
|
_workspace_rx: workspace_rx,
|
||||||
window_bc: window_bc2,
|
window_tx,
|
||||||
})
|
_window_rx: window_rx,
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ipc(&mut self, command: IpcCommand) -> Result<Vec<u8>> {
|
|
||||||
debug!("Sending command: {:?}", command);
|
|
||||||
match self.client.ipc(command) {
|
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(err) => Err(get_client_error(err)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(&mut self, cmd: String) -> Result<Vec<u8>> {
|
/// Gets an event receiver for workspace events
|
||||||
debug!("Sending command: {}", cmd);
|
pub fn subscribe_workspace(&self) -> Receiver<Box<WorkspaceEvent>> {
|
||||||
match self.client.run(cmd) {
|
self.workspace_tx.subscribe()
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(err) => Err(get_client_error(err)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe_workspace(&mut self) -> Receiver<WorkspaceEvent> {
|
/// Gets an event receiver for window events
|
||||||
trace!("Adding new workspace subscriber");
|
pub fn subscribe_window(&self) -> Receiver<Box<WindowEvent>> {
|
||||||
self.workspace_bc
|
self.window_tx.subscribe()
|
||||||
.lock()
|
|
||||||
.expect("Failed to get lock on workspace event bus")
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe_window(&mut self) -> Receiver<WindowEvent> {
|
|
||||||
trace!("Adding new window subscriber");
|
|
||||||
self.window_bc
|
|
||||||
.lock()
|
|
||||||
.expect("Failed to get lock on window event bus")
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets an error report from a `ksway` error enum variant
|
|
||||||
pub fn get_client_error(error: Error) -> Report {
|
|
||||||
match error {
|
|
||||||
Error::SockPathNotFound => Report::msg("Sway socket path not found"),
|
|
||||||
Error::SubscriptionError => Report::msg("Sway IPC subscription error"),
|
|
||||||
Error::AlreadySubscribed => Report::msg("Already subscribed to Sway IPC server"),
|
|
||||||
Error::Io(err) => Report::new(err),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CLIENT: Arc<Mutex<SwayClient>> = {
|
static ref CLIENT: AsyncOnce<Arc<Mutex<Connection>>> = AsyncOnce::new(async {
|
||||||
let client = SwayClient::connect();
|
let client = Connection::new()
|
||||||
match client {
|
.await
|
||||||
Ok(client) => Arc::new(Mutex::new(client)),
|
.expect("Failed to connect to Sway socket");
|
||||||
Err(err) => panic!("{:?}", err),
|
Arc::new(Mutex::new(client))
|
||||||
}
|
});
|
||||||
};
|
static ref SUB_CLIENT: SwayEventClient = SwayEventClient::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_client() -> Arc<Mutex<SwayClient>> {
|
/// Gets the sway IPC client
|
||||||
Arc::clone(&CLIENT)
|
pub async fn get_client() -> Arc<Mutex<Connection>> {
|
||||||
|
let client = CLIENT.get().await;
|
||||||
|
Arc::clone(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crossbeam channel wrapper
|
/// Gets the sway IPC event subscription client
|
||||||
/// which sends messages to all receivers.
|
pub fn get_sub_client() -> &'static SwayEventClient {
|
||||||
pub struct UnboundedBroadcast<T> {
|
&SUB_CLIENT
|
||||||
channels: Vec<crossbeam_channel::Sender<T>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: 'static + Clone + Send + Sync> UnboundedBroadcast<T> {
|
|
||||||
/// Creates a new broadcaster.
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self { channels: vec![] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new sender/receiver pair.
|
|
||||||
/// The sender is stored locally and the receiver is returned.
|
|
||||||
pub fn subscribe(&mut self) -> Receiver<T> {
|
|
||||||
let (tx, rx) = crossbeam_channel::unbounded();
|
|
||||||
|
|
||||||
self.channels.push(tx);
|
|
||||||
|
|
||||||
rx
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to send a messsge to all receivers.
|
|
||||||
pub fn send(&self, message: T) -> Result<(), crossbeam_channel::SendError<T>> {
|
|
||||||
for c in &self.channels {
|
|
||||||
c.send(message.clone())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,32 @@
|
||||||
use crate::sway::{SwayClient, SwayNode};
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use ksway::IpcCommand;
|
use swayipc_async::{Connection, Node, NodeType, ShellType};
|
||||||
|
|
||||||
impl SwayNode {
|
pub fn get_node_id(node: &Node) -> &str {
|
||||||
/// Gets either the `app_id` or `class`
|
node.app_id.as_ref().map_or_else(
|
||||||
/// depending on whether this is a native Wayland
|
|| {
|
||||||
/// or xwayland application.
|
node.window_properties
|
||||||
pub fn get_id(&self) -> &str {
|
.as_ref()
|
||||||
self.app_id.as_ref().map_or_else(
|
.expect("Cannot find node window properties")
|
||||||
|| {
|
.class
|
||||||
self.window_properties
|
.as_ref()
|
||||||
.as_ref()
|
.expect("Cannot find node name")
|
||||||
.expect("Cannot find node window properties")
|
},
|
||||||
.class
|
|app_id| app_id,
|
||||||
.as_ref()
|
)
|
||||||
.expect("Cannot find node name")
|
}
|
||||||
},
|
|
||||||
|app_id| app_id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether this application
|
/// Checks whether this application
|
||||||
/// is running under xwayland.
|
/// is running under xwayland.
|
||||||
pub fn is_xwayland(&self) -> bool {
|
pub fn is_node_xwayland(node: &Node) -> bool {
|
||||||
self.shell == Some(String::from("xwayland"))
|
node.shell == Some(ShellType::Xwayland)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively checks the provided node for any child application nodes.
|
/// Recursively checks the provided node for any child application nodes.
|
||||||
/// Returns a list of any found application nodes.
|
/// Returns a list of any found application nodes.
|
||||||
fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
|
fn check_node(node: Node, window_nodes: &mut Vec<Node>) {
|
||||||
if node.name.is_some() && (node.node_type == "con" || node.node_type == "floating_con") {
|
if node.name.is_some()
|
||||||
|
&& (node.node_type == NodeType::Con || node.node_type == NodeType::FloatingCon)
|
||||||
|
{
|
||||||
window_nodes.push(node);
|
window_nodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
node.nodes.into_iter().for_each(|node| {
|
node.nodes.into_iter().for_each(|node| {
|
||||||
|
@ -43,15 +39,12 @@ fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SwayClient {
|
/// Gets a flat vector of all currently open windows.
|
||||||
/// Gets a flat vector of all currently open windows.
|
pub async fn get_open_windows(client: &mut Connection) -> Result<Vec<Node>> {
|
||||||
pub fn get_open_windows(&mut self) -> Result<Vec<SwayNode>> {
|
let root_node = client.get_tree().await?;
|
||||||
let root_node = self.ipc(IpcCommand::GetTree)?;
|
|
||||||
let root_node = serde_json::from_slice(&root_node)?;
|
|
||||||
|
|
||||||
let mut window_nodes = vec![];
|
let mut window_nodes = vec![];
|
||||||
check_node(root_node, &mut window_nodes);
|
check_node(root_node, &mut window_nodes);
|
||||||
|
|
||||||
Ok(window_nodes)
|
Ok(window_nodes)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue