Skip to main content

lock_guard/
lib.rs

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    /// Plugged in
16    Charging,
17    /// Not plugged in
18    Discharging,
19}
20
21impl PowerState {
22    /// Checks the status for a battery. If the battery could not be checked, then it returns PowerState::Discharging.
23    ///
24    /// Supported statuses:
25    ///  - "Charging" - Plugged in.
26    ///  - "Not Charging" - Plugged in, but not charging.
27    ///  - "Discharging" - Not plugged in.
28    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    /// Check if any of the lock processes are running
62    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    /// Get the current power state
82    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                // NOOP
100                (PowerState::Charging, PowerState::Charging) => {
101                    started_discharging_at = None;
102                }
103                // Device unplugged
104                (PowerState::Charging, PowerState::Discharging) => {
105                    started_discharging_at = Some(Instant::now());
106                    past_power_state = PowerState::Discharging;
107                    info!("Disconnected")
108                }
109                // Device plugged in
110                (PowerState::Discharging, PowerState::Charging) => {
111                    started_discharging_at = None;
112                    past_power_state = PowerState::Charging;
113                    info!("Plugged in")
114                }
115                // Noop
116                (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}