diff --git a/crates/core/src/power.rs b/crates/core/src/power.rs index 37000f0..d2fbdba 100644 --- a/crates/core/src/power.rs +++ b/crates/core/src/power.rs @@ -14,9 +14,61 @@ pub enum PowerState { Unknown, } +use std::fs; +use std::path::Path; + +/// Parse a `/sys/class/power_supply/`-style directory and decide +/// whether the machine is on AC, on battery, or in an unknown state. +/// +/// Rules (matches `Documentation/ABI/testing/sysfs-class-power`): +/// - Any entry with `type` in {`Mains`, `USB`} and `online == 1` → OnAc. +/// - Else any entry with `type == Battery` → OnBattery. +/// - Else → Unknown. +/// +/// Failures (missing dir, unreadable files, malformed contents) all +/// fall through to Unknown rather than panicking. `Unknown` is treated +/// as `OnAc` by the caller, which preserves today's pre-clamp +/// behaviour on platforms or distros where the probe doesn't fire. +pub fn parse_power_state_from_dir(supply_dir: &Path) -> PowerState { + let read_dir = match fs::read_dir(supply_dir) { + Ok(rd) => rd, + Err(_) => return PowerState::Unknown, + }; + + let mut saw_battery = false; + for entry in read_dir.flatten() { + let path = entry.path(); + let ty = read_trimmed(&path.join("type")).unwrap_or_default(); + let online = read_trimmed(&path.join("online")).unwrap_or_default(); + match ty.as_str() { + "Mains" | "USB" => { + if online == "1" { + return PowerState::OnAc; + } + } + "Battery" => { + saw_battery = true; + } + _ => {} + } + } + + if saw_battery { + PowerState::OnBattery + } else { + PowerState::Unknown + } +} + +fn read_trimmed(path: &Path) -> Option { + fs::read_to_string(path).ok().map(|s| s.trim().to_owned()) +} + #[cfg(test)] mod tests { use super::*; + use std::fs; + use tempfile::tempdir; #[test] fn power_state_variants_are_distinct() { @@ -24,4 +76,56 @@ mod tests { assert_ne!(PowerState::OnAc, PowerState::Unknown); assert_ne!(PowerState::OnBattery, PowerState::Unknown); } + + fn write_supply(dir: &std::path::Path, name: &str, ty: &str, online: &str) { + let entry = dir.join(name); + fs::create_dir_all(&entry).unwrap(); + fs::write(entry.join("type"), format!("{ty}\n")).unwrap(); + fs::write(entry.join("online"), format!("{online}\n")).unwrap(); + } + + #[test] + fn parses_mains_online_as_on_ac() { + let dir = tempdir().unwrap(); + write_supply(dir.path(), "AC", "Mains", "1"); + write_supply(dir.path(), "BAT0", "Battery", "0"); + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnAc); + } + + #[test] + fn parses_battery_only_as_on_battery() { + let dir = tempdir().unwrap(); + write_supply(dir.path(), "AC", "Mains", "0"); + write_supply(dir.path(), "BAT0", "Battery", "0"); + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnBattery); + } + + #[test] + fn parses_usb_pd_online_as_on_ac() { + let dir = tempdir().unwrap(); + write_supply(dir.path(), "ucsi-source-psy-USBC000:001", "USB", "1"); + write_supply(dir.path(), "BAT0", "Battery", "0"); + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnAc); + } + + #[test] + fn parses_empty_dir_as_unknown() { + let dir = tempdir().unwrap(); + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::Unknown); + } + + #[test] + fn parses_missing_dir_as_unknown() { + let path = std::path::Path::new("/no/such/path/should/exist/at/this/inode/123456"); + assert_eq!(parse_power_state_from_dir(path), PowerState::Unknown); + } + + #[test] + fn parses_malformed_files_as_unknown_gracefully() { + let dir = tempdir().unwrap(); + let entry = dir.path().join("garbage"); + fs::create_dir_all(&entry).unwrap(); + // No type, no online — should not panic. + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::Unknown); + } }