diff --git a/src/alarm.rs b/src/alarm.rs index 1df3f59..d58edbf 100644 --- a/src/alarm.rs +++ b/src/alarm.rs @@ -79,7 +79,7 @@ impl AlarmRoster { // Parse string and add as alarm. pub fn add(&mut self, buffer: &String) - -> Result<(), &'static str> { + -> Result<(), &str> { let mut index = 0; let mut time: u32 = 0; @@ -111,8 +111,11 @@ impl AlarmRoster { if time == 0 { return Err("Evaluates to zero.") }; if time >= 24 * 60 * 60 { return Err("Values >24h not supported.") }; + let mut display = buffer.clone(); + display.shrink_to_fit(); + let alarm = Alarm { - display: buffer.clone(), + display, time, color_index: (self.list.len() % COLOR.len()), exceeded: false, @@ -151,9 +154,9 @@ impl AlarmRoster { pub fn check(&mut self, clock: &mut Clock, layout: &Layout, - countdown: &mut Countdown) -> bool { + countdown: &mut Countdown) -> Option { - let mut hit = false; + let mut ret: Option = None; let mut index = 0; for alarm in &mut self.list { @@ -161,7 +164,7 @@ impl AlarmRoster { if !alarm.exceeded { if alarm.time <= clock.elapsed { // Found alarm to raise. - hit = true; + ret = Some(alarm.time); alarm.exceeded = true; clock.color_index = Some(alarm.color_index); countdown.value = 0; @@ -191,7 +194,8 @@ impl AlarmRoster { line = layout.roster.line; col = layout.roster.col + 6; } else { - line = line.checked_sub(offset).unwrap_or(layout.roster.line); + line = line.checked_sub(offset) + .unwrap_or(layout.roster.line); } } countdown.position = Some(Position { col, line, }); @@ -201,11 +205,15 @@ impl AlarmRoster { } index += 1; } - hit // Return value. + ret // Return value. } // Draw alarm roster according to layout. - pub fn draw(&self, stdout: &mut RawTerminal, layout: &mut Layout) { + pub fn draw( + &self, + stdout: &mut RawTerminal, + layout: &mut Layout + ) { let mut index = 0; // Find first item to print in case we lack space to print them all. @@ -267,7 +275,7 @@ impl AlarmRoster { } // Execute the command given on the command line. -pub fn alarm_exec(config: &Config, elapsed: u32) -> Option { +pub fn exec_command(config: &Config, elapsed: u32) -> Option { let mut args: Vec = Vec::new(); let time: String; @@ -277,13 +285,14 @@ pub fn alarm_exec(config: &Config, elapsed: u32) -> Option { time = format!("{:02}:{:02}:{:02}", elapsed /3600, (elapsed / 60) % 60, elapsed % 60); } - if let Some(exec) = &config.alarm_exec { + if let Some(command) = &config.command { // Replace every occurrence of "{}". - for s in exec { + args.reserve_exact(command.len()); + for s in command { args.push(s.replace("{}", &time)); } - match Command::new(&exec[0]).args(&args[1..]) + match Command::new(&command[0]).args(&args[1..]) .stdout(Stdio::null()).stdin(Stdio::null()).spawn() { Ok(child) => return Some(child), Err(error) => { diff --git a/src/clock.rs b/src/clock.rs index 9d1e286..e6802b3 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -79,7 +79,11 @@ impl Clock { } // Draw clock according to layout. - pub fn draw(&mut self, mut stdout: &mut RawTerminal, layout: &Layout) { + pub fn draw( + &mut self, + mut stdout: &mut RawTerminal, + layout: &Layout, + ) { // Draw hours if necessary. if layout.force_redraw || self.elapsed % 3600 == 0 { if self.elapsed >= 3600 { @@ -139,7 +143,13 @@ impl Clock { layout.plain); } - fn draw_digit_pair(&self, stdout: &mut RawTerminal, value: u32, pos: &Position, plain: bool) { + fn draw_digit_pair( + &self, + stdout: &mut RawTerminal, + value: u32, + pos: &Position, + plain: bool, + ) { if let Some(c) = self.color_index { write!(stdout, "{}{}", @@ -179,8 +189,13 @@ impl Clock { } } - fn draw_colon(&self, stdout: &mut RawTerminal, pos: &Position, plain: bool) { - let dot: char = if plain {'█'} else {'■'}; + fn draw_colon( + &self, + stdout: &mut RawTerminal, + pos: &Position, + plain: bool, + ) { + let dot = if plain {'█'} else {'■'}; match self.color_index { Some(c) => { diff --git a/src/layout.rs b/src/layout.rs index e9bb1d6..7c5548d 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -66,6 +66,20 @@ impl Layout { } } + #[cfg(test)] + pub fn test_update( + &mut self, + hours: bool, + width: u16, + height: u16, + roster_width: u16, + ) { + self.width = width; + self.height = height; + self.roster_width = roster_width; + self.compute(hours); + } + // Compute the position of various elements based on the size of the // terminal. fn compute(&mut self, display_hours: bool) { diff --git a/src/main.rs b/src/main.rs index 6964d98..5858ca9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,18 +4,20 @@ mod alarm; mod clock; mod common; mod layout; +#[cfg(test)] +mod tests; use std::{time, thread, env}; use std::io::Write; use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; -use signal_hook::flag; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +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 clock::Clock; -use alarm::{Countdown, AlarmRoster, alarm_exec}; +use alarm::{Countdown, AlarmRoster, exec_command}; use layout::{Layout, Position}; @@ -28,8 +30,8 @@ PARAMETERS: [ALARM TIME] None or multiple alarm times (HH:MM:SS). OPTIONS: - -h, --help Show this help and exit. - -v, --version Show version information and exit. + -h, --help Show this help. + -v, --version Show version information. -e, --exec [COMMAND] Execute COMMAND on alarm. Every occurrence of {} will be replaced by the elapsed time in (HH:)MM:SS format. @@ -53,16 +55,16 @@ const SIGUSR2: usize = signal_hook::consts::SIGUSR2 as usize; pub struct Config { plain: bool, - auto_quit: bool, - alarm_exec: Option>, + quit: bool, + command: Option>, } fn main() { let mut config = Config { plain: false, - auto_quit: false, - alarm_exec: None, + quit: false, + command: None, }; let mut alarm_roster = AlarmRoster::new(); parse_args(&mut config, &mut alarm_roster); @@ -78,7 +80,7 @@ fn main() { let mut buffer = String::new(); let mut buffer_updated: bool = false; let mut countdown = Countdown::new(); - // Child process of alarm_exec(). + // Child process of exec_command(). let mut spawned: Option = None; // Initialise roster_width. @@ -86,7 +88,7 @@ fn main() { // Register signal handlers. let signal = Arc::new(AtomicUsize::new(0)); - register_signal_handlers(&signal, &layout); + register_signal_handlers(&signal, &layout.force_recalc); // Clear window and hide cursor. write!(stdout, @@ -249,22 +251,22 @@ fn main() { layout.update(clock.elapsed >= 3600, clock.elapsed == 3600); // Check for exceeded alarms. - if alarm_roster.check(&mut clock, &layout, &mut countdown) { + if let Some(time) = alarm_roster.check(&mut clock, &layout, &mut countdown) { // Write ASCII bell code. write!(stdout, "{}", 0x07 as char).unwrap(); layout.force_redraw = true; // Run command if configured. - if config.alarm_exec.is_some() { + if config.command.is_some() { if spawned.is_none() { - spawned = alarm_exec(&config, clock.elapsed); + spawned = exec_command(&config, time); } else { // The last command is still running. eprintln!("Not executing command, as its predecessor is still running"); } } // Quit if configured. - if config.auto_quit && !alarm_roster.active() { + if config.quit && !alarm_roster.active() { break; } } @@ -355,6 +357,7 @@ fn main() { } } +// Print usage information and exit. fn usage() { println!("{}", USAGE); std::process::exit(0); @@ -373,10 +376,10 @@ fn parse_args(config: &mut Config, alarm_roster: &mut AlarmRoster) { std::process::exit(0); }, "-p" | "--plain" => config.plain = true, - "-q" | "--quit" => config.auto_quit = true, + "-q" | "--quit" => config.quit = true, "-e" | "--exec" => { if let Some(e) = iter.next() { - config.alarm_exec = Some(input_to_exec(&e)); + config.command = Some(parse_to_command(&e)); } else { println!("Missing parameter to \"{}\".", arg); std::process::exit(1); @@ -401,9 +404,9 @@ fn parse_args(config: &mut Config, alarm_roster: &mut AlarmRoster) { } // Parse command line argument to --command into a vector of strings suitable -// for process::Command::spawn. -fn input_to_exec(input: &str) -> Vec { - let mut exec: Vec = Vec::new(); +// for process::Command::new(). +fn parse_to_command(input: &str) -> Vec { + let mut command: Vec = Vec::new(); let mut subs: String = String::new(); let mut quoted = false; let mut escaped = false; @@ -418,7 +421,7 @@ fn input_to_exec(input: &str) -> Vec { ' ' if escaped || quoted => { &subs.push(' '); }, ' ' => { if !&subs.is_empty() { - exec.push(subs.clone()); + command.push(subs.clone()); &subs.clear(); } }, @@ -430,25 +433,26 @@ fn input_to_exec(input: &str) -> Vec { } escaped = false; } - exec.push(subs.clone()); - - exec + command.push(subs); + command.shrink_to_fit(); + command } -fn register_signal_handlers(signal: &Arc, layout: &Layout) { - +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(&layout.force_recalc)).unwrap(); + flag::register(SIGWINCH as i32, Arc::clone(&recalc_flag)).unwrap(); } -// Suspend execution on SIGTSTP. +// Prepare to suspend execution. Called on SIGTSTP. fn suspend(mut stdout: &mut RawTerminal) { write!(stdout, "{}{}{}", @@ -462,7 +466,7 @@ fn suspend(mut stdout: &mut RawTerminal) { eprintln!("Failed to leave raw terminal mode prior to suspend: {}", error); }); - if let Err(error) = signal_hook::low_level::emulate_default_handler(SIGTSTP as i32) { + if let Err(error) = low_level::emulate_default_handler(SIGTSTP as i32) { eprintln!("Error raising SIGTSTP: {}", error); } @@ -487,7 +491,11 @@ fn restore_after_suspend(stdout: &mut RawTerminal) { } // Draw input buffer. -fn draw_buffer(stdout: &mut RawTerminal, layout: &Layout, buffer: &String) { +fn draw_buffer( + stdout: &mut RawTerminal, + layout: &Layout, + buffer: &String, +) { if !buffer.is_empty() { write!(stdout, "{}{}Add alarm: {}", @@ -505,8 +513,8 @@ fn draw_buffer(stdout: &mut RawTerminal, layout: &Layout, buffer: & } } -// Print error message. -fn error_msg(stdout: &mut RawTerminal, layout: &Layout, msg: &'static str) { +// Draw error message. +fn error_msg(stdout: &mut RawTerminal, layout: &Layout, msg: &str) { write!(stdout, "{}{}{}{}", cursor::Goto(layout.error.col, layout.error.line), diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..8645d2e --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,25 @@ +use crate::layout::Layout; +use crate::Config; + +fn default_config() -> Config { + Config { + plain: false, + quit: false, + command: None, + } +} + +// Test if layout computation works without panicking. +#[test] +fn layout_computation() { + let config = default_config(); + let mut layout = Layout::new(&config); + + for roster_width in &[0, 10, 20, 30, 40] { + for width in 0..256 { + for height in 0..128 { + layout.test_update(height & 1 == 0, width, height, *roster_width); + } + } + } +}