cranpose 0.1.155

Cranpose runtime and UI facade
Documentation
use std::sync::Arc;

use cranpose_services::{
    BatteryStatus, PowerCapabilities, PowerMonitor, PowerReading, ThermalState,
    set_platform_power_monitor,
};

pub(crate) fn register() {
    set_platform_power_monitor(Arc::new(DesktopPowerMonitor));
}

struct DesktopPowerMonitor;

impl PowerMonitor for DesktopPowerMonitor {
    fn capabilities(&self) -> PowerCapabilities {
        PowerCapabilities {
            thermal: cfg!(target_os = "macos"),
            battery: cfg!(target_os = "linux"),
            background_restriction: false,
        }
    }

    fn thermal_state(&self) -> PowerReading<ThermalState> {
        #[cfg(target_os = "macos")]
        {
            macos::thermal_state()
        }
        #[cfg(not(target_os = "macos"))]
        {
            PowerReading::Unsupported
        }
    }

    fn battery_status(&self) -> PowerReading<BatteryStatus> {
        #[cfg(target_os = "linux")]
        {
            linux::battery_status()
        }
        #[cfg(not(target_os = "linux"))]
        {
            PowerReading::Unsupported
        }
    }
}

#[cfg(target_os = "macos")]
mod macos {
    use super::{PowerReading, ThermalState};

    pub(super) fn thermal_state() -> PowerReading<ThermalState> {
        PowerReading::Known(crate::apple_thermal::thermal_state())
    }
}

#[cfg(target_os = "linux")]
mod linux {
    use std::path::Path;

    use super::{BatteryStatus, PowerReading};

    const POWER_SUPPLY: &str = "/sys/class/power_supply";

    pub(super) fn battery_status() -> PowerReading<BatteryStatus> {
        let Ok(entries) = std::fs::read_dir(POWER_SUPPLY) else {
            return PowerReading::Unsupported;
        };
        for entry in entries.flatten() {
            let path = entry.path();
            if read_trimmed(&path.join("type")).as_deref() != Some("Battery") {
                continue;
            }
            let Some(status) = read_battery(&path) else {
                continue;
            };
            return PowerReading::Known(status);
        }
        PowerReading::Unsupported
    }

    fn read_battery(path: &Path) -> Option<BatteryStatus> {
        let percent: u8 = read_trimmed(&path.join("capacity"))?.parse().ok()?;
        let status = read_trimmed(&path.join("status"))?;
        Some(BatteryStatus {
            percent: percent.min(100),
            charging: matches!(status.as_str(), "Charging" | "Full"),
        })
    }

    fn read_trimmed(path: &Path) -> Option<String> {
        Some(std::fs::read_to_string(path).ok()?.trim().to_owned())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn capabilities_match_what_this_build_can_actually_read() {
        let capabilities = DesktopPowerMonitor.capabilities();
        assert_eq!(
            capabilities.thermal,
            DesktopPowerMonitor.thermal_state().is_supported(),
            "a backend that claims thermal support must not answer Unsupported"
        );
        assert!(!capabilities.background_restriction);
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn a_mac_reports_a_thermal_reading_rather_than_unsupported() {
        assert!(matches!(
            DesktopPowerMonitor.thermal_state(),
            PowerReading::Known(_)
        ));
    }
}