mirror of
https://github.com/notohh/rustlings.git
synced 2024-11-24 22:47:32 -05:00
Start with the TUI
This commit is contained in:
parent
0bf51c6a0d
commit
b0f19fd862
6 changed files with 180 additions and 259 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -207,19 +207,6 @@ dependencies = [
|
||||||
"static_assertions",
|
"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]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.12"
|
version = "0.5.12"
|
||||||
|
@ -278,12 +265,6 @@ version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "encode_unicode"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -447,12 +428,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy_static"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.153"
|
version = "0.2.153"
|
||||||
|
@ -714,7 +689,6 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"clap",
|
"clap",
|
||||||
"console",
|
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"glob",
|
"glob",
|
||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
|
|
|
@ -36,7 +36,6 @@ edition.workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
clap = { version = "4.5.4", features = ["derive"] }
|
||||||
console = "0.15.8"
|
|
||||||
crossterm = "0.27.0"
|
crossterm = "0.27.0"
|
||||||
notify-debouncer-mini = "0.4.1"
|
notify-debouncer-mini = "0.4.1"
|
||||||
ratatui = "0.26.1"
|
ratatui = "0.26.1"
|
||||||
|
|
59
src/consts.rs
Normal file
59
src/consts.rs
Normal file
|
@ -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";
|
245
src/main.rs
245
src/main.rs
|
@ -1,26 +1,22 @@
|
||||||
|
use crate::consts::{DEFAULT_OUT, WELCOME};
|
||||||
use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
|
use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
|
||||||
use crate::exercise::{Exercise, ExerciseList};
|
use crate::exercise::{Exercise, ExerciseList};
|
||||||
use crate::run::run;
|
use crate::run::run;
|
||||||
|
use crate::tui::tui;
|
||||||
use crate::verify::verify;
|
use crate::verify::verify;
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use console::Emoji;
|
use std::io::Write;
|
||||||
use notify_debouncer_mini::notify::RecursiveMode;
|
|
||||||
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
|
||||||
use std::io::{BufRead, Write};
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::exit;
|
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;
|
use verify::VerifyState;
|
||||||
|
|
||||||
|
mod consts;
|
||||||
mod embedded;
|
mod embedded;
|
||||||
mod exercise;
|
mod exercise;
|
||||||
mod init;
|
mod init;
|
||||||
mod run;
|
mod run;
|
||||||
|
mod tui;
|
||||||
mod verify;
|
mod verify;
|
||||||
|
|
||||||
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
|
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
|
||||||
|
@ -37,7 +33,7 @@ enum Subcommands {
|
||||||
Init,
|
Init,
|
||||||
/// Verify all exercises according to the recommended order
|
/// Verify all exercises according to the recommended order
|
||||||
Verify,
|
Verify,
|
||||||
/// Rerun `verify` when files were edited
|
/// Same as just running `rustlings` without a subcommand.
|
||||||
Watch,
|
Watch,
|
||||||
/// Run/Test a single exercise
|
/// Run/Test a single exercise
|
||||||
Run {
|
Run {
|
||||||
|
@ -106,21 +102,20 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = args.command.unwrap_or_else(|| {
|
match args.command {
|
||||||
println!("{DEFAULT_OUT}\n");
|
None | Some(Subcommands::Watch) => {
|
||||||
exit(0);
|
println!("{DEFAULT_OUT}\n");
|
||||||
});
|
tui(&exercises)?;
|
||||||
|
}
|
||||||
match command {
|
|
||||||
// `Init` is handled above.
|
// `Init` is handled above.
|
||||||
Subcommands::Init => (),
|
Some(Subcommands::Init) => (),
|
||||||
Subcommands::List {
|
Some(Subcommands::List {
|
||||||
paths,
|
paths,
|
||||||
names,
|
names,
|
||||||
filter,
|
filter,
|
||||||
unsolved,
|
unsolved,
|
||||||
solved,
|
solved,
|
||||||
} => {
|
}) => {
|
||||||
if !paths && !names {
|
if !paths && !names {
|
||||||
println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status");
|
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);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
Some(Subcommands::Run { name }) => {
|
||||||
Subcommands::Run { name } => {
|
|
||||||
let exercise = find_exercise(&name, &exercises)?;
|
let exercise = find_exercise(&name, &exercises)?;
|
||||||
run(exercise).unwrap_or_else(|_| exit(1));
|
run(exercise).unwrap_or_else(|_| exit(1));
|
||||||
}
|
}
|
||||||
|
Some(Subcommands::Reset { name }) => {
|
||||||
Subcommands::Reset { name } => {
|
|
||||||
let exercise = find_exercise(&name, &exercises)?;
|
let exercise = find_exercise(&name, &exercises)?;
|
||||||
EMBEDDED_FILES
|
EMBEDDED_FILES
|
||||||
.write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite)
|
.write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite)
|
||||||
.with_context(|| format!("Failed to reset the exercise {exercise}"))?;
|
.with_context(|| format!("Failed to reset the exercise {exercise}"))?;
|
||||||
println!("The file {} has been reset!", exercise.path.display());
|
println!("The file {} has been reset!", exercise.path.display());
|
||||||
}
|
}
|
||||||
|
Some(Subcommands::Hint { name }) => {
|
||||||
Subcommands::Hint { name } => {
|
|
||||||
let exercise = find_exercise(&name, &exercises)?;
|
let exercise = find_exercise(&name, &exercises)?;
|
||||||
println!("{}", exercise.hint);
|
println!("{}", exercise.hint);
|
||||||
}
|
}
|
||||||
|
Some(Subcommands::Verify) => match verify(&exercises, (0, exercises.len()))? {
|
||||||
Subcommands::Verify => match verify(&exercises, (0, exercises.len()))? {
|
|
||||||
VerifyState::AllExercisesDone => println!("All exercises done!"),
|
VerifyState::AllExercisesDone => println!("All exercises done!"),
|
||||||
VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_watch_shell(
|
|
||||||
failed_exercise_hint: Arc<Mutex<Option<String>>>,
|
|
||||||
should_quit: Arc<AtomicBool>,
|
|
||||||
) {
|
|
||||||
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> {
|
fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> {
|
||||||
if name == "next" {
|
if name == "next" {
|
||||||
for exercise in exercises {
|
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)
|
.find(|e| e.name == name)
|
||||||
.with_context(|| format!("No exercise found for '{name}'!"))
|
.with_context(|| format!("No exercise found for '{name}'!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WatchStatus {
|
|
||||||
Finished,
|
|
||||||
Unfinished,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn watch(exercises: &[Exercise]) -> 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 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";
|
|
||||||
|
|
92
src/tui.rs
Normal file
92
src/tui.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use console::style;
|
use crossterm::style::{Attribute, ContentStyle, Stylize};
|
||||||
use std::io::{stdout, Write};
|
use std::io::{stdout, Write};
|
||||||
|
|
||||||
use crate::exercise::{Exercise, Mode, State};
|
use crate::exercise::{Exercise, Mode, State};
|
||||||
|
@ -50,20 +50,26 @@ pub fn verify<'a>(
|
||||||
println!(
|
println!(
|
||||||
"\nYou can keep working on this exercise,
|
"\nYou can keep working on this exercise,
|
||||||
or jump into the next one by removing the {} comment:\n",
|
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 {
|
for context_line in context {
|
||||||
let formatted_line = if context_line.important {
|
let formatted_line = if context_line.important {
|
||||||
format!("{}", style(context_line.line).bold())
|
format!("{}", context_line.line.bold())
|
||||||
} else {
|
} else {
|
||||||
context_line.line
|
context_line.line
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{:>2} {} {}",
|
"{:>2} {} {}",
|
||||||
style(context_line.number).blue().bold(),
|
ContentStyle {
|
||||||
style("|").blue(),
|
foreground_color: Some(crossterm::style::Color::Blue),
|
||||||
|
background_color: None,
|
||||||
|
underline_color: None,
|
||||||
|
attributes: Attribute::Bold.into()
|
||||||
|
}
|
||||||
|
.apply(context_line.number),
|
||||||
|
"|".blue(),
|
||||||
formatted_line,
|
formatted_line,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue