diff options
Diffstat (limited to 'src/watch')
| -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 |
3 files changed, 166 insertions, 28 deletions
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, }; |
