Compare commits

...

189 commits

Author SHA1 Message Date
Mo
dd0634c483
Merge pull request #2158 from mnshdw/mnshdw/feedback-errors6
errors6: Add alternative solution using From trait
2024-11-14 14:49:57 +01:00
Antoine Dupuis
fc0cd8f0f8 Switch comment style to // 2024-11-14 09:14:40 +01:00
Antoine Dupuis
d5cae8ff59 Add alternative solution using From trait 2024-11-13 23:51:09 +01:00
mo8it
38016cb2d6 clippy3: Make the intent more clear 2024-11-13 16:06:41 +01:00
mo8it
e6cb104294 chore: Release 2024-11-11 15:51:27 +01:00
mo8it
410eb69d25 Remove "chore: " from the commit message of releases 2024-11-11 15:49:50 +01:00
mo8it
243cf5f261 Update CHANGELOG 2024-11-11 15:49:24 +01:00
mo8it
eff2ce8a23 Ignore input while checking all exercises in watch mode 2024-11-11 14:55:58 +01:00
mo8it
fd33c29b26 Test with MSRV before release 2024-11-11 14:43:51 +01:00
mo8it
f49164e69b Fix typo 2024-11-11 14:43:38 +01:00
mo8it
9bc7bbe4b4 Update deps 2024-11-11 14:35:22 +01:00
mo8it
46ad25f925 Fix contrast in terminals with a light theme 2024-11-11 14:34:33 +01:00
mo8it
2a725fb137 Upgrade notify 2024-10-29 14:25:44 +01:00
mo8it
449858655d Update deps 2024-10-26 16:55:15 +02:00
mo8it
e8c2a79516 Deduplicate code for printing keys 2024-10-26 16:55:15 +02:00
Mo
ea85c1b46e
Merge pull request #2142 from cenviity/push-qoxkvmtkyvmv
threads1: Fix typos in description
2024-10-22 12:35:25 +02:00
Vincent Ging Ho Yim
6bec6f92c4 threads1: Fix typos in description 2024-10-22 16:53:23 +11:00
mo8it
930a0ea73b list: Highlight search match in exercise names 2024-10-17 16:00:10 +02:00
mo8it
7e2f56f41a Use the default hasher 2024-10-17 15:03:43 +02:00
mo8it
e90f5f03f3 Mention the Q&A category 2024-10-17 14:59:37 +02:00
mo8it
0e090ae112 Add required type annotation 2024-10-17 14:49:07 +02:00
mo8it
99496706c5 Apply new Clippy lints 2024-10-17 14:49:07 +02:00
mo8it
f146553dea hashmap3: Use or_default 2024-10-17 14:49:07 +02:00
Mo
0432e07864
Merge pull request #2130 from Nahor/typo
Fix typos
2024-10-14 20:06:30 +02:00
Nahor
f33ba139b4 Fix typos 2024-10-14 10:17:17 -07:00
mo8it
990a722852 Limit the maximum number of exercises to 999 2024-10-14 15:57:44 +02:00
mo8it
a675cb5754 Replace ahash with foldhash 2024-10-14 15:24:42 +02:00
Mo
baeeff389c
Merge pull request #2122 from Nahor/check_all
Improvement to "check all exercises"
2024-10-14 01:29:25 +02:00
mo8it
932bc25d88 Remove unneeded line 2024-10-14 01:28:34 +02:00
mo8it
bdc6dad8de Update names 2024-10-14 01:28:12 +02:00
mo8it
ea73af9ba3 Separate initialization with a struct 2024-10-14 01:06:11 +02:00
mo8it
fc5fc0920f Remove outdated comments 2024-10-14 00:48:12 +02:00
mo8it
9705c161b4 Remove the tracking of done and pending 2024-10-14 00:45:41 +02:00
mo8it
8cac21511c Small improvements to showing progress 2024-10-14 00:42:49 +02:00
mo8it
396ee4d618 Show progress with exercise numbers 2024-10-13 23:28:17 +02:00
mo8it
326169a7fa Improve check-all command 2024-10-13 22:02:41 +02:00
mo8it
685e069c58 First PR review changes 2024-10-10 19:43:35 +02:00
mo8it
84a42a2b24 Update third-party exercises section 2024-10-09 15:42:16 +02:00
Mo
ac6e1b7ce5
Merge pull request #2121 from sotanengel/add/link-to-THIRD_PARTY-repository-for-Japanese-translations
Add Third-Party List about rustlings-jp on README
2024-10-09 15:35:30 +02:00
mo8it
f516da4138 Avoid single char variables 2024-10-09 15:27:36 +02:00
Mo
e852e60416
Merge pull request #2124 from Polycarbohydrate/main
fix: typo in `exercises/23_conversions/from_str.rs`
2024-10-06 01:50:18 +02:00
Polycarbohydrate
bf7d171915
Update from_str.rs 2024-10-05 16:05:35 -04:00
Nahor
d3f819f86f Add command line command to check all exercises 2024-10-04 14:36:36 -07:00
Nahor
aa83fd6bc4 Show a progress bar when running check_all
Replace the "Progress: xxx/yyy" with a progress bar when checking all
the exercises
2024-10-02 15:28:42 -07:00
Nahor
e2f7734f37 Limit the amount of parallelism in check_all
Don't create more threads than there are CPU cores.
2024-10-02 14:42:50 -07:00
Nahor
5c17abd1bf Use a channel to update the check_all progress
The previous code was checking the threads in the order they were
created. So the progress update would be blocked on an earlier thread
even if later thread were already done.

Add to that that multiple instances of `cargo build` cannot run in
parallel, they will be serialized instead. So if the exercises needs to
be recompiled, depending on the order those `cargo build` are run,
the first update can be a long time coming.

So instead of relying on the thread terminating, use a channel to get
notified when an exercise check is done, regardless of the order they
finish in.
2024-10-02 14:10:26 -07:00
Nahor
c52867eb8b Add command to check all the exercises
This allows for skipping repeating "next" when multiple exercises
are done at once, or when earlier exercises have been updated/changed
(and thus must be redone) while still working of the whole set (i.e.
the final check_all is not yet available to flag those undone exercises)
2024-10-02 13:40:32 -07:00
Nahor
26fd97a209 Update all exercises during the final check
The previous code run the check on all exercises but only updates one
exercise (the first that failed) even if multiple failed. The user won't
be able to see all the failed exercises when viewing the list, and will
have to run check_all after each fixed exercise.

This change will update all the exercises so the user can see all that
failed, fix them all, and only then need run check_all again.
2024-10-02 11:45:55 -07:00
sotanengel
f0a2cdeb18
Merge branch 'rust-lang:main' into add/link-to-THIRD_PARTY-repository-for-Japanese-translations 2024-09-29 11:09:34 +09:00
mo8it
0c79f2ea3e Reset in prompt with confirmation 2024-09-26 18:15:45 +02:00
mo8it
0e9eb9e87e Replace three dots with dot in hint 2024-09-26 18:05:05 +02:00
mo8it
0d258b9e96 Update deps 2024-09-26 12:28:48 +02:00
mo8it
d4fa61e435 Debounce file change events 2024-09-26 12:26:24 +02:00
mo8it
554301b8e9 Clear terminal before final check in watch mode 2024-09-24 16:12:44 +02:00
sota.n
e3ec0abca4 add Third-Party List about rustlings-jp on README 2024-09-24 16:58:37 +09:00
Mo
a55e848359
Merge pull request #2114 from samueltardieu/push-ptorzrrnmxyp
Do not use `.as_bytes().len()` on strings
2024-09-22 11:40:45 +02:00
Samuel Tardieu
2653c3c4d4 Do not use .as_bytes().len() on strings 2024-09-22 10:49:55 +02:00
mo8it
4e4b65711a Only handle file changes for the current exercise, no jumping back 2024-09-18 01:44:13 +02:00
mo8it
89c40ba256 Optimize the file watcher 2024-09-18 01:43:48 +02:00
mo8it
e56ae6d651 Update deps 2024-09-17 23:33:48 +02:00
Mo
64b2f18d92
Merge pull request #2103 from senekor/senk/kvuzvzqqkskk
Remove redundant enum definition task
2024-09-16 12:56:28 +02:00
Mo
2894f3c45c
Merge pull request #2110 from senekor/remo/skkynvtqxkoz
Make if2 less confusing
2024-09-16 12:54:20 +02:00
Mo
1bae2dcb00
Merge pull request #2109 from bri-rose/main
grammatical error in info.toml
2024-09-14 23:52:54 +02:00
Remo Senekowitsch
b540c6df25 Make if2 less confusing
Some people would get stuck on this exercise, trying to understand the meaning
behind foo, fuzz, baz etc. Making the theme of the code make a little more sense
to humans should hopefully prevent people from getting confused by abstract and
non-sensical tests.
2024-09-14 10:03:52 +02:00
bri-rose
8b476e678a
Update info.toml
Fixed grammatical error, subject/verb agreement at line 124-125.
2024-09-13 10:23:05 -05:00
mo8it
47f8a0cbe5 Add rust-analyzer.toml on dev new 2024-09-13 16:39:28 +02:00
mo8it
9459eef032 Use Clippy with Rust-Analyzer 2024-09-13 16:38:53 +02:00
mo8it
5aaa8924a6 <s>earch isn't a typo 2024-09-13 15:07:53 +02:00
mo8it
4ffce1c297 Move lint to Rust lints 2024-09-13 14:59:34 +02:00
mo8it
0513660b05 Allow dead code for all exercises and solutions 2024-09-13 14:56:46 +02:00
mo8it
3947c4de28 Pause input while running an exercise 2024-09-12 17:46:06 +02:00
mo8it
664228ef8b Improve quit message 2024-09-12 17:46:06 +02:00
mo8it
234a61a3ee Update deps 2024-09-12 17:46:06 +02:00
mo8it
83d1275d72 Add missing # in comment 2024-09-12 17:46:06 +02:00
Mo
45abd7d59e
Merge pull request #2107 from alibektas/ratoml_for_rustlings
Add rust-analyzer.toml file
2024-09-12 15:49:31 +02:00
Ali Bektas
88e10a9e54 hardcode ratoml in init.rs 2024-09-12 15:46:09 +02:00
Ali Bektas
1f624d4c2a Add rust-analyzer.toml file 2024-09-12 15:26:40 +02:00
Remo Senekowitsch
9a25309c1c Remove redundant enum definition task
The exercise enums2.rs already contains a task where an identical enum
has to be defined.
2024-09-11 16:57:12 +02:00
mo8it
2b7caf6fcb Too polite :P 2024-09-06 16:36:36 +02:00
mo8it
938500fd2f Fix dev check in official repo 2024-09-06 16:35:12 +02:00
mo8it
2d26358602 Use the thread builder and handle the spawn error 2024-09-06 15:40:25 +02:00
mo8it
9faa5d3aa4 Avoid asking for terminal size on each rendering 2024-09-05 17:45:27 +02:00
mo8it
bcc2a136c8 Add error message when unable to get terminal size 2024-09-05 17:37:34 +02:00
mo8it
dcad002057 Only render when needed 2024-09-05 17:32:59 +02:00
mo8it
51b8d2ab25 Remove unused import 2024-09-05 17:23:56 +02:00
mo8it
aa3eda70e5 Simplify handling terminal events for unbuffered stdin 2024-09-05 17:12:26 +02:00
mo8it
2d0860fe1b Hide input and disable its line buffering 2024-09-05 02:11:19 +02:00
mo8it
17877366b7 Update deps 2024-09-05 01:55:31 +02:00
mo8it
5eb3dee59c Create solution even if the solution's directory is missing 2024-09-05 00:21:24 +02:00
mo8it
247bd19f93 Canonicalize exercise paths only once 2024-09-04 02:19:45 +02:00
mo8it
e5ed115288 Match filter once 2024-09-04 01:20:48 +02:00
mo8it
03baa471d9 Simplify handling p in list 2024-09-04 01:07:08 +02:00
mo8it
da8b3d143a Final touches to searching 2024-09-04 01:05:30 +02:00
Mo
20616ff954
Merge pull request #2098 from frroossst/main
Made the list of exercises searchable, ref #2093
2024-09-04 00:40:22 +02:00
Adhyan
f463cf8662 passes clippy lints and removed extra code from the merge 2024-09-03 15:10:44 -06:00
Adhyan
e9879eac91 merge of origin/main 2024-09-03 15:04:45 -06:00
Adhyan
47148e78a3 replaced enumerate() with position(); converted select_if_matches_search_query to apply_search_query 2024-09-03 15:03:25 -06:00
Adhyan
fea917c8f2 removed unnecessary update_rows() call and minor refactoring 2024-09-03 14:52:09 -06:00
Adhyan
948e16e3c7 moved continue to end of if-block 2024-09-03 14:40:24 -06:00
Adhyan
1e7fc46406 Merge branch 'main' of https://github.com/frroossst/rustlings 2024-09-02 11:02:21 -06:00
Adhyan
71494264ca fixed clippy lints 2024-09-02 11:02:17 -06:00
Adhyan H. Patel
3125561474
Merge branch 'rust-lang:main' into main 2024-09-02 12:00:22 -05:00
Adhyan
abf1228a0a search now filters the list first 2024-09-02 10:59:23 -06:00
Adhyan
547a9d947b escape/enter no longer exits the list, exits only the search 2024-09-02 10:45:45 -06:00
Mo
f696d98270
Merge pull request #2097 from jsejcksn/ux
style: reduce pre-formatted message line lengths to 80 columns
2024-09-02 14:20:18 +02:00
Adhyan
44ab7f995d Merge branch 'main' of https://github.com/frroossst/rustlings 2024-09-01 19:05:28 -06:00
Adhyan
92a1214dcd passes clippy lints 2024-09-01 19:05:23 -06:00
Adhyan
388f8da97f removed debug statements 2024-09-01 19:03:33 -06:00
Adhyan H. Patel
e96623588c
Merge branch 'rust-lang:main' into main 2024-09-01 19:57:35 -05:00
Adhyan
e1e316b931 Merge branch 'main' of https://github.com/frroossst/rustlings 2024-09-01 18:56:52 -06:00
Adhyan
c4fd29541b added a way to search through list, ref #2093 2024-09-01 18:52:26 -06:00
mo8it
a8b13f5a82 Remove "exercises" from the end of the progress bar 2024-09-01 22:04:09 +02:00
mo8it
86fc573d7a Remove the footer separators 2024-09-01 22:02:07 +02:00
Jesse Jackson
f82e47f2af style: reduce pre-formatted message line lengths to 80 columns 2024-09-01 14:48:28 -05:00
mo8it
75a38fa38b Add search to the help footer 2024-09-01 20:44:19 +02:00
mo8it
ac62a3713c Fix typo 2024-09-01 20:31:16 +02:00
Mo
ea52c99560
Merge pull request #2092 from wugalde19/fix-hint-example-for-primitive-types3
Fix example in 'primitive_types3' hint
2024-08-31 05:27:36 +02:00
William Ugalde Gamboa
7d4100ed8a Fix example in 'primitive_types3' hint 2024-08-30 20:27:26 -06:00
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
65 changed files with 2476 additions and 1656 deletions

View file

@ -1,7 +1,7 @@
[default.extend-words]
"earch" = "earch" # Because of <s>earch in the list footer
[files] [files]
extend-exclude = [ extend-exclude = [
"CHANGELOG.md", "CHANGELOG.md",
] ]
[default.extend-words]
"ratatui" = "ratatui"

View file

