mirror of
https://github.com/notohh/rustlings.git
synced 2024-12-17 22:58:08 -05:00
Compare commits
189 commits
ce3dcc9856
...
dd0634c483
Author | SHA1 | Date | |
---|---|---|---|
|
dd0634c483 | ||
|
fc0cd8f0f8 | ||
|
d5cae8ff59 | ||
|
38016cb2d6 | ||
|
e6cb104294 | ||
|
410eb69d25 | ||
|
243cf5f261 | ||
|
eff2ce8a23 | ||
|
fd33c29b26 | ||
|
f49164e69b | ||
|
9bc7bbe4b4 | ||
|
46ad25f925 | ||
|
2a725fb137 | ||
|
449858655d | ||
|
e8c2a79516 | ||
|
ea85c1b46e | ||
|
6bec6f92c4 | ||
|
930a0ea73b | ||
|
7e2f56f41a | ||
|
e90f5f03f3 | ||
|
0e090ae112 | ||
|
99496706c5 | ||
|
f146553dea | ||
|
0432e07864 | ||
|
f33ba139b4 | ||
|
990a722852 | ||
|
a675cb5754 | ||
|
baeeff389c | ||
|
932bc25d88 | ||
|
bdc6dad8de | ||
|
ea73af9ba3 | ||
|
fc5fc0920f | ||
|
9705c161b4 | ||
|
8cac21511c | ||
|
396ee4d618 | ||
|
326169a7fa | ||
|
685e069c58 | ||
|
84a42a2b24 | ||
|
ac6e1b7ce5 | ||
|
f516da4138 | ||
|
e852e60416 | ||
|
bf7d171915 | ||
|
d3f819f86f | ||
|
aa83fd6bc4 | ||
|
e2f7734f37 | ||
|
5c17abd1bf | ||
|
c52867eb8b | ||
|
26fd97a209 | ||
|
f0a2cdeb18 | ||
|
0c79f2ea3e | ||
|
0e9eb9e87e | ||
|
0d258b9e96 | ||
|
d4fa61e435 | ||
|
554301b8e9 | ||
|
e3ec0abca4 | ||
|
a55e848359 | ||
|
2653c3c4d4 | ||
|
4e4b65711a | ||
|
89c40ba256 | ||
|
e56ae6d651 | ||
|
64b2f18d92 | ||
|
2894f3c45c | ||
|
1bae2dcb00 | ||
|
b540c6df25 | ||
|
8b476e678a | ||
|
47f8a0cbe5 | ||
|
9459eef032 | ||
|
5aaa8924a6 | ||
|
4ffce1c297 | ||
|
0513660b05 | ||
|
3947c4de28 | ||
|
664228ef8b | ||
|
234a61a3ee | ||
|
83d1275d72 | ||
|
45abd7d59e | ||
|
88e10a9e54 | ||
|
1f624d4c2a | ||
|
9a25309c1c | ||
|
2b7caf6fcb | ||
|
938500fd2f | ||
|
2d26358602 | ||
|
9faa5d3aa4 | ||
|
bcc2a136c8 | ||
|
dcad002057 | ||
|
51b8d2ab25 | ||
|
aa3eda70e5 | ||
|
2d0860fe1b | ||
|
17877366b7 | ||
|
5eb3dee59c | ||
|
247bd19f93 | ||
|
e5ed115288 | ||
|
03baa471d9 | ||
|
da8b3d143a | ||
|
20616ff954 | ||
|
f463cf8662 | ||
|
e9879eac91 | ||
|
47148e78a3 | ||
|
fea917c8f2 | ||
|
948e16e3c7 | ||
|
1e7fc46406 | ||
|
71494264ca | ||
|
3125561474 | ||
|
abf1228a0a | ||
|
547a9d947b | ||
|
f696d98270 | ||
|
44ab7f995d | ||
|
92a1214dcd | ||
|
388f8da97f | ||
|
e96623588c | ||
|
e1e316b931 | ||
|
c4fd29541b | ||
|
a8b13f5a82 | ||
|
86fc573d7a | ||
|
f82e47f2af | ||
|
75a38fa38b | ||
|
ac62a3713c | ||
|
ea52c99560 | ||
|
7d4100ed8a | ||
|
c8d1d9c51f | ||
|
ab2eb3442e | ||
|
dbbeb7d4ed | ||
|
bfa00ffbdc | ||
|
10eb1a3aee | ||
|
fd2bf9f6f6 | ||
|
fc1f9f0124 | ||
|
789492d1a9 | ||
|
afc320bed4 | ||
|
cba4a6f9c8 | ||
|
5556d42b46 | ||
|
7d2bc1c7a4 | ||
|
c209c874a9 | ||
|
dd52e9cd72 | ||
|
0f71a150ff | ||
|
74388d4bf4 | ||
|
e811dd15b5 | ||
|
f22700a4ec | ||
|
ee25a7d458 | ||
|
594e212b8a | ||
|
5c355468c1 | ||
|
d1571d18f9 | ||
|
cb86b44dea | ||
|
833e6e0c92 | ||
|
159273e532 | ||
|
631f2db1a3 | ||
|
a1f0eaab54 | ||
|
b1898f6d8b | ||
|
d29e9e7e07 | ||
|
360605e284 | ||
|
64772544fa | ||
|
5f4875e2ba | ||
|
fd2a8c01cb | ||
|
b6129ad081 | ||
|
28d0b0a21e | ||
|
b779c43126 | ||
|
4e12725616 | ||
|
570bc9f32d | ||
|
47976caa69 | ||
|
f1abd8577c | ||
|
423b50b068 | ||
|
bedf0789f2 | ||
|
a2d1cb3b22 | ||
|
e7ba88f905 | ||
|
50f6e5232e | ||
|
8854f0a5ed | ||
|
13cc3acdfd | ||
|
5b7368c46d | ||
|
27999f2d26 | ||
|
e74f2a4274 | ||
|
d141a73493 | ||
|
631f44331e | ||
|
b01fddef8b | ||
|
78a8553f1c | ||
|
b70c1abd7c | ||
|
71f31d74bc | ||
|
72e557b3a9 | ||
|
3eaccbb61a | ||
|
b678bd8ed2 | ||
|
2baa140615 | ||
|
e760f07767 | ||
|
ca5d5f0a49 | ||
|
69b4fd49fc | ||
|
36f315c344 | ||
|
8016f5ca2d | ||
|
8ef2ff1257 | ||
|
6ce31defb6 | ||
|
0b3ad9141b | ||
|
c903db5c53 | ||
|
8a038b946c | ||
|
ed9740b72c |
65 changed files with 2476 additions and 1656 deletions
|
@ -1,7 +1,7 @@
|
||||||
|
[default.extend-words]
|
||||||
|
"earch" = "earch" # Because of <s>earch in the list footer
|
||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = [
|
extend-exclude = [
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
]
|
]
|
||||||
|
|
||||||
[default.extend-words]
|
|
||||||
"ratatui" = "ratatui"
|
|
||||||
|
|
76
CHANGELOG.md
76
CHANGELOG.md
|
@ -1,3 +1,75 @@
|
||||||
|
<a name="6.4.0"></a>
|
||||||
|
|
||||||
|
## 6.4.0 (2024-11-11)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The list of exercises is now searchable by pressing `s` or `/` 🔍️ (thanks to [@frroossst](https://github.com/frroossst))
|
||||||
|
- New option `c` in the prompt to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
|
||||||
|
- New command `check-all` to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
|
||||||
|
- Addictive animation for showing the progress of checking all exercises. A nice showcase of parallelism in Rust ✨
|
||||||
|
- New option `x` in the prompt to reset the file of the current exercise 🔄
|
||||||
|
- Allow `dead_code` for all exercises and solutions ⚰️ (thanks to [@huss4in](https://github.com/huss4in))
|
||||||
|
- Pause input while running an exercise to avoid unexpected prompt interactions ⏸️
|
||||||
|
- Limit the maximum number of exercises to 999. Any third-party exercises willing to reach that limit? 🔝
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `enums3`: Remove redundant enum definition task (thanks to [@senekor](https://github.com/senekor))
|
||||||
|
- `if2`: Make the exercise less confusing by avoiding "fizz", "fuzz", "foo", "bar" and "baz" (thanks to [@senekor](https://github.com/senekor))
|
||||||
|
- `hashmap3`: Use the method `Entry::or_default`.
|
||||||
|
- Update the state of all exercises when checking all of them (thanks to [@Nahor](https://github.com/Nahor))
|
||||||
|
- The main prompt doesn't need a confirmation with ENTER on Unix-like systems anymore.
|
||||||
|
- No more jumping back to a previous exercise when its file is changed. Use the list to jump between exercises.
|
||||||
|
- Dump the solution file after an exercise is done even if the solution's directory doesn't exist.
|
||||||
|
- Rework the footer in the list.
|
||||||
|
- Optimize the file watcher.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix bad contrast in the list on terminals with a light theme.
|
||||||
|
|
||||||
|
<a name="6.3.0"></a>
|
||||||
|
|
||||||
|
## 6.3.0 (2024-08-29)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add the following exercise lints:
|
||||||
|
- `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings.
|
||||||
|
- `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
|
||||||
|
- `forbid(todo)`: You forgot a `todo!()`.
|
||||||
|
- `forbid(empty_loop)`: This can only happen by mistake in Rustlings.
|
||||||
|
- `deny(infinite_loop)`: No infinite loops are needed in Rustlings.
|
||||||
|
- `deny(mem_forget)`: You shouldn't leak memory while still learning Rust.
|
||||||
|
- Show a link to every exercise file in the list.
|
||||||
|
- Add scroll padding in the list.
|
||||||
|
- Break the help footer of the list into two lines when the terminal width isn't big enough.
|
||||||
|
- Enable scrolling with the mouse in the list.
|
||||||
|
- `dev check`: Show the progress of checks.
|
||||||
|
- `dev check`: Check that the length of all exercise names is lower than 32.
|
||||||
|
- `dev check`: Check if exercise contains no tests and isn't marked with `test = false`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The compilation time when installing Rustlings is reduced.
|
||||||
|
- Pressing `c` in the list for "continue on" now quits the list after setting the selected exercise as the current one.
|
||||||
|
- Better highlighting of the solution file after an exercise is done.
|
||||||
|
- Don't show the output of successful tests anymore. Instead, show the pretty output for tests.
|
||||||
|
- Be explicit about `q` only quitting the list and not the whole program in the list.
|
||||||
|
- Be explicit about `r` only resetting one exercise (the selected one) in the list.
|
||||||
|
- Ignore the standard output of `git init`.
|
||||||
|
- `threads3`: Remove the queue length and improve tests.
|
||||||
|
- `errors4`: Use match instead of a comparison chain in the solution.
|
||||||
|
- `functions3`: Only take `u8` to avoid using a too high number of iterations by mistake.
|
||||||
|
- `dev check`: Always check with strict Clippy (warnings to errors) when checking the solutions.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix the error on some systems about too many open files during the final check of all exercises.
|
||||||
|
- Fix the list when the terminal height is too low.
|
||||||
|
- Restore the terminal after an error in the list.
|
||||||
|
|
||||||
<a name="6.2.0"></a>
|
<a name="6.2.0"></a>
|
||||||
|
|
||||||
## 6.2.0 (2024-08-09)
|
## 6.2.0 (2024-08-09)
|
||||||
|
@ -72,7 +144,7 @@ You can read about the motivations of this change in [this issue](https://github
|
||||||
|
|
||||||
### List mode
|
### List mode
|
||||||
|
|
||||||
A list mode was added using [Ratatui](https://ratatui.rs).
|
A new list mode was added!
|
||||||
You can enter it by entering `l` in the watch mode.
|
You can enter it by entering `l` in the watch mode.
|
||||||
It offers the following features:
|
It offers the following features:
|
||||||
|
|
||||||
|
@ -773,7 +845,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
|
|
||||||
- Update deps to version compatable with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
|
- Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
|
||||||
- **docs:**
|
- **docs:**
|
||||||
- Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f))
|
- Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f))
|
||||||
- Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9))
|
- Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9))
|
||||||
|
|
498
Cargo.lock
generated
498
Cargo.lock
generated
|
@ -2,29 +2,11 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.8.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
"zerocopy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "allocator-api2"
|
|
||||||
version = "0.2.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.15"
|
version = "0.6.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"anstyle-parse",
|
"anstyle-parse",
|
||||||
|
@ -37,49 +19,49 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.8"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-parse"
|
name = "anstyle-parse"
|
||||||
version = "0.2.5"
|
version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"utf8parse",
|
"utf8parse",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-query"
|
name = "anstyle-query"
|
||||||
version = "1.1.1"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-wincon"
|
name = "anstyle-wincon"
|
||||||
version = "3.0.4"
|
version = "3.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.86"
|
version = "1.0.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
|
@ -93,21 +75,6 @@ version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cassowary"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "castaway"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -116,9 +83,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.14"
|
version = "4.5.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c937d4061031a6d0c8da4b9a4f98a172fc2976dfb1c19213a9cf7d0d3c837e36"
|
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
@ -126,9 +93,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.14"
|
version = "4.5.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85379ba512b21a328adf887e85f7742d12e96eb31f3ef077df4ffc26b506ffed"
|
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
@ -138,9 +105,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.13"
|
version = "4.5.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -156,38 +123,9 @@ checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "compact_str"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
|
|
||||||
dependencies = [
|
|
||||||
"castaway",
|
|
||||||
"cfg-if",
|
|
||||||
"itoa",
|
|
||||||
"rustversion",
|
|
||||||
"ryu",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-channel"
|
|
||||||
version = "0.5.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-utils"
|
|
||||||
version = "0.8.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
|
@ -197,7 +135,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"mio 1.0.1",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustix",
|
"rustix",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
|
@ -214,12 +152,6 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -238,20 +170,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filetime"
|
name = "filetime"
|
||||||
version = "0.2.23"
|
version = "0.2.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
|
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall 0.4.1",
|
"libredox",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -265,13 +197,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
|
||||||
dependencies = [
|
|
||||||
"ahash",
|
|
||||||
"allocator-api2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
|
@ -287,9 +215,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.3.0"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
|
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
@ -297,9 +225,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inotify"
|
name = "inotify"
|
||||||
version = "0.9.6"
|
version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"inotify-sys",
|
"inotify-sys",
|
||||||
|
@ -316,13 +244,12 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instability"
|
name = "instant"
|
||||||
version = "0.3.2"
|
version = "0.1.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c"
|
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"cfg-if",
|
||||||
"syn",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -331,15 +258,6 @@ version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
@ -368,9 +286,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.155"
|
version = "0.2.162"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
|
@ -394,15 +323,6 @@ version = "0.4.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru"
|
|
||||||
version = "0.12.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.4"
|
version = "2.7.4"
|
||||||
|
@ -411,21 +331,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.11"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"wasi",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mio"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -436,38 +344,37 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "6.1.1"
|
version = "7.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"crossbeam-channel",
|
|
||||||
"filetime",
|
"filetime",
|
||||||
"fsevent-sys",
|
"fsevent-sys",
|
||||||
"inotify",
|
"inotify",
|
||||||
"kqueue",
|
"kqueue",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"mio 0.8.11",
|
"mio",
|
||||||
|
"notify-types",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify-debouncer-mini"
|
name = "notify-types"
|
||||||
version = "0.4.1"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
|
checksum = "7393c226621f817964ffb3dc5704f9509e107a8b024b489cc2c1b217378785df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"instant",
|
||||||
"notify",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.19.0"
|
version = "1.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_pipe"
|
name = "os_pipe"
|
||||||
|
@ -497,79 +404,43 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall 0.5.3",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paste"
|
|
||||||
version = "1.0.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.86"
|
version = "1.0.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.36"
|
version = "1.0.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ratatui"
|
|
||||||
version = "0.28.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.6.0",
|
|
||||||
"cassowary",
|
|
||||||
"compact_str",
|
|
||||||
"crossterm",
|
|
||||||
"instability",
|
|
||||||
"itertools",
|
|
||||||
"lru",
|
|
||||||
"paste",
|
|
||||||
"strum",
|
|
||||||
"strum_macros",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-truncate",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.5.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.34"
|
version = "0.38.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"errno",
|
"errno",
|
||||||
|
@ -580,14 +451,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustlings"
|
name = "rustlings"
|
||||||
version = "6.2.0"
|
version = "6.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"notify-debouncer-mini",
|
"crossterm",
|
||||||
|
"notify",
|
||||||
"os_pipe",
|
"os_pipe",
|
||||||
"ratatui",
|
"rustix",
|
||||||
"rustlings-macros",
|
"rustlings-macros",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -597,19 +468,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustlings-macros"
|
name = "rustlings-macros"
|
||||||
version = "6.2.0"
|
version = "6.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustversion"
|
|
||||||
version = "1.0.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
@ -633,18 +498,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.205"
|
version = "1.0.214"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
|
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.205"
|
version = "1.0.214"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
|
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -653,9 +518,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.122"
|
version = "1.0.132"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
|
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -665,9 +530,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.7"
|
version = "0.6.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -689,7 +554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio 1.0.1",
|
"mio",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -708,45 +573,17 @@ version = "1.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum"
|
|
||||||
version = "0.26.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
|
||||||
dependencies = [
|
|
||||||
"strum_macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum_macros"
|
|
||||||
version = "0.26.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
|
||||||
dependencies = [
|
|
||||||
"heck",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rustversion",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.72"
|
version = "2.0.87"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
|
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -755,9 +592,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.12.0"
|
version = "3.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
|
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
@ -777,9 +614,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.20"
|
version = "0.22.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -790,32 +627,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-segmentation"
|
|
||||||
version = "1.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-truncate"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
|
||||||
dependencies = [
|
|
||||||
"itertools",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
|
@ -823,12 +637,6 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
@ -876,22 +684,13 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.48.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -900,22 +699,7 @@ version = "0.59.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.48.5",
|
|
||||||
"windows_aarch64_msvc 0.48.5",
|
|
||||||
"windows_i686_gnu 0.48.5",
|
|
||||||
"windows_i686_msvc 0.48.5",
|
|
||||||
"windows_x86_64_gnu 0.48.5",
|
|
||||||
"windows_x86_64_gnullvm 0.48.5",
|
|
||||||
"windows_x86_64_msvc 0.48.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -924,46 +708,28 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -976,48 +742,24 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1026,29 +768,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.18"
|
version = "0.6.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerocopy"
|
|
||||||
version = "0.7.35"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
|
||||||
dependencies = [
|
|
||||||
"zerocopy-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerocopy-derive"
|
|
||||||
version = "0.7.35"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
41
Cargo.toml
41
Cargo.toml
|
@ -6,7 +6,7 @@ exclude = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "6.2.0"
|
version = "6.4.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
|
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
|
||||||
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
|
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
|
||||||
|
@ -19,8 +19,8 @@ edition = "2021" # On Update: Update the edition of the `rustfmt` command that c
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1.0.205", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] }
|
toml_edit = { version = "0.22.22", default-features = false, features = ["parse", "serde"] }
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "rustlings"
|
name = "rustlings"
|
||||||
|
@ -46,19 +46,21 @@ include = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ahash = { version = "0.8.11", default-features = false }
|
anyhow = "1.0.93"
|
||||||
anyhow = "1.0.86"
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
clap = { version = "4.5.14", features = ["derive"] }
|
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
|
||||||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
notify = "7.0.0"
|
||||||
os_pipe = "1.2.1"
|
os_pipe = "1.2.1"
|
||||||
ratatui = { version = "0.28.0", default-features = false, features = ["crossterm"] }
|
rustlings-macros = { path = "rustlings-macros", version = "=6.4.0" }
|
||||||
rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" }
|
serde_json = "1.0.132"
|
||||||
serde_json = "1.0.122"
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml_edit.workspace = true
|
toml_edit.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
|
rustix = { version = "0.38.38", default-features = false, features = ["std", "stdio", "termios"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.12.0"
|
tempfile = "3.14.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
@ -68,7 +70,22 @@ panic = "abort"
|
||||||
|
|
||||||
[package.metadata.release]
|
[package.metadata.release]
|
||||||
pre-release-hook = ["./release-hook.sh"]
|
pre-release-hook = ["./release-hook.sh"]
|
||||||
|
pre-release-commit-message = "Release 🎉"
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
unstable_features = "forbid"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
empty_loop = "forbid"
|
||||||
|
disallowed-types = "deny"
|
||||||
|
disallowed-methods = "deny"
|
||||||
|
infinite_loop = "deny"
|
||||||
|
mem_forget = "deny"
|
||||||
|
dbg_macro = "warn"
|
||||||
|
todo = "warn"
|
||||||
# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
|
# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
|
||||||
[lints.clippy]
|
|
||||||
needless_option_as_deref = "allow"
|
needless_option_as_deref = "allow"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
23
README.md
23
README.md
|
@ -124,21 +124,30 @@ The list allows you to…
|
||||||
|
|
||||||
- See the status of all exercises (done or pending)
|
- See the status of all exercises (done or pending)
|
||||||
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
|
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
|
||||||
- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards)
|
- `r`: Reset status and file of the selected exercise (you need to _reload/reopen_ its file in your editor afterwards)
|
||||||
|
|
||||||
See the footer of the list for all possible keys.
|
See the footer of the list for all possible keys.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you need any help while doing the exercises and the builtin-hints aren't helpful, feel free to ask in the [_Q&A_ category of the discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question wasn't asked yet 💡
|
||||||
|
|
||||||
|
## Third-Party Exercises
|
||||||
|
|
||||||
|
Third-party exercises are a set of exercises maintained by the community.
|
||||||
|
You can use the same `rustlings` program that you installed with `cargo install rustlings` to run them:
|
||||||
|
|
||||||
|
- [日本語版 Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercises.
|
||||||
|
|
||||||
|
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
|
||||||
|
Or do you want to translate the original Rustlings exercises?
|
||||||
|
Then follow the the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
|
||||||
|
|
||||||
## Continuing On
|
## Continuing On
|
||||||
|
|
||||||
Once you've completed Rustlings, put your new knowledge to good use!
|
Once you've completed Rustlings, put your new knowledge to good use!
|
||||||
Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.
|
Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.
|
||||||
|
|
||||||
## Third-Party Exercises
|
|
||||||
|
|
||||||
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
|
|
||||||
Or do you want to translate the original Rustlings exercises?
|
|
||||||
Then follow the link to the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
|
|
||||||
|
|
||||||
## Uninstalling Rustlings
|
## Uninstalling Rustlings
|
||||||
|
|
||||||
If you want to remove Rustlings from your system, run the following command:
|
If you want to remove Rustlings from your system, run the following command:
|
||||||
|
|
15
clippy.toml
Normal file
15
clippy.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
disallowed-types = [
|
||||||
|
# Inefficient. Use `.queue(…)` instead.
|
||||||
|
"crossterm::style::Stylize",
|
||||||
|
"crossterm::style::styled_content::StyledContent",
|
||||||
|
]
|
||||||
|
|
||||||
|
disallowed-methods = [
|
||||||
|
# Inefficient. Use `.queue(…)` instead.
|
||||||
|
"crossterm::style::style",
|
||||||
|
# Use `thread::Builder::spawn` instead and handle the error.
|
||||||
|
"std::thread::spawn",
|
||||||
|
"std::thread::Scope::spawn",
|
||||||
|
# Return `ExitCode` instead.
|
||||||
|
"std::process::exit",
|
||||||
|
]
|
|
@ -201,3 +201,23 @@ panic = "abort"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
# You shouldn't write unsafe code in Rustlings!
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
|
||||||
|
unstable_features = "forbid"
|
||||||
|
# Dead code warnings can't be avoided in some exercises and might distract while learning.
|
||||||
|
dead_code = "allow"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
# You forgot a `todo!()`!
|
||||||
|
todo = "forbid"
|
||||||
|
# This can only happen by mistake in Rustlings.
|
||||||
|
empty_loop = "forbid"
|
||||||
|
# No infinite loops are needed in Rustlings.
|
||||||
|
infinite_loop = "deny"
|
||||||
|
# You shouldn't leak memory while still learning Rust!
|
||||||
|
mem_forget = "deny"
|
||||||
|
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
|
||||||
|
disallowed_methods = "allow"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// TODO: We sometimes encourage you to keep trying things on a given exercise,
|
// TODO: We sometimes encourage you to keep trying things on a given exercise
|
||||||
// even after you already figured it out. If you got everything working and feel
|
// even after you already figured it out. If you got everything working and feel
|
||||||
// ready for the next exercise, enter `n` in the terminal.
|
// ready for the next exercise, enter `n` in the terminal.
|
||||||
//
|
//
|
||||||
|
@ -6,8 +6,7 @@
|
||||||
// Try adding a new `println!` and check the updated output in the terminal.
|
// Try adding a new `println!` and check the updated output in the terminal.
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello and");
|
println!(r#" Welcome to... "#);
|
||||||
println!(r#" welcome to... "#);
|
|
||||||
println!(r#" _ _ _ "#);
|
println!(r#" _ _ _ "#);
|
||||||
println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#);
|
println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#);
|
||||||
println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#);
|
println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
// TODO: Add missing keyword.
|
// TODO: Add the missing keyword.
|
||||||
x = 5;
|
x = 5;
|
||||||
|
|
||||||
println!("x has the value {x}");
|
println!("x has the value {x}");
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
fn call_me(num: u32) {
|
fn call_me(num: u8) {
|
||||||
for i in 0..num {
|
for i in 0..num {
|
||||||
println!("Ring! Call number {}", i + 1);
|
println!("Ring! Call number {}", i + 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// TODO: Fix the compiler error on this function.
|
// TODO: Fix the compiler error on this function.
|
||||||
fn foo_if_fizz(fizzish: &str) -> &str {
|
fn picky_eater(food: &str) -> &str {
|
||||||
if fizzish == "fizz" {
|
if food == "strawberry" {
|
||||||
"foo"
|
"Yummy!"
|
||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
@ -18,18 +18,20 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn foo_for_fizz() {
|
fn yummy_food() {
|
||||||
// This means that calling `foo_if_fizz` with the argument "fizz" should return "foo".
|
// This means that calling `picky_eater` with the argument "food" should return "Yummy!".
|
||||||
assert_eq!(foo_if_fizz("fizz"), "foo");
|
assert_eq!(picky_eater("strawberry"), "Yummy!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bar_for_fuzz() {
|
fn neutral_food() {
|
||||||
assert_eq!(foo_if_fizz("fuzz"), "bar");
|
assert_eq!(picky_eater("potato"), "I guess I can eat that.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_to_baz() {
|
fn default_disliked_food() {
|
||||||
assert_eq!(foo_if_fizz("literally anything"), "baz");
|
assert_eq!(picky_eater("broccoli"), "No thanks!");
|
||||||
|
assert_eq!(picky_eater("gummy bears"), "No thanks!");
|
||||||
|
assert_eq!(picky_eater("literally anything"), "No thanks!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Point {
|
struct Point {
|
||||||
x: u64,
|
x: u64,
|
||||||
|
|
|
@ -4,7 +4,11 @@ struct Point {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Message {
|
enum Message {
|
||||||
// TODO: Implement the message variant types based on their usage below.
|
Resize { width: u64, height: u64 },
|
||||||
|
Move(Point),
|
||||||
|
Echo(String),
|
||||||
|
ChangeColor(u8, u8, u8),
|
||||||
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// You can bring module paths into scopes and provide new names for them with
|
// You can bring module paths into scopes and provide new names for them with
|
||||||
// the `use` and `as` keywords.
|
// the `use` and `as` keywords.
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
mod delicious_snacks {
|
mod delicious_snacks {
|
||||||
// TODO: Add the following two `use` statements after fixing them.
|
// TODO: Add the following two `use` statements after fixing them.
|
||||||
// use self::fruits::PEAR as ???;
|
// use self::fruits::PEAR as ???;
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct TeamScores {
|
||||||
|
|
||||||
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||||
// The name of the team is the key and its associated struct is the value.
|
// The name of the team is the key and its associated struct is the value.
|
||||||
let mut scores = HashMap::new();
|
let mut scores = HashMap::<&str, TeamScores>::new();
|
||||||
|
|
||||||
for line in results.lines() {
|
for line in results.lines() {
|
||||||
let mut split_iterator = line.split(',');
|
let mut split_iterator = line.split(',');
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
// of `Option<String>`.
|
// of `Option<String>`.
|
||||||
fn generate_nametag_text(name: String) -> Option<String> {
|
fn generate_nametag_text(name: String) -> Option<String> {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
// Empty names aren't allowed.
|
// Empty names aren't allowed
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(format!("Hi! My name is {name}"))
|
Some(format!("Hi! My name is {name}"))
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#![allow(clippy::comparison_chain)]
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
enum CreationError {
|
enum CreationError {
|
||||||
Negative,
|
Negative,
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
trait Licensed {
|
trait Licensed {
|
||||||
// TODO: Add a default implementation for `licensing_info` so that
|
// TODO: Add a default implementation for `licensing_info` so that
|
||||||
// implementors like the two structs below can share that default behavior
|
// implementors like the two structs below can share that default behavior
|
||||||
|
|
|
@ -8,7 +8,6 @@ use std::rc::Rc;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Sun;
|
struct Sun;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Planet {
|
enum Planet {
|
||||||
Mercury(Rc<Sun>),
|
Mercury(Rc<Sun>),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// This program spawns multiple threads that each run for at least 250ms, and
|
// This program spawns multiple threads that each runs for at least 250ms, and
|
||||||
// each thread returns how much time they took to complete. The program should
|
// each thread returns how much time it took to complete. The program should
|
||||||
// wait until all the spawned threads have finished and should collect their
|
// wait until all the spawned threads have finished and should collect their
|
||||||
// return values into a vector.
|
// return values into a vector.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{sync::mpsc, thread, time::Duration};
|
use std::{sync::mpsc, thread, time::Duration};
|
||||||
|
|
||||||
struct Queue {
|
struct Queue {
|
||||||
length: u32,
|
|
||||||
first_half: Vec<u32>,
|
first_half: Vec<u32>,
|
||||||
second_half: Vec<u32>,
|
second_half: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
@ -9,7 +8,6 @@ struct Queue {
|
||||||
impl Queue {
|
impl Queue {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
length: 10,
|
|
||||||
first_half: vec![1, 2, 3, 4, 5],
|
first_half: vec![1, 2, 3, 4, 5],
|
||||||
second_half: vec![6, 7, 8, 9, 10],
|
second_half: vec![6, 7, 8, 9, 10],
|
||||||
}
|
}
|
||||||
|
@ -48,17 +46,15 @@ mod tests {
|
||||||
fn threads3() {
|
fn threads3() {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let queue = Queue::new();
|
let queue = Queue::new();
|
||||||
let queue_length = queue.length;
|
|
||||||
|
|
||||||
send_tx(queue, tx);
|
send_tx(queue, tx);
|
||||||
|
|
||||||
let mut total_received: u32 = 0;
|
let mut received = Vec::with_capacity(10);
|
||||||
for received in rx {
|
for value in rx {
|
||||||
println!("Got: {received}");
|
received.push(value);
|
||||||
total_received += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Number of received values: {total_received}");
|
received.sort();
|
||||||
assert_eq!(total_received, queue_length);
|
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,11 @@
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[allow(unused_variables, unused_assignments)]
|
#[allow(unused_variables, unused_assignments)]
|
||||||
fn main() {
|
fn main() {
|
||||||
let my_option: Option<()> = None;
|
let my_option: Option<&str> = None;
|
||||||
|
// Assume that you don't know the value of `my_option`.
|
||||||
|
// In the case of `Some`, we want to print its value.
|
||||||
if my_option.is_none() {
|
if my_option.is_none() {
|
||||||
println!("{:?}", my_option.unwrap());
|
println!("{}", my_option.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
let my_arr = &[
|
let my_arr = &[
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
||||||
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
||||||
|
|
||||||
// Obtain the number of bytes (not characters) in the given argument.
|
// Obtain the number of bytes (not characters) in the given argument
|
||||||
|
// (`.len()` returns the number of bytes in a string).
|
||||||
// TODO: Add the `AsRef` trait appropriately as a trait bound.
|
// TODO: Add the `AsRef` trait appropriately as a trait bound.
|
||||||
fn byte_counter<T>(arg: T) -> usize {
|
fn byte_counter<T>(arg: T) -> usize {
|
||||||
arg.as_ref().as_bytes().len()
|
arg.as_ref().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain the number of characters (not bytes) in the given argument.
|
// Obtain the number of characters (not bytes) in the given argument.
|
||||||
|
|
|
@ -25,7 +25,7 @@ enum ParsePersonError {
|
||||||
ParseInt(ParseIntError),
|
ParseInt(ParseIntError),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Complete this `From` implementation to be able to parse a `Person`
|
// TODO: Complete this `FromStr` implementation to be able to parse a `Person`
|
||||||
// out of a string in the form of "Mark,20".
|
// out of a string in the form of "Mark,20".
|
||||||
// Note that you'll need to parse the age component into a `u8` with something
|
// Note that you'll need to parse the age component into a `u8` with something
|
||||||
// like `"4".parse::<u8>()`.
|
// like `"4".parse::<u8>()`.
|
||||||
|
|
|
@ -11,3 +11,6 @@ cargo clippy -- --deny warnings
|
||||||
cargo fmt --all --check
|
cargo fmt --all --check
|
||||||
cargo test --workspace --all-targets
|
cargo test --workspace --all-targets
|
||||||
cargo run -- dev check --require-solutions
|
cargo run -- dev check --require-solutions
|
||||||
|
|
||||||
|
# MSRV
|
||||||
|
cargo +1.80 run -- dev check --require-solutions
|
||||||
|
|
|
@ -16,6 +16,9 @@ include = [
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0.36"
|
quote = "1.0.37"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml_edit.workspace = true
|
toml_edit.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
format_version = 1
|
format_version = 1
|
||||||
|
|
||||||
welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners!
|
welcome_message = """
|
||||||
|
Is this your first time? Don't worry, Rustlings is made for beginners!
|
||||||
We are going to teach you a lot of things about Rust, but before we can
|
We are going to teach you a lot of things about Rust, but before we can
|
||||||
get started, here are some notes about how Rustlings operates:
|
get started, here are some notes about how Rustlings operates:
|
||||||
|
|
||||||
|
@ -10,15 +11,16 @@ get started, here are some notes about how Rustlings operates:
|
||||||
and fix them!
|
and fix them!
|
||||||
2. Make sure to have your editor open in the `rustlings/` directory. Rustlings
|
2. Make sure to have your editor open in the `rustlings/` directory. Rustlings
|
||||||
will show you the path of the current exercise under the progress bar. Open
|
will show you the path of the current exercise under the progress bar. Open
|
||||||
the exercise file in your editor, fix errors and save the file. Rustlings will
|
the exercise file in your editor, fix errors and save the file. Rustlings
|
||||||
automatically detect the file change and rerun the exercise. If all errors are
|
will automatically detect the file change and rerun the exercise. If all
|
||||||
fixed, Rustlings will ask you to move on to the next exercise.
|
errors are fixed, Rustlings will ask you to move on to the next exercise.
|
||||||
3. If you're stuck on an exercise, enter `h` to show a hint.
|
3. If you're stuck on an exercise, enter `h` to show a hint.
|
||||||
4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
|
4. If an exercise doesn't make sense to you, feel free to open an issue on
|
||||||
(https://github.com/rust-lang/rustlings). We look at every issue, and sometimes,
|
GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and
|
||||||
other learners do too so you can help each other out!"""
|
sometimes, other learners do too so you can help each other out!"""
|
||||||
|
|
||||||
final_message = """We hope you enjoyed learning about the various aspects of Rust!
|
final_message = """
|
||||||
|
We hope you enjoyed learning about the various aspects of Rust!
|
||||||
If you noticed any issues, don't hesitate to report them on Github.
|
If you noticed any issues, don't hesitate to report them on Github.
|
||||||
You can also contribute your own exercises to help the greater community!
|
You can also contribute your own exercises to help the greater community!
|
||||||
|
|
||||||
|
@ -120,10 +122,10 @@ dir = "01_variables"
|
||||||
test = false
|
test = false
|
||||||
hint = """
|
hint = """
|
||||||
We know about variables and mutability, but there is another important type of
|
We know about variables and mutability, but there is another important type of
|
||||||
variables available: constants.
|
variable available: constants.
|
||||||
|
|
||||||
Constants are always immutable. They are declared with the keyword `const` instead
|
Constants are always immutable. They are declared with the keyword `const`
|
||||||
of `let`.
|
instead of `let`.
|
||||||
|
|
||||||
The type of Constants must always be annotated.
|
The type of Constants must always be annotated.
|
||||||
|
|
||||||
|
@ -253,7 +255,7 @@ require you to type in 100 items (but you certainly can if you want!).
|
||||||
|
|
||||||
For example, you can do:
|
For example, you can do:
|
||||||
```
|
```
|
||||||
let array = ["Are we there yet?"; 10];
|
let array = ["Are we there yet?"; 100];
|
||||||
```
|
```
|
||||||
|
|
||||||
Bonus: what are some other things you could have that would return `true`
|
Bonus: what are some other things you could have that would return `true`
|
||||||
|
@ -319,7 +321,8 @@ hint = """
|
||||||
In the first function, we create an empty vector and want to push new elements
|
In the first function, we create an empty vector and want to push new elements
|
||||||
to it.
|
to it.
|
||||||
|
|
||||||
In the second function, we map the values of the input and collect them into a vector.
|
In the second function, we map the values of the input and collect them into
|
||||||
|
a vector.
|
||||||
|
|
||||||
After you've completed both functions, decide for yourself which approach you
|
After you've completed both functions, decide for yourself which approach you
|
||||||
like better.
|
like better.
|
||||||
|
@ -332,8 +335,8 @@ What do you think is the more commonly used pattern under Rust developers?"""
|
||||||
name = "move_semantics1"
|
name = "move_semantics1"
|
||||||
dir = "06_move_semantics"
|
dir = "06_move_semantics"
|
||||||
hint = """
|
hint = """
|
||||||
So you've got the "cannot borrow `vec` as mutable, as it is not declared as mutable"
|
So you've got the "cannot borrow `vec` as mutable, as it is not declared as
|
||||||
error on the line where we push an element to the vector, right?
|
mutable" error on the line where we push an element to the vector, right?
|
||||||
|
|
||||||
The fix for this is going to be adding one keyword, and the addition is NOT on
|
The fix for this is going to be adding one keyword, and the addition is NOT on
|
||||||
the line where we push to the vector (where the error is).
|
the line where we push to the vector (where the error is).
|
||||||
|
@ -369,7 +372,8 @@ hint = """
|
||||||
Carefully reason about the range in which each mutable reference is in
|
Carefully reason about the range in which each mutable reference is in
|
||||||
scope. Does it help to update the value of `x` immediately after
|
scope. Does it help to update the value of `x` immediately after
|
||||||
the mutable reference is taken?
|
the mutable reference is taken?
|
||||||
Read more about 'Mutable References' in the book's section 'References and Borrowing':
|
Read more about 'Mutable References' in the book's section 'References and
|
||||||
|
Borrowing':
|
||||||
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references."""
|
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references."""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
|
@ -508,7 +512,8 @@ name = "strings4"
|
||||||
dir = "09_strings"
|
dir = "09_strings"
|
||||||
test = false
|
test = false
|
||||||
hint = """
|
hint = """
|
||||||
Replace `placeholder` with either `string` or `string_slice` in the `main` function.
|
Replace `placeholder` with either `string` or `string_slice` in the `main`
|
||||||
|
function.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
`placeholder("blue");`
|
`placeholder("blue");`
|
||||||
|
@ -570,12 +575,8 @@ https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-
|
||||||
name = "hashmaps3"
|
name = "hashmaps3"
|
||||||
dir = "11_hashmaps"
|
dir = "11_hashmaps"
|
||||||
hint = """
|
hint = """
|
||||||
Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of
|
Hint 1: Use the `entry()` and `or_default()` methods of `HashMap` to insert the
|
||||||
`HashMap` to insert the default value of `TeamScores` if a team doesn't
|
default value of `TeamScores` if a team doesn't exist in the table yet.
|
||||||
exist in the table yet.
|
|
||||||
|
|
||||||
Learn more in The Book:
|
|
||||||
https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value
|
|
||||||
|
|
||||||
Hint 2: If there is already an entry for a given key, the value returned by
|
Hint 2: If there is already an entry for a given key, the value returned by
|
||||||
`entry()` can be updated based on the existing value.
|
`entry()` can be updated based on the existing value.
|
||||||
|
@ -1139,7 +1140,7 @@ constants, but clippy recognizes those imprecise mathematical constants as a
|
||||||
source of potential error.
|
source of potential error.
|
||||||
|
|
||||||
See the suggestions of the Clippy warning in the compile output and use the
|
See the suggestions of the Clippy warning in the compile output and use the
|
||||||
appropriate replacement constant from `std::f32::consts`..."""
|
appropriate replacement constant from `std::f32::consts`."""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "clippy2"
|
name = "clippy2"
|
||||||
|
@ -1200,7 +1201,8 @@ hint = """
|
||||||
Is there an implementation of `TryFrom` in the standard library that can both do
|
Is there an implementation of `TryFrom` in the standard library that can both do
|
||||||
the required integer conversion and check the range of the input?
|
the required integer conversion and check the range of the input?
|
||||||
|
|
||||||
Challenge: Can you make the `TryFrom` implementations generic over many integer types?"""
|
Challenge: Can you make the `TryFrom` implementations generic over many integer
|
||||||
|
types?"""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "as_ref_mut"
|
name = "as_ref_mut"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
fn call_me(num: u32) {
|
fn call_me(num: u8) {
|
||||||
for i in 0..num {
|
for i in 0..num {
|
||||||
println!("Ring! Call number {}", i + 1);
|
println!("Ring! Call number {}", i + 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
fn foo_if_fizz(fizzish: &str) -> &str {
|
fn picky_eater(food: &str) -> &str {
|
||||||
if fizzish == "fizz" {
|
if food == "strawberry" {
|
||||||
"foo"
|
"Yummy!"
|
||||||
} else if fizzish == "fuzz" {
|
} else if food == "potato" {
|
||||||
"bar"
|
"I guess I can eat that."
|
||||||
} else {
|
} else {
|
||||||
"baz"
|
"No thanks!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,17 +17,19 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn foo_for_fizz() {
|
fn yummy_food() {
|
||||||
assert_eq!(foo_if_fizz("fizz"), "foo");
|
assert_eq!(picky_eater("strawberry"), "Yummy!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bar_for_fuzz() {
|
fn neutral_food() {
|
||||||
assert_eq!(foo_if_fizz("fuzz"), "bar");
|
assert_eq!(picky_eater("potato"), "I guess I can eat that.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_to_baz() {
|
fn default_disliked_food() {
|
||||||
assert_eq!(foo_if_fizz("literally anything"), "baz");
|
assert_eq!(picky_eater("broccoli"), "No thanks!");
|
||||||
|
assert_eq!(picky_eater("gummy bears"), "No thanks!");
|
||||||
|
assert_eq!(picky_eater("literally anything"), "No thanks!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Point {
|
struct Point {
|
||||||
x: u64,
|
x: u64,
|
||||||
|
|
|
@ -46,8 +46,8 @@ impl State {
|
||||||
match message {
|
match message {
|
||||||
Message::Resize { width, height } => self.resize(width, height),
|
Message::Resize { width, height } => self.resize(width, height),
|
||||||
Message::Move(point) => self.move_position(point),
|
Message::Move(point) => self.move_position(point),
|
||||||
Message::Echo(s) => self.echo(s),
|
Message::Echo(string) => self.echo(string),
|
||||||
Message::ChangeColor(r, g, b) => self.change_color(r, g, b),
|
Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue),
|
||||||
Message::Quit => self.quit(),
|
Message::Quit => self.quit(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,11 @@ fn main() {
|
||||||
// Here, both answers work.
|
// Here, both answers work.
|
||||||
// `.into()` converts a type into an expected type.
|
// `.into()` converts a type into an expected type.
|
||||||
// If it is called where `String` is expected, it will convert `&str` to `String`.
|
// If it is called where `String` is expected, it will convert `&str` to `String`.
|
||||||
// But if is called where `&str` is expected, then `&str` is kept `&str` since no
|
|
||||||
// conversion is needed.
|
|
||||||
string("nice weather".into());
|
string("nice weather".into());
|
||||||
|
// But if it is called where `&str` is expected, then `&str` is kept `&str` since no conversion is needed.
|
||||||
|
// If you remove the `#[allow(…)]` line, then Clippy will tell you to remove `.into()` below since it is a useless conversion.
|
||||||
|
#[allow(clippy::useless_conversion)]
|
||||||
string_slice("nice weather".into());
|
string_slice("nice weather".into());
|
||||||
// ^^^^^^^ the compiler recommends removing the `.into()`
|
|
||||||
// call because it is a useless conversion.
|
|
||||||
|
|
||||||
string(format!("Interpolation {}", "Station"));
|
string(format!("Interpolation {}", "Station"));
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#[allow(dead_code)]
|
|
||||||
mod delicious_snacks {
|
mod delicious_snacks {
|
||||||
// Added `pub` and used the expected alias after `as`.
|
// Added `pub` and used the expected alias after `as`.
|
||||||
pub use self::fruits::PEAR as fruit;
|
pub use self::fruits::PEAR as fruit;
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct TeamScores {
|
||||||
|
|
||||||
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||||
// The name of the team is the key and its associated struct is the value.
|
// The name of the team is the key and its associated struct is the value.
|
||||||
let mut scores = HashMap::new();
|
let mut scores = HashMap::<&str, TeamScores>::new();
|
||||||
|
|
||||||
for line in results.lines() {
|
for line in results.lines() {
|
||||||
let mut split_iterator = line.split(',');
|
let mut split_iterator = line.split(',');
|
||||||
|
@ -28,17 +28,13 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||||
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
|
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
|
||||||
|
|
||||||
// Insert the default with zeros if a team doesn't exist yet.
|
// Insert the default with zeros if a team doesn't exist yet.
|
||||||
let team_1 = scores
|
let team_1 = scores.entry(team_1_name).or_default();
|
||||||
.entry(team_1_name)
|
|
||||||
.or_insert_with(TeamScores::default);
|
|
||||||
// Update the values.
|
// Update the values.
|
||||||
team_1.goals_scored += team_1_score;
|
team_1.goals_scored += team_1_score;
|
||||||
team_1.goals_conceded += team_2_score;
|
team_1.goals_conceded += team_2_score;
|
||||||
|
|
||||||
// Similarely for the second team.
|
// Similarly for the second team.
|
||||||
let team_2 = scores
|
let team_2 = scores.entry(team_2_name).or_default();
|
||||||
.entry(team_2_name)
|
|
||||||
.or_insert_with(TeamScores::default);
|
|
||||||
team_2.goals_scored += team_2_score;
|
team_2.goals_scored += team_2_score;
|
||||||
team_2.goals_conceded += team_1_score;
|
team_2.goals_conceded += team_1_score;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#![allow(clippy::comparison_chain)]
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
enum CreationError {
|
enum CreationError {
|
||||||
|
@ -11,12 +11,10 @@ struct PositiveNonzeroInteger(u64);
|
||||||
|
|
||||||
impl PositiveNonzeroInteger {
|
impl PositiveNonzeroInteger {
|
||||||
fn new(value: i64) -> Result<Self, CreationError> {
|
fn new(value: i64) -> Result<Self, CreationError> {
|
||||||
if value == 0 {
|
match value.cmp(&0) {
|
||||||
Err(CreationError::Zero)
|
Ordering::Less => Err(CreationError::Negative),
|
||||||
} else if value < 0 {
|
Ordering::Equal => Err(CreationError::Zero),
|
||||||
Err(CreationError::Negative)
|
Ordering::Greater => Ok(Self(value as u64)),
|
||||||
} else {
|
|
||||||
Ok(Self(value as u64))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,21 @@ impl ParsePosNonzeroError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As an alternative solution, implementing the `From` trait allows for the
|
||||||
|
// automatic conversion from a `ParseIntError` into a `ParsePosNonzeroError`
|
||||||
|
// using the `?` operator, without the need to call `map_err`.
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// let x: i64 = s.parse()?;
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Traits like `From` will be dealt with in later exercises.
|
||||||
|
impl From<ParseIntError> for ParsePosNonzeroError {
|
||||||
|
fn from(err: ParseIntError) -> Self {
|
||||||
|
ParsePosNonzeroError::ParseInt(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
struct PositiveNonzeroInteger(u64);
|
struct PositiveNonzeroInteger(u64);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
trait Licensed {
|
trait Licensed {
|
||||||
fn licensing_info(&self) -> String {
|
fn licensing_info(&self) -> String {
|
||||||
"Default license".to_string()
|
"Default license".to_string()
|
||||||
|
|
|
@ -25,6 +25,7 @@ fn factorial_fold(num: u64) -> u64 {
|
||||||
// -> 1 * 2 is calculated, then the result 2 is multiplied by
|
// -> 1 * 2 is calculated, then the result 2 is multiplied by
|
||||||
// the second element 3 so the result 6 is returned.
|
// the second element 3 so the result 6 is returned.
|
||||||
// And so on…
|
// And so on…
|
||||||
|
#[allow(clippy::unnecessary_fold)]
|
||||||
(2..=num).fold(1, |acc, x| acc * x)
|
(2..=num).fold(1, |acc, x| acc * x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ use std::rc::Rc;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Sun;
|
struct Sun;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Planet {
|
enum Planet {
|
||||||
Mercury(Rc<Sun>),
|
Mercury(Rc<Sun>),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// This program spawns multiple threads that each run for at least 250ms, and
|
// This program spawns multiple threads that each runs for at least 250ms, and
|
||||||
// each thread returns how much time they took to complete. The program should
|
// each thread returns how much time it took to complete. The program should
|
||||||
// wait until all the spawned threads have finished and should collect their
|
// wait until all the spawned threads have finished and should collect their
|
||||||
// return values into a vector.
|
// return values into a vector.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{sync::mpsc, thread, time::Duration};
|
use std::{sync::mpsc, thread, time::Duration};
|
||||||
|
|
||||||
struct Queue {
|
struct Queue {
|
||||||
length: u32,
|
|
||||||
first_half: Vec<u32>,
|
first_half: Vec<u32>,
|
||||||
second_half: Vec<u32>,
|
second_half: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
@ -9,7 +8,6 @@ struct Queue {
|
||||||
impl Queue {
|
impl Queue {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
length: 10,
|
|
||||||
first_half: vec![1, 2, 3, 4, 5],
|
first_half: vec![1, 2, 3, 4, 5],
|
||||||
second_half: vec![6, 7, 8, 9, 10],
|
second_half: vec![6, 7, 8, 9, 10],
|
||||||
}
|
}
|
||||||
|
@ -50,17 +48,15 @@ mod tests {
|
||||||
fn threads3() {
|
fn threads3() {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let queue = Queue::new();
|
let queue = Queue::new();
|
||||||
let queue_length = queue.length;
|
|
||||||
|
|
||||||
send_tx(queue, tx);
|
send_tx(queue, tx);
|
||||||
|
|
||||||
let mut total_received: u32 = 0;
|
let mut received = Vec::with_capacity(10);
|
||||||
for received in rx {
|
for value in rx {
|
||||||
println!("Got: {received}");
|
received.push(value);
|
||||||
total_received += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Number of received values: {total_received}");
|
received.sort();
|
||||||
assert_eq!(total_received, queue_length);
|
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@ use std::mem;
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[allow(unused_variables, unused_assignments)]
|
#[allow(unused_variables, unused_assignments)]
|
||||||
fn main() {
|
fn main() {
|
||||||
let my_option: Option<()> = None;
|
let my_option: Option<&str> = None;
|
||||||
// `unwrap` of an `Option` after checking if it is `None` will panic.
|
// `unwrap` of an `Option` after checking if it is `None` will panic.
|
||||||
// Use `if-let` instead.
|
// Use `if-let` instead.
|
||||||
if let Some(value) = my_option {
|
if let Some(value) = my_option {
|
||||||
println!("{value:?}");
|
println!("{value}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A comma was missing.
|
// A comma was missing.
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
||||||
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
||||||
|
|
||||||
// Obtain the number of bytes (not characters) in the given argument.
|
// Obtain the number of bytes (not characters) in the given argument
|
||||||
|
// (`.len()` returns the number of bytes in a string).
|
||||||
fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
|
fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
|
||||||
arg.as_ref().as_bytes().len()
|
arg.as_ref().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain the number of characters (not bytes) in the given argument.
|
// Obtain the number of characters (not bytes) in the given argument.
|
||||||
|
|
458
src/app_state.rs
458
src/app_state.rs
|
@ -1,32 +1,39 @@
|
||||||
use anyhow::{bail, Context, Error, Result};
|
use anyhow::{bail, Context, Error, Result};
|
||||||
|
use crossterm::{cursor, terminal, QueueableCommand};
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
collections::HashSet,
|
||||||
io::{Read, StdoutLock, Write},
|
env,
|
||||||
path::Path,
|
fs::{File, OpenOptions},
|
||||||
|
io::{Read, Seek, StdoutLock, Write},
|
||||||
|
path::{Path, MAIN_SEPARATOR_STR},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering::Relaxed},
|
||||||
|
mpsc,
|
||||||
|
},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
clear_terminal,
|
clear_terminal,
|
||||||
cmd::CmdRunner,
|
cmd::CmdRunner,
|
||||||
collections::hash_set_with_capacity,
|
|
||||||
embedded::EMBEDDED_FILES,
|
embedded::EMBEDDED_FILES,
|
||||||
exercise::{Exercise, RunnableExercise},
|
exercise::{Exercise, RunnableExercise},
|
||||||
info_file::ExerciseInfo,
|
info_file::ExerciseInfo,
|
||||||
|
term::{self, CheckProgressVisualizer},
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
|
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
|
||||||
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
|
const DEFAULT_CHECK_PARALLELISM: usize = 8;
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub enum ExercisesProgress {
|
pub enum ExercisesProgress {
|
||||||
// All exercises are done.
|
// All exercises are done.
|
||||||
AllDone,
|
AllDone,
|
||||||
// The current exercise failed and is still pending.
|
|
||||||
CurrentPending,
|
|
||||||
// A new exercise is now pending.
|
// A new exercise is now pending.
|
||||||
NewPending,
|
NewPending,
|
||||||
|
// The current exercise is still pending.
|
||||||
|
CurrentPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum StateFileStatus {
|
pub enum StateFileStatus {
|
||||||
|
@ -34,72 +41,47 @@ pub enum StateFileStatus {
|
||||||
NotRead,
|
NotRead,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum CheckProgress {
|
||||||
|
None,
|
||||||
|
Checking,
|
||||||
|
Done,
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
current_exercise_ind: usize,
|
current_exercise_ind: usize,
|
||||||
exercises: Vec<Exercise>,
|
exercises: Vec<Exercise>,
|
||||||
// Caches the number of done exercises to avoid iterating over all exercises every time.
|
// Caches the number of done exercises to avoid iterating over all exercises every time.
|
||||||
n_done: u16,
|
n_done: u16,
|
||||||
final_message: String,
|
final_message: String,
|
||||||
|
state_file: File,
|
||||||
// Preallocated buffer for reading and writing the state file.
|
// Preallocated buffer for reading and writing the state file.
|
||||||
file_buf: Vec<u8>,
|
file_buf: Vec<u8>,
|
||||||
official_exercises: bool,
|
official_exercises: bool,
|
||||||
cmd_runner: CmdRunner,
|
cmd_runner: CmdRunner,
|
||||||
|
// Running in VS Code.
|
||||||
|
vs_code: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
// Update the app state from the state file.
|
|
||||||
fn update_from_file(&mut self) -> StateFileStatus {
|
|
||||||
self.file_buf.clear();
|
|
||||||
self.n_done = 0;
|
|
||||||
|
|
||||||
if File::open(STATE_FILE_NAME)
|
|
||||||
.and_then(|mut file| file.read_to_end(&mut self.file_buf))
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return StateFileStatus::NotRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
// See `Self::write` for more information about the file format.
|
|
||||||
let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2);
|
|
||||||
|
|
||||||
let Some(current_exercise_name) = lines.next() else {
|
|
||||||
return StateFileStatus::NotRead;
|
|
||||||
};
|
|
||||||
|
|
||||||
if current_exercise_name.is_empty() || lines.next().is_none() {
|
|
||||||
return StateFileStatus::NotRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut done_exercises = hash_set_with_capacity(self.exercises.len());
|
|
||||||
|
|
||||||
for done_exerise_name in lines {
|
|
||||||
if done_exerise_name.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
done_exercises.insert(done_exerise_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ind, exercise) in self.exercises.iter_mut().enumerate() {
|
|
||||||
if done_exercises.contains(exercise.name.as_bytes()) {
|
|
||||||
exercise.done = true;
|
|
||||||
self.n_done += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if exercise.name.as_bytes() == current_exercise_name {
|
|
||||||
self.current_exercise_ind = ind;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StateFileStatus::Read
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
exercise_infos: Vec<ExerciseInfo>,
|
exercise_infos: Vec<ExerciseInfo>,
|
||||||
final_message: String,
|
final_message: String,
|
||||||
) -> Result<(Self, StateFileStatus)> {
|
) -> Result<(Self, StateFileStatus)> {
|
||||||
let cmd_runner = CmdRunner::build()?;
|
let cmd_runner = CmdRunner::build()?;
|
||||||
|
let mut state_file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(STATE_FILE_NAME)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("Failed to open or create the state file {STATE_FILE_NAME}")
|
||||||
|
})?;
|
||||||
|
|
||||||
let exercises = exercise_infos
|
let dir_canonical_path = term::canonicalize("exercises");
|
||||||
|
let mut exercises = exercise_infos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|exercise_info| {
|
.map(|exercise_info| {
|
||||||
// Leaking to be able to borrow in the watch mode `Table`.
|
// Leaking to be able to borrow in the watch mode `Table`.
|
||||||
|
@ -110,30 +92,97 @@ impl AppState {
|
||||||
let dir = exercise_info.dir.map(|dir| &*dir.leak());
|
let dir = exercise_info.dir.map(|dir| &*dir.leak());
|
||||||
let hint = exercise_info.hint.leak().trim_ascii();
|
let hint = exercise_info.hint.leak().trim_ascii();
|
||||||
|
|
||||||
|
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
|
||||||
|
let mut canonical_path;
|
||||||
|
if let Some(dir) = dir {
|
||||||
|
canonical_path = String::with_capacity(
|
||||||
|
2 + dir_canonical_path.len() + dir.len() + name.len(),
|
||||||
|
);
|
||||||
|
canonical_path.push_str(dir_canonical_path);
|
||||||
|
canonical_path.push_str(MAIN_SEPARATOR_STR);
|
||||||
|
canonical_path.push_str(dir);
|
||||||
|
} else {
|
||||||
|
canonical_path =
|
||||||
|
String::with_capacity(1 + dir_canonical_path.len() + name.len());
|
||||||
|
canonical_path.push_str(dir_canonical_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical_path.push_str(MAIN_SEPARATOR_STR);
|
||||||
|
canonical_path.push_str(name);
|
||||||
|
canonical_path.push_str(".rs");
|
||||||
|
canonical_path
|
||||||
|
});
|
||||||
|
|
||||||
Exercise {
|
Exercise {
|
||||||
dir,
|
dir,
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
|
canonical_path,
|
||||||
test: exercise_info.test,
|
test: exercise_info.test,
|
||||||
strict_clippy: exercise_info.strict_clippy,
|
strict_clippy: exercise_info.strict_clippy,
|
||||||
hint,
|
hint,
|
||||||
// Updated in `Self::update_from_file`.
|
// Updated below.
|
||||||
done: false,
|
done: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut slf = Self {
|
let mut current_exercise_ind = 0;
|
||||||
current_exercise_ind: 0,
|
let mut n_done = 0;
|
||||||
exercises,
|
let mut file_buf = Vec::with_capacity(2048);
|
||||||
n_done: 0,
|
let state_file_status = 'block: {
|
||||||
final_message,
|
if state_file.read_to_end(&mut file_buf).is_err() {
|
||||||
file_buf: Vec::with_capacity(2048),
|
break 'block StateFileStatus::NotRead;
|
||||||
official_exercises: !Path::new("info.toml").exists(),
|
}
|
||||||
cmd_runner,
|
|
||||||
|
// See `Self::write` for more information about the file format.
|
||||||
|
let mut lines = file_buf.split(|c| *c == b'\n').skip(2);
|
||||||
|
|
||||||
|
let Some(current_exercise_name) = lines.next() else {
|
||||||
|
break 'block StateFileStatus::NotRead;
|
||||||
};
|
};
|
||||||
|
|
||||||
let state_file_status = slf.update_from_file();
|
if current_exercise_name.is_empty() || lines.next().is_none() {
|
||||||
|
break 'block StateFileStatus::NotRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut done_exercises = HashSet::with_capacity(exercises.len());
|
||||||
|
|
||||||
|
for done_exercise_name in lines {
|
||||||
|
if done_exercise_name.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done_exercises.insert(done_exercise_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ind, exercise) in exercises.iter_mut().enumerate() {
|
||||||
|
if done_exercises.contains(exercise.name.as_bytes()) {
|
||||||
|
exercise.done = true;
|
||||||
|
n_done += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if exercise.name.as_bytes() == current_exercise_name {
|
||||||
|
current_exercise_ind = ind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateFileStatus::Read
|
||||||
|
};
|
||||||
|
|
||||||
|
file_buf.clear();
|
||||||
|
file_buf.extend_from_slice(STATE_FILE_HEADER);
|
||||||
|
|
||||||
|
let slf = Self {
|
||||||
|
current_exercise_ind,
|
||||||
|
exercises,
|
||||||
|
n_done,
|
||||||
|
final_message,
|
||||||
|
state_file,
|
||||||
|
file_buf,
|
||||||
|
official_exercises: !Path::new("info.toml").exists(),
|
||||||
|
cmd_runner,
|
||||||
|
vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"),
|
||||||
|
};
|
||||||
|
|
||||||
Ok((slf, state_file_status))
|
Ok((slf, state_file_status))
|
||||||
}
|
}
|
||||||
|
@ -153,6 +202,11 @@ impl AppState {
|
||||||
self.n_done
|
self.n_done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn n_pending(&self) -> u16 {
|
||||||
|
self.exercises.len() as u16 - self.n_done
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn current_exercise(&self) -> &Exercise {
|
pub fn current_exercise(&self) -> &Exercise {
|
||||||
&self.exercises[self.current_exercise_ind]
|
&self.exercises[self.current_exercise_ind]
|
||||||
|
@ -163,6 +217,11 @@ impl AppState {
|
||||||
&self.cmd_runner
|
&self.cmd_runner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn vs_code(&self) -> bool {
|
||||||
|
self.vs_code
|
||||||
|
}
|
||||||
|
|
||||||
// Write the state file.
|
// Write the state file.
|
||||||
// The file's format is very simple:
|
// The file's format is very simple:
|
||||||
// - The first line is a comment.
|
// - The first line is a comment.
|
||||||
|
@ -172,10 +231,8 @@ impl AppState {
|
||||||
// - The fourth line is an empty line.
|
// - The fourth line is an empty line.
|
||||||
// - All remaining lines are the names of done exercises.
|
// - All remaining lines are the names of done exercises.
|
||||||
fn write(&mut self) -> Result<()> {
|
fn write(&mut self) -> Result<()> {
|
||||||
self.file_buf.clear();
|
self.file_buf.truncate(STATE_FILE_HEADER.len());
|
||||||
|
|
||||||
self.file_buf
|
|
||||||
.extend_from_slice(b"DON'T EDIT THIS FILE!\n\n");
|
|
||||||
self.file_buf
|
self.file_buf
|
||||||
.extend_from_slice(self.current_exercise().name.as_bytes());
|
.extend_from_slice(self.current_exercise().name.as_bytes());
|
||||||
self.file_buf.push(b'\n');
|
self.file_buf.push(b'\n');
|
||||||
|
@ -187,7 +244,14 @@ impl AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::write(STATE_FILE_NAME, &self.file_buf)
|
self.state_file
|
||||||
|
.rewind()
|
||||||
|
.with_context(|| format!("Failed to rewind the state file {STATE_FILE_NAME}"))?;
|
||||||
|
self.state_file
|
||||||
|
.set_len(0)
|
||||||
|
.with_context(|| format!("Failed to truncate the state file {STATE_FILE_NAME}"))?;
|
||||||
|
self.state_file
|
||||||
|
.write_all(&self.file_buf)
|
||||||
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
|
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -219,15 +283,31 @@ impl AppState {
|
||||||
self.write()
|
self.write()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
|
// Set the status of an exercise without saving. Returns `true` if the
|
||||||
|
// status actually changed (and thus needs saving later).
|
||||||
|
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
|
||||||
let exercise = self
|
let exercise = self
|
||||||
.exercises
|
.exercises
|
||||||
.get_mut(exercise_ind)
|
.get_mut(exercise_ind)
|
||||||
.context(BAD_INDEX_ERR)?;
|
.context(BAD_INDEX_ERR)?;
|
||||||
|
|
||||||
if exercise.done {
|
if exercise.done == done {
|
||||||
exercise.done = false;
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
exercise.done = done;
|
||||||
|
if done {
|
||||||
|
self.n_done += 1;
|
||||||
|
} else {
|
||||||
self.n_done -= 1;
|
self.n_done -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the status of an exercise to "pending" and save.
|
||||||
|
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
|
||||||
|
if self.set_status(exercise_ind, false)? {
|
||||||
self.write()?;
|
self.write()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,6 +351,7 @@ impl AppState {
|
||||||
Ok(exercise.path)
|
Ok(exercise.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the exercise by index and return its name.
|
||||||
pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
|
pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
|
||||||
if exercise_ind >= self.exercises.len() {
|
if exercise_ind >= self.exercises.len() {
|
||||||
bail!(BAD_INDEX_ERR);
|
bail!(BAD_INDEX_ERR);
|
||||||
|
@ -280,33 +361,30 @@ impl AppState {
|
||||||
let exercise = &self.exercises[exercise_ind];
|
let exercise = &self.exercises[exercise_ind];
|
||||||
self.reset(exercise_ind, exercise.path)?;
|
self.reset(exercise_ind, exercise.path)?;
|
||||||
|
|
||||||
Ok(exercise.path)
|
Ok(exercise.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the index of the next pending exercise or `None` if all exercises are done.
|
// Return the index of the next pending exercise or `None` if all exercises are done.
|
||||||
fn next_pending_exercise_ind(&self) -> Option<usize> {
|
fn next_pending_exercise_ind(&self) -> Option<usize> {
|
||||||
if self.current_exercise_ind == self.exercises.len() - 1 {
|
let next_ind = self.current_exercise_ind + 1;
|
||||||
// The last exercise is done.
|
self.exercises
|
||||||
// Search for exercises not done from the start.
|
// If the exercise done isn't the last, search for pending exercises after it.
|
||||||
return self.exercises[..self.current_exercise_ind]
|
.get(next_ind..)
|
||||||
.iter()
|
.and_then(|later_exercises| {
|
||||||
.position(|exercise| !exercise.done);
|
later_exercises
|
||||||
}
|
|
||||||
|
|
||||||
// The done exercise isn't the last one.
|
|
||||||
// Search for a pending exercise after the current one and then from the start.
|
|
||||||
match self.exercises[self.current_exercise_ind + 1..]
|
|
||||||
.iter()
|
.iter()
|
||||||
.position(|exercise| !exercise.done)
|
.position(|exercise| !exercise.done)
|
||||||
{
|
.map(|ind| next_ind + ind)
|
||||||
Some(ind) => Some(self.current_exercise_ind + 1 + ind),
|
})
|
||||||
None => self.exercises[..self.current_exercise_ind]
|
// Search from the start.
|
||||||
|
.or_else(|| {
|
||||||
|
self.exercises[..self.current_exercise_ind]
|
||||||
.iter()
|
.iter()
|
||||||
.position(|exercise| !exercise.done),
|
.position(|exercise| !exercise.done)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Official exercises: Dump the solution file form the binary and return its path.
|
/// Official exercises: Dump the solution file from the binary and return its path.
|
||||||
/// Third-party exercises: Check if a solution file exists and return its path in that case.
|
/// Third-party exercises: Check if a solution file exists and return its path in that case.
|
||||||
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
|
@ -320,24 +398,133 @@ impl AppState {
|
||||||
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
|
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
|
||||||
.map(Some)
|
.map(Some)
|
||||||
} else {
|
} else {
|
||||||
let solution_path = if let Some(dir) = current_exercise.dir {
|
let sol_path = current_exercise.sol_path();
|
||||||
format!("solutions/{dir}/{}.rs", current_exercise.name)
|
|
||||||
} else {
|
|
||||||
format!("solutions/{}.rs", current_exercise.name)
|
|
||||||
};
|
|
||||||
|
|
||||||
if Path::new(&solution_path).exists() {
|
if Path::new(&sol_path).exists() {
|
||||||
return Ok(Some(solution_path));
|
return Ok(Some(sol_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
||||||
|
let term_width = terminal::size()
|
||||||
|
.context("Failed to get the terminal size")?
|
||||||
|
.0;
|
||||||
|
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
|
||||||
|
|
||||||
|
let next_exercise_ind = AtomicUsize::new(0);
|
||||||
|
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
|
||||||
|
|
||||||
|
thread::scope(|s| {
|
||||||
|
let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
|
||||||
|
let n_threads = thread::available_parallelism()
|
||||||
|
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
|
||||||
|
|
||||||
|
for _ in 0..n_threads {
|
||||||
|
let exercise_progress_sender = exercise_progress_sender.clone();
|
||||||
|
let next_exercise_ind = &next_exercise_ind;
|
||||||
|
let slf = &self;
|
||||||
|
thread::Builder::new()
|
||||||
|
.spawn_scoped(s, move || loop {
|
||||||
|
let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed);
|
||||||
|
let Some(exercise) = slf.exercises.get(exercise_ind) else {
|
||||||
|
// No more exercises.
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if exercise_progress_sender
|
||||||
|
.send((exercise_ind, CheckProgress::Checking))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let success = exercise.run_exercise(None, &slf.cmd_runner);
|
||||||
|
let progress = match success {
|
||||||
|
Ok(true) => CheckProgress::Done,
|
||||||
|
Ok(false) => CheckProgress::Pending,
|
||||||
|
Err(_) => CheckProgress::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if exercise_progress_sender
|
||||||
|
.send((exercise_ind, progress))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.context("Failed to spawn a thread to check all exercises")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop this sender to detect when the last thread is done.
|
||||||
|
drop(exercise_progress_sender);
|
||||||
|
|
||||||
|
while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
|
||||||
|
progresses[exercise_ind] = progress;
|
||||||
|
progress_visualizer.update(&progresses)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<_, Error>(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut first_pending_exercise_ind = None;
|
||||||
|
for exercise_ind in 0..progresses.len() {
|
||||||
|
match progresses[exercise_ind] {
|
||||||
|
CheckProgress::Done => {
|
||||||
|
self.set_status(exercise_ind, true)?;
|
||||||
|
}
|
||||||
|
CheckProgress::Pending => {
|
||||||
|
self.set_status(exercise_ind, false)?;
|
||||||
|
if first_pending_exercise_ind.is_none() {
|
||||||
|
first_pending_exercise_ind = Some(exercise_ind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CheckProgress::None | CheckProgress::Checking => {
|
||||||
|
// If we got an error while checking all exercises in parallel,
|
||||||
|
// it could be because we exceeded the limit of open file descriptors.
|
||||||
|
// Therefore, try running exercises with errors sequentially.
|
||||||
|
progresses[exercise_ind] = CheckProgress::Checking;
|
||||||
|
progress_visualizer.update(&progresses)?;
|
||||||
|
|
||||||
|
let exercise = &self.exercises[exercise_ind];
|
||||||
|
let success = exercise.run_exercise(None, &self.cmd_runner)?;
|
||||||
|
if success {
|
||||||
|
progresses[exercise_ind] = CheckProgress::Done;
|
||||||
|
} else {
|
||||||
|
progresses[exercise_ind] = CheckProgress::Pending;
|
||||||
|
if first_pending_exercise_ind.is_none() {
|
||||||
|
first_pending_exercise_ind = Some(exercise_ind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.set_status(exercise_ind, success)?;
|
||||||
|
progress_visualizer.update(&progresses)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write()?;
|
||||||
|
|
||||||
|
Ok(first_pending_exercise_ind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the exercise index of the first pending exercise found.
|
||||||
|
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
||||||
|
stdout.queue(cursor::Hide)?;
|
||||||
|
let res = self.check_all_exercises_impl(stdout);
|
||||||
|
stdout.queue(cursor::Show)?;
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
||||||
/// If all exercises are marked as done, run all of them to make sure that they are actually
|
/// If all exercises are marked as done, run all of them to make sure that they are actually
|
||||||
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
||||||
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
|
pub fn done_current_exercise<const CLEAR_BEFORE_FINAL_CHECK: bool>(
|
||||||
|
&mut self,
|
||||||
|
stdout: &mut StdoutLock,
|
||||||
|
) -> Result<ExercisesProgress> {
|
||||||
let exercise = &mut self.exercises[self.current_exercise_ind];
|
let exercise = &mut self.exercises[self.current_exercise_ind];
|
||||||
if !exercise.done {
|
if !exercise.done {
|
||||||
exercise.done = true;
|
exercise.done = true;
|
||||||
|
@ -349,69 +536,39 @@ impl AppState {
|
||||||
return Ok(ExercisesProgress::NewPending);
|
return Ok(ExercisesProgress::NewPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
|
if CLEAR_BEFORE_FINAL_CHECK {
|
||||||
|
clear_terminal(stdout)?;
|
||||||
let n_exercises = self.exercises.len();
|
} else {
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
let pending_exercise_ind = thread::scope(|s| {
|
|
||||||
let handles = self
|
|
||||||
.exercises
|
|
||||||
.iter_mut()
|
|
||||||
.map(|exercise| {
|
|
||||||
s.spawn(|| {
|
|
||||||
let success = exercise.run_exercise(None, &self.cmd_runner)?;
|
|
||||||
exercise.done = success;
|
|
||||||
Ok::<_, Error>(success)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
for (exercise_ind, handle) in handles.into_iter().enumerate() {
|
|
||||||
write!(writer, "\rProgress: {exercise_ind}/{n_exercises}")?;
|
|
||||||
writer.flush()?;
|
|
||||||
|
|
||||||
let success = handle.join().unwrap()?;
|
|
||||||
if !success {
|
|
||||||
writer.write_all(b"\n\n")?;
|
|
||||||
return Ok(Some(exercise_ind));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<_, Error>(None)
|
if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? {
|
||||||
})?;
|
self.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||||
|
|
||||||
if let Some(pending_exercise_ind) = pending_exercise_ind {
|
|
||||||
self.current_exercise_ind = pending_exercise_ind;
|
|
||||||
self.n_done = self
|
|
||||||
.exercises
|
|
||||||
.iter()
|
|
||||||
.filter(|exercise| exercise.done)
|
|
||||||
.count() as u16;
|
|
||||||
self.write()?;
|
|
||||||
return Ok(ExercisesProgress::NewPending);
|
return Ok(ExercisesProgress::NewPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write that the last exercise is done.
|
self.render_final_message(stdout)?;
|
||||||
self.write()?;
|
|
||||||
|
|
||||||
clear_terminal(writer)?;
|
|
||||||
writer.write_all(FENISH_LINE.as_bytes())?;
|
|
||||||
|
|
||||||
let final_message = self.final_message.trim_ascii();
|
|
||||||
if !final_message.is_empty() {
|
|
||||||
writer.write_all(final_message.as_bytes())?;
|
|
||||||
writer.write_all(b"\n")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ExercisesProgress::AllDone)
|
Ok(ExercisesProgress::AllDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
|
||||||
|
clear_terminal(stdout)?;
|
||||||
|
stdout.write_all(FENISH_LINE.as_bytes())?;
|
||||||
|
|
||||||
|
let final_message = self.final_message.trim_ascii();
|
||||||
|
if !final_message.is_empty() {
|
||||||
|
stdout.write_all(final_message.as_bytes())?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
|
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
|
||||||
All exercises seem to be done.
|
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
|
||||||
Recompiling and running all exercises to make sure that all of them are actually done.
|
|
||||||
";
|
|
||||||
|
|
||||||
const FENISH_LINE: &str = "+----------------------------------------------------+
|
const FENISH_LINE: &str = "+----------------------------------------------------+
|
||||||
| You made it to the Fe-nish line! |
|
| You made it to the Fe-nish line! |
|
||||||
+-------------------------- ------------------------+
|
+-------------------------- ------------------------+
|
||||||
|
@ -443,6 +600,7 @@ mod tests {
|
||||||
dir: None,
|
dir: None,
|
||||||
name: "0",
|
name: "0",
|
||||||
path: "exercises/0.rs",
|
path: "exercises/0.rs",
|
||||||
|
canonical_path: None,
|
||||||
test: false,
|
test: false,
|
||||||
strict_clippy: false,
|
strict_clippy: false,
|
||||||
hint: "",
|
hint: "",
|
||||||
|
@ -457,9 +615,11 @@ mod tests {
|
||||||
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
|
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
|
||||||
n_done: 0,
|
n_done: 0,
|
||||||
final_message: String::new(),
|
final_message: String::new(),
|
||||||
|
state_file: tempfile::tempfile().unwrap(),
|
||||||
file_buf: Vec::new(),
|
file_buf: Vec::new(),
|
||||||
official_exercises: true,
|
official_exercises: true,
|
||||||
cmd_runner: CmdRunner::build().unwrap(),
|
cmd_runner: CmdRunner::build().unwrap(),
|
||||||
|
vs_code: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {
|
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::info_file::ExerciseInfo;
|
use crate::{exercise::RunnableExercise, info_file::ExerciseInfo};
|
||||||
|
|
||||||
/// Initial capacity of the bins buffer.
|
/// Initial capacity of the bins buffer.
|
||||||
pub const BINS_BUFFER_CAPACITY: usize = 1 << 14;
|
pub const BINS_BUFFER_CAPACITY: usize = 1 << 14;
|
||||||
|
|
10
src/cmd.rs
10
src/cmd.rs
|
@ -74,12 +74,14 @@ impl CmdRunner {
|
||||||
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
|
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_dir = serde_json::de::from_slice::<CargoMetadata>(&metadata_output.stdout)
|
let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout)
|
||||||
.context(
|
.context(
|
||||||
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
|
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
|
||||||
)?.target_directory;
|
)?;
|
||||||
|
|
||||||
Ok(Self { target_dir })
|
Ok(Self {
|
||||||
|
target_dir: metadata.target_directory,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cargo<'out>(
|
pub fn cargo<'out>(
|
||||||
|
@ -123,7 +125,7 @@ pub struct CargoSubcommand<'out> {
|
||||||
output: Option<&'out mut Vec<u8>>,
|
output: Option<&'out mut Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'out> CargoSubcommand<'out> {
|
impl CargoSubcommand<'_> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
|
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
|
||||||
where
|
where
|
||||||
|
|
|
@ -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())
|
|
||||||
}
|
|
142
src/dev/check.rs
142
src/dev/check.rs
|
@ -1,6 +1,7 @@
|
||||||
use anyhow::{anyhow, bail, Context, Error, Result};
|
use anyhow::{anyhow, bail, Context, Error, Result};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
|
collections::HashSet,
|
||||||
fs::{self, read_dir, OpenOptions},
|
fs::{self, read_dir, OpenOptions},
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -11,12 +12,14 @@ use std::{
|
||||||
use crate::{
|
use crate::{
|
||||||
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
|
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
|
||||||
cmd::CmdRunner,
|
cmd::CmdRunner,
|
||||||
collections::{hash_set_with_capacity, HashSet},
|
|
||||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
||||||
info_file::{ExerciseInfo, InfoFile},
|
info_file::{ExerciseInfo, InfoFile},
|
||||||
CURRENT_FORMAT_VERSION,
|
CURRENT_FORMAT_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_N_EXERCISES: usize = 999;
|
||||||
|
const MAX_EXERCISE_NAME_LEN: usize = 32;
|
||||||
|
|
||||||
// Find a char that isn't allowed in the exercise's `name` or `dir`.
|
// Find a char that isn't allowed in the exercise's `name` or `dir`.
|
||||||
fn forbidden_char(input: &str) -> Option<char> {
|
fn forbidden_char(input: &str) -> Option<char> {
|
||||||
input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
|
input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
|
||||||
|
@ -39,10 +42,10 @@ fn check_cargo_toml(
|
||||||
|
|
||||||
if old_bins != new_bins {
|
if old_bins != new_bins {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
|
bail!("The file `dev/Cargo.toml` is outdated. Run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
|
bail!("The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -50,8 +53,8 @@ fn check_cargo_toml(
|
||||||
|
|
||||||
// Check the info of all exercises and return their paths in a set.
|
// Check the info of all exercises and return their paths in a set.
|
||||||
fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||||
let mut names = hash_set_with_capacity(info_file.exercises.len());
|
let mut names = HashSet::with_capacity(info_file.exercises.len());
|
||||||
let mut paths = hash_set_with_capacity(info_file.exercises.len());
|
let mut paths = HashSet::with_capacity(info_file.exercises.len());
|
||||||
|
|
||||||
let mut file_buf = String::with_capacity(1 << 14);
|
let mut file_buf = String::with_capacity(1 << 14);
|
||||||
for exercise_info in &info_file.exercises {
|
for exercise_info in &info_file.exercises {
|
||||||
|
@ -59,6 +62,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
bail!("Found an empty exercise name in `info.toml`");
|
bail!("Found an empty exercise name in `info.toml`");
|
||||||
}
|
}
|
||||||
|
if name.len() > MAX_EXERCISE_NAME_LEN {
|
||||||
|
bail!("The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN}");
|
||||||
|
}
|
||||||
if let Some(c) = forbidden_char(name) {
|
if let Some(c) = forbidden_char(name) {
|
||||||
bail!("Char `{c}` in the exercise name `{name}` is not allowed");
|
bail!("Char `{c}` in the exercise name `{name}` is not allowed");
|
||||||
}
|
}
|
||||||
|
@ -97,7 +103,12 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||||
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
|
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exercise_info.test && file_buf.contains("#[test]") {
|
let contains_tests = file_buf.contains("#[test]\n");
|
||||||
|
if exercise_info.test {
|
||||||
|
if !contains_tests {
|
||||||
|
bail!("The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file");
|
||||||
|
}
|
||||||
|
} else if contains_tests {
|
||||||
bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file");
|
bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,11 +171,13 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
|
fn check_exercises_unsolved(
|
||||||
println!(
|
info_file: &'static InfoFile,
|
||||||
"Running all exercises to check that they aren't already solved. This may take a while…\n",
|
cmd_runner: &'static CmdRunner,
|
||||||
);
|
) -> Result<()> {
|
||||||
thread::scope(|s| {
|
let mut stdout = io::stdout().lock();
|
||||||
|
stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?;
|
||||||
|
|
||||||
let handles = info_file
|
let handles = info_file
|
||||||
.exercises
|
.exercises
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -173,48 +186,61 @@ fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Res
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((
|
Some(
|
||||||
exercise_info.name.as_str(),
|
thread::Builder::new()
|
||||||
s.spawn(|| exercise_info.run_exercise(None, cmd_runner)),
|
.spawn(|| exercise_info.run_exercise(None, cmd_runner))
|
||||||
))
|
.map(|handle| (exercise_info.name.as_str(), handle)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.context("Failed to spawn a thread to check if an exercise is already solved")?;
|
||||||
|
|
||||||
|
let n_handles = handles.len();
|
||||||
|
write!(stdout, "Progress: 0/{n_handles}")?;
|
||||||
|
stdout.flush()?;
|
||||||
|
let mut handle_num = 1;
|
||||||
|
|
||||||
for (exercise_name, handle) in handles {
|
for (exercise_name, handle) in handles {
|
||||||
let Ok(result) = handle.join() else {
|
let Ok(result) = handle.join() else {
|
||||||
bail!("Panic while trying to run the exericse {exercise_name}");
|
bail!("Panic while trying to run the exercise {exercise_name}");
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(true) => bail!(
|
Ok(true) => {
|
||||||
"The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",
|
bail!("The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",)
|
||||||
),
|
}
|
||||||
Ok(false) => (),
|
Ok(false) => (),
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
|
||||||
|
stdout.flush()?;
|
||||||
|
handle_num += 1;
|
||||||
}
|
}
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
|
fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> {
|
||||||
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
|
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
|
||||||
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
|
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
|
||||||
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
|
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
|
||||||
Ordering::Equal => (),
|
Ordering::Equal => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
let info_file_paths = check_info_file_exercises(info_file)?;
|
let handle = thread::Builder::new()
|
||||||
let handle = thread::spawn(move || check_unexpected_files("exercises", &info_file_paths));
|
.spawn(move || check_exercises_unsolved(info_file, cmd_runner))
|
||||||
|
.context("Failed to spawn a thread to check if any exercise is already solved")?;
|
||||||
|
|
||||||
|
let info_file_paths = check_info_file_exercises(info_file)?;
|
||||||
|
check_unexpected_files("exercises", &info_file_paths)?;
|
||||||
|
|
||||||
check_exercises_unsolved(info_file, cmd_runner)?;
|
|
||||||
handle.join().unwrap()
|
handle.join().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SolutionCheck {
|
enum SolutionCheck {
|
||||||
Success { sol_path: String },
|
Success { sol_path: String },
|
||||||
MissingRequired,
|
|
||||||
MissingOptional,
|
MissingOptional,
|
||||||
RunFailure { output: Vec<u8> },
|
RunFailure { output: Vec<u8> },
|
||||||
Err(Error),
|
Err(Error),
|
||||||
|
@ -222,20 +248,24 @@ enum SolutionCheck {
|
||||||
|
|
||||||
fn check_solutions(
|
fn check_solutions(
|
||||||
require_solutions: bool,
|
require_solutions: bool,
|
||||||
info_file: &InfoFile,
|
info_file: &'static InfoFile,
|
||||||
cmd_runner: &CmdRunner,
|
cmd_runner: &'static CmdRunner,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
println!("Running all solutions. This may take a while…\n");
|
let mut stdout = io::stdout().lock();
|
||||||
thread::scope(|s| {
|
stdout.write_all(b"Running all solutions...\n")?;
|
||||||
|
|
||||||
let handles = info_file
|
let handles = info_file
|
||||||
.exercises
|
.exercises
|
||||||
.iter()
|
.iter()
|
||||||
.map(|exercise_info| {
|
.map(|exercise_info| {
|
||||||
s.spawn(|| {
|
thread::Builder::new().spawn(move || {
|
||||||
let sol_path = exercise_info.sol_path();
|
let sol_path = exercise_info.sol_path();
|
||||||
if !Path::new(&sol_path).exists() {
|
if !Path::new(&sol_path).exists() {
|
||||||
if require_solutions {
|
if require_solutions {
|
||||||
return SolutionCheck::MissingRequired;
|
return SolutionCheck::Err(anyhow!(
|
||||||
|
"The solution of the exercise {} is missing",
|
||||||
|
exercise_info.name,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return SolutionCheck::MissingOptional;
|
return SolutionCheck::MissingOptional;
|
||||||
|
@ -249,9 +279,10 @@ fn check_solutions(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.context("Failed to spawn a thread to check a solution")?;
|
||||||
|
|
||||||
let mut sol_paths = hash_set_with_capacity(info_file.exercises.len());
|
let mut sol_paths = HashSet::with_capacity(info_file.exercises.len());
|
||||||
let mut fmt_cmd = Command::new("rustfmt");
|
let mut fmt_cmd = Command::new("rustfmt");
|
||||||
fmt_cmd
|
fmt_cmd
|
||||||
.arg("--check")
|
.arg("--check")
|
||||||
|
@ -261,10 +292,15 @@ fn check_solutions(
|
||||||
.arg("always")
|
.arg("always")
|
||||||
.stdin(Stdio::null());
|
.stdin(Stdio::null());
|
||||||
|
|
||||||
|
let n_handles = handles.len();
|
||||||
|
write!(stdout, "Progress: 0/{n_handles}")?;
|
||||||
|
stdout.flush()?;
|
||||||
|
let mut handle_num = 1;
|
||||||
|
|
||||||
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
|
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
|
||||||
let Ok(check_result) = handle.join() else {
|
let Ok(check_result) = handle.join() else {
|
||||||
bail!(
|
bail!(
|
||||||
"Panic while trying to run the solution of the exericse {}",
|
"Panic while trying to run the solution of the exercise {}",
|
||||||
exercise_info.name,
|
exercise_info.name,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -274,15 +310,10 @@ fn check_solutions(
|
||||||
fmt_cmd.arg(&sol_path);
|
fmt_cmd.arg(&sol_path);
|
||||||
sol_paths.insert(PathBuf::from(sol_path));
|
sol_paths.insert(PathBuf::from(sol_path));
|
||||||
}
|
}
|
||||||
SolutionCheck::MissingRequired => {
|
|
||||||
bail!(
|
|
||||||
"The solution of the exercise {} is missing",
|
|
||||||
exercise_info.name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
SolutionCheck::MissingOptional => (),
|
SolutionCheck::MissingOptional => (),
|
||||||
SolutionCheck::RunFailure { output } => {
|
SolutionCheck::RunFailure { output } => {
|
||||||
io::stderr().lock().write_all(&output)?;
|
stdout.write_all(b"\n\n")?;
|
||||||
|
stdout.write_all(&output)?;
|
||||||
bail!(
|
bail!(
|
||||||
"Running the solution of the exercise {} failed with the error above",
|
"Running the solution of the exercise {} failed with the error above",
|
||||||
exercise_info.name,
|
exercise_info.name,
|
||||||
|
@ -290,9 +321,18 @@ fn check_solutions(
|
||||||
}
|
}
|
||||||
SolutionCheck::Err(e) => return Err(e),
|
SolutionCheck::Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths));
|
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
|
||||||
|
stdout.flush()?;
|
||||||
|
handle_num += 1;
|
||||||
|
}
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
|
||||||
|
let handle = thread::Builder::new()
|
||||||
|
.spawn(move || check_unexpected_files("solutions", &sol_paths))
|
||||||
|
.context(
|
||||||
|
"Failed to spawn a thread to check for unexpected files in the solutions directory",
|
||||||
|
)?;
|
||||||
|
|
||||||
if !fmt_cmd
|
if !fmt_cmd
|
||||||
.status()
|
.status()
|
||||||
|
@ -303,12 +343,15 @@ fn check_solutions(
|
||||||
}
|
}
|
||||||
|
|
||||||
handle.join().unwrap()
|
handle.join().unwrap()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(require_solutions: bool) -> Result<()> {
|
pub fn check(require_solutions: bool) -> Result<()> {
|
||||||
let info_file = InfoFile::parse()?;
|
let info_file = InfoFile::parse()?;
|
||||||
|
|
||||||
|
if info_file.exercises.len() > MAX_N_EXERCISES {
|
||||||
|
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
|
||||||
|
}
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
// A hack to make `cargo run -- dev check` work when developing Rustlings.
|
// A hack to make `cargo run -- dev check` work when developing Rustlings.
|
||||||
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
|
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
|
||||||
|
@ -316,9 +359,12 @@ pub fn check(require_solutions: bool) -> Result<()> {
|
||||||
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
|
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmd_runner = CmdRunner::build()?;
|
// Leaking is fine since they are used until the end of the program.
|
||||||
check_exercises(&info_file, &cmd_runner)?;
|
let cmd_runner = Box::leak(Box::new(CmdRunner::build()?));
|
||||||
check_solutions(require_solutions, &info_file, &cmd_runner)?;
|
let info_file = Box::leak(Box::new(info_file));
|
||||||
|
|
||||||
|
check_exercises(info_file, cmd_runner)?;
|
||||||
|
check_solutions(require_solutions, info_file, cmd_runner)?;
|
||||||
|
|
||||||
println!("Everything looks fine!");
|
println!("Everything looks fine!");
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
||||||
process::Command,
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::CURRENT_FORMAT_VERSION;
|
use crate::{init::RUST_ANALYZER_TOML, CURRENT_FORMAT_VERSION};
|
||||||
|
|
||||||
// Create a directory relative to the current directory and print its path.
|
// Create a directory relative to the current directory and print its path.
|
||||||
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
|
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
|
||||||
|
@ -62,6 +62,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
|
||||||
|
|
||||||
write_rel_file("README.md", &dir_path_str, README)?;
|
write_rel_file("README.md", &dir_path_str, README)?;
|
||||||
|
|
||||||
|
write_rel_file("rust-analyzer.toml", &dir_path_str, RUST_ANALYZER_TOML)?;
|
||||||
|
|
||||||
create_rel_dir(".vscode", &dir_path_str)?;
|
create_rel_dir(".vscode", &dir_path_str)?;
|
||||||
write_rel_file(
|
write_rel_file(
|
||||||
".vscode/extensions.json",
|
".vscode/extensions.json",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{Context, Error, Result};
|
use anyhow::{Context, Error, Result};
|
||||||
use std::{
|
use std::{
|
||||||
fs::{create_dir, OpenOptions},
|
fs::{self, create_dir},
|
||||||
io::{self, Write},
|
io,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::info_file::ExerciseInfo;
|
use crate::info_file::ExerciseInfo;
|
||||||
|
@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo;
|
||||||
/// Contains all embedded files.
|
/// Contains all embedded files.
|
||||||
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum WriteStrategy {
|
|
||||||
IfNotExists,
|
|
||||||
Overwrite,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WriteStrategy {
|
|
||||||
fn write(self, path: &str, content: &[u8]) -> Result<()> {
|
|
||||||
let file = match self {
|
|
||||||
Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path),
|
|
||||||
Self::Overwrite => OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(path),
|
|
||||||
};
|
|
||||||
|
|
||||||
file.with_context(|| format!("Failed to open the file `{path}` in write mode"))?
|
|
||||||
.write_all(content)
|
|
||||||
.with_context(|| format!("Failed to write the file {path}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files related to one exercise.
|
// Files related to one exercise.
|
||||||
struct ExerciseFiles {
|
struct ExerciseFiles {
|
||||||
// The content of the exercise file.
|
// The content of the exercise file.
|
||||||
|
@ -42,6 +19,16 @@ struct ExerciseFiles {
|
||||||
dir_ind: usize,
|
dir_ind: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_dir_if_not_exists(path: &str) -> Result<()> {
|
||||||
|
if let Err(e) = create_dir(path) {
|
||||||
|
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||||
|
return Err(Error::from(e).context(format!("Failed to create the directory {path}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// A directory in the `exercises/` directory.
|
// A directory in the `exercises/` directory.
|
||||||
pub struct ExerciseDir {
|
pub struct ExerciseDir {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
|
@ -55,21 +42,13 @@ impl ExerciseDir {
|
||||||
let mut dir_path = String::with_capacity(20 + self.name.len());
|
let mut dir_path = String::with_capacity(20 + self.name.len());
|
||||||
dir_path.push_str("exercises/");
|
dir_path.push_str("exercises/");
|
||||||
dir_path.push_str(self.name);
|
dir_path.push_str(self.name);
|
||||||
|
create_dir_if_not_exists(&dir_path)?;
|
||||||
if let Err(e) = create_dir(&dir_path) {
|
|
||||||
if e.kind() == io::ErrorKind::AlreadyExists {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(
|
|
||||||
Error::from(e).context(format!("Failed to create the directory {dir_path}"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut readme_path = dir_path;
|
let mut readme_path = dir_path;
|
||||||
readme_path.push_str("/README.md");
|
readme_path.push_str("/README.md");
|
||||||
|
|
||||||
WriteStrategy::Overwrite.write(&readme_path, self.readme)
|
fs::write(&readme_path, self.readme)
|
||||||
|
.with_context(|| format!("Failed to write the file {readme_path}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,17 +65,31 @@ impl EmbeddedFiles {
|
||||||
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
||||||
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
||||||
|
|
||||||
WriteStrategy::IfNotExists.write(
|
fs::write(
|
||||||
"exercises/README.md",
|
"exercises/README.md",
|
||||||
include_bytes!("../exercises/README.md"),
|
include_bytes!("../exercises/README.md"),
|
||||||
)?;
|
)
|
||||||
|
.context("Failed to write the file exercises/README.md")?;
|
||||||
|
|
||||||
for dir in self.exercise_dirs {
|
for dir in self.exercise_dirs {
|
||||||
dir.init_on_disk()?;
|
dir.init_on_disk()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut exercise_path = String::with_capacity(64);
|
||||||
|
let prefix = "exercises/";
|
||||||
|
exercise_path.push_str(prefix);
|
||||||
|
|
||||||
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
|
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
|
||||||
WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?;
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
|
exercise_path.truncate(prefix.len());
|
||||||
|
exercise_path.push_str(dir.name);
|
||||||
|
exercise_path.push('/');
|
||||||
|
exercise_path.push_str(&exercise_info.name);
|
||||||
|
exercise_path.push_str(".rs");
|
||||||
|
|
||||||
|
fs::write(&exercise_path, exercise_files.exercise)
|
||||||
|
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -107,7 +100,8 @@ impl EmbeddedFiles {
|
||||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
dir.init_on_disk()?;
|
dir.init_on_disk()?;
|
||||||
WriteStrategy::Overwrite.write(path, exercise_files.exercise)
|
fs::write(path, exercise_files.exercise)
|
||||||
|
.with_context(|| format!("Failed to write the exercise file {path}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the solution file to disk and return its path.
|
/// Write the solution file to disk and return its path.
|
||||||
|
@ -116,19 +110,25 @@ impl EmbeddedFiles {
|
||||||
exercise_ind: usize,
|
exercise_ind: usize,
|
||||||
exercise_name: &str,
|
exercise_name: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
create_dir_if_not_exists("solutions")?;
|
||||||
|
|
||||||
let exercise_files = &self.exercise_files[exercise_ind];
|
let exercise_files = &self.exercise_files[exercise_ind];
|
||||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
// 14 = 10 + 1 + 3
|
// 14 = 10 + 1 + 3
|
||||||
// solutions/ + / + .rs
|
// solutions/ + / + .rs
|
||||||
let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||||
solution_path.push_str("solutions/");
|
dir_path.push_str("solutions/");
|
||||||
solution_path.push_str(dir.name);
|
dir_path.push_str(dir.name);
|
||||||
|
create_dir_if_not_exists(&dir_path)?;
|
||||||
|
|
||||||
|
let mut solution_path = dir_path;
|
||||||
solution_path.push('/');
|
solution_path.push('/');
|
||||||
solution_path.push_str(exercise_name);
|
solution_path.push_str(exercise_name);
|
||||||
solution_path.push_str(".rs");
|
solution_path.push_str(".rs");
|
||||||
|
|
||||||
WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?;
|
fs::write(&solution_path, exercise_files.solution)
|
||||||
|
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
|
||||||
|
|
||||||
Ok(solution_path)
|
Ok(solution_path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,31 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ratatui::crossterm::style::{style, StyledContent, Stylize};
|
use crossterm::{
|
||||||
use std::{
|
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||||
fmt::{self, Display, Formatter},
|
QueueableCommand,
|
||||||
io::Write,
|
|
||||||
};
|
};
|
||||||
|
use std::io::{self, StdoutLock, Write};
|
||||||
|
|
||||||
use crate::{cmd::CmdRunner, terminal_link::TerminalFileLink};
|
use crate::{
|
||||||
|
cmd::CmdRunner,
|
||||||
|
term::{self, terminal_file_link, write_ansi, CountedWrite},
|
||||||
|
};
|
||||||
|
|
||||||
/// The initial capacity of the output buffer.
|
/// The initial capacity of the output buffer.
|
||||||
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
||||||
|
|
||||||
|
pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::Result<()> {
|
||||||
|
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||||
|
stdout.write_all(b"Solution")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b" for comparison: ")?;
|
||||||
|
if let Some(canonical_path) = term::canonicalize(solution_path) {
|
||||||
|
terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?;
|
||||||
|
} else {
|
||||||
|
stdout.write_all(solution_path.as_bytes())?;
|
||||||
|
}
|
||||||
|
stdout.write_all(b"\n")
|
||||||
|
}
|
||||||
|
|
||||||
// Run an exercise binary and append its output to the `output` buffer.
|
// Run an exercise binary and append its output to the `output` buffer.
|
||||||
// Compilation must be done before calling this method.
|
// Compilation must be done before calling this method.
|
||||||
fn run_bin(
|
fn run_bin(
|
||||||
|
@ -18,7 +34,10 @@ fn run_bin(
|
||||||
cmd_runner: &CmdRunner,
|
cmd_runner: &CmdRunner,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
if let Some(output) = output.as_deref_mut() {
|
if let Some(output) = output.as_deref_mut() {
|
||||||
writeln!(output, "{}", "Output".underlined())?;
|
write_ansi(output, SetAttribute(Attribute::Underlined));
|
||||||
|
output.extend_from_slice(b"Output");
|
||||||
|
write_ansi(output, ResetColor);
|
||||||
|
output.push(b'\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
|
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
|
||||||
|
@ -28,13 +47,11 @@ fn run_bin(
|
||||||
// This output is important to show the user that something went wrong.
|
// This output is important to show the user that something went wrong.
|
||||||
// Otherwise, calling something like `exit(1)` in an exercise without further output
|
// Otherwise, calling something like `exit(1)` in an exercise without further output
|
||||||
// leaves the user confused about why the exercise isn't done yet.
|
// leaves the user confused about why the exercise isn't done yet.
|
||||||
writeln!(
|
write_ansi(output, SetAttribute(Attribute::Bold));
|
||||||
output,
|
write_ansi(output, SetForegroundColor(Color::Red));
|
||||||
"{}",
|
output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)");
|
||||||
"The exercise didn't run successfully (nonzero exit code)"
|
write_ansi(output, ResetColor);
|
||||||
.bold()
|
output.push(b'\n');
|
||||||
.red(),
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +64,7 @@ pub struct Exercise {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
/// Path of the exercise file starting with the `exercises/` directory.
|
/// Path of the exercise file starting with the `exercises/` directory.
|
||||||
pub path: &'static str,
|
pub path: &'static str,
|
||||||
|
pub canonical_path: Option<String>,
|
||||||
pub test: bool,
|
pub test: bool,
|
||||||
pub strict_clippy: bool,
|
pub strict_clippy: bool,
|
||||||
pub hint: &'static str,
|
pub hint: &'static str,
|
||||||
|
@ -54,25 +72,24 @@ pub struct Exercise {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Exercise {
|
impl Exercise {
|
||||||
pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
|
pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> {
|
||||||
style(TerminalFileLink(self.path)).underlined().blue()
|
if let Some(canonical_path) = self.canonical_path.as_deref() {
|
||||||
|
return terminal_file_link(writer, self.path, canonical_path, Color::Blue);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Exercise {
|
writer.write_str(self.path)
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
self.path.fmt(f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RunnableExercise {
|
pub trait RunnableExercise {
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
fn dir(&self) -> Option<&str>;
|
||||||
fn strict_clippy(&self) -> bool;
|
fn strict_clippy(&self) -> bool;
|
||||||
fn test(&self) -> bool;
|
fn test(&self) -> bool;
|
||||||
|
|
||||||
// Compile, check and run the exercise or its solution (depending on `bin_name´).
|
// Compile, check and run the exercise or its solution (depending on `bin_name´).
|
||||||
// The output is written to the `output` buffer after clearing it.
|
// The output is written to the `output` buffer after clearing it.
|
||||||
fn run(
|
fn run<const FORCE_STRICT_CLIPPY: bool>(
|
||||||
&self,
|
&self,
|
||||||
bin_name: &str,
|
bin_name: &str,
|
||||||
mut output: Option<&mut Vec<u8>>,
|
mut output: Option<&mut Vec<u8>>,
|
||||||
|
@ -98,7 +115,7 @@ pub trait RunnableExercise {
|
||||||
let output_is_some = output.is_some();
|
let output_is_some = output.is_some();
|
||||||
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
|
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
|
||||||
if output_is_some {
|
if output_is_some {
|
||||||
test_cmd.args(["--", "--color", "always", "--show-output"]);
|
test_cmd.args(["--", "--color", "always", "--format", "pretty"]);
|
||||||
}
|
}
|
||||||
let test_success = test_cmd.run("cargo test …")?;
|
let test_success = test_cmd.run("cargo test …")?;
|
||||||
if !test_success {
|
if !test_success {
|
||||||
|
@ -114,8 +131,8 @@ pub trait RunnableExercise {
|
||||||
|
|
||||||
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
|
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
|
||||||
|
|
||||||
// `--profile test` is required to also check code with `[cfg(test)]`.
|
// `--profile test` is required to also check code with `#[cfg(test)]`.
|
||||||
if self.strict_clippy() {
|
if FORCE_STRICT_CLIPPY || self.strict_clippy() {
|
||||||
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
|
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
|
||||||
} else {
|
} else {
|
||||||
clippy_cmd.args(["--profile", "test"]);
|
clippy_cmd.args(["--profile", "test"]);
|
||||||
|
@ -131,7 +148,7 @@ pub trait RunnableExercise {
|
||||||
/// The output is written to the `output` buffer after clearing it.
|
/// The output is written to the `output` buffer after clearing it.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
|
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
|
||||||
self.run(self.name(), output, cmd_runner)
|
self.run::<false>(self.name(), output, cmd_runner)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile, check and run the exercise's solution.
|
/// Compile, check and run the exercise's solution.
|
||||||
|
@ -142,7 +159,32 @@ pub trait RunnableExercise {
|
||||||
bin_name.push_str(name);
|
bin_name.push_str(name);
|
||||||
bin_name.push_str("_sol");
|
bin_name.push_str("_sol");
|
||||||
|
|
||||||
self.run(&bin_name, output, cmd_runner)
|
self.run::<true>(&bin_name, output, cmd_runner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sol_path(&self) -> String {
|
||||||
|
let name = self.name();
|
||||||
|
|
||||||
|
let mut path = if let Some(dir) = self.dir() {
|
||||||
|
// 14 = 10 + 1 + 3
|
||||||
|
// solutions/ + / + .rs
|
||||||
|
let mut path = String::with_capacity(14 + dir.len() + name.len());
|
||||||
|
path.push_str("solutions/");
|
||||||
|
path.push_str(dir);
|
||||||
|
path.push('/');
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
// 13 = 10 + 3
|
||||||
|
// solutions/ + .rs
|
||||||
|
let mut path = String::with_capacity(13 + name.len());
|
||||||
|
path.push_str("solutions/");
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
path.push_str(name);
|
||||||
|
path.push_str(".rs");
|
||||||
|
|
||||||
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +194,11 @@ impl RunnableExercise for Exercise {
|
||||||
self.name
|
self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn dir(&self) -> Option<&str> {
|
||||||
|
self.dir
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strict_clippy(&self) -> bool {
|
fn strict_clippy(&self) -> bool {
|
||||||
self.strict_clippy
|
self.strict_clippy
|
||||||
|
|
|
@ -52,30 +52,6 @@ impl ExerciseInfo {
|
||||||
|
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path to the solution file starting with the `solutions/` directory.
|
|
||||||
pub fn sol_path(&self) -> String {
|
|
||||||
let mut path = if let Some(dir) = &self.dir {
|
|
||||||
// 14 = 10 + 1 + 3
|
|
||||||
// solutions/ + / + .rs
|
|
||||||
let mut path = String::with_capacity(14 + dir.len() + self.name.len());
|
|
||||||
path.push_str("solutions/");
|
|
||||||
path.push_str(dir);
|
|
||||||
path.push('/');
|
|
||||||
path
|
|
||||||
} else {
|
|
||||||
// 13 = 10 + 3
|
|
||||||
// solutions/ + .rs
|
|
||||||
let mut path = String::with_capacity(13 + self.name.len());
|
|
||||||
path.push_str("solutions/");
|
|
||||||
path
|
|
||||||
};
|
|
||||||
|
|
||||||
path.push_str(&self.name);
|
|
||||||
path.push_str(".rs");
|
|
||||||
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunnableExercise for ExerciseInfo {
|
impl RunnableExercise for ExerciseInfo {
|
||||||
|
@ -84,6 +60,11 @@ impl RunnableExercise for ExerciseInfo {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn dir(&self) -> Option<&str> {
|
||||||
|
self.dir.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strict_clippy(&self) -> bool {
|
fn strict_clippy(&self) -> bool {
|
||||||
self.strict_clippy
|
self.strict_clippy
|
||||||
|
@ -135,4 +116,4 @@ impl InfoFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
|
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
|
||||||
If you are developing third-party exercises, add at least one exercise before testing.";
|
Add at least one exercise before testing.";
|
||||||
|
|
36
src/init.rs
36
src/init.rs
|
@ -1,5 +1,8 @@
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use ratatui::crossterm::style::Stylize;
|
use crossterm::{
|
||||||
|
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||||
|
QueueableCommand,
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
env::set_current_dir,
|
env::set_current_dir,
|
||||||
|
@ -10,8 +13,8 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile,
|
cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, exercise::RunnableExercise,
|
||||||
term::press_enter_prompt,
|
info_file::InfoFile, term::press_enter_prompt,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -127,6 +130,9 @@ pub fn init() -> Result<()> {
|
||||||
fs::write("Cargo.toml", updated_cargo_toml)
|
fs::write("Cargo.toml", updated_cargo_toml)
|
||||||
.context("Failed to create the file `rustlings/Cargo.toml`")?;
|
.context("Failed to create the file `rustlings/Cargo.toml`")?;
|
||||||
|
|
||||||
|
fs::write("rust-analyzer.toml", RUST_ANALYZER_TOML)
|
||||||
|
.context("Failed to create the file `rustlings/rust-analyzer.toml`")?;
|
||||||
|
|
||||||
fs::write(".gitignore", GITIGNORE)
|
fs::write(".gitignore", GITIGNORE)
|
||||||
.context("Failed to create the file `rustlings/.gitignore`")?;
|
.context("Failed to create the file `rustlings/.gitignore`")?;
|
||||||
|
|
||||||
|
@ -139,16 +145,19 @@ pub fn init() -> Result<()> {
|
||||||
let _ = Command::new("git")
|
let _ = Command::new("git")
|
||||||
.arg("init")
|
.arg("init")
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.status();
|
.status();
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(
|
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||||
stdout,
|
stdout.write_all("Initialization done ✓".as_bytes())?;
|
||||||
"\n{}\n\n{}",
|
stdout.queue(ResetColor)?;
|
||||||
"Initialization done ✓".green(),
|
stdout.write_all(b"\n\n")?;
|
||||||
POST_INIT_MSG.bold(),
|
|
||||||
)?;
|
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||||
|
stdout.write_all(POST_INIT_MSG)?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -163,6 +172,10 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|
||||||
|
pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy"
|
||||||
|
check.extraArgs = ["--profile", "test"]
|
||||||
|
"#;
|
||||||
|
|
||||||
const GITIGNORE: &[u8] = b"Cargo.lock
|
const GITIGNORE: &[u8] = b"Cargo.lock
|
||||||
target/
|
target/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
@ -181,5 +194,6 @@ You probably already initialized Rustlings.
|
||||||
Run `cd rustlings`
|
Run `cd rustlings`
|
||||||
Then run `rustlings` again";
|
Then run `rustlings` again";
|
||||||
|
|
||||||
const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
|
const POST_INIT_MSG: &[u8] = b"Run `cd rustlings` to go into the generated directory.
|
||||||
Then run `rustlings` to get started.";
|
Then run `rustlings` to get started.
|
||||||
|
";
|
||||||
|
|
196
src/list.rs
196
src/list.rs
|
@ -1,93 +1,135 @@
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use ratatui::{
|
use crossterm::{
|
||||||
backend::CrosstermBackend,
|
cursor,
|
||||||
crossterm::{
|
event::{
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
},
|
||||||
Terminal,
|
terminal::{
|
||||||
|
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
},
|
||||||
|
QueueableCommand,
|
||||||
};
|
};
|
||||||
use std::io;
|
use std::io::{self, StdoutLock, Write};
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
use self::state::{Filter, UiState};
|
use self::state::{Filter, ListState};
|
||||||
|
|
||||||
|
mod scroll_state;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
|
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
|
||||||
|
let mut list_state = ListState::build(app_state, stdout)?;
|
||||||
|
let mut is_searching = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match event::read().context("Failed to read terminal event")? {
|
||||||
|
Event::Key(key) => {
|
||||||
|
match key.kind {
|
||||||
|
KeyEventKind::Release => continue,
|
||||||
|
KeyEventKind::Press | KeyEventKind::Repeat => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
list_state.message.clear();
|
||||||
|
|
||||||
|
if is_searching {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc | KeyCode::Enter => {
|
||||||
|
is_searching = false;
|
||||||
|
list_state.search_query.clear();
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
list_state.search_query.push(c);
|
||||||
|
list_state.apply_search_query();
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
list_state.search_query.pop();
|
||||||
|
list_state.apply_search_query();
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
list_state.draw(stdout)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') => return Ok(()),
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => list_state.select_previous(),
|
||||||
|
KeyCode::Home | KeyCode::Char('g') => list_state.select_first(),
|
||||||
|
KeyCode::End | KeyCode::Char('G') => list_state.select_last(),
|
||||||
|
KeyCode::Char('d') => {
|
||||||
|
if list_state.filter() == Filter::Done {
|
||||||
|
list_state.set_filter(Filter::None);
|
||||||
|
list_state.message.push_str("Disabled filter DONE");
|
||||||
|
} else {
|
||||||
|
list_state.set_filter(Filter::Done);
|
||||||
|
list_state.message.push_str(
|
||||||
|
"Enabled filter DONE │ Press d again to disable the filter",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('p') => {
|
||||||
|
if list_state.filter() == Filter::Pending {
|
||||||
|
list_state.set_filter(Filter::None);
|
||||||
|
list_state.message.push_str("Disabled filter PENDING");
|
||||||
|
} else {
|
||||||
|
list_state.set_filter(Filter::Pending);
|
||||||
|
list_state.message.push_str(
|
||||||
|
"Enabled filter PENDING │ Press p again to disable the filter",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('r') => list_state.reset_selected()?,
|
||||||
|
KeyCode::Char('c') => {
|
||||||
|
if list_state.selected_to_current_exercise()? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('s' | '/') => {
|
||||||
|
is_searching = true;
|
||||||
|
list_state.apply_search_query();
|
||||||
|
}
|
||||||
|
// Redraw to remove the message.
|
||||||
|
KeyCode::Esc => (),
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(event) => match event.kind {
|
||||||
|
MouseEventKind::ScrollDown => list_state.select_next(),
|
||||||
|
MouseEventKind::ScrollUp => list_state.select_previous(),
|
||||||
|
_ => continue,
|
||||||
|
},
|
||||||
|
Event::Resize(width, height) => list_state.set_term_size(width, height),
|
||||||
|
// Ignore
|
||||||
|
Event::FocusGained | Event::FocusLost => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
list_state.draw(stdout)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list(app_state: &mut AppState) -> Result<()> {
|
pub fn list(app_state: &mut AppState) -> Result<()> {
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
stdout.execute(EnterAlternateScreen)?;
|
stdout
|
||||||
|
.queue(EnterAlternateScreen)?
|
||||||
|
.queue(cursor::Hide)?
|
||||||
|
.queue(DisableLineWrap)?
|
||||||
|
.queue(EnableMouseCapture)?;
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
|
let res = handle_list(app_state, &mut stdout);
|
||||||
terminal.clear()?;
|
|
||||||
|
|
||||||
let mut ui_state = UiState::new(app_state);
|
// Restore the terminal even if we got an error.
|
||||||
|
stdout
|
||||||
'outer: loop {
|
.queue(LeaveAlternateScreen)?
|
||||||
terminal.try_draw(|frame| ui_state.draw(frame).map_err(io::Error::other))?;
|
.queue(cursor::Show)?
|
||||||
|
.queue(EnableLineWrap)?
|
||||||
let key = loop {
|
.queue(DisableMouseCapture)?
|
||||||
match event::read()? {
|
.flush()?;
|
||||||
Event::Key(key) => match key.kind {
|
|
||||||
KeyEventKind::Press | KeyEventKind::Repeat => break key,
|
|
||||||
KeyEventKind::Release => (),
|
|
||||||
},
|
|
||||||
// Redraw
|
|
||||||
Event::Resize(_, _) => continue 'outer,
|
|
||||||
// Ignore
|
|
||||||
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ui_state.message.clear();
|
|
||||||
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('q') => break,
|
|
||||||
KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(),
|
|
||||||
KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(),
|
|
||||||
KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(),
|
|
||||||
KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
|
|
||||||
KeyCode::Char('d') => {
|
|
||||||
let message = if ui_state.filter == Filter::Done {
|
|
||||||
ui_state.filter = Filter::None;
|
|
||||||
"Disabled filter DONE"
|
|
||||||
} else {
|
|
||||||
ui_state.filter = Filter::Done;
|
|
||||||
"Enabled filter DONE │ Press d again to disable the filter"
|
|
||||||
};
|
|
||||||
|
|
||||||
ui_state = ui_state.with_updated_rows();
|
|
||||||
ui_state.message.push_str(message);
|
|
||||||
}
|
|
||||||
KeyCode::Char('p') => {
|
|
||||||
let message = if ui_state.filter == Filter::Pending {
|
|
||||||
ui_state.filter = Filter::None;
|
|
||||||
"Disabled filter PENDING"
|
|
||||||
} else {
|
|
||||||
ui_state.filter = Filter::Pending;
|
|
||||||
"Enabled filter PENDING │ Press p again to disable the filter"
|
|
||||||
};
|
|
||||||
|
|
||||||
ui_state = ui_state.with_updated_rows();
|
|
||||||
ui_state.message.push_str(message);
|
|
||||||
}
|
|
||||||
KeyCode::Char('r') => {
|
|
||||||
ui_state = ui_state.with_reset_selected()?;
|
|
||||||
}
|
|
||||||
KeyCode::Char('c') => {
|
|
||||||
ui_state.selected_to_current_exercise()?;
|
|
||||||
ui_state = ui_state.with_updated_rows();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(terminal);
|
|
||||||
stdout.execute(LeaveAlternateScreen)?;
|
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
|
|
||||||
Ok(())
|
res
|
||||||
}
|
}
|
||||||
|
|
104
src/list/scroll_state.rs
Normal file
104
src/list/scroll_state.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
pub struct ScrollState {
|
||||||
|
n_rows: usize,
|
||||||
|
max_n_rows_to_display: usize,
|
||||||
|
selected: Option<usize>,
|
||||||
|
offset: usize,
|
||||||
|
scroll_padding: usize,
|
||||||
|
max_scroll_padding: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollState {
|
||||||
|
pub fn new(n_rows: usize, selected: Option<usize>, max_scroll_padding: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
n_rows,
|
||||||
|
max_n_rows_to_display: 0,
|
||||||
|
selected,
|
||||||
|
offset: selected.map_or(0, |selected| selected.saturating_sub(max_scroll_padding)),
|
||||||
|
scroll_padding: 0,
|
||||||
|
max_scroll_padding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn offset(&self) -> usize {
|
||||||
|
self.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_offset(&mut self) {
|
||||||
|
let Some(selected) = self.selected else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let min_offset = (selected + self.scroll_padding)
|
||||||
|
.saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
|
||||||
|
let max_offset = selected.saturating_sub(self.scroll_padding);
|
||||||
|
let global_max_offset = self.n_rows.saturating_sub(self.max_n_rows_to_display);
|
||||||
|
|
||||||
|
self.offset = self
|
||||||
|
.offset
|
||||||
|
.max(min_offset)
|
||||||
|
.min(max_offset)
|
||||||
|
.min(global_max_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn selected(&self) -> Option<usize> {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_selected(&mut self, selected: usize) {
|
||||||
|
self.selected = Some(selected);
|
||||||
|
self.update_offset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next(&mut self) {
|
||||||
|
if let Some(selected) = self.selected {
|
||||||
|
self.set_selected((selected + 1).min(self.n_rows - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous(&mut self) {
|
||||||
|
if let Some(selected) = self.selected {
|
||||||
|
self.set_selected(selected.saturating_sub(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_first(&mut self) {
|
||||||
|
if self.n_rows > 0 {
|
||||||
|
self.set_selected(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_last(&mut self) {
|
||||||
|
if self.n_rows > 0 {
|
||||||
|
self.set_selected(self.n_rows - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_n_rows(&mut self, n_rows: usize) {
|
||||||
|
self.n_rows = n_rows;
|
||||||
|
|
||||||
|
if self.n_rows == 0 {
|
||||||
|
self.selected = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn update_scroll_padding(&mut self) {
|
||||||
|
self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn max_n_rows_to_display(&self) -> usize {
|
||||||
|
self.max_n_rows_to_display
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_max_n_rows_to_display(&mut self, max_n_rows_to_display: usize) {
|
||||||
|
self.max_n_rows_to_display = max_n_rows_to_display;
|
||||||
|
self.update_scroll_padding();
|
||||||
|
self.update_offset();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,36 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use ratatui::{
|
use crossterm::{
|
||||||
layout::{Constraint, Rect},
|
cursor::{MoveTo, MoveToNextLine},
|
||||||
style::{Style, Stylize},
|
style::{
|
||||||
text::{Line, Span},
|
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||||
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
|
},
|
||||||
Frame,
|
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
|
||||||
|
QueueableCommand,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt::Write as _,
|
||||||
|
io::{self, StdoutLock, Write},
|
||||||
};
|
};
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
|
use crate::{
|
||||||
|
app_state::AppState,
|
||||||
|
exercise::Exercise,
|
||||||
|
term::{progress_bar, CountedWrite, MaxLenWriter},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::scroll_state::ScrollState;
|
||||||
|
|
||||||
|
const COL_SPACING: usize = 2;
|
||||||
|
const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none()
|
||||||
|
.with(Attribute::Reverse)
|
||||||
|
.with(Attribute::Bold);
|
||||||
|
|
||||||
|
fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
stdout
|
||||||
|
.queue(Clear(ClearType::UntilNewLine))?
|
||||||
|
.queue(MoveToNextLine(1))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum Filter {
|
pub enum Filter {
|
||||||
|
@ -17,255 +39,386 @@ pub enum Filter {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiState<'a> {
|
pub struct ListState<'a> {
|
||||||
pub table: Table<'static>,
|
/// Footer message to be displayed if not empty.
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub filter: Filter,
|
pub search_query: String,
|
||||||
app_state: &'a mut AppState,
|
app_state: &'a mut AppState,
|
||||||
table_state: TableState,
|
scroll_state: ScrollState,
|
||||||
n_rows: usize,
|
name_col_padding: Vec<u8>,
|
||||||
|
path_col_padding: Vec<u8>,
|
||||||
|
filter: Filter,
|
||||||
|
term_width: u16,
|
||||||
|
term_height: u16,
|
||||||
|
show_footer: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> UiState<'a> {
|
impl<'a> ListState<'a> {
|
||||||
pub fn with_updated_rows(mut self) -> Self {
|
pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
|
||||||
let current_exercise_ind = self.app_state.current_exercise_ind();
|
stdout.queue(Clear(ClearType::All))?;
|
||||||
|
|
||||||
self.n_rows = 0;
|
let name_col_title_len = 4;
|
||||||
let rows = self
|
let path_col_title_len = 4;
|
||||||
|
let (name_col_width, path_col_width) = app_state.exercises().iter().fold(
|
||||||
|
(name_col_title_len, path_col_title_len),
|
||||||
|
|(name_col_width, path_col_width), exercise| {
|
||||||
|
(
|
||||||
|
name_col_width.max(exercise.name.len()),
|
||||||
|
path_col_width.max(exercise.path.len()),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let name_col_padding = vec![b' '; name_col_width + COL_SPACING];
|
||||||
|
let path_col_padding = vec![b' '; path_col_width];
|
||||||
|
|
||||||
|
let filter = Filter::None;
|
||||||
|
let n_rows_with_filter = app_state.exercises().len();
|
||||||
|
let selected = app_state.current_exercise_ind();
|
||||||
|
|
||||||
|
let (width, height) = terminal::size().context("Failed to get the terminal size")?;
|
||||||
|
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
|
||||||
|
|
||||||
|
let mut slf = Self {
|
||||||
|
message: String::with_capacity(128),
|
||||||
|
search_query: String::new(),
|
||||||
|
app_state,
|
||||||
|
scroll_state,
|
||||||
|
name_col_padding,
|
||||||
|
path_col_padding,
|
||||||
|
filter,
|
||||||
|
// Set by `set_term_size`
|
||||||
|
term_width: 0,
|
||||||
|
term_height: 0,
|
||||||
|
show_footer: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
slf.set_term_size(width, height);
|
||||||
|
slf.draw(stdout)?;
|
||||||
|
|
||||||
|
Ok(slf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_term_size(&mut self, width: u16, height: u16) {
|
||||||
|
self.term_width = width;
|
||||||
|
self.term_height = height;
|
||||||
|
|
||||||
|
if height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_height = 1;
|
||||||
|
// 1 progress bar, 2 footer message lines.
|
||||||
|
let footer_height = 3;
|
||||||
|
self.show_footer = height > header_height + footer_height;
|
||||||
|
|
||||||
|
self.scroll_state.set_max_n_rows_to_display(
|
||||||
|
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
|
||||||
|
as usize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_exercise_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> {
|
||||||
|
if !self.search_query.is_empty() {
|
||||||
|
if let Some((pre_highlight, highlight, post_highlight)) = exercise
|
||||||
|
.name
|
||||||
|
.find(&self.search_query)
|
||||||
|
.and_then(|ind| exercise.name.split_at_checked(ind))
|
||||||
|
.and_then(|(pre_highlight, rest)| {
|
||||||
|
rest.split_at_checked(self.search_query.len())
|
||||||
|
.map(|x| (pre_highlight, x.0, x.1))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
writer.write_str(pre_highlight)?;
|
||||||
|
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
|
||||||
|
writer.write_str(highlight)?;
|
||||||
|
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||||
|
return writer.write_str(post_highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write_str(exercise.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_rows(
|
||||||
|
&self,
|
||||||
|
stdout: &mut StdoutLock,
|
||||||
|
filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>,
|
||||||
|
) -> io::Result<usize> {
|
||||||
|
let current_exercise_ind = self.app_state.current_exercise_ind();
|
||||||
|
let row_offset = self.scroll_state.offset();
|
||||||
|
let mut n_displayed_rows = 0;
|
||||||
|
|
||||||
|
for (exercise_ind, exercise) in filtered_exercises
|
||||||
|
.skip(row_offset)
|
||||||
|
.take(self.scroll_state.max_n_rows_to_display())
|
||||||
|
{
|
||||||
|
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||||
|
|
||||||
|
if self.scroll_state.selected() == Some(row_offset + n_displayed_rows) {
|
||||||
|
// The crab emoji has the width of two ascii chars.
|
||||||
|
writer.add_to_len(2);
|
||||||
|
writer.stdout.write_all("🦀".as_bytes())?;
|
||||||
|
writer
|
||||||
|
.stdout
|
||||||
|
.queue(SetAttributes(SELECTED_ROW_ATTRIBUTES))?;
|
||||||
|
} else {
|
||||||
|
writer.write_ascii(b" ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if exercise_ind == current_exercise_ind {
|
||||||
|
writer.stdout.queue(SetForegroundColor(Color::Red))?;
|
||||||
|
writer.write_ascii(b">>>>>>> ")?;
|
||||||
|
} else {
|
||||||
|
writer.write_ascii(b" ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if exercise.done {
|
||||||
|
writer.stdout.queue(SetForegroundColor(Color::Green))?;
|
||||||
|
writer.write_ascii(b"DONE ")?;
|
||||||
|
} else {
|
||||||
|
writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
|
||||||
|
writer.write_ascii(b"PENDING")?;
|
||||||
|
}
|
||||||
|
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||||
|
writer.write_ascii(b" ")?;
|
||||||
|
|
||||||
|
self.draw_exercise_name(&mut writer, exercise)?;
|
||||||
|
|
||||||
|
writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?;
|
||||||
|
|
||||||
|
// The list links aren't shown correctly in VS Code on Windows.
|
||||||
|
// But VS Code shows its own links anyway.
|
||||||
|
if self.app_state.vs_code() {
|
||||||
|
writer.write_str(exercise.path)?;
|
||||||
|
} else {
|
||||||
|
exercise.terminal_file_link(&mut writer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?;
|
||||||
|
|
||||||
|
next_ln(stdout)?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
n_displayed_rows += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(n_displayed_rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
if self.term_height == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?;
|
||||||
|
|
||||||
|
// Header
|
||||||
|
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||||
|
writer.write_ascii(b" Current State Name")?;
|
||||||
|
writer.write_ascii(&self.name_col_padding[4..])?;
|
||||||
|
writer.write_ascii(b"Path")?;
|
||||||
|
next_ln(stdout)?;
|
||||||
|
|
||||||
|
// Rows
|
||||||
|
let iter = self.app_state.exercises().iter().enumerate();
|
||||||
|
let n_displayed_rows = match self.filter {
|
||||||
|
Filter::Done => self.draw_rows(stdout, iter.filter(|(_, exercise)| exercise.done))?,
|
||||||
|
Filter::Pending => {
|
||||||
|
self.draw_rows(stdout, iter.filter(|(_, exercise)| !exercise.done))?
|
||||||
|
}
|
||||||
|
Filter::None => self.draw_rows(stdout, iter)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
for _ in 0..self.scroll_state.max_n_rows_to_display() - n_displayed_rows {
|
||||||
|
next_ln(stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.show_footer {
|
||||||
|
progress_bar(
|
||||||
|
&mut MaxLenWriter::new(stdout, self.term_width as usize),
|
||||||
|
self.app_state.n_done(),
|
||||||
|
self.app_state.exercises().len() as u16,
|
||||||
|
self.term_width,
|
||||||
|
)?;
|
||||||
|
next_ln(stdout)?;
|
||||||
|
|
||||||
|
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||||
|
if self.message.is_empty() {
|
||||||
|
// Help footer message
|
||||||
|
if self.scroll_state.selected().is_some() {
|
||||||
|
writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
|
||||||
|
next_ln(stdout)?;
|
||||||
|
writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||||
|
|
||||||
|
writer.write_ascii(b"<s>earch | filter ")?;
|
||||||
|
} else {
|
||||||
|
// Nothing selected (and nothing shown), so only display filter and quit.
|
||||||
|
writer.write_ascii(b"filter ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.filter {
|
||||||
|
Filter::Done => {
|
||||||
|
writer
|
||||||
|
.stdout
|
||||||
|
.queue(SetForegroundColor(Color::Magenta))?
|
||||||
|
.queue(SetAttribute(Attribute::Underlined))?;
|
||||||
|
writer.write_ascii(b"<d>one")?;
|
||||||
|
writer.stdout.queue(ResetColor)?;
|
||||||
|
writer.write_ascii(b"/<p>ending")?;
|
||||||
|
}
|
||||||
|
Filter::Pending => {
|
||||||
|
writer.write_ascii(b"<d>one/")?;
|
||||||
|
writer
|
||||||
|
.stdout
|
||||||
|
.queue(SetForegroundColor(Color::Magenta))?
|
||||||
|
.queue(SetAttribute(Attribute::Underlined))?;
|
||||||
|
writer.write_ascii(b"<p>ending")?;
|
||||||
|
writer.stdout.queue(ResetColor)?;
|
||||||
|
}
|
||||||
|
Filter::None => writer.write_ascii(b"<d>one/<p>ending")?,
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write_ascii(b" | <q>uit list")?;
|
||||||
|
} else {
|
||||||
|
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
|
||||||
|
writer.write_str(&self.message)?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
next_ln(stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
next_ln(stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.queue(EndSynchronizedUpdate)?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_rows(&mut self) {
|
||||||
|
let n_rows = match self.filter {
|
||||||
|
Filter::Done => self
|
||||||
|
.app_state
|
||||||
|
.exercises()
|
||||||
|
.iter()
|
||||||
|
.filter(|exercise| exercise.done)
|
||||||
|
.count(),
|
||||||
|
Filter::Pending => self
|
||||||
|
.app_state
|
||||||
|
.exercises()
|
||||||
|
.iter()
|
||||||
|
.filter(|exercise| !exercise.done)
|
||||||
|
.count(),
|
||||||
|
Filter::None => self.app_state.exercises().len(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.scroll_state.set_n_rows(n_rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn filter(&self) -> Filter {
|
||||||
|
self.filter
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_filter(&mut self, filter: Filter) {
|
||||||
|
self.filter = filter;
|
||||||
|
self.update_rows();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn select_next(&mut self) {
|
||||||
|
self.scroll_state.select_next();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn select_previous(&mut self) {
|
||||||
|
self.scroll_state.select_previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn select_first(&mut self) {
|
||||||
|
self.scroll_state.select_first();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn select_last(&mut self) {
|
||||||
|
self.scroll_state.select_last();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_to_exercise_ind(&self, selected: usize) -> Result<usize> {
|
||||||
|
match self.filter {
|
||||||
|
Filter::Done => self
|
||||||
.app_state
|
.app_state
|
||||||
.exercises()
|
.exercises()
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(ind, exercise)| {
|
.filter(|(_, exercise)| exercise.done)
|
||||||
let exercise_state = if exercise.done {
|
.nth(selected)
|
||||||
if self.filter == Filter::Pending {
|
.context("Invalid selection index")
|
||||||
return None;
|
.map(|(ind, _)| ind),
|
||||||
}
|
Filter::Pending => self
|
||||||
|
.app_state
|
||||||
"DONE".green()
|
|
||||||
} else {
|
|
||||||
if self.filter == Filter::Done {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
"PENDING".yellow()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.n_rows += 1;
|
|
||||||
|
|
||||||
let next = if ind == current_exercise_ind {
|
|
||||||
">>>>".bold().red()
|
|
||||||
} else {
|
|
||||||
Span::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Row::new([
|
|
||||||
next,
|
|
||||||
exercise_state,
|
|
||||||
Span::raw(exercise.name),
|
|
||||||
Span::raw(exercise.path),
|
|
||||||
]))
|
|
||||||
});
|
|
||||||
|
|
||||||
self.table = self.table.rows(rows);
|
|
||||||
|
|
||||||
if self.n_rows == 0 {
|
|
||||||
self.table_state.select(None);
|
|
||||||
} else {
|
|
||||||
self.table_state.select(Some(
|
|
||||||
self.table_state
|
|
||||||
.selected()
|
|
||||||
.map_or(0, |selected| selected.min(self.n_rows - 1)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(app_state: &'a mut AppState) -> Self {
|
|
||||||
let header = Row::new(["Next", "State", "Name", "Path"]);
|
|
||||||
|
|
||||||
let max_name_len = app_state
|
|
||||||
.exercises()
|
.exercises()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|exercise| exercise.name.len())
|
.enumerate()
|
||||||
.max()
|
.filter(|(_, exercise)| !exercise.done)
|
||||||
.unwrap_or(4) as u16;
|
.nth(selected)
|
||||||
|
.context("Invalid selection index")
|
||||||
|
.map(|(ind, _)| ind),
|
||||||
|
Filter::None => Ok(selected),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let widths = [
|
pub fn reset_selected(&mut self) -> Result<()> {
|
||||||
Constraint::Length(4),
|
let Some(selected) = self.scroll_state.selected() else {
|
||||||
Constraint::Length(7),
|
self.message.push_str("Nothing selected to reset!");
|
||||||
Constraint::Length(max_name_len),
|
return Ok(());
|
||||||
Constraint::Fill(1),
|
|
||||||
];
|
|
||||||
|
|
||||||
let table = Table::default()
|
|
||||||
.widths(widths)
|
|
||||||
.header(header)
|
|
||||||
.column_spacing(2)
|
|
||||||
.highlight_spacing(HighlightSpacing::Always)
|
|
||||||
.highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
|
|
||||||
.highlight_symbol("🦀")
|
|
||||||
.block(Block::default().borders(Borders::BOTTOM));
|
|
||||||
|
|
||||||
let selected = app_state.current_exercise_ind();
|
|
||||||
let table_state = TableState::default()
|
|
||||||
.with_offset(selected.saturating_sub(10))
|
|
||||||
.with_selected(Some(selected));
|
|
||||||
|
|
||||||
let filter = Filter::None;
|
|
||||||
let n_rows = app_state.exercises().len();
|
|
||||||
|
|
||||||
let slf = Self {
|
|
||||||
table,
|
|
||||||
message: String::with_capacity(128),
|
|
||||||
filter,
|
|
||||||
app_state,
|
|
||||||
table_state,
|
|
||||||
n_rows,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
slf.with_updated_rows()
|
let exercise_ind = self.selected_to_exercise_ind(selected)?;
|
||||||
}
|
let exercise_name = self.app_state.reset_exercise_by_ind(exercise_ind)?;
|
||||||
|
self.update_rows();
|
||||||
pub fn select_next(&mut self) {
|
write!(
|
||||||
if self.n_rows > 0 {
|
self.message,
|
||||||
let next = self
|
"The exercise `{exercise_name}` has been reset",
|
||||||
.table_state
|
)?;
|
||||||
.selected()
|
|
||||||
.map_or(0, |selected| (selected + 1).min(self.n_rows - 1));
|
|
||||||
self.table_state.select(Some(next));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_previous(&mut self) {
|
|
||||||
if self.n_rows > 0 {
|
|
||||||
let previous = self
|
|
||||||
.table_state
|
|
||||||
.selected()
|
|
||||||
.map_or(0, |selected| selected.saturating_sub(1));
|
|
||||||
self.table_state.select(Some(previous));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_first(&mut self) {
|
|
||||||
if self.n_rows > 0 {
|
|
||||||
self.table_state.select(Some(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_last(&mut self) {
|
|
||||||
if self.n_rows > 0 {
|
|
||||||
self.table_state.select(Some(self.n_rows - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw(&mut self, frame: &mut Frame) -> Result<()> {
|
|
||||||
let area = frame.area();
|
|
||||||
|
|
||||||
frame.render_stateful_widget(
|
|
||||||
&self.table,
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: area.width,
|
|
||||||
height: area.height - 3,
|
|
||||||
},
|
|
||||||
&mut self.table_state,
|
|
||||||
);
|
|
||||||
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(progress_bar_ratatui(
|
|
||||||
self.app_state.n_done(),
|
|
||||||
self.app_state.exercises().len() as u16,
|
|
||||||
area.width,
|
|
||||||
)?)
|
|
||||||
.block(Block::default().borders(Borders::BOTTOM)),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: area.height - 3,
|
|
||||||
width: area.width,
|
|
||||||
height: 2,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let message = if self.message.is_empty() {
|
|
||||||
// Help footer.
|
|
||||||
let mut spans = Vec::with_capacity(4);
|
|
||||||
spans.push(Span::raw(
|
|
||||||
"↓/j ↑/k home/g end/G │ <c>ontinue at │ <r>eset │ filter ",
|
|
||||||
));
|
|
||||||
match self.filter {
|
|
||||||
Filter::Done => {
|
|
||||||
spans.push("<d>one".underlined().magenta());
|
|
||||||
spans.push(Span::raw("/<p>ending"));
|
|
||||||
}
|
|
||||||
Filter::Pending => {
|
|
||||||
spans.push(Span::raw("<d>one/"));
|
|
||||||
spans.push("<p>ending".underlined().magenta());
|
|
||||||
}
|
|
||||||
Filter::None => spans.push(Span::raw("<d>one/<p>ending")),
|
|
||||||
}
|
|
||||||
spans.push(Span::raw(" │ <q>uit"));
|
|
||||||
Line::from(spans)
|
|
||||||
} else {
|
|
||||||
Line::from(self.message.as_str().light_blue())
|
|
||||||
};
|
|
||||||
frame.render_widget(
|
|
||||||
message,
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: area.height - 1,
|
|
||||||
width: area.width,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_reset_selected(mut self) -> Result<Self> {
|
pub fn apply_search_query(&mut self) {
|
||||||
let Some(selected) = self.table_state.selected() else {
|
self.message.push_str("search:");
|
||||||
return Ok(self);
|
self.message.push_str(&self.search_query);
|
||||||
};
|
self.message.push('|');
|
||||||
|
|
||||||
let ind = self
|
if self.search_query.is_empty() {
|
||||||
.app_state
|
return;
|
||||||
.exercises()
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(ind, exercise)| match self.filter {
|
|
||||||
Filter::Done => exercise.done.then_some(ind),
|
|
||||||
Filter::Pending => (!exercise.done).then_some(ind),
|
|
||||||
Filter::None => Some(ind),
|
|
||||||
})
|
|
||||||
.nth(selected)
|
|
||||||
.context("Invalid selection index")?;
|
|
||||||
|
|
||||||
let exercise_path = self.app_state.reset_exercise_by_ind(ind)?;
|
|
||||||
write!(self.message, "The exercise {exercise_path} has been reset")?;
|
|
||||||
|
|
||||||
Ok(self.with_updated_rows())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_to_current_exercise(&mut self) -> Result<()> {
|
let is_search_result = |exercise: &Exercise| exercise.name.contains(&self.search_query);
|
||||||
let Some(selected) = self.table_state.selected() else {
|
let mut iter = self.app_state.exercises().iter();
|
||||||
return Ok(());
|
let ind = match self.filter {
|
||||||
|
Filter::None => iter.position(is_search_result),
|
||||||
|
Filter::Done => iter
|
||||||
|
.filter(|exercise| exercise.done)
|
||||||
|
.position(is_search_result),
|
||||||
|
Filter::Pending => iter
|
||||||
|
.filter(|exercise| !exercise.done)
|
||||||
|
.position(is_search_result),
|
||||||
};
|
};
|
||||||
|
|
||||||
let ind = self
|
match ind {
|
||||||
.app_state
|
Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind),
|
||||||
.exercises()
|
None => self.message.push_str(" (not found)"),
|
||||||
.iter()
|
}
|
||||||
.enumerate()
|
}
|
||||||
.filter_map(|(ind, exercise)| match self.filter {
|
|
||||||
Filter::Done => exercise.done.then_some(ind),
|
|
||||||
Filter::Pending => (!exercise.done).then_some(ind),
|
|
||||||
Filter::None => Some(ind),
|
|
||||||
})
|
|
||||||
.nth(selected)
|
|
||||||
.context("Invalid selection index")?;
|
|
||||||
|
|
||||||
self.app_state.set_current_exercise_ind(ind)
|
// Return `true` if there was something to select.
|
||||||
|
pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
|
||||||
|
let Some(selected) = self.scroll_state.selected() else {
|
||||||
|
self.message.push_str("Nothing selected to continue at!");
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
let exercise_ind = self.selected_to_exercise_ind(selected)?;
|
||||||
|
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
65
src/main.rs
65
src/main.rs
|
@ -4,26 +4,23 @@ use clap::{Parser, Subcommand};
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, IsTerminal, Write},
|
io::{self, IsTerminal, Write},
|
||||||
path::Path,
|
path::Path,
|
||||||
process::exit,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
use term::{clear_terminal, press_enter_prompt};
|
use term::{clear_terminal, press_enter_prompt};
|
||||||
|
|
||||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
|
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile};
|
||||||
|
|
||||||
mod app_state;
|
mod app_state;
|
||||||
mod cargo_toml;
|
mod cargo_toml;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod collections;
|
|
||||||
mod dev;
|
mod dev;
|
||||||
mod embedded;
|
mod embedded;
|
||||||
mod exercise;
|
mod exercise;
|
||||||
mod info_file;
|
mod info_file;
|
||||||
mod init;
|
mod init;
|
||||||
mod list;
|
mod list;
|
||||||
mod progress_bar;
|
|
||||||
mod run;
|
mod run;
|
||||||
mod term;
|
mod term;
|
||||||
mod terminal_link;
|
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
const CURRENT_FORMAT_VERSION: u8 = 1;
|
const CURRENT_FORMAT_VERSION: u8 = 1;
|
||||||
|
@ -49,6 +46,8 @@ enum Subcommands {
|
||||||
/// The name of the exercise
|
/// The name of the exercise
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
|
/// Check all the exercises, marking them as done or pending accordingly.
|
||||||
|
CheckAll,
|
||||||
/// Reset a single exercise
|
/// Reset a single exercise
|
||||||
Reset {
|
Reset {
|
||||||
/// The name of the exercise
|
/// The name of the exercise
|
||||||
|
@ -64,24 +63,26 @@ enum Subcommands {
|
||||||
Dev(DevCommands),
|
Dev(DevCommands),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<ExitCode> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
|
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
|
||||||
bail!("{OLD_METHOD_ERR}");
|
bail!("{OLD_METHOD_ERR}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
'priority_cmd: {
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(Subcommands::Init) => {
|
Some(Subcommands::Init) => init::init().context("Initialization failed")?,
|
||||||
return init::init().context("Initialization failed");
|
Some(Subcommands::Dev(dev_command)) => dev_command.run()?,
|
||||||
|
_ => break 'priority_cmd,
|
||||||
}
|
}
|
||||||
Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
|
|
||||||
_ => (),
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !Path::new("exercises").is_dir() {
|
if !Path::new("exercises").is_dir() {
|
||||||
println!("{PRE_INIT_MSG}");
|
println!("{PRE_INIT_MSG}");
|
||||||
exit(1);
|
return Ok(ExitCode::FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
let info_file = InfoFile::parse()?;
|
let info_file = InfoFile::parse()?;
|
||||||
|
@ -134,21 +135,41 @@ fn main() -> Result<()> {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
watch::watch(&mut app_state, notify_exercise_names)?;
|
||||||
match watch::watch(&mut app_state, notify_exercise_names)? {
|
|
||||||
WatchExit::Shutdown => break,
|
|
||||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
|
||||||
// the watch mode instead of trying to pause the watch threads and correct the
|
|
||||||
// watch state.
|
|
||||||
WatchExit::List => list::list(&mut app_state)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(Subcommands::Run { name }) => {
|
Some(Subcommands::Run { name }) => {
|
||||||
if let Some(name) = name {
|
if let Some(name) = name {
|
||||||
app_state.set_current_exercise_by_name(&name)?;
|
app_state.set_current_exercise_by_name(&name)?;
|
||||||
}
|
}
|
||||||
run::run(&mut app_state)?;
|
return run::run(&mut app_state);
|
||||||
|
}
|
||||||
|
Some(Subcommands::CheckAll) => {
|
||||||
|
let mut stdout = io::stdout().lock();
|
||||||
|
if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? {
|
||||||
|
if app_state.current_exercise().done {
|
||||||
|
app_state.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.write_all(b"\n\n")?;
|
||||||
|
let pending = app_state.n_pending();
|
||||||
|
if pending == 1 {
|
||||||
|
stdout.write_all(b"One exercise pending: ")?;
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
stdout,
|
||||||
|
"{pending}/{} exercises pending. The first: ",
|
||||||
|
app_state.exercises().len(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
app_state
|
||||||
|
.current_exercise()
|
||||||
|
.terminal_file_link(&mut stdout)?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
} else {
|
||||||
|
app_state.render_final_message(&mut stdout)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(Subcommands::Reset { name }) => {
|
Some(Subcommands::Reset { name }) => {
|
||||||
app_state.set_current_exercise_by_name(&name)?;
|
app_state.set_current_exercise_by_name(&name)?;
|
||||||
|
@ -165,7 +186,7 @@ fn main() -> Result<()> {
|
||||||
Some(Subcommands::Init | Subcommands::Dev(_)) => (),
|
Some(Subcommands::Init | Subcommands::Dev(_)) => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const OLD_METHOD_ERR: &str =
|
const OLD_METHOD_ERR: &str =
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use ratatui::text::{Line, Span};
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
const PREFIX: &str = "Progress: [";
|
|
||||||
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
|
|
||||||
// Leaving the last char empty (_) for `total` > 99.
|
|
||||||
const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
|
|
||||||
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
|
|
||||||
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
|
||||||
|
|
||||||
const PROGRESS_EXCEEDS_MAX_ERR: &str =
|
|
||||||
"The progress of the progress bar is higher than the maximum";
|
|
||||||
|
|
||||||
/// Terminal progress bar to be used when not using Ratataui.
|
|
||||||
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
|
|
||||||
use ratatui::crossterm::style::Stylize;
|
|
||||||
|
|
||||||
if progress > total {
|
|
||||||
bail!(PROGRESS_EXCEEDS_MAX_ERR);
|
|
||||||
}
|
|
||||||
|
|
||||||
if line_width < MIN_LINE_WIDTH {
|
|
||||||
return Ok(format!("Progress: {progress}/{total} exercises"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut line = String::with_capacity(usize::from(line_width));
|
|
||||||
line.push_str(PREFIX);
|
|
||||||
|
|
||||||
let width = line_width - WRAPPER_WIDTH;
|
|
||||||
let filled = (width * progress) / total;
|
|
||||||
|
|
||||||
let mut green_part = String::with_capacity(usize::from(filled + 1));
|
|
||||||
for _ in 0..filled {
|
|
||||||
green_part.push('#');
|
|
||||||
}
|
|
||||||
|
|
||||||
if filled < width {
|
|
||||||
green_part.push('>');
|
|
||||||
}
|
|
||||||
write!(line, "{}", green_part.green()).unwrap();
|
|
||||||
|
|
||||||
let width_minus_filled = width - filled;
|
|
||||||
if width_minus_filled > 1 {
|
|
||||||
let red_part_width = width_minus_filled - 1;
|
|
||||||
let mut red_part = String::with_capacity(usize::from(red_part_width));
|
|
||||||
for _ in 0..red_part_width {
|
|
||||||
red_part.push('-');
|
|
||||||
}
|
|
||||||
write!(line, "{}", red_part.red()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(line, "] {progress:>3}/{total} exercises").unwrap();
|
|
||||||
|
|
||||||
Ok(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Progress bar to be used with Ratataui.
|
|
||||||
// Not using Ratatui's Gauge widget to keep the progress bar consistent.
|
|
||||||
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
|
|
||||||
use ratatui::style::Stylize;
|
|
||||||
|
|
||||||
if progress > total {
|
|
||||||
bail!(PROGRESS_EXCEEDS_MAX_ERR);
|
|
||||||
}
|
|
||||||
|
|
||||||
if line_width < MIN_LINE_WIDTH {
|
|
||||||
return Ok(Line::raw(format!("Progress: {progress}/{total} exercises")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut spans = Vec::with_capacity(4);
|
|
||||||
spans.push(Span::raw(PREFIX));
|
|
||||||
|
|
||||||
let width = line_width - WRAPPER_WIDTH;
|
|
||||||
let filled = (width * progress) / total;
|
|
||||||
|
|
||||||
let mut green_part = String::with_capacity(usize::from(filled + 1));
|
|
||||||
for _ in 0..filled {
|
|
||||||
green_part.push('#');
|
|
||||||
}
|
|
||||||
|
|
||||||
if filled < width {
|
|
||||||
green_part.push('>');
|
|
||||||
}
|
|
||||||
spans.push(green_part.green());
|
|
||||||
|
|
||||||
let width_minus_filled = width - filled;
|
|
||||||
if width_minus_filled > 1 {
|
|
||||||
let red_part_width = width_minus_filled - 1;
|
|
||||||
let mut red_part = String::with_capacity(usize::from(red_part_width));
|
|
||||||
for _ in 0..red_part_width {
|
|
||||||
red_part.push('-');
|
|
||||||
}
|
|
||||||
spans.push(red_part.red());
|
|
||||||
}
|
|
||||||
|
|
||||||
spans.push(Span::raw(format!("] {progress:>3}/{total} exercises")));
|
|
||||||
|
|
||||||
Ok(Line::from(spans))
|
|
||||||
}
|
|
61
src/run.rs
61
src/run.rs
|
@ -1,14 +1,19 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::Result;
|
||||||
use ratatui::crossterm::style::{style, Stylize};
|
use crossterm::{
|
||||||
use std::io::{self, Write};
|
style::{Color, ResetColor, SetForegroundColor},
|
||||||
|
QueueableCommand,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
process::ExitCode,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{AppState, ExercisesProgress},
|
app_state::{AppState, ExercisesProgress},
|
||||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
|
||||||
terminal_link::TerminalFileLink,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(app_state: &mut AppState) -> Result<()> {
|
pub fn run(app_state: &mut AppState) -> Result<ExitCode> {
|
||||||
let exercise = app_state.current_exercise();
|
let exercise = app_state.current_exercise();
|
||||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||||
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
|
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
|
||||||
|
@ -19,33 +24,37 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||||
if !success {
|
if !success {
|
||||||
app_state.set_pending(app_state.current_exercise_ind())?;
|
app_state.set_pending(app_state.current_exercise_ind())?;
|
||||||
|
|
||||||
bail!(
|
stdout.write_all(b"Ran ")?;
|
||||||
"Ran {} with errors",
|
app_state
|
||||||
app_state.current_exercise().terminal_link(),
|
.current_exercise()
|
||||||
);
|
.terminal_file_link(&mut stdout)?;
|
||||||
|
stdout.write_all(b" with errors\n")?;
|
||||||
|
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(
|
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||||
stdout,
|
stdout.write_all("✓ Successfully ran ".as_bytes())?;
|
||||||
"{}{}",
|
stdout.write_all(exercise.path.as_bytes())?;
|
||||||
"✓ Successfully ran ".green(),
|
stdout.queue(ResetColor)?;
|
||||||
exercise.path.green(),
|
stdout.write_all(b"\n")?;
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(solution_path) = app_state.current_solution_path()? {
|
if let Some(solution_path) = app_state.current_solution_path()? {
|
||||||
println!(
|
stdout.write_all(b"\n")?;
|
||||||
"\nA solution file can be found at {}\n",
|
solution_link_line(&mut stdout, &solution_path)?;
|
||||||
style(TerminalFileLink(&solution_path)).underlined().green(),
|
stdout.write_all(b"\n")?;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match app_state.done_current_exercise(&mut stdout)? {
|
match app_state.done_current_exercise::<false>(&mut stdout)? {
|
||||||
|
ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => {
|
||||||
|
stdout.write_all(b"Next exercise: ")?;
|
||||||
|
app_state
|
||||||
|
.current_exercise()
|
||||||
|
.terminal_file_link(&mut stdout)?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
}
|
||||||
ExercisesProgress::AllDone => (),
|
ExercisesProgress::AllDone => (),
|
||||||
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!(
|
|
||||||
"Next exercise: {}",
|
|
||||||
app_state.current_exercise().terminal_link(),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
273
src/term.rs
273
src/term.rs
|
@ -1,12 +1,279 @@
|
||||||
use std::io::{self, BufRead, StdoutLock, Write};
|
use crossterm::{
|
||||||
|
cursor::MoveTo,
|
||||||
|
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||||
|
terminal::{Clear, ClearType},
|
||||||
|
Command, QueueableCommand,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt, fs,
|
||||||
|
io::{self, BufRead, StdoutLock, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app_state::CheckProgress;
|
||||||
|
|
||||||
|
pub struct MaxLenWriter<'a, 'lock> {
|
||||||
|
pub stdout: &'a mut StdoutLock<'lock>,
|
||||||
|
len: usize,
|
||||||
|
max_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'lock> MaxLenWriter<'a, 'lock> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
stdout,
|
||||||
|
len: 0,
|
||||||
|
max_len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional is for emojis that take more space.
|
||||||
|
#[inline]
|
||||||
|
pub fn add_to_len(&mut self, additional: usize) {
|
||||||
|
self.len += additional;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CountedWrite<'lock> {
|
||||||
|
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>;
|
||||||
|
fn write_str(&mut self, unicode: &str) -> io::Result<()>;
|
||||||
|
fn stdout(&mut self) -> &mut StdoutLock<'lock>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> {
|
||||||
|
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
|
||||||
|
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
|
||||||
|
if n > 0 {
|
||||||
|
self.stdout.write_all(&ascii[..n])?;
|
||||||
|
self.len += n;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
|
||||||
|
if let Some((ind, c)) = unicode
|
||||||
|
.char_indices()
|
||||||
|
.take(self.max_len.saturating_sub(self.len))
|
||||||
|
.last()
|
||||||
|
{
|
||||||
|
self.stdout
|
||||||
|
.write_all(&unicode.as_bytes()[..ind + c.len_utf8()])?;
|
||||||
|
self.len += ind + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn stdout(&mut self) -> &mut StdoutLock<'lock> {
|
||||||
|
self.stdout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CountedWrite<'a> for StdoutLock<'a> {
|
||||||
|
#[inline]
|
||||||
|
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
|
||||||
|
self.write_all(ascii)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
|
||||||
|
self.write_all(unicode.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn stdout(&mut self) -> &mut StdoutLock<'a> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CheckProgressVisualizer<'a, 'lock> {
|
||||||
|
stdout: &'a mut StdoutLock<'lock>,
|
||||||
|
n_cols: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> {
|
||||||
|
const CHECKING_COLOR: Color = Color::Blue;
|
||||||
|
const DONE_COLOR: Color = Color::Green;
|
||||||
|
const PENDING_COLOR: Color = Color::Red;
|
||||||
|
|
||||||
|
pub fn build(stdout: &'a mut StdoutLock<'lock>, term_width: u16) -> io::Result<Self> {
|
||||||
|
clear_terminal(stdout)?;
|
||||||
|
stdout.write_all("Checking all exercises…\n".as_bytes())?;
|
||||||
|
|
||||||
|
// Legend
|
||||||
|
stdout.write_all(b"Color of exercise number: ")?;
|
||||||
|
stdout.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
|
||||||
|
stdout.write_all(b"Checking")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b" - ")?;
|
||||||
|
stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
|
||||||
|
stdout.write_all(b"Done")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b" - ")?;
|
||||||
|
stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
|
||||||
|
stdout.write_all(b"Pending")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
|
||||||
|
// Exercise numbers with up to 3 digits.
|
||||||
|
// +1 because the last column doesn't end with a whitespace.
|
||||||
|
let n_cols = usize::from(term_width + 1) / 4;
|
||||||
|
|
||||||
|
Ok(Self { stdout, n_cols })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, progresses: &[CheckProgress]) -> io::Result<()> {
|
||||||
|
self.stdout.queue(MoveTo(0, 2))?;
|
||||||
|
|
||||||
|
let mut exercise_num = 1;
|
||||||
|
for exercise_progress in progresses {
|
||||||
|
match exercise_progress {
|
||||||
|
CheckProgress::None => (),
|
||||||
|
CheckProgress::Checking => {
|
||||||
|
self.stdout
|
||||||
|
.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
|
||||||
|
}
|
||||||
|
CheckProgress::Done => {
|
||||||
|
self.stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
|
||||||
|
}
|
||||||
|
CheckProgress::Pending => {
|
||||||
|
self.stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(self.stdout, "{exercise_num:<3}")?;
|
||||||
|
self.stdout.queue(ResetColor)?;
|
||||||
|
|
||||||
|
if exercise_num != progresses.len() {
|
||||||
|
if exercise_num % self.n_cols == 0 {
|
||||||
|
self.stdout.write_all(b"\n")?;
|
||||||
|
} else {
|
||||||
|
self.stdout.write_all(b" ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
exercise_num += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stdout.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn progress_bar<'a>(
|
||||||
|
writer: &mut impl CountedWrite<'a>,
|
||||||
|
progress: u16,
|
||||||
|
total: u16,
|
||||||
|
term_width: u16,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
debug_assert!(total <= 999);
|
||||||
|
debug_assert!(progress <= total);
|
||||||
|
|
||||||
|
const PREFIX: &[u8] = b"Progress: [";
|
||||||
|
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
|
||||||
|
const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16;
|
||||||
|
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
|
||||||
|
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
||||||
|
|
||||||
|
if term_width < MIN_LINE_WIDTH {
|
||||||
|
writer.write_ascii(b"Progress: ")?;
|
||||||
|
// Integers are in ASCII.
|
||||||
|
return writer.write_ascii(format!("{progress}/{total}").as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = writer.stdout();
|
||||||
|
stdout.write_all(PREFIX)?;
|
||||||
|
|
||||||
|
let width = term_width - WRAPPER_WIDTH;
|
||||||
|
let filled = (width * progress) / total;
|
||||||
|
|
||||||
|
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||||
|
for _ in 0..filled {
|
||||||
|
stdout.write_all(b"#")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if filled < width {
|
||||||
|
stdout.write_all(b">")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width_minus_filled = width - filled;
|
||||||
|
if width_minus_filled > 1 {
|
||||||
|
let red_part_width = width_minus_filled - 1;
|
||||||
|
stdout.queue(SetForegroundColor(Color::Red))?;
|
||||||
|
for _ in 0..red_part_width {
|
||||||
|
stdout.write_all(b"-")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||||
|
|
||||||
|
write!(stdout, "] {progress:>3}/{total}")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
|
stdout
|
||||||
|
.queue(MoveTo(0, 0))?
|
||||||
|
.queue(Clear(ClearType::All))?
|
||||||
|
.queue(Clear(ClearType::Purge))
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
|
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
|
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
|
||||||
stdout.write_all(b"\n")?;
|
stdout.write_all(b"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonicalize, convert to string and remove verbatim part on Windows.
|
||||||
|
pub fn canonicalize(path: &str) -> Option<String> {
|
||||||
|
fs::canonicalize(path)
|
||||||
|
.ok()?
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.ok()
|
||||||
|
.map(|mut path| {
|
||||||
|
// Windows itself can't handle its verbatim paths.
|
||||||
|
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
|
||||||
|
path.drain(..4);
|
||||||
|
}
|
||||||
|
|
||||||
|
path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn terminal_file_link<'a>(
|
||||||
|
writer: &mut impl CountedWrite<'a>,
|
||||||
|
path: &str,
|
||||||
|
canonical_path: &str,
|
||||||
|
color: Color,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
writer
|
||||||
|
.stdout()
|
||||||
|
.queue(SetForegroundColor(color))?
|
||||||
|
.queue(SetAttribute(Attribute::Underlined))?;
|
||||||
|
writer.stdout().write_all(b"\x1b]8;;file://")?;
|
||||||
|
writer.stdout().write_all(canonical_path.as_bytes())?;
|
||||||
|
writer.stdout().write_all(b"\x1b\\")?;
|
||||||
|
// Only this part is visible.
|
||||||
|
writer.write_str(path)?;
|
||||||
|
writer.stdout().write_all(b"\x1b]8;;\x1b\\")?;
|
||||||
|
writer
|
||||||
|
.stdout()
|
||||||
|
.queue(SetForegroundColor(Color::Reset))?
|
||||||
|
.queue(SetAttribute(Attribute::NoUnderline))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn write_ansi(output: &mut Vec<u8>, command: impl Command) {
|
||||||
|
struct FmtWriter<'a>(&'a mut Vec<u8>);
|
||||||
|
|
||||||
|
impl fmt::Write for FmtWriter<'_> {
|
||||||
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||||
|
self.0.extend_from_slice(s.as_bytes());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = command.write_ansi(&mut FmtWriter(output));
|
||||||
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
use std::{
|
|
||||||
fmt::{self, Display, Formatter},
|
|
||||||
fs,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct TerminalFileLink<'a>(pub &'a str);
|
|
||||||
|
|
||||||
impl<'a> Display for TerminalFileLink<'a> {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
||||||
let path = fs::canonicalize(self.0);
|
|
||||||
|
|
||||||
if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) {
|
|
||||||
// Windows itself can't handle its verbatim paths.
|
|
||||||
#[cfg(windows)]
|
|
||||||
let path = if path.len() > 5 && &path[0..4] == r"\\?\" {
|
|
||||||
&path[4..]
|
|
||||||
} else {
|
|
||||||
path
|
|
||||||
};
|
|
||||||
|
|
||||||
write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0)
|
|
||||||
} else {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
163
src/watch.rs
163
src/watch.rs
|
@ -1,109 +1,127 @@
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use notify_debouncer_mini::{
|
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
new_debouncer,
|
|
||||||
notify::{self, RecursiveMode},
|
|
||||||
};
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::mpsc::channel,
|
sync::{
|
||||||
thread,
|
atomic::{AtomicBool, Ordering::Relaxed},
|
||||||
|
mpsc::channel,
|
||||||
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app_state::{AppState, ExercisesProgress};
|
use crate::{
|
||||||
|
app_state::{AppState, ExercisesProgress},
|
||||||
use self::{
|
list,
|
||||||
notify_event::NotifyEventHandler,
|
|
||||||
state::WatchState,
|
|
||||||
terminal_event::{terminal_event_handler, InputEvent},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent};
|
||||||
|
|
||||||
mod notify_event;
|
mod notify_event;
|
||||||
mod state;
|
mod state;
|
||||||
mod terminal_event;
|
mod terminal_event;
|
||||||
|
|
||||||
|
static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
// Private unit type to force using the constructor function.
|
||||||
|
#[must_use = "When the guard is dropped, the input is unpaused"]
|
||||||
|
pub struct InputPauseGuard(());
|
||||||
|
|
||||||
|
impl InputPauseGuard {
|
||||||
|
#[inline]
|
||||||
|
pub fn scoped_pause() -> Self {
|
||||||
|
EXERCISE_RUNNING.store(true, Relaxed);
|
||||||
|
Self(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for InputPauseGuard {
|
||||||
|
#[inline]
|
||||||
|
fn drop(&mut self) {
|
||||||
|
EXERCISE_RUNNING.store(false, Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum WatchEvent {
|
enum WatchEvent {
|
||||||
Input(InputEvent),
|
Input(InputEvent),
|
||||||
FileChange { exercise_ind: usize },
|
FileChange { exercise_ind: usize },
|
||||||
TerminalResize,
|
TerminalResize { width: u16 },
|
||||||
NotifyErr(notify::Error),
|
NotifyErr(notify::Error),
|
||||||
TerminalEventErr(io::Error),
|
TerminalEventErr(io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by the watch mode to indicate what to do afterwards.
|
/// Returned by the watch mode to indicate what to do afterwards.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub enum WatchExit {
|
enum WatchExit {
|
||||||
/// Exit the program.
|
/// Exit the program.
|
||||||
Shutdown,
|
Shutdown,
|
||||||
/// Enter the list mode and restart the watch mode afterwards.
|
/// Enter the list mode and restart the watch mode afterwards.
|
||||||
List,
|
List,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `notify_exercise_names` as None activates the manual run mode.
|
fn run_watch(
|
||||||
pub fn watch(
|
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||||
) -> Result<WatchExit> {
|
) -> Result<WatchExit> {
|
||||||
let (tx, rx) = channel();
|
let (watch_event_sender, watch_event_receiver) = channel();
|
||||||
|
|
||||||
let mut manual_run = false;
|
let mut manual_run = false;
|
||||||
// Prevent dropping the guard until the end of the function.
|
// Prevent dropping the guard until the end of the function.
|
||||||
// Otherwise, the file watcher exits.
|
// Otherwise, the file watcher exits.
|
||||||
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names {
|
let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
|
||||||
let mut debouncer = new_debouncer(
|
let notify_event_handler =
|
||||||
Duration::from_millis(200),
|
NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?;
|
||||||
NotifyEventHandler {
|
|
||||||
tx: tx.clone(),
|
let mut watcher = RecommendedWatcher::new(
|
||||||
exercise_names,
|
notify_event_handler,
|
||||||
},
|
Config::default().with_poll_interval(Duration::from_secs(1)),
|
||||||
)
|
)
|
||||||
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
||||||
debouncer
|
|
||||||
.watcher()
|
watcher
|
||||||
.watch(Path::new("exercises"), RecursiveMode::Recursive)
|
.watch(Path::new("exercises"), RecursiveMode::Recursive)
|
||||||
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
||||||
|
|
||||||
Some(debouncer)
|
Some(watcher)
|
||||||
} else {
|
} else {
|
||||||
manual_run = true;
|
manual_run = true;
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut watch_state = WatchState::new(app_state, manual_run);
|
let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run)?;
|
||||||
|
let mut stdout = io::stdout().lock();
|
||||||
|
|
||||||
watch_state.run_current_exercise()?;
|
watch_state.run_current_exercise(&mut stdout)?;
|
||||||
|
|
||||||
thread::spawn(move || terminal_event_handler(tx, manual_run));
|
while let Ok(event) = watch_event_receiver.recv() {
|
||||||
|
|
||||||
while let Ok(event) = rx.recv() {
|
|
||||||
match event {
|
match event {
|
||||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
|
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
|
||||||
ExercisesProgress::AllDone => break,
|
ExercisesProgress::AllDone => break,
|
||||||
ExercisesProgress::CurrentPending => watch_state.render()?,
|
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||||
ExercisesProgress::NewPending => watch_state.run_current_exercise()?,
|
ExercisesProgress::CurrentPending => (),
|
||||||
},
|
},
|
||||||
WatchEvent::Input(InputEvent::Hint) => {
|
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
|
||||||
watch_state.show_hint()?;
|
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
|
||||||
}
|
WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
|
||||||
WatchEvent::Input(InputEvent::List) => {
|
WatchEvent::Input(InputEvent::CheckAll) => match watch_state
|
||||||
return Ok(WatchExit::List);
|
.check_all_exercises(&mut stdout)?
|
||||||
}
|
{
|
||||||
|
ExercisesProgress::AllDone => break,
|
||||||
|
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||||
|
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
|
||||||
|
},
|
||||||
|
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?,
|
||||||
WatchEvent::Input(InputEvent::Quit) => {
|
WatchEvent::Input(InputEvent::Quit) => {
|
||||||
watch_state.into_writer().write_all(QUIT_MSG)?;
|
stdout.write_all(QUIT_MSG)?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
|
|
||||||
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?,
|
|
||||||
WatchEvent::FileChange { exercise_ind } => {
|
WatchEvent::FileChange { exercise_ind } => {
|
||||||
watch_state.handle_file_change(exercise_ind)?;
|
watch_state.handle_file_change(exercise_ind, &mut stdout)?;
|
||||||
}
|
}
|
||||||
WatchEvent::TerminalResize => {
|
WatchEvent::TerminalResize { width } => {
|
||||||
watch_state.render()?;
|
watch_state.update_term_width(width, &mut stdout)?;
|
||||||
}
|
|
||||||
WatchEvent::NotifyErr(e) => {
|
|
||||||
return Err(Error::from(e).context(NOTIFY_ERR));
|
|
||||||
}
|
}
|
||||||
|
WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)),
|
||||||
WatchEvent::TerminalEventErr(e) => {
|
WatchEvent::TerminalEventErr(e) => {
|
||||||
return Err(Error::from(e).context("Terminal event listener failed"));
|
return Err(Error::from(e).context("Terminal event listener failed"));
|
||||||
}
|
}
|
||||||
|
@ -113,9 +131,52 @@ pub fn watch(
|
||||||
Ok(WatchExit::Shutdown)
|
Ok(WatchExit::Shutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn watch_list_loop(
|
||||||
|
app_state: &mut AppState,
|
||||||
|
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||||
|
) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
match run_watch(app_state, notify_exercise_names)? {
|
||||||
|
WatchExit::Shutdown => break Ok(()),
|
||||||
|
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||||
|
// the watch mode instead of trying to pause the watch threads and correct the
|
||||||
|
// watch state.
|
||||||
|
WatchExit::List => list::list(app_state)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `notify_exercise_names` as None activates the manual run mode.
|
||||||
|
pub fn watch(
|
||||||
|
app_state: &mut AppState,
|
||||||
|
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||||
|
) -> Result<()> {
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let stdin_fd = rustix::stdio::stdin();
|
||||||
|
let mut termios = rustix::termios::tcgetattr(stdin_fd)?;
|
||||||
|
let original_local_modes = termios.local_modes;
|
||||||
|
// Disable stdin line buffering and hide input.
|
||||||
|
termios.local_modes -=
|
||||||
|
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
|
||||||
|
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||||
|
|
||||||
|
let res = watch_list_loop(app_state, notify_exercise_names);
|
||||||
|
|
||||||
|
termios.local_modes = original_local_modes;
|
||||||
|
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
watch_list_loop(app_state, notify_exercise_names)
|
||||||
|
}
|
||||||
|
|
||||||
const QUIT_MSG: &[u8] = b"
|
const QUIT_MSG: &[u8] = b"
|
||||||
|
|
||||||
We hope you're enjoying learning Rust!
|
We hope you're enjoying learning Rust!
|
||||||
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
|
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory.
|
||||||
";
|
";
|
||||||
|
|
||||||
const NOTIFY_ERR: &str = "
|
const NOTIFY_ERR: &str = "
|
||||||
|
|
|
@ -1,52 +1,132 @@
|
||||||
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
|
use anyhow::{Context, Result};
|
||||||
use std::sync::mpsc::Sender;
|
use notify::{
|
||||||
|
event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode},
|
||||||
|
Event, EventKind,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::Ordering::Relaxed,
|
||||||
|
mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender},
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use super::WatchEvent;
|
use super::{WatchEvent, EXERCISE_RUNNING};
|
||||||
|
|
||||||
|
const DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
pub struct NotifyEventHandler {
|
pub struct NotifyEventHandler {
|
||||||
pub tx: Sender<WatchEvent>,
|
error_sender: Sender<WatchEvent>,
|
||||||
/// Used to report which exercise was modified.
|
// Sends the index of the updated exercise.
|
||||||
pub exercise_names: &'static [&'static [u8]],
|
update_sender: SyncSender<usize>,
|
||||||
|
// Used to report which exercise was modified.
|
||||||
|
exercise_names: &'static [&'static [u8]],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler {
|
impl NotifyEventHandler {
|
||||||
fn handle_event(&mut self, input_event: DebounceEventResult) {
|
pub fn build(
|
||||||
let output_event = match input_event {
|
watch_event_sender: Sender<WatchEvent>,
|
||||||
Ok(input_event) => {
|
exercise_names: &'static [&'static [u8]],
|
||||||
let Some(exercise_ind) = input_event
|
) -> Result<Self> {
|
||||||
.iter()
|
let (update_sender, update_receiver) = sync_channel(0);
|
||||||
.filter_map(|input_event| {
|
let error_sender = watch_event_sender.clone();
|
||||||
if input_event.kind != DebouncedEventKind::Any {
|
|
||||||
return None;
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_name = input_event.path.file_name()?.to_str()?.as_bytes();
|
*updated = false;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Disconnected) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.context("Failed to spawn a thread to debounce file changes")?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
error_sender,
|
||||||
|
update_sender,
|
||||||
|
exercise_names,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl notify::EventHandler for NotifyEventHandler {
|
||||||
|
fn handle_event(&mut self, input_event: notify::Result<Event>) {
|
||||||
|
if EXERCISE_RUNNING.load(Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_event = match input_event {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
// An error occurs when the receiver is dropped.
|
||||||
|
// After dropping the receiver, the watcher guard should also be dropped.
|
||||||
|
let _ = self.error_sender.send(WatchEvent::NotifyErr(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match input_event.kind {
|
||||||
|
EventKind::Any => (),
|
||||||
|
EventKind::Modify(modify_kind) => match modify_kind {
|
||||||
|
ModifyKind::Any | ModifyKind::Data(_) => (),
|
||||||
|
ModifyKind::Name(rename_mode) => match rename_mode {
|
||||||
|
RenameMode::Any | RenameMode::To => (),
|
||||||
|
RenameMode::From | RenameMode::Both | RenameMode::Other => return,
|
||||||
|
},
|
||||||
|
ModifyKind::Metadata(metadata_kind) => match metadata_kind {
|
||||||
|
MetadataKind::Any | MetadataKind::WriteTime => (),
|
||||||
|
MetadataKind::AccessTime
|
||||||
|
| MetadataKind::Permissions
|
||||||
|
| MetadataKind::Ownership
|
||||||
|
| MetadataKind::Extended
|
||||||
|
| MetadataKind::Other => return,
|
||||||
|
},
|
||||||
|
ModifyKind::Other => return,
|
||||||
|
},
|
||||||
|
EventKind::Access(access_kind) => match access_kind {
|
||||||
|
AccessKind::Any => (),
|
||||||
|
AccessKind::Close(access_mode) => match access_mode {
|
||||||
|
AccessMode::Any | AccessMode::Write => (),
|
||||||
|
AccessMode::Execute | AccessMode::Read | AccessMode::Other => return,
|
||||||
|
},
|
||||||
|
AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return,
|
||||||
|
},
|
||||||
|
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return,
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = input_event
|
||||||
|
.paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|path| {
|
||||||
|
let file_name = path.file_name()?.to_str()?.as_bytes();
|
||||||
|
|
||||||
|
let [file_name_without_ext @ .., b'.', b'r', b's'] = file_name else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
self.exercise_names
|
self.exercise_names
|
||||||
.iter()
|
.iter()
|
||||||
.position(|exercise_name| *exercise_name == file_name_without_ext)
|
.position(|exercise_name| *exercise_name == file_name_without_ext)
|
||||||
})
|
})
|
||||||
.min()
|
.try_for_each(|exercise_ind| self.update_sender.send(exercise_ind));
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
WatchEvent::FileChange { exercise_ind }
|
|
||||||
}
|
|
||||||
Err(e) => WatchEvent::NotifyErr(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
// An error occurs when the receiver is dropped.
|
|
||||||
// After dropping the receiver, the debouncer guard should also be dropped.
|
|
||||||
let _ = self.tx.send(output_event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,29 @@
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use ratatui::crossterm::{
|
use crossterm::{
|
||||||
style::{style, Stylize},
|
style::{
|
||||||
terminal,
|
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||||
|
},
|
||||||
|
terminal, QueueableCommand,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io::{self, Read, StdoutLock, Write},
|
||||||
|
sync::mpsc::{sync_channel, Sender, SyncSender},
|
||||||
|
thread,
|
||||||
};
|
};
|
||||||
use std::io::{self, StdoutLock, Write};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{AppState, ExercisesProgress},
|
app_state::{AppState, ExercisesProgress},
|
||||||
clear_terminal,
|
clear_terminal,
|
||||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
|
||||||
progress_bar::progress_bar,
|
term::progress_bar,
|
||||||
terminal_link::TerminalFileLink,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent};
|
||||||
|
|
||||||
|
const HEADING_ATTRIBUTES: Attributes = Attributes::none()
|
||||||
|
.with(Attribute::Bold)
|
||||||
|
.with(Attribute::Underlined);
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
enum DoneStatus {
|
enum DoneStatus {
|
||||||
DoneWithSolution(String),
|
DoneWithSolution(String),
|
||||||
|
@ -21,45 +32,65 @@ enum DoneStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WatchState<'a> {
|
pub struct WatchState<'a> {
|
||||||
writer: StdoutLock<'a>,
|
|
||||||
app_state: &'a mut AppState,
|
app_state: &'a mut AppState,
|
||||||
output: Vec<u8>,
|
output: Vec<u8>,
|
||||||
show_hint: bool,
|
show_hint: bool,
|
||||||
done_status: DoneStatus,
|
done_status: DoneStatus,
|
||||||
manual_run: bool,
|
manual_run: bool,
|
||||||
|
term_width: u16,
|
||||||
|
terminal_event_unpause_sender: SyncSender<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> WatchState<'a> {
|
impl<'a> WatchState<'a> {
|
||||||
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
|
pub fn build(
|
||||||
let writer = io::stdout().lock();
|
app_state: &'a mut AppState,
|
||||||
|
watch_event_sender: Sender<WatchEvent>,
|
||||||
|
manual_run: bool,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let term_width = terminal::size()
|
||||||
|
.context("Failed to get the terminal size")?
|
||||||
|
.0;
|
||||||
|
|
||||||
Self {
|
let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0);
|
||||||
writer,
|
|
||||||
|
thread::Builder::new()
|
||||||
|
.spawn(move || {
|
||||||
|
terminal_event_handler(
|
||||||
|
watch_event_sender,
|
||||||
|
terminal_event_unpause_receiver,
|
||||||
|
manual_run,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.context("Failed to spawn a thread to handle terminal events")?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
app_state,
|
app_state,
|
||||||
output: Vec::with_capacity(OUTPUT_CAPACITY),
|
output: Vec::with_capacity(OUTPUT_CAPACITY),
|
||||||
show_hint: false,
|
show_hint: false,
|
||||||
done_status: DoneStatus::Pending,
|
done_status: DoneStatus::Pending,
|
||||||
manual_run,
|
manual_run,
|
||||||
}
|
term_width,
|
||||||
|
terminal_event_unpause_sender,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
||||||
pub fn into_writer(self) -> StdoutLock<'a> {
|
// Ignore any input until running the exercise is done.
|
||||||
self.writer
|
let _input_pause_guard = InputPauseGuard::scoped_pause();
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_current_exercise(&mut self) -> Result<()> {
|
|
||||||
self.show_hint = false;
|
self.show_hint = false;
|
||||||
|
|
||||||
writeln!(
|
writeln!(
|
||||||
self.writer,
|
stdout,
|
||||||
"\nChecking the exercise `{}`. Please wait…",
|
"\nChecking the exercise `{}`. Please wait…",
|
||||||
self.app_state.current_exercise().name,
|
self.app_state.current_exercise().name,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let success = self
|
let success = self
|
||||||
.app_state
|
.app_state
|
||||||
.current_exercise()
|
.current_exercise()
|
||||||
.run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?;
|
.run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?;
|
||||||
|
self.output.push(b'\n');
|
||||||
if success {
|
if success {
|
||||||
self.done_status =
|
self.done_status =
|
||||||
if let Some(solution_path) = self.app_state.current_solution_path()? {
|
if let Some(solution_path) = self.app_state.current_solution_path()? {
|
||||||
|
@ -74,106 +105,194 @@ impl<'a> WatchState<'a> {
|
||||||
self.done_status = DoneStatus::Pending;
|
self.done_status = DoneStatus::Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.render()
|
self.render(stdout)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> {
|
pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
||||||
// Don't skip exercises on file changes to avoid confusion from missing exercises.
|
clear_terminal(stdout)?;
|
||||||
// Skipping exercises must be explicit in the interactive list.
|
|
||||||
// But going back to an earlier exercise on file change is fine.
|
|
||||||
if self.app_state.current_exercise_ind() < exercise_ind {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
stdout.write_all(b"Resetting will undo all your changes to the file ")?;
|
||||||
self.run_current_exercise()
|
stdout.write_all(self.app_state.current_exercise().path.as_bytes())?;
|
||||||
}
|
stdout.write_all(b"\nReset (y/n)? ")?;
|
||||||
|
stdout.flush()?;
|
||||||
|
|
||||||
/// Move on to the next exercise if the current one is done.
|
{
|
||||||
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
|
let mut stdin = io::stdin().lock();
|
||||||
if self.done_status == DoneStatus::Pending {
|
let mut answer = [0];
|
||||||
return Ok(ExercisesProgress::CurrentPending);
|
loop {
|
||||||
}
|
stdin
|
||||||
|
.read_exact(&mut answer)
|
||||||
|
.context("Failed to read the user's input")?;
|
||||||
|
|
||||||
self.app_state.done_current_exercise(&mut self.writer)
|
match answer[0] {
|
||||||
}
|
b'y' | b'Y' => {
|
||||||
|
self.app_state.reset_current_exercise()?;
|
||||||
fn show_prompt(&mut self) -> io::Result<()> {
|
|
||||||
self.writer.write_all(b"\n")?;
|
|
||||||
|
|
||||||
|
// The file watcher reruns the exercise otherwise.
|
||||||
if self.manual_run {
|
if self.manual_run {
|
||||||
write!(self.writer, "{}:run / ", 'r'.bold())?;
|
self.run_current_exercise(stdout)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'n' | b'N' => self.render(stdout)?,
|
||||||
|
_ => continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.done_status != DoneStatus::Pending {
|
break;
|
||||||
write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.show_hint {
|
self.terminal_event_unpause_sender.send(())?;
|
||||||
write!(self.writer, "{}:hint / ", 'h'.bold())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?;
|
|
||||||
|
|
||||||
self.writer.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(&mut self) -> Result<()> {
|
|
||||||
// Prevent having the first line shifted if clearing wasn't successful.
|
|
||||||
self.writer.write_all(b"\n")?;
|
|
||||||
|
|
||||||
clear_terminal(&mut self.writer)?;
|
|
||||||
|
|
||||||
self.writer.write_all(&self.output)?;
|
|
||||||
self.writer.write_all(b"\n")?;
|
|
||||||
|
|
||||||
if self.show_hint {
|
|
||||||
writeln!(
|
|
||||||
self.writer,
|
|
||||||
"{}\n{}\n",
|
|
||||||
"Hint".bold().cyan().underlined(),
|
|
||||||
self.app_state.current_exercise().hint,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.done_status != DoneStatus::Pending {
|
|
||||||
writeln!(
|
|
||||||
self.writer,
|
|
||||||
"{}\n",
|
|
||||||
"Exercise done ✓
|
|
||||||
When you are done experimenting, enter `n` to move on to the next exercise 🦀"
|
|
||||||
.bold()
|
|
||||||
.green(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status {
|
|
||||||
writeln!(
|
|
||||||
self.writer,
|
|
||||||
"A solution file can be found at {}\n",
|
|
||||||
style(TerminalFileLink(solution_path)).underlined().green(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_width = terminal::size()?.0;
|
|
||||||
let progress_bar = progress_bar(
|
|
||||||
self.app_state.n_done(),
|
|
||||||
self.app_state.exercises().len() as u16,
|
|
||||||
line_width,
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
self.writer,
|
|
||||||
"{progress_bar}Current exercise: {}",
|
|
||||||
self.app_state.current_exercise().terminal_link(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
self.show_prompt()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_hint(&mut self) -> Result<()> {
|
pub fn handle_file_change(
|
||||||
|
&mut self,
|
||||||
|
exercise_ind: usize,
|
||||||
|
stdout: &mut StdoutLock,
|
||||||
|
) -> Result<()> {
|
||||||
|
if self.app_state.current_exercise_ind() != exercise_ind {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.run_current_exercise(stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move on to the next exercise if the current one is done.
|
||||||
|
pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||||
|
match self.done_status {
|
||||||
|
DoneStatus::DoneWithSolution(_) | DoneStatus::DoneWithoutSolution => (),
|
||||||
|
DoneStatus::Pending => return Ok(ExercisesProgress::CurrentPending),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.app_state.done_current_exercise::<true>(stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
if self.done_status != DoneStatus::Pending {
|
||||||
|
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||||
|
stdout.write_all(b"n")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b":")?;
|
||||||
|
stdout.queue(SetAttribute(Attribute::Underlined))?;
|
||||||
|
stdout.write_all(b"next")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b" / ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut show_key = |key, postfix| {
|
||||||
|
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||||
|
stdout.write_all(&[key])?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(postfix)
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.manual_run {
|
||||||
|
show_key(b'r', b":run / ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.show_hint {
|
||||||
|
show_key(b'h', b":hint / ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
show_key(b'l', b":list / ")?;
|
||||||
|
show_key(b'c', b":check all / ")?;
|
||||||
|
show_key(b'x', b":reset / ")?;
|
||||||
|
show_key(b'q', b":quit ? ")?;
|
||||||
|
|
||||||
|
stdout.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
// Prevent having the first line shifted if clearing wasn't successful.
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
clear_terminal(stdout)?;
|
||||||
|
|
||||||
|
stdout.write_all(&self.output)?;
|
||||||
|
|
||||||
|
if self.show_hint {
|
||||||
|
stdout
|
||||||
|
.queue(SetAttributes(HEADING_ATTRIBUTES))?
|
||||||
|
.queue(SetForegroundColor(Color::Cyan))?;
|
||||||
|
stdout.write_all(b"Hint")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
|
||||||
|
stdout.write_all(self.app_state.current_exercise().hint.as_bytes())?;
|
||||||
|
stdout.write_all(b"\n\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.done_status != DoneStatus::Pending {
|
||||||
|
stdout
|
||||||
|
.queue(SetAttribute(Attribute::Bold))?
|
||||||
|
.queue(SetForegroundColor(Color::Green))?;
|
||||||
|
stdout.write_all("Exercise done ✓".as_bytes())?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
|
||||||
|
if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status {
|
||||||
|
solution_link_line(stdout, solution_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.write_all(
|
||||||
|
"When done experimenting, enter `n` to move on to the next exercise 🦀\n\n"
|
||||||
|
.as_bytes(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress_bar(
|
||||||
|
stdout,
|
||||||
|
self.app_state.n_done(),
|
||||||
|
self.app_state.exercises().len() as u16,
|
||||||
|
self.term_width,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
stdout.write_all(b"\nCurrent exercise: ")?;
|
||||||
|
self.app_state
|
||||||
|
.current_exercise()
|
||||||
|
.terminal_file_link(stdout)?;
|
||||||
|
stdout.write_all(b"\n\n")?;
|
||||||
|
|
||||||
|
self.show_prompt(stdout)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
if !self.show_hint {
|
||||||
self.show_hint = true;
|
self.show_hint = true;
|
||||||
self.render()
|
self.render(stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||||
|
// Ignore any input until checking all exercises is done.
|
||||||
|
let _input_pause_guard = InputPauseGuard::scoped_pause();
|
||||||
|
|
||||||
|
if let Some(first_pending_exercise_ind) = self.app_state.check_all_exercises(stdout)? {
|
||||||
|
// Only change exercise if the current one is done.
|
||||||
|
if self.app_state.current_exercise().done {
|
||||||
|
self.app_state
|
||||||
|
.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||||
|
Ok(ExercisesProgress::NewPending)
|
||||||
|
} else {
|
||||||
|
Ok(ExercisesProgress::CurrentPending)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.app_state.render_final_message(stdout)?;
|
||||||
|
Ok(ExercisesProgress::AllDone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
if self.term_width != width {
|
||||||
|
self.term_width = width;
|
||||||
|
self.render(stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,86 +1,73 @@
|
||||||
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::{
|
||||||
|
atomic::Ordering::Relaxed,
|
||||||
|
mpsc::{Receiver, Sender},
|
||||||
|
};
|
||||||
|
|
||||||
use super::WatchEvent;
|
use super::{WatchEvent, EXERCISE_RUNNING};
|
||||||
|
|
||||||
pub enum InputEvent {
|
pub enum InputEvent {
|
||||||
Run,
|
|
||||||
Next,
|
Next,
|
||||||
|
Run,
|
||||||
Hint,
|
Hint,
|
||||||
List,
|
List,
|
||||||
|
CheckAll,
|
||||||
|
Reset,
|
||||||
Quit,
|
Quit,
|
||||||
Unrecognized,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
pub fn terminal_event_handler(
|
||||||
// Only send `Unrecognized` on ENTER if the last input wasn't valid.
|
sender: Sender<WatchEvent>,
|
||||||
let mut last_input_valid = false;
|
unpause_receiver: Receiver<()>,
|
||||||
|
manual_run: bool,
|
||||||
let last_input_event = loop {
|
) {
|
||||||
let terminal_event = match event::read() {
|
let last_watch_event = loop {
|
||||||
Ok(v) => v,
|
match event::read() {
|
||||||
Err(e) => {
|
Ok(Event::Key(key)) => {
|
||||||
// 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) => {
|
|
||||||
match key.kind {
|
match key.kind {
|
||||||
KeyEventKind::Release | KeyEventKind::Repeat => continue,
|
KeyEventKind::Release | KeyEventKind::Repeat => continue,
|
||||||
KeyEventKind::Press => (),
|
KeyEventKind::Press => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
if key.modifiers != KeyModifiers::NONE {
|
if EXERCISE_RUNNING.load(Relaxed) {
|
||||||
last_input_valid = false;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let input_event = match key.code {
|
let input_event = match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Char('n') => InputEvent::Next,
|
||||||
if last_input_valid {
|
KeyCode::Char('r') if manual_run => InputEvent::Run,
|
||||||
continue;
|
KeyCode::Char('h') => InputEvent::Hint,
|
||||||
|
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
|
||||||
|
KeyCode::Char('c') => InputEvent::CheckAll,
|
||||||
|
KeyCode::Char('x') => {
|
||||||
|
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputEvent::Unrecognized
|
// Pause input until quitting the confirmation prompt.
|
||||||
}
|
if unpause_receiver.recv().is_err() {
|
||||||
KeyCode::Char(c) => {
|
return;
|
||||||
let input_event = match c {
|
|
||||||
'n' => InputEvent::Next,
|
|
||||||
'h' => InputEvent::Hint,
|
|
||||||
'l' => break InputEvent::List,
|
|
||||||
'q' => break InputEvent::Quit,
|
|
||||||
'r' if manual_run => InputEvent::Run,
|
|
||||||
_ => {
|
|
||||||
last_input_valid = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
last_input_valid = true;
|
|
||||||
input_event
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
last_input_valid = false;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit),
|
||||||
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
if sender.send(WatchEvent::Input(input_event)).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Resize(_, _) => {
|
Ok(Event::Resize(width, _)) => {
|
||||||
if tx.send(WatchEvent::TerminalResize).is_err() {
|
if sender.send(WatchEvent::TerminalResize { width }).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
|
Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue,
|
||||||
|
Err(e) => break WatchEvent::TerminalEventErr(e),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = tx.send(WatchEvent::Input(last_input_event));
|
let _ = sender.send(last_watch_event);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue