From 3947c4de284cb82945055a0fe802c2755e951bb9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 12 Sep 2024 17:45:42 +0200 Subject: [PATCH] Pause input while running an exercise --- src/watch.rs | 23 +++++--------- src/watch/notify_event.rs | 4 +-- src/watch/state.rs | 24 ++++++++++++-- src/watch/terminal_event.rs | 63 ++++++++++++++++++++++++------------- 4 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index e910fb71..c937bfbe 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Error, Result}; +use anyhow::{Error, Result}; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, @@ -7,7 +7,6 @@ use std::{ io::{self, Write}, path::Path, sync::mpsc::channel, - thread, time::Duration, }; @@ -16,11 +15,7 @@ use crate::{ list, }; -use self::{ - notify_event::NotifyEventHandler, - state::WatchState, - terminal_event::{terminal_event_handler, InputEvent}, -}; +use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}; mod notify_event; mod state; @@ -47,7 +42,7 @@ fn run_watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, ) -> Result { - let (tx, rx) = channel(); + let (watch_event_sender, watch_event_receiver) = channel(); let mut manual_run = false; // Prevent dropping the guard until the end of the function. @@ -56,7 +51,7 @@ fn run_watch( let mut debouncer = new_debouncer( Duration::from_millis(200), NotifyEventHandler { - tx: tx.clone(), + sender: watch_event_sender.clone(), exercise_names, }, ) @@ -72,16 +67,12 @@ fn run_watch( None }; - let mut watch_state = WatchState::build(app_state, manual_run)?; - + let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run)?; let mut stdout = io::stdout().lock(); + watch_state.run_current_exercise(&mut stdout)?; - thread::Builder::new() - .spawn(move || terminal_event_handler(tx, manual_run)) - .context("Failed to spawn a thread to handle terminal events")?; - - while let Ok(event) = rx.recv() { + while let Ok(event) = watch_event_receiver.recv() { match event { WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? { ExercisesProgress::AllDone => break, diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 74716409..9b235259 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -4,7 +4,7 @@ use std::sync::mpsc::Sender; use super::WatchEvent; pub struct NotifyEventHandler { - pub tx: Sender, + pub sender: Sender, /// Used to report which exercise was modified. pub exercise_names: &'static [&'static [u8]], } @@ -47,6 +47,6 @@ impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { // An error occurs when the receiver is dropped. // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.tx.send(output_event); + let _ = self.sender.send(output_event); } } diff --git a/src/watch/state.rs b/src/watch/state.rs index e66cbeeb..6e760018 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -5,7 +5,11 @@ use crossterm::{ }, terminal, QueueableCommand, }; -use std::io::{self, StdoutLock, Write}; +use std::{ + io::{self, StdoutLock, Write}, + sync::mpsc::Sender, + thread, +}; use crate::{ app_state::{AppState, ExercisesProgress}, @@ -14,6 +18,11 @@ use crate::{ term::progress_bar, }; +use super::{ + terminal_event::{terminal_event_handler, InputPauseGuard}, + WatchEvent, +}; + #[derive(PartialEq, Eq)] enum DoneStatus { DoneWithSolution(String), @@ -31,11 +40,19 @@ pub struct WatchState<'a> { } impl<'a> WatchState<'a> { - pub fn build(app_state: &'a mut AppState, manual_run: bool) -> Result { + pub fn build( + app_state: &'a mut AppState, + watch_event_sender: Sender, + manual_run: bool, + ) -> Result { let term_width = terminal::size() .context("Failed to get the terminal size")? .0; + thread::Builder::new() + .spawn(move || terminal_event_handler(watch_event_sender, manual_run)) + .context("Failed to spawn a thread to handle terminal events")?; + Ok(Self { app_state, output: Vec::with_capacity(OUTPUT_CAPACITY), @@ -47,6 +64,9 @@ impl<'a> WatchState<'a> { } pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> { + // Ignore any input until running the exercise is done. + let _input_pause_guard = InputPauseGuard::scoped_pause(); + self.show_hint = false; writeln!( diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index ca3a8464..2a1dfdcf 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,8 +1,32 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use std::sync::mpsc::Sender; +use std::sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + mpsc::Sender, +}; use super::WatchEvent; +static INPUT_PAUSED: AtomicBool = AtomicBool::new(false); + +// Private unit type to force using the constructor function. +#[must_use = "When the guard is dropped, the input is unpaused"] +pub struct InputPauseGuard(()); + +impl InputPauseGuard { + #[inline] + pub fn scoped_pause() -> Self { + INPUT_PAUSED.store(true, Relaxed); + Self(()) + } +} + +impl Drop for InputPauseGuard { + #[inline] + fn drop(&mut self) { + INPUT_PAUSED.store(false, Relaxed); + } +} + pub enum InputEvent { Run, Next, @@ -11,46 +35,41 @@ pub enum InputEvent { Quit, } -pub fn terminal_event_handler(tx: Sender, manual_run: bool) { - let last_input_event = loop { - let terminal_event = match event::read() { - Ok(v) => v, - Err(e) => { - // If `send` returns an error, then the receiver is dropped and - // a shutdown has been already initialized. - let _ = tx.send(WatchEvent::TerminalEventErr(e)); - return; - } - }; - - match terminal_event { - Event::Key(key) => { +pub fn terminal_event_handler(sender: Sender, manual_run: bool) { + let last_watch_event = loop { + match event::read() { + Ok(Event::Key(key)) => { match key.kind { KeyEventKind::Release | KeyEventKind::Repeat => continue, KeyEventKind::Press => (), } + if INPUT_PAUSED.load(Relaxed) { + continue; + } + let input_event = match key.code { KeyCode::Char('n') => InputEvent::Next, KeyCode::Char('h') => InputEvent::Hint, - KeyCode::Char('l') => break InputEvent::List, - KeyCode::Char('q') => break InputEvent::Quit, + KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), + KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit), KeyCode::Char('r') if manual_run => InputEvent::Run, _ => continue, }; - if tx.send(WatchEvent::Input(input_event)).is_err() { + if sender.send(WatchEvent::Input(input_event)).is_err() { return; } } - Event::Resize(width, _) => { - if tx.send(WatchEvent::TerminalResize { width }).is_err() { + Ok(Event::Resize(width, _)) => { + if sender.send(WatchEvent::TerminalResize { width }).is_err() { return; } } - Event::FocusGained | Event::FocusLost | Event::Mouse(_) => continue, + Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue, + Err(e) => break WatchEvent::TerminalEventErr(e), } }; - let _ = tx.send(WatchEvent::Input(last_input_event)); + let _ = sender.send(last_watch_event); }