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 output = std::process::Command::new("ioreg")
139 .args(["-r", "-c", "AppleSmartBattery"])
140 .output()
141 .ok()?;
142
143 if !output.status.success() {
144 return None;
145 }
146
147 let stdout = String::from_utf8_lossy(&output.stdout);
148 if stdout.trim().is_empty() {
149 return None;
150 }
151
152 let mut current_capacity: Option<f32> = None;
153 let mut max_capacity: Option<f32> = None;
154 let mut raw_max_capacity: Option<f32> = None;
155 let mut design_capacity: Option<f32> = None;
156 let mut is_charging = false;
157 let mut fully_charged = false;
158 let mut time_remaining_mins: Option<i32> = None;
159 let mut vendor: Option<String> = None;
160 let mut model: Option<String> = None;
161
162 for line in stdout.lines() {
163 if let Some(val) = parse_ioreg_line(line, "CurrentCapacity") {
164 current_capacity = val.parse().ok();
165 } else if let Some(val) = parse_ioreg_line(line, "MaxCapacity") {
166 max_capacity = val.parse().ok();
167 } else if let Some(val) = parse_ioreg_line(line, "AppleRawMaxCapacity") {
168 raw_max_capacity = val.parse().ok();
169 } else if let Some(val) = parse_ioreg_line(line, "DesignCapacity") {
170 design_capacity = val.parse().ok();
171 } else if let Some(val) = parse_ioreg_line(line, "IsCharging") {
172 is_charging = val == "Yes";
173 } else if let Some(val) = parse_ioreg_line(line, "FullyCharged") {
174 fully_charged = val == "Yes";
175 } else if let Some(val) = parse_ioreg_line(line, "TimeRemaining") {
176 time_remaining_mins = val.parse().ok();
177 } else if let Some(val) = parse_ioreg_line(line, "Manufacturer") {
178 vendor = Some(val);
179 } else if let Some(val) = parse_ioreg_line(line, "DeviceName") {
180 model = Some(val);
181 }
182 }
183
184 let max_cap = max_capacity?;
185 let cur_cap = current_capacity?;
186
187 let percentage = if max_cap > 0.0 {
188 (cur_cap / max_cap) * 100.0
189 } else {
190 0.0
191 };
192
193 let mut health = None;
194 if let Some(design_cap) = design_capacity {
195 if design_cap > 0.0 {
196 let h_max = raw_max_capacity.or(max_capacity);
197 if let Some(health_max) = h_max {
198 health = Some((health_max / design_cap) * 100.0);
199 }
200 }
201 }
202
203 let state = if fully_charged {
204 BatteryState::Full
205 } else if is_charging {
206 BatteryState::Charging
207 } else {
208 BatteryState::Discharging
209 };
210
211 let mut time_remaining = None;
212 if let Some(mins) = time_remaining_mins {
213 if mins > 0 && mins < 65535 {
214 time_remaining = Some(std::time::Duration::from_secs((mins * 60) as u64));
215 }
216 }
217
218 Some(BatteryInfo {
219 percentage,
220 health,
221 state,
222 time_remaining,
223 vendor,
224 model,
225 })
226}
227
228#[cfg(target_os = "macos")]
229fn parse_ioreg_line(line: &str, key: &str) -> Option<String> {
230 if let Some(pos) = line.find(&format!("\"{}\"", key)) {
231 let after_key = &line[pos + key.len() + 2..];
232 if let Some(equals_pos) = after_key.find('=') {
233 let val = &after_key[equals_pos + 1..];
234 let val = val.trim().trim_matches('"');
235 return Some(val.to_string());
236 }
237 }
238 None
239}
240
241#[cfg(target_os = "windows")]
242mod win32 {
243 #[repr(C)]
244 pub struct SYSTEM_POWER_STATUS {
245 pub ac_line_status: u8,
246 pub battery_flag: u8,
247 pub battery_life_percent: u8,
248 pub system_status: u8,
249 pub battery_life_time: u32,
250 pub battery_full_life_time: u32,
251 }
252
253 #[link(name = "kernel32")]
254 extern "system" {
255 pub fn GetSystemPowerStatus(lpSystemPowerStatus: *mut SYSTEM_POWER_STATUS) -> i32;
256 }
257}
258
259#[cfg(target_os = "windows")]
260pub fn get_battery_info() -> Option<BatteryInfo> {
261 let mut status = win32::SYSTEM_POWER_STATUS {
262 ac_line_status: 255,
263 battery_flag: 255,
264 battery_life_percent: 255,
265 system_status: 0,
266 battery_life_time: 0xffffffff,
267 battery_full_life_time: 0xffffffff,
268 };
269
270 let res = unsafe { win32::GetSystemPowerStatus(&mut status) };
271 if res == 0 || status.battery_life_percent == 255 {
272 return None;
273 }
274
275 let percentage = status.battery_life_percent as f32;
276 let state = match status.ac_line_status {
277 1 => {
278 if percentage >= 100.0 {
279 BatteryState::Full
280 } else {
281 BatteryState::Charging
282 }
283 }
284 0 => BatteryState::Discharging,
285 _ => BatteryState::Unknown,
286 };
287
288 let time_remaining = if status.battery_life_time != 0xffffffff {
289 Some(std::time::Duration::from_secs(
290 status.battery_life_time as u64,
291 ))
292 } else {
293 None
294 };
295
296 let mut info = BatteryInfo {
297 percentage,
298 health: None,
299 state,
300 time_remaining,
301 vendor: None,
302 model: None,
303 };
304
305 if let Ok(output) = std::process::Command::new("powershell")
306 .args([
307 "-Command",
308 "Get-CimInstance Win32_Battery | Select-Object DesignCapacity,FullChargeCapacity,Manufacturer,Name | Format-List",
309 ])
310 .output()
311 {
312 if let Ok(stdout) = String::from_utf8(output.stdout) {
313 let mut design_capacity: Option<f32> = None;
314 let mut full_charge_capacity: Option<f32> = None;
315
316 for line in stdout.lines() {
317 let line = line.trim();
318 if let Some(pos) = line.find(':') {
319 let key = line[..pos].trim();
320 let val = line[pos + 1..].trim();
321 if !val.is_empty() {
322 match key {
323 "DesignCapacity" => design_capacity = val.parse().ok(),
324 "FullChargeCapacity" => full_charge_capacity = val.parse().ok(),
325 "Manufacturer" => info.vendor = Some(val.to_string()),
326 "Name" => info.model = Some(val.to_string()),
327 _ => {}
328 }
329 }
330 }
331 }
332
333 if let (Some(full), Some(design)) = (full_charge_capacity, design_capacity) {
334 if design > 0.0 {
335 info.health = Some((full / design) * 100.0);
336 }
337 }
338 }
339 }
340
341 Some(info)
342}
343
344#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
345pub fn get_battery_info() -> Option<BatteryInfo> {
346 None
347}
348
349#[cfg(target_os = "linux")]
351fn read_file_to_string<P: AsRef<Path>>(path: P) -> Option<String> {
352 fs::read_to_string(path).ok().map(|s| s.trim().to_string())
353}
354
355#[cfg(target_os = "linux")]
356fn read_file_to_num<T: std::str::FromStr, P: AsRef<Path>>(path: P) -> Option<T> {
357 read_file_to_string(path).and_then(|s| s.parse().ok())
358}
359
360#[cfg(test)]
361mod tests {
362 #[cfg(target_os = "macos")]
363 use super::parse_ioreg_line;
364
365 #[test]
366 #[cfg(target_os = "macos")]
367 fn test_parse_ioreg_line() {
368 let line = r#" | "MaxCapacity" = 5000"#;
369 assert_eq!(
370 parse_ioreg_line(line, "MaxCapacity"),
371 Some("5000".to_string())
372 );
373
374 let line_str = r#" | "DeviceName" = "DELL BATTERY""#;
375 assert_eq!(
376 parse_ioreg_line(line_str, "DeviceName"),
377 Some("DELL BATTERY".to_string())
378 );
379
380 assert_eq!(parse_ioreg_line(line, "MissingKey"), None);
381 }
382}