Added test for layout calculation.
This commit is contained in:
parent
642ebcf077
commit
320453474c
5 changed files with 120 additions and 49 deletions
33
src/alarm.rs
33
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<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) => {
|
||||
|
|
23
src/clock.rs
23
src/clock.rs
|
@ -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) => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
74
src/main.rs
74
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<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
25
src/tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue