1#[cfg(target_os = "linux")]
5use std::fs;
6#[cfg(target_os = "linux")]
7use std::path::Path;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum BatteryState {
11 Charging,
12 Discharging,
13 Full,
14 Unknown,
15}
16
17#[derive(Debug, Clone)]
18pub struct BatteryInfo {
19 pub percentage: f32,
20 pub health: Option<f32>,
21 pub state: BatteryState,
22 pub time_remaining: Option<std::time::Duration>,
23 pub vendor: Option<String>,
24 pub model: Option<String>,
25}
26
27#[cfg(target_os = "linux")]
28pub fn get_battery_info() -> Option<BatteryInfo> {
29 let power_supply = Path::new("/sys/class/power_supply");
30 if !power_supply.exists() {
31 return None;
32 }
33
34 let entries = fs::read_dir(power_supply).ok()?;
35 for entry in entries.flatten() {
36 let path = entry.path();
37 let name = path.file_name()?.to_string_lossy();
38 if name.starts_with("BAT") || name.starts_with("sb-") {
39 if let Some(supply_type) = read_file_to_string(path.join("type")) {
41 if supply_type != "Battery" {
42 continue;
43 }
44 }
45
46 let percentage = read_file_to_num::<f32, _>(path.join("capacity"))?;
48
49 let state_str = read_file_to_string(path.join("status")).unwrap_or_default();
51 let state = match state_str.as_str() {
52 "Charging" => BatteryState::Charging,
53 "Discharging" => BatteryState::Discharging,
54 "Full" => BatteryState::Full,
55 _ => BatteryState::Unknown,
56 };
57
58 let vendor = read_file_to_string(path.join("manufacturer"))
60 .or_else(|| read_file_to_string(path.join("vendor")));
61 let model = read_file_to_string(path.join("model_name"))
62 .or_else(|| read_file_to_string(path.join("model")));
63
64 let mut health = None;
66 if let (Some(full), Some(design)) = (
67 read_file_to_num::<f32, _>(path.join("energy_full")),
68 read_file_to_num::<f32, _>(path.join("energy_full_design")),
69 ) {
70 if design > 0.0 {
71 health = Some((full / design) * 100.0);
72 }
73 } else if let (Some(full), Some(design)) = (
74 read_file_to_num::<f32, _>(path.join("charge_full")),
75 read_file_to_num::<f32, _>(path.join("charge_full_design")),
76 ) {
77 if design > 0.0 {
78 health = Some((full / design) * 100.0);
79 }
80 }
81
82 let mut time_remaining = None;
84 if state == BatteryState::Charging || state == BatteryState::Discharging {
85 if let (Some(power), Some(energy_now)) = (
86 read_file_to_num::<f64, _>(path.join("power_now")),
87 read_file_to_num::<f64, _>(path.join("energy_now")),
88 ) {
89 if power > 0.0 {
90 let hours = match state {
91 BatteryState::Discharging => energy_now / power,
92 BatteryState::Charging => {
93 let energy_full =
94 read_file_to_num::<f64, _>(path.join("energy_full"))
95 .unwrap_or(energy_now);
96 (energy_full - energy_now).max(0.0) / power
97 }
98 _ => 0.0,
99 };
100 time_remaining = Some(std::time::Duration::from_secs_f64(hours * 3600.0));
101 }
102 } else if let (Some(current), Some(charge_now)) = (
103 read_file_to_num::<f64, _>(path.join("current_now")),
104 read_file_to_num::<f64, _>(path.join("charge_now")),
105 ) {
106 if current > 0.0 {
107 let hours = match state {
108 BatteryState::Discharging => charge_now / current,
109 BatteryState::Charging => {
110 let charge_full =
111 read_file_to_num::<f64, _>(path.join("charge_full"))
112 .unwrap_or(charge_now);
113 (charge_full - charge_now).max(0.0) / current
114 }
115 _ => 0.0,
116 };
117 time_remaining = Some(std::time::Duration::from_secs_f64(hours * 3600.0));
118 }
119 }
120 }
121
122 return Some(BatteryInfo {
123 percentage,
124 health,
125 state,
126 time_remaining,
127 vendor,
128 model,
129 });
130 }
131 }
132
133 None
134}
135
136#[cfg(target_os = "macos")]
137pub fn get_battery_info() -> Option<BatteryInfo> {
138 let raw = crate::macos_ffi::get_battery_raw()?;
139
140 let max_cap = raw.max_mah? as f32;
141 let cur_cap = raw.current_mah? as f32;
142
143 let percentage = if max_cap > 0.0 {
144 (cur_cap / max_cap) * 100.0
145 } else {
146 0.0
147 };
148
149 let health = raw.design_mah.and_then(|design| {
150 if design == 0 {
151 return None;
152 }
153 let h_max = raw.raw_max_mah.or(raw.max_mah)? as f32;
154 Some((h_max / design as f32) * 100.0)
155 });
156
157 let state = if raw.fully_charged {
158 BatteryState::Full
159 } else if raw.is_charging {
160 BatteryState::Charging
161 } else {
162 BatteryState::Discharging
163 };
164
165 let time_remaining = raw
166 .time_remaining_mins
167 .map(|m| std::time::Duration::from_secs(m * 60));
168
169 Some(BatteryInfo {
170 percentage,
171 health,
172 state,
173 time_remaining,
174 vendor: raw.vendor,
175 model: raw.model,
176 })
177}
178
179#[cfg(target_os = "windows")]
180mod win32 {
181 #[repr(C)]
182 pub struct SYSTEM_POWER_STATUS {
183 pub ac_line_status: u8,
184 pub battery_flag: u8,
185 pub battery_life_percent: u8,
186 pub system_status: u8,
187 pub battery_life_time: u32,
188 pub battery_full_life_time: u32,
189 }
190
191 #[link(name = "kernel32")]
192 extern "system" {
193 pub fn GetSystemPowerStatus(lpSystemPowerStatus: *mut SYSTEM_POWER_STATUS) -> i32;
194 }
195}
196
197#[cfg(target_os = "windows")]
198pub fn get_battery_info() -> Option<BatteryInfo> {
199 let mut status = win32::SYSTEM_POWER_STATUS {
200 ac_line_status: 255,
201 battery_flag: 255,
202 battery_life_percent: 255,
203 system_status: 0,
204 battery_life_time: 0xffffffff,
205 battery_full_life_time: 0xffffffff,
206 };
207
208 let res = unsafe { win32::GetSystemPowerStatus(&mut status) };
209 if res == 0 || status.battery_life_percent == 255 {
210 return None;
211 }
212
213 let percentage = status.battery_life_percent as f32;
214 let state = match status.ac_line_status {
215 1 => {
216 if percentage >= 100.0 {
217 BatteryState::Full
218 } else {
219 BatteryState::Charging
220 }
221 }
222 0 => BatteryState::Discharging,
223 _ => BatteryState::Unknown,
224 };
225
226 let time_remaining = if status.battery_life_time != 0xffffffff {
227 Some(std::time::Duration::from_secs(
228 status.battery_life_time as u64,
229 ))
230 } else {
231 None
232 };
233
234 let mut info = BatteryInfo {
235 percentage,
236 health: None,
237 state,
238 time_remaining,
239 vendor: None,
240 model: None,
241 };
242
243 if let Some(device) = win_battery::first_battery() {
251 if let (Some(design), Some(full)) = (device.designed_capacity, device.full_charged_capacity)
252 {
253 if design > 0 {
254 info.health = Some((full as f32 / design as f32) * 100.0);
255 }
256 }
257 info.vendor = device.manufacturer;
258 info.model = device.device_name;
259 }
260
261 Some(info)
262}
263
264#[cfg(target_os = "windows")]
269#[derive(Default)]
270struct WinBattery {
271 designed_capacity: Option<u32>,
272 full_charged_capacity: Option<u32>,
273 manufacturer: Option<String>,
274 device_name: Option<String>,
275}
276
277#[cfg(target_os = "windows")]
287mod win_battery {
288 use super::WinBattery;
289 use crate::win_setupapi::Guid;
290 use std::ffi::{c_void, OsStr};
291 use std::mem::size_of;
292 use std::os::windows::ffi::OsStrExt;
293 use std::ptr;
294
295 type Handle = *mut c_void;
296 const INVALID_HANDLE_VALUE: Handle = -1isize as Handle;
297 const GENERIC_READ: u32 = 0x8000_0000;
298 const FILE_SHARE_READ: u32 = 0x0000_0001;
299 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
300 const OPEN_EXISTING: u32 = 3;
301
302 const GUID_DEVICE_BATTERY: Guid = Guid {
304 data1: 0x7263_1e54,
305 data2: 0x78A4,
306 data3: 0x11d0,
307 data4: [0xbc, 0xf7, 0x00, 0xaa, 0x00, 0xb7, 0xb3, 0x2a],
308 };
309
310 const IOCTL_BATTERY_QUERY_TAG: u32 = 0x0029_4040;
312 const IOCTL_BATTERY_QUERY_INFORMATION: u32 = 0x0029_4044;
313
314 const BATTERY_INFORMATION_LEVEL: u32 = 0;
316 const BATTERY_DEVICE_NAME: u32 = 4;
317 const BATTERY_MANUFACTURE_NAME: u32 = 6;
318
319 #[repr(C)]
321 struct BatteryQueryInformation {
322 battery_tag: u32,
323 information_level: u32,
324 at_rate: i32,
325 }
326
327 #[repr(C)]
334 #[derive(Default)]
335 struct BatteryInformation {
336 capabilities: u32,
337 technology: u8,
338 reserved: [u8; 3],
339 chemistry: [u8; 4],
340 designed_capacity: u32,
341 full_charged_capacity: u32,
342 default_alert1: u32,
343 default_alert2: u32,
344 critical_bias: u32,
345 cycle_count: u32,
346 }
347
348 extern "system" {
349 fn CreateFileW(
350 lp_file_name: *const u16,
351 dw_desired_access: u32,
352 dw_share_mode: u32,
353 lp_security_attributes: *mut c_void,
354 dw_creation_disposition: u32,
355 dw_flags_and_attributes: u32,
356 h_template_file: Handle,
357 ) -> Handle;
358 fn DeviceIoControl(
359 h_device: Handle,
360 dw_io_control_code: u32,
361 lp_in_buffer: *const c_void,
362 n_in_buffer_size: u32,
363 lp_out_buffer: *mut c_void,
364 n_out_buffer_size: u32,
365 lp_bytes_returned: *mut u32,
366 lp_overlapped: *mut c_void,
367 ) -> i32;
368 fn CloseHandle(h_object: Handle) -> i32;
369 }
370
371 pub fn first_battery() -> Option<WinBattery> {
376 crate::win_setupapi::present_interface_device_paths(&GUID_DEVICE_BATTERY)
377 .into_iter()
378 .find_map(|path| read_battery(&path))
379 }
380
381 fn read_battery(path: &str) -> Option<WinBattery> {
382 let wide: Vec<u16> = OsStr::new(path).encode_wide().chain(Some(0)).collect();
383 let handle = unsafe {
386 CreateFileW(
387 wide.as_ptr(),
388 GENERIC_READ,
389 FILE_SHARE_READ | FILE_SHARE_WRITE,
390 ptr::null_mut(),
391 OPEN_EXISTING,
392 0,
393 ptr::null_mut(),
394 )
395 };
396 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
397 return None;
398 }
399 let out = read_with_handle(handle);
400 unsafe {
402 CloseHandle(handle);
403 }
404 out
405 }
406
407 fn read_with_handle(handle: Handle) -> Option<WinBattery> {
408 let mut tag: u32 = 0;
411 let mut returned: u32 = 0;
412 let wait: u32 = 0;
413 let ok = unsafe {
415 DeviceIoControl(
416 handle,
417 IOCTL_BATTERY_QUERY_TAG,
418 &wait as *const u32 as *const c_void,
419 size_of::<u32>() as u32,
420 &mut tag as *mut u32 as *mut c_void,
421 size_of::<u32>() as u32,
422 &mut returned,
423 ptr::null_mut(),
424 )
425 };
426 if ok == 0 || tag == 0 {
427 return None;
428 }
429
430 let mut battery = WinBattery {
431 manufacturer: query_string(handle, tag, BATTERY_MANUFACTURE_NAME),
432 device_name: query_string(handle, tag, BATTERY_DEVICE_NAME),
433 ..Default::default()
434 };
435
436 let query = BatteryQueryInformation {
437 battery_tag: tag,
438 information_level: BATTERY_INFORMATION_LEVEL,
439 at_rate: 0,
440 };
441 let mut info = BatteryInformation::default();
442 let ok = unsafe {
445 DeviceIoControl(
446 handle,
447 IOCTL_BATTERY_QUERY_INFORMATION,
448 &query as *const _ as *const c_void,
449 size_of::<BatteryQueryInformation>() as u32,
450 &mut info as *mut _ as *mut c_void,
451 size_of::<BatteryInformation>() as u32,
452 &mut returned,
453 ptr::null_mut(),
454 )
455 };
456 if ok != 0 {
457 battery.designed_capacity =
460 (info.designed_capacity > 0).then_some(info.designed_capacity);
461 battery.full_charged_capacity =
462 (info.full_charged_capacity > 0).then_some(info.full_charged_capacity);
463 }
464 Some(battery)
465 }
466
467 fn query_string(handle: Handle, tag: u32, level: u32) -> Option<String> {
469 let query = BatteryQueryInformation {
470 battery_tag: tag,
471 information_level: level,
472 at_rate: 0,
473 };
474 let mut buf = [0u16; 128];
475 let mut returned: u32 = 0;
476 let ok = unsafe {
479 DeviceIoControl(
480 handle,
481 IOCTL_BATTERY_QUERY_INFORMATION,
482 &query as *const _ as *const c_void,
483 size_of::<BatteryQueryInformation>() as u32,
484 buf.as_mut_ptr() as *mut c_void,
485 std::mem::size_of_val(&buf) as u32,
486 &mut returned,
487 ptr::null_mut(),
488 )
489 };
490 if ok == 0 || returned == 0 {
491 return None;
492 }
493 let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
494 let s = String::from_utf16_lossy(&buf[..end]).trim().to_string();
495 (!s.is_empty()).then_some(s)
496 }
497
498 #[cfg(test)]
499 mod layout {
500 use std::mem::{offset_of, size_of};
501
502 #[test]
506 fn ffi_struct_layout() {
507 assert_eq!(size_of::<super::BatteryQueryInformation>(), 12);
508 assert_eq!(size_of::<super::BatteryInformation>(), 36);
509 assert_eq!(offset_of!(super::BatteryInformation, chemistry), 8);
510 assert_eq!(offset_of!(super::BatteryInformation, designed_capacity), 12);
511 assert_eq!(
512 offset_of!(super::BatteryInformation, full_charged_capacity),
513 16
514 );
515 }
516 }
517}
518
519#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
520pub fn get_battery_info() -> Option<BatteryInfo> {
521 None
522}
523
524#[cfg(target_os = "linux")]
526fn read_file_to_string<P: AsRef<Path>>(path: P) -> Option<String> {
527 fs::read_to_string(path).ok().map(|s| s.trim().to_string())
528}
529
530#[cfg(target_os = "linux")]
531fn read_file_to_num<T: std::str::FromStr, P: AsRef<Path>>(path: P) -> Option<T> {
532 read_file_to_string(path).and_then(|s| s.parse().ok())
533}