diff options
| author | mo8it <mo8it@proton.me> | 2024-04-07 01:17:53 +0200 |
|---|---|---|
| committer | mo8it <mo8it@proton.me> | 2024-04-07 01:17:53 +0200 |
| commit | 0819bbe21fc86315d3acdcdb2bc14b21f3acb788 (patch) | |
| tree | 98e2801a64339c125ff997a19824c0a5fcbd1e4e /src/watch.rs | |
| parent | 18342b3aa3bd43c2c013614935f45e7d6bbaea8f (diff) | |
Can't use Ratatui for the watch mode :(
Diffstat (limited to 'src/watch.rs')
| -rw-r--r-- | src/watch.rs | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/src/watch.rs b/src/watch.rs new file mode 100644 index 0000000..92da20d --- /dev/null +++ b/src/watch.rs @@ -0,0 +1,240 @@ +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::State, +}; + +enum Event { + Hint, + Clear, + Quit, +} + +struct WatchState<'a> { + writer: StdoutLock<'a>, + rx: Receiver<DebounceEventResult>, + exercises: &'a [Exercise], + exercise: &'a Exercise, + current_exercise_ind: usize, + stdout: Option<Vec<u8>>, + stderr: Option<Vec<u8>>, + message: Option<String>, + prompt: Vec<u8>, +} + +impl<'a> WatchState<'a> { + fn run_exercise(&mut self) -> Result<bool> { + 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: &State, 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.progress.iter().position(|done| *done).unwrap_or(0); + + 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(()) +} |
