#[cfg(feature = "pulseaudio")]
use {
crate::pulse::callbacks::ListResult,
crate::pulse::context::{
introspect::ServerInfo, introspect::SinkInfo, introspect::SourceInfo, subscribe::Facility,
subscribe::InterestMaskSet, subscribe::Operation as SubscribeOperation, Context, FlagSet,
State as PulseState,
},
crate::pulse::mainloop::standard::IterateResult,
crate::pulse::mainloop::standard::Mainloop,
crate::pulse::proplist::{properties, Proplist},
crate::pulse::volume::{ChannelVolumes, Volume},
crossbeam_channel::unbounded,
lazy_static::lazy_static,
std::cell::RefCell,
std::collections::HashMap,
std::convert::{TryFrom, TryInto},
std::ops::Deref,
std::rc::Rc,
std::sync::Mutex,
};
use std::cmp::{max, min};
use std::collections::BTreeMap;
use std::io::Read;
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use serde_derive::Deserialize;
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::config::{LogicalDirection, Scrolling};
use crate::errors::*;
use crate::formatting::value::Value;
use crate::formatting::FormatTemplate;
use crate::input::{I3BarEvent, MouseButton};
use crate::scheduler::Task;
use crate::subprocess::spawn_child_async;
use crate::widgets::text::TextWidget;
use crate::widgets::{I3BarWidget, Spacing, State};
trait SoundDevice {
fn volume(&self) -> u32;
fn muted(&self) -> bool;
fn output_name(&self) -> String;
fn output_description(&self) -> Option<String>;
fn get_info(&mut self) -> Result<()>;
fn set_volume(&mut self, step: i32, max_vol: Option<u32>) -> Result<()>;
fn toggle(&mut self) -> Result<()>;
fn monitor(&mut self, id: usize, tx_update_request: Sender<Task>) -> Result<()>;
}
struct AlsaSoundDevice {
name: String,
device: String,
natural_mapping: bool,
volume: u32,
muted: bool,
}
impl AlsaSoundDevice {
fn new(name: String, device: String, natural_mapping: bool) -> Result<Self> {
let mut sd = AlsaSoundDevice {
name,
device,
natural_mapping,
volume: 0,
muted: false,
};
sd.get_info()?;
Ok(sd)
}
}
impl SoundDevice for AlsaSoundDevice {
fn volume(&self) -> u32 {
self.volume
}
fn muted(&self) -> bool {
self.muted
}
fn output_name(&self) -> String {
self.name.clone()
}
fn output_description(&self) -> Option<String> {
None
}
fn get_info(&mut self) -> Result<()> {
let mut args = Vec::new();
if self.natural_mapping {
args.push("-M")
};
args.extend(&["-D", &self.device, "get", &self.name]);
let output = Command::new("amixer")
.args(&args)
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())
.block_error("sound", "could not run amixer to get sound info")?;
let last_line = &output
.lines()
.last()
.block_error("sound", "could not get sound info")?;
let last = last_line
.split_whitespace()
.filter(|x| x.starts_with('[') && !x.contains("dB"))
.map(|s| s.trim_matches(FILTER))
.collect::<Vec<&str>>();
self.volume = last
.get(0)
.block_error("sound", "could not get volume")?
.parse::<u32>()
.block_error("sound", "could not parse volume to u32")?;
self.muted = last.get(1).map(|muted| *muted == "off").unwrap_or(false);
Ok(())
}
fn set_volume(&mut self, step: i32, max_vol: Option<u32>) -> Result<()> {
let new_vol = max(0, self.volume as i32 + step) as u32;
let capped_volume = if let Some(vol_cap) = max_vol {
min(new_vol, vol_cap)
} else {
new_vol
};
let mut args = Vec::new();
if self.natural_mapping {
args.push("-M")
};
let vol_str = &format!("{}%", capped_volume);
args.extend(&["-D", &self.device, "set", &self.name, &vol_str]);
Command::new("amixer")
.args(&args)
.output()
.block_error("sound", "failed to set volume")?;
self.volume = capped_volume;
Ok(())
}
fn toggle(&mut self) -> Result<()> {
let mut args = Vec::new();
if self.natural_mapping {
args.push("-M")
};
args.extend(&["-D", &self.device, "set", &self.name, "toggle"]);
Command::new("amixer")
.args(&args)
.output()
.block_error("sound", "failed to toggle mute")?;
self.muted = !self.muted;
Ok(())
}
fn monitor(&mut self, id: usize, tx_update_request: Sender<Task>) -> Result<()> {
thread::Builder::new()
.name("sound_alsa".into())
.spawn(move || {
let mut monitor = Command::new("stdbuf")
.args(&["-oL", "alsactl", "monitor"])
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start alsactl monitor")
.stdout
.expect("Failed to pipe alsactl monitor output");
let mut buffer = [0; 1024];
loop {
if monitor.read(&mut buffer).is_ok() {
tx_update_request
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
thread::sleep(Duration::new(0, 250_000_000))
}
})
.unwrap();
Ok(())
}
}
#[cfg(feature = "pulseaudio")]
struct PulseAudioConnection {
mainloop: Rc<RefCell<Mainloop>>,
context: Rc<RefCell<Context>>,
}
#[cfg(feature = "pulseaudio")]
struct PulseAudioClient {
sender: Sender<PulseAudioClientRequest>,
}
#[cfg(feature = "pulseaudio")]
struct PulseAudioSoundDevice {
name: Option<String>,
description: Option<String>,
device_kind: DeviceKind,
volume: Option<ChannelVolumes>,
volume_avg: u32,
muted: bool,
}
#[cfg(feature = "pulseaudio")]
#[derive(Debug)]
struct PulseAudioVolInfo {
volume: ChannelVolumes,
mute: bool,
name: String,
description: Option<String>,
}
#[cfg(feature = "pulseaudio")]
impl TryFrom<&SourceInfo<'_>> for PulseAudioVolInfo {
type Error = ();
fn try_from(source_info: &SourceInfo) -> std::result::Result<Self, Self::Error> {
match source_info.name.as_ref() {
None => Err(()),
Some(name) => Ok(PulseAudioVolInfo {
volume: source_info.volume,
mute: source_info.mute,
name: name.to_string(),
description: source_info
.description
.clone()
.map(|description| description.into_owned()),
}),
}
}
}
#[cfg(feature = "pulseaudio")]
impl TryFrom<&SinkInfo<'_>> for PulseAudioVolInfo {
type Error = ();
fn try_from(sink_info: &SinkInfo) -> std::result::Result<Self, Self::Error> {
match sink_info.name.as_ref() {
None => Err(()),
Some(name) => Ok(PulseAudioVolInfo {
volume: sink_info.volume,
mute: sink_info.mute,
name: name.to_string(),
description: sink_info
.description
.clone()
.map(|description| description.into_owned()),
}),
}
}
}
#[cfg(feature = "pulseaudio")]
#[derive(Debug)]
enum PulseAudioClientRequest {
GetDefaultDevice,
GetInfoByIndex(DeviceKind, u32),
GetInfoByName(DeviceKind, String),
SetVolumeByName(DeviceKind, String, ChannelVolumes),
SetMuteByName(DeviceKind, String, bool),
}
#[cfg(feature = "pulseaudio")]
lazy_static! {
static ref PULSEAUDIO_CLIENT: Result<PulseAudioClient> = PulseAudioClient::new();
static ref PULSEAUDIO_EVENT_LISTENER: Mutex<HashMap<usize, Sender<Task>>> =
Mutex::new(HashMap::new());
static ref PULSEAUDIO_DEFAULT_SOURCE: Mutex<String> = Mutex::new("@DEFAULT_SOURCE@".into());
static ref PULSEAUDIO_DEFAULT_SINK: Mutex<String> = Mutex::new("@DEFAULT_SINK@".into());
static ref PULSEAUDIO_DEVICES: Mutex<HashMap<(DeviceKind, String), PulseAudioVolInfo>> =
Mutex::new(HashMap::new());
}
#[cfg(feature = "pulseaudio")]
impl PulseAudioConnection {
fn new() -> Result<Self> {
let mut proplist = Proplist::new().unwrap();
proplist
.set_str(properties::APPLICATION_NAME, "i3status-rs")
.block_error(
"sound",
"could not set pulseaudio APPLICATION_NAME property",
)?;
let mainloop = Rc::new(RefCell::new(
Mainloop::new().block_error("sound", "failed to create pulseaudio mainloop")?,
));
let context = Rc::new(RefCell::new(
Context::new_with_proplist(mainloop.borrow().deref(), "i3status-rs_context", &proplist)
.block_error("sound", "failed to create new pulseaudio context")?,
));
context
.borrow_mut()
.connect(None, FlagSet::NOFLAGS, None)
.block_error("sound", "failed to connect to pulseaudio context")?;
let mut connection = PulseAudioConnection { mainloop, context };
loop {
connection.iterate(false)?;
match connection.context.borrow().get_state() {
PulseState::Ready => {
break;
}
PulseState::Failed | PulseState::Terminated => {
return Err(BlockError(
"sound".into(),
"pulseaudio context state failed/terminated".into(),
))
}
_ => {}
}
}
Ok(connection)
}
fn iterate(&mut self, blocking: bool) -> Result<()> {
match self.mainloop.borrow_mut().iterate(blocking) {
IterateResult::Quit(_) | IterateResult::Err(_) => Err(BlockError(
"sound".into(),
"failed to iterate pulseaudio state".into(),
)),
IterateResult::Success(_) => Ok(()),
}
}
}
#[cfg(feature = "pulseaudio")]
impl PulseAudioClient {
fn new() -> Result<PulseAudioClient> {
let (send_req, recv_req) = unbounded();
let (send_result, recv_result) = unbounded();
let send_result2 = send_result.clone();
let new_connection = |sender: Sender<Result<()>>| -> PulseAudioConnection {
let conn = PulseAudioConnection::new();
match conn {
Ok(conn) => {
sender.send(Ok(())).unwrap();
conn
}
Err(err) => {
sender.send(Err(err)).unwrap();
panic!("failed to create pulseaudio connection");
}
}
};
let thread_result = || -> Result<()> {
recv_result
.recv()
.block_error("sound", "failed to receive from pulseaudio thread channel")?
};
thread::Builder::new()
.name("sound_pulseaudio_req".into())
.spawn(move || {
let mut connection = new_connection(send_result);
loop {
for _ in 0..10 {
connection.iterate(false).unwrap();
}
match recv_req.recv() {
Err(_) => {}
Ok(req) => {
use PulseAudioClientRequest::*;
let mut introspector = connection.context.borrow_mut().introspect();
match req {
GetDefaultDevice => {
introspector
.get_server_info(PulseAudioClient::server_info_callback);
}
GetInfoByIndex(DeviceKind::Sink, index) => {
introspector.get_sink_info_by_index(
index,
PulseAudioClient::sink_info_callback,
);
}
GetInfoByIndex(DeviceKind::Source, index) => {
introspector.get_source_info_by_index(
index,
PulseAudioClient::source_info_callback,
);
}
GetInfoByName(DeviceKind::Sink, name) => {
introspector.get_sink_info_by_name(
&name,
PulseAudioClient::sink_info_callback,
);
}
GetInfoByName(DeviceKind::Source, name) => {
introspector.get_source_info_by_name(
&name,
PulseAudioClient::source_info_callback,
);
}
SetVolumeByName(DeviceKind::Sink, name, volumes) => {
introspector.set_sink_volume_by_name(&name, &volumes, None);
}
SetVolumeByName(DeviceKind::Source, name, volumes) => {
introspector.set_source_volume_by_name(&name, &volumes, None);
}
SetMuteByName(DeviceKind::Sink, name, mute) => {
introspector.set_sink_mute_by_name(&name, mute, None);
}
SetMuteByName(DeviceKind::Source, name, mute) => {
introspector.set_source_mute_by_name(&name, mute, None);
}
};
connection.iterate(true).unwrap();
connection.iterate(true).unwrap();
}
}
}
})
.unwrap();
thread_result()?;
thread::Builder::new()
.name("sound_pulseaudio_sub".into())
.spawn(move || {
let connection = new_connection(send_result2);
connection
.context
.borrow_mut()
.set_subscribe_callback(Some(Box::new(PulseAudioClient::subscribe_callback)));
connection.context.borrow_mut().subscribe(
InterestMaskSet::SERVER | InterestMaskSet::SINK | InterestMaskSet::SOURCE,
|_| {},
);
connection.mainloop.borrow_mut().run().unwrap();
})
.unwrap();
thread_result()?;
Ok(PulseAudioClient { sender: send_req })
}
fn send(request: PulseAudioClientRequest) -> Result<()> {
match PULSEAUDIO_CLIENT.as_ref() {
Ok(client) => {
client.sender.send(request).unwrap();
Ok(())
}
Err(err) => Err(BlockError(
"sound".into(),
format!("pulseaudio connection failed with error: {}", err),
)),
}
}
fn server_info_callback(server_info: &ServerInfo) {
if let Some(default_sink) = server_info.default_sink_name.as_ref() {
*PULSEAUDIO_DEFAULT_SINK.lock().unwrap() = default_sink.to_string();
}
if let Some(default_source) = server_info.default_source_name.as_ref() {
*PULSEAUDIO_DEFAULT_SOURCE.lock().unwrap() = default_source.to_string();
}
PulseAudioClient::send_update_event();
}
fn get_info_callback<I: TryInto<PulseAudioVolInfo>>(
result: ListResult<I>,
) -> Option<PulseAudioVolInfo> {
match result {
ListResult::End | ListResult::Error => None,
ListResult::Item(info) => info.try_into().ok(),
}
}
fn sink_info_callback(result: ListResult<&SinkInfo>) {
if let Some(vol_info) = Self::get_info_callback(result) {
PULSEAUDIO_DEVICES
.lock()
.unwrap()
.insert((DeviceKind::Sink, vol_info.name.to_string()), vol_info);
PulseAudioClient::send_update_event();
}
}
fn source_info_callback(result: ListResult<&SourceInfo>) {
if let Some(vol_info) = Self::get_info_callback(result) {
PULSEAUDIO_DEVICES
.lock()
.unwrap()
.insert((DeviceKind::Source, vol_info.name.to_string()), vol_info);
PulseAudioClient::send_update_event();
}
}
fn subscribe_callback(
facility: Option<Facility>,
_operation: Option<SubscribeOperation>,
index: u32,
) {
match facility {
None => {}
Some(facility) => match facility {
Facility::Server => {
PulseAudioClient::send(PulseAudioClientRequest::GetDefaultDevice).ok();
}
Facility::Sink => {
PulseAudioClient::send(PulseAudioClientRequest::GetInfoByIndex(
DeviceKind::Sink,
index,
))
.ok();
}
Facility::Source => {
PulseAudioClient::send(PulseAudioClientRequest::GetInfoByIndex(
DeviceKind::Source,
index,
))
.ok();
}
_ => {}
},
}
}
fn send_update_event() {
for (id, tx_update_request) in &*PULSEAUDIO_EVENT_LISTENER.lock().unwrap() {
tx_update_request
.send(Task {
id: *id,
update_time: Instant::now(),
})
.unwrap();
}
}
}
#[cfg(feature = "pulseaudio")]
impl PulseAudioSoundDevice {
fn new(device_kind: DeviceKind) -> Result<Self> {
PulseAudioClient::send(PulseAudioClientRequest::GetDefaultDevice)?;
let device = PulseAudioSoundDevice {
name: None,
description: None,
device_kind,
volume: None,
volume_avg: 0,
muted: false,
};
PulseAudioClient::send(PulseAudioClientRequest::GetInfoByName(
device_kind,
device.name(),
))?;
Ok(device)
}
fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
fn name(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| self.device_kind.default_name())
}
fn volume(&mut self, volume: ChannelVolumes) {
self.volume = Some(volume);
self.volume_avg = (volume.avg().0 as f32 / Volume::NORMAL.0 as f32 * 100.0).round() as u32;
}
}
#[cfg(feature = "pulseaudio")]
impl SoundDevice for PulseAudioSoundDevice {
fn volume(&self) -> u32 {
self.volume_avg
}
fn muted(&self) -> bool {
self.muted
}
fn output_name(&self) -> String {
self.name()
}
fn output_description(&self) -> Option<String> {
self.description.clone()
}
fn get_info(&mut self) -> Result<()> {
let devices = PULSEAUDIO_DEVICES.lock().unwrap();
if let Some(info) = devices.get(&(self.device_kind, self.name())) {
self.volume(info.volume);
self.muted = info.mute;
self.description = info.description.clone();
}
Ok(())
}
fn set_volume(&mut self, step: i32, max_vol: Option<u32>) -> Result<()> {
let mut volume = self.volume.block_error("sound", "volume unknown")?;
let step = (step as f32 * Volume::NORMAL.0 as f32 / 100.0).round() as i32;
for vol in volume.get_mut().iter_mut() {
let uncapped_vol = max(0, vol.0 as i32 + step) as u32;
let capped_vol = if let Some(vol_cap) = max_vol {
min(
uncapped_vol,
(vol_cap as f32 * Volume::NORMAL.0 as f32 / 100.0).round() as u32,
)
} else {
uncapped_vol
};
vol.0 = min(capped_vol, Volume::MAX.0);
}
self.volume(volume);
PulseAudioClient::send(PulseAudioClientRequest::SetVolumeByName(
self.device_kind,
self.name(),
volume,
))?;
Ok(())
}
fn toggle(&mut self) -> Result<()> {
self.muted = !self.muted;
PulseAudioClient::send(PulseAudioClientRequest::SetMuteByName(
self.device_kind,
self.name(),
self.muted,
))?;
Ok(())
}
fn monitor(&mut self, id: usize, tx_update_request: Sender<Task>) -> Result<()> {
PULSEAUDIO_EVENT_LISTENER
.lock()
.unwrap()
.insert(id, tx_update_request);
Ok(())
}
}
pub struct Sound {
text: TextWidget,
id: usize,
device: Box<dyn SoundDevice>,
device_kind: DeviceKind,
step_width: u32,
format: FormatTemplate,
on_click: Option<String>,
show_volume_when_muted: bool,
mappings: Option<BTreeMap<String, String>>,
max_vol: Option<u32>,
scrolling: Scrolling,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum DeviceKind {
Sink,
Source,
}
#[cfg(feature = "pulseaudio")]
impl DeviceKind {
pub fn default_name(self) -> String {
match self {
Self::Sink => PULSEAUDIO_DEFAULT_SINK.lock().unwrap().to_string(),
Self::Source => PULSEAUDIO_DEFAULT_SOURCE.lock().unwrap().to_string(),
}
}
}
impl Default for DeviceKind {
fn default() -> Self {
Self::Sink
}
}
#[derive(Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum SoundDriver {
Auto,
Alsa,
#[cfg(feature = "pulseaudio")]
PulseAudio,
}
impl Default for SoundDriver {
fn default() -> Self {
SoundDriver::Auto
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct SoundConfig {
pub driver: SoundDriver,
pub name: Option<String>,
pub device: Option<String>,
pub device_kind: DeviceKind,
pub natural_mapping: bool,
pub step_width: u32,
pub format: String,
pub show_volume_when_muted: bool,
pub mappings: Option<BTreeMap<String, String>>,
pub max_vol: Option<u32>,
}
impl Default for SoundConfig {
fn default() -> Self {
Self {
driver: Default::default(),
name: None,
device: None,
device_kind: Default::default(),
natural_mapping: false,
step_width: 5,
format: "{volume}".to_string(),
show_volume_when_muted: false,
mappings: None,
max_vol: None,
}
}
}
impl Sound {
fn icon(&self, volume: u32) -> String {
let prefix = match self.device_kind {
DeviceKind::Source => "microphone",
DeviceKind::Sink => "volume",
};
let suffix = match volume {
0 => "muted",
1..=20 => "empty",
21..=70 => "half",
_ => "full",
};
format!("{}_{}", prefix, suffix)
}
}
impl ConfigBlock for Sound {
type Config = SoundConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
tx_update_request: Sender<Task>,
) -> Result<Self> {
let mut step_width = block_config.step_width;
if step_width > 50 {
step_width = 50;
}
#[cfg(not(feature = "pulseaudio"))]
type PulseAudioSoundDevice = AlsaSoundDevice;
let pulseaudio_device: Result<PulseAudioSoundDevice> = match block_config.driver {
#[cfg(feature = "pulseaudio")]
SoundDriver::Auto | SoundDriver::PulseAudio => {
let sound_device = PulseAudioSoundDevice::new(block_config.device_kind);
match block_config.name.as_ref() {
None => sound_device,
Some(name) => sound_device.map(|device| device.with_name(name.to_string())),
}
}
_ => Err(BlockError(
"sound".into(),
"PulseAudio feature or driver disabled".into(),
)),
};
let device: Box<dyn SoundDevice> = match pulseaudio_device {
Ok(dev) => Box::new(dev),
Err(_) => Box::new(AlsaSoundDevice::new(
block_config.name.unwrap_or_else(|| "Master".into()),
block_config.device.unwrap_or_else(|| "default".into()),
block_config.natural_mapping,
)?),
};
let mut sound = Self {
id,
device,
device_kind: block_config.device_kind,
format: FormatTemplate::from_string(&block_config.format)?,
step_width,
on_click: None,
show_volume_when_muted: block_config.show_volume_when_muted,
mappings: block_config.mappings,
max_vol: block_config.max_vol,
scrolling: shared_config.scrolling,
text: TextWidget::new(id, 0, shared_config).with_icon("volume_empty")?,
};
sound.device.monitor(id, tx_update_request)?;
Ok(sound)
}
fn override_on_click(&mut self) -> Option<&mut Option<String>> {
Some(&mut self.on_click)
}
}
const FILTER: &[char] = &['[', ']', '%'];
impl Block for Sound {
fn update(&mut self) -> Result<Option<Update>> {
self.device.get_info()?;
let volume = self.device.volume();
let (output_name, output_description) = {
let mut output_name = self.device.output_name();
let mut output_description = self
.device
.output_description()
.unwrap_or_else(|| output_name.clone());
if let Some(mapped_name) = if let Some(m) = &self.mappings {
m.get(&output_name)
.map(|output_name| output_name.to_string())
} else {
None
} {
output_name = mapped_name.clone();
output_description = mapped_name;
}
(output_name, output_description)
};
let values = map!(
"volume" => Value::from_integer(volume as i64).percents(),
"output_name" => Value::from_string(output_name),
"output_description" => Value::from_string(output_description),
);
let text = self.format.render(&values)?;
if self.device.muted() {
self.text.set_icon(&self.icon(0))?;
if self.show_volume_when_muted {
self.text.set_spacing(Spacing::Normal);
self.text.set_text(text);
} else {
self.text.set_text(String::new());
}
self.text.set_state(State::Warning);
} else {
self.text.set_icon(&self.icon(volume))?;
self.text.set_spacing(Spacing::Normal);
self.text.set_state(State::Idle);
self.text.set_text(text);
}
Ok(None)
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
vec![&self.text]
}
fn click(&mut self, e: &I3BarEvent) -> Result<()> {
match e.button {
MouseButton::Right => self.device.toggle()?,
MouseButton::Left => {
if let Some(ref cmd) = self.on_click {
spawn_child_async("sh", &["-c", cmd])
.block_error("sound", "could not spawn child")?;
}
}
_ => {
use LogicalDirection::*;
match self.scrolling.to_logical_direction(e.button) {
Some(Up) => self
.device
.set_volume(self.step_width as i32, self.max_vol)?,
Some(Down) => self
.device
.set_volume(-(self.step_width as i32), self.max_vol)?,
None => (),
}
}
}
self.update()?;
Ok(())
}
fn id(&self) -> usize {
self.id
}
}