commit c04bde3ba48cc482b4e741333b34cd260fd2bc03 Author: shy Date: Mon Apr 5 12:20:24 2021 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bdd921 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +\.*.swp diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6ef9447 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,57 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "kt" +version = "0.1.0" +dependencies = [ + "libc", + "termion", +] + +[[package]] +name = "libc" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f902027 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "kt" +version = "0.1.0" +authors = ["shy "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +termion = "1.5" +libc = "0.2" diff --git a/src/alarm.rs b/src/alarm.rs new file mode 100644 index 0000000..073d25d --- /dev/null +++ b/src/alarm.rs @@ -0,0 +1,280 @@ +use std::io::Write; +use std::process::{Command, Stdio}; +use termion::{color, cursor, style}; +use termion::raw::RawTerminal; +use crate::{Clock, Config, Layout, Position}; +use crate::common::COLOR; + + +pub struct Countdown { + pub value: u32, + position: Option, +} + +impl Countdown { + pub fn new() -> Countdown { + Countdown { + value: 0, + position: None, + } + } + + pub fn reset(&mut self) { + self.value = 0; + } + + // Draw countdown. + pub fn draw(&self, stdout: &mut RawTerminal) { + if let Some(pos) = &self.position { + if self.value < 3600 { + // Show minutes and seconds. + write!(stdout, + "{}(-{:02}:{:02})", + cursor::Goto(pos.col, pos.line), + (self.value / 60) % 60, + self.value % 60) + .unwrap(); + if self.value == 3599 { + // Write three additional spaces after switching from hour display to + // minute display. + write!(stdout, " ").unwrap(); + } + } else { + // Show hours, minutes and seconds. + write!(stdout, + "{}(-{:02}:{:02}:{:02})", + cursor::Goto(pos.col, pos.line), + self.value / 3600, + (self.value / 60) % 60, + self.value % 60) + .unwrap(); + } + } + } +} + +pub struct Alarm { + time: u32, + display: String, + color_index: usize, + exceeded: bool, +} + +impl Alarm { + fn reset(&mut self) { + self.exceeded = false; + } +} + +pub struct AlarmRoster { + list: Vec, +} + +impl AlarmRoster { + pub fn new() -> AlarmRoster { + AlarmRoster { + list: Vec::new(), + } + } + + pub fn add(&mut self, buffer: &String) + -> Result<(), &'static str> { + + let mut index = 0; + let mut time: u32 = 0; + + // Parse input into seconds. + for sub in buffer.rsplit(':') { + if sub.len() > 0 { + let d = sub.parse::(); + match d { + Ok(d) => time += d * 60u32.pow(index), + Err(_) => return Err("Could not parse number as ."), + } + } + index += 1; + + // More than 3 fields are an error. + if index > 3 { return Err("Too many colons to parse.") }; + } + + // Skip if time evaluated to zero. + if time == 0 { return Err("Evaluates to zero.") }; + if time >= 24 * 60 * 60 { return Err("Values >24h not supported.") }; + + let alarm = Alarm { + display: buffer.clone(), + time, + color_index: (self.list.len() % COLOR.len()), + exceeded: false, + }; + + // Add to list, insert based on alarm time. Filter out double entries. + let mut i = self.list.len(); + if i == 0 { + self.list.push(alarm); + } else { + while i > 0 { + // Filter out double entries. + if self.list[i - 1].time == time { + return Err("Already exists."); + } else if self.list[i - 1].time < time { + break; + } + i -= 1; + } + self.list.insert(i, alarm); + } + Ok(()) + } + + pub fn pop(&mut self) -> Option { + self.list.pop() + } + + // Check for exceeded alarms. + pub fn check(&mut self, + clock: &mut Clock, + layout: &Layout, + countdown: &mut Countdown) -> bool { + + let mut hit = false; + let mut index = 0; + + for alarm in &mut self.list { + // Ignore alarms already marked exceeded. + if !alarm.exceeded { + if alarm.time <= clock.elapsed { + // Found alarm to raise. + hit = true; + alarm.exceeded = true; + clock.color_index = Some(alarm.color_index); + countdown.value = 0; + countdown.position = None; + // Skip ahead to the next one. + index += 1; + continue; + } + // Reached the alarm to exceed next. Update countdown + // accordingly. + countdown.value = alarm.time - clock.elapsed; + if countdown.position.is_none() || layout.force_redraw { + // Compute position. + let mut col = + layout.roster.col + + 3 + + alarm.display.len() as u16; + let mut line = layout.roster.line + index; + + // Compensate for "hidden" items in the alarm roster. + // TODO: Make this more elegant and robust. + if let Some(offset) = (self.list.len() as u16) + .checked_sub(layout.roster_height + 1) { + + if index <= offset{ + // Draw next to placeholder ("[...]"). + line = layout.roster.line; + col = layout.roster.col + 6; + } else { + line = line.checked_sub(offset).unwrap_or(layout.roster.line); + } + } + countdown.position = Some(Position { col, line, }); + } + // Ignore other alarms. + break; + } + index += 1; + } + hit // Return value. + } + + // Draw alarm roster according to layout. + pub fn draw(&self, stdout: &mut RawTerminal, layout: &mut Layout) { + let mut width: u16 = 0; + let mut index = 0; + + // Find first item to print in case we lack space to print them all. + // Final '-1' to take account for the input buffer. + let mut first = 0; + + if self.list.len() > layout.roster_height as usize { + // Actually -1 (zero indexing) +1 (first line containing "..."). + first = self.list.len() - layout.roster_height as usize; + index += 1; + + write!(stdout, + "{}{}[...]{}", + cursor::Goto(layout.roster.col, layout.roster.line), + style::Faint, + style::Reset).unwrap(); + } + + for alarm in &self.list[first..] { + if alarm.exceeded { + write!(stdout, + "{}{} {}{} {}!{}", + cursor::Goto(layout.roster.col, layout.roster.line + index), + color::Bg(COLOR[alarm.color_index]), + color::Bg(color::Reset), + style::Bold, + alarm.display, + style::Reset) + .unwrap(); + } else { + write!(stdout, + "{}{} {} {}", + cursor::Goto(layout.roster.col, layout.roster.line + index), + color::Bg(COLOR[alarm.color_index]), + color::Bg(color::Reset), + alarm.display) + .unwrap(); + } + index += 1; + // Calculate roster width. Actual display width is 3 chars wider. + if 3 + alarm.display.len() as u16 > width { + width = 3 + alarm.display.len() as u16; + } + } + // Update layout information. + if layout.roster_width != width { + layout.roster_width = width; + layout.force_recalc = true; + } + } + + // Reset every alarm. + pub fn reset_all(&mut self) { + for a in &mut self.list { + a.reset(); + } + } +} + +// Execute the command given on the command line. +pub fn alarm_exec(config: &Config, elapsed: u32) { + let mut args: Vec = Vec::new(); + let time: String; + + if elapsed < 3600 { + time = format!("{:02}:{:02}", elapsed / 60, elapsed % 60); + } else { + time = format!("{:02}:{:02}:{:02}", elapsed /3600, (elapsed / 60) % 60, elapsed % 60); + } + + // Replace every occurrence of "%s". + for s in config.alarm_exec.iter() { + args.push(s.replace("%s", &time)); + } + + if Command::new( + &config.alarm_exec[0]) + .args(&args[1..]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .spawn().is_err() { + + eprintln!("Error: Could not execute command"); + } +} + diff --git a/src/clock.rs b/src/clock.rs new file mode 100644 index 0000000..06d3bdd --- /dev/null +++ b/src/clock.rs @@ -0,0 +1,209 @@ +use std::time; +use std::io::Write; +use termion::{color, cursor}; +use termion::raw::RawTerminal; +use crate::common::*; +use crate::Layout; +use crate::Position; +use crate::common::COLOR; + + +pub struct Clock { + pub start: time::Instant, + pub elapsed: u32, + pub days: u32, + pub paused: bool, + paused_at: Option, + pub color_index: Option, +} + +impl Clock { + pub fn new() -> Clock { + Clock { + start: time::Instant::now(), + elapsed: 0, + days: 0, + paused: false, + paused_at: None, + color_index: None, + } + } + + pub fn reset(&mut self) { + self.start = time::Instant::now(); + self.elapsed = 0; + self.days = 0; + self.color_index = None; + + // unpause will panic if we do not trigger a new pause here. + if self.paused { + self.pause(); + } + } + + fn pause(&mut self) { + self.paused_at = Some(time::Instant::now()); + self.paused = true; + } + + fn unpause(&mut self) { + // Try to derive a new start instant. + if let Some(delay) = self.paused_at { + if let Some(new_start) = self.start.checked_add(delay.elapsed()) { + self.start = new_start; + } + } + + self.paused_at = None; + self.paused = false; + } + + pub fn toggle(&mut self) { + if self.paused { + self.unpause(); + } else { + self.pause(); + } + } + + pub fn next_day(&mut self) { + // Shift start 24h into the future. + let next = self.start.clone() + time::Duration::from_secs(60 * 60 * 24); + + // Take care not to shift start into the future. + if next <= time::Instant::now() { + self.start = next; + self.elapsed = 0; + self.days = self.days.saturating_add(1); + } + } + + // Draw clock according to layout. + pub fn draw(&mut self, mut stdout: &mut RawTerminal, layout: &Layout) { + // Draw hours if necessary. + if layout.force_redraw || self.elapsed % 3600 == 0 { + if self.elapsed >= 3600 { + self.draw_digit_pair( + &mut stdout, + self.elapsed / 3600, + &layout.clock_hr, + layout.plain); + + // Draw colon. + self.draw_colon( + &mut stdout, + &layout.clock_colon1, + layout.plain); + } + + // Draw days. + if self.days > 0 { + let day_count = format!( + "+ {} {}", + self.days, + if self.days == 1 { "day" } else { "days" }); + + write!(stdout, + "{}{:>11}", + cursor::Goto( + layout.clock_days.col, + layout.clock_days.line, + ), + day_count) + .unwrap(); + } + } + + // Draw minutes if necessary. + if layout.force_redraw || self.elapsed % 60 == 0 { + self.draw_digit_pair( + &mut stdout, + (self.elapsed % 3600) / 60, + &layout.clock_min, + layout.plain); + } + + // Draw colon if necessary. + if layout.force_redraw { + self.draw_colon( + &mut stdout, + &layout.clock_colon0, + layout.plain); + } + + // Draw seconds. + self.draw_digit_pair( + &mut stdout, + self.elapsed % 60, + &layout.clock_sec, + layout.plain); + } + + fn draw_digit_pair(&self, stdout: &mut RawTerminal, value: u32, pos: &Position, plain: bool) { + if let Some(c) = self.color_index { + write!(stdout, + "{}{}", + cursor::Goto(pos.col, pos.line), + color::Fg(COLOR[c])) + .unwrap(); + } + + for l in 0..DIGIT_HEIGHT { + if plain { + write!(stdout, + "{}{} {}", + cursor::Goto(pos.col, pos.line + l), + // First digit. + DIGITS_PLAIN[(value / 10) as usize][l as usize], + // Second digit. + DIGITS_PLAIN[(value % 10) as usize][l as usize]) + .unwrap(); + } else { + write!(stdout, + "{}{} {}", + cursor::Goto(pos.col, pos.line + l), + // First digit. + DIGITS[(value / 10) as usize][l as usize], + // Second digit. + DIGITS[(value % 10) as usize][l as usize]) + .unwrap(); + } + } + + if self.color_index != None { + write!(stdout, + "{}{}", + cursor::Goto(pos.col + DIGIT_WIDTH + 1, pos.line + DIGIT_HEIGHT), + color::Fg(color::Reset)) + .unwrap(); + } + } + + fn draw_colon(&self, stdout: &mut RawTerminal, pos: &Position, plain: bool) { + let dot: char = if plain {'█'} else {'■'}; + + match self.color_index { + Some(c) => { + write!(stdout, + "{}{}{}{}{}{}", + cursor::Goto(pos.col, pos.line + 1), + color::Fg(COLOR[c]), + dot, + cursor::Goto(pos.col, pos.line + 3), + dot, + color::Fg(color::Reset)) + .unwrap(); + } + None => { + write!(stdout, + "{}{}{}{}", + cursor::Goto(pos.col, pos.line + 1), + dot, + cursor::Goto(pos.col, pos.line + 3), + dot) + .unwrap(); + } + } + } +} + diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..fb3e8e8 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,160 @@ +use termion::color; + +pub const COLOR: [&dyn color::Color; 6] = [ + &color::Cyan, + &color::Magenta, + &color::Green, + &color::Yellow, + &color::Blue, + &color::Red, +]; +pub const DIGIT_HEIGHT: u16 = 5; +pub const DIGIT_WIDTH: u16 = 5; +pub const DIGITS: [[&str; DIGIT_HEIGHT as usize]; 10] = [ + [ + // 0 + "█▀▀▀█", + "█ █", + "█ █", + "█ █", + "█▄▄▄█", + ], [ + // 1 + " ▀█ ", + " █ ", + " █ ", + " █ ", + " █ " + ], [ + // 2 + "▀▀▀▀█", + " █", + "█▀▀▀▀", + "█ ", + "█▄▄▄▄" + ], [ + // 3 + "▀▀▀▀█", + " █", + " ▀▀▀█", + " █", + "▄▄▄▄█" + ], [ + // 4 + "█ ", + "█ █ ", + "▀▀▀█▀", + " █ ", + " █ " + ], [ + // 5 + "█▀▀▀▀", + "█ ", + "▀▀▀▀█", + " █", + "▄▄▄▄█" + ], [ + // 6 + "█ ", + "█ ", + "█▀▀▀█", + "█ █", + "█▄▄▄█" + ], [ + // 7 + "▀▀▀▀█", + " █", + " █ ", + " █ ", + " █ ", + ], [ + // 8 + "█▀▀▀█", + "█ █", + "█▀▀▀█", + "█ █", + "█▄▄▄█" + ], [ + // 9 + "█▀▀▀█", + "█ █", + "▀▀▀▀█", + " █", + " █" + ] +]; + +pub const DIGITS_PLAIN: [[&str; DIGIT_HEIGHT as usize]; 10] = [ + [ + // 0 + "█████", + "█ █", + "█ █", + "█ █", + "█████" + ], [ + // 1 + " ██ ", + " █ ", + " █ ", + " █ ", + " █ " + ], [ + // 2 + "█████", + " █", + "█████", + "█ ", + "█████" + ], [ + // 3 + "█████", + " █", + " ████", + " █", + "█████" + ], [ + // 4 + "█ ", + "█ █ ", + "█████", + " █ ", + " █ " + ], [ + // 5 + "█████", + "█ ", + "█████", + " █", + "█████" + ], [ + // 6 + "█ ", + "█ ", + "█████", + "█ █", + "█████" + ], [ + // 7 + "█████", + " █", + " █ ", + " █ ", + " █ " + ], [ + // 8 + "█████", + "█ █", + "█████", + "█ █", + "█████" + ], [ + // 9 + "█████", + "█ █", + "█████", + " █", + " █" + ] +]; + diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..867198e --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,130 @@ +use crate::Config; +use crate::common::*; + +// If screen size falls below these values we skip computation of new +// positions. +const MIN_WIDTH: u16 = DIGIT_WIDTH * 6 + 13; +const MIN_HEIGHT: u16 = DIGIT_HEIGHT + 2; + +pub struct Position { + pub line: u16, + pub col: u16, +} + +pub struct Layout { + pub force_redraw: bool, // Redraw elements on screen. + pub force_recalc: bool, // Recalculate position of elements. + pub plain: bool, // Plain style clock. + pub width: u16, + pub height: u16, + pub clock_sec: Position, + pub clock_colon0: Position, + pub clock_min: Position, + pub clock_colon1: Position, + pub clock_hr: Position, + pub clock_days: Position, + pub roster: Position, + pub roster_width: u16, + pub roster_height: u16, + pub buffer: Position, + pub error: Position, +} + +impl Layout { + pub fn new(config: &Config) -> Layout { + Layout { + force_redraw: true, + force_recalc: false, + plain: config.plain, + width: 0, + height: 0, + clock_sec: Position {col: 0, line: 0}, + clock_colon0: Position {col: 0, line: 0}, + clock_min: Position {col: 0, line: 0}, + clock_colon1: Position {col: 0, line: 0}, + clock_hr: Position {col: 0, line: 0}, + clock_days: Position {col: 0, line: 0}, + roster: Position {col: 1, line: 3}, + roster_width: 0, + roster_height: 0, + buffer: Position {col: 0, line: 0}, + error: Position {col: 0, line: 0}, + } + } + + pub fn update(&mut self, display_hours: bool) { + let (width, height) = termion::terminal_size() + .expect("Could not read terminal size!"); + + if self.force_recalc || self.width != width || self.height != height { + self.width = width; + self.height = height; + self.compute(display_hours); + self.force_redraw = true; + self.force_recalc = false; + } + } + + // Compute the position of various elements based on the size of the + // terminal. + fn compute(&mut self, display_hours: bool) { + let middle: u16 = self.height / 2 - 1; + + // Prevent integer overflow on very low screen sizes. + if self.width < MIN_WIDTH || self.height < MIN_HEIGHT { return; } + + if display_hours { + // Seconds digits. + self.clock_sec.col = (self.width + self.roster_width) / 2 + DIGIT_WIDTH + 6; + // Colon separating minutes from seconds. + self.clock_colon0.col = (self.width + self.roster_width) / 2 + DIGIT_WIDTH + 3; + // Minute digits. + self.clock_min.col = (self.width + self.roster_width) / 2 - DIGIT_WIDTH; + + // Colon separating hours from minutes. + self.clock_colon1 = Position { + col: (self.width + self.roster_width) / 2 - (DIGIT_WIDTH + 3), + line: middle, + }; + + // Hour digits. + self.clock_hr = Position { + col: (self.width + self.roster_width) / 2 - (DIGIT_WIDTH * 3 + 6), + line: middle, + }; + } else { + // Seconds digits. + self.clock_sec.col = (self.width + self.roster_width) / 2 + 3; + // Colon separating minutes from seconds. + self.clock_colon0.col = (self.width + self.roster_width) / 2; + // Minute digits. + self.clock_min.col = (self.width + self.roster_width) / 2 - (DIGIT_WIDTH * 2 + 3); + } + + self.clock_sec.line = middle; + self.clock_colon0.line = middle; + self.clock_min.line = middle; + + // Days (based on position of seconds). + self.clock_days = Position { + line: self.clock_sec.line + DIGIT_HEIGHT + 1, + col: self.clock_sec.col, + }; + + // Alarm roster height. + self.roster_height = self.height - self.roster.line - 1; + + // Input buffer. + self.buffer = Position { + line: self.height, + col: 1, + }; + + // Error messages. + self.error = Position { + line: self.height, + col: 12, + }; + } +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..76643d6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,333 @@ +extern crate termion; +extern crate libc; +mod alarm; +mod clock; +mod common; +mod layout; + +use std::{time, thread, env}; +use std::io::Write; +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 = "kt (kitchentime)"; +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."; +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"; + +pub struct Config { + plain: bool, + alarm_exec: Vec, +} + + +fn main() { + let mut config = Config { + plain: false, + alarm_exec: Vec::new(), + }; + 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(); + + // Clear screen and hide cursor. + write!(stdout, + "{}{}", + clear::All, + cursor::Hide) + .unwrap_or_else(|error| { + eprintln!("Error writing to stdout: {}", error); + std::process::exit(1); + }); + + loop { + // 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); + 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; + } + }, + _ => (), + } + } + + // 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 owerflow as we reestablish a new "start" + // instant every 24 hours. + clock.start.elapsed().as_secs() as u32 + }; + + // Update screen 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 = true; + } + + // Force recalculation of layout if we start displaying hours. + if clock.elapsed == 3600 { layout.force_recalc = true }; + // Update screen 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; + + if config.alarm_exec.len() > 0 { + alarm_exec(&config, clock.elapsed); + } + } + + // Clear the screen 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 screen 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. + config.alarm_exec = env::args().skip(i + 1).collect(); + if config.alarm_exec.len() == 0 { + usage(); + } else { + // Ignore everything after this flag. + break; + } + } + _ => usage(), // Unrecognized flag. + } + } +} + +// Suspend execution by raising SIGTSTP. +fn suspend(stdout: &mut RawTerminal) { + 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); + }); + + let result = unsafe { libc::raise(libc::SIGTSTP) }; + if result != 0 { + panic!("{}", std::io::Error::last_os_error()); + } + + 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); + }); +} + +// Draw input buffer. +fn draw_buffer(stdout: &mut RawTerminal, 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(stdout: &mut RawTerminal, 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(); +} +