make_log_macro!(debug, "sound");
mod alsa;
#[cfg(feature = "pipewire")]
pub mod pipewire;
#[cfg(feature = "pulseaudio")]
mod pulseaudio;
use super::prelude::*;
use crate::wrappers::SerdeRegex;
use indexmap::IndexMap;
use regex::Regex;
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
pub driver: SoundDriver,
pub name: Option<String>,
pub device: Option<String>,
pub device_kind: DeviceKind,
pub natural_mapping: bool,
#[default(5)]
pub step_width: u32,
pub format: FormatConfig,
pub format_alt: Option<FormatConfig>,
pub headphones_indicator: bool,
pub show_volume_when_muted: bool,
pub mappings: Option<IndexMap<String, String>>,
#[default(true)]
pub mappings_use_regex: bool,
pub max_vol: Option<u32>,
pub active_port_mappings: IndexMap<SerdeRegex, String>,
}
enum Mappings<'a> {
Exact(&'a IndexMap<String, String>),
Regex(Vec<(Regex, &'a str)>),
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[
(MouseButton::Left, None, "toggle_format"),
(MouseButton::Right, None, "toggle_mute"),
(MouseButton::WheelUp, None, "volume_up"),
(MouseButton::WheelDown, None, "volume_down"),
])?;
let mut format = config.format.with_default(" $icon {$volume.eng(w:2)|} ")?;
let mut format_alt = match &config.format_alt {
Some(f) => Some(f.with_default("")?),
None => None,
};
let device_kind = config.device_kind;
let step_width = config.step_width.clamp(0, 50) as i32;
let icon = |muted: bool, device: &dyn SoundDevice| -> &'static str {
if config.headphones_indicator && device_kind == DeviceKind::Sink {
let form_factor = device.form_factor();
let active_port = device.active_port();
debug!("form_factor = {form_factor:?} active_port = {active_port:?}");
let headphones = match form_factor {
Some("headset") | Some("headphone") | Some("hands-free") | Some("portable") => true,
_ => active_port
.as_ref()
.is_some_and(|p| p.to_lowercase().contains("headphone")),
};
if headphones {
return "headphones";
}
}
if muted {
match device_kind {
DeviceKind::Source => "microphone_muted",
DeviceKind::Sink => "volume_muted",
}
} else {
match device_kind {
DeviceKind::Source => "microphone",
DeviceKind::Sink => "volume",
}
}
};
type DeviceType = Box<dyn SoundDevice>;
let mut device: DeviceType = match config.driver {
SoundDriver::Alsa => Box::new(alsa::Device::new(
config.name.clone().unwrap_or_else(|| "Master".into()),
config.device.clone().unwrap_or_else(|| "default".into()),
config.natural_mapping,
)?),
#[cfg(feature = "pipewire")]
SoundDriver::Pipewire => {
Box::new(pipewire::Device::new(config.device_kind, config.name.clone()).await?)
}
#[cfg(feature = "pulseaudio")]
SoundDriver::PulseAudio => Box::new(pulseaudio::Device::new(
config.device_kind,
config.name.clone(),
)?),
SoundDriver::Auto => 'blk: {
#[cfg(feature = "pipewire")]
if let Ok(pipewire) =
pipewire::Device::new(config.device_kind, config.name.clone()).await
{
break 'blk Box::new(pipewire);
}
#[cfg(feature = "pulseaudio")]
if let Ok(pulse) = pulseaudio::Device::new(config.device_kind, config.name.clone()) {
break 'blk Box::new(pulse);
}
Box::new(alsa::Device::new(
config.name.clone().unwrap_or_else(|| "Master".into()),
config.device.clone().unwrap_or_else(|| "default".into()),
config.natural_mapping,
)?)
}
};
let mappings = match &config.mappings {
Some(m) => {
if config.mappings_use_regex {
Some(Mappings::Regex(
m.iter()
.map(|(key, val)| {
Ok((
Regex::new(key)
.error("Failed to parse `{key}` in mappings as regex")?,
val.as_str(),
))
})
.collect::<Result<_>>()?,
))
} else {
Some(Mappings::Exact(m))
}
}
None => None,
};
loop {
device.get_info().await?;
let volume = device.volume();
let muted = device.muted();
let mut output_name = device.output_name();
let mut active_port = device.active_port();
match &mappings {
Some(Mappings::Regex(m)) => {
if let Some((regex, mapped)) =
m.iter().find(|(regex, _)| regex.is_match(&output_name))
{
output_name = regex.replace(&output_name, *mapped).into_owned();
}
}
Some(Mappings::Exact(m)) => {
if let Some(mapped) = m.get(&output_name) {
output_name.clone_from(mapped);
}
}
None => (),
}
if let Some(ap) = &active_port
&& let Some((regex, mapped)) = config
.active_port_mappings
.iter()
.find(|(regex, _)| regex.0.is_match(ap))
{
let mapped = regex.0.replace(ap, mapped);
if mapped.is_empty() {
active_port = None;
} else {
active_port = Some(mapped.into_owned());
}
}
let output_description = device
.output_description()
.unwrap_or_else(|| output_name.clone());
let mut values = map! {
"icon" => Value::icon_progression(icon(muted, &*device), volume as f64 / 100.0),
"volume" => Value::percents(volume),
"output_name" => Value::text(output_name),
"output_description" => Value::text(output_description),
[if let Some(ap) = active_port] "active_port" => Value::text(ap),
};
let mut widget = Widget::new().with_format(format.clone());
if muted {
widget.state = State::Warning;
if !config.show_volume_when_muted {
values.remove("volume");
}
}
widget.set_values(values);
api.set_widget(widget)?;
loop {
select! {
val = device.wait_for_update() => {
val?;
break;
}
_ = api.wait_for_update_request() => break,
Some(action) = actions.recv() => match action.as_ref() {
"toggle_format" => {
if let Some(format_alt) = &mut format_alt {
std::mem::swap(format_alt, &mut format);
break;
}
}
"toggle_mute" => {
device.toggle().await?;
}
"volume_up" => {
device.set_volume(step_width, config.max_vol).await?;
}
"volume_down" => {
device.set_volume(-step_width, config.max_vol).await?;
}
_ => (),
}
}
}
}
}
#[derive(Deserialize, Debug, SmartDefault, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum SoundDriver {
#[default]
Auto,
Alsa,
#[cfg(feature = "pipewire")]
Pipewire,
#[cfg(feature = "pulseaudio")]
PulseAudio,
}
#[derive(Deserialize, Debug, SmartDefault, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum DeviceKind {
#[default]
Sink,
Source,
}
#[async_trait::async_trait]
trait SoundDevice {
fn volume(&self) -> u32;
fn muted(&self) -> bool;
fn output_name(&self) -> String;
fn output_description(&self) -> Option<String>;
fn active_port(&self) -> Option<String>;
fn form_factor(&self) -> Option<&str>;
async fn get_info(&mut self) -> Result<()>;
async fn set_volume(&mut self, step: i32, max_vol: Option<u32>) -> Result<()>;
async fn toggle(&mut self) -> Result<()>;
async fn wait_for_update(&mut self) -> Result<()>;
}