Extensive refactoring.

This commit is contained in:
shy 2021-04-10 20:45:26 +02:00
parent f5490eb96b
commit d95f2faaef
5 changed files with 289 additions and 303 deletions

View file

@ -2,7 +2,9 @@ use std::io::Write;
use std::process::{Command, Stdio, Child}; use std::process::{Command, Stdio, Child};
use termion::{color, cursor, style}; use termion::{color, cursor, style};
use termion::raw::RawTerminal; 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::utils::*;
use crate::consts::{COLOR, LABEL_SIZE_LIMIT}; use crate::consts::{COLOR, LABEL_SIZE_LIMIT};

View file

@ -3,8 +3,7 @@ use std::io::Write;
use termion::{color, cursor}; use termion::{color, cursor};
use termion::raw::RawTerminal; use termion::raw::RawTerminal;
use crate::consts::*; use crate::consts::*;
use crate::Layout; use crate::layout::{Layout, Position};
use crate::Position;
pub struct Clock { pub struct Clock {

View file

@ -25,15 +25,6 @@ pub const MENUBAR_SHORT: &str =
pub const MENUBAR_INS: &str = pub const MENUBAR_INS: &str =
"Format: HH:MM:SS/LABEL [ENTER] Accept [ESC] Cancel [CTR-C] Quit"; "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] = [ pub const COLOR: [&dyn termion::color::Color; 6] = [
&termion::color::Cyan, &termion::color::Cyan,
&termion::color::Magenta, &termion::color::Magenta,

View file

@ -1,23 +1,39 @@
extern crate termion; 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::io::Write;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 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::{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 termion::cursor::DetectCursorPos; //use termion::cursor::DetectCursorPos;
use crate::clock::Clock; use utils::*;
use crate::alarm::{Countdown, AlarmRoster, exec_command}; use clock::Clock;
use crate::layout::Layout; use layout::Layout;
use crate::consts::*; use alarm::{Countdown, exec_command};
use crate::utils::*; pub use alarm::AlarmRoster;
use crate::Config; 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, config: Config,
mut alarm_roster: AlarmRoster, mut alarm_roster: AlarmRoster,
signal: Arc<AtomicUsize>, signal: Arc<AtomicUsize>,
@ -34,30 +50,22 @@ pub fn kitchentimer(
let mut buffer = String::new(); let mut buffer = String::new();
// State variables. // State variables.
// Request redraw of input buffer.
let mut update_buffer = false; let mut update_buffer = false;
// Request redraw of menu.
let mut update_menu = false; let mut update_menu = false;
// Are we in insert mode?
let mut insert_mode = false; let mut insert_mode = false;
let async_stdin = termion::async_stdin(); let async_stdin = termion::async_stdin();
let mut input_keys = async_stdin.keys(); let mut input_keys = async_stdin.keys();
let stdout = std::io::stdout(); let stdout = std::io::stdout();
let mut stdout = stdout.lock().into_raw_mode() let mut stdout = stdout.lock().into_raw_mode()?;
.unwrap_or_else(|error| {
eprintln!("Error opening stdout: {}", error);
process::exit(1);
});
// Clear window and hide cursor. // Clear window and hide cursor.
write!(stdout, write!(stdout, "{}{}", clear::All, cursor::Hide)?;
"{}{}",
clear::All,
cursor::Hide)
.unwrap_or_else(|error| {
eprintln!("Error writing to stdout: {}", error);
process::exit(1);
});
// Enter main loop. // Main loop entry.
loop { loop {
// Process received signals. // Process received signals.
match signal.swap(0, Ordering::Relaxed) { match signal.swap(0, Ordering::Relaxed) {
@ -76,7 +84,7 @@ pub fn kitchentimer(
// Continuing after SIGSTOP. // Continuing after SIGSTOP.
SIGCONT => { SIGCONT => {
// This is reached when the process was suspended by SIGSTOP. // This is reached when the process was suspended by SIGSTOP.
restore_after_suspend(&mut stdout); restore_after_suspend(&mut stdout)?;
layout.force_redraw = true; layout.force_redraw = true;
}, },
// Exit main loop on SIGTERM and SIGINT. // Exit main loop on SIGTERM and SIGINT.
@ -94,6 +102,134 @@ pub fn kitchentimer(
_ => unreachable!(), _ => 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. // Process input.
if let Some(key) = input_keys.next() { if let Some(key) = input_keys.next() {
match key.expect("Error reading input") { match key.expect("Error reading input") {
@ -197,131 +333,6 @@ pub fn kitchentimer(
// Main loop delay. // Main loop delay.
thread::sleep(time::Duration::from_millis(200)); 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. // Main loop exited. Clear window and restore cursor.
@ -334,6 +345,111 @@ pub fn kitchentimer(
Ok(()) Ok(())
} }
pub struct Config {
plain: bool,
quit: bool,
command: Option<Vec<String>>,
}
impl Config {
// Parse command line arguments into "config".
pub fn new(args: env::Args, alarm_roster: &mut AlarmRoster)
-> Result<Config, String>
{
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<String> {
let mut command: Vec<String> = 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<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(&recalc_flag)).unwrap();
}
// Draw input buffer. // Draw input buffer.
fn draw_buffer<W: Write>( fn draw_buffer<W: Write>(
stdout: &mut RawTerminal<W>, stdout: &mut RawTerminal<W>,
@ -399,12 +515,13 @@ fn suspend<W: Write>(mut stdout: &mut RawTerminal<W>)
eprintln!("Error raising SIGTSTP: {}", error); eprintln!("Error raising SIGTSTP: {}", error);
} }
restore_after_suspend(&mut stdout); restore_after_suspend(&mut stdout)
Ok(())
} }
// Set up terminal after SIGTSTP or SIGSTOP. // Set up terminal after SIGTSTP or SIGSTOP.
fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>) { fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>)
-> Result<(), std::io::Error>
{
stdout.activate_raw_mode() stdout.activate_raw_mode()
.unwrap_or_else(|error| { .unwrap_or_else(|error| {
eprintln!("Failed to re-enter raw terminal mode after suspend: {}", error); eprintln!("Failed to re-enter raw terminal mode after suspend: {}", error);
@ -413,10 +530,7 @@ fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>) {
write!(stdout, write!(stdout,
"{}{}", "{}{}",
clear::All, clear::All,
cursor::Hide) cursor::Hide)?;
.unwrap_or_else(|error| { Ok(())
eprintln!("Error writing to stdout: {}", error);
process::exit(1);
});
} }

View file

@ -1,64 +1,38 @@
extern crate signal_hook; 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::{env, process};
use std::io::Write; use std::io::Write;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize}; use std::sync::atomic::{AtomicBool, AtomicUsize};
use signal_hook::flag; use kitchentimer::{Config, AlarmRoster, register_signals, run};
use clock::Clock;
use alarm::AlarmRoster;
use layout::{Layout, Position};
use consts::*;
use kitchentimer::kitchentimer;
pub struct Config {
plain: bool,
quit: bool,
command: Option<Vec<String>>,
}
fn main() { fn main() {
let mut config = Config { let args = env::args();
plain: false,
quit: false,
command: None,
};
let mut alarm_roster = AlarmRoster::new(); let mut alarm_roster = AlarmRoster::new();
// Parse command line arguments into config and alarm roster. // 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. // Register signal handlers.
let signal = Arc::new(AtomicUsize::new(0)); let signal = Arc::new(AtomicUsize::new(0));
let sigwinch = Arc::new(AtomicBool::new(true)); let sigwinch = Arc::new(AtomicBool::new(true));
register_signal_handlers(&signal, &sigwinch); register_signals(&signal, &sigwinch);
// Spawned child process if any. // Holds spawned child process if any.
let mut spawned: Option<process::Child> = None; let mut spawned: Option<process::Child> = None;
// Runs main loop. // Run main loop.
match kitchentimer( if let Err(e) = run(config, alarm_roster, signal, sigwinch, &mut spawned) {
config, println!("Main loop exited with error: {}", e);
alarm_roster, process::exit(1);
signal,
sigwinch,
&mut spawned,
) {
Ok(_) => (),
Err(e) => eprintln!("Main loop exited with error: {}", e),
} }
// Wait for remaining spawned processes to exit. // Wait for remaining spawned processes to exit.
if let Some(ref mut child) = spawned { 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(); std::io::stdout().flush().unwrap();
match child.wait() { 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<String> {
let mut command: Vec<String> = 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<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(&recalc_flag)).unwrap();
}