Skip to main content

macstate_core/
power.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
5pub struct Power {
6    pub source: Source,
7    /// Battery charge as an integer percentage (0–100), or `null` when no
8    /// battery is present (e.g. Mac mini, iMac).
9    #[cfg_attr(feature = "schema", schemars(range(min = 0, max = 100)))]
10    pub battery_percent: Option<u8>,
11    /// Whether Low Power Mode is currently active
12    /// (`NSProcessInfo.isLowPowerModeEnabled`). This is the runtime state,
13    /// not a configured preference.
14    pub low_power_mode: bool,
15    pub energy_mode: EnergyMode,
16}
17
18#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
19#[serde(rename_all = "lowercase")]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[cfg_attr(feature = "schema", schemars(rename_all = "lowercase"))]
22#[cfg_attr(
23    feature = "schema",
24    schemars(description = "Whether the system is currently drawing from AC or battery (IOPSGetProvidingPowerSourceType).")
25)]
26pub enum Source {
27    Ac,
28    Battery,
29}
30
31#[derive(Debug, Clone, Copy, Serialize)]
32#[serde(rename_all = "lowercase")]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[cfg_attr(feature = "schema", schemars(rename_all = "lowercase"))]
35#[cfg_attr(
36    feature = "schema",
37    schemars(description = "Configured energy preference for the current power source, read from IOPM active preferences (the same data source pmset(8) uses). `high` is only available on Apple Silicon Pro/Max. `unknown` is returned when the preference is unreadable or carries an unrecognized value.")
38)]
39pub enum EnergyMode {
40    Automatic,
41    Low,
42    High,
43    /// The IOPM preference was missing or carried a value we don't recognize.
44    Unknown,
45}
46
47#[cfg(target_os = "macos")]
48impl Power {
49    pub fn collect() -> Self {
50        let (source, battery_percent) = read_power_source();
51        let low_power_mode = macstate_sys::objc::is_low_power_mode_enabled();
52        let energy_mode = read_energy_mode(source);
53        Self {
54            source,
55            battery_percent,
56            low_power_mode,
57            energy_mode,
58        }
59    }
60}
61
62#[cfg(not(target_os = "macos"))]
63impl Power {
64    pub fn collect() -> Self {
65        Self {
66            source: Source::Ac,
67            battery_percent: None,
68            low_power_mode: false,
69            energy_mode: EnergyMode::Unknown,
70        }
71    }
72}
73
74#[cfg(target_os = "macos")]
75fn read_power_source() -> (Source, Option<u8>) {
76    use macstate_sys::cf::{
77        cfstring_to_string, dict_get_i32, CFArrayGetCount, CFArrayGetValueAtIndex, CFOwned,
78    };
79    use macstate_sys::iokit::{
80        kIOPSACPowerValue, kIOPSCurrentCapacityKey, kIOPSMaxCapacityKey,
81        IOPSCopyPowerSourcesInfo, IOPSCopyPowerSourcesList, IOPSGetPowerSourceDescription,
82        IOPSGetProvidingPowerSourceType,
83    };
84
85    unsafe {
86        let snapshot = match CFOwned::from_create(IOPSCopyPowerSourcesInfo()) {
87            Some(s) => s,
88            None => return (Source::Ac, None),
89        };
90
91        let provider = IOPSGetProvidingPowerSourceType(snapshot.as_ptr());
92        let source = match cfstring_to_string(provider) {
93            Some(s) if s == kIOPSACPowerValue => Source::Ac,
94            Some(_) => Source::Battery,
95            None => Source::Ac,
96        };
97
98        let list = match CFOwned::from_create(IOPSCopyPowerSourcesList(snapshot.as_ptr())) {
99            Some(l) => l,
100            None => return (source, None),
101        };
102
103        let count = CFArrayGetCount(list.as_ptr());
104        let mut percent: Option<u8> = None;
105        for i in 0..count {
106            let ps = CFArrayGetValueAtIndex(list.as_ptr(), i);
107            if ps.is_null() {
108                continue;
109            }
110            let desc = IOPSGetPowerSourceDescription(snapshot.as_ptr(), ps);
111            if desc.is_null() {
112                continue;
113            }
114            let cur = dict_get_i32(desc, kIOPSCurrentCapacityKey);
115            let max = dict_get_i32(desc, kIOPSMaxCapacityKey);
116            if let (Some(cur), Some(max)) = (cur, max) {
117                if max > 0 {
118                    let pct = ((cur as f64 / max as f64) * 100.0).round();
119                    percent = Some(pct.clamp(0.0, 100.0) as u8);
120                    break;
121                }
122            }
123        }
124
125        (source, percent)
126    }
127}
128
129#[cfg(target_os = "macos")]
130fn read_energy_mode(source: Source) -> EnergyMode {
131    use macstate_sys::cf::{dict_get_dict, dict_get_i32, CFOwned};
132    use macstate_sys::iokit::{
133        kIOPMLowPowerModeKey, kIOPSACPowerValue, kIOPSBatteryPowerValue,
134        IOPMCopyActivePMPreferences,
135    };
136
137    unsafe {
138        let prefs = match CFOwned::from_create(IOPMCopyActivePMPreferences()) {
139            Some(p) => p,
140            None => return EnergyMode::Unknown,
141        };
142        let key = match source {
143            Source::Ac => kIOPSACPowerValue,
144            Source::Battery => kIOPSBatteryPowerValue,
145        };
146        let sub = dict_get_dict(prefs.as_ptr(), key);
147        if sub.is_null() {
148            return EnergyMode::Unknown;
149        }
150        // Despite the key being called `LowPowerMode`, the value is the
151        // unified `pmset powermode` indicator: 0=automatic, 1=low, 2=high.
152        // The sibling `HighPowerMode` key is unused on current macOS.
153        match dict_get_i32(sub, kIOPMLowPowerModeKey) {
154            Some(0) => EnergyMode::Automatic,
155            Some(1) => EnergyMode::Low,
156            Some(2) => EnergyMode::High,
157            _ => EnergyMode::Unknown,
158        }
159    }
160}