diff --git a/src/watch.rs b/src/watch.rs index 1503fdfe..967f98c1 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,25 +1,18 @@ use anyhow::Result; -use crossterm::{ - style::{Attribute, ContentStyle, Stylize}, - terminal::{Clear, ClearType}, - ExecutableCommand, -}; -use notify_debouncer_mini::{ - new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, -}; +use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; use std::{ - fmt::Write as _, - io::{self, BufRead, StdoutLock, Write}, + io::{self, BufRead, Write}, path::Path, - sync::mpsc::{channel, sync_channel, Receiver}, + sync::mpsc::{channel, sync_channel}, thread, time::Duration, }; -use crate::{ - exercise::{self, Exercise}, - state_file::StateFile, -}; +mod state; + +use crate::{exercise::Exercise, state_file::StateFile}; + +use self::state::WatchState; enum Event { Hint, @@ -27,130 +20,6 @@ enum Event { Quit, } -struct WatchState<'a> { - writer: StdoutLock<'a>, - rx: Receiver, - exercises: &'a [Exercise], - exercise: &'a Exercise, - current_exercise_ind: usize, - stdout: Option>, - stderr: Option>, - message: Option, - prompt: Vec, -} - -impl<'a> WatchState<'a> { - fn run_exercise(&mut self) -> Result { - let output = self.exercise.run()?; - - if !output.status.success() { - self.stdout = Some(output.stdout); - self.stderr = Some(output.stderr); - return Ok(false); - } - - if let exercise::State::Pending(context) = self.exercise.state()? { - let mut message = format!( - " -You can keep working on this exercise or jump into the next one by removing the {} comment: - -", - "`I AM NOT DONE`".bold(), - ); - - for context_line in context { - let formatted_line = if context_line.important { - context_line.line.bold() - } else { - context_line.line.stylize() - }; - - writeln!( - message, - "{:>2} {} {}", - ContentStyle { - foreground_color: Some(crossterm::style::Color::Blue), - background_color: None, - underline_color: None, - attributes: Attribute::Bold.into() - } - .apply(context_line.number), - "|".blue(), - formatted_line, - )?; - } - - self.stdout = Some(output.stdout); - self.message = Some(message); - return Ok(false); - } - - Ok(true) - } - - 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; - } - - Ok(()) - } - - fn prompt(&mut self) -> io::Result<()> { - self.writer.write_all(&self.prompt)?; - self.writer.flush() - } - - fn render(&mut self) -> Result<()> { - self.writer.execute(Clear(ClearType::All))?; - - if let Some(stdout) = &self.stdout { - self.writer.write_all(stdout)?; - } - - if let Some(stderr) = &self.stderr { - self.writer.write_all(stderr)?; - } - - if let Some(message) = &self.message { - self.writer.write_all(message.as_bytes())?; - } - - self.prompt()?; - - Ok(()) - } -} - pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { let (tx, rx) = channel(); let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; @@ -158,29 +27,7 @@ pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let current_exercise_ind = state_file.next_exercise_ind(); - - let exercise = &exercises[current_exercise_ind]; - - let writer = io::stdout().lock(); - - let mut watch_state = WatchState { - writer, - rx, - exercises, - exercise, - current_exercise_ind, - stdout: None, - stderr: None, - message: None, - prompt: format!( - "\n\n{}int/{}lear/{}uit? ", - "h".bold(), - "c".bold(), - "q".bold() - ) - .into_bytes(), - }; + let mut watch_state = WatchState::new(state_file, exercises, rx); watch_state.run_exercise()?; watch_state.render()?; @@ -214,24 +61,20 @@ pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { if let Ok(event) = rx.try_recv() { match event { Some(Event::Hint) => { - watch_state - .writer - .write_all(watch_state.exercise.hint.as_bytes())?; - watch_state.prompt()?; + watch_state.show_hint()?; } Some(Event::Clear) => { watch_state.render()?; } Some(Event::Quit) => break, None => { - watch_state.writer.write_all(b"Invalid command")?; - watch_state.prompt()?; + watch_state.handle_invalid_cmd()?; } } } } - watch_state.writer.write_all(b" + 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. ")?; diff --git a/src/watch/state.rs b/src/watch/state.rs new file mode 100644 index 00000000..40f48eff --- /dev/null +++ b/src/watch/state.rs @@ -0,0 +1,186 @@ +use anyhow::Result; +use crossterm::{ + style::{Attribute, ContentStyle, Stylize}, + terminal::{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}, + state_file::StateFile, +}; + +pub struct WatchState<'a> { + writer: StdoutLock<'a>, + rx: Receiver, + exercises: &'a [Exercise], + exercise: &'a Exercise, + current_exercise_ind: usize, + stdout: Option>, + stderr: Option>, + message: Option, + prompt: Vec, +} + +impl<'a> WatchState<'a> { + pub fn new( + state_file: &StateFile, + exercises: &'a [Exercise], + rx: Receiver, + ) -> Self { + let current_exercise_ind = state_file.next_exercise_ind(); + let exercise = &exercises[current_exercise_ind]; + + let writer = io::stdout().lock(); + + let prompt = format!( + "\n\n{}int/{}lear/{}uit? ", + "h".bold(), + "c".bold(), + "q".bold() + ) + .into_bytes(); + + Self { + writer, + rx, + exercises, + exercise, + current_exercise_ind, + stdout: None, + stderr: None, + message: None, + prompt, + } + } + + #[inline] + pub fn into_writer(self) -> StdoutLock<'a> { + self.writer + } + + pub fn run_exercise(&mut self) -> Result { + let output = self.exercise.run()?; + + if !output.status.success() { + self.stdout = Some(output.stdout); + self.stderr = Some(output.stderr); + return Ok(false); + } + + if let State::Pending(context) = self.exercise.state()? { + let mut message = format!( + " +You can keep working on this exercise or jump into the next one by removing the {} comment: + +", + "`I AM NOT DONE`".bold(), + ); + + for context_line in context { + let formatted_line = if context_line.important { + context_line.line.bold() + } else { + context_line.line.stylize() + }; + + writeln!( + message, + "{:>2} {} {}", + ContentStyle { + foreground_color: Some(crossterm::style::Color::Blue), + background_color: None, + underline_color: None, + attributes: Attribute::Bold.into() + } + .apply(context_line.number), + "|".blue(), + formatted_line, + )?; + } + + self.stdout = Some(output.stdout); + self.message = Some(message); + return Ok(false); + } + + 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; + } + + Ok(()) + } + + pub fn show_prompt(&mut self) -> io::Result<()> { + self.writer.write_all(&self.prompt)?; + self.writer.flush() + } + + pub fn render(&mut self) -> io::Result<()> { + self.writer.execute(Clear(ClearType::All))?; + + if let Some(stdout) = &self.stdout { + self.writer.write_all(stdout)?; + } + + if let Some(stderr) = &self.stderr { + self.writer.write_all(stderr)?; + } + + if let Some(message) = &self.message { + self.writer.write_all(message.as_bytes())?; + } + + self.show_prompt() + } + + pub fn show_hint(&mut self) -> io::Result<()> { + self.writer.write_all(self.exercise.hint.as_bytes())?; + self.show_prompt() + } + + pub fn handle_invalid_cmd(&mut self) -> io::Result<()> { + self.writer.write_all(b"Invalid command")?; + self.show_prompt() + } +}