widgets: stop aborting on machines without backlight/battery/pulseaudio

The release profile builds with panic = "abort", so a panic in any helper
thread took the whole dashboard down:

- backlight: WalkDir yields /sys/class/backlight itself, so detect() was
  always true and new() picked the class dir as the device, then panicked
  reading brightness. Use min_depth(1) and tolerate read errors.
- battery: detect() only checked that UPower's DisplayDevice answered,
  which it does on every machine; use IsPresent. The monitor thread now
  returns an error instead of unwrapping, and publishes its first reading.
- pulseaudio: a failed connection panicked ("damn"); clicks before the
  first sink query unwrapped a None channel map. Both are now handled, and
  the connect loop no longer busy-spins.
- bar widget: don't underflow when narrower than the label column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-10-03 09:15:54 +00:00
co-authored by Claude Fable 5.1
parent 01af00b5ea
commit c033335912
4 changed files with 126 additions and 111 deletions
+23 -11
View File
@@ -48,10 +48,17 @@ fn monitor(
context.borrow_mut().connect(None, FlagSet::NOFLAGS, None)?;
loop {
mainloop.iterate(false);
match mainloop.iterate(true) {
IterateResult::Quit(_) | IterateResult::Err(_) => {
return Err("pulseaudio mainloop quit while connecting".into())
}
_ => (),
}
match context.borrow().get_state() {
State::Ready => break,
State::Failed | State::Terminated => panic!("damn"),
State::Failed | State::Terminated => {
return Err("could not connect to pulseaudio".into())
}
_ => (),
}
}
@@ -120,15 +127,14 @@ fn monitor(
let mut inner = local_inner.lock().unwrap();
let mut introspector = local_context.borrow_mut().introspect();
match inner.last_click.take() {
Some(Change::Volume(volume)) => {
// do something
let mut vol = inner.channel_volume.unwrap();
// 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)) => {
vol.scale(Volume((volume * Volume::NORMAL.0 as f32).round() as u32));
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
}
Some(Change::VolumeInc(value)) => {
let mut vol = inner.channel_volume.unwrap();
(Some(Change::VolumeInc(value)), Some(mut vol)) => {
// apply step to volumes
let step = (value * Volume::NORMAL.0 as f32).round() as i32;
if step > 0 {
@@ -141,10 +147,10 @@ fn monitor(
inner.channel_volume = Some(vol);
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
}
Some(Change::ToggleMute) => {
(Some(Change::ToggleMute), _) => {
introspector.set_sink_mute_by_name("@DEFAULT_SINK@", !inner.mute, None);
}
None => (),
_ => (),
}
}),
);
@@ -165,7 +171,13 @@ fn monitor(
fn start_monitor(events: Arc<Mutex<Events>>, inner: Arc<Mutex<InnerAudio>>, pipe: RawFd) {
thread::Builder::new()
.name("audiomon".to_string())
.spawn(move || monitor(events, inner, pipe).unwrap())
.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);
}
})
.unwrap();
}