Added test for layout calculation.

This commit is contained in:
shy 2021-04-09 07:48:27 +02:00
parent 642ebcf077
commit 320453474c
5 changed files with 120 additions and 49 deletions

View file

@ -79,7 +79,7 @@ impl AlarmRoster {
// Parse string and add as alarm. // Parse string and add as alarm.
pub fn add(&mut self, buffer: &String) pub fn add(&mut self, buffer: &String)
-> Result<(), &'static str> { -> Result<(), &str> {
let mut index = 0; let mut index = 0;
let mut time: u32 = 0; let mut time: u32 = 0;
@ -111,8 +111,11 @@ impl AlarmRoster {
if time == 0 { return Err("Evaluates to zero.") }; if time == 0 { return Err("Evaluates to zero.") };
if time >= 24 * 60 * 60 { return Err("Values >24h not supported.") }; if time >= 24 * 60 * 60 { return Err("Values >24h not supported.") };
let mut display = buffer.clone();
display.shrink_to_fit();
let alarm = Alarm { let alarm = Alarm {
display: buffer.clone(), display,
time, time,
color_index: (self.list.len() % COLOR.len()), color_index: (self.list.len() % COLOR.len()),
exceeded: false, exceeded: false,
@ -151,9 +154,9 @@ impl AlarmRoster {
pub fn check(&mut self, pub fn check(&mut self,
clock: &mut Clock, clock: &mut Clock,
layout: &Layout, layout: &Layout,
countdown: &mut Countdown) -> bool { countdown: &mut Countdown) -> Option<u32> {
let mut hit = false; let mut ret: Option<u32> = None;
let mut index = 0; let mut index = 0;
for alarm in &mut self.list { for alarm in &mut self.list {
@ -161,7 +164,7 @@ impl AlarmRoster {
if !alarm.exceeded { if !alarm.exceeded {
if alarm.time <= clock.elapsed { if alarm.time <= clock.elapsed {
// Found alarm to raise. // Found alarm to raise.
hit = true; ret = Some(alarm.time);
alarm.exceeded = true; alarm.exceeded = true;
clock.color_index = Some(alarm.color_index); clock.color_index = Some(alarm.color_index);
countdown.value = 0; countdown.value = 0;
@ -191,7 +194,8 @@ impl AlarmRoster {
line = layout.roster.line; line = layout.roster.line;
col = layout.roster.col + 6; col = layout.roster.col + 6;
} else { } 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, }); countdown.position = Some(Position { col, line, });
@ -201,11 +205,15 @@ impl AlarmRoster {
} }
index += 1; index += 1;
} }
hit // Return value. ret // Return value.
} }
// Draw alarm roster according to layout. // Draw alarm roster according to layout.
pub fn draw<W: Write>(&self, stdout: &mut RawTerminal<W>, layout: &mut Layout) { pub fn draw<W: Write>(
&self,
stdout: &mut RawTerminal<W>,
layout: &mut Layout
) {
let mut index = 0; let mut index = 0;
// Find first item to print in case we lack space to print them all. // 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. // Execute the command given on the command line.
pub fn alarm_exec(config: &Config, elapsed: u32) -> Option<Child> { pub fn exec_command(config: &Config, elapsed: u32) -> Option<Child> {
let mut args: Vec<String> = Vec::new(); let mut args: Vec<String> = Vec::new();
let time: String; let time: String;
@ -277,13 +285,14 @@ pub fn alarm_exec(config: &Config, elapsed: u32) -> Option<Child> {
time = format!("{:02}:{:02}:{:02}", elapsed /3600, (elapsed / 60) % 60, elapsed % 60); 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 "{}". // Replace every occurrence of "{}".
for s in exec { args.reserve_exact(command.len());
for s in command {
args.push(s.replace("{}", &time)); 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() { .stdout(Stdio::null()).stdin(Stdio::null()).spawn() {
Ok(child) => return Some(child), Ok(child) => return Some(child),
Err(error) => { Err(error) => {

View file

@ -79,7 +79,11 @@ impl Clock {
} }
// Draw clock according to layout. // Draw clock according to layout.
pub fn draw<W: Write>(&mut self, mut stdout: &mut RawTerminal<W>, layout: &Layout) { pub fn draw<W: Write>(
&mut self,
mut stdout: &mut RawTerminal<W>,
layout: &Layout,
) {
// Draw hours if necessary. // Draw hours if necessary.
if layout.force_redraw || self.elapsed % 3600 == 0 { if layout.force_redraw || self.elapsed % 3600 == 0 {
if self.elapsed >= 3600 { if self.elapsed >= 3600 {
@ -139,7 +143,13 @@ impl Clock {
layout.plain); layout.plain);
} }
fn draw_digit_pair<W: Write>(&self, stdout: &mut RawTerminal<W>, value: u32, pos: &Position, plain: bool) { fn draw_digit_pair<W: Write>(
&self,
stdout: &mut RawTerminal<W>,
value: u32,
pos: &Position,
plain: bool,
) {
if let Some(c) = self.color_index { if let Some(c) = self.color_index {
write!(stdout, write!(stdout,
"{}{}", "{}{}",
@ -179,8 +189,13 @@ impl Clock {
} }
} }
fn draw_colon<W: Write>(&self, stdout: &mut RawTerminal<W>, pos: &Position, plain: bool) { fn draw_colon<W: Write>(
let dot: char = if plain {'█'} else {'■'}; &self,
stdout: &mut RawTerminal<W>,
pos: &Position,
plain: bool,
) {
let dot = if plain {'█'} else {'■'};
match self.color_index { match self.color_index {
Some(c) => { Some(c) => {

View file

@ -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 // Compute the position of various elements based on the size of the
// terminal. // terminal.
fn compute(&mut self, display_hours: bool) { fn compute(&mut self, display_hours: bool) {

View file

@ -4,18 +4,20 @@ mod alarm;
mod clock; mod clock;
mod common; mod common;
mod layout; mod layout;
#[cfg(test)]
mod tests;
use std::{time, thread, env}; use std::{time, thread, env};
use std::io::Write; use std::io::Write;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use signal_hook::flag; use signal_hook::{flag, low_level};
use termion::{clear, color, cursor, style}; use termion::{clear, color, cursor, style};
use termion::raw::{IntoRawMode, RawTerminal}; use termion::raw::{IntoRawMode, RawTerminal};
use termion::event::Key; use termion::event::Key;
use termion::input::TermRead; use termion::input::TermRead;
use clock::Clock; use clock::Clock;
use alarm::{Countdown, AlarmRoster, alarm_exec}; use alarm::{Countdown, AlarmRoster, exec_command};
use layout::{Layout, Position}; use layout::{Layout, Position};
@ -28,8 +30,8 @@ PARAMETERS:
[ALARM TIME] None or multiple alarm times (HH:MM:SS). [ALARM TIME] None or multiple alarm times (HH:MM:SS).
OPTIONS: OPTIONS:
-h, --help Show this help and exit. -h, --help Show this help.
-v, --version Show version information and exit. -v, --version Show version information.
-e, --exec [COMMAND] Execute COMMAND on alarm. Every occurrence of {} -e, --exec [COMMAND] Execute COMMAND on alarm. Every occurrence of {}
will be replaced by the elapsed time in (HH:)MM:SS will be replaced by the elapsed time in (HH:)MM:SS
format. format.
@ -53,16 +55,16 @@ const SIGUSR2: usize = signal_hook::consts::SIGUSR2 as usize;
pub struct Config { pub struct Config {
plain: bool, plain: bool,
auto_quit: bool, quit: bool,
alarm_exec: Option<Vec<String>>, command: Option<Vec<String>>,
} }
fn main() { fn main() {
let mut config = Config { let mut config = Config {
plain: false, plain: false,
auto_quit: false, quit: false,
alarm_exec: None, command: None,
}; };
let mut alarm_roster = AlarmRoster::new(); let mut alarm_roster = AlarmRoster::new();
parse_args(&mut config, &mut alarm_roster); parse_args(&mut config, &mut alarm_roster);
@ -78,7 +80,7 @@ fn main() {
let mut buffer = String::new(); let mut buffer = String::new();
let mut buffer_updated: bool = false; let mut buffer_updated: bool = false;
let mut countdown = Countdown::new(); let mut countdown = Countdown::new();
// Child process of alarm_exec(). // Child process of exec_command().
let mut spawned: Option<std::process::Child> = None; let mut spawned: Option<std::process::Child> = None;
// Initialise roster_width. // Initialise roster_width.
@ -86,7 +88,7 @@ fn main() {
// Register signal handlers. // Register signal handlers.
let signal = Arc::new(AtomicUsize::new(0)); let signal = Arc::new(AtomicUsize::new(0));
register_signal_handlers(&signal, &layout); register_signal_handlers(&signal, &layout.force_recalc);
// Clear window and hide cursor. // Clear window and hide cursor.
write!(stdout, write!(stdout,
@ -249,22 +251,22 @@ fn main() {
layout.update(clock.elapsed >= 3600, clock.elapsed == 3600); layout.update(clock.elapsed >= 3600, clock.elapsed == 3600);
// Check for exceeded alarms. // 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 ASCII bell code.
write!(stdout, "{}", 0x07 as char).unwrap(); write!(stdout, "{}", 0x07 as char).unwrap();
layout.force_redraw = true; layout.force_redraw = true;
// Run command if configured. // Run command if configured.
if config.alarm_exec.is_some() { if config.command.is_some() {
if spawned.is_none() { if spawned.is_none() {
spawned = alarm_exec(&config, clock.elapsed); spawned = exec_command(&config, time);
} else { } else {
// The last command is still running. // The last command is still running.
eprintln!("Not executing command, as its predecessor is still running"); eprintln!("Not executing command, as its predecessor is still running");
} }
} }
// Quit if configured. // Quit if configured.
if config.auto_quit && !alarm_roster.active() { if config.quit && !alarm_roster.active() {
break; break;
} }
} }
@ -355,6 +357,7 @@ fn main() {
} }
} }
// Print usage information and exit.
fn usage() { fn usage() {
println!("{}", USAGE); println!("{}", USAGE);
std::process::exit(0); std::process::exit(0);
@ -373,10 +376,10 @@ fn parse_args(config: &mut Config, alarm_roster: &mut AlarmRoster) {
std::process::exit(0); std::process::exit(0);
}, },
"-p" | "--plain" => config.plain = true, "-p" | "--plain" => config.plain = true,
"-q" | "--quit" => config.auto_quit = true, "-q" | "--quit" => config.quit = true,
"-e" | "--exec" => { "-e" | "--exec" => {
if let Some(e) = iter.next() { if let Some(e) = iter.next() {
config.alarm_exec = Some(input_to_exec(&e)); config.command = Some(parse_to_command(&e));
} else { } else {
println!("Missing parameter to \"{}\".", arg); println!("Missing parameter to \"{}\".", arg);
std::process::exit(1); 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 // Parse command line argument to --command into a vector of strings suitable
// for process::Command::spawn. // for process::Command::new().
fn input_to_exec(input: &str) -> Vec<String> { fn parse_to_command(input: &str) -> Vec<String> {
let mut exec: Vec<String> = Vec::new(); let mut command: Vec<String> = Vec::new();
let mut subs: String = String::new(); let mut subs: String = String::new();
let mut quoted = false; let mut quoted = false;
let mut escaped = false; let mut escaped = false;
@ -418,7 +421,7 @@ fn input_to_exec(input: &str) -> Vec<String> {
' ' if escaped || quoted => { &subs.push(' '); }, ' ' if escaped || quoted => { &subs.push(' '); },
' ' => { ' ' => {
if !&subs.is_empty() { if !&subs.is_empty() {
exec.push(subs.clone()); command.push(subs.clone());
&subs.clear(); &subs.clear();
} }
}, },
@ -430,25 +433,26 @@ fn input_to_exec(input: &str) -> Vec<String> {
} }
escaped = false; escaped = false;
} }
exec.push(subs.clone()); command.push(subs);
command.shrink_to_fit();
exec command
} }
fn register_signal_handlers(signal: &Arc<AtomicUsize>, layout: &Layout) { fn register_signal_handlers(
signal: &Arc<AtomicUsize>,
recalc_flag: &Arc<AtomicBool>,
) {
flag::register_usize(SIGTSTP as i32, Arc::clone(&signal), SIGTSTP).unwrap(); 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(SIGCONT as i32, Arc::clone(&signal), SIGCONT).unwrap();
flag::register_usize(SIGTERM as i32, Arc::clone(&signal), SIGTERM).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(SIGINT as i32, Arc::clone(&signal), SIGINT).unwrap();
flag::register_usize(SIGUSR1 as i32, Arc::clone(&signal), SIGUSR1).unwrap(); flag::register_usize(SIGUSR1 as i32, Arc::clone(&signal), SIGUSR1).unwrap();
flag::register_usize(SIGUSR2 as i32, Arc::clone(&signal), SIGUSR2).unwrap(); flag::register_usize(SIGUSR2 as i32, Arc::clone(&signal), SIGUSR2).unwrap();
// SIGWINCH sets "force_recalc" directly. // 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<W: Write>(mut stdout: &mut RawTerminal<W>) { fn suspend<W: Write>(mut stdout: &mut RawTerminal<W>) {
write!(stdout, write!(stdout,
"{}{}{}", "{}{}{}",
@ -462,7 +466,7 @@ fn suspend<W: Write>(mut stdout: &mut RawTerminal<W>) {
eprintln!("Failed to leave raw terminal mode prior to suspend: {}", error); 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); eprintln!("Error raising SIGTSTP: {}", error);
} }
@ -487,7 +491,11 @@ fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>) {
} }
// Draw input buffer. // Draw input buffer.
fn draw_buffer<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, buffer: &String) { fn draw_buffer<W: Write>(
stdout: &mut RawTerminal<W>,
layout: &Layout,
buffer: &String,
) {
if !buffer.is_empty() { if !buffer.is_empty() {
write!(stdout, write!(stdout,
"{}{}Add alarm: {}", "{}{}Add alarm: {}",
@ -505,8 +513,8 @@ fn draw_buffer<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, buffer: &
} }
} }
// Print error message. // Draw error message.
fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &'static str) { fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &str) {
write!(stdout, write!(stdout,
"{}{}{}{}", "{}{}{}{}",
cursor::Goto(layout.error.col, layout.error.line), cursor::Goto(layout.error.col, layout.error.line),

25
src/tests.rs Normal file
View file

@ -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);
}
}
}
}