Compare commits

...

208 Commits

Author SHA1 Message Date
mo8it
cb60c8887c Update deps
Some checks failed
Check / clippy (push) Has been cancelled
Check / fmt (push) Has been cancelled
Check / test (macos-latest) (push) Has been cancelled
Check / test (ubuntu-latest) (push) Has been cancelled
Check / test (windows-latest) (push) Has been cancelled
Check / dev-check (push) Has been cancelled
2025-06-03 10:30:36 +02:00
mo8it
46814d397a Fix path in action
Some checks failed
Check / clippy (push) Has been cancelled
Check / fmt (push) Has been cancelled
Check / test (macos-latest) (push) Has been cancelled
Check / test (ubuntu-latest) (push) Has been cancelled
Check / test (windows-latest) (push) Has been cancelled
Check / dev-check (push) Has been cancelled
2025-05-23 13:44:30 +02:00
mo8it
734fc482eb Make path relative 2025-05-23 13:37:15 +02:00
mo8it
520dfdc464 Add workflow_dispatch to website workflow 2025-05-23 13:33:51 +02:00
Mo Bitar
2267f99684
Merge pull request #2247 from rust-lang/website
Website
2025-05-23 13:26:52 +02:00
mo8it
bf74a3d0a7 Update the README with more context before the website link 2025-05-23 13:21:05 +02:00
mo8it
adf3ddd968 Remove rustywind from dev deps 2025-05-23 13:17:21 +02:00
mo8it
f80c2edc3d Remove fnm 2025-05-23 13:17:14 +02:00
mo8it
04520ae7ad Use the website link as header 2025-05-19 18:30:40 +02:00
mo8it
e36dd7a120 Update MSRV in the release hook
Some checks failed
Rustlings Tests / clippy (push) Has been cancelled
Rustlings Tests / fmt (push) Has been cancelled
Rustlings Tests / test (macos-latest) (push) Has been cancelled
Rustlings Tests / test (ubuntu-latest) (push) Has been cancelled
Rustlings Tests / test (windows-latest) (push) Has been cancelled
Rustlings Tests / dev-check (push) Has been cancelled
Web / Build and deploy site and docs (push) Has been cancelled
2025-05-19 18:21:40 +02:00
mo8it
edc8528dde Improve CI 2025-05-19 18:20:34 +02:00
mo8it
47e490a997 Run rustywind 2025-05-17 22:33:17 +02:00
mo8it
596e7f36cc Add website CI 2025-05-17 22:33:00 +02:00
mo8it
512ded81c4 Done community exercises page 2025-05-17 22:05:57 +02:00
mo8it
69a9e9cafc Less top margin for blockquotes 2025-05-17 22:05:48 +02:00
mo8it
54a74fd638 Use internal links for validation 2025-05-17 21:28:25 +02:00
mo8it
a51d6f1309 third-party/custom -> community 2025-05-17 21:25:19 +02:00
mo8it
f6a657a0c3 Finish the usage page 2025-05-17 21:02:45 +02:00
mo8it
8c24763259 Q/A -> Q&A 2025-05-17 21:02:01 +02:00
mo8it
dc468882cc Highlight platform 2025-05-17 20:45:28 +02:00
mo8it
5fc787f4e4 Style details body 2025-05-17 20:45:20 +02:00
mo8it
8fa598ae7e Add details shortcode 2025-05-17 20:20:19 +02:00
mo8it
2f700991f3 Remove macros 2025-05-17 19:53:30 +02:00
mo8it
b4a6b87e24 Less padding 2025-05-17 19:34:53 +02:00
mo8it
984e9fea7c Merge branch 'main' 2025-05-17 16:17:20 +02:00
mo8it
8339007633 Update setup and usage 2025-05-17 16:06:04 +02:00
mo8it
23b9aa3a15 Move Q/A menu item 2025-05-17 16:05:42 +02:00
mo8it
69fe9626da Update base URL 2025-05-17 16:05:26 +02:00
mo8it
f387f4c1d9 Add setup and usage pages 2025-05-17 15:49:26 +02:00
liv
40fe3aa741 remove oranda path_prefix
Some checks failed
Rustlings Tests / clippy (push) Has been cancelled
Rustlings Tests / fmt (push) Has been cancelled
Rustlings Tests / test (macos-latest) (push) Has been cancelled
Rustlings Tests / test (ubuntu-latest) (push) Has been cancelled
Rustlings Tests / test (windows-latest) (push) Has been cancelled
Rustlings Tests / dev-check (push) Has been cancelled
Web / Build and deploy site and docs (push) Has been cancelled
2025-05-17 15:48:04 +02:00
mo8it
b30973afa1 Compress the intro 2025-05-17 14:53:38 +02:00
mo8it
3d8bef4bc3 Remove landscape mode recommendation 2025-05-17 14:53:20 +02:00
mo8it
2673177b17 Update header and footer 2025-05-17 13:02:34 +02:00
mo8it
6d5369d4d0 Add more menu and footer items 2025-05-17 12:25:55 +02:00
mo8it
b9d1e636a4 Reduce the README to the website link 2025-05-17 12:25:32 +02:00
mo8it
7e26418952 Remove the third-party exercises file 2025-05-17 12:25:08 +02:00
mo8it
61c17cb349 Change syntax highlighting theme 2025-05-17 12:24:44 +02:00
mo8it
fda18e8895 Add Ferris SVGs 2025-05-17 12:24:27 +02:00
mo8it
7ec6986965 Add templates 2025-05-16 23:11:08 +02:00
mo8it
74ab9924b4 Start with Zola 2025-05-16 21:08:29 +02:00
mo8it
a28000acc4 Remove markdown lint 2025-05-16 11:35:46 +02:00
mo8it
08548abcc2 Remove .editorconfig 2025-05-16 11:35:24 +02:00
mo8it
5927a781a3 Remove Oranda 2025-05-16 11:29:32 +02:00
mo8it
e73fff3bd4 Add dev alias
Some checks failed
Rustlings Tests / clippy (push) Has been cancelled
Rustlings Tests / fmt (push) Has been cancelled
Rustlings Tests / test (macos-latest) (push) Has been cancelled
Rustlings Tests / test (ubuntu-latest) (push) Has been cancelled
Rustlings Tests / test (windows-latest) (push) Has been cancelled
Rustlings Tests / dev-check (push) Has been cancelled
Web / Build and deploy site and docs (push) Has been cancelled
2025-05-16 11:09:06 +02:00
mo8it
8dff0df266 Use std pipe 2025-05-16 11:09:06 +02:00
liv
5ee7dfb5c2 chore: build site with proper path prefix 2025-05-16 11:05:02 +02:00
mo8it
9a3586878d Sync solution
Some checks failed
Rustlings Tests / clippy (push) Has been cancelled
Rustlings Tests / fmt (push) Has been cancelled
Rustlings Tests / test (macos-latest) (push) Has been cancelled
Rustlings Tests / test (ubuntu-latest) (push) Has been cancelled
Rustlings Tests / test (windows-latest) (push) Has been cancelled
Rustlings Tests / dev-check (push) Has been cancelled
Web / Build and deploy site and docs (push) Has been cancelled
2025-05-13 16:24:42 +02:00
Mo Bitar
a99433c62d
Merge pull request #2215 from Rudxain/strim
test idempotence of `trim_me` in `strings3.rs`
2025-05-13 16:23:35 +02:00
mo8it
e76ca5e2b9 Use a separate target dir for rust analyzer
Some checks failed
Rustlings Tests / clippy (push) Has been cancelled
Rustlings Tests / fmt (push) Has been cancelled
Rustlings Tests / test (macos-latest) (push) Has been cancelled
Rustlings Tests / test (ubuntu-latest) (push) Has been cancelled
Rustlings Tests / test (windows-latest) (push) Has been cancelled
Rustlings Tests / dev-check (push) Has been cancelled
Web / Build and deploy site and docs (push) Has been cancelled
2025-05-12 20:38:04 +02:00
mo8it
48bab77609 Apply Clippy lints 2025-05-12 20:31:13 +02:00
mo8it
a063bcfb4c Update deps 2025-05-12 20:30:56 +02:00
mo8it
c5f49cfa48 Remove needless_option_as_deref exception 2025-05-12 20:30:51 +02:00
mo8it
9bcd4198c5 Fix formatting 2025-04-29 21:36:56 +02:00
mo8it
29dc8ea9fa Update deps 2025-04-29 21:35:58 +02:00
Mo Bitar
fa91814aa9
Merge pull request #2232 from ethdew19/main
Fix typo in traits hint
2025-04-29 21:34:47 +02:00
Mo Bitar
0b91db2195
Merge pull request #2236 from rahmatnazali/ice_cream
Fix possible typo of `icecream` to `ice cream` on `options1.rs`
2025-04-29 21:34:21 +02:00
Rahmat Nazali Salimi
7b2d42b0f0
Change icecream to ice cream 2025-04-10 15:38:43 +07:00
Ethan
bd3bdd620b Fix typo in traits hint 2025-04-08 20:23:11 -05:00
Mo Bitar
8b4562e102
Merge pull request #2219 from ubitux/no-todo
Remove TODO from 2 solutions
2025-04-03 19:36:06 +02:00
mo8it
63d8986f2a Update links 2025-04-03 18:22:55 +02:00
mo8it
ecaecc2f76 Update deps 2025-04-03 17:58:36 +02:00
Mo Bitar
78194b4441
Merge pull request #2224 from cassian-goode/patch-1
Fix typo - errors5.rs
2025-04-03 17:52:57 +02:00
Mo Bitar
44699e9b1b
Merge pull request #2227 from Hunter-Plus/patch-1
Update README.md
2025-04-03 17:52:15 +02:00
Hunter Z
9978c17d5f
Update README.md
Update the URL while add more reference.
2025-03-31 12:58:06 +08:00
cassian-goode
3cc7e0377c
Fix typo - errors5.rs
Minor typo correction in exercise instructions
2025-03-25 09:24:49 -04:00
Clément Bœsch
d2abc359cc Remove TODO from 2 solutions 2025-03-17 18:36:13 +01:00
mo8it
7c0d269279 Update README 2025-03-14 11:42:16 +01:00
mo8it
8db85946af Update deps 2025-03-14 11:33:56 +01:00
mo8it
7019f4d178 Update pipeline 2025-03-14 11:33:56 +01:00
Ricardo Fernández Serrata
fcd77a83cc
test trim idempotence 2025-03-07 19:17:11 -04:00
Mo Bitar
ae444eb3da
Merge pull request #2213 from peterneave/main
Use consistent apostrophes in markdown files
2025-03-02 17:30:29 +01:00
Peter Neave
425c9821e0 Use consistent apostrophes in markdown files 2025-02-28 11:46:39 +11:00
mo8it
46c6fb2c82 Update deps 2025-02-25 11:21:19 +01:00
mo8it
374c3874af Apply 2024 edition formatting to solutions 2025-02-21 13:08:34 +01:00
mo8it
1eb6c1e469 Update the edition of the solution format checker 2025-02-21 13:06:11 +01:00
mo8it
06af3ffc99 Bump MSRV in release hook 2025-02-18 20:17:27 +01:00
mo8it
65dc019fa6 Fix new Clippy error in solution 2025-02-18 20:15:50 +01:00
mo8it
a56ccb6f4f Fix new Clippy lint 2025-02-18 20:12:23 +01:00
mo8it
d9872f2615 Upgrade to edition 2024 2025-02-18 20:10:52 +01:00
mo8it
298be671b9 Update deps 2025-02-18 20:03:49 +01:00
mo8it
fbfd4f25e7 Disable following symlinks in the watcher 2025-01-16 10:41:48 +01:00
mo8it
d12735a573 Update deps 2025-01-16 10:41:17 +01:00
mo8it
1aec7c1152 Fix Windows CI 2025-01-01 22:07:41 +01:00
mo8it
0b55809bb9 Fix building from source on Windows 2025-01-01 22:01:39 +01:00
mo8it
bde6f7470c Co-ordinates -> Coordinates 2024-12-28 16:46:24 +01:00
mo8it
53ec59ed95 Rename translations 2024-12-28 16:41:43 +01:00
mo8it
ed1ee38923 Link to simplified Chinese translation 2024-12-28 16:40:07 +01:00
Mo
26cf4989a2
Merge pull request #2173 from JoelMarcey/if2-comment-fix
Fix argument comment in test of if2.rs
2024-12-13 19:48:16 +01:00
Joel Marcey
6e60f441e9
Fix argument comment in test of if2.rs 2024-12-13 10:44:21 -08:00
mo8it
d07de879a7 Update deps 2024-12-11 00:12:49 +01:00
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
101 changed files with 2234 additions and 1386 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[alias]
dev = ["run", "--", "dev"]

View File

@ -1,7 +0,0 @@
root = true
[*.rs]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4

View File

@ -1,10 +1,16 @@
name: Rustlings Tests
name: Check
on:
push:
branches: [main]
paths-ignore:
- website
- '*.md'
pull_request:
branches: [main]
paths-ignore:
- website
- '*.md'
env:
CARGO_TERM_COLOR: always
@ -14,30 +20,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo clippy -- --deny warnings
- name: Clippy
run: cargo clippy -- --deny warnings
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v16
with:
globs: "exercises/**/*.md"
- name: Run cargo fmt
- name: rustfmt
run: cargo fmt --all --check
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: swatinem/rust-cache@v2
- name: Run cargo test
- name: cargo test
run: cargo test --workspace
dev-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: swatinem/rust-cache@v2
- name: Run rustlings dev check
run: cargo run -- dev check --require-solutions
- name: rustlings dev check
run: cargo dev check --require-solutions

View File

@ -1,87 +0,0 @@
# Workflow to build your docs with oranda (and mdbook)
# and deploy them to Github Pages
name: Web
# We're going to push to the gh-pages branch, so we need that permission
permissions:
contents: write
# What situations do we want to build docs in?
# All of these work independently and can be removed / commented out
# if you don't want oranda/mdbook running in that situation
on:
# Check that a PR didn't break docs!
#
# Note that the "Deploy to Github Pages" step won't run in this mode,
# so this won't have any side-effects. But it will tell you if a PR
# completely broke oranda/mdbook. Sadly we don't provide previews (yet)!
pull_request:
# Whenever something gets pushed to main, update the docs!
# This is great for getting docs changes live without cutting a full release.
#
# Note that if you're using cargo-dist, this will "race" the Release workflow
# that actually builds the Github Release that oranda tries to read (and
# this will almost certainly complete first). As a result you will publish
# docs for the latest commit but the oranda landing page won't know about
# the latest release. The workflow_run trigger below will properly wait for
# cargo-dist, and so this half-published state will only last for ~10 minutes.
#
# If you only want docs to update with releases, disable this, or change it to
# a "release" branch. You can, of course, also manually trigger a workflow run
# when you want the docs to update.
push:
branches:
- main
# Whenever a workflow called "Release" completes, update the docs!
#
# If you're using cargo-dist, this is recommended, as it will ensure that
# oranda always sees the latest release right when it's available. Note
# however that Github's UI is wonky when you use workflow_run, and won't
# show this workflow as part of any commit. You have to go to the "actions"
# tab for your repo to see this one running (the gh-pages deploy will also
# only show up there).
workflow_run:
workflows: [ "Release" ]
types:
- completed
# Alright, let's do it!
jobs:
web:
name: Build and deploy site and docs
runs-on: ubuntu-latest
steps:
# Setup
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: swatinem/rust-cache@v2
# If you use any mdbook plugins, here's the place to install them!
# Install and run oranda (and mdbook)
# This will write all output to ./public/ (including copying mdbook's output to there)
- name: Install and run oranda
run: |
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.3.1/oranda-installer.sh | sh
oranda build
# Deploy to our gh-pages branch (creating it if it doesn't exist)
# the "public" dir that oranda made above will become the root dir
# of this branch.
#
# Note that once the gh-pages branch exists, you must
# go into repo's settings > pages and set "deploy from branch: gh-pages"
# the other defaults work fine.
- name: Deploy to Github Pages
uses: JamesIves/github-pages-deploy-action@v4.4.1
# ONLY if we're on main (so no PRs or feature branches allowed!)
if: ${{ github.ref == 'refs/heads/main' }}
with:
branch: gh-pages
# Gotta tell the action where to find oranda's output
folder: public
token: ${{ secrets.GITHUB_TOKEN }}
single-commit: true

43
.github/workflows/website.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Website
on:
workflow_dispatch:
push:
branches: [main]
paths: [website]
jobs:
build:
defaults:
run:
working-directory: website
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install TailwindCSS
run: npm install
- name: Build CSS
run: npx @tailwindcss/cli -m -i input.css -o static/main.css
- name: Download Zola
run: curl -fsSL https://github.com/getzola/zola/releases/download/v0.20.0/zola-v0.20.0-x86_64-unknown-linux-gnu.tar.gz | tar xz
- name: Build site
run: ./zola build
- name: Upload static files as artifact
uses: actions/upload-pages-artifact@v3
with:
path: website/public/
deploy:
needs: build
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4

4
.gitignore vendored
View File

@ -6,10 +6,6 @@ Cargo.lock
# State file
.rustlings-state.txt
# oranda
public/
.netlify
# OS
.DS_Store
.direnv/

View File

@ -1,2 +0,0 @@
# MD013/line-length Line length, Expected: 80
MD013: false

View File

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

View File

@ -1,4 +1,38 @@
<a name="6.3.0"></a>
## Unreleased
### Changed
- Upgrade to Rust edition 2024
- Raise the minimum supported Rust version to `1.87`
## 6.4.0 (2024-11-11)
### Added
- The list of exercises is now searchable by pressing `s` or `/` 🔍️ (thanks to [@frroossst](https://github.com/frroossst))
- New option `c` in the prompt to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
- New command `check-all` to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
- Addictive animation for showing the progress of checking all exercises. A nice showcase of parallelism in Rust ✨
- New option `x` in the prompt to reset the file of the current exercise 🔄
- Allow `dead_code` for all exercises and solutions ⚰️ (thanks to [@huss4in](https://github.com/huss4in))
- Pause input while running an exercise to avoid unexpected prompt interactions ⏸️
- Limit the maximum number of exercises to 999. Any community exercises willing to reach that limit? 🔝
### Changed
- `enums3`: Remove redundant enum definition task (thanks to [@senekor](https://github.com/senekor))
- `if2`: Make the exercise less confusing by avoiding "fizz", "fuzz", "foo", "bar" and "baz" (thanks to [@senekor](https://github.com/senekor))
- `hashmap3`: Use the method `Entry::or_default`.
- Update the state of all exercises when checking all of them (thanks to [@Nahor](https://github.com/Nahor))
- The main prompt doesn't need a confirmation with ENTER on Unix-like systems anymore.
- No more jumping back to a previous exercise when its file is changed. Use the list to jump between exercises.
- Dump the solution file after an exercise is done even if the solution's directory doesn't exist.
- Rework the footer in the list.
- Optimize the file watcher.
### Fixed
- Fix bad contrast in the list on terminals with a light theme.
## 6.3.0 (2024-08-29)
@ -39,8 +73,6 @@
- Fix the list when the terminal height is too low.
- Restore the terminal after an error in the list.
<a name="6.2.0"></a>
## 6.2.0 (2024-08-09)
### Added
@ -57,13 +89,11 @@
- Run the final check of all exercises in parallel.
- Small exercise improvements.
<a name="6.1.0"></a>
## 6.1.0 (2024-07-10)
#### Added
- `dev check`: Check that all exercises (including third-party ones) include at least one `TODO` comment.
- `dev check`: Check that all exercises (including community ones) include at least one `TODO` comment.
- `dev check`: Check that all exercises actually fail to run (not already solved).
#### Changed
@ -76,15 +106,11 @@
- Exit with a helpful error message on missing/unsupported terminal/TTY.
- Mark the last exercise as done.
<a name="6.0.1"></a>
## 6.0.1 (2024-07-04)
Small exercise improvements and fixes.
Most importantly, fixed that the exercise `clippy1` was already solved 😅
<a name="6.0.0"></a>
## 6.0.0 (2024-07-03)
This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨
@ -113,7 +139,7 @@ You can read about the motivations of this change in [this issue](https://github
### List mode
A list mode was added using [Ratatui](https://ratatui.rs).
A new list mode was added!
You can enter it by entering `l` in the watch mode.
It offers the following features:
@ -142,15 +168,13 @@ This should avoid issues related to the language server or to running exercises,
Clippy lints are now shown on all exercises, not only the Clippy exercises 📎
Make Clippy your friend from early on 🥰
### Third-party exercises
### Community Exercises
Rustlings now supports third-party exercises!
Rustlings now supports community exercises!
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
Or do you want to translate the original Rustlings exercises?
Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)!
<a name="5.6.1"></a>
Then follow the link to the guide about [community exercises](https://rustlings.rust-lang.org/community-exercises)!
## 5.6.1 (2023-09-18)
@ -167,8 +191,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- `as_ref_mut`: Fixed a typo in a test function name.
- `enums3`: Fixed formatting with `rustfmt`.
<a name="5.6.0"></a>
## 5.6.0 (2023-09-04)
#### Added
@ -208,16 +230,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Lots of Nix housekeeping that I don't feel qualified to write about!
- Improved CI workflows, we're now testing on multiple platforms at once.
<a name="5.5.1"></a>
## 5.5.1 (2023-05-17)
#### Fixed
- Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix.
<a name="5.5.0"></a>
## 5.5.0 (2023-05-17)
#### Added
@ -252,8 +270,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Added a markdown linter to run on GitHub actions
- Split quick installation section into two code blocks
<a name="5.4.1"></a>
## 5.4.1 (2023-03-10)
#### Changed
@ -269,8 +285,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- `macros4`: Prevented auto-fix by adding `#[rustfmt::skip]`
- `cli`: Actually show correct progress percentages
<a name="5.4.0"></a>
## 5.4.0 (2023-02-12)
#### Changed
@ -299,8 +313,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Bumped min Rust version to 1.58 in installation script
<a name="5.3.0"></a>
## 5.3.0 (2022-12-23)
#### Added
@ -333,8 +345,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Applied some Clippy and rustfmt formatting
- Added a note on Windows PowerShell and other shell compatibility
<a name="5.2.1"></a>
## 5.2.1 (2022-09-06)
#### Fixed
@ -348,8 +358,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Fixed a typo in README.md
<a name="5.2.0"></a>
## 5.2.0 (2022-08-27)
#### Added
@ -366,16 +374,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **quiz1**: Adjusted the explanations to be consistent with
the tests
<a name="5.1.1"></a>
## 5.1.1 (2022-08-17)
#### Bug Fixes
- Fixed an incorrect assertion in options1
<a name="5.1.0"></a>
## 5.1.0 (2022-08-16)
#### Features
@ -410,8 +414,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Clarified manual installation instructions using `cargo install --path .`
- Added a link to our Zulip in the readme file
<a name="5.0.0"></a>
## 5.0.0 (2022-07-16)
#### Features
@ -484,8 +486,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Updated spacing in Cargo.toml.
- Added a GitHub actions config so that tests run on every PR/commit.
<a name="4.8.0"></a>
## 4.8.0 (2022-07-01)
#### Features
@ -506,8 +506,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting.
- Removed the deprecated Rust GitPod extension.
<a name="4.7.1"></a>
## 4.7.1 (2022-04-20)
#### Features
@ -528,8 +526,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- The changelog will now be manually written instead of being automatically generated by the
Git log.
<a name="4.7.0"></a>
## 4.7.0 (2022-04-14)
#### Features
@ -570,8 +566,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886))
- Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929))
<a name="4.6.0"></a>
## 4.6.0 (2021-09-25)
#### Features
@ -594,8 +588,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1))
- **quiz1:** Fix inconsistent wording (#826) ([03131a3d](https://github.com/rust-lang/rustlings/commit/03131a3d35d9842598150f9da817f7cc26e2669a))
<a name="4.5.0"></a>
## 4.5.0 (2021-07-07)
#### Features
@ -616,8 +608,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **try_from_into, from_str:** hints for dyn Error ([11d2cf0d](https://github.com/rust-lang/rustlings/commit/11d2cf0d604dee3f5023c17802d69438e69fa50e))
- **variables5:** confine the answer further ([48ffcbd2](https://github.com/rust-lang/rustlings/commit/48ffcbd2c4cc4d936c2c7480019190f179813cc5))
<a name="4.4.0"></a>
## 4.4.0 (2021-04-24)
#### Bug Fixes
@ -659,8 +649,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913))
- added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4))
<a name="4.3.0"></a>
## 4.3.0 (2020-12-29)
#### Features
@ -683,8 +671,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755))
- **vec1:** Have test compare every element in a and v ([9b6c6293](https://github.com/rust-lang/rustlings/commit/9b6c629397b24b944f484f5b2bbd8144266b5695))
<a name="4.2.0"></a>
## 4.2.0 (2020-11-07)
#### Features
@ -705,8 +691,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e))
- **quiz3:** Second test is for odd numbers, not even. (#553) ([18e0bfef](https://github.com/rust-lang/rustlings/commit/18e0bfef1de53071e353ba1ec5837002ff7290e6))
<a name="4.1.0"></a>
## 4.1.0 (2020-10-05)
#### Bug Fixes
@ -729,8 +713,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **cli:** Added 'cls' command to 'watch' mode (#474) ([4f2468e1](https://github.com/rust-lang/rustlings/commit/4f2468e14f574a93a2e9b688367b5752ed96ae7b))
- **try_from_into:** Add insufficient length test (#469) ([523d18b8](https://github.com/rust-lang/rustlings/commit/523d18b873a319f7c09262f44bd40e2fab1830e5))
<a name="4.0.0"></a>
## 4.0.0 (2020-07-08)
#### Breaking Changes
@ -772,8 +754,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **test2:** name of type String and &str (#394) ([d6c0a688](https://github.com/rust-lang/rustlings/commit/d6c0a688e6a96f93ad60d540d4b326f342fc0d45))
- **variables6:** minor typo (#419) ([524e17df](https://github.com/rust-lang/rustlings/commit/524e17df10db95f7b90a0f75cc8997182a8a4094))
<a name="3.0.0"></a>
## 3.0.0 (2020-04-11)
#### Breaking Changes
@ -796,8 +776,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229))
- **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71))
<a name="2.2.1"></a>
### 2.2.1 (2020-02-27)
#### Bug Fixes
@ -808,13 +786,11 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921))
<a name="2.2.0"></a>
## 2.2.0 (2020-02-25)
#### Bug Fixes
- Update deps to version compatable with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
- Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
- **docs:**
- Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f))
- Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9))
@ -837,8 +813,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Added traits exercises (#274 but specifically #216, which originally added
this :heart:) ([b559cdd](https://github.com/rust-lang/rustlings/commit/b559cdd73f32c0d0cfc1feda39f82b3e3583df17))
<a name="2.1.0"></a>
## 2.1.0 (2019-11-27)
#### Bug Fixes
@ -856,8 +830,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c))
<a name="2.0.0"></a>
## 2.0.0 (2019-11-12)
#### Bug Fixes
@ -878,8 +850,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425))
- **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f))
<a name="1.5.1"></a>
### 1.5.1 (2019-11-11)
#### Bug Fixes
@ -891,8 +861,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **threads:** Move Threads behind SLT ([fbe91a67](https://github.com/rust-lang/rustlings/commit/fbe91a67a482bfe64cbcdd58d06ba830a0f39da3), closes [#205](https://github.com/rust-lang/rustlings/issues/205))
- **watch:** clear screen before each `verify()` ([3aff590](https://github.com/rust-lang/rustlings/commit/3aff59085586c24196a547c2693adbdcf4432648))
<a name="1.5.0"></a>
## 1.5.0 (2019-11-09)
#### Bug Fixes
@ -917,8 +885,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959))
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
<a name="1.4.1"></a>
### 1.4.1 (2019-08-13)
#### Bug Fixes
@ -927,8 +893,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6))
- **test1:** Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446))
<a name="1.4.0"></a>
## 1.4.0 (2019-07-13)
#### Bug Fixes
@ -945,8 +909,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724))
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
<a name="1.3.0"></a>
### 1.3.0 (2019-06-05)
#### Features
@ -962,16 +924,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Fix broken link (#164, @HanKruiger)
- Remove highlighting and syntect (#167, @komaeda)
<a name="1.2.2"></a>
### 1.2.2 (2019-05-07)
#### Bug Fixes
- Reverted `--nocapture` flag since it was causing tests to pass unconditionally
<a name="1.2.1"></a>
### 1.2.1 (2019-04-22)
#### Bug Fixes
@ -979,8 +937,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Fix the `--nocapture` feature (@komaeda)
- Provide a nicer error message for when you're in the wrong directory
<a name="1.2.0"></a>
### 1.2.0 (2019-04-22)
#### Features
@ -988,8 +944,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Add errors to exercises that compile without user changes (@yvan-sraka)
- Use --nocapture when testing, enabling `println!` when running (@komaeda)
<a name="1.1.1"></a>
### 1.1.1 (2019-04-14)
#### Bug fixes
@ -1002,8 +956,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Fix links by deleting book version (@diodfr, #142)
- Canonicalize paths to fix path matching (@cjpearce, #143)
<a name="1.1.0"></a>
### 1.1.0 (2019-03-20)
- errors2.rs: update link to Rust book (#124)
@ -1013,16 +965,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
- Give a warning when Rustlings isn't run from the right directory (#123)
- Verify that rust version is recent enough to install Rustlings (#131)
<a name="1.0.1"></a>
### 1.0.1 (2019-03-06)
- Adds a way to install Rustlings in one command (`curl -L https://git.io/rustlings | bash`)
- Makes `rustlings watch` react to create file events (@shaunbennett, #117)
- Reworks the exercise management to use an external TOML file instead of just listing them in the code
<a name="1.0.0"></a>
### 1.0.0 (2019-03-06)
Initial release.

483
Cargo.lock generated
View File

@ -1,24 +1,12 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
version = 4
[[package]]
name = "anstream"
version = "0.6.15"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
@ -31,49 +19,50 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.8"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.86"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "autocfg"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
@ -83,9 +72,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "cfg-if"
@ -95,9 +84,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.16"
version = "4.5.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
dependencies = [
"clap_builder",
"clap_derive",
@ -105,9 +94,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.15"
version = "4.5.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
dependencies = [
"anstream",
"anstyle",
@ -117,9 +106,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.13"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
@ -129,40 +118,26 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.2"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "crossbeam-channel"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "crossterm"
version = "0.28.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.9.1",
"crossterm_winapi",
"mio 1.0.2",
"document-features",
"mio",
"parking_lot",
"rustix",
"signal-hook",
@ -180,26 +155,35 @@ dependencies = [
]
[[package]]
name = "equivalent"
version = "1.0.1"
name = "document-features"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
dependencies = [
"litrs",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.9"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.1.1"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "filetime"
@ -210,7 +194,7 @@ dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
@ -223,10 +207,22 @@ dependencies = [
]
[[package]]
name = "hashbrown"
version = "0.14.5"
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]]
name = "heck"
@ -234,17 +230,11 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "indexmap"
version = "2.4.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
@ -252,11 +242,11 @@ dependencies = [
[[package]]
name = "inotify"
version = "0.9.6"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.9.1",
"inotify-sys",
"libc",
]
@ -278,15 +268,15 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.11"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "kqueue"
version = "1.0.8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
@ -304,9 +294,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.158"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libredox"
@ -314,22 +304,28 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.9.1",
"libc",
"redox_syscall",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lock_api"
version = "0.4.12"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
@ -337,9 +333,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.22"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
@ -349,79 +345,58 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mio"
version = "0.8.11"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"libc",
"log",
"wasi",
"windows-sys 0.52.0",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]]
name = "notify"
version = "6.1.1"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
dependencies = [
"bitflags 2.6.0",
"crossbeam-channel",
"bitflags 2.9.1",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
name = "notify-debouncer-mini"
version = "0.4.1"
name = "notify-types"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
dependencies = [
"log",
"notify",
]
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "once_cell"
version = "1.19.0"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "os_pipe"
version = "1.2.1"
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "parking_lot"
version = "0.12.3"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core",
@ -429,67 +404,72 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.10"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.3"
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "redox_syscall"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.9.1",
]
[[package]]
name = "rustix"
version = "0.38.35"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
name = "rustlings"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"ahash",
"anyhow",
"clap",
"crossterm",
"notify-debouncer-mini",
"os_pipe",
"notify",
"rustix",
"rustlings-macros",
"serde",
"serde_json",
@ -499,7 +479,7 @@ dependencies = [
[[package]]
name = "rustlings-macros"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"quote",
"serde",
@ -508,9 +488,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.18"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
@ -529,18 +509,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.209"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.209"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
@ -549,9 +529,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.127"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
@ -561,18 +541,18 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.7"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
@ -585,24 +565,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio 1.0.2",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.13.2"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "strsim"
@ -612,9 +592,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.76"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
@ -623,31 +603,31 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.12.0"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"cfg-if",
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.20"
version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
dependencies = [
"indexmap",
"serde",
@ -658,9 +638,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.12"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
@ -668,12 +648,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
@ -690,6 +664,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -712,7 +695,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
@ -721,46 +704,13 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
"windows-targets",
]
[[package]]
@ -769,46 +719,28 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -821,48 +753,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@ -871,29 +779,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.18"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"bitflags 2.9.1",
]

View File

@ -6,7 +6,7 @@ exclude = [
]
[workspace.package]
version = "6.3.0"
version = "6.4.0"
authors = [
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
@ -15,12 +15,12 @@ authors = [
]
repository = "https://github.com/rust-lang/rustlings"
license = "MIT"
edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions.
rust-version = "1.80"
edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and `CARGO_TOML` in `dev new`.
rust-version = "1.87"
[workspace.dependencies]
serde = { version = "1.0.209", features = ["derive"] }
toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] }
serde = { version = "1.0", features = ["derive"] }
toml_edit = { version = "0.22", default-features = false, features = ["parse", "serde"] }
[package]
name = "rustlings"
@ -46,19 +46,20 @@ include = [
]
[dependencies]
ahash = { version = "0.8.11", default-features = false }
anyhow = "1.0.86"
clap = { version = "4.5.16", features = ["derive"] }
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
notify-debouncer-mini = { version = "0.4.1", default-features = false }
os_pipe = "1.2.1"
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
serde_json = "1.0.127"
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] }
notify = "8.0"
rustlings-macros = { path = "rustlings-macros", version = "=6.4.0" }
serde_json = "1.0"
serde.workspace = true
toml_edit.workspace = true
[target.'cfg(not(windows))'.dependencies]
rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] }
[dev-dependencies]
tempfile = "3.12.0"
tempfile = "3.19"
[profile.release]
panic = "abort"
@ -68,6 +69,7 @@ panic = "abort"
[package.metadata.release]
pre-release-hook = ["./release-hook.sh"]
pre-release-commit-message = "Release 🎉"
[workspace.lints.rust]
unsafe_code = "forbid"
@ -81,8 +83,6 @@ infinite_loop = "deny"
mem_forget = "deny"
dbg_macro = "warn"
todo = "warn"
# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
needless_option_as_deref = "allow"
[lints]
workspace = true

157
README.md
View File

@ -1,156 +1,7 @@
<div class="oranda-hide">
# [Rustlings](https://rustlings.rust-lang.org) 🦀
# Rustlings 🦀❤️
Small exercises to get you used to reading and writing [Rust](https://www.rust-lang.org) code - _Recommended in parallel to reading [the official Rust book](https://doc.rust-lang.org/book) 📚_
</div>
Visit the **website** for a demo, info about setup and more:
Greetings and welcome to Rustlings.
This project contains small exercises to get you used to reading and writing Rust code.
This includes reading and responding to compiler messages!
It is recommended to do the Rustlings exercises in parallel to reading [the official Rust book](https://doc.rust-lang.org/book/), the most comprehensive resource for learning Rust 📚️
[Rust By Example](https://doc.rust-lang.org/rust-by-example/) is another recommended resource that you might find helpful.
It contains code examples and exercises similar to Rustlings, but online.
## Getting Started
### Installing Rust
Before installing Rustlings, you need to have the **latest version of Rust** installed.
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust.
This will also install _Cargo_, Rust's package/project manager.
> 🐧 If you're on Linux, make sure you've installed `gcc` (for a linker).
>
> Deb: `sudo apt install gcc`.
> Dnf: `sudo dnf install gcc`.
> 🍎 If you're on MacOS, make sure you've installed Xcode and its developer tools by running `xcode-select --install`.
### Installing Rustlings
The following command will download and compile Rustlings:
```bash
cargo install rustlings
```
<details>
<summary><strong>If the installation fails…</strong> (<em>click to expand</em>)</summary>
- Make sure you have the latest Rust version by running `rustup update`
- Try adding the `--locked` flag: `cargo install rustlings --locked`
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
</details>
### Initialization
After installing Rustlings, run the following command to initialize the `rustlings/` directory:
```bash
rustlings init
```
<details>
<summary><strong>If the command <code>rustlings</code> can't be found…</strong> (<em>click to expand</em>)</summary>
You are probably using Linux and installed Rust using your package manager.
Cargo installs binaries to the directory `~/.cargo/bin`.
Sadly, package managers often don't add `~/.cargo/bin` to your `PATH` environment variable.
The solution is to …
- either add `~/.cargo/bin` manually to `PATH`
- or to uninstall Rust from the package manager and install it using the official way with `rustup`: https://www.rust-lang.org/tools/install
</details>
Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises:
```bash
cd rustlings/
rustlings
```
## Working environment
### Editor
Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises.
### Terminal
While working with Rustlings, please use a modern terminal for the best user experience.
The default terminal on Linux and Mac should be sufficient.
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
## Doing exercises
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.
For every topic, there is an additional `README.md` file with some resources to get you started on the topic.
We highly recommend that you have a look at them before you start 📚️
Most exercises contain an error that keeps them from compiling, and it's up to you to fix it!
Some exercises contain tests that need to pass for the exercise to be done ✅
Search for `TODO` and `todo!()` to find out what you need to change.
Ask for hints by entering `h` in the _watch mode_ 💡
### Watch Mode
After [initialization](#initialization), Rustlings can be launched by simply running the command `rustlings`.
This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers).
It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
<details>
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
>
> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
</details>
### Exercise List
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
The list allows you to…
- See the status of all exercises (done or pending)
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards)
See the footer of the list for all possible keys.
## Continuing On
Once you've completed Rustlings, put your new knowledge to good use!
Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.
## Third-Party Exercises
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
Or do you want to translate the original Rustlings exercises?
Then follow the link to the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
## Uninstalling Rustlings
If you want to remove Rustlings from your system, run the following command:
```bash
cargo uninstall rustlings
```
## Contributing
See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md) 🔗
## Contributors ✨
Thanks to [all the wonderful contributors](https://github.com/rust-lang/rustlings/graphs/contributors) 🎉
## ➡️ [rustlings.rust-lang.org](https://rustlings.rust-lang.org) ⬅️

View File

@ -1,53 +0,0 @@
# Third-Party Exercises
The support of Rustlings for third-party exercises allows you to create your own set of Rustlings exercises to focus on some specific topic.
You could also offer a translation of the original Rustlings exercises as third-party exercises.
## Getting started
To create third-party exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`.
This command will, similar to `cargo new PROJECT_NAME`, create a template directory called `PROJECT_NAME` with all what you need to get started.
Read the comments in the generated `info.toml` file to understand its format.
It allows you to set a custom welcome and final message and specify the metadata of every exercise.
## Create an exercise
Here is an example of the metadata of one file:
```toml
[[exercises]]
name = "intro1"
hint = """
To finish this exercise, you need to …
This link might help you …"""
```
After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory.
The exercise needs to contain a `main` function, but it can be empty.
Adding tests is recommended.
Look at the official Rustlings exercises for inspiration.
You can optionally add a solution file `intro1.rs` to the `solutions/` directory.
Now, run `rustlings dev check`.
It will tell you about any issues with your exercises.
For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`.
`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully.
That's it!
You finished your first exercise 🎉
## Publish
Now, add more exercises and publish them as a Git repository.
Users just have to clone that repository and run `rustlings` in it to start working on your set of exercises just like the official ones.
One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise.
But you can trust the users to not look at the solution too early 😉
## Share
After publishing your set of exercises, open an issue or a pull request in the official Rustlings repository to link to your project in the README 😃

5
build.rs Normal file
View File

@ -0,0 +1,5 @@
fn main() {
// Fix building from source on Windows because it can't handle file links.
#[cfg(windows)]
let _ = std::fs::copy("dev/Cargo.toml", "dev-Cargo.toml");
}

View File

@ -5,9 +5,11 @@ disallowed-types = [
]
disallowed-methods = [
# We use `ahash` instead of the default hasher.
"std::collections::HashSet::new",
"std::collections::HashSet::with_capacity",
# Inefficient. Use `.queue(…)` instead.
"crossterm::style::style",
# Use `thread::Builder::spawn` instead and handle the error.
"std::thread::spawn",
"std::thread::Scope::spawn",
# Return `ExitCode` instead.
"std::process::exit",
]

View File

@ -1,4 +1,4 @@
# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update`. This comment line will be stripped in `rustlings init`.
# Don't edit the `bin` list manually! It is updated by `cargo dev update`. This comment line will be stripped in `rustlings init`.
bin = [
{ name = "intro1", path = "../exercises/00_intro/intro1.rs" },
{ name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" },
@ -192,7 +192,7 @@ bin = [
[package]
name = "exercises"
edition = "2021"
edition = "2024"
# Don't publish the exercises on crates.io!
publish = false
@ -203,17 +203,21 @@ panic = "abort"
panic = "abort"
[lints.rust]
# You shouldn't write unsafe code in Rustlings
# You shouldn't write unsafe code in Rustlings!
unsafe_code = "forbid"
# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust
# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
unstable_features = "forbid"
# Dead code warnings can't be avoided in some exercises and might distract while learning.
dead_code = "allow"
[lints.clippy]
# You forgot a `todo!()`
# You forgot a `todo!()`!
todo = "forbid"
# This can only happen by mistake in Rustlings
# This can only happen by mistake in Rustlings.
empty_loop = "forbid"
# No infinite loops are needed in Rustlings
# No infinite loops are needed in Rustlings.
infinite_loop = "deny"
# You shouldn't leak memory while still learning Rust
# You shouldn't leak memory while still learning Rust!
mem_forget = "deny"
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
disallowed_methods = "allow"

View File

@ -1,7 +1,7 @@
# Variables
In Rust, variables are immutable by default.
When a variable is immutable, once a value is bound to a name, you cant change that value.
When a variable is immutable, once a value is bound to a name, you can't change that value.
You can make them mutable by adding `mut` in front of the variable name.
## Further information

View File

@ -1,6 +1,6 @@
fn main() {
let number = "T-H-R-E-E"; // Don't change this line
println!("Spell a number: {}", number);
println!("Spell a number: {number}");
// TODO: Fix the compiler error by changing the line below without renaming the variable.
number = 3;

View File

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

View File

@ -1,10 +1,10 @@
# Enums
Rust allows you to define types called "enums" which enumerate possible values.
Enums are a feature in many languages, but their capabilities differ in each language. Rusts enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.
Enums are a feature in many languages, but their capabilities differ in each language. Rust's enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.
Useful in combination with enums is Rust's "pattern matching" facility, which makes it easy to run different code for different values of an enumeration.
## Further information
- [Enums](https://doc.rust-lang.org/book/ch06-00-enums.html)
- [Pattern syntax](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html)
- [Pattern syntax](https://doc.rust-lang.org/book/ch19-03-pattern-syntax.html)

View File

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

View File

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

View File

@ -23,6 +23,7 @@ mod tests {
assert_eq!(trim_me("Hello! "), "Hello!");
assert_eq!(trim_me(" What's up!"), "What's up!");
assert_eq!(trim_me(" Hola! "), "Hola!");
assert_eq!(trim_me("Hi!"), "Hi!");
}
#[test]

View File

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

View File

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

View File

@ -1,8 +1,8 @@
// This function returns how much icecream there is left in the fridge.
// This function returns how much ice cream there is left in the fridge.
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
// someone eats it all, so no icecream is left (value 0). Return `None` if
// someone eats it all, so no ice cream is left (value 0). Return `None` if
// `hour_of_day` is higher than 23.
fn maybe_icecream(hour_of_day: u16) -> Option<u16> {
fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> {
// TODO: Complete the function body.
}
@ -18,19 +18,19 @@ mod tests {
fn raw_value() {
// TODO: Fix this test. How do you get the value contained in the
// Option?
let icecreams = maybe_icecream(12);
let ice_creams = maybe_ice_cream(12);
assert_eq!(icecreams, 5); // Don't change this line.
assert_eq!(ice_creams, 5); // Don't change this line.
}
#[test]
fn check_icecream() {
assert_eq!(maybe_icecream(0), Some(5));
assert_eq!(maybe_icecream(9), Some(5));
assert_eq!(maybe_icecream(18), Some(5));
assert_eq!(maybe_icecream(22), Some(0));
assert_eq!(maybe_icecream(23), Some(0));
assert_eq!(maybe_icecream(24), None);
assert_eq!(maybe_icecream(25), None);
fn check_ice_cream() {
assert_eq!(maybe_ice_cream(0), Some(5));
assert_eq!(maybe_ice_cream(9), Some(5));
assert_eq!(maybe_ice_cream(18), Some(5));
assert_eq!(maybe_ice_cream(22), Some(0));
assert_eq!(maybe_ice_cream(23), Some(0));
assert_eq!(maybe_ice_cream(24), None);
assert_eq!(maybe_ice_cream(25), None);
}
}

View File

@ -9,7 +9,7 @@ fn main() {
// TODO: Fix the compiler error by adding something to this match statement.
match optional_point {
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
_ => panic!("No match!"),
}

View File

@ -1,8 +1,8 @@
# Error handling
Most errors arent serious enough to require the program to stop entirely.
Sometimes, when a function fails, its for a reason that you can easily interpret and respond to.
For example, if you try to open a file and that operation fails because the file doesnt exist, you might want to create the file instead of terminating the process.
Most errors aren't serious enough to require the program to stop entirely.
Sometimes, when a function fails, it's for a reason that you can easily interpret and respond to.
For example, if you try to open a file and that operation fails because the file doesn't exist, you might want to create the file instead of terminating the process.
## Further information

View File

@ -6,7 +6,7 @@
//
// In short, this particular use case for boxes is for when you want to own a
// value and you care only that it is a type which implements a particular
// trait. To do so, The `Box` is declared as of type `Box<dyn Trait>` where
// trait. To do so, the `Box` is declared as of type `Box<dyn Trait>` where
// `Trait` is the trait the compiler looks for on any value used in that
// context. For this exercise, that context is the potential errors which
// can be returned in a `Result`.

View File

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

View File

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

View File

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

View File

@ -10,5 +10,6 @@ of exercises to Rustlings, but is all about learning to write Macros.
## Further information
- [Macros](https://doc.rust-lang.org/book/ch19-06-macros.html)
- [The Rust Book - Macros](https://doc.rust-lang.org/book/ch20-05-macros.html)
- [The Little Book of Rust Macros](https://veykril.github.io/tlborm/)
- [Rust by Example - macro_rules!](https://doc.rust-lang.org/rust-by-example/macros.html)

View File

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

View File

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

View File

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

View File

@ -1,13 +0,0 @@
{
"project": {
"homepage": "https://rustlings.cool",
"repository": "https://github.com/rust-lang/rustlings"
},
"marketing": {
"analytics": {
"plausible": {
"domain": "rustlings.cool"
}
}
}
}

View File

@ -9,5 +9,8 @@ cargo upgrades
# Similar to CI
cargo clippy -- --deny warnings
cargo fmt --all --check
cargo test --workspace --all-targets
cargo run -- dev check --require-solutions
cargo test --workspace
cargo dev check --require-solutions
# MSRV
cargo +1.87 dev check --require-solutions

View File

@ -16,7 +16,7 @@ include = [
proc-macro = true
[dependencies]
quote = "1.0.37"
quote = "1.0"
serde.workspace = true
toml_edit.workspace = true

View File

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

View File

@ -1,6 +1,6 @@
fn main() {
let number = "T-H-R-E-E";
println!("Spell a number: {}", number);
println!("Spell a number: {number}");
// Using variable shadowing
// https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing

View File

@ -1,9 +1,5 @@
fn bigger(a: i32, b: i32) -> i32 {
if a > b {
a
} else {
b
}
if a > b { a } else { b }
}
fn main() {

View File

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

View File

@ -4,8 +4,6 @@ fn main() {
#[cfg(test)]
mod tests {
// TODO: Fix the compiler errors only by reordering the lines in the test.
// Don't add, change or remove any line.
#[test]
fn move_semantics4() {
let mut x = Vec::new();

View File

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

View File

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

View File

@ -26,6 +26,7 @@ mod tests {
assert_eq!(trim_me("Hello! "), "Hello!");
assert_eq!(trim_me(" What's up!"), "What's up!");
assert_eq!(trim_me(" Hola! "), "Hola!");
assert_eq!(trim_me("Hi!"), "Hi!");
}
#[test]

View File

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

View File

@ -17,7 +17,7 @@ struct TeamScores {
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new();
let mut scores = HashMap::<&str, TeamScores>::new();
for line in results.lines() {
let mut split_iterator = line.split(',');
@ -28,17 +28,13 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
// Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores
.entry(team_1_name)
.or_insert_with(TeamScores::default);
let team_1 = scores.entry(team_1_name).or_default();
// Update the values.
team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score;
// Similarly for the second team.
let team_2 = scores
.entry(team_2_name)
.or_insert_with(TeamScores::default);
let team_2 = scores.entry(team_2_name).or_default();
team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score;
}
@ -64,9 +60,11 @@ England,Spain,1,0";
fn build_scores() {
let scores = build_scores_table(RESULTS);
assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"]
.into_iter()
.all(|team_name| scores.contains_key(team_name)));
assert!(
["England", "France", "Germany", "Italy", "Poland", "Spain"]
.into_iter()
.all(|team_name| scores.contains_key(team_name))
);
}
#[test]

View File

@ -10,7 +10,7 @@ fn main() {
// Solution 1: Matching over the `Option` (not `&Option`) but without moving
// out of the `Some` variant.
match optional_point {
Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y),
Some(ref p) => println!("Coordinates are {},{}", p.x, p.y),
// ^^^ added
_ => panic!("No match!"),
}
@ -18,7 +18,8 @@ fn main() {
// Solution 2: Matching over a reference (`&Option`) by added `&` before
// `optional_point`.
match &optional_point {
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
//^ added
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
_ => panic!("No match!"),
}

View File

@ -16,7 +16,7 @@
use std::num::ParseIntError;
#[allow(unused_variables)]
#[allow(unused_variables, clippy::question_mark)]
fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
let processing_fee = 1;
let cost_per_item = 5;

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)]
struct PositiveNonzeroInteger(u64);

View File

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

View File

@ -5,11 +5,7 @@
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// ^^^^ ^^ ^^ ^^
if x.len() > y.len() {
x
} else {
y
}
if x.len() > y.len() { x } else { y }
}
fn main() {

View File

@ -1,9 +1,5 @@
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
if x.len() > y.len() { x } else { y }
}
fn main() {

View File

@ -8,7 +8,6 @@ use std::rc::Rc;
#[derive(Debug)]
struct Sun;
#[allow(dead_code)]
#[derive(Debug)]
enum Planet {
Mercury(Rc<Sun>),
@ -64,12 +63,10 @@ mod tests {
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
saturn.details();
// TODO
let uranus = Planet::Uranus(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
uranus.details();
// TODO
let neptune = Planet::Neptune(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 9 references
neptune.details();

View File

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

View File

@ -3,11 +3,11 @@ use std::mem;
#[rustfmt::skip]
#[allow(unused_variables, unused_assignments)]
fn main() {
let my_option: Option<()> = None;
let my_option: Option<&str> = None;
// `unwrap` of an `Option` after checking if it is `None` will panic.
// Use `if-let` instead.
if let Some(value) = my_option {
println!("{value:?}");
println!("{value}");
}
// A comma was missing.
@ -15,7 +15,7 @@ fn main() {
-1, -2, -3,
-4, -5, -6,
];
println!("My array! Here it is: {:?}", my_arr);
println!("My array! Here it is: {my_arr:?}");
let mut my_empty_vec = vec![1, 2, 3, 4, 5];
// `resize` mutates a vector instead of returning a new one.
@ -27,5 +27,5 @@ fn main() {
let mut value_b = 66;
// Use `mem::swap` to correctly swap two values.
mem::swap(&mut value_a, &mut value_b);
println!("value a: {}; value b: {}", value_a, value_b);
println!("value a: {value_a}; value b: {value_b}");
}

View File

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

View File

@ -62,8 +62,8 @@ mod tests {
// Import `transformer`.
use super::my_module::transformer;
use super::my_module::transformer_iter;
use super::Command;
use super::my_module::transformer_iter;
#[test]
fn it_works() {

View File

@ -1,32 +1,39 @@
use anyhow::{bail, Context, Result};
use anyhow::{Context, Error, Result, bail};
use crossterm::{QueueableCommand, cursor, terminal};
use std::{
collections::HashSet,
env,
fs::{File, OpenOptions},
io::{self, Read, Seek, StdoutLock, Write},
path::Path,
io::{Read, Seek, StdoutLock, Write},
path::{MAIN_SEPARATOR_STR, Path},
process::{Command, Stdio},
sync::{
atomic::{AtomicUsize, Ordering::Relaxed},
mpsc,
},
thread,
};
use crate::{
clear_terminal,
cmd::CmdRunner,
collections::hash_set_with_capacity,
embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo,
term::{self, CheckProgressVisualizer},
};
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const DEFAULT_CHECK_PARALLELISM: usize = 8;
#[must_use]
pub enum ExercisesProgress {
// All exercises are done.
AllDone,
// The current exercise failed and is still pending.
CurrentPending,
// A new exercise is now pending.
NewPending,
// The current exercise is still pending.
CurrentPending,
}
pub enum StateFileStatus {
@ -34,10 +41,12 @@ pub enum StateFileStatus {
NotRead,
}
enum AllExercisesCheck {
Pending(usize),
AllDone,
CheckedUntil(usize),
#[derive(Clone, Copy)]
pub enum CheckProgress {
None,
Checking,
Done,
Pending,
}
pub struct AppState {
@ -71,6 +80,7 @@ impl AppState {
format!("Failed to open or create the state file {STATE_FILE_NAME}")
})?;
let dir_canonical_path = term::canonicalize("exercises");
let mut exercises = exercise_infos
.into_iter()
.map(|exercise_info| {
@ -82,10 +92,32 @@ impl AppState {
let dir = exercise_info.dir.map(|dir| &*dir.leak());
let hint = exercise_info.hint.leak().trim_ascii();
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
let mut canonical_path;
if let Some(dir) = dir {
canonical_path = String::with_capacity(
2 + dir_canonical_path.len() + dir.len() + name.len(),
);
canonical_path.push_str(dir_canonical_path);
canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(dir);
} else {
canonical_path =
String::with_capacity(1 + dir_canonical_path.len() + name.len());
canonical_path.push_str(dir_canonical_path);
}
canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(name);
canonical_path.push_str(".rs");
canonical_path
});
Exercise {
dir,
name,
path,
canonical_path,
test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy,
hint,
@ -114,13 +146,13 @@ impl AppState {
break 'block StateFileStatus::NotRead;
}
let mut done_exercises = hash_set_with_capacity(exercises.len());
let mut done_exercises = HashSet::with_capacity(exercises.len());
for done_exerise_name in lines {
if done_exerise_name.is_empty() {
for done_exercise_name in lines {
if done_exercise_name.is_empty() {
break;
}
done_exercises.insert(done_exerise_name);
done_exercises.insert(done_exercise_name);
}
for (ind, exercise) in exercises.iter_mut().enumerate() {
@ -170,6 +202,11 @@ impl AppState {
self.n_done
}
#[inline]
pub fn n_pending(&self) -> u16 {
self.exercises.len() as u16 - self.n_done
}
#[inline]
pub fn current_exercise(&self) -> &Exercise {
&self.exercises[self.current_exercise_ind]
@ -246,15 +283,31 @@ impl AppState {
self.write()
}
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
// Set the status of an exercise without saving. Returns `true` if the
// status actually changed (and thus needs saving later).
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
let exercise = self
.exercises
.get_mut(exercise_ind)
.context(BAD_INDEX_ERR)?;
if exercise.done {
exercise.done = false;
if exercise.done == done {
return Ok(false);
}
exercise.done = done;
if done {
self.n_done += 1;
} else {
self.n_done -= 1;
}
Ok(true)
}
// Set the status of an exercise to "pending" and save.
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
if self.set_status(exercise_ind, false)? {
self.write()?;
}
@ -262,7 +315,7 @@ impl AppState {
}
// Official exercises: Dump the original file from the binary.
// Third-party exercises: Reset the exercise file with `git stash`.
// Community exercises: Reset the exercise file with `git stash`.
fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> {
if self.official_exercises {
return EMBEDDED_FILES
@ -331,8 +384,8 @@ impl AppState {
})
}
/// Official exercises: Dump the solution file form the binary and return its path.
/// Third-party exercises: Check if a solution file exists and return its path in that case.
/// Official exercises: Dump the solution file from the binary and return its path.
/// Community exercises: Check if a solution file exists and return its path in that case.
pub fn current_solution_path(&self) -> Result<Option<String>> {
if cfg!(debug_assertions) {
return Ok(None);
@ -355,62 +408,125 @@ impl AppState {
}
}
// Return the exercise index of the first pending exercise found.
fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
let n_exercises = self.exercises.len();
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
let term_width = terminal::size()
.context("Failed to get the terminal size")?
.0;
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
let status = thread::scope(|s| {
let handles = self
.exercises
.iter()
.map(|exercise| s.spawn(|| exercise.run_exercise(None, &self.cmd_runner)))
.collect::<Vec<_>>();
let next_exercise_ind = AtomicUsize::new(0);
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
for (exercise_ind, handle) in handles.into_iter().enumerate() {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
stdout.flush()?;
thread::scope(|s| {
let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
let n_threads = thread::available_parallelism()
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
let Ok(success) = handle.join().unwrap() else {
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
};
for _ in 0..n_threads {
let exercise_progress_sender = exercise_progress_sender.clone();
let next_exercise_ind = &next_exercise_ind;
let slf = &self;
thread::Builder::new()
.spawn_scoped(s, move || {
loop {
let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed);
let Some(exercise) = slf.exercises.get(exercise_ind) else {
// No more exercises.
break;
};
if !success {
return Ok(AllExercisesCheck::Pending(exercise_ind));
}
if exercise_progress_sender
.send((exercise_ind, CheckProgress::Checking))
.is_err()
{
break;
};
let success = exercise.run_exercise(None, &slf.cmd_runner);
let progress = match success {
Ok(true) => CheckProgress::Done,
Ok(false) => CheckProgress::Pending,
Err(_) => CheckProgress::None,
};
if exercise_progress_sender
.send((exercise_ind, progress))
.is_err()
{
break;
}
}
})
.context("Failed to spawn a thread to check all exercises")?;
}
Ok::<_, io::Error>(AllExercisesCheck::AllDone)
// Drop this sender to detect when the last thread is done.
drop(exercise_progress_sender);
while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
progresses[exercise_ind] = progress;
progress_visualizer.update(&progresses)?;
}
Ok::<_, Error>(())
})?;
let mut exercise_ind = match status {
AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)),
AllExercisesCheck::AllDone => return Ok(None),
AllExercisesCheck::CheckedUntil(ind) => ind,
};
let mut first_pending_exercise_ind = None;
for exercise_ind in 0..progresses.len() {
match progresses[exercise_ind] {
CheckProgress::Done => {
self.set_status(exercise_ind, true)?;
}
CheckProgress::Pending => {
self.set_status(exercise_ind, false)?;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
CheckProgress::None | CheckProgress::Checking => {
// If we got an error while checking all exercises in parallel,
// it could be because we exceeded the limit of open file descriptors.
// Therefore, try running exercises with errors sequentially.
progresses[exercise_ind] = CheckProgress::Checking;
progress_visualizer.update(&progresses)?;
// We got an error while checking all exercises in parallel.
// This could be because we exceeded the limit of open file descriptors.
// Therefore, try to continue the check sequentially.
for exercise in &self.exercises[exercise_ind..] {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
stdout.flush()?;
let success = exercise.run_exercise(None, &self.cmd_runner)?;
if !success {
return Ok(Some(exercise_ind));
let exercise = &self.exercises[exercise_ind];
let success = exercise.run_exercise(None, &self.cmd_runner)?;
if success {
progresses[exercise_ind] = CheckProgress::Done;
} else {
progresses[exercise_ind] = CheckProgress::Pending;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
self.set_status(exercise_ind, success)?;
progress_visualizer.update(&progresses)?;
}
}
exercise_ind += 1;
}
Ok(None)
self.write()?;
Ok(first_pending_exercise_ind)
}
// Return the exercise index of the first pending exercise found.
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
stdout.queue(cursor::Hide)?;
let res = self.check_all_exercises_impl(stdout);
stdout.queue(cursor::Show)?;
res
}
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
/// If all exercises are marked as done, run all of them to make sure that they are actually
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
pub fn done_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
pub fn done_current_exercise<const CLEAR_BEFORE_FINAL_CHECK: bool>(
&mut self,
stdout: &mut StdoutLock,
) -> Result<ExercisesProgress> {
let exercise = &mut self.exercises[self.current_exercise_ind];
if !exercise.done {
exercise.done = true;
@ -422,20 +538,24 @@ impl AppState {
return Ok(ExercisesProgress::NewPending);
}
if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? {
stdout.write_all(b"\n\n")?;
if CLEAR_BEFORE_FINAL_CHECK {
clear_terminal(stdout)?;
} else {
stdout.write_all(b"\n")?;
}
if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? {
self.set_current_exercise_ind(first_pending_exercise_ind)?;
self.current_exercise_ind = pending_exercise_ind;
self.exercises[pending_exercise_ind].done = false;
// All exercises were marked as done.
self.n_done -= 1;
self.write()?;
return Ok(ExercisesProgress::NewPending);
}
// Write that the last exercise is done.
self.write()?;
self.render_final_message(stdout)?;
Ok(ExercisesProgress::AllDone)
}
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
clear_terminal(stdout)?;
stdout.write_all(FENISH_LINE.as_bytes())?;
@ -445,16 +565,12 @@ impl AppState {
stdout.write_all(b"\n")?;
}
Ok(ExercisesProgress::AllDone)
Ok(())
}
}
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
All exercises seem to be done.
Recompiling and running all exercises to make sure that all of them are actually done.
";
const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! |
+-------------------------- ------------------------+
@ -486,6 +602,7 @@ mod tests {
dir: None,
name: "0",
path: "exercises/0.rs",
canonical_path: None,
test: false,
strict_clippy: false,
hint: "",

View File

@ -74,13 +74,13 @@ pub fn updated_cargo_toml(
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY);
updated_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes());
updated_cargo_toml.extend_from_slice(&current_cargo_toml.as_bytes()[..bins_start_ind]);
append_bins(
&mut updated_cargo_toml,
exercise_infos,
exercise_path_prefix,
);
updated_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes());
updated_cargo_toml.extend_from_slice(&current_cargo_toml.as_bytes()[bins_end_ind..]);
Ok(updated_cargo_toml)
}

View File

@ -1,7 +1,7 @@
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use std::{
io::Read,
io::{Read, pipe},
path::PathBuf,
process::{Command, Stdio},
};
@ -17,7 +17,7 @@ fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) ->
};
let mut handle = if let Some(output) = output {
let (mut reader, writer) = os_pipe::pipe().with_context(|| {
let (mut reader, writer) = pipe().with_context(|| {
format!("Failed to create a pipe to run the command `{description}``")
})?;
@ -125,7 +125,7 @@ pub struct CargoSubcommand<'out> {
output: Option<&'out mut Vec<u8>>,
}
impl<'out> CargoSubcommand<'out> {
impl CargoSubcommand<'_> {
#[inline]
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
where

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,4 +1,4 @@
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use clap::Subcommand;
use std::path::PathBuf;
@ -8,7 +8,7 @@ mod update;
#[derive(Subcommand)]
pub enum DevCommands {
/// Create a new project for third-party Rustlings exercises
/// Create a new project for community exercises
New {
/// The path to create the project in
path: PathBuf,

View File

@ -1,7 +1,8 @@
use anyhow::{anyhow, bail, Context, Error, Result};
use anyhow::{Context, Error, Result, anyhow, bail};
use std::{
cmp::Ordering,
fs::{self, read_dir, OpenOptions},
collections::HashSet,
fs::{self, OpenOptions, read_dir},
io::{self, Read, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
@ -9,14 +10,14 @@ use std::{
};
use crate::{
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
cmd::CmdRunner,
collections::{hash_set_with_capacity, HashSet},
exercise::{RunnableExercise, OUTPUT_CAPACITY},
info_file::{ExerciseInfo, InfoFile},
CURRENT_FORMAT_VERSION,
cargo_toml::{BINS_BUFFER_CAPACITY, append_bins, bins_start_end_ind},
cmd::CmdRunner,
exercise::{OUTPUT_CAPACITY, RunnableExercise},
info_file::{ExerciseInfo, InfoFile},
};
const MAX_N_EXERCISES: usize = 999;
const MAX_EXERCISE_NAME_LEN: usize = 32;
// Find a char that isn't allowed in the exercise's `name` or `dir`.
@ -41,10 +42,14 @@ fn check_cargo_toml(
if old_bins != new_bins {
if cfg!(debug_assertions) {
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
bail!(
"The file `dev/Cargo.toml` is outdated. Run `cargo dev update` to update it. Then run `cargo run -- dev check` again"
);
}
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
bail!(
"The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again"
);
}
Ok(())
@ -52,8 +57,8 @@ fn check_cargo_toml(
// Check the info of all exercises and return their paths in a set.
fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
let mut names = hash_set_with_capacity(info_file.exercises.len());
let mut paths = hash_set_with_capacity(info_file.exercises.len());
let mut names = HashSet::with_capacity(info_file.exercises.len());
let mut paths = HashSet::with_capacity(info_file.exercises.len());
let mut file_buf = String::with_capacity(1 << 14);
for exercise_info in &info_file.exercises {
@ -62,7 +67,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
bail!("Found an empty exercise name in `info.toml`");
}
if name.len() > MAX_EXERCISE_NAME_LEN {
bail!("The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN}");
bail!(
"The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN}"
);
}
if let Some(c) = forbidden_char(name) {
bail!("Char `{c}` in the exercise name `{name}` is not allowed");
@ -78,7 +85,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
}
if exercise_info.hint.trim_ascii().is_empty() {
bail!("The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise");
bail!(
"The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise"
);
}
if !names.insert(name) {
@ -95,20 +104,28 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
.with_context(|| format!("Failed to read the file {path}"))?;
if !file_buf.contains("fn main()") {
bail!("The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors");
bail!(
"The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors"
);
}
if !file_buf.contains("// TODO") {
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
bail!(
"Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user."
);
}
let contains_tests = file_buf.contains("#[test]\n");
if exercise_info.test {
if !contains_tests {
bail!("The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file");
bail!(
"The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file"
);
}
} else if contains_tests {
bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file");
bail!(
"The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file"
);
}
file_buf.clear();
@ -124,7 +141,10 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
// Only one level of directory nesting is allowed.
fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> Result<()> {
let unexpected_file = |path: &Path| {
anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory", path.display())
anyhow!(
"Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory",
path.display()
)
};
for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? {
@ -153,7 +173,10 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
let path = entry.path();
if !entry.file_type().unwrap().is_file() {
bail!("Found `{}` but expected only files. Only one level of exercise nesting is allowed", path.display());
bail!(
"Found `{}` but expected only files. Only one level of exercise nesting is allowed",
path.display()
);
}
let file_name = path.file_name().unwrap();
@ -185,12 +208,14 @@ fn check_exercises_unsolved(
return None;
}
Some((
exercise_info.name.as_str(),
thread::spawn(|| exercise_info.run_exercise(None, cmd_runner)),
))
Some(
thread::Builder::new()
.spawn(|| exercise_info.run_exercise(None, cmd_runner))
.map(|handle| (exercise_info.name.as_str(), handle)),
)
})
.collect::<Vec<_>>();
.collect::<Result<Vec<_>, _>>()
.context("Failed to spawn a thread to check if an exercise is already solved")?;
let n_handles = handles.len();
write!(stdout, "Progress: 0/{n_handles}")?;
@ -199,7 +224,7 @@ fn check_exercises_unsolved(
for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}");
bail!("Panic while trying to run the exercise {exercise_name}");
};
match result {
@ -221,12 +246,18 @@ fn check_exercises_unsolved(
fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> {
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
Ordering::Less => bail!(
"`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"
),
Ordering::Greater => bail!(
"`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"
),
Ordering::Equal => (),
}
let handle = thread::spawn(move || check_exercises_unsolved(info_file, cmd_runner));
let handle = thread::Builder::new()
.spawn(move || check_exercises_unsolved(info_file, cmd_runner))
.context("Failed to spawn a thread to check if any exercise is already solved")?;
let info_file_paths = check_info_file_exercises(info_file)?;
check_unexpected_files("exercises", &info_file_paths)?;
@ -253,7 +284,7 @@ fn check_solutions(
.exercises
.iter()
.map(|exercise_info| {
thread::spawn(move || {
thread::Builder::new().spawn(move || {
let sol_path = exercise_info.sol_path();
if !Path::new(&sol_path).exists() {
if require_solutions {
@ -274,14 +305,15 @@ fn check_solutions(
}
})
})
.collect::<Vec<_>>();
.collect::<Result<Vec<_>, _>>()
.context("Failed to spawn a thread to check a solution")?;
let mut sol_paths = hash_set_with_capacity(info_file.exercises.len());
let mut sol_paths = HashSet::with_capacity(info_file.exercises.len());
let mut fmt_cmd = Command::new("rustfmt");
fmt_cmd
.arg("--check")
.arg("--edition")
.arg("2021")
.arg("2024")
.arg("--color")
.arg("always")
.stdin(Stdio::null());
@ -294,7 +326,7 @@ fn check_solutions(
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
let Ok(check_result) = handle.join() else {
bail!(
"Panic while trying to run the solution of the exericse {}",
"Panic while trying to run the solution of the exercise {}",
exercise_info.name,
);
};
@ -322,7 +354,11 @@ fn check_solutions(
}
stdout.write_all(b"\n")?;
let handle = thread::spawn(move || check_unexpected_files("solutions", &sol_paths));
let handle = thread::Builder::new()
.spawn(move || check_unexpected_files("solutions", &sol_paths))
.context(
"Failed to spawn a thread to check for unexpected files in the solutions directory",
)?;
if !fmt_cmd
.status()
@ -338,8 +374,12 @@ fn check_solutions(
pub fn check(require_solutions: bool) -> Result<()> {
let info_file = InfoFile::parse()?;
if info_file.exercises.len() > MAX_N_EXERCISES {
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
}
if cfg!(debug_assertions) {
// A hack to make `cargo run -- dev check` work when developing Rustlings.
// A hack to make `cargo dev check` work when developing Rustlings.
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
} else {
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;

View File

@ -1,4 +1,4 @@
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use std::{
env::set_current_dir,
fs::{self, create_dir},
@ -6,7 +6,7 @@ use std::{
process::Command,
};
use crate::CURRENT_FORMAT_VERSION;
use crate::{CURRENT_FORMAT_VERSION, init::RUST_ANALYZER_TOML};
// Create a directory relative to the current directory and print its path.
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
@ -55,13 +55,17 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
write_rel_file(
"info.toml",
&dir_path_str,
format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"),
format!(
"{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"
),
)?;
write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?;
write_rel_file("README.md", &dir_path_str, README)?;
write_rel_file("rust-analyzer.toml", &dir_path_str, RUST_ANALYZER_TOML)?;
create_rel_dir(".vscode", &dir_path_str)?;
write_rel_file(
".vscode/extensions.json",
@ -82,10 +86,10 @@ target/
";
const INFO_FILE_BEFORE_FORMAT_VERSION: &str =
"# The format version is an indicator of the compatibility of third-party exercises with the
"# The format version is an indicator of the compatibility of community exercises with the
# Rustlings program.
# The format version is not the same as the version of the Rustlings program.
# In case Rustlings makes an unavoidable breaking change to the expected format of third-party
# In case Rustlings makes an unavoidable breaking change to the expected format of community
# exercises, you would need to raise this version and adapt to the new format.
# Otherwise, the newest version of the Rustlings program won't be able to run these exercises.
format_version = ";
@ -93,7 +97,7 @@ format_version = ";
const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#"
# Optional multi-line message to be shown to users when just starting with the exercises.
welcome_message = """Welcome to these third-party Rustlings exercises."""
welcome_message = """Welcome to these community Rustlings exercises."""
# Optional multi-line message to be shown to users after finishing all exercises.
final_message = """We hope that you found the exercises helpful :D"""
@ -128,7 +132,7 @@ bin = []
[package]
name = "exercises"
edition = "2021"
edition = "2024"
# Don't publish the exercises on crates.io!
publish = false
@ -137,7 +141,7 @@ publish = false
const README: &str = "# Rustlings 🦀
Welcome to these third-party Rustlings exercises 😃
Welcome to these community Rustlings exercises 😃
First, [install Rustlings using the official instructions](https://github.com/rust-lang/rustlings) ✅

View File

@ -28,7 +28,7 @@ pub fn update() -> Result<()> {
let info_file = InfoFile::parse()?;
if cfg!(debug_assertions) {
// A hack to make `cargo run -- dev update` work when developing Rustlings.
// A hack to make `cargo dev update` work when developing Rustlings.
update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")
.context("Failed to update the file `dev/Cargo.toml`")?;

View File

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

View File

@ -1,13 +1,13 @@
use anyhow::Result;
use crossterm::{
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
QueueableCommand,
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
};
use std::io::{self, StdoutLock, Write};
use crate::{
cmd::CmdRunner,
term::{terminal_file_link, write_ansi},
term::{self, CountedWrite, terminal_file_link, write_ansi},
};
/// The initial capacity of the output buffer.
@ -18,7 +18,11 @@ pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::R
stdout.write_all(b"Solution")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" for comparison: ")?;
terminal_file_link(stdout, solution_path, Color::Cyan)?;
if let Some(canonical_path) = term::canonicalize(solution_path) {
terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?;
} else {
stdout.write_all(solution_path.as_bytes())?;
}
stdout.write_all(b"\n")
}
@ -60,12 +64,23 @@ pub struct Exercise {
pub name: &'static str,
/// Path of the exercise file starting with the `exercises/` directory.
pub path: &'static str,
pub canonical_path: Option<String>,
pub test: bool,
pub strict_clippy: bool,
pub hint: &'static str,
pub done: bool,
}
impl Exercise {
pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> {
if let Some(canonical_path) = self.canonical_path.as_deref() {
return terminal_file_link(writer, self.path, canonical_path, Color::Blue);
}
writer.write_str(self.path)
}
}
pub trait RunnableExercise {
fn name(&self) -> &str;
fn dir(&self) -> Option<&str>;
@ -116,7 +131,7 @@ pub trait RunnableExercise {
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
// `--profile test` is required to also check code with `[cfg(test)]`.
// `--profile test` is required to also check code with `#[cfg(test)]`.
if FORCE_STRICT_CLIPPY || self.strict_clippy() {
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
} else {

View File

@ -1,4 +1,4 @@
use anyhow::{bail, Context, Error, Result};
use anyhow::{Context, Error, Result, bail};
use serde::Deserialize;
use std::{fs, io::ErrorKind};
@ -79,7 +79,7 @@ impl RunnableExercise for ExerciseInfo {
/// The deserialized `info.toml` file.
#[derive(Deserialize)]
pub struct InfoFile {
/// For possible breaking changes in the future for third-party exercises.
/// For possible breaking changes in the future for community exercises.
pub format_version: u8,
/// Shown to users when starting with the exercises.
pub welcome_message: Option<String>,
@ -91,7 +91,7 @@ pub struct InfoFile {
impl InfoFile {
/// Official exercises: Parse the embedded `info.toml` file.
/// Third-party exercises: Parse the `info.toml` file in the current directory.
/// Community exercises: Parse the `info.toml` file in the current directory.
pub fn parse() -> Result<Self> {
// Read a local `info.toml` if it exists.
let slf = match fs::read_to_string("info.toml") {

View File

@ -1,7 +1,7 @@
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use crossterm::{
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
QueueableCommand,
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
};
use serde::Deserialize;
use std::{
@ -57,7 +57,9 @@ pub fn init() -> Result<()> {
if !workspace_manifest_content.contains("[workspace]\n")
&& !workspace_manifest_content.contains("workspace.")
{
bail!("The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory");
bail!(
"The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory"
);
}
stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\nPress ENTER to continue ")?;
@ -75,7 +77,9 @@ pub fn init() -> Result<()> {
.stdout(Stdio::null())
.status()?;
if !status.success() {
bail!("Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory");
bail!(
"Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory"
);
}
stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the `Cargo.toml` file of this Cargo workspace.\n")?;
@ -130,6 +134,9 @@ pub fn init() -> Result<()> {
fs::write("Cargo.toml", updated_cargo_toml)
.context("Failed to create the file `rustlings/Cargo.toml`")?;
fs::write("rust-analyzer.toml", RUST_ANALYZER_TOML)
.context("Failed to create the file `rustlings/rust-analyzer.toml`")?;
fs::write(".gitignore", GITIGNORE)
.context("Failed to create the file `rustlings/.gitignore`")?;
@ -169,6 +176,11 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
}
";
pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy"
check.extraArgs = ["--profile", "test"]
cargo.targetDir = true
"#;
const GITIGNORE: &[u8] = b"Cargo.lock
target/
.vscode/

View File

@ -1,14 +1,13 @@
use anyhow::{Context, Result};
use crossterm::{
cursor,
QueueableCommand, cursor,
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
},
terminal::{
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
LeaveAlternateScreen,
DisableLineWrap, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode,
},
QueueableCommand,
};
use std::io::{self, StdoutLock, Write};
@ -20,7 +19,8 @@ mod scroll_state;
mod state;
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
let mut list_state = ListState::new(app_state, stdout)?;
let mut list_state = ListState::build(app_state, stdout)?;
let mut is_searching = false;
loop {
match event::read().context("Failed to read terminal event")? {
@ -32,6 +32,27 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
list_state.message.clear();
if is_searching {
match key.code {
KeyCode::Esc | KeyCode::Enter => {
is_searching = false;
list_state.search_query.clear();
}
KeyCode::Char(c) => {
list_state.search_query.push(c);
list_state.apply_search_query();
}
KeyCode::Backspace => {
list_state.search_query.pop();
list_state.apply_search_query();
}
_ => continue,
}
list_state.draw(stdout)?;
continue;
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
@ -50,15 +71,15 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
}
}
KeyCode::Char('p') => {
let message = if list_state.filter() == Filter::Pending {
if list_state.filter() == Filter::Pending {
list_state.set_filter(Filter::None);
"Disabled filter PENDING"
list_state.message.push_str("Disabled filter PENDING");
} else {
list_state.set_filter(Filter::Pending);
"Enabled filter PENDING │ Press p again to disable the filter"
};
list_state.message.push_str(message);
list_state.message.push_str(
"Enabled filter PENDING │ Press p again to disable the filter",
);
}
}
KeyCode::Char('r') => list_state.reset_selected()?,
KeyCode::Char('c') => {
@ -66,6 +87,10 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
return Ok(());
}
}
KeyCode::Char('s' | '/') => {
is_searching = true;
list_state.apply_search_query();
}
// Redraw to remove the message.
KeyCode::Esc => (),
_ => continue,

View File

@ -46,7 +46,7 @@ impl ScrollState {
self.selected
}
fn set_selected(&mut self, selected: usize) {
pub fn set_selected(&mut self, selected: usize) {
self.selected = Some(selected);
self.update_offset();
}

View File

@ -1,9 +1,11 @@
use anyhow::{Context, Result};
use crossterm::{
cursor::{MoveTo, MoveToNextLine},
style::{Attribute, Color, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor},
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
QueueableCommand,
cursor::{MoveTo, MoveToNextLine},
style::{
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
},
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
};
use std::{
fmt::Write as _,
@ -13,12 +15,15 @@ use std::{
use crate::{
app_state::AppState,
exercise::Exercise,
term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter},
term::{CountedWrite, MaxLenWriter, progress_bar},
};
use super::scroll_state::ScrollState;
const COL_SPACING: usize = 2;
const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none()
.with(Attribute::Reverse)
.with(Attribute::Bold);
fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
stdout
@ -37,48 +42,53 @@ pub enum Filter {
pub struct ListState<'a> {
/// Footer message to be displayed if not empty.
pub message: String,
pub search_query: String,
app_state: &'a mut AppState,
scroll_state: ScrollState,
name_col_padding: Vec<u8>,
path_col_padding: Vec<u8>,
filter: Filter,
term_width: u16,
term_height: u16,
separator_line: Vec<u8>,
narrow_term: bool,
show_footer: bool,
}
impl<'a> ListState<'a> {
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> {
pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
stdout.queue(Clear(ClearType::All))?;
let name_col_title_len = 4;
let name_col_width = app_state
.exercises()
.iter()
.map(|exercise| exercise.name.len())
.max()
.map_or(name_col_title_len, |max| max.max(name_col_title_len));
let path_col_title_len = 4;
let (name_col_width, path_col_width) = app_state.exercises().iter().fold(
(name_col_title_len, path_col_title_len),
|(name_col_width, path_col_width), exercise| {
(
name_col_width.max(exercise.name.len()),
path_col_width.max(exercise.path.len()),
)
},
);
let name_col_padding = vec![b' '; name_col_width + COL_SPACING];
let path_col_padding = vec![b' '; path_col_width];
let filter = Filter::None;
let n_rows_with_filter = app_state.exercises().len();
let selected = app_state.current_exercise_ind();
let (width, height) = terminal::size()?;
let (width, height) = terminal::size().context("Failed to get the terminal size")?;
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
let mut slf = Self {
message: String::with_capacity(128),
search_query: String::new(),
app_state,
scroll_state,
name_col_padding,
path_col_padding,
filter,
// Set by `set_term_size`
term_width: 0,
term_height: 0,
separator_line: Vec::new(),
narrow_term: false,
show_footer: true,
};
@ -96,25 +106,39 @@ impl<'a> ListState<'a> {
return;
}
let wide_help_footer_width = 95;
// The help footer is shorter when nothing is selected.
self.narrow_term = width < wide_help_footer_width && self.scroll_state.selected().is_some();
let header_height = 1;
// 2 separator, 1 progress bar, 1-2 footer message.
let footer_height = 4 + u16::from(self.narrow_term);
// 1 progress bar, 2 footer message lines.
let footer_height = 3;
self.show_footer = height > header_height + footer_height;
if self.show_footer {
self.separator_line = "".as_bytes().repeat(width as usize);
}
self.scroll_state.set_max_n_rows_to_display(
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
as usize,
);
}
fn draw_exercise_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> {
if !self.search_query.is_empty() {
if let Some((pre_highlight, highlight, post_highlight)) = exercise
.name
.find(&self.search_query)
.and_then(|ind| exercise.name.split_at_checked(ind))
.and_then(|(pre_highlight, rest)| {
rest.split_at_checked(self.search_query.len())
.map(|x| (pre_highlight, x.0, x.1))
})
{
writer.write_str(pre_highlight)?;
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
writer.write_str(highlight)?;
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
return writer.write_str(post_highlight);
}
}
writer.write_str(exercise.name)
}
fn draw_rows(
&self,
stdout: &mut StdoutLock,
@ -131,14 +155,12 @@ impl<'a> ListState<'a> {
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.scroll_state.selected() == Some(row_offset + n_displayed_rows) {
writer.stdout.queue(SetBackgroundColor(Color::Rgb {
r: 40,
g: 40,
b: 40,
}))?;
// The crab emoji has the width of two ascii chars.
writer.add_to_len(2);
writer.stdout.write_all("🦀".as_bytes())?;
writer
.stdout
.queue(SetAttributes(SELECTED_ROW_ATTRIBUTES))?;
} else {
writer.write_ascii(b" ")?;
}
@ -152,15 +174,16 @@ impl<'a> ListState<'a> {
if exercise.done {
writer.stdout.queue(SetForegroundColor(Color::Green))?;
writer.write_ascii(b"DONE ")?;
writer.write_ascii(b"DONE ")?;
} else {
writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
writer.write_ascii(b"PENDING ")?;
writer.write_ascii(b"PENDING")?;
}
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
writer.write_ascii(b" ")?;
self.draw_exercise_name(&mut writer, exercise)?;
writer.write_str(exercise.name)?;
writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?;
// The list links aren't shown correctly in VS Code on Windows.
@ -168,9 +191,11 @@ impl<'a> ListState<'a> {
if self.app_state.vs_code() {
writer.write_str(exercise.path)?;
} else {
terminal_file_link(&mut writer, exercise.path, Color::Blue)?;
exercise.terminal_file_link(&mut writer)?;
}
writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?;
next_ln(stdout)?;
stdout.queue(ResetColor)?;
n_displayed_rows += 1;
@ -208,9 +233,6 @@ impl<'a> ListState<'a> {
}
if self.show_footer {
stdout.write_all(&self.separator_line)?;
next_ln(stdout)?;
progress_bar(
&mut MaxLenWriter::new(stdout, self.term_width as usize),
self.app_state.n_done(),
@ -219,22 +241,15 @@ impl<'a> ListState<'a> {
)?;
next_ln(stdout)?;
stdout.write_all(&self.separator_line)?;
next_ln(stdout)?;
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.message.is_empty() {
// Help footer message
if self.scroll_state.selected().is_some() {
writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
if self.narrow_term {
next_ln(stdout)?;
writer = MaxLenWriter::new(stdout, self.term_width as usize);
next_ln(stdout)?;
writer = MaxLenWriter::new(stdout, self.term_width as usize);
writer.write_ascii(b"filter ")?;
} else {
writer.write_ascii(b" | filter ")?;
}
writer.write_ascii(b"<s>earch | filter ")?;
} else {
// Nothing selected (and nothing shown), so only display filter and quit.
writer.write_ascii(b"filter ")?;
@ -263,17 +278,14 @@ impl<'a> ListState<'a> {
}
writer.write_ascii(b" | <q>uit list")?;
next_ln(stdout)?;
} else {
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
writer.write_str(&self.message)?;
stdout.queue(ResetColor)?;
next_ln(stdout)?;
if self.narrow_term {
next_ln(stdout)?;
}
}
next_ln(stdout)?;
}
stdout.queue(EndSynchronizedUpdate)?.flush()
@ -370,6 +382,33 @@ impl<'a> ListState<'a> {
Ok(())
}
pub fn apply_search_query(&mut self) {
self.message.push_str("search:");
self.message.push_str(&self.search_query);
self.message.push('|');
if self.search_query.is_empty() {
return;
}
let is_search_result = |exercise: &Exercise| exercise.name.contains(&self.search_query);
let mut iter = self.app_state.exercises().iter();
let ind = match self.filter {
Filter::None => iter.position(is_search_result),
Filter::Done => iter
.filter(|exercise| exercise.done)
.position(is_search_result),
Filter::Pending => iter
.filter(|exercise| !exercise.done)
.position(is_search_result),
};
match ind {
Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind),
None => self.message.push_str(" (not found)"),
}
}
// Return `true` if there was something to select.
pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
let Some(selected) = self.scroll_state.selected() else {

View File

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

View File

@ -1,20 +1,19 @@
use anyhow::Result;
use crossterm::{
style::{Color, ResetColor, SetForegroundColor},
QueueableCommand,
style::{Color, ResetColor, SetForegroundColor},
};
use std::{
io::{self, Write},
process::exit,
process::ExitCode,
};
use crate::{
app_state::{AppState, ExercisesProgress},
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
term::terminal_file_link,
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line},
};
pub fn run(app_state: &mut AppState) -> Result<()> {
pub fn run(app_state: &mut AppState) -> Result<ExitCode> {
let exercise = app_state.current_exercise();
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
@ -26,9 +25,12 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
app_state.set_pending(app_state.current_exercise_ind())?;
stdout.write_all(b"Ran ")?;
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
app_state
.current_exercise()
.terminal_file_link(&mut stdout)?;
stdout.write_all(b" with errors\n")?;
exit(1);
return Ok(ExitCode::FAILURE);
}
stdout.queue(SetForegroundColor(Color::Green))?;
@ -43,14 +45,16 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
stdout.write_all(b"\n")?;
}
match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => {
match app_state.done_current_exercise::<false>(&mut stdout)? {
ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => {
stdout.write_all(b"Next exercise: ")?;
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
app_state
.current_exercise()
.terminal_file_link(&mut stdout)?;
stdout.write_all(b"\n")?;
}
ExercisesProgress::AllDone => (),
}
Ok(())
Ok(ExitCode::SUCCESS)
}

View File

@ -1,24 +1,25 @@
use crossterm::{
Command, QueueableCommand,
cursor::MoveTo,
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
terminal::{Clear, ClearType},
};
use std::{
fmt, fs,
io::{self, BufRead, StdoutLock, Write},
};
use crossterm::{
cursor::MoveTo,
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
terminal::{Clear, ClearType},
Command, QueueableCommand,
};
use crate::app_state::CheckProgress;
pub struct MaxLenWriter<'a, 'b> {
pub stdout: &'a mut StdoutLock<'b>,
pub struct MaxLenWriter<'a, 'lock> {
pub stdout: &'a mut StdoutLock<'lock>,
len: usize,
max_len: usize,
}
impl<'a, 'b> MaxLenWriter<'a, 'b> {
impl<'a, 'lock> MaxLenWriter<'a, 'lock> {
#[inline]
pub fn new(stdout: &'a mut StdoutLock<'b>, max_len: usize) -> Self {
pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self {
Self {
stdout,
len: 0,
@ -33,13 +34,13 @@ impl<'a, 'b> MaxLenWriter<'a, 'b> {
}
}
pub trait CountedWrite<'a> {
pub trait CountedWrite<'lock> {
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>;
fn write_str(&mut self, unicode: &str) -> io::Result<()>;
fn stdout(&mut self) -> &mut StdoutLock<'a>;
fn stdout(&mut self) -> &mut StdoutLock<'lock>;
}
impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> {
impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> {
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
if n > 0 {
@ -64,7 +65,7 @@ impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> {
}
#[inline]
fn stdout(&mut self) -> &mut StdoutLock<'b> {
fn stdout(&mut self) -> &mut StdoutLock<'lock> {
self.stdout
}
}
@ -86,33 +87,104 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> {
}
}
/// Terminal progress bar to be used when not using Ratataui.
pub struct CheckProgressVisualizer<'a, 'lock> {
stdout: &'a mut StdoutLock<'lock>,
n_cols: usize,
}
impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> {
const CHECKING_COLOR: Color = Color::Blue;
const DONE_COLOR: Color = Color::Green;
const PENDING_COLOR: Color = Color::Red;
pub fn build(stdout: &'a mut StdoutLock<'lock>, term_width: u16) -> io::Result<Self> {
clear_terminal(stdout)?;
stdout.write_all("Checking all exercises…\n".as_bytes())?;
// Legend
stdout.write_all(b"Color of exercise number: ")?;
stdout.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
stdout.write_all(b"Checking")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" - ")?;
stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
stdout.write_all(b"Done")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" - ")?;
stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
stdout.write_all(b"Pending")?;
stdout.queue(ResetColor)?;
stdout.write_all(b"\n")?;
// Exercise numbers with up to 3 digits.
// +1 because the last column doesn't end with a whitespace.
let n_cols = usize::from(term_width + 1) / 4;
Ok(Self { stdout, n_cols })
}
pub fn update(&mut self, progresses: &[CheckProgress]) -> io::Result<()> {
self.stdout.queue(MoveTo(0, 2))?;
let mut exercise_num = 1;
for exercise_progress in progresses {
match exercise_progress {
CheckProgress::None => (),
CheckProgress::Checking => {
self.stdout
.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
}
CheckProgress::Done => {
self.stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
}
CheckProgress::Pending => {
self.stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
}
}
write!(self.stdout, "{exercise_num:<3}")?;
self.stdout.queue(ResetColor)?;
if exercise_num != progresses.len() {
if exercise_num % self.n_cols == 0 {
self.stdout.write_all(b"\n")?;
} else {
self.stdout.write_all(b" ")?;
}
exercise_num += 1;
}
}
self.stdout.flush()
}
}
pub fn progress_bar<'a>(
writer: &mut impl CountedWrite<'a>,
progress: u16,
total: u16,
line_width: u16,
term_width: u16,
) -> io::Result<()> {
debug_assert!(total <= 999);
debug_assert!(progress <= total);
const PREFIX: &[u8] = b"Progress: [";
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
// Leaving the last char empty (_) for `total` > 99.
const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16;
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
if line_width < MIN_LINE_WIDTH {
if term_width < MIN_LINE_WIDTH {
writer.write_ascii(b"Progress: ")?;
// Integers are in ASCII.
writer.write_ascii(format!("{progress}/{total}").as_bytes())?;
return writer.write_ascii(b" exercises");
return writer.write_ascii(format!("{progress}/{total}").as_bytes());
}
let stdout = writer.stdout();
stdout.write_all(PREFIX)?;
let width = line_width - WRAPPER_WIDTH;
let width = term_width - WRAPPER_WIDTH;
let filled = (width * progress) / total;
stdout.queue(SetForegroundColor(Color::Green))?;
@ -133,8 +205,9 @@ pub fn progress_bar<'a>(
}
}
stdout.queue(ResetColor)?;
write!(stdout, "] {progress:>3}/{total} exercises")
stdout.queue(SetForegroundColor(Color::Reset))?;
write!(stdout, "] {progress:>3}/{total}")
}
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
@ -151,25 +224,29 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\n")
}
/// Canonicalize, convert to string and remove verbatim part on Windows.
pub fn canonicalize(path: &str) -> Option<String> {
fs::canonicalize(path)
.ok()?
.into_os_string()
.into_string()
.ok()
.map(|mut path| {
// Windows itself can't handle its verbatim paths.
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
path.drain(..4);
}
path
})
}
pub fn terminal_file_link<'a>(
writer: &mut impl CountedWrite<'a>,
path: &str,
canonical_path: &str,
color: Color,
) -> io::Result<()> {
let canonical_path = fs::canonicalize(path).ok();
let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else {
return writer.write_str(path);
};
// Windows itself can't handle its verbatim paths.
#[cfg(windows)]
let canonical_path = if canonical_path.len() > 5 && &canonical_path[0..4] == r"\\?\" {
&canonical_path[4..]
} else {
canonical_path
};
writer
.stdout()
.queue(SetForegroundColor(color))?

View File

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

View File

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

View File

@ -1,19 +1,30 @@
use anyhow::Result;
use anyhow::{Context, Result};
use crossterm::{
QueueableCommand,
style::{
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
},
terminal, QueueableCommand,
terminal,
};
use std::{
io::{self, Read, StdoutLock, Write},
sync::mpsc::{Sender, SyncSender, sync_channel},
thread,
};
use std::io::{self, StdoutLock, Write};
use crate::{
app_state::{AppState, ExercisesProgress},
clear_terminal,
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
term::{progress_bar, terminal_file_link},
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line},
term::progress_bar,
};
use super::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler};
const HEADING_ATTRIBUTES: Attributes = Attributes::none()
.with(Attribute::Bold)
.with(Attribute::Underlined);
#[derive(PartialEq, Eq)]
enum DoneStatus {
DoneWithSolution(String),
@ -27,20 +38,47 @@ pub struct WatchState<'a> {
show_hint: bool,
done_status: DoneStatus,
manual_run: bool,
term_width: u16,
terminal_event_unpause_sender: SyncSender<()>,
}
impl<'a> WatchState<'a> {
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
Self {
pub fn build(
app_state: &'a mut AppState,
watch_event_sender: Sender<WatchEvent>,
manual_run: bool,
) -> Result<Self> {
let term_width = terminal::size()
.context("Failed to get the terminal size")?
.0;
let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0);
thread::Builder::new()
.spawn(move || {
terminal_event_handler(
watch_event_sender,
terminal_event_unpause_receiver,
manual_run,
)
})
.context("Failed to spawn a thread to handle terminal events")?;
Ok(Self {
app_state,
output: Vec::with_capacity(OUTPUT_CAPACITY),
show_hint: false,
done_status: DoneStatus::Pending,
manual_run,
}
term_width,
terminal_event_unpause_sender,
})
}
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
// Ignore any input until running the exercise is done.
let _input_pause_guard = InputPauseGuard::scoped_pause();
self.show_hint = false;
writeln!(
@ -72,39 +110,67 @@ impl<'a> WatchState<'a> {
Ok(())
}
pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
clear_terminal(stdout)?;
stdout.write_all(b"Resetting will undo all your changes to the file ")?;
stdout.write_all(self.app_state.current_exercise().path.as_bytes())?;
stdout.write_all(b"\nReset (y/n)? ")?;
stdout.flush()?;
{
let mut stdin = io::stdin().lock();
let mut answer = [0];
loop {
stdin
.read_exact(&mut answer)
.context("Failed to read the user's input")?;
match answer[0] {
b'y' | b'Y' => {
self.app_state.reset_current_exercise()?;
// The file watcher reruns the exercise otherwise.
if self.manual_run {
self.run_current_exercise(stdout)?;
}
}
b'n' | b'N' => self.render(stdout)?,
_ => continue,
}
break;
}
}
self.terminal_event_unpause_sender.send(())?;
Ok(())
}
pub fn handle_file_change(
&mut self,
exercise_ind: usize,
stdout: &mut StdoutLock,
) -> Result<()> {
// Don't skip exercises on file changes to avoid confusion from missing exercises.
// Skipping exercises must be explicit in the interactive list.
// But going back to an earlier exercise on file change is fine.
if self.app_state.current_exercise_ind() < exercise_ind {
if self.app_state.current_exercise_ind() != exercise_ind {
return Ok(());
}
self.app_state.set_current_exercise_ind(exercise_ind)?;
self.run_current_exercise(stdout)
}
/// Move on to the next exercise if the current one is done.
pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
if self.done_status == DoneStatus::Pending {
return Ok(ExercisesProgress::CurrentPending);
match self.done_status {
DoneStatus::DoneWithSolution(_) | DoneStatus::DoneWithoutSolution => (),
DoneStatus::Pending => return Ok(ExercisesProgress::CurrentPending),
}
self.app_state.done_current_exercise(stdout)
self.app_state.done_current_exercise::<true>(stdout)
}
fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> {
if self.manual_run {
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"r")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":run / ")?;
}
if self.done_status != DoneStatus::Pending {
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"n")?;
@ -116,22 +182,25 @@ impl<'a> WatchState<'a> {
stdout.write_all(b" / ")?;
}
if !self.show_hint {
let mut show_key = |key, postfix| {
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"h")?;
stdout.write_all(&[key])?;
stdout.queue(ResetColor)?;
stdout.write_all(b":hint / ")?;
stdout.write_all(postfix)
};
if self.manual_run {
show_key(b'r', b":run / ")?;
}
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"l")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":list / ")?;
if !self.show_hint {
show_key(b'h', b":hint / ")?;
}
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"q")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":quit ? ")?;
show_key(b'l', b":list / ")?;
show_key(b'c', b":check all / ")?;
show_key(b'x', b":reset / ")?;
show_key(b'q', b":quit ? ")?;
stdout.flush()
}
@ -145,9 +214,7 @@ impl<'a> WatchState<'a> {
if self.show_hint {
stdout
.queue(SetAttributes(
Attributes::from(Attribute::Bold).with(Attribute::Underlined),
))?
.queue(SetAttributes(HEADING_ATTRIBUTES))?
.queue(SetForegroundColor(Color::Cyan))?;
stdout.write_all(b"Hint")?;
stdout.queue(ResetColor)?;
@ -175,16 +242,17 @@ impl<'a> WatchState<'a> {
)?;
}
let line_width = terminal::size()?.0;
progress_bar(
stdout,
self.app_state.n_done(),
self.app_state.exercises().len() as u16,
line_width,
self.term_width,
)?;
stdout.write_all(b"\nCurrent exercise: ")?;
terminal_file_link(stdout, self.app_state.current_exercise().path, Color::Blue)?;
self.app_state
.current_exercise()
.terminal_file_link(stdout)?;
stdout.write_all(b"\n\n")?;
self.show_prompt(stdout)?;
@ -193,7 +261,39 @@ impl<'a> WatchState<'a> {
}
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
self.show_hint = true;
self.render(stdout)
if !self.show_hint {
self.show_hint = true;
self.render(stdout)?;
}
Ok(())
}
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
// Ignore any input until checking all exercises is done.
let _input_pause_guard = InputPauseGuard::scoped_pause();
if let Some(first_pending_exercise_ind) = self.app_state.check_all_exercises(stdout)? {
// Only change exercise if the current one is done.
if self.app_state.current_exercise().done {
self.app_state
.set_current_exercise_ind(first_pending_exercise_ind)?;
Ok(ExercisesProgress::NewPending)
} else {
Ok(ExercisesProgress::CurrentPending)
}
} else {
self.app_state.render_final_message(stdout)?;
Ok(ExercisesProgress::AllDone)
}
}
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
if self.term_width != width {
self.term_width = width;
self.render(stdout)?;
}
Ok(())
}
}

View File

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

View File

@ -7,5 +7,5 @@ bin = [
[package]
name = "test_exercises"
edition = "2021"
edition = "2024"
publish = false

7
website/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/node_modules/
/package-lock.json
/public/
/static/main.css
/static/processed_images/

41
website/config.toml Normal file
View File

@ -0,0 +1,41 @@
base_url = "https://rustlings.rust-lang.org"
title = "Rustlings"
description = "Small exercises to get you used to reading and writing Rust code!"
compile_sass = false
build_search_index = false
[markdown]
highlight_code = true
highlight_theme = "dracula"
insert_anchor_links = "heading"
[extra]
logo_path = "images/happy_ferris.svg"
[[extra.menu_items]]
name = "Rustlings"
url = "@/_index.md"
[[extra.menu_items]]
name = "Setup"
url = "@/setup/index.md"
[[extra.menu_items]]
name = "Usage"
url = "@/usage/index.md"
[[extra.menu_items]]
name = "Community Exercises"
url = "@/community-exercises/index.md"
[[extra.menu_items]]
name = "Q&A"
url = "https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q="
[[extra.footer_items]]
name = "Repository"
url = "https://github.com/rust-lang/rustlings"
[[extra.footer_items]]
name = "Changelog"
url = "https://github.com/rust-lang/rustlings/blob/main/CHANGELOG.md"
[[extra.footer_items]]
name = "MIT License"
url = "https://github.com/rust-lang/rustlings/blob/main/LICENSE"

21
website/content/_index.md Normal file
View File

@ -0,0 +1,21 @@
+++
+++
Small exercises to get you used to reading and writing [Rust](https://www.rust-lang.org) code - _Recommended in parallel to reading [the official Rust book](https://doc.rust-lang.org/book) 📚_
<script src="https://asciinema.org/a/719805.js" id="asciicast-719805" async="true"></script>
## Quick start
```bash
# Installation
cargo install rustlings
# Initialization
rustlings init
# Moving into new directory
cd rustlings
# Starting Rustlings
rustlings
```
Visit the [**setup**](@/setup/index.md) page for more details 🧰

View File

@ -0,0 +1,73 @@
+++
title = "Community Exercises"
+++
## List of Community Exercises
- 🇯🇵 [Japanese Rustlings](https://github.com/sotanengel/rustlings-jp)A Japanese translation of the Rustlings exercises.
- 🇨🇳 [Simplified Chinese Rustlings](https://github.com/SandmeyerX/rustlings-zh-cn): A simplified Chinese translation of the Rustlings exercises.
> You can use the same `rustlings` program that you installed with `cargo install rustlings` to run community exercises.
## Creating Community Exercises
Rustling's support for community exercises allows you to create your own exercises to focus on some specific topic.
You could also offer a translation of the original Rustlings exercises as community exercises.
### Getting Started
To create community exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`.
This command will, similar to `cargo new PROJECT_NAME`, create the template directory `PROJECT_NAME` with all what you need to get started.
_Read the comments_ in the generated `info.toml` file to understand its format.
It allows you to set a custom welcome and final message and specify the metadata of every exercise.
### Creating an Exercise
Here is an example of the metadata of one exercise:
```toml
[[exercises]]
name = "intro1"
hint = """
To finish this exercise, you need to …
These links might help you …"""
```
After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory.
The exercise needs to contain a `main` function, but it can be empty.
Adding tests is recommended.
Look at the official Rustlings exercises for inspiration.
You can optionally add a solution file `intro1.rs` to the `solutions/` directory.
Now, run `rustlings dev check`.
It will tell you about any issues with your exercises.
For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`.
`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully.
That's it!
You finished your first exercise 🎉
### Cargo.toml
Except of the `bin` list, you can modify the `Cargo.toml` file as you want.
> The `bin` list is automatically updated by running `rustlings dev update`
- You can add dependencies in the `[dependencies]` table.
- You might want to [configure some lints](https://doc.rust-lang.org/cargo/reference/manifest.html#the-lints-section) for all exercises. You can do so in the `[lints.rust]` and `[lints.clippy]` tables.
### Publishing
Now, add more exercises and publish them as a Git repository.
Users just have to clone that repository and run `rustlings` in it to start working on your exercises (just like the official ones).
One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise.
But you can trust your users to not open the solution too early 😉
### Sharing
After publishing your community exercises, open an issue or a pull request in the [official Rustlings repository](https://github.com/rust-lang/rustlings) to add your project to the [list of community exercises](#list-of-community-exercises) 😃

View File

@ -0,0 +1,78 @@
+++
title = "Setup"
+++
<!-- toc -->
## Installing Rust
Before installing Rustlings, you must have the **latest version of Rust** installed.
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions.
This will also install _Cargo_, Rust's package/project manager.
> 🐧 If you are on **Linux**, make sure you have `gcc` installed (_for a linker_).
>
> Debian: `sudo apt install gcc`\
> Fedora: `sudo dnf install gcc`
> 🍎 If you are on **MacOS**, make sure you have _Xcode and its developer tools_ installed: `xcode-select --install`
## Installing Rustlings
The following command will download and compile Rustlings:
```bash
cargo install rustlings
```
{% details(summary="If the installation fails…") %}
- Make sure you have the latest Rust version by running `rustup update`
- Try adding the `--locked` flag: `cargo install rustlings --locked`
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
{% end %}
## Initialization
After installing Rustlings, run the following command to initialize the `rustlings/` directory:
```bash
rustlings init
```
{% details(summary="If the command <code>rustlings</code> can't be found…") %}
You are probably using Linux and installed Rust using your package manager.
Cargo installs binaries to the directory `~/.cargo/bin`.
Sadly, package managers often don't add `~/.cargo/bin` to your `PATH` environment variable.
- Either add `~/.cargo/bin` manually to `PATH`
- Or uninstall Rust from the package manager and [install it using the official way with `rustup`](https://www.rust-lang.org/tools/install)
{% end %}
Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises:
```bash
cd rustlings/
rustlings
```
## Working environment
### Editor
Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises.
### Terminal
While working with Rustlings, please use a modern terminal for the best user experience.
The default terminal on Linux and Mac should be sufficient.
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
## Usage
After being done with the setup, visit the [**usage**](@/usage/index.md) page for some info about using Rustlings 🚀

View File

@ -0,0 +1,55 @@
+++
title = "Usage"
+++
<!-- toc -->
## Doing exercises
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.
For every topic, there is an additional `README.md` file with some resources to get you started on the topic.
We highly recommend that you have a look at them before you start 📚️
Most exercises contain an error that keeps them from compiling, and it's up to you to fix it!
Some exercises contain tests that need to pass for the exercise to be done ✅
Search for `TODO` and `todo!()` to find out what you need to change.
Ask for hints by entering `h` in the _watch mode_ 💡
## Watch Mode
After the [initialization](@/setup/index.md#initialization), Rustlings can be launched by simply running the command `rustlings`.
This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers).
It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
{% details(summary="If detecting file changes in the <code>exercises/</code> directory fails…") %}
You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or a virtual machine (e.g. WSL).
{% end %}
## Exercise List
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
The list allows you to…
- See the status of all exercises (done or pending)
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
- `r`: Reset status and file of the selected exercise (you need to _reload/reopen_ its file in your editor afterwards)
See the footer of the list for all possible keys.
## Questions?
If you need any help while doing the exercises and the builtin hints aren't helpful, feel free to ask in the [_Q&A_ discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question isn't answered there 💡
## Continuing On
Once you've completed Rustlings, put your new knowledge to good use!
Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.
> If you want to create your own Rustlings exercises, visit the [**community exercises**](@/community-exercises/index.md) page 🏗️

54
website/input.css Normal file
View File

@ -0,0 +1,54 @@
@import 'tailwindcss';
@layer base {
h1 {
@apply text-4xl mt-3 mb-3 font-bold;
}
h2 {
@apply text-3xl mt-4 mb-1.5 font-bold;
}
h3 {
@apply text-2xl mt-5 mb-1.5 font-bold;
}
h4 {
@apply text-xl mt-6 mb-1.5 font-bold;
}
p {
@apply mb-2;
}
a {
@apply text-[#FFC832] underline hover:decoration-orange-400 transition duration-300;
}
ul {
@apply mt-2 mb-3 ml-1 list-disc list-inside marker:text-sky-600;
}
ol {
@apply mt-2 mb-3 ml-1 list-decimal list-inside marker:text-sky-500;
}
li {
@apply my-0.5;
}
code {
@apply bg-white/10 px-1 pb-px pt-1 rounded-md;
}
pre code {
@apply bg-inherit p-0 text-inherit;
}
hr {
@apply my-5 rounded-full;
}
img {
@apply md:w-3/4 lg:w-3/5;
}
blockquote {
@apply px-3 pt-2 pb-0.5 mb-4 mt-2 border-s-4 border-white/80 bg-white/7 rounded-sm;
}
pre {
@apply px-2 pt-2 pb-px overflow-x-auto text-sm sm:text-base rounded-sm mt-2 mb-4 after:content-[attr(data-lang)] after:text-[8px] after:opacity-40 selection:bg-white/15;
}
pre code mark {
@apply pb-0.5 pt-1 pr-px text-inherit rounded-xs;
}
}

5
website/justfile Normal file
View File

@ -0,0 +1,5 @@
zola:
zola serve --open
tailwind:
npx @tailwindcss/cli -w -i input.css -o static/main.css

5
website/package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"@tailwindcss/cli": "^4.1"
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1200 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g id="Layer-1" serif:id="Layer 1">
<g transform="matrix(1,0,0,1,1009.4,506.362)">
<path d="M0,-7.203L-12.072,-32.209C-12.009,-33.156 -11.961,-34.107 -11.961,-35.062C-11.961,-63.408 -41.439,-89.533 -91.03,-110.451L-91.03,-93.058C-95.866,-94.977 -100.901,-96.845 -106.147,-98.651L-106.147,-106.759C-177.021,-132.319 -282.53,-148.537 -400.388,-148.537C-503.361,-148.537 -596.917,-136.157 -666.179,-115.983L-666.179,-87.737L-666.181,-87.737L-666.181,-121.925C-737.141,-99.375 -781.135,-68.048 -781.135,-33.41C-781.135,-27.95 -780.034,-22.572 -777.918,-17.297L-785.146,-4.43C-785.146,-4.43 -790.938,3.082 -780.74,18.932C-771.746,32.909 -726.692,87.617 -702.913,116.267C-692.699,130.954 -685.772,140.001 -685.167,139.126C-684.212,137.74 -691.518,110.165 -711.802,78.703C-721.268,61.808 -732.57,39.42 -739.356,22.884C-720.414,34.874 -609.126,90.913 -382.124,90.685C-150.13,90.453 -47.009,17.834 -35.691,7.948C-39.646,23.837 -53.159,55.981 -63.936,78.586C-81.642,110.917 -88.056,139.064 -87.232,140.456C-86.708,141.334 -80.667,132.015 -71.756,116.913C-51.025,87.37 -11.739,30.974 -3.889,16.608C5.007,0.323 0,-7.203 0,-7.203" style="fill:rgb(165,43,0);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,1079.49,294.885)">
<path d="M0,204.135L-79.343,145.689C-80.088,143.089 -80.833,140.488 -81.603,137.908L-55.541,100.154C-52.881,96.314 -52.345,91.322 -54.072,86.943C-55.803,82.585 -59.587,79.461 -64.062,78.696L-108.128,71.217C-109.837,67.732 -111.626,64.301 -113.422,60.898L-94.907,18.51C-93.004,14.193 -93.402,9.175 -95.929,5.256C-98.446,1.319 -102.715,-0.981 -107.267,-0.802L-151.991,0.823C-154.306,-2.193 -156.658,-5.18 -159.058,-8.114L-148.78,-53.546C-147.738,-58.158 -149.054,-62.989 -152.267,-66.34C-155.462,-69.679 -160.105,-71.062 -164.52,-69.979L-208.082,-59.27C-210.902,-61.763 -213.77,-64.223 -216.67,-66.635L-215.103,-113.276C-214.935,-117.997 -217.136,-122.484 -220.915,-125.105C-224.692,-127.741 -229.485,-128.137 -233.616,-126.179L-274.254,-106.858C-277.527,-108.736 -280.819,-110.595 -284.146,-112.395L-291.327,-158.356C-292.056,-163.012 -295.051,-166.968 -299.246,-168.774C-303.431,-170.591 -308.222,-170.002 -311.894,-167.238L-348.126,-140.053C-351.695,-141.238 -355.279,-142.373 -358.905,-143.46L-374.522,-187.045C-376.11,-191.488 -379.772,-194.751 -384.238,-195.669C-388.688,-196.578 -393.266,-195.037 -396.352,-191.589L-426.851,-157.47C-430.536,-157.893 -434.228,-158.28 -437.927,-158.601L-461.476,-198.277C-463.86,-202.295 -468.073,-204.741 -472.615,-204.741C-477.144,-204.741 -481.365,-202.295 -483.733,-198.277L-507.288,-158.601C-510.989,-158.28 -514.696,-157.893 -518.376,-157.47L-548.875,-191.589C-551.965,-195.037 -556.559,-196.578 -560.997,-195.669C-565.457,-194.739 -569.125,-191.488 -570.704,-187.045L-586.333,-143.46C-589.954,-142.373 -593.538,-141.23 -597.113,-140.053L-633.333,-167.238C-637.016,-170.012 -641.811,-170.599 -646.001,-168.774C-650.182,-166.968 -653.189,-163.012 -653.914,-158.356L-661.1,-112.395C-664.422,-110.595 -667.714,-108.746 -670.995,-106.858L-711.629,-126.179C-715.756,-128.145 -720.574,-127.741 -724.333,-125.105C-728.106,-122.484 -730.313,-117.997 -730.143,-113.276L-728.581,-66.635C-731.475,-64.223 -734.337,-61.763 -737.172,-59.27L-780.726,-69.979C-785.149,-71.053 -789.788,-69.679 -792.991,-66.34C-796.212,-62.989 -797.517,-58.158 -796.482,-53.546L-786.225,-8.114C-788.603,-5.169 -790.958,-2.193 -793.267,0.823L-837.991,-0.802C-842.504,-0.937 -846.812,1.319 -849.334,5.256C-851.861,9.175 -852.244,14.193 -850.363,18.51L-831.835,60.898C-833.634,64.301 -835.421,67.732 -837.144,71.217L-881.207,78.696C-885.686,79.45 -889.459,82.572 -891.201,86.943C-892.929,91.322 -892.368,96.314 -889.727,100.154L-863.661,137.908C-863.862,138.575 -864.048,139.247 -864.248,139.916L-937.944,218.201C-937.944,218.201 -949.24,227.052 -932.797,247.855C-918.297,266.206 -843.846,338.951 -804.526,377.06C-787.92,396.408 -776.542,408.389 -775.354,407.353C-773.478,405.708 -783.326,370.506 -816.036,329.204C-841.252,292.148 -873.977,235.155 -866.303,228.586C-866.303,228.586 -857.574,217.505 -840.061,209.529C-839.42,210.041 -840.723,209.022 -840.061,209.529C-840.061,209.529 -470.466,380.02 -127.632,212.413C-88.468,205.388 -64.759,226.368 -64.759,226.368C-56.583,231.108 -77.755,289.712 -95.166,328.505C-118.845,372.555 -122.317,406.927 -120.31,408.119C-119.042,408.876 -110.427,395.766 -98.138,374.902C-67.814,332.649 -10.492,252.1 0,232.534C11.895,210.352 0,204.135 0,204.135" style="fill:rgb(247,76,0);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,917.896,244.679)">
<path d="M0,232.466C0,232.466 53.179,230 123.032,159.004L132.93,137.025C132.93,137.025 24.513,29.177 193.048,-45.266C193.048,-45.266 178.293,-21.154 182.622,72.006C182.622,72.006 233.437,54.357 248.336,-27.934C248.336,-27.934 322.456,69.79 167.834,161.443C167.834,161.443 95.294,277.732 -6.971,266.593L0,232.466Z" style="fill:rgb(247,76,0);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,676.997,488.361)">
<path d="M0,-78.192C0,-78.192 36.935,-118.635 73.871,-78.192C73.871,-78.192 102.893,-24.265 73.871,2.695C73.871,2.695 26.384,40.443 0,2.695C0,2.695 -31.658,-26.964 0,-78.192" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,719.761,425.169)">
<path d="M0,0.004C0,15.75 -9.282,28.518 -20.732,28.518C-32.18,28.518 -41.462,15.75 -41.462,0.004C-41.462,-15.746 -32.18,-28.514 -20.732,-28.514C-9.282,-28.514 0,-15.746 0,0.004" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,512.148,482.736)">
<path d="M0,-83.609C0,-83.609 63.355,-111.661 80.648,-49.047C80.648,-49.047 98.762,23.933 28.618,28.052C28.618,28.052 -60.826,10.824 0,-83.609" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,543.968,426.204)">
<path d="M0,0.002C0,16.241 -9.572,29.411 -21.381,29.411C-33.185,29.411 -42.76,16.241 -42.76,0.002C-42.76,-16.242 -33.185,-29.409 -21.381,-29.409C-9.572,-29.409 0,-16.242 0,0.002" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,593.317,576.574)">
<path d="M0,-40.271L80.796,-46.755C80.796,-46.755 78.058,-33.749 67.517,-23.986C67.517,-23.986 39.727,6.484 7.844,-26.519C7.844,-26.519 2.627,-32.148 0,-40.271" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,269.796,270.778)">
<path d="M0,190.741C-0.667,190.741 -1.321,190.79 -1.973,190.842C-28.207,184.871 -101.946,165.657 -121.437,134.479C-121.437,134.479 -22.21,21.607 -177.297,-50.54L-159.24,74.338C-159.24,74.338 -207.049,42.389 -217.366,-27.008C-217.366,-27.008 -333.789,57.486 -165.982,138.466C-165.982,138.466 -150.762,195.653 -4.633,241.281L-4.526,240.846C-3.055,241.118 -1.549,241.281 0,241.281C13.808,241.281 25.003,229.969 25.003,216.01C25.003,202.054 13.808,190.741 0,190.741" style="fill:rgb(247,76,0);fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="1434.979px" height="947px" viewBox="0 0 1434.979 947" enable-background="new 0 0 1434.979 947" xml:space="preserve">
<!--<rect fill="#FFFFFF" width="1434.979" height="947"/>-->
<path fill="#8F1F1D" d="M712.827,368.579c-131.633,0-251.228,15.825-339.77,41.615v220.298
c88.542,25.79,208.137,41.614,339.77,41.614c150.657,0,285.535-20.729,376.134-53.402V421.986
C998.361,389.311,863.483,368.579,712.827,368.579"/>
<path fill="#8F1F1D" d="M1094.847,567.972c-3.856-10.663-4.629-24.154-1.36-37.162c5.85-23.289,22.421-36.198,37.013-28.833
c3.618,1.827,6.773,4.73,9.387,8.418c0.239-0.001,0.479,0,0.715,0.016c0,0,44.552,53.106,3.313,116.003
c-0.896,3.569-76.534,91.718-94.043,94.524C1038.411,722.773,1064.762,630.939,1094.847,567.972"/>
<path fill="#8F1F1D" d="M363.903,557.551c5.3-9.631,7.158-22.788,4.217-36.426c-5.266-24.416-23.91-41.109-41.642-37.285
c-4.398,0.948-8.325,3.072-11.666,6.099c-0.282-0.059-0.564-0.113-0.845-0.153c0,0-56.292,41.952-12.057,113.924
c0.805,3.741,83.851,108.838,104.311,115.764C419.612,724.004,394.974,626.947,363.903,557.551"/>
<path fill="#E23A26" d="M284.891,563.596l0.007,0.015C285.083,563.755,285.255,563.887,284.891,563.596"/>
<path fill="#E33B26" d="M1180.543,488.433c-0.88-3.064-1.756-6.126-2.662-9.162l30.683-44.451c3.13-4.522,3.771-10.398,1.73-15.555
c-2.04-5.13-6.49-8.81-11.76-9.71l-51.887-8.805c-2.008-4.102-4.115-8.142-6.229-12.15l21.797-49.903
c2.243-5.087,1.769-10.995-1.203-15.608c-2.961-4.636-7.99-7.344-13.349-7.133l-52.656,1.913c-2.727-3.55-5.496-7.068-8.322-10.521
l12.102-53.49c1.225-5.433-0.322-11.118-4.104-15.064c-3.762-3.932-9.229-5.559-14.426-4.283l-51.289,12.608
c-3.321-2.935-6.699-5.833-10.114-8.673l1.849-54.914c0.197-5.559-2.394-10.842-6.845-13.925
c-4.445-3.104-10.093-3.573-14.955-1.266l-47.848,22.747c-3.854-2.21-7.728-4.4-11.644-6.517l-8.455-54.115
c-0.857-5.483-4.386-10.139-9.326-12.266c-4.923-2.137-10.568-1.447-14.891,1.808l-42.659,32.007
c-4.2-1.395-8.419-2.732-12.692-4.011l-18.386-51.316c-1.87-5.229-6.182-9.071-11.438-10.151c-5.238-1.072-10.63,0.742-14.263,4.802
l-35.907,40.171c-4.342-0.5-8.685-0.956-13.043-1.331l-27.723-46.713c-2.811-4.732-7.771-7.612-13.116-7.612
c-5.334,0-10.304,2.88-13.09,7.612l-27.733,46.713c-4.358,0.375-8.722,0.831-13.056,1.331l-35.91-40.171
c-3.636-4.06-9.047-5.874-14.268-4.802c-5.255,1.092-9.573,4.922-11.433,10.151l-18.402,51.316
c-4.26,1.279-8.481,2.627-12.691,4.011l-42.644-32.007c-4.336-3.266-9.98-3.955-14.916-1.808c-4.919,2.127-8.461,6.783-9.313,12.266
l-8.461,54.115c-3.914,2.117-7.789,4.294-11.653,6.517L436.1,168.34c-4.858-2.316-10.529-1.838-14.954,1.266
c-4.445,3.083-7.042,8.366-6.84,13.925l1.835,54.914c-3.405,2.84-6.774,5.738-10.112,8.673L354.75,234.51
c-5.211-1.265-10.67,0.351-14.441,4.283c-3.795,3.946-5.332,9.631-4.113,15.064l12.079,53.49c-2.802,3.467-5.575,6.971-8.293,10.521
l-52.655-1.913c-5.314-0.157-10.386,2.497-13.356,7.133c-2.974,4.613-3.425,10.521-1.211,15.608l21.814,49.903
c-2.119,4.008-4.224,8.048-6.249,12.15l-51.882,8.805c-5.271,0.888-9.715,4.566-11.765,9.71c-2.037,5.157-1.375,11.033,1.735,15.555
l30.69,44.451c-0.236,0.784-0.455,1.576-0.69,2.364l-16.863,17.911l45.341,64.05c0,0,435.152,200.731,838.797,3.396
C1169.796,558.719,1180.543,488.433,1180.543,488.433"/>
<path d="M795.716,446.557c0,0,48.162-52.734,96.324,0c0,0,37.844,70.318,0,105.473c0,0-61.922,49.223-96.324,0
C795.716,552.029,754.434,513.354,795.716,446.557"/>
<path fill="#FFFFFF" d="M855.154,481.097c0,19.782-11.66,35.82-26.041,35.82c-14.379,0-26.04-16.038-26.04-35.82
c0-19.782,11.661-35.821,26.04-35.821C843.494,445.275,855.154,461.315,855.154,481.097"/>
<path d="M578.401,430.129c0,0,84.436-37.385,107.481,46.059c0,0,24.141,97.261-69.339,102.751
C616.543,578.939,497.34,555.98,578.401,430.129"/>
<rect x="187.424" y="75.529" fill="none" width="1060" height="782"/>
<path fill="#FFFFFF" d="M627.514,481.096c0,20.579-12.13,37.27-27.095,37.27c-14.959,0-27.092-16.69-27.092-37.27
c0-20.583,12.133-37.27,27.092-37.27C615.384,443.826,627.514,460.513,627.514,481.096"/>
<path fill="#E33B26" d="M299.026,574.745c10.967-12.463,37.611-27.557,35.57-46.282c-3.653-33.526-31.456-57.999-62.099-54.658
c-7.599,0.827-14.658,3.292-20.923,7.035c-0.463-0.106-0.925-0.211-1.388-0.294c0,0-103.632,50.873-44.564,152.657
c0.557,5.137,117.847,155.668,150.787,167.131C377.968,807.836,336.498,671.694,299.026,574.745"/>
<path fill="#E33B26" d="M1140.973,570.202c-12.692-10.7-46.162-20.418-46.92-39.238c-1.355-33.697,22.512-62.021,53.312-63.26
c7.638-0.308,14.983,1.083,21.734,3.857c0.442-0.174,0.884-0.347,1.329-0.497c0,0,110.025,34.951,66.695,144.366
c0.21,5.163-93.468,171.416-124.345,187.635C1092.57,813.681,1118.285,671.635,1140.973,570.202"/>
<rect x="187.484" y="75.843" fill="none" width="1059.75" height="781.686"/>
<rect x="187.424" y="75.529" fill="none" width="1060" height="782"/>
<g>
<path fill="#E33B26" d="M283.144,565.511c0,0-137.214-4.942-161.62-140.761l57.596-25.427c0,0-13.912,96.957,106.615,110.022
L283.144,565.511"/>
<path fill="#E33B26" d="M127.552,333.083c0,0-24.965-49.774-65.807-113.261C18.721,241.035-2.671,299.05,13.482,357.484
c17.846,64.558,74.749,105.16,127.097,90.69s80.318-78.535,62.471-143.092c-7.909-28.618-23.501-52.519-42.963-69.011
C150.611,287.113,127.552,333.083,127.552,333.083"/>
</g>
<rect x="187.484" y="75.843" fill="none" width="1059.75" height="781.686"/>
<g>
<path fill="#E33B26" d="M1148.012,565.511c0,0,137.214-4.942,161.62-140.761l-57.596-25.428c0,0,13.912,96.957-106.615,110.022
L1148.012,565.511"/>
<path fill="#E33B26" d="M1303.604,333.083c0,0,24.966-49.774,65.808-113.261c43.023,21.212,64.416,79.228,48.262,137.662
c-17.846,64.558-74.748,105.16-127.096,90.689c-52.348-14.47-80.318-78.534-62.472-143.091
c7.909-28.618,23.501-52.519,42.964-69.011C1280.544,287.113,1303.604,333.083,1303.604,333.083"/>
</g>
<path d="M807.895,626.942c-7.131-58.735-72.193-61.431-72.193-61.431c-50.936,11.227-59.183,47.369-57.392,75.104L807.895,626.942z"
/>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,61 @@
<svg version="1.1" height="106" width="106" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="logo" transform="translate(53, 53)">
<path id="r" transform="translate(0.5, 0.5)" fill="white" stroke="white" stroke-width="1" stroke-linejoin="round" d="
M -9,-15 H 4 C 12,-15 12,-7 4,-7 H -9 Z
M -40,22 H 0 V 11 H -9 V 3 H 1 C 12,3 6,22 15,22 H 40
V 3 H 34 V 5 C 34,13 25,12 24,7 C 23,2 19,-2 18,-2 C 33,-10 24,-26 12,-26 H -35
V -15 H -25 V 11 H -40 Z" />
<g id="gear" mask="url(#holes)">
<circle r="43" fill="none" stroke="white" stroke-width="9" />
<g id="cogs">
<polygon id="cog" stroke="white" stroke-width="3" stroke-linejoin="round" points="46,3 51,0 46,-3" />
<use xlink:href="#cog" transform="rotate(11.25)" />
<use xlink:href="#cog" transform="rotate(22.50)" />
<use xlink:href="#cog" transform="rotate(33.75)" />
<use xlink:href="#cog" transform="rotate(45.00)" />
<use xlink:href="#cog" transform="rotate(56.25)" />
<use xlink:href="#cog" transform="rotate(67.50)" />
<use xlink:href="#cog" transform="rotate(78.75)" />
<use xlink:href="#cog" transform="rotate(90.00)" />
<use xlink:href="#cog" transform="rotate(101.25)" />
<use xlink:href="#cog" transform="rotate(112.50)" />
<use xlink:href="#cog" transform="rotate(123.75)" />
<use xlink:href="#cog" transform="rotate(135.00)" />
<use xlink:href="#cog" transform="rotate(146.25)" />
<use xlink:href="#cog" transform="rotate(157.50)" />
<use xlink:href="#cog" transform="rotate(168.75)" />
<use xlink:href="#cog" transform="rotate(180.00)" />
<use xlink:href="#cog" transform="rotate(191.25)" />
<use xlink:href="#cog" transform="rotate(202.50)" />
<use xlink:href="#cog" transform="rotate(213.75)" />
<use xlink:href="#cog" transform="rotate(225.00)" />
<use xlink:href="#cog" transform="rotate(236.25)" />
<use xlink:href="#cog" transform="rotate(247.50)" />
<use xlink:href="#cog" transform="rotate(258.75)" />
<use xlink:href="#cog" transform="rotate(270.00)" />
<use xlink:href="#cog" transform="rotate(281.25)" />
<use xlink:href="#cog" transform="rotate(292.50)" />
<use xlink:href="#cog" transform="rotate(303.75)" />
<use xlink:href="#cog" transform="rotate(315.00)" />
<use xlink:href="#cog" transform="rotate(326.25)" />
<use xlink:href="#cog" transform="rotate(337.50)" />
<use xlink:href="#cog" transform="rotate(348.75)" />
</g>
<g id="mounts">
<polygon id="mount" stroke="white" stroke-width="6" stroke-linejoin="round" points="-7,-42 0,-35 7,-42" />
<use xlink:href="#mount" transform="rotate(72)" />
<use xlink:href="#mount" transform="rotate(144)" />
<use xlink:href="#mount" transform="rotate(216)" />
<use xlink:href="#mount" transform="rotate(288)" />
</g>
</g>
<mask id="holes">
<rect x="-60" y="-60" width="120" height="120" fill="white"/>
<circle id="hole" cy="-40" r="3" />
<use xlink:href="#hole" transform="rotate(72)" />
<use xlink:href="#hole" transform="rotate(144)" />
<use xlink:href="#hole" transform="rotate(216)" />
<use xlink:href="#hole" transform="rotate(288)" />
</mask>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<div class="flex flex-col mx-auto text-center">
<h1>DON'T PANIC!</h1>
<h2>404: Page not found!</h2>
<img class="mx-auto max-h-[50vh]"
src="{{ get_url(path='images/panic.svg') | safe }}"
alt="">
<a class="text-2xl font-bold" href="{{ get_url(path='@/_index.md') }}">Back to homepage</a>
</div>
{% endblock %}

View File

@ -0,0 +1,2 @@
<a class="text-white no-underline transition-none hover:underline"
href="#{{ id }}"></a>

View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{%- set timestamp = now(timestamp=true) -%}
{%- if page.title -%}
{% set_global title = page.title %}
{%- elif section.title -%}
{% set_global title = section.title %}
{%- else -%}
{% set_global title = config.title %}
{%- endif -%}
{%- if page.description -%}
{% set_global description = page.description %}
{%- elif section.description -%}
{% set_global description = section.description %}
{%- else -%}
{% set_global description = config.description %}
{%- endif -%}
{%- if page.permalink -%}
{% set_global permalink = page.permalink %}
{%- elif section.permalink -%}
{% set_global permalink = section.permalink %}
{%- endif %}
<title>{%- block title -%}{{- title -}}{%- endblock -%}</title>
<meta name="description"
content="{%- block description -%}{{- description -}}{%- endblock -%}">
<link rel="icon"
type="image/x-icon"
href="{{ get_url(path=config.extra.logo_path) | safe }}?v={{ timestamp }}">
<link href="{{ get_url(path='main.css') | safe }}?v={{ timestamp }}"
rel="stylesheet">
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ description }}">
<meta property="og:image"
content="{{ get_url(path=config.extra.logo_path) | safe }}?v={{ timestamp }}">
{% if permalink %}<meta property="og:url" content="{{ permalink | safe }}">{% endif %}
</head>
<body class="flex flex-col p-2 mx-auto min-h-screen text-lg text-white break-words lg:px-5 2xl:container bg-[#2A3439]">
<header class="flex flex-col gap-x-4 items-center py-2 px-4 mb-1 rounded-sm sm:flex-row sm:rounded-full bg-black/30">
<a class="transition duration-500 hover:scale-110"
href="{{ get_url(path='@/_index.md') | safe }}"
aria-hidden="true">
<img class="w-12 h-12"
src="{{ get_url(path=config.extra.logo_path) | safe }}"
alt="">
</a>
<nav class="flex flex-col gap-x-6 items-center font-bold sm:flex-row">
{% for menu_item in config.extra.menu_items %}
{%- if menu_item.url is starting_with("@") -%}
{% set_global menu_item_url = get_url(path=menu_item.url) %}
{%- else -%}
{% set_global menu_item_url = menu_item.url %}
{%- endif %}
<a class="p-1 no-underline" href="{{ menu_item_url | safe }}">{{ menu_item.name }}</a>
{% endfor %}
</nav>
</header>
<main class="leading-relaxed">
{% block content %}{% endblock %}
</main>
<footer class="pt-2 pb-1 mt-auto text-sm text-center">
<div class="inline-flex gap-x-1.5 items-center mx-auto mt-2">
<img class="w-8 h-8"
src="{{ get_url(path='images/rust_logo.svg') | safe }}"
alt="">
<div class="italic">Rustlings is an official Rust project</div>
</div>
<nav class="flex flex-col gap-y-3 justify-around py-3 mt-3 rounded-sm sm:flex-row sm:rounded-full bg-black/30">
{% for footer_item in config.extra.footer_items %}
<a class="no-underline" href="{{ footer_item.url | safe }}">{{ footer_item.name }}</a>
{% endfor %}
</nav>
</footer>
</body>
</html>

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
<div class="m-3">
<h1>Rustlings</h1>
{{ section.content | safe }}
</div>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>{{ page.title }}</h1>
<div class="py-0.5 px-4 my-3 rounded-xl border-double border-s-4">
<nav>
<ul class="ml-0 list-none">
{% for parent in page.toc %}
{% if parent.level == 2 %}
<li>
{#- -#}
<a href="{{ parent.permalink | safe }}">{{ parent.title }}</a>
{#- -#}
{% if parent.children %}
<ul class="my-0 ml-5 list-none">
{% for child in parent.children %}
{% if child.level == 3 %}
<li>
{#- -#}
<a class="text-base" href="{{ child.permalink | safe }}">{{ child.title }}</a>
{#- -#}
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{#- -#}
</li>
{% endif %}
{% endfor %}
</ul>
</nav>
</div>
{{ page.content | safe }}
</article>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More