diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 810c075..b02903c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -9,18 +9,24 @@ on:
env:
CARGO_TERM_COLOR: always
+ RUSTFLAGS: '-Dwarnings'
jobs:
- build:
+ rustfmt:
runs-on: ubuntu-latest
-
+ name: 'Formatting'
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: actions-rs/toolchain@v1
- with:
- toolchain: stable
- override: true
+ - name: Check formatting
+ run: cargo fmt --check
+
+
+ clippy-base:
+ runs-on: ubuntu-latest
+ name: 'Clippy (Base features)'
+ steps:
+ - uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
@@ -28,22 +34,47 @@ jobs:
- name: Install build deps
run: |
sudo apt-get update
- sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
+ sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
- - name: Check formatting
- run: cargo fmt --check
+ - name: Clippy
+ run: cargo clippy --no-default-features --features config+json
+ env:
+ # Allow some warnings through as we'll never get it perfect in a zero-feature situation
+ RUSTFLAGS: '-A unused-imports -A unused-variables -A unused-mut -A dead-code'
- - name: Clippy (base features)
- uses: actions-rs/clippy-check@v1
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- args: --no-default-features --features config+json
- - name: Clippy (all features)
- uses: actions-rs/clippy-check@v1
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- args: --all-features
+ clippy-all:
+ runs-on: ubuntu-latest
+ name: 'Clippy (All features)'
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: Swatinem/rust-cache@v2
+ 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
+
+ - name: Clippy
+ run: cargo clippy --all-targets --all-features
+
+
+ build:
+ name: 'Build & Test'
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: Swatinem/rust-cache@v2
+ 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
- name: Build
run: cargo build --verbose
@@ -51,28 +82,4 @@ jobs:
- name: Run tests
uses: actions-rs/cargo@v1
with:
- command: test
-
-
-
- build-nix:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - uses: cachix/install-nix-action@v20
- with:
- install_url: https://nixos.org/nix/install
- extra_nix_config: |
- auto-optimise-store = true
- experimental-features = nix-command flakes
-
- - uses: cachix/cachix-action@v12
- with:
- name: jakestanger
- signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
-
- - uses: DeterminateSystems/magic-nix-cache-action@main
-
- - run: nix build --print-build-logs
\ No newline at end of file
+ command: test
\ No newline at end of file
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 117ba12..2d6e37b 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -20,7 +20,7 @@ jobs:
- name: Install build deps
run: |
sudo apt-get update
- sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
+ sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
- name: Update CHANGELOG
id: changelog
diff --git a/Cargo.lock b/Cargo.lock
index 3628363..61e928e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -168,7 +168,7 @@ dependencies = [
"log",
"parking",
"polling 2.7.0",
- "rustix 0.37.11",
+ "rustix 0.37.27",
"slab",
"socket2 0.4.9",
"waker-fn",
@@ -206,7 +206,7 @@ dependencies = [
"cfg-if",
"event-listener",
"futures-lite 1.13.0",
- "rustix 0.37.11",
+ "rustix 0.37.27",
"signal-hook",
"windows-sys 0.48.0",
]
@@ -429,9 +429,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.34"
+version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
+checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -444,9 +444,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.0"
+version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
+checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651"
dependencies = [
"clap_builder",
"clap_derive",
@@ -454,9 +454,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.0"
+version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
@@ -1352,9 +1352,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.3.17"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f"
+checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [
"bytes",
"fnv",
@@ -1362,7 +1362,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
- "indexmap 1.9.3",
+ "indexmap 2.2.5",
"slab",
"tokio",
"tokio-util",
@@ -1579,9 +1579,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.2.3"
+version = "2.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
+checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [
"equivalent",
"hashbrown 0.14.1",
@@ -1649,7 +1649,8 @@ dependencies = [
"gtk",
"gtk-layer-shell",
"hyprland",
- "indexmap 2.2.3",
+ "indexmap 2.2.5",
+ "libpulse-binding",
"mpd-utils",
"mpris",
"nix 0.27.1",
@@ -1756,6 +1757,33 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "libpulse-binding"
+version = "2.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+ "libpulse-sys",
+ "num-derive",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "libpulse-sys"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b"
+dependencies = [
+ "libc",
+ "num-derive",
+ "num-traits",
+ "pkg-config",
+ "winapi",
+]
+
[[package]]
name = "link-cplusplus"
version = "1.0.8"
@@ -1767,9 +1795,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
-version = "0.3.1"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "linux-raw-sys"
@@ -1861,9 +1889,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "0.8.9"
+version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
@@ -2009,6 +2037,17 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.35",
+ "syn 1.0.109",
+]
+
[[package]]
name = "num-traits"
version = "0.2.15"
@@ -2045,11 +2084,11 @@ checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "openssl"
-version = "0.10.50"
+version = "0.10.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e30d8bc91859781f0a943411186324d580f2bbeb71b452fe91ae344806af3f1"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.4.0",
"cfg-if",
"foreign-types",
"libc",
@@ -2077,9 +2116,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
-version = "0.9.85"
+version = "0.9.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d3d193fb1488ad46ffe3aaabc912cc931d02ee8518fe2959aea8ef52718b0c0"
+checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
dependencies = [
"cc",
"libc",
@@ -2489,9 +2528,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "reqwest"
-version = "0.11.24"
+version = "0.11.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
+checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946"
dependencies = [
"base64 0.21.0",
"bytes",
@@ -2555,15 +2594,15 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.37.11"
+version = "0.37.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77"
+checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
- "linux-raw-sys 0.3.1",
+ "linux-raw-sys 0.3.8",
"windows-sys 0.48.0",
]
@@ -2668,18 +2707,18 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
[[package]]
name = "serde"
-version = "1.0.196"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.196"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote 1.0.35",
@@ -2688,9 +2727,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.113"
+version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
@@ -2991,20 +3030,20 @@ dependencies = [
[[package]]
name = "system-configuration"
-version = "0.5.1"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.4.0",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
-version = "0.5.0"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
@@ -3050,7 +3089,7 @@ dependencies = [
"cfg-if",
"fastrand 1.9.0",
"redox_syscall 0.3.5",
- "rustix 0.37.11",
+ "rustix 0.37.27",
"windows-sys 0.45.0",
]
@@ -3228,7 +3267,7 @@ version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
- "indexmap 2.2.3",
+ "indexmap 2.2.5",
"serde",
"serde_spanned",
"toml_datetime",
@@ -3241,7 +3280,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
- "indexmap 2.2.3",
+ "indexmap 2.2.5",
"toml_datetime",
"winnow",
]
@@ -3421,9 +3460,9 @@ dependencies = [
[[package]]
name = "unsafe-libyaml"
-version = "0.2.8"
+version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6"
+checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
[[package]]
name = "upower_dbus"
@@ -3505,9 +3544,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
-version = "2.4.0"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
@@ -3999,9 +4038,9 @@ checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621"
[[package]]
name = "zbus"
-version = "3.15.0"
+version = "3.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c45d06ae3b0f9ba1fb2671268b975557d8f5a84bb5ec6e43964f87e763d8bca8"
+checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6"
dependencies = [
"async-broadcast",
"async-executor",
@@ -4041,9 +4080,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
-version = "3.15.0"
+version = "3.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4a1ba45ed0ad344b85a2bb5a1fe9830aed23d67812ea39a586e7d0136439c7d"
+checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
diff --git a/Cargo.toml b/Cargo.toml
index b9e170f..85f5c09 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,9 +20,11 @@ default = [
"launcher",
"music+all",
"networkmanager",
+ "notifications",
"sys_info",
"tray",
"upower",
+ "volume",
"workspaces+all"
]
@@ -59,12 +61,16 @@ music = ["regex"]
networkmanager = ["futures-lite", "zbus"]
+notifications = ["zbus"]
+
sys_info = ["sysinfo", "regex"]
tray = ["system-tray"]
upower = ["upower_dbus", "zbus", "futures-lite"]
+volume = ["libpulse-binding"]
+
workspaces = ["futures-util"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+sway" = ["workspaces", "swayipc-async"]
@@ -90,10 +96,10 @@ tracing-error = "0.2.0"
tracing-appender = "0.2.3"
strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.2"
-serde = { version = "1.0.196", features = ["derive"] }
-indexmap = "2.2.3"
+serde = { version = "1.0.197", features = ["derive"] }
+indexmap = "2.2.5"
dirs = "5.0.1"
-walkdir = "2.4.0"
+walkdir = "2.5.0"
notify = { version = "6.1.1", default-features = false }
wayland-client = "0.31.1"
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] }
@@ -106,19 +112,19 @@ ctrlc = "3.4.2"
cfg-if = "1.0.0"
# cli
-clap = { version = "4.5.0", optional = true, features = ["derive"] }
+clap = { version = "4.5.2", optional = true, features = ["derive"] }
# ipc
-serde_json = { version = "1.0.113", optional = true }
+serde_json = { version = "1.0.114", optional = true }
# http
-reqwest = { version = "0.11.24", optional = true }
+reqwest = { version = "0.11.25", optional = true }
# clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] }
# clock
-chrono = { version = "0.4.34", optional = true, features = ["unstable-locales"] }
+chrono = { version = "0.4.35", optional = true, features = ["unstable-locales"] }
# music
mpd-utils = { version = "0.2.0", optional = true }
@@ -133,6 +139,10 @@ system-tray = { version = "0.1.5", 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 }
hyprland = { version = "0.3.13", features = ["silent"], optional = true }
@@ -143,4 +153,4 @@ regex = { version = "1.10.3", default-features = false, features = [
"std",
], optional = true } # music, sys_info
futures-lite = { version = "2.2.0", optional = true } # networkmanager, upower
-zbus = { version = "3.15.0", optional = true } # networkmanager, upower
\ No newline at end of file
+zbus = { version = "3.15.2", optional = true } # networkmanager, notifications, upower
diff --git a/README.md b/README.md
index 08a34d8..0db0282 100644
--- a/README.md
+++ b/README.md
@@ -127,7 +127,8 @@ A flake is included with the repo which can be used with Home Manager.
-There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`.
+CI builds are automatically cached by Garnix.
+You can use their binary cache by following the steps [here](https://garnix.io/docs/caching).
### Source
@@ -182,3 +183,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.
\ No newline at end of file
diff --git a/docs/Compiling.md b/docs/Compiling.md
index 236b295..30c69df 100644
--- a/docs/Compiling.md
+++ b/docs/Compiling.md
@@ -20,6 +20,8 @@ You also need rust; only the latest stable version is supported.
pacman -S gtk3 gtk-layer-shell
# for http support
pacman -S openssl
+# for volume support
+pacman -S libpulse
```
### Ubuntu/Debian
@@ -28,6 +30,8 @@ pacman -S openssl
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
# for http support
apt install libssl-dev
+# for volume support
+apt install libpulse-dev
```
### Fedora
@@ -36,6 +40,8 @@ apt install libssl-dev
dnf install gtk3-devel gtk-layer-shell-devel
# for http support
dnf install openssl-devel
+# for volume support
+dnf install libpulseaudio-devel
```
## Features
@@ -81,6 +87,7 @@ cargo build --release --no-default-features \
| sys_info | Enables the `sys_info` module. |
| tray | Enables the `tray` module. |
| upower | Enables the `upower` module. |
+| volume | Enables the `volume` module. |
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md
index 2553fe8..766da7e 100644
--- a/docs/_Sidebar.md
+++ b/docs/_Sidebar.md
@@ -30,8 +30,10 @@
- [Label](label)
- [Launcher](launcher)
- [Music](music)
+- [Notifications](notifications)
- [Script](script)
- [Sys_Info](sys-info)
- [Tray](tray)
- [Upower](upower)
+- [Volume](volume)
- [Workspaces](workspaces)
diff --git a/docs/modules/Notifications.md b/docs/modules/Notifications.md
new file mode 100644
index 0000000..8a8608c
--- /dev/null
+++ b/docs/modules/Notifications.md
@@ -0,0 +1,116 @@
+Displays information about the current SwayNC state such as notification count and DnD.
+Clicking the widget opens the SwayNC panel.
+
+
+
+> [!NOTE]
+> This widget requires the [SwayNC](https://github.com/ErikReider/SwayNotificationCenter)
+> daemon to be running to use.
+
+## Configuration
+
+> Type: `notifications`
+
+| Name | Type | Default | Description |
+|---------------------|-----------|---------|--------------------------------------------------------------------------------------------------------|
+| `show_count` | `boolean` | `true` | Whether to show the current notification count. |
+| `icons.closed_none` | `string` | `` | Icon to show when the panel is closed, with no notifications. |
+| `icons.closed_some` | `string` | `` | Icon to show when the panel is closed, with notifications. |
+| `icons.closed_dnd` | `string` | `` | Icon to show when the panel is closed, with DnD enabled. Takes higher priority than count-based icons. |
+| `icons.open_none` | `string` | `` | Icon to show when the panel is open, with no notifications. |
+| `icons.open_some` | `string` | `` | Icon to show when the panel is open, with notifications. |
+| `icons.open_dnd` | `string` | `` | Icon to show when the panel is open, with DnD enabled. Takes higher priority than count-based icons. |
+
+
+
+JSON
+
+```json
+{
+ "end": [
+ {
+ "type": "notifications",
+ "show_count": true,
+ "icons": {
+ "closed_none": "",
+ "closed_some": "",
+ "closed_dnd": "",
+ "open_none": "",
+ "open_some": "",
+ "open_dnd": ""
+ }
+ }
+ ]
+}
+```
+
+
+
+
+TOML
+
+```toml
+[[end]]
+type = "notifications"
+show_count = true
+
+[[end.icons]]
+closed_none = ""
+closed_some = ""
+closed_dnd = ""
+open_none = ""
+open_some = ""
+open_dnd = ""
+```
+
+
+
+
+YAML
+
+```yaml
+end:
+ - type: notifications
+ show_count: true
+ icons:
+ closed_none:
+ closed_some:
+ closed_dnd:
+ open_none:
+ open_some:
+ open_dnd:
+```
+
+
+
+
+Corn
+
+```corn
+{
+ end = [
+ {
+ type = "notifications"
+ show_count = true
+
+ icons.closed_none = ""
+ icons.closed_some = ""
+ icons.closed_dnd = ""
+ icons.open_none = ""
+ icons.open_some = ""
+ icons.open_dnd = ""
+ }
+ ]
+}
+```
+
+
+
+## Styling
+
+| Selector | Description |
+|-------------------------|---------------------------------------|
+| `.notifications` | Notifications widget button |
+| `.notifications .count` | Notifications count indicator overlay |
+
+For more information on styling, please see the [styling guide](styling-guide).
\ No newline at end of file
diff --git a/docs/modules/Tray.md b/docs/modules/Tray.md
index 7115b75..a80c48e 100644
--- a/docs/modules/Tray.md
+++ b/docs/modules/Tray.md
@@ -6,7 +6,10 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
> Type: `tray`
-***This module provides no configuration options.***
+
+| 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` |
JSON
@@ -15,7 +18,8 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
{
"end": [
{
- "type": "tray"
+ "type": "tray",
+ "direction": "top_to_bottom"
}
]
}
@@ -29,6 +33,7 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
```toml
[[end]]
type = "tray"
+direction = "top_to_bottom"
```
@@ -39,6 +44,7 @@ type = "tray"
```yaml
end:
- type: "tray"
+ direction: "top_to_bottom"
```
@@ -49,7 +55,10 @@ end:
```corn
{
end = [
- { type = "tray" }
+ {
+ type = "tray"
+ direction = "top_to_bottom"
+ }
]
}
```
@@ -63,4 +72,4 @@ end:
| `.tray` | Tray widget box |
| `.tray .item` | Tray icon button |
-For more information on styling, please see the [styling guide](styling-guide).
\ No newline at end of file
+For more information on styling, please see the [styling guide](styling-guide).
diff --git a/docs/modules/Volume.md b/docs/modules/Volume.md
new file mode 100644
index 0000000..bfa4653
--- /dev/null
+++ b/docs/modules/Volume.md
@@ -0,0 +1,128 @@
+Displays the current volume level.
+Clicking on the widget opens a volume mixer, which allows you to change the device output level,
+the default playback device, and control application volume levels individually.
+
+This requires PulseAudio to function (`pipewire-pulse` is supported).
+
+
+
+## Configuration
+
+> Type: `volume`
+
+| Name | Type | Default | Description |
+|-----------------------|----------|------------------------|----------------------------------------------------------------------------------------------------------------|
+| `format` | `string` | `{icon} {percentage}%` | Format string to use for the widget button label. |
+| `max_volume` | `float` | `100` | Maximum value to allow volume sliders to reach. Pulse supports values > 100 but this may result in distortion. |
+| `icons.volume_high` | `string` | `` | Icon to show for high volume levels. |
+| `icons.volume_medium` | `string` | `` | Icon to show for medium volume levels. |
+| `icons.volume_low` | `string` | `` | Icon to show for low volume levels. |
+| `icons.muted` | `string` | `` | Icon to show for muted outputs. |
+
+
+JSON
+
+```json
+{
+ "end": [
+ {
+ "type": "volume",
+ "format": "{icon} {percentage}%",
+ "max_volume": 100,
+ "icons": {
+ "volume_high": "",
+ "volume_medium": "",
+ "volume_low": "",
+ "muted": ""
+ }
+ }
+ ]
+}
+
+```
+
+
+
+
+TOML
+
+```toml
+[[end]]
+type = "volume"
+format = "{icon} {percentage}%"
+max_volume = 100
+
+[[end.icons]]
+volume_high = ""
+volume_medium = ""
+volume_low = ""
+muted = ""
+```
+
+
+
+
+YAML
+
+```yaml
+end:
+ - type: "volume"
+ format: "{icon} {percentage}%"
+ max_volume: 100
+ icons:
+ volume_high: ""
+ volume_medium: ""
+ volume_low: ""
+ muted: ""
+```
+
+
+
+
+Corn
+
+```corn
+{
+ end = [
+ {
+ type = "volume"
+ format = "{icon} {percentage}%"
+ max_volume = 100
+ icons.volume_high = ""
+ icons.volume_medium = ""
+ icons.volume_low = ""
+ icons.muted = ""
+ }
+ ]
+}
+```
+
+
+
+### Formatting Tokens
+
+The following tokens can be used in the `format` config option:
+
+| Token | Description |
+|----------------|-------------------------------------------|
+| `{percentage}` | The active device volume percentage. |
+| `{icon}` | The icon representing the current volume. |
+| `{name}` | The active device name. |
+
+## Styling
+
+| Selector | Description |
+|----------------------------------------------|----------------------------------------------------|
+| `.volume` | Volume widget button. |
+| `.popup-volume` | Volume popup box. |
+| `.popup-volume .device-box` | Box for the device volume controls. |
+| `.popup-volume .device-box .device-selector` | Default device dropdown selector. |
+| `.popup-volume .device-box .slider` | Device volume slider. |
+| `.popup-volume .device-box .btn-mute` | Device volume mute toggle button. |
+| `.popup-volume .apps-box` | Parent box for the application volume controls. |
+| `.popup-volume .apps-box .app-box` | Box for an individual application volume controls. |
+| `.popup-volume .apps-box .app-box .title` | Name of the application playback stream. |
+| `.popup-volume .apps-box .app-box .slider` | Application volume slider. |
+| `.popup-volume .apps-box .app-box .btn-mute` | Application volume mute toggle button. |
+
+For more information on styling, please see the [styling guide](styling-guide).
\ No newline at end of file
diff --git a/examples/config.corn b/examples/config.corn
index 4cc47da..69b3d5f 100644
--- a/examples/config.corn
+++ b/examples/config.corn
@@ -33,6 +33,18 @@ let {
$mpd_local = { type = "music" player_type = "mpd" music_dir = "/home/jake/Music" truncate.mode = "end" truncate.max_length = 100 }
$mpd_server = { type = "music" player_type = "mpd" host = "chloe:6600" truncate = "end" }
+ $notifications = {
+ type = "notifications"
+ show_count = true
+
+ icons.closed_none = ""
+ icons.closed_some = ""
+ icons.closed_dnd = ""
+ icons.open_none = ""
+ icons.open_some = ""
+ icons.open_dnd = ""
+ }
+
$sys_info = {
type = "sys_info"
@@ -67,6 +79,16 @@ let {
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
+ $volume = {
+ type = "volume"
+ format = "{icon} {volume}%"
+ max_volume = 100
+ icons.volume_high = ""
+ icons.volume_medium = ""
+ icons.volume_low = ""
+ icons.muted = ""
+ }
+
$label = { type = "label" label = "random num: {{500:echo FIXME}}" }
// -- begin custom --
@@ -100,7 +122,7 @@ let {
// -- end custom --
$left = [ $workspaces $launcher $label ]
- $right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ]
+ $right = [ $mpd_local $mpd_server $phone_battery $sys_info $volume $clipboard $power_menu $clock $notifications ]
}
in {
anchor_to_edges = true
diff --git a/examples/config.json b/examples/config.json
index 028b653..87b788b 100644
--- a/examples/config.json
+++ b/examples/config.json
@@ -1,30 +1,67 @@
{
"anchor_to_edges": true,
+ "position": "bottom",
+ "icon_theme": "Paper",
+ "start": [
+ {
+ "type": "workspaces",
+ "all_monitors": false,
+ "name_map": {
+ "1": "",
+ "2": "icon:firefox",
+ "3": "",
+ "Games": "icon:steam",
+ "Code": ""
+ }
+ },
+ {
+ "type": "launcher",
+ "favorites": [
+ "firefox",
+ "discord",
+ "steam"
+ ],
+ "show_names": false,
+ "show_icons": true
+ },
+ {
+ "type": "label",
+ "label": "random num: {{500:echo FIXME}}"
+ }
+ ],
"end": [
{
+ "type": "music",
+ "player_type": "mpd",
"music_dir": "/home/jake/Music",
- "player_type": "mpd",
"truncate": {
- "max_length": 100,
- "mode": "end"
- },
- "type": "music"
+ "mode": "end",
+ "max_length": 100
+ }
},
{
- "host": "chloe:6600",
+ "type": "music",
"player_type": "mpd",
- "truncate": "end",
- "type": "music"
+ "host": "chloe:6600",
+ "truncate": "end"
},
{
+ "type": "script",
"cmd": "/home/jake/bin/phone-battery",
"show_if": {
"cmd": "/home/jake/bin/phone-connected",
"interval": 500
- },
- "type": "script"
+ }
},
{
+ "type": "sys_info",
+ "interval": {
+ "memory": 30,
+ "cpu": 1,
+ "temps": 5,
+ "disks": 300,
+ "networks": 3
+ },
"format": [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
@@ -33,103 +70,89 @@
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
" {load_average:1} | {load_average:5} | {load_average:15}",
" {uptime}"
- ],
- "interval": {
- "cpu": 1,
- "disks": 300,
- "memory": 30,
- "networks": 3,
- "temps": 5
- },
- "type": "sys_info"
+ ]
},
{
+ "type": "volume",
+ "format": "{icon} {volume}%",
+ "max_volume": 100,
+ "icons": {
+ "volume_high": "",
+ "volume_medium": "",
+ "volume_low": "",
+ "muted": ""
+ }
+ },
+ {
+ "type": "clipboard",
"max_items": 3,
"truncate": {
- "length": 50,
- "mode": "end"
- },
- "type": "clipboard"
+ "mode": "end",
+ "length": 50
+ }
},
{
+ "type": "custom",
+ "class": "power-menu",
"bar": [
{
- "label": "",
+ "type": "button",
"name": "power-btn",
- "on_click": "popup:toggle",
- "type": "button"
+ "label": "",
+ "on_click": "popup:toggle"
}
],
- "class": "power-menu",
"popup": [
{
- "orientation": "vertical",
"type": "box",
+ "orientation": "vertical",
"widgets": [
{
- "label": "Power menu",
+ "type": "label",
"name": "header",
- "type": "label"
+ "label": "Power menu"
},
{
"type": "box",
"widgets": [
{
+ "type": "button",
"class": "power-btn",
"label": "",
- "on_click": "!shutdown now",
- "type": "button"
+ "on_click": "!shutdown now"
},
{
+ "type": "button",
"class": "power-btn",
"label": "",
- "on_click": "!reboot",
- "type": "button"
+ "on_click": "!reboot"
}
]
},
{
- "label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
+ "type": "label",
"name": "uptime",
- "type": "label"
+ "label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
}
]
}
],
- "tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
- "type": "custom"
+ "tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
},
{
"type": "clock"
- }
- ],
- "icon_theme": "Paper",
- "position": "bottom",
- "start": [
- {
- "all_monitors": false,
- "name_map": {
- "1": "",
- "2": "icon:firefox",
- "3": "",
- "Code": "",
- "Games": "icon:steam"
- },
- "type": "workspaces"
},
{
- "favorites": [
- "firefox",
- "discord",
- "steam"
- ],
- "show_icons": true,
- "show_names": false,
- "type": "launcher"
- },
- {
- "label": "random num: {{500:echo FIXME}}",
- "type": "label"
+ "type": "notifications",
+ "show_count": true,
+ "icons": {
+ "closed_none": "",
+ "closed_some": "",
+ "closed_dnd": "",
+ "open_none": "",
+ "open_some": "",
+ "open_dnd": ""
+ }
}
]
}
diff --git a/examples/config.toml b/examples/config.toml
index bbff737..830e858 100644
--- a/examples/config.toml
+++ b/examples/config.toml
@@ -1,31 +1,57 @@
anchor_to_edges = true
-icon_theme = "Paper"
position = "bottom"
+icon_theme = "Paper"
+
+[[start]]
+type = "workspaces"
+all_monitors = false
+
+[start.name_map]
+1 = ""
+2 = "icon:firefox"
+3 = ""
+Games = "icon:steam"
+Code = ""
+
+[[start]]
+type = "launcher"
+favorites = [
+ "firefox",
+ "discord",
+ "steam",
+]
+show_names = false
+show_icons = true
+
+[[start]]
+type = "label"
+label = "random num: {{500:echo FIXME}}"
[[end]]
-music_dir = "/home/jake/Music"
-player_type = "mpd"
type = "music"
+player_type = "mpd"
+music_dir = "/home/jake/Music"
[end.truncate]
-max_length = 100
mode = "end"
+max_length = 100
[[end]]
-host = "chloe:6600"
-player_type = "mpd"
-truncate = "end"
type = "music"
+player_type = "mpd"
+host = "chloe:6600"
+truncate = "end"
[[end]]
-cmd = "/home/jake/bin/phone-battery"
type = "script"
+cmd = "/home/jake/bin/phone-battery"
[end.show_if]
cmd = "/home/jake/bin/phone-connected"
interval = 500
[[end]]
+type = "sys_info"
format = [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
@@ -35,88 +61,85 @@ format = [
" {load_average:1} | {load_average:5} | {load_average:15}",
" {uptime}",
]
-type = "sys_info"
[end.interval]
-cpu = 1
-disks = 300
memory = 30
-networks = 3
+cpu = 1
temps = 5
+disks = 300
+networks = 3
+
+[[end]]
+type = "volume"
+format = "{icon} {volume}%"
+max_volume = 100
+
+[end.icons]
+volume_high = ""
+volume_medium = ""
+volume_low = ""
+muted = ""
[[end]]
-max_items = 3
type = "clipboard"
+max_items = 3
[end.truncate]
-length = 50
mode = "end"
+length = 50
[[end]]
+type = "custom"
class = "power-menu"
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
-type = "custom"
[[end.bar]]
-label = ""
-name = "power-btn"
-on_click = "popup:toggle"
type = "button"
+name = "power-btn"
+label = ""
+on_click = "popup:toggle"
[[end.popup]]
-orientation = "vertical"
type = "box"
+orientation = "vertical"
[[end.popup.widgets]]
-label = "Power menu"
-name = "header"
type = "label"
+name = "header"
+label = "Power menu"
[[end.popup.widgets]]
type = "box"
[[end.popup.widgets.widgets]]
+type = "button"
class = "power-btn"
label = ""
on_click = "!shutdown now"
-type = "button"
[[end.popup.widgets.widgets]]
+type = "button"
class = "power-btn"
label = ""
on_click = "!reboot"
-type = "button"
[[end.popup.widgets]]
-label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
-name = "uptime"
type = "label"
+name = "uptime"
+label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
[[end]]
type = "clock"
-[[start]]
-all_monitors = false
-type = "workspaces"
+[[end]]
+type = "notifications"
+show_count = true
-[start.name_map]
-1 = ""
-2 = "icon:firefox"
-3 = ""
-Code = ""
-Games = "icon:steam"
-
-[[start]]
-favorites = [
- "firefox",
- "discord",
- "steam",
-]
-show_icons = true
-show_names = false
-type = "launcher"
-
-[[start]]
-label = "random num: {{500:echo FIXME}}"
-type = "label"
+[end.icons]
+closed_none = ""
+closed_some = ""
+closed_dnd = ""
+open_none = ""
+open_some = ""
+open_dnd = ""
diff --git a/examples/config.yaml b/examples/config.yaml
index c9ef8aa..7693414 100644
--- a/examples/config.yaml
+++ b/examples/config.yaml
@@ -1,21 +1,48 @@
anchor_to_edges: true
+position: bottom
+icon_theme: Paper
+start:
+- type: workspaces
+ all_monitors: false
+ name_map:
+ '1':
+ '2': icon:firefox
+ '3':
+ Games: icon:steam
+ Code:
+- type: launcher
+ favorites:
+ - firefox
+ - discord
+ - steam
+ show_names: false
+ show_icons: true
+- type: label
+ label: 'random num: {{500:echo FIXME}}'
end:
-- music_dir: /home/jake/Music
+- type: music
player_type: mpd
+ music_dir: /home/jake/Music
truncate:
- max_length: 100
mode: end
- type: music
-- host: chloe:6600
+ max_length: 100
+- type: music
player_type: mpd
+ host: chloe:6600
truncate: end
- type: music
-- cmd: /home/jake/bin/phone-battery
+- type: script
+ cmd: /home/jake/bin/phone-battery
show_if:
cmd: /home/jake/bin/phone-connected
interval: 500
- type: script
-- format:
+- type: sys_info
+ interval:
+ memory: 30
+ cpu: 1
+ temps: 5
+ disks: 300
+ networks: 3
+ format:
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
- {memory_used} / {memory_total} GB ({memory_percent}%)
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
@@ -23,65 +50,55 @@ end:
- {net_down:enp39s0} / {net_up:enp39s0} Mbps
- {load_average:1} | {load_average:5} | {load_average:15}
- {uptime}
- interval:
- cpu: 1
- disks: 300
- memory: 30
- networks: 3
- temps: 5
- type: sys_info
-- max_items: 3
+- type: volume
+ format: '{icon} {volume}%'
+ max_volume: 100
+ icons:
+ volume_high:
+ volume_medium:
+ volume_low:
+ muted:
+- type: clipboard
+ max_items: 3
truncate:
- length: 50
mode: end
- type: clipboard
-- bar:
- - label:
- name: power-btn
- on_click: popup:toggle
- type: button
+ length: 50
+- type: custom
class: power-menu
+ bar:
+ - type: button
+ name: power-btn
+ label:
+ on_click: popup:toggle
popup:
- - orientation: vertical
- type: box
+ - type: box
+ orientation: vertical
widgets:
- - label: Power menu
+ - type: label
name: header
- type: label
+ label: Power menu
- type: box
widgets:
- - class: power-btn
+ - type: button
+ class: power-btn
label:
on_click: '!shutdown now'
- type: button
- - class: power-btn
+ - type: button
+ class: power-btn
label:
on_click: '!reboot'
- type: button
- - label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
+ - type: label
name: uptime
- type: label
+ label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
- type: custom
- type: clock
-icon_theme: Paper
-position: bottom
-start:
-- all_monitors: false
- name_map:
- '1':
- '2': icon:firefox
- '3':
- Code:
- Games: icon:steam
- type: workspaces
-- favorites:
- - firefox
- - discord
- - steam
- show_icons: true
- show_names: false
- type: launcher
-- label: 'random num: {{500:echo FIXME}}'
- type: label
+- type: notifications
+ show_count: true
+ icons:
+ closed_none:
+ closed_some:
+ closed_dnd:
+ open_none:
+ open_some:
+ open_dnd:
diff --git a/examples/style.css b/examples/style.css
index c766da8..f32fbcc 100644
--- a/examples/style.css
+++ b/examples/style.css
@@ -150,6 +150,20 @@ scale trough {
border-radius: 100%;
}
+/* notifications */
+
+.notifications .count {
+ font-size: 0.6rem;
+ background-color: @color_text;
+ color: @color_bg;
+ border-radius: 100%;
+ margin-right: 3px;
+ margin-top: 3px;
+ padding-left: 4px;
+ padding-right: 4px;
+ opacity: 0.7;
+}
+
/* -- script -- */
.script {
@@ -174,6 +188,11 @@ scale trough {
margin-left: 10px;
}
+/* -- volume -- */
+
+.popup-volume .device-box {
+ border-right: 1px solid @color_border;
+}
/* -- workspaces -- */
diff --git a/flake.lock b/flake.lock
index 8bcf0de..47e5ffa 100644
--- a/flake.lock
+++ b/flake.lock
@@ -7,11 +7,11 @@
]
},
"locked": {
- "lastModified": 1706473964,
- "narHash": "sha256-Fq6xleee/TsX6NbtoRuI96bBuDHMU57PrcK9z1QEKbk=",
+ "lastModified": 1708794349,
+ "narHash": "sha256-jX+B1VGHT0ruHHL5RwS8L21R6miBn4B6s9iVyUJsJJY=",
"owner": "ipetkov",
"repo": "crane",
- "rev": "c798790eabec3e3da48190ae3698ac227aab770c",
+ "rev": "2c94ff9a6fbeb9f3ea0107f28688edbe9c81deaa",
"type": "github"
},
"original": {
@@ -58,11 +58,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1706683685,
- "narHash": "sha256-FtPPshEpxH/ewBOsdKBNhlsL2MLEFv1hEnQ19f/bFsQ=",
+ "lastModified": 1709200309,
+ "narHash": "sha256-lKdtMbhnBNU1lr978T+wEYet3sfIXXgyiDZNEgx8CV8=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "5ad9903c16126a7d949101687af0aa589b1d7d3d",
+ "rev": "ebe6e807793e7c9cc59cf81225fdee1a03413811",
"type": "github"
},
"original": {
@@ -72,11 +72,11 @@
},
"nixpkgs_2": {
"locked": {
- "lastModified": 1706550542,
- "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=",
+ "lastModified": 1709150264,
+ "narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652",
+ "rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
"type": "github"
},
"original": {
@@ -102,11 +102,11 @@
]
},
"locked": {
- "lastModified": 1706667075,
- "narHash": "sha256-KBI5jcOWh9nsOFWj2SRq7vj+fPDf8Do8ceL582kFA70=",
+ "lastModified": 1709172595,
+ "narHash": "sha256-0oYeE5VkhnPA7YBl+0Utq2cYoHcfsEhSGwraCa27Vs8=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "2c993daf3136c6955fd13bfe215d0d4faf6090f1",
+ "rev": "72fa0217f76020ad3aeb2dd9dd72490905b23b6f",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index c37ed35..ce371ff 100644
--- a/flake.nix
+++ b/flake.nix
@@ -127,6 +127,7 @@
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
+ libpulseaudio
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
diff --git a/nix/default.nix b/nix/default.nix
index 731e7ac..f5d680a 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -12,6 +12,7 @@
gtk-layer-shell,
gnome,
libxkbcommon,
+ libpulseaudio,
openssl,
pkg-config,
hicolor-icon-theme,
@@ -30,7 +31,7 @@
path = lib.cleanSource ../.;
};
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
- buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
+ buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon libpulseaudio openssl];
propagatedBuildInputs = [
gtk3
];
diff --git a/src/bar.rs b/src/bar.rs
index 0b738a0..53b8a15 100644
--- a/src/bar.rs
+++ b/src/bar.rs
@@ -10,7 +10,6 @@ use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
use gtk_layer_shell::LayerShell;
-use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use tracing::{debug, info};
@@ -18,7 +17,7 @@ use tracing::{debug, info};
#[derive(Debug, Clone)]
enum Inner {
New { config: Option },
- Loaded { popup: Rc> },
+ Loaded { popup: Rc },
}
#[derive(Debug, Clone)]
@@ -60,7 +59,7 @@ impl Bar {
window.set_widget_name(&name);
let position = config.position;
- let orientation = position.get_orientation();
+ let orientation = position.orientation();
let content = gtk::Box::builder()
.orientation(orientation)
@@ -187,7 +186,7 @@ impl Bar {
win.set_layer_shell_margin(gtk_layer_shell::Edge::Left, margin.left);
win.set_layer_shell_margin(gtk_layer_shell::Edge::Right, margin.right);
- let bar_orientation = position.get_orientation();
+ let bar_orientation = position.orientation();
win.set_anchor(
gtk_layer_shell::Edge::Top,
@@ -269,7 +268,7 @@ impl Bar {
// popup ignores module location so can bodge this for now
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
- let popup = Rc::new(RefCell::new(popup));
+ let popup = Rc::new(popup);
if let Some(modules) = config.start {
let info = info!(ModuleLocation::Left);
@@ -315,7 +314,7 @@ impl Bar {
&self.monitor_name
}
- pub fn popup(&self) -> Rc> {
+ pub fn popup(&self) -> Rc {
match &self.inner {
Inner::New { .. } => {
panic!("Attempted to get popup of uninitialized bar. This is a serious bug!")
@@ -339,7 +338,7 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
#[derive(Debug)]
struct BarLoadResult {
- popup: Rc>,
+ popup: Rc,
}
/// Adds modules into a provided GTK box,
@@ -349,9 +348,9 @@ fn add_modules(
modules: Vec,
info: &ModuleInfo,
ironbar: &Rc,
- popup: &Rc>,
+ popup: &Rc,
) -> Result<()> {
- let orientation = info.bar_position.get_orientation();
+ let orientation = info.bar_position.orientation();
macro_rules! add_module {
($module:expr, $id:expr) => {{
@@ -388,6 +387,8 @@ fn add_modules(
ModuleConfig::Music(mut module) => add_module!(module, id),
#[cfg(feature = "networkmanager")]
ModuleConfig::Networkmanager(mut module) => add_module!(module, id),
+ #[cfg(feature = "notifications")]
+ ModuleConfig::Notifications(mut module) => add_module!(module, id),
ModuleConfig::Script(mut module) => add_module!(module, id),
#[cfg(feature = "sys_info")]
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
@@ -395,6 +396,8 @@ fn add_modules(
ModuleConfig::Tray(mut module) => add_module!(module, id),
#[cfg(feature = "upower")]
ModuleConfig::Upower(mut module) => add_module!(module, id),
+ #[cfg(feature = "volume")]
+ ModuleConfig::Volume(mut module) => add_module!(module, id),
#[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
}
diff --git a/src/clients/mod.rs b/src/clients/mod.rs
index f1433bc..fa9c876 100644
--- a/src/clients/mod.rs
+++ b/src/clients/mod.rs
@@ -6,10 +6,14 @@ pub mod clipboard;
pub mod compositor;
#[cfg(feature = "music")]
pub mod music;
+#[cfg(feature = "notifications")]
+pub mod swaync;
#[cfg(feature = "tray")]
pub mod system_tray;
#[cfg(feature = "upower")]
pub mod upower;
+#[cfg(feature = "volume")]
+pub mod volume;
pub mod wayland;
/// Singleton wrapper consisting of
@@ -23,10 +27,14 @@ pub struct Clients {
clipboard: Option>,
#[cfg(feature = "music")]
music: std::collections::HashMap>,
+ #[cfg(feature = "notifications")]
+ notifications: Option>,
#[cfg(feature = "tray")]
tray: Option>,
#[cfg(feature = "upower")]
upower: Option>>,
+ #[cfg(feature = "volume")]
+ volume: Option>,
}
impl Clients {
@@ -67,6 +75,15 @@ impl Clients {
.clone()
}
+ #[cfg(feature = "notifications")]
+ pub fn notifications(&mut self) -> Arc {
+ self.notifications
+ .get_or_insert_with(|| {
+ Arc::new(crate::await_sync(async { swaync::Client::new().await }))
+ })
+ .clone()
+ }
+
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> Arc {
self.tray
@@ -86,6 +103,13 @@ impl Clients {
})
.clone()
}
+
+ #[cfg(feature = "volume")]
+ pub fn volume(&mut self) -> Arc {
+ self.volume
+ .get_or_insert_with(volume::create_client)
+ .clone()
+ }
}
/// Types implementing this trait
@@ -111,7 +135,7 @@ macro_rules! register_client {
where
TSend: Clone,
{
- fn provide(&self) -> Arc<$ty> {
+ fn provide(&self) -> std::sync::Arc<$ty> {
self.ironbar.clients.borrow_mut().$method()
}
}
diff --git a/src/clients/swaync/dbus.rs b/src/clients/swaync/dbus.rs
new file mode 100644
index 0000000..1bc292b
--- /dev/null
+++ b/src/clients/swaync/dbus.rs
@@ -0,0 +1,111 @@
+//! # D-Bus interface proxy for: `org.erikreider.swaync.cc`
+//!
+//! This code was generated by `zbus-xmlgen` `4.0.1` from D-Bus introspection data.
+//! Source: `Interface '/org/erikreider/swaync/cc' from service 'org.erikreider.swaync.cc' on session bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+
+#[zbus::dbus_proxy(
+ interface = "org.erikreider.swaync.cc",
+ default_service = "org.erikreider.swaync.cc",
+ default_path = "/org/erikreider/swaync/cc"
+)]
+trait SwayNc {
+ /// AddInhibitor method
+ fn add_inhibitor(&self, application_id: &str) -> zbus::Result;
+
+ /// ChangeConfigValue method
+ fn change_config_value(
+ &self,
+ name: &str,
+ value: &zbus::zvariant::Value<'_>,
+ write_to_file: bool,
+ path: &str,
+ ) -> zbus::Result<()>;
+
+ /// ClearInhibitors method
+ fn clear_inhibitors(&self) -> zbus::Result;
+
+ /// CloseAllNotifications method
+ fn close_all_notifications(&self) -> zbus::Result<()>;
+
+ /// CloseNotification method
+ fn close_notification(&self, id: u32) -> zbus::Result<()>;
+
+ /// GetDnd method
+ fn get_dnd(&self) -> zbus::Result;
+
+ /// GetSubscribeData method
+ fn get_subscribe_data(&self) -> zbus::Result<(bool, bool, u32, bool)>;
+
+ /// GetVisibility method
+ fn get_visibility(&self) -> zbus::Result;
+
+ /// HideLatestNotifications method
+ fn hide_latest_notifications(&self, close: bool) -> zbus::Result<()>;
+
+ /// IsInhibited method
+ fn is_inhibited(&self) -> zbus::Result;
+
+ /// NotificationCount method
+ fn notification_count(&self) -> zbus::Result;
+
+ /// NumberOfInhibitors method
+ fn number_of_inhibitors(&self) -> zbus::Result;
+
+ /// ReloadConfig method
+ fn reload_config(&self) -> zbus::Result<()>;
+
+ /// ReloadCss method
+ fn reload_css(&self) -> zbus::Result;
+
+ /// RemoveInhibitor method
+ fn remove_inhibitor(&self, application_id: &str) -> zbus::Result;
+
+ /// SetDnd method
+ fn set_dnd(&self, state: bool) -> zbus::Result<()>;
+
+ /// SetVisibility method
+ fn set_visibility(&self, visibility: bool) -> zbus::Result<()>;
+
+ /// ToggleDnd method
+ fn toggle_dnd(&self) -> zbus::Result;
+
+ /// ToggleVisibility method
+ fn toggle_visibility(&self) -> zbus::Result<()>;
+
+ /// Subscribe signal
+ #[dbus_proxy(signal)]
+ fn subscribe(&self, count: u32, dnd: bool, cc_open: bool) -> zbus::Result<()>;
+
+ /// SubscribeV2 signal
+ #[dbus_proxy(signal)]
+ fn subscribe_v2(
+ &self,
+ count: u32,
+ dnd: bool,
+ cc_open: bool,
+ inhibited: bool,
+ ) -> zbus::Result<()>;
+
+ /// Inhibited property
+ #[dbus_proxy(property)]
+ fn inhibited(&self) -> zbus::Result;
+ #[dbus_proxy(property)]
+ fn set_inhibited(&self, value: bool) -> zbus::Result<()>;
+}
diff --git a/src/clients/swaync/mod.rs b/src/clients/swaync/mod.rs
new file mode 100644
index 0000000..c7d226b
--- /dev/null
+++ b/src/clients/swaync/mod.rs
@@ -0,0 +1,88 @@
+mod dbus;
+
+use crate::{register_client, send, spawn};
+use color_eyre::{Report, Result};
+use dbus::SwayNcProxy;
+use serde::Deserialize;
+use tokio::sync::broadcast;
+use tracing::{debug, error};
+use zbus::export::ordered_stream::OrderedStreamExt;
+use zbus::zvariant::Type;
+
+#[derive(Debug, Clone, Copy, Type, Deserialize)]
+pub struct Event {
+ pub count: u32,
+ pub dnd: bool,
+ pub cc_open: bool,
+ pub inhibited: bool,
+}
+
+type GetSubscribeData = (bool, bool, u32, bool);
+
+/// Converts the data returned from
+/// `get_subscribe_data` into an event for convenience.
+impl From for Event {
+ fn from((dnd, cc_open, count, inhibited): (bool, bool, u32, bool)) -> Self {
+ Self {
+ dnd,
+ cc_open,
+ count,
+ inhibited,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Client {
+ proxy: SwayNcProxy<'static>,
+ tx: broadcast::Sender,
+ _rx: broadcast::Receiver,
+}
+
+impl Client {
+ pub async fn new() -> Self {
+ let dbus = Box::pin(zbus::Connection::session())
+ .await
+ .expect("failed to create connection to system bus");
+
+ let proxy = SwayNcProxy::new(&dbus).await.unwrap();
+ let (tx, rx) = broadcast::channel(8);
+
+ let mut stream = proxy.receive_subscribe_v2().await.unwrap();
+
+ {
+ let tx = tx.clone();
+
+ spawn(async move {
+ while let Some(ev) = stream.next().await {
+ let ev = ev.body::().expect("to deserialize");
+ debug!("Received event: {ev:?}");
+ send!(tx, ev);
+ }
+ });
+ }
+
+ Self { proxy, tx, _rx: rx }
+ }
+
+ pub fn subscribe(&self) -> broadcast::Receiver {
+ self.tx.subscribe()
+ }
+
+ pub async fn state(&self) -> Result {
+ debug!("Getting subscribe data (current state)");
+ match self.proxy.get_subscribe_data().await {
+ Ok(data) => Ok(data.into()),
+ Err(err) => Err(Report::new(err)),
+ }
+ }
+
+ pub async fn toggle_visibility(&self) {
+ debug!("Toggling visibility");
+ if let Err(err) = self.proxy.toggle_visibility().await {
+ error!("{err:?}");
+ }
+ }
+}
+
+register_client!(Client, notifications);
diff --git a/src/clients/volume/mod.rs b/src/clients/volume/mod.rs
new file mode 100644
index 0000000..3db503f
--- /dev/null
+++ b/src/clients/volume/mod.rs
@@ -0,0 +1,309 @@
+mod sink;
+mod sink_input;
+
+use crate::{arc_mut, lock, register_client, send, spawn_blocking, APP_ID};
+use libpulse_binding::callbacks::ListResult;
+use libpulse_binding::context::introspect::{Introspector, ServerInfo};
+use libpulse_binding::context::subscribe::{Facility, InterestMaskSet, Operation};
+use libpulse_binding::context::{Context, FlagSet, State};
+use libpulse_binding::mainloop::standard::{IterateResult, Mainloop};
+use libpulse_binding::proplist::Proplist;
+use libpulse_binding::volume::{ChannelVolumes, Volume};
+use std::fmt::{Debug, Formatter};
+use std::sync::{Arc, Mutex};
+use tokio::sync::broadcast;
+use tracing::{debug, error, info, warn};
+
+pub use sink::Sink;
+pub use sink_input::SinkInput;
+
+type ArcMutVec = Arc>>;
+
+#[derive(Debug, Clone)]
+pub enum Event {
+ AddSink(Sink),
+ UpdateSink(Sink),
+ RemoveSink(String),
+
+ AddInput(SinkInput),
+ UpdateInput(SinkInput),
+ RemoveInput(u32),
+}
+
+#[derive(Debug)]
+pub struct Client {
+ connection: Arc>,
+
+ data: Data,
+
+ tx: broadcast::Sender,
+ _rx: broadcast::Receiver,
+}
+
+#[derive(Debug, Default, Clone)]
+struct Data {
+ sinks: ArcMutVec,
+ sink_inputs: ArcMutVec,
+
+ default_sink_name: Arc>>,
+}
+
+pub enum ConnectionState {
+ Disconnected,
+ Connected {
+ context: Arc>,
+ introspector: Introspector,
+ },
+}
+
+impl Debug for ConnectionState {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Self::Disconnected => "Disconnected",
+ Self::Connected { .. } => "Connected",
+ }
+ )
+ }
+}
+
+impl Client {
+ pub fn new() -> Self {
+ let (tx, rx) = broadcast::channel(32);
+
+ Self {
+ connection: arc_mut!(ConnectionState::Disconnected),
+ data: Data::default(),
+ tx,
+ _rx: rx,
+ }
+ }
+
+ /// Starts the client.
+ fn run(&self) {
+ let Some(mut proplist) = Proplist::new() else {
+ error!("Failed to create PA proplist");
+ return;
+ };
+
+ if proplist.set_str("APPLICATION_NAME", APP_ID).is_err() {
+ error!("Failed to update PA proplist");
+ }
+
+ let Some(mut mainloop) = Mainloop::new() else {
+ error!("Failed to create PA mainloop");
+ return;
+ };
+
+ let Some(context) = Context::new_with_proplist(&mainloop, "Ironbar Context", &proplist)
+ else {
+ error!("Failed to create PA context");
+ return;
+ };
+
+ let context = arc_mut!(context);
+
+ let state_callback = Box::new({
+ let context = context.clone();
+ let data = self.data.clone();
+ let tx = self.tx.clone();
+
+ move || on_state_change(&context, &data, &tx)
+ });
+
+ lock!(context).set_state_callback(Some(state_callback));
+
+ if let Err(err) = lock!(context).connect(None, FlagSet::NOAUTOSPAWN, None) {
+ error!("{err:?}");
+ }
+
+ let introspector = lock!(context).introspect();
+
+ {
+ let mut inner = lock!(self.connection);
+ *inner = ConnectionState::Connected {
+ context,
+ introspector,
+ };
+ }
+
+ loop {
+ match mainloop.iterate(true) {
+ IterateResult::Success(_) => {}
+ IterateResult::Err(err) => error!("{err:?}"),
+ IterateResult::Quit(_) => break,
+ }
+ }
+ }
+
+ /// Gets an event receiver.
+ pub fn subscribe(&self) -> broadcast::Receiver {
+ self.tx.subscribe()
+ }
+}
+
+/// Creates a new Pulse volume client.
+pub fn create_client() -> Arc {
+ let client = Arc::new(Client::new());
+
+ {
+ let client = client.clone();
+ spawn_blocking(move || {
+ client.run();
+ });
+ }
+
+ client
+}
+
+fn on_state_change(context: &Arc>, data: &Data, tx: &broadcast::Sender) {
+ let Ok(state) = context.try_lock().map(|lock| lock.get_state()) else {
+ return;
+ };
+
+ match state {
+ State::Ready => {
+ info!("connected to server");
+
+ let introspect = lock!(context).introspect();
+ let introspect2 = lock!(context).introspect();
+
+ introspect.get_sink_info_list({
+ let sinks = data.sinks.clone();
+ let default_sink = data.default_sink_name.clone();
+
+ let tx = tx.clone();
+
+ move |info| match info {
+ ListResult::Item(_) => sink::add(info, &sinks, &tx),
+ ListResult::End => {
+ introspect2.get_server_info({
+ let sinks = sinks.clone();
+ let default_sink = default_sink.clone();
+ let tx = tx.clone();
+
+ move |info| set_default_sink(info, &sinks, &default_sink, &tx)
+ });
+ }
+ ListResult::Error => error!("Error while receiving sinks"),
+ }
+ });
+
+ introspect.get_sink_input_info_list({
+ let inputs = data.sink_inputs.clone();
+ let tx = tx.clone();
+
+ move |info| sink_input::add(info, &inputs, &tx)
+ });
+
+ let subscribe_callback = Box::new({
+ let context = context.clone();
+ let data = data.clone();
+ let tx = tx.clone();
+
+ move |facility, op, i| on_event(&context, &data, &tx, facility, op, i)
+ });
+
+ lock!(context).set_subscribe_callback(Some(subscribe_callback));
+ lock!(context).subscribe(
+ InterestMaskSet::SERVER | InterestMaskSet::SINK_INPUT | InterestMaskSet::SINK,
+ |_| (),
+ );
+ }
+ State::Failed => error!("Failed to connect to audio server"),
+ State::Terminated => error!("Connection to audio server terminated"),
+ _ => {}
+ }
+}
+
+fn on_event(
+ context: &Arc>,
+ data: &Data,
+ tx: &broadcast::Sender,
+ facility: Option,
+ op: Option,
+ i: u32,
+) {
+ let (Some(facility), Some(op)) = (facility, op) else {
+ return;
+ };
+
+ match facility {
+ Facility::Server => on_server_event(context, &data.sinks, &data.default_sink_name, tx),
+ Facility::Sink => sink::on_event(context, &data.sinks, &data.default_sink_name, tx, op, i),
+ Facility::SinkInput => sink_input::on_event(context, &data.sink_inputs, tx, op, i),
+ _ => error!("Received unhandled facility: {facility:?}"),
+ }
+}
+
+fn on_server_event(
+ context: &Arc>,
+ sinks: &ArcMutVec,
+ default_sink: &Arc>>,
+ tx: &broadcast::Sender,
+) {
+ lock!(context).introspect().get_server_info({
+ let sinks = sinks.clone();
+ let default_sink = default_sink.clone();
+ let tx = tx.clone();
+
+ move |info| set_default_sink(info, &sinks, &default_sink, &tx)
+ });
+}
+
+fn set_default_sink(
+ info: &ServerInfo,
+ sinks: &ArcMutVec,
+ default_sink: &Arc>>,
+ tx: &broadcast::Sender,
+) {
+ let default_sink_name = info.default_sink_name.as_ref().map(ToString::to_string);
+
+ if default_sink_name != *lock!(default_sink) {
+ if let Some(ref default_sink_name) = default_sink_name {
+ if let Some(sink) = lock!(sinks)
+ .iter_mut()
+ .find(|s| s.name.as_str() == default_sink_name.as_str())
+ {
+ sink.active = true;
+ debug!("Set sink active: {}", sink.name);
+ send!(tx, Event::UpdateSink(sink.clone()));
+ } else {
+ warn!("Couldn't find sink: {}", default_sink_name);
+ }
+ }
+ }
+
+ *lock!(default_sink) = default_sink_name;
+}
+
+/// Converts a Pulse `ChannelVolumes` struct into a single percentage value,
+/// representing the average value across all channels.
+fn volume_to_percent(volume: ChannelVolumes) -> f64 {
+ let avg = volume.avg().0;
+ let base_delta = (Volume::NORMAL.0 - Volume::MUTED.0) as f64 / 100.0;
+
+ ((avg - Volume::MUTED.0) as f64 / base_delta).round()
+}
+
+/// Converts a percentage volume into a Pulse volume value,
+/// which can be used for setting channel volumes.
+pub fn percent_to_volume(target_percent: f64) -> u32 {
+ let base_delta = (Volume::NORMAL.0 as f32 - Volume::MUTED.0 as f32) / 100.0;
+
+ if target_percent < 0.0 {
+ Volume::MUTED.0
+ } else if target_percent == 100.0 {
+ Volume::NORMAL.0
+ } else if target_percent >= 150.0 {
+ (Volume::NORMAL.0 as f32 * 1.5) as u32
+ } else if target_percent < 100.0 {
+ Volume::MUTED.0 + target_percent as u32 * base_delta as u32
+ } else {
+ Volume::NORMAL.0 + (target_percent - 100.0) as u32 * base_delta as u32
+ }
+}
+
+register_client!(Client, volume);
diff --git a/src/clients/volume/sink.rs b/src/clients/volume/sink.rs
new file mode 100644
index 0000000..d08b99e
--- /dev/null
+++ b/src/clients/volume/sink.rs
@@ -0,0 +1,175 @@
+use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event};
+use crate::{lock, send};
+use libpulse_binding::callbacks::ListResult;
+use libpulse_binding::context::introspect::SinkInfo;
+use libpulse_binding::context::subscribe::Operation;
+use libpulse_binding::context::Context;
+use libpulse_binding::def::SinkState;
+use std::sync::{mpsc, Arc, Mutex};
+use tokio::sync::broadcast;
+use tracing::{debug, error};
+
+#[derive(Debug, Clone)]
+pub struct Sink {
+ index: u32,
+ pub name: String,
+ pub description: String,
+ pub volume: f64,
+ pub muted: bool,
+ pub active: bool,
+}
+
+impl From<&SinkInfo<'_>> for Sink {
+ fn from(value: &SinkInfo) -> Self {
+ Self {
+ index: value.index,
+ name: value
+ .name
+ .as_ref()
+ .map(ToString::to_string)
+ .unwrap_or_default(),
+ description: value
+ .description
+ .as_ref()
+ .map(ToString::to_string)
+ .unwrap_or_default(),
+ muted: value.mute,
+ volume: volume_to_percent(value.volume),
+ active: value.state == SinkState::Running,
+ }
+ }
+}
+
+impl Client {
+ pub fn sinks(&self) -> Arc>> {
+ self.data.sinks.clone()
+ }
+
+ pub fn set_default_sink(&self, name: &str) {
+ if let ConnectionState::Connected { context, .. } = &*lock!(self.connection) {
+ lock!(context).set_default_sink(name, |_| {});
+ }
+ }
+
+ pub fn set_sink_volume(&self, name: &str, volume_percent: f64) {
+ if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
+ let (tx, rx) = mpsc::channel();
+
+ introspector.get_sink_info_by_name(name, move |info| {
+ let ListResult::Item(info) = info else {
+ return;
+ };
+ send!(tx, info.volume);
+ });
+
+ let new_volume = percent_to_volume(volume_percent);
+
+ let mut volume = rx.recv().expect("to receive info");
+ for v in volume.get_mut() {
+ v.0 = new_volume;
+ }
+
+ introspector.set_sink_volume_by_name(name, &volume, None);
+ }
+ }
+
+ pub fn set_sink_muted(&self, name: &str, muted: bool) {
+ if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
+ introspector.set_sink_mute_by_name(name, muted, None);
+ }
+ }
+}
+
+pub fn on_event(
+ context: &Arc>,
+ sinks: &ArcMutVec,
+ default_sink: &Arc>>,
+ tx: &broadcast::Sender,
+ op: Operation,
+ i: u32,
+) {
+ let introspect = lock!(context).introspect();
+
+ match op {
+ Operation::New => {
+ debug!("new sink");
+ introspect.get_sink_info_by_index(i, {
+ let sinks = sinks.clone();
+ let tx = tx.clone();
+
+ move |info| add(info, &sinks, &tx)
+ });
+ }
+ Operation::Changed => {
+ debug!("sink changed");
+ introspect.get_sink_info_by_index(i, {
+ let sinks = sinks.clone();
+ let default_sink = default_sink.clone();
+ let tx = tx.clone();
+
+ move |info| update(info, &sinks, &default_sink, &tx)
+ });
+ }
+ Operation::Removed => {
+ debug!("sink removed");
+ remove(i, sinks, tx);
+ }
+ }
+}
+
+pub fn add(info: ListResult<&SinkInfo>, sinks: &ArcMutVec, tx: &broadcast::Sender) {
+ let ListResult::Item(info) = info else {
+ return;
+ };
+
+ lock!(sinks).push(info.into());
+ send!(tx, Event::AddSink(info.into()));
+}
+
+fn update(
+ info: ListResult<&SinkInfo>,
+ sinks: &ArcMutVec,
+ default_sink: &Arc>>,
+ tx: &broadcast::Sender,
+) {
+ let ListResult::Item(info) = info else {
+ return;
+ };
+
+ {
+ let mut sinks = lock!(sinks);
+ let Some(pos) = sinks.iter().position(|sink| sink.index == info.index) else {
+ error!("received update to untracked sink input");
+ return;
+ };
+
+ sinks[pos] = info.into();
+
+ // update in local copy
+ if !sinks[pos].active {
+ if let Some(default_sink) = &*lock!(default_sink) {
+ sinks[pos].active = &sinks[pos].name == default_sink;
+ }
+ }
+ }
+
+ let mut sink: Sink = info.into();
+
+ // update in broadcast copy
+ if !sink.active {
+ if let Some(default_sink) = &*lock!(default_sink) {
+ sink.active = &sink.name == default_sink;
+ }
+ }
+
+ send!(tx, Event::UpdateSink(sink));
+}
+
+fn remove(index: u32, sinks: &ArcMutVec, tx: &broadcast::Sender) {
+ let mut sinks = lock!(sinks);
+
+ if let Some(pos) = sinks.iter().position(|s| s.index == index) {
+ let info = sinks.remove(pos);
+ send!(tx, Event::RemoveSink(info.name));
+ }
+}
diff --git a/src/clients/volume/sink_input.rs b/src/clients/volume/sink_input.rs
new file mode 100644
index 0000000..102aed2
--- /dev/null
+++ b/src/clients/volume/sink_input.rs
@@ -0,0 +1,148 @@
+use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event};
+use crate::{lock, send};
+use libpulse_binding::callbacks::ListResult;
+use libpulse_binding::context::introspect::SinkInputInfo;
+use libpulse_binding::context::subscribe::Operation;
+use libpulse_binding::context::Context;
+use std::sync::{mpsc, Arc, Mutex};
+use tokio::sync::broadcast;
+use tracing::{debug, error};
+
+#[derive(Debug, Clone)]
+pub struct SinkInput {
+ pub index: u32,
+ pub name: String,
+ pub volume: f64,
+ pub muted: bool,
+
+ pub can_set_volume: bool,
+}
+
+impl From<&SinkInputInfo<'_>> for SinkInput {
+ fn from(value: &SinkInputInfo) -> Self {
+ Self {
+ index: value.index,
+ name: value
+ .name
+ .as_ref()
+ .map(ToString::to_string)
+ .unwrap_or_default(),
+ muted: value.mute,
+ volume: volume_to_percent(value.volume),
+ can_set_volume: value.has_volume && value.volume_writable,
+ }
+ }
+}
+
+impl Client {
+ pub fn sink_inputs(&self) -> Arc>> {
+ self.data.sink_inputs.clone()
+ }
+
+ pub fn set_input_volume(&self, index: u32, volume_percent: f64) {
+ if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
+ let (tx, rx) = mpsc::channel();
+
+ introspector.get_sink_input_info(index, move |info| {
+ let ListResult::Item(info) = info else {
+ return;
+ };
+ send!(tx, info.volume);
+ });
+
+ let new_volume = percent_to_volume(volume_percent);
+
+ let mut volume = rx.recv().expect("to receive info");
+ for v in volume.get_mut() {
+ v.0 = new_volume;
+ }
+
+ introspector.set_sink_input_volume(index, &volume, None);
+ }
+ }
+
+ pub fn set_input_muted(&self, index: u32, muted: bool) {
+ if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
+ introspector.set_sink_input_mute(index, muted, None);
+ }
+ }
+}
+
+pub fn on_event(
+ context: &Arc>,
+ inputs: &ArcMutVec,
+ tx: &broadcast::Sender,
+ op: Operation,
+ i: u32,
+) {
+ let introspect = lock!(context).introspect();
+
+ match op {
+ Operation::New => {
+ debug!("new sink input");
+ introspect.get_sink_input_info(i, {
+ let inputs = inputs.clone();
+ let tx = tx.clone();
+
+ move |info| add(info, &inputs, &tx)
+ });
+ }
+ Operation::Changed => {
+ debug!("sink input changed");
+ introspect.get_sink_input_info(i, {
+ let inputs = inputs.clone();
+ let tx = tx.clone();
+
+ move |info| update(info, &inputs, &tx)
+ });
+ }
+ Operation::Removed => {
+ debug!("sink input removed");
+ remove(i, inputs, tx);
+ }
+ }
+}
+
+pub fn add(
+ info: ListResult<&SinkInputInfo>,
+ inputs: &ArcMutVec,
+ tx: &broadcast::Sender,
+) {
+ let ListResult::Item(info) = info else {
+ return;
+ };
+
+ lock!(inputs).push(info.into());
+ send!(tx, Event::AddInput(info.into()));
+}
+
+fn update(
+ info: ListResult<&SinkInputInfo>,
+ inputs: &ArcMutVec,
+ tx: &broadcast::Sender,
+) {
+ let ListResult::Item(info) = info else {
+ return;
+ };
+
+ {
+ let mut inputs = lock!(inputs);
+ let Some(pos) = inputs.iter().position(|input| input.index == info.index) else {
+ error!("received update to untracked sink input");
+ return;
+ };
+
+ inputs[pos] = info.into();
+ }
+
+ send!(tx, Event::UpdateInput(info.into()));
+}
+
+fn remove(index: u32, inputs: &ArcMutVec, tx: &broadcast::Sender) {
+ let mut inputs = lock!(inputs);
+
+ if let Some(pos) = inputs.iter().position(|s| s.index == index) {
+ let info = inputs.remove(pos);
+ send!(tx, Event::RemoveInput(info.index));
+ }
+}
diff --git a/src/clients/wayland/wlr_data_control/mod.rs b/src/clients/wayland/wlr_data_control/mod.rs
index 8c40a84..1cf76a5 100644
--- a/src/clients/wayland/wlr_data_control/mod.rs
+++ b/src/clients/wayland/wlr_data_control/mod.rs
@@ -179,6 +179,9 @@ impl Environment {
MimeTypeCategory::Image => {
let mut bytes = vec![];
file.read_to_end(&mut bytes)?;
+
+ debug!("Read bytes: {}", bytes.len());
+
let bytes = Bytes::from(&bytes);
ClipboardValue::Image(bytes)
@@ -234,6 +237,8 @@ impl DataControlDeviceHandler for Environment {
return;
};
+ debug!("Receiving mime type: {}", mime_type.value);
+
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
let offer_clone = cur_offer.offer.clone();
@@ -331,9 +336,9 @@ impl DataControlSourceHandler for Environment {
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
.expect("Failed to increase pipe size");
- let mut file = File::from(fd.try_clone().expect("Failed to clone fd"));
+ let mut file = File::from(fd.try_clone().expect("to be able to clone"));
- trace!("Num bytes: {}", bytes.len());
+ debug!("Writing {} bytes", bytes.len());
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::>();
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
@@ -347,20 +352,23 @@ impl DataControlSourceHandler for Environment {
while !bytes.is_empty() {
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
- trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
-
epoll_fd
.wait(&mut events, 100)
.expect("Failed to wait to epoll");
match file.write(chunk) {
- Ok(_) => bytes = &bytes[chunk.len()..],
+ Ok(written) => {
+ trace!("Wrote {} bytes ({} remain)", written, bytes.len());
+ bytes = &bytes[written..];
+ }
Err(err) => {
error!("{err:?}");
break;
}
}
}
+
+ debug!("Done writing");
} else {
error!("Failed to find source");
}
@@ -388,7 +396,7 @@ impl DataControlSourceHandler for Environment {
/// If the requested size is larger than the kernel max (normally 1MB),
/// it will be clamped at this.
///
-/// Returns the new size if succeeded
+/// Returns the new size if succeeded.
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result {
// clamp size at kernel max
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
diff --git a/src/config/impl.rs b/src/config/impl.rs
index c60e292..7b9e65d 100644
--- a/src/config/impl.rs
+++ b/src/config/impl.rs
@@ -38,7 +38,7 @@ impl<'de> Deserialize<'de> for MonitorConfig {
impl BarPosition {
/// Gets the orientation the bar and widgets should use
/// based on this position.
- pub fn get_orientation(self) -> Orientation {
+ pub fn orientation(self) -> Orientation {
if self == Self::Top || self == Self::Bottom {
Orientation::Horizontal
} else {
diff --git a/src/config/mod.rs b/src/config/mod.rs
index b8c51ed..e516b4d 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -16,6 +16,8 @@ use crate::modules::launcher::LauncherModule;
use crate::modules::music::MusicModule;
#[cfg(feature = "networkmanager")]
use crate::modules::networkmanager::NetworkmanagerModule;
+#[cfg(feature = "notifications")]
+use crate::modules::notifications::NotificationsModule;
use crate::modules::script::ScriptModule;
#[cfg(feature = "sys_info")]
use crate::modules::sysinfo::SysInfoModule;
@@ -23,6 +25,8 @@ use crate::modules::sysinfo::SysInfoModule;
use crate::modules::tray::TrayModule;
#[cfg(feature = "upower")]
use crate::modules::upower::UpowerModule;
+#[cfg(feature = "volume")]
+use crate::modules::volume::VolumeModule;
#[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule;
use cfg_if::cfg_if;
@@ -49,6 +53,8 @@ pub enum ModuleConfig {
Music(Box),
#[cfg(feature = "networkmanager")]
Networkmanager(Box),
+ #[cfg(feature = "notifications")]
+ Notifications(Box),
Script(Box),
#[cfg(feature = "sys_info")]
SysInfo(Box),
@@ -56,6 +62,8 @@ pub enum ModuleConfig {
Tray(Box),
#[cfg(feature = "upower")]
Upower(Box),
+ #[cfg(feature = "volume")]
+ Volume(Box),
#[cfg(feature = "workspaces")]
Workspaces(Box),
}
diff --git a/src/ipc/server.rs b/src/ipc/server.rs
index 4d30ee4..931a4fa 100644
--- a/src/ipc/server.rs
+++ b/src/ipc/server.rs
@@ -167,13 +167,13 @@ impl Ipc {
match bar {
Some(bar) => {
let popup = bar.popup();
- let current_widget = popup.borrow().current_widget();
+ let current_widget = popup.current_widget();
- popup.borrow_mut().hide();
+ popup.hide();
let data = popup
- .borrow()
.cache
+ .borrow()
.iter()
.find(|(_, value)| value.name == name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
@@ -181,7 +181,6 @@ impl Ipc {
match data {
Some((id, Some(button))) if current_widget != Some(id) => {
let button_id = button.popup_id();
- let mut popup = popup.borrow_mut();
if popup.is_visible() {
popup.hide();
@@ -207,11 +206,11 @@ impl Ipc {
let popup = bar.popup();
// only one popup per bar, so hide if open for another widget
- popup.borrow_mut().hide();
+ popup.hide();
let data = popup
- .borrow()
.cache
+ .borrow()
.iter()
.find(|(_, value)| value.name == name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
@@ -219,7 +218,7 @@ impl Ipc {
match data {
Some((id, Some(button))) => {
let button_id = button.popup_id();
- popup.borrow_mut().show(id, button_id);
+ popup.show(id, button_id);
Response::Ok
}
@@ -236,7 +235,7 @@ impl Ipc {
match bar {
Some(bar) => {
let popup = bar.popup();
- popup.borrow_mut().hide();
+ popup.hide();
Response::Ok
}
diff --git a/src/macros.rs b/src/macros.rs
index 028fd19..4cc986c 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -180,3 +180,10 @@ macro_rules! arc_rw {
std::sync::Arc::new(std::sync::RwLock::new($val))
};
}
+
+#[macro_export]
+macro_rules! rc_mut {
+ ($val:expr) => {
+ std::rc::Rc::new(std::cell::RefCell::new($val))
+ };
+}
diff --git a/src/main.rs b/src/main.rs
index 7245242..efa0b71 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -57,7 +57,7 @@ mod popup;
mod script;
mod style;
-const GTK_APP_ID: &str = "dev.jstanger.ironbar";
+pub const APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() {
@@ -111,7 +111,7 @@ impl Ironbar {
info!("Ironbar version {}", VERSION);
info!("Starting application");
- let app = Application::builder().application_id(GTK_APP_ID).build();
+ let app = Application::builder().application_id(APP_ID).build();
let running = AtomicBool::new(false);
diff --git a/src/modules/custom/mod.rs b/src/modules/custom/mod.rs
index 6ec7027..b6e97b9 100644
--- a/src/modules/custom/mod.rs
+++ b/src/modules/custom/mod.rs
@@ -194,7 +194,7 @@ impl Module for CustomModule {
context: WidgetContext,
info: &ModuleInfo,
) -> Result> {
- let orientation = info.bar_position.get_orientation();
+ let orientation = info.bar_position.orientation();
let container = gtk::Box::builder().orientation(orientation).build();
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
@@ -236,7 +236,7 @@ impl Module for CustomModule {
if let Some(popup) = self.popup {
let custom_context = CustomWidgetContext {
tx: &tx,
- bar_orientation: info.bar_position.get_orientation(),
+ bar_orientation: info.bar_position.orientation(),
icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
};
diff --git a/src/modules/focused.rs b/src/modules/focused.rs
index ce288e1..8c32c2b 100644
--- a/src/modules/focused.rs
+++ b/src/modules/focused.rs
@@ -113,7 +113,7 @@ impl Module for FocusedModule {
) -> Result> {
let icon_theme = info.icon_theme;
- let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
+ let container = gtk::Box::new(info.bar_position.orientation(), 5);
let icon = gtk::Image::new();
if self.show_icon {
diff --git a/src/modules/launcher/item.rs b/src/modules/launcher/item.rs
index d312cd8..924b74a 100644
--- a/src/modules/launcher/item.rs
+++ b/src/modules/launcher/item.rs
@@ -166,8 +166,12 @@ impl ItemButton {
if appearance.show_icons {
let gtk_image = gtk::Image::new();
- let image =
- ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size);
+ let input = if item.app_id.is_empty() {
+ item.name.clone()
+ } else {
+ item.app_id.clone()
+ };
+ let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size);
if let Some(image) = image {
button.set_image(Some(>k_image));
button.set_always_show_image(true);
@@ -225,9 +229,7 @@ impl ItemButton {
try_send!(
tx,
- ModuleUpdateEvent::OpenPopupAt(
- button.geometry(bar_position.get_orientation())
- )
+ ModuleUpdateEvent::OpenPopupAt(button.geometry(bar_position.orientation()))
);
} else {
try_send!(tx, ModuleUpdateEvent::ClosePopup);
diff --git a/src/modules/launcher/mod.rs b/src/modules/launcher/mod.rs
index c231cc8..41a7931 100644
--- a/src/modules/launcher/mod.rs
+++ b/src/modules/launcher/mod.rs
@@ -301,7 +301,7 @@ impl Module for LauncherModule {
) -> crate::Result> {
let icon_theme = info.icon_theme;
- let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
+ let container = gtk::Box::new(info.bar_position.orientation(), 0);
{
let container = container.clone();
diff --git a/src/modules/mod.rs b/src/modules/mod.rs
index 0a9ac9b..f59e30a 100644
--- a/src/modules/mod.rs
+++ b/src/modules/mod.rs
@@ -1,4 +1,3 @@
-use std::cell::RefCell;
use std::fmt::Debug;
use std::rc::Rc;
use std::sync::Arc;
@@ -37,6 +36,8 @@ pub mod launcher;
pub mod music;
#[cfg(feature = "networkmanager")]
pub mod networkmanager;
+#[cfg(feature = "notifications")]
+pub mod notifications;
pub mod script;
#[cfg(feature = "sys_info")]
pub mod sysinfo;
@@ -44,6 +45,8 @@ pub mod sysinfo;
pub mod tray;
#[cfg(feature = "upower")]
pub mod upower;
+#[cfg(feature = "volume")]
+pub mod volume;
#[cfg(feature = "workspaces")]
pub mod workspaces;
@@ -217,7 +220,7 @@ pub fn create_module(
ironbar: Rc,
name: Option,
info: &ModuleInfo,
- popup: &Rc>,
+ popup: &Rc,
) -> Result>
where
TModule: Module,
@@ -253,7 +256,7 @@ where
.style_context()
.add_class(&format!("popup-{module_name}"));
- register_popup_content(popup, id, instance_name, popup_content);
+ popup.register_content(id, instance_name, popup_content);
}
setup_receiver(tx, ui_rx, popup.clone(), module_name, id);
@@ -261,16 +264,6 @@ where
Ok(module_parts)
}
-/// Registers the popup content with the popup.
-fn register_popup_content(
- popup: &Rc>,
- id: usize,
- name: String,
- popup_content: ModulePopupParts,
-) {
- popup.borrow_mut().register_content(id, name, popup_content);
-}
-
/// Sets up the bridge channel receiver
/// to pick up events from the controller, widget or popup.
///
@@ -279,7 +272,7 @@ fn register_popup_content(
fn setup_receiver(
tx: broadcast::Sender,
rx: mpsc::Receiver>,
- popup: Rc>,
+ popup: Rc,
name: &'static str,
id: usize,
) where
@@ -296,7 +289,6 @@ fn setup_receiver(
}
ModuleUpdateEvent::TogglePopup(button_id) => {
debug!("Toggling popup for {} [#{}]", name, id);
- let mut popup = popup.borrow_mut();
if popup.is_visible() {
popup.hide();
} else {
@@ -311,8 +303,6 @@ fn setup_receiver(
}
ModuleUpdateEvent::OpenPopup(button_id) => {
debug!("Opening popup for {} [#{}]", name, id);
-
- let mut popup = popup.borrow_mut();
popup.hide();
popup.show(id, button_id);
@@ -326,7 +316,6 @@ fn setup_receiver(
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
- let mut popup = popup.borrow_mut();
popup.hide();
popup.show_at(id, geometry);
@@ -338,8 +327,6 @@ fn setup_receiver(
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
-
- let mut popup = popup.borrow_mut();
popup.hide();
}
}
diff --git a/src/modules/notifications.rs b/src/modules/notifications.rs
new file mode 100644
index 0000000..232b586
--- /dev/null
+++ b/src/modules/notifications.rs
@@ -0,0 +1,190 @@
+use crate::clients::swaync;
+use crate::config::CommonConfig;
+use crate::gtk_helpers::IronbarGtkExt;
+use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
+use crate::{glib_recv, send_async, spawn, try_send};
+use gtk::prelude::*;
+use gtk::{Align, Button, Label, Overlay};
+use serde::Deserialize;
+use tokio::sync::mpsc::Receiver;
+use tracing::error;
+
+#[derive(Debug, Deserialize, Clone)]
+pub struct NotificationsModule {
+ #[serde(default = "crate::config::default_true")]
+ show_count: bool,
+
+ #[serde(default)]
+ icons: Icons,
+
+ #[serde(flatten)]
+ pub common: Option,
+}
+
+#[derive(Debug, Deserialize, Clone)]
+struct Icons {
+ #[serde(default = "default_icon_closed_none")]
+ closed_none: String,
+ #[serde(default = "default_icon_closed_some")]
+ closed_some: String,
+ #[serde(default = "default_icon_closed_dnd")]
+ closed_dnd: String,
+ #[serde(default = "default_icon_open_none")]
+ open_none: String,
+ #[serde(default = "default_icon_open_some")]
+ open_some: String,
+ #[serde(default = "default_icon_open_dnd")]
+ open_dnd: String,
+}
+
+impl Default for Icons {
+ fn default() -> Self {
+ Self {
+ closed_none: default_icon_closed_none(),
+ closed_some: default_icon_closed_some(),
+ closed_dnd: default_icon_closed_dnd(),
+ open_none: default_icon_open_none(),
+ open_some: default_icon_open_some(),
+ open_dnd: default_icon_open_dnd(),
+ }
+ }
+}
+
+fn default_icon_closed_none() -> String {
+ String::from("")
+}
+
+fn default_icon_closed_some() -> String {
+ String::from("")
+}
+
+fn default_icon_closed_dnd() -> String {
+ String::from("")
+}
+
+fn default_icon_open_none() -> String {
+ String::from("")
+}
+
+fn default_icon_open_some() -> String {
+ String::from("")
+}
+
+fn default_icon_open_dnd() -> String {
+ String::from("")
+}
+
+impl Icons {
+ fn icon(&self, value: &swaync::Event) -> &str {
+ match (value.cc_open, value.count > 0, value.dnd) {
+ (true, _, true) => &self.open_dnd,
+ (true, true, false) => &self.open_some,
+ (true, false, false) => &self.open_none,
+ (false, _, true) => &self.closed_dnd,
+ (false, true, false) => &self.closed_some,
+ (false, false, false) => &self.closed_none,
+ }
+ .as_str()
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum UiEvent {
+ ToggleVisibility,
+}
+
+impl Module for NotificationsModule {
+ type SendMessage = swaync::Event;
+ type ReceiveMessage = UiEvent;
+
+ fn name() -> &'static str {
+ "notifications"
+ }
+
+ fn spawn_controller(
+ &self,
+ _info: &ModuleInfo,
+ context: &WidgetContext,
+ mut rx: Receiver,
+ ) -> color_eyre::Result<()>
+ where
+ >::SendMessage: Clone,
+ {
+ let client = context.client::();
+
+ {
+ let client = client.clone();
+ let mut rx = client.subscribe();
+ let tx = context.tx.clone();
+
+ spawn(async move {
+ let initial_state = client.state().await;
+
+ match initial_state {
+ Ok(ev) => send_async!(tx, ModuleUpdateEvent::Update(ev)),
+ Err(err) => error!("{err:?}"),
+ };
+
+ while let Ok(ev) = rx.recv().await {
+ send_async!(tx, ModuleUpdateEvent::Update(ev));
+ }
+ });
+ }
+
+ spawn(async move {
+ while let Some(event) = rx.recv().await {
+ match event {
+ UiEvent::ToggleVisibility => client.toggle_visibility().await,
+ }
+ }
+ });
+
+ Ok(())
+ }
+
+ fn into_widget(
+ self,
+ context: WidgetContext,
+ _info: &ModuleInfo,
+ ) -> color_eyre::Result>
+ where
+ >::SendMessage: Clone,
+ {
+ let overlay = Overlay::new();
+ let button = Button::with_label(&self.icons.closed_none);
+ overlay.add(&button);
+
+ let label = Label::builder()
+ .label("0")
+ .halign(Align::End)
+ .valign(Align::Start)
+ .build();
+
+ if self.show_count {
+ label.add_class("count");
+ overlay.add_overlay(&label);
+ }
+
+ let ctx = context.controller_tx.clone();
+ button.connect_clicked(move |_| {
+ try_send!(ctx, UiEvent::ToggleVisibility);
+ });
+
+ {
+ let button = button.clone();
+
+ glib_recv!(context.subscribe(), ev => {
+ let icon = self.icons.icon(&ev);
+ button.set_label(icon);
+
+ label.set_label(&ev.count.to_string());
+ label.set_visible(self.show_count && ev.count > 0);
+ });
+ }
+
+ Ok(ModuleParts {
+ widget: overlay,
+ popup: None,
+ })
+ }
+}
diff --git a/src/modules/sysinfo.rs b/src/modules/sysinfo.rs
index 420d923..c815110 100644
--- a/src/modules/sysinfo.rs
+++ b/src/modules/sysinfo.rs
@@ -188,7 +188,7 @@ impl Module for SysInfoModule {
) -> Result> {
let re = Regex::new(r"\{([^}]+)}")?;
- let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
+ let container = gtk::Box::new(info.bar_position.orientation(), 10);
let mut labels = Vec::new();
diff --git a/src/modules/tray/mod.rs b/src/modules/tray/mod.rs
index b2830a8..cf4a2ff 100644
--- a/src/modules/tray/mod.rs
+++ b/src/modules/tray/mod.rs
@@ -8,7 +8,7 @@ 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 gtk::prelude::*;
+use gtk::{prelude::*, PackDirection};
use gtk::{IconTheme, MenuBar};
use interface::TrayMenu;
use serde::Deserialize;
@@ -18,10 +18,28 @@ use tokio::sync::mpsc;
#[derive(Debug, Deserialize, Clone)]
pub struct TrayModule {
+ #[serde(default, deserialize_with = "deserialize_orientation")]
+ pub direction: Option,
#[serde(flatten)]
pub common: Option,
}
+fn deserialize_orientation<'de, D>(deserializer: D) -> Result