406 lines
14 KiB
Rust
406 lines
14 KiB
Rust
extern crate termion;
|
|
extern crate signal_hook;
|
|
mod alarm;
|
|
mod clock;
|
|
mod common;
|
|
mod layout;
|
|
|
|
use std::{time, thread, env};
|
|
use std::io::Write;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use signal_hook::flag;
|
|
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 layout::{Layout, Position};
|
|
|
|
|
|
const NAME: &str = "kitchentimer";
|
|
const VERSION: &str = "0.0.1";
|
|
const USAGE: &str =
|
|
"USAGE: kt [-h|--help] [-v|--version] [-p|--plain] [-e|--exec COMMAND [...]]
|
|
-p, --plain Use simpler block chars.
|
|
-e, --exec [COMMAND] Execute \"COMMAND\" on alarm. Must be the last flag on
|
|
the command line. Everything after it is passed as
|
|
argument to \"COMMAND\". Every \"%s\" will be replaced
|
|
with the elapsed time in [(HH:)MM:SS] format.
|
|
|
|
SIGNALS: <SIGUSR1> Reset clock.
|
|
<SIGUSR2> Pause or un-pause clock.";
|
|
const MENUBAR: &str =
|
|
"[0-9] Add alarm [d] Delete alarm [SPACE] Pause [r] Reset [c] Clear color [q] Quit";
|
|
const MENUBAR_SHORT: &str =
|
|
"[0-9] Add [d] Delete [SPACE] Pause [r] Reset [c] Clear [q] Quit";
|
|
// Needed for signal_hook.
|
|
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 struct Config {
|
|
plain: bool,
|
|
alarm_exec: Option<Vec<String>>,
|
|
}
|
|
|
|
|
|
fn main() {
|
|
let mut config = Config {
|
|
plain: false,
|
|
alarm_exec: None,
|
|
};
|
|
parse_args(&mut config);
|
|
|
|
let mut stdout = std::io::stdout().into_raw_mode()
|
|
.unwrap_or_else(|error| {
|
|
eprintln!("Error opening stdout: {}", error);
|
|
std::process::exit(1);
|
|
});
|
|
let mut input_keys = termion::async_stdin().keys();
|
|
let mut layout = Layout::new(&config);
|
|
let mut clock = Clock::new();
|
|
let mut alarm_roster = AlarmRoster::new();
|
|
let mut buffer = String::new();
|
|
let mut buffer_updated: bool = false;
|
|
let mut countdown = Countdown::new();
|
|
|
|
// Register signal handlers.
|
|
let signal = Arc::new(AtomicUsize::new(0));
|
|
register_signal_handlers(&signal);
|
|
flag::register(SIGWINCH as i32, Arc::clone(&layout.force_recalc)).unwrap();
|
|
|
|
// Clear window and hide cursor.
|
|
write!(stdout,
|
|
"{}{}",
|
|
clear::All,
|
|
cursor::Hide)
|
|
.unwrap_or_else(|error| {
|
|
eprintln!("Error writing to stdout: {}", error);
|
|
std::process::exit(1);
|
|
});
|
|
|
|
// Enter main loop.
|
|
loop {
|
|
// Process received signals.
|
|
match signal.swap(0, Ordering::Relaxed) {
|
|
// No signal received.
|
|
0 => (),
|
|
// Suspend execution on SIGTSTP.
|
|
SIGTSTP => {
|
|
suspend(&mut stdout);
|
|
// Clear SIGCONT and continue from here.
|
|
signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed);
|
|
continue_after_suspend(&mut stdout);
|
|
layout.force_redraw = true;
|
|
},
|
|
// Continuing after SIGSTOP.
|
|
SIGCONT => {
|
|
continue_after_suspend(&mut stdout);
|
|
layout.force_redraw = true;
|
|
},
|
|
// Exit main loop on SIGTERM and SIGINT.
|
|
SIGTERM | SIGINT => break,
|
|
// Reset clock on SIGUSR1.
|
|
SIGUSR1 => {
|
|
clock.reset();
|
|
alarm_roster.reset_all();
|
|
layout.force_redraw = true;
|
|
},
|
|
// (Un-)Pause clock on SIGUSR2.
|
|
SIGUSR2 => clock.toggle(),
|
|
// Window size changed.
|
|
//SIGWINCH => layout.force_recalc = true,
|
|
// We didn't register anything else.
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
// Process input.
|
|
if let Some(key) = input_keys.next() {
|
|
match key.expect("Error reading input") {
|
|
// Reset clock on 'r'.
|
|
Key::Char('r') => {
|
|
clock.reset();
|
|
alarm_roster.reset_all();
|
|
layout.force_redraw = true;
|
|
},
|
|
// (Un-)Pause on space.
|
|
Key::Char(' ') => {
|
|
clock.toggle();
|
|
},
|
|
// Clear clock color on 'c'.
|
|
Key::Char('c') => {
|
|
clock.color_index = None;
|
|
layout.force_redraw = true;
|
|
},
|
|
// Delete last alarm on 'd'.
|
|
Key::Char('d') => {
|
|
if alarm_roster.pop().is_some() {
|
|
// If we remove the last alarm we have to reset "countdown"
|
|
// manually. It is safe to do it anyway.
|
|
countdown.reset();
|
|
layout.force_redraw = true;
|
|
}
|
|
},
|
|
// Exit on q and ^C.
|
|
Key::Char('q') | Key::Ctrl('c') => break,
|
|
// Force redraw on ^R.
|
|
Key::Ctrl('r') => layout.force_redraw = true,
|
|
// Suspend an ^Z.
|
|
Key::Ctrl('z') => {
|
|
suspend(&mut stdout);
|
|
// Clear SIGCONT and continue from here.
|
|
signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed);
|
|
continue_after_suspend(&mut stdout);
|
|
layout.force_redraw = true;
|
|
},
|
|
// Enter.
|
|
Key::Char('\n') => {
|
|
if buffer.len() > 0 {
|
|
if let Err(e) = alarm_roster.add(&buffer) {
|
|
// Error while processing input buffer.
|
|
error_msg(&mut stdout, &layout, e);
|
|
} else {
|
|
// Input buffer processed without error.
|
|
layout.force_redraw = true;
|
|
}
|
|
buffer.clear();
|
|
}
|
|
},
|
|
// Escape ^W, and ^U clear input buffer.
|
|
Key::Esc | Key::Ctrl('w') | Key::Ctrl('u') => {
|
|
buffer.clear();
|
|
buffer_updated = true;
|
|
},
|
|
// Backspace.
|
|
Key::Backspace => {
|
|
// Delete last char in buffer. It makes no difference to us
|
|
// if this succeeds of fails.
|
|
let _ = buffer.pop();
|
|
buffer_updated = true;
|
|
},
|
|
Key::Char(c) => {
|
|
if c.is_ascii_digit() {
|
|
buffer.push(c);
|
|
buffer_updated = true;
|
|
} else if buffer.len() > 0 && c == ':' {
|
|
buffer.push(':');
|
|
buffer_updated = true;
|
|
}
|
|
},
|
|
// Any other key.
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
// Update input buffer display.
|
|
if buffer_updated {
|
|
draw_buffer(&mut stdout, &layout, &buffer);
|
|
buffer_updated = false;
|
|
stdout.flush().unwrap();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Force recalculation of layout if we start displaying hours.
|
|
if clock.elapsed == 3600 {
|
|
layout.force_recalc.store(true, Ordering::Relaxed);
|
|
}
|
|
// Update window size information and calculate the clock position.
|
|
layout.update(clock.elapsed >= 3600);
|
|
|
|
// Check for exceeded alarms.
|
|
if 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() {
|
|
alarm_exec(&config, clock.elapsed);
|
|
}
|
|
}
|
|
|
|
// Clear the window and redraw menu bar, alarm roster and buffer if
|
|
// requested.
|
|
if layout.force_redraw {
|
|
write!(stdout,
|
|
"{}{}{}{}{}",
|
|
clear::All,
|
|
cursor::Goto(1, 1),
|
|
style::Faint,
|
|
// Use a compressed version of the menu bar if necessary.
|
|
if layout.width >= MENUBAR.len() as u16 {
|
|
MENUBAR
|
|
} else if layout.width >= MENUBAR_SHORT.len() as u16 {
|
|
MENUBAR_SHORT
|
|
} else {
|
|
""
|
|
},
|
|
style::Reset,)
|
|
.unwrap();
|
|
|
|
// Redraw list of alarms.
|
|
alarm_roster.draw(&mut stdout, &mut layout);
|
|
|
|
// Redraw buffer.
|
|
draw_buffer(&mut stdout, &layout, &buffer);
|
|
}
|
|
|
|
clock.draw(&mut stdout, &layout);
|
|
|
|
// Display countdown.
|
|
if countdown.value > 0 {
|
|
countdown.draw(&mut stdout);
|
|
}
|
|
|
|
// Reset redraw_all and flush stdout.
|
|
layout.force_redraw = false;
|
|
stdout.flush().unwrap();
|
|
}
|
|
|
|
// Main loop delay.
|
|
thread::sleep(time::Duration::from_millis(100));
|
|
}
|
|
|
|
// Main loop exited. Clear window and restore cursor.
|
|
write!(stdout,
|
|
"{}{}{}",
|
|
clear::BeforeCursor,
|
|
cursor::Goto(1, 1),
|
|
cursor::Show)
|
|
.unwrap();
|
|
}
|
|
|
|
fn usage() {
|
|
println!("{}\n{}", NAME, USAGE);
|
|
std::process::exit(0);
|
|
}
|
|
|
|
// Parse command line arguments into "config".
|
|
fn parse_args(config: &mut Config) {
|
|
for arg in env::args().skip(1) {
|
|
match arg.as_str() {
|
|
"-h" | "--help" => usage(),
|
|
"-v" | "--version" => {
|
|
println!("{} {}", NAME, VERSION);
|
|
std::process::exit(0);
|
|
}
|
|
"-p" | "--plain" => { config.plain = true; },
|
|
"-e" | "--exec" => {
|
|
// Find position of this flag.
|
|
let i = env::args().position(|s| { s == "-e" || s == "--exec" }).unwrap();
|
|
// Copy everything thereafter.
|
|
let exec: Vec<String> = env::args().skip(i + 1).collect();
|
|
if exec.len() == 0 {
|
|
usage();
|
|
} else {
|
|
config.alarm_exec = Some(exec);
|
|
// Ignore everything after this flag.
|
|
break;
|
|
}
|
|
}
|
|
_ => usage(), // Unrecognized flag.
|
|
}
|
|
}
|
|
}
|
|
|
|
fn register_signal_handlers(signal: &Arc<AtomicUsize>) {
|
|
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();
|
|
}
|
|
|
|
// Suspend execution on SIGTSTP.
|
|
fn suspend<W: Write>(stdout: &mut RawTerminal<W>) {
|
|
write!(stdout,
|
|
"{}{}{}",
|
|
cursor::Goto(1,1),
|
|
clear::All,
|
|
cursor::Show)
|
|
.unwrap();
|
|
stdout.flush().unwrap();
|
|
stdout.suspend_raw_mode()
|
|
.unwrap_or_else(|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) {
|
|
eprintln!("Error raising SIGTSTP: {}", error);
|
|
}
|
|
}
|
|
|
|
// Set up terminal when continuing from SIGTSTP or SIGSTOP.
|
|
fn continue_after_suspend<W: Write>(stdout: &mut RawTerminal<W>) {
|
|
stdout.activate_raw_mode()
|
|
.unwrap_or_else(|error| {
|
|
eprintln!("Failed to re-enter raw terminal mode after suspend: {}", error);
|
|
std::process::exit(1);
|
|
});
|
|
write!(stdout,
|
|
"{}{}",
|
|
clear::All,
|
|
cursor::Hide)
|
|
.unwrap_or_else(|error| {
|
|
eprintln!("Error writing to stdout: {}", error);
|
|
std::process::exit(1);
|
|
});
|
|
stdout.flush().unwrap();
|
|
}
|
|
|
|
// Draw input buffer.
|
|
fn draw_buffer<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, buffer: &String) {
|
|
if buffer.len() > 0 {
|
|
write!(stdout,
|
|
"{}{}Add alarm: {}",
|
|
cursor::Goto(layout.buffer.col, layout.buffer.line),
|
|
clear::CurrentLine,
|
|
buffer)
|
|
.unwrap();
|
|
} else {
|
|
// Clear buffer display.
|
|
write!(stdout,
|
|
"{}{}",
|
|
cursor::Goto(layout.buffer.col, layout.buffer.line),
|
|
clear::CurrentLine)
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
// Print error message.
|
|
fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &'static str) {
|
|
write!(stdout,
|
|
"{}{}{}{}",
|
|
cursor::Goto(layout.error.col, layout.error.line),
|
|
color::Fg(color::LightRed),
|
|
msg,
|
|
color::Fg(color::Reset))
|
|
.unwrap();
|
|
}
|
|
|