@ -1,3 +1,75 @@
<a name="6.4.0"></a>
## 6.4.0 (2024-11-11)
### Added
- The list of exercises is now searchable by pressing `s` or `/` 🔍️ (thanks to [@frroossst](https://github.com/frroossst))
- New option `c` in the prompt to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
- New command `check-all` to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
- Addictive animation for showing the progress of checking all exercises. A nice showcase of parallelism in Rust ✨
- New option `x` in the prompt to reset the file of the current exercise 🔄
- Allow `dead_code` for all exercises and solutions ⚰️ (thanks to [@huss4in](https://github.com/huss4in))
- Pause input while running an exercise to avoid unexpected prompt interactions ⏸️
- Limit the maximum number of exercises to 999. Any third-party exercises willing to reach that limit? 🔝
### Changed
- `enums3`: Remove redundant enum definition task (thanks to [@senekor](https://github.com/senekor))
- `if2`: Make the exercise less confusing by avoiding "fizz", "fuzz", "foo", "bar" and "baz" (thanks to [@senekor](https://github.com/senekor))
- `hashmap3`: Use the method `Entry::or_default`.
- Update the state of all exercises when checking all of them (thanks to [@Nahor](https://github.com/Nahor))
- The main prompt doesn't need a confirmation with ENTER on Unix-like systems anymore.
- No more jumping back to a previous exercise when its file is changed. Use the list to jump between exercises.
- Dump the solution file after an exercise is done even if the solution's directory doesn't exist.
- Rework the footer in the list.
- Optimize the file watcher.
### Fixed
- Fix bad contrast in the list on terminals with a light theme.
<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> <a name="6.2.0"></a>
## 6.2.0 (2024-08-09) ## 6.2.0 (2024-08-09)
@ -72,7 +144,7 @@ You can read about the motivations of this change in [this issue](https://github
### List mode ### List mode
A list mode was added using [Ratatui](https://ratatui.rs). A new list mode was added!
You can enter it by entering `l` in the watch mode. You can enter it by entering `l` in the watch mode.
It offers the following features: It offers the following features:
@ -773,7 +845,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
#### Bug Fixes #### Bug Fixes
- Update deps to version compatable with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401)) - Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
- **docs:** - **docs:**
- Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f)) - Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f))
- Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9)) - Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9))

498
Cargo.lock generated
View file

@ -2,29 +2,11 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.15" version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-parse", "anstyle-parse",
@ -37,49 +19,49 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.8" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [ dependencies = [
"utf8parse", "utf8parse",
] ]
[[package]] [[package]]
name = "anstyle-query" name = "anstyle-query"
version = "1.1.1" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
name = "anstyle-wincon" name = "anstyle-wincon"
version = "3.0.4" version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.86" version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.3.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@ -93,21 +75,6 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -116,9 +83,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.14" version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c937d4061031a6d0c8da4b9a4f98a172fc2976dfb1c19213a9cf7d0d3c837e36" checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -126,9 +93,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.14" version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85379ba512b21a328adf887e85f7742d12e96eb31f3ef077df4ffc26b506ffed" checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -138,9 +105,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.13" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -156,38 +123,9 @@ checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "compact_str"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]] [[package]]
name = "crossterm" name = "crossterm"
@ -197,7 +135,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"crossterm_winapi", "crossterm_winapi",
"mio 1.0.1", "mio",
"parking_lot", "parking_lot",
"rustix", "rustix",
"signal-hook", "signal-hook",
@ -214,12 +152,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -238,20 +170,20 @@ dependencies = [
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.0" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.23" version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.4.1", "libredox",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -265,13 +197,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]] [[package]]
name = "heck" name = "heck"
@ -287,9 +215,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.3.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -297,9 +225,9 @@ dependencies = [
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.9.6" version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"inotify-sys", "inotify-sys",
@ -316,13 +244,12 @@ dependencies = [
] ]
[[package]] [[package]]
name = "instability" name = "instant"
version = "0.3.2" version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [ dependencies = [
"quote", "cfg-if",
"syn",
] ]
[[package]] [[package]]
@ -331,15 +258,6 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.11"
@ -368,9 +286,20 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.155" version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
@ -394,15 +323,6 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lru"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
dependencies = [
"hashbrown",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@ -411,21 +331,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
@ -436,38 +344,37 @@ dependencies = [
[[package]] [[package]]
name = "notify" name = "notify"
version = "6.1.1" version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"crossbeam-channel",
"filetime", "filetime",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
"kqueue", "kqueue",
"libc", "libc",
"log", "log",
"mio 0.8.11", "mio",
"notify-types",
"walkdir", "walkdir",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "notify-debouncer-mini" name = "notify-types"
version = "0.4.1" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" checksum = "7393c226621f817964ffb3dc5704f9509e107a8b024b489cc2c1b217378785df"
dependencies = [ dependencies = [
"log", "instant",
"notify",
] ]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.19.0" version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]] [[package]]
name = "os_pipe" name = "os_pipe"
@ -497,79 +404,43 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.3", "redox_syscall",
"smallvec", "smallvec",
"windows-targets 0.52.6", "windows-targets",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.86" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "ratatui"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303"
dependencies = [
"bitflags 2.6.0",
"cassowary",
"compact_str",
"crossterm",
"instability",
"itertools",
"lru",
"paste",
"strum",
"strum_macros",
"unicode-segmentation",
"unicode-truncate",
"unicode-width",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
] ]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.34" version = "0.38.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"errno", "errno",
@ -580,14 +451,14 @@ dependencies = [
[[package]] [[package]]
name = "rustlings" name = "rustlings"
version = "6.2.0" version = "6.4.0"
dependencies = [ dependencies = [
"ahash",
"anyhow", "anyhow",
"clap", "clap",
"notify-debouncer-mini", "crossterm",
"notify",
"os_pipe", "os_pipe",
"ratatui", "rustix",
"rustlings-macros", "rustlings-macros",
"serde", "serde",
"serde_json", "serde_json",
@ -597,19 +468,13 @@ dependencies = [
[[package]] [[package]]
name = "rustlings-macros" name = "rustlings-macros"
version = "6.2.0" version = "6.4.0"
dependencies = [ dependencies = [
"quote", "quote",
"serde", "serde",
"toml_edit", "toml_edit",
] ]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -633,18 +498,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.205" version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.205" version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -653,9 +518,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.122" version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -665,9 +530,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.7" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -689,7 +554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [ dependencies = [
"libc", "libc",
"mio 1.0.1", "mio",
"signal-hook", "signal-hook",
] ]
@ -708,45 +573,17 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 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]] [[package]]
name = "syn" name = "syn"
version = "2.0.72" version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -755,9 +592,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.12.0" version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
@ -777,9 +614,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.20" version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -790,32 +627,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[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]] [[package]]
name = "utf8parse" name = "utf8parse"
@ -823,12 +637,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
@ -876,22 +684,13 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [ dependencies = [
"windows-targets 0.52.6", "windows-targets",
] ]
[[package]] [[package]]
@ -900,22 +699,7 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [ dependencies = [
"windows-targets 0.52.6", "windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
] ]
[[package]] [[package]]
@ -924,46 +708,28 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.52.6", "windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.52.6", "windows_aarch64_msvc",
"windows_i686_gnu 0.52.6", "windows_i686_gnu",
"windows_i686_gnullvm", "windows_i686_gnullvm",
"windows_i686_msvc 0.52.6", "windows_i686_msvc",
"windows_x86_64_gnu 0.52.6", "windows_x86_64_gnu",
"windows_x86_64_gnullvm 0.52.6", "windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.52.6", "windows_x86_64_msvc",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@ -976,48 +742,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
@ -1026,29 +768,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.18" version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -6,7 +6,7 @@ exclude = [
] ]
[workspace.package] [workspace.package]
version = "6.2.0" version = "6.4.0"
authors = [ authors = [
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it "Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal "Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
@ -19,8 +19,8 @@ edition = "2021" # On Update: Update the edition of the `rustfmt` command that c
rust-version = "1.80" rust-version = "1.80"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1.0.205", features = ["derive"] } serde = { version = "1.0.214", features = ["derive"] }
toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } toml_edit = { version = "0.22.22", default-features = false, features = ["parse", "serde"] }
[package] [package]
name = "rustlings" name = "rustlings"
@ -46,19 +46,21 @@ include = [
] ]
[dependencies] [dependencies]
ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.93"
anyhow = "1.0.86" clap = { version = "4.5.20", features = ["derive"] }
clap = { version = "4.5.14", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
notify-debouncer-mini = { version = "0.4.1", default-features = false } notify = "7.0.0"
os_pipe = "1.2.1" os_pipe = "1.2.1"
ratatui = { version = "0.28.0", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.4.0" }
rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" } serde_json = "1.0.132"
serde_json = "1.0.122"
serde.workspace = true serde.workspace = true
toml_edit.workspace = true toml_edit.workspace = true
[target.'cfg(not(windows))'.dependencies]
rustix = { version = "0.38.38", default-features = false, features = ["std", "stdio", "termios"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.12.0" tempfile = "3.14.0"
[profile.release] [profile.release]
panic = "abort" panic = "abort"
@ -68,7 +70,22 @@ panic = "abort"
[package.metadata.release] [package.metadata.release]
pre-release-hook = ["./release-hook.sh"] pre-release-hook = ["./release-hook.sh"]
pre-release-commit-message = "Release 🎉"
[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 # TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
[lints.clippy]
needless_option_as_deref = "allow" needless_option_as_deref = "allow"
[lints]
workspace = true

View file

@ -124,21 +124,30 @@ The list allows you to…
- See the status of all exercises (done or pending) - See the status of all exercises (done or pending)
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one) - `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards) - `r`: Reset status and file of the selected exercise (you need to _reload/reopen_ its file in your editor afterwards)
See the footer of the list for all possible keys. See the footer of the list for all possible keys.
## Questions?
If you need any help while doing the exercises and the builtin-hints aren't helpful, feel free to ask in the [_Q&A_ category of the discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question wasn't asked yet 💡
## Third-Party Exercises
Third-party exercises are a set of exercises maintained by the community.
You can use the same `rustlings` program that you installed with `cargo install rustlings` to run them:
- [日本語版 Rustlings](https://github.com/sotanengel/rustlings-jp)A Japanese translation of the Rustlings exercises.
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
Or do you want to translate the original Rustlings exercises?
Then follow the the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
## Continuing On ## Continuing On
Once you've completed Rustlings, put your new knowledge to good use! Once you've completed Rustlings, put your new knowledge to good use!
Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.
## Third-Party Exercises
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
Or do you want to translate the original Rustlings exercises?
Then follow the link to the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
## Uninstalling Rustlings ## Uninstalling Rustlings
If you want to remove Rustlings from your system, run the following command: If you want to remove Rustlings from your system, run the following command:

15
clippy.toml Normal file
View file

@ -0,0 +1,15 @@
disallowed-types = [
# Inefficient. Use `.queue(…)` instead.
"crossterm::style::Stylize",
"crossterm::style::styled_content::StyledContent",
]
disallowed-methods = [
# Inefficient. Use `.queue(…)` instead.
"crossterm::style::style",
# Use `thread::Builder::spawn` instead and handle the error.
"std::thread::spawn",
"std::thread::Scope::spawn",
# Return `ExitCode` instead.
"std::process::exit",
]

View file

@ -201,3 +201,23 @@ panic = "abort"
[profile.dev] [profile.dev]
panic = "abort" 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"
# Dead code warnings can't be avoided in some exercises and might distract while learning.
dead_code = "allow"
[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"
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
disallowed_methods = "allow"

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 // even after you already figured it out. If you got everything working and feel
// ready for the next exercise, enter `n` in the terminal. // 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. // Try adding a new `println!` and check the updated output in the terminal.
fn main() { fn main() {
println!("Hello and"); println!(r#" Welcome to... "#);
println!(r#" welcome to... "#);
println!(r#" _ _ _ "#); println!(r#" _ _ _ "#);
println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#); println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#);
println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#); println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#);

View file

@ -1,5 +1,5 @@
fn main() { fn main() {
// TODO: Add missing keyword. // TODO: Add the missing keyword.
x = 5; x = 5;
println!("x has the value {x}"); 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 { for i in 0..num {
println!("Ring! Call number {}", i + 1); println!("Ring! Call number {}", i + 1);
} }

View file

@ -1,7 +1,7 @@
// TODO: Fix the compiler error on this function. // TODO: Fix the compiler error on this function.
fn foo_if_fizz(fizzish: &str) -> &str { fn picky_eater(food: &str) -> &str {
if fizzish == "fizz" { if food == "strawberry" {
"foo" "Yummy!"
} else { } else {
1 1
} }
@ -18,18 +18,20 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn foo_for_fizz() { fn yummy_food() {
// This means that calling `foo_if_fizz` with the argument "fizz" should return "foo". // This means that calling `picky_eater` with the argument "food" should return "Yummy!".
assert_eq!(foo_if_fizz("fizz"), "foo"); assert_eq!(picky_eater("strawberry"), "Yummy!");
} }
#[test] #[test]
fn bar_for_fuzz() { fn neutral_food() {
assert_eq!(foo_if_fizz("fuzz"), "bar"); assert_eq!(picky_eater("potato"), "I guess I can eat that.");
} }
#[test] #[test]
fn default_to_baz() { fn default_disliked_food() {
assert_eq!(foo_if_fizz("literally anything"), "baz"); assert_eq!(picky_eater("broccoli"), "No thanks!");
assert_eq!(picky_eater("gummy bears"), "No thanks!");
assert_eq!(picky_eater("literally anything"), "No thanks!");
} }
} }

View file

@ -1,5 +1,3 @@
#![allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
struct Point { struct Point {
x: u64, x: u64,

View file

@ -4,7 +4,11 @@ struct Point {
} }
enum Message { enum Message {
// TODO: Implement the message variant types based on their usage below. Resize { width: u64, height: u64 },
Move(Point),
Echo(String),
ChangeColor(u8, u8, u8),
Quit,
} }
struct State { struct State {

View file

@ -1,7 +1,6 @@
// You can bring module paths into scopes and provide new names for them with // You can bring module paths into scopes and provide new names for them with
// the `use` and `as` keywords. // the `use` and `as` keywords.
#[allow(dead_code)]
mod delicious_snacks { mod delicious_snacks {
// TODO: Add the following two `use` statements after fixing them. // TODO: Add the following two `use` statements after fixing them.
// use self::fruits::PEAR as ???; // use self::fruits::PEAR as ???;

View file

@ -17,7 +17,7 @@ struct TeamScores {
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value. // The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new(); let mut scores = HashMap::<&str, TeamScores>::new();
for line in results.lines() { for line in results.lines() {
let mut split_iterator = line.split(','); let mut split_iterator = line.split(',');

View file

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

View file

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

View file

@ -1,5 +1,3 @@
#![allow(dead_code)]
trait Licensed { trait Licensed {
// TODO: Add a default implementation for `licensing_info` so that // TODO: Add a default implementation for `licensing_info` so that
// implementors like the two structs below can share that default behavior // implementors like the two structs below can share that default behavior

View file

@ -8,7 +8,6 @@ use std::rc::Rc;
#[derive(Debug)] #[derive(Debug)]
struct Sun; struct Sun;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
enum Planet { enum Planet {
Mercury(Rc<Sun>), Mercury(Rc<Sun>),

View file

@ -1,5 +1,5 @@
// This program spawns multiple threads that each run for at least 250ms, and // This program spawns multiple threads that each runs for at least 250ms, and
// each thread returns how much time they took to complete. The program should // each thread returns how much time it took to complete. The program should
// wait until all the spawned threads have finished and should collect their // wait until all the spawned threads have finished and should collect their
// return values into a vector. // return values into a vector.

View file

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

View file

@ -4,9 +4,11 @@
#[rustfmt::skip] #[rustfmt::skip]
#[allow(unused_variables, unused_assignments)] #[allow(unused_variables, unused_assignments)]
fn main() { fn main() {
let my_option: Option<()> = None; let my_option: Option<&str> = None;
// Assume that you don't know the value of `my_option`.
// In the case of `Some`, we want to print its value.
if my_option.is_none() { if my_option.is_none() {
println!("{:?}", my_option.unwrap()); println!("{}", my_option.unwrap());
} }
let my_arr = &[ let my_arr = &[

View file

@ -2,10 +2,11 @@
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and // about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
// Obtain the number of bytes (not characters) in the given argument. // Obtain the number of bytes (not characters) in the given argument
// (`.len()` returns the number of bytes in a string).
// TODO: Add the `AsRef` trait appropriately as a trait bound. // TODO: Add the `AsRef` trait appropriately as a trait bound.
fn byte_counter<T>(arg: T) -> usize { fn byte_counter<T>(arg: T) -> usize {
arg.as_ref().as_bytes().len() arg.as_ref().len()
} }
// Obtain the number of characters (not bytes) in the given argument. // Obtain the number of characters (not bytes) in the given argument.

View file

@ -25,7 +25,7 @@ enum ParsePersonError {
ParseInt(ParseIntError), ParseInt(ParseIntError),
} }
// TODO: Complete this `From` implementation to be able to parse a `Person` // TODO: Complete this `FromStr` implementation to be able to parse a `Person`
// out of a string in the form of "Mark,20". // out of a string in the form of "Mark,20".
// Note that you'll need to parse the age component into a `u8` with something // Note that you'll need to parse the age component into a `u8` with something
// like `"4".parse::<u8>()`. // like `"4".parse::<u8>()`.

View file

@ -11,3 +11,6 @@ cargo clippy -- --deny warnings
cargo fmt --all --check cargo fmt --all --check
cargo test --workspace --all-targets cargo test --workspace --all-targets
cargo run -- dev check --require-solutions cargo run -- dev check --require-solutions
# MSRV
cargo +1.80 run -- dev check --require-solutions

View file

@ -16,6 +16,9 @@ include = [
proc-macro = true proc-macro = true
[dependencies] [dependencies]
quote = "1.0.36" quote = "1.0.37"
serde.workspace = true serde.workspace = true
toml_edit.workspace = true toml_edit.workspace = true
[lints]
workspace = true

View file

@ -1,6 +1,7 @@
format_version = 1 format_version = 1
welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners! welcome_message = """
Is this your first time? Don't worry, Rustlings is made for beginners!
We are going to teach you a lot of things about Rust, but before we can We are going to teach you a lot of things about Rust, but before we can
get started, here are some notes about how Rustlings operates: get started, here are some notes about how Rustlings operates:
@ -10,15 +11,16 @@ get started, here are some notes about how Rustlings operates:
and fix them! and fix them!
2. Make sure to have your editor open in the `rustlings/` directory. Rustlings 2. Make sure to have your editor open in the `rustlings/` directory. Rustlings
will show you the path of the current exercise under the progress bar. Open will show you the path of the current exercise under the progress bar. Open
the exercise file in your editor, fix errors and save the file. Rustlings will the exercise file in your editor, fix errors and save the file. Rustlings
automatically detect the file change and rerun the exercise. If all errors are will automatically detect the file change and rerun the exercise. If all
fixed, Rustlings will ask you to move on to the next exercise. errors are fixed, Rustlings will ask you to move on to the next exercise.
3. If you're stuck on an exercise, enter `h` to show a hint. 3. If you're stuck on an exercise, enter `h` to show a hint.
4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! 4. If an exercise doesn't make sense to you, feel free to open an issue on
(https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and
other learners do too so you can help each other out!""" sometimes, other learners do too so you can help each other out!"""
final_message = """We hope you enjoyed learning about the various aspects of Rust! final_message = """
We hope you enjoyed learning about the various aspects of Rust!
If you noticed any issues, don't hesitate to report them on Github. If you noticed any issues, don't hesitate to report them on Github.
You can also contribute your own exercises to help the greater community! You can also contribute your own exercises to help the greater community!
@ -120,10 +122,10 @@ dir = "01_variables"
test = false test = false
hint = """ hint = """
We know about variables and mutability, but there is another important type of We know about variables and mutability, but there is another important type of
variables available: constants. variable available: constants.
Constants are always immutable. They are declared with the keyword `const` instead Constants are always immutable. They are declared with the keyword `const`
of `let`. instead of `let`.
The type of Constants must always be annotated. The type of Constants must always be annotated.
@ -253,7 +255,7 @@ require you to type in 100 items (but you certainly can if you want!).
For example, you can do: For example, you can do:
``` ```
let array = ["Are we there yet?"; 10]; let array = ["Are we there yet?"; 100];
``` ```
Bonus: what are some other things you could have that would return `true` Bonus: what are some other things you could have that would return `true`
@ -319,7 +321,8 @@ hint = """
In the first function, we create an empty vector and want to push new elements In the first function, we create an empty vector and want to push new elements
to it. to it.
In the second function, we map the values of the input and collect them into a vector. In the second function, we map the values of the input and collect them into
a vector.
After you've completed both functions, decide for yourself which approach you After you've completed both functions, decide for yourself which approach you
like better. like better.
@ -332,8 +335,8 @@ What do you think is the more commonly used pattern under Rust developers?"""
name = "move_semantics1" name = "move_semantics1"
dir = "06_move_semantics" dir = "06_move_semantics"
hint = """ hint = """
So you've got the "cannot borrow `vec` as mutable, as it is not declared as mutable" So you've got the "cannot borrow `vec` as mutable, as it is not declared as
error on the line where we push an element to the vector, right? mutable" error on the line where we push an element to the vector, right?
The fix for this is going to be adding one keyword, and the addition is NOT on The fix for this is going to be adding one keyword, and the addition is NOT on
the line where we push to the vector (where the error is). the line where we push to the vector (where the error is).
@ -369,7 +372,8 @@ hint = """
Carefully reason about the range in which each mutable reference is in Carefully reason about the range in which each mutable reference is in
scope. Does it help to update the value of `x` immediately after scope. Does it help to update the value of `x` immediately after
the mutable reference is taken? the mutable reference is taken?
Read more about 'Mutable References' in the book's section 'References and Borrowing': Read more about 'Mutable References' in the book's section 'References and
Borrowing':
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references.""" https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references."""
[[exercises]] [[exercises]]
@ -508,7 +512,8 @@ name = "strings4"
dir = "09_strings" dir = "09_strings"
test = false test = false
hint = """ hint = """
Replace `placeholder` with either `string` or `string_slice` in the `main` function. Replace `placeholder` with either `string` or `string_slice` in the `main`
function.
Example: Example:
`placeholder("blue");` `placeholder("blue");`
@ -570,12 +575,8 @@ https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-
name = "hashmaps3" name = "hashmaps3"
dir = "11_hashmaps" dir = "11_hashmaps"
hint = """ hint = """
Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of Hint 1: Use the `entry()` and `or_default()` methods of `HashMap` to insert the
`HashMap` to insert the default value of `TeamScores` if a team doesn't default value of `TeamScores` if a team doesn't exist in the table yet.
exist in the table yet.
Learn more in The Book:
https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value
Hint 2: If there is already an entry for a given key, the value returned by Hint 2: If there is already an entry for a given key, the value returned by
`entry()` can be updated based on the existing value. `entry()` can be updated based on the existing value.
@ -1139,7 +1140,7 @@ constants, but clippy recognizes those imprecise mathematical constants as a
source of potential error. source of potential error.
See the suggestions of the Clippy warning in the compile output and use the See the suggestions of the Clippy warning in the compile output and use the
appropriate replacement constant from `std::f32::consts`...""" appropriate replacement constant from `std::f32::consts`."""
[[exercises]] [[exercises]]
name = "clippy2" name = "clippy2"
@ -1200,7 +1201,8 @@ hint = """
Is there an implementation of `TryFrom` in the standard library that can both do Is there an implementation of `TryFrom` in the standard library that can both do
the required integer conversion and check the range of the input? the required integer conversion and check the range of the input?
Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" Challenge: Can you make the `TryFrom` implementations generic over many integer
types?"""
[[exercises]] [[exercises]]
name = "as_ref_mut" name = "as_ref_mut"

View file

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

View file

@ -1,10 +1,10 @@
fn foo_if_fizz(fizzish: &str) -> &str { fn picky_eater(food: &str) -> &str {
if fizzish == "fizz" { if food == "strawberry" {
"foo" "Yummy!"
} else if fizzish == "fuzz" { } else if food == "potato" {
"bar" "I guess I can eat that."
} else { } else {
"baz" "No thanks!"
} }
} }
@ -17,17 +17,19 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn foo_for_fizz() { fn yummy_food() {
assert_eq!(foo_if_fizz("fizz"), "foo"); assert_eq!(picky_eater("strawberry"), "Yummy!");
} }
#[test] #[test]
fn bar_for_fuzz() { fn neutral_food() {
assert_eq!(foo_if_fizz("fuzz"), "bar"); assert_eq!(picky_eater("potato"), "I guess I can eat that.");
} }
#[test] #[test]
fn default_to_baz() { fn default_disliked_food() {
assert_eq!(foo_if_fizz("literally anything"), "baz"); assert_eq!(picky_eater("broccoli"), "No thanks!");
assert_eq!(picky_eater("gummy bears"), "No thanks!");
assert_eq!(picky_eater("literally anything"), "No thanks!");
} }
} }

View file

@ -1,5 +1,3 @@
#![allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
struct Point { struct Point {
x: u64, x: u64,

View file

@ -46,8 +46,8 @@ impl State {
match message { match message {
Message::Resize { width, height } => self.resize(width, height), Message::Resize { width, height } => self.resize(width, height),
Message::Move(point) => self.move_position(point), Message::Move(point) => self.move_position(point),
Message::Echo(s) => self.echo(s), Message::Echo(string) => self.echo(string),
Message::ChangeColor(r, g, b) => self.change_color(r, g, b), Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue),
Message::Quit => self.quit(), Message::Quit => self.quit(),
} }
} }

View file

@ -18,12 +18,11 @@ fn main() {
// Here, both answers work. // Here, both answers work.
// `.into()` converts a type into an expected type. // `.into()` converts a type into an expected type.
// If it is called where `String` is expected, it will convert `&str` to `String`. // 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()); 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()); string_slice("nice weather".into());
// ^^^^^^^ the compiler recommends removing the `.into()`
// call because it is a useless conversion.
string(format!("Interpolation {}", "Station")); string(format!("Interpolation {}", "Station"));

View file

@ -1,4 +1,3 @@
#[allow(dead_code)]
mod delicious_snacks { mod delicious_snacks {
// Added `pub` and used the expected alias after `as`. // Added `pub` and used the expected alias after `as`.
pub use self::fruits::PEAR as fruit; pub use self::fruits::PEAR as fruit;

View file

@ -17,7 +17,7 @@ struct TeamScores {
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value. // The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new(); let mut scores = HashMap::<&str, TeamScores>::new();
for line in results.lines() { for line in results.lines() {
let mut split_iterator = line.split(','); let mut split_iterator = line.split(',');
@ -28,17 +28,13 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
// Insert the default with zeros if a team doesn't exist yet. // Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores let team_1 = scores.entry(team_1_name).or_default();
.entry(team_1_name)
.or_insert_with(TeamScores::default);
// Update the values. // Update the values.
team_1.goals_scored += team_1_score; team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score; team_1.goals_conceded += team_2_score;
// Similarely for the second team. // Similarly for the second team.
let team_2 = scores let team_2 = scores.entry(team_2_name).or_default();
.entry(team_2_name)
.or_insert_with(TeamScores::default);
team_2.goals_scored += team_2_score; team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_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)] #[derive(PartialEq, Debug)]
enum CreationError { enum CreationError {
@ -11,12 +11,10 @@ struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger { impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> { fn new(value: i64) -> Result<Self, CreationError> {
if value == 0 { match value.cmp(&0) {
Err(CreationError::Zero) Ordering::Less => Err(CreationError::Negative),
} else if value < 0 { Ordering::Equal => Err(CreationError::Zero),
Err(CreationError::Negative) Ordering::Greater => Ok(Self(value as u64)),
} else {
Ok(Self(value as u64))
} }
} }
} }

View file

@ -29,6 +29,21 @@ impl ParsePosNonzeroError {
} }
} }
// As an alternative solution, implementing the `From` trait allows for the
// automatic conversion from a `ParseIntError` into a `ParsePosNonzeroError`
// using the `?` operator, without the need to call `map_err`.
//
// ```
// let x: i64 = s.parse()?;
// ```
//
// Traits like `From` will be dealt with in later exercises.
impl From<ParseIntError> for ParsePosNonzeroError {
fn from(err: ParseIntError) -> Self {
ParsePosNonzeroError::ParseInt(err)
}
}
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64); struct PositiveNonzeroInteger(u64);

View file

@ -1,5 +1,3 @@
#![allow(dead_code)]
trait Licensed { trait Licensed {
fn licensing_info(&self) -> String { fn licensing_info(&self) -> String {
"Default license".to_string() "Default license".to_string()

View file

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

View file

@ -8,7 +8,6 @@ use std::rc::Rc;
#[derive(Debug)] #[derive(Debug)]
struct Sun; struct Sun;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
enum Planet { enum Planet {
Mercury(Rc<Sun>), Mercury(Rc<Sun>),

View file

@ -1,5 +1,5 @@
// This program spawns multiple threads that each run for at least 250ms, and // This program spawns multiple threads that each runs for at least 250ms, and
// each thread returns how much time they took to complete. The program should // each thread returns how much time it took to complete. The program should
// wait until all the spawned threads have finished and should collect their // wait until all the spawned threads have finished and should collect their
// return values into a vector. // return values into a vector.

View file

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

View file

@ -3,11 +3,11 @@ use std::mem;
#[rustfmt::skip] #[rustfmt::skip]
#[allow(unused_variables, unused_assignments)] #[allow(unused_variables, unused_assignments)]
fn main() { fn main() {
let my_option: Option<()> = None; let my_option: Option<&str> = None;
// `unwrap` of an `Option` after checking if it is `None` will panic. // `unwrap` of an `Option` after checking if it is `None` will panic.
// Use `if-let` instead. // Use `if-let` instead.
if let Some(value) = my_option { if let Some(value) = my_option {
println!("{value:?}"); println!("{value}");
} }
// A comma was missing. // A comma was missing.

View file

@ -2,9 +2,10 @@
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and // about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
// Obtain the number of bytes (not characters) in the given argument. // Obtain the number of bytes (not characters) in the given argument
// (`.len()` returns the number of bytes in a string).
fn byte_counter<T: AsRef<str>>(arg: T) -> usize { fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
arg.as_ref().as_bytes().len() arg.as_ref().len()
} }
// Obtain the number of characters (not bytes) in the given argument. // Obtain the number of characters (not bytes) in the given argument.

View file

@ -1,32 +1,39 @@
use anyhow::{bail, Context, Error, Result}; use anyhow::{bail, Context, Error, Result};
use crossterm::{cursor, terminal, QueueableCommand};
use std::{ use std::{
fs::{self, File}, collections::HashSet,
io::{Read, StdoutLock, Write}, env,
path::Path, fs::{File, OpenOptions},
io::{Read, Seek, StdoutLock, Write},
path::{Path, MAIN_SEPARATOR_STR},
process::{Command, Stdio}, process::{Command, Stdio},
sync::{
atomic::{AtomicUsize, Ordering::Relaxed},
mpsc,
},
thread, thread,
}; };
use crate::{ use crate::{
clear_terminal, clear_terminal,
cmd::CmdRunner, cmd::CmdRunner,
collections::hash_set_with_capacity,
embedded::EMBEDDED_FILES, embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise}, exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo, info_file::ExerciseInfo,
term::{self, CheckProgressVisualizer},
}; };
const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const DEFAULT_CHECK_PARALLELISM: usize = 8;
#[must_use] #[must_use]
pub enum ExercisesProgress { pub enum ExercisesProgress {
// All exercises are done. // All exercises are done.
AllDone, AllDone,
// The current exercise failed and is still pending.
CurrentPending,
// A new exercise is now pending. // A new exercise is now pending.
NewPending, NewPending,
// The current exercise is still pending.
CurrentPending,
} }
pub enum StateFileStatus { pub enum StateFileStatus {
@ -34,72 +41,47 @@ pub enum StateFileStatus {
NotRead, NotRead,
} }
#[derive(Clone, Copy)]
pub enum CheckProgress {
None,
Checking,
Done,
Pending,
}
pub struct AppState { pub struct AppState {
current_exercise_ind: usize, current_exercise_ind: usize,
exercises: Vec<Exercise>, exercises: Vec<Exercise>,
// Caches the number of done exercises to avoid iterating over all exercises every time. // Caches the number of done exercises to avoid iterating over all exercises every time.
n_done: u16, n_done: u16,
final_message: String, final_message: String,
state_file: File,
// Preallocated buffer for reading and writing the state file. // Preallocated buffer for reading and writing the state file.
file_buf: Vec<u8>, file_buf: Vec<u8>,
official_exercises: bool, official_exercises: bool,
cmd_runner: CmdRunner, cmd_runner: CmdRunner,
// Running in VS Code.
vs_code: bool,
} }
impl AppState { 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 = hash_set_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( pub fn new(
exercise_infos: Vec<ExerciseInfo>, exercise_infos: Vec<ExerciseInfo>,
final_message: String, final_message: String,
) -> Result<(Self, StateFileStatus)> { ) -> Result<(Self, StateFileStatus)> {
let cmd_runner = CmdRunner::build()?; 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 dir_canonical_path = term::canonicalize("exercises");
let mut exercises = exercise_infos
.into_iter() .into_iter()
.map(|exercise_info| { .map(|exercise_info| {
// Leaking to be able to borrow in the watch mode `Table`. // Leaking to be able to borrow in the watch mode `Table`.
@ -110,30 +92,97 @@ impl AppState {
let dir = exercise_info.dir.map(|dir| &*dir.leak()); let dir = exercise_info.dir.map(|dir| &*dir.leak());
let hint = exercise_info.hint.leak().trim_ascii(); let hint = exercise_info.hint.leak().trim_ascii();
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
let mut canonical_path;
if let Some(dir) = dir {
canonical_path = String::with_capacity(
2 + dir_canonical_path.len() + dir.len() + name.len(),
);
canonical_path.push_str(dir_canonical_path);
canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(dir);
} else {
canonical_path =
String::with_capacity(1 + dir_canonical_path.len() + name.len());
canonical_path.push_str(dir_canonical_path);
}
canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(name);
canonical_path.push_str(".rs");
canonical_path
});
Exercise { Exercise {
dir, dir,
name, name,
path, path,
canonical_path,
test: exercise_info.test, test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy, strict_clippy: exercise_info.strict_clippy,
hint, hint,
// Updated in `Self::update_from_file`. // Updated below.
done: false, done: false,
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut slf = Self { let mut current_exercise_ind = 0;
current_exercise_ind: 0, let mut n_done = 0;
exercises, let mut file_buf = Vec::with_capacity(2048);
n_done: 0, let state_file_status = 'block: {
final_message, if state_file.read_to_end(&mut file_buf).is_err() {
file_buf: Vec::with_capacity(2048), break 'block StateFileStatus::NotRead;
official_exercises: !Path::new("info.toml").exists(), }
cmd_runner,
// 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;
}; };
let state_file_status = slf.update_from_file(); if current_exercise_name.is_empty() || lines.next().is_none() {
break 'block StateFileStatus::NotRead;
}
let mut done_exercises = HashSet::with_capacity(exercises.len());
for done_exercise_name in lines {
if done_exercise_name.is_empty() {
break;
}
done_exercises.insert(done_exercise_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
};
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)) Ok((slf, state_file_status))
} }
@ -153,6 +202,11 @@ impl AppState {
self.n_done self.n_done
} }
#[inline]
pub fn n_pending(&self) -> u16 {
self.exercises.len() as u16 - self.n_done
}
#[inline] #[inline]
pub fn current_exercise(&self) -> &Exercise { pub fn current_exercise(&self) -> &Exercise {
&self.exercises[self.current_exercise_ind] &self.exercises[self.current_exercise_ind]
@ -163,6 +217,11 @@ impl AppState {
&self.cmd_runner &self.cmd_runner
} }
#[inline]
pub fn vs_code(&self) -> bool {
self.vs_code
}
// Write the state file. // Write the state file.
// The file's format is very simple: // The file's format is very simple:
// - The first line is a comment. // - The first line is a comment.
@ -172,10 +231,8 @@ impl AppState {
// - The fourth line is an empty line. // - The fourth line is an empty line.
// - All remaining lines are the names of done exercises. // - All remaining lines are the names of done exercises.
fn write(&mut self) -> Result<()> { 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 self.file_buf
.extend_from_slice(self.current_exercise().name.as_bytes()); .extend_from_slice(self.current_exercise().name.as_bytes());
self.file_buf.push(b'\n'); self.file_buf.push(b'\n');
@ -187,7 +244,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}"))?; .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
Ok(()) Ok(())
@ -219,15 +283,31 @@ impl AppState {
self.write() self.write()
} }
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> { // Set the status of an exercise without saving. Returns `true` if the
// status actually changed (and thus needs saving later).
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
let exercise = self let exercise = self
.exercises .exercises
.get_mut(exercise_ind) .get_mut(exercise_ind)
.context(BAD_INDEX_ERR)?; .context(BAD_INDEX_ERR)?;
if exercise.done { if exercise.done == done {
exercise.done = false; return Ok(false);
}
exercise.done = done;
if done {
self.n_done += 1;
} else {
self.n_done -= 1; self.n_done -= 1;
}
Ok(true)
}
// Set the status of an exercise to "pending" and save.
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
if self.set_status(exercise_ind, false)? {
self.write()?; self.write()?;
} }
@ -271,6 +351,7 @@ impl AppState {
Ok(exercise.path) 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> { pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
if exercise_ind >= self.exercises.len() { if exercise_ind >= self.exercises.len() {
bail!(BAD_INDEX_ERR); bail!(BAD_INDEX_ERR);
@ -280,33 +361,30 @@ impl AppState {
let exercise = &self.exercises[exercise_ind]; let exercise = &self.exercises[exercise_ind];
self.reset(exercise_ind, exercise.path)?; 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. // Return the index of the next pending exercise or `None` if all exercises are done.
fn next_pending_exercise_ind(&self) -> Option<usize> { fn next_pending_exercise_ind(&self) -> Option<usize> {
if self.current_exercise_ind == self.exercises.len() - 1 { let next_ind = self.current_exercise_ind + 1;
// The last exercise is done. self.exercises
// Search for exercises not done from the start. // If the exercise done isn't the last, search for pending exercises after it.
return self.exercises[..self.current_exercise_ind] .get(next_ind..)
.iter() .and_then(|later_exercises| {
.position(|exercise| !exercise.done); later_exercises
}
// 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() .iter()
.position(|exercise| !exercise.done) .position(|exercise| !exercise.done)
{ .map(|ind| next_ind + ind)
Some(ind) => Some(self.current_exercise_ind + 1 + ind), })
None => self.exercises[..self.current_exercise_ind] // Search from the start.
.or_else(|| {
self.exercises[..self.current_exercise_ind]
.iter() .iter()
.position(|exercise| !exercise.done), .position(|exercise| !exercise.done)
} })
} }
/// Official exercises: Dump the solution file form the binary and return its path. /// Official exercises: Dump the solution file from the binary and return its path.
/// Third-party exercises: Check if a solution file exists and return its path in that case. /// 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>> { pub fn current_solution_path(&self) -> Result<Option<String>> {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
@ -320,24 +398,133 @@ impl AppState {
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name) .write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
.map(Some) .map(Some)
} else { } else {
let solution_path = if let Some(dir) = current_exercise.dir { let sol_path = current_exercise.sol_path();
format!("solutions/{dir}/{}.rs", current_exercise.name)
} else {
format!("solutions/{}.rs", current_exercise.name)
};
if Path::new(&solution_path).exists() { if Path::new(&sol_path).exists() {
return Ok(Some(solution_path)); return Ok(Some(sol_path));
} }
Ok(None) Ok(None)
} }
} }
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
let term_width = terminal::size()
.context("Failed to get the terminal size")?
.0;
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
let next_exercise_ind = AtomicUsize::new(0);
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
thread::scope(|s| {
let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
let n_threads = thread::available_parallelism()
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
for _ in 0..n_threads {
let exercise_progress_sender = exercise_progress_sender.clone();
let next_exercise_ind = &next_exercise_ind;
let slf = &self;
thread::Builder::new()
.spawn_scoped(s, move || loop {
let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed);
let Some(exercise) = slf.exercises.get(exercise_ind) else {
// No more exercises.
break;
};
if exercise_progress_sender
.send((exercise_ind, CheckProgress::Checking))
.is_err()
{
break;
};
let success = exercise.run_exercise(None, &slf.cmd_runner);
let progress = match success {
Ok(true) => CheckProgress::Done,
Ok(false) => CheckProgress::Pending,
Err(_) => CheckProgress::None,
};
if exercise_progress_sender
.send((exercise_ind, progress))
.is_err()
{
break;
}
})
.context("Failed to spawn a thread to check all exercises")?;
}
// Drop this sender to detect when the last thread is done.
drop(exercise_progress_sender);
while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
progresses[exercise_ind] = progress;
progress_visualizer.update(&progresses)?;
}
Ok::<_, Error>(())
})?;
let mut first_pending_exercise_ind = None;
for exercise_ind in 0..progresses.len() {
match progresses[exercise_ind] {
CheckProgress::Done => {
self.set_status(exercise_ind, true)?;
}
CheckProgress::Pending => {
self.set_status(exercise_ind, false)?;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
CheckProgress::None | CheckProgress::Checking => {
// If we got an error while checking all exercises in parallel,
// it could be because we exceeded the limit of open file descriptors.
// Therefore, try running exercises with errors sequentially.
progresses[exercise_ind] = CheckProgress::Checking;
progress_visualizer.update(&progresses)?;
let exercise = &self.exercises[exercise_ind];
let success = exercise.run_exercise(None, &self.cmd_runner)?;
if success {
progresses[exercise_ind] = CheckProgress::Done;
} else {
progresses[exercise_ind] = CheckProgress::Pending;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
self.set_status(exercise_ind, success)?;
progress_visualizer.update(&progresses)?;
}
}
}
self.write()?;
Ok(first_pending_exercise_ind)
}
// Return the exercise index of the first pending exercise found.
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
stdout.queue(cursor::Hide)?;
let res = self.check_all_exercises_impl(stdout);
stdout.queue(cursor::Show)?;
res
}
/// Mark the current exercise as done and move on to the next pending exercise if one exists. /// 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 /// 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. /// 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<const CLEAR_BEFORE_FINAL_CHECK: bool>(
&mut self,
stdout: &mut StdoutLock,
) -> Result<ExercisesProgress> {
let exercise = &mut self.exercises[self.current_exercise_ind]; let exercise = &mut self.exercises[self.current_exercise_ind];
if !exercise.done { if !exercise.done {
exercise.done = true; exercise.done = true;
@ -349,69 +536,39 @@ impl AppState {
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; if CLEAR_BEFORE_FINAL_CHECK {
clear_terminal(stdout)?;
let n_exercises = self.exercises.len(); } else {
stdout.write_all(b"\n")?;
let pending_exercise_ind = thread::scope(|s| {
let handles = self
.exercises
.iter_mut()
.map(|exercise| {
s.spawn(|| {
let success = exercise.run_exercise(None, &self.cmd_runner)?;
exercise.done = success;
Ok::<_, Error>(success)
})
})
.collect::<Vec<_>>();
for (exercise_ind, handle) in handles.into_iter().enumerate() {
write!(writer, "\rProgress: {exercise_ind}/{n_exercises}")?;
writer.flush()?;
let success = handle.join().unwrap()?;
if !success {
writer.write_all(b"\n\n")?;
return Ok(Some(exercise_ind));
}
} }
Ok::<_, Error>(None) if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? {
})?; self.set_current_exercise_ind(first_pending_exercise_ind)?;
if let Some(pending_exercise_ind) = pending_exercise_ind {
self.current_exercise_ind = pending_exercise_ind;
self.n_done = self
.exercises
.iter()
.filter(|exercise| exercise.done)
.count() as u16;
self.write()?;
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
// Write that the last exercise is done. self.render_final_message(stdout)?;
self.write()?;
clear_terminal(writer)?;
writer.write_all(FENISH_LINE.as_bytes())?;
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")?;
}
Ok(ExercisesProgress::AllDone) Ok(ExercisesProgress::AllDone)
} }
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
clear_terminal(stdout)?;
stdout.write_all(FENISH_LINE.as_bytes())?;
let final_message = self.final_message.trim_ascii();
if !final_message.is_empty() {
stdout.write_all(final_message.as_bytes())?;
stdout.write_all(b"\n")?;
} }
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" Ok(())
All exercises seem to be done. }
Recompiling and running all exercises to make sure that all of them are actually done. }
";
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 FENISH_LINE: &str = "+----------------------------------------------------+ const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! | | You made it to the Fe-nish line! |
+-------------------------- ------------------------+ +-------------------------- ------------------------+
@ -443,6 +600,7 @@ mod tests {
dir: None, dir: None,
name: "0", name: "0",
path: "exercises/0.rs", path: "exercises/0.rs",
canonical_path: None,
test: false, test: false,
strict_clippy: false, strict_clippy: false,
hint: "", hint: "",
@ -457,9 +615,11 @@ mod tests {
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
n_done: 0, n_done: 0,
final_message: String::new(), final_message: String::new(),
state_file: tempfile::tempfile().unwrap(),
file_buf: Vec::new(), file_buf: Vec::new(),
official_exercises: true, official_exercises: true,
cmd_runner: CmdRunner::build().unwrap(), cmd_runner: CmdRunner::build().unwrap(),
vs_code: false,
}; };
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| { let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {

View file

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

View file

@ -74,12 +74,14 @@ impl CmdRunner {
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?"); bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
} }
let target_dir = serde_json::de::from_slice::<CargoMetadata>(&metadata_output.stdout) let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout)
.context( .context(
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`", "Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
)?.target_directory; )?;
Ok(Self { target_dir }) Ok(Self {
target_dir: metadata.target_directory,
})
} }
pub fn cargo<'out>( pub fn cargo<'out>(
@ -123,7 +125,7 @@ pub struct CargoSubcommand<'out> {
output: Option<&'out mut Vec<u8>>, output: Option<&'out mut Vec<u8>>,
} }
impl<'out> CargoSubcommand<'out> { impl CargoSubcommand<'_> {
#[inline] #[inline]
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
where where

View file

@ -1,10 +0,0 @@
use ahash::AHasher;
use std::hash::BuildHasherDefault;
/// DOS attacks aren't a concern for Rustlings. Therefore, we use `ahash` with fixed seeds.
pub type HashSet<T> = std::collections::HashSet<T, BuildHasherDefault<AHasher>>;
#[inline]
pub fn hash_set_with_capacity<T>(capacity: usize) -> HashSet<T> {
HashSet::with_capacity_and_hasher(capacity, BuildHasherDefault::<AHasher>::default())
}

View file

@ -1,6 +1,7 @@
use anyhow::{anyhow, bail, Context, Error, Result}; use anyhow::{anyhow, bail, Context, Error, Result};
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::HashSet,
fs::{self, read_dir, OpenOptions}, fs::{self, read_dir, OpenOptions},
io::{self, Read, Write}, io::{self, Read, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -11,12 +12,14 @@ use std::{
use crate::{ use crate::{
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
cmd::CmdRunner, cmd::CmdRunner,
collections::{hash_set_with_capacity, HashSet},
exercise::{RunnableExercise, OUTPUT_CAPACITY}, exercise::{RunnableExercise, OUTPUT_CAPACITY},
info_file::{ExerciseInfo, InfoFile}, info_file::{ExerciseInfo, InfoFile},
CURRENT_FORMAT_VERSION, CURRENT_FORMAT_VERSION,
}; };
const MAX_N_EXERCISES: usize = 999;
const MAX_EXERCISE_NAME_LEN: usize = 32;
// Find a char that isn't allowed in the exercise's `name` or `dir`. // Find a char that isn't allowed in the exercise's `name` or `dir`.
fn forbidden_char(input: &str) -> Option<char> { fn forbidden_char(input: &str) -> Option<char> {
input.chars().find(|c| !c.is_alphanumeric() && *c != '_') input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
@ -39,10 +42,10 @@ fn check_cargo_toml(
if old_bins != new_bins { if old_bins != new_bins {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again"); bail!("The file `dev/Cargo.toml` is outdated. Run `cargo 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. Then run `rustlings dev check` again"); bail!("The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again");
} }
Ok(()) Ok(())
@ -50,8 +53,8 @@ fn check_cargo_toml(
// Check the info of all exercises and return their paths in a set. // Check the info of all exercises and return their paths in a set.
fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> { fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
let mut names = hash_set_with_capacity(info_file.exercises.len()); let mut names = HashSet::with_capacity(info_file.exercises.len());
let mut paths = hash_set_with_capacity(info_file.exercises.len()); let mut paths = HashSet::with_capacity(info_file.exercises.len());
let mut file_buf = String::with_capacity(1 << 14); let mut file_buf = String::with_capacity(1 << 14);
for exercise_info in &info_file.exercises { for exercise_info in &info_file.exercises {
@ -59,6 +62,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
if name.is_empty() { if name.is_empty() {
bail!("Found an empty exercise name in `info.toml`"); 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) { if let Some(c) = forbidden_char(name) {
bail!("Char `{c}` in the exercise name `{name}` is not allowed"); bail!("Char `{c}` in the exercise name `{name}` is not allowed");
} }
@ -97,7 +103,12 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user."); bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
} }
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"); bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file");
} }
@ -160,11 +171,13 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
Ok(()) Ok(())
} }
fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { fn check_exercises_unsolved(
println!( info_file: &'static InfoFile,
"Running all exercises to check that they aren't already solved. This may take a while…\n", cmd_runner: &'static CmdRunner,
); ) -> Result<()> {
thread::scope(|s| { let mut stdout = io::stdout().lock();
stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?;
let handles = info_file let handles = info_file
.exercises .exercises
.iter() .iter()
@ -173,48 +186,61 @@ fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Res
return None; return None;
} }
Some(( Some(
exercise_info.name.as_str(), thread::Builder::new()
s.spawn(|| exercise_info.run_exercise(None, cmd_runner)), .spawn(|| exercise_info.run_exercise(None, cmd_runner))
)) .map(|handle| (exercise_info.name.as_str(), handle)),
)
}) })
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()
.context("Failed to spawn a thread to check if an exercise is already solved")?;
let n_handles = handles.len();
write!(stdout, "Progress: 0/{n_handles}")?;
stdout.flush()?;
let mut handle_num = 1;
for (exercise_name, handle) in handles { for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else { let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}"); bail!("Panic while trying to run the exercise {exercise_name}");
}; };
match result { match result {
Ok(true) => bail!( Ok(true) => {
"The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}", bail!("The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",)
), }
Ok(false) => (), Ok(false) => (),
Err(e) => return Err(e), Err(e) => return Err(e),
} }
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
stdout.flush()?;
handle_num += 1;
} }
stdout.write_all(b"\n")?;
Ok(()) Ok(())
})
} }
fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> {
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { 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::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::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
Ordering::Equal => (), Ordering::Equal => (),
} }
let info_file_paths = check_info_file_exercises(info_file)?; let handle = thread::Builder::new()
let handle = thread::spawn(move || check_unexpected_files("exercises", &info_file_paths)); .spawn(move || check_exercises_unsolved(info_file, cmd_runner))
.context("Failed to spawn a thread to check if any exercise is already solved")?;
let info_file_paths = check_info_file_exercises(info_file)?;
check_unexpected_files("exercises", &info_file_paths)?;
check_exercises_unsolved(info_file, cmd_runner)?;
handle.join().unwrap() handle.join().unwrap()
} }
enum SolutionCheck { enum SolutionCheck {
Success { sol_path: String }, Success { sol_path: String },
MissingRequired,
MissingOptional, MissingOptional,
RunFailure { output: Vec<u8> }, RunFailure { output: Vec<u8> },
Err(Error), Err(Error),
@ -222,20 +248,24 @@ enum SolutionCheck {
fn check_solutions( fn check_solutions(
require_solutions: bool, require_solutions: bool,
info_file: &InfoFile, info_file: &'static InfoFile,
cmd_runner: &CmdRunner, cmd_runner: &'static CmdRunner,
) -> Result<()> { ) -> Result<()> {
println!("Running all solutions. This may take a while…\n"); let mut stdout = io::stdout().lock();
thread::scope(|s| { stdout.write_all(b"Running all solutions...\n")?;
let handles = info_file let handles = info_file
.exercises .exercises
.iter() .iter()
.map(|exercise_info| { .map(|exercise_info| {
s.spawn(|| { thread::Builder::new().spawn(move || {
let sol_path = exercise_info.sol_path(); let sol_path = exercise_info.sol_path();
if !Path::new(&sol_path).exists() { if !Path::new(&sol_path).exists() {
if require_solutions { if require_solutions {
return SolutionCheck::MissingRequired; return SolutionCheck::Err(anyhow!(
"The solution of the exercise {} is missing",
exercise_info.name,
));
} }
return SolutionCheck::MissingOptional; return SolutionCheck::MissingOptional;
@ -249,9 +279,10 @@ fn check_solutions(
} }
}) })
}) })
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()
.context("Failed to spawn a thread to check a solution")?;
let mut sol_paths = hash_set_with_capacity(info_file.exercises.len()); let mut sol_paths = HashSet::with_capacity(info_file.exercises.len());
let mut fmt_cmd = Command::new("rustfmt"); let mut fmt_cmd = Command::new("rustfmt");
fmt_cmd fmt_cmd
.arg("--check") .arg("--check")
@ -261,10 +292,15 @@ fn check_solutions(
.arg("always") .arg("always")
.stdin(Stdio::null()); .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) { for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
let Ok(check_result) = handle.join() else { let Ok(check_result) = handle.join() else {
bail!( bail!(
"Panic while trying to run the solution of the exericse {}", "Panic while trying to run the solution of the exercise {}",
exercise_info.name, exercise_info.name,
); );
}; };
@ -274,15 +310,10 @@ fn check_solutions(
fmt_cmd.arg(&sol_path); fmt_cmd.arg(&sol_path);
sol_paths.insert(PathBuf::from(sol_path)); sol_paths.insert(PathBuf::from(sol_path));
} }
SolutionCheck::MissingRequired => {
bail!(
"The solution of the exercise {} is missing",
exercise_info.name,
);
}
SolutionCheck::MissingOptional => (), SolutionCheck::MissingOptional => (),
SolutionCheck::RunFailure { output } => { SolutionCheck::RunFailure { output } => {
io::stderr().lock().write_all(&output)?; stdout.write_all(b"\n\n")?;
stdout.write_all(&output)?;
bail!( bail!(
"Running the solution of the exercise {} failed with the error above", "Running the solution of the exercise {} failed with the error above",
exercise_info.name, exercise_info.name,
@ -290,9 +321,18 @@ fn check_solutions(
} }
SolutionCheck::Err(e) => return Err(e), SolutionCheck::Err(e) => return Err(e),
} }
}
let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths)); write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
stdout.flush()?;
handle_num += 1;
}
stdout.write_all(b"\n")?;
let handle = thread::Builder::new()
.spawn(move || check_unexpected_files("solutions", &sol_paths))
.context(
"Failed to spawn a thread to check for unexpected files in the solutions directory",
)?;
if !fmt_cmd if !fmt_cmd
.status() .status()
@ -303,12 +343,15 @@ fn check_solutions(
} }
handle.join().unwrap() handle.join().unwrap()
})
} }
pub fn check(require_solutions: bool) -> Result<()> { pub fn check(require_solutions: bool) -> Result<()> {
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
if info_file.exercises.len() > MAX_N_EXERCISES {
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
}
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
// A hack to make `cargo run -- dev check` work when developing Rustlings. // A hack to make `cargo run -- dev check` work when developing Rustlings.
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?; check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
@ -316,9 +359,12 @@ pub fn check(require_solutions: bool) -> Result<()> {
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?; check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
} }
let cmd_runner = CmdRunner::build()?; // Leaking is fine since they are used until the end of the program.
check_exercises(&info_file, &cmd_runner)?; let cmd_runner = Box::leak(Box::new(CmdRunner::build()?));
check_solutions(require_solutions, &info_file, &cmd_runner)?; let info_file = Box::leak(Box::new(info_file));
check_exercises(info_file, cmd_runner)?;
check_solutions(require_solutions, info_file, cmd_runner)?;
println!("Everything looks fine!"); println!("Everything looks fine!");

View file

@ -6,7 +6,7 @@ use std::{
process::Command, process::Command,
}; };
use crate::CURRENT_FORMAT_VERSION; use crate::{init::RUST_ANALYZER_TOML, CURRENT_FORMAT_VERSION};
// Create a directory relative to the current directory and print its path. // Create a directory relative to the current directory and print its path.
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> { fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
@ -62,6 +62,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
write_rel_file("README.md", &dir_path_str, README)?; write_rel_file("README.md", &dir_path_str, README)?;
write_rel_file("rust-analyzer.toml", &dir_path_str, RUST_ANALYZER_TOML)?;
create_rel_dir(".vscode", &dir_path_str)?; create_rel_dir(".vscode", &dir_path_str)?;
write_rel_file( write_rel_file(
".vscode/extensions.json", ".vscode/extensions.json",

View file

@ -1,7 +1,7 @@
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use std::{ use std::{
fs::{create_dir, OpenOptions}, fs::{self, create_dir},
io::{self, Write}, io,
}; };
use crate::info_file::ExerciseInfo; use crate::info_file::ExerciseInfo;
@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo;
/// Contains all embedded files. /// Contains all embedded files.
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
#[derive(Clone, Copy)]
pub enum WriteStrategy {
IfNotExists,
Overwrite,
}
impl WriteStrategy {
fn write(self, path: &str, content: &[u8]) -> Result<()> {
let file = match self {
Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path),
Self::Overwrite => OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path),
};
file.with_context(|| format!("Failed to open the file `{path}` in write mode"))?
.write_all(content)
.with_context(|| format!("Failed to write the file {path}"))
}
}
// Files related to one exercise. // Files related to one exercise.
struct ExerciseFiles { struct ExerciseFiles {
// The content of the exercise file. // The content of the exercise file.
@ -42,6 +19,16 @@ struct ExerciseFiles {
dir_ind: usize, dir_ind: usize,
} }
fn create_dir_if_not_exists(path: &str) -> Result<()> {
if let Err(e) = create_dir(path) {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(Error::from(e).context(format!("Failed to create the directory {path}")));
}
}
Ok(())
}
// A directory in the `exercises/` directory. // A directory in the `exercises/` directory.
pub struct ExerciseDir { pub struct ExerciseDir {
pub name: &'static str, pub name: &'static str,
@ -55,21 +42,13 @@ impl ExerciseDir {
let mut dir_path = String::with_capacity(20 + self.name.len()); let mut dir_path = String::with_capacity(20 + self.name.len());
dir_path.push_str("exercises/"); dir_path.push_str("exercises/");
dir_path.push_str(self.name); dir_path.push_str(self.name);
create_dir_if_not_exists(&dir_path)?;
if let Err(e) = create_dir(&dir_path) {
if e.kind() == io::ErrorKind::AlreadyExists {
return Ok(());
}
return Err(
Error::from(e).context(format!("Failed to create the directory {dir_path}"))
);
}
let mut readme_path = dir_path; let mut readme_path = dir_path;
readme_path.push_str("/README.md"); readme_path.push_str("/README.md");
WriteStrategy::Overwrite.write(&readme_path, self.readme) fs::write(&readme_path, self.readme)
.with_context(|| format!("Failed to write the file {readme_path}"))
} }
} }
@ -86,17 +65,31 @@ impl EmbeddedFiles {
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
create_dir("exercises").context("Failed to create the directory `exercises`")?; create_dir("exercises").context("Failed to create the directory `exercises`")?;
WriteStrategy::IfNotExists.write( fs::write(
"exercises/README.md", "exercises/README.md",
include_bytes!("../exercises/README.md"), include_bytes!("../exercises/README.md"),
)?; )
.context("Failed to write the file exercises/README.md")?;
for dir in self.exercise_dirs { for dir in self.exercise_dirs {
dir.init_on_disk()?; dir.init_on_disk()?;
} }
let mut exercise_path = String::with_capacity(64);
let prefix = "exercises/";
exercise_path.push_str(prefix);
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) { for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?; let dir = &self.exercise_dirs[exercise_files.dir_ind];
exercise_path.truncate(prefix.len());
exercise_path.push_str(dir.name);
exercise_path.push('/');
exercise_path.push_str(&exercise_info.name);
exercise_path.push_str(".rs");
fs::write(&exercise_path, exercise_files.exercise)
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
} }
Ok(()) Ok(())
@ -107,7 +100,8 @@ impl EmbeddedFiles {
let dir = &self.exercise_dirs[exercise_files.dir_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind];
dir.init_on_disk()?; dir.init_on_disk()?;
WriteStrategy::Overwrite.write(path, exercise_files.exercise) fs::write(path, exercise_files.exercise)
.with_context(|| format!("Failed to write the exercise file {path}"))
} }
/// Write the solution file to disk and return its path. /// Write the solution file to disk and return its path.
@ -116,19 +110,25 @@ impl EmbeddedFiles {
exercise_ind: usize, exercise_ind: usize,
exercise_name: &str, exercise_name: &str,
) -> Result<String> { ) -> Result<String> {
create_dir_if_not_exists("solutions")?;
let exercise_files = &self.exercise_files[exercise_ind]; let exercise_files = &self.exercise_files[exercise_ind];
let dir = &self.exercise_dirs[exercise_files.dir_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind];
// 14 = 10 + 1 + 3 // 14 = 10 + 1 + 3
// solutions/ + / + .rs // solutions/ + / + .rs
let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
solution_path.push_str("solutions/"); dir_path.push_str("solutions/");
solution_path.push_str(dir.name); dir_path.push_str(dir.name);
create_dir_if_not_exists(&dir_path)?;
let mut solution_path = dir_path;
solution_path.push('/'); solution_path.push('/');
solution_path.push_str(exercise_name); solution_path.push_str(exercise_name);
solution_path.push_str(".rs"); solution_path.push_str(".rs");
WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?; fs::write(&solution_path, exercise_files.solution)
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
Ok(solution_path) Ok(solution_path)
} }

