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:
co-authored by
Claude Fable 5.1
parent
01af00b5ea
commit
c033335912
+28
-21
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+68
-64
@@ -47,77 +47,84 @@ struct InnerBattery {
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
fn get_battery_state() -> Result<UpowerBatteryState, Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<Mutex<InnerBattery>>, events: Arc<Mutex<Events>>) -> Result<(), Box<dyn Error>> {
|
||||
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<Mutex<InnerBattery>>, events: Arc<Mutex<Events>>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-11
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user