cranpose_services/
power.rs1use std::sync::{Arc, Mutex, OnceLock};
11
12use cranpose_core::{ProcessIds, State, rememberEventStream};
13
14use crate::registry::ServiceRegistry;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
18pub enum ThermalState {
19 #[default]
21 Normal,
22 Light,
24 Moderate,
26 Severe,
28 Critical,
30 Emergency,
32 Shutdown,
34}
35
36impl ThermalState {
37 pub fn should_pause_work(self) -> bool {
39 self >= Self::Severe
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct BatteryStatus {
46 pub percent: u8,
48 pub charging: bool,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum PowerReading<T> {
59 Known(T),
61 Unsupported,
63 Unknown,
65}
66
67impl<T> PowerReading<T> {
68 pub fn known(self) -> Option<T> {
70 match self {
71 Self::Known(value) => Some(value),
72 _ => None,
73 }
74 }
75
76 pub fn is_supported(&self) -> bool {
78 !matches!(self, Self::Unsupported)
79 }
80
81 pub fn unwrap_or(self, fallback: T) -> T {
83 self.known().unwrap_or(fallback)
84 }
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub struct PowerCapabilities {
90 pub thermal: bool,
92 pub battery: bool,
94 pub background_restriction: bool,
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub struct PowerState {
101 pub thermal: PowerReading<ThermalState>,
103 pub battery: PowerReading<BatteryStatus>,
105 pub unrestricted_background_work: PowerReading<bool>,
107}
108
109impl PowerState {
110 pub const fn unsupported() -> Self {
112 Self {
113 thermal: PowerReading::Unsupported,
114 battery: PowerReading::Unsupported,
115 unrestricted_background_work: PowerReading::Unsupported,
116 }
117 }
118
119 pub fn should_pause_work(&self) -> bool {
122 matches!(self.thermal, PowerReading::Known(level) if level.should_pause_work())
123 }
124}
125
126pub trait PowerMonitor: Send + Sync {
128 fn capabilities(&self) -> PowerCapabilities {
130 PowerCapabilities::default()
131 }
132
133 fn thermal_state(&self) -> PowerReading<ThermalState> {
135 PowerReading::Unsupported
136 }
137
138 fn battery_status(&self) -> PowerReading<BatteryStatus> {
140 PowerReading::Unsupported
141 }
142
143 fn unrestricted_background_work(&self) -> PowerReading<bool> {
145 PowerReading::Unsupported
146 }
147
148 fn request_unrestricted_background_work(&self) {}
150}
151
152pub type PowerMonitorRef = Arc<dyn PowerMonitor>;
154
155struct DefaultPowerMonitor;
156impl PowerMonitor for DefaultPowerMonitor {}
157
158static PLATFORM_POWER_MONITOR: ServiceRegistry<dyn PowerMonitor> = ServiceRegistry::new();
159
160pub fn set_platform_power_monitor(monitor: PowerMonitorRef) {
162 PLATFORM_POWER_MONITOR.set(monitor);
163 publish_power_state(power_state());
164}
165
166pub fn clear_platform_power_monitor() {
168 PLATFORM_POWER_MONITOR.clear();
169 if let Ok(mut observers) = power_observers().lock() {
170 observers.clear();
171 }
172}
173
174pub fn power_monitor() -> PowerMonitorRef {
176 PLATFORM_POWER_MONITOR
177 .get()
178 .unwrap_or_else(|| Arc::new(DefaultPowerMonitor))
179}
180
181pub fn power_state() -> PowerState {
183 let monitor = power_monitor();
184 PowerState {
185 thermal: monitor.thermal_state(),
186 battery: monitor.battery_status(),
187 unrestricted_background_work: monitor.unrestricted_background_work(),
188 }
189}
190
191pub fn power_capabilities() -> PowerCapabilities {
193 power_monitor().capabilities()
194}
195
196type PowerObserver = Arc<dyn Fn(PowerState) + Send + Sync>;
197
198fn power_observers() -> &'static Mutex<Vec<(u64, PowerObserver)>> {
199 static SLOT: OnceLock<Mutex<Vec<(u64, PowerObserver)>>> = OnceLock::new();
200 SLOT.get_or_init(|| Mutex::new(Vec::new()))
201}
202
203static NEXT_OBSERVER: ProcessIds = ProcessIds::starting_at(1);
204
205pub struct PowerObserverRegistration {
207 id: u64,
208}
209
210impl Drop for PowerObserverRegistration {
211 fn drop(&mut self) {
212 if let Ok(mut observers) = power_observers().lock() {
213 observers.retain(|(id, _)| *id != self.id);
214 }
215 }
216}
217
218pub fn observe_power_state(
221 observer: impl Fn(PowerState) + Send + Sync + 'static,
222) -> PowerObserverRegistration {
223 let id = NEXT_OBSERVER.next();
224 if let Ok(mut observers) = power_observers().lock() {
225 observers.push((id, Arc::new(observer)));
226 }
227 PowerObserverRegistration { id }
228}
229
230pub fn publish_power_state(state: PowerState) {
233 let observers = power_observers()
234 .lock()
235 .map(|observers| {
236 observers
237 .iter()
238 .map(|(_, observer)| Arc::clone(observer))
239 .collect::<Vec<_>>()
240 })
241 .unwrap_or_default();
242 for observer in observers {
243 observer(state);
244 }
245}
246
247#[expect(non_snake_case, reason = "names follow Compose's casing")]
250#[track_caller]
251pub fn rememberPowerState() -> State<PowerState> {
252 let updates = rememberEventStream((), |sender| {
253 observe_power_state(move |state| sender.send(state))
254 });
255 cranpose_core::collectAsState(updates, (), power_state())
256}
257
258#[cfg(test)]
259#[path = "tests/power_tests.rs"]
260mod tests;