diff --git a/Cargo.lock b/Cargo.lock index d5391c2..5c43208 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,13 +12,14 @@ version = "0.0.1" dependencies = [ "signal-hook", "termion", + "unicode-segmentation", ] [[package]] name = "libc" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" [[package]] name = "numtoa" @@ -74,3 +75,9 @@ dependencies = [ "redox_syscall", "redox_termios", ] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" diff --git a/Cargo.toml b/Cargo.toml index c626c22..4882fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ edition = "2018" [dependencies] termion = "1.5.6" signal-hook = "0.3.8" +unicode-segmentation = "1.7.1" diff --git a/src/alarm.rs b/src/alarm.rs index d58edbf..b0593df 100644 --- a/src/alarm.rs +++ b/src/alarm.rs @@ -2,8 +2,8 @@ use std::io::Write; use std::process::{Command, Stdio, Child}; use termion::{color, cursor, style}; use termion::raw::RawTerminal; -use crate::{Clock, Config, Layout, Position}; -use crate::common::COLOR; +use crate::{Clock, Layout, Position}; +use crate::common::{COLOR, Config, str_length}; pub struct Countdown { @@ -55,7 +55,7 @@ impl Countdown { pub struct Alarm { time: u32, - display: String, + label: String, color_index: usize, exceeded: bool, } @@ -78,15 +78,25 @@ impl AlarmRoster { } // Parse string and add as alarm. - pub fn add(&mut self, buffer: &String) - -> Result<(), &str> { - + pub fn add(&mut self, input: &String) -> Result<(), &str> { let mut index = 0; let mut time: u32 = 0; + let mut label: String; + let time_str: &str; + + if let Some(i) = input.find('/') { + label = input[(i + 1)..].trim().to_string(); + // TODO: Make decision yes/no. + //label.truncate(24); + time_str = &input[..i].trim(); + } else { + label = input.clone(); + time_str = &input.trim(); + } // Parse input into seconds. - if buffer.find(':').is_some() { - for sub in buffer.rsplit(':') { + if time_str.contains(':') { + for sub in time_str.rsplit(':') { if !sub.is_empty() { match sub.parse::() { // Valid. @@ -101,9 +111,9 @@ impl AlarmRoster { } } else { // Parse as seconds only. - match buffer.parse::() { + match time_str.parse::() { Ok(d) => time = d, - Err(_) => return Err("Could not parse as ."), + Err(_) => return Err("Could not parse as integer."), } } @@ -111,11 +121,9 @@ 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(); - + label.shrink_to_fit(); let alarm = Alarm { - display, + label, time, color_index: (self.list.len() % COLOR.len()), exceeded: false, @@ -181,7 +189,7 @@ impl AlarmRoster { let mut col = layout.roster.col + 3 - + alarm.display.len() as u16; + + str_length(&alarm.label); let mut line = layout.roster.line + index; // Compensate for "hidden" items in the alarm roster. @@ -240,7 +248,7 @@ impl AlarmRoster { color::Bg(COLOR[alarm.color_index]), color::Bg(color::Reset), style::Bold, - alarm.display, + alarm.label, style::Reset) .unwrap(); } else { @@ -249,7 +257,7 @@ impl AlarmRoster { cursor::Goto(layout.roster.col, layout.roster.line + index), color::Bg(COLOR[alarm.color_index]), color::Bg(color::Reset), - alarm.display) + alarm.label) .unwrap(); } index += 1; @@ -260,7 +268,8 @@ impl AlarmRoster { pub fn width(&self) -> u16 { let mut width: u16 = 0; for alarm in &self.list { - if alarm.display.len() as u16 > width { width = alarm.display.len() as u16; } + let length = str_length(&alarm.label); + if length > width { width = length }; } // Actual width is 3 columns wider if it's not 0. if width == 0 { 0 } else { width.saturating_add(3) } diff --git a/src/common.rs b/src/common.rs index fb3e8e8..9ef7777 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,5 +1,18 @@ +use unicode_segmentation::UnicodeSegmentation; use termion::color; + +pub struct Config { + pub plain: bool, + pub quit: bool, + pub command: Option>, +} + +pub fn str_length(input: &str) -> u16 { + let length = UnicodeSegmentation::graphemes(input, true).count(); + length as u16 +} + pub const COLOR: [&dyn color::Color; 6] = [ &color::Cyan, &color::Magenta, diff --git a/src/layout.rs b/src/layout.rs index 7c5548d..b5527ab 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,6 +1,5 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use crate::Config; use crate::common::*; // If screen size falls below these values we skip computation of new @@ -30,6 +29,7 @@ pub struct Layout { pub roster_height: u16, pub buffer: Position, pub error: Position, + pub cursor: Position, } impl Layout { @@ -52,6 +52,7 @@ impl Layout { roster_height: 0, buffer: Position {col: 0, line: 0}, error: Position {col: 0, line: 0}, + cursor: Position {col: 1, line: 1}, } } @@ -80,6 +81,11 @@ impl Layout { self.compute(hours); } + pub fn can_hold(&self, other: &str) -> bool { + // Only valid for ascii strings. + self.width >= other.len() as u16 + } + // Compute the position of various elements based on the size of the // terminal. fn compute(&mut self, display_hours: bool) { @@ -140,6 +146,9 @@ impl Layout { line: self.height, col: 12, }; + + // Cursor. Column will be set by main loop. + self.cursor.line = self.buffer.line; } pub fn set_roster_width(&mut self, width: u16) { diff --git a/src/main.rs b/src/main.rs index 5858ca9..e746f46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ extern crate termion; extern crate signal_hook; +extern crate unicode_segmentation; mod alarm; mod clock; mod common; @@ -19,6 +20,7 @@ use termion::input::TermRead; use clock::Clock; use alarm::{Countdown, AlarmRoster, exec_command}; use layout::{Layout, Position}; +use common::{Config, str_length}; const NAME: &str = env!("CARGO_PKG_NAME"); @@ -44,6 +46,8 @@ 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; @@ -53,12 +57,6 @@ 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() { let mut config = Config { @@ -78,8 +76,11 @@ fn main() { let mut layout = Layout::new(&config); let mut clock = Clock::new(); let mut buffer = String::new(); - let mut buffer_updated: bool = false; + 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; @@ -140,6 +141,47 @@ fn main() { // 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(); @@ -180,33 +222,12 @@ fn main() { // Jump to the start of the main loop. continue; }, - // 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(); - } - }, - // 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. - if buffer.pop().is_some() { buffer_updated = true }; - }, 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(':'); @@ -220,7 +241,7 @@ fn main() { // Update input buffer display. if buffer_updated { - draw_buffer(&mut stdout, &layout, &buffer); + draw_buffer(&mut stdout, &mut layout, &buffer); buffer_updated = false; stdout.flush().unwrap(); } @@ -274,27 +295,34 @@ fn main() { // 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(); + write!(stdout, "{}", clear::All).unwrap(); // Redraw list of alarms. alarm_roster.draw(&mut stdout, &mut layout); // Redraw buffer. - draw_buffer(&mut stdout, &layout, &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); @@ -304,6 +332,15 @@ fn main() { 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() { @@ -493,22 +530,25 @@ fn restore_after_suspend(stdout: &mut RawTerminal) { // Draw input buffer. fn draw_buffer( stdout: &mut RawTerminal, - layout: &Layout, + layout: &mut Layout, buffer: &String, ) { if !buffer.is_empty() { write!(stdout, - "{}{}Add alarm: {}", + "{}{}Add alarm: {}{}", cursor::Goto(layout.buffer.col, layout.buffer.line), clear::CurrentLine, + cursor::Show, buffer) .unwrap(); + layout.cursor.col = layout.buffer.col + 11 + str_length(buffer); } else { // Clear buffer display. write!(stdout, - "{}{}", + "{}{}{}", cursor::Goto(layout.buffer.col, layout.buffer.line), - clear::CurrentLine) + clear::CurrentLine, + cursor::Hide) .unwrap(); } } @@ -516,11 +556,12 @@ fn draw_buffer( // 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)) + color::Fg(color::Reset), + cursor::Hide) .unwrap(); }