diff options
| author | mo8it <mo8it@proton.me> | 2024-04-09 21:07:53 +0200 |
|---|---|---|
| committer | mo8it <mo8it@proton.me> | 2024-04-09 21:07:53 +0200 |
| commit | f0ce2c1afa21fdaa34aed8f21c1ef4d3c47cebdd (patch) | |
| tree | bf428b63104f597cb3ab5c4fd60b49c5b47ee809 | |
| parent | 850c1d0234b2c1ae09a8f1c8f669e23a324fd644 (diff) | |
Improve event handling in the watch mode
| -rw-r--r-- | src/main.rs | 5 | ||||
| -rw-r--r-- | src/watch.rs | 150 | ||||
| -rw-r--r-- | src/watch/state.rs | 73 |
3 files changed, 133 insertions, 95 deletions
diff --git a/src/main.rs b/src/main.rs index 356b77c..6af66bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,7 +85,8 @@ Did you already install Rust? Try running `cargo --version` to diagnose the problem.", )?; - let exercises = InfoFile::parse()?.exercises; + // Leaking is not a problem since the exercises are used until the end of the program. + let exercises = InfoFile::parse()?.exercises.leak(); if matches!(args.command, Some(Subcommands::Init)) { init::init(&exercises).context("Initialization failed")?; @@ -110,7 +111,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini match args.command { None | Some(Subcommands::Watch) => { - watch::watch(&state_file, &exercises)?; + watch::watch(&state_file, exercises)?; } // `Init` is handled above. Some(Subcommands::Init) => (), diff --git a/src/watch.rs b/src/watch.rs index 967f98c..abf4002 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,9 +1,11 @@ -use anyhow::Result; -use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; +use anyhow::{bail, Context, Result}; +use notify_debouncer_mini::{ + new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, +}; use std::{ io::{self, BufRead, Write}, path::Path, - sync::mpsc::{channel, sync_channel}, + sync::mpsc::{channel, Sender}, thread, time::Duration, }; @@ -14,70 +16,130 @@ use crate::{exercise::Exercise, state_file::StateFile}; use self::state::WatchState; -enum Event { +enum InputEvent { Hint, Clear, Quit, + Unrecognized, } -pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { - let (tx, rx) = channel(); - let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; - debouncer - .watcher() - .watch(Path::new("exercises"), RecursiveMode::Recursive)?; +enum WatchEvent { + Input(InputEvent), + FileChange { exercise_ind: usize }, + TerminalResize, +} - let mut watch_state = WatchState::new(state_file, exercises, rx); +struct DebouceEventHandler { + tx: Sender<WatchEvent>, + exercises: &'static [Exercise], +} - watch_state.run_exercise()?; - watch_state.render()?; +impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { + fn handle_event(&mut self, event: DebounceEventResult) { + let Ok(event) = event else { + // TODO + return; + }; + + let Some(exercise_ind) = event + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } - let (tx, rx) = sync_channel(0); - thread::spawn(move || { - let mut stdin = io::stdin().lock(); - let mut stdin_buf = String::with_capacity(8); + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + else { + return; + }; - loop { - stdin.read_line(&mut stdin_buf).unwrap(); + self.tx.send(WatchEvent::FileChange { exercise_ind }); + } +} - let event = match stdin_buf.trim() { - "h" | "hint" => Some(Event::Hint), - "c" | "clear" => Some(Event::Clear), - "q" | "quit" => Some(Event::Quit), - _ => None, - }; +fn input_handler(tx: Sender<WatchEvent>) -> Result<()> { + let mut stdin = io::stdin().lock(); + let mut stdin_buf = String::with_capacity(8); + + loop { + stdin + .read_line(&mut stdin_buf) + .context("Failed to read the user's input from stdin")?; - stdin_buf.clear(); + let event = match stdin_buf.trim() { + "h" | "hint" => InputEvent::Hint, + "c" | "clear" => InputEvent::Clear, + "q" | "quit" => InputEvent::Quit, + _ => InputEvent::Unrecognized, + }; - if tx.send(event).is_err() { - break; - }; + stdin_buf.clear(); + + if tx.send(WatchEvent::Input(event)).is_err() { + return Ok(()); } - }); + } +} - loop { - watch_state.try_recv_event()?; +pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> { + let (tx, rx) = channel(); + let mut debouncer = new_debouncer( + Duration::from_secs(1), + DebouceEventHandler { + tx: tx.clone(), + exercises, + }, + )?; + debouncer + .watcher() + .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - if let Ok(event) = rx.try_recv() { - match event { - Some(Event::Hint) => { - watch_state.show_hint()?; - } - Some(Event::Clear) => { - watch_state.render()?; - } - Some(Event::Quit) => break, - None => { - watch_state.handle_invalid_cmd()?; - } + let mut watch_state = WatchState::new(state_file, exercises); + + // TODO: bool + watch_state.run_exercise()?; + watch_state.render()?; + + let input_thread = thread::spawn(move || input_handler(tx)); + + while let Ok(event) = rx.recv() { + match event { + WatchEvent::Input(InputEvent::Hint) => { + watch_state.show_hint()?; + } + WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => { + watch_state.render()?; + } + WatchEvent::Input(InputEvent::Quit) => break, + WatchEvent::Input(InputEvent::Unrecognized) => { + watch_state.handle_invalid_cmd()?; + } + WatchEvent::FileChange { exercise_ind } => { + // TODO: bool + watch_state.run_exercise_with_ind(exercise_ind)?; + watch_state.render()?; } } } + // Drop the receiver for the sender threads to exit. + drop(rx); + watch_state.into_writer().write_all(b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; + match input_thread.join() { + Ok(res) => res?, + Err(_) => bail!("The input thread panicked"), + } + Ok(()) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 40f48ef..f614ae0 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,26 +1,23 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use crossterm::{ style::{Attribute, ContentStyle, Stylize}, - terminal::{Clear, ClearType}, + terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; use std::{ fmt::Write as _, io::{self, StdoutLock, Write as _}, - sync::mpsc::Receiver, - time::Duration, }; use crate::{ exercise::{Exercise, State}, + progress_bar::progress_bar, state_file::StateFile, }; pub struct WatchState<'a> { writer: StdoutLock<'a>, - rx: Receiver<DebounceEventResult>, - exercises: &'a [Exercise], + exercises: &'static [Exercise], exercise: &'a Exercise, current_exercise_ind: usize, stdout: Option<Vec<u8>>, @@ -30,11 +27,7 @@ pub struct WatchState<'a> { } impl<'a> WatchState<'a> { - pub fn new( - state_file: &StateFile, - exercises: &'a [Exercise], - rx: Receiver<DebounceEventResult>, - ) -> Self { + pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { let current_exercise_ind = state_file.next_exercise_ind(); let exercise = &exercises[current_exercise_ind]; @@ -50,7 +43,6 @@ impl<'a> WatchState<'a> { Self { writer, - rx, exercises, exercise, current_exercise_ind, @@ -114,41 +106,14 @@ You can keep working on this exercise or jump into the next one by removing the Ok(true) } - pub fn try_recv_event(&mut self) -> Result<()> { - let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { - return Ok(()); - }; - - if let Some(current_exercise_ind) = events? - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises - .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - { - self.current_exercise_ind = current_exercise_ind; - } else { - return Ok(()); - }; - - while self.current_exercise_ind < self.exercises.len() { - self.exercise = &self.exercises[self.current_exercise_ind]; - if !self.run_exercise()? { - break; - } - - self.current_exercise_ind += 1; - } + pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<bool> { + self.exercise = self + .exercises + .get(exercise_ind) + .context("Invalid exercise index")?; + self.current_exercise_ind = exercise_ind; - Ok(()) + self.run_exercise() } pub fn show_prompt(&mut self) -> io::Result<()> { @@ -156,7 +121,7 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.flush() } - pub fn render(&mut self) -> io::Result<()> { + pub fn render(&mut self) -> Result<()> { self.writer.execute(Clear(ClearType::All))?; if let Some(stdout) = &self.stdout { @@ -171,7 +136,17 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.write_all(message.as_bytes())?; } - self.show_prompt() + let line_width = size()?.0; + let progress_bar = progress_bar( + self.current_exercise_ind as u16, + self.exercises.len() as u16, + line_width, + )?; + self.writer.write_all(progress_bar.as_bytes())?; + + self.show_prompt()?; + + Ok(()) } pub fn show_hint(&mut self) -> io::Result<()> { |
