1use std::fs;
9use std::path::Path;
10use std::process::Command;
11
12pub fn is_metered_connection() -> bool {
14 let output = match Command::new("nmcli")
15 .args([
16 "-t",
17 "-f",
18 "GENERAL.METERED",
19 "connection",
20 "show",
21 "--active",
22 ])
23 .output()
24 {
25 Ok(output) if output.status.success() => output,
26 _ => return false,
27 };
28
29 is_metered_connection_from_nmcli_output(&String::from_utf8_lossy(&output.stdout))
30}
31
32pub fn is_on_battery_power() -> bool {
34 let power_supply_root = Path::new("/sys/class/power_supply");
35 let entries = match fs::read_dir(power_supply_root) {
36 Ok(entries) => entries,
37 Err(_) => return false,
38 };
39
40 let mut statuses = Vec::new();
41
42 for entry in entries.flatten() {
43 let supply_path = entry.path();
44 let supply_type = fs::read_to_string(supply_path.join("type"))
45 .ok()
46 .map(|value| value.trim().to_string());
47 let online = fs::read_to_string(supply_path.join("online"))
48 .ok()
49 .map(|value| value.trim() == "1");
50 let status = fs::read_to_string(supply_path.join("status"))
51 .ok()
52 .map(|value| value.trim().to_ascii_lowercase());
53 statuses.push((supply_type, online, status));
54 }
55
56 is_on_battery_power_from_statuses(&statuses)
57}
58
59fn is_metered_connection_from_nmcli_output(stdout: &str) -> bool {
61 let stdout = stdout.to_ascii_lowercase();
62 stdout.contains("yes") || stdout.contains("guessed-yes")
63}
64
65fn is_on_battery_power_from_statuses(
67 statuses: &[(Option<String>, Option<bool>, Option<String>)],
68) -> bool {
69 let mut found_battery = false;
70 let mut mains_online = false;
71
72 for (supply_type, online, status) in statuses {
73 match supply_type.as_deref() {
74 Some("Mains") | Some("USB") | Some("USB_C") if online.unwrap_or(false) => {
75 mains_online = true;
76 }
77 Some("Battery") => {
78 found_battery = true;
79 let status = status.as_deref().unwrap_or_default();
80 if status == "charging" || status == "full" {
81 return false;
82 }
83 }
84 _ => {}
85 }
86 }
87
88 found_battery && !mains_online
89}
90
91#[cfg(test)]
92mod tests {
93 use super::{
94 is_metered_connection, is_metered_connection_from_nmcli_output, is_on_battery_power,
95 is_on_battery_power_from_statuses,
96 };
97
98 #[test]
99 fn runtime_checks_are_safe() {
100 let _ = is_metered_connection();
101 let _ = is_on_battery_power();
102 }
103
104 #[test]
105 fn test_metered_connection_parser() {
106 assert!(is_metered_connection_from_nmcli_output(
107 "GENERAL.METERED:yes\n"
108 ));
109 assert!(is_metered_connection_from_nmcli_output(
110 "GENERAL.METERED:guessed-yes\n"
111 ));
112 assert!(!is_metered_connection_from_nmcli_output(
113 "GENERAL.METERED:no\n"
114 ));
115 }
116
117 #[test]
118 fn test_battery_power_detection_from_statuses() {
119 assert!(is_on_battery_power_from_statuses(&[
120 (Some("Battery".into()), None, Some("discharging".into())),
121 (Some("Mains".into()), Some(false), None),
122 ]));
123
124 assert!(!is_on_battery_power_from_statuses(&[
125 (Some("Battery".into()), None, Some("charging".into())),
126 (Some("Mains".into()), Some(true), None),
127 ]));
128
129 assert!(!is_on_battery_power_from_statuses(&[(
130 Some("Mains".into()),
131 Some(true),
132 None,
133 )]));
134 }
135}