mirror of
https://github.com/rust-lang/rustlings.git
synced 2025-06-07 06:04:47 +00:00
Compare commits
208 Commits
rustlings-
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cb60c8887c | ||
![]() |
46814d397a | ||
![]() |
734fc482eb | ||
![]() |
520dfdc464 | ||
![]() |
2267f99684 | ||
![]() |
bf74a3d0a7 | ||
![]() |
adf3ddd968 | ||
![]() |
f80c2edc3d | ||
![]() |
04520ae7ad | ||
![]() |
e36dd7a120 | ||
![]() |
edc8528dde | ||
![]() |
47e490a997 | ||
![]() |
596e7f36cc | ||
![]() |
512ded81c4 | ||
![]() |
69a9e9cafc | ||
![]() |
54a74fd638 | ||
![]() |
a51d6f1309 | ||
![]() |
f6a657a0c3 | ||
![]() |
8c24763259 | ||
![]() |
dc468882cc | ||
![]() |
5fc787f4e4 | ||
![]() |
8fa598ae7e | ||
![]() |
2f700991f3 | ||
![]() |
b4a6b87e24 | ||
![]() |
984e9fea7c | ||
![]() |
8339007633 | ||
![]() |
23b9aa3a15 | ||
![]() |
69fe9626da | ||
![]() |
f387f4c1d9 | ||
![]() |
40fe3aa741 | ||
![]() |
b30973afa1 | ||
![]() |
3d8bef4bc3 | ||
![]() |
2673177b17 | ||
![]() |
6d5369d4d0 | ||
![]() |
b9d1e636a4 | ||
![]() |
7e26418952 | ||
![]() |
61c17cb349 | ||
![]() |
fda18e8895 | ||
![]() |
7ec6986965 | ||
![]() |
74ab9924b4 | ||
![]() |
a28000acc4 | ||
![]() |
08548abcc2 | ||
![]() |
5927a781a3 | ||
![]() |
e73fff3bd4 | ||
![]() |
8dff0df266 | ||
![]() |
5ee7dfb5c2 | ||
![]() |
9a3586878d | ||
![]() |
a99433c62d | ||
![]() |
e76ca5e2b9 | ||
![]() |
48bab77609 | ||
![]() |
a063bcfb4c | ||
![]() |
c5f49cfa48 | ||
![]() |
9bcd4198c5 | ||
![]() |
29dc8ea9fa | ||
![]() |
fa91814aa9 | ||
![]() |
0b91db2195 | ||
![]() |
7b2d42b0f0 | ||
![]() |
bd3bdd620b | ||
![]() |
8b4562e102 | ||
![]() |
63d8986f2a | ||
![]() |
ecaecc2f76 | ||
![]() |
78194b4441 | ||
![]() |
44699e9b1b | ||
![]() |
9978c17d5f | ||
![]() |
3cc7e0377c | ||
![]() |
d2abc359cc | ||
![]() |
7c0d269279 | ||
![]() |
8db85946af | ||
![]() |
7019f4d178 | ||
![]() |
fcd77a83cc | ||
![]() |
ae444eb3da | ||
![]() |
425c9821e0 | ||
![]() |
46c6fb2c82 | ||
![]() |
374c3874af | ||
![]() |
1eb6c1e469 | ||
![]() |
06af3ffc99 | ||
![]() |
65dc019fa6 | ||
![]() |
a56ccb6f4f | ||
![]() |
d9872f2615 | ||
![]() |
298be671b9 | ||
![]() |
fbfd4f25e7 | ||
![]() |
d12735a573 | ||
![]() |
1aec7c1152 | ||
![]() |
0b55809bb9 | ||
![]() |
bde6f7470c | ||
![]() |
53ec59ed95 | ||
![]() |
ed1ee38923 | ||
![]() |
26cf4989a2 | ||
![]() |
6e60f441e9 | ||
![]() |
d07de879a7 | ||
![]() |
dd0634c483 | ||
![]() |
fc0cd8f0f8 | ||
![]() |
d5cae8ff59 | ||
![]() |
38016cb2d6 | ||
![]() |
e6cb104294 | ||
![]() |
410eb69d25 | ||
![]() |
243cf5f261 | ||
![]() |
eff2ce8a23 | ||
![]() |
fd33c29b26 | ||
![]() |
f49164e69b | ||
![]() |
9bc7bbe4b4 | ||
![]() |
46ad25f925 | ||
![]() |
2a725fb137 | ||
![]() |
449858655d | ||
![]() |
e8c2a79516 | ||
![]() |
ea85c1b46e | ||
![]() |
6bec6f92c4 | ||
![]() |
930a0ea73b | ||
![]() |
7e2f56f41a | ||
![]() |
e90f5f03f3 | ||
![]() |
0e090ae112 | ||
![]() |
99496706c5 | ||
![]() |
f146553dea | ||
![]() |
0432e07864 | ||
![]() |
f33ba139b4 | ||
![]() |
990a722852 | ||
![]() |
a675cb5754 | ||
![]() |
baeeff389c | ||
![]() |
932bc25d88 | ||
![]() |
bdc6dad8de | ||
![]() |
ea73af9ba3 | ||
![]() |
fc5fc0920f | ||
![]() |
9705c161b4 | ||
![]() |
8cac21511c | ||
![]() |
396ee4d618 | ||
![]() |
326169a7fa | ||
![]() |
685e069c58 | ||
![]() |
84a42a2b24 | ||
![]() |
ac6e1b7ce5 | ||
![]() |
f516da4138 | ||
![]() |
e852e60416 | ||
![]() |
bf7d171915 | ||
![]() |
d3f819f86f | ||
![]() |
aa83fd6bc4 | ||
![]() |
e2f7734f37 | ||
![]() |
5c17abd1bf | ||
![]() |
c52867eb8b | ||
![]() |
26fd97a209 | ||
![]() |
f0a2cdeb18 | ||
![]() |
0c79f2ea3e | ||
![]() |
0e9eb9e87e | ||
![]() |
0d258b9e96 | ||
![]() |
d4fa61e435 | ||
![]() |
554301b8e9 | ||
![]() |
e3ec0abca4 | ||
![]() |
a55e848359 | ||
![]() |
2653c3c4d4 | ||
![]() |
4e4b65711a | ||
![]() |
89c40ba256 | ||
![]() |
e56ae6d651 | ||
![]() |
64b2f18d92 | ||
![]() |
2894f3c45c | ||
![]() |
1bae2dcb00 | ||
![]() |
b540c6df25 | ||
![]() |
8b476e678a | ||
![]() |
47f8a0cbe5 | ||
![]() |
9459eef032 | ||
![]() |
5aaa8924a6 | ||
![]() |
4ffce1c297 | ||
![]() |
0513660b05 | ||
![]() |
3947c4de28 | ||
![]() |
664228ef8b | ||
![]() |
234a61a3ee | ||
![]() |
83d1275d72 | ||
![]() |
45abd7d59e | ||
![]() |
88e10a9e54 | ||
![]() |
1f624d4c2a | ||
![]() |
9a25309c1c | ||
![]() |
2b7caf6fcb | ||
![]() |
938500fd2f | ||
![]() |
2d26358602 | ||
![]() |
9faa5d3aa4 | ||
![]() |
bcc2a136c8 | ||
![]() |
dcad002057 | ||
![]() |
51b8d2ab25 | ||
![]() |
aa3eda70e5 | ||
![]() |
2d0860fe1b | ||
![]() |
17877366b7 | ||
![]() |
5eb3dee59c | ||
![]() |
247bd19f93 | ||
![]() |
e5ed115288 | ||
![]() |
03baa471d9 | ||
![]() |
da8b3d143a | ||
![]() |
20616ff954 | ||
![]() |
f463cf8662 | ||
![]() |
e9879eac91 | ||
![]() |
47148e78a3 | ||
![]() |
fea917c8f2 | ||
![]() |
948e16e3c7 | ||
![]() |
1e7fc46406 | ||
![]() |
71494264ca | ||
![]() |
3125561474 | ||
![]() |
abf1228a0a | ||
![]() |
547a9d947b | ||
![]() |
f696d98270 | ||
![]() |
44ab7f995d | ||
![]() |
92a1214dcd | ||
![]() |
388f8da97f | ||
![]() |
e96623588c | ||
![]() |
e1e316b931 | ||
![]() |
c4fd29541b | ||
![]() |
a8b13f5a82 | ||
![]() |
86fc573d7a | ||
![]() |
f82e47f2af | ||
![]() |
75a38fa38b | ||
![]() |
ac62a3713c | ||
![]() |
ea52c99560 | ||
![]() |
7d4100ed8a |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
dev = ["run", "--", "dev"]
|
@ -1,7 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*.rs]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
24
.github/workflows/rust.yml
vendored
24
.github/workflows/rust.yml
vendored
@ -1,10 +1,16 @@
|
||||
name: Rustlings Tests
|
||||
name: Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- website
|
||||
- '*.md'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- website
|
||||
- '*.md'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@ -14,30 +20,28 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cargo clippy -- --deny warnings
|
||||
- name: Clippy
|
||||
run: cargo clippy -- --deny warnings
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
with:
|
||||
globs: "exercises/**/*.md"
|
||||
- name: Run cargo fmt
|
||||
- name: rustfmt
|
||||
run: cargo fmt --all --check
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: swatinem/rust-cache@v2
|
||||
- name: Run cargo test
|
||||
- name: cargo test
|
||||
run: cargo test --workspace
|
||||
dev-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: swatinem/rust-cache@v2
|
||||
- name: Run rustlings dev check
|
||||
run: cargo run -- dev check --require-solutions
|
||||
- name: rustlings dev check
|
||||
run: cargo dev check --require-solutions
|
||||
|
87
.github/workflows/web.yml
vendored
87
.github/workflows/web.yml
vendored
@ -1,87 +0,0 @@
|
||||
# Workflow to build your docs with oranda (and mdbook)
|
||||
# and deploy them to Github Pages
|
||||
name: Web
|
||||
|
||||
# We're going to push to the gh-pages branch, so we need that permission
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
# What situations do we want to build docs in?
|
||||
# All of these work independently and can be removed / commented out
|
||||
# if you don't want oranda/mdbook running in that situation
|
||||
on:
|
||||
# Check that a PR didn't break docs!
|
||||
#
|
||||
# Note that the "Deploy to Github Pages" step won't run in this mode,
|
||||
# so this won't have any side-effects. But it will tell you if a PR
|
||||
# completely broke oranda/mdbook. Sadly we don't provide previews (yet)!
|
||||
pull_request:
|
||||
|
||||
# Whenever something gets pushed to main, update the docs!
|
||||
# This is great for getting docs changes live without cutting a full release.
|
||||
#
|
||||
# Note that if you're using cargo-dist, this will "race" the Release workflow
|
||||
# that actually builds the Github Release that oranda tries to read (and
|
||||
# this will almost certainly complete first). As a result you will publish
|
||||
# docs for the latest commit but the oranda landing page won't know about
|
||||
# the latest release. The workflow_run trigger below will properly wait for
|
||||
# cargo-dist, and so this half-published state will only last for ~10 minutes.
|
||||
#
|
||||
# If you only want docs to update with releases, disable this, or change it to
|
||||
# a "release" branch. You can, of course, also manually trigger a workflow run
|
||||
# when you want the docs to update.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Whenever a workflow called "Release" completes, update the docs!
|
||||
#
|
||||
# If you're using cargo-dist, this is recommended, as it will ensure that
|
||||
# oranda always sees the latest release right when it's available. Note
|
||||
# however that Github's UI is wonky when you use workflow_run, and won't
|
||||
# show this workflow as part of any commit. You have to go to the "actions"
|
||||
# tab for your repo to see this one running (the gh-pages deploy will also
|
||||
# only show up there).
|
||||
workflow_run:
|
||||
workflows: [ "Release" ]
|
||||
types:
|
||||
- completed
|
||||
|
||||
# Alright, let's do it!
|
||||
jobs:
|
||||
web:
|
||||
name: Build and deploy site and docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Setup
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: swatinem/rust-cache@v2
|
||||
|
||||
# If you use any mdbook plugins, here's the place to install them!
|
||||
|
||||
# Install and run oranda (and mdbook)
|
||||
# This will write all output to ./public/ (including copying mdbook's output to there)
|
||||
- name: Install and run oranda
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.3.1/oranda-installer.sh | sh
|
||||
oranda build
|
||||
|
||||
# Deploy to our gh-pages branch (creating it if it doesn't exist)
|
||||
# the "public" dir that oranda made above will become the root dir
|
||||
# of this branch.
|
||||
#
|
||||
# Note that once the gh-pages branch exists, you must
|
||||
# go into repo's settings > pages and set "deploy from branch: gh-pages"
|
||||
# the other defaults work fine.
|
||||
- name: Deploy to Github Pages
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||
# ONLY if we're on main (so no PRs or feature branches allowed!)
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
with:
|
||||
branch: gh-pages
|
||||
# Gotta tell the action where to find oranda's output
|
||||
folder: public
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
single-commit: true
|
43
.github/workflows/website.yml
vendored
Normal file
43
.github/workflows/website.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: Website
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: [website]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: website
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install TailwindCSS
|
||||
run: npm install
|
||||
- name: Build CSS
|
||||
run: npx @tailwindcss/cli -m -i input.css -o static/main.css
|
||||
- name: Download Zola
|
||||
run: curl -fsSL https://github.com/getzola/zola/releases/download/v0.20.0/zola-v0.20.0-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
||||
- name: Build site
|
||||
run: ./zola build
|
||||
- name: Upload static files as artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: website/public/
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: actions/deploy-pages@v4
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -6,10 +6,6 @@ Cargo.lock
|
||||
# State file
|
||||
.rustlings-state.txt
|
||||
|
||||
# oranda
|
||||
public/
|
||||
.netlify
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.direnv/
|
||||
|
@ -1,2 +0,0 @@
|
||||
# MD013/line-length Line length, Expected: 80
|
||||
MD013: false
|
@ -1,3 +1,6 @@
|
||||
[default.extend-words]
|
||||
"earch" = "earch" # Because of <s>earch in the list footer
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"CHANGELOG.md",
|
||||
|
134
CHANGELOG.md
134
CHANGELOG.md
@ -1,4 +1,38 @@
|
||||
<a name="6.3.0"></a>
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Rust edition 2024
|
||||
- Raise the minimum supported Rust version to `1.87`
|
||||
|
||||
## 6.4.0 (2024-11-11)
|
||||
|
||||
### Added
|
||||
|
||||
- The list of exercises is now searchable by pressing `s` or `/` 🔍️ (thanks to [@frroossst](https://github.com/frroossst))
|
||||
- New option `c` in the prompt to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
|
||||
- New command `check-all` to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
|
||||
- Addictive animation for showing the progress of checking all exercises. A nice showcase of parallelism in Rust ✨
|
||||
- New option `x` in the prompt to reset the file of the current exercise 🔄
|
||||
- Allow `dead_code` for all exercises and solutions ⚰️ (thanks to [@huss4in](https://github.com/huss4in))
|
||||
- Pause input while running an exercise to avoid unexpected prompt interactions ⏸️
|
||||
- Limit the maximum number of exercises to 999. Any community exercises willing to reach that limit? 🔝
|
||||
|
||||
### Changed
|
||||
|
||||
- `enums3`: Remove redundant enum definition task (thanks to [@senekor](https://github.com/senekor))
|
||||
- `if2`: Make the exercise less confusing by avoiding "fizz", "fuzz", "foo", "bar" and "baz" (thanks to [@senekor](https://github.com/senekor))
|
||||
- `hashmap3`: Use the method `Entry::or_default`.
|
||||
- Update the state of all exercises when checking all of them (thanks to [@Nahor](https://github.com/Nahor))
|
||||
- The main prompt doesn't need a confirmation with ENTER on Unix-like systems anymore.
|
||||
- No more jumping back to a previous exercise when its file is changed. Use the list to jump between exercises.
|
||||
- Dump the solution file after an exercise is done even if the solution's directory doesn't exist.
|
||||
- Rework the footer in the list.
|
||||
- Optimize the file watcher.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix bad contrast in the list on terminals with a light theme.
|
||||
|
||||
## 6.3.0 (2024-08-29)
|
||||
|
||||
@ -39,8 +73,6 @@
|
||||
- Fix the list when the terminal height is too low.
|
||||
- Restore the terminal after an error in the list.
|
||||
|
||||
<a name="6.2.0"></a>
|
||||
|
||||
## 6.2.0 (2024-08-09)
|
||||
|
||||
### Added
|
||||
@ -57,13 +89,11 @@
|
||||
- Run the final check of all exercises in parallel.
|
||||
- Small exercise improvements.
|
||||
|
||||
<a name="6.1.0"></a>
|
||||
|
||||
## 6.1.0 (2024-07-10)
|
||||
|
||||
#### Added
|
||||
|
||||
- `dev check`: Check that all exercises (including third-party ones) include at least one `TODO` comment.
|
||||
- `dev check`: Check that all exercises (including community ones) include at least one `TODO` comment.
|
||||
- `dev check`: Check that all exercises actually fail to run (not already solved).
|
||||
|
||||
#### Changed
|
||||
@ -76,15 +106,11 @@
|
||||
- Exit with a helpful error message on missing/unsupported terminal/TTY.
|
||||
- Mark the last exercise as done.
|
||||
|
||||
<a name="6.0.1"></a>
|
||||
|
||||
## 6.0.1 (2024-07-04)
|
||||
|
||||
Small exercise improvements and fixes.
|
||||
Most importantly, fixed that the exercise `clippy1` was already solved 😅
|
||||
|
||||
<a name="6.0.0"></a>
|
||||
|
||||
## 6.0.0 (2024-07-03)
|
||||
|
||||
This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨
|
||||
@ -113,7 +139,7 @@ You can read about the motivations of this change in [this issue](https://github
|
||||
|
||||
### List mode
|
||||
|
||||
A list mode was added using [Ratatui](https://ratatui.rs).
|
||||
A new list mode was added!
|
||||
You can enter it by entering `l` in the watch mode.
|
||||
It offers the following features:
|
||||
|
||||
@ -142,15 +168,13 @@ This should avoid issues related to the language server or to running exercises,
|
||||
Clippy lints are now shown on all exercises, not only the Clippy exercises 📎
|
||||
Make Clippy your friend from early on 🥰
|
||||
|
||||
### Third-party exercises
|
||||
### Community Exercises
|
||||
|
||||
Rustlings now supports third-party exercises!
|
||||
Rustlings now supports community exercises!
|
||||
|
||||
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
|
||||
Or do you want to translate the original Rustlings exercises?
|
||||
Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)!
|
||||
|
||||
<a name="5.6.1"></a>
|
||||
Then follow the link to the guide about [community exercises](https://rustlings.rust-lang.org/community-exercises)!
|
||||
|
||||
## 5.6.1 (2023-09-18)
|
||||
|
||||
@ -167,8 +191,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- `as_ref_mut`: Fixed a typo in a test function name.
|
||||
- `enums3`: Fixed formatting with `rustfmt`.
|
||||
|
||||
<a name="5.6.0"></a>
|
||||
|
||||
## 5.6.0 (2023-09-04)
|
||||
|
||||
#### Added
|
||||
@ -208,16 +230,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Lots of Nix housekeeping that I don't feel qualified to write about!
|
||||
- Improved CI workflows, we're now testing on multiple platforms at once.
|
||||
|
||||
<a name="5.5.1"></a>
|
||||
|
||||
## 5.5.1 (2023-05-17)
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix.
|
||||
|
||||
<a name="5.5.0"></a>
|
||||
|
||||
## 5.5.0 (2023-05-17)
|
||||
|
||||
#### Added
|
||||
@ -252,8 +270,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Added a markdown linter to run on GitHub actions
|
||||
- Split quick installation section into two code blocks
|
||||
|
||||
<a name="5.4.1"></a>
|
||||
|
||||
## 5.4.1 (2023-03-10)
|
||||
|
||||
#### Changed
|
||||
@ -269,8 +285,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- `macros4`: Prevented auto-fix by adding `#[rustfmt::skip]`
|
||||
- `cli`: Actually show correct progress percentages
|
||||
|
||||
<a name="5.4.0"></a>
|
||||
|
||||
## 5.4.0 (2023-02-12)
|
||||
|
||||
#### Changed
|
||||
@ -299,8 +313,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
|
||||
- Bumped min Rust version to 1.58 in installation script
|
||||
|
||||
<a name="5.3.0"></a>
|
||||
|
||||
## 5.3.0 (2022-12-23)
|
||||
|
||||
#### Added
|
||||
@ -333,8 +345,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Applied some Clippy and rustfmt formatting
|
||||
- Added a note on Windows PowerShell and other shell compatibility
|
||||
|
||||
<a name="5.2.1"></a>
|
||||
|
||||
## 5.2.1 (2022-09-06)
|
||||
|
||||
#### Fixed
|
||||
@ -348,8 +358,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
|
||||
- Fixed a typo in README.md
|
||||
|
||||
<a name="5.2.0"></a>
|
||||
|
||||
## 5.2.0 (2022-08-27)
|
||||
|
||||
#### Added
|
||||
@ -366,16 +374,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- **quiz1**: Adjusted the explanations to be consistent with
|
||||
the tests
|
||||
|
||||
<a name="5.1.1"></a>
|
||||
|
||||
## 5.1.1 (2022-08-17)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Fixed an incorrect assertion in options1
|
||||
|
||||
<a name="5.1.0"></a>
|
||||
|
||||
## 5.1.0 (2022-08-16)
|
||||
|
||||
#### Features
|
||||
@ -410,8 +414,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Clarified manual installation instructions using `cargo install --path .`
|
||||
- Added a link to our Zulip in the readme file
|
||||
|
||||
<a name="5.0.0"></a>
|
||||
|
||||
## 5.0.0 (2022-07-16)
|
||||
|
||||
#### Features
|
||||
@ -484,8 +486,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Updated spacing in Cargo.toml.
|
||||
- Added a GitHub actions config so that tests run on every PR/commit.
|
||||
|
||||
<a name="4.8.0"></a>
|
||||
|
||||
## 4.8.0 (2022-07-01)
|
||||
|
||||
#### Features
|
||||
@ -506,8 +506,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting.
|
||||
- Removed the deprecated Rust GitPod extension.
|
||||
|
||||
<a name="4.7.1"></a>
|
||||
|
||||
## 4.7.1 (2022-04-20)
|
||||
|
||||
#### Features
|
||||
@ -528,8 +526,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- The changelog will now be manually written instead of being automatically generated by the
|
||||
Git log.
|
||||
|
||||
<a name="4.7.0"></a>
|
||||
|
||||
## 4.7.0 (2022-04-14)
|
||||
|
||||
#### Features
|
||||
@ -570,8 +566,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886))
|
||||
- Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929))
|
||||
|
||||
<a name="4.6.0"></a>
|
||||
|
||||
## 4.6.0 (2021-09-25)
|
||||
|
||||
#### Features
|
||||
@ -594,8 +588,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1))
|
||||
- **quiz1:** Fix inconsistent wording (#826) ([03131a3d](https://github.com/rust-lang/rustlings/commit/03131a3d35d9842598150f9da817f7cc26e2669a))
|
||||
|
||||
<a name="4.5.0"></a>
|
||||
|
||||
## 4.5.0 (2021-07-07)
|
||||
|
||||
#### Features
|
||||
@ -616,8 +608,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- **try_from_into, from_str:** hints for dyn Error ([11d2cf0d](https://github.com/rust-lang/rustlings/commit/11d2cf0d604dee3f5023c17802d69438e69fa50e))
|
||||
- **variables5:** confine the answer further ([48ffcbd2](https://github.com/rust-lang/rustlings/commit/48ffcbd2c4cc4d936c2c7480019190f179813cc5))
|
||||
|
||||
<a name="4.4.0"></a>
|
||||
|
||||
## 4.4.0 (2021-04-24)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -659,8 +649,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913))
|
||||
- added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4))
|
||||
|
||||
<a name="4.3.0"></a>
|
||||
|
||||
## 4.3.0 (2020-12-29)
|
||||
|
||||
#### Features
|
||||
@ -683,8 +671,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755))
|
||||
- **vec1:** Have test compare every element in a and v ([9b6c6293](https://github.com/rust-lang/rustlings/commit/9b6c629397b24b944f484f5b2bbd8144266b5695))
|
||||
|
||||
<a name="4.2.0"></a>
|
||||
|
||||
## 4.2.0 (2020-11-07)
|
||||
|
||||
#### Features
|
||||
@ -705,8 +691,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e))
|
||||
- **quiz3:** Second test is for odd numbers, not even. (#553) ([18e0bfef](https://github.com/rust-lang/rustlings/commit/18e0bfef1de53071e353ba1ec5837002ff7290e6))
|
||||
|
||||
<a name="4.1.0"></a>
|
||||
|
||||
## 4.1.0 (2020-10-05)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -729,8 +713,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- **cli:** Added 'cls' command to 'watch' mode (#474) ([4f2468e1](https://github.com/rust-lang/rustlings/commit/4f2468e14f574a93a2e9b688367b5752ed96ae7b))
|
||||
- **try_from_into:** Add insufficient length test (#469) ([523d18b8](https://github.com/rust-lang/rustlings/commit/523d18b873a319f7c09262f44bd40e2fab1830e5))
|
||||
|
||||
<a name="4.0.0"></a>
|
||||
|
||||
## 4.0.0 (2020-07-08)
|
||||
|
||||
#### Breaking Changes
|
||||
@ -772,8 +754,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- **test2:** name of type String and &str (#394) ([d6c0a688](https://github.com/rust-lang/rustlings/commit/d6c0a688e6a96f93ad60d540d4b326f342fc0d45))
|
||||
- **variables6:** minor typo (#419) ([524e17df](https://github.com/rust-lang/rustlings/commit/524e17df10db95f7b90a0f75cc8997182a8a4094))
|
||||
|
||||
<a name="3.0.0"></a>
|
||||
|
||||
## 3.0.0 (2020-04-11)
|
||||
|
||||
#### Breaking Changes
|
||||
@ -796,8 +776,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229))
|
||||
- **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71))
|
||||
|
||||
<a name="2.2.1"></a>
|
||||
|
||||
### 2.2.1 (2020-02-27)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -808,13 +786,11 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
|
||||
- Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921))
|
||||
|
||||
<a name="2.2.0"></a>
|
||||
|
||||
## 2.2.0 (2020-02-25)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Update deps to version compatable with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
|
||||
- Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
|
||||
- **docs:**
|
||||
- Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f))
|
||||
- Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9))
|
||||
@ -837,8 +813,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Added traits exercises (#274 but specifically #216, which originally added
|
||||
this :heart:) ([b559cdd](https://github.com/rust-lang/rustlings/commit/b559cdd73f32c0d0cfc1feda39f82b3e3583df17))
|
||||
|
||||
<a name="2.1.0"></a>
|
||||
|
||||
## 2.1.0 (2019-11-27)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -856,8 +830,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
|
||||
- **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c))
|
||||
|
||||
<a name="2.0.0"></a>
|
||||
|
||||
## 2.0.0 (2019-11-12)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -878,8 +850,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425))
|
||||
- **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f))
|
||||
|
||||
<a name="1.5.1"></a>
|
||||
|
||||
### 1.5.1 (2019-11-11)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -891,8 +861,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- **threads:** Move Threads behind SLT ([fbe91a67](https://github.com/rust-lang/rustlings/commit/fbe91a67a482bfe64cbcdd58d06ba830a0f39da3), closes [#205](https://github.com/rust-lang/rustlings/issues/205))
|
||||
- **watch:** clear screen before each `verify()` ([3aff590](https://github.com/rust-lang/rustlings/commit/3aff59085586c24196a547c2693adbdcf4432648))
|
||||
|
||||
<a name="1.5.0"></a>
|
||||
|
||||
## 1.5.0 (2019-11-09)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -917,8 +885,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959))
|
||||
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
|
||||
|
||||
<a name="1.4.1"></a>
|
||||
|
||||
### 1.4.1 (2019-08-13)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -927,8 +893,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6))
|
||||
- **test1:** Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446))
|
||||
|
||||
<a name="1.4.0"></a>
|
||||
|
||||
## 1.4.0 (2019-07-13)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -945,8 +909,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724))
|
||||
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
|
||||
|
||||
<a name="1.3.0"></a>
|
||||
|
||||
### 1.3.0 (2019-06-05)
|
||||
|
||||
#### Features
|
||||
@ -962,16 +924,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Fix broken link (#164, @HanKruiger)
|
||||
- Remove highlighting and syntect (#167, @komaeda)
|
||||
|
||||
<a name="1.2.2"></a>
|
||||
|
||||
### 1.2.2 (2019-05-07)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Reverted `--nocapture` flag since it was causing tests to pass unconditionally
|
||||
|
||||
<a name="1.2.1"></a>
|
||||
|
||||
### 1.2.1 (2019-04-22)
|
||||
|
||||
#### Bug Fixes
|
||||
@ -979,8 +937,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Fix the `--nocapture` feature (@komaeda)
|
||||
- Provide a nicer error message for when you're in the wrong directory
|
||||
|
||||
<a name="1.2.0"></a>
|
||||
|
||||
### 1.2.0 (2019-04-22)
|
||||
|
||||
#### Features
|
||||
@ -988,8 +944,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Add errors to exercises that compile without user changes (@yvan-sraka)
|
||||
- Use --nocapture when testing, enabling `println!` when running (@komaeda)
|
||||
|
||||
<a name="1.1.1"></a>
|
||||
|
||||
### 1.1.1 (2019-04-14)
|
||||
|
||||
#### Bug fixes
|
||||
@ -1002,8 +956,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Fix links by deleting book version (@diodfr, #142)
|
||||
- Canonicalize paths to fix path matching (@cjpearce, #143)
|
||||
|
||||
<a name="1.1.0"></a>
|
||||
|
||||
### 1.1.0 (2019-03-20)
|
||||
|
||||
- errors2.rs: update link to Rust book (#124)
|
||||
@ -1013,16 +965,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
- Give a warning when Rustlings isn't run from the right directory (#123)
|
||||
- Verify that rust version is recent enough to install Rustlings (#131)
|
||||
|
||||
<a name="1.0.1"></a>
|
||||
|
||||
### 1.0.1 (2019-03-06)
|
||||
|
||||
- Adds a way to install Rustlings in one command (`curl -L https://git.io/rustlings | bash`)
|
||||
- Makes `rustlings watch` react to create file events (@shaunbennett, #117)
|
||||
- Reworks the exercise management to use an external TOML file instead of just listing them in the code
|
||||
|
||||
<a name="1.0.0"></a>
|
||||
|
||||
### 1.0.0 (2019-03-06)
|
||||
|
||||
Initial release.
|
||||
|
483
Cargo.lock
generated
483
Cargo.lock
generated
@ -1,24 +1,12 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@ -31,49 +19,50 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.4"
|
||||
version = "3.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@ -83,9 +72,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@ -95,9 +84,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.16"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
|
||||
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -105,9 +94,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.15"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
|
||||
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -117,9 +106,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.13"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@ -129,40 +118,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.2"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bitflags 2.9.1",
|
||||
"crossterm_winapi",
|
||||
"mio 1.0.2",
|
||||
"document-features",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
@ -180,26 +155,35 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
name = "document-features"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.1"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
@ -210,7 +194,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -223,10 +207,22 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@ -234,17 +230,11 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.4.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
@ -252,11 +242,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags 2.9.1",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
@ -278,15 +268,15 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.0.8"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
@ -304,9 +294,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.158"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
@ -314,22 +304,28 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
@ -337,9 +333,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@ -349,79 +345,58 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.52.0",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"crossbeam-channel",
|
||||
"bitflags 2.9.1",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-debouncer-mini"
|
||||
version = "0.4.1"
|
||||
name = "notify-types"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify",
|
||||
]
|
||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.1"
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
@ -429,67 +404,72 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.10"
|
||||
version = "0.9.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.3"
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.35"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
|
||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bitflags 2.9.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustlings"
|
||||
version = "6.3.0"
|
||||
version = "6.4.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"notify-debouncer-mini",
|
||||
"os_pipe",
|
||||
"notify",
|
||||
"rustix",
|
||||
"rustlings-macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -499,7 +479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustlings-macros"
|
||||
version = "6.3.0"
|
||||
version = "6.4.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"serde",
|
||||
@ -508,9 +488,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
@ -529,18 +509,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.209"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.209"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -549,9 +529,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.127"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@ -561,18 +541,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.7"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
@ -585,24 +565,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 1.0.2",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
version = "1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
@ -612,9 +592,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.76"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -623,31 +603,31 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.12.0"
|
||||
version = "3.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
|
||||
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.20"
|
||||
version = "0.22.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@ -658,9 +638,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
@ -668,12 +648,6 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@ -690,6 +664,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -712,7 +695,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -721,46 +704,13 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -769,46 +719,28 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@ -821,48 +753,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
@ -871,29 +779,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.18"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
32
Cargo.toml
32
Cargo.toml
@ -6,7 +6,7 @@ exclude = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "6.3.0"
|
||||
version = "6.4.0"
|
||||
authors = [
|
||||
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
|
||||
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
|
||||
@ -15,12 +15,12 @@ authors = [
|
||||
]
|
||||
repository = "https://github.com/rust-lang/rustlings"
|
||||
license = "MIT"
|
||||
edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions.
|
||||
rust-version = "1.80"
|
||||
edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and `CARGO_TOML` in `dev new`.
|
||||
rust-version = "1.87"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1.0.209", features = ["derive"] }
|
||||
toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml_edit = { version = "0.22", default-features = false, features = ["parse", "serde"] }
|
||||
|
||||
[package]
|
||||
name = "rustlings"
|
||||
@ -46,19 +46,20 @@ include = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
ahash = { version = "0.8.11", default-features = false }
|
||||
anyhow = "1.0.86"
|
||||
clap = { version = "4.5.16", features = ["derive"] }
|
||||
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
|
||||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
||||
os_pipe = "1.2.1"
|
||||
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
|
||||
serde_json = "1.0.127"
|
||||
anyhow = "1.0"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] }
|
||||
notify = "8.0"
|
||||
rustlings-macros = { path = "rustlings-macros", version = "=6.4.0" }
|
||||
serde_json = "1.0"
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.12.0"
|
||||
tempfile = "3.19"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
@ -68,6 +69,7 @@ panic = "abort"
|
||||
|
||||
[package.metadata.release]
|
||||
pre-release-hook = ["./release-hook.sh"]
|
||||
pre-release-commit-message = "Release 🎉"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
@ -81,8 +83,6 @@ infinite_loop = "deny"
|
||||
mem_forget = "deny"
|
||||
dbg_macro = "warn"
|
||||
todo = "warn"
|
||||
# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
|
||||
needless_option_as_deref = "allow"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
157
README.md
157
README.md
@ -1,156 +1,7 @@
|
||||
<div class="oranda-hide">
|
||||
# [Rustlings](https://rustlings.rust-lang.org) 🦀
|
||||
|
||||
# Rustlings 🦀❤️
|
||||
Small exercises to get you used to reading and writing [Rust](https://www.rust-lang.org) code - _Recommended in parallel to reading [the official Rust book](https://doc.rust-lang.org/book) 📚️_
|
||||
|
||||
</div>
|
||||
Visit the **website** for a demo, info about setup and more:
|
||||
|
||||
Greetings and welcome to Rustlings.
|
||||
This project contains small exercises to get you used to reading and writing Rust code.
|
||||
This includes reading and responding to compiler messages!
|
||||
|
||||
It is recommended to do the Rustlings exercises in parallel to reading [the official Rust book](https://doc.rust-lang.org/book/), the most comprehensive resource for learning Rust 📚️
|
||||
|
||||
[Rust By Example](https://doc.rust-lang.org/rust-by-example/) is another recommended resource that you might find helpful.
|
||||
It contains code examples and exercises similar to Rustlings, but online.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installing Rust
|
||||
|
||||
Before installing Rustlings, you need to have the **latest version of Rust** installed.
|
||||
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust.
|
||||
This will also install _Cargo_, Rust's package/project manager.
|
||||
|
||||
> 🐧 If you're on Linux, make sure you've installed `gcc` (for a linker).
|
||||
>
|
||||
> Deb: `sudo apt install gcc`.
|
||||
> Dnf: `sudo dnf install gcc`.
|
||||
|
||||
> 🍎 If you're on MacOS, make sure you've installed Xcode and its developer tools by running `xcode-select --install`.
|
||||
|
||||
### Installing Rustlings
|
||||
|
||||
The following command will download and compile Rustlings:
|
||||
|
||||
```bash
|
||||
cargo install rustlings
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>If the installation fails…</strong> (<em>click to expand</em>)</summary>
|
||||
|
||||
- Make sure you have the latest Rust version by running `rustup update`
|
||||
- Try adding the `--locked` flag: `cargo install rustlings --locked`
|
||||
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
|
||||
|
||||
</details>
|
||||
|
||||
### Initialization
|
||||
|
||||
After installing Rustlings, run the following command to initialize the `rustlings/` directory:
|
||||
|
||||
```bash
|
||||
rustlings init
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>If the command <code>rustlings</code> can't be found…</strong> (<em>click to expand</em>)</summary>
|
||||
|
||||
You are probably using Linux and installed Rust using your package manager.
|
||||
|
||||
Cargo installs binaries to the directory `~/.cargo/bin`.
|
||||
Sadly, package managers often don't add `~/.cargo/bin` to your `PATH` environment variable.
|
||||
|
||||
The solution is to …
|
||||
|
||||
- either add `~/.cargo/bin` manually to `PATH`
|
||||
- or to uninstall Rust from the package manager and install it using the official way with `rustup`: https://www.rust-lang.org/tools/install
|
||||
|
||||
</details>
|
||||
|
||||
Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises:
|
||||
|
||||
```bash
|
||||
cd rustlings/
|
||||
rustlings
|
||||
```
|
||||
|
||||
## Working environment
|
||||
|
||||
### Editor
|
||||
|
||||
Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
||||
But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises.
|
||||
|
||||
### Terminal
|
||||
|
||||
While working with Rustlings, please use a modern terminal for the best user experience.
|
||||
The default terminal on Linux and Mac should be sufficient.
|
||||
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
|
||||
|
||||
## Doing exercises
|
||||
|
||||
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.
|
||||
For every topic, there is an additional `README.md` file with some resources to get you started on the topic.
|
||||
We highly recommend that you have a look at them before you start 📚️
|
||||
|
||||
Most exercises contain an error that keeps them from compiling, and it's up to you to fix it!
|
||||
Some exercises contain tests that need to pass for the exercise to be done ✅
|
||||
|
||||
Search for `TODO` and `todo!()` to find out what you need to change.
|
||||
Ask for hints by entering `h` in the _watch mode_ 💡
|
||||
|
||||
### Watch Mode
|
||||
|
||||
After [initialization](#initialization), Rustlings can be launched by simply running the command `rustlings`.
|
||||
|
||||
This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers).
|
||||
It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
|
||||
|
||||
<details>
|
||||
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>
|
||||
|
||||
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
|
||||
>
|
||||
> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
|
||||
|
||||
</details>
|
||||
|
||||
### Exercise List
|
||||
|
||||
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
|
||||
|
||||
The list allows you to…
|
||||
|
||||
- See the status of all exercises (done or pending)
|
||||
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
|
||||
- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards)
|
||||
|
||||
See the footer of the list for all possible keys.
|
||||
|
||||
## 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.
|
||||
|
||||
## Third-Party Exercises
|
||||
|
||||
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
|
||||
Or do you want to translate the original Rustlings exercises?
|
||||
Then follow the link to the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
|
||||
|
||||
## Uninstalling Rustlings
|
||||
|
||||
If you want to remove Rustlings from your system, run the following command:
|
||||
|
||||
```bash
|
||||
cargo uninstall rustlings
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md) 🔗
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks to [all the wonderful contributors](https://github.com/rust-lang/rustlings/graphs/contributors) 🎉
|
||||
## ➡️ [rustlings.rust-lang.org](https://rustlings.rust-lang.org) ⬅️
|
||||
|
@ -1,53 +0,0 @@
|
||||
# Third-Party Exercises
|
||||
|
||||
The support of Rustlings for third-party exercises allows you to create your own set of Rustlings exercises to focus on some specific topic.
|
||||
You could also offer a translation of the original Rustlings exercises as third-party exercises.
|
||||
|
||||
## Getting started
|
||||
|
||||
To create third-party exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`.
|
||||
This command will, similar to `cargo new PROJECT_NAME`, create a template directory called `PROJECT_NAME` with all what you need to get started.
|
||||
|
||||
Read the comments in the generated `info.toml` file to understand its format.
|
||||
It allows you to set a custom welcome and final message and specify the metadata of every exercise.
|
||||
|
||||
## Create an exercise
|
||||
|
||||
Here is an example of the metadata of one file:
|
||||
|
||||
```toml
|
||||
[[exercises]]
|
||||
name = "intro1"
|
||||
hint = """
|
||||
To finish this exercise, you need to …
|
||||
This link might help you …"""
|
||||
```
|
||||
|
||||
After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory.
|
||||
The exercise needs to contain a `main` function, but it can be empty.
|
||||
Adding tests is recommended.
|
||||
Look at the official Rustlings exercises for inspiration.
|
||||
|
||||
You can optionally add a solution file `intro1.rs` to the `solutions/` directory.
|
||||
|
||||
Now, run `rustlings dev check`.
|
||||
It will tell you about any issues with your exercises.
|
||||
For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`.
|
||||
|
||||
`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully.
|
||||
|
||||
That's it!
|
||||
You finished your first exercise 🎉
|
||||
|
||||
## Publish
|
||||
|
||||
Now, add more exercises and publish them as a Git repository.
|
||||
|
||||
Users just have to clone that repository and run `rustlings` in it to start working on your set of exercises just like the official ones.
|
||||
|
||||
One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise.
|
||||
But you can trust the users to not look at the solution too early 😉
|
||||
|
||||
## Share
|
||||
|
||||
After publishing your set of exercises, open an issue or a pull request in the official Rustlings repository to link to your project in the README 😃
|
5
build.rs
Normal file
5
build.rs
Normal file
@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
// Fix building from source on Windows because it can't handle file links.
|
||||
#[cfg(windows)]
|
||||
let _ = std::fs::copy("dev/Cargo.toml", "dev-Cargo.toml");
|
||||
}
|
@ -5,9 +5,11 @@ disallowed-types = [
|
||||
]
|
||||
|
||||
disallowed-methods = [
|
||||
# We use `ahash` instead of the default hasher.
|
||||
"std::collections::HashSet::new",
|
||||
"std::collections::HashSet::with_capacity",
|
||||
# Inefficient. Use `.queue(…)` instead.
|
||||
"crossterm::style::style",
|
||||
# Use `thread::Builder::spawn` instead and handle the error.
|
||||
"std::thread::spawn",
|
||||
"std::thread::Scope::spawn",
|
||||
# Return `ExitCode` instead.
|
||||
"std::process::exit",
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update`. This comment line will be stripped in `rustlings init`.
|
||||
# Don't edit the `bin` list manually! It is updated by `cargo dev update`. This comment line will be stripped in `rustlings init`.
|
||||
bin = [
|
||||
{ name = "intro1", path = "../exercises/00_intro/intro1.rs" },
|
||||
{ name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" },
|
||||
@ -192,7 +192,7 @@ bin = [
|
||||
|
||||
[package]
|
||||
name = "exercises"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
# Don't publish the exercises on crates.io!
|
||||
publish = false
|
||||
|
||||
@ -203,17 +203,21 @@ panic = "abort"
|
||||
panic = "abort"
|
||||
|
||||
[lints.rust]
|
||||
# You shouldn't write unsafe code in Rustlings
|
||||
# You shouldn't write unsafe code in Rustlings!
|
||||
unsafe_code = "forbid"
|
||||
# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust
|
||||
# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
|
||||
unstable_features = "forbid"
|
||||
# Dead code warnings can't be avoided in some exercises and might distract while learning.
|
||||
dead_code = "allow"
|
||||
|
||||
[lints.clippy]
|
||||
# You forgot a `todo!()`
|
||||
# You forgot a `todo!()`!
|
||||
todo = "forbid"
|
||||
# This can only happen by mistake in Rustlings
|
||||
# This can only happen by mistake in Rustlings.
|
||||
empty_loop = "forbid"
|
||||
# No infinite loops are needed in Rustlings
|
||||
# No infinite loops are needed in Rustlings.
|
||||
infinite_loop = "deny"
|
||||
# You shouldn't leak memory while still learning Rust
|
||||
# You shouldn't leak memory while still learning Rust!
|
||||
mem_forget = "deny"
|
||||
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
|
||||
disallowed_methods = "allow"
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Variables
|
||||
|
||||
In Rust, variables are immutable by default.
|
||||
When a variable is immutable, once a value is bound to a name, you can’t change that value.
|
||||
When a variable is immutable, once a value is bound to a name, you can't change that value.
|
||||
You can make them mutable by adding `mut` in front of the variable name.
|
||||
|
||||
## Further information
|
||||
|
@ -1,6 +1,6 @@
|
||||
fn main() {
|
||||
let number = "T-H-R-E-E"; // Don't change this line
|
||||
println!("Spell a number: {}", number);
|
||||
println!("Spell a number: {number}");
|
||||
|
||||
// TODO: Fix the compiler error by changing the line below without renaming the variable.
|
||||
number = 3;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// TODO: Fix the compiler error on this function.
|
||||
fn foo_if_fizz(fizzish: &str) -> &str {
|
||||
if fizzish == "fizz" {
|
||||
"foo"
|
||||
fn picky_eater(food: &str) -> &str {
|
||||
if food == "strawberry" {
|
||||
"Yummy!"
|
||||
} else {
|
||||
1
|
||||
}
|
||||
@ -18,18 +18,20 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn foo_for_fizz() {
|
||||
// This means that calling `foo_if_fizz` with the argument "fizz" should return "foo".
|
||||
assert_eq!(foo_if_fizz("fizz"), "foo");
|
||||
fn yummy_food() {
|
||||
// This means that calling `picky_eater` with the argument "strawberry" should return "Yummy!".
|
||||
assert_eq!(picky_eater("strawberry"), "Yummy!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_for_fuzz() {
|
||||
assert_eq!(foo_if_fizz("fuzz"), "bar");
|
||||
fn neutral_food() {
|
||||
assert_eq!(picky_eater("potato"), "I guess I can eat that.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_to_baz() {
|
||||
assert_eq!(foo_if_fizz("literally anything"), "baz");
|
||||
fn default_disliked_food() {
|
||||
assert_eq!(picky_eater("broccoli"), "No thanks!");
|
||||
assert_eq!(picky_eater("gummy bears"), "No thanks!");
|
||||
assert_eq!(picky_eater("literally anything"), "No thanks!");
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
# Enums
|
||||
|
||||
Rust allows you to define types called "enums" which enumerate possible values.
|
||||
Enums are a feature in many languages, but their capabilities differ in each language. Rust’s enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.
|
||||
Enums are a feature in many languages, but their capabilities differ in each language. Rust's enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.
|
||||
Useful in combination with enums is Rust's "pattern matching" facility, which makes it easy to run different code for different values of an enumeration.
|
||||
|
||||
## Further information
|
||||
|
||||
- [Enums](https://doc.rust-lang.org/book/ch06-00-enums.html)
|
||||
- [Pattern syntax](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html)
|
||||
- [Pattern syntax](https://doc.rust-lang.org/book/ch19-03-pattern-syntax.html)
|
||||
|
@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Point {
|
||||
x: u64,
|
||||
|
@ -4,7 +4,11 @@ struct Point {
|
||||
}
|
||||
|
||||
enum Message {
|
||||
// TODO: Implement the message variant types based on their usage below.
|
||||
Resize { width: u64, height: u64 },
|
||||
Move(Point),
|
||||
Echo(String),
|
||||
ChangeColor(u8, u8, u8),
|
||||
Quit,
|
||||
}
|
||||
|
||||
struct State {
|
||||
|
@ -23,6 +23,7 @@ mod tests {
|
||||
assert_eq!(trim_me("Hello! "), "Hello!");
|
||||
assert_eq!(trim_me(" What's up!"), "What's up!");
|
||||
assert_eq!(trim_me(" Hola! "), "Hola!");
|
||||
assert_eq!(trim_me("Hi!"), "Hi!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1,7 +1,6 @@
|
||||
// You can bring module paths into scopes and provide new names for them with
|
||||
// the `use` and `as` keywords.
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod delicious_snacks {
|
||||
// TODO: Add the following two `use` statements after fixing them.
|
||||
// use self::fruits::PEAR as ???;
|
||||
|
@ -17,7 +17,7 @@ struct TeamScores {
|
||||
|
||||
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||
// The name of the team is the key and its associated struct is the value.
|
||||
let mut scores = HashMap::new();
|
||||
let mut scores = HashMap::<&str, TeamScores>::new();
|
||||
|
||||
for line in results.lines() {
|
||||
let mut split_iterator = line.split(',');
|
||||
|
@ -1,8 +1,8 @@
|
||||
// This function returns how much icecream there is left in the fridge.
|
||||
// This function returns how much ice cream there is left in the fridge.
|
||||
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
|
||||
// someone eats it all, so no icecream is left (value 0). Return `None` if
|
||||
// someone eats it all, so no ice cream is left (value 0). Return `None` if
|
||||
// `hour_of_day` is higher than 23.
|
||||
fn maybe_icecream(hour_of_day: u16) -> Option<u16> {
|
||||
fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> {
|
||||
// TODO: Complete the function body.
|
||||
}
|
||||
|
||||
@ -18,19 +18,19 @@ mod tests {
|
||||
fn raw_value() {
|
||||
// TODO: Fix this test. How do you get the value contained in the
|
||||
// Option?
|
||||
let icecreams = maybe_icecream(12);
|
||||
let ice_creams = maybe_ice_cream(12);
|
||||
|
||||
assert_eq!(icecreams, 5); // Don't change this line.
|
||||
assert_eq!(ice_creams, 5); // Don't change this line.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_icecream() {
|
||||
assert_eq!(maybe_icecream(0), Some(5));
|
||||
assert_eq!(maybe_icecream(9), Some(5));
|
||||
assert_eq!(maybe_icecream(18), Some(5));
|
||||
assert_eq!(maybe_icecream(22), Some(0));
|
||||
assert_eq!(maybe_icecream(23), Some(0));
|
||||
assert_eq!(maybe_icecream(24), None);
|
||||
assert_eq!(maybe_icecream(25), None);
|
||||
fn check_ice_cream() {
|
||||
assert_eq!(maybe_ice_cream(0), Some(5));
|
||||
assert_eq!(maybe_ice_cream(9), Some(5));
|
||||
assert_eq!(maybe_ice_cream(18), Some(5));
|
||||
assert_eq!(maybe_ice_cream(22), Some(0));
|
||||
assert_eq!(maybe_ice_cream(23), Some(0));
|
||||
assert_eq!(maybe_ice_cream(24), None);
|
||||
assert_eq!(maybe_ice_cream(25), None);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ fn main() {
|
||||
|
||||
// TODO: Fix the compiler error by adding something to this match statement.
|
||||
match optional_point {
|
||||
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
|
||||
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
|
||||
_ => panic!("No match!"),
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Error handling
|
||||
|
||||
Most errors aren’t serious enough to require the program to stop entirely.
|
||||
Sometimes, when a function fails, it’s for a reason that you can easily interpret and respond to.
|
||||
For example, if you try to open a file and that operation fails because the file doesn’t exist, you might want to create the file instead of terminating the process.
|
||||
Most errors aren't serious enough to require the program to stop entirely.
|
||||
Sometimes, when a function fails, it's for a reason that you can easily interpret and respond to.
|
||||
For example, if you try to open a file and that operation fails because the file doesn't exist, you might want to create the file instead of terminating the process.
|
||||
|
||||
## Further information
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
//
|
||||
// In short, this particular use case for boxes is for when you want to own a
|
||||
// value and you care only that it is a type which implements a particular
|
||||
// trait. To do so, The `Box` is declared as of type `Box<dyn Trait>` where
|
||||
// trait. To do so, the `Box` is declared as of type `Box<dyn Trait>` where
|
||||
// `Trait` is the trait the compiler looks for on any value used in that
|
||||
// context. For this exercise, that context is the potential errors which
|
||||
// can be returned in a `Result`.
|
||||
|
@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
trait Licensed {
|
||||
// TODO: Add a default implementation for `licensing_info` so that
|
||||
// implementors like the two structs below can share that default behavior
|
||||
|
@ -8,7 +8,6 @@ use std::rc::Rc;
|
||||
#[derive(Debug)]
|
||||
struct Sun;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum Planet {
|
||||
Mercury(Rc<Sun>),
|
||||
|
@ -1,5 +1,5 @@
|
||||
// This program spawns multiple threads that each run for at least 250ms, and
|
||||
// each thread returns how much time they took to complete. The program should
|
||||
// This program spawns multiple threads that each runs for at least 250ms, and
|
||||
// each thread returns how much time it took to complete. The program should
|
||||
// wait until all the spawned threads have finished and should collect their
|
||||
// return values into a vector.
|
||||
|
||||
|
@ -10,5 +10,6 @@ of exercises to Rustlings, but is all about learning to write Macros.
|
||||
|
||||
## Further information
|
||||
|
||||
- [Macros](https://doc.rust-lang.org/book/ch19-06-macros.html)
|
||||
- [The Rust Book - Macros](https://doc.rust-lang.org/book/ch20-05-macros.html)
|
||||
- [The Little Book of Rust Macros](https://veykril.github.io/tlborm/)
|
||||
- [Rust by Example - macro_rules!](https://doc.rust-lang.org/rust-by-example/macros.html)
|
||||
|
@ -4,9 +4,11 @@
|
||||
#[rustfmt::skip]
|
||||
#[allow(unused_variables, unused_assignments)]
|
||||
fn main() {
|
||||
let my_option: Option<()> = None;
|
||||
let my_option: Option<&str> = None;
|
||||
// Assume that you don't know the value of `my_option`.
|
||||
// In the case of `Some`, we want to print its value.
|
||||
if my_option.is_none() {
|
||||
println!("{:?}", my_option.unwrap());
|
||||
println!("{}", my_option.unwrap());
|
||||
}
|
||||
|
||||
let my_arr = &[
|
||||
|
@ -2,10 +2,11 @@
|
||||
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
||||
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
||||
|
||||
// Obtain the number of bytes (not characters) in the given argument.
|
||||
// Obtain the number of bytes (not characters) in the given argument
|
||||
// (`.len()` returns the number of bytes in a string).
|
||||
// TODO: Add the `AsRef` trait appropriately as a trait bound.
|
||||
fn byte_counter<T>(arg: T) -> usize {
|
||||
arg.as_ref().as_bytes().len()
|
||||
arg.as_ref().len()
|
||||
}
|
||||
|
||||
// Obtain the number of characters (not bytes) in the given argument.
|
||||
|
@ -25,7 +25,7 @@ enum ParsePersonError {
|
||||
ParseInt(ParseIntError),
|
||||
}
|
||||
|
||||
// TODO: Complete this `From` implementation to be able to parse a `Person`
|
||||
// TODO: Complete this `FromStr` implementation to be able to parse a `Person`
|
||||
// out of a string in the form of "Mark,20".
|
||||
// Note that you'll need to parse the age component into a `u8` with something
|
||||
// like `"4".parse::<u8>()`.
|
||||
|
13
oranda.json
13
oranda.json
@ -1,13 +0,0 @@
|
||||
{
|
||||
"project": {
|
||||
"homepage": "https://rustlings.cool",
|
||||
"repository": "https://github.com/rust-lang/rustlings"
|
||||
},
|
||||
"marketing": {
|
||||
"analytics": {
|
||||
"plausible": {
|
||||
"domain": "rustlings.cool"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,5 +9,8 @@ cargo upgrades
|
||||
# Similar to CI
|
||||
cargo clippy -- --deny warnings
|
||||
cargo fmt --all --check
|
||||
cargo test --workspace --all-targets
|
||||
cargo run -- dev check --require-solutions
|
||||
cargo test --workspace
|
||||
cargo dev check --require-solutions
|
||||
|
||||
# MSRV
|
||||
cargo +1.87 dev check --require-solutions
|
||||
|
@ -16,7 +16,7 @@ include = [
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0.37"
|
||||
quote = "1.0"
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
format_version = 1
|
||||
|
||||
welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners!
|
||||
welcome_message = """
|
||||
Is this your first time? Don't worry, Rustlings is made for beginners!
|
||||
We are going to teach you a lot of things about Rust, but before we can
|
||||
get started, here are some notes about how Rustlings operates:
|
||||
|
||||
@ -10,15 +11,16 @@ get started, here are some notes about how Rustlings operates:
|
||||
and fix them!
|
||||
2. Make sure to have your editor open in the `rustlings/` directory. Rustlings
|
||||
will show you the path of the current exercise under the progress bar. Open
|
||||
the exercise file in your editor, fix errors and save the file. Rustlings will
|
||||
automatically detect the file change and rerun the exercise. If all errors are
|
||||
fixed, Rustlings will ask you to move on to the next exercise.
|
||||
the exercise file in your editor, fix errors and save the file. Rustlings
|
||||
will automatically detect the file change and rerun the exercise. If all
|
||||
errors are fixed, Rustlings will ask you to move on to the next exercise.
|
||||
3. If you're stuck on an exercise, enter `h` to show a hint.
|
||||
4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
|
||||
(https://github.com/rust-lang/rustlings). We look at every issue, and sometimes,
|
||||
other learners do too so you can help each other out!"""
|
||||
4. If an exercise doesn't make sense to you, feel free to open an issue on
|
||||
GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and
|
||||
sometimes, other learners do too so you can help each other out!"""
|
||||
|
||||
final_message = """We hope you enjoyed learning about the various aspects of Rust!
|
||||
final_message = """
|
||||
We hope you enjoyed learning about the various aspects of Rust!
|
||||
If you noticed any issues, don't hesitate to report them on Github.
|
||||
You can also contribute your own exercises to help the greater community!
|
||||
|
||||
@ -120,10 +122,10 @@ dir = "01_variables"
|
||||
test = false
|
||||
hint = """
|
||||
We know about variables and mutability, but there is another important type of
|
||||
variables available: constants.
|
||||
variable available: constants.
|
||||
|
||||
Constants are always immutable. They are declared with the keyword `const` instead
|
||||
of `let`.
|
||||
Constants are always immutable. They are declared with the keyword `const`
|
||||
instead of `let`.
|
||||
|
||||
The type of Constants must always be annotated.
|
||||
|
||||
@ -253,7 +255,7 @@ require you to type in 100 items (but you certainly can if you want!).
|
||||
|
||||
For example, you can do:
|
||||
```
|
||||
let array = ["Are we there yet?"; 10];
|
||||
let array = ["Are we there yet?"; 100];
|
||||
```
|
||||
|
||||
Bonus: what are some other things you could have that would return `true`
|
||||
@ -319,7 +321,8 @@ hint = """
|
||||
In the first function, we create an empty vector and want to push new elements
|
||||
to it.
|
||||
|
||||
In the second function, we map the values of the input and collect them into a vector.
|
||||
In the second function, we map the values of the input and collect them into
|
||||
a vector.
|
||||
|
||||
After you've completed both functions, decide for yourself which approach you
|
||||
like better.
|
||||
@ -332,8 +335,8 @@ What do you think is the more commonly used pattern under Rust developers?"""
|
||||
name = "move_semantics1"
|
||||
dir = "06_move_semantics"
|
||||
hint = """
|
||||
So you've got the "cannot borrow `vec` as mutable, as it is not declared as mutable"
|
||||
error on the line where we push an element to the vector, right?
|
||||
So you've got the "cannot borrow `vec` as mutable, as it is not declared as
|
||||
mutable" error on the line where we push an element to the vector, right?
|
||||
|
||||
The fix for this is going to be adding one keyword, and the addition is NOT on
|
||||
the line where we push to the vector (where the error is).
|
||||
@ -369,7 +372,8 @@ hint = """
|
||||
Carefully reason about the range in which each mutable reference is in
|
||||
scope. Does it help to update the value of `x` immediately after
|
||||
the mutable reference is taken?
|
||||
Read more about 'Mutable References' in the book's section 'References and Borrowing':
|
||||
Read more about 'Mutable References' in the book's section 'References and
|
||||
Borrowing':
|
||||
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references."""
|
||||
|
||||
[[exercises]]
|
||||
@ -508,7 +512,8 @@ name = "strings4"
|
||||
dir = "09_strings"
|
||||
test = false
|
||||
hint = """
|
||||
Replace `placeholder` with either `string` or `string_slice` in the `main` function.
|
||||
Replace `placeholder` with either `string` or `string_slice` in the `main`
|
||||
function.
|
||||
|
||||
Example:
|
||||
`placeholder("blue");`
|
||||
@ -570,12 +575,8 @@ https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-
|
||||
name = "hashmaps3"
|
||||
dir = "11_hashmaps"
|
||||
hint = """
|
||||
Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of
|
||||
`HashMap` to insert the default value of `TeamScores` if a team doesn't
|
||||
exist in the table yet.
|
||||
|
||||
Learn more in The Book:
|
||||
https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value
|
||||
Hint 1: Use the `entry()` and `or_default()` methods of `HashMap` to insert the
|
||||
default value of `TeamScores` if a team doesn't exist in the table yet.
|
||||
|
||||
Hint 2: If there is already an entry for a given key, the value returned by
|
||||
`entry()` can be updated based on the existing value.
|
||||
@ -763,7 +764,7 @@ Notice how the trait takes ownership of `self` and returns `Self`.
|
||||
|
||||
Although the signature of `append_bar` in the trait takes `self` as argument,
|
||||
the implementation can take `mut self` instead. This is possible because the
|
||||
the value is owned anyway."""
|
||||
value is owned anyway."""
|
||||
|
||||
[[exercises]]
|
||||
name = "traits3"
|
||||
@ -1139,7 +1140,7 @@ constants, but clippy recognizes those imprecise mathematical constants as a
|
||||
source of potential error.
|
||||
|
||||
See the suggestions of the Clippy warning in the compile output and use the
|
||||
appropriate replacement constant from `std::f32::consts`..."""
|
||||
appropriate replacement constant from `std::f32::consts`."""
|
||||
|
||||
[[exercises]]
|
||||
name = "clippy2"
|
||||
@ -1200,7 +1201,8 @@ hint = """
|
||||
Is there an implementation of `TryFrom` in the standard library that can both do
|
||||
the required integer conversion and check the range of the input?
|
||||
|
||||
Challenge: Can you make the `TryFrom` implementations generic over many integer types?"""
|
||||
Challenge: Can you make the `TryFrom` implementations generic over many integer
|
||||
types?"""
|
||||
|
||||
[[exercises]]
|
||||
name = "as_ref_mut"
|
||||
|
@ -1,6 +1,6 @@
|
||||
fn main() {
|
||||
let number = "T-H-R-E-E";
|
||||
println!("Spell a number: {}", number);
|
||||
println!("Spell a number: {number}");
|
||||
|
||||
// Using variable shadowing
|
||||
// https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing
|
||||
|
@ -1,9 +1,5 @@
|
||||
fn bigger(a: i32, b: i32) -> i32 {
|
||||
if a > b {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
if a > b { a } else { b }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
@ -1,10 +1,10 @@
|
||||
fn foo_if_fizz(fizzish: &str) -> &str {
|
||||
if fizzish == "fizz" {
|
||||
"foo"
|
||||
} else if fizzish == "fuzz" {
|
||||
"bar"
|
||||
fn picky_eater(food: &str) -> &str {
|
||||
if food == "strawberry" {
|
||||
"Yummy!"
|
||||
} else if food == "potato" {
|
||||
"I guess I can eat that."
|
||||
} else {
|
||||
"baz"
|
||||
"No thanks!"
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,17 +17,19 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn foo_for_fizz() {
|
||||
assert_eq!(foo_if_fizz("fizz"), "foo");
|
||||
fn yummy_food() {
|
||||
assert_eq!(picky_eater("strawberry"), "Yummy!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_for_fuzz() {
|
||||
assert_eq!(foo_if_fizz("fuzz"), "bar");
|
||||
fn neutral_food() {
|
||||
assert_eq!(picky_eater("potato"), "I guess I can eat that.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_to_baz() {
|
||||
assert_eq!(foo_if_fizz("literally anything"), "baz");
|
||||
fn default_disliked_food() {
|
||||
assert_eq!(picky_eater("broccoli"), "No thanks!");
|
||||
assert_eq!(picky_eater("gummy bears"), "No thanks!");
|
||||
assert_eq!(picky_eater("literally anything"), "No thanks!");
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ fn main() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// TODO: Fix the compiler errors only by reordering the lines in the test.
|
||||
// Don't add, change or remove any line.
|
||||
#[test]
|
||||
fn move_semantics4() {
|
||||
let mut x = Vec::new();
|
||||
|
@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Point {
|
||||
x: u64,
|
||||
|
@ -46,8 +46,8 @@ impl State {
|
||||
match message {
|
||||
Message::Resize { width, height } => self.resize(width, height),
|
||||
Message::Move(point) => self.move_position(point),
|
||||
Message::Echo(s) => self.echo(s),
|
||||
Message::ChangeColor(r, g, b) => self.change_color(r, g, b),
|
||||
Message::Echo(string) => self.echo(string),
|
||||
Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue),
|
||||
Message::Quit => self.quit(),
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ mod tests {
|
||||
assert_eq!(trim_me("Hello! "), "Hello!");
|
||||
assert_eq!(trim_me(" What's up!"), "What's up!");
|
||||
assert_eq!(trim_me(" Hola! "), "Hola!");
|
||||
assert_eq!(trim_me("Hi!"), "Hi!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1,4 +1,3 @@
|
||||
#[allow(dead_code)]
|
||||
mod delicious_snacks {
|
||||
// Added `pub` and used the expected alias after `as`.
|
||||
pub use self::fruits::PEAR as fruit;
|
||||
|
@ -17,7 +17,7 @@ struct TeamScores {
|
||||
|
||||
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||
// The name of the team is the key and its associated struct is the value.
|
||||
let mut scores = HashMap::new();
|
||||
let mut scores = HashMap::<&str, TeamScores>::new();
|
||||
|
||||
for line in results.lines() {
|
||||
let mut split_iterator = line.split(',');
|
||||
@ -28,17 +28,13 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
|
||||
|
||||
// Insert the default with zeros if a team doesn't exist yet.
|
||||
let team_1 = scores
|
||||
.entry(team_1_name)
|
||||
.or_insert_with(TeamScores::default);
|
||||
let team_1 = scores.entry(team_1_name).or_default();
|
||||
// Update the values.
|
||||
team_1.goals_scored += team_1_score;
|
||||
team_1.goals_conceded += team_2_score;
|
||||
|
||||
// Similarly for the second team.
|
||||
let team_2 = scores
|
||||
.entry(team_2_name)
|
||||
.or_insert_with(TeamScores::default);
|
||||
let team_2 = scores.entry(team_2_name).or_default();
|
||||
team_2.goals_scored += team_2_score;
|
||||
team_2.goals_conceded += team_1_score;
|
||||
}
|
||||
@ -64,9 +60,11 @@ England,Spain,1,0";
|
||||
fn build_scores() {
|
||||
let scores = build_scores_table(RESULTS);
|
||||
|
||||
assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"]
|
||||
.into_iter()
|
||||
.all(|team_name| scores.contains_key(team_name)));
|
||||
assert!(
|
||||
["England", "France", "Germany", "Italy", "Poland", "Spain"]
|
||||
.into_iter()
|
||||
.all(|team_name| scores.contains_key(team_name))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -10,7 +10,7 @@ fn main() {
|
||||
// Solution 1: Matching over the `Option` (not `&Option`) but without moving
|
||||
// out of the `Some` variant.
|
||||
match optional_point {
|
||||
Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y),
|
||||
Some(ref p) => println!("Coordinates are {},{}", p.x, p.y),
|
||||
// ^^^ added
|
||||
_ => panic!("No match!"),
|
||||
}
|
||||
@ -18,7 +18,8 @@ fn main() {
|
||||
// Solution 2: Matching over a reference (`&Option`) by added `&` before
|
||||
// `optional_point`.
|
||||
match &optional_point {
|
||||
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
|
||||
//^ added
|
||||
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
|
||||
_ => panic!("No match!"),
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
use std::num::ParseIntError;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[allow(unused_variables, clippy::question_mark)]
|
||||
fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
|
||||
let processing_fee = 1;
|
||||
let cost_per_item = 5;
|
||||
|
@ -29,6 +29,21 @@ impl ParsePosNonzeroError {
|
||||
}
|
||||
}
|
||||
|
||||
// As an alternative solution, implementing the `From` trait allows for the
|
||||
// automatic conversion from a `ParseIntError` into a `ParsePosNonzeroError`
|
||||
// using the `?` operator, without the need to call `map_err`.
|
||||
//
|
||||
// ```
|
||||
// let x: i64 = s.parse()?;
|
||||
// ```
|
||||
//
|
||||
// Traits like `From` will be dealt with in later exercises.
|
||||
impl From<ParseIntError> for ParsePosNonzeroError {
|
||||
fn from(err: ParseIntError) -> Self {
|
||||
ParsePosNonzeroError::ParseInt(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct PositiveNonzeroInteger(u64);
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
trait Licensed {
|
||||
fn licensing_info(&self) -> String {
|
||||
"Default license".to_string()
|
||||
|
@ -5,11 +5,7 @@
|
||||
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
// ^^^^ ^^ ^^ ^^
|
||||
if x.len() > y.len() {
|
||||
x
|
||||
} else {
|
||||
y
|
||||
}
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
@ -1,9 +1,5 @@
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
if x.len() > y.len() {
|
||||
x
|
||||
} else {
|
||||
y
|
||||
}
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
@ -8,7 +8,6 @@ use std::rc::Rc;
|
||||
#[derive(Debug)]
|
||||
struct Sun;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum Planet {
|
||||
Mercury(Rc<Sun>),
|
||||
@ -64,12 +63,10 @@ mod tests {
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
|
||||
saturn.details();
|
||||
|
||||
// TODO
|
||||
let uranus = Planet::Uranus(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
|
||||
uranus.details();
|
||||
|
||||
// TODO
|
||||
let neptune = Planet::Neptune(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 9 references
|
||||
neptune.details();
|
||||
|
@ -1,5 +1,5 @@
|
||||
// This program spawns multiple threads that each run for at least 250ms, and
|
||||
// each thread returns how much time they took to complete. The program should
|
||||
// This program spawns multiple threads that each runs for at least 250ms, and
|
||||
// each thread returns how much time it took to complete. The program should
|
||||
// wait until all the spawned threads have finished and should collect their
|
||||
// return values into a vector.
|
||||
|
||||
|
@ -3,11 +3,11 @@ use std::mem;
|
||||
#[rustfmt::skip]
|
||||
#[allow(unused_variables, unused_assignments)]
|
||||
fn main() {
|
||||
let my_option: Option<()> = None;
|
||||
let my_option: Option<&str> = None;
|
||||
// `unwrap` of an `Option` after checking if it is `None` will panic.
|
||||
// Use `if-let` instead.
|
||||
if let Some(value) = my_option {
|
||||
println!("{value:?}");
|
||||
println!("{value}");
|
||||
}
|
||||
|
||||
// A comma was missing.
|
||||
@ -15,7 +15,7 @@ fn main() {
|
||||
-1, -2, -3,
|
||||
-4, -5, -6,
|
||||
];
|
||||
println!("My array! Here it is: {:?}", my_arr);
|
||||
println!("My array! Here it is: {my_arr:?}");
|
||||
|
||||
let mut my_empty_vec = vec![1, 2, 3, 4, 5];
|
||||
// `resize` mutates a vector instead of returning a new one.
|
||||
@ -27,5 +27,5 @@ fn main() {
|
||||
let mut value_b = 66;
|
||||
// Use `mem::swap` to correctly swap two values.
|
||||
mem::swap(&mut value_a, &mut value_b);
|
||||
println!("value a: {}; value b: {}", value_a, value_b);
|
||||
println!("value a: {value_a}; value b: {value_b}");
|
||||
}
|
||||
|
@ -2,9 +2,10 @@
|
||||
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
||||
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
||||
|
||||
// Obtain the number of bytes (not characters) in the given argument.
|
||||
// Obtain the number of bytes (not characters) in the given argument
|
||||
// (`.len()` returns the number of bytes in a string).
|
||||
fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
|
||||
arg.as_ref().as_bytes().len()
|
||||
arg.as_ref().len()
|
||||
}
|
||||
|
||||
// Obtain the number of characters (not bytes) in the given argument.
|
||||
|
@ -62,8 +62,8 @@ mod tests {
|
||||
// Import `transformer`.
|
||||
use super::my_module::transformer;
|
||||
|
||||
use super::my_module::transformer_iter;
|
||||
use super::Command;
|
||||
use super::my_module::transformer_iter;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
|
263
src/app_state.rs
263
src/app_state.rs
@ -1,32 +1,39 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Error, Result, bail};
|
||||
use crossterm::{QueueableCommand, cursor, terminal};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, Read, Seek, StdoutLock, Write},
|
||||
path::Path,
|
||||
io::{Read, Seek, StdoutLock, Write},
|
||||
path::{MAIN_SEPARATOR_STR, Path},
|
||||
process::{Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::Relaxed},
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
clear_terminal,
|
||||
cmd::CmdRunner,
|
||||
collections::hash_set_with_capacity,
|
||||
embedded::EMBEDDED_FILES,
|
||||
exercise::{Exercise, RunnableExercise},
|
||||
info_file::ExerciseInfo,
|
||||
term::{self, CheckProgressVisualizer},
|
||||
};
|
||||
|
||||
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
|
||||
const DEFAULT_CHECK_PARALLELISM: usize = 8;
|
||||
|
||||
#[must_use]
|
||||
pub enum ExercisesProgress {
|
||||
// All exercises are done.
|
||||
AllDone,
|
||||
// The current exercise failed and is still pending.
|
||||
CurrentPending,
|
||||
// A new exercise is now pending.
|
||||
NewPending,
|
||||
// The current exercise is still pending.
|
||||
CurrentPending,
|
||||
}
|
||||
|
||||
pub enum StateFileStatus {
|
||||
@ -34,10 +41,12 @@ pub enum StateFileStatus {
|
||||
NotRead,
|
||||
}
|
||||
|
||||
enum AllExercisesCheck {
|
||||
Pending(usize),
|
||||
AllDone,
|
||||
CheckedUntil(usize),
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CheckProgress {
|
||||
None,
|
||||
Checking,
|
||||
Done,
|
||||
Pending,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
@ -71,6 +80,7 @@ impl AppState {
|
||||
format!("Failed to open or create the state file {STATE_FILE_NAME}")
|
||||
})?;
|
||||
|
||||
let dir_canonical_path = term::canonicalize("exercises");
|
||||
let mut exercises = exercise_infos
|
||||
.into_iter()
|
||||
.map(|exercise_info| {
|
||||
@ -82,10 +92,32 @@ impl AppState {
|
||||
let dir = exercise_info.dir.map(|dir| &*dir.leak());
|
||||
let hint = exercise_info.hint.leak().trim_ascii();
|
||||
|
||||
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
|
||||
let mut canonical_path;
|
||||
if let Some(dir) = dir {
|
||||
canonical_path = String::with_capacity(
|
||||
2 + dir_canonical_path.len() + dir.len() + name.len(),
|
||||
);
|
||||
canonical_path.push_str(dir_canonical_path);
|
||||
canonical_path.push_str(MAIN_SEPARATOR_STR);
|
||||
canonical_path.push_str(dir);
|
||||
} else {
|
||||
canonical_path =
|
||||
String::with_capacity(1 + dir_canonical_path.len() + name.len());
|
||||
canonical_path.push_str(dir_canonical_path);
|
||||
}
|
||||
|
||||
canonical_path.push_str(MAIN_SEPARATOR_STR);
|
||||
canonical_path.push_str(name);
|
||||
canonical_path.push_str(".rs");
|
||||
canonical_path
|
||||
});
|
||||
|
||||
Exercise {
|
||||
dir,
|
||||
name,
|
||||
path,
|
||||
canonical_path,
|
||||
test: exercise_info.test,
|
||||
strict_clippy: exercise_info.strict_clippy,
|
||||
hint,
|
||||
@ -114,13 +146,13 @@ impl AppState {
|
||||
break 'block StateFileStatus::NotRead;
|
||||
}
|
||||
|
||||
let mut done_exercises = hash_set_with_capacity(exercises.len());
|
||||
let mut done_exercises = HashSet::with_capacity(exercises.len());
|
||||
|
||||
for done_exerise_name in lines {
|
||||
if done_exerise_name.is_empty() {
|
||||
for done_exercise_name in lines {
|
||||
if done_exercise_name.is_empty() {
|
||||
break;
|
||||
}
|
||||
done_exercises.insert(done_exerise_name);
|
||||
done_exercises.insert(done_exercise_name);
|
||||
}
|
||||
|
||||
for (ind, exercise) in exercises.iter_mut().enumerate() {
|
||||
@ -170,6 +202,11 @@ impl AppState {
|
||||
self.n_done
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn n_pending(&self) -> u16 {
|
||||
self.exercises.len() as u16 - self.n_done
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current_exercise(&self) -> &Exercise {
|
||||
&self.exercises[self.current_exercise_ind]
|
||||
@ -246,15 +283,31 @@ impl AppState {
|
||||
self.write()
|
||||
}
|
||||
|
||||
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
|
||||
// Set the status of an exercise without saving. Returns `true` if the
|
||||
// status actually changed (and thus needs saving later).
|
||||
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
|
||||
let exercise = self
|
||||
.exercises
|
||||
.get_mut(exercise_ind)
|
||||
.context(BAD_INDEX_ERR)?;
|
||||
|
||||
if exercise.done {
|
||||
exercise.done = false;
|
||||
if exercise.done == done {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
exercise.done = done;
|
||||
if done {
|
||||
self.n_done += 1;
|
||||
} else {
|
||||
self.n_done -= 1;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
// Set the status of an exercise to "pending" and save.
|
||||
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
|
||||
if self.set_status(exercise_ind, false)? {
|
||||
self.write()?;
|
||||
}
|
||||
|
||||
@ -262,7 +315,7 @@ impl AppState {
|
||||
}
|
||||
|
||||
// Official exercises: Dump the original file from the binary.
|
||||
// Third-party exercises: Reset the exercise file with `git stash`.
|
||||
// Community exercises: Reset the exercise file with `git stash`.
|
||||
fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> {
|
||||
if self.official_exercises {
|
||||
return EMBEDDED_FILES
|
||||
@ -331,8 +384,8 @@ impl AppState {
|
||||
})
|
||||
}
|
||||
|
||||
/// Official exercises: Dump the solution file form the binary and return its path.
|
||||
/// Third-party exercises: Check if a solution file exists and return its path in that case.
|
||||
/// Official exercises: Dump the solution file from the binary and return its path.
|
||||
/// Community exercises: Check if a solution file exists and return its path in that case.
|
||||
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
||||
if cfg!(debug_assertions) {
|
||||
return Ok(None);
|
||||
@ -355,62 +408,125 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
// Return the exercise index of the first pending exercise found.
|
||||
fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
||||
stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
|
||||
let n_exercises = self.exercises.len();
|
||||
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
||||
let term_width = terminal::size()
|
||||
.context("Failed to get the terminal size")?
|
||||
.0;
|
||||
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
|
||||
|
||||
let status = thread::scope(|s| {
|
||||
let handles = self
|
||||
.exercises
|
||||
.iter()
|
||||
.map(|exercise| s.spawn(|| exercise.run_exercise(None, &self.cmd_runner)))
|
||||
.collect::<Vec<_>>();
|
||||
let next_exercise_ind = AtomicUsize::new(0);
|
||||
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
|
||||
|
||||
for (exercise_ind, handle) in handles.into_iter().enumerate() {
|
||||
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
|
||||
stdout.flush()?;
|
||||
thread::scope(|s| {
|
||||
let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
|
||||
let n_threads = thread::available_parallelism()
|
||||
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
|
||||
|
||||
let Ok(success) = handle.join().unwrap() else {
|
||||
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
|
||||
};
|
||||
for _ in 0..n_threads {
|
||||
let exercise_progress_sender = exercise_progress_sender.clone();
|
||||
let next_exercise_ind = &next_exercise_ind;
|
||||
let slf = &self;
|
||||
thread::Builder::new()
|
||||
.spawn_scoped(s, move || {
|
||||
loop {
|
||||
let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed);
|
||||
let Some(exercise) = slf.exercises.get(exercise_ind) else {
|
||||
// No more exercises.
|
||||
break;
|
||||
};
|
||||
|
||||
if !success {
|
||||
return Ok(AllExercisesCheck::Pending(exercise_ind));
|
||||
}
|
||||
if exercise_progress_sender
|
||||
.send((exercise_ind, CheckProgress::Checking))
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
};
|
||||
|
||||
let success = exercise.run_exercise(None, &slf.cmd_runner);
|
||||
let progress = match success {
|
||||
Ok(true) => CheckProgress::Done,
|
||||
Ok(false) => CheckProgress::Pending,
|
||||
Err(_) => CheckProgress::None,
|
||||
};
|
||||
|
||||
if exercise_progress_sender
|
||||
.send((exercise_ind, progress))
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.context("Failed to spawn a thread to check all exercises")?;
|
||||
}
|
||||
|
||||
Ok::<_, io::Error>(AllExercisesCheck::AllDone)
|
||||
// Drop this sender to detect when the last thread is done.
|
||||
drop(exercise_progress_sender);
|
||||
|
||||
while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
|
||||
progresses[exercise_ind] = progress;
|
||||
progress_visualizer.update(&progresses)?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
})?;
|
||||
|
||||
let mut exercise_ind = match status {
|
||||
AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)),
|
||||
AllExercisesCheck::AllDone => return Ok(None),
|
||||
AllExercisesCheck::CheckedUntil(ind) => ind,
|
||||
};
|
||||
let mut first_pending_exercise_ind = None;
|
||||
for exercise_ind in 0..progresses.len() {
|
||||
match progresses[exercise_ind] {
|
||||
CheckProgress::Done => {
|
||||
self.set_status(exercise_ind, true)?;
|
||||
}
|
||||
CheckProgress::Pending => {
|
||||
self.set_status(exercise_ind, false)?;
|
||||
if first_pending_exercise_ind.is_none() {
|
||||
first_pending_exercise_ind = Some(exercise_ind);
|
||||
}
|
||||
}
|
||||
CheckProgress::None | CheckProgress::Checking => {
|
||||
// If we got an error while checking all exercises in parallel,
|
||||
// it could be because we exceeded the limit of open file descriptors.
|
||||
// Therefore, try running exercises with errors sequentially.
|
||||
progresses[exercise_ind] = CheckProgress::Checking;
|
||||
progress_visualizer.update(&progresses)?;
|
||||
|
||||
// We got an error while checking all exercises in parallel.
|
||||
// This could be because we exceeded the limit of open file descriptors.
|
||||
// Therefore, try to continue the check sequentially.
|
||||
for exercise in &self.exercises[exercise_ind..] {
|
||||
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
|
||||
stdout.flush()?;
|
||||
|
||||
let success = exercise.run_exercise(None, &self.cmd_runner)?;
|
||||
if !success {
|
||||
return Ok(Some(exercise_ind));
|
||||
let exercise = &self.exercises[exercise_ind];
|
||||
let success = exercise.run_exercise(None, &self.cmd_runner)?;
|
||||
if success {
|
||||
progresses[exercise_ind] = CheckProgress::Done;
|
||||
} else {
|
||||
progresses[exercise_ind] = CheckProgress::Pending;
|
||||
if first_pending_exercise_ind.is_none() {
|
||||
first_pending_exercise_ind = Some(exercise_ind);
|
||||
}
|
||||
}
|
||||
self.set_status(exercise_ind, success)?;
|
||||
progress_visualizer.update(&progresses)?;
|
||||
}
|
||||
}
|
||||
|
||||
exercise_ind += 1;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
self.write()?;
|
||||
|
||||
Ok(first_pending_exercise_ind)
|
||||
}
|
||||
|
||||
// Return the exercise index of the first pending exercise found.
|
||||
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
||||
stdout.queue(cursor::Hide)?;
|
||||
let res = self.check_all_exercises_impl(stdout);
|
||||
stdout.queue(cursor::Show)?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
||||
/// If all exercises are marked as done, run all of them to make sure that they are actually
|
||||
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
||||
pub fn done_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||
pub fn done_current_exercise<const CLEAR_BEFORE_FINAL_CHECK: bool>(
|
||||
&mut self,
|
||||
stdout: &mut StdoutLock,
|
||||
) -> Result<ExercisesProgress> {
|
||||
let exercise = &mut self.exercises[self.current_exercise_ind];
|
||||
if !exercise.done {
|
||||
exercise.done = true;
|
||||
@ -422,20 +538,24 @@ impl AppState {
|
||||
return Ok(ExercisesProgress::NewPending);
|
||||
}
|
||||
|
||||
if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? {
|
||||
stdout.write_all(b"\n\n")?;
|
||||
if CLEAR_BEFORE_FINAL_CHECK {
|
||||
clear_terminal(stdout)?;
|
||||
} else {
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
|
||||
if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? {
|
||||
self.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||
|
||||
self.current_exercise_ind = pending_exercise_ind;
|
||||
self.exercises[pending_exercise_ind].done = false;
|
||||
// All exercises were marked as done.
|
||||
self.n_done -= 1;
|
||||
self.write()?;
|
||||
return Ok(ExercisesProgress::NewPending);
|
||||
}
|
||||
|
||||
// Write that the last exercise is done.
|
||||
self.write()?;
|
||||
self.render_final_message(stdout)?;
|
||||
|
||||
Ok(ExercisesProgress::AllDone)
|
||||
}
|
||||
|
||||
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
|
||||
clear_terminal(stdout)?;
|
||||
stdout.write_all(FENISH_LINE.as_bytes())?;
|
||||
|
||||
@ -445,16 +565,12 @@ impl AppState {
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
|
||||
Ok(ExercisesProgress::AllDone)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
|
||||
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
|
||||
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.
|
||||
";
|
||||
const FENISH_LINE: &str = "+----------------------------------------------------+
|
||||
| You made it to the Fe-nish line! |
|
||||
+-------------------------- ------------------------+
|
||||
@ -486,6 +602,7 @@ mod tests {
|
||||
dir: None,
|
||||
name: "0",
|
||||
path: "exercises/0.rs",
|
||||
canonical_path: None,
|
||||
test: false,
|
||||
strict_clippy: false,
|
||||
hint: "",
|
||||
|
@ -74,13 +74,13 @@ pub fn updated_cargo_toml(
|
||||
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
|
||||
|
||||
let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY);
|
||||
updated_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes());
|
||||
updated_cargo_toml.extend_from_slice(¤t_cargo_toml.as_bytes()[..bins_start_ind]);
|
||||
append_bins(
|
||||
&mut updated_cargo_toml,
|
||||
exercise_infos,
|
||||
exercise_path_prefix,
|
||||
);
|
||||
updated_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes());
|
||||
updated_cargo_toml.extend_from_slice(¤t_cargo_toml.as_bytes()[bins_end_ind..]);
|
||||
|
||||
Ok(updated_cargo_toml)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
io::Read,
|
||||
io::{Read, pipe},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
@ -17,7 +17,7 @@ fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) ->
|
||||
};
|
||||
|
||||
let mut handle = if let Some(output) = output {
|
||||
let (mut reader, writer) = os_pipe::pipe().with_context(|| {
|
||||
let (mut reader, writer) = pipe().with_context(|| {
|
||||
format!("Failed to create a pipe to run the command `{description}``")
|
||||
})?;
|
||||
|
||||
@ -125,7 +125,7 @@ pub struct CargoSubcommand<'out> {
|
||||
output: Option<&'out mut Vec<u8>>,
|
||||
}
|
||||
|
||||
impl<'out> CargoSubcommand<'out> {
|
||||
impl CargoSubcommand<'_> {
|
||||
#[inline]
|
||||
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
|
||||
where
|
||||
|
@ -1,10 +0,0 @@
|
||||
use ahash::AHasher;
|
||||
use std::hash::BuildHasherDefault;
|
||||
|
||||
/// DOS attacks aren't a concern for Rustlings. Therefore, we use `ahash` with fixed seeds.
|
||||
pub type HashSet<T> = std::collections::HashSet<T, BuildHasherDefault<AHasher>>;
|
||||
|
||||
#[inline]
|
||||
pub fn hash_set_with_capacity<T>(capacity: usize) -> HashSet<T> {
|
||||
HashSet::with_capacity_and_hasher(capacity, BuildHasherDefault::<AHasher>::default())
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Subcommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@ -8,7 +8,7 @@ mod update;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum DevCommands {
|
||||
/// Create a new project for third-party Rustlings exercises
|
||||
/// Create a new project for community exercises
|
||||
New {
|
||||
/// The path to create the project in
|
||||
path: PathBuf,
|
||||
|
110
src/dev/check.rs
110
src/dev/check.rs
@ -1,7 +1,8 @@
|
||||
use anyhow::{anyhow, bail, Context, Error, Result};
|
||||
use anyhow::{Context, Error, Result, anyhow, bail};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fs::{self, read_dir, OpenOptions},
|
||||
collections::HashSet,
|
||||
fs::{self, OpenOptions, read_dir},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
@ -9,14 +10,14 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
|
||||
cmd::CmdRunner,
|
||||
collections::{hash_set_with_capacity, HashSet},
|
||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
||||
info_file::{ExerciseInfo, InfoFile},
|
||||
CURRENT_FORMAT_VERSION,
|
||||
cargo_toml::{BINS_BUFFER_CAPACITY, append_bins, bins_start_end_ind},
|
||||
cmd::CmdRunner,
|
||||
exercise::{OUTPUT_CAPACITY, RunnableExercise},
|
||||
info_file::{ExerciseInfo, InfoFile},
|
||||
};
|
||||
|
||||
const MAX_N_EXERCISES: usize = 999;
|
||||
const MAX_EXERCISE_NAME_LEN: usize = 32;
|
||||
|
||||
// Find a char that isn't allowed in the exercise's `name` or `dir`.
|
||||
@ -41,10 +42,14 @@ fn check_cargo_toml(
|
||||
|
||||
if old_bins != new_bins {
|
||||
if cfg!(debug_assertions) {
|
||||
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
|
||||
bail!(
|
||||
"The file `dev/Cargo.toml` is outdated. Run `cargo dev update` to update it. Then run `cargo run -- dev check` again"
|
||||
);
|
||||
}
|
||||
|
||||
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
|
||||
bail!(
|
||||
"The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -52,8 +57,8 @@ fn check_cargo_toml(
|
||||
|
||||
// Check the info of all exercises and return their paths in a set.
|
||||
fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||
let mut names = hash_set_with_capacity(info_file.exercises.len());
|
||||
let mut paths = hash_set_with_capacity(info_file.exercises.len());
|
||||
let mut names = HashSet::with_capacity(info_file.exercises.len());
|
||||
let mut paths = HashSet::with_capacity(info_file.exercises.len());
|
||||
|
||||
let mut file_buf = String::with_capacity(1 << 14);
|
||||
for exercise_info in &info_file.exercises {
|
||||
@ -62,7 +67,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||
bail!("Found an empty exercise name in `info.toml`");
|
||||
}
|
||||
if name.len() > MAX_EXERCISE_NAME_LEN {
|
||||
bail!("The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN}");
|
||||
bail!(
|
||||
"The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN}"
|
||||
);
|
||||
}
|
||||
if let Some(c) = forbidden_char(name) {
|
||||
bail!("Char `{c}` in the exercise name `{name}` is not allowed");
|
||||
@ -78,7 +85,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||
}
|
||||
|
||||
if exercise_info.hint.trim_ascii().is_empty() {
|
||||
bail!("The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise");
|
||||
bail!(
|
||||
"The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise"
|
||||
);
|
||||
}
|
||||
|
||||
if !names.insert(name) {
|
||||
@ -95,20 +104,28 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||
.with_context(|| format!("Failed to read the file {path}"))?;
|
||||
|
||||
if !file_buf.contains("fn main()") {
|
||||
bail!("The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors");
|
||||
bail!(
|
||||
"The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors"
|
||||
);
|
||||
}
|
||||
|
||||
if !file_buf.contains("// TODO") {
|
||||
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
|
||||
bail!(
|
||||
"Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user."
|
||||
);
|
||||
}
|
||||
|
||||
let contains_tests = file_buf.contains("#[test]\n");
|
||||
if exercise_info.test {
|
||||
if !contains_tests {
|
||||
bail!("The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file");
|
||||
bail!(
|
||||
"The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file"
|
||||
);
|
||||
}
|
||||
} else if contains_tests {
|
||||
bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file");
|
||||
bail!(
|
||||
"The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file"
|
||||
);
|
||||
}
|
||||
|
||||
file_buf.clear();
|
||||
@ -124,7 +141,10 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||
// Only one level of directory nesting is allowed.
|
||||
fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> Result<()> {
|
||||
let unexpected_file = |path: &Path| {
|
||||
anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory", path.display())
|
||||
anyhow!(
|
||||
"Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory",
|
||||
path.display()
|
||||
)
|
||||
};
|
||||
|
||||
for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? {
|
||||
@ -153,7 +173,10 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
|
||||
let path = entry.path();
|
||||
|
||||
if !entry.file_type().unwrap().is_file() {
|
||||
bail!("Found `{}` but expected only files. Only one level of exercise nesting is allowed", path.display());
|
||||
bail!(
|
||||
"Found `{}` but expected only files. Only one level of exercise nesting is allowed",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let file_name = path.file_name().unwrap();
|
||||
@ -185,12 +208,14 @@ fn check_exercises_unsolved(
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((
|
||||
exercise_info.name.as_str(),
|
||||
thread::spawn(|| exercise_info.run_exercise(None, cmd_runner)),
|
||||
))
|
||||
Some(
|
||||
thread::Builder::new()
|
||||
.spawn(|| exercise_info.run_exercise(None, cmd_runner))
|
||||
.map(|handle| (exercise_info.name.as_str(), handle)),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Failed to spawn a thread to check if an exercise is already solved")?;
|
||||
|
||||
let n_handles = handles.len();
|
||||
write!(stdout, "Progress: 0/{n_handles}")?;
|
||||
@ -199,7 +224,7 @@ fn check_exercises_unsolved(
|
||||
|
||||
for (exercise_name, handle) in handles {
|
||||
let Ok(result) = handle.join() else {
|
||||
bail!("Panic while trying to run the exericse {exercise_name}");
|
||||
bail!("Panic while trying to run the exercise {exercise_name}");
|
||||
};
|
||||
|
||||
match result {
|
||||
@ -221,12 +246,18 @@ fn check_exercises_unsolved(
|
||||
|
||||
fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> {
|
||||
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
|
||||
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
|
||||
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
|
||||
Ordering::Less => bail!(
|
||||
"`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"
|
||||
),
|
||||
Ordering::Greater => bail!(
|
||||
"`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"
|
||||
),
|
||||
Ordering::Equal => (),
|
||||
}
|
||||
|
||||
let handle = thread::spawn(move || check_exercises_unsolved(info_file, cmd_runner));
|
||||
let handle = thread::Builder::new()
|
||||
.spawn(move || check_exercises_unsolved(info_file, cmd_runner))
|
||||
.context("Failed to spawn a thread to check if any exercise is already solved")?;
|
||||
|
||||
let info_file_paths = check_info_file_exercises(info_file)?;
|
||||
check_unexpected_files("exercises", &info_file_paths)?;
|
||||
@ -253,7 +284,7 @@ fn check_solutions(
|
||||
.exercises
|
||||
.iter()
|
||||
.map(|exercise_info| {
|
||||
thread::spawn(move || {
|
||||
thread::Builder::new().spawn(move || {
|
||||
let sol_path = exercise_info.sol_path();
|
||||
if !Path::new(&sol_path).exists() {
|
||||
if require_solutions {
|
||||
@ -274,14 +305,15 @@ fn check_solutions(
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Failed to spawn a thread to check a solution")?;
|
||||
|
||||
let mut sol_paths = hash_set_with_capacity(info_file.exercises.len());
|
||||
let mut sol_paths = HashSet::with_capacity(info_file.exercises.len());
|
||||
let mut fmt_cmd = Command::new("rustfmt");
|
||||
fmt_cmd
|
||||
.arg("--check")
|
||||
.arg("--edition")
|
||||
.arg("2021")
|
||||
.arg("2024")
|
||||
.arg("--color")
|
||||
.arg("always")
|
||||
.stdin(Stdio::null());
|
||||
@ -294,7 +326,7 @@ fn check_solutions(
|
||||
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
|
||||
let Ok(check_result) = handle.join() else {
|
||||
bail!(
|
||||
"Panic while trying to run the solution of the exericse {}",
|
||||
"Panic while trying to run the solution of the exercise {}",
|
||||
exercise_info.name,
|
||||
);
|
||||
};
|
||||
@ -322,7 +354,11 @@ fn check_solutions(
|
||||
}
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
let handle = thread::spawn(move || check_unexpected_files("solutions", &sol_paths));
|
||||
let handle = thread::Builder::new()
|
||||
.spawn(move || check_unexpected_files("solutions", &sol_paths))
|
||||
.context(
|
||||
"Failed to spawn a thread to check for unexpected files in the solutions directory",
|
||||
)?;
|
||||
|
||||
if !fmt_cmd
|
||||
.status()
|
||||
@ -338,8 +374,12 @@ fn check_solutions(
|
||||
pub fn check(require_solutions: bool) -> Result<()> {
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
if info_file.exercises.len() > MAX_N_EXERCISES {
|
||||
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
|
||||
}
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
// A hack to make `cargo run -- dev check` work when developing Rustlings.
|
||||
// A hack to make `cargo dev check` work when developing Rustlings.
|
||||
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
|
||||
} else {
|
||||
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
|
||||
|
@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::{
|
||||
env::set_current_dir,
|
||||
fs::{self, create_dir},
|
||||
@ -6,7 +6,7 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::CURRENT_FORMAT_VERSION;
|
||||
use crate::{CURRENT_FORMAT_VERSION, init::RUST_ANALYZER_TOML};
|
||||
|
||||
// Create a directory relative to the current directory and print its path.
|
||||
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
|
||||
@ -55,13 +55,17 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
|
||||
write_rel_file(
|
||||
"info.toml",
|
||||
&dir_path_str,
|
||||
format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"),
|
||||
format!(
|
||||
"{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"
|
||||
),
|
||||
)?;
|
||||
|
||||
write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?;
|
||||
|
||||
write_rel_file("README.md", &dir_path_str, README)?;
|
||||
|
||||
write_rel_file("rust-analyzer.toml", &dir_path_str, RUST_ANALYZER_TOML)?;
|
||||
|
||||
create_rel_dir(".vscode", &dir_path_str)?;
|
||||
write_rel_file(
|
||||
".vscode/extensions.json",
|
||||
@ -82,10 +86,10 @@ target/
|
||||
";
|
||||
|
||||
const INFO_FILE_BEFORE_FORMAT_VERSION: &str =
|
||||
"# The format version is an indicator of the compatibility of third-party exercises with the
|
||||
"# The format version is an indicator of the compatibility of community exercises with the
|
||||
# Rustlings program.
|
||||
# The format version is not the same as the version of the Rustlings program.
|
||||
# In case Rustlings makes an unavoidable breaking change to the expected format of third-party
|
||||
# In case Rustlings makes an unavoidable breaking change to the expected format of community
|
||||
# exercises, you would need to raise this version and adapt to the new format.
|
||||
# Otherwise, the newest version of the Rustlings program won't be able to run these exercises.
|
||||
format_version = ";
|
||||
@ -93,7 +97,7 @@ format_version = ";
|
||||
const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#"
|
||||
|
||||
# Optional multi-line message to be shown to users when just starting with the exercises.
|
||||
welcome_message = """Welcome to these third-party Rustlings exercises."""
|
||||
welcome_message = """Welcome to these community Rustlings exercises."""
|
||||
|
||||
# Optional multi-line message to be shown to users after finishing all exercises.
|
||||
final_message = """We hope that you found the exercises helpful :D"""
|
||||
@ -128,7 +132,7 @@ bin = []
|
||||
|
||||
[package]
|
||||
name = "exercises"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
# Don't publish the exercises on crates.io!
|
||||
publish = false
|
||||
|
||||
@ -137,7 +141,7 @@ publish = false
|
||||
|
||||
const README: &str = "# Rustlings 🦀
|
||||
|
||||
Welcome to these third-party Rustlings exercises 😃
|
||||
Welcome to these community Rustlings exercises 😃
|
||||
|
||||
First, [install Rustlings using the official instructions](https://github.com/rust-lang/rustlings) ✅
|
||||
|
||||
|
@ -28,7 +28,7 @@ pub fn update() -> Result<()> {
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
// A hack to make `cargo run -- dev update` work when developing Rustlings.
|
||||
// A hack to make `cargo dev update` work when developing Rustlings.
|
||||
update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")
|
||||
.context("Failed to update the file `dev/Cargo.toml`")?;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use anyhow::{Context, Error, Result};
|
||||
use std::{
|
||||
fs::{create_dir, OpenOptions},
|
||||
io::{self, Write},
|
||||
fs::{self, create_dir},
|
||||
io,
|
||||
};
|
||||
|
||||
use crate::info_file::ExerciseInfo;
|
||||
@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo;
|
||||
/// Contains all embedded files.
|
||||
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum WriteStrategy {
|
||||
IfNotExists,
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
impl WriteStrategy {
|
||||
fn write(self, path: &str, content: &[u8]) -> Result<()> {
|
||||
let file = match self {
|
||||
Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path),
|
||||
Self::Overwrite => OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path),
|
||||
};
|
||||
|
||||
file.with_context(|| format!("Failed to open the file `{path}` in write mode"))?
|
||||
.write_all(content)
|
||||
.with_context(|| format!("Failed to write the file {path}"))
|
||||
}
|
||||
}
|
||||
|
||||
// Files related to one exercise.
|
||||
struct ExerciseFiles {
|
||||
// The content of the exercise file.
|
||||
@ -42,6 +19,16 @@ struct ExerciseFiles {
|
||||
dir_ind: usize,
|
||||
}
|
||||
|
||||
fn create_dir_if_not_exists(path: &str) -> Result<()> {
|
||||
if let Err(e) = create_dir(path) {
|
||||
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||
return Err(Error::from(e).context(format!("Failed to create the directory {path}")));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// A directory in the `exercises/` directory.
|
||||
pub struct ExerciseDir {
|
||||
pub name: &'static str,
|
||||
@ -55,21 +42,13 @@ impl ExerciseDir {
|
||||
let mut dir_path = String::with_capacity(20 + self.name.len());
|
||||
dir_path.push_str("exercises/");
|
||||
dir_path.push_str(self.name);
|
||||
|
||||
if let Err(e) = create_dir(&dir_path) {
|
||||
if e.kind() == io::ErrorKind::AlreadyExists {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(
|
||||
Error::from(e).context(format!("Failed to create the directory {dir_path}"))
|
||||
);
|
||||
}
|
||||
create_dir_if_not_exists(&dir_path)?;
|
||||
|
||||
let mut readme_path = dir_path;
|
||||
readme_path.push_str("/README.md");
|
||||
|
||||
WriteStrategy::Overwrite.write(&readme_path, self.readme)
|
||||
fs::write(&readme_path, self.readme)
|
||||
.with_context(|| format!("Failed to write the file {readme_path}"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,17 +65,31 @@ impl EmbeddedFiles {
|
||||
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
||||
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
||||
|
||||
WriteStrategy::IfNotExists.write(
|
||||
fs::write(
|
||||
"exercises/README.md",
|
||||
include_bytes!("../exercises/README.md"),
|
||||
)?;
|
||||
)
|
||||
.context("Failed to write the file exercises/README.md")?;
|
||||
|
||||
for dir in self.exercise_dirs {
|
||||
dir.init_on_disk()?;
|
||||
}
|
||||
|
||||
let mut exercise_path = String::with_capacity(64);
|
||||
let prefix = "exercises/";
|
||||
exercise_path.push_str(prefix);
|
||||
|
||||
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
|
||||
WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?;
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
exercise_path.truncate(prefix.len());
|
||||
exercise_path.push_str(dir.name);
|
||||
exercise_path.push('/');
|
||||
exercise_path.push_str(&exercise_info.name);
|
||||
exercise_path.push_str(".rs");
|
||||
|
||||
fs::write(&exercise_path, exercise_files.exercise)
|
||||
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -107,7 +100,8 @@ impl EmbeddedFiles {
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
dir.init_on_disk()?;
|
||||
WriteStrategy::Overwrite.write(path, exercise_files.exercise)
|
||||
fs::write(path, exercise_files.exercise)
|
||||
.with_context(|| format!("Failed to write the exercise file {path}"))
|
||||
}
|
||||
|
||||
/// Write the solution file to disk and return its path.
|
||||
@ -116,19 +110,25 @@ impl EmbeddedFiles {
|
||||
exercise_ind: usize,
|
||||
exercise_name: &str,
|
||||
) -> Result<String> {
|
||||
create_dir_if_not_exists("solutions")?;
|
||||
|
||||
let exercise_files = &self.exercise_files[exercise_ind];
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
// 14 = 10 + 1 + 3
|
||||
// solutions/ + / + .rs
|
||||
let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||
solution_path.push_str("solutions/");
|
||||
solution_path.push_str(dir.name);
|
||||
let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||
dir_path.push_str("solutions/");
|
||||
dir_path.push_str(dir.name);
|
||||
create_dir_if_not_exists(&dir_path)?;
|
||||
|
||||
let mut solution_path = dir_path;
|
||||
solution_path.push('/');
|
||||
solution_path.push_str(exercise_name);
|
||||
solution_path.push_str(".rs");
|
||||
|
||||
WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?;
|
||||
fs::write(&solution_path, exercise_files.solution)
|
||||
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
|
||||
|
||||
Ok(solution_path)
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
QueueableCommand,
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
use crate::{
|
||||
cmd::CmdRunner,
|
||||
term::{terminal_file_link, write_ansi},
|
||||
term::{self, CountedWrite, terminal_file_link, write_ansi},
|
||||
};
|
||||
|
||||
/// The initial capacity of the output buffer.
|
||||
@ -18,7 +18,11 @@ pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::R
|
||||
stdout.write_all(b"Solution")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b" for comparison: ")?;
|
||||
terminal_file_link(stdout, solution_path, Color::Cyan)?;
|
||||
if let Some(canonical_path) = term::canonicalize(solution_path) {
|
||||
terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?;
|
||||
} else {
|
||||
stdout.write_all(solution_path.as_bytes())?;
|
||||
}
|
||||
stdout.write_all(b"\n")
|
||||
}
|
||||
|
||||
@ -60,12 +64,23 @@ pub struct Exercise {
|
||||
pub name: &'static str,
|
||||
/// Path of the exercise file starting with the `exercises/` directory.
|
||||
pub path: &'static str,
|
||||
pub canonical_path: Option<String>,
|
||||
pub test: bool,
|
||||
pub strict_clippy: bool,
|
||||
pub hint: &'static str,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
impl Exercise {
|
||||
pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> {
|
||||
if let Some(canonical_path) = self.canonical_path.as_deref() {
|
||||
return terminal_file_link(writer, self.path, canonical_path, Color::Blue);
|
||||
}
|
||||
|
||||
writer.write_str(self.path)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RunnableExercise {
|
||||
fn name(&self) -> &str;
|
||||
fn dir(&self) -> Option<&str>;
|
||||
@ -116,7 +131,7 @@ pub trait RunnableExercise {
|
||||
|
||||
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
|
||||
|
||||
// `--profile test` is required to also check code with `[cfg(test)]`.
|
||||
// `--profile test` is required to also check code with `#[cfg(test)]`.
|
||||
if FORCE_STRICT_CLIPPY || self.strict_clippy() {
|
||||
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
|
||||
} else {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Context, Error, Result};
|
||||
use anyhow::{Context, Error, Result, bail};
|
||||
use serde::Deserialize;
|
||||
use std::{fs, io::ErrorKind};
|
||||
|
||||
@ -79,7 +79,7 @@ impl RunnableExercise for ExerciseInfo {
|
||||
/// The deserialized `info.toml` file.
|
||||
#[derive(Deserialize)]
|
||||
pub struct InfoFile {
|
||||
/// For possible breaking changes in the future for third-party exercises.
|
||||
/// For possible breaking changes in the future for community exercises.
|
||||
pub format_version: u8,
|
||||
/// Shown to users when starting with the exercises.
|
||||
pub welcome_message: Option<String>,
|
||||
@ -91,7 +91,7 @@ pub struct InfoFile {
|
||||
|
||||
impl InfoFile {
|
||||
/// Official exercises: Parse the embedded `info.toml` file.
|
||||
/// Third-party exercises: Parse the `info.toml` file in the current directory.
|
||||
/// Community exercises: Parse the `info.toml` file in the current directory.
|
||||
pub fn parse() -> Result<Self> {
|
||||
// Read a local `info.toml` if it exists.
|
||||
let slf = match fs::read_to_string("info.toml") {
|
||||
|
20
src/init.rs
20
src/init.rs
@ -1,7 +1,7 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use crossterm::{
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
QueueableCommand,
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@ -57,7 +57,9 @@ pub fn init() -> Result<()> {
|
||||
if !workspace_manifest_content.contains("[workspace]\n")
|
||||
&& !workspace_manifest_content.contains("workspace.")
|
||||
{
|
||||
bail!("The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory");
|
||||
bail!(
|
||||
"The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory"
|
||||
);
|
||||
}
|
||||
|
||||
stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\nPress ENTER to continue ")?;
|
||||
@ -75,7 +77,9 @@ pub fn init() -> Result<()> {
|
||||
.stdout(Stdio::null())
|
||||
.status()?;
|
||||
if !status.success() {
|
||||
bail!("Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory");
|
||||
bail!(
|
||||
"Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory"
|
||||
);
|
||||
}
|
||||
|
||||
stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the `Cargo.toml` file of this Cargo workspace.\n")?;
|
||||
@ -130,6 +134,9 @@ pub fn init() -> Result<()> {
|
||||
fs::write("Cargo.toml", updated_cargo_toml)
|
||||
.context("Failed to create the file `rustlings/Cargo.toml`")?;
|
||||
|
||||
fs::write("rust-analyzer.toml", RUST_ANALYZER_TOML)
|
||||
.context("Failed to create the file `rustlings/rust-analyzer.toml`")?;
|
||||
|
||||
fs::write(".gitignore", GITIGNORE)
|
||||
.context("Failed to create the file `rustlings/.gitignore`")?;
|
||||
|
||||
@ -169,6 +176,11 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
|
||||
}
|
||||
";
|
||||
|
||||
pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy"
|
||||
check.extraArgs = ["--profile", "test"]
|
||||
cargo.targetDir = true
|
||||
"#;
|
||||
|
||||
const GITIGNORE: &[u8] = b"Cargo.lock
|
||||
target/
|
||||
.vscode/
|
||||
|
47
src/list.rs
47
src/list.rs
@ -1,14 +1,13 @@
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
cursor,
|
||||
QueueableCommand, cursor,
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
|
||||
},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
|
||||
LeaveAlternateScreen,
|
||||
DisableLineWrap, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
disable_raw_mode, enable_raw_mode,
|
||||
},
|
||||
QueueableCommand,
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
@ -20,7 +19,8 @@ mod scroll_state;
|
||||
mod state;
|
||||
|
||||
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
|
||||
let mut list_state = ListState::new(app_state, stdout)?;
|
||||
let mut list_state = ListState::build(app_state, stdout)?;
|
||||
let mut is_searching = false;
|
||||
|
||||
loop {
|
||||
match event::read().context("Failed to read terminal event")? {
|
||||
@ -32,6 +32,27 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
|
||||
|
||||
list_state.message.clear();
|
||||
|
||||
if is_searching {
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Enter => {
|
||||
is_searching = false;
|
||||
list_state.search_query.clear();
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
list_state.search_query.push(c);
|
||||
list_state.apply_search_query();
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
list_state.search_query.pop();
|
||||
list_state.apply_search_query();
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
list_state.draw(stdout)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
|
||||
@ -50,15 +71,15 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
|
||||
}
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let message = if list_state.filter() == Filter::Pending {
|
||||
if list_state.filter() == Filter::Pending {
|
||||
list_state.set_filter(Filter::None);
|
||||
"Disabled filter PENDING"
|
||||
list_state.message.push_str("Disabled filter PENDING");
|
||||
} else {
|
||||
list_state.set_filter(Filter::Pending);
|
||||
"Enabled filter PENDING │ Press p again to disable the filter"
|
||||
};
|
||||
|
||||
list_state.message.push_str(message);
|
||||
list_state.message.push_str(
|
||||
"Enabled filter PENDING │ Press p again to disable the filter",
|
||||
);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') => list_state.reset_selected()?,
|
||||
KeyCode::Char('c') => {
|
||||
@ -66,6 +87,10 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s' | '/') => {
|
||||
is_searching = true;
|
||||
list_state.apply_search_query();
|
||||
}
|
||||
// Redraw to remove the message.
|
||||
KeyCode::Esc => (),
|
||||
_ => continue,
|
||||
|
@ -46,7 +46,7 @@ impl ScrollState {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn set_selected(&mut self, selected: usize) {
|
||||
pub fn set_selected(&mut self, selected: usize) {
|
||||
self.selected = Some(selected);
|
||||
self.update_offset();
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
cursor::{MoveTo, MoveToNextLine},
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor},
|
||||
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
|
||||
QueueableCommand,
|
||||
cursor::{MoveTo, MoveToNextLine},
|
||||
style::{
|
||||
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
|
||||
};
|
||||
use std::{
|
||||
fmt::Write as _,
|
||||
@ -13,12 +15,15 @@ use std::{
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
exercise::Exercise,
|
||||
term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter},
|
||||
term::{CountedWrite, MaxLenWriter, progress_bar},
|
||||
};
|
||||
|
||||
use super::scroll_state::ScrollState;
|
||||
|
||||
const COL_SPACING: usize = 2;
|
||||
const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none()
|
||||
.with(Attribute::Reverse)
|
||||
.with(Attribute::Bold);
|
||||
|
||||
fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout
|
||||
@ -37,48 +42,53 @@ pub enum Filter {
|
||||
pub struct ListState<'a> {
|
||||
/// Footer message to be displayed if not empty.
|
||||
pub message: String,
|
||||
pub search_query: String,
|
||||
app_state: &'a mut AppState,
|
||||
scroll_state: ScrollState,
|
||||
name_col_padding: Vec<u8>,
|
||||
path_col_padding: Vec<u8>,
|
||||
filter: Filter,
|
||||
term_width: u16,
|
||||
term_height: u16,
|
||||
separator_line: Vec<u8>,
|
||||
narrow_term: bool,
|
||||
show_footer: bool,
|
||||
}
|
||||
|
||||
impl<'a> ListState<'a> {
|
||||
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> {
|
||||
pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
|
||||
stdout.queue(Clear(ClearType::All))?;
|
||||
|
||||
let name_col_title_len = 4;
|
||||
let name_col_width = app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.map(|exercise| exercise.name.len())
|
||||
.max()
|
||||
.map_or(name_col_title_len, |max| max.max(name_col_title_len));
|
||||
let path_col_title_len = 4;
|
||||
let (name_col_width, path_col_width) = app_state.exercises().iter().fold(
|
||||
(name_col_title_len, path_col_title_len),
|
||||
|(name_col_width, path_col_width), exercise| {
|
||||
(
|
||||
name_col_width.max(exercise.name.len()),
|
||||
path_col_width.max(exercise.path.len()),
|
||||
)
|
||||
},
|
||||
);
|
||||
let name_col_padding = vec![b' '; name_col_width + COL_SPACING];
|
||||
let path_col_padding = vec![b' '; path_col_width];
|
||||
|
||||
let filter = Filter::None;
|
||||
let n_rows_with_filter = app_state.exercises().len();
|
||||
let selected = app_state.current_exercise_ind();
|
||||
|
||||
let (width, height) = terminal::size()?;
|
||||
let (width, height) = terminal::size().context("Failed to get the terminal size")?;
|
||||
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
|
||||
|
||||
let mut slf = Self {
|
||||
message: String::with_capacity(128),
|
||||
search_query: String::new(),
|
||||
app_state,
|
||||
scroll_state,
|
||||
name_col_padding,
|
||||
path_col_padding,
|
||||
filter,
|
||||
// Set by `set_term_size`
|
||||
term_width: 0,
|
||||
term_height: 0,
|
||||
separator_line: Vec::new(),
|
||||
narrow_term: false,
|
||||
show_footer: true,
|
||||
};
|
||||
|
||||
@ -96,25 +106,39 @@ impl<'a> ListState<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
let wide_help_footer_width = 95;
|
||||
// The help footer is shorter when nothing is selected.
|
||||
self.narrow_term = width < wide_help_footer_width && self.scroll_state.selected().is_some();
|
||||
|
||||
let header_height = 1;
|
||||
// 2 separator, 1 progress bar, 1-2 footer message.
|
||||
let footer_height = 4 + u16::from(self.narrow_term);
|
||||
// 1 progress bar, 2 footer message lines.
|
||||
let footer_height = 3;
|
||||
self.show_footer = height > header_height + footer_height;
|
||||
|
||||
if self.show_footer {
|
||||
self.separator_line = "─".as_bytes().repeat(width as usize);
|
||||
}
|
||||
|
||||
self.scroll_state.set_max_n_rows_to_display(
|
||||
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
|
||||
as usize,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_exercise_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> {
|
||||
if !self.search_query.is_empty() {
|
||||
if let Some((pre_highlight, highlight, post_highlight)) = exercise
|
||||
.name
|
||||
.find(&self.search_query)
|
||||
.and_then(|ind| exercise.name.split_at_checked(ind))
|
||||
.and_then(|(pre_highlight, rest)| {
|
||||
rest.split_at_checked(self.search_query.len())
|
||||
.map(|x| (pre_highlight, x.0, x.1))
|
||||
})
|
||||
{
|
||||
writer.write_str(pre_highlight)?;
|
||||
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
|
||||
writer.write_str(highlight)?;
|
||||
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||
return writer.write_str(post_highlight);
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_str(exercise.name)
|
||||
}
|
||||
|
||||
fn draw_rows(
|
||||
&self,
|
||||
stdout: &mut StdoutLock,
|
||||
@ -131,14 +155,12 @@ impl<'a> ListState<'a> {
|
||||
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||
|
||||
if self.scroll_state.selected() == Some(row_offset + n_displayed_rows) {
|
||||
writer.stdout.queue(SetBackgroundColor(Color::Rgb {
|
||||
r: 40,
|
||||
g: 40,
|
||||
b: 40,
|
||||
}))?;
|
||||
// The crab emoji has the width of two ascii chars.
|
||||
writer.add_to_len(2);
|
||||
writer.stdout.write_all("🦀".as_bytes())?;
|
||||
writer
|
||||
.stdout
|
||||
.queue(SetAttributes(SELECTED_ROW_ATTRIBUTES))?;
|
||||
} else {
|
||||
writer.write_ascii(b" ")?;
|
||||
}
|
||||
@ -152,15 +174,16 @@ impl<'a> ListState<'a> {
|
||||
|
||||
if exercise.done {
|
||||
writer.stdout.queue(SetForegroundColor(Color::Green))?;
|
||||
writer.write_ascii(b"DONE ")?;
|
||||
writer.write_ascii(b"DONE ")?;
|
||||
} else {
|
||||
writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
|
||||
writer.write_ascii(b"PENDING ")?;
|
||||
writer.write_ascii(b"PENDING")?;
|
||||
}
|
||||
|
||||
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||
writer.write_ascii(b" ")?;
|
||||
|
||||
self.draw_exercise_name(&mut writer, exercise)?;
|
||||
|
||||
writer.write_str(exercise.name)?;
|
||||
writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?;
|
||||
|
||||
// The list links aren't shown correctly in VS Code on Windows.
|
||||
@ -168,9 +191,11 @@ impl<'a> ListState<'a> {
|
||||
if self.app_state.vs_code() {
|
||||
writer.write_str(exercise.path)?;
|
||||
} else {
|
||||
terminal_file_link(&mut writer, exercise.path, Color::Blue)?;
|
||||
exercise.terminal_file_link(&mut writer)?;
|
||||
}
|
||||
|
||||
writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?;
|
||||
|
||||
next_ln(stdout)?;
|
||||
stdout.queue(ResetColor)?;
|
||||
n_displayed_rows += 1;
|
||||
@ -208,9 +233,6 @@ impl<'a> ListState<'a> {
|
||||
}
|
||||
|
||||
if self.show_footer {
|
||||
stdout.write_all(&self.separator_line)?;
|
||||
next_ln(stdout)?;
|
||||
|
||||
progress_bar(
|
||||
&mut MaxLenWriter::new(stdout, self.term_width as usize),
|
||||
self.app_state.n_done(),
|
||||
@ -219,22 +241,15 @@ impl<'a> ListState<'a> {
|
||||
)?;
|
||||
next_ln(stdout)?;
|
||||
|
||||
stdout.write_all(&self.separator_line)?;
|
||||
next_ln(stdout)?;
|
||||
|
||||
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||
if self.message.is_empty() {
|
||||
// Help footer message
|
||||
if self.scroll_state.selected().is_some() {
|
||||
writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
|
||||
if self.narrow_term {
|
||||
next_ln(stdout)?;
|
||||
writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||
next_ln(stdout)?;
|
||||
writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||
|
||||
writer.write_ascii(b"filter ")?;
|
||||
} else {
|
||||
writer.write_ascii(b" | filter ")?;
|
||||
}
|
||||
writer.write_ascii(b"<s>earch | filter ")?;
|
||||
} else {
|
||||
// Nothing selected (and nothing shown), so only display filter and quit.
|
||||
writer.write_ascii(b"filter ")?;
|
||||
@ -263,17 +278,14 @@ impl<'a> ListState<'a> {
|
||||
}
|
||||
|
||||
writer.write_ascii(b" | <q>uit list")?;
|
||||
next_ln(stdout)?;
|
||||
} else {
|
||||
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
|
||||
writer.write_str(&self.message)?;
|
||||
stdout.queue(ResetColor)?;
|
||||
next_ln(stdout)?;
|
||||
|
||||
if self.narrow_term {
|
||||
next_ln(stdout)?;
|
||||
}
|
||||
}
|
||||
|
||||
next_ln(stdout)?;
|
||||
}
|
||||
|
||||
stdout.queue(EndSynchronizedUpdate)?.flush()
|
||||
@ -370,6 +382,33 @@ impl<'a> ListState<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_search_query(&mut self) {
|
||||
self.message.push_str("search:");
|
||||
self.message.push_str(&self.search_query);
|
||||
self.message.push('|');
|
||||
|
||||
if self.search_query.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_search_result = |exercise: &Exercise| exercise.name.contains(&self.search_query);
|
||||
let mut iter = self.app_state.exercises().iter();
|
||||
let ind = match self.filter {
|
||||
Filter::None => iter.position(is_search_result),
|
||||
Filter::Done => iter
|
||||
.filter(|exercise| exercise.done)
|
||||
.position(is_search_result),
|
||||
Filter::Pending => iter
|
||||
.filter(|exercise| !exercise.done)
|
||||
.position(is_search_result),
|
||||
};
|
||||
|
||||
match ind {
|
||||
Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind),
|
||||
None => self.message.push_str(" (not found)"),
|
||||
}
|
||||
}
|
||||
|
||||
// Return `true` if there was something to select.
|
||||
pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
|
||||
let Some(selected) = self.scroll_state.selected() else {
|
||||
|
69
src/main.rs
69
src/main.rs
@ -1,19 +1,18 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use app_state::StateFileStatus;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::{
|
||||
io::{self, IsTerminal, Write},
|
||||
path::Path,
|
||||
process::exit,
|
||||
process::ExitCode,
|
||||
};
|
||||
use term::{clear_terminal, press_enter_prompt};
|
||||
|
||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
|
||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile};
|
||||
|
||||
mod app_state;
|
||||
mod cargo_toml;
|
||||
mod cmd;
|
||||
mod collections;
|
||||
mod dev;
|
||||
mod embedded;
|
||||
mod exercise;
|
||||
@ -47,6 +46,8 @@ enum Subcommands {
|
||||
/// The name of the exercise
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Check all the exercises, marking them as done or pending accordingly.
|
||||
CheckAll,
|
||||
/// Reset a single exercise
|
||||
Reset {
|
||||
/// The name of the exercise
|
||||
@ -57,27 +58,31 @@ enum Subcommands {
|
||||
/// The name of the exercise
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Commands for developing (third-party) Rustlings exercises
|
||||
/// Commands for developing (community) Rustlings exercises
|
||||
#[command(subcommand)]
|
||||
Dev(DevCommands),
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
fn main() -> Result<ExitCode> {
|
||||
let args = Args::parse();
|
||||
|
||||
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
|
||||
bail!("{OLD_METHOD_ERR}");
|
||||
}
|
||||
|
||||
match args.command {
|
||||
Some(Subcommands::Init) => return init::init().context("Initialization failed"),
|
||||
Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
|
||||
_ => (),
|
||||
'priority_cmd: {
|
||||
match args.command {
|
||||
Some(Subcommands::Init) => init::init().context("Initialization failed")?,
|
||||
Some(Subcommands::Dev(dev_command)) => dev_command.run()?,
|
||||
_ => break 'priority_cmd,
|
||||
}
|
||||
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
if !Path::new("exercises").is_dir() {
|
||||
println!("{PRE_INIT_MSG}");
|
||||
exit(1);
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
let info_file = InfoFile::parse()?;
|
||||
@ -130,21 +135,41 @@ fn main() -> Result<()> {
|
||||
)
|
||||
};
|
||||
|
||||
loop {
|
||||
match watch::watch(&mut app_state, notify_exercise_names)? {
|
||||
WatchExit::Shutdown => break,
|
||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||
// the watch mode instead of trying to pause the watch threads and correct the
|
||||
// watch state.
|
||||
WatchExit::List => list::list(&mut app_state)?,
|
||||
}
|
||||
}
|
||||
watch::watch(&mut app_state, notify_exercise_names)?;
|
||||
}
|
||||
Some(Subcommands::Run { name }) => {
|
||||
if let Some(name) = name {
|
||||
app_state.set_current_exercise_by_name(&name)?;
|
||||
}
|
||||
run::run(&mut app_state)?;
|
||||
return run::run(&mut app_state);
|
||||
}
|
||||
Some(Subcommands::CheckAll) => {
|
||||
let mut stdout = io::stdout().lock();
|
||||
if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? {
|
||||
if app_state.current_exercise().done {
|
||||
app_state.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||
}
|
||||
|
||||
stdout.write_all(b"\n\n")?;
|
||||
let pending = app_state.n_pending();
|
||||
if pending == 1 {
|
||||
stdout.write_all(b"One exercise pending: ")?;
|
||||
} else {
|
||||
write!(
|
||||
stdout,
|
||||
"{pending}/{} exercises pending. The first: ",
|
||||
app_state.exercises().len(),
|
||||
)?;
|
||||
}
|
||||
app_state
|
||||
.current_exercise()
|
||||
.terminal_file_link(&mut stdout)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
return Ok(ExitCode::FAILURE);
|
||||
} else {
|
||||
app_state.render_final_message(&mut stdout)?;
|
||||
}
|
||||
}
|
||||
Some(Subcommands::Reset { name }) => {
|
||||
app_state.set_current_exercise_by_name(&name)?;
|
||||
@ -161,7 +186,7 @@ fn main() -> Result<()> {
|
||||
Some(Subcommands::Init | Subcommands::Dev(_)) => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
const OLD_METHOD_ERR: &str =
|
||||
|
26
src/run.rs
26
src/run.rs
@ -1,20 +1,19 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
style::{Color, ResetColor, SetForegroundColor},
|
||||
QueueableCommand,
|
||||
style::{Color, ResetColor, SetForegroundColor},
|
||||
};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
process::exit,
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
|
||||
term::terminal_file_link,
|
||||
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line},
|
||||
};
|
||||
|
||||
pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||
pub fn run(app_state: &mut AppState) -> Result<ExitCode> {
|
||||
let exercise = app_state.current_exercise();
|
||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
|
||||
@ -26,9 +25,12 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||
app_state.set_pending(app_state.current_exercise_ind())?;
|
||||
|
||||
stdout.write_all(b"Ran ")?;
|
||||
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
|
||||
app_state
|
||||
.current_exercise()
|
||||
.terminal_file_link(&mut stdout)?;
|
||||
stdout.write_all(b" with errors\n")?;
|
||||
exit(1);
|
||||
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||
@ -43,14 +45,16 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
|
||||
match app_state.done_current_exercise(&mut stdout)? {
|
||||
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => {
|
||||
match app_state.done_current_exercise::<false>(&mut stdout)? {
|
||||
ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => {
|
||||
stdout.write_all(b"Next exercise: ")?;
|
||||
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
|
||||
app_state
|
||||
.current_exercise()
|
||||
.terminal_file_link(&mut stdout)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
ExercisesProgress::AllDone => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
153
src/term.rs
153
src/term.rs
@ -1,24 +1,25 @@
|
||||
use crossterm::{
|
||||
Command, QueueableCommand,
|
||||
cursor::MoveTo,
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
terminal::{Clear, ClearType},
|
||||
};
|
||||
use std::{
|
||||
fmt, fs,
|
||||
io::{self, BufRead, StdoutLock, Write},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
cursor::MoveTo,
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
terminal::{Clear, ClearType},
|
||||
Command, QueueableCommand,
|
||||
};
|
||||
use crate::app_state::CheckProgress;
|
||||
|
||||
pub struct MaxLenWriter<'a, 'b> {
|
||||
pub stdout: &'a mut StdoutLock<'b>,
|
||||
pub struct MaxLenWriter<'a, 'lock> {
|
||||
pub stdout: &'a mut StdoutLock<'lock>,
|
||||
len: usize,
|
||||
max_len: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'b> MaxLenWriter<'a, 'b> {
|
||||
impl<'a, 'lock> MaxLenWriter<'a, 'lock> {
|
||||
#[inline]
|
||||
pub fn new(stdout: &'a mut StdoutLock<'b>, max_len: usize) -> Self {
|
||||
pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self {
|
||||
Self {
|
||||
stdout,
|
||||
len: 0,
|
||||
@ -33,13 +34,13 @@ impl<'a, 'b> MaxLenWriter<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CountedWrite<'a> {
|
||||
pub trait CountedWrite<'lock> {
|
||||
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>;
|
||||
fn write_str(&mut self, unicode: &str) -> io::Result<()>;
|
||||
fn stdout(&mut self) -> &mut StdoutLock<'a>;
|
||||
fn stdout(&mut self) -> &mut StdoutLock<'lock>;
|
||||
}
|
||||
|
||||
impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> {
|
||||
impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> {
|
||||
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
|
||||
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
|
||||
if n > 0 {
|
||||
@ -64,7 +65,7 @@ impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn stdout(&mut self) -> &mut StdoutLock<'b> {
|
||||
fn stdout(&mut self) -> &mut StdoutLock<'lock> {
|
||||
self.stdout
|
||||
}
|
||||
}
|
||||
@ -86,33 +87,104 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Terminal progress bar to be used when not using Ratataui.
|
||||
pub struct CheckProgressVisualizer<'a, 'lock> {
|
||||
stdout: &'a mut StdoutLock<'lock>,
|
||||
n_cols: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> {
|
||||
const CHECKING_COLOR: Color = Color::Blue;
|
||||
const DONE_COLOR: Color = Color::Green;
|
||||
const PENDING_COLOR: Color = Color::Red;
|
||||
|
||||
pub fn build(stdout: &'a mut StdoutLock<'lock>, term_width: u16) -> io::Result<Self> {
|
||||
clear_terminal(stdout)?;
|
||||
stdout.write_all("Checking all exercises…\n".as_bytes())?;
|
||||
|
||||
// Legend
|
||||
stdout.write_all(b"Color of exercise number: ")?;
|
||||
stdout.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
|
||||
stdout.write_all(b"Checking")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b" - ")?;
|
||||
stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
|
||||
stdout.write_all(b"Done")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b" - ")?;
|
||||
stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
|
||||
stdout.write_all(b"Pending")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
// Exercise numbers with up to 3 digits.
|
||||
// +1 because the last column doesn't end with a whitespace.
|
||||
let n_cols = usize::from(term_width + 1) / 4;
|
||||
|
||||
Ok(Self { stdout, n_cols })
|
||||
}
|
||||
|
||||
pub fn update(&mut self, progresses: &[CheckProgress]) -> io::Result<()> {
|
||||
self.stdout.queue(MoveTo(0, 2))?;
|
||||
|
||||
let mut exercise_num = 1;
|
||||
for exercise_progress in progresses {
|
||||
match exercise_progress {
|
||||
CheckProgress::None => (),
|
||||
CheckProgress::Checking => {
|
||||
self.stdout
|
||||
.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
|
||||
}
|
||||
CheckProgress::Done => {
|
||||
self.stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
|
||||
}
|
||||
CheckProgress::Pending => {
|
||||
self.stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(self.stdout, "{exercise_num:<3}")?;
|
||||
self.stdout.queue(ResetColor)?;
|
||||
|
||||
if exercise_num != progresses.len() {
|
||||
if exercise_num % self.n_cols == 0 {
|
||||
self.stdout.write_all(b"\n")?;
|
||||
} else {
|
||||
self.stdout.write_all(b" ")?;
|
||||
}
|
||||
|
||||
exercise_num += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress_bar<'a>(
|
||||
writer: &mut impl CountedWrite<'a>,
|
||||
progress: u16,
|
||||
total: u16,
|
||||
line_width: u16,
|
||||
term_width: u16,
|
||||
) -> io::Result<()> {
|
||||
debug_assert!(total <= 999);
|
||||
debug_assert!(progress <= total);
|
||||
|
||||
const PREFIX: &[u8] = b"Progress: [";
|
||||
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
|
||||
// Leaving the last char empty (_) for `total` > 99.
|
||||
const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
|
||||
const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16;
|
||||
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
|
||||
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
||||
|
||||
if line_width < MIN_LINE_WIDTH {
|
||||
if term_width < MIN_LINE_WIDTH {
|
||||
writer.write_ascii(b"Progress: ")?;
|
||||
// Integers are in ASCII.
|
||||
writer.write_ascii(format!("{progress}/{total}").as_bytes())?;
|
||||
return writer.write_ascii(b" exercises");
|
||||
return writer.write_ascii(format!("{progress}/{total}").as_bytes());
|
||||
}
|
||||
|
||||
let stdout = writer.stdout();
|
||||
stdout.write_all(PREFIX)?;
|
||||
|
||||
let width = line_width - WRAPPER_WIDTH;
|
||||
let width = term_width - WRAPPER_WIDTH;
|
||||
let filled = (width * progress) / total;
|
||||
|
||||
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||
@ -133,8 +205,9 @@ pub fn progress_bar<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
stdout.queue(ResetColor)?;
|
||||
write!(stdout, "] {progress:>3}/{total} exercises")
|
||||
stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||
|
||||
write!(stdout, "] {progress:>3}/{total}")
|
||||
}
|
||||
|
||||
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
@ -151,25 +224,29 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout.write_all(b"\n")
|
||||
}
|
||||
|
||||
/// Canonicalize, convert to string and remove verbatim part on Windows.
|
||||
pub fn canonicalize(path: &str) -> Option<String> {
|
||||
fs::canonicalize(path)
|
||||
.ok()?
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.ok()
|
||||
.map(|mut path| {
|
||||
// Windows itself can't handle its verbatim paths.
|
||||
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
|
||||
path.drain(..4);
|
||||
}
|
||||
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
pub fn terminal_file_link<'a>(
|
||||
writer: &mut impl CountedWrite<'a>,
|
||||
path: &str,
|
||||
canonical_path: &str,
|
||||
color: Color,
|
||||
) -> io::Result<()> {
|
||||
let canonical_path = fs::canonicalize(path).ok();
|
||||
|
||||
let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else {
|
||||
return writer.write_str(path);
|
||||
};
|
||||
|
||||
// Windows itself can't handle its verbatim paths.
|
||||
#[cfg(windows)]
|
||||
let canonical_path = if canonical_path.len() > 5 && &canonical_path[0..4] == r"\\?\" {
|
||||
&canonical_path[4..]
|
||||
} else {
|
||||
canonical_path
|
||||
};
|
||||
|
||||
writer
|
||||
.stdout()
|
||||
.queue(SetForegroundColor(color))?
|
||||
|
150
src/watch.rs
150
src/watch.rs
@ -1,106 +1,129 @@
|
||||
use anyhow::{Error, Result};
|
||||
use notify_debouncer_mini::{
|
||||
new_debouncer,
|
||||
notify::{self, RecursiveMode},
|
||||
};
|
||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
sync::mpsc::channel,
|
||||
thread,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::Relaxed},
|
||||
mpsc::channel,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::app_state::{AppState, ExercisesProgress};
|
||||
|
||||
use self::{
|
||||
notify_event::NotifyEventHandler,
|
||||
state::WatchState,
|
||||
terminal_event::{terminal_event_handler, InputEvent},
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
list,
|
||||
};
|
||||
|
||||
use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent};
|
||||
|
||||
mod notify_event;
|
||||
mod state;
|
||||
mod terminal_event;
|
||||
|
||||
static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// Private unit type to force using the constructor function.
|
||||
#[must_use = "When the guard is dropped, the input is unpaused"]
|
||||
pub struct InputPauseGuard(());
|
||||
|
||||
impl InputPauseGuard {
|
||||
#[inline]
|
||||
pub fn scoped_pause() -> Self {
|
||||
EXERCISE_RUNNING.store(true, Relaxed);
|
||||
Self(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InputPauseGuard {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
EXERCISE_RUNNING.store(false, Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
enum WatchEvent {
|
||||
Input(InputEvent),
|
||||
FileChange { exercise_ind: usize },
|
||||
TerminalResize,
|
||||
TerminalResize { width: u16 },
|
||||
NotifyErr(notify::Error),
|
||||
TerminalEventErr(io::Error),
|
||||
}
|
||||
|
||||
/// Returned by the watch mode to indicate what to do afterwards.
|
||||
#[must_use]
|
||||
pub enum WatchExit {
|
||||
enum WatchExit {
|
||||
/// Exit the program.
|
||||
Shutdown,
|
||||
/// Enter the list mode and restart the watch mode afterwards.
|
||||
List,
|
||||
}
|
||||
|
||||
/// `notify_exercise_names` as None activates the manual run mode.
|
||||
pub fn watch(
|
||||
fn run_watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<WatchExit> {
|
||||
let (tx, rx) = channel();
|
||||
let (watch_event_sender, watch_event_receiver) = channel();
|
||||
|
||||
let mut manual_run = false;
|
||||
// Prevent dropping the guard until the end of the function.
|
||||
// Otherwise, the file watcher exits.
|
||||
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names {
|
||||
let mut debouncer = new_debouncer(
|
||||
Duration::from_millis(200),
|
||||
NotifyEventHandler {
|
||||
tx: tx.clone(),
|
||||
exercise_names,
|
||||
},
|
||||
let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
|
||||
let notify_event_handler =
|
||||
NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?;
|
||||
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
notify_event_handler,
|
||||
Config::default()
|
||||
.with_follow_symlinks(false)
|
||||
.with_poll_interval(Duration::from_secs(1)),
|
||||
)
|
||||
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
||||
debouncer
|
||||
.watcher()
|
||||
|
||||
watcher
|
||||
.watch(Path::new("exercises"), RecursiveMode::Recursive)
|
||||
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
||||
|
||||
Some(debouncer)
|
||||
Some(watcher)
|
||||
} else {
|
||||
manual_run = true;
|
||||
None
|
||||
};
|
||||
|
||||
let mut watch_state = WatchState::new(app_state, manual_run);
|
||||
|
||||
let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run)?;
|
||||
let mut stdout = io::stdout().lock();
|
||||
|
||||
watch_state.run_current_exercise(&mut stdout)?;
|
||||
|
||||
thread::spawn(move || terminal_event_handler(tx, manual_run));
|
||||
|
||||
while let Ok(event) = rx.recv() {
|
||||
while let Ok(event) = watch_event_receiver.recv() {
|
||||
match event {
|
||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
|
||||
ExercisesProgress::AllDone => break,
|
||||
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
|
||||
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||
ExercisesProgress::CurrentPending => (),
|
||||
},
|
||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::List) => {
|
||||
return Ok(WatchExit::List);
|
||||
}
|
||||
WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
|
||||
WatchEvent::Input(InputEvent::CheckAll) => match watch_state
|
||||
.check_all_exercises(&mut stdout)?
|
||||
{
|
||||
ExercisesProgress::AllDone => break,
|
||||
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
|
||||
},
|
||||
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::Quit) => {
|
||||
stdout.write_all(QUIT_MSG)?;
|
||||
break;
|
||||
}
|
||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render(&mut stdout)?,
|
||||
WatchEvent::FileChange { exercise_ind } => {
|
||||
watch_state.handle_file_change(exercise_ind, &mut stdout)?;
|
||||
}
|
||||
WatchEvent::TerminalResize => watch_state.render(&mut stdout)?,
|
||||
WatchEvent::NotifyErr(e) => {
|
||||
return Err(Error::from(e).context(NOTIFY_ERR));
|
||||
WatchEvent::TerminalResize { width } => {
|
||||
watch_state.update_term_width(width, &mut stdout)?;
|
||||
}
|
||||
WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)),
|
||||
WatchEvent::TerminalEventErr(e) => {
|
||||
return Err(Error::from(e).context("Terminal event listener failed"));
|
||||
}
|
||||
@ -110,9 +133,52 @@ pub fn watch(
|
||||
Ok(WatchExit::Shutdown)
|
||||
}
|
||||
|
||||
fn watch_list_loop(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
match run_watch(app_state, notify_exercise_names)? {
|
||||
WatchExit::Shutdown => break Ok(()),
|
||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||
// the watch mode instead of trying to pause the watch threads and correct the
|
||||
// watch state.
|
||||
WatchExit::List => list::list(app_state)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `notify_exercise_names` as None activates the manual run mode.
|
||||
pub fn watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<()> {
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let stdin_fd = rustix::stdio::stdin();
|
||||
let mut termios = rustix::termios::tcgetattr(stdin_fd)?;
|
||||
let original_local_modes = termios.local_modes;
|
||||
// Disable stdin line buffering and hide input.
|
||||
termios.local_modes -=
|
||||
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
|
||||
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||
|
||||
let res = watch_list_loop(app_state, notify_exercise_names);
|
||||
|
||||
termios.local_modes = original_local_modes;
|
||||
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
watch_list_loop(app_state, notify_exercise_names)
|
||||
}
|
||||
|
||||
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.
|
||||
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory.
|
||||
";
|
||||
|
||||
const NOTIFY_ERR: &str = "
|
||||
|
@ -1,52 +1,132 @@
|
||||
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
|
||||
use std::sync::mpsc::Sender;
|
||||
use anyhow::{Context, Result};
|
||||
use notify::{
|
||||
Event, EventKind,
|
||||
event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode},
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::Ordering::Relaxed,
|
||||
mpsc::{RecvTimeoutError, Sender, SyncSender, sync_channel},
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::WatchEvent;
|
||||
use super::{EXERCISE_RUNNING, WatchEvent};
|
||||
|
||||
const DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
|
||||
|
||||
pub struct NotifyEventHandler {
|
||||
pub tx: Sender<WatchEvent>,
|
||||
/// Used to report which exercise was modified.
|
||||
pub exercise_names: &'static [&'static [u8]],
|
||||
error_sender: Sender<WatchEvent>,
|
||||
// Sends the index of the updated exercise.
|
||||
update_sender: SyncSender<usize>,
|
||||
// Used to report which exercise was modified.
|
||||
exercise_names: &'static [&'static [u8]],
|
||||
}
|
||||
|
||||
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler {
|
||||
fn handle_event(&mut self, input_event: DebounceEventResult) {
|
||||
let output_event = match input_event {
|
||||
Ok(input_event) => {
|
||||
let Some(exercise_ind) = input_event
|
||||
.iter()
|
||||
.filter_map(|input_event| {
|
||||
if input_event.kind != DebouncedEventKind::Any {
|
||||
return None;
|
||||
impl NotifyEventHandler {
|
||||
pub fn build(
|
||||
watch_event_sender: Sender<WatchEvent>,
|
||||
exercise_names: &'static [&'static [u8]],
|
||||
) -> Result<Self> {
|
||||
let (update_sender, update_receiver) = sync_channel(0);
|
||||
let error_sender = watch_event_sender.clone();
|
||||
|
||||
// Debouncer
|
||||
thread::Builder::new()
|
||||
.spawn(move || {
|
||||
let mut exercise_updated = vec![false; exercise_names.len()];
|
||||
|
||||
loop {
|
||||
match update_receiver.recv_timeout(DEBOUNCE_DURATION) {
|
||||
Ok(exercise_ind) => exercise_updated[exercise_ind] = true,
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
for (exercise_ind, updated) in exercise_updated.iter_mut().enumerate() {
|
||||
if *updated {
|
||||
if watch_event_sender
|
||||
.send(WatchEvent::FileChange { exercise_ind })
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
*updated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
.context("Failed to spawn a thread to debounce file changes")?;
|
||||
|
||||
let file_name = input_event.path.file_name()?.to_str()?.as_bytes();
|
||||
|
||||
if file_name.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3);
|
||||
|
||||
if ext != b".rs" {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.exercise_names
|
||||
.iter()
|
||||
.position(|exercise_name| *exercise_name == file_name_without_ext)
|
||||
})
|
||||
.min()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
WatchEvent::FileChange { exercise_ind }
|
||||
}
|
||||
Err(e) => WatchEvent::NotifyErr(e),
|
||||
};
|
||||
|
||||
// An error occurs when the receiver is dropped.
|
||||
// After dropping the receiver, the debouncer guard should also be dropped.
|
||||
let _ = self.tx.send(output_event);
|
||||
Ok(Self {
|
||||
error_sender,
|
||||
update_sender,
|
||||
exercise_names,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl notify::EventHandler for NotifyEventHandler {
|
||||
fn handle_event(&mut self, input_event: notify::Result<Event>) {
|
||||
if EXERCISE_RUNNING.load(Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input_event = match input_event {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
// An error occurs when the receiver is dropped.
|
||||
// After dropping the receiver, the watcher guard should also be dropped.
|
||||
let _ = self.error_sender.send(WatchEvent::NotifyErr(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match input_event.kind {
|
||||
EventKind::Any => (),
|
||||
EventKind::Modify(modify_kind) => match modify_kind {
|
||||
ModifyKind::Any | ModifyKind::Data(_) => (),
|
||||
ModifyKind::Name(rename_mode) => match rename_mode {
|
||||
RenameMode::Any | RenameMode::To => (),
|
||||
RenameMode::From | RenameMode::Both | RenameMode::Other => return,
|
||||
},
|
||||
ModifyKind::Metadata(metadata_kind) => match metadata_kind {
|
||||
MetadataKind::Any | MetadataKind::WriteTime => (),
|
||||
MetadataKind::AccessTime
|
||||
| MetadataKind::Permissions
|
||||
| MetadataKind::Ownership
|
||||
| MetadataKind::Extended
|
||||
| MetadataKind::Other => return,
|
||||
},
|
||||
ModifyKind::Other => return,
|
||||
},
|
||||
EventKind::Access(access_kind) => match access_kind {
|
||||
AccessKind::Any => (),
|
||||
AccessKind::Close(access_mode) => match access_mode {
|
||||
AccessMode::Any | AccessMode::Write => (),
|
||||
AccessMode::Execute | AccessMode::Read | AccessMode::Other => return,
|
||||
},
|
||||
AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return,
|
||||
},
|
||||
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return,
|
||||
}
|
||||
|
||||
let _ = input_event
|
||||
.paths
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
let file_name = path.file_name()?.to_str()?.as_bytes();
|
||||
|
||||
let [file_name_without_ext @ .., b'.', b'r', b's'] = file_name else {
|
||||
return None;
|
||||
};
|
||||
|
||||
self.exercise_names
|
||||
.iter()
|
||||
.position(|exercise_name| *exercise_name == file_name_without_ext)
|
||||
})
|
||||
.try_for_each(|exercise_ind| self.update_sender.send(exercise_ind));
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,30 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
QueueableCommand,
|
||||
style::{
|
||||
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||
},
|
||||
terminal, QueueableCommand,
|
||||
terminal,
|
||||
};
|
||||
use std::{
|
||||
io::{self, Read, StdoutLock, Write},
|
||||
sync::mpsc::{Sender, SyncSender, sync_channel},
|
||||
thread,
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
clear_terminal,
|
||||
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
|
||||
term::{progress_bar, terminal_file_link},
|
||||
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line},
|
||||
term::progress_bar,
|
||||
};
|
||||
|
||||
use super::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler};
|
||||
|
||||
const HEADING_ATTRIBUTES: Attributes = Attributes::none()
|
||||
.with(Attribute::Bold)
|
||||
.with(Attribute::Underlined);
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum DoneStatus {
|
||||
DoneWithSolution(String),
|
||||
@ -27,20 +38,47 @@ pub struct WatchState<'a> {
|
||||
show_hint: bool,
|
||||
done_status: DoneStatus,
|
||||
manual_run: bool,
|
||||
term_width: u16,
|
||||
terminal_event_unpause_sender: SyncSender<()>,
|
||||
}
|
||||
|
||||
impl<'a> WatchState<'a> {
|
||||
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
|
||||
Self {
|
||||
pub fn build(
|
||||
app_state: &'a mut AppState,
|
||||
watch_event_sender: Sender<WatchEvent>,
|
||||
manual_run: bool,
|
||||
) -> Result<Self> {
|
||||
let term_width = terminal::size()
|
||||
.context("Failed to get the terminal size")?
|
||||
.0;
|
||||
|
||||
let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0);
|
||||
|
||||
thread::Builder::new()
|
||||
.spawn(move || {
|
||||
terminal_event_handler(
|
||||
watch_event_sender,
|
||||
terminal_event_unpause_receiver,
|
||||
manual_run,
|
||||
)
|
||||
})
|
||||
.context("Failed to spawn a thread to handle terminal events")?;
|
||||
|
||||
Ok(Self {
|
||||
app_state,
|
||||
output: Vec::with_capacity(OUTPUT_CAPACITY),
|
||||
show_hint: false,
|
||||
done_status: DoneStatus::Pending,
|
||||
manual_run,
|
||||
}
|
||||
term_width,
|
||||
terminal_event_unpause_sender,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
||||
// Ignore any input until running the exercise is done.
|
||||
let _input_pause_guard = InputPauseGuard::scoped_pause();
|
||||
|
||||
self.show_hint = false;
|
||||
|
||||
writeln!(
|
||||
@ -72,39 +110,67 @@ impl<'a> WatchState<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
||||
clear_terminal(stdout)?;
|
||||
|
||||
stdout.write_all(b"Resetting will undo all your changes to the file ")?;
|
||||
stdout.write_all(self.app_state.current_exercise().path.as_bytes())?;
|
||||
stdout.write_all(b"\nReset (y/n)? ")?;
|
||||
stdout.flush()?;
|
||||
|
||||
{
|
||||
let mut stdin = io::stdin().lock();
|
||||
let mut answer = [0];
|
||||
loop {
|
||||
stdin
|
||||
.read_exact(&mut answer)
|
||||
.context("Failed to read the user's input")?;
|
||||
|
||||
match answer[0] {
|
||||
b'y' | b'Y' => {
|
||||
self.app_state.reset_current_exercise()?;
|
||||
|
||||
// The file watcher reruns the exercise otherwise.
|
||||
if self.manual_run {
|
||||
self.run_current_exercise(stdout)?;
|
||||
}
|
||||
}
|
||||
b'n' | b'N' => self.render(stdout)?,
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.terminal_event_unpause_sender.send(())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_file_change(
|
||||
&mut self,
|
||||
exercise_ind: usize,
|
||||
stdout: &mut StdoutLock,
|
||||
) -> Result<()> {
|
||||
// Don't skip exercises on file changes to avoid confusion from missing exercises.
|
||||
// Skipping exercises must be explicit in the interactive list.
|
||||
// But going back to an earlier exercise on file change is fine.
|
||||
if self.app_state.current_exercise_ind() < exercise_ind {
|
||||
if self.app_state.current_exercise_ind() != exercise_ind {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||
self.run_current_exercise(stdout)
|
||||
}
|
||||
|
||||
/// Move on to the next exercise if the current one is done.
|
||||
pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||
if self.done_status == DoneStatus::Pending {
|
||||
return Ok(ExercisesProgress::CurrentPending);
|
||||
match self.done_status {
|
||||
DoneStatus::DoneWithSolution(_) | DoneStatus::DoneWithoutSolution => (),
|
||||
DoneStatus::Pending => return Ok(ExercisesProgress::CurrentPending),
|
||||
}
|
||||
|
||||
self.app_state.done_current_exercise(stdout)
|
||||
self.app_state.done_current_exercise::<true>(stdout)
|
||||
}
|
||||
|
||||
fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
if self.manual_run {
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(b"r")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b":run / ")?;
|
||||
}
|
||||
|
||||
if self.done_status != DoneStatus::Pending {
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(b"n")?;
|
||||
@ -116,22 +182,25 @@ impl<'a> WatchState<'a> {
|
||||
stdout.write_all(b" / ")?;
|
||||
}
|
||||
|
||||
if !self.show_hint {
|
||||
let mut show_key = |key, postfix| {
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(b"h")?;
|
||||
stdout.write_all(&[key])?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b":hint / ")?;
|
||||
stdout.write_all(postfix)
|
||||
};
|
||||
|
||||
if self.manual_run {
|
||||
show_key(b'r', b":run / ")?;
|
||||
}
|
||||
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(b"l")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b":list / ")?;
|
||||
if !self.show_hint {
|
||||
show_key(b'h', b":hint / ")?;
|
||||
}
|
||||
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(b"q")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b":quit ? ")?;
|
||||
show_key(b'l', b":list / ")?;
|
||||
show_key(b'c', b":check all / ")?;
|
||||
show_key(b'x', b":reset / ")?;
|
||||
show_key(b'q', b":quit ? ")?;
|
||||
|
||||
stdout.flush()
|
||||
}
|
||||
@ -145,9 +214,7 @@ impl<'a> WatchState<'a> {
|
||||
|
||||
if self.show_hint {
|
||||
stdout
|
||||
.queue(SetAttributes(
|
||||
Attributes::from(Attribute::Bold).with(Attribute::Underlined),
|
||||
))?
|
||||
.queue(SetAttributes(HEADING_ATTRIBUTES))?
|
||||
.queue(SetForegroundColor(Color::Cyan))?;
|
||||
stdout.write_all(b"Hint")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
@ -175,16 +242,17 @@ impl<'a> WatchState<'a> {
|
||||
)?;
|
||||
}
|
||||
|
||||
let line_width = terminal::size()?.0;
|
||||
progress_bar(
|
||||
stdout,
|
||||
self.app_state.n_done(),
|
||||
self.app_state.exercises().len() as u16,
|
||||
line_width,
|
||||
self.term_width,
|
||||
)?;
|
||||
|
||||
stdout.write_all(b"\nCurrent exercise: ")?;
|
||||
terminal_file_link(stdout, self.app_state.current_exercise().path, Color::Blue)?;
|
||||
self.app_state
|
||||
.current_exercise()
|
||||
.terminal_file_link(stdout)?;
|
||||
stdout.write_all(b"\n\n")?;
|
||||
|
||||
self.show_prompt(stdout)?;
|
||||
@ -193,7 +261,39 @@ impl<'a> WatchState<'a> {
|
||||
}
|
||||
|
||||
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
self.show_hint = true;
|
||||
self.render(stdout)
|
||||
if !self.show_hint {
|
||||
self.show_hint = true;
|
||||
self.render(stdout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||
// Ignore any input until checking all exercises is done.
|
||||
let _input_pause_guard = InputPauseGuard::scoped_pause();
|
||||
|
||||
if let Some(first_pending_exercise_ind) = self.app_state.check_all_exercises(stdout)? {
|
||||
// Only change exercise if the current one is done.
|
||||
if self.app_state.current_exercise().done {
|
||||
self.app_state
|
||||
.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||
Ok(ExercisesProgress::NewPending)
|
||||
} else {
|
||||
Ok(ExercisesProgress::CurrentPending)
|
||||
}
|
||||
} else {
|
||||
self.app_state.render_final_message(stdout)?;
|
||||
Ok(ExercisesProgress::AllDone)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
if self.term_width != width {
|
||||
self.term_width = width;
|
||||
self.render(stdout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,86 +1,73 @@
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use std::sync::mpsc::Sender;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use std::sync::{
|
||||
atomic::Ordering::Relaxed,
|
||||
mpsc::{Receiver, Sender},
|
||||
};
|
||||
|
||||
use super::WatchEvent;
|
||||
use super::{EXERCISE_RUNNING, WatchEvent};
|
||||
|
||||
pub enum InputEvent {
|
||||
Run,
|
||||
Next,
|
||||
Run,
|
||||
Hint,
|
||||
List,
|
||||
CheckAll,
|
||||
Reset,
|
||||
Quit,
|
||||
Unrecognized,
|
||||
}
|
||||
|
||||
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
||||
// Only send `Unrecognized` on ENTER if the last input wasn't valid.
|
||||
let mut last_input_valid = false;
|
||||
|
||||
let last_input_event = loop {
|
||||
let terminal_event = match event::read() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
// If `send` returns an error, then the receiver is dropped and
|
||||
// a shutdown has been already initialized.
|
||||
let _ = tx.send(WatchEvent::TerminalEventErr(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match terminal_event {
|
||||
Event::Key(key) => {
|
||||
pub fn terminal_event_handler(
|
||||
sender: Sender<WatchEvent>,
|
||||
unpause_receiver: Receiver<()>,
|
||||
manual_run: bool,
|
||||
) {
|
||||
let last_watch_event = loop {
|
||||
match event::read() {
|
||||
Ok(Event::Key(key)) => {
|
||||
match key.kind {
|
||||
KeyEventKind::Release | KeyEventKind::Repeat => continue,
|
||||
KeyEventKind::Press => (),
|
||||
}
|
||||
|
||||
if key.modifiers != KeyModifiers::NONE {
|
||||
last_input_valid = false;
|
||||
if EXERCISE_RUNNING.load(Relaxed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let input_event = match key.code {
|
||||
KeyCode::Enter => {
|
||||
if last_input_valid {
|
||||
continue;
|
||||
KeyCode::Char('n') => InputEvent::Next,
|
||||
KeyCode::Char('r') if manual_run => InputEvent::Run,
|
||||
KeyCode::Char('h') => InputEvent::Hint,
|
||||
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
|
||||
KeyCode::Char('c') => InputEvent::CheckAll,
|
||||
KeyCode::Char('x') => {
|
||||
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
InputEvent::Unrecognized
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let input_event = match c {
|
||||
'n' => InputEvent::Next,
|
||||
'h' => InputEvent::Hint,
|
||||
'l' => break InputEvent::List,
|
||||
'q' => break InputEvent::Quit,
|
||||
'r' if manual_run => InputEvent::Run,
|
||||
_ => {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
// Pause input until quitting the confirmation prompt.
|
||||
if unpause_receiver.recv().is_err() {
|
||||
return;
|
||||
};
|
||||
|
||||
last_input_valid = true;
|
||||
input_event
|
||||
}
|
||||
_ => {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
||||
if sender.send(WatchEvent::Input(input_event)).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Event::Resize(_, _) => {
|
||||
if tx.send(WatchEvent::TerminalResize).is_err() {
|
||||
Ok(Event::Resize(width, _)) => {
|
||||
if sender.send(WatchEvent::TerminalResize { width }).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Event::FocusGained | Event::FocusLost | Event::Mouse(_) => continue,
|
||||
Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue,
|
||||
Err(e) => break WatchEvent::TerminalEventErr(e),
|
||||
}
|
||||
};
|
||||
|
||||
let _ = tx.send(WatchEvent::Input(last_input_event));
|
||||
let _ = sender.send(last_watch_event);
|
||||
}
|
||||
|
@ -7,5 +7,5 @@ bin = [
|
||||
|
||||
[package]
|
||||
name = "test_exercises"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
7
website/.gitignore
vendored
Normal file
7
website/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/node_modules/
|
||||
/package-lock.json
|
||||
|
||||
/public/
|
||||
|
||||
/static/main.css
|
||||
/static/processed_images/
|
41
website/config.toml
Normal file
41
website/config.toml
Normal file
@ -0,0 +1,41 @@
|
||||
base_url = "https://rustlings.rust-lang.org"
|
||||
title = "Rustlings"
|
||||
description = "Small exercises to get you used to reading and writing Rust code!"
|
||||
|
||||
compile_sass = false
|
||||
build_search_index = false
|
||||
|
||||
[markdown]
|
||||
highlight_code = true
|
||||
highlight_theme = "dracula"
|
||||
|
||||
insert_anchor_links = "heading"
|
||||
|
||||
[extra]
|
||||
logo_path = "images/happy_ferris.svg"
|
||||
|
||||
[[extra.menu_items]]
|
||||
name = "Rustlings"
|
||||
url = "@/_index.md"
|
||||
[[extra.menu_items]]
|
||||
name = "Setup"
|
||||
url = "@/setup/index.md"
|
||||
[[extra.menu_items]]
|
||||
name = "Usage"
|
||||
url = "@/usage/index.md"
|
||||
[[extra.menu_items]]
|
||||
name = "Community Exercises"
|
||||
url = "@/community-exercises/index.md"
|
||||
[[extra.menu_items]]
|
||||
name = "Q&A"
|
||||
url = "https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q="
|
||||
|
||||
[[extra.footer_items]]
|
||||
name = "Repository"
|
||||
url = "https://github.com/rust-lang/rustlings"
|
||||
[[extra.footer_items]]
|
||||
name = "Changelog"
|
||||
url = "https://github.com/rust-lang/rustlings/blob/main/CHANGELOG.md"
|
||||
[[extra.footer_items]]
|
||||
name = "MIT License"
|
||||
url = "https://github.com/rust-lang/rustlings/blob/main/LICENSE"
|
21
website/content/_index.md
Normal file
21
website/content/_index.md
Normal file
@ -0,0 +1,21 @@
|
||||
+++
|
||||
+++
|
||||
|
||||
Small exercises to get you used to reading and writing [Rust](https://www.rust-lang.org) code - _Recommended in parallel to reading [the official Rust book](https://doc.rust-lang.org/book) 📚️_
|
||||
|
||||
<script src="https://asciinema.org/a/719805.js" id="asciicast-719805" async="true"></script>
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Installation
|
||||
cargo install rustlings
|
||||
# Initialization
|
||||
rustlings init
|
||||
# Moving into new directory
|
||||
cd rustlings
|
||||
# Starting Rustlings
|
||||
rustlings
|
||||
```
|
||||
|
||||
Visit the [**setup**](@/setup/index.md) page for more details 🧰
|
73
website/content/community-exercises/index.md
Normal file
73
website/content/community-exercises/index.md
Normal file
@ -0,0 +1,73 @@
|
||||
+++
|
||||
title = "Community Exercises"
|
||||
+++
|
||||
|
||||
## List of Community Exercises
|
||||
|
||||
- 🇯🇵 [Japanese Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercises.
|
||||
- 🇨🇳 [Simplified Chinese Rustlings](https://github.com/SandmeyerX/rustlings-zh-cn): A simplified Chinese translation of the Rustlings exercises.
|
||||
|
||||
> You can use the same `rustlings` program that you installed with `cargo install rustlings` to run community exercises.
|
||||
|
||||
## Creating Community Exercises
|
||||
|
||||
Rustling's support for community exercises allows you to create your own exercises to focus on some specific topic.
|
||||
You could also offer a translation of the original Rustlings exercises as community exercises.
|
||||
|
||||
### Getting Started
|
||||
|
||||
To create community exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`.
|
||||
This command will, similar to `cargo new PROJECT_NAME`, create the template directory `PROJECT_NAME` with all what you need to get started.
|
||||
|
||||
_Read the comments_ in the generated `info.toml` file to understand its format.
|
||||
It allows you to set a custom welcome and final message and specify the metadata of every exercise.
|
||||
|
||||
### Creating an Exercise
|
||||
|
||||
Here is an example of the metadata of one exercise:
|
||||
|
||||
```toml
|
||||
[[exercises]]
|
||||
name = "intro1"
|
||||
hint = """
|
||||
To finish this exercise, you need to …
|
||||
These links might help you …"""
|
||||
```
|
||||
|
||||
After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory.
|
||||
The exercise needs to contain a `main` function, but it can be empty.
|
||||
Adding tests is recommended.
|
||||
Look at the official Rustlings exercises for inspiration.
|
||||
|
||||
You can optionally add a solution file `intro1.rs` to the `solutions/` directory.
|
||||
|
||||
Now, run `rustlings dev check`.
|
||||
It will tell you about any issues with your exercises.
|
||||
For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`.
|
||||
|
||||
`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully.
|
||||
|
||||
That's it!
|
||||
You finished your first exercise 🎉
|
||||
|
||||
### Cargo.toml
|
||||
|
||||
Except of the `bin` list, you can modify the `Cargo.toml` file as you want.
|
||||
|
||||
> The `bin` list is automatically updated by running `rustlings dev update`
|
||||
|
||||
- You can add dependencies in the `[dependencies]` table.
|
||||
- You might want to [configure some lints](https://doc.rust-lang.org/cargo/reference/manifest.html#the-lints-section) for all exercises. You can do so in the `[lints.rust]` and `[lints.clippy]` tables.
|
||||
|
||||
### Publishing
|
||||
|
||||
Now, add more exercises and publish them as a Git repository.
|
||||
|
||||
Users just have to clone that repository and run `rustlings` in it to start working on your exercises (just like the official ones).
|
||||
|
||||
One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise.
|
||||
But you can trust your users to not open the solution too early 😉
|
||||
|
||||
### Sharing
|
||||
|
||||
After publishing your community exercises, open an issue or a pull request in the [official Rustlings repository](https://github.com/rust-lang/rustlings) to add your project to the [list of community exercises](#list-of-community-exercises) 😃
|
78
website/content/setup/index.md
Normal file
78
website/content/setup/index.md
Normal file
@ -0,0 +1,78 @@
|
||||
+++
|
||||
title = "Setup"
|
||||
+++
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Installing Rust
|
||||
|
||||
Before installing Rustlings, you must have the **latest version of Rust** installed.
|
||||
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions.
|
||||
This will also install _Cargo_, Rust's package/project manager.
|
||||
|
||||
> 🐧 If you are on **Linux**, make sure you have `gcc` installed (_for a linker_).
|
||||
>
|
||||
> Debian: `sudo apt install gcc`\
|
||||
> Fedora: `sudo dnf install gcc`
|
||||
|
||||
> 🍎 If you are on **MacOS**, make sure you have _Xcode and its developer tools_ installed: `xcode-select --install`
|
||||
|
||||
## Installing Rustlings
|
||||
|
||||
The following command will download and compile Rustlings:
|
||||
|
||||
```bash
|
||||
cargo install rustlings
|
||||
```
|
||||
|
||||
{% details(summary="If the installation fails…") %}
|
||||
|
||||
- Make sure you have the latest Rust version by running `rustup update`
|
||||
- Try adding the `--locked` flag: `cargo install rustlings --locked`
|
||||
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
|
||||
|
||||
{% end %}
|
||||
|
||||
## Initialization
|
||||
|
||||
After installing Rustlings, run the following command to initialize the `rustlings/` directory:
|
||||
|
||||
```bash
|
||||
rustlings init
|
||||
```
|
||||
|
||||
{% details(summary="If the command <code>rustlings</code> can't be found…") %}
|
||||
|
||||
You are probably using Linux and installed Rust using your package manager.
|
||||
|
||||
Cargo installs binaries to the directory `~/.cargo/bin`.
|
||||
Sadly, package managers often don't add `~/.cargo/bin` to your `PATH` environment variable.
|
||||
|
||||
- Either add `~/.cargo/bin` manually to `PATH`
|
||||
- Or uninstall Rust from the package manager and [install it using the official way with `rustup`](https://www.rust-lang.org/tools/install)
|
||||
|
||||
{% end %}
|
||||
|
||||
Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises:
|
||||
|
||||
```bash
|
||||
cd rustlings/
|
||||
rustlings
|
||||
```
|
||||
|
||||
## Working environment
|
||||
|
||||
### Editor
|
||||
|
||||
Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
||||
But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises.
|
||||
|
||||
### Terminal
|
||||
|
||||
While working with Rustlings, please use a modern terminal for the best user experience.
|
||||
The default terminal on Linux and Mac should be sufficient.
|
||||
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
|
||||
|
||||
## Usage
|
||||
|
||||
After being done with the setup, visit the [**usage**](@/usage/index.md) page for some info about using Rustlings 🚀
|
55
website/content/usage/index.md
Normal file
55
website/content/usage/index.md
Normal file
@ -0,0 +1,55 @@
|
||||
+++
|
||||
title = "Usage"
|
||||
+++
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Doing exercises
|
||||
|
||||
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.
|
||||
For every topic, there is an additional `README.md` file with some resources to get you started on the topic.
|
||||
We highly recommend that you have a look at them before you start 📚️
|
||||
|
||||
Most exercises contain an error that keeps them from compiling, and it's up to you to fix it!
|
||||
Some exercises contain tests that need to pass for the exercise to be done ✅
|
||||
|
||||
Search for `TODO` and `todo!()` to find out what you need to change.
|
||||
Ask for hints by entering `h` in the _watch mode_ 💡
|
||||
|
||||
## Watch Mode
|
||||
|
||||
After the [initialization](@/setup/index.md#initialization), Rustlings can be launched by simply running the command `rustlings`.
|
||||
|
||||
This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers).
|
||||
It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
|
||||
|
||||
{% details(summary="If detecting file changes in the <code>exercises/</code> directory fails…") %}
|
||||
|
||||
You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
|
||||
|
||||
Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or a virtual machine (e.g. WSL).
|
||||
|
||||
{% end %}
|
||||
|
||||
## Exercise List
|
||||
|
||||
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
|
||||
|
||||
The list allows you to…
|
||||
|
||||
- See the status of all exercises (done or pending)
|
||||
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
|
||||
- `r`: Reset status and file of the selected exercise (you need to _reload/reopen_ its file in your editor afterwards)
|
||||
|
||||
See the footer of the list for all possible keys.
|
||||
|
||||
## Questions?
|
||||
|
||||
If you need any help while doing the exercises and the builtin hints aren't helpful, feel free to ask in the [_Q&A_ discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question isn't answered there 💡
|
||||
|
||||
## 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.
|
||||
|
||||
> If you want to create your own Rustlings exercises, visit the [**community exercises**](@/community-exercises/index.md) page 🏗️
|
54
website/input.css
Normal file
54
website/input.css
Normal file
@ -0,0 +1,54 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@layer base {
|
||||
h1 {
|
||||
@apply text-4xl mt-3 mb-3 font-bold;
|
||||
}
|
||||
h2 {
|
||||
@apply text-3xl mt-4 mb-1.5 font-bold;
|
||||
}
|
||||
h3 {
|
||||
@apply text-2xl mt-5 mb-1.5 font-bold;
|
||||
}
|
||||
h4 {
|
||||
@apply text-xl mt-6 mb-1.5 font-bold;
|
||||
}
|
||||
p {
|
||||
@apply mb-2;
|
||||
}
|
||||
a {
|
||||
@apply text-[#FFC832] underline hover:decoration-orange-400 transition duration-300;
|
||||
}
|
||||
ul {
|
||||
@apply mt-2 mb-3 ml-1 list-disc list-inside marker:text-sky-600;
|
||||
}
|
||||
ol {
|
||||
@apply mt-2 mb-3 ml-1 list-decimal list-inside marker:text-sky-500;
|
||||
}
|
||||
li {
|
||||
@apply my-0.5;
|
||||
}
|
||||
code {
|
||||
@apply bg-white/10 px-1 pb-px pt-1 rounded-md;
|
||||
}
|
||||
pre code {
|
||||
@apply bg-inherit p-0 text-inherit;
|
||||
}
|
||||
hr {
|
||||
@apply my-5 rounded-full;
|
||||
}
|
||||
img {
|
||||
@apply md:w-3/4 lg:w-3/5;
|
||||
}
|
||||
blockquote {
|
||||
@apply px-3 pt-2 pb-0.5 mb-4 mt-2 border-s-4 border-white/80 bg-white/7 rounded-sm;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply px-2 pt-2 pb-px overflow-x-auto text-sm sm:text-base rounded-sm mt-2 mb-4 after:content-[attr(data-lang)] after:text-[8px] after:opacity-40 selection:bg-white/15;
|
||||
}
|
||||
pre code mark {
|
||||
@apply pb-0.5 pt-1 pr-px text-inherit rounded-xs;
|
||||
}
|
||||
}
|
||||
|
5
website/justfile
Normal file
5
website/justfile
Normal file
@ -0,0 +1,5 @@
|
||||
zola:
|
||||
zola serve --open
|
||||
|
||||
tailwind:
|
||||
npx @tailwindcss/cli -w -i input.css -o static/main.css
|
5
website/package.json
Normal file
5
website/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.1"
|
||||
}
|
||||
}
|
33
website/static/images/happy_ferris.svg
Normal file
33
website/static/images/happy_ferris.svg
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1200 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||
<g id="Layer-1" serif:id="Layer 1">
|
||||
<g transform="matrix(1,0,0,1,1009.4,506.362)">
|
||||
<path d="M0,-7.203L-12.072,-32.209C-12.009,-33.156 -11.961,-34.107 -11.961,-35.062C-11.961,-63.408 -41.439,-89.533 -91.03,-110.451L-91.03,-93.058C-95.866,-94.977 -100.901,-96.845 -106.147,-98.651L-106.147,-106.759C-177.021,-132.319 -282.53,-148.537 -400.388,-148.537C-503.361,-148.537 -596.917,-136.157 -666.179,-115.983L-666.179,-87.737L-666.181,-87.737L-666.181,-121.925C-737.141,-99.375 -781.135,-68.048 -781.135,-33.41C-781.135,-27.95 -780.034,-22.572 -777.918,-17.297L-785.146,-4.43C-785.146,-4.43 -790.938,3.082 -780.74,18.932C-771.746,32.909 -726.692,87.617 -702.913,116.267C-692.699,130.954 -685.772,140.001 -685.167,139.126C-684.212,137.74 -691.518,110.165 -711.802,78.703C-721.268,61.808 -732.57,39.42 -739.356,22.884C-720.414,34.874 -609.126,90.913 -382.124,90.685C-150.13,90.453 -47.009,17.834 -35.691,7.948C-39.646,23.837 -53.159,55.981 -63.936,78.586C-81.642,110.917 -88.056,139.064 -87.232,140.456C-86.708,141.334 -80.667,132.015 -71.756,116.913C-51.025,87.37 -11.739,30.974 -3.889,16.608C5.007,0.323 0,-7.203 0,-7.203" style="fill:rgb(165,43,0);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,1079.49,294.885)">
|
||||
<path d="M0,204.135L-79.343,145.689C-80.088,143.089 -80.833,140.488 -81.603,137.908L-55.541,100.154C-52.881,96.314 -52.345,91.322 -54.072,86.943C-55.803,82.585 -59.587,79.461 -64.062,78.696L-108.128,71.217C-109.837,67.732 -111.626,64.301 -113.422,60.898L-94.907,18.51C-93.004,14.193 -93.402,9.175 -95.929,5.256C-98.446,1.319 -102.715,-0.981 -107.267,-0.802L-151.991,0.823C-154.306,-2.193 -156.658,-5.18 -159.058,-8.114L-148.78,-53.546C-147.738,-58.158 -149.054,-62.989 -152.267,-66.34C-155.462,-69.679 -160.105,-71.062 -164.52,-69.979L-208.082,-59.27C-210.902,-61.763 -213.77,-64.223 -216.67,-66.635L-215.103,-113.276C-214.935,-117.997 -217.136,-122.484 -220.915,-125.105C-224.692,-127.741 -229.485,-128.137 -233.616,-126.179L-274.254,-106.858C-277.527,-108.736 -280.819,-110.595 -284.146,-112.395L-291.327,-158.356C-292.056,-163.012 -295.051,-166.968 -299.246,-168.774C-303.431,-170.591 -308.222,-170.002 -311.894,-167.238L-348.126,-140.053C-351.695,-141.238 -355.279,-142.373 -358.905,-143.46L-374.522,-187.045C-376.11,-191.488 -379.772,-194.751 -384.238,-195.669C-388.688,-196.578 -393.266,-195.037 -396.352,-191.589L-426.851,-157.47C-430.536,-157.893 -434.228,-158.28 -437.927,-158.601L-461.476,-198.277C-463.86,-202.295 -468.073,-204.741 -472.615,-204.741C-477.144,-204.741 -481.365,-202.295 -483.733,-198.277L-507.288,-158.601C-510.989,-158.28 -514.696,-157.893 -518.376,-157.47L-548.875,-191.589C-551.965,-195.037 -556.559,-196.578 -560.997,-195.669C-565.457,-194.739 -569.125,-191.488 -570.704,-187.045L-586.333,-143.46C-589.954,-142.373 -593.538,-141.23 -597.113,-140.053L-633.333,-167.238C-637.016,-170.012 -641.811,-170.599 -646.001,-168.774C-650.182,-166.968 -653.189,-163.012 -653.914,-158.356L-661.1,-112.395C-664.422,-110.595 -667.714,-108.746 -670.995,-106.858L-711.629,-126.179C-715.756,-128.145 -720.574,-127.741 -724.333,-125.105C-728.106,-122.484 -730.313,-117.997 -730.143,-113.276L-728.581,-66.635C-731.475,-64.223 -734.337,-61.763 -737.172,-59.27L-780.726,-69.979C-785.149,-71.053 -789.788,-69.679 -792.991,-66.34C-796.212,-62.989 -797.517,-58.158 -796.482,-53.546L-786.225,-8.114C-788.603,-5.169 -790.958,-2.193 -793.267,0.823L-837.991,-0.802C-842.504,-0.937 -846.812,1.319 -849.334,5.256C-851.861,9.175 -852.244,14.193 -850.363,18.51L-831.835,60.898C-833.634,64.301 -835.421,67.732 -837.144,71.217L-881.207,78.696C-885.686,79.45 -889.459,82.572 -891.201,86.943C-892.929,91.322 -892.368,96.314 -889.727,100.154L-863.661,137.908C-863.862,138.575 -864.048,139.247 -864.248,139.916L-937.944,218.201C-937.944,218.201 -949.24,227.052 -932.797,247.855C-918.297,266.206 -843.846,338.951 -804.526,377.06C-787.92,396.408 -776.542,408.389 -775.354,407.353C-773.478,405.708 -783.326,370.506 -816.036,329.204C-841.252,292.148 -873.977,235.155 -866.303,228.586C-866.303,228.586 -857.574,217.505 -840.061,209.529C-839.42,210.041 -840.723,209.022 -840.061,209.529C-840.061,209.529 -470.466,380.02 -127.632,212.413C-88.468,205.388 -64.759,226.368 -64.759,226.368C-56.583,231.108 -77.755,289.712 -95.166,328.505C-118.845,372.555 -122.317,406.927 -120.31,408.119C-119.042,408.876 -110.427,395.766 -98.138,374.902C-67.814,332.649 -10.492,252.1 0,232.534C11.895,210.352 0,204.135 0,204.135" style="fill:rgb(247,76,0);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,917.896,244.679)">
|
||||
<path d="M0,232.466C0,232.466 53.179,230 123.032,159.004L132.93,137.025C132.93,137.025 24.513,29.177 193.048,-45.266C193.048,-45.266 178.293,-21.154 182.622,72.006C182.622,72.006 233.437,54.357 248.336,-27.934C248.336,-27.934 322.456,69.79 167.834,161.443C167.834,161.443 95.294,277.732 -6.971,266.593L0,232.466Z" style="fill:rgb(247,76,0);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,676.997,488.361)">
|
||||
<path d="M0,-78.192C0,-78.192 36.935,-118.635 73.871,-78.192C73.871,-78.192 102.893,-24.265 73.871,2.695C73.871,2.695 26.384,40.443 0,2.695C0,2.695 -31.658,-26.964 0,-78.192" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,719.761,425.169)">
|
||||
<path d="M0,0.004C0,15.75 -9.282,28.518 -20.732,28.518C-32.18,28.518 -41.462,15.75 -41.462,0.004C-41.462,-15.746 -32.18,-28.514 -20.732,-28.514C-9.282,-28.514 0,-15.746 0,0.004" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,512.148,482.736)">
|
||||
<path d="M0,-83.609C0,-83.609 63.355,-111.661 80.648,-49.047C80.648,-49.047 98.762,23.933 28.618,28.052C28.618,28.052 -60.826,10.824 0,-83.609" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,543.968,426.204)">
|
||||
<path d="M0,0.002C0,16.241 -9.572,29.411 -21.381,29.411C-33.185,29.411 -42.76,16.241 -42.76,0.002C-42.76,-16.242 -33.185,-29.409 -21.381,-29.409C-9.572,-29.409 0,-16.242 0,0.002" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,593.317,576.574)">
|
||||
<path d="M0,-40.271L80.796,-46.755C80.796,-46.755 78.058,-33.749 67.517,-23.986C67.517,-23.986 39.727,6.484 7.844,-26.519C7.844,-26.519 2.627,-32.148 0,-40.271" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,269.796,270.778)">
|
||||
<path d="M0,190.741C-0.667,190.741 -1.321,190.79 -1.973,190.842C-28.207,184.871 -101.946,165.657 -121.437,134.479C-121.437,134.479 -22.21,21.607 -177.297,-50.54L-159.24,74.338C-159.24,74.338 -207.049,42.389 -217.366,-27.008C-217.366,-27.008 -333.789,57.486 -165.982,138.466C-165.982,138.466 -150.762,195.653 -4.633,241.281L-4.526,240.846C-3.055,241.118 -1.549,241.281 0,241.281C13.808,241.281 25.003,229.969 25.003,216.01C25.003,202.054 13.808,190.741 0,190.741" style="fill:rgb(247,76,0);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.3 KiB |
70
website/static/images/panic.svg
Normal file
70
website/static/images/panic.svg
Normal file
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="1434.979px" height="947px" viewBox="0 0 1434.979 947" enable-background="new 0 0 1434.979 947" xml:space="preserve">
|
||||
<!--<rect fill="#FFFFFF" width="1434.979" height="947"/>-->
|
||||
<path fill="#8F1F1D" d="M712.827,368.579c-131.633,0-251.228,15.825-339.77,41.615v220.298
|
||||
c88.542,25.79,208.137,41.614,339.77,41.614c150.657,0,285.535-20.729,376.134-53.402V421.986
|
||||
C998.361,389.311,863.483,368.579,712.827,368.579"/>
|
||||
<path fill="#8F1F1D" d="M1094.847,567.972c-3.856-10.663-4.629-24.154-1.36-37.162c5.85-23.289,22.421-36.198,37.013-28.833
|
||||
c3.618,1.827,6.773,4.73,9.387,8.418c0.239-0.001,0.479,0,0.715,0.016c0,0,44.552,53.106,3.313,116.003
|
||||
c-0.896,3.569-76.534,91.718-94.043,94.524C1038.411,722.773,1064.762,630.939,1094.847,567.972"/>
|
||||
<path fill="#8F1F1D" d="M363.903,557.551c5.3-9.631,7.158-22.788,4.217-36.426c-5.266-24.416-23.91-41.109-41.642-37.285
|
||||
c-4.398,0.948-8.325,3.072-11.666,6.099c-0.282-0.059-0.564-0.113-0.845-0.153c0,0-56.292,41.952-12.057,113.924
|
||||
c0.805,3.741,83.851,108.838,104.311,115.764C419.612,724.004,394.974,626.947,363.903,557.551"/>
|
||||
<path fill="#E23A26" d="M284.891,563.596l0.007,0.015C285.083,563.755,285.255,563.887,284.891,563.596"/>
|
||||
<path fill="#E33B26" d="M1180.543,488.433c-0.88-3.064-1.756-6.126-2.662-9.162l30.683-44.451c3.13-4.522,3.771-10.398,1.73-15.555
|
||||
c-2.04-5.13-6.49-8.81-11.76-9.71l-51.887-8.805c-2.008-4.102-4.115-8.142-6.229-12.15l21.797-49.903
|
||||
c2.243-5.087,1.769-10.995-1.203-15.608c-2.961-4.636-7.99-7.344-13.349-7.133l-52.656,1.913c-2.727-3.55-5.496-7.068-8.322-10.521
|
||||
l12.102-53.49c1.225-5.433-0.322-11.118-4.104-15.064c-3.762-3.932-9.229-5.559-14.426-4.283l-51.289,12.608
|
||||
c-3.321-2.935-6.699-5.833-10.114-8.673l1.849-54.914c0.197-5.559-2.394-10.842-6.845-13.925
|
||||
c-4.445-3.104-10.093-3.573-14.955-1.266l-47.848,22.747c-3.854-2.21-7.728-4.4-11.644-6.517l-8.455-54.115
|
||||
c-0.857-5.483-4.386-10.139-9.326-12.266c-4.923-2.137-10.568-1.447-14.891,1.808l-42.659,32.007
|
||||
c-4.2-1.395-8.419-2.732-12.692-4.011l-18.386-51.316c-1.87-5.229-6.182-9.071-11.438-10.151c-5.238-1.072-10.63,0.742-14.263,4.802
|
||||
l-35.907,40.171c-4.342-0.5-8.685-0.956-13.043-1.331l-27.723-46.713c-2.811-4.732-7.771-7.612-13.116-7.612
|
||||
c-5.334,0-10.304,2.88-13.09,7.612l-27.733,46.713c-4.358,0.375-8.722,0.831-13.056,1.331l-35.91-40.171
|
||||
c-3.636-4.06-9.047-5.874-14.268-4.802c-5.255,1.092-9.573,4.922-11.433,10.151l-18.402,51.316
|
||||
c-4.26,1.279-8.481,2.627-12.691,4.011l-42.644-32.007c-4.336-3.266-9.98-3.955-14.916-1.808c-4.919,2.127-8.461,6.783-9.313,12.266
|
||||
l-8.461,54.115c-3.914,2.117-7.789,4.294-11.653,6.517L436.1,168.34c-4.858-2.316-10.529-1.838-14.954,1.266
|
||||
c-4.445,3.083-7.042,8.366-6.84,13.925l1.835,54.914c-3.405,2.84-6.774,5.738-10.112,8.673L354.75,234.51
|
||||
c-5.211-1.265-10.67,0.351-14.441,4.283c-3.795,3.946-5.332,9.631-4.113,15.064l12.079,53.49c-2.802,3.467-5.575,6.971-8.293,10.521
|
||||
l-52.655-1.913c-5.314-0.157-10.386,2.497-13.356,7.133c-2.974,4.613-3.425,10.521-1.211,15.608l21.814,49.903
|
||||
c-2.119,4.008-4.224,8.048-6.249,12.15l-51.882,8.805c-5.271,0.888-9.715,4.566-11.765,9.71c-2.037,5.157-1.375,11.033,1.735,15.555
|
||||
l30.69,44.451c-0.236,0.784-0.455,1.576-0.69,2.364l-16.863,17.911l45.341,64.05c0,0,435.152,200.731,838.797,3.396
|
||||
C1169.796,558.719,1180.543,488.433,1180.543,488.433"/>
|
||||
<path d="M795.716,446.557c0,0,48.162-52.734,96.324,0c0,0,37.844,70.318,0,105.473c0,0-61.922,49.223-96.324,0
|
||||
C795.716,552.029,754.434,513.354,795.716,446.557"/>
|
||||
<path fill="#FFFFFF" d="M855.154,481.097c0,19.782-11.66,35.82-26.041,35.82c-14.379,0-26.04-16.038-26.04-35.82
|
||||
c0-19.782,11.661-35.821,26.04-35.821C843.494,445.275,855.154,461.315,855.154,481.097"/>
|
||||
<path d="M578.401,430.129c0,0,84.436-37.385,107.481,46.059c0,0,24.141,97.261-69.339,102.751
|
||||
C616.543,578.939,497.34,555.98,578.401,430.129"/>
|
||||
<rect x="187.424" y="75.529" fill="none" width="1060" height="782"/>
|
||||
<path fill="#FFFFFF" d="M627.514,481.096c0,20.579-12.13,37.27-27.095,37.27c-14.959,0-27.092-16.69-27.092-37.27
|
||||
c0-20.583,12.133-37.27,27.092-37.27C615.384,443.826,627.514,460.513,627.514,481.096"/>
|
||||
<path fill="#E33B26" d="M299.026,574.745c10.967-12.463,37.611-27.557,35.57-46.282c-3.653-33.526-31.456-57.999-62.099-54.658
|
||||
c-7.599,0.827-14.658,3.292-20.923,7.035c-0.463-0.106-0.925-0.211-1.388-0.294c0,0-103.632,50.873-44.564,152.657
|
||||
c0.557,5.137,117.847,155.668,150.787,167.131C377.968,807.836,336.498,671.694,299.026,574.745"/>
|
||||
<path fill="#E33B26" d="M1140.973,570.202c-12.692-10.7-46.162-20.418-46.92-39.238c-1.355-33.697,22.512-62.021,53.312-63.26
|
||||
c7.638-0.308,14.983,1.083,21.734,3.857c0.442-0.174,0.884-0.347,1.329-0.497c0,0,110.025,34.951,66.695,144.366
|
||||
c0.21,5.163-93.468,171.416-124.345,187.635C1092.57,813.681,1118.285,671.635,1140.973,570.202"/>
|
||||
<rect x="187.484" y="75.843" fill="none" width="1059.75" height="781.686"/>
|
||||
<rect x="187.424" y="75.529" fill="none" width="1060" height="782"/>
|
||||
<g>
|
||||
<path fill="#E33B26" d="M283.144,565.511c0,0-137.214-4.942-161.62-140.761l57.596-25.427c0,0-13.912,96.957,106.615,110.022
|
||||
L283.144,565.511"/>
|
||||
<path fill="#E33B26" d="M127.552,333.083c0,0-24.965-49.774-65.807-113.261C18.721,241.035-2.671,299.05,13.482,357.484
|
||||
c17.846,64.558,74.749,105.16,127.097,90.69s80.318-78.535,62.471-143.092c-7.909-28.618-23.501-52.519-42.963-69.011
|
||||
C150.611,287.113,127.552,333.083,127.552,333.083"/>
|
||||
</g>
|
||||
<rect x="187.484" y="75.843" fill="none" width="1059.75" height="781.686"/>
|
||||
<g>
|
||||
<path fill="#E33B26" d="M1148.012,565.511c0,0,137.214-4.942,161.62-140.761l-57.596-25.428c0,0,13.912,96.957-106.615,110.022
|
||||
L1148.012,565.511"/>
|
||||
<path fill="#E33B26" d="M1303.604,333.083c0,0,24.966-49.774,65.808-113.261c43.023,21.212,64.416,79.228,48.262,137.662
|
||||
c-17.846,64.558-74.748,105.16-127.096,90.689c-52.348-14.47-80.318-78.534-62.472-143.091
|
||||
c7.909-28.618,23.501-52.519,42.964-69.011C1280.544,287.113,1303.604,333.083,1303.604,333.083"/>
|
||||
</g>
|
||||
<path d="M807.895,626.942c-7.131-58.735-72.193-61.431-72.193-61.431c-50.936,11.227-59.183,47.369-57.392,75.104L807.895,626.942z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
61
website/static/images/rust_logo.svg
Normal file
61
website/static/images/rust_logo.svg
Normal file
@ -0,0 +1,61 @@
|
||||
<svg version="1.1" height="106" width="106" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="logo" transform="translate(53, 53)">
|
||||
<path id="r" transform="translate(0.5, 0.5)" fill="white" stroke="white" stroke-width="1" stroke-linejoin="round" d="
|
||||
M -9,-15 H 4 C 12,-15 12,-7 4,-7 H -9 Z
|
||||
M -40,22 H 0 V 11 H -9 V 3 H 1 C 12,3 6,22 15,22 H 40
|
||||
V 3 H 34 V 5 C 34,13 25,12 24,7 C 23,2 19,-2 18,-2 C 33,-10 24,-26 12,-26 H -35
|
||||
V -15 H -25 V 11 H -40 Z" />
|
||||
<g id="gear" mask="url(#holes)">
|
||||
<circle r="43" fill="none" stroke="white" stroke-width="9" />
|
||||
<g id="cogs">
|
||||
<polygon id="cog" stroke="white" stroke-width="3" stroke-linejoin="round" points="46,3 51,0 46,-3" />
|
||||
<use xlink:href="#cog" transform="rotate(11.25)" />
|
||||
<use xlink:href="#cog" transform="rotate(22.50)" />
|
||||
<use xlink:href="#cog" transform="rotate(33.75)" />
|
||||
<use xlink:href="#cog" transform="rotate(45.00)" />
|
||||
<use xlink:href="#cog" transform="rotate(56.25)" />
|
||||
<use xlink:href="#cog" transform="rotate(67.50)" />
|
||||
<use xlink:href="#cog" transform="rotate(78.75)" />
|
||||
<use xlink:href="#cog" transform="rotate(90.00)" />
|
||||
<use xlink:href="#cog" transform="rotate(101.25)" />
|
||||
<use xlink:href="#cog" transform="rotate(112.50)" />
|
||||
<use xlink:href="#cog" transform="rotate(123.75)" />
|
||||
<use xlink:href="#cog" transform="rotate(135.00)" />
|
||||
<use xlink:href="#cog" transform="rotate(146.25)" />
|
||||
<use xlink:href="#cog" transform="rotate(157.50)" />
|
||||
<use xlink:href="#cog" transform="rotate(168.75)" />
|
||||
<use xlink:href="#cog" transform="rotate(180.00)" />
|
||||
<use xlink:href="#cog" transform="rotate(191.25)" />
|
||||
<use xlink:href="#cog" transform="rotate(202.50)" />
|
||||
<use xlink:href="#cog" transform="rotate(213.75)" />
|
||||
<use xlink:href="#cog" transform="rotate(225.00)" />
|
||||
<use xlink:href="#cog" transform="rotate(236.25)" />
|
||||
<use xlink:href="#cog" transform="rotate(247.50)" />
|
||||
<use xlink:href="#cog" transform="rotate(258.75)" />
|
||||
<use xlink:href="#cog" transform="rotate(270.00)" />
|
||||
<use xlink:href="#cog" transform="rotate(281.25)" />
|
||||
<use xlink:href="#cog" transform="rotate(292.50)" />
|
||||
<use xlink:href="#cog" transform="rotate(303.75)" />
|
||||
<use xlink:href="#cog" transform="rotate(315.00)" />
|
||||
<use xlink:href="#cog" transform="rotate(326.25)" />
|
||||
<use xlink:href="#cog" transform="rotate(337.50)" />
|
||||
<use xlink:href="#cog" transform="rotate(348.75)" />
|
||||
</g>
|
||||
<g id="mounts">
|
||||
<polygon id="mount" stroke="white" stroke-width="6" stroke-linejoin="round" points="-7,-42 0,-35 7,-42" />
|
||||
<use xlink:href="#mount" transform="rotate(72)" />
|
||||
<use xlink:href="#mount" transform="rotate(144)" />
|
||||
<use xlink:href="#mount" transform="rotate(216)" />
|
||||
<use xlink:href="#mount" transform="rotate(288)" />
|
||||
</g>
|
||||
</g>
|
||||
<mask id="holes">
|
||||
<rect x="-60" y="-60" width="120" height="120" fill="white"/>
|
||||
<circle id="hole" cy="-40" r="3" />
|
||||
<use xlink:href="#hole" transform="rotate(72)" />
|
||||
<use xlink:href="#hole" transform="rotate(144)" />
|
||||
<use xlink:href="#hole" transform="rotate(216)" />
|
||||
<use xlink:href="#hole" transform="rotate(288)" />
|
||||
</mask>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
14
website/templates/404.html
Normal file
14
website/templates/404.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col mx-auto text-center">
|
||||
<h1>DON'T PANIC!</h1>
|
||||
<h2>404: Page not found!</h2>
|
||||
|
||||
<img class="mx-auto max-h-[50vh]"
|
||||
src="{{ get_url(path='images/panic.svg') | safe }}"
|
||||
alt="">
|
||||
|
||||
<a class="text-2xl font-bold" href="{{ get_url(path='@/_index.md') }}">Back to homepage</a>
|
||||
</div>
|
||||
{% endblock %}
|
2
website/templates/anchor-link.html
Normal file
2
website/templates/anchor-link.html
Normal file
@ -0,0 +1,2 @@
|
||||
<a class="text-white no-underline transition-none hover:underline"
|
||||
href="#{{ id }}"></a>
|
92
website/templates/base.html
Normal file
92
website/templates/base.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
{%- set timestamp = now(timestamp=true) -%}
|
||||
|
||||
{%- if page.title -%}
|
||||
{% set_global title = page.title %}
|
||||
{%- elif section.title -%}
|
||||
{% set_global title = section.title %}
|
||||
{%- else -%}
|
||||
{% set_global title = config.title %}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if page.description -%}
|
||||
{% set_global description = page.description %}
|
||||
{%- elif section.description -%}
|
||||
{% set_global description = section.description %}
|
||||
{%- else -%}
|
||||
{% set_global description = config.description %}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if page.permalink -%}
|
||||
{% set_global permalink = page.permalink %}
|
||||
{%- elif section.permalink -%}
|
||||
{% set_global permalink = section.permalink %}
|
||||
{%- endif %}
|
||||
|
||||
<title>{%- block title -%}{{- title -}}{%- endblock -%}</title>
|
||||
|
||||
<meta name="description"
|
||||
content="{%- block description -%}{{- description -}}{%- endblock -%}">
|
||||
|
||||
<link rel="icon"
|
||||
type="image/x-icon"
|
||||
href="{{ get_url(path=config.extra.logo_path) | safe }}?v={{ timestamp }}">
|
||||
|
||||
<link href="{{ get_url(path='main.css') | safe }}?v={{ timestamp }}"
|
||||
rel="stylesheet">
|
||||
|
||||
<meta property="og:title" content="{{ title }}">
|
||||
<meta property="og:description" content="{{ description }}">
|
||||
<meta property="og:image"
|
||||
content="{{ get_url(path=config.extra.logo_path) | safe }}?v={{ timestamp }}">
|
||||
{% if permalink %}<meta property="og:url" content="{{ permalink | safe }}">{% endif %}
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col p-2 mx-auto min-h-screen text-lg text-white break-words lg:px-5 2xl:container bg-[#2A3439]">
|
||||
<header class="flex flex-col gap-x-4 items-center py-2 px-4 mb-1 rounded-sm sm:flex-row sm:rounded-full bg-black/30">
|
||||
<a class="transition duration-500 hover:scale-110"
|
||||
href="{{ get_url(path='@/_index.md') | safe }}"
|
||||
aria-hidden="true">
|
||||
<img class="w-12 h-12"
|
||||
src="{{ get_url(path=config.extra.logo_path) | safe }}"
|
||||
alt="">
|
||||
</a>
|
||||
|
||||
<nav class="flex flex-col gap-x-6 items-center font-bold sm:flex-row">
|
||||
{% for menu_item in config.extra.menu_items %}
|
||||
{%- if menu_item.url is starting_with("@") -%}
|
||||
{% set_global menu_item_url = get_url(path=menu_item.url) %}
|
||||
{%- else -%}
|
||||
{% set_global menu_item_url = menu_item.url %}
|
||||
{%- endif %}
|
||||
|
||||
<a class="p-1 no-underline" href="{{ menu_item_url | safe }}">{{ menu_item.name }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="leading-relaxed">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="pt-2 pb-1 mt-auto text-sm text-center">
|
||||
<div class="inline-flex gap-x-1.5 items-center mx-auto mt-2">
|
||||
<img class="w-8 h-8"
|
||||
src="{{ get_url(path='images/rust_logo.svg') | safe }}"
|
||||
alt="">
|
||||
<div class="italic">Rustlings is an official Rust project</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-col gap-y-3 justify-around py-3 mt-3 rounded-sm sm:flex-row sm:rounded-full bg-black/30">
|
||||
{% for footer_item in config.extra.footer_items %}
|
||||
<a class="no-underline" href="{{ footer_item.url | safe }}">{{ footer_item.name }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
9
website/templates/index.html
Normal file
9
website/templates/index.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="m-3">
|
||||
<h1>Rustlings</h1>
|
||||
|
||||
{{ section.content | safe }}
|
||||
</div>
|
||||
{% endblock %}
|
39
website/templates/page.html
Normal file
39
website/templates/page.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1>{{ page.title }}</h1>
|
||||
|
||||
<div class="py-0.5 px-4 my-3 rounded-xl border-double border-s-4">
|
||||
<nav>
|
||||
<ul class="ml-0 list-none">
|
||||
{% for parent in page.toc %}
|
||||
{% if parent.level == 2 %}
|
||||
<li>
|
||||
{#- -#}
|
||||
<a href="{{ parent.permalink | safe }}">{{ parent.title }}</a>
|
||||
{#- -#}
|
||||
{% if parent.children %}
|
||||
<ul class="my-0 ml-5 list-none">
|
||||
{% for child in parent.children %}
|
||||
{% if child.level == 3 %}
|
||||
<li>
|
||||
{#- -#}
|
||||
<a class="text-base" href="{{ child.permalink | safe }}">{{ child.title }}</a>
|
||||
{#- -#}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{#- -#}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{ page.content | safe }}
|
||||
</article>
|
||||
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user