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.
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<u32> {
let mut hit = false;
let mut ret: Option<u32> = 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<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;
// 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<Child> {
pub fn exec_command(config: &Config, elapsed: u32) -> Option<Child> {
let mut args: Vec<String> = Vec::new();
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);
}
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) => {

View file

@ -79,7 +79,11 @@ impl Clock {
}
// 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.
if layout.force_redraw || self.elapsed % 3600 == 0 {
if self.elapsed >= 3600 {
@ -139,7 +143,13 @@ impl Clock {
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 {
write!(stdout,
"{}{}",
@ -179,8 +189,13 @@ impl Clock {
}
}
fn draw_colon<W: Write>(&self, stdout: &mut RawTerminal<W>, pos: &Position, plain: bool) {
let dot: char = if plain {'█'} else {'■'};
fn draw_colon<W: Write>(
&self,
stdout: &mut RawTerminal<W>,
pos: &Position,
plain: bool,
) {
let dot = if plain {'█'} else {'■'};
match self.color_index {
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
// terminal.
fn compute(&mut self, display_hours: bool) {

View file

@ -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<Vec<String>>,
quit: bool,
command: Option<Vec<String>>,
}
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<std::process::Child> = 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<String> {
let mut exec: Vec<String> = Vec::new();
// for process::Command::new().
fn parse_to_command(input: &str) -> Vec<String> {
let mut command: Vec<String> = 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<String> {
' ' 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<String> {
}
escaped = false;
}
exec.push(subs.clone());
exec
command.push(subs);
command.shrink_to_fit();
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(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<W: Write>(mut stdout: &mut RawTerminal<W>) {
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);
});
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<W: Write>(stdout: &mut RawTerminal<W>) {
}
// 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() {
write!(stdout,
"{}{}Add alarm: {}",
@ -505,8 +513,8 @@ fn draw_buffer<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, buffer: &
}
}
// Print error message.
fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &'static str) {
// Draw error message.
fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &str) {
write!(stdout,
"{}{}{}{}",
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);
}
}
}
}