diff --git a/Cargo.lock b/Cargo.lock index 45e6680..49213ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" [[package]] name = "redox_syscall" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" dependencies = [ "bitflags", ] diff --git a/src/alarm.rs b/src/alarm.rs index b380202..cfbb570 100644 --- a/src/alarm.rs +++ b/src/alarm.rs @@ -22,7 +22,6 @@ use crate::utils::*; use crate::Config; use std::io::BufRead; use std::io::Write; -use std::process::{Child, Command, Stdio}; use termion::raw::RawTerminal; use termion::{color, cursor, style}; use unicode_width::UnicodeWidthStr; @@ -439,41 +438,3 @@ impl AlarmRoster { Ok(()) } } - -// Execute the command given on the command line. -pub fn exec_command( - command: &Vec, - elapsed: u32, - label: &String -) -> Option { - let time = if elapsed < 3600 { - format!("{:02}:{:02}", elapsed / 60, elapsed % 60) - } else { - format!( - "{:02}:{:02}:{:02}", - elapsed / 3600, - (elapsed / 60) % 60, - elapsed % 60 - ) - }; - - let mut args: Vec = Vec::new(); - // Build vector of command line arguments. Replace every occurrence of - // "{t}" and "{l}". - for s in command.iter().skip(1) { - args.push(s.replace("{t}", &time).replace("{l}", &label)); - } - - match Command::new(&command[0]) - .args(args) - .stdout(Stdio::null()) - .stdin(Stdio::null()) - .spawn() - { - Ok(child) => Some(child), - Err(error) => { - eprintln!("Error: Could not execute command. ({})", error); - None - } - } -} diff --git a/src/cradle.rs b/src/cradle.rs new file mode 100644 index 0000000..6eb87ce --- /dev/null +++ b/src/cradle.rs @@ -0,0 +1,162 @@ +// Copyright 2021, Shy. +// +// This file is part of Kitchentimer. +// +// Kitchentimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Kitchentimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kitchentimer. If not, see . + +use std::process::{self, Command, Stdio}; + +// Manages spawned child processes. +pub struct Cradle { + commands: Vec>, + children: Vec, +} + +impl Drop for Cradle { + fn drop(&mut self) { + for child in self.children.iter_mut() { + eprint!( + "Waiting for spawned process (PID {}) to finish ...", + child.id() + ); + + match child.wait() { + Ok(status) if status.success() => eprintln!(" ok"), + Ok(status) if status.code().is_none() => eprintln!(" interrupted ({})", status), + Ok(status) => eprintln!(" ok ({})", status), + Err(error) => eprintln!(" failed ({})", error), + } + } + } +} + +impl Cradle { + pub fn new() -> Cradle { + Cradle { + commands: Vec::new(), + children: Vec::new(), + } + } + + pub fn add(&mut self, mut command: Vec) { + // Vector will never change from here on. + command.shrink_to_fit(); + self.commands.push(command); + self.children.reserve(self.commands.len()); + } + + pub fn run_all(&mut self, time: u32, label: &String) { + // Do nothing if there are still running child processes. + if !self.children.is_empty() { return; } + + let time = if time < 3600 { + format!("{:02}:{:02}", time / 60, time % 60) + } else { + format!( + "{:02}:{:02}:{:02}", + time / 3600, + (time / 60) % 60, + time % 60 + ) + }; + + for command in self.commands.iter() { + let mut args: Vec = Vec::new(); + // Build vector of command line arguments. Replace every occurrence of + // "{t}" and "{l}". + for s in command.iter().skip(1) { + args.push(s.replace("{t}", &time).replace("{l}", &label)); + } + + match Command::new(&command[0]) + .args(args) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .spawn() + { + Ok(child) => self.children.push(child), + Err(error) => eprintln!("Error: Could not execute command. ({})", error), + } + } + } + + pub fn tend(&mut self) { + while let Some(mut child) = self.children.pop() { + match child.try_wait() { + // Process exited successfully. + Ok(Some(status)) if status.success() => (), + // Abnormal exit. + Ok(Some(status)) => eprintln!("Spawned process terminated with non-zero exit status. ({})", status), + // Process is still running. Put back child and return. + // Leaving any other children unattended, which shouldn't be + // a problem, as we will not spawn any further commands, as + // long as self.children isn't empty. + Ok(None) => { + self.children.push(child); + break; + } + // Other error. + Err(error) => eprintln!("Error executing command. ({})", error), + } + } + } + + // Parse command line argument to --command into a vector of strings suitable + // for process::Command::new(). + pub fn parse(input: &str) -> Vec { + let mut command: Vec = Vec::new(); + let mut segment: String = String::new(); + let mut quoted = false; + let mut escaped = false; + + for c in input.chars() { + match c { + '\\' if !escaped => { + // Next char is escaped. (If not escaped itself.) + escaped = true; + continue; + } + // Keep spaces when escaped or quoted. + ' ' if escaped || quoted => { + &segment.push(' '); + } + // Otherwise end the current segment. + ' ' => { + if !&segment.is_empty() { + command.push(segment.clone()); + &segment.clear(); + } + } + // Quotation marks toggle quote. + '"' | '\'' if !escaped => quoted = !quoted, + // Carry everything else. Escape if found escaped. + _ => { + if escaped { + &segment.push('\\'); + } + &segment.push(c); + } + } + escaped = false; + } + command.push(segment); + // Vector will not change from here on. + for segment in command.iter_mut() { + segment.shrink_to_fit(); + } + command.shrink_to_fit(); + command + } +} + diff --git a/src/lib.rs b/src/lib.rs index ffefc77..95e51a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,20 +17,22 @@ extern crate signal_hook; extern crate termion; -pub mod alarm; +mod alarm; mod buffer; -pub mod clock; -pub mod consts; -pub mod layout; +mod clock; +mod consts; +mod cradle; +mod layout; #[cfg(test)] mod tests; -pub mod utils; +mod utils; pub use alarm::AlarmRoster; -use alarm::{exec_command, Countdown}; +use alarm::Countdown; use buffer::Buffer; use clock::{font, Clock}; pub use consts::ui::*; +use cradle::Cradle; use layout::Layout; use signal_hook::consts::signal::*; use signal_hook::iterator::Signals; @@ -43,9 +45,9 @@ use termion::raw::{IntoRawMode, RawTerminal}; use termion::{clear, cursor, style}; pub fn run( - config: Config, + mut config: Config, mut alarm_roster: AlarmRoster, -) -> Result, std::io::Error> { +) -> Result<(), std::io::Error> { let mut layout = Layout::new(); // Initialise roster_width. layout.set_roster_width(alarm_roster.width()); @@ -57,7 +59,6 @@ pub fn run( let mut input_keys = async_stdin.keys(); let stdout = std::io::stdout(); let mut stdout = stdout.lock().into_raw_mode()?; - let mut child: Option = None; let mut force_redraw = true; // Register signals. @@ -129,28 +130,8 @@ pub fn run( } // Check on last spawned child process prior to processing the - // alarm roster and possibly starting a new one. - if let Some(ref mut spawn) = child { - match spawn.try_wait() { - // Process exited successfully. - Ok(Some(status)) if status.success() => child = None, - // Abnormal exit. - Ok(Some(status)) => { - eprintln!( - "Spawned process terminated with non-zero exit status. ({})", - status - ); - child = None; - } - // Process is still running. - Ok(None) => (), - // Other error. - Err(error) => { - eprintln!("Error executing command. ({})", error); - child = None; - } - } - } + // alarm roster and possibly spawning a new set. + config.commands.tend(); // Check for exceeded alarms. if let Some(alarm) = @@ -163,17 +144,9 @@ pub fn run( // Write ASCII bell code. write!(stdout, "{}", 0x07 as char)?; - match config.command { - // Run command if configured and no command is running. - Some(ref command) if child.is_none() => { - child = exec_command(command, alarm.time, &alarm.label); - } - // Last command is still running. - Some(_) => { - eprintln!("Not executing command, as its predecessor is still running") - } - None => (), - } + // Run commands. + config.commands.run_all(alarm.time, &alarm.label); + // Quit if configured. if config.quit && alarm_roster.idle() { break; @@ -365,14 +338,14 @@ pub fn run( write!(stdout, "{}{}{}", clear::All, cursor::Restore, cursor::Show)?; stdout.flush()?; - Ok(child) + Ok(()) } pub struct Config { quit: bool, fancy: bool, font: &'static font::Font, - command: Option>, + commands: Cradle, } impl Config { @@ -385,7 +358,7 @@ impl Config { quit: false, fancy: false, font: &font::NORMAL, - command: None, + commands: Cradle::new(), }; let mut iter = args.skip(1); @@ -408,8 +381,9 @@ impl Config { } "-q" | "--quit" => config.quit = true, "-e" | "--exec" => { - if let Some(e) = iter.next() { - config.command = Some(Config::parse_to_command(&e)); + if let Some(cmd) = iter.next() { + //config.command = Some(Config::parse_to_command(&e)); + config.commands.add(Cradle::parse(&cmd)); } else { return Err(format!("Missing parameter to \"{}\".", arg)); } @@ -431,49 +405,6 @@ impl Config { } Ok(config) } - - // Parse command line argument to --command into a vector of strings suitable - // for process::Command::new(). - fn parse_to_command(input: &str) -> Vec { - let mut command: Vec = Vec::new(); - let mut segment: String = String::new(); - let mut quoted = false; - let mut escaped = false; - - for c in input.chars() { - match c { - '\\' if !escaped => { - // Next char is escaped. (If not escaped itself.) - escaped = true; - continue; - } - // Keep spaces when escaped or quoted. - ' ' if escaped || quoted => { - &segment.push(' '); - } - // Otherwise end the current segment. - ' ' => { - if !&segment.is_empty() { - command.push(segment.clone()); - &segment.clear(); - } - } - // Quotation marks toggle quote. - '"' | '\'' if !escaped => quoted = !quoted, - // Carry everything else. Escape if found escaped. - _ => { - if escaped { - &segment.push('\\'); - } - &segment.push(c); - } - } - escaped = false; - } - command.push(segment); - command.shrink_to_fit(); - command - } } // Prepare to suspend execution. Called on SIGTSTP. diff --git a/src/main.rs b/src/main.rs index 951a08a..298f691 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,27 +37,8 @@ fn main() { } // Run main loop. Returns spawned child process if any. - let child = match run(config, alarm_roster) { - Ok(child) => child, - Err(error) => { - eprintln!("Main loop exited with error: {}", error); - process::exit(1); - } - }; - - // Wait for remaining spawned process to exit. - if let Some(mut child) = child { - eprint!( - "Waiting for spawned process (PID {}) to finish ...", - child.id() - ); - - match child.wait() { - Ok(status) if status.success() => eprintln!(" ok"), - // Unix only. - Ok(status) if status.code().is_none() => eprintln!(" interrupted ({})", status), - Ok(status) => eprintln!(" ok ({})", status), - Err(error) => eprintln!(" failed ({})", error), - } + if let Err(error) = run(config, alarm_roster) { + eprintln!("Main loop exited with error: {}", error); + process::exit(1); } }