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,
|
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 {
|
impl Backlight {
|
||||||
fn update(&mut self) {
|
fn update(&mut self) {
|
||||||
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.max = read_file_as_u64(self.device_path.join("max_brightness").as_path()).unwrap();
|
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 {
|
pub fn new(path: Option<&str>, fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
|
||||||
let mut device_path = None;
|
let device_path = match path {
|
||||||
if let Some(path) = path {
|
Some(path) => Some(Path::new("/sys/class/backlight").join(path)),
|
||||||
device_path = Some(Path::new("/sys/class/backlight").to_path_buf().join(path));
|
None => find_device(),
|
||||||
} else {
|
};
|
||||||
for entry in WalkDir::new("/sys/class/backlight") {
|
|
||||||
if let Ok(entry) = entry {
|
|
||||||
device_path = Some(entry.path().to_path_buf());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut dev = Backlight {
|
let mut dev = Backlight {
|
||||||
device_path: device_path.expect("did not find backlight device"),
|
device_path: device_path.expect("did not find backlight device"),
|
||||||
cur: 0,
|
cur: 0,
|
||||||
@@ -61,18 +71,15 @@ impl Backlight {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn detect() -> bool {
|
pub fn detect() -> bool {
|
||||||
for entry in WalkDir::new("/sys/class/backlight") {
|
find_device().is_some()
|
||||||
if entry.is_ok() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&mut self, brightness: f32) {
|
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);
|
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
|
self.dirty
|
||||||
}
|
}
|
||||||
fn value(&mut self) -> f32 {
|
fn value(&mut self) -> f32 {
|
||||||
if self.cur > self.max {
|
if self.max == 0 || self.cur > self.max {
|
||||||
// what.
|
// what.
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,21 +62,13 @@ impl Widget for BarWidget {
|
|||||||
let val = self.inner_widget.value();
|
let val = self.inner_widget.value();
|
||||||
|
|
||||||
let c = self.inner_widget.color();
|
let c = self.inner_widget.color();
|
||||||
draw_bar(
|
let bar_width = self.geometry.width.saturating_sub(bar_offset);
|
||||||
&mut view.offset((bar_offset, 0)),
|
if bar_width < 2 {
|
||||||
c,
|
return self.geometry;
|
||||||
self.geometry.width - bar_offset,
|
}
|
||||||
size,
|
draw_bar(&mut view.offset((bar_offset, 0)), c, bar_width, size, val).unwrap();
|
||||||
val,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
draw_box(
|
draw_box(&mut view.offset((bar_offset, 0)), c, (bar_width, size)).unwrap();
|
||||||
&mut view.offset((bar_offset, 0)),
|
|
||||||
c,
|
|
||||||
(self.geometry.width - bar_offset, size),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
self.geometry
|
self.geometry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +100,7 @@ impl Widget for BarWidget {
|
|||||||
match event {
|
match event {
|
||||||
Event::PointerEvent(ev) => {
|
Event::PointerEvent(ev) => {
|
||||||
let offset = 4 * self.size.ceil() as u32;
|
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;
|
let val = (ev.pos.0 - offset) as f32 / (self.geometry.width - offset) as f32;
|
||||||
self.inner_widget.click(val, ev.button);
|
self.inner_widget.click(val, ev.button);
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-38
@@ -47,77 +47,84 @@ struct InnerBattery {
|
|||||||
dirty: bool,
|
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 conn = LocalConnection::new_system()?;
|
||||||
let device_path = "/org/freedesktop/UPower/devices/DisplayDevice";
|
|
||||||
let proxy = conn.with_proxy(
|
let proxy = conn.with_proxy(
|
||||||
"org.freedesktop.UPower",
|
"org.freedesktop.UPower",
|
||||||
device_path,
|
DEVICE_PATH,
|
||||||
Duration::from_millis(500),
|
Duration::from_millis(500),
|
||||||
);
|
);
|
||||||
let state: u32 = proxy.get("org.freedesktop.UPower.Device", "State")?;
|
let present: bool = proxy.get(DEVICE_IFACE, "IsPresent")?;
|
||||||
Ok(UpowerBatteryState::from_dbus(state as u64))
|
Ok(present)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_monitor(inner: Arc<Mutex<InnerBattery>>, events: Arc<Mutex<Events>>) {
|
fn monitor(inner: Arc<Mutex<InnerBattery>>, events: Arc<Mutex<Events>>) -> Result<(), Box<dyn Error>> {
|
||||||
thread::Builder::new()
|
let conn = LocalConnection::new_system()?;
|
||||||
.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(
|
let proxy = conn.with_proxy(
|
||||||
"org.freedesktop.UPower",
|
"org.freedesktop.UPower",
|
||||||
device_path,
|
DEVICE_PATH,
|
||||||
Duration::from_millis(500),
|
Duration::from_millis(500),
|
||||||
);
|
);
|
||||||
|
|
||||||
let capacity: f64 = proxy
|
let capacity: f64 = proxy.get(DEVICE_IFACE, "Percentage")?;
|
||||||
.get("org.freedesktop.UPower.Device", "Percentage")
|
let state: u32 = proxy.get(DEVICE_IFACE, "State")?;
|
||||||
.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();
|
let mut inner = inner.lock().unwrap();
|
||||||
inner.value = capacity as f32 / 100.;
|
inner.value = capacity as f32 / 100.;
|
||||||
inner.state = UpowerBatteryState::from_dbus(state as u64);
|
inner.state = UpowerBatteryState::from_dbus(state as u64);
|
||||||
|
inner.dirty = true;
|
||||||
}
|
}
|
||||||
|
events.lock().unwrap().add_event(Event::PowerUpdate);
|
||||||
|
|
||||||
proxy
|
proxy.match_signal(
|
||||||
.match_signal(
|
move |c: PropertiesPropertiesChanged, _: &LocalConnection, _: &dbus::Message| {
|
||||||
move |c: PropertiesPropertiesChanged,
|
if c.interface_name != DEVICE_IFACE {
|
||||||
_: &LocalConnection,
|
|
||||||
_: &dbus::Message| {
|
|
||||||
if c.interface_name != "org.freedesktop.UPower.Device" {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let mut inner = inner.lock().unwrap();
|
let mut inner = inner.lock().unwrap();
|
||||||
for (key, value) in c.changed_properties {
|
for (key, value) in c.changed_properties {
|
||||||
match key.as_str() {
|
match key.as_str() {
|
||||||
"State" => {
|
"State" => {
|
||||||
inner.state =
|
if let Some(v) = value.0.as_u64() {
|
||||||
UpowerBatteryState::from_dbus(value.0.as_u64().unwrap());
|
inner.state = UpowerBatteryState::from_dbus(v);
|
||||||
inner.dirty = true;
|
inner.dirty = true;
|
||||||
let mut events = events.lock().unwrap();
|
events.lock().unwrap().add_event(Event::PowerUpdate);
|
||||||
events.add_event(Event::PowerUpdate);
|
}
|
||||||
}
|
}
|
||||||
"Percentage" => {
|
"Percentage" => {
|
||||||
inner.value = value.0.as_f64().unwrap() as f32 / 100.;
|
if let Some(v) = value.0.as_f64() {
|
||||||
|
inner.value = v as f32 / 100.;
|
||||||
inner.dirty = true;
|
inner.dirty = true;
|
||||||
let mut events = events.lock().unwrap();
|
events.lock().unwrap().add_event(Event::PowerUpdate);
|
||||||
events.add_event(Event::PowerUpdate);
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
conn.process(Duration::from_millis(60000)).unwrap();
|
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 || {
|
||||||
|
// 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();
|
.unwrap();
|
||||||
@@ -146,10 +153,7 @@ impl Battery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn detect() -> bool {
|
pub fn detect() -> bool {
|
||||||
match get_battery_state() {
|
battery_present().unwrap_or(false)
|
||||||
Ok(_) => true,
|
|
||||||
Err(_) => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-11
@@ -48,10 +48,17 @@ fn monitor(
|
|||||||
context.borrow_mut().connect(None, FlagSet::NOFLAGS, None)?;
|
context.borrow_mut().connect(None, FlagSet::NOFLAGS, None)?;
|
||||||
|
|
||||||
loop {
|
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() {
|
match context.borrow().get_state() {
|
||||||
State::Ready => break,
|
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 inner = local_inner.lock().unwrap();
|
||||||
let mut introspector = local_context.borrow_mut().introspect();
|
let mut introspector = local_context.borrow_mut().introspect();
|
||||||
match inner.last_click.take() {
|
// Clicks arriving before the first sink query finished have no
|
||||||
Some(Change::Volume(volume)) => {
|
// channel map to work with; drop them instead of panicking.
|
||||||
// do something
|
match (inner.last_click.take(), inner.channel_volume) {
|
||||||
let mut vol = inner.channel_volume.unwrap();
|
(Some(Change::Volume(volume)), Some(mut vol)) => {
|
||||||
vol.scale(Volume((volume * Volume::NORMAL.0 as f32).round() as u32));
|
vol.scale(Volume((volume * Volume::NORMAL.0 as f32).round() as u32));
|
||||||
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
|
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
|
||||||
}
|
}
|
||||||
Some(Change::VolumeInc(value)) => {
|
(Some(Change::VolumeInc(value)), Some(mut vol)) => {
|
||||||
let mut vol = inner.channel_volume.unwrap();
|
|
||||||
// apply step to volumes
|
// apply step to volumes
|
||||||
let step = (value * Volume::NORMAL.0 as f32).round() as i32;
|
let step = (value * Volume::NORMAL.0 as f32).round() as i32;
|
||||||
if step > 0 {
|
if step > 0 {
|
||||||
@@ -141,10 +147,10 @@ fn monitor(
|
|||||||
inner.channel_volume = Some(vol);
|
inner.channel_volume = Some(vol);
|
||||||
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
|
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);
|
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) {
|
fn start_monitor(events: Arc<Mutex<Events>>, inner: Arc<Mutex<InnerAudio>>, pipe: RawFd) {
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("audiomon".to_string())
|
.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();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user