Skip to main content

macstate_core/
power.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct Power {
5    pub source: Source,
6    pub battery_percent: Option<u8>,
7    pub low_power_mode: bool,
8    pub energy_mode: EnergyMode,
9}
10
11#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
12#[serde(rename_all = "lowercase")]
13pub enum Source {
14    Ac,
15    Battery,
16}
17
18#[derive(Debug, Clone, Copy, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum EnergyMode {
21    Automatic,
22    Low,
23    High,
24    /// The IOPM preference was missing or carried a value we don't recognize.
25    Unknown,
26}
27
28#[cfg(target_os = "macos")]
29impl Power {
30    pub fn collect() -> Self {
31        let (source, battery_percent) = read_power_source();
32        let low_power_mode = macstate_sys::objc::is_low_power_mode_enabled();
33        let energy_mode = read_energy_mode(source);
34        Self {
35            source,
36            battery_percent,
37            low_power_mode,
38            energy_mode,
39        }
40    }
41}
42
43#[cfg(not(target_os = "macos"))]
44impl Power {
45    pub fn collect() -> Self {
46        Self {
47            source: Source::Ac,
48            battery_percent: None,
49            low_power_mode: false,
50            energy_mode: EnergyMode::Unknown,
51        }
52    }
53}
54
55#[cfg(target_os = "macos")]
56fn read_power_source() -> (Source, Option<u8>) {
57    use macstate_sys::cf::{
58        cfstring_to_string, dict_get_i32, CFArrayGetCount, CFArrayGetValueAtIndex, CFOwned,
59    };
60    use macstate_sys::iokit::{
61        kIOPSACPowerValue, kIOPSCurrentCapacityKey, kIOPSMaxCapacityKey,
62        IOPSCopyPowerSourcesInfo, IOPSCopyPowerSourcesList, IOPSGetPowerSourceDescription,
63        IOPSGetProvidingPowerSourceType,
64    };
65
66    unsafe {
67        let snapshot = match CFOwned::from_create(IOPSCopyPowerSourcesInfo()) {
68            Some(s) => s,
69            None => return (Source::Ac, None),
70        };
71
72        let provider = IOPSGetProvidingPowerSourceType(snapshot.as_ptr());
73        let source = match cfstring_to_string(provider) {
74            Some(s) if s == kIOPSACPowerValue => Source::Ac,
75            Some(_) => Source::Battery,
76            None => Source::Ac,
77        };
78
79        let list = match CFOwned::from_create(IOPSCopyPowerSourcesList(snapshot.as_ptr())) {
80            Some(l) => l,
81            None => return (source, None),
82        };
83
84        let count = CFArrayGetCount(list.as_ptr());
85        let mut percent: Option<u8> = None;
86        for i in 0..count {
87            let ps = CFArrayGetValueAtIndex(list.as_ptr(), i);
88            if ps.is_null() {
89                continue;
90            }
91            let desc = IOPSGetPowerSourceDescription(snapshot.as_ptr(), ps);
92            if desc.is_null() {
93                continue;
94            }
95            let cur = dict_get_i32(desc, kIOPSCurrentCapacityKey);
96            let max = dict_get_i32(desc, kIOPSMaxCapacityKey);
97            if let (Some(cur), Some(max)) = (cur, max) {
98                if max > 0 {
99                    let pct = ((cur as f64 / max as f64) * 100.0).round();
100                    percent = Some(pct.clamp(0.0, 100.0) as u8);
101                    break;
102                }
103            }
104        }
105
106        (source, percent)
107    }
108}
109
110#[cfg(target_os = "macos")]
111fn read_energy_mode(source: Source) -> EnergyMode {
112    use macstate_sys::cf::{dict_get_dict, dict_get_i32, CFOwned};
113    use macstate_sys::iokit::{
114        kIOPMLowPowerModeKey, kIOPSACPowerValue, kIOPSBatteryPowerValue,
115        IOPMCopyActivePMPreferences,
116    };
117
118    unsafe {
119        let prefs = match CFOwned::from_create(IOPMCopyActivePMPreferences()) {
120            Some(p) => p,
121            None => return EnergyMode::Unknown,
122        };
123        let key = match source {
124            Source::Ac => kIOPSACPowerValue,
125            Source::Battery => kIOPSBatteryPowerValue,
126        };
127        let sub = dict_get_dict(prefs.as_ptr(), key);
128        if sub.is_null() {
129            return EnergyMode::Unknown;
130        }
131        // Despite the key being called `LowPowerMode`, the value is the
132        // unified `pmset powermode` indicator: 0=automatic, 1=low, 2=high.
133        // The sibling `HighPowerMode` key is unused on current macOS.
134        match dict_get_i32(sub, kIOPMLowPowerModeKey) {
135            Some(0) => EnergyMode::Automatic,
136            Some(1) => EnergyMode::Low,
137            Some(2) => EnergyMode::High,
138            _ => EnergyMode::Unknown,
139        }
140    }
141}