From 1e749c7cbd5d718e05e982ada1e81386b617fe8c Mon Sep 17 00:00:00 2001 From: shy Date: Sat, 10 Apr 2021 07:57:41 +0200 Subject: [PATCH] Extensive refactoring. --- src/alarm.rs | 8 +- src/clock.rs | 3 +- src/{common.rs => consts.rs} | 63 +++-- src/kitchentimer.rs | 410 +++++++++++++++++++++++++++++++ src/layout.rs | 3 +- src/main.rs | 460 ++--------------------------------- src/utils.rs | 15 ++ 7 files changed, 498 insertions(+), 464 deletions(-) rename src/{common.rs => consts.rs} (57%) create mode 100644 src/kitchentimer.rs create mode 100644 src/utils.rs diff --git a/src/alarm.rs b/src/alarm.rs index 4e32821..5311985 100644 --- a/src/alarm.rs +++ b/src/alarm.rs @@ -2,9 +2,9 @@ use std::io::Write; use std::process::{Command, Stdio, Child}; use termion::{color, cursor, style}; use termion::raw::RawTerminal; -use crate::{Clock, Layout, Position}; -use crate::common::{COLOR, LABEL_SIZE_LIMIT}; -use crate::common::{Config, unicode_length, unicode_truncate}; +use crate::{Clock, Config, Layout, Position}; +use crate::utils::*; +use crate::consts::{COLOR, LABEL_SIZE_LIMIT}; pub struct Countdown { @@ -86,7 +86,7 @@ impl AlarmRoster { let time_str: &str; if let Some(i) = input.find('/') { - label = input[(i + 1)..].trim().to_string(); + label = input[(i + 1)..].to_string(); // Truncate label. unicode_truncate(&mut label, LABEL_SIZE_LIMIT); time_str = &input[..i].trim(); diff --git a/src/clock.rs b/src/clock.rs index e6802b3..75f7a6c 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -2,10 +2,9 @@ use std::time; use std::io::Write; use termion::{color, cursor}; use termion::raw::RawTerminal; -use crate::common::*; +use crate::consts::*; use crate::Layout; use crate::Position; -use crate::common::COLOR; pub struct Clock { diff --git a/src/common.rs b/src/consts.rs similarity index 57% rename from src/common.rs rename to src/consts.rs index 732bb30..88b8440 100644 --- a/src/common.rs +++ b/src/consts.rs @@ -1,33 +1,48 @@ -use unicode_segmentation::UnicodeSegmentation; -use termion::color; +pub const NAME: &str = env!("CARGO_PKG_NAME"); +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const USAGE: &str = concat!("USAGE: ", env!("CARGO_PKG_NAME"), +" [-h|-v] [-e|--exec COMMAND] [-p] [-q] [ALARM[/LABEL]] +PARAMETERS: + [ALARM TIME[/LABEL]] Any number of alarm times (HH:MM:SS) with optional + label. -pub struct Config { - pub plain: bool, - pub quit: bool, - pub command: Option>, -} +OPTIONS: + -h, --help Show this help. + -v, --version Show version information. + -e, --exec [COMMAND] Execute COMMAND on alarm. Occurrences of {t} will + be replaced by the alarm time in (HH:)MM:SS format. + Occurrences of {l} by alarm label. + -p, --plain Use simpler block chars. + -q, --quit Quit program after last alarm. -pub fn unicode_length(input: &str) -> u16 { - let length = UnicodeSegmentation::graphemes(input, true).count(); - length as u16 -} +SIGNALS: Reset clock. + Pause or un-pause clock."); +pub const MENUBAR: &str = +"[0-9] Add alarm [d] Delete alarm [SPACE] Pause [r] Reset [c] Clear color [q] Quit"; +pub const MENUBAR_SHORT: &str = +"[0-9] Add [d] Delete [SPACE] Pause [r] Reset [c] Clear [q] Quit"; +pub const MENUBAR_INS: &str = +"Format: HH:MM:SS/LABEL [ENTER] Accept [ESC] Cancel [CTR-C] Quit"; -pub fn unicode_truncate(input: &mut String, limit: usize) { - match UnicodeSegmentation::grapheme_indices(input.as_str(), true).nth(limit) { - Some((i, _)) => input.truncate(i), - None => (), - } -} +// 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 color::Color; 6] = [ - &color::Cyan, - &color::Magenta, - &color::Green, - &color::Yellow, - &color::Blue, - &color::Red, +pub const COLOR: [&dyn termion::color::Color; 6] = [ + &termion::color::Cyan, + &termion::color::Magenta, + &termion::color::Green, + &termion::color::Yellow, + &termion::color::Blue, + &termion::color::Red, ]; + // Maximum length of labels. pub const LABEL_SIZE_LIMIT: usize = 48; pub const DIGIT_HEIGHT: u16 = 5; diff --git a/src/kitchentimer.rs b/src/kitchentimer.rs new file mode 100644 index 0000000..49501e8 --- /dev/null +++ b/src/kitchentimer.rs @@ -0,0 +1,410 @@ +extern crate termion; + +use std::{time, thread}; +use std::io::Write; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use signal_hook::low_level; +use termion::{clear, color, cursor, style}; +use termion::raw::{IntoRawMode, RawTerminal}; +use termion::event::Key; +use termion::input::TermRead; +use crate::clock::Clock; +use crate::alarm::{Countdown, AlarmRoster, exec_command}; +use crate::layout::Layout; +use crate::consts::*; +use crate::utils::*; +use crate::Config; + +pub fn kitchentimer( + config: Config, + mut alarm_roster: AlarmRoster, + signal: Arc, + sigwinch: Arc, + spawned: &mut Option, +) { + let mut layout = Layout::new(&config); + layout.force_recalc = sigwinch; + // Initialise roster_width. + layout.set_roster_width(alarm_roster.width()); + let mut clock = Clock::new(); + let mut countdown = Countdown::new(); + + let mut buffer = String::new(); + let mut input_keys = termion::async_stdin().keys(); + + // State variables. + let mut update_buffer = false; + let mut update_menu = false; + let mut insert_mode = false; + + let mut stdout = std::io::stdout().into_raw_mode() + .unwrap_or_else(|error| { + eprintln!("Error opening stdout: {}", error); + std::process::exit(1); + }); + + // 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, as we have already taken care to reset the + // terminal. + signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed); + layout.force_redraw = true; + // Jump to the start of the main loop. + continue; + }, + // Continuing after SIGSTOP. + SIGCONT => { + // This is reached when the process was suspended by SIGSTOP. + restore_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_recalc.store(true, Ordering::Relaxed); + layout.force_redraw = true; + }, + // (Un-)Pause clock on SIGUSR2. + SIGUSR2 => clock.toggle(), + // We didn't register anything else. + _ => unreachable!(), + } + + // Process input. + if let Some(key) = input_keys.next() { + match key.expect("Error reading input") { + // Enter. + Key::Char('\n') => { + if !buffer.is_empty() { + 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.set_roster_width(alarm_roster.width()); + layout.force_redraw = true; + } + buffer.clear(); + insert_mode = false; + update_menu = true; + } + }, + // Escape ^W, and ^U clear input buffer. + Key::Esc | Key::Ctrl('w') | Key::Ctrl('u') => { + buffer.clear(); + insert_mode = false; + update_menu = true; + layout.force_redraw = true; + update_buffer = true; + }, + // Backspace. + Key::Backspace => { + // Delete last char in buffer. + if buffer.pop().is_some() { + if buffer.is_empty() { + insert_mode = false; + update_menu = true; + layout.force_redraw = true; + } + } + update_buffer = true; + }, + // Forward every char if in insert mode. + Key::Char(c) if insert_mode => { + buffer.push(c); + update_buffer = true; + }, + // Reset clock on 'r'. + Key::Char('r') => { + clock.reset(); + alarm_roster.reset_all(); + layout.force_recalc.store(true, Ordering::Relaxed); + 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.drop_last() { + // If we remove the last alarm we have to reset "countdown" + // manually. It is safe to do it anyway. + layout.set_roster_width(alarm_roster.width()); + 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, as we have already taken care to reset + // the terminal. + signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed); + layout.force_redraw = true; + // Jump to the start of the main loop. + continue; + }, + Key::Char(c) => { + if c.is_ascii_digit() { + buffer.push(c); + insert_mode = true; + update_menu = true; + layout.force_redraw = true; + update_buffer = true; + } else if !buffer.is_empty() && c == ':' { + buffer.push(':'); + update_buffer = true; + } + }, + // Any other key. + _ => (), + } + } + + // Update input buffer display. + if update_buffer { + draw_buffer(&mut stdout, &mut layout, &buffer); + update_buffer = 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); + } + + // 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).unwrap(); + 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).unwrap(); + + // 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,) + .unwrap(); + } + + 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)) + .unwrap(); + } + + // 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().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(); +} + +// Draw input buffer. +fn draw_buffer( + stdout: &mut RawTerminal, + layout: &mut Layout, + buffer: &String, +) { + if !buffer.is_empty() { + write!(stdout, + "{}{}Add alarm: {}{}", + cursor::Goto(layout.buffer.col, layout.buffer.line), + clear::CurrentLine, + cursor::Show, + buffer) + .unwrap(); + layout.cursor.col = layout.buffer.col + 11 + unicode_length(buffer); + } else { + // Clear buffer display. + write!(stdout, + "{}{}{}", + cursor::Goto(layout.buffer.col, layout.buffer.line), + clear::CurrentLine, + cursor::Hide) + .unwrap(); + } +} + +// Draw error message at input buffer position. +fn error_msg(stdout: &mut RawTerminal, layout: &Layout, msg: &str) { + write!(stdout, + "{}{}{}{}{}", + cursor::Goto(layout.error.col, layout.error.line), + color::Fg(color::LightRed), + msg, + color::Fg(color::Reset), + cursor::Hide) + .unwrap(); +} + +// Prepare to suspend execution. Called on SIGTSTP. +fn suspend(mut 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); + }); + + if let Err(error) = low_level::emulate_default_handler(SIGTSTP as i32) { + eprintln!("Error raising SIGTSTP: {}", error); + } + + restore_after_suspend(&mut stdout); +} + +// Set up terminal after SIGTSTP or SIGSTOP. +fn restore_after_suspend(stdout: &mut RawTerminal) { + 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); + }); +} + diff --git a/src/layout.rs b/src/layout.rs index b5527ab..ae95ba3 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use crate::common::*; +use crate::Config; +use crate::consts::*; // If screen size falls below these values we skip computation of new // positions. diff --git a/src/main.rs b/src/main.rs index 56ddb68..d1ddeb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,62 +1,30 @@ -extern crate termion; extern crate signal_hook; -extern crate unicode_segmentation; mod alarm; mod clock; -mod common; +mod consts; +mod kitchentimer; mod layout; +mod utils; #[cfg(test)] mod tests; -use std::{time, thread, env}; +use std::env; use std::io::Write; use std::sync::Arc; -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 std::sync::atomic::{AtomicBool, AtomicUsize}; +use signal_hook::flag; use clock::Clock; -use alarm::{Countdown, AlarmRoster, exec_command}; +use alarm::AlarmRoster; use layout::{Layout, Position}; -use common::{Config, unicode_length}; +use consts::*; +use kitchentimer::kitchentimer; -const NAME: &str = env!("CARGO_PKG_NAME"); -const VERSION: &str = env!("CARGO_PKG_VERSION"); -const USAGE: &str = concat!("USAGE: ", env!("CARGO_PKG_NAME"), -" [-h|-v] [-e|--exec COMMAND] [-p] [-q] [ALARM[/LABEL]] - -PARAMETERS: - [ALARM TIME[/LABEL]] Any number of alarm times (HH:MM:SS) with optional - label. - -OPTIONS: - -h, --help Show this help. - -v, --version Show version information. - -e, --exec [COMMAND] Execute COMMAND on alarm. Occurrences of {t} will - be replaced by the alarm time in (HH:)MM:SS format. - Occurrences of {l} by alarm label. - -p, --plain Use simpler block chars. - -q, --quit Quit program after last alarm. - -SIGNALS: Reset clock. - 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"; -const MENUBAR_INS: &str = -"Format: HH:MM:SS/LABEL [ENTER] Accept [ESC] Cancel [CTR-C] 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, + quit: bool, + command: Option>, +} fn main() { @@ -66,322 +34,24 @@ fn main() { command: None, }; let mut alarm_roster = AlarmRoster::new(); + // Parse command line arguments into config and alarm roster. parse_args(&mut config, &mut alarm_roster); - 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 buffer = String::new(); - let mut buffer_updated = false; - let mut countdown = Countdown::new(); - // True if in insert mode. - let mut insert_mode = false; - let mut update_menu = true; - // Child process of exec_command(). - let mut spawned: Option = None; - - // Initialise roster_width. - layout.set_roster_width(alarm_roster.width()); - // Register signal handlers. let signal = Arc::new(AtomicUsize::new(0)); - register_signal_handlers(&signal, &layout.force_recalc); - - // 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); - }); + let sigwinch = Arc::new(AtomicBool::new(true)); + register_signal_handlers(&signal, &sigwinch); + // Spawned child process if any. + let mut spawned: Option = None; - // 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, as we have already taken care to reset the - // terminal. - signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed); - layout.force_redraw = true; - // Jump to the start of the main loop. - continue; - }, - // Continuing after SIGSTOP. - SIGCONT => { - // This is reached when the process was suspended by SIGSTOP. - restore_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_recalc.store(true, Ordering::Relaxed); - layout.force_redraw = true; - }, - // (Un-)Pause clock on SIGUSR2. - SIGUSR2 => clock.toggle(), - // We didn't register anything else. - _ => unreachable!(), - } - - // Process input. - if let Some(key) = input_keys.next() { - match key.expect("Error reading input") { - // Enter. - Key::Char('\n') => { - if !buffer.is_empty() { - 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.set_roster_width(alarm_roster.width()); - layout.force_redraw = true; - } - buffer.clear(); - insert_mode = false; - update_menu = true; - } - }, - // Escape ^W, and ^U clear input buffer. - Key::Esc | Key::Ctrl('w') | Key::Ctrl('u') => { - buffer.clear(); - insert_mode = false; - update_menu = true; - layout.force_redraw = true; - buffer_updated = true; - }, - // Backspace. - Key::Backspace => { - // Delete last char in buffer. - if buffer.pop().is_some() { - if buffer.is_empty() { - insert_mode = false; - update_menu = true; - layout.force_redraw = true; - } - } - buffer_updated = true; - }, - // Forward every char if in insert mode. - Key::Char(c) if insert_mode => { - buffer.push(c); - buffer_updated = true; - }, - // Reset clock on 'r'. - Key::Char('r') => { - clock.reset(); - alarm_roster.reset_all(); - layout.force_recalc.store(true, Ordering::Relaxed); - 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.drop_last() { - // If we remove the last alarm we have to reset "countdown" - // manually. It is safe to do it anyway. - layout.set_roster_width(alarm_roster.width()); - 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, as we have already taken care to reset - // the terminal. - signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed); - layout.force_redraw = true; - // Jump to the start of the main loop. - continue; - }, - Key::Char(c) => { - if c.is_ascii_digit() { - buffer.push(c); - insert_mode = true; - update_menu = true; - layout.force_redraw = true; - buffer_updated = true; - } else if !buffer.is_empty() && c == ':' { - buffer.push(':'); - buffer_updated = true; - } - }, - // Any other key. - _ => (), - } - } - - // Update input buffer display. - if buffer_updated { - draw_buffer(&mut stdout, &mut 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); - } - - // 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).unwrap(); - 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).unwrap(); - - // 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,) - .unwrap(); - } - - 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)) - .unwrap(); - } - - // 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().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(); - - // Reset terminal. - drop(stdout); - drop(input_keys); + // Runs main loop. + kitchentimer( + config, + alarm_roster, + signal, + sigwinch, + &mut spawned, + ); // Wait for remaining spawned processes to exit. if let Some(ref mut child) = spawned { @@ -490,79 +160,3 @@ fn register_signal_handlers( flag::register(SIGWINCH as i32, Arc::clone(&recalc_flag)).unwrap(); } -// Prepare to suspend execution. Called on SIGTSTP. -fn suspend(mut 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); - }); - - if let Err(error) = low_level::emulate_default_handler(SIGTSTP as i32) { - eprintln!("Error raising SIGTSTP: {}", error); - } - - restore_after_suspend(&mut stdout); -} - -// Set up terminal after SIGTSTP or SIGSTOP. -fn restore_after_suspend(stdout: &mut RawTerminal) { - 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: &mut Layout, - buffer: &String, -) { - if !buffer.is_empty() { - write!(stdout, - "{}{}Add alarm: {}{}", - cursor::Goto(layout.buffer.col, layout.buffer.line), - clear::CurrentLine, - cursor::Show, - buffer) - .unwrap(); - layout.cursor.col = layout.buffer.col + 11 + unicode_length(buffer); - } else { - // Clear buffer display. - write!(stdout, - "{}{}{}", - cursor::Goto(layout.buffer.col, layout.buffer.line), - clear::CurrentLine, - cursor::Hide) - .unwrap(); - } -} - -// Draw error message. -fn error_msg(stdout: &mut RawTerminal, layout: &Layout, msg: &str) { - write!(stdout, - "{}{}{}{}{}", - cursor::Goto(layout.error.col, layout.error.line), - color::Fg(color::LightRed), - msg, - color::Fg(color::Reset), - cursor::Hide) - .unwrap(); -} - diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..5972a74 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,15 @@ +extern crate unicode_segmentation; +use unicode_segmentation::UnicodeSegmentation; + +pub fn unicode_length(input: &str) -> u16 { + let length = UnicodeSegmentation::graphemes(input, true).count(); + length as u16 +} + +pub fn unicode_truncate(input: &mut String, limit: usize) { + match UnicodeSegmentation::grapheme_indices(input.as_str(), true).nth(limit) { + Some((i, _)) => input.truncate(i), + None => (), + } +} +