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
+28 -21
View File
@@ -32,23 +32,33 @@ pub struct Backlight {
dirty: bool,
}
/// First device under /sys/class/backlight, if any. `min_depth(1)` skips the
/// class directory itself, which otherwise got picked up as a "device".
fn find_device() -> Option<PathBuf> {
WalkDir::new("/sys/class/backlight")
.min_depth(1)
.max_depth(1)
.into_iter()
.flatten()
.map(|e| e.path().to_path_buf())
.next()
}
impl Backlight {
fn update(&mut self) {
self.cur = read_file_as_u64(self.device_path.join("brightness").as_path()).unwrap();
self.max = read_file_as_u64(self.device_path.join("max_brightness").as_path()).unwrap();
if let Ok(cur) = read_file_as_u64(self.device_path.join("brightness").as_path()) {
self.cur = cur;
}
if let Ok(max) = read_file_as_u64(self.device_path.join("max_brightness").as_path()) {
self.max = max;
}
}
pub fn new(path: Option<&str>, fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
let mut device_path = None;
if let Some(path) = path {
device_path = Some(Path::new("/sys/class/backlight").to_path_buf().join(path));
} else {
for entry in WalkDir::new("/sys/class/backlight") {
if let Ok(entry) = entry {
device_path = Some(entry.path().to_path_buf());
}
}
}
let device_path = match path {
Some(path) => Some(Path::new("/sys/class/backlight").join(path)),
None => find_device(),
};
let mut dev = Backlight {
device_path: device_path.expect("did not find backlight device"),
cur: 0,
@@ -61,18 +71,15 @@ impl Backlight {
}
pub fn detect() -> bool {
for entry in WalkDir::new("/sys/class/backlight") {
if entry.is_ok() {
return true;
}
}
return false;
find_device().is_some()
}
pub fn set(&mut self, brightness: f32) {
let val = (self.max as f32 * brightness) as u64;
let val = (self.max as f32 * brightness.clamp(0.0, 1.0)) as u64;
let _ = write_file_as_u64(self.device_path.join("brightness").as_path(), val);
self.cur = read_file_as_u64(self.device_path.join("brightness").as_path()).unwrap();
if let Ok(cur) = read_file_as_u64(self.device_path.join("brightness").as_path()) {
self.cur = cur;
}
}
}
@@ -84,7 +91,7 @@ impl BarWidgetImpl for Backlight {
self.dirty
}
fn value(&mut self) -> f32 {
if self.cur > self.max {
if self.max == 0 || self.cur > self.max {
// what.
return 1.0;
}