shepherd-launcher/crates/shepherd-hud/src/battery.rs
2025-12-29 17:51:55 -05:00

129 lines
4 KiB
Rust

//! Battery monitoring module
//!
//! Monitors battery status via sysfs or UPower D-Bus interface.
use std::fs;
use std::path::Path;
/// Battery status
#[derive(Debug, Clone, Default)]
pub struct BatteryStatus {
/// Battery percentage (0-100)
pub percent: Option<u8>,
/// Whether the battery is charging
pub charging: bool,
/// Whether AC power is connected
pub ac_connected: bool,
}
impl BatteryStatus {
/// Read battery status from sysfs
pub fn read() -> Self {
let mut status = BatteryStatus::default();
// Try to find a battery in /sys/class/power_supply
let power_supply = Path::new("/sys/class/power_supply");
if !power_supply.exists() {
return status;
}
if let Ok(entries) = fs::read_dir(power_supply) {
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
// Check for battery
if name_str.starts_with("BAT")
&& let Some((percent, charging)) = read_battery_info(&path) {
status.percent = Some(percent);
status.charging = charging;
}
// Check for AC adapter
if (name_str.starts_with("AC") || name_str.contains("ADP"))
&& let Some(online) = read_ac_status(&path) {
status.ac_connected = online;
}
}
}
status
}
/// Get an icon name for the current battery status
pub fn icon_name(&self) -> &'static str {
match (self.percent, self.charging) {
(None, _) => "battery-missing-symbolic",
(Some(p), true) if p >= 90 => "battery-full-charging-symbolic",
(Some(p), true) if p >= 60 => "battery-good-charging-symbolic",
(Some(p), true) if p >= 30 => "battery-low-charging-symbolic",
(Some(_), true) => "battery-caution-charging-symbolic",
(Some(p), false) if p >= 90 => "battery-full-symbolic",
(Some(p), false) if p >= 60 => "battery-good-symbolic",
(Some(p), false) if p >= 30 => "battery-low-symbolic",
(Some(p), false) if p >= 10 => "battery-caution-symbolic",
(Some(_), false) => "battery-empty-symbolic",
}
}
/// Check if battery is critically low
#[allow(dead_code)]
pub fn is_critical(&self) -> bool {
matches!(self.percent, Some(p) if p < 10 && !self.charging)
}
}
fn read_battery_info(path: &Path) -> Option<(u8, bool)> {
// Read capacity
let capacity_path = path.join("capacity");
let capacity: u8 = fs::read_to_string(&capacity_path)
.ok()?
.trim()
.parse()
.ok()?;
// Read status
let status_path = path.join("status");
let status = fs::read_to_string(&status_path).ok()?;
let charging = status.trim().eq_ignore_ascii_case("charging")
|| status.trim().eq_ignore_ascii_case("full");
Some((capacity.min(100), charging))
}
fn read_ac_status(path: &Path) -> Option<bool> {
let online_path = path.join("online");
let online = fs::read_to_string(&online_path).ok()?;
Some(online.trim() == "1")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_battery_icon_names() {
let status = BatteryStatus {
percent: Some(95),
charging: false,
ac_connected: false,
};
assert_eq!(status.icon_name(), "battery-full-symbolic");
let status = BatteryStatus {
percent: Some(50),
charging: true,
ac_connected: true,
};
assert_eq!(status.icon_name(), "battery-low-charging-symbolic");
let status = BatteryStatus {
percent: Some(5),
charging: false,
ac_connected: false,
};
assert_eq!(status.icon_name(), "battery-empty-symbolic");
assert!(status.is_critical());
}
}