diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/error.rs | 27 | ||||
-rw-r--r-- | src/main.rs | 184 |
2 files changed, 211 insertions, 0 deletions
diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..f25bc5d --- /dev/null +++ b/src/error.rs @@ -0,0 +1,27 @@ +use std::io; + +pub trait IntoIOError { + fn ioerr(self) -> io::Error; + fn perror(self, parent: io::Error) -> io::Error; +} + +// This impl bound is taken directly from the io::Error::new function. +impl<E: Into<Box<dyn std::error::Error + Send + Sync>>> IntoIOError for E { + fn ioerr(self) -> io::Error { + io::Error::new(io::ErrorKind::Other, self) + } + + fn perror(self, parent: io::Error) -> io::Error { + io::Error::new(parent.kind(), format!("{}: {}", self.into(), parent)) + } +} + +pub trait IntoIOResult<T> { + fn iores(self) -> io::Result<T>; +} + +impl<T, E: IntoIOError> IntoIOResult<T> for Result<T, E> { + fn iores(self) -> io::Result<T> { + self.map_err(|e| e.ioerr()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7eb4a41 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,184 @@ +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::{Command, Child, Stdio}; +use std::sync::{Mutex, Arc}; +use systray; +use dialog::{self, DialogBox}; +use tempfile::tempdir; +use crate::error::*; + +mod error; + +const ICON_EXCLAM_PNG: &[u8] = include_bytes!("../resources/exclam.png"); +const ICON_OK_PNG: &[u8] = include_bytes!("../resources/ok.png"); + +fn join(sep: &str, v: &[String]) -> String { + let mut res = String::new(); + let mut first = true; + for item in v { + if first { first = false; } + else { res.push_str(sep); } + res.push_str(item); + } + res +} + +struct State { + // Child is 'cat >/dev/null', with stdin piped. + // Close stdin to terminate. + inhibitor: Option<(Child, Vec<String>)>, + ok_path: String, + exclam_path: String, +} + +impl State { + fn new(ok_path: &str, exclam_path: &str) -> State { + State { + inhibitor: None, + ok_path: ok_path.to_string(), + exclam_path: exclam_path.to_string(), + } + } + + fn what_inhibited(&self) -> Option<&[String]> { + match &self.inhibitor { + Some((_, v)) => Some(v), + None => None, + } + } + + fn inhibit(&mut self, what: &[String]) -> io::Result<()> { + self.uninhibit()?; + + let mut whatarg = "--what=".to_string(); + whatarg.push_str(&join(":", what)); + + let child = Command::new("systemd-inhibit") + .arg(whatarg) + .arg("--who=Tom's acpi-inhibitor") + .arg("--why=User request") + .arg("cat") + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .spawn()?; + self.inhibitor = Some((child, what.to_vec())); + + Ok(()) + } + + fn uninhibit(&mut self) -> io::Result<()> { + if let Some((ch, _)) = &mut self.inhibitor { + // This automatically closes the child's stdin + ch.wait()?; + self.inhibitor = None; + } + + Ok(()) + } + + fn poll(&mut self) -> io::Result<()> { + if let Some((ch, _)) = &mut self.inhibitor { + if ch.try_wait()?.is_some() { + self.inhibitor = None; + } + } + + Ok(()) + } +} + +fn show_dialog(title: &str, body: &str) -> io::Result<()> { + dialog::Message::new(body).title(title).show() + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{}", e))) +} + +fn status_message(state: &mut State) -> io::Result<String> { + state.poll()?; + + let mut msg; + match state.what_inhibited() { + Some(what) => { + msg = "Currently inhibiting: ".to_string(); + msg.push_str(&join(", ", what)); + } + None => { + msg = "Currently functioning normally (not inhibited)".to_string(); + } + } + + Ok(msg) +} + +fn update_icon(app: &mut systray::Application, state: &mut State) -> io::Result<()> { + match state.what_inhibited() { + Some(_) => app.set_icon_from_file(&state.exclam_path).iores(), + None => app.set_icon_from_file(&state.ok_path).iores(), + } +} + +fn materialise_icon(dir: &Path, fname: &str, bytes: &[u8]) -> io::Result<PathBuf> { + let mut pathbuf = dir.to_path_buf(); + pathbuf.push(fname); + fs::write(&pathbuf, bytes)?; + Ok(pathbuf) +} + +fn main() -> io::Result<()> { + let resources_dir = tempdir()?; + let resources_path = resources_dir.path(); + + let icon_ok_path = materialise_icon(&resources_path, "ok.png", ICON_OK_PNG)?; + let icon_exclam_path = materialise_icon(&resources_path, "exclam.png", ICON_EXCLAM_PNG)?; + + let mut app = systray::Application::new().iores()?; + + let state = Arc::new(Mutex::new(State::new( + icon_ok_path.to_str().unwrap(), + icon_exclam_path.to_str().unwrap() + ))); + + app.set_icon_from_file(icon_ok_path.to_str().unwrap()).iores()?; + + { + let state = state.clone(); + app.add_menu_item("Status", move |window| { + let mut state = state.as_ref().lock().unwrap(); + let msg = status_message(&mut state)?; + update_icon(window, &mut state)?; + show_dialog("acpi-inhibitor status", &msg) + }).iores()?; + } + + app.add_menu_separator().iores()?; + + macro_rules! switcher_menu { + ($label:expr, $keys:expr) => {{ + let state = state.clone(); + app.add_menu_item($label, move |mut window| { + let mut state = state.as_ref().lock().unwrap(); + let keys = $keys; + if state.what_inhibited() == Some(keys) { + state.uninhibit()?; + } else { + state.inhibit(keys)?; + } + update_icon(&mut window, &mut state)?; + Ok::<_, io::Error>(()) + }).iores()?; + }} + } + + switcher_menu!("Shutdown/sleep/idle", &["shutdown".to_string(), "sleep".to_string(), "idle".to_string()]); + + switcher_menu!("Lid switch", &["handle-lid-switch".to_string()]); + + app.add_menu_separator().iores()?; + + app.add_menu_item("Quit", |window| { + window.quit(); + Ok::<_, systray::Error>(()) + }).iores()?; + + app.wait_for_message().iores() +} |