2023-02-02 23:52:59 +01:00
|
|
|
use std::{
|
2023-02-08 22:53:41 +01:00
|
|
|
cell::RefCell,
|
|
|
|
|
error::Error,
|
|
|
|
|
os::fd::RawFd,
|
|
|
|
|
rc::Rc,
|
2023-02-02 23:52:59 +01:00
|
|
|
sync::{Arc, Mutex},
|
|
|
|
|
thread,
|
|
|
|
|
};
|
|
|
|
|
|
2023-02-08 22:53:41 +01:00
|
|
|
use nix::unistd::{pipe, read, write};
|
2023-02-02 23:52:59 +01:00
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
color::Color,
|
|
|
|
|
event::{Event, Events, PointerButton},
|
|
|
|
|
fonts::FontMap,
|
|
|
|
|
widgets::bar_widget::{BarWidget, BarWidgetImpl},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use libpulse_binding::volume::ChannelVolumes;
|
|
|
|
|
|
2023-02-08 22:53:41 +01:00
|
|
|
fn monitor(
|
|
|
|
|
events: Arc<Mutex<Events>>,
|
|
|
|
|
inner: Arc<Mutex<InnerAudio>>,
|
|
|
|
|
pipe: RawFd,
|
|
|
|
|
) -> Result<(), Box<dyn Error>> {
|
2023-02-02 23:52:59 +01:00
|
|
|
use libpulse_binding::{
|
|
|
|
|
callbacks::ListResult,
|
|
|
|
|
context::{
|
2023-02-08 22:53:41 +01:00
|
|
|
Context, FlagSet, State,
|
2026-10-03 09:16:15 +00:00
|
|
|
subscribe::{Facility, InterestMaskSet},
|
2023-02-08 22:53:41 +01:00
|
|
|
},
|
|
|
|
|
mainloop::{
|
|
|
|
|
self, api::Mainloop, events::io::FlagSet as IoFlagSet, standard::IterateResult,
|
2023-02-02 23:52:59 +01:00
|
|
|
},
|
2026-10-03 09:16:15 +00:00
|
|
|
proplist::{Proplist, properties},
|
2023-02-02 23:52:59 +01:00
|
|
|
volume::Volume,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proplist = Proplist::new().unwrap();
|
|
|
|
|
proplist
|
|
|
|
|
.set_str(properties::APPLICATION_NAME, "wldash")
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut mainloop = mainloop::standard::Mainloop::new().unwrap();
|
2023-02-08 22:53:41 +01:00
|
|
|
let context = Rc::new(RefCell::new(
|
|
|
|
|
Context::new_with_proplist(&mainloop, "wldash_context", &proplist).unwrap(),
|
|
|
|
|
));
|
2023-02-02 23:52:59 +01:00
|
|
|
context.borrow_mut().connect(None, FlagSet::NOFLAGS, None)?;
|
|
|
|
|
|
|
|
|
|
loop {
|
2026-10-03 09:15:54 +00:00
|
|
|
match mainloop.iterate(true) {
|
|
|
|
|
IterateResult::Quit(_) | IterateResult::Err(_) => {
|
2026-10-03 09:16:15 +00:00
|
|
|
return Err("pulseaudio mainloop quit while connecting".into());
|
2026-10-03 09:15:54 +00:00
|
|
|
}
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
2023-02-02 23:52:59 +01:00
|
|
|
match context.borrow().get_state() {
|
|
|
|
|
State::Ready => break,
|
2026-10-03 09:15:54 +00:00
|
|
|
State::Failed | State::Terminated => {
|
2026-10-03 09:16:15 +00:00
|
|
|
return Err("could not connect to pulseaudio".into());
|
2026-10-03 09:15:54 +00:00
|
|
|
}
|
2023-02-02 23:52:59 +01:00
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let local_context = context.clone();
|
|
|
|
|
let local_inner = inner.clone();
|
|
|
|
|
let local_events = events.clone();
|
2023-02-08 22:53:41 +01:00
|
|
|
context.borrow_mut().set_subscribe_callback(Some(Box::new(
|
2026-10-03 09:20:25 +00:00
|
|
|
move |facility, _operation, index| {
|
|
|
|
|
if let Some(Facility::Sink) = facility {
|
2023-02-08 22:53:41 +01:00
|
|
|
let introspector = local_context.borrow_mut().introspect();
|
|
|
|
|
let inner = local_inner.clone();
|
|
|
|
|
let events = local_events.clone();
|
|
|
|
|
introspector.get_sink_info_by_index(index, move |res| match res {
|
|
|
|
|
ListResult::End | ListResult::Error => (),
|
|
|
|
|
ListResult::Item(sink_info) => {
|
2026-10-03 09:20:25 +00:00
|
|
|
if sink_info.name.is_some() {
|
2023-02-08 22:53:41 +01:00
|
|
|
let avg = sink_info.volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
|
|
|
|
|
let mut inner = inner.lock().unwrap();
|
|
|
|
|
inner.volume = avg;
|
|
|
|
|
inner.mute = sink_info.mute;
|
|
|
|
|
inner.dirty = true;
|
|
|
|
|
drop(inner);
|
|
|
|
|
let mut events = events.lock().unwrap();
|
|
|
|
|
events.add_event(Event::AudioUpdate);
|
2023-02-02 23:52:59 +01:00
|
|
|
}
|
2023-02-08 22:53:41 +01:00
|
|
|
}
|
|
|
|
|
});
|
2023-02-02 23:52:59 +01:00
|
|
|
}
|
2023-02-08 22:53:41 +01:00
|
|
|
},
|
|
|
|
|
)));
|
2023-02-02 23:52:59 +01:00
|
|
|
|
|
|
|
|
context
|
|
|
|
|
.borrow_mut()
|
|
|
|
|
.subscribe(InterestMaskSet::SINK, |_| {});
|
|
|
|
|
|
|
|
|
|
let introspector = context.borrow_mut().introspect();
|
|
|
|
|
let local_inner = inner.clone();
|
2023-02-08 22:53:41 +01:00
|
|
|
introspector.get_sink_info_by_name("@DEFAULT_SINK@", move |res| match res {
|
|
|
|
|
ListResult::End | ListResult::Error => (),
|
|
|
|
|
ListResult::Item(sink_info) => {
|
2026-10-03 09:20:25 +00:00
|
|
|
if sink_info.name.is_some() {
|
2023-02-02 23:52:59 +01:00
|
|
|
let avg = sink_info.volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
|
|
|
|
|
let mut inner = local_inner.lock().unwrap();
|
|
|
|
|
inner.volume = avg;
|
|
|
|
|
inner.channel_volume = Some(sink_info.volume);
|
|
|
|
|
inner.mute = sink_info.mute;
|
|
|
|
|
inner.dirty = true;
|
|
|
|
|
drop(inner);
|
|
|
|
|
let mut events = events.lock().unwrap();
|
|
|
|
|
events.add_event(Event::AudioUpdate);
|
2023-02-08 22:53:41 +01:00
|
|
|
}
|
2023-02-02 23:52:59 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
drop(introspector);
|
|
|
|
|
|
|
|
|
|
let local_context = context.clone();
|
|
|
|
|
let local_inner = inner.clone();
|
2023-02-08 22:53:41 +01:00
|
|
|
let event_source = mainloop.new_io_event(
|
2026-10-03 09:20:25 +00:00
|
|
|
pipe,
|
2023-02-08 22:53:41 +01:00
|
|
|
IoFlagSet::INPUT,
|
|
|
|
|
Box::new(move |_, _, _| {
|
|
|
|
|
let mut buf = [0; 8];
|
|
|
|
|
_ = read(pipe, &mut buf);
|
2023-02-02 23:52:59 +01:00
|
|
|
|
2023-02-08 22:53:41 +01:00
|
|
|
let mut inner = local_inner.lock().unwrap();
|
|
|
|
|
let mut introspector = local_context.borrow_mut().introspect();
|
2026-10-03 09:15:54 +00:00
|
|
|
// Clicks arriving before the first sink query finished have no
|
|
|
|
|
// channel map to work with; drop them instead of panicking.
|
|
|
|
|
match (inner.last_click.take(), inner.channel_volume) {
|
|
|
|
|
(Some(Change::Volume(volume)), Some(mut vol)) => {
|
2023-02-08 22:53:41 +01:00
|
|
|
vol.scale(Volume((volume * Volume::NORMAL.0 as f32).round() as u32));
|
|
|
|
|
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
|
2023-02-02 23:52:59 +01:00
|
|
|
}
|
2026-10-03 09:15:54 +00:00
|
|
|
(Some(Change::VolumeInc(value)), Some(mut vol)) => {
|
2023-02-08 22:53:41 +01:00
|
|
|
// apply step to volumes
|
|
|
|
|
let step = (value * Volume::NORMAL.0 as f32).round() as i32;
|
|
|
|
|
if step > 0 {
|
|
|
|
|
vol.inc_clamp(Volume(step as u32), Volume::NORMAL);
|
|
|
|
|
} else {
|
|
|
|
|
vol.decrease(Volume(-step as u32));
|
|
|
|
|
}
|
|
|
|
|
// HACK: It would be better to only update the volume from feedback, but axis
|
|
|
|
|
// events fire fast and require the last set volume immediately for reference.
|
|
|
|
|
inner.channel_volume = Some(vol);
|
|
|
|
|
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
|
|
|
|
|
}
|
2026-10-03 09:15:54 +00:00
|
|
|
(Some(Change::ToggleMute), _) => {
|
2023-02-08 22:53:41 +01:00
|
|
|
introspector.set_sink_mute_by_name("@DEFAULT_SINK@", !inner.mute, None);
|
|
|
|
|
}
|
2026-10-03 09:15:54 +00:00
|
|
|
_ => (),
|
2023-02-08 22:53:41 +01:00
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
2023-02-02 23:52:59 +01:00
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
match mainloop.iterate(true) {
|
|
|
|
|
IterateResult::Quit(_) | IterateResult::Err(_) => break,
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Need to keep the event source alive
|
|
|
|
|
_ = event_source;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn start_monitor(events: Arc<Mutex<Events>>, inner: Arc<Mutex<InnerAudio>>, pipe: RawFd) {
|
|
|
|
|
thread::Builder::new()
|
|
|
|
|
.name("audiomon".to_string())
|
2026-10-03 09:15:54 +00:00
|
|
|
.spawn(move || {
|
|
|
|
|
// panic = "abort" in release: a failed audio connection must not
|
|
|
|
|
// kill the dashboard. The widget simply stays at 0.
|
|
|
|
|
if let Err(e) = monitor(events, inner, pipe) {
|
|
|
|
|
eprintln!("wldash: audio monitor stopped: {}", e);
|
|
|
|
|
}
|
|
|
|
|
})
|
2023-02-02 23:52:59 +01:00
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum Change {
|
|
|
|
|
Volume(f32),
|
|
|
|
|
VolumeInc(f32),
|
|
|
|
|
ToggleMute,
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-06 18:34:07 +01:00
|
|
|
struct InnerAudio {
|
2023-02-02 23:52:59 +01:00
|
|
|
volume: f32,
|
|
|
|
|
channel_volume: Option<ChannelVolumes>,
|
|
|
|
|
mute: bool,
|
|
|
|
|
dirty: bool,
|
|
|
|
|
last_click: Option<Change>,
|
|
|
|
|
pipe: RawFd,
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-06 18:34:07 +01:00
|
|
|
pub struct PulseAudio {
|
2023-02-02 23:52:59 +01:00
|
|
|
inner: Arc<Mutex<InnerAudio>>,
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-06 18:34:07 +01:00
|
|
|
impl PulseAudio {
|
2026-10-03 09:20:25 +00:00
|
|
|
// Wraps the impl in the generic BarWidget on purpose.
|
|
|
|
|
#[allow(clippy::new_ret_no_self)]
|
2023-02-08 22:53:41 +01:00
|
|
|
pub fn new(
|
|
|
|
|
events: Arc<Mutex<Events>>,
|
|
|
|
|
fm: &mut FontMap,
|
|
|
|
|
font: &'static str,
|
|
|
|
|
size: f32,
|
|
|
|
|
) -> BarWidget {
|
2023-02-02 23:52:59 +01:00
|
|
|
let (a, b) = pipe().unwrap();
|
2023-02-06 18:34:07 +01:00
|
|
|
let audio = PulseAudio {
|
2023-02-08 22:53:41 +01:00
|
|
|
inner: Arc::new(Mutex::new(InnerAudio {
|
2023-02-02 23:52:59 +01:00
|
|
|
volume: 0.0,
|
|
|
|
|
channel_volume: None,
|
|
|
|
|
mute: false,
|
|
|
|
|
dirty: false,
|
|
|
|
|
last_click: None,
|
|
|
|
|
pipe: b,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
start_monitor(events, audio.inner.clone(), a);
|
|
|
|
|
BarWidget::new(Box::new(audio), fm, font, size)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-06 18:34:07 +01:00
|
|
|
impl BarWidgetImpl for PulseAudio {
|
2023-02-02 23:52:59 +01:00
|
|
|
fn get_dirty(&self) -> bool {
|
|
|
|
|
self.inner.lock().unwrap().dirty
|
|
|
|
|
}
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
"volume"
|
|
|
|
|
}
|
2023-02-06 18:34:07 +01:00
|
|
|
fn value(&mut self) -> f32 {
|
|
|
|
|
let mut inner = self.inner.lock().unwrap();
|
|
|
|
|
inner.dirty = false;
|
|
|
|
|
inner.volume
|
2023-02-02 23:52:59 +01:00
|
|
|
}
|
|
|
|
|
fn color(&self) -> Color {
|
|
|
|
|
match self.inner.lock().unwrap().mute {
|
|
|
|
|
true => Color::YELLOW,
|
|
|
|
|
false => Color::WHITE,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fn click(&mut self, pos: f32, btn: PointerButton) {
|
|
|
|
|
let mut inner = self.inner.lock().unwrap();
|
|
|
|
|
inner.last_click = match btn {
|
2023-02-08 22:53:41 +01:00
|
|
|
PointerButton::Left => Some(Change::Volume(pos)),
|
2023-02-02 23:52:59 +01:00
|
|
|
PointerButton::Right => Some(Change::ToggleMute),
|
|
|
|
|
PointerButton::ScrollVertical(val) => Some(Change::VolumeInc((val / 512.) as f32)),
|
|
|
|
|
PointerButton::ScrollHorizontal(val) => Some(Change::VolumeInc((val / 100.) as f32)),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
_ = write(inner.pipe, &[0]);
|
|
|
|
|
}
|
|
|
|
|
}
|