Compare commits

...

16 commits

Author SHA1 Message Date
mo8it 24539666af Show the final message 2024-04-12 20:06:56 +02:00
mo8it 757723a7e8 Add missing newline 2024-04-12 19:30:36 +02:00
mo8it ff4c752984 Print FAILED 2024-04-12 19:30:29 +02:00
mo8it 06d1089714 Set pending on fail in run mode 2024-04-12 19:24:26 +02:00
mo8it 6e827da570 It doesn't take minutes :P 2024-04-12 19:18:16 +02:00
mo8it 279ebdc153 Remove the modifier filter in the list mode 2024-04-12 19:16:52 +02:00
mo8it 9b0eeb815a Fix Display for Exercise 2024-04-12 19:07:17 +02:00
mo8it 44824718b2 Remove unused import 2024-04-12 18:58:01 +02:00
mo8it 8bd03093eb Add newline at the end of the generated .gitignore 2024-04-12 18:57:39 +02:00
mo8it d5a6dee1b3 Handle the case when all exercises are done 2024-04-12 18:57:04 +02:00
mo8it a534de0312 Implement going to the next exercise 2024-04-12 15:27:29 +02:00
mo8it 98c5088a39 Update deps 2024-04-12 14:52:50 +02:00
mo8it 6807e63c5f Show done message 2024-04-12 02:45:54 +02:00
mo8it 2a95a3e966 Deal with long strings 2024-04-12 01:24:01 +02:00
mo8it 1e3745ccdf Update winnow 2024-04-12 00:58:26 +02:00
mo8it d8160f9113 Remove outdated installation methods 2024-04-12 00:56:40 +02:00
20 changed files with 279 additions and 453 deletions

View file

@ -1,8 +0,0 @@
{
"image": "mcr.microsoft.com/devcontainers/rust:1",
"updateContentCommand": ["cargo", "build"],
"postAttachCommand": ["rustlings", "watch"],
"remoteEnv": {
"PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/target/debug"
}
}

4
.gitignore vendored
View file

