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. + +![Notifications widget in its closed state showing 3 notifications](https://f.jstanger.dev/github/ironbar/notifications.png) + +> [!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). + +![The volume widget, with its popup open. A single stream is playing audio.](https://f.jstanger.dev/github/ironbar/volume.png) + +## 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, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + value + .map(|v| match v.as_str() { + "left_to_right" => Ok(PackDirection::Ltr), + "right_to_left" => Ok(PackDirection::Rtl), + "top_to_bottom" => Ok(PackDirection::Ttb), + "bottom_to_top" => Ok(PackDirection::Btt), + _ => Err(serde::de::Error::custom("invalid value for orientation")), + }) + .transpose() +} + impl Module for TrayModule { type SendMessage = NotifierItemMessage; type ReceiveMessage = NotifierItemCommand; @@ -70,6 +88,17 @@ impl Module for TrayModule { ) -> Result> { let container = MenuBar::new(); + let direction = self.direction.unwrap_or( + if info.bar_position.orientation() == gtk::Orientation::Vertical { + PackDirection::Ttb + } else { + PackDirection::Ltr + }, + ); + + container.set_pack_direction(direction); + container.set_child_pack_direction(direction); + { let container = container.clone(); let mut menus = HashMap::new(); diff --git a/src/modules/volume.rs b/src/modules/volume.rs new file mode 100644 index 0000000..bb33337 --- /dev/null +++ b/src/modules/volume.rs @@ -0,0 +1,426 @@ +use crate::clients::volume::{self, Event}; +use crate::config::CommonConfig; +use crate::gtk_helpers::IronbarGtkExt; +use crate::modules::{ + Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext, +}; +use crate::{glib_recv, lock, send_async, spawn, try_send}; +use glib::Propagation; +use gtk::pango::EllipsizeMode; +use gtk::prelude::*; +use gtk::{Button, CellRendererText, ComboBoxText, Label, Orientation, Scale, ToggleButton}; +use serde::Deserialize; +use std::collections::HashMap; +use tokio::sync::mpsc; + +#[derive(Debug, Clone, Deserialize)] +pub struct VolumeModule { + #[serde(default = "default_format")] + format: String, + + #[serde(default = "default_max_volume")] + max_volume: f64, + + #[serde(default)] + icons: Icons, + + #[serde(flatten)] + pub common: Option, +} + +fn default_format() -> String { + String::from("{icon} {percentage}%") +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Icons { + #[serde(default = "default_icon_volume_high")] + volume_high: String, + #[serde(default = "default_icon_volume_medium")] + volume_medium: String, + #[serde(default = "default_icon_volume_low")] + volume_low: String, + #[serde(default = "default_icon_muted")] + muted: String, +} + +impl Icons { + fn volume_icon(&self, volume_percent: f64) -> &str { + match volume_percent as u32 { + 0..=33 => &self.volume_low, + 34..=66 => &self.volume_medium, + 67.. => &self.volume_high, + } + } +} + +impl Default for Icons { + fn default() -> Self { + Self { + volume_high: default_icon_volume_high(), + volume_medium: default_icon_volume_medium(), + volume_low: default_icon_volume_low(), + muted: default_icon_muted(), + } + } +} + +const fn default_max_volume() -> f64 { + 100.0 +} + +fn default_icon_volume_high() -> String { + String::from("󰕾") +} + +fn default_icon_volume_medium() -> String { + String::from("󰖀") +} + +fn default_icon_volume_low() -> String { + String::from("󰕿") +} + +fn default_icon_muted() -> String { + String::from("󰝟") +} + +#[derive(Debug, Clone)] +pub enum Update { + SinkChange(String), + SinkVolume(String, f64), + SinkMute(String, bool), + + InputVolume(u32, f64), + InputMute(u32, bool), +} + +impl Module