summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/watch.rs181
-rw-r--r--src/watch/state.rs186
2 files changed, 198 insertions, 169 deletions
diff --git a/src/watch.rs b/src/watch.rs
index 1503fdf..967f98c 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<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_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 0000000..40f48ef
--- /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<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> {
+ pub fn new(
+ state_file: &StateFile,
+ exercises: &'a [Exercise],
+ rx: Receiver<DebounceEventResult>,
+ ) -> 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<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 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()
+ }
+}