View file

@ -1,15 +1,31 @@
use anyhow::Result; use anyhow::Result;
use ratatui::crossterm::style::{style, StyledContent, Stylize}; use crossterm::{
use std::{ style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
fmt::{self, Display, Formatter}, QueueableCommand,
io::Write,
}; };
use std::io::{self, StdoutLock, Write};
use crate::{cmd::CmdRunner, terminal_link::TerminalFileLink}; use crate::{
cmd::CmdRunner,
term::{self, terminal_file_link, write_ansi, CountedWrite},
};
/// The initial capacity of the output buffer. /// The initial capacity of the output buffer.
pub const OUTPUT_CAPACITY: usize = 1 << 14; 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: ")?;
if let Some(canonical_path) = term::canonicalize(solution_path) {
terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?;
} else {
stdout.write_all(solution_path.as_bytes())?;
}
stdout.write_all(b"\n")
}
// Run an exercise binary and append its output to the `output` buffer. // Run an exercise binary and append its output to the `output` buffer.
// Compilation must be done before calling this method. // Compilation must be done before calling this method.
fn run_bin( fn run_bin(
@ -18,7 +34,10 @@ fn run_bin(
cmd_runner: &CmdRunner, cmd_runner: &CmdRunner,
) -> Result<bool> { ) -> Result<bool> {
if let Some(output) = output.as_deref_mut() { if let Some(output) = output.as_deref_mut() {
writeln!(output, "{}", "Output".underlined())?; write_ansi(output, SetAttribute(Attribute::Underlined));
output.extend_from_slice(b"Output");
write_ansi(output, ResetColor);
output.push(b'\n');
} }
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?; let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
@ -28,13 +47,11 @@ fn run_bin(
// This output is important to show the user that something went wrong. // This output is important to show the user that something went wrong.
// Otherwise, calling something like `exit(1)` in an exercise without further output // Otherwise, calling something like `exit(1)` in an exercise without further output
// leaves the user confused about why the exercise isn't done yet. // leaves the user confused about why the exercise isn't done yet.
writeln!( write_ansi(output, SetAttribute(Attribute::Bold));
output, write_ansi(output, SetForegroundColor(Color::Red));
"{}", output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)");
"The exercise didn't run successfully (nonzero exit code)" write_ansi(output, ResetColor);
.bold() output.push(b'\n');
.red(),
)?;
} }
} }
@ -47,6 +64,7 @@ pub struct Exercise {
pub name: &'static str, pub name: &'static str,
/// Path of the exercise file starting with the `exercises/` directory. /// Path of the exercise file starting with the `exercises/` directory.
pub path: &'static str, pub path: &'static str,
pub canonical_path: Option<String>,
pub test: bool, pub test: bool,
pub strict_clippy: bool, pub strict_clippy: bool,
pub hint: &'static str, pub hint: &'static str,
@ -54,25 +72,24 @@ pub struct Exercise {
} }
impl Exercise { impl Exercise {
pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> { pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> {
style(TerminalFileLink(self.path)).underlined().blue() if let Some(canonical_path) = self.canonical_path.as_deref() {
} return terminal_file_link(writer, self.path, canonical_path, Color::Blue);
} }
impl Display for Exercise { writer.write_str(self.path)
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.path.fmt(f)
} }
} }
pub trait RunnableExercise { pub trait RunnableExercise {
fn name(&self) -> &str; fn name(&self) -> &str;
fn dir(&self) -> Option<&str>;
fn strict_clippy(&self) -> bool; fn strict_clippy(&self) -> bool;
fn test(&self) -> bool; fn test(&self) -> bool;
// Compile, check and run the exercise or its solution (depending on `bin_name´). // Compile, check and run the exercise or its solution (depending on `bin_name´).
// The output is written to the `output` buffer after clearing it. // The output is written to the `output` buffer after clearing it.
fn run( fn run<const FORCE_STRICT_CLIPPY: bool>(
&self, &self,
bin_name: &str, bin_name: &str,
mut output: Option<&mut Vec<u8>>, mut output: Option<&mut Vec<u8>>,
@ -98,7 +115,7 @@ pub trait RunnableExercise {
let output_is_some = output.is_some(); let output_is_some = output.is_some();
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut()); let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
if output_is_some { if output_is_some {
test_cmd.args(["--", "--color", "always", "--show-output"]); test_cmd.args(["--", "--color", "always", "--format", "pretty"]);
} }
let test_success = test_cmd.run("cargo test …")?; let test_success = test_cmd.run("cargo test …")?;
if !test_success { if !test_success {
@ -114,8 +131,8 @@ pub trait RunnableExercise {
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut()); let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
// `--profile test` is required to also check code with `[cfg(test)]`. // `--profile test` is required to also check code with `#[cfg(test)]`.
if self.strict_clippy() { if FORCE_STRICT_CLIPPY || self.strict_clippy() {
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]); clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
} else { } else {
clippy_cmd.args(["--profile", "test"]); clippy_cmd.args(["--profile", "test"]);
@ -131,7 +148,7 @@ pub trait RunnableExercise {
/// The output is written to the `output` buffer after clearing it. /// The output is written to the `output` buffer after clearing it.
#[inline] #[inline]
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> { fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
self.run(self.name(), output, cmd_runner) self.run::<false>(self.name(), output, cmd_runner)
} }
/// Compile, check and run the exercise's solution. /// Compile, check and run the exercise's solution.
@ -142,7 +159,32 @@ pub trait RunnableExercise {
bin_name.push_str(name); bin_name.push_str(name);
bin_name.push_str("_sol"); bin_name.push_str("_sol");
self.run(&bin_name, output, cmd_runner) 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
} }
} }
@ -152,6 +194,11 @@ impl RunnableExercise for Exercise {
self.name self.name
} }
#[inline]
fn dir(&self) -> Option<&str> {
self.dir
}
#[inline] #[inline]
fn strict_clippy(&self) -> bool { fn strict_clippy(&self) -> bool {
self.strict_clippy self.strict_clippy

View file

@ -52,30 +52,6 @@ impl ExerciseInfo {
path 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 { impl RunnableExercise for ExerciseInfo {
@ -84,6 +60,11 @@ impl RunnableExercise for ExerciseInfo {
&self.name &self.name
} }
#[inline]
fn dir(&self) -> Option<&str> {
self.dir.as_deref()
}
#[inline] #[inline]
fn strict_clippy(&self) -> bool { fn strict_clippy(&self) -> bool {
self.strict_clippy self.strict_clippy
@ -135,4 +116,4 @@ impl InfoFile {
} }
const NO_EXERCISES_ERR: &str = "There are no exercises yet! 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,5 +1,8 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use ratatui::crossterm::style::Stylize; use crossterm::{
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
QueueableCommand,
};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
env::set_current_dir, env::set_current_dir,
@ -10,8 +13,8 @@ use std::{
}; };
use crate::{ use crate::{
cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile, cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, exercise::RunnableExercise,
term::press_enter_prompt, info_file::InfoFile, term::press_enter_prompt,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -127,6 +130,9 @@ pub fn init() -> Result<()> {
fs::write("Cargo.toml", updated_cargo_toml) fs::write("Cargo.toml", updated_cargo_toml)
.context("Failed to create the file `rustlings/Cargo.toml`")?; .context("Failed to create the file `rustlings/Cargo.toml`")?;
fs::write("rust-analyzer.toml", RUST_ANALYZER_TOML)
.context("Failed to create the file `rustlings/rust-analyzer.toml`")?;
fs::write(".gitignore", GITIGNORE) fs::write(".gitignore", GITIGNORE)
.context("Failed to create the file `rustlings/.gitignore`")?; .context("Failed to create the file `rustlings/.gitignore`")?;
@ -139,16 +145,19 @@ pub fn init() -> Result<()> {
let _ = Command::new("git") let _ = Command::new("git")
.arg("init") .arg("init")
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.status(); .status();
} }
writeln!( stdout.queue(SetForegroundColor(Color::Green))?;
stdout, stdout.write_all("Initialization done ✓".as_bytes())?;
"\n{}\n\n{}", stdout.queue(ResetColor)?;
"Initialization done ✓".green(), stdout.write_all(b"\n\n")?;
POST_INIT_MSG.bold(),
)?; stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(POST_INIT_MSG)?;
stdout.queue(ResetColor)?;
Ok(()) Ok(())
} }
@ -163,6 +172,10 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
} }
"; ";
pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy"
check.extraArgs = ["--profile", "test"]
"#;
const GITIGNORE: &[u8] = b"Cargo.lock const GITIGNORE: &[u8] = b"Cargo.lock
target/ target/
.vscode/ .vscode/
@ -181,5 +194,6 @@ You probably already initialized Rustlings.
Run `cd rustlings` Run `cd rustlings`
Then run `rustlings` again"; Then run `rustlings` again";
const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. const POST_INIT_MSG: &[u8] = b"Run `cd rustlings` to go into the generated directory.
Then run `rustlings` to get started."; Then run `rustlings` to get started.
";

View file

@ -1,93 +1,135 @@
use anyhow::Result; use anyhow::{Context, Result};
use ratatui::{ use crossterm::{
backend::CrosstermBackend, cursor,
crossterm::{ event::{
event::{self, Event, KeyCode, KeyEventKind}, self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
}, },
Terminal, terminal::{
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
LeaveAlternateScreen,
},
QueueableCommand,
}; };
use std::io; use std::io::{self, StdoutLock, Write};
use crate::app_state::AppState; use crate::app_state::AppState;
use self::state::{Filter, UiState}; use self::state::{Filter, ListState};
mod scroll_state;
mod state; mod state;
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
let mut list_state = ListState::build(app_state, stdout)?;
let mut is_searching = false;
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();
if is_searching {
match key.code {
KeyCode::Esc | KeyCode::Enter => {
is_searching = false;
list_state.search_query.clear();
}
KeyCode::Char(c) => {
list_state.search_query.push(c);
list_state.apply_search_query();
}
KeyCode::Backspace => {
list_state.search_query.pop();
list_state.apply_search_query();
}
_ => continue,
}
list_state.draw(stdout)?;
continue;
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
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') => {
if list_state.filter() == Filter::Pending {
list_state.set_filter(Filter::None);
list_state.message.push_str("Disabled filter PENDING");
} else {
list_state.set_filter(Filter::Pending);
list_state.message.push_str(
"Enabled filter PENDING │ Press p again to disable the filter",
);
}
}
KeyCode::Char('r') => list_state.reset_selected()?,
KeyCode::Char('c') => {
if list_state.selected_to_current_exercise()? {
return Ok(());
}
}
KeyCode::Char('s' | '/') => {
is_searching = true;
list_state.apply_search_query();
}
// 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<()> { pub fn list(app_state: &mut AppState) -> Result<()> {
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
stdout.execute(EnterAlternateScreen)?; stdout
.queue(EnterAlternateScreen)?
.queue(cursor::Hide)?
.queue(DisableLineWrap)?
.queue(EnableMouseCapture)?;
enable_raw_mode()?; enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; let res = handle_list(app_state, &mut stdout);
terminal.clear()?;
let mut ui_state = UiState::new(app_state); // Restore the terminal even if we got an error.
stdout
'outer: loop { .queue(LeaveAlternateScreen)?
terminal.try_draw(|frame| ui_state.draw(frame).map_err(io::Error::other))?; .queue(cursor::Show)?
.queue(EnableLineWrap)?
let key = loop { .queue(DisableMouseCapture)?
match event::read()? { .flush()?;
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)?;
disable_raw_mode()?; 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
}
pub 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,36 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use ratatui::{ use crossterm::{
layout::{Constraint, Rect}, cursor::{MoveTo, MoveToNextLine},
style::{Style, Stylize}, style::{
text::{Line, Span}, Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, },
Frame, 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, CountedWrite, MaxLenWriter},
};
use super::scroll_state::ScrollState;
const COL_SPACING: usize = 2;
const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none()
.with(Attribute::Reverse)
.with(Attribute::Bold);
fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
stdout
.queue(Clear(ClearType::UntilNewLine))?
.queue(MoveToNextLine(1))?;
Ok(())
}
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum Filter { pub enum Filter {
@ -17,255 +39,386 @@ pub enum Filter {
None, None,
} }
pub struct UiState<'a> { pub struct ListState<'a> {
pub table: Table<'static>, /// Footer message to be displayed if not empty.
pub message: String, pub message: String,
pub filter: Filter, pub search_query: String,
app_state: &'a mut AppState, app_state: &'a mut AppState,
table_state: TableState, scroll_state: ScrollState,
n_rows: usize, name_col_padding: Vec<u8>,
path_col_padding: Vec<u8>,
filter: Filter,
term_width: u16,
term_height: u16,
show_footer: bool,
} }
impl<'a> UiState<'a> { impl<'a> ListState<'a> {
pub fn with_updated_rows(mut self) -> Self { pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
let current_exercise_ind = self.app_state.current_exercise_ind(); stdout.queue(Clear(ClearType::All))?;
self.n_rows = 0; let name_col_title_len = 4;
let rows = self let path_col_title_len = 4;
let (name_col_width, path_col_width) = app_state.exercises().iter().fold(
(name_col_title_len, path_col_title_len),
|(name_col_width, path_col_width), exercise| {
(
name_col_width.max(exercise.name.len()),
path_col_width.max(exercise.path.len()),
)
},
);
let name_col_padding = vec![b' '; name_col_width + COL_SPACING];
let path_col_padding = vec![b' '; path_col_width];
let filter = Filter::None;
let n_rows_with_filter = app_state.exercises().len();
let selected = app_state.current_exercise_ind();
let (width, height) = terminal::size().context("Failed to get the terminal size")?;
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
let mut slf = Self {
message: String::with_capacity(128),
search_query: String::new(),
app_state,
scroll_state,
name_col_padding,
path_col_padding,
filter,
// Set by `set_term_size`
term_width: 0,
term_height: 0,
show_footer: true,
};
slf.set_term_size(width, height);
slf.draw(stdout)?;
Ok(slf)
}
pub fn set_term_size(&mut self, width: u16, height: u16) {
self.term_width = width;
self.term_height = height;
if height == 0 {
return;
}
let header_height = 1;
// 1 progress bar, 2 footer message lines.
let footer_height = 3;
self.show_footer = height > header_height + footer_height;
self.scroll_state.set_max_n_rows_to_display(
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
as usize,
);
}
fn draw_exercise_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> {
if !self.search_query.is_empty() {
if let Some((pre_highlight, highlight, post_highlight)) = exercise
.name
.find(&self.search_query)
.and_then(|ind| exercise.name.split_at_checked(ind))
.and_then(|(pre_highlight, rest)| {
rest.split_at_checked(self.search_query.len())
.map(|x| (pre_highlight, x.0, x.1))
})
{
writer.write_str(pre_highlight)?;
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
writer.write_str(highlight)?;
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
return writer.write_str(post_highlight);
}
}
writer.write_str(exercise.name)
}
fn draw_rows(
&self,
stdout: &mut StdoutLock,
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) {
// The crab emoji has the width of two ascii chars.
writer.add_to_len(2);
writer.stdout.write_all("🦀".as_bytes())?;
writer
.stdout
.queue(SetAttributes(SELECTED_ROW_ATTRIBUTES))?;
} else {
writer.write_ascii(b" ")?;
}
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_ascii(b" ")?;
self.draw_exercise_name(&mut writer, exercise)?;
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 {
exercise.terminal_file_link(&mut writer)?;
}
writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?;
next_ln(stdout)?;
stdout.queue(ResetColor)?;
n_displayed_rows += 1;
}
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 {
progress_bar(
&mut MaxLenWriter::new(stdout, self.term_width as usize),
self.app_state.n_done(),
self.app_state.exercises().len() as u16,
self.term_width,
)?;
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")?;
next_ln(stdout)?;
writer = MaxLenWriter::new(stdout, self.term_width as usize);
writer.write_ascii(b"<s>earch | filter ")?;
} else {
// Nothing selected (and nothing shown), so only display filter and quit.
writer.write_ascii(b"filter ")?;
}
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")?;
} else {
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
writer.write_str(&self.message)?;
stdout.queue(ResetColor)?;
next_ln(stdout)?;
}
next_ln(stdout)?;
}
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(),
};
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 .app_state
.exercises() .exercises()
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(ind, exercise)| { .filter(|(_, exercise)| exercise.done)
let exercise_state = if exercise.done { .nth(selected)
if self.filter == Filter::Pending { .context("Invalid selection index")
return None; .map(|(ind, _)| ind),
} Filter::Pending => self
.app_state
"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
.exercises() .exercises()
.iter() .iter()
.map(|exercise| exercise.name.len()) .enumerate()
.max() .filter(|(_, exercise)| !exercise.done)
.unwrap_or(4) as u16; .nth(selected)
.context("Invalid selection index")
.map(|(ind, _)| ind),
Filter::None => Ok(selected),
}
}
let widths = [ pub fn reset_selected(&mut self) -> Result<()> {
Constraint::Length(4), let Some(selected) = self.scroll_state.selected() else {
Constraint::Length(7), self.message.push_str("Nothing selected to reset!");
Constraint::Length(max_name_len), return Ok(());
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));
let filter = Filter::None;
let n_rows = app_state.exercises().len();
let slf = Self {
table,
message: String::with_capacity(128),
filter,
app_state,
table_state,
n_rows,
}; };
slf.with_updated_rows() let exercise_ind = self.selected_to_exercise_ind(selected)?;
} let exercise_name = self.app_state.reset_exercise_by_ind(exercise_ind)?;
self.update_rows();
pub fn select_next(&mut self) { write!(
if self.n_rows > 0 { self.message,
let next = self "The exercise `{exercise_name}` has been reset",
.table_state )?;
.selected()
.map_or(0, |selected| (selected + 1).min(self.n_rows - 1));
self.table_state.select(Some(next));
}
}
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));
}
}
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.area();
frame.render_stateful_widget(
&self.table,
Rect {
x: 0,
y: 0,
width: area.width,
height: area.height - 3,
},
&mut self.table_state,
);
frame.render_widget(
Paragraph::new(progress_bar_ratatui(
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,
},
);
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"));
}
Filter::Pending => {
spans.push(Span::raw("<d>one/"));
spans.push("<p>ending".underlined().magenta());
}
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())
};
frame.render_widget(
message,
Rect {
x: 0,
y: area.height - 1,
width: area.width,
height: 1,
},
);
Ok(()) Ok(())
} }
pub fn with_reset_selected(mut self) -> Result<Self> { pub fn apply_search_query(&mut self) {
let Some(selected) = self.table_state.selected() else { self.message.push_str("search:");
return Ok(self); self.message.push_str(&self.search_query);
self.message.push('|');
if self.search_query.is_empty() {
return;
}
let is_search_result = |exercise: &Exercise| exercise.name.contains(&self.search_query);
let mut iter = self.app_state.exercises().iter();
let ind = match self.filter {
Filter::None => iter.position(is_search_result),
Filter::Done => iter
.filter(|exercise| exercise.done)
.position(is_search_result),
Filter::Pending => iter
.filter(|exercise| !exercise.done)
.position(is_search_result),
}; };
let ind = self match ind {
.app_state Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind),
.exercises() None => self.message.push_str(" (not found)"),
.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_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<()> { // Return `true` if there was something to select.
let Some(selected) = self.table_state.selected() else { pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
return Ok(()); let Some(selected) = self.scroll_state.selected() else {
self.message.push_str("Nothing selected to continue at!");
return Ok(false);
}; };
let ind = self let exercise_ind = self.selected_to_exercise_ind(selected)?;
.app_state self.app_state.set_current_exercise_ind(exercise_ind)?;
.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

@ -4,26 +4,23 @@ use clap::{Parser, Subcommand};
use std::{ use std::{
io::{self, IsTerminal, Write}, io::{self, IsTerminal, Write},
path::Path, path::Path,
process::exit, process::ExitCode,
}; };
use term::{clear_terminal, press_enter_prompt}; use term::{clear_terminal, press_enter_prompt};
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile};
mod app_state; mod app_state;
mod cargo_toml; mod cargo_toml;
mod cmd; mod cmd;
mod collections;
mod dev; mod dev;
mod embedded; mod embedded;
mod exercise; mod exercise;
mod info_file; mod info_file;
mod init; mod init;
mod list; mod list;
mod progress_bar;
mod run; mod run;
mod term; mod term;
mod terminal_link;
mod watch; mod watch;
const CURRENT_FORMAT_VERSION: u8 = 1; const CURRENT_FORMAT_VERSION: u8 = 1;
@ -49,6 +46,8 @@ enum Subcommands {
/// The name of the exercise /// The name of the exercise
name: Option<String>, name: Option<String>,
}, },
/// Check all the exercises, marking them as done or pending accordingly.
CheckAll,
/// Reset a single exercise /// Reset a single exercise
Reset { Reset {
/// The name of the exercise /// The name of the exercise
@ -64,24 +63,26 @@ enum Subcommands {
Dev(DevCommands), Dev(DevCommands),
} }
fn main() -> Result<()> { fn main() -> Result<ExitCode> {
let args = Args::parse(); let args = Args::parse();
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() { if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
bail!("{OLD_METHOD_ERR}"); bail!("{OLD_METHOD_ERR}");
} }
'priority_cmd: {
match args.command { match args.command {
Some(Subcommands::Init) => { Some(Subcommands::Init) => init::init().context("Initialization failed")?,
return init::init().context("Initialization failed"); Some(Subcommands::Dev(dev_command)) => dev_command.run()?,
_ => break 'priority_cmd,
} }
Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
_ => (), return Ok(ExitCode::SUCCESS);
} }
if !Path::new("exercises").is_dir() { if !Path::new("exercises").is_dir() {
println!("{PRE_INIT_MSG}"); println!("{PRE_INIT_MSG}");
exit(1); return Ok(ExitCode::FAILURE);
} }
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
@ -134,21 +135,41 @@ fn main() -> Result<()> {
) )
}; };
loop { watch::watch(&mut app_state, notify_exercise_names)?;
match watch::watch(&mut app_state, notify_exercise_names)? {
WatchExit::Shutdown => break,
// It is much easier to exit the watch mode, launch the list mode and then restart
// the watch mode instead of trying to pause the watch threads and correct the
// watch state.
WatchExit::List => list::list(&mut app_state)?,
}
}
} }
Some(Subcommands::Run { name }) => { Some(Subcommands::Run { name }) => {
if let Some(name) = name { if let Some(name) = name {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
} }
run::run(&mut app_state)?; return run::run(&mut app_state);
}
Some(Subcommands::CheckAll) => {
let mut stdout = io::stdout().lock();
if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? {
if app_state.current_exercise().done {
app_state.set_current_exercise_ind(first_pending_exercise_ind)?;
}
stdout.write_all(b"\n\n")?;
let pending = app_state.n_pending();
if pending == 1 {
stdout.write_all(b"One exercise pending: ")?;
} else {
write!(
stdout,
"{pending}/{} exercises pending. The first: ",
app_state.exercises().len(),
)?;
}
app_state
.current_exercise()
.terminal_file_link(&mut stdout)?;
stdout.write_all(b"\n")?;
return Ok(ExitCode::FAILURE);
} else {
app_state.render_final_message(&mut stdout)?;
}
} }
Some(Subcommands::Reset { name }) => { Some(Subcommands::Reset { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
@ -165,7 +186,7 @@ fn main() -> Result<()> {
Some(Subcommands::Init | Subcommands::Dev(_)) => (), Some(Subcommands::Init | Subcommands::Dev(_)) => (),
} }
Ok(()) Ok(ExitCode::SUCCESS)
} }
const OLD_METHOD_ERR: &str = const OLD_METHOD_ERR: &str =

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

