diff --git a/src/main.rs b/src/main.rs index c8c6584..20ec290 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,9 @@ use clap::{Parser, Subcommand}; use console::Emoji; use notify_debouncer_mini::notify::RecursiveMode; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; -use shlex::Shlex; use std::io::{BufRead, Write}; use std::path::Path; -use std::process::{exit, Command}; +use std::process::exit; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; @@ -31,9 +30,6 @@ mod verify; #[derive(Parser)] #[command(version)] struct Args { - /// Show outputs from the test exercises - #[arg(long)] - nocapture: bool, #[command(subcommand)] command: Option, } @@ -45,11 +41,7 @@ enum Subcommands { /// Verify all exercises according to the recommended order Verify, /// Rerun `verify` when files were edited - Watch { - /// Show hints on success - #[arg(long)] - success_hints: bool, - }, + Watch, /// Run/Test a single exercise Run { /// The name of the exercise @@ -117,7 +109,6 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { println!("{DEFAULT_OUT}\n"); exit(0); @@ -203,7 +194,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Subcommands::Run { name } => { let exercise = find_exercise(&name, &exercises)?; - run(exercise, verbose).unwrap_or_else(|_| exit(1)); + run(exercise).unwrap_or_else(|_| exit(1)); } Subcommands::Reset { name } => { @@ -219,12 +210,12 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini println!("{}", exercise.hint); } - Subcommands::Verify => match verify(&exercises, (0, exercises.len()), verbose, false)? { + Subcommands::Verify => match verify(&exercises, (0, exercises.len()))? { VerifyState::AllExercisesDone => println!("All exercises done!"), VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), }, - Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { + Subcommands::Watch => match watch(&exercises) { Err(e) => { println!("Error: Could not watch your progress. Error message was {e:?}."); println!("Most likely you've run out of disk space or your 'inotify limit' has been reached."); @@ -277,17 +268,6 @@ fn spawn_watch_shell( println!("Bye!"); } else if input == "help" { println!("{WATCH_MODE_HELP_MESSAGE}"); - } else if let Some(cmd) = input.strip_prefix('!') { - let mut parts = Shlex::new(cmd); - - let Some(program) = parts.next() else { - println!("no command provided"); - continue; - }; - - if let Err(e) = Command::new(program).args(parts).status() { - println!("failed to execute command `{cmd}`: {e}"); - } } else { println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}"); } @@ -319,7 +299,7 @@ enum WatchStatus { Unfinished, } -fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result { +fn watch(exercises: &[Exercise]) -> Result { /* Clears the terminal with an ANSI escape code. Works in UNIX and newer Windows terminals. */ fn clear_screen() { @@ -336,11 +316,10 @@ fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result return Ok(WatchStatus::Finished), - VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), - }; + let failed_exercise_hint = match verify(exercises, (0, exercises.len()))? { + VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), + VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), + }; spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit)); @@ -364,8 +343,6 @@ fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result return Ok(WatchStatus::Finished), VerifyState::Failed(exercise) => { @@ -429,7 +406,6 @@ const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: hint - prints the current exercise's hint clear - clears the screen quit - quits watch mode - ! - executes a command, like `!rustc --explain E0381` help - displays this help message Watch mode automatically re-evaluates the current exercise diff --git a/src/run.rs b/src/run.rs index 3f93f14..0a09ecc 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,39 +1,27 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use std::io::{stdout, Write}; -use std::time::Duration; -use crate::exercise::{Exercise, Mode}; -use crate::verify::test; -use indicatif::ProgressBar; +use crate::exercise::Exercise; // Invoke the rust compiler on the path of the given exercise, // and run the ensuing binary. // The verbose argument helps determine whether or not to show // the output from the test harnesses (if the mode of the exercise is test) -pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> { - match exercise.mode { - Mode::Test => test(exercise, verbose), - Mode::Compile | Mode::Clippy => compile_and_run(exercise), - } -} - -// Compile and run an exercise. -// This is strictly for non-test binaries, so output is displayed -fn compile_and_run(exercise: &Exercise) -> Result<()> { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Running {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - +pub fn run(exercise: &Exercise) -> Result<()> { let output = exercise.run()?; - progress_bar.finish_and_clear(); - stdout().write_all(&output.stdout)?; - if !output.status.success() { - stdout().write_all(&output.stderr)?; - warn!("Ran {} with errors", exercise); - bail!("TODO"); + { + let mut stdout = stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; + } + + if output.status.success() { + success!("Successfully ran {}", exercise); + } else { + warn!("Ran {} with errors", exercise); } - success!("Successfully ran {}", exercise); Ok(()) } diff --git a/src/verify.rs b/src/verify.rs index ef966f6..5b05394 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,12 +1,6 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use console::style; -use indicatif::{ProgressBar, ProgressStyle}; -use std::{ - env, - io::{stdout, Write}, - process::Output, - time::Duration, -}; +use std::io::{stdout, Write}; use crate::exercise::{Exercise, Mode, State}; @@ -23,201 +17,64 @@ pub enum VerifyState<'a> { pub fn verify<'a>( pending_exercises: impl IntoIterator, progress: (usize, usize), - verbose: bool, - success_hints: bool, ) -> Result> { - let (num_done, total) = progress; - let bar = ProgressBar::new(total as u64); - let mut percentage = num_done as f32 / total as f32 * 100.0; - bar.set_style( - ProgressStyle::default_bar() - .template("Progress: [{bar:60.green/red}] {pos}/{len} {msg}") - .expect("Progressbar template should be valid!") - .progress_chars("#>-"), + let (mut num_done, total) = progress; + println!( + "Progress: {num_done}/{total} ({:.1}%)\n", + num_done as f32 / total as f32 * 100.0, ); - bar.set_position(num_done as u64); - bar.set_message(format!("({percentage:.1} %)")); for exercise in pending_exercises { - let compile_result = match exercise.mode { - Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints)?, - Mode::Compile => compile_and_run_interactively(exercise, success_hints)?, - Mode::Clippy => compile_only(exercise, success_hints)?, - }; - if !compile_result { + let output = exercise.run()?; + + { + let mut stdout = stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; + } + + if !output.status.success() { return Ok(VerifyState::Failed(exercise)); } - percentage += 100.0 / total as f32; - bar.inc(1); - bar.set_message(format!("({percentage:.1} %)")); - } - bar.finish(); - println!("You completed all exercises!"); + println!(); + match exercise.mode { + Mode::Compile => success!("Successfully ran {}!", exercise), + Mode::Test => success!("Successfully tested {}!", exercise), + Mode::Clippy => success!("Successfully checked {}!", exercise), + } + + if let State::Pending(context) = exercise.state()? { + println!( + "\nYou can keep working on this exercise, +or jump into the next one by removing the {} comment:\n", + style("`I AM NOT DONE`").bold() + ); + + for context_line in context { + let formatted_line = if context_line.important { + format!("{}", style(context_line.line).bold()) + } else { + context_line.line + }; + + println!( + "{:>2} {} {}", + style(context_line.number).blue().bold(), + style("|").blue(), + formatted_line, + ); + } + return Ok(VerifyState::Failed(exercise)); + } + + num_done += 1; + println!( + "Progress: {num_done}/{total} ({:.1}%)\n", + num_done as f32 / total as f32 * 100.0, + ); + } Ok(VerifyState::AllExercisesDone) } - -#[derive(PartialEq, Eq)] -enum RunMode { - Interactive, - NonInteractive, -} - -// Compile and run the resulting test harness of the given Exercise -pub fn test(exercise: &Exercise, verbose: bool) -> Result<()> { - compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?; - Ok(()) -} - -// Invoke the rust compiler without running the resulting binary -fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Compiling {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let _ = exercise.run()?; - progress_bar.finish_and_clear(); - - prompt_for_completion(exercise, None, success_hints) -} - -// Compile the given Exercise and run the resulting binary in an interactive mode -fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Running {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let output = exercise.run()?; - progress_bar.finish_and_clear(); - - if !output.status.success() { - warn!("Ran {} with errors", exercise); - { - let mut stdout = stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; - } - bail!("TODO"); - } - - prompt_for_completion(exercise, Some(output), success_hints) -} - -// Compile the given Exercise as a test harness and display -// the output if verbose is set to true -fn compile_and_test( - exercise: &Exercise, - run_mode: RunMode, - verbose: bool, - success_hints: bool, -) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Testing {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let output = exercise.run()?; - progress_bar.finish_and_clear(); - - if !output.status.success() { - warn!( - "Testing of {} failed! Please try again. Here's the output:", - exercise - ); - { - let mut stdout = stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; - } - bail!("TODO"); - } - - if verbose { - stdout().write_all(&output.stdout)?; - } - - if run_mode == RunMode::Interactive { - prompt_for_completion(exercise, None, success_hints) - } else { - Ok(true) - } -} - -fn prompt_for_completion( - exercise: &Exercise, - prompt_output: Option, - success_hints: bool, -) -> Result { - let context = match exercise.state()? { - State::Done => return Ok(true), - State::Pending(context) => context, - }; - match exercise.mode { - Mode::Compile => success!("Successfully ran {}!", exercise), - Mode::Test => success!("Successfully tested {}!", exercise), - Mode::Clippy => success!("Successfully compiled {}!", exercise), - } - - let no_emoji = env::var("NO_EMOJI").is_ok(); - - let clippy_success_msg = if no_emoji { - "The code is compiling, and Clippy is happy!" - } else { - "The code is compiling, and šŸ“Ž Clippy šŸ“Ž is happy!" - }; - - let success_msg = match exercise.mode { - Mode::Compile => "The code is compiling!", - Mode::Test => "The code is compiling, and the tests pass!", - Mode::Clippy => clippy_success_msg, - }; - - if no_emoji { - println!("\n~*~ {success_msg} ~*~\n"); - } else { - println!("\nšŸŽ‰ šŸŽ‰ {success_msg} šŸŽ‰ šŸŽ‰\n"); - } - - if let Some(output) = prompt_output { - let separator = separator(); - println!("Output:\n{separator}"); - stdout().write_all(&output.stdout).unwrap(); - println!("\n{separator}\n"); - } - if success_hints { - println!( - "Hints:\n{separator}\n{}\n{separator}\n", - exercise.hint, - separator = separator(), - ); - } - - println!("You can keep working on this exercise,"); - println!( - "or jump into the next one by removing the {} comment:", - style("`I AM NOT DONE`").bold() - ); - println!(); - for context_line in context { - let formatted_line = if context_line.important { - format!("{}", style(context_line.line).bold()) - } else { - context_line.line - }; - - println!( - "{:>2} {} {}", - style(context_line.number).blue().bold(), - style("|").blue(), - formatted_line, - ); - } - - Ok(false) -} - -fn separator() -> console::StyledObject<&'static str> { - style("====================").bold() -}