diff --git a/src/widgets/backlight.rs b/src/widgets/backlight.rs index 21853fd..cfb7244 100644 --- a/src/widgets/backlight.rs +++ b/src/widgets/backlight.rs @@ -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 { + 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; } diff --git a/src/widgets/bar_widget.rs b/src/widgets/bar_widget.rs index a4adc37..336d24e 100644 --- a/src/widgets/bar_widget.rs +++ b/src/widgets/bar_widget.rs @@ -62,21 +62,13 @@ impl Widget for BarWidget { let val = self.inner_widget.value(); let c = self.inner_widget.color(); - draw_bar( - &mut view.offset((bar_offset, 0)), - c, - self.geometry.width - bar_offset, - size, - val, - ) - .unwrap(); + let bar_width = self.geometry.width.saturating_sub(bar_offset); + if bar_width < 2 { + return self.geometry; + } + draw_bar(&mut view.offset((bar_offset, 0)), c, bar_width, size, val).unwrap(); - draw_box( - &mut view.offset((bar_offset, 0)), - c, - (self.geometry.width - bar_offset, size), - ) - .unwrap(); + draw_box(&mut view.offset((bar_offset, 0)), c, (bar_width, size)).unwrap(); self.geometry } @@ -108,7 +100,7 @@ impl Widget for BarWidget { match event { Event::PointerEvent(ev) => { let offset = 4 * self.size.ceil() as u32; - if ev.pos.0 >= offset { + if ev.pos.0 >= offset && self.geometry.width > offset { let val = (ev.pos.0 - offset) as f32 / (self.geometry.width - offset) as f32; self.inner_widget.click(val, ev.button); } diff --git a/src/widgets/battery.rs b/src/widgets/battery.rs index 61121e6..c67eece 100644 --- a/src/widgets/battery.rs +++ b/src/widgets/battery.rs @@ -47,77 +47,84 @@ struct InnerBattery { dirty: bool, } -fn get_battery_state() -> Result> { +const DEVICE_PATH: &str = "/org/freedesktop/UPower/devices/DisplayDevice"; +const DEVICE_IFACE: &str = "org.freedesktop.UPower.Device"; + +/// True if UPower reports a present battery. The DisplayDevice object exists +/// even on machines without a battery, so checking for mere reachability +/// (as before) made every desktop grow a battery widget. +fn battery_present() -> Result> { let conn = LocalConnection::new_system()?; - let device_path = "/org/freedesktop/UPower/devices/DisplayDevice"; let proxy = conn.with_proxy( "org.freedesktop.UPower", - device_path, + DEVICE_PATH, Duration::from_millis(500), ); - let state: u32 = proxy.get("org.freedesktop.UPower.Device", "State")?; - Ok(UpowerBatteryState::from_dbus(state as u64)) + let present: bool = proxy.get(DEVICE_IFACE, "IsPresent")?; + Ok(present) +} + +fn monitor(inner: Arc>, events: Arc>) -> Result<(), Box> { + let conn = LocalConnection::new_system()?; + let proxy = conn.with_proxy( + "org.freedesktop.UPower", + DEVICE_PATH, + Duration::from_millis(500), + ); + + let capacity: f64 = proxy.get(DEVICE_IFACE, "Percentage")?; + let state: u32 = proxy.get(DEVICE_IFACE, "State")?; + + { + let mut inner = inner.lock().unwrap(); + inner.value = capacity as f32 / 100.; + inner.state = UpowerBatteryState::from_dbus(state as u64); + inner.dirty = true; + } + events.lock().unwrap().add_event(Event::PowerUpdate); + + proxy.match_signal( + move |c: PropertiesPropertiesChanged, _: &LocalConnection, _: &dbus::Message| { + if c.interface_name != DEVICE_IFACE { + return true; + } + let mut inner = inner.lock().unwrap(); + for (key, value) in c.changed_properties { + match key.as_str() { + "State" => { + if let Some(v) = value.0.as_u64() { + inner.state = UpowerBatteryState::from_dbus(v); + inner.dirty = true; + events.lock().unwrap().add_event(Event::PowerUpdate); + } + } + "Percentage" => { + if let Some(v) = value.0.as_f64() { + inner.value = v as f32 / 100.; + inner.dirty = true; + events.lock().unwrap().add_event(Event::PowerUpdate); + } + } + _ => (), + } + } + true + }, + )?; + + loop { + conn.process(Duration::from_millis(60000))?; + } } fn start_monitor(inner: Arc>, events: Arc>) { thread::Builder::new() .name("battmon".to_string()) .spawn(move || { - let conn = LocalConnection::new_system().unwrap(); - let device_path = "/org/freedesktop/UPower/devices/DisplayDevice"; - let proxy = conn.with_proxy( - "org.freedesktop.UPower", - device_path, - Duration::from_millis(500), - ); - - let capacity: f64 = proxy - .get("org.freedesktop.UPower.Device", "Percentage") - .expect("unable to get property"); - let state: u32 = proxy - .get("org.freedesktop.UPower.Device", "State") - .expect("unable to get state"); - - { - let mut inner = inner.lock().unwrap(); - inner.value = capacity as f32 / 100.; - inner.state = UpowerBatteryState::from_dbus(state as u64); - } - - proxy - .match_signal( - move |c: PropertiesPropertiesChanged, - _: &LocalConnection, - _: &dbus::Message| { - if c.interface_name != "org.freedesktop.UPower.Device" { - return true; - } - let mut inner = inner.lock().unwrap(); - for (key, value) in c.changed_properties { - match key.as_str() { - "State" => { - inner.state = - UpowerBatteryState::from_dbus(value.0.as_u64().unwrap()); - inner.dirty = true; - let mut events = events.lock().unwrap(); - events.add_event(Event::PowerUpdate); - } - "Percentage" => { - inner.value = value.0.as_f64().unwrap() as f32 / 100.; - inner.dirty = true; - let mut events = events.lock().unwrap(); - events.add_event(Event::PowerUpdate); - } - _ => (), - } - } - true - }, - ) - .unwrap(); - - loop { - conn.process(Duration::from_millis(60000)).unwrap(); + // The release profile uses panic = "abort": a panic here would take + // the whole dashboard down, so report and leave the bar at 0. + if let Err(e) = monitor(inner, events) { + eprintln!("wldash: battery monitor stopped: {}", e); } }) .unwrap(); @@ -146,10 +153,7 @@ impl Battery { } pub fn detect() -> bool { - match get_battery_state() { - Ok(_) => true, - Err(_) => false, - } + battery_present().unwrap_or(false) } } diff --git a/src/widgets/pulseaudio.rs b/src/widgets/pulseaudio.rs index 0b03e44..9879529 100644 --- a/src/widgets/pulseaudio.rs +++ b/src/widgets/pulseaudio.rs @@ -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>, inner: Arc>, 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(); }