1use std::time::Duration;
27
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub struct Power {
31 pub on_mains: Option<bool>,
33 pub power_saver: Option<bool>,
35 pub idle: Option<bool>,
37 pub metered: Option<bool>,
39}
40
41impl Power {
42 pub fn supplied(&self) -> bool {
45 self.on_mains == Some(true) && self.power_saver == Some(false)
46 }
47}
48
49pub const IDLE: Duration = Duration::from_secs(15 * 60);
51
52#[cfg_attr(not(all(unix, not(any(target_os = "macos", target_os = "android")))), allow(dead_code))]
54const ASK: Duration = Duration::from_secs(1);
55
56pub fn read() -> Power {
58 imp::read(true)
59}
60
61pub fn supply() -> Power {
64 imp::read(false)
65}
66
67pub fn metered_of(value: u32) -> Option<bool> {
70 match value {
71 1 | 3 => Some(true),
72 2 | 4 => Some(false),
73 _ => None,
74 }
75}
76
77pub fn idle_of(locked: bool, shortest: Option<Duration>) -> Option<bool> {
80 if locked {
81 return Some(true);
82 }
83 shortest.map(|d| d >= IDLE)
84}
85
86#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
87mod imp {
88 use super::{ASK, Power, idle_of, metered_of};
89 use dbus::blocking::Connection;
90 use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
91 use std::time::Duration;
92
93 pub(super) fn read(all: bool) -> Power {
94 let Ok(system) = Connection::new_system() else { return Power::default() };
95 let mut power = Power { on_mains: on_mains(&system), power_saver: power_saver(&system), ..Power::default() };
96 if all {
97 power.metered = metered(&system);
98 power.idle = idle(&system);
99 }
100 power
101 }
102
103 fn on_mains(system: &Connection) -> Option<bool> {
104 let on_battery: bool = system.with_proxy("org.freedesktop.UPower", "/org/freedesktop/UPower", ASK).get("org.freedesktop.UPower", "OnBattery").ok()?;
105 Some(!on_battery)
106 }
107
108 fn power_saver(system: &Connection) -> Option<bool> {
110 [("org.freedesktop.UPower.PowerProfiles", "/org/freedesktop/UPower/PowerProfiles"), ("net.hadess.PowerProfiles", "/net/hadess/PowerProfiles")]
111 .into_iter()
112 .find_map(|(name, path)| system.with_proxy(name, path, ASK).get::<String>(name, "ActiveProfile").ok())
113 .map(|profile| profile == "power-saver")
114 }
115
116 fn metered(system: &Connection) -> Option<bool> {
117 let value: u32 = system.with_proxy("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", ASK).get("org.freedesktop.NetworkManager", "Metered").ok()?;
118 metered_of(value)
119 }
120
121 fn idle(system: &Connection) -> Option<bool> {
122 let mut locked = false;
123 let mut times: Vec<Duration> = Vec::new();
124 if let Ok(session) = Connection::new_session() {
125 let saver = session.with_proxy("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", ASK);
126 let active: Result<(bool,), _> = saver.method_call("org.freedesktop.ScreenSaver", "GetActive", ());
127 locked |= active.is_ok_and(|(on,)| on);
128 let since: Result<(u32,), _> = saver.method_call("org.freedesktop.ScreenSaver", "GetSessionIdleTime", ());
130 times.extend(since.ok().map(|(ms,)| Duration::from_millis(u64::from(ms))));
131 let mutter: Result<(u64,), _> = session.with_proxy("org.gnome.Mutter.IdleMonitor", "/org/gnome/Mutter/IdleMonitor/Core", ASK).method_call("org.gnome.Mutter.IdleMonitor", "GetIdletime", ());
132 times.extend(mutter.ok().map(|(ms,)| Duration::from_millis(ms)));
133 let gnome: Result<(bool,), _> = session.with_proxy("org.gnome.ScreenSaver", "/org/gnome/ScreenSaver", ASK).method_call("org.gnome.ScreenSaver", "GetActive", ());
134 locked |= gnome.is_ok_and(|(on,)| on);
135 }
136 let login = system.with_proxy("org.freedesktop.login1", "/org/freedesktop/login1/session/auto", ASK);
137 locked |= login.get::<bool>("org.freedesktop.login1.Session", "LockedHint").unwrap_or(false);
138 if login.get::<bool>("org.freedesktop.login1.Session", "IdleHint").unwrap_or(false)
140 && let Ok(since) = login.get::<u64>("org.freedesktop.login1.Session", "IdleSinceHint")
141 && let Ok(now) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
142 {
143 times.push(now.saturating_sub(Duration::from_micros(since)));
144 }
145 idle_of(locked, times.into_iter().min())
146 }
147}
148
149#[cfg(windows)]
150mod imp {
151 use super::{IDLE, Power};
152
153 #[repr(C)]
155 #[derive(Default)]
156 struct PowerStatus {
157 ac_line_status: u8,
158 battery_flag: u8,
159 battery_life_percent: u8,
160 system_status_flag: u8,
162 battery_life_time: u32,
163 battery_full_life_time: u32,
164 }
165
166 #[repr(C)]
168 struct LastInput {
169 size: u32,
170 time: u32,
171 }
172
173 #[link(name = "kernel32")]
174 unsafe extern "system" {
175 fn GetSystemPowerStatus(status: *mut PowerStatus) -> i32;
176 fn GetTickCount() -> u32;
177 }
178
179 #[link(name = "user32")]
180 unsafe extern "system" {
181 fn GetLastInputInfo(info: *mut LastInput) -> i32;
182 }
183
184 pub(super) fn read(all: bool) -> Power {
185 let mut status = PowerStatus::default();
186 let read = unsafe { GetSystemPowerStatus(&mut status) } != 0;
188 let on_mains = if read {
189 match status.ac_line_status {
190 0 => Some(false),
191 1 => Some(true),
192 _ => None,
193 }
194 } else {
195 None
196 };
197 let power_saver = read.then_some(status.system_status_flag == 1);
198 let mut power = Power { on_mains, power_saver, ..Power::default() };
199 if all {
200 let mut input = LastInput { size: std::mem::size_of::<LastInput>() as u32, time: 0 };
201 power.idle = (unsafe { GetLastInputInfo(&mut input) } != 0).then(|| {
203 let now = unsafe { GetTickCount() };
204 u128::from(now.wrapping_sub(input.time)) >= IDLE.as_millis()
205 });
206 }
207 power
208 }
209}
210
211#[cfg(target_os = "macos")]
212mod imp {
213 use super::{IDLE, Power};
214 use std::ffi::{CStr, c_char, c_void};
215
216 #[link(name = "IOKit", kind = "framework")]
217 unsafe extern "C" {
218 fn IOPSCopyPowerSourcesInfo() -> *const c_void;
219 fn IOPSGetProvidingPowerSourceType(snapshot: *const c_void) -> *const c_void;
220 }
221
222 #[link(name = "CoreFoundation", kind = "framework")]
223 unsafe extern "C" {
224 fn CFStringGetCString(string: *const c_void, buffer: *mut c_char, size: isize, encoding: u32) -> u8;
225 fn CFRelease(object: *const c_void);
226 }
227
228 #[link(name = "CoreGraphics", kind = "framework")]
229 unsafe extern "C" {
230 fn CGEventSourceSecondsSinceLastEventType(state: i32, event_type: u32) -> f64;
231 }
232
233 const UTF8: u32 = 0x0800_0100;
235
236 pub(super) fn read(all: bool) -> Power {
237 let mut power = Power { on_mains: on_mains(), power_saver: low_power_mode(), ..Power::default() };
238 if all {
239 let seconds = unsafe { CGEventSourceSecondsSinceLastEventType(0, u32::MAX) };
242 power.idle = (seconds.is_finite() && seconds >= 0.0).then(|| seconds >= IDLE.as_secs_f64());
243 }
244 power
245 }
246
247 fn on_mains() -> Option<bool> {
249 unsafe {
252 let snapshot = IOPSCopyPowerSourcesInfo();
253 if snapshot.is_null() {
254 return None;
255 }
256 let kind = IOPSGetProvidingPowerSourceType(snapshot);
257 let mut buffer = [0 as c_char; 64];
258 let read = !kind.is_null() && CFStringGetCString(kind, buffer.as_mut_ptr(), buffer.len() as isize, UTF8) != 0;
259 let name = read.then(|| CStr::from_ptr(buffer.as_ptr()).to_string_lossy().into_owned());
260 CFRelease(snapshot);
261 name.map(|n| n == "AC Power")
262 }
263 }
264
265 fn low_power_mode() -> Option<bool> {
267 let out = std::process::Command::new("pmset").arg("-g").output().ok().filter(|o| o.status.success())?;
268 let text = String::from_utf8_lossy(&out.stdout);
269 Some(text.lines().any(|line| {
270 let mut words = line.split_whitespace();
271 words.next() == Some("lowpowermode") && words.next() == Some("1")
272 }))
273 }
274}
275
276#[cfg(any(target_os = "android", not(any(unix, windows))))]
277mod imp {
278 pub(super) fn read(_all: bool) -> super::Power {
279 super::Power::default()
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
289 fn metered_as_networkmanager_says() {
290 assert_eq!([0, 1, 2, 3, 4, 9].map(metered_of), [None, Some(true), Some(false), Some(true), Some(false), None]);
291 }
292
293 #[test]
295 fn idle_as_the_session_says() {
296 let minutes = |m: u64| Some(Duration::from_secs(m * 60));
297 assert_eq!(idle_of(true, None), Some(true));
298 assert_eq!(idle_of(true, minutes(1)), Some(true));
299 assert_eq!(idle_of(false, minutes(15)), Some(true));
300 assert_eq!(idle_of(false, minutes(14)), Some(false));
301 assert_eq!(idle_of(false, None), None);
302 }
303
304 #[test]
306 fn supplied_needs_both_known() {
307 let p = |on_mains, power_saver| Power { on_mains, power_saver, ..Power::default() };
308 assert!(p(Some(true), Some(false)).supplied());
309 for (on_mains, saver) in [(Some(false), Some(false)), (Some(true), Some(true)), (None, Some(false)), (Some(true), None), (None, None)] {
310 assert!(!p(on_mains, saver).supplied(), "{on_mains:?} {saver:?}");
311 }
312 }
313}