use crate::consts::{DEFAULT_OUT, 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 std::io::Write; use std::path::Path; use std::process::exit; use verify::VerifyState; mod consts; mod embedded; mod exercise; mod init; mod run; mod state; mod tui; mod verify; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] struct Args { #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Subcommands { /// Initialize Rustlings Init, /// Verify all exercises according to the recommended order Verify, /// Same as just running `rustlings` without a subcommand. Watch, /// Run/Test a single exercise Run { /// The name of the exercise name: String, }, /// Reset a single exercise Reset { /// The name of the exercise name: String, }, /// Return a hint for the given exercise Hint { /// The name of the exercise name: String, }, /// List the exercises available in Rustlings List { /// Show only the paths of the exercises #[arg(short, long)] paths: bool, /// Show only the names of the exercises #[arg(short, long)] names: bool, /// Provide a string to match exercise names. /// Comma separated patterns are accepted #[arg(short, long)] filter: Option, /// Display only exercises not yet solved #[arg(short, long)] unsolved: bool, /// Display only exercises that have been solved #[arg(short, long)] solved: bool, }, } 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? Try running `cargo --version` to diagnose the problem.", )?; let exercises = ExerciseList::parse()?.exercises; if matches!(args.command, Some(Subcommands::Init)) { init::init_rustlings(&exercises).context("Initialization failed")?; println!( "\nDone initialization!\n Run `cd rustlings` to go into the generated directory. 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. If you are just starting with Rustlings, run the command `rustlings init` to initialize it." ); exit(1); } match args.command { None | Some(Subcommands::Watch) => { println!("{DEFAULT_OUT}\n"); tui(&exercises)?; } // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::List { paths, names, filter, unsolved, solved, }) => { if !paths && !names { println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status"); } let mut exercises_done: u16 = 0; let lowercase_filter = filter .as_ref() .map(|s| s.to_lowercase()) .unwrap_or_default(); let filters = lowercase_filter .split(',') .filter_map(|f| { let f = f.trim(); if f.is_empty() { None } else { Some(f) } }) .collect::>(); for exercise in &exercises { let fname = exercise.path.to_string_lossy(); let filter_cond = filters .iter() .any(|f| exercise.name.contains(f) || fname.contains(f)); let looks_done = exercise.looks_done()?; let status = if looks_done { exercises_done += 1; "Done" } else { "Pending" }; let solve_cond = (looks_done && solved) || (!looks_done && unsolved) || (!solved && !unsolved); if solve_cond && (filter_cond || filter.is_none()) { let line = if paths { format!("{fname}\n") } else if names { format!("{}\n", exercise.name) } else { format!("{:<17}\t{fname:<46}\t{status:<7}\n", exercise.name) }; // Somehow using println! leads to the binary panicking // when its output is piped. // So, we're handling a Broken Pipe error and exiting with 0 anyway let stdout = std::io::stdout(); { let mut handle = stdout.lock(); handle.write_all(line.as_bytes()).unwrap_or_else(|e| { match e.kind() { std::io::ErrorKind::BrokenPipe => exit(0), _ => exit(1), }; }); } } } let percentage_progress = exercises_done as f32 / exercises.len() as f32 * 100.0; println!( "Progress: You completed {} / {} exercises ({:.1} %).", exercises_done, exercises.len(), percentage_progress ); exit(0); } Some(Subcommands::Run { name }) => { let exercise = find_exercise(&name, &exercises)?; run(exercise).unwrap_or_else(|_| exit(1)); } Some(Subcommands::Reset { name }) => { let exercise = find_exercise(&name, &exercises)?; EMBEDDED_FILES .write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) .with_context(|| format!("Failed to reset the exercise {exercise}"))?; println!("The file {} has been reset!", exercise.path.display()); } Some(Subcommands::Hint { name }) => { let exercise = find_exercise(&name, &exercises)?; println!("{}", exercise.hint); } Some(Subcommands::Verify) => match verify(&exercises, (0, exercises.len()))? { VerifyState::AllExercisesDone => println!("All exercises done!"), VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), }, } Ok(()) } fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> { if name == "next" { for exercise in exercises { if !exercise.looks_done()? { return Ok(exercise); } } println!("🎉 Congratulations! You have done all the exercises!"); println!("🔚 There are no more exercises to do next!"); exit(0); } exercises .iter() .find(|e| e.name == name) .with_context(|| format!("No exercise found for '{name}'!")) }