Tolerate changes in the state file

This commit is contained in:
mo8it 2024-04-14 01:15:43 +02:00
parent 2a26dfcb00
commit 5c0073a948
15 changed files with 513 additions and 368 deletions

1
Cargo.lock generated
View file

@ -684,6 +684,7 @@ dependencies = [
"assert_cmd", "assert_cmd",
"clap", "clap",
"crossterm", "crossterm",
"hashbrown",
"notify-debouncer-mini", "notify-debouncer-mini",
"predicates", "predicates",
"ratatui", "ratatui",

View file

@ -37,6 +37,7 @@ edition.workspace = true
anyhow.workspace = true anyhow.workspace = true
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
crossterm = "0.27.0" crossterm = "0.27.0"
hashbrown = "0.14.3"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"
ratatui = "0.26.1" ratatui = "0.26.1"
rustlings-macros = { path = "rustlings-macros" } rustlings-macros = { path = "rustlings-macros" }

View file

@ -1,6 +1,5 @@
// intro1.rs // intro1.rs
// //
// TODO: Update comment
// We sometimes encourage you to keep trying things on a given exercise, even // 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 // 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. // ready for the next exercise, remove the `I AM NOT DONE` comment below.

272
info.toml
View file

@ -33,10 +33,11 @@ https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md
# INTRO # INTRO
# TODO: Update exercise
[[exercises]] [[exercises]]
name = "intro1" name = "intro1"
path = "exercises/00_intro/intro1.rs" dir = "00_intro"
mode = "compile" mode = "run"
# TODO: Fix hint # TODO: Fix hint
hint = """ hint = """
Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file 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]] [[exercises]]
name = "intro2" name = "intro2"
path = "exercises/00_intro/intro2.rs" dir = "00_intro"
mode = "compile" mode = "run"
hint = """ hint = """
The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" 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]] [[exercises]]
name = "variables1" name = "variables1"
path = "exercises/01_variables/variables1.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
The declaration in the first line in the main function is missing a keyword 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.""" that is needed in Rust to create a new variable binding."""
[[exercises]] [[exercises]]
name = "variables2" name = "variables2"
path = "exercises/01_variables/variables2.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
The compiler message is saying that Rust cannot infer the type that the The compiler message is saying that Rust cannot infer the type that the
variable binding `x` has with what is given here. 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]] [[exercises]]
name = "variables3" name = "variables3"
path = "exercises/01_variables/variables3.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
Oops! In this exercise, we have a variable binding that we've created on in the 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, 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]] [[exercises]]
name = "variables4" name = "variables4"
path = "exercises/01_variables/variables4.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
In Rust, variable bindings are immutable by default. But here we're trying 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 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]] [[exercises]]
name = "variables5" name = "variables5"
path = "exercises/01_variables/variables5.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
In `variables4` we already learned how to make an immutable variable mutable 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 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]] [[exercises]]
name = "variables6" name = "variables6"
path = "exercises/01_variables/variables6.rs" dir = "01_variables"
mode = "compile" mode = "run"
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
variable available: constants. variable available: constants.
@ -141,8 +142,8 @@ https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants
[[exercises]] [[exercises]]
name = "functions1" name = "functions1"
path = "exercises/02_functions/functions1.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
This main function is calling a function that it expects to exist, but the 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`. 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]] [[exercises]]
name = "functions2" name = "functions2"
path = "exercises/02_functions/functions2.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
Rust requires that all parts of a function's signature have type annotations, Rust requires that all parts of a function's signature have type annotations,
but `call_me` is missing the type annotation of `num`.""" but `call_me` is missing the type annotation of `num`."""
[[exercises]] [[exercises]]
name = "functions3" name = "functions3"
path = "exercises/02_functions/functions3.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
This time, the function *declaration* is okay, but there's something wrong This time, the function *declaration* is okay, but there's something wrong
with the place where we're calling the function.""" with the place where we're calling the function."""
[[exercises]] [[exercises]]
name = "functions4" name = "functions4"
path = "exercises/02_functions/functions4.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
The error message points to the function `sale_price` and says it expects a type 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 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]] [[exercises]]
name = "functions5" name = "functions5"
path = "exercises/02_functions/functions5.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
This is a really common error that can be fixed by removing one character. This is a really common error that can be fixed by removing one character.
It happens because Rust distinguishes between expressions and statements: It happens because Rust distinguishes between expressions and statements:
@ -198,7 +199,7 @@ They are not the same. There are two solutions:
[[exercises]] [[exercises]]
name = "if1" name = "if1"
path = "exercises/03_if/if1.rs" dir = "03_if"
mode = "test" mode = "test"
hint = """ hint = """
It's possible to do this in one line if you would like! It's possible to do this in one line if you would like!
@ -214,7 +215,7 @@ Remember in Rust that:
[[exercises]] [[exercises]]
name = "if2" name = "if2"
path = "exercises/03_if/if2.rs" dir = "03_if"
mode = "test" mode = "test"
hint = """ hint = """
For that first compiler error, it's important in Rust that each conditional For that first compiler error, it's important in Rust that each conditional
@ -223,7 +224,7 @@ conditions checking different input values."""
[[exercises]] [[exercises]]
name = "if3" name = "if3"
path = "exercises/03_if/if3.rs" dir = "03_if"
mode = "test" mode = "test"
hint = """ hint = """
In Rust, every arm of an `if` expression has to return the same type of value. 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]] [[exercises]]
name = "quiz1" name = "quiz1"
path = "exercises/quiz1.rs"
mode = "test" mode = "test"
hint = "No hints this time ;)" hint = "No hints this time ;)"
@ -241,20 +241,20 @@ hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "primitive_types1" name = "primitive_types1"
path = "exercises/04_primitive_types/primitive_types1.rs" dir = "04_primitive_types"
mode = "compile" mode = "run"
hint = "No hints this time ;)" hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "primitive_types2" name = "primitive_types2"
path = "exercises/04_primitive_types/primitive_types2.rs" dir = "04_primitive_types"
mode = "compile" mode = "run"
hint = "No hints this time ;)" hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "primitive_types3" name = "primitive_types3"
path = "exercises/04_primitive_types/primitive_types3.rs" dir = "04_primitive_types"
mode = "compile" mode = "run"
hint = """ hint = """
There's a shorthand to initialize Arrays with a certain size that does not 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!). require you to type in 100 items (but you certainly can if you want!).
@ -269,7 +269,7 @@ for `a.len() >= 100`?"""
[[exercises]] [[exercises]]
name = "primitive_types4" name = "primitive_types4"
path = "exercises/04_primitive_types/primitive_types4.rs" dir = "04_primitive_types"
mode = "test" mode = "test"
hint = """ hint = """
Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section
@ -284,8 +284,8 @@ https://doc.rust-lang.org/nomicon/coercions.html"""
[[exercises]] [[exercises]]
name = "primitive_types5" name = "primitive_types5"
path = "exercises/04_primitive_types/primitive_types5.rs" dir = "04_primitive_types"
mode = "compile" mode = "run"
hint = """ hint = """
Take a look at the 'Data Types -> The Tuple Type' section of the book: 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 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]] [[exercises]]
name = "primitive_types6" name = "primitive_types6"
path = "exercises/04_primitive_types/primitive_types6.rs" dir = "04_primitive_types"
mode = "test" mode = "test"
hint = """ hint = """
While you could use a destructuring `let` for the tuple here, try 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]] [[exercises]]
name = "vecs1" name = "vecs1"
path = "exercises/05_vecs/vecs1.rs" dir = "05_vecs"
mode = "test" mode = "test"
hint = """ hint = """
In Rust, there are two ways to define a Vector. In Rust, there are two ways to define a Vector.
@ -325,7 +325,7 @@ of the Rust book to learn more.
[[exercises]] [[exercises]]
name = "vecs2" name = "vecs2"
path = "exercises/05_vecs/vecs2.rs" dir = "05_vecs"
mode = "test" mode = "test"
hint = """ hint = """
In the first function we are looping over the Vector and getting a reference to 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]] [[exercises]]
name = "move_semantics1" name = "move_semantics1"
path = "exercises/06_move_semantics/move_semantics1.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
So you've got the "cannot borrow immutable local variable `vec` as mutable" So you've got the "cannot borrow immutable local variable `vec` as mutable"
@ -362,7 +362,7 @@ happens!"""
[[exercises]] [[exercises]]
name = "move_semantics2" name = "move_semantics2"
path = "exercises/06_move_semantics/move_semantics2.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
When running this exercise for the first time, you'll notice an error about When running this exercise for the first time, you'll notice an error about
@ -383,7 +383,7 @@ try them all:
[[exercises]] [[exercises]]
name = "move_semantics3" name = "move_semantics3"
path = "exercises/06_move_semantics/move_semantics3.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
The difference between this one and the previous ones is that the first line 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]] [[exercises]]
name = "move_semantics4" name = "move_semantics4"
path = "exercises/06_move_semantics/move_semantics4.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
Stop reading whenever you feel like you have enough direction :) Or try Stop reading whenever you feel like you have enough direction :) Or try
@ -407,7 +407,7 @@ So the end goal is to:
[[exercises]] [[exercises]]
name = "move_semantics5" name = "move_semantics5"
path = "exercises/06_move_semantics/move_semantics5.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ 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
@ -419,8 +419,8 @@ https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-ref
[[exercises]] [[exercises]]
name = "move_semantics6" name = "move_semantics6"
path = "exercises/06_move_semantics/move_semantics6.rs" dir = "06_move_semantics"
mode = "compile" mode = "run"
hint = """ hint = """
To find the answer, you can consult the book section "References and Borrowing": 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 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]] [[exercises]]
name = "structs1" name = "structs1"
path = "exercises/07_structs/structs1.rs" dir = "07_structs"
mode = "test" mode = "test"
hint = """ hint = """
Rust has more than one type of struct. Three actually, all variants are used to 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]] [[exercises]]
name = "structs2" name = "structs2"
path = "exercises/07_structs/structs2.rs" dir = "07_structs"
mode = "test" mode = "test"
hint = """ hint = """
Creating instances of structs is easy, all you need to do is assign some values 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]] [[exercises]]
name = "structs3" name = "structs3"
path = "exercises/07_structs/structs3.rs" dir = "07_structs"
mode = "test" mode = "test"
hint = """ hint = """
For `is_international`: What makes a package international? Seems related to 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]] [[exercises]]
name = "enums1" name = "enums1"
path = "exercises/08_enums/enums1.rs" dir = "08_enums"
mode = "compile" mode = "run"
hint = "No hints this time ;)" hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "enums2" name = "enums2"
path = "exercises/08_enums/enums2.rs" dir = "08_enums"
mode = "compile" mode = "run"
hint = """ hint = """
You can create enumerations that have different variants with different types You can create enumerations that have different variants with different types
such as no data, anonymous structs, a single string, tuples, ...etc""" such as no data, anonymous structs, a single string, tuples, ...etc"""
[[exercises]] [[exercises]]
name = "enums3" name = "enums3"
path = "exercises/08_enums/enums3.rs" dir = "08_enums"
mode = "test" mode = "test"
hint = """ hint = """
As a first step, you can define enums to compile this code without errors. 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]] [[exercises]]
name = "strings1" name = "strings1"
path = "exercises/09_strings/strings1.rs" dir = "09_strings"
mode = "compile" mode = "run"
hint = """ hint = """
The `current_favorite_color` function is currently returning a string slice 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 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]] [[exercises]]
name = "strings2" name = "strings2"
path = "exercises/09_strings/strings2.rs" dir = "09_strings"
mode = "compile" mode = "run"
hint = """ hint = """
Yes, it would be really easy to fix this by just changing the value bound to 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 `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]] [[exercises]]
name = "strings3" name = "strings3"
path = "exercises/09_strings/strings3.rs" dir = "09_strings"
mode = "test" mode = "test"
hint = """ hint = """
There's tons of useful standard library functions for strings. Let's try and use some of them: 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]] [[exercises]]
name = "strings4" name = "strings4"
path = "exercises/09_strings/strings4.rs" dir = "09_strings"
mode = "compile" mode = "run"
hint = "No hints this time ;)" hint = "No hints this time ;)"
# MODULES # MODULES
[[exercises]] [[exercises]]
name = "modules1" name = "modules1"
path = "exercises/10_modules/modules1.rs" dir = "10_modules"
mode = "compile" mode = "run"
hint = """ hint = """
Everything is private in Rust by default-- but there's a keyword we can use 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 to make something public! The compiler error should point to the thing that
@ -573,8 +573,8 @@ needs to be public."""
[[exercises]] [[exercises]]
name = "modules2" name = "modules2"
path = "exercises/10_modules/modules2.rs" dir = "10_modules"
mode = "compile" mode = "run"
hint = """ hint = """
The delicious_snacks module is trying to present an external interface that is The delicious_snacks module is trying to present an external interface that is
different than its internal structure (the `fruits` and `veggies` modules and 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]] [[exercises]]
name = "modules3" name = "modules3"
path = "exercises/10_modules/modules3.rs" dir = "10_modules"
mode = "compile" mode = "run"
hint = """ hint = """
`UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a `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 `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]] [[exercises]]
name = "hashmaps1" name = "hashmaps1"
path = "exercises/11_hashmaps/hashmaps1.rs" dir = "11_hashmaps"
mode = "test" mode = "test"
hint = """ hint = """
Hint 1: Take a look at the return type of the function to figure out 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]] [[exercises]]
name = "hashmaps2" name = "hashmaps2"
path = "exercises/11_hashmaps/hashmaps2.rs" dir = "11_hashmaps"
mode = "test" mode = "test"
hint = """ hint = """
Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. 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]] [[exercises]]
name = "hashmaps3" name = "hashmaps3"
path = "exercises/11_hashmaps/hashmaps3.rs" dir = "11_hashmaps"
mode = "test" mode = "test"
hint = """ hint = """
Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert 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]] [[exercises]]
name = "quiz2" name = "quiz2"
path = "exercises/quiz2.rs"
mode = "test" mode = "test"
hint = "No hints this time ;)" hint = "No hints this time ;)"
@ -643,7 +642,7 @@ hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "options1" name = "options1"
path = "exercises/12_options/options1.rs" dir = "12_options"
mode = "test" mode = "test"
hint = """ hint = """
Options can have a `Some` value, with an inner value, or a `None` value, 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]] [[exercises]]
name = "options2" name = "options2"
path = "exercises/12_options/options2.rs" dir = "12_options"
mode = "test" mode = "test"
hint = """ hint = """
Check out: Check out:
@ -672,8 +671,8 @@ Also see `Option::flatten`
[[exercises]] [[exercises]]
name = "options3" name = "options3"
path = "exercises/12_options/options3.rs" dir = "12_options"
mode = "compile" mode = "run"
hint = """ hint = """
The compiler says a partial move happened in the `match` statement. How can The compiler says a partial move happened in the `match` statement. How can
this be avoided? The compiler shows the correction needed. this be avoided? The compiler shows the correction needed.
@ -685,7 +684,7 @@ https://doc.rust-lang.org/std/keyword.ref.html"""
[[exercises]] [[exercises]]
name = "errors1" name = "errors1"
path = "exercises/13_error_handling/errors1.rs" dir = "13_error_handling"
mode = "test" mode = "test"
hint = """ hint = """
`Ok` and `Err` are the two variants of `Result`, so what the tests are saying `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]] [[exercises]]
name = "errors2" name = "errors2"
path = "exercises/13_error_handling/errors2.rs" dir = "13_error_handling"
mode = "test" mode = "test"
hint = """ hint = """
One way to handle this is using a `match` statement on One way to handle this is using a `match` statement on
@ -717,8 +716,8 @@ and give it a try!"""
[[exercises]] [[exercises]]
name = "errors3" name = "errors3"
path = "exercises/13_error_handling/errors3.rs" dir = "13_error_handling"
mode = "compile" mode = "run"
hint = """ hint = """
If other functions can return a `Result`, why shouldn't `main`? It's a fairly 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 common convention to return something like `Result<(), ErrorType>` from your
@ -729,7 +728,7 @@ positive results."""
[[exercises]] [[exercises]]
name = "errors4" name = "errors4"
path = "exercises/13_error_handling/errors4.rs" dir = "13_error_handling"
mode = "test" mode = "test"
hint = """ hint = """
`PositiveNonzeroInteger::new` is always creating a new instance and returning `PositiveNonzeroInteger::new` is always creating a new instance and returning
@ -741,8 +740,8 @@ everything is... okay :)"""
[[exercises]] [[exercises]]
name = "errors5" name = "errors5"
path = "exercises/13_error_handling/errors5.rs" dir = "13_error_handling"
mode = "compile" mode = "run"
hint = """ hint = """
There are two different possible `Result` types produced within `main()`, which There are two different possible `Result` types produced within `main()`, which
are propagated using `?` operators. How do we declare a return type from 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]] [[exercises]]
name = "errors6" name = "errors6"
path = "exercises/13_error_handling/errors6.rs" dir = "13_error_handling"
mode = "test" mode = "test"
hint = """ hint = """
This exercise uses a completed version of `PositiveNonzeroInteger` from 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]] [[exercises]]
name = "generics1" name = "generics1"
path = "exercises/14_generics/generics1.rs" dir = "14_generics"
mode = "compile" mode = "run"
hint = """ hint = """
Vectors in Rust make use of generics to create dynamically sized arrays of any Vectors in Rust make use of generics to create dynamically sized arrays of any
type. type.
@ -797,7 +796,7 @@ You need to tell the compiler what type we are pushing onto this vector."""
[[exercises]] [[exercises]]
name = "generics2" name = "generics2"
path = "exercises/14_generics/generics2.rs" dir = "14_generics"
mode = "test" mode = "test"
hint = """ hint = """
Currently we are wrapping only values of type `u32`. 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]] [[exercises]]
name = "traits1" name = "traits1"
path = "exercises/15_traits/traits1.rs" dir = "15_traits"
mode = "test" mode = "test"
hint = """ hint = """
A discussion about Traits in Rust can be found at: 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]] [[exercises]]
name = "traits2" name = "traits2"
path = "exercises/15_traits/traits2.rs" dir = "15_traits"
mode = "test" mode = "test"
hint = """ hint = """
Notice how the trait takes ownership of `self`, and returns `Self`. 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]] [[exercises]]
name = "traits3" name = "traits3"
path = "exercises/15_traits/traits3.rs" dir = "15_traits"
mode = "test" mode = "test"
hint = """ hint = """
Traits can have a default implementation for functions. Structs that implement 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]] [[exercises]]
name = "traits4" name = "traits4"
path = "exercises/15_traits/traits4.rs" dir = "15_traits"
mode = "test" mode = "test"
hint = """ hint = """
Instead of using concrete types as parameters you can use traits. Try replacing 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]] [[exercises]]
name = "traits5" name = "traits5"
path = "exercises/15_traits/traits5.rs" dir = "15_traits"
mode = "compile" mode = "run"
hint = """ hint = """
To ensure a parameter implements multiple traits use the '+ syntax'. Try To ensure a parameter implements multiple traits use the '+ syntax'. Try
replacing the '??' with 'impl <> + <>'. replacing the '??' with 'impl <> + <>'.
@ -869,7 +868,6 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#spe
[[exercises]] [[exercises]]
name = "quiz3" name = "quiz3"
path = "exercises/quiz3.rs"
mode = "test" mode = "test"
hint = """ hint = """
To find the best solution to this challenge you're going to need to think back 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]] [[exercises]]
name = "lifetimes1" name = "lifetimes1"
path = "exercises/16_lifetimes/lifetimes1.rs" dir = "16_lifetimes"
mode = "compile" mode = "run"
hint = """ hint = """
Let the compiler guide you. Also take a look at the book if you need help: 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""" https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html"""
[[exercises]] [[exercises]]
name = "lifetimes2" name = "lifetimes2"
path = "exercises/16_lifetimes/lifetimes2.rs" dir = "16_lifetimes"
mode = "compile" mode = "run"
hint = """ hint = """
Remember that the generic lifetime `'a` will get the concrete lifetime that is Remember that the generic lifetime `'a` will get the concrete lifetime that is
equal to the smaller of the lifetimes of `x` and `y`. equal to the smaller of the lifetimes of `x` and `y`.
@ -903,8 +901,8 @@ inner block:
[[exercises]] [[exercises]]
name = "lifetimes3" name = "lifetimes3"
path = "exercises/16_lifetimes/lifetimes3.rs" dir = "16_lifetimes"
mode = "compile" mode = "run"
hint = """ hint = """
If you use a lifetime annotation in a struct's fields, where else does it need If you use a lifetime annotation in a struct's fields, where else does it need
to be added?""" to be added?"""
@ -913,7 +911,7 @@ to be added?"""
[[exercises]] [[exercises]]
name = "tests1" name = "tests1"
path = "exercises/17_tests/tests1.rs" dir = "17_tests"
mode = "test" mode = "test"
hint = """ hint = """
You don't even need to write any code to test -- you can just test values and 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]] [[exercises]]
name = "tests2" name = "tests2"
path = "exercises/17_tests/tests2.rs" dir = "17_tests"
mode = "test" mode = "test"
hint = """ hint = """
Like the previous exercise, you don't need to write any code to get this test 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]] [[exercises]]
name = "tests3" name = "tests3"
path = "exercises/17_tests/tests3.rs" dir = "17_tests"
mode = "test" mode = "test"
hint = """ hint = """
You can call a function right where you're passing arguments to `assert!`. So 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]] [[exercises]]
name = "tests4" name = "tests4"
path = "exercises/17_tests/tests4.rs" dir = "17_tests"
mode = "test" mode = "test"
hint = """ hint = """
We expect method `Rectangle::new()` to panic for negative values. 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]] [[exercises]]
name = "iterators1" name = "iterators1"
path = "exercises/18_iterators/iterators1.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
Step 1: Step 1:
@ -989,7 +987,7 @@ https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas.
[[exercises]] [[exercises]]
name = "iterators2" name = "iterators2"
path = "exercises/18_iterators/iterators2.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
Step 1: Step 1:
@ -1015,7 +1013,7 @@ powerful and very general. Rust just needs to know the desired type."""
[[exercises]] [[exercises]]
name = "iterators3" name = "iterators3"
path = "exercises/18_iterators/iterators3.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
The `divide` function needs to return the correct error when even division is 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]] [[exercises]]
name = "iterators4" name = "iterators4"
path = "exercises/18_iterators/iterators4.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
In an imperative language, you might write a `for` loop that updates a mutable 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]] [[exercises]]
name = "iterators5" name = "iterators5"
path = "exercises/18_iterators/iterators5.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
The documentation for the `std::iter::Iterator` trait contains numerous methods 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]] [[exercises]]
name = "box1" name = "box1"
path = "exercises/19_smart_pointers/box1.rs" dir = "19_smart_pointers"
mode = "test" mode = "test"
hint = """ hint = """
Step 1: Step 1:
@ -1089,7 +1087,7 @@ definition and try other types!
[[exercises]] [[exercises]]
name = "rc1" name = "rc1"
path = "exercises/19_smart_pointers/rc1.rs" dir = "19_smart_pointers"
mode = "test" mode = "test"
hint = """ hint = """
This is a straightforward exercise to use the `Rc<T>` type. Each `Planet` has 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]] [[exercises]]
name = "arc1" name = "arc1"
path = "exercises/19_smart_pointers/arc1.rs" dir = "19_smart_pointers"
mode = "compile" mode = "run"
hint = """ hint = """
Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order 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` 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]] [[exercises]]
name = "cow1" name = "cow1"
path = "exercises/19_smart_pointers/cow1.rs" dir = "19_smart_pointers"
mode = "test" mode = "test"
hint = """ hint = """
If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is 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]] [[exercises]]
name = "threads1" name = "threads1"
path = "exercises/20_threads/threads1.rs" dir = "20_threads"
mode = "compile" mode = "run"
hint = """ hint = """
`JoinHandle` is a struct that is returned from a spawned thread: `JoinHandle` is a struct that is returned from a spawned thread:
https://doc.rust-lang.org/std/thread/fn.spawn.html 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]] [[exercises]]
name = "threads2" name = "threads2"
path = "exercises/20_threads/threads2.rs" dir = "20_threads"
mode = "compile" mode = "run"
hint = """ hint = """
`Arc` is an Atomic Reference Counted pointer that allows safe, shared access `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` 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]] [[exercises]]
name = "threads3" name = "threads3"
path = "exercises/20_threads/threads3.rs" dir = "20_threads"
mode = "test" mode = "test"
hint = """ hint = """
An alternate way to handle concurrency between threads is to use an `mpsc` 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]] [[exercises]]
name = "macros1" name = "macros1"
path = "exercises/21_macros/macros1.rs" dir = "21_macros"
mode = "compile" mode = "run"
hint = """ hint = """
When you call a macro, you need to add something special compared to a 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 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]] [[exercises]]
name = "macros2" name = "macros2"
path = "exercises/21_macros/macros2.rs" dir = "21_macros"
mode = "compile" mode = "run"
hint = """ hint = """
Macros don't quite play by the same rules as the rest of Rust, in terms of Macros don't quite play by the same rules as the rest of Rust, in terms of
what's available where. what's available where.
@ -1219,8 +1217,8 @@ Unlike other things in Rust, the order of "where you define a macro" versus
[[exercises]] [[exercises]]
name = "macros3" name = "macros3"
path = "exercises/21_macros/macros3.rs" dir = "21_macros"
mode = "compile" mode = "run"
hint = """ hint = """
In order to use a macro outside of its module, you need to do something 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. 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]] [[exercises]]
name = "macros4" name = "macros4"
path = "exercises/21_macros/macros4.rs" dir = "21_macros"
mode = "compile" mode = "run"
hint = """ hint = """
You only need to add a single character to make this compile. You only need to add a single character to make this compile.
@ -1247,7 +1245,7 @@ https://veykril.github.io/tlborm/"""
[[exercises]] [[exercises]]
name = "clippy1" name = "clippy1"
path = "exercises/22_clippy/clippy1.rs" dir = "22_clippy"
mode = "clippy" mode = "clippy"
hint = """ hint = """
Rust stores the highest precision version of any long or infinite precision Rust stores the highest precision version of any long or infinite precision
@ -1263,14 +1261,14 @@ appropriate replacement constant from `std::f32::consts`..."""
[[exercises]] [[exercises]]
name = "clippy2" name = "clippy2"
path = "exercises/22_clippy/clippy2.rs" dir = "22_clippy"
mode = "clippy" mode = "clippy"
hint = """ hint = """
`for` loops over `Option` values are more clearly expressed as an `if let`""" `for` loops over `Option` values are more clearly expressed as an `if let`"""
[[exercises]] [[exercises]]
name = "clippy3" name = "clippy3"
path = "exercises/22_clippy/clippy3.rs" dir = "22_clippy"
mode = "clippy" mode = "clippy"
hint = "No hints this time!" hint = "No hints this time!"
@ -1278,7 +1276,7 @@ hint = "No hints this time!"
[[exercises]] [[exercises]]
name = "using_as" name = "using_as"
path = "exercises/23_conversions/using_as.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
Use the `as` operator to cast one of the operands in the last line of the 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]] [[exercises]]
name = "from_into" name = "from_into"
path = "exercises/23_conversions/from_into.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
Follow the steps provided right before the `From` implementation""" Follow the steps provided right before the `From` implementation"""
[[exercises]] [[exercises]]
name = "from_str" name = "from_str"
path = "exercises/23_conversions/from_str.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
The implementation of `FromStr` should return an `Ok` with a `Person` object, 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]] [[exercises]]
name = "try_from_into" name = "try_from_into"
path = "exercises/23_conversions/try_from_into.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
Follow the steps provided right before the `TryFrom` implementation. 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]] [[exercises]]
name = "as_ref_mut" name = "as_ref_mut"
path = "exercises/23_conversions/as_ref_mut.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions.""" Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions."""

View file

@ -4,53 +4,17 @@ use crossterm::{
terminal::{Clear, ClearType}, terminal::{Clear, ClearType},
ExecutableCommand, ExecutableCommand,
}; };
use serde::{Deserialize, Serialize}; use std::io::{StdoutLock, Write};
use std::{
fs,
io::{StdoutLock, Write},
};
use crate::{exercise::Exercise, FENISH_LINE}; mod state_file;
use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE};
use self::state_file::{write, StateFileDeser};
const STATE_FILE_NAME: &str = ".rustlings-state.json";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; 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()?;
let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
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()],
})
}
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`")?;
Ok(())
}
}
#[must_use] #[must_use]
pub enum ExercisesProgress { pub enum ExercisesProgress {
AllDone, AllDone,
@ -58,52 +22,85 @@ pub enum ExercisesProgress {
} }
pub struct AppState { pub struct AppState {
state_file: StateFile, current_exercise_ind: usize,
exercises: &'static [Exercise], exercises: Vec<Exercise>,
n_done: u16, n_done: u16,
current_exercise: &'static Exercise, welcome_message: String,
final_message: &'static str, final_message: String,
} }
impl AppState { impl AppState {
pub fn new(mut exercises: Vec<Exercise>, mut final_message: String) -> Self { pub fn new(info_file: InfoFile) -> Self {
// Leaking especially for sending the exercises to the debounce event handler. let mut exercises = info_file
// Leaking is not a problem because the `AppState` instance lives until .exercises
// the end of the program. .into_iter()
exercises.shrink_to_fit(); .map(|mut exercise_info| {
let exercises = exercises.leak(); // Leaking to be able to borrow in the watch mode `Table`.
final_message.shrink_to_fit(); // Leaking is not a problem because the `AppState` instance lives until
let final_message = final_message.leak(); // the end of the program.
let path = Box::leak(exercise_info.path().into_boxed_path());
let state_file = StateFile::read_or_default(exercises); exercise_info.name.shrink_to_fit();
let n_done = state_file let name = exercise_info.name.leak();
.progress
.iter() let hint = exercise_info.hint.trim().to_owned();
.fold(0, |acc, done| acc + u16::from(*done));
let current_exercise = &exercises[state_file.current_exercise_ind]; 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 { Self {
state_file, current_exercise_ind,
exercises, exercises,
n_done, n_done,
current_exercise, welcome_message: info_file.welcome_message.unwrap_or_default(),
final_message, final_message: info_file.final_message.unwrap_or_default(),
} }
} }
#[inline] #[inline]
pub fn current_exercise_ind(&self) -> usize { pub fn current_exercise_ind(&self) -> usize {
self.state_file.current_exercise_ind self.current_exercise_ind
} }
#[inline] #[inline]
pub fn progress(&self) -> &[bool] { pub fn exercises(&self) -> &[Exercise] {
&self.state_file.progress &self.exercises
}
#[inline]
pub fn exercises(&self) -> &'static [Exercise] {
self.exercises
} }
#[inline] #[inline]
@ -112,8 +109,8 @@ impl AppState {
} }
#[inline] #[inline]
pub fn current_exercise(&self) -> &'static Exercise { pub fn current_exercise(&self) -> &Exercise {
self.current_exercise &self.exercises[self.current_exercise_ind]
} }
pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> {
@ -121,70 +118,61 @@ impl AppState {
bail!(BAD_INDEX_ERR); bail!(BAD_INDEX_ERR);
} }
self.state_file.current_exercise_ind = ind; self.current_exercise_ind = ind;
self.current_exercise = &self.exercises[ind];
self.state_file.write() write(self)
} }
pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { 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 .exercises
.iter() .iter()
.enumerate() .position(|exercise| exercise.name == name)
.find(|(_, exercise)| exercise.name == name)
.with_context(|| format!("No exercise found for '{name}'!"))?; .with_context(|| format!("No exercise found for '{name}'!"))?;
self.state_file.current_exercise_ind = ind; write(self)
self.current_exercise = exercise;
self.state_file.write()
} }
pub fn set_pending(&mut self, ind: usize) -> Result<()> { pub fn set_pending(&mut self, ind: usize) -> Result<()> {
let done = self let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?;
.state_file
.progress
.get_mut(ind)
.context(BAD_INDEX_ERR)?;
if *done { if exercise.done {
*done = false; exercise.done = false;
self.n_done -= 1; self.n_done -= 1;
self.state_file.write()?; write(self)?;
} }
Ok(()) Ok(())
} }
fn next_pending_exercise_ind(&self) -> Option<usize> { fn next_pending_exercise_ind(&self) -> Option<usize> {
let current_ind = self.state_file.current_exercise_ind; if self.current_exercise_ind == self.exercises.len() - 1 {
if current_ind == self.state_file.progress.len() - 1 {
// The last exercise is done. // The last exercise is done.
// Search for exercises not done from the start. // Search for exercises not done from the start.
return self.state_file.progress[..current_ind] return self.exercises[..self.current_exercise_ind]
.iter() .iter()
.position(|done| !done); .position(|exercise| !exercise.done);
} }
// The done exercise isn't the last one. // The done exercise isn't the last one.
// Search for a pending exercise after the current one and then from the start. // 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() .iter()
.position(|done| !done) .position(|exercise| !exercise.done)
{ {
Some(ind) => Some(current_ind + 1 + ind), Some(ind) => Some(self.current_exercise_ind + 1 + ind),
None => self.state_file.progress[..current_ind] None => self.exercises[..self.current_exercise_ind]
.iter() .iter()
.position(|done| !done), .position(|exercise| !exercise.done),
} }
} }
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> { 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]; let exercise = &mut self.exercises[self.current_exercise_ind];
if !*done { if !exercise.done {
*done = true; exercise.done = true;
self.n_done += 1; self.n_done += 1;
} }
@ -198,15 +186,14 @@ impl AppState {
if !exercise.run()?.status.success() { if !exercise.run()?.status.success() {
writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?;
self.state_file.current_exercise_ind = exercise_ind; self.current_exercise_ind = exercise_ind;
self.current_exercise = exercise;
// No check if the exercise is done before setting it to pending // No check if the exercise is done before setting it to pending
// because no pending exercise was found. // because no pending exercise was found.
self.state_file.progress[exercise_ind] = false; self.exercises[exercise_ind].done = false;
self.n_done -= 1; self.n_done -= 1;
self.state_file.write()?; write(self)?;
return Ok(ExercisesProgress::Pending); return Ok(ExercisesProgress::Pending);
} }

112
src/app_state/state_file.rs Normal file
View file

@ -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));
}
}

View file

@ -1,66 +1,25 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Deserialize;
use std::{ use std::{
fmt::{self, Debug, Display, Formatter}, fmt::{self, Display, Formatter},
fs::{self}, path::Path,
path::PathBuf,
process::{Command, Output}, process::{Command, Output},
}; };
use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::{
embedded::{WriteStrategy, EMBEDDED_FILES},
info_file::Mode,
};
// 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)
}
}
// Deserialized from the `info.toml` file.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Exercise { pub struct Exercise {
// Name of the exercise // Exercise's unique name
pub name: String, pub name: &'static str,
// The path to the file containing the exercise's source code // Exercise's path
pub path: PathBuf, pub path: &'static Path,
// The mode of the exercise // The mode of the exercise
pub mode: Mode, pub mode: Mode,
// The hint text associated with the exercise // The hint text associated with the exercise
pub hint: String, pub hint: String,
pub done: bool,
} }
impl Exercise { impl Exercise {
@ -79,7 +38,7 @@ impl Exercise {
.arg("always") .arg("always")
.arg("-q") .arg("-q")
.arg("--bin") .arg("--bin")
.arg(&self.name) .arg(self.name)
.args(args) .args(args)
.output() .output()
.context("Failed to run Cargo") .context("Failed to run Cargo")
@ -87,7 +46,7 @@ impl Exercise {
pub fn run(&self) -> Result<Output> { pub fn run(&self) -> Result<Output> {
match self.mode { 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::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]),
Mode::Clippy => self.cargo_cmd( Mode::Clippy => self.cargo_cmd(
"clippy", "clippy",
@ -98,7 +57,7 @@ impl Exercise {
pub fn reset(&self) -> Result<()> { pub fn reset(&self) -> Result<()> {
EMBEDDED_FILES 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}")) .with_context(|| format!("Failed to reset the exercise {self}"))
} }
} }
@ -108,6 +67,3 @@ impl Display for Exercise {
Display::fmt(&self.path.display(), f) 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.";

81
src/info_file.rs Normal file
View file

@ -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.";

View file

@ -6,17 +6,21 @@ use std::{
path::Path, 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); let mut cargo_toml = Vec::with_capacity(1 << 13);
cargo_toml.extend_from_slice(b"bin = [\n"); 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(b" { name = \"");
cargo_toml.extend_from_slice(exercise.name.as_bytes()); cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
cargo_toml.extend_from_slice(b"\", path = \""); cargo_toml.extend_from_slice(b"\", path = \"exercises/");
cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); if let Some(dir) = &exercise_info.dir {
cargo_toml.extend_from_slice(b"\" },\n"); 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( cargo_toml.extend_from_slice(
@ -54,7 +58,7 @@ fn create_vscode_dir() -> Result<()> {
Ok(()) 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() { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
} }
@ -74,7 +78,8 @@ pub fn init(exercises: &[Exercise]) -> Result<()> {
.init_exercises_dir() .init_exercises_dir()
.context("Failed to initialize the `rustlings/exercises` directory")?; .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`")?; create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?;

View file

@ -5,7 +5,7 @@ use crossterm::{
ExecutableCommand, ExecutableCommand,
}; };
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use std::{fmt::Write, io}; use std::io;
mod state; mod state;
@ -72,14 +72,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
ui_state.message.push_str(message); ui_state.message.push_str(message);
} }
KeyCode::Char('r') => { KeyCode::Char('r') => {
let Some(exercise) = ui_state.reset_selected()? else { ui_state = ui_state.with_reset_selected()?;
continue;
};
ui_state = ui_state.with_updated_rows();
ui_state
.message
.write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
} }
KeyCode::Char('c') => { KeyCode::Char('c') => {
ui_state.selected_to_current_exercise()?; ui_state.selected_to_current_exercise()?;

View file

@ -6,8 +6,9 @@ use ratatui::{
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
Frame, 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)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum Filter { pub enum Filter {
@ -34,10 +35,9 @@ impl<'a> UiState<'a> {
.app_state .app_state
.exercises() .exercises()
.iter() .iter()
.zip(self.app_state.progress().iter().copied())
.enumerate() .enumerate()
.filter_map(|(ind, (exercise, done))| { .filter_map(|(ind, exercise)| {
let exercise_state = if done { let exercise_state = if exercise.done {
if self.filter == Filter::Pending { if self.filter == Filter::Pending {
return None; return None;
} }
@ -62,7 +62,7 @@ impl<'a> UiState<'a> {
Some(Row::new([ Some(Row::new([
next, next,
exercise_state, exercise_state,
Span::raw(&exercise.name), Span::raw(exercise.name),
Span::raw(exercise.path.to_string_lossy()), Span::raw(exercise.path.to_string_lossy()),
])) ]))
}); });
@ -212,29 +212,30 @@ impl<'a> UiState<'a> {
Ok(()) 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 { let Some(selected) = self.table_state.selected() else {
return Ok(None); return Ok(self);
}; };
let (ind, exercise) = self let (ind, exercise) = self
.app_state .app_state
.exercises() .exercises()
.iter() .iter()
.zip(self.app_state.progress())
.enumerate() .enumerate()
.filter_map(|(ind, (exercise, done))| match self.filter { .filter_map(|(ind, exercise)| match self.filter {
Filter::Done => done.then_some((ind, exercise)), Filter::Done => exercise.done.then_some((ind, exercise)),
Filter::Pending => (!done).then_some((ind, exercise)), Filter::Pending => (!exercise.done).then_some((ind, exercise)),
Filter::None => Some((ind, exercise)), Filter::None => Some((ind, exercise)),
}) })
.nth(selected) .nth(selected)
.context("Invalid selection index")?; .context("Invalid selection index")?;
self.app_state.set_pending(ind)?;
exercise.reset()?; 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<()> { pub fn selected_to_current_exercise(&mut self) -> Result<()> {
@ -244,12 +245,12 @@ impl<'a> UiState<'a> {
let ind = self let ind = self
.app_state .app_state
.progress() .exercises()
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(ind, done)| match self.filter { .filter_map(|(ind, exercise)| match self.filter {
Filter::Done => done.then_some(ind), Filter::Done => exercise.done.then_some(ind),
Filter::Pending => (!done).then_some(ind), Filter::Pending => (!exercise.done).then_some(ind),
Filter::None => Some(ind), Filter::None => Some(ind),
}) })
.nth(selected) .nth(selected)

View file

@ -5,6 +5,7 @@ use std::{path::Path, process::exit};
mod app_state; mod app_state;
mod embedded; mod embedded;
mod exercise; mod exercise;
mod info_file;
mod init; mod init;
mod list; mod list;
mod progress_bar; mod progress_bar;
@ -13,7 +14,7 @@ mod watch;
use self::{ use self::{
app_state::AppState, app_state::AppState,
exercise::InfoFile, info_file::InfoFile,
init::init, init::init,
list::list, list::list,
run::run, run::run,
@ -54,12 +55,10 @@ fn main() -> Result<()> {
which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; which::which("cargo").context(CARGO_NOT_FOUND_ERR)?;
let mut info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
info_file.exercises.shrink_to_fit();
let exercises = info_file.exercises;
if matches!(args.command, Some(Subcommands::Init)) { if matches!(args.command, Some(Subcommands::Init)) {
init(&exercises).context("Initialization failed")?; init(&info_file.exercises).context("Initialization failed")?;
println!("{POST_INIT_MSG}"); println!("{POST_INIT_MSG}");
return Ok(()); return Ok(());
@ -68,18 +67,29 @@ fn main() -> Result<()> {
exit(1); 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 { match args.command {
None => loop { None => {
match watch(&mut app_state)? { // For the the notify event handler thread.
WatchExit::Shutdown => break, // Leaking is not a problem because the slice lives until the end of the program.
// It is much easier to exit the watch mode, launch the list mode and then restart let exercise_paths = app_state
// the watch mode instead of trying to pause the watch threads and correct the .exercises()
// watch state. .iter()
WatchExit::List => list(&mut app_state)?, .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. // `Init` is handled above.
Some(Subcommands::Init) => (), Some(Subcommands::Init) => (),
Some(Subcommands::Run { name }) => { Some(Subcommands::Run { name }) => {
@ -90,10 +100,10 @@ fn main() -> Result<()> {
} }
Some(Subcommands::Reset { name }) => { Some(Subcommands::Reset { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
app_state.set_pending(app_state.current_exercise_ind())?;
let exercise = app_state.current_exercise(); let exercise = app_state.current_exercise();
exercise.reset()?; exercise.reset()?;
println!("The exercise {exercise} has been reset!"); println!("The exercise {exercise} has been reset!");
app_state.set_pending(app_state.current_exercise_ind())?;
} }
Some(Subcommands::Hint { name }) => { Some(Subcommands::Hint { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;

View file

@ -17,7 +17,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
if !output.status.success() { if !output.status.success() {
app_state.set_pending(app_state.current_exercise_ind())?; 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!( stdout.write_fmt(format_args!(

View file

@ -11,14 +11,14 @@ use std::{
time::Duration, time::Duration,
}; };
mod debounce_event; mod notify_event;
mod state; mod state;
mod terminal_event; mod terminal_event;
use crate::app_state::{AppState, ExercisesProgress}; use crate::app_state::{AppState, ExercisesProgress};
use self::{ use self::{
debounce_event::DebounceEventHandler, notify_event::DebounceEventHandler,
state::WatchState, state::WatchState,
terminal_event::{terminal_event_handler, InputEvent}, terminal_event::{terminal_event_handler, InputEvent},
}; };
@ -40,13 +40,16 @@ pub enum WatchExit {
List, 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 (tx, rx) = channel();
let mut debouncer = new_debouncer( let mut debouncer = new_debouncer(
Duration::from_secs(1), Duration::from_secs(1),
DebounceEventHandler { DebounceEventHandler {
tx: tx.clone(), tx: tx.clone(),
exercises: app_state.exercises(), exercise_paths,
}, },
)?; )?;
debouncer debouncer
@ -85,10 +88,10 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
watch_state.render()?; watch_state.render()?;
} }
WatchEvent::NotifyErr(e) => { 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) => { WatchEvent::TerminalEventErr(e) => {
return Err(Error::from(e).context("Terminal event listener failed")) return Err(Error::from(e).context("Terminal event listener failed"));
} }
} }
} }

View file

@ -1,13 +1,11 @@
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
use std::sync::mpsc::Sender; use std::{path::Path, sync::mpsc::Sender};
use crate::exercise::Exercise;
use super::WatchEvent; use super::WatchEvent;
pub struct DebounceEventHandler { pub struct DebounceEventHandler {
pub tx: Sender<WatchEvent>, pub tx: Sender<WatchEvent>,
pub exercises: &'static [Exercise], pub exercise_paths: &'static [&'static Path],
} }
impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
@ -23,9 +21,9 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
return None; return None;
} }
self.exercises self.exercise_paths
.iter() .iter()
.position(|exercise| event.path.ends_with(&exercise.path)) .position(|path| event.path.ends_with(path))
}) })
.min() .min()
else { else {