@ -19,9 +19,5 @@ public/
.idea
*.iml
# VS Code extension recommendations
.vscode/*
!.vscode/extensions.json
# Ignore file for editors like Helix
.ignore

View file

@ -1,7 +0,0 @@
tasks:
- init: /workspace/rustlings/install.sh
command: /workspace/.cargo/bin/rustlings watch
vscode:
extensions:
- rust-lang.rust-analyzer@0.3.1348

View file

@ -1,5 +0,0 @@
{
"recommendations": [
"rust-lang.rust-analyzer"
]
}

8
Cargo.lock generated
View file

@ -25,9 +25,9 @@ dependencies = [
[[package]]
name = "allocator-api2"
version = "0.2.16"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "anstream"
@ -1130,9 +1130,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.6.5"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
dependencies = [
"memchr",
]

View file

@ -18,78 +18,7 @@ _Note: If you're on Linux, make sure you've installed gcc. Deb: `sudo apt instal
You will need to have Rust installed. You can get it by visiting <https://rustup.rs>. This'll also install Cargo, Rust's package/project manager.
## MacOS/Linux
Just run:
```bash
curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash
```
Or if you want it to be installed to a different path:
```bash
curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash -s mypath/
```
This will install Rustlings and give you access to the `rustlings` command. Run it to get started!
### Nix
Basically: Clone the repository at the latest tag, finally run `nix develop` or `nix-shell`.
```bash
# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1)
git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings
cd rustlings
# if nix version > 2.3
nix develop
# if nix version <= 2.3
nix-shell
```
## Windows
In PowerShell (Run as Administrator), set `ExecutionPolicy` to `RemoteSigned`:
```ps1
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
Then, you can run:
```ps1
Start-BitsTransfer -Source https://raw.githubusercontent.com/rust-lang/rustlings/main/install.ps1 -Destination $env:TMP/install_rustlings.ps1; Unblock-File $env:TMP/install_rustlings.ps1; Invoke-Expression $env:TMP/install_rustlings.ps1
```
To install Rustlings. Same as on MacOS/Linux, you will have access to the `rustlings` command after it. Keep in mind that this works best in PowerShell, and any other terminals may give you errors.
If you get a permission denied message, you might have to exclude the directory where you cloned Rustlings in your antivirus.
## Browser
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/rust-lang/rustlings)
[![Open Rustlings On Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/?repo=rust-lang%2Frustlings&ref=main)
## Manually
Basically: Clone the repository at the latest tag, run `cargo install --path .`.
```bash
# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1)
git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings
cd rustlings
cargo install --force --path .
```
If there are installation errors, ensure that your toolchain is up to date. For the latest, run:
```bash
rustup update
```
Then, same as above, run `rustlings` to get started.
<!-- TODO: Installation with Cargo -->
## Doing exercises
@ -138,10 +67,6 @@ rustlings list
After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. These quizzes are found in `exercises/quizN.rs`.
## Enabling `rust-analyzer`
Run the command `rustlings lsp` which will generate a `rust-project.json` at the root of the project, this allows [rust-analyzer](https://rust-analyzer.github.io/) to parse each exercise.
## Continuing On
Once you've completed Rustlings, put your new knowledge to good use! Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.

View file

@ -1,78 +0,0 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1694183432,
"narHash": "sha256-YyPGNapgZNNj51ylQMw9lAgvxtM2ai1HZVUu3GS8Fng=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,78 +0,0 @@
{
description = "Small exercises to get you used to reading and writing Rust code";
inputs = {
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
};
outputs = { self, flake-utils, nixpkgs, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
cargoBuildInputs = with pkgs; lib.optionals stdenv.isDarwin [
darwin.apple_sdk.frameworks.CoreServices
];
rustlings =
pkgs.rustPlatform.buildRustPackage {
name = "rustlings";
version = "5.6.1";
buildInputs = cargoBuildInputs;
nativeBuildInputs = [pkgs.git];
src = with pkgs.lib; cleanSourceWith {
src = self;
# a function that returns a bool determining if the path should be included in the cleaned source
filter = path: type:
let
# filename
baseName = builtins.baseNameOf (toString path);
# path from root directory
path' = builtins.replaceStrings [ "${self}/" ] [ "" ] path;
# checks if path is in the directory
inDirectory = directory: hasPrefix directory path';
in
inDirectory "src" ||
inDirectory "tests" ||
hasPrefix "Cargo" baseName ||
baseName == "info.toml";
};
cargoLock.lockFile = ./Cargo.lock;
};
in
{
devShell = pkgs.mkShell {
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
buildInputs = with pkgs; [
cargo
rustc
rust-analyzer
rustlings
rustfmt
clippy
] ++ cargoBuildInputs;
};
apps = let
rustlings-app = {
type = "app";
program = "${rustlings}/bin/rustlings";
};
in {
default = rustlings-app;
rustlings = rustlings-app;
};
packages = {
inherit rustlings;
default = rustlings;
};
});
}

View file

@ -1,3 +1,36 @@
welcome_message = """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!
"""
final_message = """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
"""
# INTRO
[[exercises]]

View file

@ -1,6 +0,0 @@
(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in fetchTarball {
url =
"https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}) { src = ./.; }).shellNix

View file

@ -1,8 +1,16 @@
use anyhow::{bail, Context, Result};
use crossterm::{
style::Stylize,
terminal::{Clear, ClearType},
ExecutableCommand,
};
use serde::{Deserialize, Serialize};
use std::fs;
use std::{
fs,
io::{StdoutLock, Write},
};
use crate::exercise::Exercise;
use crate::{exercise::Exercise, FENISH_LINE};
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
@ -43,24 +51,29 @@ impl StateFile {
}
}
pub struct AppState {
state_file: StateFile,
exercises: &'static [Exercise],
n_done: u16,
current_exercise: &'static Exercise,
}
#[must_use]
pub enum ExercisesProgress {
AllDone,
Pending,
}
pub struct AppState {
state_file: StateFile,
exercises: &'static [Exercise],
n_done: u16,
current_exercise: &'static Exercise,
final_message: &'static str,
}
impl AppState {
pub fn new(exercises: Vec<Exercise>) -> Self {
// Leaking for sending the exercises to the debounce event handler.
// Leaking is not a problem since the exercises' slice is used until the end of the program.
pub fn new(mut exercises: Vec<Exercise>, mut final_message: String) -> Self {
// Leaking especially for sending the exercises to the debounce event handler.
// Leaking is not a problem because the `AppState` instance lives until
// the end of the program.
exercises.shrink_to_fit();
let exercises = exercises.leak();
final_message.shrink_to_fit();
let final_message = final_message.leak();
let state_file = StateFile::read_or_default(exercises);
let n_done = state_file
@ -74,6 +87,7 @@ impl AppState {
exercises,
n_done,
current_exercise,
final_message,
}
}
@ -143,7 +157,7 @@ impl AppState {
Ok(())
}
fn next_exercise_ind(&self) -> Option<usize> {
fn next_pending_exercise_ind(&self) -> Option<usize> {
let current_ind = self.state_file.current_exercise_ind;
if current_ind == self.state_file.progress.len() - 1 {
@ -167,14 +181,44 @@ impl AppState {
}
}
pub fn done_current_exercise(&mut self) -> Result<ExercisesProgress> {
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
let done = &mut self.state_file.progress[self.state_file.current_exercise_ind];
if !*done {
*done = true;
self.n_done += 1;
}
let Some(ind) = self.next_exercise_ind() else {
let Some(ind) = self.next_pending_exercise_ind() else {
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
for (exercise_ind, exercise) in self.exercises().iter().enumerate() {
writer.write_fmt(format_args!("Running {exercise} ... "))?;
writer.flush()?;
if !exercise.run()?.status.success() {
writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?;
self.state_file.current_exercise_ind = exercise_ind;
self.current_exercise = exercise;
// No check if the exercise is done before setting it to pending
// because no pending exercise was found.
self.state_file.progress[exercise_ind] = false;
self.n_done -= 1;
self.state_file.write()?;
return Ok(ExercisesProgress::Pending);
}
writer.write_fmt(format_args!("{}\n", "ok".green()))?;
}
writer.execute(Clear(ClearType::All))?;
writer.write_all(FENISH_LINE.as_bytes())?;
writer.write_all(self.final_message.as_bytes())?;
writer.write_all(b"\n")?;
return Ok(ExercisesProgress::AllDone);
};
@ -183,3 +227,9 @@ impl AppState {
Ok(ExercisesProgress::Pending)
}
}
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
All exercises seem to be done.
Recompiling and running all exercises to make sure that all of them are actually done.
";

View file

@ -1,59 +0,0 @@
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";

View file

@ -24,6 +24,9 @@ pub enum Mode {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InfoFile {
// TODO
pub welcome_message: Option<String>,
pub final_message: Option<String>,
pub exercises: Vec<Exercise>,
}
@ -39,10 +42,7 @@ impl InfoFile {
.context("Failed to parse `info.toml`")?;
if slf.exercises.is_empty() {
panic!(
"There are no exercises yet!
If you are developing third-party exercises, add at least one exercise before testing."
);
panic!("{NO_EXERCISES_ERR}");
}
Ok(slf)
@ -116,6 +116,9 @@ impl Exercise {
impl Display for Exercise {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.path.fmt(f)
Display::fmt(&self.path.display(), f)
}
}
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
If you are developing third-party exercises, add at least one exercise before testing.";

View file

@ -36,47 +36,33 @@ publish = false
}
fn create_gitignore() -> io::Result<()> {
let gitignore = b"/target
/.rustlings-state.json";
OpenOptions::new()
.create_new(true)
.write(true)
.open(".gitignore")?
.write_all(gitignore)
.write_all(GITIGNORE)
}
fn create_vscode_dir() -> Result<()> {
create_dir(".vscode").context("Failed to create the directory `.vscode`")?;
let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
OpenOptions::new()
.create_new(true)
.write(true)
.open(".vscode/extensions.json")?
.write_all(vs_code_extensions_json)?;
.write_all(VS_CODE_EXTENSIONS_JSON)?;
Ok(())
}
pub fn init(exercises: &[Exercise]) -> Result<()> {
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
bail!(
"A directory with the name `exercises` and a file with the name `Cargo.toml` already exist
in the current directory. It looks like Rustlings was already initialized here.
Run `rustlings` for instructions on getting started with the exercises.
If you didn't already initialize Rustlings, please initialize it in another directory."
);
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
}
let rustlings_path = Path::new("rustlings");
if let Err(e) = create_dir(rustlings_path) {
if e.kind() == ErrorKind::AlreadyExists {
bail!(
"A directory with the name `rustlings` already exists in the current directory.
You probably already initialized Rustlings.
Run `cd rustlings`
Then run `rustlings` again"
);
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
}
return Err(e.into());
}
@ -96,3 +82,22 @@ Then run `rustlings` again"
Ok(())
}
const GITIGNORE: &[u8] = b"/target
/.rustlings-state.json
";
const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str =
"A directory with the name `exercises` and a file with the name `Cargo.toml` already exist
in the current directory. It looks like Rustlings was already initialized here.
Run `rustlings` for instructions on getting started with the exercises.
If you didn't already initialize Rustlings, please initialize it in another directory.";
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
"A directory with the name `rustlings` already exists in the current directory.
You probably already initialized Rustlings.
Run `cd rustlings`
Then run `rustlings` again";

View file

@ -1,6 +1,6 @@
use anyhow::Result;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
@ -28,16 +28,10 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
let key = loop {
match event::read()? {
Event::Key(key) => {
if key.modifiers != KeyModifiers::NONE {
continue;
}
match key.kind {
KeyEventKind::Press | KeyEventKind::Repeat => break key,
KeyEventKind::Release => (),
}
}
Event::Key(key) => match key.kind {
KeyEventKind::Press | KeyEventKind::Repeat => break key,
KeyEventKind::Release => (),
},
// Redraw
Event::Resize(_, _) => continue 'outer,
// Ignore

View file

@ -3,7 +3,6 @@ use clap::{Parser, Subcommand};
use std::{path::Path, process::exit};
mod app_state;
mod consts;
mod embedded;
mod exercise;
mod init;
@ -14,7 +13,6 @@ mod watch;
use self::{
app_state::AppState,
consts::WELCOME,
exercise::InfoFile,
init::init,
list::list,
@ -54,11 +52,7 @@ enum Subcommands {
fn main() -> Result<()> {
let args = Args::parse();
which::which("cargo").context(
"Failed to find `cargo`.
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.",
)?;
which::which("cargo").context(CARGO_NOT_FOUND_ERR)?;
let mut info_file = InfoFile::parse()?;
info_file.exercises.shrink_to_fit();
@ -66,24 +60,15 @@ Try running `cargo --version` to diagnose the problem.",
if matches!(args.command, Some(Subcommands::Init)) {
init(&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."
);
println!("{POST_INIT_MSG}");
return Ok(());
} else if !Path::new("exercises").is_dir() {
println!(
"
{WELCOME}
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."
);
println!("{PRE_INIT_MSG}");
exit(1);
}
let mut app_state = AppState::new(exercises);
let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default());
match args.command {
None => loop {
@ -118,3 +103,47 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
Ok(())
}
const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`.
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.";
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.";
const POST_INIT_MSG: &str = "
Done initialization!
Run `cd rustlings` to go into the generated directory.
Then run `rustlings` for further instructions on getting started.";
const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! |
+-------------------------- ------------------------+
\\/\x1b[31m
\x1b[0m
";

View file

@ -1,6 +1,6 @@
use anyhow::{bail, Result};
use crossterm::style::Stylize;
use std::io::{stdout, Write};
use std::io::{self, Write};
use crate::app_state::{AppState, ExercisesProgress};
@ -8,28 +8,26 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
let exercise = app_state.current_exercise();
let output = exercise.run()?;
{
let mut stdout = stdout().lock();
stdout.write_all(&output.stdout)?;
stdout.write_all(&output.stderr)?;
stdout.flush()?;
}
let mut stdout = io::stdout().lock();
stdout.write_all(&output.stdout)?;
stdout.write_all(b"\n")?;
stdout.write_all(&output.stderr)?;
stdout.flush()?;
if !output.status.success() {
app_state.set_pending(app_state.current_exercise_ind())?;
bail!("Ran {exercise} with errors");
}
println!(
"{}{}",
stdout.write_fmt(format_args!(
"{}{}\n",
"✓ Successfully ran ".green(),
exercise.path.to_string_lossy().green(),
);
))?;
match app_state.done_current_exercise()? {
ExercisesProgress::AllDone => println!(
"🎉 Congratulations! You have done all the exercises!
🔚 There are no more exercises to do next!"
),
match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::AllDone => (),
ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()),
}

View file

@ -15,7 +15,7 @@ mod debounce_event;
mod state;
mod terminal_event;
use crate::app_state::AppState;
use crate::app_state::{AppState, ExercisesProgress};
use self::{
debounce_event::DebounceEventHandler,
@ -26,12 +26,13 @@ use self::{
enum WatchEvent {
Input(InputEvent),
FileChange { exercise_ind: usize },
TerminalResize,
NotifyErr(notify::Error),
TerminalEventErr(io::Error),
TerminalResize,
}
/// Returned by the watch mode to indicate what to do afterwards.
#[must_use]
pub enum WatchExit {
/// Exit the program.
Shutdown,
@ -54,30 +55,33 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
let mut watch_state = WatchState::new(app_state);
// TODO: bool
watch_state.run_current_exercise()?;
watch_state.render()?;
thread::spawn(move || terminal_event_handler(tx));
while let Ok(event) = rx.recv() {
match event {
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
ExercisesProgress::AllDone => break,
ExercisesProgress::Pending => watch_state.run_current_exercise()?,
},
WatchEvent::Input(InputEvent::Hint) => {
watch_state.show_hint()?;
}
WatchEvent::Input(InputEvent::List) => {
return Ok(WatchExit::List);
}
WatchEvent::TerminalResize => {
watch_state.render()?;
WatchEvent::Input(InputEvent::Quit) => {
watch_state.into_writer().write_all(QUIT_MSG)?;
break;
}
WatchEvent::Input(InputEvent::Quit) => break,
WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
watch_state.handle_invalid_cmd(&cmd)?;
}
WatchEvent::FileChange { exercise_ind } => {
// TODO: bool
watch_state.run_exercise_with_ind(exercise_ind)?;
}
WatchEvent::TerminalResize => {
watch_state.render()?;
}
WatchEvent::NotifyErr(e) => {
@ -89,10 +93,10 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
}
}
watch_state.into_writer().write_all(b"
We hope you're enjoying learning Rust!
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
")?;
Ok(WatchExit::Shutdown)
}
const QUIT_MSG: &[u8] = b"
We hope you're enjoying learning Rust!
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
";

View file

@ -6,15 +6,18 @@ use crossterm::{
};
use std::io::{self, StdoutLock, Write};
use crate::{app_state::AppState, progress_bar::progress_bar};
use crate::{
app_state::{AppState, ExercisesProgress},
progress_bar::progress_bar,
};
pub struct WatchState<'a> {
writer: StdoutLock<'a>,
app_state: &'a mut AppState,
stdout: Option<Vec<u8>>,
stderr: Option<Vec<u8>>,
message: Option<String>,
hint_displayed: bool,
show_hint: bool,
show_done: bool,
}
impl<'a> WatchState<'a> {
@ -26,8 +29,8 @@ impl<'a> WatchState<'a> {
app_state,
stdout: None,
stderr: None,
message: None,
hint_displayed: false,
show_hint: false,
show_done: false,
}
}
@ -36,29 +39,50 @@ impl<'a> WatchState<'a> {
self.writer
}
pub fn run_current_exercise(&mut self) -> Result<bool> {
pub fn run_current_exercise(&mut self) -> Result<()> {
self.show_hint = false;
let output = self.app_state.current_exercise().run()?;
self.stdout = Some(output.stdout);
if !output.status.success() {
if output.status.success() {
self.stderr = None;
self.show_done = true;
} else {
self.app_state
.set_pending(self.app_state.current_exercise_ind())?;
self.stderr = Some(output.stderr);
return Ok(false);
self.show_done = false;
}
self.stderr = None;
Ok(true)
self.render()
}
pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<bool> {
pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> {
self.app_state.set_current_exercise_ind(exercise_ind)?;
self.run_current_exercise()
}
fn show_prompt(&mut self) -> io::Result<()> {
self.writer.write_all(b"\n\n")?;
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
if !self.show_done {
self.writer
.write_all(b"The current exercise isn't done yet\n")?;
self.show_prompt()?;
return Ok(ExercisesProgress::Pending);
}
if !self.hint_displayed {
self.app_state.done_current_exercise(&mut self.writer)
}
fn show_prompt(&mut self) -> io::Result<()> {
self.writer.write_all(b"\n")?;
if self.show_done {
self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?;
}
if !self.show_hint {
self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?;
}
@ -69,7 +93,7 @@ impl<'a> WatchState<'a> {
}
pub fn render(&mut self) -> Result<()> {
// Prevent having the first line shifted after clearing because of the prompt.
// Prevent having the first line shifted.
self.writer.write_all(b"\n")?;
self.writer.execute(Clear(ClearType::All))?;
@ -84,18 +108,24 @@ impl<'a> WatchState<'a> {
self.writer.write_all(b"\n")?;
}
if let Some(message) = &self.message {
self.writer.write_all(message.as_bytes())?;
}
self.writer.write_all(b"\n")?;
if self.hint_displayed {
self.writer
.write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?;
self.writer
.write_all(self.app_state.current_exercise().hint.as_bytes())?;
self.writer.write_all(b"\n\n")?;
if self.show_hint {
self.writer.write_fmt(format_args!(
"{}\n{}\n\n",
"Hint".bold().cyan().underlined(),
self.app_state.current_exercise().hint,
))?;
}
if self.show_done {
self.writer.write_fmt(format_args!(
"{}\n\n",
"Exercise done ✓
When you are done experimenting, enter `n` or `next` to go to the next exercise 🦀"
.bold()
.green(),
))?;
}
let line_width = size()?.0;
@ -104,11 +134,8 @@ impl<'a> WatchState<'a> {
self.app_state.exercises().len() as u16,
line_width,
)?;
self.writer.write_all(progress_bar.as_bytes())?;
self.writer.write_all(b"Current exercise: ")?;
self.writer.write_fmt(format_args!(
"{}",
"{progress_bar}Current exercise: {}\n",
self.app_state
.current_exercise()
.path
@ -122,7 +149,7 @@ impl<'a> WatchState<'a> {
}
pub fn show_hint(&mut self) -> Result<()> {
self.hint_displayed = true;
self.show_hint = true;
self.render()
}
@ -133,6 +160,7 @@ impl<'a> WatchState<'a> {
self.writer
.write_all(b" (confusing input can occur after resizing the terminal)")?;
}
self.writer.write_all(b"\n")?;
self.show_prompt()
}
}

View file

@ -4,6 +4,7 @@ use std::sync::mpsc::Sender;
use super::WatchEvent;
pub enum InputEvent {
Next,
Hint,
List,
Quit,
@ -38,6 +39,7 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>) {
match key.code {
KeyCode::Enter => {
let input_event = match input.trim() {
"n" | "next" => InputEvent::Next,
"h" | "hint" => InputEvent::Hint,
"l" | "list" => break InputEvent::List,
"q" | "quit" => break InputEvent::Quit,