summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authormo8it <mo8it@proton.me>2024-07-05 13:39:50 +0200
committermo8it <mo8it@proton.me>2024-07-05 13:39:50 +0200
commit7123c7ae3a9605fbe962e4ef0a0f1424cd16fef8 (patch)
treec67f7e62bb9a179ae4fdbab492501cb6847e64c7 /src/main.rs
parent77b687d501771c24bd83294d97b8e6f9ffa92d6b (diff)
parent4d9c346a173bb722b929f3ea3c00f84954483e24 (diff)
Merge remote-tracking branch 'upstream/main' into fix-enum-variant-inconsistency
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs573
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.";