1use std::{
2 ffi::OsStr,
3 fs::File,
4 io::Read,
5 path::PathBuf,
6 process::Command,
7 thread,
8 time::{Duration, Instant},
9};
10
11use log::{debug, error, info, warn};
12use sysinfo::{ProcessRefreshKind, RefreshKind, System};
13
14pub enum PowerState {
15 Charging,
17 Discharging,
19}
20
21impl PowerState {
22 fn current(battery_name: &String) -> Self {
29 let battery_path = PathBuf::from(format!("/sys/class/power_supply/{battery_name}/status"));
30
31 let mut battery_status = format!("Failed to read from {battery_path:?}");
32 {
33 let _ = File::open(battery_path).and_then(|mut x| {
34 battery_status.clear();
35 x.read_to_string(&mut battery_status)
36 });
37 }
38
39 let trimmed_status = battery_status.trim();
40
41 match trimmed_status {
42 "Charging" => PowerState::Charging,
43 "Not Charging" => PowerState::Charging,
44 "Discharging" => PowerState::Discharging,
45 _ => {
46 warn!("Powerstate: {trimmed_status} unknown. Returning Discharging");
47 PowerState::Discharging
48 }
49 }
50 }
51}
52
53pub struct LockWatcher {
54 pub lock_process_names: Vec<String>,
55 pub battery_id: String,
56 pub polling_rate: Duration,
57 pub time_until_shutdown: Duration,
58}
59
60impl LockWatcher {
61 fn system_locked(&self) -> bool {
63 let system = System::new_with_specifics(
64 RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()),
65 );
66
67 for process_name in &self.lock_process_names {
68 if system
69 .processes_by_name(OsStr::new(process_name))
70 .next()
71 .is_some()
72 {
73 debug!("{process_name} is running");
74 return true;
75 }
76 }
77
78 false
79 }
80
81 fn get_power_state(&self) -> PowerState {
83 PowerState::current(&self.battery_id)
84 }
85
86 pub fn loop_forever(&self) -> ! {
87 let mut past_power_state: PowerState = self.get_power_state();
88 let mut started_discharging_at: Option<Instant> = None;
89
90 loop {
91 thread::sleep(self.polling_rate);
92
93 if !self.system_locked() {
94 past_power_state = self.get_power_state();
95 continue;
96 }
97
98 match (&past_power_state, self.get_power_state()) {
99 (PowerState::Charging, PowerState::Charging) => {
101 started_discharging_at = None;
102 }
103 (PowerState::Charging, PowerState::Discharging) => {
105 started_discharging_at = Some(Instant::now());
106 past_power_state = PowerState::Discharging;
107 info!("Disconnected")
108 }
109 (PowerState::Discharging, PowerState::Charging) => {
111 started_discharging_at = None;
112 past_power_state = PowerState::Charging;
113 info!("Plugged in")
114 }
115 (PowerState::Discharging, PowerState::Discharging) => {}
117 }
118
119 if let Some(discharging_since) = started_discharging_at {
120 let elapsed = discharging_since.elapsed();
121 let until_shutdown = self.time_until_shutdown.as_secs_f32() - elapsed.as_secs_f32();
122 info!("Time until shutdown: {:.2} seconds", until_shutdown);
123
124 if elapsed > self.time_until_shutdown {
125 info!("Shutting down");
126
127 match Command::new("shutdown").arg("now").output() {
128 Ok(_) => {}
129 Err(v) => error!("Could not shut down: {:?}", v),
130 }
131
132 thread::sleep(Duration::from_secs_f32(2.0));
133 }
134 }
135 }
136 }
137}