summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo8it <mo8it@proton.me>2024-04-14 01:15:43 +0200
committermo8it <mo8it@proton.me>2024-04-14 01:15:43 +0200
commit5c0073a9485c4226e58b657cb49628919a28a942 (patch)
tree00cd8cb2ca8926e5e6cf68e44d51d14f77cc4695
parent2a26dfcb005d2a9ee24e920462b37dfb6d235c32 (diff)
Tolerate changes in the state file
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--exercises/00_intro/intro1.rs1
-rw-r--r--info.toml272
-rw-r--r--src/app_state.rs207
-rw-r--r--src/app_state/state_file.rs112
-rw-r--r--src/exercise.rs72
-rw-r--r--src/info_file.rs81
-rw-r--r--src/init.rs23
-rw-r--r--src/list.rs11
-rw-r--r--src/list/state.rs35
-rw-r--r--src/main.rs40
-rw-r--r--src/run.rs2
-rw-r--r--src/watch.rs15
-rw-r--r--src/watch/notify_event.rs (renamed from src/watch/debounce_event.rs)10
15 files changed, 514 insertions, 369 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6c64661..dbf1923 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -684,6 +684,7 @@ dependencies = [
"assert_cmd",
"clap",
"crossterm",
+ "hashbrown",
"notify-debouncer-mini",
"predicates",
"ratatui",
diff --git a/Cargo.toml b/Cargo.toml
index 285e7df..14ae9a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,6 +37,7 @@ edition.workspace = true
anyhow.workspace = true
clap = { version = "4.5.4", features = ["derive"] }
crossterm = "0.27.0"
+hashbrown = "0.14.3"
notify-debouncer-mini = "0.4.1"
ratatui = "0.26.1"
rustlings-macros = { path = "rustlings-macros" }
diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs
index e4e0444..170d195 100644
--- a/exercises/00_intro/intro1.rs
+++ b/exercises/00_intro/intro1.rs
@@ -1,6 +1,5 @@
// intro1.rs
//
-// TODO: Update comment
// We sometimes encourage you to keep trying things on a given exercise, even
// after you already figured it out. If you got everything working and feel
// ready for the next exercise, remove the `I AM NOT DONE` comment below.
diff --git a/info.toml b/info.toml
index b6b6800..fa90ad7 100644
--- a/info.toml
+++ b/info.toml
@@ -33,10 +33,11 @@ https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md
# INTRO
+# TODO: Update exercise
[[exercises]]
name = "intro1"
-path = "exercises/00_intro/intro1.rs"
-mode = "compile"
+dir = "00_intro"
+mode = "run"
# TODO: Fix hint
hint = """
Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file
@@ -44,8 +45,8 @@ to move on to the next exercise."""
[[exercises]]
name = "intro2"
-path = "exercises/00_intro/intro2.rs"
-mode = "compile"
+dir = "00_intro"
+mode = "run"
hint = """
The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative."""
@@ -53,16 +54,16 @@ The compiler is informing us that we've got the name of the print macro wrong, a
[[exercises]]
name = "variables1"
-path = "exercises/01_variables/variables1.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
The declaration in the first line in the main function is missing a keyword
that is needed in Rust to create a new variable binding."""
[[exercises]]
name = "variables2"
-path = "exercises/01_variables/variables2.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
The compiler message is saying that Rust cannot infer the type that the
variable binding `x` has with what is given here.
@@ -80,8 +81,8 @@ What if `x` is the same type as `10`? What if it's a different type?"""
[[exercises]]
name = "variables3"
-path = "exercises/01_variables/variables3.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
Oops! In this exercise, we have a variable binding that we've created on in the
first line in the `main` function, and we're trying to use it in the next line,
@@ -94,8 +95,8 @@ programming language -- thankfully the Rust compiler has caught this for us!"""
[[exercises]]
name = "variables4"
-path = "exercises/01_variables/variables4.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
In Rust, variable bindings are immutable by default. But here we're trying
to reassign a different value to `x`! There's a keyword we can use to make
@@ -103,8 +104,8 @@ a variable binding mutable instead."""
[[exercises]]
name = "variables5"
-path = "exercises/01_variables/variables5.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
In `variables4` we already learned how to make an immutable variable mutable
using a special keyword. Unfortunately this doesn't help us much in this
@@ -121,8 +122,8 @@ Try to solve this exercise afterwards using this technique."""
[[exercises]]
name = "variables6"
-path = "exercises/01_variables/variables6.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
We know about variables and mutability, but there is another important type of
variable available: constants.
@@ -141,8 +142,8 @@ https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants
[[exercises]]
name = "functions1"
-path = "exercises/02_functions/functions1.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
This main function is calling a function that it expects to exist, but the
function doesn't exist. It expects this function to have the name `call_me`.
@@ -151,24 +152,24 @@ Sounds a lot like `main`, doesn't it?"""
[[exercises]]
name = "functions2"
-path = "exercises/02_functions/functions2.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
Rust requires that all parts of a function's signature have type annotations,
but `call_me` is missing the type annotation of `num`."""
[[exercises]]
name = "functions3"
-path = "exercises/02_functions/functions3.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
This time, the function *declaration* is okay, but there's something wrong
with the place where we're calling the function."""
[[exercises]]
name = "functions4"
-path = "exercises/02_functions/functions4.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
The error message points to the function `sale_price` and says it expects a type
after the `->`. This is where the function's return type should be -- take a
@@ -179,8 +180,8 @@ for the inputs of the functions here, since the original prices shouldn't be neg
[[exercises]]
name = "functions5"
-path = "exercises/02_functions/functions5.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
This is a really common error that can be fixed by removing one character.
It happens because Rust distinguishes between expressions and statements:
@@ -198,7 +199,7 @@ They are not the same. There are two solutions:
[[exercises]]
name = "if1"
-path = "exercises/03_if/if1.rs"
+dir = "03_if"
mode = "test"
hint = """
It's possible to do this in one line if you would like!
@@ -214,7 +215,7 @@ Remember in Rust that:
[[exercises]]
name = "if2"
-path = "exercises/03_if/if2.rs"
+dir = "03_if"
mode = "test"
hint = """
For that first compiler error, it's important in Rust that each conditional
@@ -223,7 +224,7 @@ conditions checking different input values."""
[[exercises]]
name = "if3"
-path = "exercises/03_if/if3.rs"
+dir = "03_if"
mode = "test"
hint = """
In Rust, every arm of an `if` expression has to return the same type of value.
@@ -233,7 +234,6 @@ Make sure the type is consistent across all arms."""
[[exercises]]
name = "quiz1"
-path = "exercises/quiz1.rs"
mode = "test"
hint = "No hints this time ;)"
@@ -241,20 +241,20 @@ hint = "No hints this time ;)"
[[exercises]]
name = "primitive_types1"
-path = "exercises/04_primitive_types/primitive_types1.rs"
-mode = "compile"
+dir = "04_primitive_types"
+mode = "run"
hint = "No hints this time ;)"
[[exercises]]
name = "primitive_types2"
-path = "exercises/04_primitive_types/primitive_types2.rs"
-mode = "compile"
+dir = "04_primitive_types"
+mode = "run"
hint = "No hints this time ;)"
[[exercises]]
name = "primitive_types3"
-path = "exercises/04_primitive_types/primitive_types3.rs"
-mode = "compile"
+dir = "04_primitive_types"
+mode = "run"
hint = """
There's a shorthand to initialize Arrays with a certain size that does not
require you to type in 100 items (but you certainly can if you want!).
@@ -269,7 +269,7 @@ for `a.len() >= 100`?"""
[[exercises]]
name = "primitive_types4"
-path = "exercises/04_primitive_types/primitive_types4.rs"
+dir = "04_primitive_types"
mode = "test"
hint = """
Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section
@@ -284,8 +284,8 @@ https://doc.rust-lang.org/nomicon/coercions.html"""
[[exercises]]
name = "primitive_types5"
-path = "exercises/04_primitive_types/primitive_types5.rs"
-mode = "compile"
+dir = "04_primitive_types"
+mode = "run"
hint = """
Take a look at the 'Data Types -> The Tuple Type' section of the book:
https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type
@@ -297,7 +297,7 @@ of the tuple. You can do it!!"""
[[exercises]]
name = "primitive_types6"
-path = "exercises/04_primitive_types/primitive_types6.rs"
+dir = "04_primitive_types"
mode = "test"
hint = """
While you could use a destructuring `let` for the tuple here, try
@@ -310,7 +310,7 @@ Now you have another tool in your toolbox!"""
[[exercises]]
name = "vecs1"
-path = "exercises/05_vecs/vecs1.rs"
+dir = "05_vecs"
mode = "test"
hint = """
In Rust, there are two ways to define a Vector.
@@ -325,7 +325,7 @@ of the Rust book to learn more.
[[exercises]]
name = "vecs2"
-path = "exercises/05_vecs/vecs2.rs"
+dir = "05_vecs"
mode = "test"
hint = """
In the first function we are looping over the Vector and getting a reference to
@@ -348,7 +348,7 @@ What do you think is the more commonly used pattern under Rust developers?
[[exercises]]
name = "move_semantics1"
-path = "exercises/06_move_semantics/move_semantics1.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
So you've got the "cannot borrow immutable local variable `vec` as mutable"
@@ -362,7 +362,7 @@ happens!"""
[[exercises]]
name = "move_semantics2"
-path = "exercises/06_move_semantics/move_semantics2.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
When running this exercise for the first time, you'll notice an error about
@@ -383,7 +383,7 @@ try them all:
[[exercises]]
name = "move_semantics3"
-path = "exercises/06_move_semantics/move_semantics3.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
The difference between this one and the previous ones is that the first line
@@ -393,7 +393,7 @@ an existing binding to be a mutable binding instead of an immutable one :)"""
[[exercises]]
name = "move_semantics4"
-path = "exercises/06_move_semantics/move_semantics4.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
Stop reading whenever you feel like you have enough direction :) Or try
@@ -407,7 +407,7 @@ So the end goal is to:
[[exercises]]
name = "move_semantics5"
-path = "exercises/06_move_semantics/move_semantics5.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
Carefully reason about the range in which each mutable reference is in
@@ -419,8 +419,8 @@ https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-ref
[[exercises]]
name = "move_semantics6"
-path = "exercises/06_move_semantics/move_semantics6.rs"
-mode = "compile"
+dir = "06_move_semantics"
+mode = "run"
hint = """
To find the answer, you can consult the book section "References and Borrowing":
https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html
@@ -440,7 +440,7 @@ Another hint: it has to do with the `&` character."""
[[exercises]]
name = "structs1"
-path = "exercises/07_structs/structs1.rs"
+dir = "07_structs"
mode = "test"
hint = """
Rust has more than one type of struct. Three actually, all variants are used to
@@ -460,7 +460,7 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html"""
[[exercises]]
name = "structs2"
-path = "exercises/07_structs/structs2.rs"
+dir = "07_structs"
mode = "test"
hint = """
Creating instances of structs is easy, all you need to do is assign some values
@@ -472,7 +472,7 @@ https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-ins
[[exercises]]
name = "structs3"
-path = "exercises/07_structs/structs3.rs"
+dir = "07_structs"
mode = "test"
hint = """
For `is_international`: What makes a package international? Seems related to
@@ -488,21 +488,21 @@ https://doc.rust-lang.org/book/ch05-03-method-syntax.html"""
[[exercises]]
name = "enums1"
-path = "exercises/08_enums/enums1.rs"
-mode = "compile"
+dir = "08_enums"
+mode = "run"
hint = "No hints this time ;)"
[[exercises]]
name = "enums2"
-path = "exercises/08_enums/enums2.rs"
-mode = "compile"
+dir = "08_enums"
+mode = "run"
hint = """
You can create enumerations that have different variants with different types
such as no data, anonymous structs, a single string, tuples, ...etc"""
[[exercises]]
name = "enums3"
-path = "exercises/08_enums/enums3.rs"
+dir = "08_enums"
mode = "test"
hint = """
As a first step, you can define enums to compile this code without errors.
@@ -516,8 +516,8 @@ to get value in the variant."""
[[exercises]]
name = "strings1"
-path = "exercises/09_strings/strings1.rs"
-mode = "compile"
+dir = "09_strings"
+mode = "run"
hint = """
The `current_favorite_color` function is currently returning a string slice
with the `'static` lifetime. We know this because the data of the string lives
@@ -530,8 +530,8 @@ another way that uses the `From` trait."""
[[exercises]]
name = "strings2"
-path = "exercises/09_strings/strings2.rs"
-mode = "compile"
+dir = "09_strings"
+mode = "run"
hint = """
Yes, it would be really easy to fix this by just changing the value bound to
`word` to be a string slice instead of a `String`, wouldn't it?? There is a way
@@ -545,7 +545,7 @@ https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercion
[[exercises]]
name = "strings3"
-path = "exercises/09_strings/strings3.rs"
+dir = "09_strings"
mode = "test"
hint = """
There's tons of useful standard library functions for strings. Let's try and use some of them:
@@ -556,16 +556,16 @@ the string slice into an owned string, which you can then freely extend."""
[[exercises]]
name = "strings4"
-path = "exercises/09_strings/strings4.rs"
-mode = "compile"
+dir = "09_strings"
+mode = "run"
hint = "No hints this time ;)"
# MODULES
[[exercises]]
name = "modules1"
-path = "exercises/10_modules/modules1.rs"
-mode = "compile"
+dir = "10_modules"
+mode = "run"
hint = """
Everything is private in Rust by default-- but there's a keyword we can use
to make something public! The compiler error should point to the thing that
@@ -573,8 +573,8 @@ needs to be public."""
[[exercises]]
name = "modules2"
-path = "exercises/10_modules/modules2.rs"
-mode = "compile"
+dir = "10_modules"
+mode = "run"
hint = """
The delicious_snacks module is trying to present an external interface that is
different than its internal structure (the `fruits` and `veggies` modules and
@@ -585,8 +585,8 @@ Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-w
[[exercises]]
name = "modules3"
-path = "exercises/10_modules/modules3.rs"
-mode = "compile"
+dir = "10_modules"
+mode = "run"
hint = """
`UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a
`use` statement for these two to bring them into scope. You can use nested
@@ -596,7 +596,7 @@ paths or the glob operator to bring these two in using only one line."""
[[exercises]]
name = "hashmaps1"
-path = "exercises/11_hashmaps/hashmaps1.rs"
+dir = "11_hashmaps"
mode = "test"
hint = """
Hint 1: Take a look at the return type of the function to figure out
@@ -608,7 +608,7 @@ Hint 2: Number of fruits should be at least 5. And you have to put
[[exercises]]
name = "hashmaps2"
-path = "exercises/11_hashmaps/hashmaps2.rs"
+dir = "11_hashmaps"
mode = "test"
hint = """
Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this.
@@ -617,7 +617,7 @@ Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-
[[exercises]]
name = "hashmaps3"
-path = "exercises/11_hashmaps/hashmaps3.rs"
+dir = "11_hashmaps"
mode = "test"
hint = """
Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert
@@ -635,7 +635,6 @@ Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-v
[[exercises]]
name = "quiz2"
-path = "exercises/quiz2.rs"
mode = "test"
hint = "No hints this time ;)"
@@ -643,7 +642,7 @@ hint = "No hints this time ;)"
[[exercises]]
name = "options1"
-path = "exercises/12_options/options1.rs"
+dir = "12_options"
mode = "test"
hint = """
Options can have a `Some` value, with an inner value, or a `None` value,
@@ -655,7 +654,7 @@ it doesn't panic in your face later?"""
[[exercises]]
name = "options2"
-path = "exercises/12_options/options2.rs"
+dir = "12_options"
mode = "test"
hint = """
Check out:
@@ -672,8 +671,8 @@ Also see `Option::flatten`
[[exercises]]
name = "options3"
-path = "exercises/12_options/options3.rs"
-mode = "compile"
+dir = "12_options"
+mode = "run"
hint = """
The compiler says a partial move happened in the `match` statement. How can
this be avoided? The compiler shows the correction needed.
@@ -685,7 +684,7 @@ https://doc.rust-lang.org/std/keyword.ref.html"""
[[exercises]]
name = "errors1"
-path = "exercises/13_error_handling/errors1.rs"
+dir = "13_error_handling"
mode = "test"
hint = """
`Ok` and `Err` are the two variants of `Result`, so what the tests are saying
@@ -701,7 +700,7 @@ To make this change, you'll need to:
[[exercises]]
name = "errors2"
-path = "exercises/13_error_handling/errors2.rs"
+dir = "13_error_handling"
mode = "test"
hint = """
One way to handle this is using a `match` statement on
@@ -717,8 +716,8 @@ and give it a try!"""
[[exercises]]
name = "errors3"
-path = "exercises/13_error_handling/errors3.rs"
-mode = "compile"
+dir = "13_error_handling"
+mode = "run"
hint = """
If other functions can return a `Result`, why shouldn't `main`? It's a fairly
common convention to return something like `Result<(), ErrorType>` from your
@@ -729,7 +728,7 @@ positive results."""
[[exercises]]
name = "errors4"
-path = "exercises/13_error_handling/errors4.rs"
+dir = "13_error_handling"
mode = "test"
hint = """
`PositiveNonzeroInteger::new` is always creating a new instance and returning
@@ -741,8 +740,8 @@ everything is... okay :)"""
[[exercises]]
name = "errors5"
-path = "exercises/13_error_handling/errors5.rs"
-mode = "compile"
+dir = "13_error_handling"
+mode = "run"
hint = """
There are two different possible `Result` types produced within `main()`, which
are propagated using `?` operators. How do we declare a return type from
@@ -765,7 +764,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen
[[exercises]]
name = "errors6"
-path = "exercises/13_error_handling/errors6.rs"
+dir = "13_error_handling"
mode = "test"
hint = """
This exercise uses a completed version of `PositiveNonzeroInteger` from
@@ -787,8 +786,8 @@ https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err"""
[[exercises]]
name = "generics1"
-path = "exercises/14_generics/generics1.rs"
-mode = "compile"
+dir = "14_generics"
+mode = "run"
hint = """
Vectors in Rust make use of generics to create dynamically sized arrays of any
type.
@@ -797,7 +796,7 @@ You need to tell the compiler what type we are pushing onto this vector."""
[[exercises]]
name = "generics2"
-path = "exercises/14_generics/generics2.rs"
+dir = "14_generics"
mode = "test"
hint = """
Currently we are wrapping only values of type `u32`.
@@ -811,7 +810,7 @@ If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html
[[exercises]]
name = "traits1"
-path = "exercises/15_traits/traits1.rs"
+dir = "15_traits"
mode = "test"
hint = """
A discussion about Traits in Rust can be found at:
@@ -820,7 +819,7 @@ https://doc.rust-lang.org/book/ch10-02-traits.html
[[exercises]]
name = "traits2"
-path = "exercises/15_traits/traits2.rs"
+dir = "15_traits"
mode = "test"
hint = """
Notice how the trait takes ownership of `self`, and returns `Self`.
@@ -833,7 +832,7 @@ the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html"""
[[exercises]]
name = "traits3"
-path = "exercises/15_traits/traits3.rs"
+dir = "15_traits"
mode = "test"
hint = """
Traits can have a default implementation for functions. Structs that implement
@@ -845,7 +844,7 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#def
[[exercises]]
name = "traits4"
-path = "exercises/15_traits/traits4.rs"
+dir = "15_traits"
mode = "test"
hint = """
Instead of using concrete types as parameters you can use traits. Try replacing
@@ -856,8 +855,8 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#tra
[[exercises]]
name = "traits5"
-path = "exercises/15_traits/traits5.rs"
-mode = "compile"
+dir = "15_traits"
+mode = "run"
hint = """
To ensure a parameter implements multiple traits use the '+ syntax'. Try
replacing the '??' with 'impl <> + <>'.
@@ -869,7 +868,6 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#spe
[[exercises]]
name = "quiz3"
-path = "exercises/quiz3.rs"
mode = "test"
hint = """
To find the best solution to this challenge you're going to need to think back
@@ -881,16 +879,16 @@ You may also need this: `use std::fmt::Display;`."""
[[exercises]]
name = "lifetimes1"
-path = "exercises/16_lifetimes/lifetimes1.rs"
-mode = "compile"
+dir = "16_lifetimes"
+mode = "run"
hint = """
Let the compiler guide you. Also take a look at the book if you need help:
https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html"""
[[exercises]]
name = "lifetimes2"
-path = "exercises/16_lifetimes/lifetimes2.rs"
-mode = "compile"
+dir = "16_lifetimes"
+mode = "run"
hint = """
Remember that the generic lifetime `'a` will get the concrete lifetime that is
equal to the smaller of the lifetimes of `x` and `y`.
@@ -903,8 +901,8 @@ inner block:
[[exercises]]
name = "lifetimes3"
-path = "exercises/16_lifetimes/lifetimes3.rs"
-mode = "compile"
+dir = "16_lifetimes"
+mode = "run"
hint = """
If you use a lifetime annotation in a struct's fields, where else does it need
to be added?"""
@@ -913,7 +911,7 @@ to be added?"""
[[exercises]]
name = "tests1"
-path = "exercises/17_tests/tests1.rs"
+dir = "17_tests"
mode = "test"
hint = """
You don't even need to write any code to test -- you can just test values and
@@ -928,7 +926,7 @@ ones pass, and which ones fail :)"""
[[exercises]]
name = "tests2"
-path = "exercises/17_tests/tests2.rs"
+dir = "17_tests"
mode = "test"
hint = """
Like the previous exercise, you don't need to write any code to get this test
@@ -941,7 +939,7 @@ argument comes first and which comes second!"""
[[exercises]]
name = "tests3"
-path = "exercises/17_tests/tests3.rs"
+dir = "17_tests"
mode = "test"
hint = """
You can call a function right where you're passing arguments to `assert!`. So
@@ -952,7 +950,7 @@ what you're doing using `!`, like `assert!(!having_fun())`."""
[[exercises]]
name = "tests4"
-path = "exercises/17_tests/tests4.rs"
+dir = "17_tests"
mode = "test"
hint = """
We expect method `Rectangle::new()` to panic for negative values.
@@ -966,7 +964,7 @@ https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-pa
[[exercises]]
name = "iterators1"
-path = "exercises/18_iterators/iterators1.rs"
+dir = "18_iterators"
mode = "test"
hint = """
Step 1:
@@ -989,7 +987,7 @@ https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas.
[[exercises]]
name = "iterators2"
-path = "exercises/18_iterators/iterators2.rs"
+dir = "18_iterators"
mode = "test"
hint = """
Step 1:
@@ -1015,7 +1013,7 @@ powerful and very general. Rust just needs to know the desired type."""
[[exercises]]
name = "iterators3"
-path = "exercises/18_iterators/iterators3.rs"
+dir = "18_iterators"
mode = "test"
hint = """
The `divide` function needs to return the correct error when even division is
@@ -1034,7 +1032,7 @@ powerful! It can make the solution to this exercise infinitely easier."""
[[exercises]]
name = "iterators4"
-path = "exercises/18_iterators/iterators4.rs"
+dir = "18_iterators"
mode = "test"
hint = """
In an imperative language, you might write a `for` loop that updates a mutable
@@ -1046,7 +1044,7 @@ Hint 2: Check out the `fold` and `rfold` methods!"""
[[exercises]]
name = "iterators5"
-path = "exercises/18_iterators/iterators5.rs"
+dir = "18_iterators"
mode = "test"
hint = """
The documentation for the `std::iter::Iterator` trait contains numerous methods
@@ -1065,7 +1063,7 @@ a different method that could make your code more compact than using `fold`."""
[[exercises]]
name = "box1"
-path = "exercises/19_smart_pointers/box1.rs"
+dir = "19_smart_pointers"
mode = "test"
hint = """
Step 1:
@@ -1089,7 +1087,7 @@ definition and try other types!
[[exercises]]
name = "rc1"
-path = "exercises/19_smart_pointers/rc1.rs"
+dir = "19_smart_pointers"
mode = "test"
hint = """
This is a straightforward exercise to use the `Rc<T>` type. Each `Planet` has
@@ -1108,8 +1106,8 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html
[[exercises]]
name = "arc1"
-path = "exercises/19_smart_pointers/arc1.rs"
-mode = "compile"
+dir = "19_smart_pointers"
+mode = "run"
hint = """
Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order
to avoid creating a copy of `numbers`, you'll need to create `child_numbers`
@@ -1126,7 +1124,7 @@ https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html
[[exercises]]
name = "cow1"
-path = "exercises/19_smart_pointers/cow1.rs"
+dir = "19_smart_pointers"
mode = "test"
hint = """
If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is
@@ -1140,8 +1138,8 @@ on the `Cow` type.
[[exercises]]
name = "threads1"
-path = "exercises/20_threads/threads1.rs"
-mode = "compile"
+dir = "20_threads"
+mode = "run"
hint = """
`JoinHandle` is a struct that is returned from a spawned thread:
https://doc.rust-lang.org/std/thread/fn.spawn.html
@@ -1158,8 +1156,8 @@ https://doc.rust-lang.org/std/thread/struct.JoinHandle.html
[[exercises]]
name = "threads2"
-path = "exercises/20_threads/threads2.rs"
-mode = "compile"
+dir = "20_threads"
+mode = "run"
hint = """
`Arc` is an Atomic Reference Counted pointer that allows safe, shared access
to **immutable** data. But we want to *change* the number of `jobs_completed`
@@ -1180,7 +1178,7 @@ https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-betwee
[[exercises]]
name = "threads3"
-path = "exercises/20_threads/threads3.rs"
+dir = "20_threads"
mode = "test"
hint = """
An alternate way to handle concurrency between threads is to use an `mpsc`
@@ -1199,8 +1197,8 @@ See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info.
[[exercises]]
name = "macros1"
-path = "exercises/21_macros/macros1.rs"
-mode = "compile"
+dir = "21_macros"
+mode = "run"
hint = """
When you call a macro, you need to add something special compared to a
regular function call. If you're stuck, take a look at what's inside
@@ -1208,8 +1206,8 @@ regular function call. If you're stuck, take a look at what's inside
[[exercises]]
name = "macros2"
-path = "exercises/21_macros/macros2.rs"
-mode = "compile"
+dir = "21_macros"
+mode = "run"
hint = """
Macros don't quite play by the same rules as the rest of Rust, in terms of
what's available where.
@@ -1219,8 +1217,8 @@ Unlike other things in Rust, the order of "where you define a macro" versus
[[exercises]]
name = "macros3"
-path = "exercises/21_macros/macros3.rs"
-mode = "compile"
+dir = "21_macros"
+mode = "run"
hint = """
In order to use a macro outside of its module, you need to do something
special to the module to lift the macro out into its parent.
@@ -1230,8 +1228,8 @@ exported macros, if you've seen any of those around."""
[[exercises]]
name = "macros4"
-path = "exercises/21_macros/macros4.rs"
-mode = "compile"
+dir = "21_macros"
+mode = "run"
hint = """
You only need to add a single character to make this compile.
@@ -1247,7 +1245,7 @@ https://veykril.github.io/tlborm/"""
[[exercises]]
name = "clippy1"
-path = "exercises/22_clippy/clippy1.rs"
+dir = "22_clippy"
mode = "clippy"
hint = """
Rust stores the highest precision version of any long or infinite precision
@@ -1263,14 +1261,14 @@ appropriate replacement constant from `std::f32::consts`..."""
[[exercises]]
name = "clippy2"
-path = "exercises/22_clippy/clippy2.rs"
+dir = "22_clippy"
mode = "clippy"
hint = """
`for` loops over `Option` values are more clearly expressed as an `if let`"""
[[exercises]]
name = "clippy3"
-path = "exercises/22_clippy/clippy3.rs"
+dir = "22_clippy"
mode = "clippy"
hint = "No hints this time!"
@@ -1278,7 +1276,7 @@ hint = "No hints this time!"
[[exercises]]
name = "using_as"
-path = "exercises/23_conversions/using_as.rs"
+dir = "23_conversions"
mode = "test"
hint = """
Use the `as` operator to cast one of the operands in the last line of the
@@ -1286,14 +1284,14 @@ Use the `as` operator to cast one of the operands in the last line of the
[[exercises]]
name = "from_into"
-path = "exercises/23_conversions/from_into.rs"
+dir = "23_conversions"
mode = "test"
hint = """
Follow the steps provided right before the `From` implementation"""
[[exercises]]
name = "from_str"
-path = "exercises/23_conversions/from_str.rs"
+dir = "23_conversions"
mode = "test"
hint = """
The implementation of `FromStr` should return an `Ok` with a `Person` object,
@@ -1314,7 +1312,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen
[[exercises]]
name = "try_from_into"
-path = "exercises/23_conversions/try_from_into.rs"
+dir = "23_conversions"
mode = "test"
hint = """
Follow the steps provided right before the `TryFrom` implementation.
@@ -1337,7 +1335,7 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer
[[exercises]]
name = "as_ref_mut"
-path = "exercises/23_conversions/as_ref_mut.rs"
+dir = "23_conversions"
mode = "test"
hint = """
Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions."""
diff --git a/src/app_state.rs b/src/app_state.rs
index 2ea3db4..1a051b9 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -4,52 +4,16 @@ use crossterm::{
terminal::{Clear, ClearType},
ExecutableCommand,
};
-use serde::{Deserialize, Serialize};
-use std::{
- fs,
- io::{StdoutLock, Write},
-};
-
-use crate::{exercise::Exercise, FENISH_LINE};
-
-const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
-
-#[derive(Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct StateFile {
- current_exercise_ind: usize,
- progress: Vec<bool>,
-}
-
-impl StateFile {
- fn read(exercises: &[Exercise]) -> Option<Self> {
- let file_content = fs::read(".rustlings-state.json").ok()?;
+use std::io::{StdoutLock, Write};
- let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
+mod state_file;
- if slf.progress.len() != exercises.len() || slf.current_exercise_ind >= exercises.len() {
- return None;
- }
-
- Some(slf)
- }
-
- fn read_or_default(exercises: &[Exercise]) -> Self {
- Self::read(exercises).unwrap_or_else(|| Self {
- current_exercise_ind: 0,
- progress: vec![false; exercises.len()],
- })
- }
+use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE};
- fn write(&self) -> Result<()> {
- let mut buf = Vec::with_capacity(1024);
- serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?;
- fs::write(".rustlings-state.json", buf)
- .context("Failed to write the state file `.rustlings-state.json`")?;
+use self::state_file::{write, StateFileDeser};
- Ok(())
- }
-}
+const STATE_FILE_NAME: &str = ".rustlings-state.json";
+const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
#[must_use]
pub enum ExercisesProgress {
@@ -58,52 +22,85 @@ pub enum ExercisesProgress {
}
pub struct AppState {
- state_file: StateFile,
- exercises: &'static [Exercise],
+ current_exercise_ind: usize,
+ exercises: Vec<Exercise>,
n_done: u16,
- current_exercise: &'static Exercise,
- final_message: &'static str,
+ welcome_message: String,
+ final_message: String,
}
impl AppState {
- pub fn new(mut exercises: Vec<Exercise>, mut final_message: String) -> Self {
- // Leaking especially for sending the exercises to the debounce event handler.
- // Leaking is not a problem because the `AppState` instance lives until
- // the end of the program.
- exercises.shrink_to_fit();
- let exercises = exercises.leak();
- final_message.shrink_to_fit();
- let final_message = final_message.leak();
-
- let state_file = StateFile::read_or_default(exercises);
- let n_done = state_file
- .progress
- .iter()
- .fold(0, |acc, done| acc + u16::from(*done));
- let current_exercise = &exercises[state_file.current_exercise_ind];
+ pub fn new(info_file: InfoFile) -> Self {
+ let mut exercises = info_file
+ .exercises
+ .into_iter()
+ .map(|mut exercise_info| {
+ // Leaking to be able to borrow in the watch mode `Table`.
+ // Leaking is not a problem because the `AppState` instance lives until
+ // the end of the program.
+ let path = Box::leak(exercise_info.path().into_boxed_path());
+
+ exercise_info.name.shrink_to_fit();
+ let name = exercise_info.name.leak();
+
+ let hint = exercise_info.hint.trim().to_owned();
+
+ Exercise {
+ name,
+ path,
+ mode: exercise_info.mode,
+ hint,
+ done: false,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let (current_exercise_ind, n_done) = StateFileDeser::read().map_or((0, 0), |state_file| {
+ let mut state_file_exercises =
+ hashbrown::HashMap::with_capacity(state_file.exercises.len());
+
+ for (ind, exercise_state) in state_file.exercises.into_iter().enumerate() {
+ state_file_exercises.insert(
+ exercise_state.name,
+ (ind == state_file.current_exercise_ind, exercise_state.done),
+ );
+ }
+
+ let mut current_exercise_ind = 0;
+ let mut n_done = 0;
+ for (ind, exercise) in exercises.iter_mut().enumerate() {
+ if let Some((current, done)) = state_file_exercises.get(exercise.name) {
+ if *done {
+ exercise.done = true;
+ n_done += 1;
+ }
+
+ if *current {
+ current_exercise_ind = ind;
+ }
+ }
+ }
+
+ (current_exercise_ind, n_done)
+ });
Self {
- state_file,
+ current_exercise_ind,
exercises,
n_done,
- current_exercise,
- final_message,
+ welcome_message: info_file.welcome_message.unwrap_or_default(),
+ final_message: info_file.final_message.unwrap_or_default(),
}
}
#[inline]
pub fn current_exercise_ind(&self) -> usize {
- self.state_file.current_exercise_ind
- }
-
- #[inline]
- pub fn progress(&self) -> &[bool] {
- &self.state_file.progress
+ self.current_exercise_ind
}
#[inline]
- pub fn exercises(&self) -> &'static [Exercise] {
- self.exercises
+ pub fn exercises(&self) -> &[Exercise] {
+ &self.exercises
}
#[inline]
@@ -112,8 +109,8 @@ impl AppState {
}
#[inline]
- pub fn current_exercise(&self) -> &'static Exercise {
- self.current_exercise
+ pub fn current_exercise(&self) -> &Exercise {
+ &self.exercises[self.current_exercise_ind]
}
pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> {
@@ -121,70 +118,61 @@ impl AppState {
bail!(BAD_INDEX_ERR);
}
- self.state_file.current_exercise_ind = ind;
- self.current_exercise = &self.exercises[ind];
+ self.current_exercise_ind = ind;
- self.state_file.write()
+ write(self)
}
pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
- let (ind, exercise) = self
+ // O(N) is fine since this method is used only once until the program exits.
+ // Building a hashmap would have more overhead.
+ self.current_exercise_ind = self
.exercises
.iter()
- .enumerate()
- .find(|(_, exercise)| exercise.name == name)
+ .position(|exercise| exercise.name == name)
.with_context(|| format!("No exercise found for '{name}'!"))?;
- self.state_file.current_exercise_ind = ind;
- self.current_exercise = exercise;
-
- self.state_file.write()
+ write(self)
}
pub fn set_pending(&mut self, ind: usize) -> Result<()> {
- let done = self
- .state_file
- .progress
- .get_mut(ind)
- .context(BAD_INDEX_ERR)?;
-
- if *done {
- *done = false;
+ let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?;
+
+ if exercise.done {
+ exercise.done = false;
self.n_done -= 1;
- self.state_file.write()?;
+ write(self)?;
}
Ok(())
}
fn next_pending_exercise_ind(&self) -> Option<usize> {
- let current_ind = self.state_file.current_exercise_ind;
-
- if current_ind == self.state_file.progress.len() - 1 {
+ if self.current_exercise_ind == self.exercises.len() - 1 {
// The last exercise is done.
// Search for exercises not done from the start.
- return self.state_file.progress[..current_ind]
+ return self.exercises[..self.current_exercise_ind]
.iter()
- .position(|done| !done);
+ .position(|exercise| !exercise.done);
}
// The done exercise isn't the last one.
// Search for a pending exercise after the current one and then from the start.
- match self.state_file.progress[current_ind + 1..]
+ match self.exercises[self.current_exercise_ind + 1..]
.iter()
- .position(|done| !done)
+ .position(|exercise| !exercise.done)
{
- Some(ind) => Some(current_ind + 1 + ind),
- None => self.state_file.progress[..current_ind]
+ Some(ind) => Some(self.current_exercise_ind + 1 + ind),
+ None => self.exercises[..self.current_exercise_ind]
.iter()
- .position(|done| !done),
+ .position(|exercise| !exercise.done),
}
}
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
- let done = &mut self.state_file.progress[self.state_file.current_exercise_ind];
- if !*done {
- *done = true;
+ let exercise = &mut self.exercises[self.current_exercise_ind];
+ if !exercise.done {
+ exercise.done = true;
self.n_done += 1;
}
@@ -198,15 +186,14 @@ impl AppState {
if !exercise.run()?.status.success() {
writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?;
- self.state_file.current_exercise_ind = exercise_ind;
- self.current_exercise = exercise;
+ self.current_exercise_ind = exercise_ind;
// No check if the exercise is done before setting it to pending
// because no pending exercise was found.
- self.state_file.progress[exercise_ind] = false;
+ self.exercises[exercise_ind].done = false;
self.n_done -= 1;
- self.state_file.write()?;
+ write(self)?;
return Ok(ExercisesProgress::Pending);
}
diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs
new file mode 100644
index 0000000..364a1fa
--- /dev/null
+++ b/src/app_state/state_file.rs
@@ -0,0 +1,112 @@
+use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+use std::fs;
+
+use crate::exercise::Exercise;
+
+use super::{AppState, STATE_FILE_NAME};
+
+#[derive(Deserialize)]
+pub struct ExerciseStateDeser {
+ pub name: String,
+ pub done: bool,
+}
+
+#[derive(Serialize)]
+struct ExerciseStateSer<'a> {
+ name: &'a str,
+ done: bool,
+}
+
+struct ExercisesStateSerializer<'a>(&'a [Exercise]);
+
+impl<'a> Serialize for ExercisesStateSerializer<'a> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let iter = self.0.iter().map(|exercise| ExerciseStateSer {
+ name: exercise.name,
+ done: exercise.done,
+ });
+
+ serializer.collect_seq(iter)
+ }
+}
+
+#[derive(Deserialize)]
+pub struct StateFileDeser {
+ pub current_exercise_ind: usize,
+ pub exercises: Vec<ExerciseStateDeser>,
+}
+
+#[derive(Serialize)]
+struct StateFileSer<'a> {
+ current_exercise_ind: usize,
+ exercises: ExercisesStateSerializer<'a>,
+}
+
+impl StateFileDeser {
+ pub fn read() -> Option<Self> {
+ let file_content = fs::read(STATE_FILE_NAME).ok()?;
+ serde_json::de::from_slice(&file_content).ok()
+ }
+}
+
+pub fn write(app_state: &AppState) -> Result<()> {
+ let content = StateFileSer {
+ current_exercise_ind: app_state.current_exercise_ind,
+ exercises: ExercisesStateSerializer(&app_state.exercises),
+ };
+
+ let mut buf = Vec::with_capacity(1024);
+ serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?;
+ fs::write(STATE_FILE_NAME, buf)
+ .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?;
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::Path;
+
+ use crate::info_file::Mode;
+
+ use super::*;
+
+ #[test]
+ fn ser_deser_sync() {
+ let current_exercise_ind = 1;
+ let exercises = [
+ Exercise {
+ name: "1",
+ path: Path::new("exercises/1.rs"),
+ mode: Mode::Run,
+ hint: String::new(),
+ done: true,
+ },
+ Exercise {
+ name: "2",
+ path: Path::new("exercises/2.rs"),
+ mode: Mode::Test,
+ hint: String::new(),
+ done: false,
+ },
+ ];
+
+ let ser = StateFileSer {
+ current_exercise_ind,
+ exercises: ExercisesStateSerializer(&exercises),
+ };
+ let deser: StateFileDeser =
+ serde_json::de::from_slice(&serde_json::ser::to_vec(&ser).unwrap()).unwrap();
+
+ assert_eq!(deser.current_exercise_ind, current_exercise_ind);
+ assert!(deser
+ .exercises
+ .iter()
+ .zip(exercises)
+ .all(|(deser, ser)| deser.name == ser.name && deser.done == ser.done));
+ }
+}
diff --git a/src/exercise.rs b/src/exercise.rs
index 6aa3b82..c5ece5f 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -1,66 +1,25 @@
use anyhow::{Context, Result};
-use serde::Deserialize;
use std::{
- fmt::{self, Debug, Display, Formatter},
- fs::{self},
- path::PathBuf,
+ fmt::{self, Display, Formatter},
+ path::Path,
process::{Command, Output},
};
-use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
-
-// The mode of the exercise.
-#[derive(Deserialize, Copy, Clone)]
-#[serde(rename_all = "lowercase")]
-pub enum Mode {
- // The exercise should be compiled as a binary
- Compile,
- // The exercise should be compiled as a test harness
- Test,
- // The exercise should be linted with clippy
- Clippy,
-}
-
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct InfoFile {
- // TODO
- pub welcome_message: Option<String>,
- pub final_message: Option<String>,
- pub exercises: Vec<Exercise>,
-}
-
-impl InfoFile {
- pub fn parse() -> Result<Self> {
- // Read a local `info.toml` if it exists.
- // Mainly to let the tests work for now.
- let slf: Self = if let Ok(file_content) = fs::read_to_string("info.toml") {
- toml_edit::de::from_str(&file_content)
- } else {
- toml_edit::de::from_str(include_str!("../info.toml"))
- }
- .context("Failed to parse `info.toml`")?;
-
- if slf.exercises.is_empty() {
- panic!("{NO_EXERCISES_ERR}");
- }
-
- Ok(slf)
- }
-}
+use crate::{
+ embedded::{WriteStrategy, EMBEDDED_FILES},
+ info_file::Mode,
+};
-// Deserialized from the `info.toml` file.
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
pub struct Exercise {
- // Name of the exercise
- pub name: String,
- // The path to the file containing the exercise's source code
- pub path: PathBuf,
+ // Exercise's unique name
+ pub name: &'static str,
+ // Exercise's path
+ pub path: &'static Path,
// The mode of the exercise
pub mode: Mode,
// The hint text associated with the exercise
pub hint: String,
+ pub done: bool,
}
impl Exercise {
@@ -79,7 +38,7 @@ impl Exercise {
.arg("always")
.arg("-q")
.arg("--bin")
- .arg(&self.name)
+ .arg(self.name)
.args(args)
.output()
.context("Failed to run Cargo")
@@ -87,7 +46,7 @@ impl Exercise {
pub fn run(&self) -> Result<Output> {
match self.mode {
- Mode::Compile => self.cargo_cmd("run", &[]),
+ Mode::Run => self.cargo_cmd("run", &[]),
Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]),
Mode::Clippy => self.cargo_cmd(
"clippy",
@@ -98,7 +57,7 @@ impl Exercise {
pub fn reset(&self) -> Result<()> {
EMBEDDED_FILES
- .write_exercise_to_disk(&self.path, WriteStrategy::Overwrite)
+ .write_exercise_to_disk(self.path, WriteStrategy::Overwrite)
.with_context(|| format!("Failed to reset the exercise {self}"))
}
}
@@ -108,6 +67,3 @@ impl Display for Exercise {
Display::fmt(&self.path.display(), f)
}
}
-
-const NO_EXERCISES_ERR: &str = "There are no exercises yet!
-If you are developing third-party exercises, add at least one exercise before testing.";
diff --git a/src/info_file.rs b/src/info_file.rs
new file mode 100644
index 0000000..dc97b92
--- /dev/null
+++ b/src/info_file.rs
@@ -0,0 +1,81 @@
+use anyhow::{bail, Context, Error, Result};
+use serde::Deserialize;
+use std::{fs, path::PathBuf};
+
+// The mode of the exercise.
+#[derive(Deserialize, Copy, Clone)]
+#[serde(rename_all = "lowercase")]
+pub enum Mode {
+ // The exercise should be compiled as a binary
+ Run,
+ // The exercise should be compiled as a test harness
+ Test,
+ // The exercise should be linted with clippy
+ Clippy,
+}
+
+// Deserialized from the `info.toml` file.
+#[derive(Deserialize)]
+pub struct ExerciseInfo {
+ // Name of the exercise
+ pub name: String,
+ // The exercise's directory inside the `exercises` directory
+ pub dir: Option<String>,
+ // The mode of the exercise
+ pub mode: Mode,
+ // The hint text associated with the exercise
+ pub hint: String,
+}
+
+impl ExerciseInfo {
+ pub fn path(&self) -> PathBuf {
+ let path = if let Some(dir) = &self.dir {
+ format!("exercises/{dir}/{}.rs", self.name)
+ } else {
+ format!("exercises/{}.rs", self.name)
+ };
+
+ PathBuf::from(path)
+ }
+}
+
+#[derive(Deserialize)]
+pub struct InfoFile {
+ pub welcome_message: Option<String>,
+ pub final_message: Option<String>,
+ pub exercises: Vec<ExerciseInfo>,
+}
+
+impl InfoFile {
+ pub fn parse() -> Result<Self> {
+ // Read a local `info.toml` if it exists.
+ let slf: Self = match fs::read_to_string("info.toml") {
+ Ok(file_content) => toml_edit::de::from_str(&file_content)
+ .context("Failed to parse the `info.toml` file")?,
+ Err(e) => match e.kind() {
+ std::io::ErrorKind::NotFound => {
+ toml_edit::de::from_str(include_str!("../info.toml"))
+ .context("Failed to parse the embedded `info.toml` file")?
+ }
+ _ => return Err(Error::from(e).context("Failed to read the `info.toml` file")),
+ },
+ };
+
+ if slf.exercises.is_empty() {
+ bail!("{NO_EXERCISES_ERR}");
+ }
+
+ let mut names_set = hashbrown::HashSet::with_capacity(slf.exercises.len());
+ for exercise in &slf.exercises {
+ if !names_set.insert(exercise.name.as_str()) {
+ bail!("Exercise names must all be unique!")
+ }
+ }
+ drop(names_set);
+
+ Ok(slf)
+ }
+}
+
+const NO_EXERCISES_ERR: &str = "There are no exercises yet!
+If you are developing third-party exercises, add at least one exercise before testing.";
diff --git a/src/init.rs b/src/init.rs
index 093610a..2badf37 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -6,17 +6,21 @@ use std::{
path::Path,
};
-use crate::{embedded::EMBEDDED_FILES, exercise::Exercise};
+use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo};
-fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> {
+fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> {
let mut cargo_toml = Vec::with_capacity(1 << 13);
cargo_toml.extend_from_slice(b"bin = [\n");
- for exercise in exercises {
+ for exercise_info in exercise_infos {
cargo_toml.extend_from_slice(b" { name = \"");
- cargo_toml.extend_from_slice(exercise.name.as_bytes());
- cargo_toml.extend_from_slice(b"\", path = \"");
- cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes());
- cargo_toml.extend_from_slice(b"\" },\n");
+ cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
+ cargo_toml.extend_from_slice(b"\", path = \"exercises/");
+ if let Some(dir) = &exercise_info.dir {
+ cargo_toml.extend_from_slice(dir.as_bytes());
+ cargo_toml.extend_from_slice(b"/");
+ }
+ cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
+ cargo_toml.extend_from_slice(b".rs\" },\n");
}
cargo_toml.extend_from_slice(
@@ -54,7 +58,7 @@ fn create_vscode_dir() -> Result<()> {
Ok(())
}
-pub fn init(exercises: &[Exercise]) -> Result<()> {
+pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> {
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
}
@@ -74,7 +78,8 @@ pub fn init(exercises: &[Exercise]) -> Result<()> {
.init_exercises_dir()
.context("Failed to initialize the `rustlings/exercises` directory")?;
- create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?;
+ create_cargo_toml(exercise_infos)
+ .context("Failed to create the file `rustlings/Cargo.toml`")?;
create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?;
diff --git a/src/list.rs b/src/list.rs
index de120ea..2bb813d 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -5,7 +5,7 @@ use crossterm::{
ExecutableCommand,
};
use ratatui::{backend::CrosstermBackend, Terminal};
-use std::{fmt::Write, io};
+use std::io;
mod state;
@@ -72,14 +72,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
ui_state.message.push_str(message);
}
KeyCode::Char('r') => {
- let Some(exercise) = ui_state.reset_selected()? else {
- continue;
- };
-
- ui_state = ui_state.with_updated_rows();
- ui_state
- .message
- .write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
+ ui_state = ui_state.with_reset_selected()?;
}
KeyCode::Char('c') => {
ui_state.selected_to_current_exercise()?;
diff --git a/src/list/state.rs b/src/list/state.rs
index 0dcfe88..38391a4 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -6,8 +6,9 @@ use ratatui::{
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
Frame,
};
+use std::fmt::Write;
-use crate::{app_state::AppState, exercise::Exercise, progress_bar::progress_bar_ratatui};
+use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Filter {
@@ -34,10 +35,9 @@ impl<'a> UiState<'a> {
.app_state
.exercises()
.iter()
- .zip(self.app_state.progress().iter().copied())
.enumerate()
- .filter_map(|(ind, (exercise, done))| {
- let exercise_state = if done {
+ .filter_map(|(ind, exercise)| {
+ let exercise_state = if exercise.done {
if self.filter == Filter::Pending {
return None;
}
@@ -62,7 +62,7 @@ impl<'a> UiState<'a> {
Some(Row::new([
next,
exercise_state,
- Span::raw(&exercise.name),
+ Span::raw(exercise.name),
Span::raw(exercise.path.to_string_lossy()),
]))
});
@@ -212,29 +212,30 @@ impl<'a> UiState<'a> {
Ok(())
}
- pub fn reset_selected(&mut self) -> Result<Option<&'static Exercise>> {
+ pub fn with_reset_selected(mut self) -> Result<Self> {
let Some(selected) = self.table_state.selected() else {
- return Ok(None);
+ return Ok(self);
};
let (ind, exercise) = self
.app_state
.exercises()
.iter()
- .zip(self.app_state.progress())
.enumerate()
- .filter_map(|(ind, (exercise, done))| match self.filter {
- Filter::Done => done.then_some((ind, exercise)),
- Filter::Pending => (!done).then_some((ind, exercise)),
+ .filter_map(|(ind, exercise)| match self.filter {
+ Filter::Done => exercise.done.then_some((ind, exercise)),
+ Filter::Pending => (!exercise.done).then_some((ind, exercise)),
Filter::None => Some((ind, exercise)),
})
.nth(selected)
.context("Invalid selection index")?;
- self.app_state.set_pending(ind)?;
exercise.reset()?;
+ self.message
+ .write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
+ self.app_state.set_pending(ind)?;
- Ok(Some(exercise))
+ Ok(self.with_updated_rows())
}
pub fn selected_to_current_exercise(&mut self) -> Result<()> {
@@ -244,12 +245,12 @@ impl<'a> UiState<'a> {
let ind = self
.app_state
- .progress()
+ .exercises()
.iter()
.enumerate()
- .filter_map(|(ind, done)| match self.filter {
- Filter::Done => done.then_some(ind),
- Filter::Pending => (!done).then_some(ind),
+ .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)
diff --git a/src/main.rs b/src/main.rs
index cdfa21f..a96e323 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ use std::{path::Path, process::exit};
mod app_state;
mod embedded;
mod exercise;
+mod info_file;
mod init;
mod list;
mod progress_bar;
@@ -13,7 +14,7 @@ mod watch;
use self::{
app_state::AppState,
- exercise::InfoFile,
+ info_file::InfoFile,
init::init,
list::list,
run::run,
@@ -54,12 +55,10 @@ fn main() -> Result<()> {
which::which("cargo").context(CARGO_NOT_FOUND_ERR)?;
- let mut info_file = InfoFile::parse()?;
- info_file.exercises.shrink_to_fit();
- let exercises = info_file.exercises;
+ let info_file = InfoFile::parse()?;
if matches!(args.command, Some(Subcommands::Init)) {
- init(&exercises).context("Initialization failed")?;
+ init(&info_file.exercises).context("Initialization failed")?;
println!("{POST_INIT_MSG}");
return Ok(());
@@ -68,18 +67,29 @@ fn main() -> Result<()> {
exit(1);
}
- let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default());
+ let mut app_state = AppState::new(info_file);
match args.command {
- None => loop {
- match watch(&mut app_state)? {
- 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(&mut app_state)?,
+ None => {
+ // For the the notify event handler thread.
+ // Leaking is not a problem because the slice lives until the end of the program.
+ let exercise_paths = app_state
+ .exercises()
+ .iter()
+ .map(|exercise| exercise.path)
+ .collect::<Vec<_>>()
+ .leak();
+
+ loop {
+ match watch(&mut app_state, exercise_paths)? {
+ 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(&mut app_state)?,
+ }
}
- },
+ }
// `Init` is handled above.
Some(Subcommands::Init) => (),
Some(Subcommands::Run { name }) => {
@@ -90,10 +100,10 @@ fn main() -> Result<()> {
}
Some(Subcommands::Reset { name }) => {
app_state.set_current_exercise_by_name(&name)?;
- app_state.set_pending(app_state.current_exercise_ind())?;
let exercise = app_state.current_exercise();
exercise.reset()?;
println!("The exercise {exercise} has been reset!");
+ app_state.set_pending(app_state.current_exercise_ind())?;
}
Some(Subcommands::Hint { name }) => {
app_state.set_current_exercise_by_name(&name)?;
diff --git a/src/run.rs b/src/run.rs
index 4748549..9c504b5 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -17,7 +17,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
if !output.status.success() {
app_state.set_pending(app_state.current_exercise_ind())?;
- bail!("Ran {exercise} with errors");
+ bail!("Ran {} with errors", app_state.current_exercise());
}
stdout.write_fmt(format_args!(
diff --git a/src/watch.rs b/src/watch.rs
index beb69b3..58e829f 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -11,14 +11,14 @@ use std::{
time::Duration,
};
-mod debounce_event;
+mod notify_event;
mod state;
mod terminal_event;
use crate::app_state::{AppState, ExercisesProgress};
use self::{
- debounce_event::DebounceEventHandler,
+ notify_event::DebounceEventHandler,
state::WatchState,
terminal_event::{terminal_event_handler, InputEvent},
};
@@ -40,13 +40,16 @@ pub enum WatchExit {
List,
}
-pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
+pub fn watch(
+ app_state: &mut AppState,
+ exercise_paths: &'static [&'static Path],
+) -> Result<WatchExit> {
let (tx, rx) = channel();
let mut debouncer = new_debouncer(
Duration::from_secs(1),
DebounceEventHandler {
tx: tx.clone(),
- exercises: app_state.exercises(),
+ exercise_paths,
},
)?;
debouncer
@@ -85,10 +88,10 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
watch_state.render()?;
}
WatchEvent::NotifyErr(e) => {
- return Err(Error::from(e).context("Exercise file watcher failed"))
+ return Err(Error::from(e).context("Exercise file watcher failed"));
}
WatchEvent::TerminalEventErr(e) => {
- return Err(Error::from(e).context("Terminal event listener failed"))
+ return Err(Error::from(e).context("Terminal event listener failed"));
}
}
}
diff --git a/src/watch/debounce_event.rs b/src/watch/notify_event.rs
index 1dc92cb..0c8d669 100644
--- a/src/watch/debounce_event.rs
+++ b/src/watch/notify_event.rs
@@ -1,13 +1,11 @@
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
-use std::sync::mpsc::Sender;
-
-use crate::exercise::Exercise;
+use std::{path::Path, sync::mpsc::Sender};
use super::WatchEvent;
pub struct DebounceEventHandler {
pub tx: Sender<WatchEvent>,
- pub exercises: &'static [Exercise],
+ pub exercise_paths: &'static [&'static Path],
}
impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
@@ -23,9 +21,9 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
return None;
}
- self.exercises
+ self.exercise_paths
.iter()
- .position(|exercise| event.path.ends_with(&exercise.path))
+ .position(|path| event.path.ends_with(path))
})
.min()
else {