From b0f19fd862d659d2d4b01f2faa6b006fe2c60561 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 5 Apr 2024 03:04:53 +0200 Subject: [PATCH] Start with the TUI --- Cargo.lock | 26 ------ Cargo.toml | 1 - src/consts.rs | 59 ++++++++++++ src/main.rs | 245 ++++---------------------------------------------- src/tui.rs | 92 +++++++++++++++++++ src/verify.rs | 16 ++-- 6 files changed, 180 insertions(+), 259 deletions(-) create mode 100644 src/consts.rs create mode 100644 src/tui.rs diff --git a/Cargo.lock b/Cargo.lock index e03980ca..33d3030a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,19 +207,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.52.0", -] - [[package]] name = "crossbeam-channel" version = "0.5.12" @@ -278,12 +265,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "equivalent" version = "1.0.1" @@ -447,12 +428,6 @@ dependencies = [ "libc", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.153" @@ -714,7 +689,6 @@ dependencies = [ "anyhow", "assert_cmd", "clap", - "console", "crossterm", "glob", "notify-debouncer-mini", diff --git a/Cargo.toml b/Cargo.toml index d80550a0..da09ba18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ edition.workspace = true [dependencies] anyhow.workspace = true clap = { version = "4.5.4", features = ["derive"] } -console = "0.15.8" crossterm = "0.27.0" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 00000000..40bf150f --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,59 @@ +pub const WELCOME: &str = r" welcome to... + _ _ _ + _ __ _ _ ___| |_| (_)_ __ __ _ ___ + | '__| | | / __| __| | | '_ \ / _` / __| + | | | |_| \__ \ |_| | | | | | (_| \__ \ + |_| \__,_|___/\__|_|_|_| |_|\__, |___/ + |___/"; + +pub const DEFAULT_OUT: &str = + "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! + +Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the `rustlings` directory!"; + +pub const FENISH_LINE: &str = "+----------------------------------------------------+ +| You made it to the Fe-nish line! | ++-------------------------- ------------------------+ + \\/\x1b[31m + ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ + ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ + ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ + ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ + ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m + +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"; diff --git a/src/main.rs b/src/main.rs index c62837d3..47afd019 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,22 @@ +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 console::Emoji; -use notify_debouncer_mini::notify::RecursiveMode; -use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; -use std::io::{BufRead, Write}; +use std::io::Write; use std::path::Path; use std::process::exit; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::{channel, RecvTimeoutError}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; -use std::{io, thread}; use verify::VerifyState; +mod consts; mod embedded; mod exercise; mod init; mod run; +mod tui; mod verify; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code @@ -37,7 +33,7 @@ enum Subcommands { Init, /// Verify all exercises according to the recommended order Verify, - /// Rerun `verify` when files were edited + /// Same as just running `rustlings` without a subcommand. Watch, /// Run/Test a single exercise Run { @@ -106,21 +102,20 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let command = args.command.unwrap_or_else(|| { - println!("{DEFAULT_OUT}\n"); - exit(0); - }); - - match command { + match args.command { + None | Some(Subcommands::Watch) => { + println!("{DEFAULT_OUT}\n"); + tui(&exercises)?; + } // `Init` is handled above. - Subcommands::Init => (), - Subcommands::List { + Some(Subcommands::Init) => (), + Some(Subcommands::List { paths, names, filter, unsolved, solved, - } => { + }) => { if !paths && !names { println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status"); } @@ -188,90 +183,30 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini ); exit(0); } - - Subcommands::Run { name } => { + Some(Subcommands::Run { name }) => { let exercise = find_exercise(&name, &exercises)?; run(exercise).unwrap_or_else(|_| exit(1)); } - - Subcommands::Reset { name } => { + 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()); } - - Subcommands::Hint { name } => { + Some(Subcommands::Hint { name }) => { let exercise = find_exercise(&name, &exercises)?; println!("{}", exercise.hint); } - - Subcommands::Verify => match verify(&exercises, (0, exercises.len()))? { + Some(Subcommands::Verify) => match verify(&exercises, (0, exercises.len()))? { VerifyState::AllExercisesDone => println!("All exercises done!"), VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), }, - - 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."); - 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"); - } - }, } Ok(()) } -fn spawn_watch_shell( - failed_exercise_hint: Arc>>, - should_quit: Arc, -) { - println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here."); - - thread::spawn(move || { - let mut input = String::with_capacity(32); - let mut stdin = io::stdin().lock(); - - loop { - // Recycle input buffer. - input.clear(); - - if let Err(e) = stdin.read_line(&mut input) { - println!("error reading command: {e}"); - } - - 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 == "quit" { - should_quit.store(true, Ordering::SeqCst); - println!("Bye!"); - } else if input == "help" { - println!("{WATCH_MODE_HELP_MESSAGE}"); - } else { - println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}"); - } - } - }); -} - fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> { if name == "next" { for exercise in exercises { @@ -290,147 +225,3 @@ fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exerci .find(|e| e.name == name) .with_context(|| format!("No exercise found for '{name}'!")) } - -enum WatchStatus { - Finished, - Unfinished, -} - -fn watch(exercises: &[Exercise]) -> Result { - /* 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 debouncer = new_debouncer(Duration::from_secs(1), tx)?; - debouncer - .watcher() - .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - - clear_screen(); - - 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)); - - 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(); - - clear_screen(); - - match verify( - pending_exercises.iter().copied(), - (num_done, exercises.len()), - )? { - VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), - VerifyState::Failed(exercise) => { - let hint = exercise.hint.clone(); - *failed_exercise_hint.lock().unwrap() = Some(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:?}"), - } - // Check if we need to exit - if should_quit.load(Ordering::SeqCst) { - return Ok(WatchStatus::Unfinished); - } - } -} - -const WELCOME: &str = r" welcome to... - _ _ _ - _ __ _ _ ___| |_| (_)_ __ __ _ ___ - | '__| | | / __| __| | | '_ \ / _` / __| - | | | |_| \__ \ |_| | | | | | (_| \__ \ - |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; - -const DEFAULT_OUT: &str = - "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! - -Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. -Make sure to have your editor open in the `rustlings` directory!"; - -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 - help - displays this help message - -Watch mode automatically re-evaluates the current exercise -when you edit a file's contents."; - -const FENISH_LINE: &str = "+----------------------------------------------------+ -| You made it to the Fe-nish line! | -+-------------------------- ------------------------+ - \\/\x1b[31m - ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ - ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ - ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ - ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ - ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m - -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"; diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 00000000..bb873652 --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,92 @@ +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/verify.rs b/src/verify.rs index 5beb2069..aec2185c 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use console::style; +use crossterm::style::{Attribute, ContentStyle, Stylize}; use std::io::{stdout, Write}; use crate::exercise::{Exercise, Mode, State}; @@ -50,20 +50,26 @@ pub fn verify<'a>( 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() + "`I AM NOT DONE`".bold() ); for context_line in context { let formatted_line = if context_line.important { - format!("{}", style(context_line.line).bold()) + format!("{}", context_line.line.bold()) } else { context_line.line }; println!( "{:>2} {} {}", - style(context_line.number).blue().bold(), - style("|").blue(), + 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, ); }