use anyhow::Result; use crossterm::{ style::{Attribute, ContentStyle, Stylize}, terminal::{Clear, ClearType}, ExecutableCommand, }; use notify_debouncer_mini::{ new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, }; use std::{ fmt::Write as _, io::{self, BufRead, StdoutLock, Write}, path::Path, sync::mpsc::{channel, sync_channel, Receiver}, thread, time::Duration, }; use crate::{ exercise::{self, Exercise}, state_file::StateFile, }; enum Event { Hint, Clear, 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)?; debouncer .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(), }; watch_state.run_exercise()?; watch_state.render()?; let (tx, rx) = sync_channel(0); thread::spawn(move || { let mut stdin = io::stdin().lock(); let mut stdin_buf = String::with_capacity(8); loop { stdin.read_line(&mut stdin_buf).unwrap(); let event = match stdin_buf.trim() { "h" | "hint" => Some(Event::Hint), "c" | "clear" => Some(Event::Clear), "q" | "quit" => Some(Event::Quit), _ => None, }; stdin_buf.clear(); if tx.send(event).is_err() { break; }; } }); loop { watch_state.try_recv_event()?; 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()?; } Some(Event::Clear) => { watch_state.render()?; } Some(Event::Quit) => break, None => { watch_state.writer.write_all(b"Invalid command")?; watch_state.prompt()?; } } } } watch_state.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. ")?; Ok(()) }