mirror of
https://github.com/rust-lang/rustlings.git
synced 2025-01-09 20:03:24 +03:00
Compare commits
No commits in common. "24539666afb0e8c80fbccbca7ad212ba8fbd1189" and "6494a8c50be2e3b8fbd9bb0ae50d8dfbf0569e2a" have entirely different histories.
24539666af
...
6494a8c50b
8
.devcontainer/devcontainer.json
Normal file
8
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/rust:1",
|
||||||
|
"updateContentCommand": ["cargo", "build"],
|
||||||
|
"postAttachCommand": ["rustlings", "watch"],
|
||||||
|
"remoteEnv": {
|
||||||
|
"PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/target/debug"
|
||||||
|
}
|
||||||
|
}
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -19,5 +19,9 @@ public/
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
|
# VS Code extension recommendations
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# Ignore file for editors like Helix
|
# Ignore file for editors like Helix
|
||||||
.ignore
|
.ignore
|
||||||
|
|
7
.gitpod.yml
Normal file
7
.gitpod.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
tasks:
|
||||||
|
- init: /workspace/rustlings/install.sh
|
||||||
|
command: /workspace/.cargo/bin/rustlings watch
|
||||||
|
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- rust-lang.rust-analyzer@0.3.1348
|
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
|
}
|
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -25,9 +25,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allocator-api2"
|
name = "allocator-api2"
|
||||||
version = "0.2.18"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
|
@ -1130,9 +1130,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.6"
|
version = "0.6.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
|
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
77
README.md
77
README.md
|
@ -18,7 +18,78 @@ _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.
|
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.
|
||||||
|
|
||||||
<!-- TODO: Installation with Cargo -->
|
## 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.
|
||||||
|
|
||||||
## Doing exercises
|
## Doing exercises
|
||||||
|
|
||||||
|
@ -67,6 +138,10 @@ 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`.
|
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
|
## 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.
|
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.
|
||||||
|
|
78
flake.lock
Normal file
78
flake.lock
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
78
flake.nix
Normal file
78
flake.nix
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
33
info.toml
33
info.toml
|
@ -1,36 +1,3 @@
|
||||||
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
|
# INTRO
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
|
|
6
shell.nix
Normal file
6
shell.nix
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
(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
|
|
@ -1,16 +1,8 @@
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use crossterm::{
|
|
||||||
style::Stylize,
|
|
||||||
terminal::{Clear, ClearType},
|
|
||||||
ExecutableCommand,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::fs;
|
||||||
fs,
|
|
||||||
io::{StdoutLock, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{exercise::Exercise, FENISH_LINE};
|
use crate::exercise::Exercise;
|
||||||
|
|
||||||
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
|
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
|
||||||
|
|
||||||
|
@ -51,29 +43,24 @@ impl StateFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
state_file: StateFile,
|
||||||
|
exercises: &'static [Exercise],
|
||||||
|
n_done: u16,
|
||||||
|
current_exercise: &'static Exercise,
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub enum ExercisesProgress {
|
pub enum ExercisesProgress {
|
||||||
AllDone,
|
AllDone,
|
||||||
Pending,
|
Pending,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
|
||||||
state_file: StateFile,
|
|
||||||
exercises: &'static [Exercise],
|
|
||||||
n_done: u16,
|
|
||||||
current_exercise: &'static Exercise,
|
|
||||||
final_message: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(mut exercises: Vec<Exercise>, mut final_message: String) -> Self {
|
pub fn new(exercises: Vec<Exercise>) -> Self {
|
||||||
// Leaking especially for sending the exercises to the debounce event handler.
|
// Leaking for sending the exercises to the debounce event handler.
|
||||||
// Leaking is not a problem because the `AppState` instance lives until
|
// Leaking is not a problem since the exercises' slice is used until the end of the program.
|
||||||
// the end of the program.
|
|
||||||
exercises.shrink_to_fit();
|
|
||||||
let exercises = exercises.leak();
|
let exercises = exercises.leak();
|
||||||
final_message.shrink_to_fit();
|
|
||||||
let final_message = final_message.leak();
|
|
||||||
|
|
||||||
let state_file = StateFile::read_or_default(exercises);
|
let state_file = StateFile::read_or_default(exercises);
|
||||||
let n_done = state_file
|
let n_done = state_file
|
||||||
|
@ -87,7 +74,6 @@ impl AppState {
|
||||||
exercises,
|
exercises,
|
||||||
n_done,
|
n_done,
|
||||||
current_exercise,
|
current_exercise,
|
||||||
final_message,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +143,7 @@ impl AppState {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_pending_exercise_ind(&self) -> Option<usize> {
|
fn next_exercise_ind(&self) -> Option<usize> {
|
||||||
let current_ind = self.state_file.current_exercise_ind;
|
let current_ind = self.state_file.current_exercise_ind;
|
||||||
|
|
||||||
if current_ind == self.state_file.progress.len() - 1 {
|
if current_ind == self.state_file.progress.len() - 1 {
|
||||||
|
@ -181,44 +167,14 @@ impl AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
|
pub fn done_current_exercise(&mut self) -> Result<ExercisesProgress> {
|
||||||
let done = &mut self.state_file.progress[self.state_file.current_exercise_ind];
|
let done = &mut self.state_file.progress[self.state_file.current_exercise_ind];
|
||||||
if !*done {
|
if !*done {
|
||||||
*done = true;
|
*done = true;
|
||||||
self.n_done += 1;
|
self.n_done += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ind) = self.next_pending_exercise_ind() else {
|
let Some(ind) = self.next_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);
|
return Ok(ExercisesProgress::AllDone);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -227,9 +183,3 @@ impl AppState {
|
||||||
Ok(ExercisesProgress::Pending)
|
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.
|
|
||||||
|
|
||||||
";
|
|
||||||
|
|
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";
|
|
@ -24,9 +24,6 @@ pub enum Mode {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct InfoFile {
|
pub struct InfoFile {
|
||||||
// TODO
|
|
||||||
pub welcome_message: Option<String>,
|
|
||||||
pub final_message: Option<String>,
|
|
||||||
pub exercises: Vec<Exercise>,
|
pub exercises: Vec<Exercise>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +39,10 @@ impl InfoFile {
|
||||||
.context("Failed to parse `info.toml`")?;
|
.context("Failed to parse `info.toml`")?;
|
||||||
|
|
||||||
if slf.exercises.is_empty() {
|
if slf.exercises.is_empty() {
|
||||||
panic!("{NO_EXERCISES_ERR}");
|
panic!(
|
||||||
|
"There are no exercises yet!
|
||||||
|
If you are developing third-party exercises, add at least one exercise before testing."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(slf)
|
Ok(slf)
|
||||||
|
@ -116,9 +116,6 @@ impl Exercise {
|
||||||
|
|
||||||
impl Display for Exercise {
|
impl Display for Exercise {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
Display::fmt(&self.path.display(), f)
|
self.path.fmt(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.";
|
|
||||||
|
|
41
src/init.rs
41
src/init.rs
|
@ -36,33 +36,47 @@ publish = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_gitignore() -> io::Result<()> {
|
fn create_gitignore() -> io::Result<()> {
|
||||||
|
let gitignore = b"/target
|
||||||
|
/.rustlings-state.json";
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(".gitignore")?
|
.open(".gitignore")?
|
||||||
.write_all(GITIGNORE)
|
.write_all(gitignore)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_vscode_dir() -> Result<()> {
|
fn create_vscode_dir() -> Result<()> {
|
||||||
create_dir(".vscode").context("Failed to create the directory `.vscode`")?;
|
create_dir(".vscode").context("Failed to create the directory `.vscode`")?;
|
||||||
|
let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(".vscode/extensions.json")?
|
.open(".vscode/extensions.json")?
|
||||||
.write_all(VS_CODE_EXTENSIONS_JSON)?;
|
.write_all(vs_code_extensions_json)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(exercises: &[Exercise]) -> Result<()> {
|
pub fn init(exercises: &[Exercise]) -> Result<()> {
|
||||||
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
|
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
|
||||||
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
|
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."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rustlings_path = Path::new("rustlings");
|
let rustlings_path = Path::new("rustlings");
|
||||||
if let Err(e) = create_dir(rustlings_path) {
|
if let Err(e) = create_dir(rustlings_path) {
|
||||||
if e.kind() == ErrorKind::AlreadyExists {
|
if e.kind() == ErrorKind::AlreadyExists {
|
||||||
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
|
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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
@ -82,22 +96,3 @@ pub fn init(exercises: &[Exercise]) -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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";
|
|
||||||
|
|
16
src/list.rs
16
src/list.rs
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
ExecutableCommand,
|
ExecutableCommand,
|
||||||
};
|
};
|
||||||
|
@ -28,10 +28,16 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
|
||||||
|
|
||||||
let key = loop {
|
let key = loop {
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
Event::Key(key) => match key.kind {
|
Event::Key(key) => {
|
||||||
KeyEventKind::Press | KeyEventKind::Repeat => break key,
|
if key.modifiers != KeyModifiers::NONE {
|
||||||
KeyEventKind::Release => (),
|
continue;
|
||||||
},
|
}
|
||||||
|
|
||||||
|
match key.kind {
|
||||||
|
KeyEventKind::Press | KeyEventKind::Repeat => break key,
|
||||||
|
KeyEventKind::Release => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
// Redraw
|
// Redraw
|
||||||
Event::Resize(_, _) => continue 'outer,
|
Event::Resize(_, _) => continue 'outer,
|
||||||
// Ignore
|
// Ignore
|
||||||
|
|
69
src/main.rs
69
src/main.rs
|
@ -3,6 +3,7 @@ use clap::{Parser, Subcommand};
|
||||||
use std::{path::Path, process::exit};
|
use std::{path::Path, process::exit};
|
||||||
|
|
||||||
mod app_state;
|
mod app_state;
|
||||||
|
mod consts;
|
||||||
mod embedded;
|
mod embedded;
|
||||||
mod exercise;
|
mod exercise;
|
||||||
mod init;
|
mod init;
|
||||||
|
@ -13,6 +14,7 @@ mod watch;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
app_state::AppState,
|
app_state::AppState,
|
||||||
|
consts::WELCOME,
|
||||||
exercise::InfoFile,
|
exercise::InfoFile,
|
||||||
init::init,
|
init::init,
|
||||||
list::list,
|
list::list,
|
||||||
|
@ -52,7 +54,11 @@ enum Subcommands {
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
which::which("cargo").context(CARGO_NOT_FOUND_ERR)?;
|
which::which("cargo").context(
|
||||||
|
"Failed to find `cargo`.
|
||||||
|
Did you already install Rust?
|
||||||
|
Try running `cargo --version` to diagnose the problem.",
|
||||||
|
)?;
|
||||||
|
|
||||||
let mut info_file = InfoFile::parse()?;
|
let mut info_file = InfoFile::parse()?;
|
||||||
info_file.exercises.shrink_to_fit();
|
info_file.exercises.shrink_to_fit();
|
||||||
|
@ -60,15 +66,24 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
if matches!(args.command, Some(Subcommands::Init)) {
|
if matches!(args.command, Some(Subcommands::Init)) {
|
||||||
init(&exercises).context("Initialization failed")?;
|
init(&exercises).context("Initialization failed")?;
|
||||||
|
println!(
|
||||||
println!("{POST_INIT_MSG}");
|
"\nDone initialization!\n
|
||||||
|
Run `cd rustlings` to go into the generated directory.
|
||||||
|
Then run `rustlings` for further instructions on getting started."
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if !Path::new("exercises").is_dir() {
|
} else if !Path::new("exercises").is_dir() {
|
||||||
println!("{PRE_INIT_MSG}");
|
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."
|
||||||
|
);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default());
|
let mut app_state = AppState::new(exercises);
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
None => loop {
|
None => loop {
|
||||||
|
@ -103,47 +118,3 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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
|
|
||||||
|
|
||||||
";
|
|
||||||
|
|
28
src/run.rs
28
src/run.rs
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use std::io::{self, Write};
|
use std::io::{stdout, Write};
|
||||||
|
|
||||||
use crate::app_state::{AppState, ExercisesProgress};
|
use crate::app_state::{AppState, ExercisesProgress};
|
||||||
|
|
||||||
|
@ -8,26 +8,28 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||||
let exercise = app_state.current_exercise();
|
let exercise = app_state.current_exercise();
|
||||||
let output = exercise.run()?;
|
let output = exercise.run()?;
|
||||||
|
|
||||||
let mut stdout = io::stdout().lock();
|
{
|
||||||
stdout.write_all(&output.stdout)?;
|
let mut stdout = stdout().lock();
|
||||||
stdout.write_all(b"\n")?;
|
stdout.write_all(&output.stdout)?;
|
||||||
stdout.write_all(&output.stderr)?;
|
stdout.write_all(&output.stderr)?;
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
app_state.set_pending(app_state.current_exercise_ind())?;
|
|
||||||
|
|
||||||
bail!("Ran {exercise} with errors");
|
bail!("Ran {exercise} with errors");
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout.write_fmt(format_args!(
|
println!(
|
||||||
"{}{}\n",
|
"{}{}",
|
||||||
"✓ Successfully ran ".green(),
|
"✓ Successfully ran ".green(),
|
||||||
exercise.path.to_string_lossy().green(),
|
exercise.path.to_string_lossy().green(),
|
||||||
))?;
|
);
|
||||||
|
|
||||||
match app_state.done_current_exercise(&mut stdout)? {
|
match app_state.done_current_exercise()? {
|
||||||
ExercisesProgress::AllDone => (),
|
ExercisesProgress::AllDone => println!(
|
||||||
|
"🎉 Congratulations! You have done all the exercises!
|
||||||
|
🔚 There are no more exercises to do next!"
|
||||||
|
),
|
||||||
ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()),
|
ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
src/watch.rs
30
src/watch.rs
|
@ -15,7 +15,7 @@ mod debounce_event;
|
||||||
mod state;
|
mod state;
|
||||||
mod terminal_event;
|
mod terminal_event;
|
||||||
|
|
||||||
use crate::app_state::{AppState, ExercisesProgress};
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
debounce_event::DebounceEventHandler,
|
debounce_event::DebounceEventHandler,
|
||||||
|
@ -26,13 +26,12 @@ use self::{
|
||||||
enum WatchEvent {
|
enum WatchEvent {
|
||||||
Input(InputEvent),
|
Input(InputEvent),
|
||||||
FileChange { exercise_ind: usize },
|
FileChange { exercise_ind: usize },
|
||||||
TerminalResize,
|
|
||||||
NotifyErr(notify::Error),
|
NotifyErr(notify::Error),
|
||||||
TerminalEventErr(io::Error),
|
TerminalEventErr(io::Error),
|
||||||
|
TerminalResize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by the watch mode to indicate what to do afterwards.
|
/// Returned by the watch mode to indicate what to do afterwards.
|
||||||
#[must_use]
|
|
||||||
pub enum WatchExit {
|
pub enum WatchExit {
|
||||||
/// Exit the program.
|
/// Exit the program.
|
||||||
Shutdown,
|
Shutdown,
|
||||||
|
@ -55,33 +54,30 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
|
||||||
|
|
||||||
let mut watch_state = WatchState::new(app_state);
|
let mut watch_state = WatchState::new(app_state);
|
||||||
|
|
||||||
|
// TODO: bool
|
||||||
watch_state.run_current_exercise()?;
|
watch_state.run_current_exercise()?;
|
||||||
|
watch_state.render()?;
|
||||||
|
|
||||||
thread::spawn(move || terminal_event_handler(tx));
|
thread::spawn(move || terminal_event_handler(tx));
|
||||||
|
|
||||||
while let Ok(event) = rx.recv() {
|
while let Ok(event) = rx.recv() {
|
||||||
match event {
|
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) => {
|
WatchEvent::Input(InputEvent::Hint) => {
|
||||||
watch_state.show_hint()?;
|
watch_state.show_hint()?;
|
||||||
}
|
}
|
||||||
WatchEvent::Input(InputEvent::List) => {
|
WatchEvent::Input(InputEvent::List) => {
|
||||||
return Ok(WatchExit::List);
|
return Ok(WatchExit::List);
|
||||||
}
|
}
|
||||||
WatchEvent::Input(InputEvent::Quit) => {
|
WatchEvent::TerminalResize => {
|
||||||
watch_state.into_writer().write_all(QUIT_MSG)?;
|
watch_state.render()?;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
WatchEvent::Input(InputEvent::Quit) => break,
|
||||||
WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
|
WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
|
||||||
watch_state.handle_invalid_cmd(&cmd)?;
|
watch_state.handle_invalid_cmd(&cmd)?;
|
||||||
}
|
}
|
||||||
WatchEvent::FileChange { exercise_ind } => {
|
WatchEvent::FileChange { exercise_ind } => {
|
||||||
|
// TODO: bool
|
||||||
watch_state.run_exercise_with_ind(exercise_ind)?;
|
watch_state.run_exercise_with_ind(exercise_ind)?;
|
||||||
}
|
|
||||||
WatchEvent::TerminalResize => {
|
|
||||||
watch_state.render()?;
|
watch_state.render()?;
|
||||||
}
|
}
|
||||||
WatchEvent::NotifyErr(e) => {
|
WatchEvent::NotifyErr(e) => {
|
||||||
|
@ -93,10 +89,10 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(WatchExit::Shutdown)
|
watch_state.into_writer().write_all(b"
|
||||||
}
|
|
||||||
|
|
||||||
const QUIT_MSG: &[u8] = b"
|
|
||||||
We hope you're enjoying learning Rust!
|
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.
|
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
|
||||||
";
|
")?;
|
||||||
|
|
||||||
|
Ok(WatchExit::Shutdown)
|
||||||
|
}
|
||||||
|
|
|
@ -6,18 +6,15 @@ use crossterm::{
|
||||||
};
|
};
|
||||||
use std::io::{self, StdoutLock, Write};
|
use std::io::{self, StdoutLock, Write};
|
||||||
|
|
||||||
use crate::{
|
use crate::{app_state::AppState, progress_bar::progress_bar};
|
||||||
app_state::{AppState, ExercisesProgress},
|
|
||||||
progress_bar::progress_bar,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct WatchState<'a> {
|
pub struct WatchState<'a> {
|
||||||
writer: StdoutLock<'a>,
|
writer: StdoutLock<'a>,
|
||||||
app_state: &'a mut AppState,
|
app_state: &'a mut AppState,
|
||||||
stdout: Option<Vec<u8>>,
|
stdout: Option<Vec<u8>>,
|
||||||
stderr: Option<Vec<u8>>,
|
stderr: Option<Vec<u8>>,
|
||||||
show_hint: bool,
|
message: Option<String>,
|
||||||
show_done: bool,
|
hint_displayed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> WatchState<'a> {
|
impl<'a> WatchState<'a> {
|
||||||
|
@ -29,8 +26,8 @@ impl<'a> WatchState<'a> {
|
||||||
app_state,
|
app_state,
|
||||||
stdout: None,
|
stdout: None,
|
||||||
stderr: None,
|
stderr: None,
|
||||||
show_hint: false,
|
message: None,
|
||||||
show_done: false,
|
hint_displayed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,50 +36,29 @@ impl<'a> WatchState<'a> {
|
||||||
self.writer
|
self.writer
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_current_exercise(&mut self) -> Result<()> {
|
pub fn run_current_exercise(&mut self) -> Result<bool> {
|
||||||
self.show_hint = false;
|
|
||||||
|
|
||||||
let output = self.app_state.current_exercise().run()?;
|
let output = self.app_state.current_exercise().run()?;
|
||||||
self.stdout = Some(output.stdout);
|
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);
|
self.stderr = Some(output.stderr);
|
||||||
self.show_done = false;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.render()
|
self.stderr = None;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> {
|
pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<bool> {
|
||||||
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||||
self.run_current_exercise()
|
self.run_current_exercise()
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.app_state.done_current_exercise(&mut self.writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_prompt(&mut self) -> io::Result<()> {
|
fn show_prompt(&mut self) -> io::Result<()> {
|
||||||
self.writer.write_all(b"\n")?;
|
self.writer.write_all(b"\n\n")?;
|
||||||
|
|
||||||
if self.show_done {
|
if !self.hint_displayed {
|
||||||
self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.show_hint {
|
|
||||||
self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?;
|
self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +69,7 @@ impl<'a> WatchState<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self) -> Result<()> {
|
pub fn render(&mut self) -> Result<()> {
|
||||||
// Prevent having the first line shifted.
|
// Prevent having the first line shifted after clearing because of the prompt.
|
||||||
self.writer.write_all(b"\n")?;
|
self.writer.write_all(b"\n")?;
|
||||||
|
|
||||||
self.writer.execute(Clear(ClearType::All))?;
|
self.writer.execute(Clear(ClearType::All))?;
|
||||||
|
@ -108,24 +84,18 @@ impl<'a> WatchState<'a> {
|
||||||
self.writer.write_all(b"\n")?;
|
self.writer.write_all(b"\n")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.writer.write_all(b"\n")?;
|
if let Some(message) = &self.message {
|
||||||
|
self.writer.write_all(message.as_bytes())?;
|
||||||
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_all(b"\n")?;
|
||||||
self.writer.write_fmt(format_args!(
|
|
||||||
"{}\n\n",
|
if self.hint_displayed {
|
||||||
"Exercise done ✓
|
self.writer
|
||||||
When you are done experimenting, enter `n` or `next` to go to the next exercise 🦀"
|
.write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?;
|
||||||
.bold()
|
self.writer
|
||||||
.green(),
|
.write_all(self.app_state.current_exercise().hint.as_bytes())?;
|
||||||
))?;
|
self.writer.write_all(b"\n\n")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_width = size()?.0;
|
let line_width = size()?.0;
|
||||||
|
@ -134,8 +104,11 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise
|
||||||
self.app_state.exercises().len() as u16,
|
self.app_state.exercises().len() as u16,
|
||||||
line_width,
|
line_width,
|
||||||
)?;
|
)?;
|
||||||
|
self.writer.write_all(progress_bar.as_bytes())?;
|
||||||
|
|
||||||
|
self.writer.write_all(b"Current exercise: ")?;
|
||||||
self.writer.write_fmt(format_args!(
|
self.writer.write_fmt(format_args!(
|
||||||
"{progress_bar}Current exercise: {}\n",
|
"{}",
|
||||||
self.app_state
|
self.app_state
|
||||||
.current_exercise()
|
.current_exercise()
|
||||||
.path
|
.path
|
||||||
|
@ -149,7 +122,7 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_hint(&mut self) -> Result<()> {
|
pub fn show_hint(&mut self) -> Result<()> {
|
||||||
self.show_hint = true;
|
self.hint_displayed = true;
|
||||||
self.render()
|
self.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +133,6 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise
|
||||||
self.writer
|
self.writer
|
||||||
.write_all(b" (confusing input can occur after resizing the terminal)")?;
|
.write_all(b" (confusing input can occur after resizing the terminal)")?;
|
||||||
}
|
}
|
||||||
self.writer.write_all(b"\n")?;
|
|
||||||
self.show_prompt()
|
self.show_prompt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use std::sync::mpsc::Sender;
|
||||||
use super::WatchEvent;
|
use super::WatchEvent;
|
||||||
|
|
||||||
pub enum InputEvent {
|
pub enum InputEvent {
|
||||||
Next,
|
|
||||||
Hint,
|
Hint,
|
||||||
List,
|
List,
|
||||||
Quit,
|
Quit,
|
||||||
|
@ -39,7 +38,6 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>) {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let input_event = match input.trim() {
|
let input_event = match input.trim() {
|
||||||
"n" | "next" => InputEvent::Next,
|
|
||||||
"h" | "hint" => InputEvent::Hint,
|
"h" | "hint" => InputEvent::Hint,
|
||||||
"l" | "list" => break InputEvent::List,
|
"l" | "list" => break InputEvent::List,
|
||||||
"q" | "quit" => break InputEvent::Quit,
|
"q" | "quit" => break InputEvent::Quit,
|
||||||
|
|
Loading…
Reference in a new issue