diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app_state.rs | 16 | ||||
| -rw-r--r-- | src/run.rs | 2 | ||||
| -rw-r--r-- | src/watch.rs | 11 | ||||
| -rw-r--r-- | src/watch/notify_event.rs | 91 | ||||
| -rw-r--r-- | src/watch/state.rs | 75 | ||||
| -rw-r--r-- | src/watch/terminal_event.rs | 28 |
6 files changed, 185 insertions, 38 deletions
diff --git a/src/app_state.rs b/src/app_state.rs index ecb4689..c879955 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -381,7 +381,7 @@ impl AppState { // Return the exercise index of the first pending exercise found. fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> { - stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + stdout.write_all(FINAL_CHECK_MSG)?; let n_exercises = self.exercises.len(); let status = thread::scope(|s| { @@ -441,7 +441,10 @@ impl AppState { /// Mark the current exercise as done and move on to the next pending exercise if one exists. /// If all exercises are marked as done, run all of them to make sure that they are actually /// done. If an exercise which is marked as done fails, mark it as pending and continue on it. - pub fn done_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> { + pub fn done_current_exercise<const CLEAR_BEFORE_FINAL_CHECK: bool>( + &mut self, + stdout: &mut StdoutLock, + ) -> Result<ExercisesProgress> { let exercise = &mut self.exercises[self.current_exercise_ind]; if !exercise.done { exercise.done = true; @@ -453,6 +456,12 @@ impl AppState { return Ok(ExercisesProgress::NewPending); } + if CLEAR_BEFORE_FINAL_CHECK { + clear_terminal(stdout)?; + } else { + stdout.write_all(b"\n")?; + } + if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? { stdout.write_all(b"\n\n")?; @@ -482,8 +491,7 @@ impl AppState { const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; -const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" -All exercises seem to be done. +const FINAL_CHECK_MSG: &[u8] = b"All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. "; const FENISH_LINE: &str = "+----------------------------------------------------+ @@ -44,7 +44,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { stdout.write_all(b"\n")?; } - match app_state.done_current_exercise(&mut stdout)? { + match app_state.done_current_exercise::<false>(&mut stdout)? { ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => { stdout.write_all(b"Next exercise: ")?; app_state diff --git a/src/watch.rs b/src/watch.rs index fd89b29..35533b0 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -69,11 +69,11 @@ fn run_watch( // Prevent dropping the guard until the end of the function. // Otherwise, the file watcher exits. let _watcher_guard = if let Some(exercise_names) = notify_exercise_names { + let notify_event_handler = + NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?; + let mut watcher = RecommendedWatcher::new( - NotifyEventHandler { - sender: watch_event_sender.clone(), - exercise_names, - }, + notify_event_handler, Config::default().with_poll_interval(Duration::from_secs(1)), ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; @@ -100,13 +100,14 @@ fn run_watch( ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?, ExercisesProgress::CurrentPending => (), }, + WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List), + WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Quit) => { stdout.write_all(QUIT_MSG)?; break; } - WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, WatchEvent::FileChange { exercise_ind } => { watch_state.handle_file_change(exercise_ind, &mut stdout)?; } diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 5ed8fd1..2051e54 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,15 +1,71 @@ +use anyhow::{Context, Result}; use notify::{ - event::{MetadataKind, ModifyKind}, + event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode}, Event, EventKind, }; -use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; +use std::{ + sync::{ + atomic::Ordering::Relaxed, + mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender}, + }, + thread, + time::Duration, +}; use super::{WatchEvent, EXERCISE_RUNNING}; +const DEBOUNCE_DURATION: Duration = Duration::from_millis(200); + pub struct NotifyEventHandler { - pub sender: Sender<WatchEvent>, - /// Used to report which exercise was modified. - pub exercise_names: &'static [&'static [u8]], + error_sender: Sender<WatchEvent>, + // Sends the index of the updated exercise. + update_sender: SyncSender<usize>, + // Used to report which exercise was modified. + exercise_names: &'static [&'static [u8]], +} + +impl NotifyEventHandler { + pub fn build( + watch_event_sender: Sender<WatchEvent>, + exercise_names: &'static [&'static [u8]], + ) -> Result<Self> { + let (update_sender, update_receiver) = sync_channel(0); + let error_sender = watch_event_sender.clone(); + + // Debouncer + thread::Builder::new() + .spawn(move || { + let mut exercise_updated = vec![false; exercise_names.len()]; + + loop { + match update_receiver.recv_timeout(DEBOUNCE_DURATION) { + Ok(exercise_ind) => exercise_updated[exercise_ind] = true, + Err(RecvTimeoutError::Timeout) => { + for (exercise_ind, updated) in exercise_updated.iter_mut().enumerate() { + if *updated { + if watch_event_sender + .send(WatchEvent::FileChange { exercise_ind }) + .is_err() + { + break; + } + + *updated = false; + } + } + } + Err(RecvTimeoutError::Disconnected) => break, + } + } + }) + .context("Failed to spawn a thread to debounce file changes")?; + + Ok(Self { + error_sender, + update_sender, + exercise_names, + }) + } } impl notify::EventHandler for NotifyEventHandler { @@ -22,8 +78,8 @@ impl notify::EventHandler for NotifyEventHandler { Ok(v) => v, Err(e) => { // An error occurs when the receiver is dropped. - // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.sender.send(WatchEvent::NotifyErr(e)); + // After dropping the receiver, the watcher guard should also be dropped. + let _ = self.error_sender.send(WatchEvent::NotifyErr(e)); return; } }; @@ -32,6 +88,10 @@ impl notify::EventHandler for NotifyEventHandler { EventKind::Any => (), EventKind::Modify(modify_kind) => match modify_kind { ModifyKind::Any | ModifyKind::Data(_) => (), + ModifyKind::Name(rename_mode) => match rename_mode { + RenameMode::Any | RenameMode::To => (), + RenameMode::From | RenameMode::Both | RenameMode::Other => return, + }, ModifyKind::Metadata(metadata_kind) => match metadata_kind { MetadataKind::Any | MetadataKind::WriteTime => (), MetadataKind::AccessTime @@ -40,12 +100,17 @@ impl notify::EventHandler for NotifyEventHandler { | MetadataKind::Extended | MetadataKind::Other => return, }, - ModifyKind::Name(_) | ModifyKind::Other => return, + ModifyKind::Other => return, + }, + EventKind::Access(access_kind) => match access_kind { + AccessKind::Any => (), + AccessKind::Close(access_mode) => match access_mode { + AccessMode::Any | AccessMode::Write => (), + AccessMode::Execute | AccessMode::Read | AccessMode::Other => return, + }, + AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return, }, - EventKind::Access(_) - | EventKind::Create(_) - | EventKind::Remove(_) - | EventKind::Other => return, + EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return, } let _ = input_event @@ -62,6 +127,6 @@ impl notify::EventHandler for NotifyEventHandler { .iter() .position(|exercise_name| *exercise_name == file_name_without_ext) }) - .try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind })); + .try_for_each(|exercise_ind| self.update_sender.send(exercise_ind)); } } diff --git a/src/watch/state.rs b/src/watch/state.rs index cb79b35..19910f0 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -6,8 +6,8 @@ use crossterm::{ terminal, QueueableCommand, }; use std::{ - io::{self, StdoutLock, Write}, - sync::mpsc::Sender, + io::{self, Read, StdoutLock, Write}, + sync::mpsc::{sync_channel, Sender, SyncSender}, thread, }; @@ -34,6 +34,7 @@ pub struct WatchState<'a> { done_status: DoneStatus, manual_run: bool, term_width: u16, + terminal_event_unpause_sender: SyncSender<()>, } impl<'a> WatchState<'a> { @@ -46,8 +47,16 @@ impl<'a> WatchState<'a> { .context("Failed to get the terminal size")? .0; + let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0); + thread::Builder::new() - .spawn(move || terminal_event_handler(watch_event_sender, manual_run)) + .spawn(move || { + terminal_event_handler( + watch_event_sender, + terminal_event_unpause_receiver, + manual_run, + ) + }) .context("Failed to spawn a thread to handle terminal events")?; Ok(Self { @@ -57,6 +66,7 @@ impl<'a> WatchState<'a> { done_status: DoneStatus::Pending, manual_run, term_width, + terminal_event_unpause_sender, }) } @@ -95,6 +105,44 @@ impl<'a> WatchState<'a> { Ok(()) } + pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> { + clear_terminal(stdout)?; + + stdout.write_all(b"Resetting will undo all your changes to the file ")?; + stdout.write_all(self.app_state.current_exercise().path.as_bytes())?; + stdout.write_all(b"\nReset (y/n)? ")?; + stdout.flush()?; + + { + let mut stdin = io::stdin().lock(); + let mut answer = [0]; + loop { + stdin + .read_exact(&mut answer) + .context("Failed to read the user's input")?; + + match answer[0] { + b'y' | b'Y' => { + self.app_state.reset_current_exercise()?; + + // The file watcher reruns the exercise otherwise. + if self.manual_run { + self.run_current_exercise(stdout)?; + } + } + b'n' | b'N' => self.render(stdout)?, + _ => continue, + } + + break; + } + } + + self.terminal_event_unpause_sender.send(())?; + + Ok(()) + } + pub fn handle_file_change( &mut self, exercise_ind: usize, @@ -113,17 +161,10 @@ impl<'a> WatchState<'a> { return Ok(ExercisesProgress::CurrentPending); } - self.app_state.done_current_exercise(stdout) + self.app_state.done_current_exercise::<true>(stdout) } fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> { - if self.manual_run { - stdout.queue(SetAttribute(Attribute::Bold))?; - stdout.write_all(b"r")?; - stdout.queue(ResetColor)?; - stdout.write_all(b":run / ")?; - } - if self.done_status != DoneStatus::Pending { stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"n")?; @@ -135,6 +176,13 @@ impl<'a> WatchState<'a> { stdout.write_all(b" / ")?; } + if self.manual_run { + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"r")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":run / ")?; + } + if !self.show_hint { stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"h")?; @@ -148,6 +196,11 @@ impl<'a> WatchState<'a> { stdout.write_all(b":list / ")?; stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"x")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":reset / ")?; + + stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"q")?; stdout.queue(ResetColor)?; stdout.write_all(b":quit ? ")?; diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 050c4ac..1ed681d 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,17 +1,25 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; +use std::sync::{ + atomic::Ordering::Relaxed, + mpsc::{Receiver, Sender}, +}; use super::{WatchEvent, EXERCISE_RUNNING}; pub enum InputEvent { - Run, Next, + Run, Hint, List, + Reset, Quit, } -pub fn terminal_event_handler(sender: Sender<WatchEvent>, manual_run: bool) { +pub fn terminal_event_handler( + sender: Sender<WatchEvent>, + unpause_receiver: Receiver<()>, + manual_run: bool, +) { let last_watch_event = loop { match event::read() { Ok(Event::Key(key)) => { @@ -26,10 +34,22 @@ pub fn terminal_event_handler(sender: Sender<WatchEvent>, manual_run: bool) { let input_event = match key.code { KeyCode::Char('n') => InputEvent::Next, + KeyCode::Char('r') if manual_run => InputEvent::Run, KeyCode::Char('h') => InputEvent::Hint, KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), + KeyCode::Char('x') => { + if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() { + return; + } + + // Pause input until quitting the confirmation prompt. + if unpause_receiver.recv().is_err() { + return; + }; + + continue; + } KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit), - KeyCode::Char('r') if manual_run => InputEvent::Run, _ => continue, }; |
