diff --git a/src/alarm.rs b/src/alarm.rs index 5311985..3acc57d 100644 --- a/src/alarm.rs +++ b/src/alarm.rs @@ -2,7 +2,9 @@ use std::io::Write; use std::process::{Command, Stdio, Child}; use termion::{color, cursor, style}; use termion::raw::RawTerminal; -use crate::{Clock, Config, Layout, Position}; +use crate::Config; +use crate::clock::Clock; +use crate::layout::{Layout, Position}; use crate::utils::*; use crate::consts::{COLOR, LABEL_SIZE_LIMIT}; diff --git a/src/clock.rs b/src/clock.rs index 75f7a6c..a15ae9a 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -3,8 +3,7 @@ use std::io::Write; use termion::{color, cursor}; use termion::raw::RawTerminal; use crate::consts::*; -use crate::Layout; -use crate::Position; +use crate::layout::{Layout, Position}; pub struct Clock { diff --git a/src/consts.rs b/src/consts.rs index 88b8440..b04413d 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -25,15 +25,6 @@ pub const MENUBAR_SHORT: &str = pub const MENUBAR_INS: &str = "Format: HH:MM:SS/LABEL [ENTER] Accept [ESC] Cancel [CTR-C] Quit"; -// Needed for signal_hook. -pub const SIGTSTP: usize = signal_hook::consts::SIGTSTP as usize; -pub const SIGWINCH: usize = signal_hook::consts::SIGWINCH as usize; -pub const SIGCONT: usize = signal_hook::consts::SIGCONT as usize; -pub const SIGTERM: usize = signal_hook::consts::SIGTERM as usize; -pub const SIGINT: usize = signal_hook::consts::SIGINT as usize; -pub const SIGUSR1: usize = signal_hook::consts::SIGUSR1 as usize; -pub const SIGUSR2: usize = signal_hook::consts::SIGUSR2 as usize; - pub const COLOR: [&dyn termion::color::Color; 6] = [ &termion::color::Cyan, &termion::color::Magenta, diff --git a/src/kitchentimer.rs b/src/lib.rs similarity index 71% rename from src/kitchentimer.rs rename to src/lib.rs index baf75b2..3b9cccd 100644 --- a/src/kitchentimer.rs +++ b/src/lib.rs @@ -1,23 +1,39 @@ extern crate termion; +pub mod alarm; +pub mod clock; +pub mod consts; +pub mod layout; +pub mod utils; +#[cfg(test)] +mod tests; -use std::{process, thread, time}; +use std::{env, process, thread, time}; use std::io::Write; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use signal_hook::low_level; +use signal_hook::{flag, low_level}; use termion::{clear, color, cursor, style}; use termion::raw::{IntoRawMode, RawTerminal}; use termion::event::Key; use termion::input::TermRead; //use termion::cursor::DetectCursorPos; -use crate::clock::Clock; -use crate::alarm::{Countdown, AlarmRoster, exec_command}; -use crate::layout::Layout; -use crate::consts::*; -use crate::utils::*; -use crate::Config; +use utils::*; +use clock::Clock; +use layout::Layout; +use alarm::{Countdown, exec_command}; +pub use alarm::AlarmRoster; +pub use consts::*; -pub fn kitchentimer( +const SIGTSTP: usize = signal_hook::consts::SIGTSTP as usize; +const SIGWINCH: usize = signal_hook::consts::SIGWINCH as usize; +const SIGCONT: usize = signal_hook::consts::SIGCONT as usize; +const SIGTERM: usize = signal_hook::consts::SIGTERM as usize; +const SIGINT: usize = signal_hook::consts::SIGINT as usize; +const SIGUSR1: usize = signal_hook::consts::SIGUSR1 as usize; +const SIGUSR2: usize = signal_hook::consts::SIGUSR2 as usize; + + +pub fn run( config: Config, mut alarm_roster: AlarmRoster, signal: Arc, @@ -34,30 +50,22 @@ pub fn kitchentimer( let mut buffer = String::new(); // State variables. + // Request redraw of input buffer. let mut update_buffer = false; + // Request redraw of menu. let mut update_menu = false; + // Are we in insert mode? let mut insert_mode = false; let async_stdin = termion::async_stdin(); let mut input_keys = async_stdin.keys(); let stdout = std::io::stdout(); - let mut stdout = stdout.lock().into_raw_mode() - .unwrap_or_else(|error| { - eprintln!("Error opening stdout: {}", error); - process::exit(1); - }); + let mut stdout = stdout.lock().into_raw_mode()?; // Clear window and hide cursor. - write!(stdout, - "{}{}", - clear::All, - cursor::Hide) - .unwrap_or_else(|error| { - eprintln!("Error writing to stdout: {}", error); - process::exit(1); - }); + write!(stdout, "{}{}", clear::All, cursor::Hide)?; - // Enter main loop. + // Main loop entry. loop { // Process received signals. match signal.swap(0, Ordering::Relaxed) { @@ -76,7 +84,7 @@ pub fn kitchentimer( // Continuing after SIGSTOP. SIGCONT => { // This is reached when the process was suspended by SIGSTOP. - restore_after_suspend(&mut stdout); + restore_after_suspend(&mut stdout)?; layout.force_redraw = true; }, // Exit main loop on SIGTERM and SIGINT. @@ -94,6 +102,134 @@ pub fn kitchentimer( _ => unreachable!(), } + // Update input buffer display, if requested. + if update_buffer { + draw_buffer(&mut stdout, &mut layout, &buffer)?; + stdout.flush()?; + update_buffer = false; + } + + // Update elapsed time. + let elapsed = if clock.paused { + clock.elapsed + } else { + // Should never overflow as we reestablish a new "start" + // instant every 24 hours. + clock.start.elapsed().as_secs() as u32 + }; + + // Conditional inner loop. Runs once every second or when explicitly + // requested. + if elapsed != clock.elapsed || layout.force_redraw { + // Update clock. Advance one day after 24 hours. + if elapsed < 24 * 60 * 60 { + clock.elapsed = elapsed; + } else { + clock.next_day(); + // "clock.elapsed" set by "clock.next_day()". + alarm_roster.reset_all(); + layout.force_recalc.store(true, Ordering::Relaxed); + } + + // Update window size information and calculate the clock position. + // Also enforce recalculation of layout if we start displaying + // hours. + layout.update(clock.elapsed >= 3600, clock.elapsed == 3600); + + // Check for exceeded alarms. + if let Some((time, label)) = alarm_roster.check(&mut clock, &layout, &mut countdown) { + // Write ASCII bell code. + write!(stdout, "{}", 0x07 as char)?; + layout.force_redraw = true; + + // Run command if configured. + if config.command.is_some() { + if spawned.is_none() { + *spawned = exec_command(&config, time, &label); + } else { + // The last command is still running. + eprintln!("Not executing command, as its predecessor is still running"); + } + } + // Quit if configured. + if config.quit && !alarm_roster.active() { + break; + } + } + + // Clear the window and redraw menu bar, alarm roster and buffer if + // requested. + if layout.force_redraw { + write!(stdout, "{}", clear::All)?; + + // Redraw list of alarms. + alarm_roster.draw(&mut stdout, &mut layout); + + // Redraw buffer. + draw_buffer(&mut stdout, &mut layout, &buffer)?; + + // Schedule menu redraw. + update_menu = true; + } + + if update_menu { + update_menu = false; + write!(stdout, + "{}{}{}{}", + cursor::Goto(1, 1), + style::Faint, + // Switch menu bars. Use a compressed version or none at + // all if necessary. + match insert_mode { + true if layout.can_hold(MENUBAR_INS) => MENUBAR_INS, + false if layout.can_hold(MENUBAR) => MENUBAR, + false if layout.can_hold(MENUBAR_SHORT) => MENUBAR_SHORT, + _ => "", + }, + style::Reset)?; + } + + clock.draw(&mut stdout, &layout); + + // Display countdown. + if countdown.value > 0 { + countdown.draw(&mut stdout); + } + + // Move cursor to buffer position. + if insert_mode { + write!( + stdout, + "{}", + cursor::Goto(layout.cursor.col, layout.cursor.line))?; + } + + // Check any spawned child process. + if let Some(ref mut child) = spawned { + match child.try_wait() { + // Process exited successfully. + Ok(Some(status)) if status.success() => *spawned = None, + // Abnormal exit. + Ok(Some(status)) => { + eprintln!("Spawned process terminated with non-zero exit status. ({})", status); + *spawned = None; + }, + // Process is still running. + Ok(None) => (), + // Other error. + Err(error) => { + eprintln!("Error executing command. ({})", error); + *spawned = None; + }, + } + } + + // End of conditional inner loop. + // Reset redraw_all and flush stdout. + layout.force_redraw = false; + stdout.flush()?; + } + // Process input. if let Some(key) = input_keys.next() { match key.expect("Error reading input") { @@ -197,131 +333,6 @@ pub fn kitchentimer( // Main loop delay. thread::sleep(time::Duration::from_millis(200)); } - - // Update input buffer display. - if update_buffer { - draw_buffer(&mut stdout, &mut layout, &buffer)?; - update_buffer = false; - stdout.flush()?; - } - - let elapsed = if clock.paused { - clock.elapsed - } else { - // Should never overflow as we reestablish a new "start" - // instant every 24 hours. - clock.start.elapsed().as_secs() as u32 - }; - - // Update window content if necessary. - if elapsed != clock.elapsed || layout.force_redraw { - // Update clock. Advance one day after 24 hours. - if elapsed < 24 * 60 * 60 { - clock.elapsed = elapsed; - } else { - clock.next_day(); - // "clock.elapsed" set by "clock.next_day()". - alarm_roster.reset_all(); - layout.force_recalc.store(true, Ordering::Relaxed); - } - - // Update window size information and calculate the clock position. - // Also enforce recalculation of layout if we start displaying - // hours. - layout.update(clock.elapsed >= 3600, clock.elapsed == 3600); - - // Check for exceeded alarms. - if let Some((time, label)) = alarm_roster.check(&mut clock, &layout, &mut countdown) { - // Write ASCII bell code. - write!(stdout, "{}", 0x07 as char)?; - layout.force_redraw = true; - - // Run command if configured. - if config.command.is_some() { - if spawned.is_none() { - *spawned = exec_command(&config, time, &label); - } else { - // The last command is still running. - eprintln!("Not executing command, as its predecessor is still running"); - } - } - // Quit if configured. - if config.quit && !alarm_roster.active() { - break; - } - } - - // Clear the window and redraw menu bar, alarm roster and buffer if - // requested. - if layout.force_redraw { - write!(stdout, "{}", clear::All)?; - - // Redraw list of alarms. - alarm_roster.draw(&mut stdout, &mut layout); - - // Redraw buffer. - draw_buffer(&mut stdout, &mut layout, &buffer)?; - - // Schedule menu redraw. - update_menu = true; - } - - if update_menu { - update_menu = false; - write!(stdout, - "{}{}{}{}", - cursor::Goto(1, 1), - style::Faint, - // Switch menu bars. Use a compressed version or none at - // all if necessary. - match insert_mode { - true if layout.can_hold(MENUBAR_INS) => MENUBAR_INS, - false if layout.can_hold(MENUBAR) => MENUBAR, - false if layout.can_hold(MENUBAR_SHORT) => MENUBAR_SHORT, - _ => "", - }, - style::Reset)?; - } - - clock.draw(&mut stdout, &layout); - - // Display countdown. - if countdown.value > 0 { - countdown.draw(&mut stdout); - } - - // Move cursor to buffer position. - if insert_mode { - write!( - stdout, - "{}", - cursor::Goto(layout.cursor.col, layout.cursor.line))?; - } - - // Check any spawned child process. - if let Some(ref mut child) = spawned { - match child.try_wait() { - // Process exited successfully. - Ok(Some(status)) if status.success() => *spawned = None, - // Abnormal exit. - Ok(Some(status)) => { - eprintln!("Spawned process terminated with non-zero exit status. ({})", status); - *spawned = None; - }, - // Process is still running. - Ok(None) => (), - // Other error. - Err(error) => { - eprintln!("Error executing command. ({})", error); - *spawned = None; - }, - } - } - - // Reset redraw_all and flush stdout. - layout.force_redraw = false; - stdout.flush()?; - } } // Main loop exited. Clear window and restore cursor. @@ -334,6 +345,111 @@ pub fn kitchentimer( Ok(()) } +pub struct Config { + plain: bool, + quit: bool, + command: Option>, +} + +impl Config { + // Parse command line arguments into "config". + pub fn new(args: env::Args, alarm_roster: &mut AlarmRoster) + -> Result + { + let mut config = Config { + plain: false, + quit: false, + command: None, + }; + let mut iter = args.skip(1); + + loop { + if let Some(arg) = iter.next() { + match arg.as_str() { + "-h" | "--help" => { + // Print usage information and exit + println!("{}", USAGE); + process::exit(0); + }, + "-v" | "--version" => { + println!("{} {}", NAME, VERSION); + process::exit(0); + }, + "-p" | "--plain" => config.plain = true, + "-q" | "--quit" => config.quit = true, + "-e" | "--exec" => { + if let Some(e) = iter.next() { + config.command = Some(Config::parse_to_command(&e)); + } else { + return Err(format!("Missing parameter to \"{}\".", arg)); + } + }, + any if any.starts_with('-') => { + // Unrecognized flag. + return Err(format!("Unrecognized option: \"{}\"\nUse \"-h\" or \"--help\" for a list of valid command line options.", any)); + }, + any => { + // Alarm to add. + if let Err(error) = alarm_roster.add(&String::from(any)) { + return Err(format!("Error adding \"{}\" as alarm. ({})", any, error)); + } + }, + } + } else { break; } // All command line parameters processed. + } + 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 buffer: String = String::new(); + let mut quoted = false; + let mut escaped = false; + + for byte in input.chars() { + match byte { + '\\' if !escaped => { + // Next char is escaped. + escaped = true; + continue; + }, + ' ' if escaped || quoted => { &buffer.push(' '); }, + ' ' => { + if !&buffer.is_empty() { + command.push(buffer.clone()); + &buffer.clear(); + } + }, + '"' | '\'' if !escaped => quoted = !quoted, + _ => { + if escaped { &buffer.push('\\'); } + &buffer.push(byte); + }, + } + escaped = false; + } + command.push(buffer); + command.shrink_to_fit(); + command + } +} + +pub fn register_signals( + signal: &Arc, + recalc_flag: &Arc, +) { + flag::register_usize(SIGTSTP as i32, Arc::clone(&signal), SIGTSTP).unwrap(); + flag::register_usize(SIGCONT as i32, Arc::clone(&signal), SIGCONT).unwrap(); + flag::register_usize(SIGTERM as i32, Arc::clone(&signal), SIGTERM).unwrap(); + flag::register_usize(SIGINT as i32, Arc::clone(&signal), SIGINT).unwrap(); + flag::register_usize(SIGUSR1 as i32, Arc::clone(&signal), SIGUSR1).unwrap(); + flag::register_usize(SIGUSR2 as i32, Arc::clone(&signal), SIGUSR2).unwrap(); + // SIGWINCH sets "force_recalc" directly. + flag::register(SIGWINCH as i32, Arc::clone(&recalc_flag)).unwrap(); +} + // Draw input buffer. fn draw_buffer( stdout: &mut RawTerminal, @@ -399,12 +515,13 @@ fn suspend(mut stdout: &mut RawTerminal) eprintln!("Error raising SIGTSTP: {}", error); } - restore_after_suspend(&mut stdout); - Ok(()) + restore_after_suspend(&mut stdout) } // Set up terminal after SIGTSTP or SIGSTOP. -fn restore_after_suspend(stdout: &mut RawTerminal) { +fn restore_after_suspend(stdout: &mut RawTerminal) + -> Result<(), std::io::Error> +{ stdout.activate_raw_mode() .unwrap_or_else(|error| { eprintln!("Failed to re-enter raw terminal mode after suspend: {}", error); @@ -413,10 +530,7 @@ fn restore_after_suspend(stdout: &mut RawTerminal) { write!(stdout, "{}{}", clear::All, - cursor::Hide) - .unwrap_or_else(|error| { - eprintln!("Error writing to stdout: {}", error); - process::exit(1); - }); + cursor::Hide)?; + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 4be89ad..85b934b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,64 +1,38 @@ extern crate signal_hook; -mod alarm; -mod clock; -mod consts; -mod kitchentimer; -mod layout; -mod utils; -#[cfg(test)] -mod tests; use std::{env, process}; use std::io::Write; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicUsize}; -use signal_hook::flag; -use clock::Clock; -use alarm::AlarmRoster; -use layout::{Layout, Position}; -use consts::*; -use kitchentimer::kitchentimer; - - -pub struct Config { - plain: bool, - quit: bool, - command: Option>, -} +use kitchentimer::{Config, AlarmRoster, register_signals, run}; fn main() { - let mut config = Config { - plain: false, - quit: false, - command: None, - }; + let args = env::args(); let mut alarm_roster = AlarmRoster::new(); // Parse command line arguments into config and alarm roster. - parse_args(&mut config, &mut alarm_roster); + let config = Config::new(args, &mut alarm_roster) + .unwrap_or_else(|e| { + println!("{}", e); + process::exit(1); + }); // Register signal handlers. let signal = Arc::new(AtomicUsize::new(0)); let sigwinch = Arc::new(AtomicBool::new(true)); - register_signal_handlers(&signal, &sigwinch); - // Spawned child process if any. + register_signals(&signal, &sigwinch); + // Holds spawned child process if any. let mut spawned: Option = None; - // Runs main loop. - match kitchentimer( - config, - alarm_roster, - signal, - sigwinch, - &mut spawned, - ) { - Ok(_) => (), - Err(e) => eprintln!("Main loop exited with error: {}", e), + // Run main loop. + if let Err(e) = run(config, alarm_roster, signal, sigwinch, &mut spawned) { + println!("Main loop exited with error: {}", e); + process::exit(1); } // Wait for remaining spawned processes to exit. if let Some(ref mut child) = spawned { - print!("Waiting for spawned processes (PID {}) to exit ...", child.id()); + print!("Waiting for spawned process (PID {}) to exit ...", child.id()); std::io::stdout().flush().unwrap(); match child.wait() { @@ -68,98 +42,4 @@ fn main() { } } -// Print usage information and exit. -fn usage() { - println!("{}", USAGE); - process::exit(0); -} - -// Parse command line arguments into "config". -fn parse_args(config: &mut Config, alarm_roster: &mut AlarmRoster) { - let mut iter = env::args().skip(1); - - loop { - if let Some(arg) = iter.next() { - match arg.as_str() { - "-h" | "--help" => usage(), - "-v" | "--version" => { - println!("{} {}", NAME, VERSION); - process::exit(0); - }, - "-p" | "--plain" => config.plain = true, - "-q" | "--quit" => config.quit = true, - "-e" | "--exec" => { - if let Some(e) = iter.next() { - config.command = Some(parse_to_command(&e)); - } else { - println!("Missing parameter to \"{}\".", arg); - process::exit(1); - } - }, - any if any.starts_with('-') => { - // Unrecognized flag. - println!("Unrecognized option: \"{}\"", any); - println!("Use \"-h\" or \"--help\" for a list of valid command line options"); - process::exit(1); - }, - any => { - // Alarm to add. - if let Err(error) = alarm_roster.add(&String::from(any)) { - println!("Error adding \"{}\" as alarm. ({})", any, error); - process::exit(1); - } - }, - } - } else { break; } // All command line parameters processed. - } -} - -// 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 buffer: String = String::new(); - let mut quoted = false; - let mut escaped = false; - - for byte in input.chars() { - match byte { - '\\' if !escaped => { - // Next char is escaped. - escaped = true; - continue; - }, - ' ' if escaped || quoted => { &buffer.push(' '); }, - ' ' => { - if !&buffer.is_empty() { - command.push(buffer.clone()); - &buffer.clear(); - } - }, - '"' | '\'' if !escaped => quoted = !quoted, - _ => { - if escaped { &buffer.push('\\'); } - &buffer.push(byte); - }, - } - escaped = false; - } - command.push(buffer); - command.shrink_to_fit(); - command -} - -fn register_signal_handlers( - signal: &Arc, - recalc_flag: &Arc, -) { - flag::register_usize(SIGTSTP as i32, Arc::clone(&signal), SIGTSTP).unwrap(); - flag::register_usize(SIGCONT as i32, Arc::clone(&signal), SIGCONT).unwrap(); - flag::register_usize(SIGTERM as i32, Arc::clone(&signal), SIGTERM).unwrap(); - flag::register_usize(SIGINT as i32, Arc::clone(&signal), SIGINT).unwrap(); - flag::register_usize(SIGUSR1 as i32, Arc::clone(&signal), SIGUSR1).unwrap(); - flag::register_usize(SIGUSR2 as i32, Arc::clone(&signal), SIGUSR2).unwrap(); - // SIGWINCH sets "force_recalc" directly. - flag::register(SIGWINCH as i32, Arc::clone(&recalc_flag)).unwrap(); -}