129 lines
4 KiB
Rust
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());
|
|
}
|
|
}
|