From fa9fed58f445d8237176b735224f7cf145258e8a Mon Sep 17 00:00:00 2001 From: Roman Stingler Date: Tue, 11 Aug 2026 21:40:51 +0200 Subject: [PATCH] run optional command after switching audio sink/source --- src/app.rs | 4 +- src/config.rs | 6 +++ src/modules/settings/audio.rs | 49 +++++++++++++++++-- src/modules/settings/mod.rs | 6 ++- website/docs/configuration/full_config.md | 2 + .../docs/configuration/modules/settings.md | 13 +++++ 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index 09a946153..e12811de5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -200,7 +200,9 @@ impl App { self.tempo .update(modules::tempo::Message::ConfigReloaded(config.tempo)); self.settings - .update(modules::settings::Message::ConfigReloaded(config.settings)); + .update(modules::settings::Message::ConfigReloaded(Box::new( + config.settings, + ))); self.media_player .update(modules::media_player::Message::ConfigReloaded( config.media_player, diff --git a/src/config.rs b/src/config.rs index 0363b9351..897be2896 100644 --- a/src/config.rs +++ b/src/config.rs @@ -633,6 +633,10 @@ pub struct SettingsModuleConfig { #[serde(default, deserialize_with = "empty_string_as_none")] pub audio_sources_more_cmd: Option, #[serde(default, deserialize_with = "empty_string_as_none")] + pub audio_sink_post_switch_cmd: Option, + #[serde(default, deserialize_with = "empty_string_as_none")] + pub audio_source_post_switch_cmd: Option, + #[serde(default, deserialize_with = "empty_string_as_none")] pub wifi_more_cmd: Option, #[serde(default, deserialize_with = "empty_string_as_none")] pub vpn_more_cmd: Option, @@ -669,6 +673,8 @@ impl Default for SettingsModuleConfig { brightness_indicator_format: SettingsFormat::Icon, audio_sinks_more_cmd: Default::default(), audio_sources_more_cmd: Default::default(), + audio_sink_post_switch_cmd: Default::default(), + audio_source_post_switch_cmd: Default::default(), wifi_more_cmd: Default::default(), vpn_more_cmd: Default::default(), bluetooth_more_cmd: Default::default(), diff --git a/src/modules/settings/audio.rs b/src/modules/settings/audio.rs index bd196e9df..b11dd8169 100644 --- a/src/modules/settings/audio.rs +++ b/src/modules/settings/audio.rs @@ -8,7 +8,7 @@ use crate::{ config::SettingsFormat, services::{ ReadOnlyService, Service, ServiceEvent, - audio::{AudioCommand, AudioService, ChannelVolumesExt, DevicePortType, Port}, + audio::{AudioCommand, AudioEvent, AudioService, ChannelVolumesExt, DevicePortType, Port}, }, t, theme::use_theme, @@ -57,6 +57,8 @@ pub enum Action { pub struct AudioSettingsConfig { pub sinks_more_cmd: Option, pub sources_more_cmd: Option, + pub sink_post_switch_cmd: Option, + pub source_post_switch_cmd: Option, pub volume_step: u8, pub max_volume: u8, pub indicator_format: SettingsFormat, @@ -64,9 +66,12 @@ pub struct AudioSettingsConfig { } impl AudioSettingsConfig { + #[allow(clippy::too_many_arguments)] pub fn new( sinks_more_cmd: Option, sources_more_cmd: Option, + sink_post_switch_cmd: Option, + source_post_switch_cmd: Option, volume_step: u8, max_volume: u8, indicator_format: SettingsFormat, @@ -75,6 +80,8 @@ impl AudioSettingsConfig { Self { sinks_more_cmd, sources_more_cmd, + sink_post_switch_cmd, + source_post_switch_cmd, volume_step, max_volume, indicator_format, @@ -86,6 +93,8 @@ impl AudioSettingsConfig { pub struct AudioSettings { config: AudioSettingsConfig, service: Option, + pending_sink_switch: bool, + pending_source_switch: bool, } pub struct SubmenuEntry { @@ -106,6 +115,8 @@ impl AudioSettings { Self { config, service: None, + pending_sink_switch: false, + pending_source_switch: false, } } @@ -219,8 +230,32 @@ impl AudioSettings { } ServiceEvent::Update(data) => { if let Some(service) = self.service.as_mut() { + let post_switch = match &data { + AudioEvent::ServerInfo(info) => { + let sink_changed = self.pending_sink_switch + && info.default_sink != service.server_info.default_sink; + let source_changed = self.pending_source_switch + && info.default_source != service.server_info.default_source; + self.pending_sink_switch = false; + self.pending_source_switch = false; + (sink_changed, source_changed) + } + _ => (false, false), + }; + service.update(data); + if post_switch.0 + && let Some(cmd) = &self.config.sink_post_switch_cmd + { + crate::utils::launcher::execute_command(cmd); + } + if post_switch.1 + && let Some(cmd) = &self.config.source_post_switch_cmd + { + crate::utils::launcher::execute_command(cmd); + } + if !service.has_multiple_sinks() { return Action::CloseSubMenu; } @@ -255,7 +290,11 @@ impl AudioSettings { } Message::DefaultSinkChanged(name, port) => { if let Some(service) = self.service.as_mut() { - let _ = service.command(AudioCommand::DefaultSink(name, port)); + let is_noop = name == service.server_info.default_sink && port.is_none(); + if !is_noop { + self.pending_sink_switch = true; + let _ = service.command(AudioCommand::DefaultSink(name, port)); + } } Action::None } @@ -281,7 +320,11 @@ impl AudioSettings { } Message::DefaultSourceChanged(name, port) => { if let Some(service) = self.service.as_mut() { - let _ = service.command(AudioCommand::DefaultSource(name, port)); + let is_noop = name == service.server_info.default_source && port.is_none(); + if !is_noop { + self.pending_source_switch = true; + let _ = service.command(AudioCommand::DefaultSource(name, port)); + } } Action::None } diff --git a/src/modules/settings/mod.rs b/src/modules/settings/mod.rs index fdaf703a2..cda6d0822 100644 --- a/src/modules/settings/mod.rs +++ b/src/modules/settings/mod.rs @@ -105,7 +105,7 @@ pub enum Message { CustomButton(String), CustomButtonsStatus(Vec<(String, Option)>), MenuOpened, - ConfigReloaded(SettingsModuleConfig), + ConfigReloaded(Box), AudioTooltipHover(ButtonUIRef, SurfaceId), BluetoothTooltipHover(ButtonUIRef, SurfaceId), WifiTooltipHover(ButtonUIRef, SurfaceId), @@ -218,6 +218,8 @@ impl Settings { audio: AudioSettings::new(AudioSettingsConfig::new( config.audio_sinks_more_cmd, config.audio_sources_more_cmd, + config.audio_sink_post_switch_cmd, + config.audio_source_post_switch_cmd, config.volume_step, config.max_volume, config.audio_indicator_format, @@ -519,6 +521,8 @@ impl Settings { .update(audio::Message::ConfigReloaded(AudioSettingsConfig::new( config.audio_sinks_more_cmd, config.audio_sources_more_cmd, + config.audio_sink_post_switch_cmd, + config.audio_source_post_switch_cmd, config.volume_step, config.max_volume, config.audio_indicator_format, diff --git a/website/docs/configuration/full_config.md b/website/docs/configuration/full_config.md index 5e1fbcf69..ff47c3d0f 100644 --- a/website/docs/configuration/full_config.md +++ b/website/docs/configuration/full_config.md @@ -156,6 +156,8 @@ lock_cmd = "playerctl --all-players pause; nixGL hyprlock &" # logout_cmd = "loginctl kill-user $(whoami)" # (default) audio_sinks_more_cmd = "pavucontrol -t 3" audio_sources_more_cmd = "pavucontrol -t 4" +# audio_sink_post_switch_cmd = "systemctl --user restart wireplumber" # (default: None) +# audio_source_post_switch_cmd = "..." # (default: None) wifi_more_cmd = "nm-connection-editor" vpn_more_cmd = "nm-connection-editor" bluetooth_more_cmd = "blueberry" diff --git a/website/docs/configuration/modules/settings.md b/website/docs/configuration/modules/settings.md index b6acab234..6b98c550f 100644 --- a/website/docs/configuration/modules/settings.md +++ b/website/docs/configuration/modules/settings.md @@ -58,6 +58,18 @@ options you can set commands to open the audio settings for sinks and sources, if not set the related buttons will not appear. When configured, right-clicking the speaker or microphone indicators (or their quick settings buttons) launches the respective command immediately. +With the `audio_sink_post_switch_cmd` and `audio_source_post_switch_cmd` options +you can set commands that are automatically executed after switching the audio output or input device. +This is useful for working around PipeWire or WirePlumber issues where audio doesn't +properly route to the new device without a service restart. + +The command is executed asynchronously (fire-and-forget) via `bash -c` after every sink or source switch. + +```toml +[settings] +audio_sink_post_switch_cmd = "systemctl --user restart wireplumber" +``` + With the `wifi_more_cmd`, `vpn_more_cmd` and `bluetooth_more_cmd` options you can set commands to open the network, VPN and bluetooth settings. Right-clicking the Wi-Fi, VPN, Bluetooth or airplane-mode quick settings buttons (and the Wi-Fi indicator in the bar) triggers these commands directly when they are set. @@ -404,6 +416,7 @@ We also disable the airplane mode button and the idle inhibitor button. lock_cmd = "hyprlock &" audio_sinks_more_cmd = "pavucontrol -t 3" audio_sources_more_cmd = "pavucontrol -t 4" +audio_sink_post_switch_cmd = "systemctl --user restart wireplumber" wifi_more_cmd = "nm-connection-editor" vpn_more_cmd = "nm-connection-editor" bluetooth_more_cmd = "blueman-manager"