View file

@ -1,12 +1,279 @@
use std::io::{self, BufRead, StdoutLock, Write}; use crossterm::{
cursor::MoveTo,
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
terminal::{Clear, ClearType},
Command, QueueableCommand,
};
use std::{
fmt, fs,
io::{self, BufRead, StdoutLock, Write},
};
use crate::app_state::CheckProgress;
pub struct MaxLenWriter<'a, 'lock> {
pub stdout: &'a mut StdoutLock<'lock>,
len: usize,
max_len: usize,
}
impl<'a, 'lock> MaxLenWriter<'a, 'lock> {
#[inline]
pub fn new(stdout: &'a mut StdoutLock<'lock>, 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<'lock> {
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>;
fn write_str(&mut self, unicode: &str) -> io::Result<()>;
fn stdout(&mut self) -> &mut StdoutLock<'lock>;
}
impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> {
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
if n > 0 {
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<'lock> {
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
}
}
pub struct CheckProgressVisualizer<'a, 'lock> {
stdout: &'a mut StdoutLock<'lock>,
n_cols: usize,
}
impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> {
const CHECKING_COLOR: Color = Color::Blue;
const DONE_COLOR: Color = Color::Green;
const PENDING_COLOR: Color = Color::Red;
pub fn build(stdout: &'a mut StdoutLock<'lock>, term_width: u16) -> io::Result<Self> {
clear_terminal(stdout)?;
stdout.write_all("Checking all exercises…\n".as_bytes())?;
// Legend
stdout.write_all(b"Color of exercise number: ")?;
stdout.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
stdout.write_all(b"Checking")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" - ")?;
stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
stdout.write_all(b"Done")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" - ")?;
stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
stdout.write_all(b"Pending")?;
stdout.queue(ResetColor)?;
stdout.write_all(b"\n")?;
// Exercise numbers with up to 3 digits.
// +1 because the last column doesn't end with a whitespace.
let n_cols = usize::from(term_width + 1) / 4;
Ok(Self { stdout, n_cols })
}
pub fn update(&mut self, progresses: &[CheckProgress]) -> io::Result<()> {
self.stdout.queue(MoveTo(0, 2))?;
let mut exercise_num = 1;
for exercise_progress in progresses {
match exercise_progress {
CheckProgress::None => (),
CheckProgress::Checking => {
self.stdout
.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
}
CheckProgress::Done => {
self.stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
}
CheckProgress::Pending => {
self.stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
}
}
write!(self.stdout, "{exercise_num:<3}")?;
self.stdout.queue(ResetColor)?;
if exercise_num != progresses.len() {
if exercise_num % self.n_cols == 0 {
self.stdout.write_all(b"\n")?;
} else {
self.stdout.write_all(b" ")?;
}
exercise_num += 1;
}
}
self.stdout.flush()
}
}
pub fn progress_bar<'a>(
writer: &mut impl CountedWrite<'a>,
progress: u16,
total: u16,
term_width: u16,
) -> io::Result<()> {
debug_assert!(total <= 999);
debug_assert!(progress <= total);
const PREFIX: &[u8] = b"Progress: [";
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16;
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
if term_width < MIN_LINE_WIDTH {
writer.write_ascii(b"Progress: ")?;
// Integers are in ASCII.
return writer.write_ascii(format!("{progress}/{total}").as_bytes());
}
let stdout = writer.stdout();
stdout.write_all(PREFIX)?;
let width = term_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(SetForegroundColor(Color::Reset))?;
write!(stdout, "] {progress:>3}/{total}")
}
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") stdout
.queue(MoveTo(0, 0))?
.queue(Clear(ClearType::All))?
.queue(Clear(ClearType::Purge))
.map(|_| ())
} }
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.flush()?; stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?; io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
stdout.write_all(b"\n")?; stdout.write_all(b"\n")
}
/// Canonicalize, convert to string and remove verbatim part on Windows.
pub fn canonicalize(path: &str) -> Option<String> {
fs::canonicalize(path)
.ok()?
.into_os_string()
.into_string()
.ok()
.map(|mut path| {
// Windows itself can't handle its verbatim paths.
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
path.drain(..4);
}
path
})
}
pub fn terminal_file_link<'a>(
writer: &mut impl CountedWrite<'a>,
path: &str,
canonical_path: &str,
color: Color,
) -> io::Result<()> {
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(()) 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

@ -1,109 +1,127 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use notify_debouncer_mini::{ use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
new_debouncer,
notify::{self, RecursiveMode},
};
use std::{ use std::{
io::{self, Write}, io::{self, Write},
path::Path, path::Path,
sync::mpsc::channel, sync::{
thread, atomic::{AtomicBool, Ordering::Relaxed},
mpsc::channel,
},
time::Duration, time::Duration,
}; };
use crate::app_state::{AppState, ExercisesProgress}; use crate::{
app_state::{AppState, ExercisesProgress},
use self::{ list,
notify_event::NotifyEventHandler,
state::WatchState,
terminal_event::{terminal_event_handler, InputEvent},
}; };
use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent};
mod notify_event; mod notify_event;
mod state; mod state;
mod terminal_event; mod terminal_event;
static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false);
// Private unit type to force using the constructor function.
#[must_use = "When the guard is dropped, the input is unpaused"]
pub struct InputPauseGuard(());
impl InputPauseGuard {
#[inline]
pub fn scoped_pause() -> Self {
EXERCISE_RUNNING.store(true, Relaxed);
Self(())
}
}
impl Drop for InputPauseGuard {
#[inline]
fn drop(&mut self) {
EXERCISE_RUNNING.store(false, Relaxed);
}
}
enum WatchEvent { enum WatchEvent {
Input(InputEvent), Input(InputEvent),
FileChange { exercise_ind: usize }, FileChange { exercise_ind: usize },
TerminalResize, TerminalResize { width: u16 },
NotifyErr(notify::Error), NotifyErr(notify::Error),
TerminalEventErr(io::Error), TerminalEventErr(io::Error),
} }
/// Returned by the watch mode to indicate what to do afterwards. /// Returned by the watch mode to indicate what to do afterwards.
#[must_use] #[must_use]
pub enum WatchExit { enum WatchExit {
/// Exit the program. /// Exit the program.
Shutdown, Shutdown,
/// Enter the list mode and restart the watch mode afterwards. /// Enter the list mode and restart the watch mode afterwards.
List, List,
} }
/// `notify_exercise_names` as None activates the manual run mode. fn run_watch(
pub fn watch(
app_state: &mut AppState, app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>, notify_exercise_names: Option<&'static [&'static [u8]]>,
) -> Result<WatchExit> { ) -> Result<WatchExit> {
let (tx, rx) = channel(); let (watch_event_sender, watch_event_receiver) = channel();
let mut manual_run = false; let mut manual_run = false;
// Prevent dropping the guard until the end of the function. // Prevent dropping the guard until the end of the function.
// Otherwise, the file watcher exits. // Otherwise, the file watcher exits.
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
let mut debouncer = new_debouncer( let notify_event_handler =
Duration::from_millis(200), NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?;
NotifyEventHandler {
tx: tx.clone(), let mut watcher = RecommendedWatcher::new(
exercise_names, notify_event_handler,
}, Config::default().with_poll_interval(Duration::from_secs(1)),
) )
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
debouncer
.watcher() watcher
.watch(Path::new("exercises"), RecursiveMode::Recursive) .watch(Path::new("exercises"), RecursiveMode::Recursive)
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
Some(debouncer) Some(watcher)
} else { } else {
manual_run = true; manual_run = true;
None None
}; };
let mut watch_state = WatchState::new(app_state, manual_run); let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run)?;
let mut stdout = io::stdout().lock();
watch_state.run_current_exercise()?; watch_state.run_current_exercise(&mut stdout)?;
thread::spawn(move || terminal_event_handler(tx, manual_run)); while let Ok(event) = watch_event_receiver.recv() {
while let Ok(event) = rx.recv() {
match event { 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::AllDone => break,
ExercisesProgress::CurrentPending => watch_state.render()?, ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
ExercisesProgress::NewPending => watch_state.run_current_exercise()?, ExercisesProgress::CurrentPending => (),
}, },
WatchEvent::Input(InputEvent::Hint) => { WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
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::List) => { WatchEvent::Input(InputEvent::CheckAll) => match watch_state
return Ok(WatchExit::List); .check_all_exercises(&mut stdout)?
} {
ExercisesProgress::AllDone => break,
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
},
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?,
WatchEvent::Input(InputEvent::Quit) => { WatchEvent::Input(InputEvent::Quit) => {
watch_state.into_writer().write_all(QUIT_MSG)?; stdout.write_all(QUIT_MSG)?;
break; break;
} }
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?,
WatchEvent::FileChange { exercise_ind } => { WatchEvent::FileChange { exercise_ind } => {
watch_state.handle_file_change(exercise_ind)?; watch_state.handle_file_change(exercise_ind, &mut stdout)?;
} }
WatchEvent::TerminalResize => { WatchEvent::TerminalResize { width } => {
watch_state.render()?; watch_state.update_term_width(width, &mut stdout)?;
}
WatchEvent::NotifyErr(e) => {
return Err(Error::from(e).context(NOTIFY_ERR));
} }
WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)),
WatchEvent::TerminalEventErr(e) => { WatchEvent::TerminalEventErr(e) => {
return Err(Error::from(e).context("Terminal event listener failed")); return Err(Error::from(e).context("Terminal event listener failed"));
} }
@ -113,9 +131,52 @@ pub fn watch(
Ok(WatchExit::Shutdown) Ok(WatchExit::Shutdown)
} }
fn watch_list_loop(
app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>,
) -> Result<()> {
loop {
match run_watch(app_state, notify_exercise_names)? {
WatchExit::Shutdown => break Ok(()),
// It is much easier to exit the watch mode, launch the list mode and then restart
// the watch mode instead of trying to pause the watch threads and correct the
// watch state.
WatchExit::List => list::list(app_state)?,
}
}
}
/// `notify_exercise_names` as None activates the manual run mode.
pub fn watch(
app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>,
) -> Result<()> {
#[cfg(not(windows))]
{
let stdin_fd = rustix::stdio::stdin();
let mut termios = rustix::termios::tcgetattr(stdin_fd)?;
let original_local_modes = termios.local_modes;
// Disable stdin line buffering and hide input.
termios.local_modes -=
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
let res = watch_list_loop(app_state, notify_exercise_names);
termios.local_modes = original_local_modes;
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
res
}
#[cfg(windows)]
watch_list_loop(app_state, notify_exercise_names)
}
const QUIT_MSG: &[u8] = b" const QUIT_MSG: &[u8] = b"
We hope you're enjoying learning Rust! We hope you're enjoying learning Rust!
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory.
"; ";
const NOTIFY_ERR: &str = " const NOTIFY_ERR: &str = "

