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

Merge branch 'master' into feat/networkmanager

This commit is contained in:
Reinout Meliesie 2024-03-29 20:40:46 +01:00
commit 4e2352c9e9
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
22 changed files with 579 additions and 390 deletions

20
.github/scripts/ubuntu_setup.sh vendored Executable file
View file

@ -0,0 +1,20 @@
#!/bin/sh
# sudo needed for github runner, not available by default for cross images
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
SUDO=""
fi
# Needed for cross-compilation
if [ -n "$CROSS_DEB_ARCH" ]; then
$SUDO dpkg --add-architecture "$CROSS_DEB_ARCH"
fi
# CROSS_DEB_ARCH is empty for native builds
$SUDO apt-get update && $SUDO apt-get install --assume-yes \
libssl-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libgtk-3-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libgtk-layer-shell-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libpulse-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH}

39
.github/workflows/binary.yml vendored Normal file
View file

@ -0,0 +1,39 @@
# .github/workflows/binary.yml
name: Binary
on:
release:
types: [created]
jobs:
build:
name: Build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform:
- {target: x86_64-unknown-linux-gnu, zipext: ".tar.gz"}
- {target: aarch64-unknown-linux-gnu, zipext: ".tar.gz"}
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@v2
with:
tool: cross
- name: Add OpenSSL crate (vendored)
run: cargo add openssl --features vendored
- name: Cross Build Release
run: cross build --locked --release --target=${{ matrix.platform.target }}
- name: Get name of Binary from metadata
run: echo "BINARY_NAME=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].targets[] | select( .kind | map(. == "bin") | any ) | .name')" >> $GITHUB_ENV
- name: Compress the built binary
if: ${{ matrix.platform.zipext == '.tar.gz' }}
run: tar -zcvf ${{env.BINARY_NAME}}-${{github.ref_name}}-${{matrix.platform.target}}.tar.gz -C target/${{matrix.platform.target}}/release ${{env.BINARY_NAME}}
- name: Upload to release
run: gh release upload ${GITHUB_REF#refs/*/} ${{env.BINARY_NAME}}-${{github.ref_name}}-${{matrix.platform.target}}${{matrix.platform.zipext}}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -32,9 +32,7 @@ jobs:
name: Cache dependencies
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
run: ./.github/scripts/ubuntu_setup.sh
- name: Clippy
run: cargo clippy --no-default-features --features config+json
@ -53,9 +51,7 @@ jobs:
name: Cache dependencies
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
run: ./.github/scripts/ubuntu_setup.sh
- name: Clippy
run: cargo clippy --all-targets --all-features
@ -72,9 +68,7 @@ jobs:
name: Cache dependencies
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
run: ./.github/scripts/ubuntu_setup.sh
- name: Build
run: cargo build --verbose
@ -82,4 +76,4 @@ jobs:
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
command: test

View file

@ -18,9 +18,7 @@ jobs:
override: true
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
run: ./.github/scripts/ubuntu_setup.sh
- name: Update CHANGELOG
id: changelog
@ -48,4 +46,4 @@ jobs:
- uses: katyo/publish-crates@v1
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}

212
Cargo.lock generated
View file

