summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-29 09:10:42 +0200
committercashmere <cashmere@autistici.org>2026-07-29 09:10:42 +0200
commit5b77a6d8f19500eee21a497ce84db538ded05c00 (patch)
tree13e72121a7835fd0fed34f709b6e277e624a56dc
parente2f5e1267cc30b73202c85aacfaee0cd6b18193c (diff)
downloadi3bar-mango-5b77a6d8f19500eee21a497ce84db538ded05c00.tar.gz
add collapsible SNI system tray, rename project to i3bar-mango
Tray: the bar acts as org.kde.StatusNotifierWatcher plus host (zbus), rendering items on the right edge with icon/name; [tray] config toggles expanded, show_icon, show_name, show_description. Clicks map to Activate/SecondaryActivate/ContextMenu. Rename: package, binary, config dir, layer-shell namespace and DBus names are now i3bar-mango; mango is the default feature and the first WM provider tried.
-rw-r--r--Cargo.lock719
-rw-r--r--Cargo.toml12
-rw-r--r--README.md37
-rw-r--r--nix/package.nix2
-rw-r--r--src/bar.rs192
-rw-r--r--src/button_manager.rs8
-rw-r--r--src/config.rs28
-rw-r--r--src/icon.rs28
-rw-r--r--src/main.rs1
-rw-r--r--src/shared_state.rs4
-rw-r--r--src/state.rs37
-rw-r--r--src/tray.rs615
-rw-r--r--src/wm_info_provider.rs11
13 files changed, 1649 insertions, 45 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2156e01..6a5789e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -15,6 +15,143 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -27,6 +164,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
name = "cairo-rs"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -60,6 +216,12 @@ dependencies = [
]
[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
name = "clap"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -88,7 +250,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.101",
]
[[package]]
@@ -98,12 +260,90 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -136,6 +376,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -143,7 +396,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.101",
]
[[package]]
@@ -167,6 +420,17 @@ dependencies = [
]
[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+]
+
+[[package]]
name = "gio"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -193,7 +457,7 @@ dependencies = [
"gobject-sys",
"libc",
"system-deps",
- "windows-sys",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -227,7 +491,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.101",
]
[[package]]
@@ -264,15 +528,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
-name = "i3bar-river"
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "i3bar-mango"
version = "1.1.0"
dependencies = [
"anyhow",
"cairo-rs",
"clap",
+ "futures-channel",
+ "futures-lite",
"libc",
"memchr",
"pangocairo",
+ "parking_lot",
"serde",
"serde_json",
"signal-hook",
@@ -280,6 +559,7 @@ dependencies = [
"wayrs-client",
"wayrs-protocols",
"wayrs-utils",
+ "zbus",
]
[[package]]
@@ -299,10 +579,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "libc"
-version = "0.2.172"
+version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
[[package]]
name = "memchr"
@@ -320,6 +626,31 @@ dependencies = [
]
[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
name = "pango"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -370,6 +701,35 @@ dependencies = [
]
[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -382,12 +742,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -424,29 +809,79 @@ dependencies = [
]
[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
name = "serde"
-version = "1.0.219"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.219"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.3",
]
[[package]]
@@ -462,6 +897,17 @@ dependencies = [
]
[[package]]
+name = "serde_repr"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -525,6 +971,17 @@ dependencies = [
]
[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
name = "system-deps"
version = "7.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -544,6 +1001,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "toml"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -574,7 +1044,49 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
- "winnow",
+ "winnow 0.7.10",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -584,12 +1096,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
+name = "uuid"
+version = "1.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
+dependencies = [
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "version-compare"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
name = "wayrs-client"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -652,6 +1220,12 @@ dependencies = [
]
[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -661,6 +1235,15 @@ dependencies = [
]
[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -734,7 +1317,117 @@ dependencies = [
]
[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "xcursor"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
+
+[[package]]
+name = "zbus"
+version = "5.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 1.0.4",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zvariant",
+]
+
+[[package]]
+name = "zvariant"
+version = "5.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 1.0.4",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.101",
+ "winnow 1.0.4",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 36d4d93..6546843 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
-name = "i3bar-river"
-description = "A port of i3bar for wlroots-based compositors "
+name = "i3bar-mango"
+description = "A port of i3bar for mangowm and other wlroots-based compositors"
repository = "https://github.com/MaxVerevkin/i3bar-river"
readme = "README.md"
version = "1.1.0"
@@ -22,13 +22,17 @@ wayrs-protocols = { version = "0.14", features = ["wlr-layer-shell-unstable-v1",
wayrs-utils = { version = "0.17", features = ["cursor", "shm_alloc", "seats"] }
clap = { version = "4.3", default-features = false, features = ["derive", "std", "help", "usage"] }
libc = "0.2"
+zbus = "5"
+futures-lite = "2"
+futures-channel = "0.3"
+parking_lot = "0.12.5"
[features]
-default = ["river", "mango"]
+default = ["mango"]
+mango = []
river = []
niri = []
hyprland = []
-mango = []
[profile.release]
lto = "fat"
diff --git a/README.md b/README.md
index c507402..909ac19 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# i3bar-river
+# i3bar-mango
-This is a port of `i3bar` for wlroots-based window managers. Tags/workspaces are implemented for [river](https://codeberg.org/river/river), [hyprland](https://github.com/hyprwm/Hyprland) and [niri](https://github.com/YaLTeR/niri).
+This is a port of `i3bar` for [mangowm](https://github.com/mangowm/mango), with support for other wlroots-based window managers ([river](https://codeberg.org/river/river), [hyprland](https://github.com/hyprwm/Hyprland) and [niri](https://github.com/YaLTeR/niri)) available as compile-time features.
## i3bar compatibility
@@ -9,36 +9,31 @@ I've tested [`i3status-rs`](https://github.com/greshake/i3status-rust), [`bumble
A list of things that are missing (for now):
- `border[_top|_right|_bottom|_left]`
- Click events lack some info (IDK if anyone actually relies on `x`, `y`, `width`, etc.)
-- Tray icons
## Features
-- `river` support (obviously)
+- `mango` support (obviously)
- `short_text` switching is "progressive" (see https://github.com/i3/i3/issues/4113)
- Support for rounded corners
-- Show/hide with `pkill -SIGUSR1 i3bar-river`
+- Show/hide with `pkill -SIGUSR1 i3bar-mango`
## Installation
-[![Packaging status](https://repology.org/badge/vertical-allrepos/i3bar-river.svg)](https://repology.org/project/i3bar-river/versions)
+[![Packaging status](https://repology.org/badge/vertical-allrepos/i3bar-mango.svg)](https://repology.org/project/i3bar-mango/versions)
### From Source
External dependencies: `libpango1.0-dev`.
```
-cargo install --locked i3bar-river
+cargo install --locked i3bar-mango
```
## Configuration
-Add this to the end of your river init script:
+Add `i3bar-mango` to your mangowm autostart.
-```
-riverctl spawn i3bar-river
-```
-
-The configuration file should be stored in `$XDG_CONFIG_HOME/i3bar-river/config.toml` or `~/.config/i3bar-river/config.toml`.
+The configuration file should be stored in `$XDG_CONFIG_HOME/i3bar-mango/config.toml` or `~/.config/i3bar-mango/config.toml`.
The default configuration (every parameter is optional):
@@ -85,6 +80,14 @@ blend = true # whether tags/blocks colors should blend with bar's background
show_mode = true
start_hidden = false # whether the bar is initially in the 'hidden' state
+# System tray (StatusNotifierItem)
+[tray]
+enabled = true # host SNI tray icons (the bar acts as the StatusNotifierWatcher)
+expanded = false # start collapsed; the ▸/▾ button toggles the tray
+show_icon = true # show the item icon
+show_name = true # show the item title
+show_description = false # append the item's tooltip description after the title
+
# WM-specific options
[wm.river]
max_tag = 9 # Show only the first nine tags
@@ -106,7 +109,7 @@ to implement, for example, buttons.
_Short text management and corner rounding are performed on per-logical-block basis._
-`i3bar-river` defines a logical block as a continuous series of "json blocks" with the same `name`.
+`i3bar-mango` defines a logical block as a continuous series of "json blocks" with the same `name`.
Also, only the last "json block" is allowed to have a non zero (or absent) `separator_block_width`,
all the other "json blocks" should explicitly set it to zero. If you think this definition is not
the best one, feel free to open a new github issue.
@@ -121,7 +124,7 @@ Sometimes `pango` lives a gap between "powerline separators" and the blocks (see
![Native separators demo](../assets/native_demo.png?raw=true)
-`i3bar-river`
+`i3bar-mango`
```toml
font = "JetBrainsMono Nerd Font 10"
@@ -146,7 +149,7 @@ critical_fg = "#fb4934"
![Powerline separators demo](../assets/powerline_demo.png?raw=true)
-`i3bar-river`
+`i3bar-mango`
```toml
font = "JetBrainsMono Nerd Font 10"
@@ -165,7 +168,7 @@ theme = "slick"
![Rounded corners demo](../assets/rounded_corners_demo.png?raw=true)
-`i3bar-river`
+`i3bar-mango`
```toml
font = "JetBrainsMono Nerd Font 10"
diff --git a/nix/package.nix b/nix/package.nix
index a9eace2..9cfa6cd 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -31,6 +31,6 @@ rustPlatform.buildRustPackage {
description = cargoToml.package.description;
homepage = cargoToml.package.repository;
license = lib.licenses.gpl3Only;
- mainProgram = "i3bar-river";
+ mainProgram = "i3bar-mango";
};
}
diff --git a/src/bar.rs b/src/bar.rs
index 102978d..84bbecc 100644
--- a/src/bar.rs
+++ b/src/bar.rs
@@ -14,6 +14,7 @@ use crate::protocol::*;
use crate::shared_state::SharedState;
use crate::state::State;
use crate::text::{self, ComputedText, RenderOptions};
+use crate::tray::TrayCommand;
use crate::wm_info_provider::{FocusedWindow, Tag};
pub struct Bar {
@@ -39,6 +40,14 @@ pub struct Bar {
layout_name_computed: Option<ComputedText>,
mode_computed: Option<ComputedText>,
focused_window_computed: Option<ComputedText>,
+ tray_btns: ButtonManager<TrayBtn>,
+ tray_toggle_computed: Option<ComputedText>,
+}
+
+#[derive(Clone)]
+enum TrayBtn {
+ Toggle,
+ Item(String),
}
#[derive(Debug, PartialEq)]
@@ -60,7 +69,7 @@ impl Bar {
surface,
Some(output.wl),
state.shared_state.config.layer.into(),
- c"i3bar-river".into(),
+ c"i3bar-mango".into(),
layer_surface_cb,
);
@@ -87,6 +96,8 @@ impl Bar {
layout_name_computed: None,
mode_computed: None,
focused_window_computed: None,
+ tray_btns: ButtonManager::default(),
+ tray_toggle_computed: None,
}
}
@@ -128,7 +139,7 @@ impl Bar {
button: PointerBtn,
seat: WlSeat,
x: f64,
- _y: f64,
+ y: f64,
) -> anyhow::Result<()> {
if let Some(tag_id) = self.tags_btns.click(x) {
ss.wm_info_provider
@@ -136,6 +147,31 @@ impl Bar {
} else if self.tags_btns.is_between(x) {
ss.wm_info_provider
.click_on_tag(conn, &self.output, seat, None, button);
+ } else if let Some(tray_btn) = self.tray_btns.click(x).cloned() {
+ match tray_btn {
+ TrayBtn::Toggle => {
+ ss.tray_expanded = !ss.tray_expanded;
+ self.tray_toggle_computed = None;
+ self.frame(conn, ss);
+ }
+ TrayBtn::Item(key) => {
+ if let Some(tray) = &ss.tray {
+ let cmd = match button {
+ PointerBtn::Left => Some(TrayCommand::Activate { key }),
+ PointerBtn::Middle => Some(TrayCommand::SecondaryActivate { key }),
+ PointerBtn::Right => Some(TrayCommand::ContextMenu {
+ key,
+ x: x as i32,
+ y: y as i32,
+ }),
+ _ => None,
+ };
+ if let Some(cmd) = cmd {
+ tray.send(cmd);
+ }
+ }
+ }
+ }
} else if let Some((name, instance)) = self.blocks_btns.click(x) {
if let Some(cmd) = &mut ss.status_cmd {
cmd.send_click_event(&i3bar_protocol::Event {
@@ -381,6 +417,14 @@ impl Bar {
}
}
+ // Display the tray
+ let tray_width = if ss.config.tray.enabled {
+ self.render_tray(&cairo_ctx, ss, width_f, height_f)
+ } else {
+ self.tray_btns.clear();
+ 0.0
+ };
+
// Display the blocks
render_blocks(
&cairo_ctx,
@@ -388,7 +432,7 @@ impl Bar {
ss.blocks_cache.get_computed(),
&mut self.blocks_btns,
offset_left,
- width_f,
+ width_f - tray_width,
height_f,
);
@@ -417,6 +461,148 @@ impl Bar {
self.surface.commit(conn);
}
+ fn render_tray(
+ &mut self,
+ context: &cairo::Context,
+ ss: &mut SharedState,
+ full_width: f64,
+ full_height: f64,
+ ) -> f64 {
+ self.tray_btns.clear();
+ if ss.tray_items.is_empty() {
+ return 0.0;
+ }
+
+ let cfg = ss.config.tray;
+ let expanded = ss.tray_expanded;
+ let icon_size = full_height - 4.0;
+ let spacing = 8.0;
+ let bar_height = self.height;
+
+ let toggle = self.tray_toggle_computed.get_or_insert_with(|| {
+ ComputedText::new(
+ if expanded { "\u{25BE}" } else { "\u{25B8}" },
+ text::Attributes {
+ font: &ss.config.font,
+ padding_left: 8.0,
+ padding_right: 8.0,
+ min_width: None,
+ align: Default::default(),
+ markup: false,
+ },
+ )
+ });
+
+ struct Rendered {
+ key: String,
+ icon: Option<cairo::ImageSurface>,
+ text: Option<ComputedText>,
+ width: f64,
+ }
+
+ let mut rendered = Vec::new();
+ if expanded {
+ for item in &ss.tray_items {
+ let label = match (cfg.show_name, cfg.show_description && !item.tooltip.is_empty()) {
+ (true, true) if item.tooltip != item.title => {
+ format!("{} \u{2014} {}", item.title, item.tooltip)
+ }
+ (true, _) => item.title.clone(),
+ (false, true) => item.tooltip.clone(),
+ (false, false) => String::new(),
+ };
+ let text = if label.is_empty() {
+ None
+ } else {
+ Some(ComputedText::new(
+ &label,
+ text::Attributes {
+ font: &ss.config.font,
+ padding_left: 4.0,
+ padding_right: 4.0,
+ min_width: None,
+ align: Default::default(),
+ markup: false,
+ },
+ ))
+ };
+ let icon = if cfg.show_icon {
+ item.pixmap.clone().or_else(|| {
+ ss.icon_cache
+ .get_tray_icon(&item.icon_name, &item.icon_theme_path, bar_height)
+ .cloned()
+ })
+ } else {
+ None
+ };
+ let mut width = text.as_ref().map_or(0.0, |t| t.width);
+ if icon.is_some() {
+ width += icon_size + 4.0;
+ }
+ if width > 0.0 {
+ rendered.push(Rendered {
+ key: item.key.clone(),
+ icon,
+ text,
+ width,
+ });
+ }
+ }
+ }
+
+ let items_width: f64 = rendered.iter().map(|r| r.width + spacing).sum();
+ let total_width = items_width + toggle.width;
+ let mut x = full_width - total_width;
+
+ for r in rendered {
+ let mut item_x = x;
+ if let Some(icon) = &r.icon {
+ let scale = icon_size / (icon.width() as f64).max(icon.height() as f64);
+ let icon_w = icon.width() as f64 * scale;
+ let icon_h = icon.height() as f64 * scale;
+ context.save().unwrap();
+ context.translate(item_x + (icon_size - icon_w) / 2.0, (full_height - icon_h) / 2.0);
+ context.scale(scale, scale);
+ context.set_source_surface(icon, 0.0, 0.0).unwrap();
+ context.paint().unwrap();
+ context.restore().unwrap();
+ item_x += icon_size + 4.0;
+ }
+ if let Some(text) = &r.text {
+ text.render(
+ context,
+ RenderOptions {
+ x_offset: item_x,
+ bar_height: full_height,
+ fg_color: ss.config.color,
+ bg_color: None,
+ r_left: 0.0,
+ r_right: 0.0,
+ overlap: 0.0,
+ },
+ );
+ }
+ self.tray_btns.push(x, r.width, TrayBtn::Item(r.key));
+ x += r.width + spacing;
+ }
+
+ toggle.render(
+ context,
+ RenderOptions {
+ x_offset: x,
+ bar_height: full_height,
+ fg_color: ss.config.color,
+ bg_color: None,
+ r_left: 0.0,
+ r_right: 0.0,
+ overlap: 0.0,
+ },
+ );
+ self.tray_btns.push(x, toggle.width, TrayBtn::Toggle);
+
+ total_width
+ }
+
pub fn show(&mut self, conn: &mut Connection<State>, shared_state: &SharedState) {
assert!(!self.mapped);
diff --git a/src/button_manager.rs b/src/button_manager.rs
index 13f9124..9915460 100644
--- a/src/button_manager.rs
+++ b/src/button_manager.rs
@@ -1,6 +1,12 @@
-#[derive(Debug, Default)]
+#[derive(Debug)]
pub struct ButtonManager<T = usize>(Vec<(f64, f64, T)>);
+impl<T> Default for ButtonManager<T> {
+ fn default() -> Self {
+ Self(Vec::new())
+ }
+}
+
impl<T> ButtonManager<T> {
pub fn push(&mut self, x_offset: f64, width: f64, elem: T) {
self.0.push((x_offset, width, elem));
diff --git a/src/config.rs b/src/config.rs
index e5c04e7..f93d7a8 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -50,6 +50,8 @@ pub struct Config {
pub blend: bool,
pub show_mode: bool,
pub start_hidden: bool,
+ // tray
+ pub tray: TrayConfig,
// wm-specific
pub wm: WmConfig,
// overrides
@@ -98,6 +100,8 @@ impl Default for Config {
show_mode: true,
start_hidden: false,
+ tray: TrayConfig::default(),
+
wm: WmConfig {
river: RiverConfig { max_tag: 9 },
},
@@ -148,7 +152,7 @@ fn config_dir() -> Option<PathBuf> {
fn config_path() -> Option<PathBuf> {
let mut path = config_dir()?;
- path.push("i3bar-river");
+ path.push("i3bar-mango");
path.push("config.toml");
path.exists().then_some(path)
}
@@ -189,6 +193,28 @@ impl From<Layer> for zwlr_layer_shell_v1::Layer {
}
}
+#[derive(Deserialize, Debug, Clone, Copy)]
+#[serde(deny_unknown_fields, default)]
+pub struct TrayConfig {
+ pub enabled: bool,
+ pub expanded: bool,
+ pub show_icon: bool,
+ pub show_name: bool,
+ pub show_description: bool,
+}
+
+impl Default for TrayConfig {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ expanded: false,
+ show_icon: true,
+ show_name: true,
+ show_description: false,
+ }
+ }
+}
+
#[derive(Debug, Deserialize)]
pub struct WmConfig {
pub river: RiverConfig,
diff --git a/src/icon.rs b/src/icon.rs
index 4dae1a9..1173629 100644
--- a/src/icon.rs
+++ b/src/icon.rs
@@ -17,6 +17,34 @@ impl IconCache {
.or_insert_with(|| resolve_icon(app_id, bar_height))
.as_ref()
}
+
+ /// Get a tray icon by name, preferring the given SNI IconThemePath.
+ pub fn get_tray_icon(
+ &mut self,
+ name: &str,
+ theme_path: &str,
+ bar_height: u32,
+ ) -> Option<&cairo::ImageSurface> {
+ if name.is_empty() {
+ return None;
+ }
+ self.map
+ .entry(format!("{theme_path}:{name}"))
+ .or_insert_with(|| resolve_tray_icon(name, theme_path, bar_height))
+ .as_ref()
+ }
+}
+
+fn resolve_tray_icon(name: &str, theme_path: &str, bar_height: u32) -> Option<cairo::ImageSurface> {
+ if !theme_path.is_empty() {
+ let path = PathBuf::from(theme_path).join(format!("{name}.png"));
+ if let Ok(mut file) = std::fs::File::open(path)
+ && let Ok(surface) = cairo::ImageSurface::create_from_png(&mut file)
+ {
+ return Some(surface);
+ }
+ }
+ resolve_icon(name, bar_height)
}
fn resolve_icon(app_id: &str, bar_height: u32) -> Option<cairo::ImageSurface> {
diff --git a/src/main.rs b/src/main.rs
index b43443c..a4751d5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,6 +16,7 @@ mod shared_state;
mod state;
mod status_cmd;
mod text;
+mod tray;
mod utils;
mod wm_info_provider;
diff --git a/src/shared_state.rs b/src/shared_state.rs
index c484d24..3bf05e5 100644
--- a/src/shared_state.rs
+++ b/src/shared_state.rs
@@ -5,6 +5,7 @@ use crate::{
config::Config,
icon::IconCache,
status_cmd::StatusCmd,
+ tray::{TrayHandle, TrayItem},
wm_info_provider::{self, WmInfoProvider},
};
@@ -17,6 +18,9 @@ pub struct SharedState {
pub blocks_cache: BlocksCache,
pub icon_cache: IconCache,
pub wm_info_provider: Box<dyn WmInfoProvider>,
+ pub tray: Option<TrayHandle>,
+ pub tray_items: Vec<TrayItem>,
+ pub tray_expanded: bool,
}
impl SharedState {
diff --git a/src/state.rs b/src/state.rs
index ccd0d52..2853731 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -5,6 +5,7 @@ use crate::protocol::*;
use crate::wm_info_provider;
use std::fmt::Display;
+use std::io;
use std::os::unix::io::{AsRawFd, RawFd};
use std::path::Path;
@@ -15,6 +16,9 @@ use wayrs_utils::cursor::{CursorImage, CursorShape, CursorTheme, ThemedPointer};
use wayrs_utils::seats::{SeatHandler, Seats};
use wayrs_utils::shm_alloc::ShmAlloc;
+use crate::event_loop::Action;
+use crate::tray::TrayHandle;
+
use crate::{
bar::Bar, config::Config, i3bar_protocol::Block, pointer_btn::PointerBtn,
shared_state::SharedState, status_cmd::StatusCmd,
@@ -83,6 +87,29 @@ impl State {
let wm_info_provider = wm_info_provider::bind(conn, &config.wm);
wm_info_provider.register(event_loop);
+ let tray = if config.tray.enabled {
+ match io::pipe() {
+ Ok((tray_read, tray_write)) => {
+ let tray = TrayHandle::spawn(tray_write.into());
+ let fd = tray_read.as_raw_fd();
+ event_loop.register_with_fd(fd, move |ctx| {
+ let mut buf = [0u8; 64];
+ while io::Read::read(&mut &tray_read, &mut buf).is_ok_and(|n| n == 64) {}
+ ctx.state.tray_updated(ctx.conn);
+ Ok(Action::Keep)
+ });
+ Some(tray)
+ }
+ Err(e) => {
+ eprintln!("tray: failed to create wake pipe: {e}");
+ None
+ }
+ }
+ } else {
+ None
+ };
+ let tray_expanded = config.tray.expanded;
+
let mut this = Self {
wl_compositor,
layer_shell: conn.bind_singleton(1..=4).unwrap(),
@@ -105,6 +132,9 @@ impl State {
blocks_cache: BlocksCache::default(),
icon_cache: Default::default(),
wm_info_provider,
+ tray,
+ tray_items: Vec::new(),
+ tray_expanded,
},
cursor_theme,
@@ -144,6 +174,13 @@ impl State {
}
}
+ pub fn tray_updated(&mut self, conn: &mut Connection<Self>) {
+ if let Some(tray) = &self.shared_state.tray {
+ self.shared_state.tray_items = tray.items();
+ }
+ self.draw_all(conn);
+ }
+
pub fn status_cmd_fd(&self) -> Option<RawFd> {
self.shared_state
.status_cmd
diff --git a/src/tray.rs b/src/tray.rs
new file mode 100644
index 0000000..69fa9e2
--- /dev/null
+++ b/src/tray.rs
@@ -0,0 +1,615 @@
+use std::os::fd::{AsRawFd, OwnedFd};
+use std::sync::Arc;
+
+use parking_lot::Mutex;
+use std::time::Duration;
+
+use futures_channel::mpsc;
+use futures_lite::stream::StreamExt;
+use pangocairo::cairo;
+use zbus::object_server::SignalEmitter;
+use zbus::{Connection, interface, proxy};
+
+#[derive(Clone)]
+pub struct TrayItem {
+ pub key: String,
+ pub title: String,
+ pub icon_name: String,
+ pub icon_theme_path: String,
+ pub tooltip: String,
+ pub pixmap: Option<cairo::ImageSurface>,
+}
+
+unsafe impl Send for TrayItem {}
+
+pub enum TrayCommand {
+ Activate { key: String },
+ SecondaryActivate { key: String },
+ ContextMenu { key: String, x: i32, y: i32 },
+}
+
+pub struct TrayHandle {
+ items: Arc<Mutex<Vec<TrayItem>>>,
+ commands: mpsc::UnboundedSender<TrayCommand>,
+}
+
+impl TrayHandle {
+ pub fn spawn(wake: OwnedFd) -> TrayHandle {
+ let items = Arc::new(Mutex::new(Vec::new()));
+ let (commands, commands_rx) = mpsc::unbounded();
+ let worker_items = items.clone();
+ std::thread::spawn(move || worker(wake, worker_items, commands_rx));
+ TrayHandle { items, commands }
+ }
+
+ pub fn items(&self) -> Vec<TrayItem> {
+ self.items.lock().clone()
+ }
+
+ pub fn send(&self, cmd: TrayCommand) {
+ let _ = self.commands.unbounded_send(cmd);
+ }
+}
+
+#[derive(Clone, Copy)]
+enum SignalKind {
+ Title,
+ Icon,
+ Status,
+ ToolTip,
+ IconThemePath,
+}
+
+enum Event {
+ Register {
+ bus_name: String,
+ path: String,
+ announced: String,
+ },
+ Vanished(String),
+ Signal {
+ key: String,
+ kind: SignalKind,
+ },
+}
+
+enum Input {
+ Event(Event),
+ Command(TrayCommand),
+}
+
+enum Method {
+ Activate,
+ SecondaryActivate,
+ ContextMenu,
+}
+
+#[derive(Default)]
+struct WatcherShared {
+ registered: Vec<String>,
+ hosts: Vec<String>,
+}
+
+struct WatcherService {
+ shared: Arc<Mutex<WatcherShared>>,
+ events: mpsc::UnboundedSender<Event>,
+}
+
+#[interface(name = "org.kde.StatusNotifierWatcher")]
+impl WatcherService {
+ async fn register_status_notifier_item(
+ &self,
+ service: &str,
+ #[zbus(header)] header: zbus::message::Header<'_>,
+ #[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
+ ) -> zbus::fdo::Result<()> {
+ let sender = header.sender().map(|name| name.as_str()).unwrap_or("");
+ let (bus_name, path) = parse_registration(service, sender);
+ let announced = bus_name.clone();
+ {
+ let mut shared = self.shared.lock();
+ if shared.registered.contains(&announced) {
+ return Ok(());
+ }
+ shared.registered.push(announced.clone());
+ }
+ emitter.status_notifier_item_registered(&announced).await?;
+ self.registered_status_notifier_items_changed(&emitter).await?;
+ let _ = self.events.unbounded_send(Event::Register {
+ bus_name,
+ path,
+ announced,
+ });
+ Ok(())
+ }
+
+ async fn register_status_notifier_host(
+ &self,
+ service: &str,
+ #[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
+ ) -> zbus::fdo::Result<()> {
+ {
+ let mut shared = self.shared.lock();
+ if shared.hosts.iter().any(|host| host == service) {
+ return Ok(());
+ }
+ shared.hosts.push(service.to_string());
+ }
+ emitter.status_notifier_host_registered().await?;
+ self.is_status_notifier_host_registered_changed(&emitter).await?;
+ Ok(())
+ }
+
+ #[zbus(property)]
+ fn registered_status_notifier_items(&self) -> Vec<String> {
+ self.shared.lock().registered.clone()
+ }
+
+ #[zbus(property)]
+ fn is_status_notifier_host_registered(&self) -> bool {
+ !self.shared.lock().hosts.is_empty()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"))]
+ fn protocol_version(&self) -> i32 {
+ 0
+ }
+
+ #[zbus(signal)]
+ async fn status_notifier_item_registered(
+ emitter: &SignalEmitter<'_>,
+ service: &str,
+ ) -> zbus::Result<()>;
+
+ #[zbus(signal)]
+ async fn status_notifier_item_unregistered(
+ emitter: &SignalEmitter<'_>,
+ service: &str,
+ ) -> zbus::Result<()>;
+
+ #[zbus(signal)]
+ async fn status_notifier_host_registered(emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
+
+ #[zbus(signal)]
+ async fn status_notifier_host_unregistered(emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
+}
+
+#[proxy(
+ interface = "org.kde.StatusNotifierItem",
+ gen_blocking = false,
+ assume_defaults = false
+)]
+trait StatusNotifierItem {
+ fn activate(&self, x: i32, y: i32) -> zbus::Result<()>;
+ fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>;
+ fn context_menu(&self, x: i32, y: i32) -> zbus::Result<()>;
+
+ #[zbus(property(emits_changed_signal = "false"))]
+ fn id(&self) -> zbus::Result<String>;
+
+ #[zbus(property(emits_changed_signal = "false"))]
+ fn title(&self) -> zbus::Result<String>;
+
+ #[zbus(property(emits_changed_signal = "false"))]
+ fn status(&self) -> zbus::Result<String>;
+
+ #[zbus(property(emits_changed_signal = "false"))]
+ fn icon_name(&self) -> zbus::Result<String>;
+
+ #[zbus(property(emits_changed_signal = "false"))]
+ fn icon_theme_path(&self) -> zbus::Result<String>;
+
+ #[zbus(property(emits_changed_signal = "false"))]
+ fn tool_tip(&self) -> zbus::Result<(String, Vec<(i32, i32, Vec<u8>)>, String, String)>;
+
+ #[zbus(property(emits_changed_signal = "false"))]
+ fn icon_pixmap(&self) -> zbus::Result<Vec<(i32, i32, Vec<u8>)>>;
+
+ #[zbus(signal)]
+ fn new_title(&self);
+
+ #[zbus(signal)]
+ fn new_icon(&self);
+
+ #[zbus(signal)]
+ fn new_status(&self);
+
+ #[zbus(signal)]
+ fn new_tool_tip(&self);
+
+ #[zbus(signal)]
+ fn new_icon_theme_path(&self);
+}
+
+struct ActiveItem {
+ key: String,
+ bus_name: String,
+ unique_name: String,
+ announced: String,
+ status: String,
+ id: String,
+ proxy: StatusNotifierItemProxy<'static>,
+ item: TrayItem,
+ _tasks: Vec<zbus::Task<()>>,
+}
+
+fn worker(
+ wake: OwnedFd,
+ snapshot: Arc<Mutex<Vec<TrayItem>>>,
+ mut commands: mpsc::UnboundedReceiver<TrayCommand>,
+) {
+ loop {
+ match futures_lite::future::block_on(run(&wake, &snapshot, &mut commands)) {
+ Ok(()) => break,
+ Err(err) => eprintln!("i3bar-mango: tray: {err}; retrying in 5s"),
+ }
+ std::thread::sleep(Duration::from_secs(5));
+ }
+}
+
+async fn run(
+ wake: &OwnedFd,
+ snapshot: &Arc<Mutex<Vec<TrayItem>>>,
+ commands: &mut mpsc::UnboundedReceiver<TrayCommand>,
+) -> zbus::Result<()> {
+ let shared = Arc::new(Mutex::new(WatcherShared::default()));
+ let (events_tx, mut events_rx) = mpsc::unbounded::<Event>();
+
+ let conn = zbus::connection::Builder::session()?
+ .serve_at(
+ "/StatusNotifierWatcher",
+ WatcherService {
+ shared: shared.clone(),
+ events: events_tx.clone(),
+ },
+ )?
+ .name("org.kde.StatusNotifierWatcher")?
+ .name(format!(
+ "org.kde.StatusNotifierHost-i3bar-mango-{}",
+ std::process::id()
+ ))?
+ .build()
+ .await?;
+
+ let dbus = zbus::fdo::DBusProxy::new(&conn).await?;
+
+ let owner_stream = dbus.receive_name_owner_changed().await?;
+ let tx = events_tx.clone();
+ conn.executor()
+ .spawn(
+ async move {
+ futures_lite::pin!(owner_stream);
+ while let Some(signal) = owner_stream.next().await {
+ if let Ok(args) = signal.args() {
+ if args.new_owner().is_none() {
+ let _ = tx.unbounded_send(Event::Vanished(args.name().to_string()));
+ }
+ }
+ }
+ },
+ "tray-name-owner",
+ )
+ .detach();
+
+ let mut items: Vec<ActiveItem> = Vec::new();
+
+ loop {
+ let input = futures_lite::future::or(
+ async { events_rx.next().await.map(Input::Event) },
+ async { commands.next().await.map(Input::Command) },
+ )
+ .await;
+
+ match input {
+ None => return Ok(()),
+ Some(Input::Command(cmd)) => {
+ let (key, method, x, y) = match cmd {
+ TrayCommand::Activate { key } => (key, Method::Activate, 0, 0),
+ TrayCommand::SecondaryActivate { key } => (key, Method::SecondaryActivate, 0, 0),
+ TrayCommand::ContextMenu { key, x, y } => (key, Method::ContextMenu, x, y),
+ };
+ if let Some(active) = items.iter().find(|item| item.key == key) {
+ let proxy = active.proxy.clone();
+ conn.executor()
+ .spawn(
+ async move {
+ let result = match method {
+ Method::Activate => proxy.activate(0, 0).await,
+ Method::SecondaryActivate => {
+ proxy.secondary_activate(0, 0).await
+ }
+ Method::ContextMenu => proxy.context_menu(x, y).await,
+ };
+ if let Err(err) = result {
+ eprintln!("i3bar-mango: tray: command failed: {err}");
+ }
+ },
+ "tray-command",
+ )
+ .detach();
+ }
+ }
+ Some(Input::Event(Event::Register {
+ bus_name,
+ path,
+ announced,
+ })) => {
+ let key = format!("{bus_name}/{path}");
+ if items.iter().any(|item| item.key == key) {
+ continue;
+ }
+ let builder = StatusNotifierItemProxy::builder(&conn)
+ .destination(bus_name.clone())
+ .and_then(|builder| builder.path(path.clone()));
+ let proxy = match builder {
+ Ok(builder) => match builder.build().await {
+ Ok(proxy) => proxy,
+ Err(err) => {
+ eprintln!("i3bar-mango: tray: failed to reach {key}: {err}");
+ unregister(&conn, &shared, &announced).await;
+ continue;
+ }
+ },
+ Err(err) => {
+ eprintln!("i3bar-mango: tray: invalid registration {key}: {err}");
+ unregister(&conn, &shared, &announced).await;
+ continue;
+ }
+ };
+ let id = match proxy.id().await {
+ Ok(id) => id,
+ Err(err) => {
+ eprintln!("i3bar-mango: tray: failed to read properties of {key}: {err}");
+ unregister(&conn, &shared, &announced).await;
+ continue;
+ }
+ };
+ let title = proxy.title().await.unwrap_or_default();
+ let icon_name = proxy.icon_name().await.unwrap_or_default();
+ let icon_theme_path = proxy.icon_theme_path().await.unwrap_or_default();
+ let status = proxy.status().await.unwrap_or_default();
+ let tooltip = proxy
+ .tool_tip()
+ .await
+ .map(|tooltip| strip_html(&tooltip.3))
+ .unwrap_or_default();
+ let pixmap = match proxy.icon_pixmap().await {
+ Ok(pixmaps) => best_pixmap(&pixmaps)
+ .and_then(|(width, height, data)| pixmap_to_surface(*width, *height, data)),
+ Err(_) => None,
+ };
+ let title = if title.is_empty() { id.clone() } else { title };
+ let unique_name = if bus_name.starts_with(':') {
+ bus_name.clone()
+ } else {
+ match zbus::names::BusName::try_from(bus_name.as_str()) {
+ Ok(name) => dbus
+ .get_name_owner(name)
+ .await
+ .map(|owner| owner.to_string())
+ .unwrap_or_default(),
+ Err(_) => String::new(),
+ }
+ };
+ let tasks = spawn_signal_tasks(&conn, &proxy, &key, &events_tx).await;
+ items.push(ActiveItem {
+ key: key.clone(),
+ bus_name,
+ unique_name,
+ announced,
+ status,
+ id,
+ proxy,
+ item: TrayItem {
+ key,
+ title,
+ icon_name,
+ icon_theme_path,
+ tooltip,
+ pixmap,
+ },
+ _tasks: tasks,
+ });
+ update_snapshot(&items, snapshot, wake);
+ }
+ Some(Input::Event(Event::Vanished(name))) => {
+ let removed = if let Some(pos) = items
+ .iter()
+ .position(|item| item.bus_name == name || item.unique_name == name)
+ {
+ Some(items.remove(pos).announced)
+ } else {
+ let is_host = shared.lock().hosts.iter().any(|host| *host == name);
+ if is_host {
+ let mut state = shared.lock();
+ state.hosts.retain(|host| *host != name);
+ if let Ok(iface) = conn
+ .object_server()
+ .interface::<_, WatcherService>("/StatusNotifierWatcher")
+ .await
+ {
+ let _ = iface.status_notifier_host_unregistered().await;
+ let guard = iface.get().await;
+ let _ = guard
+ .is_status_notifier_host_registered_changed(iface.signal_emitter())
+ .await;
+ }
+ None
+ } else if shared.lock().registered.iter().any(|n| *n == name) {
+ Some(name)
+ } else {
+ None
+ }
+ };
+ if let Some(announced) = removed {
+ unregister(&conn, &shared, &announced).await;
+ update_snapshot(&items, snapshot, wake);
+ }
+ }
+ Some(Input::Event(Event::Signal { key, kind })) => {
+ let Some(index) = items.iter().position(|item| item.key == key) else {
+ continue;
+ };
+ let active = &mut items[index];
+ match kind {
+ SignalKind::Title => {
+ if let Ok(title) = active.proxy.title().await {
+ active.item.title = if title.is_empty() {
+ active.id.clone()
+ } else {
+ title
+ };
+ }
+ }
+ SignalKind::Icon => {
+ if let Ok(icon_name) = active.proxy.icon_name().await {
+ active.item.icon_name = icon_name;
+ }
+ if let Ok(pixmaps) = active.proxy.icon_pixmap().await {
+ active.item.pixmap = best_pixmap(&pixmaps).and_then(
+ |(width, height, data)| pixmap_to_surface(*width, *height, data),
+ );
+ }
+ }
+ SignalKind::Status => {
+ if let Ok(status) = active.proxy.status().await {
+ active.status = status;
+ }
+ }
+ SignalKind::ToolTip => {
+ if let Ok(tooltip) = active.proxy.tool_tip().await {
+ active.item.tooltip = strip_html(&tooltip.3);
+ }
+ }
+ SignalKind::IconThemePath => {
+ if let Ok(path) = active.proxy.icon_theme_path().await {
+ active.item.icon_theme_path = path;
+ }
+ }
+ }
+ update_snapshot(&items, snapshot, wake);
+ }
+ }
+ }
+}
+
+async fn unregister(conn: &Connection, shared: &Arc<Mutex<WatcherShared>>, announced: &str) {
+ {
+ let mut state = shared.lock();
+ let Some(pos) = state.registered.iter().position(|name| name == announced) else {
+ return;
+ };
+ state.registered.remove(pos);
+ }
+ if let Ok(iface) = conn
+ .object_server()
+ .interface::<_, WatcherService>("/StatusNotifierWatcher")
+ .await
+ {
+ let _ = iface.status_notifier_item_unregistered(announced).await;
+ let guard = iface.get().await;
+ let _ = guard
+ .registered_status_notifier_items_changed(iface.signal_emitter())
+ .await;
+ }
+}
+
+async fn spawn_signal_tasks(
+ conn: &Connection,
+ proxy: &StatusNotifierItemProxy<'static>,
+ key: &str,
+ events: &mpsc::UnboundedSender<Event>,
+) -> Vec<zbus::Task<()>> {
+ let mut tasks = Vec::new();
+ macro_rules! forward {
+ ($stream:expr, $kind:expr) => {
+ if let Ok(stream) = $stream.await {
+ let key = key.to_string();
+ let events = events.clone();
+ tasks.push(conn.executor().spawn(
+ async move {
+ futures_lite::pin!(stream);
+ while stream.next().await.is_some() {
+ let _ = events.unbounded_send(Event::Signal {
+ key: key.clone(),
+ kind: $kind,
+ });
+ }
+ },
+ "tray-signal",
+ ));
+ }
+ };
+ }
+ forward!(proxy.receive_new_title(), SignalKind::Title);
+ forward!(proxy.receive_new_icon(), SignalKind::Icon);
+ forward!(proxy.receive_new_status(), SignalKind::Status);
+ forward!(proxy.receive_new_tool_tip(), SignalKind::ToolTip);
+ forward!(proxy.receive_new_icon_theme_path(), SignalKind::IconThemePath);
+ tasks
+}
+
+fn update_snapshot(items: &[ActiveItem], snapshot: &Arc<Mutex<Vec<TrayItem>>>, wake: &OwnedFd) {
+ let visible: Vec<TrayItem> = items
+ .iter()
+ .filter(|item| item.status != "Passive")
+ .map(|item| item.item.clone())
+ .collect();
+ *snapshot.lock() = visible;
+ let result = unsafe { libc::write(wake.as_raw_fd(), [1u8].as_ptr().cast(), 1) };
+ if result < 0 {
+ eprintln!(
+ "i3bar-mango: tray: wake pipe write failed: {}",
+ std::io::Error::last_os_error()
+ );
+ }
+}
+
+fn parse_registration(arg: &str, sender: &str) -> (String, String) {
+ if arg.starts_with('/') {
+ (sender.to_string(), arg.to_string())
+ } else if let Some((name, path)) = arg.split_once('/') {
+ (name.to_string(), format!("/{path}"))
+ } else {
+ (arg.to_string(), "/StatusNotifierItem".to_string())
+ }
+}
+
+fn best_pixmap<'a>(pixmaps: &'a [(i32, i32, Vec<u8>)]) -> Option<&'a (i32, i32, Vec<u8>)> {
+ pixmaps
+ .iter()
+ .filter(|(width, height, data)| {
+ *width > 0 && *height > 0 && data.len() >= (*width * *height * 4) as usize
+ })
+ .min_by_key(|(width, height, _)| {
+ let size = (*width).max(*height);
+ if size >= 24 { size } else { i32::MAX - size }
+ })
+}
+
+fn pixmap_to_surface(width: i32, height: i32, data: &[u8]) -> Option<cairo::ImageSurface> {
+ let stride = width * 4;
+ let mut buffer = vec![0u8; (height * stride) as usize];
+ for (src, dst) in data.chunks_exact(4).zip(buffer.chunks_exact_mut(4)) {
+ let a = u32::from(src[0]);
+ let r = (u32::from(src[1]) * a + 127) / 255;
+ let g = (u32::from(src[2]) * a + 127) / 255;
+ let b = (u32::from(src[3]) * a + 127) / 255;
+ dst.copy_from_slice(&((a << 24) | (r << 16) | (g << 8) | b).to_ne_bytes());
+ }
+ cairo::ImageSurface::create_for_data(buffer, cairo::Format::ARgb32, width, height, stride).ok()
+}
+
+fn strip_html(text: &str) -> String {
+ let mut out = String::with_capacity(text.len());
+ let mut in_tag = false;
+ for ch in text.chars() {
+ match ch {
+ '<' => in_tag = true,
+ '>' if in_tag => in_tag = false,
+ _ if !in_tag => out.push(ch),
+ _ => {}
+ }
+ }
+ out.trim().to_string()
+}
diff --git a/src/wm_info_provider.rs b/src/wm_info_provider.rs
index 67e5f83..f46c4e0 100644
--- a/src/wm_info_provider.rs
+++ b/src/wm_info_provider.rs
@@ -62,7 +62,13 @@ pub trait WmInfoProvider: Any {
}
}
+#[cfg_attr(not(feature = "river"), allow(unused_variables))]
pub fn bind(conn: &mut Connection<State>, config: &WmConfig) -> Box<dyn WmInfoProvider> {
+ #[cfg(feature = "mango")]
+ if let Some(mango) = MangoInfoProvider::new() {
+ return Box::new(mango);
+ }
+
#[cfg(feature = "river")]
if let Some(river) = RiverInfoProvider::bind(conn, config) {
return Box::new(river);
@@ -78,11 +84,6 @@ pub fn bind(conn: &mut Connection<State>, config: &WmConfig) -> Box<dyn WmInfoPr
return Box::new(niri);
}
- #[cfg(feature = "mango")]
- if let Some(mango) = MangoInfoProvider::new() {
- return Box::new(mango);
- }
-
Box::new(DummyInfoProvider)
}