View file

@ -1,52 +1,132 @@
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; use anyhow::{Context, Result};
use std::sync::mpsc::Sender; use notify::{
event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode},
Event, EventKind,
};
use std::{
sync::{
atomic::Ordering::Relaxed,
mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender},
},
thread,
time::Duration,
};
use super::WatchEvent; use super::{WatchEvent, EXERCISE_RUNNING};
const DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
pub struct NotifyEventHandler { pub struct NotifyEventHandler {
pub tx: Sender<WatchEvent>, error_sender: Sender<WatchEvent>,
/// Used to report which exercise was modified. // Sends the index of the updated exercise.
pub exercise_names: &'static [&'static [u8]], update_sender: SyncSender<usize>,
// Used to report which exercise was modified.
exercise_names: &'static [&'static [u8]],
} }
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { impl NotifyEventHandler {
fn handle_event(&mut self, input_event: DebounceEventResult) { pub fn build(
let output_event = match input_event { watch_event_sender: Sender<WatchEvent>,
Ok(input_event) => { exercise_names: &'static [&'static [u8]],
let Some(exercise_ind) = input_event ) -> Result<Self> {
.iter() let (update_sender, update_receiver) = sync_channel(0);
.filter_map(|input_event| { let error_sender = watch_event_sender.clone();
if input_event.kind != DebouncedEventKind::Any {
// Debouncer
thread::Builder::new()
.spawn(move || {
let mut exercise_updated = vec![false; exercise_names.len()];
loop {
match update_receiver.recv_timeout(DEBOUNCE_DURATION) {
Ok(exercise_ind) => exercise_updated[exercise_ind] = true,
Err(RecvTimeoutError::Timeout) => {
for (exercise_ind, updated) in exercise_updated.iter_mut().enumerate() {
if *updated {
if watch_event_sender
.send(WatchEvent::FileChange { exercise_ind })
.is_err()
{
break;
}
*updated = false;
}
}
}
Err(RecvTimeoutError::Disconnected) => break,
}
}
})
.context("Failed to spawn a thread to debounce file changes")?;
Ok(Self {
error_sender,
update_sender,
exercise_names,
})
}
}
impl notify::EventHandler for NotifyEventHandler {
fn handle_event(&mut self, input_event: notify::Result<Event>) {
if EXERCISE_RUNNING.load(Relaxed) {
return;
}
let input_event = match input_event {
Ok(v) => v,
Err(e) => {
// An error occurs when the receiver is dropped.
// After dropping the receiver, the watcher guard should also be dropped.
let _ = self.error_sender.send(WatchEvent::NotifyErr(e));
return;
}
};
match input_event.kind {
EventKind::Any => (),
EventKind::Modify(modify_kind) => match modify_kind {
ModifyKind::Any | ModifyKind::Data(_) => (),
ModifyKind::Name(rename_mode) => match rename_mode {
RenameMode::Any | RenameMode::To => (),
RenameMode::From | RenameMode::Both | RenameMode::Other => return,
},
ModifyKind::Metadata(metadata_kind) => match metadata_kind {
MetadataKind::Any | MetadataKind::WriteTime => (),
MetadataKind::AccessTime
| MetadataKind::Permissions
| MetadataKind::Ownership
| MetadataKind::Extended
| MetadataKind::Other => return,
},
ModifyKind::Other => return,
},
EventKind::Access(access_kind) => match access_kind {
AccessKind::Any => (),
AccessKind::Close(access_mode) => match access_mode {
AccessMode::Any | AccessMode::Write => (),
AccessMode::Execute | AccessMode::Read | AccessMode::Other => return,
},
AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return,
},
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return,
}
let _ = input_event
.paths
.into_iter()
.filter_map(|path| {
let file_name = path.file_name()?.to_str()?.as_bytes();
let [file_name_without_ext @ .., b'.', b'r', b's'] = file_name else {
return None; return None;
} };
let file_name = input_event.path.file_name()?.to_str()?.as_bytes();
if file_name.len() < 4 {
return None;
}
let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3);
if ext != b".rs" {
return None;
}
self.exercise_names self.exercise_names
.iter() .iter()
.position(|exercise_name| *exercise_name == file_name_without_ext) .position(|exercise_name| *exercise_name == file_name_without_ext)
}) })
.min() .try_for_each(|exercise_ind| self.update_sender.send(exercise_ind));
else {
return;
};
WatchEvent::FileChange { exercise_ind }
}
Err(e) => WatchEvent::NotifyErr(e),
};
// An error occurs when the receiver is dropped.
// After dropping the receiver, the debouncer guard should also be dropped.
let _ = self.tx.send(output_event);
} }
} }

View file

@ -1,18 +1,29 @@
use anyhow::Result; use anyhow::{Context, Result};
use ratatui::crossterm::{ use crossterm::{
style::{style, Stylize}, style::{
terminal, Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
},
terminal, QueueableCommand,
};
use std::{
io::{self, Read, StdoutLock, Write},
sync::mpsc::{sync_channel, Sender, SyncSender},
thread,
}; };
use std::io::{self, StdoutLock, Write};
use crate::{ use crate::{
app_state::{AppState, ExercisesProgress}, app_state::{AppState, ExercisesProgress},
clear_terminal, clear_terminal,
exercise::{RunnableExercise, OUTPUT_CAPACITY}, exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
progress_bar::progress_bar, term::progress_bar,
terminal_link::TerminalFileLink,
}; };
use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent};
const HEADING_ATTRIBUTES: Attributes = Attributes::none()
.with(Attribute::Bold)
.with(Attribute::Underlined);
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
enum DoneStatus { enum DoneStatus {
DoneWithSolution(String), DoneWithSolution(String),
@ -21,45 +32,65 @@ enum DoneStatus {
} }
pub struct WatchState<'a> { pub struct WatchState<'a> {
writer: StdoutLock<'a>,
app_state: &'a mut AppState, app_state: &'a mut AppState,
output: Vec<u8>, output: Vec<u8>,
show_hint: bool, show_hint: bool,
done_status: DoneStatus, done_status: DoneStatus,
manual_run: bool, manual_run: bool,
term_width: u16,
terminal_event_unpause_sender: SyncSender<()>,
} }
impl<'a> WatchState<'a> { impl<'a> WatchState<'a> {
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self { pub fn build(
let writer = io::stdout().lock(); app_state: &'a mut AppState,
watch_event_sender: Sender<WatchEvent>,
manual_run: bool,
) -> Result<Self> {
let term_width = terminal::size()
.context("Failed to get the terminal size")?
.0;
Self { let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0);
writer,
thread::Builder::new()
.spawn(move || {
terminal_event_handler(
watch_event_sender,
terminal_event_unpause_receiver,
manual_run,
)
})
.context("Failed to spawn a thread to handle terminal events")?;
Ok(Self {
app_state, app_state,
output: Vec::with_capacity(OUTPUT_CAPACITY), output: Vec::with_capacity(OUTPUT_CAPACITY),
show_hint: false, show_hint: false,
done_status: DoneStatus::Pending, done_status: DoneStatus::Pending,
manual_run, manual_run,
} term_width,
terminal_event_unpause_sender,
})
} }
#[inline] pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
pub fn into_writer(self) -> StdoutLock<'a> { // Ignore any input until running the exercise is done.
self.writer let _input_pause_guard = InputPauseGuard::scoped_pause();
}
pub fn run_current_exercise(&mut self) -> Result<()> {
self.show_hint = false; self.show_hint = false;
writeln!( writeln!(
self.writer, stdout,
"\nChecking the exercise `{}`. Please wait…", "\nChecking the exercise `{}`. Please wait…",
self.app_state.current_exercise().name, self.app_state.current_exercise().name,
)?; )?;
let success = self let success = self
.app_state .app_state
.current_exercise() .current_exercise()
.run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?; .run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?;
self.output.push(b'\n');
if success { if success {
self.done_status = self.done_status =
if let Some(solution_path) = self.app_state.current_solution_path()? { if let Some(solution_path) = self.app_state.current_solution_path()? {
@ -74,106 +105,194 @@ impl<'a> WatchState<'a> {
self.done_status = DoneStatus::Pending; self.done_status = DoneStatus::Pending;
} }
self.render() self.render(stdout)?;
Ok(())
} }
pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> { pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
// Don't skip exercises on file changes to avoid confusion from missing exercises. clear_terminal(stdout)?;
// Skipping exercises must be explicit in the interactive list.
// But going back to an earlier exercise on file change is fine.
if self.app_state.current_exercise_ind() < exercise_ind {
return Ok(());
}
self.app_state.set_current_exercise_ind(exercise_ind)?; stdout.write_all(b"Resetting will undo all your changes to the file ")?;
self.run_current_exercise() stdout.write_all(self.app_state.current_exercise().path.as_bytes())?;
} stdout.write_all(b"\nReset (y/n)? ")?;
stdout.flush()?;
/// Move on to the next exercise if the current one is done. {
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> { let mut stdin = io::stdin().lock();
if self.done_status == DoneStatus::Pending { let mut answer = [0];
return Ok(ExercisesProgress::CurrentPending); loop {
} stdin
.read_exact(&mut answer)
.context("Failed to read the user's input")?;
self.app_state.done_current_exercise(&mut self.writer) match answer[0] {
} b'y' | b'Y' => {
self.app_state.reset_current_exercise()?;
fn show_prompt(&mut self) -> io::Result<()> {
self.writer.write_all(b"\n")?;
// The file watcher reruns the exercise otherwise.
if self.manual_run { if self.manual_run {
write!(self.writer, "{}:run / ", 'r'.bold())?; self.run_current_exercise(stdout)?;
}
}
b'n' | b'N' => self.render(stdout)?,
_ => continue,
} }
if self.done_status != DoneStatus::Pending { break;
write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?; }
} }
if !self.show_hint { self.terminal_event_unpause_sender.send(())?;
write!(self.writer, "{}:hint / ", 'h'.bold())?;
}
write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?;
self.writer.flush()
}
pub fn render(&mut self) -> Result<()> {
// Prevent having the first line shifted if clearing wasn't successful.
self.writer.write_all(b"\n")?;
clear_terminal(&mut self.writer)?;
self.writer.write_all(&self.output)?;
self.writer.write_all(b"\n")?;
if self.show_hint {
writeln!(
self.writer,
"{}\n{}\n",
"Hint".bold().cyan().underlined(),
self.app_state.current_exercise().hint,
)?;
}
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(),
)?;
}
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(),
)?;
}
let line_width = terminal::size()?.0;
let progress_bar = progress_bar(
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()?;
Ok(()) Ok(())
} }
pub fn show_hint(&mut self) -> Result<()> { pub fn handle_file_change(
&mut self,
exercise_ind: usize,
stdout: &mut StdoutLock,
) -> Result<()> {
if self.app_state.current_exercise_ind() != exercise_ind {
return Ok(());
}
self.run_current_exercise(stdout)
}
/// Move on to the next exercise if the current one is done.
pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
match self.done_status {
DoneStatus::DoneWithSolution(_) | DoneStatus::DoneWithoutSolution => (),
DoneStatus::Pending => return Ok(ExercisesProgress::CurrentPending),
}
self.app_state.done_current_exercise::<true>(stdout)
}
fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> {
if self.done_status != DoneStatus::Pending {
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" / ")?;
}
let mut show_key = |key, postfix| {
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(&[key])?;
stdout.queue(ResetColor)?;
stdout.write_all(postfix)
};
if self.manual_run {
show_key(b'r', b":run / ")?;
}
if !self.show_hint {
show_key(b'h', b":hint / ")?;
}
show_key(b'l', b":list / ")?;
show_key(b'c', b":check all / ")?;
show_key(b'x', b":reset / ")?;
show_key(b'q', b":quit ? ")?;
stdout.flush()
}
pub fn render(&self, stdout: &mut StdoutLock) -> io::Result<()> {
// Prevent having the first line shifted if clearing wasn't successful.
stdout.write_all(b"\n")?;
clear_terminal(stdout)?;
stdout.write_all(&self.output)?;
if self.show_hint {
stdout
.queue(SetAttributes(HEADING_ATTRIBUTES))?
.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 {
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 {
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(),
)?;
}
progress_bar(
stdout,
self.app_state.n_done(),
self.app_state.exercises().len() as u16,
self.term_width,
)?;
stdout.write_all(b"\nCurrent exercise: ")?;
self.app_state
.current_exercise()
.terminal_file_link(stdout)?;
stdout.write_all(b"\n\n")?;
self.show_prompt(stdout)?;
Ok(())
}
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
if !self.show_hint {
self.show_hint = true; self.show_hint = true;
self.render() self.render(stdout)?;
}
Ok(())
}
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
// Ignore any input until checking all exercises is done.
let _input_pause_guard = InputPauseGuard::scoped_pause();
if let Some(first_pending_exercise_ind) = self.app_state.check_all_exercises(stdout)? {
// Only change exercise if the current one is done.
if self.app_state.current_exercise().done {
self.app_state
.set_current_exercise_ind(first_pending_exercise_ind)?;
Ok(ExercisesProgress::NewPending)
} else {
Ok(ExercisesProgress::CurrentPending)
}
} else {
self.app_state.render_final_message(stdout)?;
Ok(ExercisesProgress::AllDone)
}
}
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
if self.term_width != width {
self.term_width = width;
self.render(stdout)?;
}
Ok(())
} }
} }

