mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-01 10:41:03 +02:00
commit
3c7e434e52
21 changed files with 1295 additions and 7 deletions
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
|
@ -34,7 +34,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: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy --no-default-features --features config+json
|
run: cargo clippy --no-default-features --features config+json
|
||||||
|
@ -55,7 +55,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: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy --all-targets --all-features
|
run: cargo clippy --all-targets --all-features
|
||||||
|
@ -74,7 +74,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: Build
|
- name: Build
|
||||||
run: cargo build --verbose
|
run: cargo build --verbose
|
||||||
|
|
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
|
||||||
|
|
39
Cargo.lock
generated
39
Cargo.lock
generated
|
@ -1650,6 +1650,7 @@ dependencies = [
|
||||||
"gtk-layer-shell",
|
"gtk-layer-shell",
|
||||||
"hyprland",
|
"hyprland",
|
||||||
"indexmap 2.2.5",
|
"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"
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -22,6 +22,7 @@ default = [
|
||||||
"sys_info",
|
"sys_info",
|
||||||
"tray",
|
"tray",
|
||||||
"upower",
|
"upower",
|
||||||
|
"volume",
|
||||||
"workspaces+all"
|
"workspaces+all"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -62,6 +63,8 @@ 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"]
|
||||||
|
@ -132,6 +135,10 @@ upower_dbus = { version = "0.3.2", optional = true }
|
||||||
futures-lite = { version = "2.2.0", optional = true }
|
futures-lite = { version = "2.2.0", optional = true }
|
||||||
zbus = { version = "3.15.2", optional = true }
|
zbus = { version = "3.15.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 }
|
||||||
|
@ -140,4 +147,4 @@ futures-util = { version = "0.3.30", optional = true }
|
||||||
# shared
|
# shared
|
||||||
regex = { version = "1.10.3", default-features = false, features = [
|
regex = { version = "1.10.3", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
], optional = true } # music, sys_info
|
], optional = true } # music, sys_info
|
||||||
|
|
|
@ -183,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. |
|
||||||
|
|
|
@ -34,4 +34,5 @@
|
||||||
- [Sys_Info](sys-info)
|
- [Sys_Info](sys-info)
|
||||||
- [Tray](tray)
|
- [Tray](tray)
|
||||||
- [Upower](upower)
|
- [Upower](upower)
|
||||||
|
- [Volume](volume)
|
||||||
- [Workspaces](workspaces)
|
- [Workspaces](workspaces)
|
||||||
|
|
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).
|
||||||
|
|
||||||
|
TODO: Screenshot
|
||||||
|
|
||||||
|
## 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).
|
|
@ -67,6 +67,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 +110,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 ]
|
||||||
}
|
}
|
||||||
in {
|
in {
|
||||||
anchor_to_edges = true
|
anchor_to_edges = true
|
||||||
|
|
|
@ -174,6 +174,11 @@ scale trough {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -- volume -- */
|
||||||
|
|
||||||
|
.popup-volume .device-box {
|
||||||
|
border-right: 1px solid @color_border;
|
||||||
|
}
|
||||||
|
|
||||||
/* -- workspaces -- */
|
/* -- workspaces -- */
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
];
|
];
|
||||||
|
|
|
@ -392,6 +392,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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ pub mod music;
|
||||||
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
|
||||||
|
@ -27,6 +29,8 @@ pub struct Clients {
|
||||||
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 {
|
||||||
|
@ -86,6 +90,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
|
||||||
|
|
312
src/clients/volume/mod.rs
Normal file
312
src/clients/volume/mod.rs
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
mod sink;
|
||||||
|
mod sink_input;
|
||||||
|
|
||||||
|
use crate::{arc_mut, lock, register_client, send, spawn_blocking};
|
||||||
|
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", "dev.jstanger.ironbar")
|
||||||
|
.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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,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;
|
||||||
|
@ -52,6 +54,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>),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,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;
|
||||||
|
|
||||||
|
|
427
src/modules/volume.rs
Normal file
427
src/modules/volume.rs
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
use crate::clients::volume;
|
||||||
|
use crate::clients::volume::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,
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue