diff --git a/src/alarm.rs b/src/alarm.rs index 05b711d..e310575 100644 --- a/src/alarm.rs +++ b/src/alarm.rs @@ -31,6 +31,18 @@ impl Countdown { self.position = None; } + pub fn set(&mut self, value: u32) { + self.value = value; + } + + pub fn set_position(&mut self, position: Position) { + self.position = Some(position); + } + + pub fn has_position(&self) -> bool { + self.position.is_some() + } + // Draw countdown. pub fn draw(&self, stdout: &mut RawTerminal) -> Result<(), std::io::Error> @@ -170,6 +182,7 @@ impl AlarmRoster { clock: &mut Clock, layout: &Layout, countdown: &mut Countdown, + force_redraw: bool, ) -> Option<(u32, &String)> { let mut ret = None; @@ -189,8 +202,8 @@ impl AlarmRoster { continue; } // Reached the alarm to exceed next. Update countdown accordingly. - countdown.value = alarm.time - clock.elapsed; - if countdown.position.is_none() || layout.force_redraw { + countdown.set(alarm.time - clock.elapsed); + if !countdown.has_position() || force_redraw { // Compute position. let mut col = layout.roster.col @@ -210,7 +223,7 @@ impl AlarmRoster { .unwrap_or(layout.roster.line); } } - countdown.position = Some(Position { col, line, }); + countdown.set_position(Position { col, line }); } // Ignore other alarms. break; @@ -317,6 +330,12 @@ impl AlarmRoster { } } + pub fn time_travel(&mut self, time: u32) { + for alarm in self.list.iter_mut() { + alarm.exceeded = alarm.time <= time; + } + } + // Read alarm times from stdin. pub fn from_stdin(&mut self, stdin: std::io::Stdin) -> Result<(), String> diff --git a/src/clock.rs b/src/clock.rs index 5d4cd82..398dd47 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -8,12 +8,18 @@ use crate::consts::COLOR; use crate::Config; use crate::layout::{Layout, Position}; +enum Pause { + Instant(time::Instant), + Secs(u32), + None, +} + pub struct Clock { pub start: time::Instant, pub elapsed: u32, pub days: u32, pub paused: bool, - paused_at: Option, + paused_at: Pause, pub color_index: Option, pub font: &'static font::Font, } @@ -25,7 +31,7 @@ impl Clock { elapsed: 0, days: 0, paused: false, - paused_at: None, + paused_at: Pause::None, color_index: None, font: config.font, } @@ -44,19 +50,24 @@ impl Clock { } fn pause(&mut self) { - self.paused_at = Some(time::Instant::now()); + self.paused_at = Pause::Instant(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; - } + match self.paused_at { + Pause::Instant(delay) => { + if let Some(start) = self.start.checked_add(delay.elapsed()) { + self.start = start; + } + }, + Pause::Secs(secs) => { + self.start = time::Instant::now() - time::Duration::from_secs(secs as u64); + }, + Pause::None => (), // O_o } - self.paused_at = None; + self.paused_at = Pause::None; self.paused = false; } @@ -68,6 +79,16 @@ impl Clock { } } + pub fn shift(&mut self, shift: i32) { + let secs = if shift.is_negative() { + self.elapsed.saturating_sub(shift.abs() as u32) + } else { + self.elapsed.saturating_add(shift as u32) + }; + self.paused_at = Pause::Secs(secs); + self.elapsed = secs; + } + pub fn get_width(&self) -> u16 { if self.elapsed >= 3600 { // Hours @@ -95,6 +116,7 @@ impl Clock { &self, mut stdout: &mut RawTerminal, layout: &Layout, + force_redraw: bool, ) -> Result<(), std::io::Error> { // Setup style and color if appropriate. @@ -106,7 +128,7 @@ impl Clock { } // Run once every hour or on request. - if layout.force_redraw || self.elapsed % 3600 == 0 { + if force_redraw || self.elapsed % 3600 == 0 { // Draw hours if necessary. if self.elapsed >= 3600 { self.draw_digit_pair( @@ -142,7 +164,7 @@ impl Clock { } // Draw minutes if necessary. Once every minute or on request. - if layout.force_redraw || self.elapsed % 60 == 0 { + if force_redraw || self.elapsed % 60 == 0 { self.draw_digit_pair( &mut stdout, (self.elapsed % 3600) / 60, @@ -151,7 +173,7 @@ impl Clock { } // Draw colon if necessary. - if layout.force_redraw { + if force_redraw { self.draw_colon( &mut stdout, &layout.clock_colon0, diff --git a/src/consts.rs b/src/consts.rs index 461d5a6..b670ceb 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -39,5 +39,7 @@ SIGNALS: Reset clock. "[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 const MENUBAR_PAUSED: &str = + "[SPACE] Continue [r] Reset [UP]/[DOWN] Set clock"; } diff --git a/src/layout.rs b/src/layout.rs index 03ab1cd..1e24390 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -7,7 +7,7 @@ pub struct Position { pub struct Layout { pub force_redraw: bool, // Redraw elements on screen. - pub force_recalc: bool, // Recalculate position of elements. + force_recalc: bool, // Recalculate position of elements. pub width: u16, pub height: u16, clock_width: u16, @@ -48,8 +48,9 @@ impl Layout { } } + // Update layout. Returns true when changes were made. pub fn update(&mut self, clock: &Clock, force: bool) - -> Result<(), std::io::Error> + -> Result { if self.force_recalc || force { self.force_recalc = false; @@ -60,9 +61,14 @@ impl Layout { self.clock_height = clock.font.height; self.digit_width = clock.font.width; self.compute(clock.elapsed >= 3600); - self.force_redraw = true; + Ok(true) + } else { + Ok(false) } - Ok(()) + } + + pub fn schedule_recalc(&mut self) { + self.force_recalc = true; } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index a71e987..c7574cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub fn run( let mut input_keys = async_stdin.keys(); let stdout = std::io::stdout(); let mut stdout = stdout.lock().into_raw_mode()?; + let mut force_redraw = true; // Register signals. let mut signals = Signals::new(&[ @@ -65,22 +66,22 @@ pub fn run( // Continuing after SIGTSTP or SIGSTOP. SIGCONT => { restore_after_suspend(&mut stdout)?; - layout.force_redraw = true; + force_redraw = true; }, - SIGWINCH => layout.force_recalc = true, + SIGWINCH => layout.schedule_recalc(), // Exit main loop on SIGTERM and SIGINT. SIGTERM | SIGINT => break 'outer, // Reset clock on SIGUSR1. SIGUSR1 => { clock.reset(); alarm_roster.reset_all(); - layout.force_recalc = true; - layout.force_redraw = true; + layout.schedule_recalc(); + force_redraw = true; }, // (Un-)Pause clock on SIGUSR2. SIGUSR2 => { clock.toggle(); - layout.force_redraw = true; + force_redraw = true; }, // We didn't register anything else. _ => unreachable!(), @@ -98,7 +99,7 @@ pub fn run( // Conditional inner loop. Runs once every second or when explicitly // requested. - if elapsed != clock.elapsed || layout.force_redraw { + if elapsed != clock.elapsed || force_redraw { // Update clock. Advance one day after 24 hours. if elapsed < 24 * 60 * 60 { clock.elapsed = elapsed; @@ -106,36 +107,49 @@ pub fn run( clock.next_day(); // "clock.elapsed" set by "clock.next_day()". alarm_roster.reset_all(); - layout.force_recalc = true; + layout.schedule_recalc(); } // Update window size information and calculate the clock position. // Also enforce recalculation of layout if we start displaying // hours. - layout.update(&clock, clock.elapsed == 3600)?; + match layout.update(&clock, clock.elapsed == 3600) { + Ok(true) => force_redraw = true, + Ok(false) => (), + Err(e) => return Err(e), + } // 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; + if let Some((time, label)) = alarm_roster.check( + &mut clock, + &layout, + &mut countdown, + force_redraw) + { + // Do not react to exceeded alarms if the clock is paused. + if !clock.paused { + force_redraw = true; - match config.command { - // Run command if configured and no command is running. - Some(ref command) if spawned.is_none() => { - *spawned = exec_command(command, time, &label); - }, - // Last command is still running. - Some(_) => eprintln!("Not executing command, as its predecessor is still running"), - None => (), + // Write ASCII bell code. + write!(stdout, "{}", 0x07 as char)?; + + match config.command { + // Run command if configured and no command is running. + Some(ref command) if spawned.is_none() => { + *spawned = exec_command(command, time, &label); + }, + // Last command is still running. + Some(_) => eprintln!("Not executing command, as its predecessor is still running"), + None => (), + } + // Quit if configured. + if config.quit && alarm_roster.idle() { break }; } - // Quit if configured. - if config.quit && alarm_roster.idle() { break }; } // Clear the window and redraw menu bar, alarm roster and buffer if // requested. - if layout.force_redraw { + if force_redraw { // Write menu at the top. write!(stdout, "{}{}{}{}{}", @@ -144,6 +158,7 @@ pub fn run( // Switch menu bars. Use a compressed version or none at // all if necessary. match buffer.visible { + _ if clock.paused && layout.can_hold(MENUBAR_PAUSED) => MENUBAR_PAUSED, 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, @@ -162,7 +177,7 @@ pub fn run( buffer.draw(&mut stdout, &mut layout)?; } - clock.draw(&mut stdout, &layout)?; + clock.draw(&mut stdout, &layout, force_redraw)?; // Display countdown. if countdown.value > 0 { @@ -191,7 +206,7 @@ pub fn run( // End of conditional inner loop. // Reset redraw_all and flush stdout. - layout.force_redraw = false; + force_redraw = false; stdout.flush()?; } @@ -216,21 +231,21 @@ pub fn run( } buffer.clear(); buffer.visible = false; - layout.force_redraw = true; + force_redraw = true; } }, // Escape and ^U clear input buffer. Key::Esc | Key::Ctrl('u') => { buffer.reset(); buffer.visible = false; - layout.force_redraw = true; + force_redraw = true; }, // ^W removes last word. Key::Ctrl('w') => { buffer.strip_word(); if buffer.is_empty() { buffer.visible = false; - layout.force_redraw = true; + force_redraw = true; } }, // Backspace. @@ -239,9 +254,25 @@ pub fn run( buffer.strip_char(); if buffer.is_empty() { buffer.visible = false; - layout.force_redraw = true; + force_redraw = true; } }, + // Set clock. + Key::Up if clock.paused => { + clock.shift(10); + // We would very likely not detect us passing the hour + // barrier and would panic when trying to draw hours + // without position if we do not schedule a recalculation + // here. + layout.schedule_recalc(); + force_redraw = true; + }, + Key::Down if clock.paused => { + clock.shift(-10); + alarm_roster.time_travel(clock.elapsed); + layout.schedule_recalc(); + force_redraw = true; + }, // Forward every char if in insert mode. Key::Char(c) if buffer.visible => { buffer.push(c); @@ -250,18 +281,18 @@ pub fn run( Key::Char('r') => { clock.reset(); alarm_roster.reset_all(); - layout.force_recalc = true; - layout.force_redraw = true; + layout.schedule_recalc(); + force_redraw = true; }, // (Un-)Pause on space. Key::Char(' ') => { clock.toggle(); - layout.force_redraw = true; + force_redraw = true; }, // Clear clock color on 'c'. Key::Char('c') => { clock.color_index = None; - layout.force_redraw = true; + force_redraw = true; }, // Delete last alarm on 'd'. Key::Char('d') => { @@ -270,20 +301,20 @@ pub fn run( // manually. It is safe to do it anyway. layout.set_roster_width(alarm_roster.width()); countdown.reset(); - layout.force_redraw = true; + 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, + Key::Ctrl('r') => 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; + force_redraw = true; // Jump to the start of the main loop. continue; }, @@ -291,7 +322,7 @@ pub fn run( if c.is_ascii_digit() { buffer.push(c); buffer.visible = true; - layout.force_redraw = true; + force_redraw = true; } else if !buffer.is_empty() && c == ':' { buffer.push(':'); } @@ -300,7 +331,7 @@ pub fn run( _ => (), } } else { - // Main loop delay. + // Main loop delay. Skipped after key press. thread::sleep(time::Duration::from_millis(100)); } }