mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-04-19 19:34:24 +02:00
Merge branch 'master' into feat/networkmanager
This commit is contained in:
commit
9031438f56
42 changed files with 2378 additions and 383 deletions
97
.github/workflows/build.yml
vendored
97
.github/workflows/build.yml
vendored
|
@ -9,18 +9,24 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
RUSTFLAGS: '-Dwarnings'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
rustfmt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
name: 'Formatting'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- name: Check formatting
|
||||||
with:
|
run: cargo fmt --check
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
|
clippy-base:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: 'Clippy (Base features)'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
name: Cache dependencies
|
name: Cache dependencies
|
||||||
|
@ -28,22 +34,47 @@ jobs:
|
||||||
- name: Install build deps
|
- name: Install build deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: Clippy
|
||||||
run: cargo fmt --check
|
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)
|
clippy-all:
|
||||||
uses: actions-rs/clippy-check@v1
|
runs-on: ubuntu-latest
|
||||||
with:
|
name: 'Clippy (All features)'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
steps:
|
||||||
args: --all-features
|
- 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
|
- name: Build
|
||||||
run: cargo build --verbose
|
run: cargo build --verbose
|
||||||
|
@ -51,28 +82,4 @@ jobs:
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
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
|
|
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
- name: Install build deps
|
- name: Install build deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: Update CHANGELOG
|
||||||
id: changelog
|
id: changelog
|
||||||
|
|
139
Cargo.lock
generated
139
Cargo.lock
generated
|
@ -168,7 +168,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"parking",
|
"parking",
|
||||||
"polling 2.7.0",
|
"polling 2.7.0",
|
||||||
"rustix 0.37.11",
|
"rustix 0.37.27",
|
||||||
"slab",
|
"slab",
|
||||||
"socket2 0.4.9",
|
"socket2 0.4.9",
|
||||||
"waker-fn",
|
"waker-fn",
|
||||||
|
@ -206,7 +206,7 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"event-listener",
|
"event-listener",
|
||||||
"futures-lite 1.13.0",
|
"futures-lite 1.13.0",
|
||||||
"rustix 0.37.11",
|
"rustix 0.37.27",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
@ -429,9 +429,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.34"
|
version = "0.4.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
|
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
@ -444,9 +444,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.0"
|
version = "4.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
|
checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
@ -454,9 +454,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.0"
|
version = "4.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
|
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
@ -1352,9 +1352,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.17"
|
version = "0.3.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f"
|
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
|
@ -1362,7 +1362,7 @@ dependencies = [
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"indexmap 1.9.3",
|
"indexmap 2.2.5",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -1579,9 +1579,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.3"
|
version = "2.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
|
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.1",
|
"hashbrown 0.14.1",
|
||||||
|
@ -1649,7 +1649,8 @@ dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"gtk-layer-shell",
|
"gtk-layer-shell",
|
||||||
"hyprland",
|
"hyprland",
|
||||||
"indexmap 2.2.3",
|
"indexmap 2.2.5",
|
||||||
|
"libpulse-binding",
|
||||||
"mpd-utils",
|
"mpd-utils",
|
||||||
"mpris",
|
"mpris",
|
||||||
"nix 0.27.1",
|
"nix 0.27.1",
|
||||||
|
@ -1756,6 +1757,33 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "link-cplusplus"
|
name = "link-cplusplus"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
|
@ -1767,9 +1795,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.3.1"
|
version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
|
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
|
@ -1861,9 +1889,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.9"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
@ -2009,6 +2037,17 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -2045,11 +2084,11 @@ checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.50"
|
version = "0.10.64"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e30d8bc91859781f0a943411186324d580f2bbeb71b452fe91ae344806af3f1"
|
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 2.4.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -2077,9 +2116,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.85"
|
version = "0.9.101"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d3d193fb1488ad46ffe3aaabc912cc931d02ee8518fe2959aea8ef52718b0c0"
|
checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -2489,9 +2528,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.11.24"
|
version = "0.11.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
|
checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.0",
|
"base64 0.21.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -2555,15 +2594,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.37.11"
|
version = "0.37.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77"
|
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"errno",
|
"errno",
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.3.1",
|
"linux-raw-sys 0.3.8",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2668,18 +2707,18 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.196"
|
version = "1.0.197"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.196"
|
version = "1.0.197"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.35",
|
"quote 1.0.35",
|
||||||
|
@ -2688,9 +2727,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.113"
|
version = "1.0.114"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
@ -2991,20 +3030,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-configuration"
|
name = "system-configuration"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 2.4.0",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"system-configuration-sys",
|
"system-configuration-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-configuration-sys"
|
name = "system-configuration-sys"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -3050,7 +3089,7 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand 1.9.0",
|
"fastrand 1.9.0",
|
||||||
"redox_syscall 0.3.5",
|
"redox_syscall 0.3.5",
|
||||||
"rustix 0.37.11",
|
"rustix 0.37.27",
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3228,7 +3267,7 @@ version = "0.19.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.2.3",
|
"indexmap 2.2.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
|
@ -3241,7 +3280,7 @@ version = "0.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.2.3",
|
"indexmap 2.2.5",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
@ -3421,9 +3460,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.8"
|
version = "0.2.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6"
|
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "upower_dbus"
|
name = "upower_dbus"
|
||||||
|
@ -3505,9 +3544,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"same-file",
|
"same-file",
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
|
@ -3999,9 +4038,9 @@ checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zbus"
|
name = "zbus"
|
||||||
version = "3.15.0"
|
version = "3.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c45d06ae3b0f9ba1fb2671268b975557d8f5a84bb5ec6e43964f87e763d8bca8"
|
checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"async-executor",
|
"async-executor",
|
||||||
|
@ -4041,9 +4080,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zbus_macros"
|
name = "zbus_macros"
|
||||||
version = "3.15.0"
|
version = "3.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4a1ba45ed0ad344b85a2bb5a1fe9830aed23d67812ea39a586e7d0136439c7d"
|
checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 1.3.1",
|
"proc-macro-crate 1.3.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
26
Cargo.toml
26
Cargo.toml
|
@ -20,9 +20,11 @@ default = [
|
||||||
"launcher",
|
"launcher",
|
||||||
"music+all",
|
"music+all",
|
||||||
"networkmanager",
|
"networkmanager",
|
||||||
|
"notifications",
|
||||||
"sys_info",
|
"sys_info",
|
||||||
"tray",
|
"tray",
|
||||||
"upower",
|
"upower",
|
||||||
|
"volume",
|
||||||
"workspaces+all"
|
"workspaces+all"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -59,12 +61,16 @@ music = ["regex"]
|
||||||
|
|
||||||
networkmanager = ["futures-lite", "zbus"]
|
networkmanager = ["futures-lite", "zbus"]
|
||||||
|
|
||||||
|
notifications = ["zbus"]
|
||||||
|
|
||||||
sys_info = ["sysinfo", "regex"]
|
sys_info = ["sysinfo", "regex"]
|
||||||
|
|
||||||
tray = ["system-tray"]
|
tray = ["system-tray"]
|
||||||
|
|
||||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||||
|
|
||||||
|
volume = ["libpulse-binding"]
|
||||||
|
|
||||||
workspaces = ["futures-util"]
|
workspaces = ["futures-util"]
|
||||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
||||||
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
||||||
|
@ -90,10 +96,10 @@ tracing-error = "0.2.0"
|
||||||
tracing-appender = "0.2.3"
|
tracing-appender = "0.2.3"
|
||||||
strip-ansi-escapes = "0.2.0"
|
strip-ansi-escapes = "0.2.0"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
serde = { version = "1.0.196", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
indexmap = "2.2.3"
|
indexmap = "2.2.5"
|
||||||
dirs = "5.0.1"
|
dirs = "5.0.1"
|
||||||
walkdir = "2.4.0"
|
walkdir = "2.5.0"
|
||||||
notify = { version = "6.1.1", default-features = false }
|
notify = { version = "6.1.1", default-features = false }
|
||||||
wayland-client = "0.31.1"
|
wayland-client = "0.31.1"
|
||||||
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] }
|
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] }
|
||||||
|
@ -106,19 +112,19 @@ ctrlc = "3.4.2"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
|
|
||||||
# cli
|
# cli
|
||||||
clap = { version = "4.5.0", optional = true, features = ["derive"] }
|
clap = { version = "4.5.2", optional = true, features = ["derive"] }
|
||||||
|
|
||||||
# ipc
|
# ipc
|
||||||
serde_json = { version = "1.0.113", optional = true }
|
serde_json = { version = "1.0.114", optional = true }
|
||||||
|
|
||||||
# http
|
# http
|
||||||
reqwest = { version = "0.11.24", optional = true }
|
reqwest = { version = "0.11.25", optional = true }
|
||||||
|
|
||||||
# clipboard
|
# clipboard
|
||||||
nix = { version = "0.27.1", optional = true, features = ["event"] }
|
nix = { version = "0.27.1", optional = true, features = ["event"] }
|
||||||
|
|
||||||
# clock
|
# clock
|
||||||
chrono = { version = "0.4.34", optional = true, features = ["unstable-locales"] }
|
chrono = { version = "0.4.35", optional = true, features = ["unstable-locales"] }
|
||||||
|
|
||||||
# music
|
# music
|
||||||
mpd-utils = { version = "0.2.0", optional = true }
|
mpd-utils = { version = "0.2.0", optional = true }
|
||||||
|
@ -133,6 +139,10 @@ system-tray = { version = "0.1.5", optional = true }
|
||||||
# upower
|
# upower
|
||||||
upower_dbus = { version = "0.3.2", optional = true }
|
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
|
# workspaces
|
||||||
swayipc-async = { version = "2.0.1", optional = true }
|
swayipc-async = { version = "2.0.1", optional = true }
|
||||||
hyprland = { version = "0.3.13", features = ["silent"], 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",
|
"std",
|
||||||
], optional = true } # music, sys_info
|
], optional = true } # music, sys_info
|
||||||
futures-lite = { version = "2.2.0", optional = true } # networkmanager, upower
|
futures-lite = { version = "2.2.0", optional = true } # networkmanager, upower
|
||||||
zbus = { version = "3.15.0", optional = true } # networkmanager, upower
|
zbus = { version = "3.15.2", optional = true } # networkmanager, notifications, upower
|
||||||
|
|
|
@ -127,7 +127,8 @@ A flake is included with the repo which can be used with Home Manager.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
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
|
### 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
|
- [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
|
- [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
|
- [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.
|
|
@ -20,6 +20,8 @@ You also need rust; only the latest stable version is supported.
|
||||||
pacman -S gtk3 gtk-layer-shell
|
pacman -S gtk3 gtk-layer-shell
|
||||||
# for http support
|
# for http support
|
||||||
pacman -S openssl
|
pacman -S openssl
|
||||||
|
# for volume support
|
||||||
|
pacman -S libpulse
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ubuntu/Debian
|
### Ubuntu/Debian
|
||||||
|
@ -28,6 +30,8 @@ pacman -S openssl
|
||||||
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
|
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
|
||||||
# for http support
|
# for http support
|
||||||
apt install libssl-dev
|
apt install libssl-dev
|
||||||
|
# for volume support
|
||||||
|
apt install libpulse-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora
|
### Fedora
|
||||||
|
@ -36,6 +40,8 @@ apt install libssl-dev
|
||||||
dnf install gtk3-devel gtk-layer-shell-devel
|
dnf install gtk3-devel gtk-layer-shell-devel
|
||||||
# for http support
|
# for http support
|
||||||
dnf install openssl-devel
|
dnf install openssl-devel
|
||||||
|
# for volume support
|
||||||
|
dnf install libpulseaudio-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
@ -81,6 +87,7 @@ cargo build --release --no-default-features \
|
||||||
| sys_info | Enables the `sys_info` module. |
|
| sys_info | Enables the `sys_info` module. |
|
||||||
| tray | Enables the `tray` module. |
|
| tray | Enables the `tray` module. |
|
||||||
| upower | Enables the `upower` module. |
|
| upower | Enables the `upower` module. |
|
||||||
|
| volume | Enables the `volume` module. |
|
||||||
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
||||||
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
||||||
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
||||||
|
|
|
@ -30,8 +30,10 @@
|
||||||
- [Label](label)
|
- [Label](label)
|
||||||
- [Launcher](launcher)
|
- [Launcher](launcher)
|
||||||
- [Music](music)
|
- [Music](music)
|
||||||
|
- [Notifications](notifications)
|
||||||
- [Script](script)
|
- [Script](script)
|
||||||
- [Sys_Info](sys-info)
|
- [Sys_Info](sys-info)
|
||||||
- [Tray](tray)
|
- [Tray](tray)
|
||||||
- [Upower](upower)
|
- [Upower](upower)
|
||||||
|
- [Volume](volume)
|
||||||
- [Workspaces](workspaces)
|
- [Workspaces](workspaces)
|
||||||
|
|
116
docs/modules/Notifications.md
Normal file
116
docs/modules/Notifications.md
Normal file
|
@ -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. |
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>JSON</summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"end": [
|
||||||
|
{
|
||||||
|
"type": "notifications",
|
||||||
|
"show_count": true,
|
||||||
|
"icons": {
|
||||||
|
"closed_none": "",
|
||||||
|
"closed_some": "",
|
||||||
|
"closed_dnd": "",
|
||||||
|
"open_none": "",
|
||||||
|
"open_some": "",
|
||||||
|
"open_dnd": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>TOML</summary>
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[end]]
|
||||||
|
type = "notifications"
|
||||||
|
show_count = true
|
||||||
|
|
||||||
|
[[end.icons]]
|
||||||
|
closed_none = ""
|
||||||
|
closed_some = ""
|
||||||
|
closed_dnd = ""
|
||||||
|
open_none = ""
|
||||||
|
open_some = ""
|
||||||
|
open_dnd = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>YAML</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
end:
|
||||||
|
- type: notifications
|
||||||
|
show_count: true
|
||||||
|
icons:
|
||||||
|
closed_none:
|
||||||
|
closed_some:
|
||||||
|
closed_dnd:
|
||||||
|
open_none:
|
||||||
|
open_some:
|
||||||
|
open_dnd:
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Corn</summary>
|
||||||
|
|
||||||
|
```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 = ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 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).
|
|
@ -6,7 +6,10 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
|
||||||
|
|
||||||
> Type: `tray`
|
> 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` |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
@ -15,7 +18,8 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
|
||||||
{
|
{
|
||||||
"end": [
|
"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
|
```toml
|
||||||
[[end]]
|
[[end]]
|
||||||
type = "tray"
|
type = "tray"
|
||||||
|
direction = "top_to_bottom"
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -39,6 +44,7 @@ type = "tray"
|
||||||
```yaml
|
```yaml
|
||||||
end:
|
end:
|
||||||
- type: "tray"
|
- type: "tray"
|
||||||
|
direction: "top_to_bottom"
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -49,7 +55,10 @@ end:
|
||||||
```corn
|
```corn
|
||||||
{
|
{
|
||||||
end = [
|
end = [
|
||||||
{ type = "tray" }
|
{
|
||||||
|
type = "tray"
|
||||||
|
direction = "top_to_bottom"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -63,4 +72,4 @@ end:
|
||||||
| `.tray` | Tray widget box |
|
| `.tray` | Tray widget box |
|
||||||
| `.tray .item` | Tray icon button |
|
| `.tray .item` | Tray icon button |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
For more information on styling, please see the [styling guide](styling-guide).
|
||||||
|
|
128
docs/modules/Volume.md
Normal file
128
docs/modules/Volume.md
Normal file
|
@ -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. |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>JSON</summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"end": [
|
||||||
|
{
|
||||||
|
"type": "volume",
|
||||||
|
"format": "{icon} {percentage}%",
|
||||||
|
"max_volume": 100,
|
||||||
|
"icons": {
|
||||||
|
"volume_high": "",
|
||||||
|
"volume_medium": "",
|
||||||
|
"volume_low": "",
|
||||||
|
"muted": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>TOML</summary>
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[end]]
|
||||||
|
type = "volume"
|
||||||
|
format = "{icon} {percentage}%"
|
||||||
|
max_volume = 100
|
||||||
|
|
||||||
|
[[end.icons]]
|
||||||
|
volume_high = ""
|
||||||
|
volume_medium = ""
|
||||||
|
volume_low = ""
|
||||||
|
muted = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>YAML</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
end:
|
||||||
|
- type: "volume"
|
||||||
|
format: "{icon} {percentage}%"
|
||||||
|
max_volume: 100
|
||||||
|
icons:
|
||||||
|
volume_high: ""
|
||||||
|
volume_medium: ""
|
||||||
|
volume_low: ""
|
||||||
|
muted: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Corn</summary>
|
||||||
|
|
||||||
|
```corn
|
||||||
|
{
|
||||||
|
end = [
|
||||||
|
{
|
||||||
|
type = "volume"
|
||||||
|
format = "{icon} {percentage}%"
|
||||||
|
max_volume = 100
|
||||||
|
icons.volume_high = ""
|
||||||
|
icons.volume_medium = ""
|
||||||
|
icons.volume_low = ""
|
||||||
|
icons.muted = ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 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).
|
|
@ -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_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" }
|
$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 = {
|
$sys_info = {
|
||||||
type = "sys_info"
|
type = "sys_info"
|
||||||
|
|
||||||
|
@ -67,6 +79,16 @@ let {
|
||||||
|
|
||||||
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
|
$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}}" }
|
$label = { type = "label" label = "random num: {{500:echo FIXME}}" }
|
||||||
|
|
||||||
// -- begin custom --
|
// -- begin custom --
|
||||||
|
@ -100,7 +122,7 @@ let {
|
||||||
// -- end custom --
|
// -- end custom --
|
||||||
|
|
||||||
$left = [ $workspaces $launcher $label ]
|
$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 {
|
in {
|
||||||
anchor_to_edges = true
|
anchor_to_edges = true
|
||||||
|
|
|
@ -1,30 +1,67 @@
|
||||||
{
|
{
|
||||||
"anchor_to_edges": true,
|
"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": [
|
"end": [
|
||||||
{
|
{
|
||||||
|
"type": "music",
|
||||||
|
"player_type": "mpd",
|
||||||
"music_dir": "/home/jake/Music",
|
"music_dir": "/home/jake/Music",
|
||||||
"player_type": "mpd",
|
|
||||||
"truncate": {
|
"truncate": {
|
||||||
"max_length": 100,
|
"mode": "end",
|
||||||
"mode": "end"
|
"max_length": 100
|
||||||
},
|
}
|
||||||
"type": "music"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"host": "chloe:6600",
|
"type": "music",
|
||||||
"player_type": "mpd",
|
"player_type": "mpd",
|
||||||
"truncate": "end",
|
"host": "chloe:6600",
|
||||||
"type": "music"
|
"truncate": "end"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"type": "script",
|
||||||
"cmd": "/home/jake/bin/phone-battery",
|
"cmd": "/home/jake/bin/phone-battery",
|
||||||
"show_if": {
|
"show_if": {
|
||||||
"cmd": "/home/jake/bin/phone-connected",
|
"cmd": "/home/jake/bin/phone-connected",
|
||||||
"interval": 500
|
"interval": 500
|
||||||
},
|
}
|
||||||
"type": "script"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"type": "sys_info",
|
||||||
|
"interval": {
|
||||||
|
"memory": 30,
|
||||||
|
"cpu": 1,
|
||||||
|
"temps": 5,
|
||||||
|
"disks": 300,
|
||||||
|
"networks": 3
|
||||||
|
},
|
||||||
"format": [
|
"format": [
|
||||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||||
|
@ -33,103 +70,89 @@
|
||||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||||
" {load_average:1} | {load_average:5} | {load_average:15}",
|
" {load_average:1} | {load_average:5} | {load_average:15}",
|
||||||
" {uptime}"
|
" {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,
|
"max_items": 3,
|
||||||
"truncate": {
|
"truncate": {
|
||||||
"length": 50,
|
"mode": "end",
|
||||||
"mode": "end"
|
"length": 50
|
||||||
},
|
}
|
||||||
"type": "clipboard"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"type": "custom",
|
||||||
|
"class": "power-menu",
|
||||||
"bar": [
|
"bar": [
|
||||||
{
|
{
|
||||||
"label": "",
|
"type": "button",
|
||||||
"name": "power-btn",
|
"name": "power-btn",
|
||||||
"on_click": "popup:toggle",
|
"label": "",
|
||||||
"type": "button"
|
"on_click": "popup:toggle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"class": "power-menu",
|
|
||||||
"popup": [
|
"popup": [
|
||||||
{
|
{
|
||||||
"orientation": "vertical",
|
|
||||||
"type": "box",
|
"type": "box",
|
||||||
|
"orientation": "vertical",
|
||||||
"widgets": [
|
"widgets": [
|
||||||
{
|
{
|
||||||
"label": "Power menu",
|
"type": "label",
|
||||||
"name": "header",
|
"name": "header",
|
||||||
"type": "label"
|
"label": "Power menu"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "box",
|
"type": "box",
|
||||||
"widgets": [
|
"widgets": [
|
||||||
{
|
{
|
||||||
|
"type": "button",
|
||||||
"class": "power-btn",
|
"class": "power-btn",
|
||||||
"label": "<span font-size='40pt'></span>",
|
"label": "<span font-size='40pt'></span>",
|
||||||
"on_click": "!shutdown now",
|
"on_click": "!shutdown now"
|
||||||
"type": "button"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"type": "button",
|
||||||
"class": "power-btn",
|
"class": "power-btn",
|
||||||
"label": "<span font-size='40pt'></span>",
|
"label": "<span font-size='40pt'></span>",
|
||||||
"on_click": "!reboot",
|
"on_click": "!reboot"
|
||||||
"type": "button"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
"type": "label",
|
||||||
"name": "uptime",
|
"name": "uptime",
|
||||||
"type": "label"
|
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
"tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||||
"type": "custom"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "clock"
|
"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": [
|
"type": "notifications",
|
||||||
"firefox",
|
"show_count": true,
|
||||||
"discord",
|
"icons": {
|
||||||
"steam"
|
"closed_none": "",
|
||||||
],
|
"closed_some": "",
|
||||||
"show_icons": true,
|
"closed_dnd": "",
|
||||||
"show_names": false,
|
"open_none": "",
|
||||||
"type": "launcher"
|
"open_some": "",
|
||||||
},
|
"open_dnd": ""
|
||||||
{
|
}
|
||||||
"label": "random num: {{500:echo FIXME}}",
|
|
||||||
"type": "label"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,57 @@
|
||||||
anchor_to_edges = true
|
anchor_to_edges = true
|
||||||
icon_theme = "Paper"
|
|
||||||
position = "bottom"
|
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]]
|
[[end]]
|
||||||
music_dir = "/home/jake/Music"
|
|
||||||
player_type = "mpd"
|
|
||||||
type = "music"
|
type = "music"
|
||||||
|
player_type = "mpd"
|
||||||
|
music_dir = "/home/jake/Music"
|
||||||
|
|
||||||
[end.truncate]
|
[end.truncate]
|
||||||
max_length = 100
|
|
||||||
mode = "end"
|
mode = "end"
|
||||||
|
max_length = 100
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
host = "chloe:6600"
|
|
||||||
player_type = "mpd"
|
|
||||||
truncate = "end"
|
|
||||||
type = "music"
|
type = "music"
|
||||||
|
player_type = "mpd"
|
||||||
|
host = "chloe:6600"
|
||||||
|
truncate = "end"
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
cmd = "/home/jake/bin/phone-battery"
|
|
||||||
type = "script"
|
type = "script"
|
||||||
|
cmd = "/home/jake/bin/phone-battery"
|
||||||
|
|
||||||
[end.show_if]
|
[end.show_if]
|
||||||
cmd = "/home/jake/bin/phone-connected"
|
cmd = "/home/jake/bin/phone-connected"
|
||||||
interval = 500
|
interval = 500
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
|
type = "sys_info"
|
||||||
format = [
|
format = [
|
||||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||||
|
@ -35,88 +61,85 @@ format = [
|
||||||
" {load_average:1} | {load_average:5} | {load_average:15}",
|
" {load_average:1} | {load_average:5} | {load_average:15}",
|
||||||
" {uptime}",
|
" {uptime}",
|
||||||
]
|
]
|
||||||
type = "sys_info"
|
|
||||||
|
|
||||||
[end.interval]
|
[end.interval]
|
||||||
cpu = 1
|
|
||||||
disks = 300
|
|
||||||
memory = 30
|
memory = 30
|
||||||
networks = 3
|
cpu = 1
|
||||||
temps = 5
|
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]]
|
[[end]]
|
||||||
max_items = 3
|
|
||||||
type = "clipboard"
|
type = "clipboard"
|
||||||
|
max_items = 3
|
||||||
|
|
||||||
[end.truncate]
|
[end.truncate]
|
||||||
length = 50
|
|
||||||
mode = "end"
|
mode = "end"
|
||||||
|
length = 50
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
|
type = "custom"
|
||||||
class = "power-menu"
|
class = "power-menu"
|
||||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||||
type = "custom"
|
|
||||||
|
|
||||||
[[end.bar]]
|
[[end.bar]]
|
||||||
label = ""
|
|
||||||
name = "power-btn"
|
|
||||||
on_click = "popup:toggle"
|
|
||||||
type = "button"
|
type = "button"
|
||||||
|
name = "power-btn"
|
||||||
|
label = ""
|
||||||
|
on_click = "popup:toggle"
|
||||||
|
|
||||||
[[end.popup]]
|
[[end.popup]]
|
||||||
orientation = "vertical"
|
|
||||||
type = "box"
|
type = "box"
|
||||||
|
orientation = "vertical"
|
||||||
|
|
||||||
[[end.popup.widgets]]
|
[[end.popup.widgets]]
|
||||||
label = "Power menu"
|
|
||||||
name = "header"
|
|
||||||
type = "label"
|
type = "label"
|
||||||
|
name = "header"
|
||||||
|
label = "Power menu"
|
||||||
|
|
||||||
[[end.popup.widgets]]
|
[[end.popup.widgets]]
|
||||||
type = "box"
|
type = "box"
|
||||||
|
|
||||||
[[end.popup.widgets.widgets]]
|
[[end.popup.widgets.widgets]]
|
||||||
|
type = "button"
|
||||||
class = "power-btn"
|
class = "power-btn"
|
||||||
label = "<span font-size='40pt'></span>"
|
label = "<span font-size='40pt'></span>"
|
||||||
on_click = "!shutdown now"
|
on_click = "!shutdown now"
|
||||||
type = "button"
|
|
||||||
|
|
||||||
[[end.popup.widgets.widgets]]
|
[[end.popup.widgets.widgets]]
|
||||||
|
type = "button"
|
||||||
class = "power-btn"
|
class = "power-btn"
|
||||||
label = "<span font-size='40pt'></span>"
|
label = "<span font-size='40pt'></span>"
|
||||||
on_click = "!reboot"
|
on_click = "!reboot"
|
||||||
type = "button"
|
|
||||||
|
|
||||||
[[end.popup.widgets]]
|
[[end.popup.widgets]]
|
||||||
label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
|
||||||
name = "uptime"
|
|
||||||
type = "label"
|
type = "label"
|
||||||
|
name = "uptime"
|
||||||
|
label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
type = "clock"
|
type = "clock"
|
||||||
|
|
||||||
[[start]]
|
[[end]]
|
||||||
all_monitors = false
|
type = "notifications"
|
||||||
type = "workspaces"
|
show_count = true
|
||||||
|
|
||||||
[start.name_map]
|
[end.icons]
|
||||||
1 = ""
|
closed_none = ""
|
||||||
2 = "icon:firefox"
|
closed_some = ""
|
||||||
3 = ""
|
closed_dnd = ""
|
||||||
Code = ""
|
open_none = ""
|
||||||
Games = "icon:steam"
|
open_some = ""
|
||||||
|
open_dnd = ""
|
||||||
[[start]]
|
|
||||||
favorites = [
|
|
||||||
"firefox",
|
|
||||||
"discord",
|
|
||||||
"steam",
|
|
||||||
]
|
|
||||||
show_icons = true
|
|
||||||
show_names = false
|
|
||||||
type = "launcher"
|
|
||||||
|
|
||||||
[[start]]
|
|
||||||
label = "random num: {{500:echo FIXME}}"
|
|
||||||
type = "label"
|
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,48 @@
|
||||||
anchor_to_edges: true
|
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:
|
end:
|
||||||
- music_dir: /home/jake/Music
|
- type: music
|
||||||
player_type: mpd
|
player_type: mpd
|
||||||
|
music_dir: /home/jake/Music
|
||||||
truncate:
|
truncate:
|
||||||
max_length: 100
|
|
||||||
mode: end
|
mode: end
|
||||||
type: music
|
max_length: 100
|
||||||
- host: chloe:6600
|
- type: music
|
||||||
player_type: mpd
|
player_type: mpd
|
||||||
|
host: chloe:6600
|
||||||
truncate: end
|
truncate: end
|
||||||
type: music
|
- type: script
|
||||||
- cmd: /home/jake/bin/phone-battery
|
cmd: /home/jake/bin/phone-battery
|
||||||
show_if:
|
show_if:
|
||||||
cmd: /home/jake/bin/phone-connected
|
cmd: /home/jake/bin/phone-connected
|
||||||
interval: 500
|
interval: 500
|
||||||
type: script
|
- type: sys_info
|
||||||
- format:
|
interval:
|
||||||
|
memory: 30
|
||||||
|
cpu: 1
|
||||||
|
temps: 5
|
||||||
|
disks: 300
|
||||||
|
networks: 3
|
||||||
|
format:
|
||||||
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
||||||
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
||||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||||
|
@ -23,65 +50,55 @@ end:
|
||||||
- {net_down:enp39s0} / {net_up:enp39s0} Mbps
|
- {net_down:enp39s0} / {net_up:enp39s0} Mbps
|
||||||
- {load_average:1} | {load_average:5} | {load_average:15}
|
- {load_average:1} | {load_average:5} | {load_average:15}
|
||||||
- {uptime}
|
- {uptime}
|
||||||
interval:
|
- type: volume
|
||||||
cpu: 1
|
format: '{icon} {volume}%'
|
||||||
disks: 300
|
max_volume: 100
|
||||||
memory: 30
|
icons:
|
||||||
networks: 3
|
volume_high:
|
||||||
temps: 5
|
volume_medium:
|
||||||
type: sys_info
|
volume_low:
|
||||||
- max_items: 3
|
muted:
|
||||||
|
- type: clipboard
|
||||||
|
max_items: 3
|
||||||
truncate:
|
truncate:
|
||||||
length: 50
|
|
||||||
mode: end
|
mode: end
|
||||||
type: clipboard
|
length: 50
|
||||||
- bar:
|
- type: custom
|
||||||
- label:
|
|
||||||
name: power-btn
|
|
||||||
on_click: popup:toggle
|
|
||||||
type: button
|
|
||||||
class: power-menu
|
class: power-menu
|
||||||
|
bar:
|
||||||
|
- type: button
|
||||||
|
name: power-btn
|
||||||
|
label:
|
||||||
|
on_click: popup:toggle
|
||||||
popup:
|
popup:
|
||||||
- orientation: vertical
|
- type: box
|
||||||
type: box
|
orientation: vertical
|
||||||
widgets:
|
widgets:
|
||||||
- label: Power menu
|
- type: label
|
||||||
name: header
|
name: header
|
||||||
type: label
|
label: Power menu
|
||||||
- type: box
|
- type: box
|
||||||
widgets:
|
widgets:
|
||||||
- class: power-btn
|
- type: button
|
||||||
|
class: power-btn
|
||||||
label: <span font-size='40pt'></span>
|
label: <span font-size='40pt'></span>
|
||||||
on_click: '!shutdown now'
|
on_click: '!shutdown now'
|
||||||
type: button
|
- type: button
|
||||||
- class: power-btn
|
class: power-btn
|
||||||
label: <span font-size='40pt'></span>
|
label: <span font-size='40pt'></span>
|
||||||
on_click: '!reboot'
|
on_click: '!reboot'
|
||||||
type: button
|
- type: label
|
||||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
|
||||||
name: uptime
|
name: uptime
|
||||||
type: label
|
label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||||
type: custom
|
|
||||||
- type: clock
|
- type: clock
|
||||||
icon_theme: Paper
|
- type: notifications
|
||||||
position: bottom
|
show_count: true
|
||||||
start:
|
icons:
|
||||||
- all_monitors: false
|
closed_none:
|
||||||
name_map:
|
closed_some:
|
||||||
'1':
|
closed_dnd:
|
||||||
'2': icon:firefox
|
open_none:
|
||||||
'3':
|
open_some:
|
||||||
Code:
|
open_dnd:
|
||||||
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
|
|
||||||
|
|
||||||
|
|
|
@ -150,6 +150,20 @@ scale trough {
|
||||||
border-radius: 100%;
|
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 -- */
|
||||||
|
|
||||||
.script {
|
.script {
|
||||||
|
@ -174,6 +188,11 @@ scale trough {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -- volume -- */
|
||||||
|
|
||||||
|
.popup-volume .device-box {
|
||||||
|
border-right: 1px solid @color_border;
|
||||||
|
}
|
||||||
|
|
||||||
/* -- workspaces -- */
|
/* -- workspaces -- */
|
||||||
|
|
||||||
|
|
24
flake.lock
generated
24
flake.lock
generated
|
@ -7,11 +7,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706473964,
|
"lastModified": 1708794349,
|
||||||
"narHash": "sha256-Fq6xleee/TsX6NbtoRuI96bBuDHMU57PrcK9z1QEKbk=",
|
"narHash": "sha256-jX+B1VGHT0ruHHL5RwS8L21R6miBn4B6s9iVyUJsJJY=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "c798790eabec3e3da48190ae3698ac227aab770c",
|
"rev": "2c94ff9a6fbeb9f3ea0107f28688edbe9c81deaa",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -58,11 +58,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706683685,
|
"lastModified": 1709200309,
|
||||||
"narHash": "sha256-FtPPshEpxH/ewBOsdKBNhlsL2MLEFv1hEnQ19f/bFsQ=",
|
"narHash": "sha256-lKdtMbhnBNU1lr978T+wEYet3sfIXXgyiDZNEgx8CV8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "5ad9903c16126a7d949101687af0aa589b1d7d3d",
|
"rev": "ebe6e807793e7c9cc59cf81225fdee1a03413811",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -72,11 +72,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706550542,
|
"lastModified": 1709150264,
|
||||||
"narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=",
|
"narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "97b17f32362e475016f942bbdfda4a4a72a8a652",
|
"rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -102,11 +102,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706667075,
|
"lastModified": 1709172595,
|
||||||
"narHash": "sha256-KBI5jcOWh9nsOFWj2SRq7vj+fPDf8Do8ceL582kFA70=",
|
"narHash": "sha256-0oYeE5VkhnPA7YBl+0Utq2cYoHcfsEhSGwraCa27Vs8=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "2c993daf3136c6955fd13bfe215d0d4faf6090f1",
|
"rev": "72fa0217f76020ad3aeb2dd9dd72490905b23b6f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -127,6 +127,7 @@
|
||||||
hicolor-icon-theme
|
hicolor-icon-theme
|
||||||
gsettings-desktop-schemas
|
gsettings-desktop-schemas
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
|
libpulseaudio
|
||||||
];
|
];
|
||||||
|
|
||||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
gtk-layer-shell,
|
gtk-layer-shell,
|
||||||
gnome,
|
gnome,
|
||||||
libxkbcommon,
|
libxkbcommon,
|
||||||
|
libpulseaudio,
|
||||||
openssl,
|
openssl,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
hicolor-icon-theme,
|
hicolor-icon-theme,
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
path = lib.cleanSource ../.;
|
path = lib.cleanSource ../.;
|
||||||
};
|
};
|
||||||
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
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 = [
|
propagatedBuildInputs = [
|
||||||
gtk3
|
gtk3
|
||||||
];
|
];
|
||||||
|
|
21
src/bar.rs
21
src/bar.rs
|
@ -10,7 +10,6 @@ use gtk::gdk::Monitor;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
|
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
|
||||||
use gtk_layer_shell::LayerShell;
|
use gtk_layer_shell::LayerShell;
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
@ -18,7 +17,7 @@ use tracing::{debug, info};
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Inner {
|
enum Inner {
|
||||||
New { config: Option<Config> },
|
New { config: Option<Config> },
|
||||||
Loaded { popup: Rc<RefCell<Popup>> },
|
Loaded { popup: Rc<Popup> },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -60,7 +59,7 @@ impl Bar {
|
||||||
window.set_widget_name(&name);
|
window.set_widget_name(&name);
|
||||||
|
|
||||||
let position = config.position;
|
let position = config.position;
|
||||||
let orientation = position.get_orientation();
|
let orientation = position.orientation();
|
||||||
|
|
||||||
let content = gtk::Box::builder()
|
let content = gtk::Box::builder()
|
||||||
.orientation(orientation)
|
.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::Left, margin.left);
|
||||||
win.set_layer_shell_margin(gtk_layer_shell::Edge::Right, margin.right);
|
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(
|
win.set_anchor(
|
||||||
gtk_layer_shell::Edge::Top,
|
gtk_layer_shell::Edge::Top,
|
||||||
|
@ -269,7 +268,7 @@ impl Bar {
|
||||||
|
|
||||||
// popup ignores module location so can bodge this for now
|
// popup ignores module location so can bodge this for now
|
||||||
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
|
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 {
|
if let Some(modules) = config.start {
|
||||||
let info = info!(ModuleLocation::Left);
|
let info = info!(ModuleLocation::Left);
|
||||||
|
@ -315,7 +314,7 @@ impl Bar {
|
||||||
&self.monitor_name
|
&self.monitor_name
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn popup(&self) -> Rc<RefCell<Popup>> {
|
pub fn popup(&self) -> Rc<Popup> {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
Inner::New { .. } => {
|
Inner::New { .. } => {
|
||||||
panic!("Attempted to get popup of uninitialized bar. This is a serious bug!")
|
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)]
|
#[derive(Debug)]
|
||||||
struct BarLoadResult {
|
struct BarLoadResult {
|
||||||
popup: Rc<RefCell<Popup>>,
|
popup: Rc<Popup>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds modules into a provided GTK box,
|
/// Adds modules into a provided GTK box,
|
||||||
|
@ -349,9 +348,9 @@ fn add_modules(
|
||||||
modules: Vec<ModuleConfig>,
|
modules: Vec<ModuleConfig>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
ironbar: &Rc<Ironbar>,
|
ironbar: &Rc<Ironbar>,
|
||||||
popup: &Rc<RefCell<Popup>>,
|
popup: &Rc<Popup>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.orientation();
|
||||||
|
|
||||||
macro_rules! add_module {
|
macro_rules! add_module {
|
||||||
($module:expr, $id:expr) => {{
|
($module:expr, $id:expr) => {{
|
||||||
|
@ -388,6 +387,8 @@ fn add_modules(
|
||||||
ModuleConfig::Music(mut module) => add_module!(module, id),
|
ModuleConfig::Music(mut module) => add_module!(module, id),
|
||||||
#[cfg(feature = "networkmanager")]
|
#[cfg(feature = "networkmanager")]
|
||||||
ModuleConfig::Networkmanager(mut module) => add_module!(module, id),
|
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),
|
ModuleConfig::Script(mut module) => add_module!(module, id),
|
||||||
#[cfg(feature = "sys_info")]
|
#[cfg(feature = "sys_info")]
|
||||||
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
|
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
|
||||||
|
@ -395,6 +396,8 @@ fn add_modules(
|
||||||
ModuleConfig::Tray(mut module) => add_module!(module, id),
|
ModuleConfig::Tray(mut module) => add_module!(module, id),
|
||||||
#[cfg(feature = "upower")]
|
#[cfg(feature = "upower")]
|
||||||
ModuleConfig::Upower(mut module) => add_module!(module, id),
|
ModuleConfig::Upower(mut module) => add_module!(module, id),
|
||||||
|
#[cfg(feature = "volume")]
|
||||||
|
ModuleConfig::Volume(mut module) => add_module!(module, id),
|
||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
|
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,14 @@ pub mod clipboard;
|
||||||
pub mod compositor;
|
pub mod compositor;
|
||||||
#[cfg(feature = "music")]
|
#[cfg(feature = "music")]
|
||||||
pub mod music;
|
pub mod music;
|
||||||
|
#[cfg(feature = "notifications")]
|
||||||
|
pub mod swaync;
|
||||||
#[cfg(feature = "tray")]
|
#[cfg(feature = "tray")]
|
||||||
pub mod system_tray;
|
pub mod system_tray;
|
||||||
#[cfg(feature = "upower")]
|
#[cfg(feature = "upower")]
|
||||||
pub mod upower;
|
pub mod upower;
|
||||||
|
#[cfg(feature = "volume")]
|
||||||
|
pub mod volume;
|
||||||
pub mod wayland;
|
pub mod wayland;
|
||||||
|
|
||||||
/// Singleton wrapper consisting of
|
/// Singleton wrapper consisting of
|
||||||
|
@ -23,10 +27,14 @@ pub struct Clients {
|
||||||
clipboard: Option<Arc<clipboard::Client>>,
|
clipboard: Option<Arc<clipboard::Client>>,
|
||||||
#[cfg(feature = "music")]
|
#[cfg(feature = "music")]
|
||||||
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
|
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
|
||||||
|
#[cfg(feature = "notifications")]
|
||||||
|
notifications: Option<Arc<swaync::Client>>,
|
||||||
#[cfg(feature = "tray")]
|
#[cfg(feature = "tray")]
|
||||||
tray: Option<Arc<system_tray::TrayEventReceiver>>,
|
tray: Option<Arc<system_tray::TrayEventReceiver>>,
|
||||||
#[cfg(feature = "upower")]
|
#[cfg(feature = "upower")]
|
||||||
upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>,
|
upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>,
|
||||||
|
#[cfg(feature = "volume")]
|
||||||
|
volume: Option<Arc<volume::Client>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clients {
|
impl Clients {
|
||||||
|
@ -67,6 +75,15 @@ impl Clients {
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notifications")]
|
||||||
|
pub fn notifications(&mut self) -> Arc<swaync::Client> {
|
||||||
|
self.notifications
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
Arc::new(crate::await_sync(async { swaync::Client::new().await }))
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tray")]
|
#[cfg(feature = "tray")]
|
||||||
pub fn tray(&mut self) -> Arc<system_tray::TrayEventReceiver> {
|
pub fn tray(&mut self) -> Arc<system_tray::TrayEventReceiver> {
|
||||||
self.tray
|
self.tray
|
||||||
|
@ -86,6 +103,13 @@ impl Clients {
|
||||||
})
|
})
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "volume")]
|
||||||
|
pub fn volume(&mut self) -> Arc<volume::Client> {
|
||||||
|
self.volume
|
||||||
|
.get_or_insert_with(volume::create_client)
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Types implementing this trait
|
/// Types implementing this trait
|
||||||
|
@ -111,7 +135,7 @@ macro_rules! register_client {
|
||||||
where
|
where
|
||||||
TSend: Clone,
|
TSend: Clone,
|
||||||
{
|
{
|
||||||
fn provide(&self) -> Arc<$ty> {
|
fn provide(&self) -> std::sync::Arc<$ty> {
|
||||||
self.ironbar.clients.borrow_mut().$method()
|
self.ironbar.clients.borrow_mut().$method()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
111
src/clients/swaync/dbus.rs
Normal file
111
src/clients/swaync/dbus.rs
Normal file
|
@ -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<bool>;
|
||||||
|
|
||||||
|
/// 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<bool>;
|
||||||
|
|
||||||
|
/// 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<bool>;
|
||||||
|
|
||||||
|
/// GetSubscribeData method
|
||||||
|
fn get_subscribe_data(&self) -> zbus::Result<(bool, bool, u32, bool)>;
|
||||||
|
|
||||||
|
/// GetVisibility method
|
||||||
|
fn get_visibility(&self) -> zbus::Result<bool>;
|
||||||
|
|
||||||
|
/// HideLatestNotifications method
|
||||||
|
fn hide_latest_notifications(&self, close: bool) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
/// IsInhibited method
|
||||||
|
fn is_inhibited(&self) -> zbus::Result<bool>;
|
||||||
|
|
||||||
|
/// NotificationCount method
|
||||||
|
fn notification_count(&self) -> zbus::Result<u32>;
|
||||||
|
|
||||||
|
/// NumberOfInhibitors method
|
||||||
|
fn number_of_inhibitors(&self) -> zbus::Result<u32>;
|
||||||
|
|
||||||
|
/// ReloadConfig method
|
||||||
|
fn reload_config(&self) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
/// ReloadCss method
|
||||||
|
fn reload_css(&self) -> zbus::Result<bool>;
|
||||||
|
|
||||||
|
/// RemoveInhibitor method
|
||||||
|
fn remove_inhibitor(&self, application_id: &str) -> zbus::Result<bool>;
|
||||||
|
|
||||||
|
/// 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<bool>;
|
||||||
|
|
||||||
|
/// 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<bool>;
|
||||||
|
#[dbus_proxy(property)]
|
||||||
|
fn set_inhibited(&self, value: bool) -> zbus::Result<()>;
|
||||||
|
}
|
88
src/clients/swaync/mod.rs
Normal file
88
src/clients/swaync/mod.rs
Normal file
|
@ -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<GetSubscribeData> 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<Event>,
|
||||||
|
_rx: broadcast::Receiver<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<Event>().expect("to deserialize");
|
||||||
|
debug!("Received event: {ev:?}");
|
||||||
|
send!(tx, ev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { proxy, tx, _rx: rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
|
||||||
|
self.tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn state(&self) -> Result<Event> {
|
||||||
|
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);
|
309
src/clients/volume/mod.rs
Normal file
309
src/clients/volume/mod.rs
Normal file
|
@ -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<T> = Arc<Mutex<Vec<T>>>;
|
||||||
|
|
||||||
|
#[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<Mutex<ConnectionState>>,
|
||||||
|
|
||||||
|
data: Data,
|
||||||
|
|
||||||
|
tx: broadcast::Sender<Event>,
|
||||||
|
_rx: broadcast::Receiver<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
struct Data {
|
||||||
|
sinks: ArcMutVec<Sink>,
|
||||||
|
sink_inputs: ArcMutVec<SinkInput>,
|
||||||
|
|
||||||
|
default_sink_name: Arc<Mutex<Option<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ConnectionState {
|
||||||
|
Disconnected,
|
||||||
|
Connected {
|
||||||
|
context: Arc<Mutex<Context>>,
|
||||||
|
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<Event> {
|
||||||
|
self.tx.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Pulse volume client.
|
||||||
|
pub fn create_client() -> Arc<Client> {
|
||||||
|
let client = Arc::new(Client::new());
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = client.clone();
|
||||||
|
spawn_blocking(move || {
|
||||||
|
client.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
client
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_state_change(context: &Arc<Mutex<Context>>, data: &Data, tx: &broadcast::Sender<Event>) {
|
||||||
|
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<Mutex<Context>>,
|
||||||
|
data: &Data,
|
||||||
|
tx: &broadcast::Sender<Event>,
|
||||||
|
facility: Option<Facility>,
|
||||||
|
op: Option<Operation>,
|
||||||
|
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<Mutex<Context>>,
|
||||||
|
sinks: &ArcMutVec<Sink>,
|
||||||
|
default_sink: &Arc<Mutex<Option<String>>>,
|
||||||
|
tx: &broadcast::Sender<Event>,
|
||||||
|
) {
|
||||||
|
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<Sink>,
|
||||||
|
default_sink: &Arc<Mutex<Option<String>>>,
|
||||||
|
tx: &broadcast::Sender<Event>,
|
||||||
|
) {
|
||||||
|
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);
|
175
src/clients/volume/sink.rs
Normal file
175
src/clients/volume/sink.rs
Normal file
|
@ -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<Mutex<Vec<Sink>>> {
|
||||||
|
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<Mutex<Context>>,
|
||||||
|
sinks: &ArcMutVec<Sink>,
|
||||||
|
default_sink: &Arc<Mutex<Option<String>>>,
|
||||||
|
tx: &broadcast::Sender<Event>,
|
||||||
|
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<Sink>, tx: &broadcast::Sender<Event>) {
|
||||||
|
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<Sink>,
|
||||||
|
default_sink: &Arc<Mutex<Option<String>>>,
|
||||||
|
tx: &broadcast::Sender<Event>,
|
||||||
|
) {
|
||||||
|
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<Sink>, tx: &broadcast::Sender<Event>) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
148
src/clients/volume/sink_input.rs
Normal file
148
src/clients/volume/sink_input.rs
Normal file
|
@ -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<Mutex<Vec<SinkInput>>> {
|
||||||
|
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<Mutex<Context>>,
|
||||||
|
inputs: &ArcMutVec<SinkInput>,
|
||||||
|
tx: &broadcast::Sender<Event>,
|
||||||
|
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<SinkInput>,
|
||||||
|
tx: &broadcast::Sender<Event>,
|
||||||
|
) {
|
||||||
|
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<SinkInput>,
|
||||||
|
tx: &broadcast::Sender<Event>,
|
||||||
|
) {
|
||||||
|
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<SinkInput>, tx: &broadcast::Sender<Event>) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -179,6 +179,9 @@ impl Environment {
|
||||||
MimeTypeCategory::Image => {
|
MimeTypeCategory::Image => {
|
||||||
let mut bytes = vec![];
|
let mut bytes = vec![];
|
||||||
file.read_to_end(&mut bytes)?;
|
file.read_to_end(&mut bytes)?;
|
||||||
|
|
||||||
|
debug!("Read bytes: {}", bytes.len());
|
||||||
|
|
||||||
let bytes = Bytes::from(&bytes);
|
let bytes = Bytes::from(&bytes);
|
||||||
|
|
||||||
ClipboardValue::Image(bytes)
|
ClipboardValue::Image(bytes)
|
||||||
|
@ -234,6 +237,8 @@ impl DataControlDeviceHandler for Environment {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debug!("Receiving mime type: {}", mime_type.value);
|
||||||
|
|
||||||
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
|
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
|
||||||
let offer_clone = cur_offer.offer.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())
|
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
|
||||||
.expect("Failed to increase pipe size");
|
.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::<Vec<_>>();
|
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
||||||
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
||||||
|
@ -347,20 +352,23 @@ impl DataControlSourceHandler for Environment {
|
||||||
while !bytes.is_empty() {
|
while !bytes.is_empty() {
|
||||||
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
||||||
|
|
||||||
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
|
|
||||||
|
|
||||||
epoll_fd
|
epoll_fd
|
||||||
.wait(&mut events, 100)
|
.wait(&mut events, 100)
|
||||||
.expect("Failed to wait to epoll");
|
.expect("Failed to wait to epoll");
|
||||||
|
|
||||||
match file.write(chunk) {
|
match file.write(chunk) {
|
||||||
Ok(_) => bytes = &bytes[chunk.len()..],
|
Ok(written) => {
|
||||||
|
trace!("Wrote {} bytes ({} remain)", written, bytes.len());
|
||||||
|
bytes = &bytes[written..];
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("Done writing");
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to find source");
|
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),
|
/// If the requested size is larger than the kernel max (normally 1MB),
|
||||||
/// it will be clamped at this.
|
/// 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<i32> {
|
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
|
||||||
// clamp size at kernel max
|
// clamp size at kernel max
|
||||||
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
|
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
|
||||||
|
|
|
@ -38,7 +38,7 @@ impl<'de> Deserialize<'de> for MonitorConfig {
|
||||||
impl BarPosition {
|
impl BarPosition {
|
||||||
/// Gets the orientation the bar and widgets should use
|
/// Gets the orientation the bar and widgets should use
|
||||||
/// based on this position.
|
/// based on this position.
|
||||||
pub fn get_orientation(self) -> Orientation {
|
pub fn orientation(self) -> Orientation {
|
||||||
if self == Self::Top || self == Self::Bottom {
|
if self == Self::Top || self == Self::Bottom {
|
||||||
Orientation::Horizontal
|
Orientation::Horizontal
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,6 +16,8 @@ use crate::modules::launcher::LauncherModule;
|
||||||
use crate::modules::music::MusicModule;
|
use crate::modules::music::MusicModule;
|
||||||
#[cfg(feature = "networkmanager")]
|
#[cfg(feature = "networkmanager")]
|
||||||
use crate::modules::networkmanager::NetworkmanagerModule;
|
use crate::modules::networkmanager::NetworkmanagerModule;
|
||||||
|
#[cfg(feature = "notifications")]
|
||||||
|
use crate::modules::notifications::NotificationsModule;
|
||||||
use crate::modules::script::ScriptModule;
|
use crate::modules::script::ScriptModule;
|
||||||
#[cfg(feature = "sys_info")]
|
#[cfg(feature = "sys_info")]
|
||||||
use crate::modules::sysinfo::SysInfoModule;
|
use crate::modules::sysinfo::SysInfoModule;
|
||||||
|
@ -23,6 +25,8 @@ use crate::modules::sysinfo::SysInfoModule;
|
||||||
use crate::modules::tray::TrayModule;
|
use crate::modules::tray::TrayModule;
|
||||||
#[cfg(feature = "upower")]
|
#[cfg(feature = "upower")]
|
||||||
use crate::modules::upower::UpowerModule;
|
use crate::modules::upower::UpowerModule;
|
||||||
|
#[cfg(feature = "volume")]
|
||||||
|
use crate::modules::volume::VolumeModule;
|
||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
use crate::modules::workspaces::WorkspacesModule;
|
use crate::modules::workspaces::WorkspacesModule;
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
|
@ -49,6 +53,8 @@ pub enum ModuleConfig {
|
||||||
Music(Box<MusicModule>),
|
Music(Box<MusicModule>),
|
||||||
#[cfg(feature = "networkmanager")]
|
#[cfg(feature = "networkmanager")]
|
||||||
Networkmanager(Box<NetworkmanagerModule>),
|
Networkmanager(Box<NetworkmanagerModule>),
|
||||||
|
#[cfg(feature = "notifications")]
|
||||||
|
Notifications(Box<NotificationsModule>),
|
||||||
Script(Box<ScriptModule>),
|
Script(Box<ScriptModule>),
|
||||||
#[cfg(feature = "sys_info")]
|
#[cfg(feature = "sys_info")]
|
||||||
SysInfo(Box<SysInfoModule>),
|
SysInfo(Box<SysInfoModule>),
|
||||||
|
@ -56,6 +62,8 @@ pub enum ModuleConfig {
|
||||||
Tray(Box<TrayModule>),
|
Tray(Box<TrayModule>),
|
||||||
#[cfg(feature = "upower")]
|
#[cfg(feature = "upower")]
|
||||||
Upower(Box<UpowerModule>),
|
Upower(Box<UpowerModule>),
|
||||||
|
#[cfg(feature = "volume")]
|
||||||
|
Volume(Box<VolumeModule>),
|
||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
Workspaces(Box<WorkspacesModule>),
|
Workspaces(Box<WorkspacesModule>),
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,13 +167,13 @@ impl Ipc {
|
||||||
match bar {
|
match bar {
|
||||||
Some(bar) => {
|
Some(bar) => {
|
||||||
let popup = bar.popup();
|
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
|
let data = popup
|
||||||
.borrow()
|
|
||||||
.cache
|
.cache
|
||||||
|
.borrow()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, value)| value.name == name)
|
.find(|(_, value)| value.name == name)
|
||||||
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
|
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
|
||||||
|
@ -181,7 +181,6 @@ impl Ipc {
|
||||||
match data {
|
match data {
|
||||||
Some((id, Some(button))) if current_widget != Some(id) => {
|
Some((id, Some(button))) if current_widget != Some(id) => {
|
||||||
let button_id = button.popup_id();
|
let button_id = button.popup_id();
|
||||||
let mut popup = popup.borrow_mut();
|
|
||||||
|
|
||||||
if popup.is_visible() {
|
if popup.is_visible() {
|
||||||
popup.hide();
|
popup.hide();
|
||||||
|
@ -207,11 +206,11 @@ impl Ipc {
|
||||||
let popup = bar.popup();
|
let popup = bar.popup();
|
||||||
|
|
||||||
// only one popup per bar, so hide if open for another widget
|
// only one popup per bar, so hide if open for another widget
|
||||||
popup.borrow_mut().hide();
|
popup.hide();
|
||||||
|
|
||||||
let data = popup
|
let data = popup
|
||||||
.borrow()
|
|
||||||
.cache
|
.cache
|
||||||
|
.borrow()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, value)| value.name == name)
|
.find(|(_, value)| value.name == name)
|
||||||
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
|
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
|
||||||
|
@ -219,7 +218,7 @@ impl Ipc {
|
||||||
match data {
|
match data {
|
||||||
Some((id, Some(button))) => {
|
Some((id, Some(button))) => {
|
||||||
let button_id = button.popup_id();
|
let button_id = button.popup_id();
|
||||||
popup.borrow_mut().show(id, button_id);
|
popup.show(id, button_id);
|
||||||
|
|
||||||
Response::Ok
|
Response::Ok
|
||||||
}
|
}
|
||||||
|
@ -236,7 +235,7 @@ impl Ipc {
|
||||||
match bar {
|
match bar {
|
||||||
Some(bar) => {
|
Some(bar) => {
|
||||||
let popup = bar.popup();
|
let popup = bar.popup();
|
||||||
popup.borrow_mut().hide();
|
popup.hide();
|
||||||
|
|
||||||
Response::Ok
|
Response::Ok
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,3 +180,10 @@ macro_rules! arc_rw {
|
||||||
std::sync::Arc::new(std::sync::RwLock::new($val))
|
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))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ mod popup;
|
||||||
mod script;
|
mod script;
|
||||||
mod style;
|
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");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -111,7 +111,7 @@ impl Ironbar {
|
||||||
info!("Ironbar version {}", VERSION);
|
info!("Ironbar version {}", VERSION);
|
||||||
info!("Starting application");
|
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);
|
let running = AtomicBool::new(false);
|
||||||
|
|
||||||
|
|
|
@ -194,7 +194,7 @@ impl Module<gtk::Box> for CustomModule {
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<gtk::Box>> {
|
) -> Result<ModuleParts<gtk::Box>> {
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.orientation();
|
||||||
let container = gtk::Box::builder().orientation(orientation).build();
|
let container = gtk::Box::builder().orientation(orientation).build();
|
||||||
|
|
||||||
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
|
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
@ -236,7 +236,7 @@ impl Module<gtk::Box> for CustomModule {
|
||||||
if let Some(popup) = self.popup {
|
if let Some(popup) = self.popup {
|
||||||
let custom_context = CustomWidgetContext {
|
let custom_context = CustomWidgetContext {
|
||||||
tx: &tx,
|
tx: &tx,
|
||||||
bar_orientation: info.bar_position.get_orientation(),
|
bar_orientation: info.bar_position.orientation(),
|
||||||
icon_theme: info.icon_theme,
|
icon_theme: info.icon_theme,
|
||||||
popup_buttons: Rc::new(RefCell::new(vec![])),
|
popup_buttons: Rc::new(RefCell::new(vec![])),
|
||||||
};
|
};
|
||||||
|
|
|
@ -113,7 +113,7 @@ impl Module<gtk::Box> for FocusedModule {
|
||||||
) -> Result<ModuleParts<gtk::Box>> {
|
) -> Result<ModuleParts<gtk::Box>> {
|
||||||
let icon_theme = info.icon_theme;
|
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();
|
let icon = gtk::Image::new();
|
||||||
if self.show_icon {
|
if self.show_icon {
|
||||||
|
|
|
@ -166,8 +166,12 @@ impl ItemButton {
|
||||||
|
|
||||||
if appearance.show_icons {
|
if appearance.show_icons {
|
||||||
let gtk_image = gtk::Image::new();
|
let gtk_image = gtk::Image::new();
|
||||||
let image =
|
let input = if item.app_id.is_empty() {
|
||||||
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size);
|
item.name.clone()
|
||||||
|
} else {
|
||||||
|
item.app_id.clone()
|
||||||
|
};
|
||||||
|
let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size);
|
||||||
if let Some(image) = image {
|
if let Some(image) = image {
|
||||||
button.set_image(Some(>k_image));
|
button.set_image(Some(>k_image));
|
||||||
button.set_always_show_image(true);
|
button.set_always_show_image(true);
|
||||||
|
@ -225,9 +229,7 @@ impl ItemButton {
|
||||||
|
|
||||||
try_send!(
|
try_send!(
|
||||||
tx,
|
tx,
|
||||||
ModuleUpdateEvent::OpenPopupAt(
|
ModuleUpdateEvent::OpenPopupAt(button.geometry(bar_position.orientation()))
|
||||||
button.geometry(bar_position.get_orientation())
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||||
|
|
|
@ -301,7 +301,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||||
) -> crate::Result<ModuleParts<gtk::Box>> {
|
) -> crate::Result<ModuleParts<gtk::Box>> {
|
||||||
let icon_theme = info.icon_theme;
|
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();
|
let container = container.clone();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -37,6 +36,8 @@ pub mod launcher;
|
||||||
pub mod music;
|
pub mod music;
|
||||||
#[cfg(feature = "networkmanager")]
|
#[cfg(feature = "networkmanager")]
|
||||||
pub mod networkmanager;
|
pub mod networkmanager;
|
||||||
|
#[cfg(feature = "notifications")]
|
||||||
|
pub mod notifications;
|
||||||
pub mod script;
|
pub mod script;
|
||||||
#[cfg(feature = "sys_info")]
|
#[cfg(feature = "sys_info")]
|
||||||
pub mod sysinfo;
|
pub mod sysinfo;
|
||||||
|
@ -44,6 +45,8 @@ pub mod sysinfo;
|
||||||
pub mod tray;
|
pub mod tray;
|
||||||
#[cfg(feature = "upower")]
|
#[cfg(feature = "upower")]
|
||||||
pub mod upower;
|
pub mod upower;
|
||||||
|
#[cfg(feature = "volume")]
|
||||||
|
pub mod volume;
|
||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
pub mod workspaces;
|
pub mod workspaces;
|
||||||
|
|
||||||
|
@ -217,7 +220,7 @@ pub fn create_module<TModule, TWidget, TSend, TRec>(
|
||||||
ironbar: Rc<Ironbar>,
|
ironbar: Rc<Ironbar>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
popup: &Rc<RefCell<Popup>>,
|
popup: &Rc<Popup>,
|
||||||
) -> Result<ModuleParts<TWidget>>
|
) -> Result<ModuleParts<TWidget>>
|
||||||
where
|
where
|
||||||
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
||||||
|
@ -253,7 +256,7 @@ where
|
||||||
.style_context()
|
.style_context()
|
||||||
.add_class(&format!("popup-{module_name}"));
|
.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);
|
setup_receiver(tx, ui_rx, popup.clone(), module_name, id);
|
||||||
|
@ -261,16 +264,6 @@ where
|
||||||
Ok(module_parts)
|
Ok(module_parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers the popup content with the popup.
|
|
||||||
fn register_popup_content(
|
|
||||||
popup: &Rc<RefCell<Popup>>,
|
|
||||||
id: usize,
|
|
||||||
name: String,
|
|
||||||
popup_content: ModulePopupParts,
|
|
||||||
) {
|
|
||||||
popup.borrow_mut().register_content(id, name, popup_content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets up the bridge channel receiver
|
/// Sets up the bridge channel receiver
|
||||||
/// to pick up events from the controller, widget or popup.
|
/// to pick up events from the controller, widget or popup.
|
||||||
///
|
///
|
||||||
|
@ -279,7 +272,7 @@ fn register_popup_content(
|
||||||
fn setup_receiver<TSend>(
|
fn setup_receiver<TSend>(
|
||||||
tx: broadcast::Sender<TSend>,
|
tx: broadcast::Sender<TSend>,
|
||||||
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
||||||
popup: Rc<RefCell<Popup>>,
|
popup: Rc<Popup>,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
id: usize,
|
id: usize,
|
||||||
) where
|
) where
|
||||||
|
@ -296,7 +289,6 @@ fn setup_receiver<TSend>(
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::TogglePopup(button_id) => {
|
ModuleUpdateEvent::TogglePopup(button_id) => {
|
||||||
debug!("Toggling popup for {} [#{}]", name, id);
|
debug!("Toggling popup for {} [#{}]", name, id);
|
||||||
let mut popup = popup.borrow_mut();
|
|
||||||
if popup.is_visible() {
|
if popup.is_visible() {
|
||||||
popup.hide();
|
popup.hide();
|
||||||
} else {
|
} else {
|
||||||
|
@ -311,8 +303,6 @@ fn setup_receiver<TSend>(
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::OpenPopup(button_id) => {
|
ModuleUpdateEvent::OpenPopup(button_id) => {
|
||||||
debug!("Opening popup for {} [#{}]", name, id);
|
debug!("Opening popup for {} [#{}]", name, id);
|
||||||
|
|
||||||
let mut popup = popup.borrow_mut();
|
|
||||||
popup.hide();
|
popup.hide();
|
||||||
popup.show(id, button_id);
|
popup.show(id, button_id);
|
||||||
|
|
||||||
|
@ -326,7 +316,6 @@ fn setup_receiver<TSend>(
|
||||||
ModuleUpdateEvent::OpenPopupAt(geometry) => {
|
ModuleUpdateEvent::OpenPopupAt(geometry) => {
|
||||||
debug!("Opening popup for {} [#{}]", name, id);
|
debug!("Opening popup for {} [#{}]", name, id);
|
||||||
|
|
||||||
let mut popup = popup.borrow_mut();
|
|
||||||
popup.hide();
|
popup.hide();
|
||||||
popup.show_at(id, geometry);
|
popup.show_at(id, geometry);
|
||||||
|
|
||||||
|
@ -338,8 +327,6 @@ fn setup_receiver<TSend>(
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::ClosePopup => {
|
ModuleUpdateEvent::ClosePopup => {
|
||||||
debug!("Closing popup for {} [#{}]", name, id);
|
debug!("Closing popup for {} [#{}]", name, id);
|
||||||
|
|
||||||
let mut popup = popup.borrow_mut();
|
|
||||||
popup.hide();
|
popup.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
190
src/modules/notifications.rs
Normal file
190
src/modules/notifications.rs
Normal file
|
@ -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<CommonConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Overlay> for NotificationsModule {
|
||||||
|
type SendMessage = swaync::Event;
|
||||||
|
type ReceiveMessage = UiEvent;
|
||||||
|
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"notifications"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> color_eyre::Result<()>
|
||||||
|
where
|
||||||
|
<Self as Module<Overlay>>::SendMessage: Clone,
|
||||||
|
{
|
||||||
|
let client = context.client::<swaync::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<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> color_eyre::Result<ModuleParts<Overlay>>
|
||||||
|
where
|
||||||
|
<Self as Module<Overlay>>::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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -188,7 +188,7 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||||
) -> Result<ModuleParts<gtk::Box>> {
|
) -> Result<ModuleParts<gtk::Box>> {
|
||||||
let re = Regex::new(r"\{([^}]+)}")?;
|
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();
|
let mut labels = Vec::new();
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::modules::tray::diff::get_diffs;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||||
use crate::{glib_recv, spawn};
|
use crate::{glib_recv, spawn};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use gtk::prelude::*;
|
use gtk::{prelude::*, PackDirection};
|
||||||
use gtk::{IconTheme, MenuBar};
|
use gtk::{IconTheme, MenuBar};
|
||||||
use interface::TrayMenu;
|
use interface::TrayMenu;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -18,10 +18,28 @@ use tokio::sync::mpsc;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct TrayModule {
|
pub struct TrayModule {
|
||||||
|
#[serde(default, deserialize_with = "deserialize_orientation")]
|
||||||
|
pub direction: Option<PackDirection>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub common: Option<CommonConfig>,
|
pub common: Option<CommonConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = Option::<String>::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<MenuBar> for TrayModule {
|
impl Module<MenuBar> for TrayModule {
|
||||||
type SendMessage = NotifierItemMessage;
|
type SendMessage = NotifierItemMessage;
|
||||||
type ReceiveMessage = NotifierItemCommand;
|
type ReceiveMessage = NotifierItemCommand;
|
||||||
|
@ -70,6 +88,17 @@ impl Module<MenuBar> for TrayModule {
|
||||||
) -> Result<ModuleParts<MenuBar>> {
|
) -> Result<ModuleParts<MenuBar>> {
|
||||||
let container = MenuBar::new();
|
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 container = container.clone();
|
||||||
let mut menus = HashMap::new();
|
let mut menus = HashMap::new();
|
||||||
|
|
426
src/modules/volume.rs
Normal file
426
src/modules/volume.rs
Normal file
|
@ -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<CommonConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Button> for VolumeModule {
|
||||||
|
type SendMessage = Event;
|
||||||
|
type ReceiveMessage = Update;
|
||||||
|
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"volume"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> color_eyre::Result<()>
|
||||||
|
where
|
||||||
|
<Self as Module<Button>>::SendMessage: Clone,
|
||||||
|
{
|
||||||
|
let client = context.client::<volume::Client>();
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = client.clone();
|
||||||
|
let mut rx = client.subscribe();
|
||||||
|
let tx = context.tx.clone();
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
// init
|
||||||
|
let sinks = {
|
||||||
|
let sinks = client.sinks();
|
||||||
|
let sinks = lock!(sinks);
|
||||||
|
sinks.iter().cloned().collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let inputs = {
|
||||||
|
let inputs = client.sink_inputs();
|
||||||
|
let inputs = lock!(inputs);
|
||||||
|
inputs.iter().cloned().collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
for sink in sinks {
|
||||||
|
send_async!(tx, ModuleUpdateEvent::Update(Event::AddSink(sink)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for input in inputs {
|
||||||
|
send_async!(
|
||||||
|
tx,
|
||||||
|
ModuleUpdateEvent::Update(Event::AddInput(input.clone()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// recv loop
|
||||||
|
while let Ok(event) = rx.recv().await {
|
||||||
|
send_async!(tx, ModuleUpdateEvent::Update(event));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ui events
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(update) = rx.recv().await {
|
||||||
|
match update {
|
||||||
|
Update::SinkChange(name) => client.set_default_sink(&name),
|
||||||
|
Update::SinkVolume(name, volume) => client.set_sink_volume(&name, volume),
|
||||||
|
Update::SinkMute(name, muted) => client.set_sink_muted(&name, muted),
|
||||||
|
Update::InputVolume(index, volume) => client.set_input_volume(index, volume),
|
||||||
|
Update::InputMute(index, muted) => client.set_input_muted(index, muted),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
) -> color_eyre::Result<ModuleParts<Button>>
|
||||||
|
where
|
||||||
|
<Self as Module<Button>>::SendMessage: Clone,
|
||||||
|
{
|
||||||
|
let button = Button::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = context.tx.clone();
|
||||||
|
|
||||||
|
button.connect_clicked(move |button| {
|
||||||
|
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let rx = context.subscribe();
|
||||||
|
let icons = self.icons.clone();
|
||||||
|
let button = button.clone();
|
||||||
|
|
||||||
|
let format = self.format.clone();
|
||||||
|
|
||||||
|
glib_recv!(rx, event => {
|
||||||
|
match event {
|
||||||
|
Event::AddSink(sink) | Event::UpdateSink(sink) if sink.active => {
|
||||||
|
let label = format
|
||||||
|
.replace("{icon}", if sink.muted { &icons.muted } else { icons.volume_icon(sink.volume) })
|
||||||
|
.replace("{percentage}", &sink.volume.to_string())
|
||||||
|
.replace("{name}", &sink.description);
|
||||||
|
|
||||||
|
button.set_label(&label);
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let popup = self
|
||||||
|
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
|
||||||
|
.into_popup_parts(vec![&button]);
|
||||||
|
|
||||||
|
Ok(ModuleParts::new(button, popup))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_popup(
|
||||||
|
self,
|
||||||
|
tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||||
|
rx: tokio::sync::broadcast::Receiver<Self::SendMessage>,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
) -> Option<gtk::Box>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let container = gtk::Box::new(Orientation::Horizontal, 10);
|
||||||
|
|
||||||
|
let sink_container = gtk::Box::new(Orientation::Vertical, 5);
|
||||||
|
sink_container.add_class("device-box");
|
||||||
|
|
||||||
|
let input_container = gtk::Box::new(Orientation::Vertical, 5);
|
||||||
|
input_container.add_class("apps-box");
|
||||||
|
|
||||||
|
container.add(&sink_container);
|
||||||
|
container.add(&input_container);
|
||||||
|
|
||||||
|
let sink_selector = ComboBoxText::new();
|
||||||
|
sink_selector.add_class("device-selector");
|
||||||
|
|
||||||
|
let renderer = sink_selector
|
||||||
|
.cells()
|
||||||
|
.first()
|
||||||
|
.expect("to exist")
|
||||||
|
.clone()
|
||||||
|
.downcast::<CellRendererText>()
|
||||||
|
.expect("to be valid cast");
|
||||||
|
|
||||||
|
renderer.set_width_chars(20);
|
||||||
|
renderer.set_ellipsize(EllipsizeMode::End);
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
sink_selector.connect_changed(move |selector| {
|
||||||
|
if let Some(name) = selector.active_id() {
|
||||||
|
try_send!(tx, Update::SinkChange(name.into()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sink_container.add(&sink_selector);
|
||||||
|
|
||||||
|
let slider = Scale::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.height_request(100)
|
||||||
|
.inverted(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
slider.add_class("slider");
|
||||||
|
|
||||||
|
slider.set_range(0.0, self.max_volume);
|
||||||
|
slider.set_value(50.0);
|
||||||
|
sink_container.add(&slider);
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
let selector = sink_selector.clone();
|
||||||
|
|
||||||
|
slider.connect_button_release_event(move |scale, _| {
|
||||||
|
if let Some(sink) = selector.active_id() {
|
||||||
|
// GTK will send values outside min/max range
|
||||||
|
let val = scale.value().clamp(0.0, self.max_volume);
|
||||||
|
try_send!(tx, Update::SinkVolume(sink.into(), val));
|
||||||
|
}
|
||||||
|
|
||||||
|
Propagation::Proceed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let btn_mute = ToggleButton::new();
|
||||||
|
btn_mute.add_class("btn-mute");
|
||||||
|
sink_container.add(&btn_mute);
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
let selector = sink_selector.clone();
|
||||||
|
|
||||||
|
btn_mute.connect_toggled(move |btn| {
|
||||||
|
if let Some(sink) = selector.active_id() {
|
||||||
|
let muted = btn.is_active();
|
||||||
|
try_send!(tx, Update::SinkMute(sink.into(), muted));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
container.show_all();
|
||||||
|
|
||||||
|
let mut inputs = HashMap::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let input_container = input_container.clone();
|
||||||
|
|
||||||
|
let mut sinks = vec![];
|
||||||
|
|
||||||
|
glib_recv!(rx, event => {
|
||||||
|
match event {
|
||||||
|
Event::AddSink(info) => {
|
||||||
|
sink_selector.append(Some(&info.name), &info.description);
|
||||||
|
|
||||||
|
if info.active {
|
||||||
|
sink_selector.set_active(Some(sinks.len() as u32));
|
||||||
|
slider.set_value(info.volume);
|
||||||
|
|
||||||
|
btn_mute.set_active(info.muted);
|
||||||
|
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
|
||||||
|
}
|
||||||
|
|
||||||
|
sinks.push(info);
|
||||||
|
}
|
||||||
|
Event::UpdateSink(info) => {
|
||||||
|
if info.active {
|
||||||
|
if let Some(pos) = sinks.iter().position(|s| s.name == info.name) {
|
||||||
|
sink_selector.set_active(Some(pos as u32));
|
||||||
|
slider.set_value(info.volume);
|
||||||
|
|
||||||
|
btn_mute.set_active(info.muted);
|
||||||
|
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::RemoveSink(name) => {
|
||||||
|
if let Some(pos) = sinks.iter().position(|s| s.name == name) {
|
||||||
|
ComboBoxTextExt::remove(&sink_selector, pos as i32);
|
||||||
|
sinks.remove(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Event::AddInput(info) => {
|
||||||
|
let index = info.index;
|
||||||
|
|
||||||
|
let item_container = gtk::Box::new(Orientation::Vertical, 0);
|
||||||
|
item_container.add_class("app-box");
|
||||||
|
|
||||||
|
let label = Label::new(Some(&info.name));
|
||||||
|
label.add_class("title");
|
||||||
|
|
||||||
|
let slider = Scale::builder().sensitive(info.can_set_volume).build();
|
||||||
|
slider.set_range(0.0, self.max_volume);
|
||||||
|
slider.set_value(info.volume);
|
||||||
|
slider.add_class("slider");
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
slider.connect_button_release_event(move |scale, _| {
|
||||||
|
// GTK will send values outside min/max range
|
||||||
|
let val = scale.value().clamp(0.0, self.max_volume);
|
||||||
|
try_send!(tx, Update::InputVolume(index, val));
|
||||||
|
|
||||||
|
Propagation::Proceed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let btn_mute = ToggleButton::new();
|
||||||
|
btn_mute.add_class("btn-mute");
|
||||||
|
|
||||||
|
btn_mute.set_active(info.muted);
|
||||||
|
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
btn_mute.connect_toggled(move |btn| {
|
||||||
|
let muted = btn.is_active();
|
||||||
|
try_send!(tx, Update::InputMute(index, muted));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
item_container.add(&label);
|
||||||
|
item_container.add(&slider);
|
||||||
|
item_container.add(&btn_mute);
|
||||||
|
item_container.show_all();
|
||||||
|
|
||||||
|
input_container.add(&item_container);
|
||||||
|
|
||||||
|
inputs.insert(info.index, InputUi {
|
||||||
|
container: item_container,
|
||||||
|
label,
|
||||||
|
slider,
|
||||||
|
btn_mute
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Event::UpdateInput(info) => {
|
||||||
|
if let Some(ui) = inputs.get(&info.index) {
|
||||||
|
ui.label.set_label(&info.name);
|
||||||
|
ui.slider.set_value(info.volume);
|
||||||
|
ui.slider.set_sensitive(info.can_set_volume);
|
||||||
|
ui.btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::RemoveInput(index) => {
|
||||||
|
if let Some(ui) = inputs.remove(&index) {
|
||||||
|
input_container.remove(&ui.container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InputUi {
|
||||||
|
container: gtk::Box,
|
||||||
|
label: Label,
|
||||||
|
slider: Scale,
|
||||||
|
btn_mute: ToggleButton,
|
||||||
|
}
|
|
@ -189,7 +189,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<gtk::Box>> {
|
) -> Result<ModuleParts<gtk::Box>> {
|
||||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
|
let container = gtk::Box::new(info.bar_position.orientation(), 0);
|
||||||
|
|
||||||
let name_map = self.name_map.clone().unwrap_or_default();
|
let name_map = self.name_map.clone().unwrap_or_default();
|
||||||
let favs = self.favorites.clone();
|
let favs = self.favorites.clone();
|
||||||
|
@ -239,10 +239,11 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
|
|
||||||
let mut add_favourites = |names: &Vec<String>| {
|
let mut add_favourites = |names: &Vec<String>| {
|
||||||
for name in names {
|
for name in names {
|
||||||
|
fav_names.push(name.to_string());
|
||||||
|
|
||||||
if !added.contains(name) {
|
if !added.contains(name) {
|
||||||
add_workspace(name, Visibility::Hidden);
|
add_workspace(name, Visibility::Hidden);
|
||||||
added.insert(name.to_string());
|
added.insert(name.to_string());
|
||||||
fav_names.push(name.to_string());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
120
src/popup.rs
120
src/popup.rs
|
@ -1,11 +1,13 @@
|
||||||
use glib::Propagation;
|
use glib::Propagation;
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gtk::gdk::Monitor;
|
use gtk::gdk::Monitor;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{ApplicationWindow, Orientation};
|
use gtk::{ApplicationWindow, Button, Orientation};
|
||||||
use gtk_layer_shell::LayerShell;
|
use gtk_layer_shell::LayerShell;
|
||||||
use tracing::debug;
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use crate::config::BarPosition;
|
use crate::config::BarPosition;
|
||||||
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
|
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
|
||||||
|
@ -21,10 +23,10 @@ pub struct PopupCacheValue {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Popup {
|
pub struct Popup {
|
||||||
pub window: ApplicationWindow,
|
pub window: ApplicationWindow,
|
||||||
pub cache: HashMap<usize, PopupCacheValue>,
|
pub cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
|
||||||
monitor: Monitor,
|
monitor: Monitor,
|
||||||
pos: BarPosition,
|
pos: BarPosition,
|
||||||
current_widget: Option<usize>,
|
current_widget: Rc<RefCell<Option<(usize, usize)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Popup {
|
impl Popup {
|
||||||
|
@ -33,7 +35,7 @@ impl Popup {
|
||||||
/// and an empty `gtk::Box` container.
|
/// and an empty `gtk::Box` container.
|
||||||
pub fn new(module_info: &ModuleInfo, gap: i32) -> Self {
|
pub fn new(module_info: &ModuleInfo, gap: i32) -> Self {
|
||||||
let pos = module_info.bar_position;
|
let pos = module_info.bar_position;
|
||||||
let orientation = pos.get_orientation();
|
let orientation = pos.orientation();
|
||||||
|
|
||||||
let win = ApplicationWindow::builder()
|
let win = ApplicationWindow::builder()
|
||||||
.application(module_info.app)
|
.application(module_info.app)
|
||||||
|
@ -104,14 +106,14 @@ impl Popup {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
window: win,
|
window: win,
|
||||||
cache: HashMap::new(),
|
cache: Rc::new(RefCell::new(HashMap::new())),
|
||||||
monitor: module_info.monitor.clone(),
|
monitor: module_info.monitor.clone(),
|
||||||
pos,
|
pos,
|
||||||
current_widget: None,
|
current_widget: Rc::new(RefCell::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) {
|
pub fn register_content(&self, key: usize, name: String, content: ModulePopupParts) {
|
||||||
debug!("Registered popup content for #{}", key);
|
debug!("Registered popup content for #{}", key);
|
||||||
|
|
||||||
for button in &content.buttons {
|
for button in &content.buttons {
|
||||||
|
@ -119,45 +121,94 @@ impl Popup {
|
||||||
button.set_tag("popup-id", id);
|
button.set_tag("popup-id", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cache.insert(key, PopupCacheValue { name, content });
|
let orientation = self.pos.orientation();
|
||||||
|
let monitor = self.monitor.clone();
|
||||||
|
let window = self.window.clone();
|
||||||
|
|
||||||
|
let current_widget = self.current_widget.clone();
|
||||||
|
let cache = self.cache.clone();
|
||||||
|
|
||||||
|
content
|
||||||
|
.container
|
||||||
|
.connect_size_allocate(move |container, rect| {
|
||||||
|
if container.is_visible() {
|
||||||
|
trace!("Resized: {}x{}", rect.width(), rect.height());
|
||||||
|
|
||||||
|
if let Some((widget_id, button_id)) = *current_widget.borrow() {
|
||||||
|
if let Some(PopupCacheValue { content, .. }) =
|
||||||
|
cache.borrow().get(&widget_id)
|
||||||
|
{
|
||||||
|
Self::set_position(
|
||||||
|
&content.buttons,
|
||||||
|
button_id,
|
||||||
|
orientation,
|
||||||
|
&monitor,
|
||||||
|
&window,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.cache
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(key, PopupCacheValue { name, content });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(&mut self, widget_id: usize, button_id: usize) {
|
pub fn show(&self, widget_id: usize, button_id: usize) {
|
||||||
self.clear_window();
|
self.clear_window();
|
||||||
|
|
||||||
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) {
|
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
|
||||||
self.current_widget = Some(widget_id);
|
*self.current_widget.borrow_mut() = Some((widget_id, button_id));
|
||||||
|
|
||||||
content.container.style_context().add_class("popup");
|
content.container.style_context().add_class("popup");
|
||||||
self.window.add(&content.container);
|
self.window.add(&content.container);
|
||||||
|
|
||||||
self.window.show();
|
self.window.show();
|
||||||
|
|
||||||
let button = content
|
Self::set_position(
|
||||||
.buttons
|
&content.buttons,
|
||||||
.iter()
|
button_id,
|
||||||
.find(|b| b.popup_id() == button_id)
|
self.pos.orientation(),
|
||||||
.expect("to find valid button");
|
&self.monitor,
|
||||||
|
&self.window,
|
||||||
let orientation = self.pos.get_orientation();
|
);
|
||||||
let geometry = button.geometry(orientation);
|
|
||||||
|
|
||||||
self.set_pos(geometry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
|
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
|
||||||
self.clear_window();
|
self.clear_window();
|
||||||
|
|
||||||
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) {
|
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
|
||||||
content.container.style_context().add_class("popup");
|
content.container.style_context().add_class("popup");
|
||||||
self.window.add(&content.container);
|
self.window.add(&content.container);
|
||||||
|
|
||||||
self.window.show();
|
self.window.show();
|
||||||
self.set_pos(geometry);
|
Self::set_pos(
|
||||||
|
geometry,
|
||||||
|
self.pos.orientation(),
|
||||||
|
&self.monitor,
|
||||||
|
&self.window,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_position(
|
||||||
|
buttons: &[Button],
|
||||||
|
button_id: usize,
|
||||||
|
orientation: Orientation,
|
||||||
|
monitor: &Monitor,
|
||||||
|
window: &ApplicationWindow,
|
||||||
|
) {
|
||||||
|
let button = buttons
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.popup_id() == button_id)
|
||||||
|
.expect("to find valid button");
|
||||||
|
|
||||||
|
let geometry = button.geometry(orientation);
|
||||||
|
Self::set_pos(geometry, orientation, monitor, window);
|
||||||
|
}
|
||||||
|
|
||||||
fn clear_window(&self) {
|
fn clear_window(&self) {
|
||||||
let children = self.window.children();
|
let children = self.window.children();
|
||||||
for child in children {
|
for child in children {
|
||||||
|
@ -166,8 +217,8 @@ impl Popup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hides the popover
|
/// Hides the popover
|
||||||
pub fn hide(&mut self) {
|
pub fn hide(&self) {
|
||||||
self.current_widget = None;
|
*self.current_widget.borrow_mut() = None;
|
||||||
self.window.hide();
|
self.window.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,22 +228,25 @@ impl Popup {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_widget(&self) -> Option<usize> {
|
pub fn current_widget(&self) -> Option<usize> {
|
||||||
self.current_widget
|
self.current_widget.borrow().map(|w| w.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the popup's X/Y position relative to the left or border of the screen
|
/// Sets the popup's X/Y position relative to the left or border of the screen
|
||||||
/// (depending on orientation).
|
/// (depending on orientation).
|
||||||
fn set_pos(&self, geometry: WidgetGeometry) {
|
fn set_pos(
|
||||||
let orientation = self.pos.get_orientation();
|
geometry: WidgetGeometry,
|
||||||
|
orientation: Orientation,
|
||||||
let mon_workarea = self.monitor.workarea();
|
monitor: &Monitor,
|
||||||
|
window: &ApplicationWindow,
|
||||||
|
) {
|
||||||
|
let mon_workarea = monitor.workarea();
|
||||||
let screen_size = if orientation == Orientation::Horizontal {
|
let screen_size = if orientation == Orientation::Horizontal {
|
||||||
mon_workarea.width()
|
mon_workarea.width()
|
||||||
} else {
|
} else {
|
||||||
mon_workarea.height()
|
mon_workarea.height()
|
||||||
};
|
};
|
||||||
|
|
||||||
let (popup_width, popup_height) = self.window.size();
|
let (popup_width, popup_height) = window.size();
|
||||||
let popup_size = if orientation == Orientation::Horizontal {
|
let popup_size = if orientation == Orientation::Horizontal {
|
||||||
popup_width
|
popup_width
|
||||||
} else {
|
} else {
|
||||||
|
@ -217,6 +271,6 @@ impl Popup {
|
||||||
gtk_layer_shell::Edge::Top
|
gtk_layer_shell::Edge::Top
|
||||||
};
|
};
|
||||||
|
|
||||||
self.window.set_layer_shell_margin(edge, offset as i32);
|
window.set_layer_shell_margin(edge, offset as i32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue