1
0
mirror of https://github.com/rust-lang/rustlings.git synced 2025-12-26 00:11:49 +02:00

Compare commits

..

149 Commits

Author SHA1 Message Date
mo8it
c8d1d9c51f chore: Release 2024-08-29 17:20:17 +02:00
mo8it
ab2eb3442e Update changelog 2024-08-29 17:10:39 +02:00
mo8it
dbbeb7d4ed Fix displaying the list message in narrow mode 2024-08-29 17:06:37 +02:00
mo8it
bfa00ffbdc Update deps 2024-08-29 16:40:40 +02:00
mo8it
10eb1a3aee Fix header padding 2024-08-29 16:01:41 +02:00
mo8it
fd2bf9f6f6 Simplify next_pending_exercise_ind 2024-08-29 01:59:04 +02:00
mo8it
fc1f9f0124 Optimize reading and writing the state file 2024-08-29 01:56:45 +02:00
mo8it
789492d1a9 The number of exercises can't be zero, but still 2024-08-29 00:32:58 +02:00
mo8it
afc320bed4 Fix error about too many open files during the final check 2024-08-29 00:17:22 +02:00
mo8it
cba4a6f9c8 Only disable links in VS code in the list 2024-08-28 01:19:53 +02:00
mo8it
5556d42b46 Use sol_path 2024-08-28 01:10:19 +02:00
mo8it
7d2bc1c7a4 Use a Vec for the name col padding 2024-08-28 00:56:22 +02:00
mo8it
c209c874a9 Check the exercise name length 2024-08-28 00:34:24 +02:00
mo8it
dd52e9cd72 Separate the scroll state 2024-08-27 00:03:50 +02:00
mo8it
0f71a150ff Making code prettier :P 2024-08-26 22:03:09 +02:00
mo8it
74388d4bf4 Only trigger write when needed 2024-08-26 04:41:26 +02:00
mo8it
e811dd15b5 Fix list on terminals that don't disable line wrapping 2024-08-26 04:29:58 +02:00
mo8it
f22700a4ec Use the correct environment variable 2024-08-26 02:43:08 +02:00
mo8it
ee25a7d458 Disable terminal links in VS-Code 2024-08-26 02:41:22 +02:00
mo8it
594e212b8a Darker highlighting in the list 2024-08-26 00:53:42 +02:00
mo8it
5c355468c1 File link in the list? No problem :D 2024-08-26 00:49:56 +02:00
mo8it
d1571d18f9 Only reset color and underline after link 2024-08-26 00:48:12 +02:00
mo8it
cb86b44dea LOL, swapped colors 2024-08-26 00:40:30 +02:00
mo8it
833e6e0c92 Newline after resetting attributes 2024-08-26 00:24:39 +02:00
mo8it
159273e532 Take stdout as argument in watch mode 2024-08-26 00:09:04 +02:00
mo8it
631f2db1a3 Lower the maximum scroll padding 2024-08-25 23:54:18 +02:00
mo8it
a1f0eaab54 Add disallowed types and methods in Clippy 2024-08-25 23:54:04 +02:00
mo8it
b1898f6d8b Use queue instead of Stylize 2024-08-25 23:53:50 +02:00
mo8it
d29e9e7e07 Update deps 2024-08-25 20:42:13 +02:00
mo8it
360605e284 Merge branch 'rm-ratatui' 2024-08-25 20:31:08 +02:00
mo8it
64772544fa Final touches :D 2024-08-25 20:29:54 +02:00
mo8it
5f4875e2ba Almost done with list 2024-08-25 19:24:12 +02:00
mo8it
fd2a8c01cb Separate drawing rows 2024-08-24 19:18:13 +02:00
mo8it
b6129ad081 Use the full length for the wide footer 2024-08-24 17:45:38 +02:00
mo8it
28d0b0a21e Highlight selected row 2024-08-24 17:45:02 +02:00
mo8it
b779c43126 Almost done with list display 2024-08-24 17:17:56 +02:00
mo8it
4e12725616 Don't exit the list on "to current" if nothing is selected 2024-08-24 00:23:45 +02:00
mo8it
570bc9f32d Start list without Ratatui 2024-08-24 00:14:12 +02:00
mo8it
47976caa69 Import Ordering 2024-08-22 14:42:17 +02:00
mo8it
f1abd8577c Add missing Clippy allows to solutions 2024-08-22 14:41:25 +02:00
mo8it
423b50b068 Use match instead of comparison chain 2024-08-22 14:37:47 +02:00
mo8it
bedf0789f2 Always use strict Clippy when checking solutions 2024-08-22 14:25:14 +02:00
mo8it
a2d1cb3b22 Push newline after running an exercise instead on each rendering 2024-08-20 16:05:52 +02:00
mo8it
e7ba88f905 Highlight the solution file 2024-08-20 16:04:29 +02:00
mo8it
50f6e5232e Leak info_file and cmd_runner in dev check 2024-08-20 14:47:08 +02:00
mo8it
8854f0a5ed Use anyhow! 2024-08-20 14:32:47 +02:00
mo8it
13cc3acdfd Improve readability 2024-08-20 13:56:52 +02:00
mo8it
5b7368c46d Improve error message if no exercise exists 2024-08-20 13:54:20 +02:00
mo8it
27999f2d26 Check if exercise doesn't contain tests 2024-08-20 13:49:48 +02:00
mo8it
e74f2a4274 Check for #[test] with newline at the end 2024-08-20 13:39:14 +02:00
mo8it
d141a73493 threads3: Improve the test 2024-08-20 13:35:07 +02:00
mo8it
631f44331e Remove --show-output for tests and use --format pretty 2024-08-20 13:08:15 +02:00
mo8it
b01fddef8b Show progress of dev check 2024-08-19 23:52:22 +02:00
mo8it
78a8553f1c "Continue at" quits the list 2024-08-19 23:29:17 +02:00
mo8it
b70c1abd7c Update deps 2024-08-19 23:28:53 +02:00
mo8it
71f31d74bc Update deps 2024-08-17 16:57:58 +02:00
mo8it
72e557b3a9 Break help footer on narrow terminals 2024-08-17 16:54:44 +02:00
mo8it
3eaccbb61a Restore the terminal after an error in the list 2024-08-17 16:49:07 +02:00
mo8it
b678bd8ed2 Disable mouse in the list 2024-08-17 16:34:43 +02:00
mo8it
2baa140615 q only quits the list 2024-08-17 15:53:34 +02:00
mo8it
e760f07767 Make it clear that reset only resets one exercise 2024-08-17 15:53:24 +02:00
mo8it
ca5d5f0a49 Remove dot for copy-pasta 2024-08-17 15:45:02 +02:00
mo8it
69b4fd49fc Only take a u8 to avoid huge output 2024-08-17 14:59:00 +02:00
mo8it
36f315c344 Add "the" 2024-08-17 14:56:52 +02:00
mo8it
8016f5ca2d Remove unneeded comma 2024-08-17 14:55:58 +02:00
mo8it
8ef2ff1257 Remove "Hello and" 2024-08-17 14:54:13 +02:00
mo8it
6ce31defb6 Ignore stdout of git init 2024-08-17 14:40:09 +02:00
mo8it
0b3ad9141b Add exercise lints 2024-08-16 00:24:45 +02:00
mo8it
c903db5c53 Add project lints 2024-08-16 00:24:45 +02:00
Mo
8a038b946c Merge pull request #2084 from crd477/patch-1
fix typo
2024-08-16 00:12:58 +02:00
Chad Dougherty
ed9740b72c fix typo
Similarely -> Similarly in comment
2024-08-15 14:21:27 -04:00
mo8it
ce3dcc9856 Fix typo 2024-08-09 12:47:32 +02:00
mo8it
4472d50eba chore: Release 2024-08-09 11:52:31 +02:00
mo8it
a1d5702ba0 Ready for publish 2024-08-09 11:51:56 +02:00
mo8it
52a231ce2f Update Ratatui 2024-08-09 02:17:01 +02:00
mo8it
16af981772 Hide stderr of cargo locate-project 2024-08-09 01:27:31 +02:00
mo8it
fc141b8dfc Put Cargo.toml in `` 2024-08-09 01:16:45 +02:00
mo8it
82ebd29ff6 Add a special confirmation for initialization in a workspace 2024-08-09 01:14:08 +02:00
mo8it
f5737b5a49 Fix typos 2024-08-09 01:08:52 +02:00
mo8it
55e68d2c63 Update deps 2024-08-09 01:06:27 +02:00
mo8it
479f45da9b test_dir is a str anyway 2024-08-09 01:05:44 +02:00
mo8it
140c4e4812 Improve initialization in a Cargo workspace 2024-08-09 00:49:30 +02:00
mo8it
337460d299 Check the status of the cargo metadata command 2024-08-09 00:12:49 +02:00
mo8it
e41c3a7c92 Use fixed seeds with ahash 2024-08-08 23:48:54 +02:00
mo8it
1b9faa4d61 Update CHANGELOG 2024-08-08 23:48:54 +02:00
Mo
9f9a754a64 Merge pull request #2076 from senekor/remo/snryotxotoxv
Improve initialization in workspace
2024-08-08 23:48:09 +02:00
Mo
f7b0cfe8d1 Merge pull request #2075 from senekor/remo/swzqnkxqzutw
Replace hashbrown with ahash
2024-08-08 23:12:43 +02:00
mo8it
4ce8667b9d Show the exercise name in the waiting message 2024-08-08 22:48:53 +02:00
mo8it
0785b24192 Show a message before running the exercise 2024-08-08 22:41:41 +02:00
mo8it
34f02cf83d Attach error message as context 2024-08-08 22:37:56 +02:00
Remo Senekowitsch
8b43d79257 Fix integration tests 2024-08-08 14:08:06 +02:00
Remo Senekowitsch
dc086c6bf1 Improve initialization in workspace
- Detect if we are in a cargo project more reliably.
  (e.g. if `rustlings init` is run in the `src/` directory)

- Refuse to initialize rustlings in a non-workspace cargo project.

- Automatically populate the `workspace.members` field if `rustlings init` is
  run in a workspace.

  This may be considered risky, as there is no guarantee that's what the user
  wanted to do. However, it is consistent with the behavior of `cargo new`.
  Also, newcomers to Rust are unlikely to accidentally be in a cargo workspace,
  as they won't know how to create one in the first place.

  The use case for initialization in a workspace is when a workshop organizer
  wants to use rustlings alongside other exerices and provide a single
  repository with everything in one place.
2024-08-08 13:34:27 +02:00
Remo Senekowitsch
dc0ffbe16e Replace hashbrown with ahash
hashbrown is already used in the standard library, but we want the
improved performance of the different hash algorithm.
Using ahash directly conveys this intent more clearly.
2024-08-08 11:12:17 +02:00
mo8it
8df66f7991 Allow initialization in a workspace 2024-08-08 02:45:18 +02:00
mo8it
39580381fa rust-analyzer problem isn't fixed :( 2024-08-08 01:48:57 +02:00
mo8it
06a0f278e5 Don't recommend the builtin VS-Code terminal because it can't clear scrollback 2024-08-08 01:35:47 +02:00
mo8it
fd97470f35 Adapt type name in hint 2024-08-08 00:42:26 +02:00
mo8it
11fc3f1e56 Fix errors not being shown after the welcome message 2024-08-08 00:41:12 +02:00
mo8it
693bb708b2 Add README to the solutions dir 2024-08-08 00:41:12 +02:00
mo8it
97719fe8da Remove state file and solutions dir from .gitignore 2024-08-08 00:41:12 +02:00
mo8it
4933ace50b Add panic = "abort" for exercises 2024-08-08 00:41:12 +02:00
mo8it
81bf0a6430 Remove redundant rustfmt check for solutions 2024-08-08 00:41:12 +02:00
mo8it
24aed1b14e Update CHANGELOG 2024-08-08 00:41:12 +02:00
Mo
09c3ac02f8 Merge pull request #2062 from jimbo5922/jimbo5922-fix-hashmap3-struct-name
update struct name in hashmap3
2024-08-08 00:40:51 +02:00
Mo
45a39585b3 Merge pull request #2066 from matthewjnield/main
chore: Fix snakecase convention in errors6.rs
2024-08-08 00:36:46 +02:00
mo8it
286a455fa9 Avoid using RUSTFLAGS to not trigger rebuilding, especially in rust-analyzer 2024-08-07 23:35:50 +02:00
mo8it
bdf4960b6a Fix exercise name shift in exercise check 2024-08-07 23:25:22 +02:00
mo8it
13124aafe3 Update deps 2024-08-05 03:15:43 +02:00
Matt Nield
2128be8b28 chore: Fix snakecase convention in errors6.rs
Exercise errors6.rs prompts the user to add a method named `from_parseint`. This commit changes the method name to the corrected snakecase format, `from_parse_int`.
2024-08-04 02:36:45 -04:00
mo8it
175294fa5d Add rust-version 2024-08-02 16:40:06 +02:00
mo8it
5016c7cf7c Use trim_ascii instead of trim 2024-08-02 16:28:05 +02:00
mo8it
1468206052 Stop on first exercise solved 2024-08-02 15:54:14 +02:00
mo8it
d1ff4b5cf0 Remove newline 2024-08-01 19:19:25 +02:00
mo8it
700a065abd Fix rustfmt option 2024-08-01 19:19:14 +02:00
mo8it
3fc462f90f Fix tests 2024-08-01 19:17:40 +02:00
mo8it
65a8f6bb4b Run rustfmt on solutions in dev check 2024-08-01 19:14:09 +02:00
mo8it
e0f0944bff Refactor check_solutions 2024-08-01 15:53:32 +02:00
mo8it
c7590dd752 Improve the runner 2024-08-01 15:23:54 +02:00
mo8it
33a5680328 Hide cargo build warnings if there is no output 2024-08-01 11:28:26 +02:00
mo8it
455d87cadd Fix capacity 2024-08-01 11:26:30 +02:00
Yudai Kawabuchi
e65ae09789 fix format 2024-08-01 09:55:25 +09:00
Yudai Kawabuchi
dacdce1ea2 fix: update struct name in hashmap3 2024-08-01 09:47:50 +09:00
mo8it
766f3c50ec Add hint to run dev check again after dev update 2024-08-01 01:07:56 +02:00
mo8it
802b97b2ed Set stdin to null when running the binary of an exercise 2024-08-01 01:07:31 +02:00
mo8it
2ad408f2b8 Update deps 2024-07-31 18:54:24 +02:00
mo8it
c8fddd8f62 Add Github profile links for every author 2024-07-31 18:53:25 +02:00
mo8it
74fab994e2 Make the output optional 2024-07-28 20:30:23 +02:00
mo8it
3a99542f73 Run the final check in parallel 2024-07-28 17:39:46 +02:00
mo8it
2ae9f3555b Update deps 2024-07-28 13:30:31 +02:00
mo8it
1937b4bf66 Use the rexported crossterm from ratatui 2024-07-25 16:26:48 +02:00
mo8it
8beb290842 Test initialization 2024-07-25 16:14:38 +02:00
mo8it
8fec5155c7 Clean up tests 2024-07-25 15:12:14 +02:00
mo8it
3f49decce9 Remove assert_cmd and predicates 2024-07-25 14:34:43 +02:00
mo8it
e2492f65a0 Update deps 2024-07-25 12:51:44 +02:00
mo8it
5116a812fb tests3: Fix panic message 2024-07-22 12:02:59 +02:00
mo8it
82409c060f Update deps 2024-07-22 12:01:41 +02:00
mo8it
183ed3f88f Update dep 2024-07-17 14:33:29 +02:00
mo8it
447ac3c40b strings3: Improve hint 2024-07-17 14:32:45 +02:00
Mo
96f96927da Merge pull request #2050 from yapjiahong/main
doc: enchane string3 exercise hint
2024-07-17 14:31:17 +02:00
yapjiahong
2c79e29483 doc: enchane string3 exercise hint 2024-07-17 00:43:42 +08:00
mo8it
362473dde0 Sync exercise and solution 2024-07-16 18:21:07 +02:00
Mo
8339682112 Merge pull request #2049 from Vexcited/patch-1
fix: Lyche becomes Lychee
2024-07-16 18:18:46 +02:00
Mikkel ALMONTE--RINGAUD
3f06d767b5 fix: Lyche becomes Lychee
Small typo.
2024-07-16 17:20:26 +02:00
mo8it
2854dc9ab3 Update CI and release hook 2024-07-13 12:32:23 +02:00
mo8it
516fcf9168 Update section 2024-07-13 12:07:52 +02:00
mo8it
12d1971b0d Update section about command not found 2024-07-13 12:02:39 +02:00
mo8it
3e09e509d6 Add section about rustlings not found 2024-07-13 12:00:22 +02:00
mo8it
99fb11cc72 Update syn 2024-07-13 11:53:59 +02:00
mo8it
d176ddd27e Improve TODO comment 2024-07-12 16:29:41 +02:00
74 changed files with 2174 additions and 1743 deletions

View File

@@ -23,7 +23,7 @@ jobs:
with:
globs: "exercises/**/*.md"
- name: Run cargo fmt
run: cargo fmt --all -- --check
run: cargo fmt --all --check
test:
runs-on: ${{ matrix.os }}
strategy:
@@ -33,7 +33,7 @@ jobs:
- uses: actions/checkout@v4
- uses: swatinem/rust-cache@v2
- name: Run cargo test
run: cargo test
run: cargo test --workspace
dev-check:
runs-on: ubuntu-latest
steps:

View File

@@ -2,6 +2,3 @@
extend-exclude = [
"CHANGELOG.md",
]
[default.extend-words]
"ratatui" = "ratatui"

View File

@@ -1,3 +1,62 @@
<a name="6.3.0"></a>
## 6.3.0 (2024-08-29)
### Added
- Add the following exercise lints:
- `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings.
- `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
- `forbid(todo)`: You forgot a `todo!()`.
- `forbid(empty_loop)`: This can only happen by mistake in Rustlings.
- `deny(infinite_loop)`: No infinite loops are needed in Rustlings.
- `deny(mem_forget)`: You shouldn't leak memory while still learning Rust.
- Show a link to every exercise file in the list.
- Add scroll padding in the list.
- Break the help footer of the list into two lines when the terminal width isn't big enough.
- Enable scrolling with the mouse in the list.
- `dev check`: Show the progress of checks.
- `dev check`: Check that the length of all exercise names is lower than 32.
- `dev check`: Check if exercise contains no tests and isn't marked with `test = false`.
### Changed
- The compilation time when installing Rustlings is reduced.
- Pressing `c` in the list for "continue on" now quits the list after setting the selected exercise as the current one.
- Better highlighting of the solution file after an exercise is done.
- Don't show the output of successful tests anymore. Instead, show the pretty output for tests.
- Be explicit about `q` only quitting the list and not the whole program in the list.
- Be explicit about `r` only resetting one exercise (the selected one) in the list.
- Ignore the standard output of `git init`.
- `threads3`: Remove the queue length and improve tests.
- `errors4`: Use match instead of a comparison chain in the solution.
- `functions3`: Only take `u8` to avoid using a too high number of iterations by mistake.
- `dev check`: Always check with strict Clippy (warnings to errors) when checking the solutions.
### Fixed
- Fix the error on some systems about too many open files during the final check of all exercises.
- 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
- Show a message before checking and running an exercise. This gives the user instant feedback and avoids confusion if the checks take too long.
- Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports.
- Add a `README.md` file to the `solutions/` directory.
- Allow initializing Rustlings in a Cargo workspace.
- `dev check`: Check that all solutions are formatted with `rustfmt`.
### Changed
- Remove the state file and the solutions directory from the generated `.gitignore` file.
- Run the final check of all exercises in parallel.
- Small exercise improvements.
<a name="6.1.0"></a>
## 6.1.0 (2024-07-10)

514
Cargo.lock generated
View File

@@ -14,26 +14,11 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "anstream"
version = "0.6.14"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -46,33 +31,33 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.7"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
@@ -84,21 +69,6 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "assert_cmd"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "autocfg"
version = "1.3.0"
@@ -117,32 +87,6 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bstr"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
dependencies = [
"rustversion",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -151,9 +95,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.9"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
@@ -161,9 +105,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.9"
version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [
"anstream",
"anstyle",
@@ -173,9 +117,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.8"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck",
"proc-macro2",
@@ -185,28 +129,15 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "colorchoice"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]]
name = "compact_str"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"ryu",
"static_assertions",
]
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "crossbeam-channel"
@@ -225,15 +156,15 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crossterm"
version = "0.27.0"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.6.0",
"crossterm_winapi",
"libc",
"mio",
"mio 1.0.2",
"parking_lot",
"rustix",
"signal-hook",
"signal-hook-mio",
"winapi",
@@ -248,24 +179,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
version = "1.0.1"
@@ -273,24 +186,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "filetime"
version = "0.2.23"
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.4.1",
"windows-sys 0.52.0",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
name = "fastrand"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"num-traits",
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
@@ -307,10 +227,6 @@ name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "heck"
@@ -319,10 +235,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.2.6"
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "indexmap"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
dependencies = [
"equivalent",
"hashbrown",
@@ -350,18 +272,9 @@ dependencies = [
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
@@ -391,9 +304,26 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.155"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"libc",
"redox_syscall",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
@@ -411,15 +341,6 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lru"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
dependencies = [
"hashbrown",
]
[[package]]
name = "memchr"
version = "2.7.4"
@@ -439,10 +360,17 @@ dependencies = [
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
name = "mio"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"libc",
"log",
"wasi",
"windows-sys 0.52.0",
]
[[package]]
name = "notify"
@@ -458,7 +386,7 @@ dependencies = [
"kqueue",
"libc",
"log",
"mio",
"mio 0.8.11",
"walkdir",
"windows-sys 0.48.0",
]
@@ -473,15 +401,6 @@ dependencies = [
"notify",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
@@ -490,12 +409,12 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "os_pipe"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209"
checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -516,47 +435,11 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.2",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "predicates"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8"
dependencies = [
"anstyle",
"difflib",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
[[package]]
name = "predicates-tree"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
@@ -568,115 +451,61 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.36"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ratatui"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3"
dependencies = [
"bitflags 2.6.0",
"cassowary",
"compact_str",
"crossterm",
"itertools",
"lru",
"paste",
"stability",
"strum",
"strum_macros",
"unicode-segmentation",
"unicode-truncate",
"unicode-width",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "regex"
version = "1.10.5"
name = "rustix"
version = "0.38.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "rustlings"
version = "6.1.0"
version = "6.3.0"
dependencies = [
"ahash",
"anyhow",
"assert_cmd",
"clap",
"crossterm",
"hashbrown",
"notify-debouncer-mini",
"os_pipe",
"predicates",
"ratatui",
"rustlings-macros",
"serde",
"serde_json",
"tempfile",
"toml_edit",
]
[[package]]
name = "rustlings-macros"
version = "6.1.0"
version = "6.3.0"
dependencies = [
"quote",
"serde",
"toml_edit",
]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ryu"
version = "1.0.18"
@@ -700,18 +529,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.204"
version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.204"
version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [
"proc-macro2",
"quote",
@@ -720,20 +549,21 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.120"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.6"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [
"serde",
]
@@ -750,12 +580,12 @@ dependencies = [
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio",
"mio 1.0.2",
"signal-hook",
]
@@ -774,55 +604,17 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "stability"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.70"
version = "2.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
dependencies = [
"proc-macro2",
"quote",
@@ -830,25 +622,32 @@ dependencies = [
]
[[package]]
name = "termtree"
version = "0.4.1"
name = "tempfile"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
dependencies = [
"cfg-if",
"fastrand",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "toml_datetime"
version = "0.6.6"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.15"
version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap",
"serde",
@@ -863,29 +662,6 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-truncate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "unicode-width"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -894,18 +670,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.4"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
@@ -941,11 +708,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -972,6 +739,15 @@ 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"
@@ -1095,9 +871,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.13"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [
"memchr",
]

View File

@@ -1,27 +1,26 @@
[workspace]
resolver = "2"
exclude = [
"tests/fixture/failure",
"tests/fixture/state",
"tests/fixture/success",
"tests/test_exercises",
"dev",
]
[workspace.package]
version = "6.1.0"
version = "6.3.0"
authors = [
"Liv <mokou@fastmail.com>",
"Mo Bitar <mo8it@proton.me>",
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
# Alumni
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>",
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>", # https://github.com/carols10cents
]
repository = "https://github.com/rust-lang/rustlings"
license = "MIT"
edition = "2021"
edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions.
rust-version = "1.80"
[workspace.dependencies]
serde = { version = "1.0.204", features = ["derive"] }
toml_edit = { version = "0.22.15", default-features = false, features = ["parse", "serde"] }
serde = { version = "1.0.209", features = ["derive"] }
toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] }
[package]
name = "rustlings"
@@ -31,6 +30,7 @@ authors.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
keywords = [
"exercise",
"learning",
@@ -46,21 +46,19 @@ include = [
]
[dependencies]
ahash = { version = "0.8.11", default-features = false }
anyhow = "1.0.86"
clap = { version = "4.5.9", features = ["derive"] }
crossterm = "0.27.0"
hashbrown = "0.14.5"
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.0"
ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] }
rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" }
serde_json = "1.0.120"
os_pipe = "1.2.1"
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
serde_json = "1.0.127"
serde.workspace = true
toml_edit.workspace = true
[dev-dependencies]
assert_cmd = "2.0.14"
predicates = "3.1.0"
tempfile = "3.12.0"
[profile.release]
panic = "abort"
@@ -70,3 +68,21 @@ panic = "abort"
[package.metadata.release]
pre-release-hook = ["./release-hook.sh"]
[workspace.lints.rust]
unsafe_code = "forbid"
unstable_features = "forbid"
[workspace.lints.clippy]
empty_loop = "forbid"
disallowed-types = "deny"
disallowed-methods = "deny"
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

View File

@@ -17,7 +17,7 @@ It contains code examples and exercises similar to Rustlings, but online.
### Installing Rust
Before installing Rustlings, you need to have _Rust installed_.
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.
@@ -53,6 +53,21 @@ After installing Rustlings, run the following command to initialize the `rustlin
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
@@ -73,8 +88,6 @@ While working with Rustlings, please use a modern terminal for the best user exp
The default terminal on Linux and Mac should be sufficient.
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
If you use VS Code, the builtin terminal should also be fine.
## Doing exercises
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.

13
clippy.toml Normal file
View File

@@ -0,0 +1,13 @@
disallowed-types = [
# Inefficient. Use `.queue(…)` instead.
"crossterm::style::Stylize",
"crossterm::style::styled_content::StyledContent",
]
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",
]

View File

@@ -195,3 +195,25 @@ name = "exercises"
edition = "2021"
# Don't publish the exercises on crates.io!
publish = false
[profile.release]
panic = "abort"
[profile.dev]
panic = "abort"
[lints.rust]
# 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
unstable_features = "forbid"
[lints.clippy]
# You forgot a `todo!()`
todo = "forbid"
# This can only happen by mistake in Rustlings
empty_loop = "forbid"
# No infinite loops are needed in Rustlings
infinite_loop = "deny"
# You shouldn't leak memory while still learning Rust
mem_forget = "deny"

View File

@@ -1,4 +1,4 @@
// TODO: We sometimes encourage you to keep trying things on a given exercise,
// TODO: We sometimes encourage you to keep trying things on a given exercise
// even after you already figured it out. If you got everything working and feel
// ready for the next exercise, enter `n` in the terminal.
//
@@ -6,8 +6,7 @@
// Try adding a new `println!` and check the updated output in the terminal.
fn main() {
println!("Hello and");
println!(r#" welcome to... "#);
println!(r#" Welcome to... "#);
println!(r#" _ _ _ "#);
println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#);
println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#);

View File

@@ -1,5 +1,5 @@
fn main() {
// TODO: Add missing keyword.
// TODO: Add the missing keyword.
x = 5;
println!("x has the value {x}");

View File

@@ -1,4 +1,4 @@
fn call_me(num: u32) {
fn call_me(num: u8) {
for i in 0..num {
println!("Ring! Call number {}", i + 1);
}

View File

@@ -6,7 +6,7 @@
// must add fruit to the basket so that there is at least one of each kind and
// more than 11 in total - we have a lot of mouths to feed. You are not allowed
// to insert any more of the fruits that are already in the basket (Apple,
// Mango, and Lyche).
// Mango, and Lychee).
use std::collections::HashMap;

View File

@@ -10,12 +10,12 @@ use std::collections::HashMap;
// A structure to store the goal details of a team.
#[derive(Default)]
struct Team {
struct TeamScores {
goals_scored: u8,
goals_conceded: u8,
}
fn build_scores_table(results: &str) -> HashMap<&str, Team> {
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();

View File

@@ -6,7 +6,7 @@
// of `Option<String>`.
fn generate_nametag_text(name: String) -> Option<String> {
if name.is_empty() {
// Empty names aren't allowed.
// Empty names aren't allowed
None
} else {
Some(format!("Hi! My name is {name}"))

View File

@@ -1,5 +1,3 @@
#![allow(clippy::comparison_chain)]
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,

View File

@@ -25,7 +25,7 @@ impl ParsePosNonzeroError {
}
// TODO: Add another error conversion function here.
// fn from_parseint(???) -> Self { ??? }
// fn from_parse_int(???) -> Self { ??? }
}
#[derive(PartialEq, Debug)]

View File

@@ -9,7 +9,7 @@ impl Rectangle {
if width <= 0 || height <= 0 {
// Returning a `Result` would be better here. But we want to learn
// how to test functions that can panic.
panic!("Rectangle width and height can't be negative");
panic!("Rectangle width and height must be positive");
}
Rectangle { width, height }

View File

@@ -1,7 +1,6 @@
use std::{sync::mpsc, thread, time::Duration};
struct Queue {
length: u32,
first_half: Vec<u32>,
second_half: Vec<u32>,
}
@@ -9,7 +8,6 @@ struct Queue {
impl Queue {
fn new() -> Self {
Self {
length: 10,
first_half: vec![1, 2, 3, 4, 5],
second_half: vec![6, 7, 8, 9, 10],
}
@@ -48,17 +46,15 @@ mod tests {
fn threads3() {
let (tx, rx) = mpsc::channel();
let queue = Queue::new();
let queue_length = queue.length;
send_tx(queue, tx);
let mut total_received: u32 = 0;
for received in rx {
println!("Got: {received}");
total_received += 1;
let mut received = Vec::with_capacity(10);
for value in rx {
received.push(value);
}
println!("Number of received values: {total_received}");
assert_eq!(total_received, queue_length);
received.sort();
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}
}

View File

@@ -26,7 +26,7 @@ enum Command {
mod my_module {
use super::Command;
// TODO: Complete the function.
// TODO: Complete the function as described above.
// pub fn transformer(input: ???) -> ??? { ??? }
}

View File

@@ -3,7 +3,11 @@
# Error out if any command fails
set -e
cargo run -- dev check
typos
cargo outdated -w --exit-code 1
cargo upgrades
# Similar to CI
cargo clippy -- --deny warnings
cargo fmt --all --check
cargo test --workspace --all-targets
cargo run -- dev check --require-solutions

View File

@@ -6,6 +6,7 @@ authors.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
include = [
"/src/",
"/info.toml",
@@ -15,6 +16,9 @@ include = [
proc-macro = true
[dependencies]
quote = "1.0.36"
quote = "1.0.37"
serde.workspace = true
toml_edit.workspace = true
[lints]
workspace = true

View File

@@ -498,7 +498,10 @@ some of them:
https://doc.rust-lang.org/std/string/struct.String.html#method.trim
For the `compose_me` method: You can either use the `format!` macro, or convert
the string slice into an owned string, which you can then freely extend."""
the string slice into an owned string, which you can then freely extend.
For the `replace_me` method, you can check out the `replace` method:
https://doc.rust-lang.org/std/string/struct.String.html#method.replace"""
[[exercises]]
name = "strings4"
@@ -568,7 +571,7 @@ 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 `Team` if a team doesn't
`HashMap` to insert the default value of `TeamScores` if a team doesn't
exist in the table yet.
Learn more in The Book:

View File

@@ -1,4 +1,4 @@
fn call_me(num: u32) {
fn call_me(num: u8) {
for i in 0..num {
println!("Ring! Call number {}", i + 1);
}

View File

@@ -18,12 +18,11 @@ fn main() {
// Here, both answers work.
// `.into()` converts a type into an expected type.
// If it is called where `String` is expected, it will convert `&str` to `String`.
// But if is called where `&str` is expected, then `&str` is kept `&str` since no
// conversion is needed.
string("nice weather".into());
// But if it is called where `&str` is expected, then `&str` is kept `&str` since no conversion is needed.
// If you remove the `#[allow(…)]` line, then Clippy will tell you to remove `.into()` below since it is a useless conversion.
#[allow(clippy::useless_conversion)]
string_slice("nice weather".into());
// ^^^^^^^ the compiler recommends removing the `.into()`
// call because it is a useless conversion.
string(format!("Interpolation {}", "Station"));

View File

@@ -5,7 +5,8 @@
// Apple (4), Mango (2) and Lychee (5) are already in the basket hash map. You
// must add fruit to the basket so that there is at least one of each kind and
// more than 11 in total - we have a lot of mouths to feed. You are not allowed
// to insert any more of these fruits!
// to insert any more of the fruits that are already in the basket (Apple,
// Mango, and Lychee).
use std::collections::HashMap;

View File

@@ -10,12 +10,12 @@ use std::collections::HashMap;
// A structure to store the goal details of a team.
#[derive(Default)]
struct Team {
struct TeamScores {
goals_scored: u8,
goals_conceded: u8,
}
fn build_scores_table(results: &str) -> HashMap<&str, Team> {
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();
@@ -28,13 +28,17 @@ fn build_scores_table(results: &str) -> HashMap<&str, Team> {
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(Team::default);
let team_1 = scores
.entry(team_1_name)
.or_insert_with(TeamScores::default);
// Update the values.
team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score;
// Similarely for the second team.
let team_2 = scores.entry(team_2_name).or_insert_with(Team::default);
// Similarly for the second team.
let team_2 = scores
.entry(team_2_name)
.or_insert_with(TeamScores::default);
team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score;
}

View File

@@ -1,4 +1,4 @@
#![allow(clippy::comparison_chain)]
use std::cmp::Ordering;
#[derive(PartialEq, Debug)]
enum CreationError {
@@ -11,12 +11,10 @@ struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
if value == 0 {
Err(CreationError::Zero)
} else if value < 0 {
Err(CreationError::Negative)
} else {
Ok(Self(value as u64))
match value.cmp(&0) {
Ordering::Less => Err(CreationError::Negative),
Ordering::Equal => Err(CreationError::Zero),
Ordering::Greater => Ok(Self(value as u64)),
}
}
}

View File

@@ -24,7 +24,7 @@ impl ParsePosNonzeroError {
Self::Creation(err)
}
fn from_parseint(err: ParseIntError) -> Self {
fn from_parse_int(err: ParseIntError) -> Self {
Self::ParseInt(err)
}
}
@@ -44,7 +44,7 @@ impl PositiveNonzeroInteger {
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
// Return an appropriate error instead of panicking when `parse()`
// returns an error.
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?;
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Self::new(x).map_err(ParsePosNonzeroError::from_creation)
}

View File

@@ -9,7 +9,7 @@ impl Rectangle {
if width <= 0 || height <= 0 {
// Returning a `Result` would be better here. But we want to learn
// how to test functions that can panic.
panic!("Rectangle width and height can't be negative");
panic!("Rectangle width and height must be positive");
}
Rectangle { width, height }

View File

@@ -25,6 +25,7 @@ fn factorial_fold(num: u64) -> u64 {
// -> 1 * 2 is calculated, then the result 2 is multiplied by
// the second element 3 so the result 6 is returned.
// And so on…
#[allow(clippy::unnecessary_fold)]
(2..=num).fold(1, |acc, x| acc * x)
}

View File

@@ -1,7 +1,6 @@
use std::{sync::mpsc, thread, time::Duration};
struct Queue {
length: u32,
first_half: Vec<u32>,
second_half: Vec<u32>,
}
@@ -9,7 +8,6 @@ struct Queue {
impl Queue {
fn new() -> Self {
Self {
length: 10,
first_half: vec![1, 2, 3, 4, 5],
second_half: vec![6, 7, 8, 9, 10],
}
@@ -50,17 +48,15 @@ mod tests {
fn threads3() {
let (tx, rx) = mpsc::channel();
let queue = Queue::new();
let queue_length = queue.length;
send_tx(queue, tx);
let mut total_received: u32 = 0;
for received in rx {
println!("Got: {received}");
total_received += 1;
let mut received = Vec::with_capacity(10);
for value in rx {
received.push(value);
}
println!("Number of received values: {total_received}");
assert_eq!(total_received, queue_length);
received.sort();
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}
}

6
solutions/README.md Normal file
View File

@@ -0,0 +1,6 @@
# Official Rustlings solutions
Before you finish an exercise, its solution file will only contain an empty `main` function.
The content of this file will be automatically replaced by the actual solution once you finish the exercise.
Note that these solutions are often only _one possibility_ to solve an exercise.

View File

@@ -1,23 +1,23 @@
use anyhow::{bail, Context, Result};
use crossterm::style::Stylize;
use serde::Deserialize;
use std::{
fs::{self, File},
io::{Read, StdoutLock, Write},
path::{Path, PathBuf},
env,
fs::{File, OpenOptions},
io::{self, Read, Seek, StdoutLock, Write},
path::Path,
process::{Command, Stdio},
thread,
};
use crate::{
clear_terminal,
cmd::CmdRunner,
collections::hash_set_with_capacity,
embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY},
exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo,
DEBUG_PROFILE,
};
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
#[must_use]
pub enum ExercisesProgress {
@@ -34,29 +34,10 @@ pub enum StateFileStatus {
NotRead,
}
// Parses parts of the output of `cargo metadata`.
#[derive(Deserialize)]
struct CargoMetadata {
target_directory: PathBuf,
}
pub fn parse_target_dir() -> Result<PathBuf> {
// Get the target directory from Cargo.
let metadata_output = Command::new("cargo")
.arg("metadata")
.arg("-q")
.arg("--format-version")
.arg("1")
.arg("--no-deps")
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.output()
.context(CARGO_METADATA_ERR)?
.stdout;
serde_json::de::from_slice::<CargoMetadata>(&metadata_output)
.context("Failed to read the field `target_directory` from the `cargo metadata` output")
.map(|metadata| metadata.target_directory)
enum AllExercisesCheck {
Pending(usize),
AllDone,
CheckedUntil(usize),
}
pub struct AppState {
@@ -65,67 +46,32 @@ pub struct AppState {
// Caches the number of done exercises to avoid iterating over all exercises every time.
n_done: u16,
final_message: String,
state_file: File,
// Preallocated buffer for reading and writing the state file.
file_buf: Vec<u8>,
official_exercises: bool,
// Cargo's target directory.
target_dir: PathBuf,
cmd_runner: CmdRunner,
// Running in VS Code.
vs_code: bool,
}
impl AppState {
// Update the app state from the state file.
fn update_from_file(&mut self) -> StateFileStatus {
self.file_buf.clear();
self.n_done = 0;
if File::open(STATE_FILE_NAME)
.and_then(|mut file| file.read_to_end(&mut self.file_buf))
.is_err()
{
return StateFileStatus::NotRead;
}
// See `Self::write` for more information about the file format.
let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2);
let Some(current_exercise_name) = lines.next() else {
return StateFileStatus::NotRead;
};
if current_exercise_name.is_empty() || lines.next().is_none() {
return StateFileStatus::NotRead;
}
let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len());
for done_exerise_name in lines {
if done_exerise_name.is_empty() {
break;
}
done_exercises.insert(done_exerise_name);
}
for (ind, exercise) in self.exercises.iter_mut().enumerate() {
if done_exercises.contains(exercise.name.as_bytes()) {
exercise.done = true;
self.n_done += 1;
}
if exercise.name.as_bytes() == current_exercise_name {
self.current_exercise_ind = ind;
}
}
StateFileStatus::Read
}
pub fn new(
exercise_infos: Vec<ExerciseInfo>,
final_message: String,
) -> Result<(Self, StateFileStatus)> {
let target_dir = parse_target_dir()?;
let cmd_runner = CmdRunner::build()?;
let mut state_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(STATE_FILE_NAME)
.with_context(|| {
format!("Failed to open or create the state file {STATE_FILE_NAME}")
})?;
let exercises = exercise_infos
let mut exercises = exercise_infos
.into_iter()
.map(|exercise_info| {
// Leaking to be able to borrow in the watch mode `Table`.
@@ -134,8 +80,7 @@ impl AppState {
let path = exercise_info.path().leak();
let name = exercise_info.name.leak();
let dir = exercise_info.dir.map(|dir| &*dir.leak());
let hint = exercise_info.hint.trim().to_owned();
let hint = exercise_info.hint.leak().trim_ascii();
Exercise {
dir,
@@ -144,23 +89,68 @@ impl AppState {
test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy,
hint,
// Updated in `Self::update_from_file`.
// Updated below.
done: false,
}
})
.collect::<Vec<_>>();
let mut slf = Self {
current_exercise_ind: 0,
exercises,
n_done: 0,
final_message,
file_buf: Vec::with_capacity(2048),
official_exercises: !Path::new("info.toml").exists(),
target_dir,
let mut current_exercise_ind = 0;
let mut n_done = 0;
let mut file_buf = Vec::with_capacity(2048);
let state_file_status = 'block: {
if state_file.read_to_end(&mut file_buf).is_err() {
break 'block StateFileStatus::NotRead;
}
// See `Self::write` for more information about the file format.
let mut lines = file_buf.split(|c| *c == b'\n').skip(2);
let Some(current_exercise_name) = lines.next() else {
break 'block StateFileStatus::NotRead;
};
if current_exercise_name.is_empty() || lines.next().is_none() {
break 'block StateFileStatus::NotRead;
}
let mut done_exercises = hash_set_with_capacity(exercises.len());
for done_exerise_name in lines {
if done_exerise_name.is_empty() {
break;
}
done_exercises.insert(done_exerise_name);
}
for (ind, exercise) in exercises.iter_mut().enumerate() {
if done_exercises.contains(exercise.name.as_bytes()) {
exercise.done = true;
n_done += 1;
}
if exercise.name.as_bytes() == current_exercise_name {
current_exercise_ind = ind;
}
}
StateFileStatus::Read
};
let state_file_status = slf.update_from_file();
file_buf.clear();
file_buf.extend_from_slice(STATE_FILE_HEADER);
let slf = Self {
current_exercise_ind,
exercises,
n_done,
final_message,
state_file,
file_buf,
official_exercises: !Path::new("info.toml").exists(),
cmd_runner,
vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"),
};
Ok((slf, state_file_status))
}
@@ -186,8 +176,13 @@ impl AppState {
}
#[inline]
pub fn target_dir(&self) -> &Path {
&self.target_dir
pub fn cmd_runner(&self) -> &CmdRunner {
&self.cmd_runner
}
#[inline]
pub fn vs_code(&self) -> bool {
self.vs_code
}
// Write the state file.
@@ -199,10 +194,8 @@ impl AppState {
// - The fourth line is an empty line.
// - All remaining lines are the names of done exercises.
fn write(&mut self) -> Result<()> {
self.file_buf.clear();
self.file_buf.truncate(STATE_FILE_HEADER.len());
self.file_buf
.extend_from_slice(b"DON'T EDIT THIS FILE!\n\n");
self.file_buf
.extend_from_slice(self.current_exercise().name.as_bytes());
self.file_buf.push(b'\n');
@@ -214,7 +207,14 @@ impl AppState {
}
}
fs::write(STATE_FILE_NAME, &self.file_buf)
self.state_file
.rewind()
.with_context(|| format!("Failed to rewind the state file {STATE_FILE_NAME}"))?;
self.state_file
.set_len(0)
.with_context(|| format!("Failed to truncate the state file {STATE_FILE_NAME}"))?;
self.state_file
.write_all(&self.file_buf)
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
Ok(())
@@ -298,6 +298,7 @@ impl AppState {
Ok(exercise.path)
}
// Reset the exercise by index and return its name.
pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
if exercise_ind >= self.exercises.len() {
bail!(BAD_INDEX_ERR);
@@ -307,36 +308,33 @@ impl AppState {
let exercise = &self.exercises[exercise_ind];
self.reset(exercise_ind, exercise.path)?;
Ok(exercise.path)
Ok(exercise.name)
}
// Return the index of the next pending exercise or `None` if all exercises are done.
fn next_pending_exercise_ind(&self) -> Option<usize> {
if self.current_exercise_ind == self.exercises.len() - 1 {
// The last exercise is done.
// Search for exercises not done from the start.
return self.exercises[..self.current_exercise_ind]
.iter()
.position(|exercise| !exercise.done);
}
// The done exercise isn't the last one.
// Search for a pending exercise after the current one and then from the start.
match self.exercises[self.current_exercise_ind + 1..]
.iter()
.position(|exercise| !exercise.done)
{
Some(ind) => Some(self.current_exercise_ind + 1 + ind),
None => self.exercises[..self.current_exercise_ind]
.iter()
.position(|exercise| !exercise.done),
}
let next_ind = self.current_exercise_ind + 1;
self.exercises
// If the exercise done isn't the last, search for pending exercises after it.
.get(next_ind..)
.and_then(|later_exercises| {
later_exercises
.iter()
.position(|exercise| !exercise.done)
.map(|ind| next_ind + ind)
})
// Search from the start.
.or_else(|| {
self.exercises[..self.current_exercise_ind]
.iter()
.position(|exercise| !exercise.done)
})
}
/// 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.
pub fn current_solution_path(&self) -> Result<Option<String>> {
if DEBUG_PROFILE {
if cfg!(debug_assertions) {
return Ok(None);
}
@@ -347,24 +345,72 @@ impl AppState {
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
.map(Some)
} else {
let solution_path = if let Some(dir) = current_exercise.dir {
format!("solutions/{dir}/{}.rs", current_exercise.name)
} else {
format!("solutions/{}.rs", current_exercise.name)
};
let sol_path = current_exercise.sol_path();
if Path::new(&solution_path).exists() {
return Ok(Some(solution_path));
if Path::new(&sol_path).exists() {
return Ok(Some(sol_path));
}
Ok(None)
}
}
// 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();
let status = thread::scope(|s| {
let handles = self
.exercises
.iter()
.map(|exercise| s.spawn(|| exercise.run_exercise(None, &self.cmd_runner)))
.collect::<Vec<_>>();
for (exercise_ind, handle) in handles.into_iter().enumerate() {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
stdout.flush()?;
let Ok(success) = handle.join().unwrap() else {
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
};
if !success {
return Ok(AllExercisesCheck::Pending(exercise_ind));
}
}
Ok::<_, io::Error>(AllExercisesCheck::AllDone)
})?;
let mut exercise_ind = match status {
AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)),
AllExercisesCheck::AllDone => return Ok(None),
AllExercisesCheck::CheckedUntil(ind) => ind,
};
// 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));
}
exercise_ind += 1;
}
Ok(None)
}
/// 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, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
pub fn done_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
let exercise = &mut self.exercises[self.current_exercise_ind];
if !exercise.done {
exercise.done = true;
@@ -373,62 +419,42 @@ impl AppState {
if let Some(ind) = self.next_pending_exercise_ind() {
self.set_current_exercise_ind(ind)?;
return Ok(ExercisesProgress::NewPending);
}
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? {
stdout.write_all(b"\n\n")?;
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
for (exercise_ind, exercise) in self.exercises().iter().enumerate() {
write!(writer, "Running {exercise} ... ")?;
writer.flush()?;
let success = exercise.run_exercise(&mut output, &self.target_dir)?;
if !success {
writeln!(writer, "{}\n", "FAILED".red())?;
self.current_exercise_ind = exercise_ind;
// No check if the exercise is done before setting it to pending
// because no pending exercise was found.
self.exercises[exercise_ind].done = false;
self.n_done -= 1;
self.write()?;
return Ok(ExercisesProgress::NewPending);
}
writeln!(writer, "{}", "ok".green())?;
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()?;
clear_terminal(writer)?;
writer.write_all(FENISH_LINE.as_bytes())?;
clear_terminal(stdout)?;
stdout.write_all(FENISH_LINE.as_bytes())?;
let final_message = self.final_message.trim();
let final_message = self.final_message.trim_ascii();
if !final_message.is_empty() {
writer.write_all(final_message.as_bytes())?;
writer.write_all(b"\n")?;
stdout.write_all(final_message.as_bytes())?;
stdout.write_all(b"\n")?;
}
Ok(ExercisesProgress::AllDone)
}
}
const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …`
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.";
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! |
+-------------------------- ------------------------+
@@ -462,7 +488,7 @@ mod tests {
path: "exercises/0.rs",
test: false,
strict_clippy: false,
hint: String::new(),
hint: "",
done: false,
}
}
@@ -474,9 +500,11 @@ mod tests {
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
n_done: 0,
final_message: String::new(),
state_file: tempfile::tempfile().unwrap(),
file_buf: Vec::new(),
official_exercises: true,
target_dir: PathBuf::new(),
cmd_runner: CmdRunner::build().unwrap(),
vs_code: false,
};
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {

View File

@@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use std::path::Path;
use crate::info_file::ExerciseInfo;
use crate::{exercise::RunnableExercise, info_file::ExerciseInfo};
/// Initial capacity of the bins buffer.
pub const BINS_BUFFER_CAPACITY: usize = 1 << 14;

View File

@@ -1,30 +1,44 @@
use anyhow::{Context, Result};
use std::{io::Read, path::Path, process::Command};
use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::{
io::Read,
path::PathBuf,
process::{Command, Stdio},
};
/// Run a command with a description for a possible error and append the merged stdout and stderr.
/// The boolean in the returned `Result` is true if the command's exit status is success.
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> {
let (mut reader, writer) = os_pipe::pipe()
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?;
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
let spawn = |mut cmd: Command| {
// NOTE: The closure drops `cmd` which prevents a pipe deadlock.
cmd.stdin(Stdio::null())
.spawn()
.with_context(|| format!("Failed to run the command `{description}`"))
};
let writer_clone = writer.try_clone().with_context(|| {
format!("Failed to clone the pipe writer for the command `{description}`")
})?;
let mut handle = if let Some(output) = output {
let (mut reader, writer) = os_pipe::pipe().with_context(|| {
format!("Failed to create a pipe to run the command `{description}``")
})?;
let mut handle = cmd
.stdout(writer_clone)
.stderr(writer)
.spawn()
.with_context(|| format!("Failed to run the command `{description}`"))?;
let writer_clone = writer.try_clone().with_context(|| {
format!("Failed to clone the pipe writer for the command `{description}`")
})?;
// Prevent pipe deadlock.
drop(cmd);
cmd.stdout(writer_clone).stderr(writer);
let handle = spawn(cmd)?;
reader
.read_to_end(output)
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
reader
.read_to_end(output)
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
output.push(b'\n');
output.push(b'\n');
handle
} else {
cmd.stdout(Stdio::null()).stderr(Stdio::null());
spawn(cmd)?
};
handle
.wait()
@@ -32,50 +46,106 @@ pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Res
.map(|status| status.success())
}
pub struct CargoCmd<'a> {
pub subcommand: &'a str,
pub args: &'a [&'a str],
pub bin_name: &'a str,
pub description: &'a str,
/// RUSTFLAGS="-A warnings"
pub hide_warnings: bool,
/// Added as `--target-dir` if `Self::dev` is true.
pub target_dir: &'a Path,
/// The output buffer to append the merged stdout and stderr.
pub output: &'a mut Vec<u8>,
/// true while developing Rustlings.
pub dev: bool,
// Parses parts of the output of `cargo metadata`.
#[derive(Deserialize)]
struct CargoMetadata {
target_directory: PathBuf,
}
impl<'a> CargoCmd<'a> {
/// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
pub fn run(&mut self) -> Result<bool> {
pub struct CmdRunner {
target_dir: PathBuf,
}
impl CmdRunner {
pub fn build() -> Result<Self> {
// Get the target directory from Cargo.
let metadata_output = Command::new("cargo")
.arg("metadata")
.arg("-q")
.arg("--format-version")
.arg("1")
.arg("--no-deps")
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.output()
.context(CARGO_METADATA_ERR)?;
if !metadata_output.status.success() {
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
}
let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout)
.context(
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
)?;
Ok(Self {
target_dir: metadata.target_directory,
})
}
pub fn cargo<'out>(
&self,
subcommand: &str,
bin_name: &str,
output: Option<&'out mut Vec<u8>>,
) -> CargoSubcommand<'out> {
let mut cmd = Command::new("cargo");
cmd.arg(self.subcommand);
cmd.arg(subcommand).arg("-q").arg("--bin").arg(bin_name);
// A hack to make `cargo run` work when developing Rustlings.
if self.dev {
cmd.arg("--manifest-path")
.arg("dev/Cargo.toml")
.arg("--target-dir")
.arg(self.target_dir);
#[cfg(debug_assertions)]
cmd.arg("--manifest-path")
.arg("dev/Cargo.toml")
.arg("--target-dir")
.arg(&self.target_dir);
if output.is_some() {
cmd.arg("--color").arg("always");
}
cmd.arg("--color")
.arg("always")
.arg("-q")
.arg("--bin")
.arg(self.bin_name)
.args(self.args);
CargoSubcommand { cmd, output }
}
if self.hide_warnings {
cmd.env("RUSTFLAGS", "-A warnings");
}
/// The boolean in the returned `Result` is true if the command's exit status is success.
pub fn run_debug_bin(&self, bin_name: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
// 7 = "/debug/".len()
let mut bin_path =
PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len());
bin_path.push(&self.target_dir);
bin_path.push("debug");
bin_path.push(bin_name);
run_cmd(cmd, self.description, self.output)
run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)
}
}
pub struct CargoSubcommand<'out> {
cmd: Command,
output: Option<&'out mut Vec<u8>>,
}
impl<'out> CargoSubcommand<'out> {
#[inline]
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = &'arg str>,
{
self.cmd.args(args);
self
}
/// The boolean in the returned `Result` is true if the command's exit status is success.
#[inline]
pub fn run(self, description: &str) -> Result<bool> {
run_cmd(self.cmd, description, self.output)
}
}
const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …`
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.";
#[cfg(test)]
mod tests {
use super::*;
@@ -86,7 +156,7 @@ mod tests {
cmd.arg("Hello");
let mut output = Vec::with_capacity(8);
run_cmd(cmd, "echo …", &mut output).unwrap();
run_cmd(cmd, "echo …", Some(&mut output)).unwrap();
assert_eq!(output, b"Hello\n\n");
}

10
src/collections.rs Normal file
View File

@@ -0,0 +1,10 @@
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())
}

View File

@@ -2,8 +2,6 @@ use anyhow::{bail, Context, Result};
use clap::Subcommand;
use std::path::PathBuf;
use crate::DEBUG_PROFILE;
mod check;
mod new;
mod update;
@@ -32,7 +30,7 @@ impl DevCommands {
pub fn run(self) -> Result<()> {
match self {
Self::New { path, no_git } => {
if DEBUG_PROFILE {
if cfg!(debug_assertions) {
bail!("Disabled in the debug build");
}

View File

@@ -1,56 +1,59 @@
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, bail, Context, Error, Result};
use std::{
cmp::Ordering,
fs::{self, read_dir, OpenOptions},
io::{self, Read, Write},
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Mutex,
},
process::{Command, Stdio},
thread,
};
use crate::{
app_state::parse_target_dir,
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, DEBUG_PROFILE,
CURRENT_FORMAT_VERSION,
};
const MAX_EXERCISE_NAME_LEN: usize = 32;
// Find a char that isn't allowed in the exercise's `name` or `dir`.
fn forbidden_char(input: &str) -> Option<char> {
input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
}
// Check that the Cargo.toml file is up-to-date.
// Check that the `Cargo.toml` file is up-to-date.
fn check_cargo_toml(
exercise_infos: &[ExerciseInfo],
current_cargo_toml: &str,
cargo_toml_path: &str,
exercise_path_prefix: &[u8],
) -> Result<()> {
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
let current_cargo_toml = fs::read_to_string(cargo_toml_path)
.with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?;
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(&current_cargo_toml)?;
let old_bins = &current_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind];
let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY);
append_bins(&mut new_bins, exercise_infos, exercise_path_prefix);
if old_bins != new_bins {
if DEBUG_PROFILE {
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it");
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 `Cargo.toml` is outdated. Please run `rustlings dev update` to update it");
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
}
Ok(())
}
// Check the info of all exercises and return their paths in a set.
fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<PathBuf>> {
let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len());
let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len());
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 file_buf = String::with_capacity(1 << 14);
for exercise_info in &info_file.exercises {
@@ -58,6 +61,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
if name.is_empty() {
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}");
}
if let Some(c) = forbidden_char(name) {
bail!("Char `{c}` in the exercise name `{name}` is not allowed");
}
@@ -71,7 +77,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
}
}
if exercise_info.hint.trim().is_empty() {
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");
}
@@ -96,7 +102,12 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
}
if !exercise_info.test && file_buf.contains("#[test]") {
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");
}
} 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");
}
@@ -111,10 +122,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
// Check `dir` for unexpected files.
// Only Rust files in `allowed_rust_files` and `README.md` files are allowed.
// Only one level of directory nesting is allowed.
fn check_unexpected_files(
dir: &str,
allowed_rust_files: &hashbrown::HashSet<PathBuf>,
) -> Result<()> {
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())
};
@@ -162,135 +170,191 @@ fn check_unexpected_files(
Ok(())
}
fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
let error_occurred = AtomicBool::new(false);
fn check_exercises_unsolved(
info_file: &'static InfoFile,
cmd_runner: &'static CmdRunner,
) -> Result<()> {
let mut stdout = io::stdout().lock();
stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?;
println!(
"Running all exercises to check that they aren't already solved. This may take a while…\n",
);
thread::scope(|s| {
for exercise_info in &info_file.exercises {
let handles = info_file
.exercises
.iter()
.filter_map(|exercise_info| {
if exercise_info.skip_check_unsolved {
continue;
return None;
}
s.spawn(|| {
let error = |e| {
let mut stderr = io::stderr().lock();
stderr.write_all(e).unwrap();
stderr.write_all(b"\nProblem with the exercise ").unwrap();
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
stderr.write_all(SEPARATOR).unwrap();
error_occurred.store(true, atomic::Ordering::Relaxed);
};
Some((
exercise_info.name.as_str(),
thread::spawn(|| exercise_info.run_exercise(None, cmd_runner)),
))
})
.collect::<Vec<_>>();
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
match exercise_info.run_exercise(&mut output, target_dir) {
Ok(true) => error(b"Already solved!"),
Ok(false) => (),
Err(e) => error(e.to_string().as_bytes()),
}
});
let n_handles = handles.len();
write!(stdout, "Progress: 0/{n_handles}")?;
stdout.flush()?;
let mut handle_num = 1;
for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}");
};
match result {
Ok(true) => {
bail!("The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",)
}
Ok(false) => (),
Err(e) => return Err(e),
}
});
if error_occurred.load(atomic::Ordering::Relaxed) {
bail!(CHECK_EXERCISES_UNSOLVED_ERR);
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
stdout.flush()?;
handle_num += 1;
}
stdout.write_all(b"\n")?;
Ok(())
}
fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
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::Equal => (),
}
let handle = thread::spawn(move || check_exercises_unsolved(info_file, cmd_runner));
let info_file_paths = check_info_file_exercises(info_file)?;
check_unexpected_files("exercises", &info_file_paths)?;
check_exercises_unsolved(info_file, target_dir)
handle.join().unwrap()
}
fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &Path) -> Result<()> {
let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len()));
let error_occurred = AtomicBool::new(false);
enum SolutionCheck {
Success { sol_path: String },
MissingOptional,
RunFailure { output: Vec<u8> },
Err(Error),
}
println!("Running all solutions. This may take a while…\n");
thread::scope(|s| {
for exercise_info in &info_file.exercises {
s.spawn(|| {
let error = |e| {
let mut stderr = io::stderr().lock();
stderr.write_all(e).unwrap();
stderr
.write_all(b"\nFailed to run the solution of the exercise ")
.unwrap();
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
stderr.write_all(SEPARATOR).unwrap();
error_occurred.store(true, atomic::Ordering::Relaxed);
};
fn check_solutions(
require_solutions: bool,
info_file: &'static InfoFile,
cmd_runner: &'static CmdRunner,
) -> Result<()> {
let mut stdout = io::stdout().lock();
stdout.write_all(b"Running all solutions...\n")?;
let path = exercise_info.sol_path();
if !Path::new(&path).exists() {
let handles = info_file
.exercises
.iter()
.map(|exercise_info| {
thread::spawn(move || {
let sol_path = exercise_info.sol_path();
if !Path::new(&sol_path).exists() {
if require_solutions {
error(b"Solution missing");
return SolutionCheck::Err(anyhow!(
"The solution of the exercise {} is missing",
exercise_info.name,
));
}
// No solution to check.
return;
return SolutionCheck::MissingOptional;
}
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
match exercise_info.run_solution(&mut output, target_dir) {
Ok(true) => {
paths.lock().unwrap().insert(PathBuf::from(path));
}
Ok(false) => error(&output),
Err(e) => error(e.to_string().as_bytes()),
match exercise_info.run_solution(Some(&mut output), cmd_runner) {
Ok(true) => SolutionCheck::Success { sol_path },
Ok(false) => SolutionCheck::RunFailure { output },
Err(e) => SolutionCheck::Err(e),
}
});
}
});
})
})
.collect::<Vec<_>>();
if error_occurred.load(atomic::Ordering::Relaxed) {
bail!("At least one solution failed. See the output above.");
let mut sol_paths = hash_set_with_capacity(info_file.exercises.len());
let mut fmt_cmd = Command::new("rustfmt");
fmt_cmd
.arg("--check")
.arg("--edition")
.arg("2021")
.arg("--color")
.arg("always")
.stdin(Stdio::null());
let n_handles = handles.len();
write!(stdout, "Progress: 0/{n_handles}")?;
stdout.flush()?;
let mut handle_num = 1;
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 {}",
exercise_info.name,
);
};
match check_result {
SolutionCheck::Success { sol_path } => {
fmt_cmd.arg(&sol_path);
sol_paths.insert(PathBuf::from(sol_path));
}
SolutionCheck::MissingOptional => (),
SolutionCheck::RunFailure { output } => {
stdout.write_all(b"\n\n")?;
stdout.write_all(&output)?;
bail!(
"Running the solution of the exercise {} failed with the error above",
exercise_info.name,
);
}
SolutionCheck::Err(e) => return Err(e),
}
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
stdout.flush()?;
handle_num += 1;
}
stdout.write_all(b"\n")?;
let handle = thread::spawn(move || check_unexpected_files("solutions", &sol_paths));
if !fmt_cmd
.status()
.context("Failed to run `rustfmt` on all solution files")?
.success()
{
bail!("Some solutions aren't formatted. Run `rustfmt` on them");
}
check_unexpected_files("solutions", &paths.into_inner().unwrap())?;
Ok(())
handle.join().unwrap()
}
pub fn check(require_solutions: bool) -> Result<()> {
let info_file = InfoFile::parse()?;
// A hack to make `cargo run -- dev check` work when developing Rustlings.
if DEBUG_PROFILE {
check_cargo_toml(
&info_file.exercises,
include_str!("../../dev-Cargo.toml"),
b"../",
)?;
if cfg!(debug_assertions) {
// A hack to make `cargo run -- dev check` work when developing Rustlings.
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
} else {
let current_cargo_toml =
fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?;
check_cargo_toml(&info_file.exercises, &current_cargo_toml, b"")?;
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
}
let target_dir = parse_target_dir()?;
check_exercises(&info_file, &target_dir)?;
check_solutions(require_solutions, &info_file, &target_dir)?;
// Leaking is fine since they are used until the end of the program.
let cmd_runner = Box::leak(Box::new(CmdRunner::build()?));
let info_file = Box::leak(Box::new(info_file));
println!("\nEverything looks fine!");
check_exercises(info_file, cmd_runner)?;
check_solutions(require_solutions, info_file, cmd_runner)?;
println!("Everything looks fine!");
Ok(())
}
const SEPARATOR: &[u8] =
b"\n========================================================================================\n";
const CHECK_EXERCISES_UNSOLVED_ERR: &str = "At least one exercise is already solved or failed to run. See the output above.
If this is an intro exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file.";
const SKIP_CHECK_UNSOLVED_HINT: &str = "If this is an introduction exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file";

View File

@@ -76,8 +76,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
pub const GITIGNORE: &[u8] = b".rustlings-state.txt
Cargo.lock
target
.vscode
target/
.vscode/
!.vscode/extensions.json
";

View File

@@ -4,18 +4,19 @@ use std::fs;
use crate::{
cargo_toml::updated_cargo_toml,
info_file::{ExerciseInfo, InfoFile},
DEBUG_PROFILE,
};
// Update the `Cargo.toml` file.
fn update_cargo_toml(
exercise_infos: &[ExerciseInfo],
current_cargo_toml: &str,
exercise_path_prefix: &[u8],
cargo_toml_path: &str,
exercise_path_prefix: &[u8],
) -> Result<()> {
let current_cargo_toml = fs::read_to_string(cargo_toml_path)
.with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?;
let updated_cargo_toml =
updated_cargo_toml(exercise_infos, current_cargo_toml, exercise_path_prefix)?;
updated_cargo_toml(exercise_infos, &current_cargo_toml, exercise_path_prefix)?;
fs::write(cargo_toml_path, updated_cargo_toml)
.context("Failed to write the `Cargo.toml` file")?;
@@ -26,21 +27,14 @@ fn update_cargo_toml(
pub fn update() -> Result<()> {
let info_file = InfoFile::parse()?;
// A hack to make `cargo run -- dev update` work when developing Rustlings.
if DEBUG_PROFILE {
update_cargo_toml(
&info_file.exercises,
include_str!("../../dev-Cargo.toml"),
b"../",
"dev/Cargo.toml",
)
.context("Failed to update the file `dev/Cargo.toml`")?;
if cfg!(debug_assertions) {
// A hack to make `cargo run -- 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`")?;
println!("Updated `dev/Cargo.toml`");
} else {
let current_cargo_toml =
fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?;
update_cargo_toml(&info_file.exercises, &current_cargo_toml, b"", "Cargo.toml")
update_cargo_toml(&info_file.exercises, "Cargo.toml", &[])
.context("Failed to update the file `Cargo.toml`")?;
println!("Updated `Cargo.toml`");

View File

@@ -1,46 +1,54 @@
use anyhow::Result;
use crossterm::style::{style, StyledContent, Stylize};
use std::{
fmt::{self, Display, Formatter},
io::Write,
path::{Path, PathBuf},
process::Command,
use crossterm::{
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
QueueableCommand,
};
use std::io::{self, StdoutLock, Write};
use crate::{
cmd::{run_cmd, CargoCmd},
in_official_repo,
terminal_link::TerminalFileLink,
DEBUG_PROFILE,
cmd::CmdRunner,
term::{terminal_file_link, write_ansi},
};
/// The initial capacity of the output buffer.
pub const OUTPUT_CAPACITY: usize = 1 << 14;
pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::Result<()> {
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"Solution")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" for comparison: ")?;
terminal_file_link(stdout, solution_path, Color::Cyan)?;
stdout.write_all(b"\n")
}
// Run an exercise binary and append its output to the `output` buffer.
// Compilation must be done before calling this method.
fn run_bin(bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
writeln!(output, "{}", "Output".underlined())?;
fn run_bin(
bin_name: &str,
mut output: Option<&mut Vec<u8>>,
cmd_runner: &CmdRunner,
) -> Result<bool> {
if let Some(output) = output.as_deref_mut() {
write_ansi(output, SetAttribute(Attribute::Underlined));
output.extend_from_slice(b"Output");
write_ansi(output, ResetColor);
output.push(b'\n');
}
// 7 = "/debug/".len()
let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len());
bin_path.push(target_dir);
bin_path.push("debug");
bin_path.push(bin_name);
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?;
if !success {
// This output is important to show the user that something went wrong.
// Otherwise, calling something like `exit(1)` in an exercise without further output
// leaves the user confused about why the exercise isn't done yet.
writeln!(
output,
"{}",
"The exercise didn't run successfully (nonzero exit code)"
.bold()
.red(),
)?;
if let Some(output) = output {
if !success {
// This output is important to show the user that something went wrong.
// Otherwise, calling something like `exit(1)` in an exercise without further output
// leaves the user confused about why the exercise isn't done yet.
write_ansi(output, SetAttribute(Attribute::Bold));
write_ansi(output, SetForegroundColor(Color::Red));
output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)");
write_ansi(output, ResetColor);
output.push(b'\n');
}
}
Ok(success)
@@ -54,112 +62,114 @@ pub struct Exercise {
pub path: &'static str,
pub test: bool,
pub strict_clippy: bool,
pub hint: String,
pub hint: &'static str,
pub done: bool,
}
impl Exercise {
pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
style(TerminalFileLink(self.path)).underlined().blue()
}
}
impl Display for Exercise {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.path.fmt(f)
}
}
pub trait RunnableExercise {
fn name(&self) -> &str;
fn dir(&self) -> Option<&str>;
fn strict_clippy(&self) -> bool;
fn test(&self) -> bool;
// Compile, check and run the exercise or its solution (depending on `bin_name´).
// The output is written to the `output` buffer after clearing it.
fn run(&self, bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
output.clear();
// Developing the official Rustlings.
let dev = DEBUG_PROFILE && in_official_repo();
let build_success = CargoCmd {
subcommand: "build",
args: &[],
bin_name,
description: "cargo build …",
hide_warnings: false,
target_dir,
output,
dev,
fn run<const FORCE_STRICT_CLIPPY: bool>(
&self,
bin_name: &str,
mut output: Option<&mut Vec<u8>>,
cmd_runner: &CmdRunner,
) -> Result<bool> {
if let Some(output) = output.as_deref_mut() {
output.clear();
}
.run()?;
let build_success = cmd_runner
.cargo("build", bin_name, output.as_deref_mut())
.run("cargo build …")?;
if !build_success {
return Ok(false);
}
// Discard the output of `cargo build` because it will be shown again by Clippy.
output.clear();
// Discard the compiler output because it will be shown again by `cargo test` or Clippy.
if let Some(output) = output.as_deref_mut() {
output.clear();
}
if self.test() {
let output_is_some = output.is_some();
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
if output_is_some {
test_cmd.args(["--", "--color", "always", "--format", "pretty"]);
}
let test_success = test_cmd.run("cargo test …")?;
if !test_success {
run_bin(bin_name, output, cmd_runner)?;
return Ok(false);
}
// Discard the compiler output because it will be shown again by Clippy.
if let Some(output) = output.as_deref_mut() {
output.clear();
}
}
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)]`.
let clippy_args: &[&str] = if self.strict_clippy() {
&["--profile", "test", "--", "-D", "warnings"]
if FORCE_STRICT_CLIPPY || self.strict_clippy() {
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
} else {
&["--profile", "test"]
};
let clippy_success = CargoCmd {
subcommand: "clippy",
args: clippy_args,
bin_name,
description: "cargo clippy …",
hide_warnings: false,
target_dir,
output,
dev,
}
.run()?;
if !clippy_success {
return Ok(false);
clippy_cmd.args(["--profile", "test"]);
}
if !self.test() {
return run_bin(bin_name, output, target_dir);
}
let clippy_success = clippy_cmd.run("cargo clippy …")?;
let run_success = run_bin(bin_name, output, cmd_runner)?;
let test_success = CargoCmd {
subcommand: "test",
args: &["--", "--color", "always", "--show-output"],
bin_name,
description: "cargo test …",
// Hide warnings because they are shown by Clippy.
hide_warnings: true,
target_dir,
output,
dev,
}
.run()?;
let run_success = run_bin(bin_name, output, target_dir)?;
Ok(test_success && run_success)
Ok(clippy_success && run_success)
}
/// Compile, check and run the exercise.
/// The output is written to the `output` buffer after clearing it.
#[inline]
fn run_exercise(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
self.run(self.name(), output, target_dir)
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
self.run::<false>(self.name(), output, cmd_runner)
}
/// Compile, check and run the exercise's solution.
/// The output is written to the `output` buffer after clearing it.
fn run_solution(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
fn run_solution(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
let name = self.name();
let mut bin_name = String::with_capacity(name.len());
let mut bin_name = String::with_capacity(name.len() + 4);
bin_name.push_str(name);
bin_name.push_str("_sol");
self.run(&bin_name, output, target_dir)
self.run::<true>(&bin_name, output, cmd_runner)
}
fn sol_path(&self) -> String {
let name = self.name();
let mut path = if let Some(dir) = self.dir() {
// 14 = 10 + 1 + 3
// solutions/ + / + .rs
let mut path = String::with_capacity(14 + dir.len() + name.len());
path.push_str("solutions/");
path.push_str(dir);
path.push('/');
path
} else {
// 13 = 10 + 3
// solutions/ + .rs
let mut path = String::with_capacity(13 + name.len());
path.push_str("solutions/");
path
};
path.push_str(name);
path.push_str(".rs");
path
}
}
@@ -169,6 +179,11 @@ impl RunnableExercise for Exercise {
self.name
}
#[inline]
fn dir(&self) -> Option<&str> {
self.dir
}
#[inline]
fn strict_clippy(&self) -> bool {
self.strict_clippy

View File

@@ -52,30 +52,6 @@ impl ExerciseInfo {
path
}
/// Path to the solution file starting with the `solutions/` directory.
pub fn sol_path(&self) -> String {
let mut path = if let Some(dir) = &self.dir {
// 14 = 10 + 1 + 3
// solutions/ + / + .rs
let mut path = String::with_capacity(14 + dir.len() + self.name.len());
path.push_str("solutions/");
path.push_str(dir);
path.push('/');
path
} else {
// 13 = 10 + 3
// solutions/ + .rs
let mut path = String::with_capacity(13 + self.name.len());
path.push_str("solutions/");
path
};
path.push_str(&self.name);
path.push_str(".rs");
path
}
}
impl RunnableExercise for ExerciseInfo {
@@ -84,6 +60,11 @@ impl RunnableExercise for ExerciseInfo {
&self.name
}
#[inline]
fn dir(&self) -> Option<&str> {
self.dir.as_deref()
}
#[inline]
fn strict_clippy(&self) -> bool {
self.strict_clippy
@@ -135,4 +116,4 @@ impl InfoFile {
}
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
If you are developing third-party exercises, add at least one exercise before testing.";
Add at least one exercise before testing.";

View File

@@ -1,32 +1,94 @@
use anyhow::{bail, Context, Result};
use crossterm::style::Stylize;
use crossterm::{
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
QueueableCommand,
};
use serde::Deserialize;
use std::{
env::set_current_dir,
fs::{self, create_dir},
io::ErrorKind,
path::Path,
io::{self, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
};
use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile};
use crate::{
cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, exercise::RunnableExercise,
info_file::InfoFile, term::press_enter_prompt,
};
#[derive(Deserialize)]
struct CargoLocateProject {
root: PathBuf,
}
pub fn init() -> Result<()> {
// Prevent initialization in a directory that contains the file `Cargo.toml`.
// This can mean that Rustlings was already initialized in this directory.
// Otherwise, this can cause problems with Cargo workspaces.
if Path::new("Cargo.toml").exists() {
bail!(CARGO_TOML_EXISTS_ERR);
let rustlings_dir = Path::new("rustlings");
if rustlings_dir.exists() {
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
}
let rustlings_path = Path::new("rustlings");
if let Err(e) = create_dir(rustlings_path) {
if e.kind() == ErrorKind::AlreadyExists {
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
let locate_project_output = Command::new("cargo")
.arg("locate-project")
.arg("-q")
.arg("--workspace")
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.context(CARGO_LOCATE_PROJECT_ERR)?;
let mut stdout = io::stdout().lock();
let mut init_git = true;
if locate_project_output.status.success() {
if Path::new("exercises").exists() && Path::new("solutions").exists() {
bail!(IN_INITIALIZED_DIR_ERR);
}
return Err(e.into());
let workspace_manifest =
serde_json::de::from_slice::<CargoLocateProject>(&locate_project_output.stdout)
.context(
"Failed to read the field `root` from the output of `cargo locate-project …`",
)?
.root;
let workspace_manifest_content = fs::read_to_string(&workspace_manifest)
.with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?;
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");
}
stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\nPress ENTER to continue ")?;
press_enter_prompt(&mut stdout)?;
// Make sure "rustlings" is added to `workspace.members` by making
// Cargo initialize a new project.
let status = Command::new("cargo")
.arg("new")
.arg("-q")
.arg("--vcs")
.arg("none")
.arg("rustlings")
.stdin(Stdio::null())
.stdout(Stdio::null())
.status()?;
if !status.success() {
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")?;
fs::remove_dir_all("rustlings")
.context("Failed to remove the temporary directory `rustlings/`")?;
init_git = false;
} else {
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
press_enter_prompt(&mut stdout)?;
}
set_current_dir("rustlings")
create_dir(rustlings_dir).context("Failed to create the `rustlings/` directory")?;
set_current_dir(rustlings_dir)
.context("Failed to change the current directory to `rustlings/`")?;
let info_file = InfoFile::parse()?;
@@ -35,6 +97,11 @@ pub fn init() -> Result<()> {
.context("Failed to initialize the `rustlings/exercises` directory")?;
create_dir("solutions").context("Failed to create the `solutions/` directory")?;
fs::write(
"solutions/README.md",
include_bytes!("../solutions/README.md"),
)
.context("Failed to create the file rustlings/solutions/README.md")?;
for dir in EMBEDDED_FILES.exercise_dirs {
let mut dir_path = String::with_capacity(10 + dir.name.len());
dir_path.push_str("solutions/");
@@ -70,41 +137,49 @@ pub fn init() -> Result<()> {
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
// Ignore any Git error because Git initialization is not required.
let _ = Command::new("git")
.arg("init")
.stdin(Stdio::null())
.stderr(Stdio::null())
.status();
if init_git {
// Ignore any Git error because Git initialization is not required.
let _ = Command::new("git")
.arg("init")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
println!(
"\n{}\n\n{}",
"Initialization done ✓".green(),
POST_INIT_MSG.bold(),
);
stdout.queue(SetForegroundColor(Color::Green))?;
stdout.write_all("Initialization done ✓".as_bytes())?;
stdout.queue(ResetColor)?;
stdout.write_all(b"\n\n")?;
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(POST_INIT_MSG)?;
stdout.queue(ResetColor)?;
Ok(())
}
const CARGO_LOCATE_PROJECT_ERR: &str = "Failed to run the command `cargo locate-project …`
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.";
const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
// DON'T EDIT THIS SOLUTION FILE!
// It will be automatically filled after you finish the exercise.
}
";
const GITIGNORE: &[u8] = b".rustlings-state.txt
solutions
Cargo.lock
target
.vscode
const GITIGNORE: &[u8] = b"Cargo.lock
target/
.vscode/
";
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`.
const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory.
If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
Otherwise, please run `rustlings init` again in another directory.";
Otherwise, please run `rustlings init` again in a different directory.";
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
"A directory with the name `rustlings` already exists in the current directory.
@@ -112,5 +187,6 @@ You probably already initialized Rustlings.
Run `cd rustlings`
Then run `rustlings` again";
const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
Then run `rustlings` to get started.";
const POST_INIT_MSG: &[u8] = b"Run `cd rustlings` to go into the generated directory.
Then run `rustlings` to get started.
";

View File

@@ -1,90 +1,109 @@
use anyhow::Result;
use anyhow::{Context, Result};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
cursor,
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
},
terminal::{
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
LeaveAlternateScreen,
},
QueueableCommand,
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::io::{self, StdoutLock, Write};
use crate::app_state::AppState;
use self::state::{Filter, UiState};
use self::state::{Filter, ListState};
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)?;
loop {
match event::read().context("Failed to read terminal event")? {
Event::Key(key) => {
match key.kind {
KeyEventKind::Release => continue,
KeyEventKind::Press | KeyEventKind::Repeat => (),
}
list_state.message.clear();
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
KeyCode::Up | KeyCode::Char('k') => list_state.select_previous(),
KeyCode::Home | KeyCode::Char('g') => list_state.select_first(),
KeyCode::End | KeyCode::Char('G') => list_state.select_last(),
KeyCode::Char('d') => {
if list_state.filter() == Filter::Done {
list_state.set_filter(Filter::None);
list_state.message.push_str("Disabled filter DONE");
} else {
list_state.set_filter(Filter::Done);
list_state.message.push_str(
"Enabled filter DONE │ Press d again to disable the filter",
);
}
}
KeyCode::Char('p') => {
let message = if list_state.filter() == Filter::Pending {
list_state.set_filter(Filter::None);
"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);
}
KeyCode::Char('r') => list_state.reset_selected()?,
KeyCode::Char('c') => {
if list_state.selected_to_current_exercise()? {
return Ok(());
}
}
// Redraw to remove the message.
KeyCode::Esc => (),
_ => continue,
}
}
Event::Mouse(event) => match event.kind {
MouseEventKind::ScrollDown => list_state.select_next(),
MouseEventKind::ScrollUp => list_state.select_previous(),
_ => continue,
},
Event::Resize(width, height) => list_state.set_term_size(width, height),
// Ignore
Event::FocusGained | Event::FocusLost => continue,
}
list_state.draw(stdout)?;
}
}
pub fn list(app_state: &mut AppState) -> Result<()> {
let mut stdout = io::stdout().lock();
stdout.execute(EnterAlternateScreen)?;
stdout
.queue(EnterAlternateScreen)?
.queue(cursor::Hide)?
.queue(DisableLineWrap)?
.queue(EnableMouseCapture)?;
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
terminal.clear()?;
let res = handle_list(app_state, &mut stdout);
let mut ui_state = UiState::new(app_state);
'outer: loop {
terminal.draw(|frame| ui_state.draw(frame).unwrap())?;
let key = loop {
match event::read()? {
Event::Key(key) => match key.kind {
KeyEventKind::Press | KeyEventKind::Repeat => break key,
KeyEventKind::Release => (),
},
// Redraw
Event::Resize(_, _) => continue 'outer,
// Ignore
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (),
}
};
ui_state.message.clear();
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(),
KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(),
KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(),
KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
KeyCode::Char('d') => {
let message = if ui_state.filter == Filter::Done {
ui_state.filter = Filter::None;
"Disabled filter DONE"
} else {
ui_state.filter = Filter::Done;
"Enabled filter DONE │ Press d again to disable the filter"
};
ui_state = ui_state.with_updated_rows();
ui_state.message.push_str(message);
}
KeyCode::Char('p') => {
let message = if ui_state.filter == Filter::Pending {
ui_state.filter = Filter::None;
"Disabled filter PENDING"
} else {
ui_state.filter = Filter::Pending;
"Enabled filter PENDING │ Press p again to disable the filter"
};
ui_state = ui_state.with_updated_rows();
ui_state.message.push_str(message);
}
KeyCode::Char('r') => {
ui_state = ui_state.with_reset_selected()?;
}
KeyCode::Char('c') => {
ui_state.selected_to_current_exercise()?;
ui_state = ui_state.with_updated_rows();
}
_ => (),
}
}
drop(terminal);
stdout.execute(LeaveAlternateScreen)?;
// Restore the terminal even if we got an error.
stdout
.queue(LeaveAlternateScreen)?
.queue(cursor::Show)?
.queue(EnableLineWrap)?
.queue(DisableMouseCapture)?
.flush()?;
disable_raw_mode()?;
Ok(())
res
}

104
src/list/scroll_state.rs Normal file
View File

@@ -0,0 +1,104 @@
pub struct ScrollState {
n_rows: usize,
max_n_rows_to_display: usize,
selected: Option<usize>,
offset: usize,
scroll_padding: usize,
max_scroll_padding: usize,
}
impl ScrollState {
pub fn new(n_rows: usize, selected: Option<usize>, max_scroll_padding: usize) -> Self {
Self {
n_rows,
max_n_rows_to_display: 0,
selected,
offset: selected.map_or(0, |selected| selected.saturating_sub(max_scroll_padding)),
scroll_padding: 0,
max_scroll_padding,
}
}
#[inline]
pub fn offset(&self) -> usize {
self.offset
}
fn update_offset(&mut self) {
let Some(selected) = self.selected else {
return;
};
let min_offset = (selected + self.scroll_padding)
.saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
let max_offset = selected.saturating_sub(self.scroll_padding);
let global_max_offset = self.n_rows.saturating_sub(self.max_n_rows_to_display);
self.offset = self
.offset
.max(min_offset)
.min(max_offset)
.min(global_max_offset);
}
#[inline]
pub fn selected(&self) -> Option<usize> {
self.selected
}
fn set_selected(&mut self, selected: usize) {
self.selected = Some(selected);
self.update_offset();
}
pub fn select_next(&mut self) {
if let Some(selected) = self.selected {
self.set_selected((selected + 1).min(self.n_rows - 1));
}
}
pub fn select_previous(&mut self) {
if let Some(selected) = self.selected {
self.set_selected(selected.saturating_sub(1));
}
}
pub fn select_first(&mut self) {
if self.n_rows > 0 {
self.set_selected(0);
}
}
pub fn select_last(&mut self) {
if self.n_rows > 0 {
self.set_selected(self.n_rows - 1);
}
}
pub fn set_n_rows(&mut self, n_rows: usize) {
self.n_rows = n_rows;
if self.n_rows == 0 {
self.selected = None;
return;
}
self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1)));
}
#[inline]
fn update_scroll_padding(&mut self) {
self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding);
}
#[inline]
pub fn max_n_rows_to_display(&self) -> usize {
self.max_n_rows_to_display
}
pub fn set_max_n_rows_to_display(&mut self, max_n_rows_to_display: usize) {
self.max_n_rows_to_display = max_n_rows_to_display;
self.update_scroll_padding();
self.update_offset();
}
}

View File

@@ -1,14 +1,31 @@
use anyhow::{Context, Result};
use ratatui::{
layout::{Constraint, Rect},
style::{Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
Frame,
use crossterm::{
cursor::{MoveTo, MoveToNextLine},
style::{Attribute, Color, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor},
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
QueueableCommand,
};
use std::{
fmt::Write as _,
io::{self, StdoutLock, Write},
};
use std::fmt::Write;
use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
use crate::{
app_state::AppState,
exercise::Exercise,
term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter},
};
use super::scroll_state::ScrollState;
const COL_SPACING: usize = 2;
fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
stdout
.queue(Clear(ClearType::UntilNewLine))?
.queue(MoveToNextLine(1))?;
Ok(())
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Filter {
@@ -17,255 +34,352 @@ pub enum Filter {
None,
}
pub struct UiState<'a> {
pub table: Table<'static>,
pub struct ListState<'a> {
/// Footer message to be displayed if not empty.
pub message: String,
pub filter: Filter,
app_state: &'a mut AppState,
table_state: TableState,
n_rows: usize,
scroll_state: ScrollState,
name_col_padding: Vec<u8>,
filter: Filter,
term_width: u16,
term_height: u16,
separator_line: Vec<u8>,
narrow_term: bool,
show_footer: bool,
}
impl<'a> UiState<'a> {
pub fn with_updated_rows(mut self) -> Self {
let current_exercise_ind = self.app_state.current_exercise_ind();
impl<'a> ListState<'a> {
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> {
stdout.queue(Clear(ClearType::All))?;
self.n_rows = 0;
let rows = self
.app_state
.exercises()
.iter()
.enumerate()
.filter_map(|(ind, exercise)| {
let exercise_state = if exercise.done {
if self.filter == Filter::Pending {
return None;
}
"DONE".green()
} else {
if self.filter == Filter::Done {
return None;
}
"PENDING".yellow()
};
self.n_rows += 1;
let next = if ind == current_exercise_ind {
">>>>".bold().red()
} else {
Span::default()
};
Some(Row::new([
next,
exercise_state,
Span::raw(exercise.name),
Span::raw(exercise.path),
]))
});
self.table = self.table.rows(rows);
if self.n_rows == 0 {
self.table_state.select(None);
} else {
self.table_state.select(Some(
self.table_state
.selected()
.map_or(0, |selected| selected.min(self.n_rows - 1)),
));
}
self
}
pub fn new(app_state: &'a mut AppState) -> Self {
let header = Row::new(["Next", "State", "Name", "Path"]);
let max_name_len = app_state
let name_col_title_len = 4;
let name_col_width = app_state
.exercises()
.iter()
.map(|exercise| exercise.name.len())
.max()
.unwrap_or(4) as u16;
let widths = [
Constraint::Length(4),
Constraint::Length(7),
Constraint::Length(max_name_len),
Constraint::Fill(1),
];
let table = Table::default()
.widths(widths)
.header(header)
.column_spacing(2)
.highlight_spacing(HighlightSpacing::Always)
.highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
.highlight_symbol("🦀")
.block(Block::default().borders(Borders::BOTTOM));
let selected = app_state.current_exercise_ind();
let table_state = TableState::default()
.with_offset(selected.saturating_sub(10))
.with_selected(Some(selected));
.map_or(name_col_title_len, |max| max.max(name_col_title_len));
let name_col_padding = vec![b' '; name_col_width + COL_SPACING];
let filter = Filter::None;
let n_rows = app_state.exercises().len();
let n_rows_with_filter = app_state.exercises().len();
let selected = app_state.current_exercise_ind();
let slf = Self {
table,
let (width, height) = terminal::size()?;
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
let mut slf = Self {
message: String::with_capacity(128),
filter,
app_state,
table_state,
n_rows,
scroll_state,
name_col_padding,
filter,
// Set by `set_term_size`
term_width: 0,
term_height: 0,
separator_line: Vec::new(),
narrow_term: false,
show_footer: true,
};
slf.with_updated_rows()
slf.set_term_size(width, height);
slf.draw(stdout)?;
Ok(slf)
}
pub fn select_next(&mut self) {
if self.n_rows > 0 {
let next = self
.table_state
.selected()
.map_or(0, |selected| (selected + 1).min(self.n_rows - 1));
self.table_state.select(Some(next));
pub fn set_term_size(&mut self, width: u16, height: u16) {
self.term_width = width;
self.term_height = height;
if height == 0 {
return;
}
}
pub fn select_previous(&mut self) {
if self.n_rows > 0 {
let previous = self
.table_state
.selected()
.map_or(0, |selected| selected.saturating_sub(1));
self.table_state.select(Some(previous));
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);
self.show_footer = height > header_height + footer_height;
if self.show_footer {
self.separator_line = "".as_bytes().repeat(width as usize);
}
}
pub fn select_first(&mut self) {
if self.n_rows > 0 {
self.table_state.select(Some(0));
}
}
pub fn select_last(&mut self) {
if self.n_rows > 0 {
self.table_state.select(Some(self.n_rows - 1));
}
}
pub fn draw(&mut self, frame: &mut Frame) -> Result<()> {
let area = frame.size();
frame.render_stateful_widget(
&self.table,
Rect {
x: 0,
y: 0,
width: area.width,
height: area.height - 3,
},
&mut self.table_state,
self.scroll_state.set_max_n_rows_to_display(
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
as usize,
);
}
frame.render_widget(
Paragraph::new(progress_bar_ratatui(
fn draw_rows(
&self,
stdout: &mut StdoutLock,
filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>,
) -> io::Result<usize> {
let current_exercise_ind = self.app_state.current_exercise_ind();
let row_offset = self.scroll_state.offset();
let mut n_displayed_rows = 0;
for (exercise_ind, exercise) in filtered_exercises
.skip(row_offset)
.take(self.scroll_state.max_n_rows_to_display())
{
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())?;
} else {
writer.write_ascii(b" ")?;
}
if exercise_ind == current_exercise_ind {
writer.stdout.queue(SetForegroundColor(Color::Red))?;
writer.write_ascii(b">>>>>>> ")?;
} else {
writer.write_ascii(b" ")?;
}
if exercise.done {
writer.stdout.queue(SetForegroundColor(Color::Green))?;
writer.write_ascii(b"DONE ")?;
} else {
writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
writer.write_ascii(b"PENDING ")?;
}
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
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.
// But VS Code shows its own links anyway.
if self.app_state.vs_code() {
writer.write_str(exercise.path)?;
} else {
terminal_file_link(&mut writer, exercise.path, Color::Blue)?;
}
next_ln(stdout)?;
stdout.queue(ResetColor)?;
n_displayed_rows += 1;
}
Ok(n_displayed_rows)
}
pub fn draw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
if self.term_height == 0 {
return Ok(());
}
stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?;
// Header
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
writer.write_ascii(b" Current State Name")?;
writer.write_ascii(&self.name_col_padding[4..])?;
writer.write_ascii(b"Path")?;
next_ln(stdout)?;
// Rows
let iter = self.app_state.exercises().iter().enumerate();
let n_displayed_rows = match self.filter {
Filter::Done => self.draw_rows(stdout, iter.filter(|(_, exercise)| exercise.done))?,
Filter::Pending => {
self.draw_rows(stdout, iter.filter(|(_, exercise)| !exercise.done))?
}
Filter::None => self.draw_rows(stdout, iter)?,
};
for _ in 0..self.scroll_state.max_n_rows_to_display() - n_displayed_rows {
next_ln(stdout)?;
}
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(),
self.app_state.exercises().len() as u16,
area.width,
)?)
.block(Block::default().borders(Borders::BOTTOM)),
Rect {
x: 0,
y: area.height - 3,
width: area.width,
height: 2,
},
);
self.term_width,
)?;
next_ln(stdout)?;
let message = if self.message.is_empty() {
// Help footer.
let mut spans = Vec::with_capacity(4);
spans.push(Span::raw(
"↓/j ↑/k home/g end/G │ <c>ontinue at │ <r>eset │ filter ",
));
match self.filter {
Filter::Done => {
spans.push("<d>one".underlined().magenta());
spans.push(Span::raw("/<p>ending"));
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);
writer.write_ascii(b"filter ")?;
} else {
writer.write_ascii(b" | filter ")?;
}
} else {
// Nothing selected (and nothing shown), so only display filter and quit.
writer.write_ascii(b"filter ")?;
}
Filter::Pending => {
spans.push(Span::raw("<d>one/"));
spans.push("<p>ending".underlined().magenta());
match self.filter {
Filter::Done => {
writer
.stdout
.queue(SetForegroundColor(Color::Magenta))?
.queue(SetAttribute(Attribute::Underlined))?;
writer.write_ascii(b"<d>one")?;
writer.stdout.queue(ResetColor)?;
writer.write_ascii(b"/<p>ending")?;
}
Filter::Pending => {
writer.write_ascii(b"<d>one/")?;
writer
.stdout
.queue(SetForegroundColor(Color::Magenta))?
.queue(SetAttribute(Attribute::Underlined))?;
writer.write_ascii(b"<p>ending")?;
writer.stdout.queue(ResetColor)?;
}
Filter::None => writer.write_ascii(b"<d>one/<p>ending")?,
}
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)?;
}
Filter::None => spans.push(Span::raw("<d>one/<p>ending")),
}
spans.push(Span::raw(" │ <q>uit"));
Line::from(spans)
} else {
Line::from(self.message.as_str().light_blue())
}
stdout.queue(EndSynchronizedUpdate)?.flush()
}
fn update_rows(&mut self) {
let n_rows = match self.filter {
Filter::Done => self
.app_state
.exercises()
.iter()
.filter(|exercise| exercise.done)
.count(),
Filter::Pending => self
.app_state
.exercises()
.iter()
.filter(|exercise| !exercise.done)
.count(),
Filter::None => self.app_state.exercises().len(),
};
frame.render_widget(
message,
Rect {
x: 0,
y: area.height - 1,
width: area.width,
height: 1,
},
);
self.scroll_state.set_n_rows(n_rows);
}
#[inline]
pub fn filter(&self) -> Filter {
self.filter
}
pub fn set_filter(&mut self, filter: Filter) {
self.filter = filter;
self.update_rows();
}
#[inline]
pub fn select_next(&mut self) {
self.scroll_state.select_next();
}
#[inline]
pub fn select_previous(&mut self) {
self.scroll_state.select_previous();
}
#[inline]
pub fn select_first(&mut self) {
self.scroll_state.select_first();
}
#[inline]
pub fn select_last(&mut self) {
self.scroll_state.select_last();
}
fn selected_to_exercise_ind(&self, selected: usize) -> Result<usize> {
match self.filter {
Filter::Done => self
.app_state
.exercises()
.iter()
.enumerate()
.filter(|(_, exercise)| exercise.done)
.nth(selected)
.context("Invalid selection index")
.map(|(ind, _)| ind),
Filter::Pending => self
.app_state
.exercises()
.iter()
.enumerate()
.filter(|(_, exercise)| !exercise.done)
.nth(selected)
.context("Invalid selection index")
.map(|(ind, _)| ind),
Filter::None => Ok(selected),
}
}
pub fn reset_selected(&mut self) -> Result<()> {
let Some(selected) = self.scroll_state.selected() else {
self.message.push_str("Nothing selected to reset!");
return Ok(());
};
let exercise_ind = self.selected_to_exercise_ind(selected)?;
let exercise_name = self.app_state.reset_exercise_by_ind(exercise_ind)?;
self.update_rows();
write!(
self.message,
"The exercise `{exercise_name}` has been reset",
)?;
Ok(())
}
pub fn with_reset_selected(mut self) -> Result<Self> {
let Some(selected) = self.table_state.selected() else {
return Ok(self);
// 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 {
self.message.push_str("Nothing selected to continue at!");
return Ok(false);
};
let ind = self
.app_state
.exercises()
.iter()
.enumerate()
.filter_map(|(ind, exercise)| match self.filter {
Filter::Done => exercise.done.then_some(ind),
Filter::Pending => (!exercise.done).then_some(ind),
Filter::None => Some(ind),
})
.nth(selected)
.context("Invalid selection index")?;
let exercise_ind = self.selected_to_exercise_ind(selected)?;
self.app_state.set_current_exercise_ind(exercise_ind)?;
let exercise_path = self.app_state.reset_exercise_by_ind(ind)?;
write!(self.message, "The exercise {exercise_path} has been reset")?;
Ok(self.with_updated_rows())
}
pub fn selected_to_current_exercise(&mut self) -> Result<()> {
let Some(selected) = self.table_state.selected() else {
return Ok(());
};
let ind = self
.app_state
.exercises()
.iter()
.enumerate()
.filter_map(|(ind, exercise)| match self.filter {
Filter::Done => exercise.done.then_some(ind),
Filter::Pending => (!exercise.done).then_some(ind),
Filter::None => Some(ind),
})
.nth(selected)
.context("Invalid selection index")?;
self.app_state.set_current_exercise_ind(ind)
Ok(true)
}
}

View File

@@ -2,53 +2,29 @@ use anyhow::{bail, Context, Result};
use app_state::StateFileStatus;
use clap::{Parser, Subcommand};
use std::{
io::{self, BufRead, IsTerminal, StdoutLock, Write},
io::{self, IsTerminal, Write},
path::Path,
process::exit,
};
use term::{clear_terminal, press_enter_prompt};
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
mod app_state;
mod cargo_toml;
mod cmd;
mod collections;
mod dev;
mod embedded;
mod exercise;
mod info_file;
mod init;
mod list;
mod progress_bar;
mod run;
mod terminal_link;
mod term;
mod watch;
const CURRENT_FORMAT_VERSION: u8 = 1;
const DEBUG_PROFILE: bool = {
#[allow(unused_assignments, unused_mut)]
let mut debug_profile = false;
#[cfg(debug_assertions)]
{
debug_profile = true;
}
debug_profile
};
// The current directory is the official Rustligns repository.
fn in_official_repo() -> bool {
Path::new("dev/rustlings-repo.txt").exists()
}
fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
}
fn press_enter_prompt() -> io::Result<()> {
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
Ok(())
}
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
#[derive(Parser)]
@@ -89,26 +65,12 @@ enum Subcommands {
fn main() -> Result<()> {
let args = Args::parse();
if !DEBUG_PROFILE && in_official_repo() {
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
bail!("{OLD_METHOD_ERR}");
}
match args.command {
Some(Subcommands::Init) => {
if DEBUG_PROFILE {
bail!("Disabled in the debug build");
}
{
let mut stdout = io::stdout().lock();
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
stdout.flush()?;
press_enter_prompt()?;
stdout.write_all(b"\n")?;
}
return init::init().context("Initialization failed");
}
Some(Subcommands::Init) => return init::init().context("Initialization failed"),
Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
_ => (),
}
@@ -136,11 +98,12 @@ fn main() -> Result<()> {
let mut stdout = io::stdout().lock();
clear_terminal(&mut stdout)?;
let welcome_message = welcome_message.trim();
let welcome_message = welcome_message.trim_ascii();
write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
stdout.flush()?;
press_enter_prompt()?;
press_enter_prompt(&mut stdout)?;
clear_terminal(&mut stdout)?;
// Flush to be able to show errors occurring before printing a newline to stdout.
stdout.flush()?;
}
StateFileStatus::Read => (),
}

View File

@@ -1,100 +0,0 @@
use anyhow::{bail, Result};
use ratatui::text::{Line, Span};
use std::fmt::Write;
const PREFIX: &str = "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 WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
const PROGRESS_EXCEEDS_MAX_ERR: &str =
"The progress of the progress bar is higher than the maximum";
/// Terminal progress bar to be used when not using Ratataui.
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
use crossterm::style::Stylize;
if progress > total {
bail!(PROGRESS_EXCEEDS_MAX_ERR);
}
if line_width < MIN_LINE_WIDTH {
return Ok(format!("Progress: {progress}/{total} exercises"));
}
let mut line = String::with_capacity(usize::from(line_width));
line.push_str(PREFIX);
let width = line_width - WRAPPER_WIDTH;
let filled = (width * progress) / total;
let mut green_part = String::with_capacity(usize::from(filled + 1));
for _ in 0..filled {
green_part.push('#');
}
if filled < width {
green_part.push('>');
}
write!(line, "{}", green_part.green()).unwrap();
let width_minus_filled = width - filled;
if width_minus_filled > 1 {
let red_part_width = width_minus_filled - 1;
let mut red_part = String::with_capacity(usize::from(red_part_width));
for _ in 0..red_part_width {
red_part.push('-');
}
write!(line, "{}", red_part.red()).unwrap();
}
writeln!(line, "] {progress:>3}/{total} exercises").unwrap();
Ok(line)
}
/// Progress bar to be used with Ratataui.
// Not using Ratatui's Gauge widget to keep the progress bar consistent.
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
use ratatui::style::Stylize;
if progress > total {
bail!(PROGRESS_EXCEEDS_MAX_ERR);
}
if line_width < MIN_LINE_WIDTH {
return Ok(Line::raw(format!("Progress: {progress}/{total} exercises")));
}
let mut spans = Vec::with_capacity(4);
spans.push(Span::raw(PREFIX));
let width = line_width - WRAPPER_WIDTH;
let filled = (width * progress) / total;
let mut green_part = String::with_capacity(usize::from(filled + 1));
for _ in 0..filled {
green_part.push('#');
}
if filled < width {
green_part.push('>');
}
spans.push(green_part.green());
let width_minus_filled = width - filled;
if width_minus_filled > 1 {
let red_part_width = width_minus_filled - 1;
let mut red_part = String::with_capacity(usize::from(red_part_width));
for _ in 0..red_part_width {
red_part.push('-');
}
spans.push(red_part.red());
}
spans.push(Span::raw(format!("] {progress:>3}/{total} exercises")));
Ok(Line::from(spans))
}

View File

@@ -1,17 +1,23 @@
use anyhow::{bail, Result};
use crossterm::style::{style, Stylize};
use std::io::{self, Write};
use anyhow::Result;
use crossterm::{
style::{Color, ResetColor, SetForegroundColor},
QueueableCommand,
};
use std::{
io::{self, Write},
process::exit,
};
use crate::{
app_state::{AppState, ExercisesProgress},
exercise::{RunnableExercise, OUTPUT_CAPACITY},
terminal_link::TerminalFileLink,
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
term::terminal_file_link,
};
pub fn run(app_state: &mut AppState) -> Result<()> {
let exercise = app_state.current_exercise();
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
let success = exercise.run_exercise(&mut output, app_state.target_dir())?;
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
let mut stdout = io::stdout().lock();
stdout.write_all(&output)?;
@@ -19,32 +25,31 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
if !success {
app_state.set_pending(app_state.current_exercise_ind())?;
bail!(
"Ran {} with errors",
app_state.current_exercise().terminal_link(),
);
stdout.write_all(b"Ran ")?;
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
stdout.write_all(b" with errors\n")?;
exit(1);
}
writeln!(
stdout,
"{}{}",
"✓ Successfully ran ".green(),
exercise.path.green(),
)?;
stdout.queue(SetForegroundColor(Color::Green))?;
stdout.write_all("✓ Successfully ran ".as_bytes())?;
stdout.write_all(exercise.path.as_bytes())?;
stdout.queue(ResetColor)?;
stdout.write_all(b"\n")?;
if let Some(solution_path) = app_state.current_solution_path()? {
println!(
"\nA solution file can be found at {}\n",
style(TerminalFileLink(&solution_path)).underlined().green(),
);
stdout.write_all(b"\n")?;
solution_link_line(&mut stdout, &solution_path)?;
stdout.write_all(b"\n")?;
}
match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => {
stdout.write_all(b"Next exercise: ")?;
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
stdout.write_all(b"\n")?;
}
ExercisesProgress::AllDone => (),
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!(
"Next exercise: {}",
app_state.current_exercise().terminal_link(),
),
}
Ok(())

202
src/term.rs Normal file
View File

@@ -0,0 +1,202 @@
use std::{
fmt, fs,
io::{self, BufRead, StdoutLock, Write},
};
use crossterm::{
cursor::MoveTo,
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
terminal::{Clear, ClearType},
Command, QueueableCommand,
};
pub struct MaxLenWriter<'a, 'b> {
pub stdout: &'a mut StdoutLock<'b>,
len: usize,
max_len: usize,
}
impl<'a, 'b> MaxLenWriter<'a, 'b> {
#[inline]
pub fn new(stdout: &'a mut StdoutLock<'b>, max_len: usize) -> Self {
Self {
stdout,
len: 0,
max_len,
}
}
// Additional is for emojis that take more space.
#[inline]
pub fn add_to_len(&mut self, additional: usize) {
self.len += additional;
}
}
pub trait CountedWrite<'a> {
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>;
}
impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> {
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
if n > 0 {
self.stdout.write_all(&ascii[..n])?;
self.len += n;
}
Ok(())
}
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
if let Some((ind, c)) = unicode
.char_indices()
.take(self.max_len.saturating_sub(self.len))
.last()
{
self.stdout
.write_all(&unicode.as_bytes()[..ind + c.len_utf8()])?;
self.len += ind + 1;
}
Ok(())
}
#[inline]
fn stdout(&mut self) -> &mut StdoutLock<'b> {
self.stdout
}
}
impl<'a> CountedWrite<'a> for StdoutLock<'a> {
#[inline]
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
self.write_all(ascii)
}
#[inline]
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
self.write_all(unicode.as_bytes())
}
#[inline]
fn stdout(&mut self) -> &mut StdoutLock<'a> {
self
}
}
/// Terminal progress bar to be used when not using Ratataui.
pub fn progress_bar<'a>(
writer: &mut impl CountedWrite<'a>,
progress: u16,
total: u16,
line_width: u16,
) -> io::Result<()> {
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 WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
if line_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");
}
let stdout = writer.stdout();
stdout.write_all(PREFIX)?;
let width = line_width - WRAPPER_WIDTH;
let filled = (width * progress) / total;
stdout.queue(SetForegroundColor(Color::Green))?;
for _ in 0..filled {
stdout.write_all(b"#")?;
}
if filled < width {
stdout.write_all(b">")?;
}
let width_minus_filled = width - filled;
if width_minus_filled > 1 {
let red_part_width = width_minus_filled - 1;
stdout.queue(SetForegroundColor(Color::Red))?;
for _ in 0..red_part_width {
stdout.write_all(b"-")?;
}
}
stdout.queue(ResetColor)?;
write!(stdout, "] {progress:>3}/{total} exercises")
}
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout
.queue(MoveTo(0, 0))?
.queue(Clear(ClearType::All))?
.queue(Clear(ClearType::Purge))
.map(|_| ())
}
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
stdout.write_all(b"\n")
}
pub fn terminal_file_link<'a>(
writer: &mut impl CountedWrite<'a>,
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))?
.queue(SetAttribute(Attribute::Underlined))?;
writer.stdout().write_all(b"\x1b]8;;file://")?;
writer.stdout().write_all(canonical_path.as_bytes())?;
writer.stdout().write_all(b"\x1b\\")?;
// Only this part is visible.
writer.write_str(path)?;
writer.stdout().write_all(b"\x1b]8;;\x1b\\")?;
writer
.stdout()
.queue(SetForegroundColor(Color::Reset))?
.queue(SetAttribute(Attribute::NoUnderline))?;
Ok(())
}
pub fn write_ansi(output: &mut Vec<u8>, command: impl Command) {
struct FmtWriter<'a>(&'a mut Vec<u8>);
impl fmt::Write for FmtWriter<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.0.extend_from_slice(s.as_bytes());
Ok(())
}
}
let _ = command.write_ansi(&mut FmtWriter(output));
}

View File

@@ -1,26 +0,0 @@
use std::{
fmt::{self, Display, Formatter},
fs,
};
pub struct TerminalFileLink<'a>(pub &'a str);
impl<'a> Display for TerminalFileLink<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let path = fs::canonicalize(self.0);
if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) {
// Windows itself can't handle its verbatim paths.
#[cfg(windows)]
let path = if path.len() > 5 && &path[0..4] == r"\\?\" {
&path[4..]
} else {
path
};
write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0)
} else {
write!(f, "{}", self.0)
}
}
}

View File

@@ -72,38 +72,34 @@ pub fn watch(
let mut watch_state = WatchState::new(app_state, manual_run);
watch_state.run_current_exercise()?;
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() {
match event {
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
ExercisesProgress::AllDone => break,
ExercisesProgress::CurrentPending => watch_state.render()?,
ExercisesProgress::NewPending => watch_state.run_current_exercise()?,
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
},
WatchEvent::Input(InputEvent::Hint) => {
watch_state.show_hint()?;
}
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
WatchEvent::Input(InputEvent::List) => {
return Ok(WatchExit::List);
}
WatchEvent::Input(InputEvent::Quit) => {
watch_state.into_writer().write_all(QUIT_MSG)?;
stdout.write_all(QUIT_MSG)?;
break;
}
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?,
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)?;
}
WatchEvent::TerminalResize => {
watch_state.render()?;
watch_state.handle_file_change(exercise_ind, &mut stdout)?;
}
WatchEvent::TerminalResize => watch_state.render(&mut stdout)?,
WatchEvent::NotifyErr(e) => {
watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?;
return Err(Error::from(e));
return Err(Error::from(e).context(NOTIFY_ERR));
}
WatchEvent::TerminalEventErr(e) => {
return Err(Error::from(e).context("Terminal event listener failed"));

View File

@@ -1,16 +1,17 @@
use anyhow::Result;
use crossterm::{
style::{style, Stylize},
terminal,
style::{
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
},
terminal, QueueableCommand,
};
use std::io::{self, StdoutLock, Write};
use crate::{
app_state::{AppState, ExercisesProgress},
clear_terminal,
exercise::{RunnableExercise, OUTPUT_CAPACITY},
progress_bar::progress_bar,
terminal_link::TerminalFileLink,
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
term::{progress_bar, terminal_file_link},
};
#[derive(PartialEq, Eq)]
@@ -21,7 +22,6 @@ enum DoneStatus {
}
pub struct WatchState<'a> {
writer: StdoutLock<'a>,
app_state: &'a mut AppState,
output: Vec<u8>,
show_hint: bool,
@@ -31,10 +31,7 @@ pub struct WatchState<'a> {
impl<'a> WatchState<'a> {
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
let writer = io::stdout().lock();
Self {
writer,
app_state,
output: Vec::with_capacity(OUTPUT_CAPACITY),
show_hint: false,
@@ -43,18 +40,20 @@ impl<'a> WatchState<'a> {
}
}
#[inline]
pub fn into_writer(self) -> StdoutLock<'a> {
self.writer
}
pub fn run_current_exercise(&mut self) -> Result<()> {
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
self.show_hint = false;
writeln!(
stdout,
"\nChecking the exercise `{}`. Please wait…",
self.app_state.current_exercise().name,
)?;
let success = self
.app_state
.current_exercise()
.run_exercise(&mut self.output, self.app_state.target_dir())?;
.run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?;
self.output.push(b'\n');
if success {
self.done_status =
if let Some(solution_path) = self.app_state.current_solution_path()? {
@@ -69,10 +68,15 @@ impl<'a> WatchState<'a> {
self.done_status = DoneStatus::Pending;
}
self.render()
self.render(stdout)?;
Ok(())
}
pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> {
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.
@@ -81,94 +85,115 @@ impl<'a> WatchState<'a> {
}
self.app_state.set_current_exercise_ind(exercise_ind)?;
self.run_current_exercise()
self.run_current_exercise(stdout)
}
/// Move on to the next exercise if the current one is done.
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
if self.done_status == DoneStatus::Pending {
return Ok(ExercisesProgress::CurrentPending);
}
self.app_state.done_current_exercise(&mut self.writer)
self.app_state.done_current_exercise(stdout)
}
fn show_prompt(&mut self) -> io::Result<()> {
self.writer.write_all(b"\n")?;
fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> {
if self.manual_run {
write!(self.writer, "{}:run / ", 'r'.bold())?;
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"r")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":run / ")?;
}
if self.done_status != DoneStatus::Pending {
write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?;
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"n")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":")?;
stdout.queue(SetAttribute(Attribute::Underlined))?;
stdout.write_all(b"next")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" / ")?;
}
if !self.show_hint {
write!(self.writer, "{}:hint / ", 'h'.bold())?;
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"h")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":hint / ")?;
}
write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?;
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"l")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":list / ")?;
self.writer.flush()
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"q")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":quit ? ")?;
stdout.flush()
}
pub fn render(&mut self) -> Result<()> {
pub fn render(&self, stdout: &mut StdoutLock) -> io::Result<()> {
// Prevent having the first line shifted if clearing wasn't successful.
self.writer.write_all(b"\n")?;
stdout.write_all(b"\n")?;
clear_terminal(stdout)?;
clear_terminal(&mut self.writer)?;
self.writer.write_all(&self.output)?;
self.writer.write_all(b"\n")?;
stdout.write_all(&self.output)?;
if self.show_hint {
writeln!(
self.writer,
"{}\n{}\n",
"Hint".bold().cyan().underlined(),
self.app_state.current_exercise().hint,
)?;
stdout
.queue(SetAttributes(
Attributes::from(Attribute::Bold).with(Attribute::Underlined),
))?
.queue(SetForegroundColor(Color::Cyan))?;
stdout.write_all(b"Hint")?;
stdout.queue(ResetColor)?;
stdout.write_all(b"\n")?;
stdout.write_all(self.app_state.current_exercise().hint.as_bytes())?;
stdout.write_all(b"\n\n")?;
}
if self.done_status != DoneStatus::Pending {
writeln!(
self.writer,
"{}\n",
"Exercise done ✓
When you are done experimenting, enter `n` to move on to the next exercise 🦀"
.bold()
.green(),
)?;
}
stdout
.queue(SetAttribute(Attribute::Bold))?
.queue(SetForegroundColor(Color::Green))?;
stdout.write_all("Exercise done ✓".as_bytes())?;
stdout.queue(ResetColor)?;
stdout.write_all(b"\n")?;
if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status {
writeln!(
self.writer,
"A solution file can be found at {}\n",
style(TerminalFileLink(solution_path)).underlined().green(),
if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status {
solution_link_line(stdout, solution_path)?;
}
stdout.write_all(
"When done experimenting, enter `n` to move on to the next exercise 🦀\n\n"
.as_bytes(),
)?;
}
let line_width = terminal::size()?.0;
let progress_bar = progress_bar(
progress_bar(
stdout,
self.app_state.n_done(),
self.app_state.exercises().len() as u16,
line_width,
)?;
writeln!(
self.writer,
"{progress_bar}Current exercise: {}",
self.app_state.current_exercise().terminal_link(),
)?;
self.show_prompt()?;
stdout.write_all(b"\nCurrent exercise: ")?;
terminal_file_link(stdout, self.app_state.current_exercise().path, Color::Blue)?;
stdout.write_all(b"\n\n")?;
self.show_prompt(stdout)?;
Ok(())
}
pub fn show_hint(&mut self) -> Result<()> {
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
self.show_hint = true;
self.render()
self.render(stdout)
}
}

View File

@@ -78,7 +78,7 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
return;
}
}
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
Event::FocusGained | Event::FocusLost | Event::Mouse(_) => continue,
}
};

View File

@@ -1,20 +0,0 @@
[package]
name = "failure"
edition = "2021"
publish = false
[[bin]]
name = "compFailure"
path = "exercises/compFailure.rs"
[[bin]]
name = "compNoExercise"
path = "exercises/compNoExercise.rs"
[[bin]]
name = "testFailure"
path = "exercises/testFailure.rs"
[[bin]]
name = "testNotPassed"
path = "exercises/testNotPassed.rs"

View File

@@ -1,6 +0,0 @@
fn main() {}
#[test]
fn passing() {
asset!(true);
}

View File

@@ -1,6 +0,0 @@
fn main() {}
#[test]
fn not_passing() {
assert!(false);
}

View File

@@ -1,10 +0,0 @@
format_version = 1
[[exercises]]
name = "compFailure"
test = false
hint = ""
[[exercises]]
name = "testFailure"
hint = "Hello!"

View File

@@ -1,16 +0,0 @@
[package]
name = "state"
edition = "2021"
publish = false
[[bin]]
name = "pending_exercise"
path = "exercises/pending_exercise.rs"
[[bin]]
name = "pending_test_exercise"
path = "exercises/pending_test_exercise.rs"
[[bin]]
name = "finished_exercise"
path = "exercises/finished_exercise.rs"

View File

@@ -1 +0,0 @@
fn main() {}

View File

@@ -1,4 +0,0 @@
fn main() {}
#[test]
fn it_works() {}

View File

@@ -1,15 +0,0 @@
format_version = 1
[[exercises]]
name = "pending_exercise"
test = false
hint = """"""
[[exercises]]
name = "pending_test_exercise"
hint = """"""
[[exercises]]
name = "finished_exercise"
test = false
hint = """"""

View File

@@ -1,12 +0,0 @@
[package]
name = "success"
edition = "2021"
publish = false
[[bin]]
name = "compSuccess"
path = "exercises/compSuccess.rs"
[[bin]]
name = "testSuccess"
path = "exercises/testSuccess.rs"

View File

@@ -1 +0,0 @@
fn main() {}

View File

@@ -1,7 +0,0 @@
fn main() {}
#[test]
fn passing() {
println!("THIS TEST TOO SHALL PASS");
assert!(true);
}

View File

@@ -1,10 +0,0 @@
format_version = 1
[[exercises]]
name = "compSuccess"
test = false
hint = """"""
[[exercises]]
name = "testSuccess"
hint = """"""

View File

@@ -1,134 +1,182 @@
use assert_cmd::prelude::*;
use std::process::Command;
use std::{
env::{self, consts::EXE_SUFFIX},
process::{Command, Stdio},
str::from_utf8,
};
#[test]
fn fails_when_in_wrong_dir() {
Command::cargo_bin("rustlings")
.unwrap()
.current_dir("tests/")
.assert()
.code(1);
enum Output<'a> {
FullStdout(&'a str),
PartialStdout(&'a str),
PartialStderr(&'a str),
}
use Output::*;
#[derive(Default)]
struct Cmd<'a> {
current_dir: Option<&'a str>,
args: &'a [&'a str],
output: Option<Output<'a>>,
}
impl<'a> Cmd<'a> {
#[inline]
fn current_dir(&mut self, current_dir: &'a str) -> &mut Self {
self.current_dir = Some(current_dir);
self
}
#[inline]
fn args(&mut self, args: &'a [&'a str]) -> &mut Self {
self.args = args;
self
}
#[inline]
fn output(&mut self, output: Output<'a>) -> &mut Self {
self.output = Some(output);
self
}
fn assert(&self, success: bool) {
let rustlings_bin = {
let mut path = env::current_exe().unwrap();
// Pop test binary name
path.pop();
// Pop `/deps`
path.pop();
path.push("rustlings");
let mut path = path.into_os_string();
path.push(EXE_SUFFIX);
path
};
let mut cmd = Command::new(rustlings_bin);
if let Some(current_dir) = self.current_dir {
cmd.current_dir(current_dir);
}
cmd.args(self.args).stdin(Stdio::null());
let status = match self.output {
None => cmd
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.unwrap(),
Some(FullStdout(stdout)) => {
let output = cmd.stderr(Stdio::null()).output().unwrap();
assert_eq!(from_utf8(&output.stdout).unwrap(), stdout);
output.status
}
Some(PartialStdout(stdout)) => {
let output = cmd.stderr(Stdio::null()).output().unwrap();
assert!(from_utf8(&output.stdout).unwrap().contains(stdout));
output.status
}
Some(PartialStderr(stderr)) => {
let output = cmd.stdout(Stdio::null()).output().unwrap();
assert!(from_utf8(&output.stderr).unwrap().contains(stderr));
output.status
}
};
assert_eq!(status.success(), success, "{cmd:?}");
}
#[inline]
fn success(&self) {
self.assert(true);
}
#[inline]
fn fail(&self) {
self.assert(false);
}
}
#[test]
fn run_single_compile_success() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "compSuccess"])
.current_dir("tests/fixture/success/")
.assert()
fn run_compilation_success() {
Cmd::default()
.current_dir("tests/test_exercises")
.args(&["run", "compilation_success"])
.success();
}
#[test]
fn run_single_compile_failure() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "compFailure"])
.current_dir("tests/fixture/failure/")
.assert()
.code(1);
fn run_compilation_failure() {
Cmd::default()
.current_dir("tests/test_exercises")
.args(&["run", "compilation_failure"])
.fail();
}
#[test]
fn run_single_test_success() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "testSuccess"])
.current_dir("tests/fixture/success/")
.assert()
fn run_test_success() {
Cmd::default()
.current_dir("tests/test_exercises")
.args(&["run", "test_success"])
.output(PartialStdout("\nOutput from `main` function\n"))
.success();
}
#[test]
fn run_single_test_failure() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "testFailure"])
.current_dir("tests/fixture/failure/")
.assert()
.code(1);
fn run_test_failure() {
Cmd::default()
.current_dir("tests/test_exercises")
.args(&["run", "test_failure"])
.fail();
}
#[test]
fn run_single_test_not_passed() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "testNotPassed.rs"])
.current_dir("tests/fixture/failure/")
.assert()
.code(1);
fn run_exercise_not_in_info() {
Cmd::default()
.current_dir("tests/test_exercises")
.args(&["run", "not_in_info"])
.fail();
}
#[test]
fn run_single_test_no_exercise() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "compNoExercise.rs"])
.current_dir("tests/fixture/failure")
.assert()
.code(1);
fn reset_without_exercise_name() {
Cmd::default().args(&["reset"]).fail();
}
#[test]
fn reset_single_exercise() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["reset", "intro1"])
.assert()
.code(0);
fn hint() {
Cmd::default()
.current_dir("tests/test_exercises")
.args(&["hint", "test_failure"])
.output(FullStdout("The answer to everything: 42\n"))
.success();
}
#[test]
fn reset_no_exercise() {
Command::cargo_bin("rustlings")
.unwrap()
.arg("reset")
.assert()
.code(2)
.stderr(predicates::str::contains(
"required arguments were not provided",
));
}
fn init() {
let test_dir = tempfile::TempDir::new().unwrap();
let test_dir = test_dir.path().to_str().unwrap();
#[test]
fn get_hint_for_single_test() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["hint", "testFailure"])
.current_dir("tests/fixture/failure")
.assert()
.code(0)
.stdout("Hello!\n");
}
Cmd::default().current_dir(test_dir).fail();
#[test]
fn run_compile_exercise_does_not_prompt() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "pending_exercise"])
.current_dir("tests/fixture/state")
.assert()
.code(0);
}
Cmd::default()
.current_dir(test_dir)
.args(&["init"])
.success();
#[test]
fn run_test_exercise_does_not_prompt() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "pending_test_exercise"])
.current_dir("tests/fixture/state")
.assert()
.code(0);
}
// Running `init` after a successful initialization.
Cmd::default()
.current_dir(test_dir)
.args(&["init"])
.output(PartialStderr("`cd rustlings`"))
.fail();
#[test]
fn run_single_test_success_with_output() {
Command::cargo_bin("rustlings")
.unwrap()
.args(["run", "testSuccess"])
.current_dir("tests/fixture/success/")
.assert()
.code(0)
.stdout(predicates::str::contains("THIS TEST TOO SHALL PASS"));
let initialized_dir = format!("{test_dir}/rustlings");
// Running `init` in the initialized directory.
Cmd::default()
.current_dir(&initialized_dir)
.args(&["init"])
.output(PartialStderr("already initialized"))
.fail();
}

View File

@@ -0,0 +1,11 @@
bin = [
{ name = "compilation_success", path = "../exercises/compilation_success.rs" },
{ name = "compilation_failure", path = "../exercises/compilation_failure.rs" },
{ name = "test_success", path = "../exercises/test_success.rs" },
{ name = "test_failure", path = "../exercises/test_failure.rs" },
]
[package]
name = "test_exercises"
edition = "2021"
publish = false

View File

@@ -0,0 +1,9 @@
fn main() {}
#[cfg(test)]
mod tests {
#[test]
fn fails() {
assert!(false);
}
}

View File

@@ -0,0 +1,9 @@
fn main() {
println!("Output from `main` function");
}
#[cfg(test)]
mod tests {
#[test]
fn passes() {}
}

View File

@@ -0,0 +1,19 @@
format_version = 1
[[exercises]]
name = "compilation_success"
test = false
hint = ""
[[exercises]]
name = "compilation_failure"
test = false
hint = ""
[[exercises]]
name = "test_success"
hint = ""
[[exercises]]
name = "test_failure"
hint = "The answer to everything: 42"