@ -101,12 +101,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anyhow"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
[[package]]
name = "async-broadcast"
version = "0.5.1"
@ -444,9 +438,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.2"
version = "4.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651"
checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
dependencies = [
"clap_builder",
"clap_derive",
@ -466,11 +460,11 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.0"
version = "4.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
dependencies = [
"heck 0.4.1",
"heck 0.5.0",
"proc-macro2",
"quote 1.0.35",
"syn 2.0.48",
@ -494,9 +488,9 @@ dependencies = [
[[package]]
name = "color-eyre"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204"
checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5"
dependencies = [
"backtrace",
"color-spantrace",
@ -1043,9 +1037,9 @@ dependencies = [
[[package]]
name = "futures-lite"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
dependencies = [
"fastrand 2.0.1",
"futures-core",
@ -1352,9 +1346,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.24"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4"
dependencies = [
"bytes",
"fnv",
@ -1362,7 +1356,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 2.2.5",
"indexmap 2.2.6",
"slab",
"tokio",
"tokio-util",
@ -1396,6 +1390,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.2.6"
@ -1419,9 +1419,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "0.2.9"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
@ -1430,12 +1430,24 @@ dependencies = [
[[package]]
name = "http-body"
version = "0.4.5"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
@ -1445,47 +1457,60 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.26"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.4.9",
"smallvec",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"pin-project-lite",
"socket2 0.5.5",
"tokio",
"tower",
"tower-service",
"tracing",
]
[[package]]
@ -1579,9 +1604,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.2.5"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown 0.14.1",
@ -1643,13 +1668,13 @@ dependencies = [
"color-eyre",
"ctrlc",
"dirs",
"futures-lite 2.2.0",
"futures-lite 2.3.0",
"futures-util",
"glib",
"gtk",
"gtk-layer-shell",
"hyprland",
"indexmap 2.2.5",
"indexmap 2.2.6",
"libpulse-binding",
"mpd-utils",
"mpris",
@ -2078,9 +2103,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.17.1"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl"
@ -2264,6 +2289,26 @@ dependencies = [
"sha2",
]
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote 1.0.35",
"syn 2.0.48",
]
[[package]]
name = "pin-project-lite"
version = "0.2.12"
@ -2484,9 +2529,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.3"
version = "1.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"memchr",
@ -2528,9 +2573,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "reqwest"
version = "0.11.25"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946"
checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338"
dependencies = [
"base64 0.21.0",
"bytes",
@ -2540,8 +2585,10 @@ dependencies = [
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
@ -2842,9 +2889,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.10.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smithay-client-toolkit"
@ -3030,20 +3077,20 @@ dependencies = [
[[package]]
name = "system-configuration"
version = "0.6.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 2.4.0",
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
@ -3064,18 +3111,13 @@ dependencies = [
[[package]]
name = "system-tray"
version = "0.1.5"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a456e3e6cbd396f1a3a91f8f74d1fdcf2bde85c97afe174442c367f4749fc09b"
checksum = "82a053bfb84b11f5eb8655a762ba826a2524d02a2f355b0fd6fce4125272f2e0"
dependencies = [
"anyhow",
"byteorder",
"chrono",
"log",
"serde",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
"zbus",
]
@ -3104,18 +3146,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.56"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.56"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote 1.0.35",
@ -3190,7 +3232,6 @@ dependencies = [
"signal-hook-registry",
"socket2 0.5.5",
"tokio-macros",
"tracing",
"windows-sys 0.48.0",
]
@ -3215,17 +3256,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.7"
@ -3267,7 +3297,7 @@ version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap 2.2.5",
"indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",
@ -3280,11 +3310,33 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap 2.2.5",
"indexmap 2.2.6",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
@ -3297,6 +3349,7 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@ -4068,7 +4121,6 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"winapi",

View file

@ -95,9 +95,9 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-error = "0.2.0"
tracing-appender = "0.2.3"
strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.2"
color-eyre = "0.6.3"
serde = { version = "1.0.197", features = ["derive"] }
indexmap = "2.2.5"
indexmap = "2.2.6"
dirs = "5.0.1"
walkdir = "2.5.0"
notify = { version = "6.1.1", default-features = false }
@ -112,13 +112,13 @@ ctrlc = "3.4.2"
cfg-if = "1.0.0"
# cli
clap = { version = "4.5.2", optional = true, features = ["derive"] }
clap = { version = "4.5.3", optional = true, features = ["derive"] }
# ipc
serde_json = { version = "1.0.114", optional = true }
# http
reqwest = { version = "0.11.25", optional = true }
reqwest = { version = "0.12.2", optional = true }
# clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] }
@ -134,14 +134,13 @@ mpris = { version = "2.0.1", optional = true }
sysinfo = { version = "0.29.11", optional = true }
# tray
system-tray = { version = "0.1.5", optional = true }
system-tray = { version = "0.2.0", optional = true }
# upower
upower_dbus = { version = "0.3.2", optional = true }
# volume
libpulse-binding = { version = "2.28.1", optional = true }
# libpulse-glib-binding = { version = "2.27.1", optional = true }
# workspaces
swayipc-async = { version = "2.0.1", optional = true }
@ -149,8 +148,8 @@ hyprland = { version = "0.3.13", features = ["silent"], optional = true }
futures-util = { version = "0.3.30", optional = true }
# shared
regex = { version = "1.10.3", default-features = false, features = [
regex = { version = "1.10.4", default-features = false, features = [
"std",
], optional = true } # music, sys_info
futures-lite = { version = "2.2.0", optional = true } # networkmanager, upower
futures-lite = { version = "2.3.0", optional = true } # networkmanager, upower
zbus = { version = "3.15.2", optional = true } # networkmanager, notifications, upower

7
Cross.toml Normal file
View file

@ -0,0 +1,7 @@
[build]
pre-build = "./.github/scripts/ubuntu_setup.sh"
[target.aarch64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main"
[target.x86_64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main"

View file

@ -57,6 +57,8 @@ Ironbar is designed to support anything from a lightweight bar to a full desktop
## Installation
[![Packaging status](https://repology.org/badge/vertical-allrepos/ironbar.svg)](https://repology.org/project/ironbar/versions)
### Cargo
[crate](https://crates.io/crates/ironbar)
@ -130,6 +132,14 @@ A flake is included with the repo which can be used with Home Manager.
CI builds are automatically cached by Garnix.
You can use their binary cache by following the steps [here](https://garnix.io/docs/caching).
### Void Linux
[void package](https://github.com/void-linux/void-packages/tree/master/srcpkgs/ironbar)
```sh
xbps-install ironbar
```
### Source
[repo](https://github.com/jakestanger/ironbar)
@ -183,4 +193,4 @@ All are welcome, but I ask a few basic things to help make things easier. Please
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this
- [Mixxc](https://github.com/Elvyria/Mixxc) - Basis for Ironbar's PulseAudio client code and a cool standalone volume widget.
- [Mixxc](https://github.com/Elvyria/Mixxc) - Basis for Ironbar's PulseAudio client code and a cool standalone volume widget.

View file

@ -1,4 +1,4 @@
Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
![Screenshot showing icon tray widget](https://user-images.githubusercontent.com/5057870/184540135-78ffd79d-f802-4c79-b09a-05a733dadc55.png)
@ -6,10 +6,11 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
> Type: `tray`
| Name | Type | Default | Description |
|-------------|----------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| `direction` | `string` | `left_to_right` if bar is horizontal, `top_to_bottom` otherwise | Direction to display the tray items. Possible values: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left` |
| Name | Type | Default | Description |
|----------------------|-----------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `direction` | `string` | `left_to_right` if bar is horizontal, `top_to_bottom` otherwise | Direction to display the tray items. Possible values: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left` |
| `icon_size` | `integer` | `16` | Size in pixels to display tray icons as. |
| `prefer_theme_icons` | `bool` | `true` | Requests that icons from the theme be used over the item-provided item. Most items only provide one or the other so this will have no effect in most circumstances. |
<details>
<summary>JSON</summary>
@ -54,12 +55,12 @@ end:
```corn
{
end = [
end = [
{
type = "tray"
direction = "top_to_bottom"
type = "tray"
direction = "top_to_bottom"
}
]
]
}
```

View file

@ -1,3 +1,4 @@
use crate::Ironbar;
use std::sync::Arc;
#[cfg(feature = "clipboard")]
@ -9,7 +10,7 @@ pub mod music;
#[cfg(feature = "notifications")]
pub mod swaync;
#[cfg(feature = "tray")]
pub mod system_tray;
pub mod tray;
#[cfg(feature = "upower")]
pub mod upower;
#[cfg(feature = "volume")]
@ -30,7 +31,7 @@ pub struct Clients {
#[cfg(feature = "notifications")]
notifications: Option<Arc<swaync::Client>>,
#[cfg(feature = "tray")]
tray: Option<Arc<system_tray::TrayEventReceiver>>,
tray: Option<Arc<tray::Client>>,
#[cfg(feature = "upower")]
upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>,
#[cfg(feature = "volume")]
@ -85,11 +86,17 @@ impl Clients {
}
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> Arc<system_tray::TrayEventReceiver> {
pub fn tray(&mut self) -> Arc<tray::Client> {
// TODO: Error handling here isn't great - should throw a user-friendly error
self.tray
.get_or_insert_with(|| {
Arc::new(crate::await_sync(async {
system_tray::create_client().await
let service_name =
format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
tray::Client::new(&service_name)
.await
.expect("to be able to start client")
}))
})
.clone()

View file

@ -34,14 +34,15 @@ pub struct Track {
pub cover_path: Option<String>,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Default)]
pub enum PlayerState {
#[default]
Stopped,
Playing,
Paused,
Stopped,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Default)]
pub struct Status {
pub state: PlayerState,
pub volume_percent: Option<u8>,

View file

@ -18,6 +18,11 @@ pub struct Client {
_rx: broadcast::Receiver<PlayerUpdate>,
}
const NO_ACTIVE_PLAYER: &str = "com.github.altdesktop.playerctld.NoActivePlayer";
const NO_REPLY: &str = "org.freedesktop.DBus.Error.NoReply";
const NO_SERVICE: &str = "org.freedesktop.DBus.Error.ServiceUnknown";
const NO_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
impl Client {
pub(crate) fn new() -> Self {
let (tx, rx) = broadcast::channel(32);
@ -35,44 +40,48 @@ impl Client {
// D-Bus gives no event for new players,
// so we have to keep polling the player list
loop {
let players = player_finder
.find_all()
.expect("Failed to connect to D-Bus");
// mpris-rs does not filter NoActivePlayer errors, so we have to do it ourselves
let players = player_finder.find_all().unwrap_or_else(|e| match e {
mpris::FindingError::DBusError(DBusError::TransportError(
transport_error,
)) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
|| transport_error.name() == Some(NO_REPLY) =>
{
Vec::new()
}
_ => panic!("Failed to connect to D-Bus"),
});
// Acquire the lock of current_player before players to avoid deadlock.
// There are places where we lock on current_player and players, but we always lock on current_player first.
// This is because we almost never need to lock on players without locking on current_player.
{
let mut current_player_lock = lock!(current_player);
let mut players_list_val = lock!(players_list);
for player in players {
let identity = player.identity();
let mut players_list_val = lock!(players_list);
for player in players {
let identity = player.identity();
if !players_list_val.contains(identity) {
debug!("Adding MPRIS player '{identity}'");
players_list_val.insert(identity.to_string());
if current_player_lock.is_none() {
debug!("Setting active player to '{identity}'");
current_player_lock.replace(identity.to_string());
let status = player
.get_playback_status()
.expect("Failed to connect to D-Bus");
{
let mut current_player = lock!(current_player);
if status == PlaybackStatus::Playing || current_player.is_none() {
debug!("Setting active player to '{identity}'");
current_player.replace(identity.to_string());
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
}
if !players_list_val.contains(identity) {
debug!("Adding MPRIS player '{identity}'");
players_list_val.insert(identity.to_string());
Self::listen_player_events(
identity.to_string(),
players_list.clone(),
current_player.clone(),
tx.clone(),
);
Self::listen_player_events(
identity.to_string(),
players_list.clone(),
current_player.clone(),
tx.clone(),
);
}
}
}
// wait 1 second before re-checking players
sleep(Duration::from_secs(1));
}
@ -111,28 +120,56 @@ impl Client {
if let Ok(player) = player_finder.find_by_name(&player_id) {
let identity = player.identity();
let handle_shutdown = |current_player_lock_option: Option<
std::sync::MutexGuard<'_, Option<String>>,
>| {
debug!("Player '{identity}' shutting down");
// Lock of player before players (see new() to make sure order is consistent)
if let Some(mut guard) = current_player_lock_option {
guard.take();
} else {
lock!(current_player).take();
}
let mut players_locked = lock!(players);
players_locked.remove(identity);
if players_locked.is_empty() {
send!(tx, PlayerUpdate::Update(Box::new(None), Status::default()));
}
};
for event in player.events()? {
trace!("Received player event from '{identity}': {event:?}");
match event {
Ok(Event::PlayerShutDown) => {
lock!(current_player).take();
lock!(players).remove(identity);
handle_shutdown(None);
break;
}
Ok(Event::Playing) => {
lock!(current_player).replace(identity.to_string());
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
Err(mpris::EventError::DBusError(DBusError::TransportError(
transport_error,
))) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
|| transport_error.name() == Some(NO_REPLY)
|| transport_error.name() == Some(NO_METHOD)
|| transport_error.name() == Some(NO_SERVICE) =>
{
handle_shutdown(None);
break;
}
Ok(_) => {
let current_player = lock!(current_player);
let current_player = current_player.as_ref();
if let Some(current_player) = current_player {
if current_player == identity {
let mut current_player_lock = lock!(current_player);
if matches!(event, Ok(Event::Playing)) {
current_player_lock.replace(identity.to_string());
}
if let Some(current_identity) = current_player_lock.as_ref() {
if current_identity == identity {
if let Err(err) = Self::send_update(&player, &tx) {
if let Some(DBusError::TransportError(transport_error)) =
err.downcast_ref::<DBusError>()
{
if transport_error.name() == Some(NO_SERVICE) {
handle_shutdown(Some(current_player_lock));
break;
}
}
error!("{err:?}");
}
}

View file

@ -1,127 +0,0 @@
use crate::{arc_mut, lock, register_client, send, spawn, Ironbar};
use color_eyre::Report;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use system_tray::message::menu::TrayMenu;
use system_tray::message::tray::StatusNotifierItem;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
use system_tray::StatusNotifierWatcher;
use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error, trace};
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
#[derive(Debug)]
pub struct TrayEventReceiver {
tx: mpsc::Sender<NotifierItemCommand>,
b_tx: broadcast::Sender<NotifierItemMessage>,
_b_rx: broadcast::Receiver<NotifierItemMessage>,
tray: Arc<Mutex<Tray>>,
}
impl TrayEventReceiver {
async fn new() -> system_tray::error::Result<Self> {
let id = format!("ironbar-{}", Ironbar::unique_id());
let (tx, rx) = mpsc::channel(16);
let (b_tx, b_rx) = broadcast::channel(64);
let tray = StatusNotifierWatcher::new(rx).await?;
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
let tray = arc_mut!(BTreeMap::new());
{
let b_tx = b_tx.clone();
let tray = tray.clone();
spawn(async move {
while let Ok(message) = host.recv().await {
trace!("Received message: {message:?}");
send!(b_tx, message.clone());
let mut tray = lock!(tray);
match message {
NotifierItemMessage::Update {
address,
item,
menu,
} => {
debug!("Adding/updating item with address '{address}'");
tray.insert(address, (item, menu));
}
NotifierItemMessage::Remove { address } => {
debug!("Removing item with address '{address}'");
tray.remove(&address);
}
}
}
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
});
}
Ok(Self {
tx,
b_tx,
_b_rx: b_rx,
tray,
})
}
pub fn subscribe(
&self,
) -> (
mpsc::Sender<NotifierItemCommand>,
broadcast::Receiver<NotifierItemMessage>,
) {
let tx = self.tx.clone();
let b_rx = self.b_tx.subscribe();
let tray = lock!(self.tray).clone();
for (address, (item, menu)) in tray {
let update = NotifierItemMessage::Update {
address,
item,
menu,
};
send!(self.b_tx, update);
}
(tx, b_rx)
}
}
/// Attempts to create a new `TrayEventReceiver` instance,
/// retrying a maximum of 10 times before panicking the thread.
pub async fn create_client() -> TrayEventReceiver {
const MAX_RETRIES: i32 = 10;
// sometimes this can fail
let mut retries = 0;
let value = loop {
retries += 1;
let tray = Box::pin(TrayEventReceiver::new()).await;
match tray {
Ok(tray) => break Some(tray),
Err(err) => error!(
"{:?}",
Report::new(err).wrap_err(format!(
"Failed to create StatusNotifierWatcher (attempt {retries})"
))
),
}
if retries == MAX_RETRIES {
break None;
}
};
value.expect("Failed to create StatusNotifierWatcher")
}
register_client!(TrayEventReceiver, tray);

4
src/clients/tray.rs Normal file
View file

@ -0,0 +1,4 @@
use crate::register_client;
pub use system_tray::client::Client;
register_client!(Client, tray);

View file

@ -98,6 +98,7 @@ pub enum Response {
}
#[derive(Debug)]
#[allow(dead_code)]
struct BroadcastChannel<T>(broadcast::Sender<T>, Arc<Mutex<broadcast::Receiver<T>>>);
impl<T> From<(broadcast::Sender<T>, broadcast::Receiver<T>)> for BroadcastChannel<T> {

View file

@ -11,7 +11,7 @@ use gtk::{IconLookupFlags, IconTheme};
use std::path::{Path, PathBuf};
#[cfg(feature = "http")]
use tokio::sync::mpsc;
use tracing::warn;
use tracing::{debug, warn};
cfg_if!(
if #[cfg(feature = "http")] {
@ -45,6 +45,7 @@ impl<'a> ImageProvider<'a> {
/// but no other check is performed.
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
debug!("Resolved {input} --> {location:?} (size: {size})");
Some(Self { location, size })
}
@ -171,7 +172,7 @@ impl<'a> ImageProvider<'a> {
);
// Different error types makes this a bit awkward
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image))
{
Ok(Err(err)) => error!("{err:?}"),
Err(err) => error!("{err:?}"),
@ -202,7 +203,7 @@ impl<'a> ImageProvider<'a> {
_ => unreachable!(), // handled above
}?;
Self::create_and_load_surface(&pixbuf, image, scale)
Self::create_and_load_surface(&pixbuf, image)
}
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
@ -210,10 +211,13 @@ impl<'a> ImageProvider<'a> {
/// The surface is then loaded into the provided image.
///
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
fn create_and_load_surface(pixbuf: &Pixbuf, image: &gtk::Image, scale: i32) -> Result<()> {
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: &gtk::Image) -> Result<()> {
let surface = unsafe {
let ptr =
gdk_cairo_surface_create_from_pixbuf(pixbuf.as_ptr(), scale, std::ptr::null_mut());
let ptr = gdk_cairo_surface_create_from_pixbuf(
pixbuf.as_ptr(),
image.scale_factor(),
std::ptr::null_mut(),
);
Surface::from_raw_full(ptr)
}?;

View file

@ -181,6 +181,13 @@ macro_rules! arc_rw {
};
}
/// Wraps `val` in a new `Rc<RefCell<T>>`.
///
/// # Usage
///
/// ```rs
/// let val = rc_mut!(MyService::new())
/// ```
#[macro_export]
macro_rules! rc_mut {
($val:expr) => {

View file

@ -1,9 +1,9 @@
use system_tray::message::menu::{MenuItem as MenuItemInfo, ToggleState};
use system_tray::menu::{MenuItem, ToggleState};
/// Diff change type and associated info.
#[derive(Debug, Clone)]
pub enum Diff {
Add(MenuItemInfo),
Add(MenuItem),
Update(i32, MenuItemDiff),
Remove(i32),
}
@ -12,7 +12,7 @@ pub enum Diff {
#[derive(Debug, Clone)]
pub struct MenuItemDiff {
/// Text of the item,
pub label: Option<String>,
pub label: Option<Option<String>>,
/// Whether the item can be activated or not.
pub enabled: Option<bool>,
/// True if the item is visible in the menu.
@ -29,7 +29,7 @@ pub struct MenuItemDiff {
}
impl MenuItemDiff {
fn new(old: &MenuItemInfo, new: &MenuItemInfo) -> Self {
fn new(old: &MenuItem, new: &MenuItem) -> Self {
macro_rules! diff {
($field:ident) => {
if old.$field == new.$field {
@ -70,7 +70,7 @@ impl MenuItemDiff {
}
/// Gets a diff set between old and new state.
pub fn get_diffs(old: &[MenuItemInfo], new: &[MenuItemInfo]) -> Vec<Diff> {
pub fn get_diffs(old: &[MenuItem], new: &[MenuItem]) -> Vec<Diff> {
let mut diffs = vec![];
for new_item in new {

View file

@ -1,14 +1,16 @@
use crate::image::ImageProvider;
use crate::modules::tray::interface::TrayMenu;
use color_eyre::{Report, Result};
use glib::ffi::g_strfreev;
use glib::translate::ToGlibPtr;
use gtk::ffi::gtk_icon_theme_get_search_path;
use gtk::gdk_pixbuf::{Colorspace, InterpType};
use gtk::gdk_pixbuf::{Colorspace, InterpType, Pixbuf};
use gtk::prelude::IconThemeExt;
use gtk::{gdk_pixbuf, IconLookupFlags, IconTheme, Image};
use gtk::{IconLookupFlags, IconTheme, Image};
use std::collections::HashSet;
use std::ffi::CStr;
use std::os::raw::{c_char, c_int};
use std::ptr;
use system_tray::message::tray::StatusNotifierItem;
/// Gets the GTK icon theme search paths by calling the FFI function.
/// Conveniently returns the result as a `HashSet`.
@ -36,40 +38,60 @@ fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
paths
}
pub fn get_image(
item: &TrayMenu,
icon_theme: &IconTheme,
size: u32,
prefer_icons: bool,
) -> Result<Image> {
if !prefer_icons && item.icon_pixmap.is_some() {
get_image_from_pixmap(item, size)
} else {
get_image_from_icon_name(item, icon_theme, size)
.or_else(|_| get_image_from_pixmap(item, size))
}
}
/// Attempts to get a GTK `Image` component
/// for the status notifier item's icon.
pub(crate) fn get_image_from_icon_name(
item: &StatusNotifierItem,
icon_theme: &IconTheme,
) -> Option<Image> {
fn get_image_from_icon_name(item: &TrayMenu, icon_theme: &IconTheme, size: u32) -> Result<Image> {
if let Some(path) = item.icon_theme_path.as_ref() {
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) {
icon_theme.append_search_path(path);
}
}
item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
})
let icon_info = item.icon_name.as_ref().and_then(|icon_name| {
icon_theme.lookup_icon(icon_name, size as i32, IconLookupFlags::empty())
});
if let Some(icon_info) = icon_info {
let pixbuf = icon_info.load_icon()?;
let image = Image::new();
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
Ok(image)
} else {
Err(Report::msg("could not find icon"))
}
}
/// Attempts to get an image from the item pixmap.
///
/// The pixmap is supplied in ARGB32 format,
/// which has 8 bits per sample and a bit stride of `4*width`.
pub(crate) fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image> {
fn get_image_from_pixmap(item: &TrayMenu, size: u32) -> Result<Image> {
const BITS_PER_SAMPLE: i32 = 8;
let pixmap = item
.icon_pixmap
.as_ref()
.and_then(|pixmap| pixmap.first())?;
.and_then(|pixmap| pixmap.first())
.ok_or_else(|| Report::msg("Failed to get pixmap from tray icon"))?;
let bytes = glib::Bytes::from(&pixmap.pixels);
let row_stride = pixmap.width * 4; //
let row_stride = pixmap.width * 4;
let pixbuf = gdk_pixbuf::Pixbuf::from_bytes(
let pixbuf = Pixbuf::from_bytes(
&bytes,
Colorspace::Rgb,
true,
@ -80,7 +102,10 @@ pub(crate) fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image>
);
let pixbuf = pixbuf
.scale_simple(16, 16, InterpType::Bilinear)
.scale_simple(size as i32, size as i32, InterpType::Bilinear)
.unwrap_or(pixbuf);
Some(Image::from_pixbuf(Some(&pixbuf)))
let image = Image::new();
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
Ok(image)
}

View file

@ -1,10 +1,12 @@
use crate::modules::tray::diff::{Diff, MenuItemDiff};
use super::diff::{Diff, MenuItemDiff};
use crate::{spawn, try_send};
use glib::Propagation;
use gtk::prelude::*;
use gtk::{CheckMenuItem, Image, Label, Menu, MenuItem, SeparatorMenuItem};
use std::collections::HashMap;
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType};
use system_tray::message::NotifierItemCommand;
use system_tray::client::ActivateRequest;
use system_tray::item::{IconPixmap, StatusNotifierItem};
use system_tray::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType};
use tokio::sync::mpsc;
/// Calls a method on the underlying widget,
@ -49,37 +51,47 @@ macro_rules! call {
/// Main tray icon to show on the bar
pub(crate) struct TrayMenu {
pub(crate) widget: MenuItem,
pub widget: MenuItem,
menu_widget: Menu,
image_widget: Option<Image>,
label_widget: Option<Label>,
menu: HashMap<i32, TrayMenuItem>,
state: Vec<MenuItemInfo>,
icon_name: Option<String>,
pub title: Option<String>,
pub icon_name: Option<String>,
pub icon_theme_path: Option<String>,
pub icon_pixmap: Option<Vec<IconPixmap>>,
tx: mpsc::Sender<i32>,
}
impl TrayMenu {
pub fn new(tx: mpsc::Sender<NotifierItemCommand>, address: String, path: String) -> Self {
pub fn new(
tx: mpsc::Sender<ActivateRequest>,
address: String,
item: StatusNotifierItem,
) -> Self {
let widget = MenuItem::new();
widget.style_context().add_class("item");
let (item_tx, mut item_rx) = mpsc::channel(8);
spawn(async move {
while let Some(id) = item_rx.recv().await {
try_send!(
tx,
NotifierItemCommand::MenuItemClicked {
submenu_id: id,
menu_path: path.clone(),
notifier_address: address.clone(),
}
);
}
});
if let Some(menu) = item.menu {
spawn(async move {
while let Some(id) = item_rx.recv().await {
try_send!(
tx,
ActivateRequest {
submenu_id: id,
menu_path: menu.clone(),
address: address.clone(),
}
);
}
});
}
let menu = Menu::new();
widget.set_submenu(Some(&menu));
@ -90,7 +102,10 @@ impl TrayMenu {
image_widget: None,
label_widget: None,
state: vec![],
icon_name: None,
title: item.title,
icon_name: item.icon_name,
icon_theme_path: item.icon_theme_path,
icon_pixmap: item.icon_pixmap,
menu: HashMap::new(),
tx: item_tx,
}
@ -112,6 +127,18 @@ impl TrayMenu {
.set_label(text);
}
/// Shows the label, using its current text.
/// The image is hidden if present.
pub fn show_label(&self) {
if let Some(image) = &self.image_widget {
image.hide();
}
if let Some(label) = &self.label_widget {
label.show();
}
}
/// Updates the image, and shows it in favour of the label.
pub fn set_image(&mut self, image: &Image) {
if let Some(label) = &self.label_widget {
@ -134,6 +161,7 @@ impl TrayMenu {
let item = TrayMenuItem::new(&info, self.tx.clone());
call!(self.menu_widget, add, item.widget);
self.menu.insert(item.id, item);
// self.widget.show_all();
}
Diff::Update(id, info) => {
if let Some(item) = self.menu.get_mut(&id) {
@ -188,36 +216,61 @@ enum TrayMenuWidget {
impl TrayMenuItem {
fn new(info: &MenuItemInfo, tx: mpsc::Sender<i32>) -> Self {
let mut submenu = HashMap::new();
let menu = Menu::new();
macro_rules! add_submenu {
($menu:expr, $widget:expr) => {
if !info.submenu.is_empty() {
for sub_item in &info.submenu {
let sub_item = TrayMenuItem::new(sub_item, tx.clone());
call!($menu, add, sub_item.widget);
submenu.insert(sub_item.id, sub_item);
}
$widget.set_submenu(Some(&menu));
}
};
}
let widget = match (info.menu_type, info.toggle_type) {
(MenuType::Separator, _) => TrayMenuWidget::Separator(SeparatorMenuItem::new()),
(MenuType::Standard, ToggleType::Checkmark) => {
let widget = CheckMenuItem::builder()
.label(info.label.as_str())
.visible(info.visible)
.sensitive(info.enabled)
.active(info.toggle_state == ToggleState::On)
.build();
if let Some(label) = &info.label {
widget.set_label(label);
}
add_submenu!(menu, widget);
{
let tx = tx.clone();
let id = info.id;
widget.connect_activate(move |_item| {
widget.connect_button_press_event(move |_item, _button| {
try_send!(tx, id);
Propagation::Proceed
});
}
TrayMenuWidget::Checkbox(widget)
}
(MenuType::Standard, _) => {
let builder = MenuItem::builder()
.label(&info.label)
let widget = MenuItem::builder()
.visible(info.visible)
.sensitive(info.enabled);
.sensitive(info.enabled)
.build();
let widget = builder.build();
if let Some(label) = &info.label {
widget.set_label(label);
}
add_submenu!(menu, widget);
{
let tx = tx.clone();
@ -236,7 +289,7 @@ impl TrayMenuItem {
id: info.id,
widget,
menu_widget: menu,
submenu: HashMap::new(),
submenu,
tx,
}
}
@ -247,6 +300,7 @@ impl TrayMenuItem {
/// applying the submenu diffs to any further submenu items.
fn apply_diff(&mut self, diff: MenuItemDiff) {
if let Some(label) = diff.label {
let label = label.unwrap_or_default();
match &self.widget {
TrayMenuWidget::Separator(widget) => widget.set_label(&label),
TrayMenuWidget::Standard(widget) => widget.set_label(&label),

View file

@ -2,28 +2,41 @@ mod diff;
mod icon;
mod interface;
use crate::clients::system_tray::TrayEventReceiver;
use crate::clients::tray;
use crate::config::CommonConfig;
use crate::modules::tray::diff::get_diffs;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, spawn};
use color_eyre::Result;
use crate::{glib_recv, lock, send_async, spawn};
use color_eyre::{Report, Result};
use gtk::{prelude::*, PackDirection};
use gtk::{IconTheme, MenuBar};
use interface::TrayMenu;
use serde::Deserialize;
use std::collections::HashMap;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
use system_tray::client::Event;
use system_tray::client::{ActivateRequest, UpdateEvent};
use tokio::sync::mpsc;
use tracing::{debug, error, warn};
#[derive(Debug, Deserialize, Clone)]
pub struct TrayModule {
#[serde(default = "crate::config::default_true")]
prefer_theme_icons: bool,
#[serde(default = "default_icon_size")]
icon_size: u32,
#[serde(default, deserialize_with = "deserialize_orientation")]
pub direction: Option<PackDirection>,
direction: Option<PackDirection>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
const fn default_icon_size() -> u32 {
16
}
fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
where
D: serde::Deserializer<'de>,
@ -41,8 +54,8 @@ where
}
impl Module<MenuBar> for TrayModule {
type SendMessage = NotifierItemMessage;
type ReceiveMessage = NotifierItemCommand;
type SendMessage = Event;
type ReceiveMessage = ActivateRequest;
fn name() -> &'static str {
"tray"
@ -56,26 +69,39 @@ impl Module<MenuBar> for TrayModule {
) -> Result<()> {
let tx = context.tx.clone();
let client = context.client::<TrayEventReceiver>();
let client = context.client::<tray::Client>();
let mut tray_rx = client.subscribe();
let (tray_tx, mut tray_rx) = client.subscribe();
let initial_items = lock!(client.items()).clone();
// listen to tray updates
spawn(async move {
while let Ok(message) = tray_rx.recv().await {
tx.send(ModuleUpdateEvent::Update(message)).await?;
for (key, (item, menu)) in initial_items.into_iter() {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::Add(key.clone(), item.into()))
);
if let Some(menu) = menu.clone() {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::Update(key, UpdateEvent::Menu(menu)))
);
}
}
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
while let Ok(message) = tray_rx.recv().await {
send_async!(tx, ModuleUpdateEvent::Update(message))
}
});
// send tray commands
spawn(async move {
while let Some(cmd) = rx.recv().await {
tray_tx.send(cmd).await?;
client.activate(cmd).await?;
}
Ok::<(), mpsc::error::SendError<NotifierItemCommand>>(())
Ok::<_, Report>(())
});
Ok(())
@ -106,7 +132,7 @@ impl Module<MenuBar> for TrayModule {
// listen for UI updates
glib_recv!(context.subscribe(), update =>
on_update(update, &container, &mut menus, &icon_theme, &context.controller_tx)
on_update(update, &container, &mut menus, &icon_theme, self.icon_size, self.prefer_theme_icons, &context.controller_tx)
);
};
@ -120,53 +146,81 @@ impl Module<MenuBar> for TrayModule {
/// Handles UI updates as callback,
/// getting the diff since the previous update and applying it to the menu.
fn on_update(
update: NotifierItemMessage,
update: Event,
container: &MenuBar,
menus: &mut HashMap<Box<str>, TrayMenu>,
icon_theme: &IconTheme,
tx: &mpsc::Sender<NotifierItemCommand>,
icon_size: u32,
prefer_icons: bool,
tx: &mpsc::Sender<ActivateRequest>,
) {
match update {
NotifierItemMessage::Update {
item,
address,
menu,
} => {
if let (Some(menu_opts), Some(menu_path)) = (menu, &item.menu) {
let submenus = menu_opts.submenus;
Event::Add(address, item) => {
debug!("Received new tray item at '{address}': {item:?}");
let mut menu_item = menus.remove(address.as_str()).unwrap_or_else(|| {
let item = TrayMenu::new(tx.clone(), address.clone(), menu_path.to_string());
container.add(&item.widget);
let mut menu_item = TrayMenu::new(tx.clone(), address.clone(), *item);
container.add(&menu_item.widget);
item
});
let label = item.title.as_ref().unwrap_or(&address);
if let Some(label_widget) = menu_item.label_widget() {
label_widget.set_label(label);
match icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
Ok(image) => menu_item.set_image(&image),
Err(_) => {
let label = menu_item.title.clone().unwrap_or(address.clone());
menu_item.set_label(&label)
}
};
if item.icon_name.as_ref() != menu_item.icon_name() {
match icon::get_image_from_icon_name(&item, icon_theme)
.or_else(|| icon::get_image_from_pixmap(&item))
{
Some(image) => menu_item.set_image(&image),
None => menu_item.set_label(label),
};
menu_item.widget.show();
menus.insert(address.into(), menu_item);
}
Event::Update(address, update) => {
debug!("Received tray update for '{address}': {update:?}");
let Some(menu_item) = menus.get_mut(address.as_str()) else {
error!("Attempted to update menu at '{address}' but could not find it");
return;
};
match update {
UpdateEvent::AttentionIcon(_icon) => {
warn!("received unimplemented NewAttentionIcon event");
}
UpdateEvent::Icon(icon) => {
if icon.as_ref() != menu_item.icon_name() {
match icon::get_image(menu_item, icon_theme, icon_size, prefer_icons) {
Ok(image) => menu_item.set_image(&image),
Err(_) => menu_item.show_label(),
};
}
let diffs = get_diffs(menu_item.state(), &submenus);
menu_item.apply_diffs(diffs);
menu_item.widget.show();
menu_item.set_icon_name(icon);
}
UpdateEvent::OverlayIcon(_icon) => {
warn!("received unimplemented NewOverlayIcon event");
}
UpdateEvent::Status(_status) => {
warn!("received unimplemented NewStatus event");
}
UpdateEvent::Title(title) => {
if let Some(label_widget) = menu_item.label_widget() {
label_widget.set_label(&title.unwrap_or_default());
}
}
// UpdateEvent::Tooltip(_tooltip) => {
// warn!("received unimplemented NewAttentionIcon event");
// }
UpdateEvent::Menu(menu) => {
debug!("received new menu for '{}'", address);
menu_item.set_state(submenus);
menu_item.set_icon_name(item.icon_name);
let diffs = get_diffs(menu_item.state(), &menu.submenus);
menus.insert(address.into(), menu_item);
menu_item.apply_diffs(diffs);
menu_item.set_state(menu.submenus);
}
}
}
NotifierItemMessage::Remove { address } => {
Event::Remove(address) => {
debug!("Removing tray item at '{address}'");
if let Some(menu) = menus.get(address.as_str()) {
container.remove(&menu.widget);
}

View file

@ -199,7 +199,9 @@ impl Module<gtk::Button> for UpowerModule {
let format = format.replace("{percentage}", &properties.percentage.to_string())
.replace("{time_remaining}", &time_remaining)
.replace("{state}", battery_state_to_string(state));
let icon_name = String::from("icon:") + &properties.icon_name;
let mut icon_name = String::from("icon:");
icon_name.push_str(&properties.icon_name);
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone()));