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().to_lowercase();
40 let trimmed_status = trimmed_status.as_str();
41
42 match trimmed_status {
43 "charging" => PowerState::Charging,
44 "full" => PowerState::Charging,
45 "not charging" => PowerState::Charging,
46 "discharging" => PowerState::Discharging,
47 _ => {
48 warn!("Powerstate: {trimmed_status} unknown. Returning Discharging");
49 PowerState::Discharging
50 }
51 }
52 }
53}
54
55pub struct LockWatcher {
56 pub lock_process_names: Vec<String>,
57 pub battery_id: String,
58 pub polling_rate: Duration,
59 pub time_until_shutdown: Duration,
60}
61
62impl LockWatcher {
63 fn system_locked(&self) -> bool {
65 let system = System::new_with_specifics(
66 RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()),
67 );
68
69 for process_name in &self.lock_process_names {
70 if system
71 .processes_by_name(OsStr::new(process_name))
72 .next()
73 .is_some()
74 {
75 debug!("{process_name} is running");
76 return true;
77 }
78 }
79
80 false
81 }
82
83 fn get_power_state(&self) -> PowerState {
85 PowerState::current(&self.battery_id)
86 }
87
88 pub fn loop_forever(&self) -> ! {
89 let mut past_power_state: PowerState = self.get_power_state();
90 let mut started_discharging_at: Option<Instant> = None;
91
92 loop {
93 thread::sleep(self.polling_rate);
94
95 if !self.system_locked() {
96 past_power_state = self.get_power_state();
97 continue;
98 }
99
100 match (&past_power_state, self.get_power_state()) {
101 (PowerState::Charging, PowerState::Charging) => {
103 started_discharging_at = None;
104 }
105 (PowerState::Charging, PowerState::Discharging) => {
107 started_discharging_at = Some(Instant::now());
108 past_power_state = PowerState::Discharging;
109 info!("Disconnected")
110 }
111 (PowerState::Discharging, PowerState::Charging) => {
113 started_discharging_at = None;
114 past_power_state = PowerState::Charging;
115 info!("Plugged in")
116 }
117 (PowerState::Discharging, PowerState::Discharging) => {}
119 }
120
121 if let Some(discharging_since) = started_discharging_at {
122 let elapsed = discharging_since.elapsed();
123 let until_shutdown = self.time_until_shutdown.as_secs_f32() - elapsed.as_secs_f32();
124 info!("Time until shutdown: {:.2} seconds", until_shutdown);
125
126 if elapsed > self.time_until_shutdown {
127 info!("Shutting down");
128
129 match Command::new("shutdown").arg("now").output() {
130 Ok(_) => {}
131 Err(v) => error!("Could not shut down: {:?}", v),
132 }
133
134 thread::sleep(Duration::from_secs_f32(2.0));
135 }
136 }
137 }
138 }
139}