diff options
| author | mo8it <mo8it@proton.me> | 2024-07-05 13:39:50 +0200 |
|---|---|---|
| committer | mo8it <mo8it@proton.me> | 2024-07-05 13:39:50 +0200 |
| commit | 7123c7ae3a9605fbe962e4ef0a0f1424cd16fef8 (patch) | |
| tree | c67f7e62bb9a179ae4fdbab492501cb6847e64c7 /src/main.rs | |
| parent | 77b687d501771c24bd83294d97b8e6f9ffa92d6b (diff) | |
| parent | 4d9c346a173bb722b929f3ea3c00f84954483e24 (diff) | |
Merge remote-tracking branch 'upstream/main' into fix-enum-variant-inconsistency
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 573 |
1 files changed, 161 insertions, 412 deletions
diff --git a/src/main.rs b/src/main.rs index bbff712..2233d8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,471 +1,220 @@ -use crate::exercise::{Exercise, ExerciseList}; -use crate::project::RustAnalyzerProject; -use crate::run::{reset, run}; -use crate::verify::verify; +use anyhow::{bail, Context, Result}; +use app_state::StateFileStatus; use clap::{Parser, Subcommand}; -use console::Emoji; -use notify::DebouncedEvent; -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; -use std::ffi::OsStr; -use std::fs; -use std::io::{self, prelude::*}; -use std::path::Path; -use std::process::{Command, Stdio}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::{channel, RecvTimeoutError}; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; - -#[macro_use] -mod ui; - +use std::{ + io::{self, BufRead, StdoutLock, Write}, + path::Path, + process::exit, +}; + +use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; + +mod app_state; +mod cargo_toml; +mod cmd; +mod dev; +mod embedded; mod exercise; -mod project; +mod info_file; +mod init; +mod list; +mod progress_bar; mod run; -mod verify; +mod terminal_link; +mod watch; + +const CURRENT_FORMAT_VERSION: u8 = 1; +const DEBUG_PROFILE: bool = { + #[allow(unused_assignments, unused_mut)] + let mut debug_profile = false; + + #[cfg(debug_assertions)] + { + debug_profile = true; + } + + debug_profile +}; + +// The current directory is the official Rustligns repository. +fn in_official_repo() -> bool { + Path::new("dev/rustlings-repo.txt").exists() +} + +fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { + stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") +} + +fn press_enter_prompt() -> io::Result<()> { + io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + Ok(()) +} /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] struct Args { - /// Show outputs from the test exercises - #[arg(long)] - nocapture: bool, #[command(subcommand)] command: Option<Subcommands>, + /// Manually run the current exercise using `r` in the watch mode. + /// Only use this if Rustlings fails to detect exercise file changes. + #[arg(long)] + manual_run: bool, } #[derive(Subcommand)] 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, - }, - /// Run/Test a single exercise + /// Initialize the official Rustlings exercises + Init, + /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified Run { /// The name of the exercise - name: String, + name: Option<String>, }, - /// Reset a single exercise using "git stash -- <filename>" + /// Reset a single exercise Reset { /// The name of the exercise name: String, }, - /// Return a hint for the given exercise + /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified Hint { /// The name of the exercise - name: String, + name: Option<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<String>, - /// Display only exercises not yet solved - #[arg(short, long)] - unsolved: bool, - /// Display only exercises that have been solved - #[arg(short, long)] - solved: bool, - }, - /// Enable rust-analyzer for exercises - Lsp, + /// Commands for developing (third-party) Rustlings exercises + #[command(subcommand)] + Dev(DevCommands), } -fn main() { +fn main() -> Result<()> { let args = Args::parse(); - if args.command.is_none() { - println!("\n{WELCOME}\n"); - } - - if !Path::new("info.toml").exists() { - println!( - "{} must be run from the rustlings directory", - std::env::current_exe().unwrap().to_str().unwrap() - ); - println!("Try `cd rustlings/`!"); - std::process::exit(1); + if !DEBUG_PROFILE && in_official_repo() { + bail!("{OLD_METHOD_ERR}"); } - if !rustc_exists() { - println!("We cannot find `rustc`."); - println!("Try running `rustc --version` to diagnose your problem."); - println!("For instructions on how to install Rust, check the README."); - std::process::exit(1); - } - - let toml_str = &fs::read_to_string("info.toml").unwrap(); - let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises; - let verbose = args.nocapture; - - let command = args.command.unwrap_or_else(|| { - println!("{DEFAULT_OUT}\n"); - std::process::exit(0); - }); - - match command { - Subcommands::List { - paths, - names, - filter, - unsolved, - solved, - } => { - if !paths && !names { - println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status"); + match args.command { + Some(Subcommands::Init) => { + if DEBUG_PROFILE { + bail!("Disabled in the debug build"); } - let mut exercises_done: u16 = 0; - let filters = filter.clone().unwrap_or_default().to_lowercase(); - exercises.iter().for_each(|e| { - let fname = format!("{}", e.path.display()); - let filter_cond = filters - .split(',') - .filter(|f| !f.trim().is_empty()) - .any(|f| e.name.contains(f) || fname.contains(f)); - let status = if e.looks_done() { - exercises_done += 1; - "Done" - } else { - "Pending" - }; - let solve_cond = { - (e.looks_done() && solved) - || (!e.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", e.name) - } else { - format!("{:<17}\t{fname:<46}\t{status:<7}\n", e.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 => std::process::exit(0), - _ => std::process::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 - ); - std::process::exit(0); - } - - Subcommands::Run { name } => { - let exercise = find_exercise(&name, &exercises); - - run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1)); - } - Subcommands::Reset { name } => { - let exercise = find_exercise(&name, &exercises); + { + let mut stdout = io::stdout().lock(); + stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; + stdout.flush()?; + press_enter_prompt()?; + stdout.write_all(b"\n")?; + } - reset(exercise).unwrap_or_else(|_| std::process::exit(1)); + return init::init().context("Initialization failed"); } + Some(Subcommands::Dev(dev_command)) => return dev_command.run(), + _ => (), + } - Subcommands::Hint { name } => { - let exercise = find_exercise(&name, &exercises); - - println!("{}", exercise.hint); - } + if !Path::new("exercises").is_dir() { + println!("{PRE_INIT_MSG}"); + exit(1); + } - Subcommands::Verify => { - verify(&exercises, (0, exercises.len()), verbose, false) - .unwrap_or_else(|_| std::process::exit(1)); - } + let info_file = InfoFile::parse()?; - Subcommands::Lsp => { - let mut project = RustAnalyzerProject::new(); - project - .get_sysroot_src() - .expect("Couldn't find toolchain path, do you have `rustc` installed?"); - project - .exercises_to_json() - .expect("Couldn't parse rustlings exercises files"); + if info_file.format_version > CURRENT_FORMAT_VERSION { + bail!(FORMAT_VERSION_HIGHER_ERR); + } - if project.crates.is_empty() { - println!("Failed find any exercises, make sure you're in the `rustlings` folder"); - } else if project.write_to_disk().is_err() { - println!("Failed to write rust-project.json to disk for rust-analyzer"); - } else { - println!("Successfully generated rust-project.json"); - println!("rust-analyzer will now parse exercises, restart your language server or editor") + let (mut app_state, state_file_status) = AppState::new( + info_file.exercises, + info_file.final_message.unwrap_or_default(), + )?; + + // Show the welcome message if the state file doesn't exist yet. + if let Some(welcome_message) = info_file.welcome_message { + match state_file_status { + StateFileStatus::NotRead => { + let mut stdout = io::stdout().lock(); + clear_terminal(&mut stdout)?; + + let welcome_message = welcome_message.trim(); + write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; + stdout.flush()?; + press_enter_prompt()?; + clear_terminal(&mut stdout)?; } + StateFileStatus::Read => (), } - - Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { - 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."); - std::process::exit(1); - } - Ok(WatchStatus::Finished) => { - println!( - "{emoji} All exercises completed! {emoji}", - emoji = Emoji("🎉", "★") - ); - println!("\n{FENISH_LINE}\n"); - } - Ok(WatchStatus::Unfinished) => { - 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"); - } - }, } -} -fn spawn_watch_shell( - failed_exercise_hint: &Arc<Mutex<Option<String>>>, - should_quit: Arc<AtomicBool>, -) { - let failed_exercise_hint = Arc::clone(failed_exercise_hint); - println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here."); - thread::spawn(move || loop { - let mut input = String::new(); - match io::stdin().read_line(&mut input) { - Ok(_) => { - let input = input.trim(); - if input == "hint" { - if let Some(hint) = &*failed_exercise_hint.lock().unwrap() { - println!("{hint}"); - } - } else if input == "clear" { - println!("\x1B[2J\x1B[1;1H"); - } else if input.eq("quit") { - should_quit.store(true, Ordering::SeqCst); - println!("Bye!"); - } else if input.eq("help") { - println!("Commands available to you in watch mode:"); - println!(" hint - prints the current exercise's hint"); - println!(" clear - clears the screen"); - println!(" quit - quits watch mode"); - println!(" !<cmd> - executes a command, like `!rustc --explain E0381`"); - println!(" help - displays this help message"); - println!(); - println!("Watch mode automatically re-evaluates the current exercise"); - println!("when you edit a file's contents.") - } else if let Some(cmd) = input.strip_prefix('!') { - let parts: Vec<&str> = cmd.split_whitespace().collect(); - if parts.is_empty() { - println!("no command provided"); - } else if let Err(e) = Command::new(parts[0]).args(&parts[1..]).status() { - println!("failed to execute command `{}`: {}", cmd, e); - } - } else { - println!("unknown command: {input}"); + match args.command { + None => { + let notify_exercise_names = if args.manual_run { + None + } else { + // For the notify event handler thread. + // Leaking is not a problem because the slice lives until the end of the program. + Some( + &*app_state + .exercises() + .iter() + .map(|exercise| exercise.name.as_bytes()) + .collect::<Vec<_>>() + .leak(), + ) + }; + + loop { + match watch::watch(&mut app_state, notify_exercise_names)? { + WatchExit::Shutdown => break, + // It is much easier to exit the watch mode, launch the list mode and then restart + // the watch mode instead of trying to pause the watch threads and correct the + // watch state. + WatchExit::List => list::list(&mut app_state)?, } } - Err(error) => println!("error reading command: {error}"), } - }); -} - -fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> &'a Exercise { - if name.eq("next") { - exercises - .iter() - .find(|e| !e.looks_done()) - .unwrap_or_else(|| { - println!("🎉 Congratulations! You have done all the exercises!"); - println!("🔚 There are no more exercises to do next!"); - std::process::exit(1) - }) - } else { - exercises - .iter() - .find(|e| e.name == name) - .unwrap_or_else(|| { - println!("No exercise found for '{name}'!"); - std::process::exit(1) - }) - } -} - -enum WatchStatus { - Finished, - Unfinished, -} - -fn watch( - exercises: &[Exercise], - verbose: bool, - success_hints: bool, -) -> notify::Result<WatchStatus> { - /* Clears the terminal with an ANSI escape code. - Works in UNIX and newer Windows terminals. */ - fn clear_screen() { - println!("\x1Bc"); - } - - let (tx, rx) = channel(); - let should_quit = Arc::new(AtomicBool::new(false)); - - let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))?; - watcher.watch(Path::new("./exercises"), RecursiveMode::Recursive)?; - - clear_screen(); - - let to_owned_hint = |t: &Exercise| t.hint.to_owned(); - let failed_exercise_hint = match verify( - exercises.iter(), - (0, exercises.len()), - verbose, - success_hints, - ) { - Ok(_) => return Ok(WatchStatus::Finished), - Err(exercise) => Arc::new(Mutex::new(Some(to_owned_hint(exercise)))), - }; - spawn_watch_shell(&failed_exercise_hint, Arc::clone(&should_quit)); - loop { - match rx.recv_timeout(Duration::from_secs(1)) { - Ok(event) => match event { - DebouncedEvent::Create(b) | DebouncedEvent::Chmod(b) | DebouncedEvent::Write(b) => { - if b.extension() == Some(OsStr::new("rs")) && b.exists() { - let filepath = b.as_path().canonicalize().unwrap(); - let pending_exercises = exercises - .iter() - .find(|e| filepath.ends_with(&e.path)) - .into_iter() - .chain( - exercises - .iter() - .filter(|e| !e.looks_done() && !filepath.ends_with(&e.path)), - ); - let num_done = exercises - .iter() - .filter(|e| e.looks_done() && !filepath.ends_with(&e.path)) - .count(); - clear_screen(); - match verify( - pending_exercises, - (num_done, exercises.len()), - verbose, - success_hints, - ) { - Ok(_) => return Ok(WatchStatus::Finished), - Err(exercise) => { - let mut failed_exercise_hint = failed_exercise_hint.lock().unwrap(); - *failed_exercise_hint = Some(to_owned_hint(exercise)); - } - } - } - } - _ => {} - }, - Err(RecvTimeoutError::Timeout) => { - // the timeout expired, just check the `should_quit` variable below then loop again + Some(Subcommands::Run { name }) => { + if let Some(name) = name { + app_state.set_current_exercise_by_name(&name)?; } - Err(e) => println!("watch error: {e:?}"), + run::run(&mut app_state)?; + } + Some(Subcommands::Reset { name }) => { + app_state.set_current_exercise_by_name(&name)?; + let exercise_path = app_state.reset_current_exercise()?; + println!("The exercise {exercise_path} has been reset"); } - // Check if we need to exit - if should_quit.load(Ordering::SeqCst) { - return Ok(WatchStatus::Unfinished); + Some(Subcommands::Hint { name }) => { + if let Some(name) = name { + app_state.set_current_exercise_by_name(&name)?; + } + println!("{}", app_state.current_exercise().hint); } + // Handled in an earlier match. + Some(Subcommands::Init | Subcommands::Dev(_)) => (), } -} -fn rustc_exists() -> bool { - Command::new("rustc") - .args(["--version"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .stdin(Stdio::null()) - .spawn() - .and_then(|mut child| child.wait()) - .map(|status| status.success()) - .unwrap_or(false) + Ok(()) } -const DEFAULT_OUT: &str = r#"Thanks for installing Rustlings! - -Is this your first time? Don't worry, Rustlings was made for beginners! We are -going to teach you a lot of things about Rust, but before we can get -started, here's a couple of notes about how Rustlings operates: - -1. The central concept behind Rustlings is that you solve exercises. These - exercises usually have some sort of syntax error in them, which will cause - them to fail compilation or testing. Sometimes there's a logic error instead - of a syntax error. No matter what error, it's your job to find it and fix it! - You'll know when you fixed it because then, the exercise will compile and - Rustlings will be able to move on to the next exercise. -2. If you run Rustlings in watch mode (which we recommend), it'll automatically - start with the first exercise. Don't get confused by an error message popping - up as soon as you run Rustlings! This is part of the exercise that you're - supposed to solve, so open the exercise file in an editor and start your - detective work! -3. If you're stuck on an exercise, there is a helpful hint you can view by typing - 'hint' (in watch mode), or running `rustlings hint exercise_name`. -4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! - (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, - and sometimes, other learners do too so you can help each other out! -5. If you want to use `rust-analyzer` with exercises, which provides features like - autocompletion, run the command `rustlings lsp`. +const OLD_METHOD_ERR: &str = "You are trying to run Rustlings using the old method before v6. +The new method doesn't include cloning the Rustlings' repository. +Please follow the instructions in the README: +https://github.com/rust-lang/rustlings#getting-started"; -Got all that? Great! To get started, run `rustlings watch` in order to get the first -exercise. Make sure to have your editor open!"#; +const FORMAT_VERSION_HIGHER_ERR: &str = + "The format version specified in the `info.toml` file is higher than the last one supported. +It is possible that you have an outdated version of Rustlings. +Try to install the latest Rustlings version first."; -const FENISH_LINE: &str = r"+----------------------------------------------------+ -| You made it to the Fe-nish line! | -+-------------------------- ------------------------+ - \\/ - ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ - ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ - ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ - ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ - ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒ - -We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, please don't hesitate to report them to our repo. -You can also contribute your own exercises to help the greater community! - -Before reporting an issue or contributing, please read our guidelines: -https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; - -const WELCOME: &str = r" welcome to... +const PRE_INIT_MSG: &str = r" + Welcome to... _ _ _ _ __ _ _ ___| |_| (_)_ __ __ _ ___ | '__| | | / __| __| | | '_ \ / _` / __| | | | |_| \__ \ |_| | | | | | (_| \__ \ |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; + |___/ + +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."; |
