summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs22
-rw-r--r--src/tui.rs92
-rw-r--r--src/watch.rs240
3 files changed, 251 insertions, 103 deletions
diff --git a/src/main.rs b/src/main.rs
index 5051785..e8218ef 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,11 +1,11 @@
-use crate::consts::{DEFAULT_OUT, WELCOME};
+use crate::consts::WELCOME;
use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
use crate::exercise::{Exercise, ExerciseList};
use crate::run::run;
-use crate::tui::tui;
use crate::verify::verify;
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
+use state::State;
use std::io::Write;
use std::path::Path;
use std::process::exit;
@@ -17,8 +17,8 @@ mod exercise;
mod init;
mod run;
mod state;
-mod tui;
mod verify;
+mod watch;
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
#[derive(Parser)]
@@ -75,10 +75,6 @@ enum Subcommands {
fn main() -> Result<()> {
let args = Args::parse();
- if args.command.is_none() {
- println!("\n{WELCOME}\n");
- }
-
which::which("cargo").context(
"Failed to find `cargo`.
Did you already install Rust?
@@ -97,16 +93,20 @@ Then run `rustlings` for further instructions on getting started."
return Ok(());
} else if !Path::new("exercises").is_dir() {
println!(
- "\nThe `exercises` directory wasn't found in the current directory.
+ "
+{WELCOME}
+
+The `exercises` directory wasn't found in the current directory.
If you are just starting with Rustlings, run the command `rustlings init` to initialize it."
);
exit(1);
}
+ let state = State::read_or_default(&exercises);
+
match args.command {
None | Some(Subcommands::Watch) => {
- println!("{DEFAULT_OUT}\n");
- tui(&exercises)?;
+ watch::watch(&state, &exercises)?;
}
// `Init` is handled above.
Some(Subcommands::Init) => (),
@@ -199,7 +199,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
let exercise = find_exercise(&name, &exercises)?;
println!("{}", exercise.hint);
}
- Some(Subcommands::Verify) => match verify(&exercises, (0, exercises.len()))? {
+ Some(Subcommands::Verify) => match verify(&exercises, 0)? {
VerifyState::AllExercisesDone => println!("All exercises done!"),
VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
},
diff --git a/src/tui.rs b/src/tui.rs
deleted file mode 100644
index bb87365..0000000
--- a/src/tui.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use anyhow::Result;
-use crossterm::{
- terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
- ExecutableCommand,
-};
-use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode, DebouncedEventKind};
-use ratatui::{backend::CrosstermBackend, Terminal};
-use std::{
- io::stdout,
- path::Path,
- sync::mpsc::{channel, RecvTimeoutError},
- time::Duration,
-};
-
-use crate::{
- exercise::Exercise,
- verify::{verify, VerifyState},
-};
-
-fn watch(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 mut failed_exercise_hint = match verify(exercises, (0, exercises.len()))? {
- VerifyState::AllExercisesDone => return Ok(()),
- VerifyState::Failed(exercise) => Some(&exercise.hint),
- };
-
- let mut pending_exercises = Vec::with_capacity(exercises.len());
- loop {
- match rx.recv_timeout(Duration::from_secs(1)) {
- Ok(event) => match event {
- Ok(events) => {
- for event in events {
- if event.kind == DebouncedEventKind::Any
- && event.path.extension().is_some_and(|ext| ext == "rs")
- {
- pending_exercises.extend(exercises.iter().filter(|exercise| {
- !exercise.looks_done().unwrap_or(false)
- || event.path.ends_with(&exercise.path)
- }));
- let num_done = exercises.len() - pending_exercises.len();
-
- match verify(
- pending_exercises.iter().copied(),
- (num_done, exercises.len()),
- )? {
- VerifyState::AllExercisesDone => return Ok(()),
- VerifyState::Failed(exercise) => {
- failed_exercise_hint = Some(&exercise.hint);
- }
- }
-
- pending_exercises.clear();
- }
- }
- }
- Err(e) => println!("watch error: {e:?}"),
- },
- Err(RecvTimeoutError::Timeout) => {
- // the timeout expired, just check the `should_quit` variable below then loop again
- }
- Err(e) => println!("watch error: {e:?}"),
- }
-
- // TODO: Check if we need to exit
- }
-}
-
-pub fn tui(exercises: &[Exercise]) -> Result<()> {
- let mut stdout = stdout().lock();
- stdout.execute(EnterAlternateScreen)?;
- enable_raw_mode()?;
- let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
- terminal.clear()?;
-
- watch(exercises)?;
-
- drop(terminal);
- stdout.execute(LeaveAlternateScreen)?;
- disable_raw_mode()?;
-
- // TODO
- println!("We hope you're enjoying learning about Rust!");
- println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again");
-
- Ok(())
-}
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(())
+}