View file

@ -1,86 +1,73 @@
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use std::sync::mpsc::Sender; use std::sync::{
atomic::Ordering::Relaxed,
use super::WatchEvent; mpsc::{Receiver, Sender},
pub enum InputEvent {
Run,
Next,
Hint,
List,
Quit,
Unrecognized,
}
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
// Only send `Unrecognized` on ENTER if the last input wasn't valid.
let mut last_input_valid = false;
let last_input_event = loop {
let terminal_event = match event::read() {
Ok(v) => v,
Err(e) => {
// If `send` returns an error, then the receiver is dropped and
// a shutdown has been already initialized.
let _ = tx.send(WatchEvent::TerminalEventErr(e));
return;
}
}; };
match terminal_event { use super::{WatchEvent, EXERCISE_RUNNING};
Event::Key(key) => {
pub enum InputEvent {
Next,
Run,
Hint,
List,
CheckAll,
Reset,
Quit,
}
pub fn terminal_event_handler(
sender: Sender<WatchEvent>,
unpause_receiver: Receiver<()>,
manual_run: bool,
) {
let last_watch_event = loop {
match event::read() {
Ok(Event::Key(key)) => {
match key.kind { match key.kind {
KeyEventKind::Release | KeyEventKind::Repeat => continue, KeyEventKind::Release | KeyEventKind::Repeat => continue,
KeyEventKind::Press => (), KeyEventKind::Press => (),
} }
if key.modifiers != KeyModifiers::NONE { if EXERCISE_RUNNING.load(Relaxed) {
last_input_valid = false;
continue; continue;
} }
let input_event = match key.code { let input_event = match key.code {
KeyCode::Enter => { KeyCode::Char('n') => InputEvent::Next,
if last_input_valid { KeyCode::Char('r') if manual_run => InputEvent::Run,
continue; KeyCode::Char('h') => InputEvent::Hint,
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
KeyCode::Char('c') => InputEvent::CheckAll,
KeyCode::Char('x') => {
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
return;
} }
InputEvent::Unrecognized // Pause input until quitting the confirmation prompt.
} if unpause_receiver.recv().is_err() {
KeyCode::Char(c) => { return;
let input_event = match c {
'n' => InputEvent::Next,
'h' => InputEvent::Hint,
'l' => break InputEvent::List,
'q' => break InputEvent::Quit,
'r' if manual_run => InputEvent::Run,
_ => {
last_input_valid = false;
continue;
}
}; };
last_input_valid = true;
input_event
}
_ => {
last_input_valid = false;
continue; continue;
} }
KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit),
_ => continue,
}; };
if tx.send(WatchEvent::Input(input_event)).is_err() { if sender.send(WatchEvent::Input(input_event)).is_err() {
return; return;
} }
} }
Event::Resize(_, _) => { Ok(Event::Resize(width, _)) => {
if tx.send(WatchEvent::TerminalResize).is_err() { if sender.send(WatchEvent::TerminalResize { width }).is_err() {
return; return;
} }
} }
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue,
Err(e) => break WatchEvent::TerminalEventErr(e),
} }
}; };
let _ = tx.send(WatchEvent::Input(last_input_event)); let _ = sender.send(last_watch_event);
} }