From 95a597eb8236c5ab180ac38284a655533f83e715 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Tue, 23 Sep 2025 15:26:06 +0200 Subject: [PATCH 01/62] Fix workspace detection with windows line endings Some cargo workspaces may contain windows line endings. Even if the file is stored in a repo with unix line endings, users may have some setting activated that automatically translates them to windows line endings when working locally. --- src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 68011ed4..16ea35e8 100644 --- a/src/init.rs +++ b/src/init.rs @@ -74,7 +74,7 @@ pub fn init() -> Result<()> { let workspace_manifest_content = fs::read_to_string(&workspace_manifest) .with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?; - if !workspace_manifest_content.contains("[workspace]\n") + if !workspace_manifest_content.contains("[workspace]") && !workspace_manifest_content.contains("workspace.") { bail!( From d8f4b06c91c54bccf934b84560641da3a7f202a8 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Wed, 24 Sep 2025 20:56:58 +0200 Subject: [PATCH 02/62] Remove use of `map` in early vecs2 exercise Students do not have the necessary knowledge at this point to understand what's happening with the iterator combinators. This topic is covered well by the dedicated exercises about iterators later. closes #2102 --- CHANGELOG.md | 4 ++++ exercises/05_vecs/vecs2.rs | 34 ---------------------------------- rustlings-macros/info.toml | 11 +---------- solutions/05_vecs/vecs2.rs | 30 ------------------------------ 4 files changed, 5 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18e0aa66..2d2b4153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +### Changed + +- `vecs2`: Removed the use of `map` and `collect`, which are only taught later. + ## 6.5.0 (2025-08-21) ### Added diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs index a9be2580..0c996266 100644 --- a/exercises/05_vecs/vecs2.rs +++ b/exercises/05_vecs/vecs2.rs @@ -9,26 +9,6 @@ fn vec_loop(input: &[i32]) -> Vec { output } -fn vec_map_example(input: &[i32]) -> Vec { - // An example of collecting a vector after mapping. - // We map each element of the `input` slice to its value plus 1. - // If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`. - input.iter().map(|element| element + 1).collect() -} - -fn vec_map(input: &[i32]) -> Vec { - // TODO: Here, we also want to multiply each element in the `input` slice - // by 2, but with iterator mapping instead of manually pushing into an empty - // vector. - // See the example in the function `vec_map_example` above. - input - .iter() - .map(|element| { - // ??? - }) - .collect() -} - fn main() { // You can optionally experiment here. } @@ -43,18 +23,4 @@ mod tests { let ans = vec_loop(&input); assert_eq!(ans, [4, 8, 12, 16, 20]); } - - #[test] - fn test_vec_map_example() { - let input = [1, 2, 3]; - let ans = vec_map_example(&input); - assert_eq!(ans, [2, 3, 4]); - } - - #[test] - fn test_vec_map() { - let input = [2, 4, 6, 8, 10]; - let ans = vec_map(&input); - assert_eq!(ans, [4, 8, 12, 16, 20]); - } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 516fd321..ca3ecf1f 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -318,16 +318,7 @@ of the Rust book to learn more.""" name = "vecs2" dir = "05_vecs" hint = """ -In the first function, we create an empty vector and want to push new elements -to it. - -In the second function, we map the values of the input and collect them into -a vector. - -After you've completed both functions, decide for yourself which approach you -like better. - -What do you think is the more commonly used pattern under Rust developers?""" +Use the `.push()` method on the vector to push new elements to it.""" # MOVE SEMANTICS diff --git a/solutions/05_vecs/vecs2.rs b/solutions/05_vecs/vecs2.rs index 87f7625a..aae71038 100644 --- a/solutions/05_vecs/vecs2.rs +++ b/solutions/05_vecs/vecs2.rs @@ -8,22 +8,6 @@ fn vec_loop(input: &[i32]) -> Vec { output } -fn vec_map_example(input: &[i32]) -> Vec { - // An example of collecting a vector after mapping. - // We map each element of the `input` slice to its value plus 1. - // If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`. - input.iter().map(|element| element + 1).collect() -} - -fn vec_map(input: &[i32]) -> Vec { - // We will dive deeper into iterators, but for now, this is all what you - // had to do! - // Advanced note: This method is more efficient because it automatically - // preallocates enough capacity. This can be done manually in `vec_loop` - // using `Vec::with_capacity(input.len())` instead of `Vec::new()`. - input.iter().map(|element| 2 * element).collect() -} - fn main() { // You can optionally experiment here. } @@ -38,18 +22,4 @@ mod tests { let ans = vec_loop(&input); assert_eq!(ans, [4, 8, 12, 16, 20]); } - - #[test] - fn test_vec_map_example() { - let input = [1, 2, 3]; - let ans = vec_map_example(&input); - assert_eq!(ans, [2, 3, 4]); - } - - #[test] - fn test_vec_map() { - let input = [2, 4, 6, 8, 10]; - let ans = vec_map(&input); - assert_eq!(ans, [4, 8, 12, 16, 20]); - } } From 8753dd6b2ebbd666249e830a4d2569e24a036b27 Mon Sep 17 00:00:00 2001 From: Marlon Date: Wed, 12 Nov 2025 21:58:28 +0100 Subject: [PATCH 03/62] Fixed typo in the exercieses README.md --- exercises/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/README.md b/exercises/README.md index 1df5cc37..24ebd069 100644 --- a/exercises/README.md +++ b/exercises/README.md @@ -9,7 +9,7 @@ | vecs | §8.1 | | move_semantics | §4.1-2 | | structs | §5.1, §5.3 | -| enums | §6, §18.3 | +| enums | §6, §19.3 | | strings | §8.2 | | modules | §7 | | hashmaps | §8.3 | From b5d440fdc3a1fadad6dc6196dad2acddabdc671f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 20 Nov 2025 12:49:07 +0100 Subject: [PATCH 04/62] Fix clippy3 --- exercises/22_clippy/clippy3.rs | 9 +++++---- solutions/22_clippy/clippy3.rs | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs index 7a3cb390..3f23aaee 100644 --- a/exercises/22_clippy/clippy3.rs +++ b/exercises/22_clippy/clippy3.rs @@ -1,7 +1,6 @@ -// Here are some more easy Clippy fixes so you can see its utility 📎 +// Here are some more easy Clippy fixes so you can see its utility. // TODO: Fix all the Clippy lints. -#[rustfmt::skip] #[allow(unused_variables, unused_assignments)] fn main() { let my_option: Option<&str> = None; @@ -11,14 +10,16 @@ fn main() { println!("{}", my_option.unwrap()); } + #[rustfmt::skip] let my_arr = &[ -1, -2, -3 -4, -5, -6 ]; println!("My array! Here it is: {my_arr:?}"); - let my_empty_vec = vec![1, 2, 3, 4, 5].resize(0, 5); - println!("This Vec is empty, see? {my_empty_vec:?}"); + let mut my_vec = vec![1, 2, 3, 4, 5]; + my_vec.resize(0, 5); + println!("This Vec is empty, see? {my_vec:?}"); let mut value_a = 45; let mut value_b = 66; diff --git a/solutions/22_clippy/clippy3.rs b/solutions/22_clippy/clippy3.rs index 81f381e0..8fc26704 100644 --- a/solutions/22_clippy/clippy3.rs +++ b/solutions/22_clippy/clippy3.rs @@ -1,6 +1,5 @@ use std::mem; -#[rustfmt::skip] #[allow(unused_variables, unused_assignments)] fn main() { let my_option: Option<&str> = None; @@ -11,17 +10,18 @@ fn main() { } // A comma was missing. + #[rustfmt::skip] let my_arr = &[ -1, -2, -3, -4, -5, -6, ]; println!("My array! Here it is: {my_arr:?}"); - let mut my_empty_vec = vec![1, 2, 3, 4, 5]; + let mut my_vec = vec![1, 2, 3, 4, 5]; // `resize` mutates a vector instead of returning a new one. // `resize(0, …)` clears a vector, so it is better to use `clear`. - my_empty_vec.clear(); - println!("This Vec is empty, see? {my_empty_vec:?}"); + my_vec.clear(); + println!("This Vec is empty, see? {my_vec:?}"); let mut value_a = 45; let mut value_b = 66; From 1ebb4d25a62199a402e00ec4a95a9df4211c5daf Mon Sep 17 00:00:00 2001 From: Jatin Sanghvi <20547963+JatinSanghvi@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:17:47 +0530 Subject: [PATCH 05/62] Update solution files to match exercise files --- solutions/09_strings/strings4.rs | 2 +- solutions/11_hashmaps/hashmaps1.rs | 2 +- solutions/12_options/options1.rs | 26 +++++++++++++------------- solutions/13_error_handling/errors5.rs | 2 +- solutions/16_lifetimes/lifetimes2.rs | 4 ++-- solutions/21_macros/macros3.rs | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs index 3c69b976..087b0386 100644 --- a/solutions/09_strings/strings4.rs +++ b/solutions/09_strings/strings4.rs @@ -19,7 +19,7 @@ fn main() { // `.into()` converts a type into an expected type. // If it is called where `String` is expected, it will convert `&str` to `String`. string("nice weather".into()); - // But if it is called where `&str` is expected, then `&str` is kept `&str` since no conversion is needed. + // But if it is called where `&str` is expected, then `&str` is kept as `&str` since no conversion is needed. // If you remove the `#[allow(…)]` line, then Clippy will tell you to remove `.into()` below since it is a useless conversion. #[allow(clippy::useless_conversion)] string_slice("nice weather".into()); diff --git a/solutions/11_hashmaps/hashmaps1.rs b/solutions/11_hashmaps/hashmaps1.rs index 3a787c43..0a654b88 100644 --- a/solutions/11_hashmaps/hashmaps1.rs +++ b/solutions/11_hashmaps/hashmaps1.rs @@ -1,7 +1,7 @@ // A basket of fruits in the form of a hash map needs to be defined. The key // represents the name of the fruit and the value represents how many of that // particular fruit is in the basket. You have to put at least 3 different -// types of fruits (e.g apple, banana, mango) in the basket and the total count +// types of fruits (e.g. apple, banana, mango) in the basket and the total count // of all the fruits should be at least 5. use std::collections::HashMap; diff --git a/solutions/12_options/options1.rs b/solutions/12_options/options1.rs index 4d615dd6..a8e6457d 100644 --- a/solutions/12_options/options1.rs +++ b/solutions/12_options/options1.rs @@ -1,8 +1,8 @@ -// This function returns how much icecream there is left in the fridge. +// This function returns how much ice cream there is left in the fridge. // If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, -// someone eats it all, so no icecream is left (value 0). Return `None` if +// someone eats it all, so no ice cream is left (value 0). Return `None` if // `hour_of_day` is higher than 23. -fn maybe_icecream(hour_of_day: u16) -> Option { +fn maybe_ice_cream(hour_of_day: u16) -> Option { match hour_of_day { 0..=21 => Some(5), 22..=23 => Some(0), @@ -21,19 +21,19 @@ mod tests { #[test] fn raw_value() { // Using `unwrap` is fine in a test. - let icecreams = maybe_icecream(12).unwrap(); + let ice_creams = maybe_ice_cream(12).unwrap(); - assert_eq!(icecreams, 5); + assert_eq!(ice_creams, 5); } #[test] - fn check_icecream() { - assert_eq!(maybe_icecream(0), Some(5)); - assert_eq!(maybe_icecream(9), Some(5)); - assert_eq!(maybe_icecream(18), Some(5)); - assert_eq!(maybe_icecream(22), Some(0)); - assert_eq!(maybe_icecream(23), Some(0)); - assert_eq!(maybe_icecream(24), None); - assert_eq!(maybe_icecream(25), None); + fn check_ice_cream() { + assert_eq!(maybe_ice_cream(0), Some(5)); + assert_eq!(maybe_ice_cream(9), Some(5)); + assert_eq!(maybe_ice_cream(18), Some(5)); + assert_eq!(maybe_ice_cream(22), Some(0)); + assert_eq!(maybe_ice_cream(23), Some(0)); + assert_eq!(maybe_ice_cream(24), None); + assert_eq!(maybe_ice_cream(25), None); } } diff --git a/solutions/13_error_handling/errors5.rs b/solutions/13_error_handling/errors5.rs index c1424eee..93ab3b9d 100644 --- a/solutions/13_error_handling/errors5.rs +++ b/solutions/13_error_handling/errors5.rs @@ -6,7 +6,7 @@ // // In short, this particular use case for boxes is for when you want to own a // value and you care only that it is a type which implements a particular -// trait. To do so, The `Box` is declared as of type `Box` where +// trait. To do so, the `Box` is declared as of type `Box` where // `Trait` is the trait the compiler looks for on any value used in that // context. For this exercise, that context is the potential errors which // can be returned in a `Result`. diff --git a/solutions/16_lifetimes/lifetimes2.rs b/solutions/16_lifetimes/lifetimes2.rs index 3ca49093..412c6021 100644 --- a/solutions/16_lifetimes/lifetimes2.rs +++ b/solutions/16_lifetimes/lifetimes2.rs @@ -4,7 +4,7 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { fn main() { let string1 = String::from("long string is long"); - // Solution1: You can move `strings2` out of the inner block so that it is + // Solution 1: You can move `strings2` out of the inner block so that it is // not dropped before the print statement. let string2 = String::from("xyz"); let result; @@ -21,7 +21,7 @@ fn main() { { let string2 = String::from("xyz"); result = longest(&string1, &string2); - // Solution2: You can move the print statement into the inner block so + // Solution 2: You can move the print statement into the inner block so // that it is executed before `string2` is dropped. println!("The longest string is '{result}'"); // `string2` dropped here (end of the inner scope). diff --git a/solutions/21_macros/macros3.rs b/solutions/21_macros/macros3.rs index df35be4d..9d574f89 100644 --- a/solutions/21_macros/macros3.rs +++ b/solutions/21_macros/macros3.rs @@ -1,4 +1,4 @@ -// Added the attribute `macro_use` attribute. +// Added the `macro_use` attribute. #[macro_use] mod macros { macro_rules! my_macro { From 45f789114bae670245cddaf234d442e8a5c4a20c Mon Sep 17 00:00:00 2001 From: Rod Elias Date: Mon, 12 Jan 2026 00:22:59 -0300 Subject: [PATCH 06/62] chore: minor improvements --- exercises/09_strings/README.md | 2 +- rustlings-macros/info.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/09_strings/README.md b/exercises/09_strings/README.md index fa2104cc..a3d22f03 100644 --- a/exercises/09_strings/README.md +++ b/exercises/09_strings/README.md @@ -1,6 +1,6 @@ # Strings -Rust has two string types, a string slice (`&str`) and an owned string (`String`). +Rust has two string types: a string slice (`&str`) and an owned string (`String`). We're not going to dictate when you should use which one, but we'll show you how to identify and create them, as well as use them. diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index ca3ecf1f..e42b0f26 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -21,7 +21,7 @@ get started, here are some notes about how Rustlings operates: final_message = """ We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, don't hesitate to report them on Github. +If you noticed any issues, don't hesitate to report them on GitHub. You can also contribute your own exercises to help the greater community! Before reporting an issue or contributing, please read our guidelines: From 1b47fd97c02524b74096f293249f223a5856556c Mon Sep 17 00:00:00 2001 From: Padraic Slattery Date: Thu, 22 Jan 2026 14:13:29 +0100 Subject: [PATCH 07/62] chore: Update outdated GitHub Actions versions --- .github/workflows/rust.yml | 8 ++++---- .github/workflows/website.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0317f351..0d130904 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,13 +19,13 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clippy run: cargo clippy -- --deny warnings fmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: rustfmt run: cargo fmt --all --check test: @@ -34,14 +34,14 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - name: cargo test run: cargo test --workspace dev-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - name: rustlings dev check run: cargo dev check --require-solutions diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 936cd562..d7513581 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -13,7 +13,7 @@ jobs: working-directory: website runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install TailwindCSS run: npm install - name: Build CSS From de695c46f370f7628608c6efbe610c8b8bc5d44c Mon Sep 17 00:00:00 2001 From: Dipan Chakraborty Date: Tue, 10 Feb 2026 19:00:41 +0530 Subject: [PATCH 08/62] docs: add str and String documentation links --- exercises/09_strings/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exercises/09_strings/README.md b/exercises/09_strings/README.md index fa2104cc..58b50e1e 100644 --- a/exercises/09_strings/README.md +++ b/exercises/09_strings/README.md @@ -6,4 +6,6 @@ to identify and create them, as well as use them. ## Further information -- [Strings](https://doc.rust-lang.org/book/ch08-02-strings.html) +- [Strings (Rust Book)](https://doc.rust-lang.org/book/ch08-02-strings.html) +- [`str` methods](https://doc.rust-lang.org/std/primitive.str.html) +- [`String` methods](https://doc.rust-lang.org/std/string/struct.String.html) From 3a00274335be26c82c4045e7ec7154a79380b410 Mon Sep 17 00:00:00 2001 From: "cheoleon (pstor)" <26815428+eoncheole@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:44:49 +0900 Subject: [PATCH 09/62] docs: add Korean Rustlings to community exercises list --- website/content/community-exercises/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/content/community-exercises/index.md b/website/content/community-exercises/index.md index 0f713d7c..b87d02c8 100644 --- a/website/content/community-exercises/index.md +++ b/website/content/community-exercises/index.md @@ -6,6 +6,7 @@ title = "Community Exercises" - 🇯🇵 [Japanese Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercises. - 🇨🇳 [Simplified Chinese Rustlings](https://github.com/SandmeyerX/rustlings-zh-cn): A simplified Chinese translation of the Rustlings exercises. +- 🇰🇷 [Korean Rustlings](https://github.com/eoncheole/rustlings-kr): A Korean translation of the Rustlings exercises. > You can use the same `rustlings` program that you installed with `cargo install rustlings` to run community exercises. From 4817abcc1476e2d57965d19095d24ff805ba851c Mon Sep 17 00:00:00 2001 From: Piotr Spieker Date: Thu, 12 Feb 2026 11:56:25 +0100 Subject: [PATCH 10/62] Mention struct-like variants in enums2 hint instead of anonymous structs --- rustlings-macros/info.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index ca3ecf1f..dd1f04be 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -440,7 +440,7 @@ dir = "08_enums" test = false hint = """ You can create enumerations that have different variants with different types -such as anonymous structs, structs, a single string, tuples, no data, etc.""" +such as struct-like variants, regular structs, a single string, tuples, no data, etc.""" [[exercises]] name = "enums3" From 13564207cb5eaa2781c79e562cee7ab47b39d564 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 20 Nov 2025 12:56:58 +0100 Subject: [PATCH 11/62] Update deps --- Cargo.lock | 600 +++++++++++++++++++++--------------- Cargo.toml | 12 +- rustlings-macros/Cargo.toml | 2 +- 3 files changed, 366 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f883653f..c2c15813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -34,35 +34,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bitflags" @@ -72,21 +66,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.45" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -94,9 +88,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -106,9 +100,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -118,9 +112,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -134,7 +128,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", "crossterm_winapi", "document-features", "mio", @@ -156,9 +150,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -171,12 +165,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -185,6 +179,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -196,14 +196,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", + "wasip3", ] [[package]] @@ -211,6 +212,15 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -219,13 +229,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "indexmap" -version = "2.10.0" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -234,7 +252,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", "inotify-sys", "libc", ] @@ -250,15 +268,15 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "kqueue" @@ -281,55 +299,60 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.175" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -338,7 +361,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", "fsevent-sys", "inotify", "kqueue", @@ -352,9 +375,12 @@ dependencies = [ [[package]] name = "notify-types" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] [[package]] name = "once_cell" @@ -364,15 +390,15 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -380,31 +406,41 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -417,24 +453,24 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", ] [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -462,12 +498,6 @@ dependencies = [ "toml", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -484,19 +514,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.219" +name = "semver" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -505,23 +551,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -536,9 +583,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -547,10 +594,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -568,9 +616,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -579,25 +627,25 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "toml" -version = "0.9.5" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", - "serde", + "serde_core", "serde_spanned", "toml_datetime", "toml_parser", @@ -607,33 +655,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.0" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "utf8parse" @@ -658,12 +712,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] @@ -684,11 +781,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -699,18 +796,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -718,149 +806,179 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags 2.9.2", + "wit-bindgen-rust-macro", ] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 4469b281..25415ce4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and rust-version = "1.88" [workspace.dependencies] -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1", features = ["derive"] } toml = { version = "0.9", default-features = false, features = ["std", "parse", "serde"] } [package] @@ -45,12 +45,12 @@ include = [ ] [dependencies] -anyhow = "1.0" -clap = { version = "4.5", features = ["derive"] } +anyhow = "1" +clap = { version = "4", features = ["derive"] } crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] } -notify = "8.0" +notify = "8" rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" } -serde_json = "1.0" +serde_json = "1" serde.workspace = true toml.workspace = true @@ -58,7 +58,7 @@ toml.workspace = true rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] } [dev-dependencies] -tempfile = "3.21" +tempfile = "3" [profile.release] panic = "abort" diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index 5df648b2..5d123b51 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -16,7 +16,7 @@ include = [ proc-macro = true [dependencies] -quote = "1.0" +quote = "1" serde.workspace = true toml.workspace = true From 0cbcb8964c434066826b65633f2b208d69c334d3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 15:55:07 +0100 Subject: [PATCH 12/62] Borrow deserialized values --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- rustlings-macros/src/lib.rs | 13 +++++++------ src/app_state.rs | 25 ++++++++++++------------- src/cargo_toml.rs | 14 +++++++------- src/dev/check.rs | 6 +++--- src/embedded.rs | 11 ++++++----- src/info_file.rs | 21 +++++++++++---------- src/init.rs | 9 +++++---- src/main.rs | 2 +- 10 files changed, 57 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2c15813..c690e819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.0.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" dependencies = [ "indexmap", "serde_core", @@ -655,9 +655,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] diff --git a/Cargo.toml b/Cargo.toml index 25415ce4..068a3f49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ rust-version = "1.88" [workspace.dependencies] serde = { version = "1", features = ["derive"] } -toml = { version = "0.9", default-features = false, features = ["std", "parse", "serde"] } +toml = { version = "1", default-features = false, features = ["std", "parse", "serde"] } [package] name = "rustlings" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index b20c6f1d..db758d59 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -3,14 +3,15 @@ use quote::quote; use serde::Deserialize; #[derive(Deserialize)] -struct ExerciseInfo { - name: String, - dir: String, +struct ExerciseInfo<'a> { + name: &'a str, + dir: &'a str, } #[derive(Deserialize)] -struct InfoFile { - exercises: Vec, +struct InfoFile<'a> { + #[serde(borrow)] + exercises: Vec>, } #[proc_macro] @@ -37,7 +38,7 @@ pub fn include_files(_: TokenStream) -> TokenStream { continue; } - dirs.push(exercise.dir.as_str()); + dirs.push(exercise.dir); *dir_ind = dirs.len() - 1; } diff --git a/src/app_state.rs b/src/app_state.rs index d654d042..71a4fbbf 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -54,7 +54,7 @@ pub struct AppState { exercises: Vec, // Caches the number of done exercises to avoid iterating over all exercises every time. n_done: u16, - final_message: String, + final_message: &'static str, state_file: File, // Preallocated buffer for reading and writing the state file. file_buf: Vec, @@ -66,7 +66,7 @@ pub struct AppState { impl AppState { pub fn new( exercise_infos: Vec, - final_message: String, + final_message: &'static str, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -87,34 +87,33 @@ impl AppState { // Leaking is not a problem because the `AppState` instance lives until // the end of the program. let path = exercise_info.path().leak(); - let name = exercise_info.name.leak(); - let dir = exercise_info.dir.map(|dir| &*dir.leak()); - let hint = exercise_info.hint.leak().trim_ascii(); + let hint = exercise_info.hint.trim_ascii(); let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { let mut canonical_path; - if let Some(dir) = dir { + if let Some(dir) = exercise_info.dir { canonical_path = String::with_capacity( - 2 + dir_canonical_path.len() + dir.len() + name.len(), + 2 + dir_canonical_path.len() + dir.len() + exercise_info.name.len(), ); canonical_path.push_str(dir_canonical_path); canonical_path.push_str(MAIN_SEPARATOR_STR); canonical_path.push_str(dir); } else { - canonical_path = - String::with_capacity(1 + dir_canonical_path.len() + name.len()); + canonical_path = String::with_capacity( + 1 + dir_canonical_path.len() + exercise_info.name.len(), + ); canonical_path.push_str(dir_canonical_path); } canonical_path.push_str(MAIN_SEPARATOR_STR); - canonical_path.push_str(name); + canonical_path.push_str(exercise_info.name); canonical_path.push_str(".rs"); canonical_path }); Exercise { - dir, - name, + dir: exercise_info.dir, + name: exercise_info.name, path, canonical_path, test: exercise_info.test, @@ -616,7 +615,7 @@ mod tests { current_exercise_ind: 0, exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], n_done: 0, - final_message: String::new(), + final_message: "", state_file: tempfile::tempfile().unwrap(), file_buf: Vec::new(), official_exercises: true, diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index ce0dfd0c..9297da82 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -38,7 +38,7 @@ pub fn append_bins( buf.extend_from_slice(b"\", path = \""); buf.extend_from_slice(exercise_path_prefix); buf.extend_from_slice(b"exercises/"); - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { buf.extend_from_slice(dir.as_bytes()); buf.push(b'/'); } @@ -56,7 +56,7 @@ pub fn append_bins( buf.extend_from_slice(b"\", path = \""); buf.extend_from_slice(exercise_path_prefix); buf.extend_from_slice(b"solutions/"); - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { buf.extend_from_slice(dir.as_bytes()); buf.push(b'/'); } @@ -106,19 +106,19 @@ mod tests { fn test_bins() { let exercise_infos = [ ExerciseInfo { - name: String::from("1"), + name: "1", dir: None, test: true, strict_clippy: true, - hint: String::new(), + hint: "", skip_check_unsolved: false, }, ExerciseInfo { - name: String::from("2"), - dir: Some(String::from("d")), + name: "2", + dir: Some("d"), test: false, strict_clippy: false, - hint: String::new(), + hint: "", skip_check_unsolved: false, }, ]; diff --git a/src/dev/check.rs b/src/dev/check.rs index f7111063..16d04563 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -63,7 +63,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { let mut file_buf = String::with_capacity(1 << 14); for exercise_info in &info_file.exercises { - let name = exercise_info.name.as_str(); + let name = exercise_info.name; if name.is_empty() { bail!("Found an empty exercise name in `info.toml`"); } @@ -76,7 +76,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { bail!("Char `{c}` in the exercise name `{name}` is not allowed"); } - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { if dir.is_empty() { bail!("The exercise `{name}` has an empty dir name in `info.toml`"); } @@ -214,7 +214,7 @@ fn check_exercises_unsolved( Some( thread::Builder::new() .spawn(|| exercise_info.run_exercise(None, cmd_runner)) - .map(|handle| (exercise_info.name.as_str(), handle)), + .map(|handle| (exercise_info.name, handle)), ) }) .collect::, _>>() diff --git a/src/embedded.rs b/src/embedded.rs index 61a5f581..bee4119c 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -85,7 +85,7 @@ impl EmbeddedFiles { exercise_path.truncate(prefix.len()); exercise_path.push_str(dir.name); exercise_path.push('/'); - exercise_path.push_str(&exercise_info.name); + exercise_path.push_str(exercise_info.name); exercise_path.push_str(".rs"); fs::write(&exercise_path, exercise_files.exercise) @@ -141,13 +141,14 @@ mod tests { use super::*; #[derive(Deserialize)] - struct ExerciseInfo { - dir: String, + struct ExerciseInfo<'a> { + dir: &'a str, } #[derive(Deserialize)] - struct InfoFile { - exercises: Vec, + struct InfoFile<'a> { + #[serde(borrow)] + exercises: Vec>, } #[test] diff --git a/src/info_file.rs b/src/info_file.rs index 04e5d644..8a90fcca 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -8,9 +8,9 @@ use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; #[derive(Deserialize)] pub struct ExerciseInfo { /// Exercise's unique name. - pub name: String, + pub name: &'static str, /// Exercise's directory name inside the `exercises/` directory. - pub dir: Option, + pub dir: Option<&'static str>, /// Run `cargo test` on the exercise. #[serde(default = "default_true")] pub test: bool, @@ -18,7 +18,7 @@ pub struct ExerciseInfo { #[serde(default)] pub strict_clippy: bool, /// The exercise's hint to be shown to the user on request. - pub hint: String, + pub hint: &'static str, /// The exercise is already solved. Ignore it when checking that all exercises are unsolved. #[serde(default)] pub skip_check_unsolved: bool, @@ -31,7 +31,7 @@ const fn default_true() -> bool { impl ExerciseInfo { /// Path to the exercise file starting with the `exercises/` directory. pub fn path(&self) -> String { - let mut path = if let Some(dir) = &self.dir { + let mut path = if let Some(dir) = self.dir { // 14 = 10 + 1 + 3 // exercises/ + / + .rs let mut path = String::with_capacity(14 + dir.len() + self.name.len()); @@ -47,7 +47,7 @@ impl ExerciseInfo { path }; - path.push_str(&self.name); + path.push_str(self.name); path.push_str(".rs"); path @@ -57,12 +57,12 @@ impl ExerciseInfo { impl RunnableExercise for ExerciseInfo { #[inline] fn name(&self) -> &str { - &self.name + self.name } #[inline] fn dir(&self) -> Option<&str> { - self.dir.as_deref() + self.dir } #[inline] @@ -82,9 +82,9 @@ pub struct InfoFile { /// For possible breaking changes in the future for community exercises. pub format_version: u8, /// Shown to users when starting with the exercises. - pub welcome_message: Option, + pub welcome_message: Option<&'static str>, /// Shown to users after finishing all exercises. - pub final_message: Option, + pub final_message: Option<&'static str>, /// List of all exercises. pub exercises: Vec, } @@ -95,7 +95,8 @@ impl InfoFile { pub fn parse() -> Result { // Read a local `info.toml` if it exists. let slf = match fs::read_to_string("info.toml") { - Ok(file_content) => toml::de::from_str::(&file_content) + // Leaking is fine since `InfoFile` is used until the end of the program. + Ok(file_content) => toml::de::from_str::(file_content.leak()) .context("Failed to parse the `info.toml` file")?, Err(e) => { if e.kind() == ErrorKind::NotFound { diff --git a/src/init.rs b/src/init.rs index 16ea35e8..9ef211e1 100644 --- a/src/init.rs +++ b/src/init.rs @@ -8,7 +8,7 @@ use std::{ env::set_current_dir, fs::{self, create_dir}, io::{self, Write}, - path::{Path, PathBuf}, + path::Path, process::{Command, Stdio}, }; @@ -18,8 +18,9 @@ use crate::{ }; #[derive(Deserialize)] -struct CargoLocateProject { - root: PathBuf, +struct CargoLocateProject<'a> { + #[serde(borrow)] + root: &'a Path, } pub fn init() -> Result<()> { @@ -72,7 +73,7 @@ pub fn init() -> Result<()> { )? .root; - let workspace_manifest_content = fs::read_to_string(&workspace_manifest) + let workspace_manifest_content = fs::read_to_string(workspace_manifest) .with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?; if !workspace_manifest_content.contains("[workspace]") && !workspace_manifest_content.contains("workspace.") diff --git a/src/main.rs b/src/main.rs index ffd2dfa7..b541b68f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,7 +128,7 @@ fn main() -> Result { None } else { // For the notify event handler thread. - // Leaking is not a problem because the slice lives until the end of the program. + // Leaking is fine since the slice is used until the end of the program. Some( &*app_state .exercises() From 2512701e2fc902273d4e8e36447ef8f6b6842cbc Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:05:53 +0100 Subject: [PATCH 13/62] Keep exercise path owned --- src/app_state.rs | 20 +++++++------------- src/exercise.rs | 6 +++--- src/info_file.rs | 2 +- src/list/state.rs | 2 +- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 71a4fbbf..765425a9 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -83,12 +83,6 @@ impl AppState { let mut exercises = exercise_infos .into_iter() .map(|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 = exercise_info.path().leak(); - let hint = exercise_info.hint.trim_ascii(); - let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { let mut canonical_path; if let Some(dir) = exercise_info.dir { @@ -114,11 +108,11 @@ impl AppState { Exercise { dir: exercise_info.dir, name: exercise_info.name, - path, + path: exercise_info.path(), canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, - hint, + hint: exercise_info.hint.trim_ascii(), // Updated below. done: false, } @@ -342,12 +336,12 @@ impl AppState { Ok(()) } - pub fn reset_current_exercise(&mut self) -> Result<&'static str> { + pub fn reset_current_exercise(&mut self) -> Result<&str> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, exercise.path)?; + self.reset(self.current_exercise_ind, &exercise.path)?; - Ok(exercise.path) + Ok(&exercise.path) } // Reset the exercise by index and return its name. @@ -358,7 +352,7 @@ impl AppState { self.set_pending(exercise_ind)?; let exercise = &self.exercises[exercise_ind]; - self.reset(exercise_ind, exercise.path)?; + self.reset(exercise_ind, &exercise.path)?; Ok(exercise.name) } @@ -600,7 +594,7 @@ mod tests { Exercise { dir: None, name: "0", - path: "exercises/0.rs", + path: String::from("exercises/0.rs"), canonical_path: None, test: false, strict_clippy: false, diff --git a/src/exercise.rs b/src/exercise.rs index a0596b5b..ee1f42f9 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -69,7 +69,7 @@ pub struct Exercise { pub dir: Option<&'static str>, pub name: &'static str, /// Path of the exercise file starting with the `exercises/` directory. - pub path: &'static str, + pub path: String, pub canonical_path: Option, pub test: bool, pub strict_clippy: bool, @@ -85,9 +85,9 @@ impl Exercise { ) -> io::Result<()> { file_path(writer, Color::Blue, |writer| { if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { - terminal_file_link(writer, self.path, canonical_path) + terminal_file_link(writer, &self.path, canonical_path) } else { - writer.write_str(self.path) + writer.write_str(&self.path) } }) } diff --git a/src/info_file.rs b/src/info_file.rs index 8a90fcca..23df0d16 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -95,7 +95,7 @@ impl InfoFile { pub fn parse() -> Result { // Read a local `info.toml` if it exists. let slf = match fs::read_to_string("info.toml") { - // Leaking is fine since `InfoFile` is used until the end of the program. + // Leaking is fine since the info file is used until the end of the program. Ok(file_content) => toml::de::from_str::(file_content.leak()) .context("Failed to parse the `info.toml` file")?, Err(e) => { diff --git a/src/list/state.rs b/src/list/state.rs index 4fd1301d..55ccb4c9 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -366,11 +366,11 @@ impl<'a> ListState<'a> { let exercise_ind = self.selected_to_exercise_ind(selected)?; let exercise_name = self.app_state.reset_exercise_by_ind(exercise_ind)?; - self.update_rows(); write!( self.message, "The exercise `{exercise_name}` has been reset", )?; + self.update_rows(); Ok(()) } From 9011d349876bcbd3088bcc06013e4af32bb9d955 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:12:08 +0100 Subject: [PATCH 14/62] Swap name and dir to stay consistent with the info file --- src/app_state.rs | 4 ++-- src/exercise.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 765425a9..a566e14b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -106,8 +106,8 @@ impl AppState { }); Exercise { - dir: exercise_info.dir, name: exercise_info.name, + dir: exercise_info.dir, path: exercise_info.path(), canonical_path, test: exercise_info.test, @@ -592,8 +592,8 @@ mod tests { fn dummy_exercise() -> Exercise { Exercise { - dir: None, name: "0", + dir: None, path: String::from("exercises/0.rs"), canonical_path: None, test: false, diff --git a/src/exercise.rs b/src/exercise.rs index ee1f42f9..c07a94e8 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -66,8 +66,8 @@ fn run_bin( /// See `info_file::ExerciseInfo` pub struct Exercise { - pub dir: Option<&'static str>, pub name: &'static str, + pub dir: Option<&'static str>, /// Path of the exercise file starting with the `exercises/` directory. pub path: String, pub canonical_path: Option, From 8738518699ac4eb9b010d28a342108a0ac0707a4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:20:40 +0100 Subject: [PATCH 15/62] Use `rustlings_dir` when deleting the temporary dir before recreating it --- src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 9ef211e1..971b0e51 100644 --- a/src/init.rs +++ b/src/init.rs @@ -107,7 +107,7 @@ pub fn init() -> Result<()> { } stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the `Cargo.toml` file of this Cargo workspace.\n")?; - fs::remove_dir_all("rustlings") + fs::remove_dir_all(rustlings_dir) .context("Failed to remove the temporary directory `rustlings/`")?; init_git = false; } else { From aaf8cad7782beb57197ccb9edd04e5d18100ba21 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:20:40 +0100 Subject: [PATCH 16/62] Add backtrace to CI --- .github/workflows/rust.yml | 2 +- tests/integration_tests.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0d130904..828afa1a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,7 +37,7 @@ jobs: - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - name: cargo test - run: cargo test --workspace + run: RUST_BACKTRACE=1 cargo test --workspace dev-check: runs-on: ubuntu-latest steps: diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bb3a084b..bd8ef544 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -38,6 +38,7 @@ impl<'a> Cmd<'a> { self } + #[track_caller] fn assert(&self, success: bool) { let rustlings_bin = { let mut path = env::current_exe().unwrap(); @@ -87,11 +88,13 @@ impl<'a> Cmd<'a> { } #[inline] + #[track_caller] fn success(&self) { self.assert(true); } #[inline] + #[track_caller] fn fail(&self) { self.assert(false); } From e91647b02345fbd8c4cc4e7b536e63719b5b2be8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:26:23 +0100 Subject: [PATCH 17/62] Add RUSTBACKTRACE as env --- .github/workflows/rust.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 828afa1a..9a7ae2c0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,7 +37,9 @@ jobs: - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - name: cargo test - run: RUST_BACKTRACE=1 cargo test --workspace + env: + RUST_BACKTRACE: 1 + run: cargo test --workspace dev-check: runs-on: ubuntu-latest steps: From c163bfe563cec427b4b311ef681c6127b042df13 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 26 Feb 2026 17:35:44 +0100 Subject: [PATCH 18/62] Improve error messages if tests fail --- tests/integration_tests.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bd8ef544..520429d0 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -61,30 +61,32 @@ impl<'a> Cmd<'a> { cmd.args(self.args).stdin(Stdio::null()); - let status = match self.output { - None => cmd - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .unwrap(), + let output = match self.output { + None => cmd.output().unwrap(), Some(FullStdout(stdout)) => { let output = cmd.stderr(Stdio::null()).output().unwrap(); assert_eq!(from_utf8(&output.stdout).unwrap(), stdout); - output.status + output } Some(PartialStdout(stdout)) => { let output = cmd.stderr(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); - output.status + output } Some(PartialStderr(stderr)) => { let output = cmd.stdout(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stderr).unwrap().contains(stderr)); - output.status + output } }; - assert_eq!(status.success(), success, "{cmd:?}"); + assert_eq!( + output.status.success(), + success, + "{cmd:?}\n\nstdout:\n{}\nstderr:\n{}", + from_utf8(&output.stdout).unwrap(), + from_utf8(&output.stderr).unwrap(), + ); } #[inline] From 7e5793b64228248cdd3c1fe250f89badff3c9418 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 17:39:00 +0100 Subject: [PATCH 19/62] Remove \r on Windows --- rustlings-macros/src/lib.rs | 12 ++++++++++-- src/info_file.rs | 15 +++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index db758d59..40eebc16 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -16,8 +16,16 @@ struct InfoFile<'a> { #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { - let info_file = include_str!("../info.toml"); - let exercises = toml::de::from_str::(info_file) + // Remove `\r` on Windows + let info_file = String::from_utf8( + include_bytes!("../info.toml") + .iter() + .copied() + .filter(|c| *c != b'\r') + .collect(), + ) + .expect("Failed to parse `info.toml` as UTF8"); + let exercises = toml::de::from_str::(&info_file) .expect("Failed to parse `info.toml`") .exercises; diff --git a/src/info_file.rs b/src/info_file.rs index 23df0d16..13206fd8 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -94,10 +94,17 @@ impl InfoFile { /// Community exercises: Parse the `info.toml` file in the current directory. pub fn parse() -> Result { // Read a local `info.toml` if it exists. - let slf = match fs::read_to_string("info.toml") { - // Leaking is fine since the info file is used until the end of the program. - Ok(file_content) => toml::de::from_str::(file_content.leak()) - .context("Failed to parse the `info.toml` file")?, + let slf = match fs::read("info.toml") { + Ok(file_content) => { + // Remove `\r` on Windows. + // Leaking is fine since the info file is used until the end of the program. + let file_content = + String::from_utf8(file_content.into_iter().filter(|c| *c != b'\r').collect()) + .context("Failed to parse `info.toml` as UTF8")? + .leak(); + toml::de::from_str::(file_content) + .context("Failed to parse the `info.toml` file")? + } Err(e) => { if e.kind() == ErrorKind::NotFound { return toml::de::from_str(EMBEDDED_FILES.info_file) From 17ff88902ba089285a8d635c4bda05dbaea94131 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Wed, 10 Sep 2025 18:19:41 +0200 Subject: [PATCH 20/62] Avoid initializing nested Git repository Previously a Git repository was initialized if a Cargo workspace was detected. However, it's also possible for users to initialize rustlings in an existing Git repository that doesn't contain a Cargo workspace. In that case, it's still undesirable to initialize a nested Git repository for rustlings. We therefore search all ancestors of the current working directory for `.git` or `.jj` directories to determine if rustlings is being initialized in an existing Git repository. --- src/init.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 971b0e51..2987907e 100644 --- a/src/init.rs +++ b/src/init.rs @@ -29,6 +29,21 @@ pub fn init() -> Result<()> { bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); } + let is_inside_vcs_repository = 'detect_repo: { + let Ok(mut dir) = std::env::current_dir() else { + break 'detect_repo false; + }; + loop { + if dir.join(".git").exists() || dir.join(".jj").exists() { + break 'detect_repo true; + } + match dir.parent() { + Some(parent) => dir = parent.into(), + None => break 'detect_repo false, + } + } + }; + let locate_project_output = Command::new("cargo") .arg("locate-project") .arg("-q") @@ -59,7 +74,7 @@ pub fn init() -> Result<()> { } let mut stdout = io::stdout().lock(); - let mut init_git = true; + let mut init_git = !is_inside_vcs_repository; if locate_project_output.status.success() { if Path::new("exercises").exists() && Path::new("solutions").exists() { From 064f057b102bd6bb0090d4ee3e307b40c28f4fa4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 18:01:17 +0100 Subject: [PATCH 21/62] Improve integration testing --- tests/integration_tests.rs | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 520429d0..c7119914 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -5,7 +5,7 @@ use std::{ }; enum Output<'a> { - FullStdout(&'a str), + FullStdout(&'a [u8]), PartialStdout(&'a str), PartialStderr(&'a str), } @@ -61,32 +61,27 @@ impl<'a> Cmd<'a> { cmd.args(self.args).stdin(Stdio::null()); - let output = match self.output { - None => cmd.output().unwrap(), + let output = cmd.output().unwrap(); + match self.output { + None => (), Some(FullStdout(stdout)) => { - let output = cmd.stderr(Stdio::null()).output().unwrap(); - assert_eq!(from_utf8(&output.stdout).unwrap(), stdout); - output + assert_eq!(output.stdout, stdout); } Some(PartialStdout(stdout)) => { - let output = cmd.stderr(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); - output } Some(PartialStderr(stderr)) => { - let output = cmd.stdout(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stderr).unwrap().contains(stderr)); - output } }; - assert_eq!( - output.status.success(), - success, - "{cmd:?}\n\nstdout:\n{}\nstderr:\n{}", - from_utf8(&output.stdout).unwrap(), - from_utf8(&output.stderr).unwrap(), - ); + if output.status.success() != success { + panic!( + "{cmd:?}\n\nstdout:\n{}\n\nstderr:\n{}", + from_utf8(&output.stdout).unwrap(), + from_utf8(&output.stderr).unwrap(), + ); + } } #[inline] @@ -153,7 +148,7 @@ fn hint() { Cmd::default() .current_dir("tests/test_exercises") .args(&["hint", "test_failure"]) - .output(FullStdout("The answer to everything: 42\n")) + .output(FullStdout(b"The answer to everything: 42\n")) .success(); } From d87a3b6ca53016ed51da6ea94a1291a596a40db4 Mon Sep 17 00:00:00 2001 From: Gabriel Feceoru Date: Sat, 14 Mar 2026 16:26:22 +0100 Subject: [PATCH 22/62] Fix u16 mul overflow with big term width When running rustlings in Rover IDE, term width could have a value of 2480 which causes u16 mul overflow. --- src/term.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/term.rs b/src/term.rs index 3d149b33..b661dfa2 100644 --- a/src/term.rs +++ b/src/term.rs @@ -216,7 +216,9 @@ pub fn progress_bar<'a>( stdout.write_all(PREFIX)?; let width = term_width - WRAPPER_WIDTH; - let filled = (width * progress) / total; + // Use u32 to prevent the intermediate multiplication from overflowing u16 + let filled = (width as u32 * progress as u32) / total as u32; + let filled = filled as u16; stdout.queue(SetForegroundColor(Color::Green))?; for _ in 0..filled { From 611d62951f74040a750589bd42f84dfaccead8cb Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 16:57:50 +0100 Subject: [PATCH 23/62] Update deps --- Cargo.lock | 68 +++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c690e819..c0f0147a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -19,15 +19,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -78,9 +78,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -100,9 +100,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -112,15 +112,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "crossterm" @@ -196,9 +196,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", @@ -248,9 +248,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ "bitflags 2.11.0", "inotify-sys", @@ -306,9 +306,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "linux-raw-sys" @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -438,18 +438,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "redox_syscall" @@ -627,9 +627,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.3+spec-1.1.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" dependencies = [ "indexmap", "serde_core", @@ -885,9 +885,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "wit-bindgen" From 0ffeb1440294c0ec63b488fb7a1ac7ff1510c2fa Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 17:10:11 +0100 Subject: [PATCH 24/62] Avoid unneeded computation on full progress bar --- src/term.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/term.rs b/src/term.rs index b661dfa2..96b8745a 100644 --- a/src/term.rs +++ b/src/term.rs @@ -227,14 +227,13 @@ pub fn progress_bar<'a>( if filled < width { stdout.write_all(b">")?; - } - let width_minus_filled = width - filled; - if width_minus_filled > 1 { - let red_part_width = width_minus_filled - 1; - stdout.queue(SetForegroundColor(Color::Red))?; - for _ in 0..red_part_width { - stdout.write_all(b"-")?; + let width_minus_filled = width - filled; + if width_minus_filled > 1 { + stdout.queue(SetForegroundColor(Color::Red))?; + for _ in 1..width_minus_filled { + stdout.write_all(b"-")?; + } } } From 337f6b152132438aa22c739aa79dcb17d9c9e239 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 17:18:39 +0100 Subject: [PATCH 25/62] Apply pedantic Clippy lints --- src/app_state.rs | 2 +- src/info_file.rs | 2 +- src/main.rs | 4 ++-- src/term.rs | 6 +++--- src/watch/state.rs | 2 +- src/watch/terminal_event.rs | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index a566e14b..ad63c6de 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -433,7 +433,7 @@ impl AppState { .is_err() { break; - }; + } let success = exercise.run_exercise(None, &slf.cmd_runner); let progress = match success { diff --git a/src/info_file.rs b/src/info_file.rs index 13206fd8..54a21a5c 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -23,7 +23,7 @@ pub struct ExerciseInfo { #[serde(default)] pub skip_check_unsolved: bool, } -#[inline(always)] +#[inline] const fn default_true() -> bool { true } diff --git a/src/main.rs b/src/main.rs index b541b68f..c39e8629 100644 --- a/src/main.rs +++ b/src/main.rs @@ -171,9 +171,9 @@ fn main() -> Result { stdout.write_all(b"\n")?; return Ok(ExitCode::FAILURE); - } else { - app_state.render_final_message(&mut stdout)?; } + + app_state.render_final_message(&mut stdout)?; } Some(Subcommands::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; diff --git a/src/term.rs b/src/term.rs index 96b8745a..65e4c511 100644 --- a/src/term.rs +++ b/src/term.rs @@ -197,15 +197,15 @@ pub fn progress_bar<'a>( total: u16, term_width: u16, ) -> io::Result<()> { - debug_assert!(total <= 999); - debug_assert!(progress <= total); - const PREFIX: &[u8] = b"Progress: ["; const PREFIX_WIDTH: u16 = PREFIX.len() as u16; const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16; const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; + debug_assert!(total <= 999); + debug_assert!(progress <= total); + if term_width < MIN_LINE_WIDTH { writer.write_ascii(b"Progress: ")?; // Integers are in ASCII. diff --git a/src/watch/state.rs b/src/watch/state.rs index a92dd2d6..44cbd439 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -60,7 +60,7 @@ impl<'a> WatchState<'a> { watch_event_sender, terminal_event_unpause_receiver, manual_run, - ) + ); }) .context("Failed to spawn a thread to handle terminal events")?; diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 2400a3df..439e4730 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -47,7 +47,7 @@ pub fn terminal_event_handler( // Pause input until quitting the confirmation prompt. if unpause_receiver.recv().is_err() { return; - }; + } continue; } @@ -64,7 +64,7 @@ pub fn terminal_event_handler( return; } } - Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue, + Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => (), Err(e) => break WatchEvent::TerminalEventErr(e), } }; From ceb98475e2a3a3c83df4c381a2da14afe3f8a2e8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 17:36:18 +0100 Subject: [PATCH 26/62] Avoid unneeded castings --- src/app_state.rs | 10 +++++----- src/list/state.rs | 2 +- src/term.rs | 11 +++++------ src/watch/state.rs | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index ad63c6de..5722e607 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -52,8 +52,8 @@ pub enum CheckProgress { pub struct AppState { current_exercise_ind: usize, exercises: Vec, - // Caches the number of done exercises to avoid iterating over all exercises every time. - n_done: u16, + // Cache the number of done exercises to avoid iterating over all exercises every time. + n_done: u32, final_message: &'static str, state_file: File, // Preallocated buffer for reading and writing the state file. @@ -191,13 +191,13 @@ impl AppState { } #[inline] - pub fn n_done(&self) -> u16 { + pub fn n_done(&self) -> u32 { self.n_done } #[inline] - pub fn n_pending(&self) -> u16 { - self.exercises.len() as u16 - self.n_done + pub fn n_pending(&self) -> u32 { + self.exercises.len() as u32 - self.n_done } #[inline] diff --git a/src/list/state.rs b/src/list/state.rs index 55ccb4c9..58aa4961 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -229,7 +229,7 @@ impl<'a> ListState<'a> { progress_bar( &mut MaxLenWriter::new(stdout, self.term_width as usize), self.app_state.n_done(), - self.app_state.exercises().len() as u16, + self.app_state.exercises().len() as u32, self.term_width, )?; next_ln(stdout)?; diff --git a/src/term.rs b/src/term.rs index 65e4c511..8cab5005 100644 --- a/src/term.rs +++ b/src/term.rs @@ -193,8 +193,8 @@ impl Drop for ProgressCounter<'_, '_> { pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, - progress: u16, - total: u16, + progress: u32, + total: u32, term_width: u16, ) -> io::Result<()> { const PREFIX: &[u8] = b"Progress: ["; @@ -215,10 +215,9 @@ pub fn progress_bar<'a>( let stdout = writer.stdout(); stdout.write_all(PREFIX)?; - let width = term_width - WRAPPER_WIDTH; - // Use u32 to prevent the intermediate multiplication from overflowing u16 - let filled = (width as u32 * progress as u32) / total as u32; - let filled = filled as u16; + // Use u32 to prevent the intermediate multiplication from overflowing + let width = u32::from(term_width - WRAPPER_WIDTH); + let filled = (width * progress) / total; stdout.queue(SetForegroundColor(Color::Green))?; for _ in 0..filled { diff --git a/src/watch/state.rs b/src/watch/state.rs index 44cbd439..f93ea0cf 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -245,7 +245,7 @@ impl<'a> WatchState<'a> { progress_bar( stdout, self.app_state.n_done(), - self.app_state.exercises().len() as u16, + self.app_state.exercises().len() as u32, self.term_width, )?; From a28b9eda84db5d1810f77225aee9d95ae960da2b Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 18:22:27 +0100 Subject: [PATCH 27/62] Delay inside_vcs_repo check until Git initialization --- src/init.rs | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/init.rs b/src/init.rs index 2987907e..f043bd48 100644 --- a/src/init.rs +++ b/src/init.rs @@ -5,7 +5,7 @@ use crossterm::{ }; use serde::Deserialize; use std::{ - env::set_current_dir, + env::{current_dir, set_current_dir}, fs::{self, create_dir}, io::{self, Write}, path::Path, @@ -29,21 +29,6 @@ pub fn init() -> Result<()> { bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); } - let is_inside_vcs_repository = 'detect_repo: { - let Ok(mut dir) = std::env::current_dir() else { - break 'detect_repo false; - }; - loop { - if dir.join(".git").exists() || dir.join(".jj").exists() { - break 'detect_repo true; - } - match dir.parent() { - Some(parent) => dir = parent.into(), - None => break 'detect_repo false, - } - } - }; - let locate_project_output = Command::new("cargo") .arg("locate-project") .arg("-q") @@ -74,7 +59,7 @@ pub fn init() -> Result<()> { } let mut stdout = io::stdout().lock(); - let mut init_git = !is_inside_vcs_repository; + let mut init_git = true; if locate_project_output.status.success() { if Path::new("exercises").exists() && Path::new("solutions").exists() { @@ -184,14 +169,27 @@ pub fn init() -> Result<()> { fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; - if init_git { - // Ignore any Git error because Git initialization is not required. - let _ = Command::new("git") - .arg("init") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + if init_git && let Ok(dir) = current_dir() { + let mut dir = dir.as_path(); + + loop { + if dir.join(".git").exists() || dir.join(".jj").exists() { + break; + } + + if let Some(parent) = dir.parent() { + dir = parent; + } else { + // Ignore any Git error because Git initialization is not required. + let _ = Command::new("git") + .arg("init") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + break; + } + } } stdout.queue(SetForegroundColor(Color::Green))?; From 2c9c31e8a2845f83bb41fe5af385b013473c4867 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 17 Mar 2026 12:05:38 +0100 Subject: [PATCH 28/62] Remove cargo-upgrades from release hook --- release-hook.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/release-hook.sh b/release-hook.sh index 49349337..0e955d1f 100755 --- a/release-hook.sh +++ b/release-hook.sh @@ -4,7 +4,6 @@ set -e typos -cargo upgrades # Similar to CI cargo clippy -- --deny warnings From d3df1051675fc45cbfca6d8bc8a07f7c4e63cbf1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 17 Mar 2026 12:17:07 +0100 Subject: [PATCH 29/62] Upgrade to Zola 0.22 --- .github/workflows/website.yml | 4 ++-- website/config.toml | 6 +++--- website/input.css | 8 ++------ website/package.json | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index d7513581..399d4674 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -19,11 +19,11 @@ jobs: - name: Build CSS run: npx @tailwindcss/cli -m -i input.css -o static/main.css - name: Download Zola - run: curl -fsSL https://github.com/getzola/zola/releases/download/v0.20.0/zola-v0.20.0-x86_64-unknown-linux-gnu.tar.gz | tar xz + run: curl -fsSL https://github.com/getzola/zola/releases/download/v0.22.1/zola-v0.22.1-x86_64-unknown-linux-gnu.tar.gz | tar xz - name: Build site run: ./zola build - name: Upload static files as artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: website/public/ diff --git a/website/config.toml b/website/config.toml index 0c01dc7d..73ae7f2c 100644 --- a/website/config.toml +++ b/website/config.toml @@ -6,11 +6,11 @@ compile_sass = false build_search_index = false [markdown] -highlight_code = true -highlight_theme = "dracula" - insert_anchor_links = "heading" +[markdown.highlighting] +theme = "dracula" + [extra] logo_path = "images/happy_ferris.svg" diff --git a/website/input.css b/website/input.css index af0675d8..01c956d6 100644 --- a/website/input.css +++ b/website/input.css @@ -41,14 +41,10 @@ @apply md:w-3/4 lg:w-3/5; } blockquote { - @apply px-3 pt-2 pb-0.5 mb-4 mt-2 border-s-4 border-white/80 bg-white/7 rounded-sm; + @apply px-3 pt-2 pb-px mb-4 mt-2 border-s-4 border-white/80 bg-white/7 rounded-sm; } pre { - @apply px-2 pt-2 pb-px overflow-x-auto text-sm sm:text-base rounded-sm mt-2 mb-4 after:content-[attr(data-lang)] after:text-[8px] after:opacity-40 selection:bg-white/15; - } - pre code mark { - @apply pb-0.5 pt-1 pr-px text-inherit rounded-xs; + @apply px-2 pt-2 pb-1.5 overflow-x-auto text-sm sm:text-base rounded-sm mt-2 mb-4 selection:bg-white/15; } } - diff --git a/website/package.json b/website/package.json index 38dd27e9..80c84872 100644 --- a/website/package.json +++ b/website/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@tailwindcss/cli": "^4.1" + "@tailwindcss/cli": "^4" } } From 573d5a2acd5a6541c459b228d994e5281b764684 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 17 Mar 2026 11:56:36 +0100 Subject: [PATCH 30/62] Fix integration tests for build dir layout v2 --- tests/integration_tests.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index c7119914..91d0536e 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,5 +1,4 @@ use std::{ - env::{self, consts::EXE_SUFFIX}, process::{Command, Stdio}, str::from_utf8, }; @@ -40,20 +39,7 @@ impl<'a> Cmd<'a> { #[track_caller] fn assert(&self, success: bool) { - let rustlings_bin = { - let mut path = env::current_exe().unwrap(); - // Pop test binary name - path.pop(); - // Pop `/deps` - path.pop(); - - path.push("rustlings"); - let mut path = path.into_os_string(); - path.push(EXE_SUFFIX); - path - }; - - let mut cmd = Command::new(rustlings_bin); + let mut cmd = Command::new(env!("CARGO_BIN_EXE_rustlings")); if let Some(current_dir) = self.current_dir { cmd.current_dir(current_dir); From 08eb634db5f76c891a3f0f7e916d4cc41ec03da4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 25 Mar 2026 18:24:41 +0100 Subject: [PATCH 31/62] Upgrade deply-pages --- .github/workflows/website.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 399d4674..79d70f74 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -40,4 +40,4 @@ jobs: runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 From 3e46d8c50a23ae2ceea7f9abb350c3049ff79fb3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 25 Mar 2026 18:28:39 +0100 Subject: [PATCH 32/62] Update deps --- Cargo.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0f0147a..a65ecff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -274,9 +274,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "kqueue" @@ -564,9 +564,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" dependencies = [ "indexmap", "serde_core", @@ -655,27 +655,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" [[package]] name = "unicode-ident" @@ -885,9 +885,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" [[package]] name = "wit-bindgen" From 37cbcd9049627653a46f3a90d0d5dd5468464e1c Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 25 Mar 2026 18:28:39 +0100 Subject: [PATCH 33/62] Adjust CI triggers --- .github/workflows/rust.yml | 2 ++ .github/workflows/website.yml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9a7ae2c0..72f3675b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,11 +5,13 @@ on: branches: [main] paths-ignore: - website + - .github/workflows/website.yml - '*.md' pull_request: branches: [main] paths-ignore: - website + - .github/workflows/website.yml - '*.md' env: diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 79d70f74..7ea41e73 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -4,7 +4,9 @@ on: workflow_dispatch: push: branches: [main] - paths: [website] + paths: + - website + - .github/workflows/website.yml jobs: build: From 7150a9eb79842f68e95ec045cf21ffbc0bb07915 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 30 Mar 2026 16:55:07 +0200 Subject: [PATCH 34/62] Add rumdl --- .github/workflows/rust.yml | 5 + .github/workflows/website.yml | 7 +- .rumdl.toml | 7 ++ CHANGELOG.md | 192 +++++++++++++++++----------------- CONTRIBUTING.md | 2 +- 5 files changed, 116 insertions(+), 97 deletions(-) create mode 100644 .rumdl.toml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 72f3675b..a52d0730 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -49,3 +49,8 @@ jobs: - uses: swatinem/rust-cache@v2 - name: rustlings dev check run: cargo dev check --require-solutions + rumdl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: rvben/rumdl@v0 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 7ea41e73..d437cf03 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -9,7 +9,13 @@ on: - .github/workflows/website.yml jobs: + rumdl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: rvben/rumdl@v0 build: + needs: rumdl defaults: run: working-directory: website @@ -28,7 +34,6 @@ jobs: uses: actions/upload-pages-artifact@v4 with: path: website/public/ - deploy: needs: build # Grant GITHUB_TOKEN the permissions required to make a Pages deployment diff --git a/.rumdl.toml b/.rumdl.toml new file mode 100644 index 00000000..528e0e31 --- /dev/null +++ b/.rumdl.toml @@ -0,0 +1,7 @@ +[global] +output-format = "full" +disable = ["MD013", "MD057"] + +[per-file-ignores] +"website/content/_index.md" = ["MD041"] +"website/content/**/*.md" = ["MD028", "MD033"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2b4153..0f326b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# Changelog + ## Unreleased ### Changed @@ -109,17 +111,17 @@ ## 6.1.0 (2024-07-10) -#### Added +### Added - `dev check`: Check that all exercises (including community ones) include at least one `TODO` comment. - `dev check`: Check that all exercises actually fail to run (not already solved). -#### Changed +### Changed - Make enum variants more consistent between enum exercises. - `iterators3`: Teach about the possible case of integer overflow during division. -#### Fixed +### Fixed - Exit with a helpful error message on missing/unsupported terminal/TTY. - Mark the last exercise as done. @@ -196,11 +198,11 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.6.1 (2023-09-18) -#### Changed +### Changed - Converted all exercises with assertions to test mode. -#### Fixed +### Fixed - `cow1`: Reverted regression introduced by calling `to_mut` where it shouldn't have been called, and clarified comment. @@ -211,7 +213,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.6.0 (2023-09-04) -#### Added +### Added - New exercise: `if3`, teaching the user about `if let` statements. - `hashmaps2`: Added an extra test function to check if the amount of fruits is higher than zero. @@ -219,7 +221,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `if1`: Added a test case to check equal values. - `if3`: Added a note specifying that there are no test changes needed. -#### Changed +### Changed - Swapped the order of threads and smart pointer exercises. - Rewrote the CLI to use `clap` - it's matured much since we switched to `argh` :) @@ -227,7 +229,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same concepts. -#### Fixed +### Fixed - `iterators5`: - Removed an outdated part of the hint. @@ -242,7 +244,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `cow1`: Added `.to_mut()` to distinguish from the previous test case. - `threads2`: Updated hint text to reference the correct book heading. -#### Housekeeping +### Housekeeping - Cleaned up the explanation paragraphs at the start of each exercise. - Lots of Nix housekeeping that I don't feel qualified to write about! @@ -250,13 +252,13 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.5.1 (2023-05-17) -#### Fixed +### Fixed - Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix. ## 5.5.0 (2023-05-17) -#### Added +### Added - `strings2`: Added a reference to the book chapter for reference conversion - `lifetimes`: Added a link to the lifetimekata project @@ -264,7 +266,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Added a `!` prefix command to watch mode that runs an external command - Added a `--success-hints` option to watch mode that shows hints on exercise success -#### Changed +### Changed - `vecs2`: Renamed iterator variable bindings for clarify - `lifetimes`: Changed order of book references @@ -273,7 +275,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `options2`: Improved tests for layering options - `modules2`: Added more information to the hint -#### Fixed +### Fixed - `errors2`: Corrected a comment wording - `iterators2`: Fixed a spelling mistake in the hint text @@ -283,20 +285,20 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `options3`: Changed exercise to panic when no match is found - `rustlings lsp` now generates absolute paths, which should fix VSCode `rust-analyzer` usage on Windows -#### Housekeeping +### Housekeeping - Added a markdown linter to run on GitHub actions - Split quick installation section into two code blocks ## 5.4.1 (2023-03-10) -#### Changed +### Changed - `vecs`: Added links to `iter_mut` and `map` to README.md - `cow1`: Changed main to tests - `iterators1`: Formatted according to rustfmt -#### Fixed +### Fixed - `errors5`: Unified undisclosed type notation - `arc1`: Improved readability by avoiding implicit dereference @@ -305,7 +307,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.4.0 (2023-02-12) -#### Changed +### Changed - Reordered exercises - Unwrapped `standard_library_types` into `iterators` and `smart_pointers` @@ -317,7 +319,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Made progress bar update proportional to amount of files verified - Decreased `watch` delay from 2 to 1 second -#### Fixed +### Fixed - Capitalized "Rust" in exercise hints - **enums3**: Removed superfluous tuple brackets @@ -327,23 +329,23 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Fixed a typo in a method name - Specified the edition in `rustc` commands -#### Housekeeping +### Housekeeping - Bumped min Rust version to 1.58 in installation script ## 5.3.0 (2022-12-23) -#### Added +### Added - **cli**: Added a percentage display in watch mode - Added a `flake.nix` for Nix users -#### Changed +### Changed - **structs3**: Added an additional test - **macros**: Added a link to MacroKata in the README -#### Fixed +### Fixed - **strings3**: Added a link to `std` in the hint - **threads1**: Corrected a hint link @@ -357,7 +359,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **enums2**: Removed unnecessary indirection of self - **enums3**: Added an extra tuple comment -#### Housekeeping +### Housekeeping - Added a VSCode extension recommendation - Applied some Clippy and rustfmt formatting @@ -365,28 +367,28 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.2.1 (2022-09-06) -#### Fixed +### Fixed - **quiz1**: Reworded the comment to actually reflect what's going on in the tests. Also added another assert just to make sure. - **rc1**: Fixed a typo in the hint. - **lifetimes**: Add quotes to the `println!` output, for readability. -#### Housekeeping +### Housekeeping - Fixed a typo in README.md ## 5.2.0 (2022-08-27) -#### Added +### Added - Added a `reset` command -#### Changed +### Changed - **options2**: Convert the exercise to use tests -#### Fixed +### Fixed - **threads3**: Fixed a typo - **quiz1**: Adjusted the explanations to be consistent with @@ -394,18 +396,18 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.1.1 (2022-08-17) -#### Bug Fixes +### Bug Fixes - Fixed an incorrect assertion in options1 ## 5.1.0 (2022-08-16) -#### Features +### Features - Added a new `rc1` exercise. - Added a new `cow1` exercise. -#### Bug Fixes +### Bug Fixes - **variables5**: Corrected reference to previous exercise - **functions4**: Fixed line number reference @@ -425,7 +427,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Added more granular tests - Fixed some comment syntax shenanigans in info.toml -#### Housekeeping +### Housekeeping - Fixed a typo in .editorconfig - Fixed a typo in integration_tests.rs @@ -434,7 +436,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.0.0 (2022-07-16) -#### Features +### Features - Hint comments in exercises now also include a reference to the `hint` watch mode subcommand. @@ -466,7 +468,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Added 3 new lifetimes exercises. - Added 3 new traits exercises. -#### Bug Fixes +### Bug Fixes - **variables2**: Made output messages more verbose. - **variables5**: Added a nudging hint about shadowing. @@ -490,7 +492,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. `Box`. - **try_from_into**: Fixed the function name in comment. -#### Removed +### Removed - Removed the legacy LSP feature that was using `mod.rs` files. - Removed `quiz4`. @@ -498,7 +500,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. order, and I've always felt like they didn't quite fit in with the mostly simple, book-following style we've had in Rustlings. -#### Housekeeping +### Housekeeping - Added missing exercises to the book index. - Updated spacing in Cargo.toml. @@ -506,53 +508,53 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.8.0 (2022-07-01) -#### Features +### Features - Added a progress indicator for `rustlings watch`. - The installation script now checks for Rustup being installed. - Added a `rustlings lsp` command to enable `rust-analyzer`. -#### Bug Fixes +### Bug Fixes - **move_semantics5**: Replaced "in vogue" with "in scope" in hint. - **if2**: Fixed a typo in the hint. - **variables1**: Fixed an incorrect line reference in the hint. - Fixed an out of bounds check in the installation Bash script. -#### Housekeeping +### Housekeeping - Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting. - Removed the deprecated Rust GitPod extension. ## 4.7.1 (2022-04-20) -#### Features +### Features - The amount of dependency crates that need to be compiled went down from ~65 to ~45 by bumping dependency versions. - The minimum Rust version in the install scripts has been bumped to 1.56.0 (this isn't in the release itself, since install scripts don't really get versioned) -#### Bug Fixes +### Bug Fixes - **arc1**: A small part has been rewritten using a more functional code style (#968). - **using_as**: A small part has been refactored to use `sum` instead of `fold`, resulting in better readability. -#### Housekeeping +### Housekeeping - The changelog will now be manually written instead of being automatically generated by the Git log. ## 4.7.0 (2022-04-14) -#### Features +### Features - Add move_semantics6.rs exercise (#908) ([3f0e1303](https://github.com/rust-lang/rustlings/commit/3f0e1303e0b3bf3fecc0baced3c8b8a37f83c184)) - **intro:** Add intro section. ([21c9f441](https://github.com/rust-lang/rustlings/commit/21c9f44168394e08338fd470b5f49b1fd235986f)) - Include exercises folder in the project structure behind a feature, enabling rust-analyzer to work (#917) ([179a75a6](https://github.com/rust-lang/rustlings/commit/179a75a68d03ac9518dec2297fb17f91a4fc506b)) -#### Bug Fixes +### Bug Fixes - Fix a few spelling mistakes ([1c0fe3cb](https://github.com/rust-lang/rustlings/commit/1c0fe3cbcca85f90b3985985b8e265ee872a2ab2)) - **cli:** @@ -579,14 +581,14 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **structs3.rs:** assigned value to cents_per_gram in test ([d1ee2daf](https://github.com/rust-lang/rustlings/commit/d1ee2daf14f19105e6db3f9c610f44293d688532)) - **traits1:** rename test functions to snake case (#854) ([1663a16e](https://github.com/rust-lang/rustlings/commit/1663a16eade6ca646b6ed061735f7982434d530d)) -#### Documentation improvements +### Documentation improvements - Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886)) - Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929)) ## 4.6.0 (2021-09-25) -#### Features +### Features - add advanced_errs2 ([abd6b70c](https://github.com/rust-lang/rustlings/commit/abd6b70c72dc6426752ff41f09160b839e5c449e)) - add advanced_errs1 ([882d535b](https://github.com/rust-lang/rustlings/commit/882d535ba8628d5e0b37e8664b3e2f26260b2671)) @@ -595,7 +597,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **modules:** update exercises, add modules3 (#822) ([dfd2fab4](https://github.com/rust-lang/rustlings/commit/dfd2fab4f33d1bf59e2e5ee03123c0c9a67a9481)) - **quiz1:** add default function name in comment (#838) ([0a11bad7](https://github.com/rust-lang/rustlings/commit/0a11bad71402b5403143d642f439f57931278c07)) -#### Bug Fixes +### Bug Fixes - Correct small typo in exercises/conversions/from_str.rs ([86cc8529](https://github.com/rust-lang/rustlings/commit/86cc85295ae36948963ae52882e285d7e3e29323)) - **cli:** typo in exercise.rs (#848) ([06d5c097](https://github.com/rust-lang/rustlings/commit/06d5c0973a3dffa3c6c6f70acb775d4c6630323c)) @@ -608,12 +610,12 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.5.0 (2021-07-07) -#### Features +### Features - Add move_semantics5 exercise. (#746) ([399ab328](https://github.com/rust-lang/rustlings/commit/399ab328d8d407265c09563aa4ef4534b2503ff2)) - **cli:** Add "next" to run the next unsolved exercise. (#785) ([d20e413a](https://github.com/rust-lang/rustlings/commit/d20e413a68772cd493561f2651cf244e822b7ca5)) -#### Bug Fixes +### Bug Fixes - rename result1 to errors4 ([50ab289d](https://github.com/rust-lang/rustlings/commit/50ab289da6b9eb19a7486c341b00048c516b88c0)) - move_semantics5 hints ([1b858285](https://github.com/rust-lang/rustlings/commit/1b85828548f46f58b622b5e0c00f8c989f928807)) @@ -628,7 +630,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.4.0 (2021-04-24) -#### Bug Fixes +### Bug Fixes - Fix spelling error in main.rs ([91ee27f2](https://github.com/rust-lang/rustlings/commit/91ee27f22bd3797a9db57e5fd430801c170c5db8)) - typo in default out text ([644c49f1](https://github.com/rust-lang/rustlings/commit/644c49f1e04cbb24e95872b3a52b07d692ae3bc8)) @@ -656,7 +658,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **threads1:** line number correction ([7857b0a6](https://github.com/rust-lang/rustlings/commit/7857b0a689b0847f48d8c14cbd1865e3b812d5ca)) - **try_from_into:** use trait objects ([2e93a588](https://github.com/rust-lang/rustlings/commit/2e93a588e0abe8badb7eafafb9e7d073c2be5df8)) -#### Features +### Features - Replace clap with argh ([7928122f](https://github.com/rust-lang/rustlings/commit/7928122fcef9ca7834d988b1ec8ca0687478beeb)) - Replace emojis when NO_EMOJI env variable present ([8d62a996](https://github.com/rust-lang/rustlings/commit/8d62a9963708dbecd9312e8bcc4b47049c72d155)) @@ -669,7 +671,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.3.0 (2020-12-29) -#### Features +### Features - Rewrite default out text ([44d39112](https://github.com/rust-lang/rustlings/commit/44d39112ff122b29c9793fe52e605df1612c6490)) - match exercise order to book chapters (#541) ([033bf119](https://github.com/rust-lang/rustlings/commit/033bf1198fc8bfce1b570e49da7cde010aa552e3)) @@ -677,7 +679,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - add "rustlings list" command ([838f9f30](https://github.com/rust-lang/rustlings/commit/838f9f30083d0b23fd67503dcf0fbeca498e6647)) - **try_from_into:** remove duplicate annotation ([04f1d079](https://github.com/rust-lang/rustlings/commit/04f1d079aa42a2f49af694bc92c67d731d31a53f)) -#### Bug Fixes +### Bug Fixes - update structs README ([bcf14cf6](https://github.com/rust-lang/rustlings/commit/bcf14cf677adb3a38a3ac3ca53f3c69f61153025)) - added missing exercises to info.toml ([90cfb6ff](https://github.com/rust-lang/rustlings/commit/90cfb6ff28377531bfc34acb70547bdb13374f6b)) @@ -691,14 +693,14 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.2.0 (2020-11-07) -#### Features +### Features - Add HashMap exercises ([633c00cf](https://github.com/rust-lang/rustlings/commit/633c00cf8071e1e82959a3010452a32f34f29fc9)) - Add Vec exercises ([0c12fa31](https://github.com/rust-lang/rustlings/commit/0c12fa31c57c03c6287458a0a8aca7afd057baf6)) - **primitive_types6:** Add a test (#548) ([2b1fb2b7](https://github.com/rust-lang/rustlings/commit/2b1fb2b739bf9ad8d6b7b12af25fee173011bfc4)) - **try_from_into:** Add tests (#571) ([95ccd926](https://github.com/rust-lang/rustlings/commit/95ccd92616ae79ba287cce221101e0bbe4f68cdc)) -#### Bug Fixes +### Bug Fixes - log error output when inotify limit is exceeded ([d61b4e5a](https://github.com/rust-lang/rustlings/commit/d61b4e5a13b44d72d004082f523fa1b6b24c1aca)) - more unique temp_file ([5643ef05](https://github.com/rust-lang/rustlings/commit/5643ef05bc81e4a840e9456f4406a769abbe1392)) @@ -711,7 +713,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.1.0 (2020-10-05) -#### Bug Fixes +### Bug Fixes - Update rustlings version in Cargo.lock ([1cc40bc9](https://github.com/rust-lang/rustlings/commit/1cc40bc9ce95c23d56f6d91fa1c4deb646231fef)) - **arc1:** index mod should equal thread count ([b4062ef6](https://github.com/rust-lang/rustlings/commit/b4062ef6993e80dac107c4093ea85166ad3ee0fa)) @@ -721,7 +723,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **structs3:** Small adjustment of variable name ([114b54cb](https://github.com/rust-lang/rustlings/commit/114b54cbdb977234b39e5f180d937c14c78bb8b2)) - **using_as:** Add test so that proper type is returned. (#512) ([3286c5ec](https://github.com/rust-lang/rustlings/commit/3286c5ec19ea5fb7ded81d047da5f8594108a490)) -#### Features +### Features - Added iterators1.rs exercise ([9642f5a3](https://github.com/rust-lang/rustlings/commit/9642f5a3f686270a4f8f6ba969919ddbbc4f7fdd)) - Add ability to run rustlings on repl.it (#471) ([8f7b5bd0](https://github.com/rust-lang/rustlings/commit/8f7b5bd00eb83542b959830ef55192d2d76db90a)) @@ -733,12 +735,12 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.0.0 (2020-07-08) -#### Breaking Changes +### Breaking Changes - Add a --nocapture option to display test harnesses' outputs ([8ad5f9bf](https://github.com/rust-lang/rustlings/commit/8ad5f9bf531a4848b1104b7b389a20171624c82f)) - Rename test to quiz, fixes #244 ([010a0456](https://github.com/rust-lang/rustlings/commit/010a04569282149cea7f7a76fc4d7f4c9f0f08dd)) -#### Features +### Features - Add traits README ([173bb141](https://github.com/rust-lang/rustlings/commit/173bb14140c5530cbdb59e53ace3991a99d804af)) - Add box1.rs exercise ([7479a473](https://github.com/rust-lang/rustlings/commit/7479a4737bdcac347322ad0883ca528c8675e720)) @@ -747,7 +749,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Added exercise structs3.rs ([b66e2e09](https://github.com/rust-lang/rustlings/commit/b66e2e09622243e086a0f1258dd27e1a2d61c891)) - Add exercise variables6 covering const (#352) ([5999acd2](https://github.com/rust-lang/rustlings/commit/5999acd24a4f203292be36e0fd18d385887ec481)) -#### Bug Fixes +### Bug Fixes - Change then to than ([ddd98ad7](https://github.com/rust-lang/rustlings/commit/ddd98ad75d3668fbb10eff74374148aa5ed2344d)) - rename quiz1 to tests1 in info (#420) ([0dd1c6ca](https://github.com/rust-lang/rustlings/commit/0dd1c6ca6b389789e0972aa955fe17aa15c95f29)) @@ -774,11 +776,11 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 3.0.0 (2020-04-11) -#### Breaking Changes +### Breaking Changes - make "compile" exercises print output (#278) ([3b6d5c](https://github.com/fmoko/rustlings/commit/3b6d5c3aaa27a242a832799eb66e96897d26fde3)) -#### Bug Fixes +### Bug Fixes - **primitive_types:** revert primitive_types4 (#296) ([b3a3351e](https://github.com/rust-lang/rustlings/commit/b3a3351e8e6a0bdee07077d7b0382953821649ae)) - **run:** compile clippy exercise files (#295) ([3ab084a4](https://github.com/rust-lang/rustlings/commit/3ab084a421c0f140ae83bf1fc3f47b39342e7373)) @@ -787,26 +789,26 @@ Then follow the link to the guide about [community exercises](https://rustlings. - remove duplicate not done comment (#292) ([dab90f](https://github.com/fmoko/rustlings/commit/dab90f7b91a6000fe874e3d664f244048e5fa342)) - don't hardcode documentation version for traits (#288) ([30e6af](https://github.com/fmoko/rustlings/commit/30e6af60690c326fb5d3a9b7335f35c69c09137d)) -#### Features +### Features - add Option2 exercise (#290) ([86b5c08b](https://github.com/rust-lang/rustlings/commit/86b5c08b9bea1576127a7c5f599f5752072c087d)) - add exercise for option (#282) ([135e5d47](https://github.com/rust-lang/rustlings/commit/135e5d47a7c395aece6f6022117fb20c82f2d3d4)) - add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229)) - **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71)) -### 2.2.1 (2020-02-27) +## 2.2.1 (2020-02-27) -#### Bug Fixes +### Bug Fixes - Re-add cloning the repo to install scripts ([3d9b03c5](https://github.com/rust-lang/rustlings/commit/3d9b03c52b8dc51b140757f6fd25ad87b5782ef5)) -#### Features +### Features - Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921)) ## 2.2.0 (2020-02-25) -#### Bug Fixes +### Bug Fixes - Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401)) - **docs:** @@ -821,7 +823,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Change test command ([fe10e06c](https://github.com/rust-lang/rustlings/commit/fe10e06c3733ddb4a21e90d09bf79bfe618e97ce) - Correct test command in tests1.rs comment (#263) ([39fa7ae](https://github.com/rust-lang/rustlings/commit/39fa7ae8b70ad468da49b06f11b2383135a63bcf)) -#### Features +### Features - Add variables5.rs exercise (#264) ([0c73609e](https://github.com/rust-lang/rustlings/commit/0c73609e6f2311295e95d6f96f8c747cfc4cba03)) - Show a completion message when watching (#253) ([d25ee55a](https://github.com/rust-lang/rustlings/commit/d25ee55a3205882d35782e370af855051b39c58c)) @@ -833,7 +835,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 2.1.0 (2019-11-27) -#### Bug Fixes +### Bug Fixes - add line numbers in several exercises and hints ([b565c4d3](https://github.com/rust-lang/rustlings/commit/b565c4d3e74e8e110bef201a082fa1302722a7c3)) - **arc1:** Fix some words in the comment ([c42c3b21](https://github.com/rust-lang/rustlings/commit/c42c3b2101df9164c8cd7bb344def921e5ba3e61)) @@ -844,33 +846,33 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **strings2:** update line number in hint ([a09f684f](https://github.com/rust-lang/rustlings/commit/a09f684f05c58d239a6fc59ec5f81c2533e8b820)) - **variables1:** Correct wrong word in comment ([fda5a470](https://github.com/rust-lang/rustlings/commit/fda5a47069e0954f16a04e8e50945e03becb71a5)) -#### Features +### Features - **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c)) ## 2.0.0 (2019-11-12) -#### Bug Fixes +### Bug Fixes - **default:** Clarify the installation procedure ([c371b853](https://github.com/rust-lang/rustlings/commit/c371b853afa08947ddeebec0edd074b171eeaae0)) - **info:** Fix trailing newlines for hints ([795b6e34](https://github.com/rust-lang/rustlings/commit/795b6e348094a898e9227a14f6232f7bb94c8d31)) - **run:** make `run` never prompt ([4b265465](https://github.com/rust-lang/rustlings/commit/4b26546589f7d2b50455429482cf1f386ceae8b3)) -#### Breaking Changes +### Breaking Changes - Refactor hint system ([9bdb0a12](https://github.com/rust-lang/rustlings/commit/9bdb0a12e45a8e9f9f6a4bd4a9c172c5376c7f60)) - improve `watch` execution mode ([2cdd6129](https://github.com/rust-lang/rustlings/commit/2cdd61294f0d9a53775ee24ad76435bec8a21e60)) - Index exercises by name ([627cdc07](https://github.com/rust-lang/rustlings/commit/627cdc07d07dfe6a740e885e0ddf6900e7ec336b)) - **run:** makes `run` never prompt ([4b265465](https://github.com/rust-lang/rustlings/commit/4b26546589f7d2b50455429482cf1f386ceae8b3)) -#### Features +### Features - **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425)) - **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f)) -### 1.5.1 (2019-11-11) +## 1.5.1 (2019-11-11) -#### Bug Fixes +### Bug Fixes - **errors3:** Update hint ([dcfb427b](https://github.com/rust-lang/rustlings/commit/dcfb427b09585f0193f0a294443fdf99f11c64cb), closes [#185](https://github.com/rust-lang/rustlings/issues/185)) - **if1:** Remove `return` reference ([ad03d180](https://github.com/rust-lang/rustlings/commit/ad03d180c9311c0093e56a3531eec1a9a70cdb45)) @@ -881,7 +883,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 1.5.0 (2019-11-09) -#### Bug Fixes +### Bug Fixes - **test1:** Rewrite logic ([79a56942](https://github.com/rust-lang/rustlings/commit/79a569422c8309cfc9e4aed25bf4ab3b3859996b)) - **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6)) @@ -897,15 +899,15 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446)) - renamed function name to snake case closes #180 ([89d5186c](https://github.com/rust-lang/rustlings/commit/89d5186c0dae8135ecabf90ee8bb35949bc2d29b)) -#### Features +### Features - Add enums exercises ([dc150321](https://github.com/rust-lang/rustlings/commit/dc15032112fc485226a573a18139e5ce928b1755)) - Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959)) - **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031)) -### 1.4.1 (2019-08-13) +## 1.4.1 (2019-08-13) -#### Bug Fixes +### Bug Fixes - **iterators2:** Remove syntax resulting in misleading error message ([4cde8664](https://github.com/rust-lang/rustlings/commit/4cde86643e12db162a66e62f23b78962986046ac)) - **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6)) @@ -913,7 +915,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 1.4.0 (2019-07-13) -#### Bug Fixes +### Bug Fixes - **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6)) - **iterators:** Rename iterator3.rs ([433d2115](https://github.com/rust-lang/rustlings/commit/433d2115bc1c04b6d34a335a18c9a8f3e2672bc6)) @@ -922,18 +924,18 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **cli:** Check if changed exercise file exists before calling verify ([ba85ca3](https://github.com/rust-lang/rustlings/commit/ba85ca32c4cfc61de46851ab89f9c58a28f33c88)) - **structs1:** Fix the irrefutable let pattern warning ([cc6a141](https://github.com/rust-lang/rustlings/commit/cc6a14104d7c034eadc98297eaaa972d09c50b1f)) -#### Features +### Features - **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724)) - **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031)) -### 1.3.0 (2019-06-05) +## 1.3.0 (2019-06-05) -#### Features +### Features - Adds a simple exercise for structures (#163, @briankung) -#### Bug Fixes +### Bug Fixes - Add Result type signature as it is difficult for new comers to understand Generics and Error all at once. (#157, @veggiemonk) - Rustfmt and whitespace fixes (#161, @eddyp) @@ -942,29 +944,29 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Fix broken link (#164, @HanKruiger) - Remove highlighting and syntect (#167, @komaeda) -### 1.2.2 (2019-05-07) +## 1.2.2 (2019-05-07) -#### Bug Fixes +### Bug Fixes - Reverted `--nocapture` flag since it was causing tests to pass unconditionally -### 1.2.1 (2019-04-22) +## 1.2.1 (2019-04-22) -#### Bug Fixes +### Bug Fixes - Fix the `--nocapture` feature (@komaeda) - Provide a nicer error message for when you're in the wrong directory -### 1.2.0 (2019-04-22) +## 1.2.0 (2019-04-22) -#### Features +### Features - Add errors to exercises that compile without user changes (@yvan-sraka) - Use --nocapture when testing, enabling `println!` when running (@komaeda) -### 1.1.1 (2019-04-14) +## 1.1.1 (2019-04-14) -#### Bug fixes +### Bug fixes - Fix permissions on exercise files (@zacanger, #133) - Make installation checks more thorough (@komaeda, 1b3469f236bc6979c27f6e1a04e4138a88e55de3) @@ -974,7 +976,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Fix links by deleting book version (@diodfr, #142) - Canonicalize paths to fix path matching (@cjpearce, #143) -### 1.1.0 (2019-03-20) +## 1.1.0 (2019-03-20) - errors2.rs: update link to Rust book (#124) - Start verification at most recently modified file (#120) @@ -983,12 +985,12 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Give a warning when Rustlings isn't run from the right directory (#123) - Verify that rust version is recent enough to install Rustlings (#131) -### 1.0.1 (2019-03-06) +## 1.0.1 (2019-03-06) - Adds a way to install Rustlings in one command (`curl -L https://git.io/rustlings | bash`) - Makes `rustlings watch` react to create file events (@shaunbennett, #117) - Reworks the exercise management to use an external TOML file instead of just listing them in the code -### 1.0.0 (2019-03-06) +## 1.0.0 (2019-03-06) Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95605f70..7b684d39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ I want to … ## Issues -You can open an issue [here](https://github.com/rust-lang/rustlings/issues/new). +You can [open an issue](https://github.com/rust-lang/rustlings/issues/new). If you're reporting a bug, please include the output of the following commands: - `cargo --version` From 7d53dc4c95d6b27761f157f00b958c120104f066 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 30 Mar 2026 17:35:45 +0200 Subject: [PATCH 35/62] Update deps --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a65ecff7..4c2d6d75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" [[package]] name = "foldhash" @@ -236,9 +236,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -306,9 +306,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "linux-raw-sys" @@ -345,9 +345,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -515,9 +515,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -564,9 +564,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -655,27 +655,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "unicode-ident" @@ -885,9 +885,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" [[package]] name = "wit-bindgen" From 7ed231604099c347823c53c3874a8bdba59ca87a Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 15:45:31 +0200 Subject: [PATCH 36/62] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f326b60..8b9b356b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,14 @@ ## Unreleased +### Fixed + +- Fix integer overflow on big terminal widths [@gabfec](https://github.com/gabfec) +- Fix workspace detection on Windows [@senekor](https://github.com/senekor) + ### Changed +- Avoid initializing a nested Git repository [@senekor](https://github.com/senekor) - `vecs2`: Removed the use of `map` and `collect`, which are only taught later. ## 6.5.0 (2025-08-21) From c466d01da938029e12d14514e47deb17c4b4e726 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 17:19:11 +0200 Subject: [PATCH 37/62] Unify imports --- src/cli.rs | 41 +++++++++++++++++++++++++ src/dev.rs | 4 +-- src/list.rs | 7 +++-- src/list/state.rs | 3 +- src/main.rs | 61 +++++++++---------------------------- src/watch.rs | 3 +- src/watch/notify_event.rs | 2 +- src/watch/state.rs | 3 +- src/watch/terminal_event.rs | 2 +- 9 files changed, 66 insertions(+), 60 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..2bea5544 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,41 @@ +use clap::{Parser, Subcommand}; + +use crate::dev::DevCommand; + +/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code +#[derive(Parser)] +#[command(version)] +pub struct Args { + #[command(subcommand)] + pub command: Option, + /// Manually run the current exercise using `r` in the watch mode. + /// Only use this if Rustlings fails to detect exercise file changes. + #[arg(long)] + pub manual_run: bool, +} + +#[derive(Subcommand)] +pub enum Command { + /// Initialize the official Rustlings exercises + Init, + /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified + Run { + /// The name of the exercise + name: Option, + }, + /// Check all the exercises, marking them as done or pending accordingly. + CheckAll, + /// Reset a single exercise + Reset { + /// The name of the exercise + name: String, + }, + /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified + Hint { + /// The name of the exercise + name: Option, + }, + /// Commands for developing (community) Rustlings exercises + #[command(subcommand)] + Dev(DevCommand), +} diff --git a/src/dev.rs b/src/dev.rs index 41fddbeb..f2be6066 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -7,7 +7,7 @@ mod new; mod update; #[derive(Subcommand)] -pub enum DevCommands { +pub enum DevCommand { /// Create a new project for community exercises New { /// The path to create the project in @@ -26,7 +26,7 @@ pub enum DevCommands { Update, } -impl DevCommands { +impl DevCommand { pub fn run(self) -> Result<()> { match self { Self::New { path, no_git } => { diff --git a/src/list.rs b/src/list.rs index a2eee9e1..c60a5299 100644 --- a/src/list.rs +++ b/src/list.rs @@ -11,9 +11,10 @@ use crossterm::{ }; use std::io::{self, StdoutLock, Write}; -use crate::app_state::AppState; - -use self::state::{Filter, ListState}; +use crate::{ + app_state::AppState, + list::state::{Filter, ListState}, +}; mod scroll_state; mod state; diff --git a/src/list/state.rs b/src/list/state.rs index 58aa4961..4e097613 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -15,11 +15,10 @@ use std::{ use crate::{ app_state::AppState, exercise::Exercise, + list::scroll_state::ScrollState, term::{CountedWrite, MaxLenWriter, progress_bar}, }; -use super::scroll_state::ScrollState; - const COL_SPACING: usize = 2; const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Reverse) diff --git a/src/main.rs b/src/main.rs index c39e8629..8f31703b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, bail}; use app_state::StateFileStatus; -use clap::{Parser, Subcommand}; +use clap::Parser; use std::{ io::{self, IsTerminal, Write}, path::Path, @@ -8,10 +8,15 @@ use std::{ }; use term::{clear_terminal, press_enter_prompt}; -use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile}; +use crate::{ + app_state::AppState, + cli::{Args, Command}, + info_file::InfoFile, +}; mod app_state; mod cargo_toml; +mod cli; mod cmd; mod dev; mod embedded; @@ -25,44 +30,6 @@ mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; -/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code -#[derive(Parser)] -#[command(version)] -struct Args { - #[command(subcommand)] - command: Option, - /// Manually run the current exercise using `r` in the watch mode. - /// Only use this if Rustlings fails to detect exercise file changes. - #[arg(long)] - manual_run: bool, -} - -#[derive(Subcommand)] -enum Subcommands { - /// Initialize the official Rustlings exercises - Init, - /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified - Run { - /// The name of the exercise - name: Option, - }, - /// Check all the exercises, marking them as done or pending accordingly. - CheckAll, - /// Reset a single exercise - Reset { - /// The name of the exercise - name: String, - }, - /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified - Hint { - /// The name of the exercise - name: Option, - }, - /// Commands for developing (community) Rustlings exercises - #[command(subcommand)] - Dev(DevCommands), -} - fn main() -> Result { let args = Args::parse(); @@ -72,8 +39,8 @@ fn main() -> Result { 'priority_cmd: { match args.command { - Some(Subcommands::Init) => init::init().context("Initialization failed")?, - Some(Subcommands::Dev(dev_command)) => dev_command.run()?, + Some(Command::Init) => init::init().context("Initialization failed")?, + Some(Command::Dev(dev_command)) => dev_command.run()?, _ => break 'priority_cmd, } @@ -141,13 +108,13 @@ fn main() -> Result { watch::watch(&mut app_state, notify_exercise_names)?; } - Some(Subcommands::Run { name }) => { + Some(Command::Run { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } return run::run(&mut app_state); } - Some(Subcommands::CheckAll) => { + Some(Command::CheckAll) => { let mut stdout = io::stdout().lock(); if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? { if app_state.current_exercise().done { @@ -175,19 +142,19 @@ fn main() -> Result { app_state.render_final_message(&mut stdout)?; } - Some(Subcommands::Reset { name }) => { + Some(Command::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; let exercise_path = app_state.reset_current_exercise()?; println!("The exercise {exercise_path} has been reset"); } - Some(Subcommands::Hint { name }) => { + Some(Command::Hint { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } println!("{}", app_state.current_exercise().hint); } // Handled in an earlier match. - Some(Subcommands::Init | Subcommands::Dev(_)) => (), + Some(Command::Init | Command::Dev(_)) => (), } Ok(ExitCode::SUCCESS) diff --git a/src/watch.rs b/src/watch.rs index 3a56b4b6..e0b5ccd0 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -13,10 +13,9 @@ use std::{ use crate::{ app_state::{AppState, ExercisesProgress}, list, + watch::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}, }; -use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}; - mod notify_event; mod state; mod terminal_event; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 9c05f10d..edd9c720 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -12,7 +12,7 @@ use std::{ time::Duration, }; -use super::{EXERCISE_RUNNING, WatchEvent}; +use crate::watch::{EXERCISE_RUNNING, WatchEvent}; const DEBOUNCE_DURATION: Duration = Duration::from_millis(200); diff --git a/src/watch/state.rs b/src/watch/state.rs index f93ea0cf..1b285d67 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -17,10 +17,9 @@ use crate::{ clear_terminal, exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line}, term::progress_bar, + watch::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler}, }; -use super::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler}; - const HEADING_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Bold) .with(Attribute::Underlined); diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 439e4730..4f0685b6 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -4,7 +4,7 @@ use std::sync::{ mpsc::{Receiver, Sender}, }; -use super::{EXERCISE_RUNNING, WatchEvent}; +use crate::watch::{EXERCISE_RUNNING, WatchEvent}; pub enum InputEvent { Next, From 95b6160b54120515bc538c0c88b40beb0552ab29 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 17:28:48 +0200 Subject: [PATCH 38/62] Don't manually inline --- src/app_state.rs | 7 ------- src/cmd.rs | 2 -- src/exercise.rs | 5 ----- src/info_file.rs | 5 ----- src/list/scroll_state.rs | 4 ---- src/list/state.rs | 5 ----- src/term.rs | 6 ------ src/watch.rs | 2 -- tests/integration_tests.rs | 5 ----- 9 files changed, 41 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 5722e607..411afe36 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -180,37 +180,30 @@ impl AppState { Ok((slf, state_file_status)) } - #[inline] pub fn current_exercise_ind(&self) -> usize { self.current_exercise_ind } - #[inline] pub fn exercises(&self) -> &[Exercise] { &self.exercises } - #[inline] pub fn n_done(&self) -> u32 { self.n_done } - #[inline] pub fn n_pending(&self) -> u32 { self.exercises.len() as u32 - self.n_done } - #[inline] pub fn current_exercise(&self) -> &Exercise { &self.exercises[self.current_exercise_ind] } - #[inline] pub fn cmd_runner(&self) -> &CmdRunner { &self.cmd_runner } - #[inline] pub fn emit_file_links(&self) -> bool { self.emit_file_links } diff --git a/src/cmd.rs b/src/cmd.rs index b2c58f6a..6442e449 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -126,7 +126,6 @@ pub struct CargoSubcommand<'out> { } impl CargoSubcommand<'_> { - #[inline] pub fn args<'arg, I>(&mut self, args: I) -> &mut Self where I: IntoIterator, @@ -136,7 +135,6 @@ impl CargoSubcommand<'_> { } /// The boolean in the returned `Result` is true if the command's exit status is success. - #[inline] pub fn run(self, description: &str) -> Result { run_cmd(self.cmd, description, self.output) } diff --git a/src/exercise.rs b/src/exercise.rs index c07a94e8..987428e6 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -158,7 +158,6 @@ pub trait RunnableExercise { /// Compile, check and run the exercise. /// The output is written to the `output` buffer after clearing it. - #[inline] fn run_exercise(&self, output: Option<&mut Vec>, cmd_runner: &CmdRunner) -> Result { self.run::(self.name(), output, cmd_runner) } @@ -201,22 +200,18 @@ pub trait RunnableExercise { } impl RunnableExercise for Exercise { - #[inline] fn name(&self) -> &str { self.name } - #[inline] fn dir(&self) -> Option<&str> { self.dir } - #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy } - #[inline] fn test(&self) -> bool { self.test } diff --git a/src/info_file.rs b/src/info_file.rs index 54a21a5c..26bb1a2c 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -23,7 +23,6 @@ pub struct ExerciseInfo { #[serde(default)] pub skip_check_unsolved: bool, } -#[inline] const fn default_true() -> bool { true } @@ -55,22 +54,18 @@ impl ExerciseInfo { } impl RunnableExercise for ExerciseInfo { - #[inline] fn name(&self) -> &str { self.name } - #[inline] fn dir(&self) -> Option<&str> { self.dir } - #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy } - #[inline] fn test(&self) -> bool { self.test } diff --git a/src/list/scroll_state.rs b/src/list/scroll_state.rs index 2c02ed4f..299db568 100644 --- a/src/list/scroll_state.rs +++ b/src/list/scroll_state.rs @@ -19,7 +19,6 @@ impl ScrollState { } } - #[inline] pub fn offset(&self) -> usize { self.offset } @@ -41,7 +40,6 @@ impl ScrollState { .min(global_max_offset); } - #[inline] pub fn selected(&self) -> Option { self.selected } @@ -86,12 +84,10 @@ impl ScrollState { self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1))); } - #[inline] fn update_scroll_padding(&mut self) { self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding); } - #[inline] pub fn max_n_rows_to_display(&self) -> usize { self.max_n_rows_to_display } diff --git a/src/list/state.rs b/src/list/state.rs index 4e097613..4fcbd3c3 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -303,7 +303,6 @@ impl<'a> ListState<'a> { self.scroll_state.set_n_rows(n_rows); } - #[inline] pub fn filter(&self) -> Filter { self.filter } @@ -313,22 +312,18 @@ impl<'a> ListState<'a> { self.update_rows(); } - #[inline] pub fn select_next(&mut self) { self.scroll_state.select_next(); } - #[inline] pub fn select_previous(&mut self) { self.scroll_state.select_previous(); } - #[inline] pub fn select_first(&mut self) { self.scroll_state.select_first(); } - #[inline] pub fn select_last(&mut self) { self.scroll_state.select_last(); } diff --git a/src/term.rs b/src/term.rs index 8cab5005..2467b450 100644 --- a/src/term.rs +++ b/src/term.rs @@ -18,7 +18,6 @@ pub struct MaxLenWriter<'a, 'lock> { } impl<'a, 'lock> MaxLenWriter<'a, 'lock> { - #[inline] pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self { Self { stdout, @@ -28,7 +27,6 @@ impl<'a, 'lock> MaxLenWriter<'a, 'lock> { } // Additional is for emojis that take more space. - #[inline] pub fn add_to_len(&mut self, additional: usize) { self.len += additional; } @@ -64,24 +62,20 @@ impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> { Ok(()) } - #[inline] fn stdout(&mut self) -> &mut StdoutLock<'lock> { self.stdout } } impl<'a> CountedWrite<'a> for StdoutLock<'a> { - #[inline] fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> { self.write_all(ascii) } - #[inline] fn write_str(&mut self, unicode: &str) -> io::Result<()> { self.write_all(unicode.as_bytes()) } - #[inline] fn stdout(&mut self) -> &mut StdoutLock<'a> { self } diff --git a/src/watch.rs b/src/watch.rs index e0b5ccd0..ab96893e 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -27,7 +27,6 @@ static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false); pub struct InputPauseGuard(()); impl InputPauseGuard { - #[inline] pub fn scoped_pause() -> Self { EXERCISE_RUNNING.store(true, Relaxed); Self(()) @@ -35,7 +34,6 @@ impl InputPauseGuard { } impl Drop for InputPauseGuard { - #[inline] fn drop(&mut self) { EXERCISE_RUNNING.store(false, Relaxed); } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 91d0536e..bb1e398c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -19,19 +19,16 @@ struct Cmd<'a> { } impl<'a> Cmd<'a> { - #[inline] fn current_dir(&mut self, current_dir: &'a str) -> &mut Self { self.current_dir = Some(current_dir); self } - #[inline] fn args(&mut self, args: &'a [&'a str]) -> &mut Self { self.args = args; self } - #[inline] fn output(&mut self, output: Output<'a>) -> &mut Self { self.output = Some(output); self @@ -70,13 +67,11 @@ impl<'a> Cmd<'a> { } } - #[inline] #[track_caller] fn success(&self) { self.assert(true); } - #[inline] #[track_caller] fn fail(&self) { self.assert(false); From 7c1d8ebf49f5dcfe4c6bee0fd881a6922cbc0395 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 25 Sep 2025 17:18:26 +0200 Subject: [PATCH 39/62] Make users type method syntax themselves in `structs3` closes #2286 --- CHANGELOG.md | 1 + exercises/07_structs/structs3.rs | 99 +++++++++++--------------------- rustlings-macros/info.toml | 9 ++- solutions/07_structs/structs3.rs | 89 ++++++++++------------------ 4 files changed, 69 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f326b60..61491446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - `vecs2`: Removed the use of `map` and `collect`, which are only taught later. +- `structs3`: Rewrote the exercise to make users type method syntax themselves. ## 6.5.0 (2025-08-21) diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 69e5ced7..b5457de6 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -1,37 +1,33 @@ // Structs contain data, but can also have logic. In this exercise, we have -// defined the `Package` struct, and we want to test some logic attached to it. +// defined the `Fireworks` struct and a couple of functions that work with it. +// Turn these free-standing functions into methods and associated functions +// to express that relationship more clearly in the code. + +#![deny(clippy::use_self)] // practice using the `Self` type #[derive(Debug)] -struct Package { - sender_country: String, - recipient_country: String, - weight_in_grams: u32, +struct Fireworks { + rockets: usize, } -impl Package { - fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { - if weight_in_grams < 10 { - // This isn't how you should handle errors in Rust, but we will - // learn about error handling later. - panic!("Can't ship a package with weight below 10 grams"); - } +// TODO: Turn this function into an associated function on `Fireworks`. +fn new_fireworks() -> Fireworks { + Fireworks { rockets: 0 } +} - Self { - sender_country, - recipient_country, - weight_in_grams, - } - } +// TODO: Turn this function into a method on `Fireworks`. +fn add_rockets(fireworks: &mut Fireworks, rockets: usize) { + fireworks.rockets += rockets +} - // TODO: Add the correct return type to the function signature. - fn is_international(&self) { - // TODO: Read the tests that use this method to find out when a package - // is considered international. - } - - // TODO: Add the correct return type to the function signature. - fn get_fees(&self, cents_per_gram: u32) { - // TODO: Calculate the package's fees. +// TODO: Turn this function into a method on `Fireworks`. +fn start(fireworks: Fireworks) -> String { + if fireworks.rockets < 5 { + String::from("small") + } else if fireworks.rockets < 20 { + String::from("medium") + } else { + String::from("big") } } @@ -44,44 +40,19 @@ mod tests { use super::*; #[test] - #[should_panic] - fn fail_creating_weightless_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Austria"); + fn start_some_fireworks() { + let mut f = Fireworks::new(); + f.add_rockets(3); + assert_eq!(f.start(), "small"); - Package::new(sender_country, recipient_country, 5); - } + let mut f = Fireworks::new(); + f.add_rockets(15); + assert_eq!(f.start(), "medium"); - #[test] - fn create_international_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Russia"); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(package.is_international()); - } - - #[test] - fn create_local_package() { - let sender_country = String::from("Canada"); - let recipient_country = sender_country.clone(); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(!package.is_international()); - } - - #[test] - fn calculate_transport_fees() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Spain"); - - let cents_per_gram = 3; - - let package = Package::new(sender_country, recipient_country, 1500); - - assert_eq!(package.get_fees(cents_per_gram), 4500); - assert_eq!(package.get_fees(cents_per_gram * 2), 9000); + let mut f = Fireworks::new(); + f.add_rockets(100); + // We don't use method syntax in the last test to ensure the `start` + // function takes ownership of the fireworks. + assert_eq!(Fireworks::start(f), "big"); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index e42b0f26..353d405c 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -417,11 +417,10 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances- name = "structs3" dir = "07_structs" hint = """ -For `is_international`: What makes a package international? Seems related to -the places it goes through right? - -For `get_fees`: This method takes an additional argument, is there a field in -the `Package` struct that this relates to? +Methods and associated functions are both declared in an `impl MyType {}` +block. Methods have a `self`, `&self` or `&mut self` parameter, where `self` +implicitly has the type of the impl block. Associated functions do not have +a `self` parameter. Have a look in The Book to find out more about method implementations: https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs index 3f878cc8..a8928443 100644 --- a/solutions/07_structs/structs3.rs +++ b/solutions/07_structs/structs3.rs @@ -1,33 +1,27 @@ +#![deny(clippy::use_self)] // practice using the `Self` type + #[derive(Debug)] -struct Package { - sender_country: String, - recipient_country: String, - weight_in_grams: u32, +struct Fireworks { + rockets: usize, } -impl Package { - fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { - if weight_in_grams < 10 { - // This isn't how you should handle errors in Rust, but we will - // learn about error handling later. - panic!("Can't ship a package with weight below 10 grams"); - } - - Self { - sender_country, - recipient_country, - weight_in_grams, - } +impl Fireworks { + fn new() -> Self { + Self { rockets: 0 } } - fn is_international(&self) -> bool { - // ^^^^^^^ added - self.sender_country != self.recipient_country + fn add_rockets(&mut self, rockets: usize) { + self.rockets += rockets } - fn get_fees(&self, cents_per_gram: u32) -> u32 { - // ^^^^^^ added - self.weight_in_grams * cents_per_gram + fn start(self) -> String { + if self.rockets < 5 { + String::from("small") + } else if self.rockets < 20 { + String::from("medium") + } else { + String::from("big") + } } } @@ -40,44 +34,19 @@ mod tests { use super::*; #[test] - #[should_panic] - fn fail_creating_weightless_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Austria"); + fn start_some_fireworks() { + let mut f = Fireworks::new(); + f.add_rockets(3); + assert_eq!(f.start(), "small"); - Package::new(sender_country, recipient_country, 5); - } + let mut f = Fireworks::new(); + f.add_rockets(15); + assert_eq!(f.start(), "medium"); - #[test] - fn create_international_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Russia"); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(package.is_international()); - } - - #[test] - fn create_local_package() { - let sender_country = String::from("Canada"); - let recipient_country = sender_country.clone(); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(!package.is_international()); - } - - #[test] - fn calculate_transport_fees() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Spain"); - - let cents_per_gram = 3; - - let package = Package::new(sender_country, recipient_country, 1500); - - assert_eq!(package.get_fees(cents_per_gram), 4500); - assert_eq!(package.get_fees(cents_per_gram * 2), 9000); + let mut f = Fireworks::new(); + f.add_rockets(100); + // We don't use method syntax in the last test to ensure the `start` + // function takes ownership of the fireworks. + assert_eq!(Fireworks::start(f), "big"); } } From 4d97c31c0f748acda001e6f0f9c63261ec1d7afe Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 18:17:10 +0200 Subject: [PATCH 40/62] Add Zellij support --- src/app_state.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++- src/cli.rs | 13 +++-- src/main.rs | 1 + src/watch/state.rs | 11 ++-- tmp.txt | 1 + 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 tmp.txt diff --git a/src/app_state.rs b/src/app_state.rs index 411afe36..31053f73 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Error, Result, bail}; use crossterm::{QueueableCommand, cursor, terminal}; +use serde::Deserialize; use std::{ collections::HashSet, env, @@ -11,7 +12,7 @@ use std::{ atomic::{AtomicUsize, Ordering::Relaxed}, mpsc, }, - thread, + thread::{self, JoinHandle}, }; use crate::{ @@ -49,6 +50,44 @@ pub enum CheckProgress { Pending, } +#[derive(Deserialize)] +struct Pane { + id: u32, +} + +#[must_use] +pub struct EditCmdJoinHandle(Option>>); + +fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { + // Remove newline + let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; + let id_str = str::from_utf8(b).ok()?; + + let (first, rest) = b.split_first()?; + let mut id = u32::from(first - b'0'); + + for c in rest { + id = 10 * id + u32::from(c - b'0'); + } + + Some((id_str.to_owned(), id)) +} + +fn close_pane(pane_id: &str) -> Result<()> { + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `zellij action close-pane -p ID`")?; + + Ok(()) +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -61,12 +100,15 @@ pub struct AppState { official_exercises: bool, cmd_runner: CmdRunner, emit_file_links: bool, + zellij: bool, + open_pane: Option<(String, u32, usize)>, } impl AppState { pub fn new( exercise_infos: Vec, final_message: &'static str, + zellij: bool, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -175,6 +217,8 @@ impl AppState { cmd_runner, // VS Code has its own file link handling emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"), + zellij, + open_pane: None, }; Ok((slf, state_file_status)) @@ -553,6 +597,86 @@ impl AppState { Ok(()) } + + pub fn close_pane(&mut self) -> Result<()> { + if let Some((pane_id_str, _, _)) = self.open_pane.take() { + close_pane(&pane_id_str)?; + } + + Ok(()) + } + + pub fn edit_cmd(&mut self) -> Result { + if !self.zellij { + return Ok(EditCmdJoinHandle(None)); + } + + let open_pane = self.open_pane.take(); + let current_exercise_ind = self.current_exercise_ind; + let mut edit_cmd = Command::new("zellij"); + edit_cmd + .arg("action") + .arg("edit") + .arg(&self.current_exercise().path) + .stdin(Stdio::null()) + .stderr(Stdio::null()); + + let handle = thread::Builder::new() + .spawn(move || { + if let Some((pane_id_str, pane_id, exercise_ind)) = open_pane { + if exercise_ind == current_exercise_ind { + // Check if the pane is still open + let mut output = Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij action list-panes -j`")?; + + if !output.status.success() { + bail!("`zellij action list-panes -j` didn't exit successfully"); + } + + // Remove newline + output.stdout.pop(); + + let panes = serde_json::de::from_slice::>(&output.stdout) + .context( + "Failed to parse the output of `zellij action list-panes -j`", + )?; + + if panes.iter().any(|pane| pane.id == pane_id) { + return Ok((pane_id_str, pane_id)); + } + } else { + close_pane(&pane_id_str)?; + } + } + + let output = edit_cmd.output()?; + + if !output.status.success() { + bail!("Failed to open a new Zellij editor pane"); + } + + parse_pane_id(&output.stdout) + .context("Failed to parse the ID of the new Zellij pane") + }) + .context("Failed to spawn a thread to open and close Zellij panes")?; + + Ok(EditCmdJoinHandle(Some(handle))) + } + + pub fn join_edit_cmd(&mut self, handle: EditCmdJoinHandle) -> Result<()> { + if let Some(handle) = handle.0 { + let (pane_id_str, pane_id) = handle.join().unwrap()?; + self.open_pane = Some((pane_id_str, pane_id, self.current_exercise_ind)); + } + + Ok(()) + } } const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -608,6 +732,8 @@ mod tests { official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), emit_file_links: true, + zellij: false, + open_pane: None, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/cli.rs b/src/cli.rs index 2bea5544..dbc31f9c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,28 +9,33 @@ pub struct Args { #[command(subcommand)] pub command: Option, /// Manually run the current exercise using `r` in the watch mode. - /// Only use this if Rustlings fails to detect exercise file changes. + /// Only use this if Rustlings fails to detect exercise file changes #[arg(long)] pub manual_run: bool, + /// Open the current exercise in a new Zellij pane and close the last one if exists + #[arg(long)] + pub zellij: bool, } #[derive(Subcommand)] pub enum Command { /// Initialize the official Rustlings exercises Init, - /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified + /// Run a single exercise. + /// Runs the next pending exercise if the exercise name is not specified Run { /// The name of the exercise name: Option, }, - /// Check all the exercises, marking them as done or pending accordingly. + /// Check all the exercises, marking them as done or pending accordingly CheckAll, /// Reset a single exercise Reset { /// The name of the exercise name: String, }, - /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified + /// Show a hint. + /// Shows the hint of the next pending exercise if the exercise name is not specified Hint { /// The name of the exercise name: Option, diff --git a/src/main.rs b/src/main.rs index 8f31703b..29ec3d01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,7 @@ fn main() -> Result { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), + args.zellij, )?; // Show the welcome message if the state file doesn't exist yet. diff --git a/src/watch/state.rs b/src/watch/state.rs index 1b285d67..9e7d1ddc 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -78,14 +78,16 @@ impl<'a> WatchState<'a> { // Ignore any input until running the exercise is done. let _input_pause_guard = InputPauseGuard::scoped_pause(); - self.show_hint = false; - writeln!( stdout, "\nChecking the exercise `{}`. Please wait…", self.app_state.current_exercise().name, )?; + let edit_cmd_handle = self.app_state.edit_cmd()?; + + self.show_hint = false; + let success = self .app_state .current_exercise() @@ -105,7 +107,9 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } + self.app_state.join_edit_cmd(edit_cmd_handle)?; self.render(stdout)?; + Ok(()) } @@ -127,9 +131,10 @@ impl<'a> WatchState<'a> { match answer[0] { b'y' | b'Y' => { + self.app_state.close_pane()?; self.app_state.reset_current_exercise()?; - // The file watcher reruns the exercise otherwise. + // The file watcher reruns the exercise otherwise if self.manual_run { self.run_current_exercise(stdout)?; } diff --git a/tmp.txt b/tmp.txt new file mode 100644 index 00000000..3aefeefc --- /dev/null +++ b/tmp.txt @@ -0,0 +1 @@ +226.867688ms \ No newline at end of file From c9ccedcff6a40f6b77c1b608ef0d3942215138c9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 14:01:34 +0200 Subject: [PATCH 41/62] Support VSCode and --edit-cmd as editor --- src/app_state.rs | 150 ++++++++----------------------------------- src/cli.rs | 7 +- src/editor.rs | 137 +++++++++++++++++++++++++++++++++++++++ src/editor/zellij.rs | 62 ++++++++++++++++++ src/exercise.rs | 6 +- src/main.rs | 4 +- src/watch/state.rs | 6 +- 7 files changed, 238 insertions(+), 134 deletions(-) create mode 100644 src/editor.rs create mode 100644 src/editor/zellij.rs diff --git a/src/app_state.rs b/src/app_state.rs index 31053f73..bc1d520d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,9 +1,7 @@ use anyhow::{Context, Error, Result, bail}; use crossterm::{QueueableCommand, cursor, terminal}; -use serde::Deserialize; use std::{ collections::HashSet, - env, fs::{File, OpenOptions}, io::{Read, Seek, StdoutLock, Write}, path::{MAIN_SEPARATOR_STR, Path}, @@ -12,12 +10,13 @@ use std::{ atomic::{AtomicUsize, Ordering::Relaxed}, mpsc, }, - thread::{self, JoinHandle}, + thread, }; use crate::{ clear_terminal, cmd::CmdRunner, + editor::{Editor, EditorJoinHandle}, embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, @@ -50,44 +49,6 @@ pub enum CheckProgress { Pending, } -#[derive(Deserialize)] -struct Pane { - id: u32, -} - -#[must_use] -pub struct EditCmdJoinHandle(Option>>); - -fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { - // Remove newline - let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; - let id_str = str::from_utf8(b).ok()?; - - let (first, rest) = b.split_first()?; - let mut id = u32::from(first - b'0'); - - for c in rest { - id = 10 * id + u32::from(c - b'0'); - } - - Some((id_str.to_owned(), id)) -} - -fn close_pane(pane_id: &str) -> Result<()> { - Command::new("zellij") - .arg("action") - .arg("close-pane") - .arg("-p") - .arg(pane_id) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `zellij action close-pane -p ID`")?; - - Ok(()) -} - pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -100,15 +61,14 @@ pub struct AppState { official_exercises: bool, cmd_runner: CmdRunner, emit_file_links: bool, - zellij: bool, - open_pane: Option<(String, u32, usize)>, + editor: Option, } impl AppState { pub fn new( exercise_infos: Vec, final_message: &'static str, - zellij: bool, + editor: Option, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -150,7 +110,9 @@ impl AppState { Exercise { name: exercise_info.name, dir: exercise_info.dir, - path: exercise_info.path(), + // Leaking for `Editor::open`. + // Leaking is fine since the app state exists until the end of the program. + path: exercise_info.path().leak(), canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, @@ -216,9 +178,8 @@ impl AppState { official_exercises: !Path::new("info.toml").exists(), cmd_runner, // VS Code has its own file link handling - emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"), - zellij, - open_pane: None, + emit_file_links: !matches!(editor, Some(Editor::VSCode)), + editor, }; Ok((slf, state_file_status)) @@ -376,9 +337,9 @@ impl AppState { pub fn reset_current_exercise(&mut self) -> Result<&str> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, &exercise.path)?; + self.reset(self.current_exercise_ind, exercise.path)?; - Ok(&exercise.path) + Ok(exercise.path) } // Reset the exercise by index and return its name. @@ -389,7 +350,7 @@ impl AppState { self.set_pending(exercise_ind)?; let exercise = &self.exercises[exercise_ind]; - self.reset(exercise_ind, &exercise.path)?; + self.reset(exercise_ind, exercise.path)?; Ok(exercise.name) } @@ -598,81 +559,23 @@ impl AppState { Ok(()) } - pub fn close_pane(&mut self) -> Result<()> { - if let Some((pane_id_str, _, _)) = self.open_pane.take() { - close_pane(&pane_id_str)?; + pub fn open_editor(&mut self) -> Result { + if let Some(editor) = self.editor.take() { + return editor.open(self.current_exercise_ind, self.current_exercise().path); } + Ok(EditorJoinHandle::default()) + } + + pub fn join_editor_handle(&mut self, handle: EditorJoinHandle) -> Result<()> { + self.editor = handle.join()?; + Ok(()) } - pub fn edit_cmd(&mut self) -> Result { - if !self.zellij { - return Ok(EditCmdJoinHandle(None)); - } - - let open_pane = self.open_pane.take(); - let current_exercise_ind = self.current_exercise_ind; - let mut edit_cmd = Command::new("zellij"); - edit_cmd - .arg("action") - .arg("edit") - .arg(&self.current_exercise().path) - .stdin(Stdio::null()) - .stderr(Stdio::null()); - - let handle = thread::Builder::new() - .spawn(move || { - if let Some((pane_id_str, pane_id, exercise_ind)) = open_pane { - if exercise_ind == current_exercise_ind { - // Check if the pane is still open - let mut output = Command::new("zellij") - .arg("action") - .arg("list-panes") - .arg("-j") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij action list-panes -j`")?; - - if !output.status.success() { - bail!("`zellij action list-panes -j` didn't exit successfully"); - } - - // Remove newline - output.stdout.pop(); - - let panes = serde_json::de::from_slice::>(&output.stdout) - .context( - "Failed to parse the output of `zellij action list-panes -j`", - )?; - - if panes.iter().any(|pane| pane.id == pane_id) { - return Ok((pane_id_str, pane_id)); - } - } else { - close_pane(&pane_id_str)?; - } - } - - let output = edit_cmd.output()?; - - if !output.status.success() { - bail!("Failed to open a new Zellij editor pane"); - } - - parse_pane_id(&output.stdout) - .context("Failed to parse the ID of the new Zellij pane") - }) - .context("Failed to spawn a thread to open and close Zellij panes")?; - - Ok(EditCmdJoinHandle(Some(handle))) - } - - pub fn join_edit_cmd(&mut self, handle: EditCmdJoinHandle) -> Result<()> { - if let Some(handle) = handle.0 { - let (pane_id_str, pane_id) = handle.join().unwrap()?; - self.open_pane = Some((pane_id_str, pane_id, self.current_exercise_ind)); + pub fn close_editor(&mut self) -> Result<()> { + if let Some(editor) = &mut self.editor { + editor.close()?; } Ok(()) @@ -711,7 +614,7 @@ mod tests { Exercise { name: "0", dir: None, - path: String::from("exercises/0.rs"), + path: "exercises/0.rs", canonical_path: None, test: false, strict_clippy: false, @@ -732,8 +635,7 @@ mod tests { official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), emit_file_links: true, - zellij: false, - open_pane: None, + editor: None, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/cli.rs b/src/cli.rs index dbc31f9c..8bbe25aa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,13 +8,14 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, + /// Open the current exercise by running the provided `EDIT_CMD EXERCISE_NAME`. + /// Ignored in VS Code + #[arg(long)] + pub edit_cmd: Option, /// Manually run the current exercise using `r` in the watch mode. /// Only use this if Rustlings fails to detect exercise file changes #[arg(long)] pub manual_run: bool, - /// Open the current exercise in a new Zellij pane and close the last one if exists - #[arg(long)] - pub zellij: bool, } #[derive(Subcommand)] diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 00000000..be24e3ef --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,137 @@ +use std::{ + env, + process::{Command, Stdio}, + thread::{self, JoinHandle}, +}; + +use anyhow::{Context, Result, bail}; + +mod zellij; + +pub enum Editor { + VSCode, + Cmd(String, Vec), + Zellij(Option<(String, u32, usize)>), +} + +impl Editor { + pub fn new(cmd: Option) -> Option { + if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { + return Some(Self::VSCode); + } + + if let Some(cmd) = cmd { + todo!() + } + + if env::var_os("ZELLIJ").is_some() { + return Some(Self::Zellij(None)); + } + + None + } + + pub fn open( + self, + exercise_ind: usize, + exercise_path: &'static str, + ) -> Result { + let handle = thread::Builder::new() + .spawn(move || match self { + Editor::VSCode => { + if !Command::new("code") + .arg(exercise_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `code` to open the current exercise file")? + .success() + { + bail!("Failed to run `code PATH` to open the current exercise file"); + } + + Ok(Self::VSCode) + } + Editor::Cmd(program, args) => { + if !Command::new("code") + .arg(exercise_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run the command from `--edit-cmd`") + .is_ok_and(|status| status.success()) + { + bail!("Failed to run the command from `--edit-cmd`"); + } + + Ok(Self::Cmd(program, args)) + } + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { + if open_exercise_ind == exercise_ind { + if zellij::pane_open(pane_id)? { + return Ok(Self::Zellij(Some(( + pane_id_str, + pane_id, + exercise_ind, + )))); + } + } else { + zellij::close_pane(&pane_id_str)?; + } + } + + let output = Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij`")?; + + if !output.status.success() { + bail!("Failed to open a new Zellij editor pane"); + } + + let (pane_id_str, pane_id) = zellij::parse_pane_id(&output.stdout) + .context("Failed to parse the ID of the new Zellij pane")?; + + Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) + } + }) + .context("Failed to spawn a thread to open the editor")?; + + Ok(EditorJoinHandle(Some(handle))) + } + + pub fn close(&mut self) -> Result<()> { + match self { + Editor::VSCode | Editor::Cmd(_, _) => (), + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, _, _)) = open_pane.take() { + zellij::close_pane(&pane_id_str)?; + } + } + } + + Ok(()) + } +} + +#[must_use] +#[derive(Default)] +pub struct EditorJoinHandle(Option>>); + +impl EditorJoinHandle { + pub fn join(self) -> Result> { + if let Some(handle) = self.0 { + let editor = handle.join().unwrap()?; + return Ok(Some(editor)); + } + + Ok(None) + } +} diff --git a/src/editor/zellij.rs b/src/editor/zellij.rs new file mode 100644 index 00000000..ffa906dc --- /dev/null +++ b/src/editor/zellij.rs @@ -0,0 +1,62 @@ +use std::process::{Command, Stdio}; + +use anyhow::{Context, Result, bail}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Pane { + id: u32, +} + +pub fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { + // Remove newline + let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; + let id_str = str::from_utf8(b).ok()?; + + let (first, rest) = b.split_first()?; + let mut id = u32::from(first - b'0'); + + for c in rest { + id = 10 * id + u32::from(c - b'0'); + } + + Some((id_str.to_owned(), id)) +} + +pub fn pane_open(pane_id: u32) -> Result { + let mut output = Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij action list-panes -j`")?; + + if !output.status.success() { + bail!("`zellij action list-panes -j` didn't exit successfully"); + } + + // Remove newline + output.stdout.pop(); + + let panes = serde_json::de::from_slice::>(&output.stdout) + .context("Failed to parse the output of `zellij action list-panes -j`")?; + + Ok(panes.iter().any(|pane| pane.id == pane_id)) +} + +pub fn close_pane(pane_id: &str) -> Result<()> { + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `zellij action close-pane -p ID`")?; + + Ok(()) +} diff --git a/src/exercise.rs b/src/exercise.rs index 987428e6..b969c69a 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -69,7 +69,7 @@ pub struct Exercise { pub name: &'static str, pub dir: Option<&'static str>, /// Path of the exercise file starting with the `exercises/` directory. - pub path: String, + pub path: &'static str, pub canonical_path: Option, pub test: bool, pub strict_clippy: bool, @@ -85,9 +85,9 @@ impl Exercise { ) -> io::Result<()> { file_path(writer, Color::Blue, |writer| { if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { - terminal_file_link(writer, &self.path, canonical_path) + terminal_file_link(writer, self.path, canonical_path) } else { - writer.write_str(&self.path) + writer.write_str(self.path) } }) } diff --git a/src/main.rs b/src/main.rs index 29ec3d01..1d6c98b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use term::{clear_terminal, press_enter_prompt}; use crate::{ app_state::AppState, cli::{Args, Command}, + editor::Editor, info_file::InfoFile, }; @@ -19,6 +20,7 @@ mod cargo_toml; mod cli; mod cmd; mod dev; +mod editor; mod embedded; mod exercise; mod info_file; @@ -61,7 +63,7 @@ fn main() -> Result { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), - args.zellij, + Editor::new(args.edit_cmd), )?; // Show the welcome message if the state file doesn't exist yet. diff --git a/src/watch/state.rs b/src/watch/state.rs index 9e7d1ddc..8bbdc585 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -84,7 +84,7 @@ impl<'a> WatchState<'a> { self.app_state.current_exercise().name, )?; - let edit_cmd_handle = self.app_state.edit_cmd()?; + let editor_handle = self.app_state.open_editor()?; self.show_hint = false; @@ -107,7 +107,7 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } - self.app_state.join_edit_cmd(edit_cmd_handle)?; + self.app_state.join_editor_handle(editor_handle)?; self.render(stdout)?; Ok(()) @@ -131,7 +131,7 @@ impl<'a> WatchState<'a> { match answer[0] { b'y' | b'Y' => { - self.app_state.close_pane()?; + self.app_state.close_editor()?; self.app_state.reset_current_exercise()?; // The file watcher reruns the exercise otherwise From dace3e39534f31adc2ef95a036fc8aee1fbaf004 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 16:12:49 +0200 Subject: [PATCH 42/62] Add run_cmd --- src/editor.rs | 63 ++++++++++++++++++++------------------------ src/editor/zellij.rs | 45 +++++++++++++------------------ 2 files changed, 47 insertions(+), 61 deletions(-) diff --git a/src/editor.rs b/src/editor.rs index be24e3ef..af5c2fbe 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -8,6 +8,25 @@ use anyhow::{Context, Result, bail}; mod zellij; +fn run_cmd(cmd: &mut Command) -> Result> { + let output = cmd + .stdin(Stdio::null()) + .output() + .with_context(|| format!("Failed to run the command {cmd:?}"))?; + + if !output.status.success() { + bail!( + "The command {cmd:?} didn't run successfully\n\n\ + stdout:\n{}\n\n\ + stderr:\n{}", + str::from_utf8(&output.stdout).unwrap_or_default(), + str::from_utf8(&output.stderr).unwrap_or_default(), + ); + } + + Ok(output.stdout) +} + pub enum Editor { VSCode, Cmd(String, Vec), @@ -39,32 +58,12 @@ impl Editor { let handle = thread::Builder::new() .spawn(move || match self { Editor::VSCode => { - if !Command::new("code") - .arg(exercise_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `code` to open the current exercise file")? - .success() - { - bail!("Failed to run `code PATH` to open the current exercise file"); - } + run_cmd(Command::new("code").arg(exercise_path))?; Ok(Self::VSCode) } Editor::Cmd(program, args) => { - if !Command::new("code") - .arg(exercise_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run the command from `--edit-cmd`") - .is_ok_and(|status| status.success()) - { - bail!("Failed to run the command from `--edit-cmd`"); - } + run_cmd(Command::new(&program).args(&args).arg(exercise_path))?; Ok(Self::Cmd(program, args)) } @@ -83,20 +82,14 @@ impl Editor { } } - let output = Command::new("zellij") - .arg("action") - .arg("edit") - .arg(exercise_path) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij`")?; + let stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path), + )?; - if !output.status.success() { - bail!("Failed to open a new Zellij editor pane"); - } - - let (pane_id_str, pane_id) = zellij::parse_pane_id(&output.stdout) + let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) .context("Failed to parse the ID of the new Zellij pane")?; Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) diff --git a/src/editor/zellij.rs b/src/editor/zellij.rs index ffa906dc..b628a682 100644 --- a/src/editor/zellij.rs +++ b/src/editor/zellij.rs @@ -1,8 +1,10 @@ -use std::process::{Command, Stdio}; +use std::process::Command; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result}; use serde::Deserialize; +use crate::editor::run_cmd; + #[derive(Deserialize)] struct Pane { id: u32, @@ -24,39 +26,30 @@ pub fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { } pub fn pane_open(pane_id: u32) -> Result { - let mut output = Command::new("zellij") - .arg("action") - .arg("list-panes") - .arg("-j") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij action list-panes -j`")?; - - if !output.status.success() { - bail!("`zellij action list-panes -j` didn't exit successfully"); - } + let mut stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j"), + )?; // Remove newline - output.stdout.pop(); + stdout.pop(); - let panes = serde_json::de::from_slice::>(&output.stdout) + let panes = serde_json::de::from_slice::>(&stdout) .context("Failed to parse the output of `zellij action list-panes -j`")?; Ok(panes.iter().any(|pane| pane.id == pane_id)) } pub fn close_pane(pane_id: &str) -> Result<()> { - Command::new("zellij") - .arg("action") - .arg("close-pane") - .arg("-p") - .arg(pane_id) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `zellij action close-pane -p ID`")?; + run_cmd( + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id), + )?; Ok(()) } From b48663030b492abf335a3338b5c8bd00613a0bdd Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 16:55:10 +0200 Subject: [PATCH 43/62] Add shlex --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/editor.rs | 17 ++++++++++++----- src/main.rs | 3 ++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c2d6d75..b8db4360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,6 +485,7 @@ dependencies = [ "rustlings-macros", "serde", "serde_json", + "shlex", "tempfile", "toml", ] @@ -571,6 +572,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" diff --git a/Cargo.toml b/Cargo.toml index 068a3f49..192eeb61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ notify = "8" rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" } serde_json = "1" serde.workspace = true +shlex = "1" toml.workspace = true [target.'cfg(not(windows))'.dependencies] diff --git a/src/editor.rs b/src/editor.rs index af5c2fbe..6e712605 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -5,6 +5,7 @@ use std::{ }; use anyhow::{Context, Result, bail}; +use shlex::Shlex; mod zellij; @@ -34,20 +35,26 @@ pub enum Editor { } impl Editor { - pub fn new(cmd: Option) -> Option { + pub fn new(cmd: Option) -> Result> { if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { - return Some(Self::VSCode); + return Ok(Some(Self::VSCode)); } if let Some(cmd) = cmd { - todo!() + let shlex = &mut Shlex::new(&cmd); + let program = shlex.next().context("Program missing in `--edit-cmd`")?; + let args = shlex.collect(); + if shlex.had_error { + bail!("Failed to parse the command in `--edit-cmd`"); + } + return Ok(Some(Self::Cmd(program, args))); } if env::var_os("ZELLIJ").is_some() { - return Some(Self::Zellij(None)); + return Ok(Some(Self::Zellij(None))); } - None + Ok(None) } pub fn open( diff --git a/src/main.rs b/src/main.rs index 1d6c98b1..7cd5c80b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,10 +60,11 @@ fn main() -> Result { bail!(FORMAT_VERSION_HIGHER_ERR); } + let editor = Editor::new(args.edit_cmd)?; let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), - Editor::new(args.edit_cmd), + editor, )?; // Show the welcome message if the state file doesn't exist yet. From b0dc0140406ef00ce3eb41d0a9adb8fca8f1539b Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 17:24:43 +0200 Subject: [PATCH 44/62] Improve description of --edit-cmd --- src/cli.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8bbe25aa..5830cbed 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,8 +8,14 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, - /// Open the current exercise by running the provided `EDIT_CMD EXERCISE_NAME`. - /// Ignored in VS Code + /// Open the current exercise by running `EDIT_CMD EXERCISE_PATH`. + /// The command is not allowed to block (e.g. `vim`). + /// It should communicate with an editor in a different process. + /// `EDIT_CMD` can contain arguments like `--edit-cmd "PROGRAM -x --arg1"`. + /// The current exercise's path is added by Rustlings as the last argument. + /// `--edit-cmd` is ignored in VS Code. + /// + /// Example: `--edit-cmd "code"` (default behavior if running in a VS Code terminal) #[arg(long)] pub edit_cmd: Option, /// Manually run the current exercise using `r` in the watch mode. From bc0b4e9f9a716e577e80667b2b1521b55724d71b Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 17:27:35 +0200 Subject: [PATCH 45/62] Simplify Editor::open --- src/editor.rs | 64 ++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/editor.rs b/src/editor.rs index 6e712605..3f36e266 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -58,49 +58,45 @@ impl Editor { } pub fn open( - self, + mut self, exercise_ind: usize, exercise_path: &'static str, ) -> Result { let handle = thread::Builder::new() - .spawn(move || match self { - Editor::VSCode => { - run_cmd(Command::new("code").arg(exercise_path))?; - - Ok(Self::VSCode) - } - Editor::Cmd(program, args) => { - run_cmd(Command::new(&program).args(&args).arg(exercise_path))?; - - Ok(Self::Cmd(program, args)) - } - Editor::Zellij(open_pane) => { - if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { - if open_exercise_ind == exercise_ind { - if zellij::pane_open(pane_id)? { - return Ok(Self::Zellij(Some(( - pane_id_str, - pane_id, - exercise_ind, - )))); - } - } else { - zellij::close_pane(&pane_id_str)?; - } + .spawn(move || { + match &mut self { + Editor::VSCode => { + run_cmd(Command::new("code").arg(exercise_path))?; } + Editor::Cmd(program, args) => { + run_cmd(Command::new(program).args(args).arg(exercise_path))?; + } + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { + if *open_exercise_ind == exercise_ind { + if zellij::pane_open(*pane_id)? { + return Ok(self); + } + } else { + zellij::close_pane(pane_id_str)?; + } + } - let stdout = run_cmd( - Command::new("zellij") - .arg("action") - .arg("edit") - .arg(exercise_path), - )?; + let stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path), + )?; - let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) - .context("Failed to parse the ID of the new Zellij pane")?; + let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) + .context("Failed to parse the ID of the new Zellij pane")?; - Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) + *open_pane = Some((pane_id_str, pane_id, exercise_ind)); + } } + + Ok(self) }) .context("Failed to spawn a thread to open the editor")?; From 95499f18dd7cb0811dd304acd051e493fa8661d6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:11:13 +0200 Subject: [PATCH 46/62] Close editor on quit --- src/main.rs | 1 + src/watch.rs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7cd5c80b..652d1468 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,6 +111,7 @@ fn main() -> Result { }; watch::watch(&mut app_state, notify_exercise_names)?; + app_state.close_editor()?; } Some(Command::Run { name }) => { if let Some(name) = name { diff --git a/src/watch.rs b/src/watch.rs index ab96893e..857ccfde 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -172,8 +172,7 @@ pub fn watch( watch_list_loop(app_state, notify_exercise_names) } -const QUIT_MSG: &[u8] = b" - +const QUIT_MSG: &[u8] = b"q\n We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory. "; From f403d9e1b68b601a5b6e9e57201f767b134e2e67 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:21:15 +0200 Subject: [PATCH 47/62] Show current exercise on hint command --- CHANGELOG.md | 4 ++++ src/main.rs | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9b356b..b85a3e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Show the file link of the current exercise when running `rustlings hint` + ### Fixed - Fix integer overflow on big terminal widths [@gabfec](https://github.com/gabfec) diff --git a/src/main.rs b/src/main.rs index 652d1468..564e0719 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,15 @@ fn main() -> Result { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } - println!("{}", app_state.current_exercise().hint); + + let current_exercise = app_state.current_exercise(); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"Current exercise: ")?; + current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?; + + stdout.write_all(b"\n\nHint:\n")?; + stdout.write_all(current_exercise.hint.as_bytes())?; + stdout.write_all(b"\n")?; } // Handled in an earlier match. Some(Command::Init | Command::Dev(_)) => (), From 695f927893ae4fc1f1aaa629e2feb62a9cdcc78d Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:26:36 +0200 Subject: [PATCH 48/62] Show file link on reset command --- CHANGELOG.md | 2 +- src/app_state.rs | 6 ++---- src/main.rs | 9 +++++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b85a3e0b..ec74cf10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Show the file link of the current exercise when running `rustlings hint` +- Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` ### Fixed diff --git a/src/app_state.rs b/src/app_state.rs index bc1d520d..9980aeea 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -334,12 +334,10 @@ impl AppState { Ok(()) } - pub fn reset_current_exercise(&mut self) -> Result<&str> { + pub fn reset_current_exercise(&mut self) -> Result<()> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, exercise.path)?; - - Ok(exercise.path) + self.reset(self.current_exercise_ind, exercise.path) } // Reset the exercise by index and return its name. diff --git a/src/main.rs b/src/main.rs index 564e0719..fb976653 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,8 +149,13 @@ fn main() -> Result { } Some(Command::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; - let exercise_path = app_state.reset_current_exercise()?; - println!("The exercise {exercise_path} has been reset"); + app_state.reset_current_exercise()?; + + let current_exercise = app_state.current_exercise(); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"The exercise ")?; + current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?; + stdout.write_all(b" has been reset\n")?; } Some(Command::Hint { name }) => { if let Some(name) = name { From b5fbf59c0c79a78e06d0fffd9db86abf0774e0f6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:55:30 +0200 Subject: [PATCH 49/62] Check if editor program exists before choosing it --- CHANGELOG.md | 3 +++ src/app_state.rs | 3 ++- src/editor.rs | 35 +++++++++++++++++++++++------------ src/main.rs | 5 ++++- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec74cf10..0d2f003f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added +- Automatically open the current file if Rustlings is running in a VS Code terminal +- Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) +- New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise - Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` ### Fixed diff --git a/src/app_state.rs b/src/app_state.rs index 9980aeea..78b9c205 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -69,6 +69,7 @@ impl AppState { exercise_infos: Vec, final_message: &'static str, editor: Option, + vs_code_term: bool, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -178,7 +179,7 @@ impl AppState { official_exercises: !Path::new("info.toml").exists(), cmd_runner, // VS Code has its own file link handling - emit_file_links: !matches!(editor, Some(Editor::VSCode)), + emit_file_links: !vs_code_term, editor, }; diff --git a/src/editor.rs b/src/editor.rs index 3f36e266..3c189c78 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, env, process::{Command, Stdio}, thread::{self, JoinHandle}, @@ -28,16 +29,29 @@ fn run_cmd(cmd: &mut Command) -> Result> { Ok(output.stdout) } +fn program_exists(program: &str) -> bool { + Command::new(program) + .arg("--version") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()) +} + pub enum Editor { - VSCode, - Cmd(String, Vec), + Cmd(Cow<'static, str>, Vec), Zellij(Option<(String, u32, usize)>), } impl Editor { - pub fn new(cmd: Option) -> Result> { - if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { - return Ok(Some(Self::VSCode)); + pub fn new(cmd: Option, vs_code_term: bool) -> Result> { + if vs_code_term { + for program in ["code", "codium"] { + if program_exists(program) { + return Ok(Some(Self::Cmd(Cow::Borrowed(program), Vec::new()))); + } + } } if let Some(cmd) = cmd { @@ -47,10 +61,10 @@ impl Editor { if shlex.had_error { bail!("Failed to parse the command in `--edit-cmd`"); } - return Ok(Some(Self::Cmd(program, args))); + return Ok(Some(Self::Cmd(Cow::Owned(program), args))); } - if env::var_os("ZELLIJ").is_some() { + if env::var_os("ZELLIJ").is_some() && program_exists("zellij") { return Ok(Some(Self::Zellij(None))); } @@ -65,11 +79,8 @@ impl Editor { let handle = thread::Builder::new() .spawn(move || { match &mut self { - Editor::VSCode => { - run_cmd(Command::new("code").arg(exercise_path))?; - } Editor::Cmd(program, args) => { - run_cmd(Command::new(program).args(args).arg(exercise_path))?; + run_cmd(Command::new(&**program).args(args).arg(exercise_path))?; } Editor::Zellij(open_pane) => { if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { @@ -105,7 +116,7 @@ impl Editor { pub fn close(&mut self) -> Result<()> { match self { - Editor::VSCode | Editor::Cmd(_, _) => (), + Editor::Cmd(_, _) => (), Editor::Zellij(open_pane) => { if let Some((pane_id_str, _, _)) = open_pane.take() { zellij::close_pane(&pane_id_str)?; diff --git a/src/main.rs b/src/main.rs index fb976653..0fa9b75d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result, bail}; use app_state::StateFileStatus; use clap::Parser; use std::{ + env, io::{self, IsTerminal, Write}, path::Path, process::ExitCode, @@ -60,11 +61,13 @@ fn main() -> Result { bail!(FORMAT_VERSION_HIGHER_ERR); } - let editor = Editor::new(args.edit_cmd)?; + let vs_code_term = env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"); + let editor = Editor::new(args.edit_cmd, vs_code_term)?; let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), editor, + vs_code_term, )?; // Show the welcome message if the state file doesn't exist yet. From 432d1f84ea68d21e865215281764094a73236da0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 7 Apr 2026 00:07:41 +0200 Subject: [PATCH 50/62] Add --no-editor --- CHANGELOG.md | 1 + src/cli.rs | 4 ++++ src/main.rs | 7 ++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2f003f..cdcdbb14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Automatically open the current file if Rustlings is running in a VS Code terminal - Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) +- New argument `--no-editor` to disable automatic opening of the current file in VS Code or Zellij - New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise - Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` diff --git a/src/cli.rs b/src/cli.rs index 5830cbed..153994be 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,6 +8,10 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, + /// Disable automatic opening of the current file in VS Code or Zellij. + /// Ignores `--edit-cmd` + #[arg(long)] + pub no_editor: bool, /// Open the current exercise by running `EDIT_CMD EXERCISE_PATH`. /// The command is not allowed to block (e.g. `vim`). /// It should communicate with an editor in a different process. diff --git a/src/main.rs b/src/main.rs index 0fa9b75d..8da36f7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,7 +62,12 @@ fn main() -> Result { } let vs_code_term = env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"); - let editor = Editor::new(args.edit_cmd, vs_code_term)?; + let editor = if args.no_editor { + None + } else { + Editor::new(args.edit_cmd, vs_code_term)? + }; + let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), From a307599b0bce21bd8c14741fba81d3d076f99b14 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 7 Apr 2026 00:15:24 +0200 Subject: [PATCH 51/62] Fix test --- tests/integration_tests.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bb1e398c..d38d4e93 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -4,7 +4,6 @@ use std::{ }; enum Output<'a> { - FullStdout(&'a [u8]), PartialStdout(&'a str), PartialStderr(&'a str), } @@ -47,9 +46,6 @@ impl<'a> Cmd<'a> { let output = cmd.output().unwrap(); match self.output { None => (), - Some(FullStdout(stdout)) => { - assert_eq!(output.stdout, stdout); - } Some(PartialStdout(stdout)) => { assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); } @@ -129,7 +125,7 @@ fn hint() { Cmd::default() .current_dir("tests/test_exercises") .args(&["hint", "test_failure"]) - .output(FullStdout(b"The answer to everything: 42\n")) + .output(PartialStdout("\n\nHint:\nThe answer to everything: 42\n")) .success(); } From b59f444bbc83231ec52cd2a9bf80bdc7a0fe5755 Mon Sep 17 00:00:00 2001 From: k7a-tomohiro <83070078+k7a-tomohiro@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:26:23 +0900 Subject: [PATCH 52/62] remove --- tmp.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tmp.txt diff --git a/tmp.txt b/tmp.txt deleted file mode 100644 index 3aefeefc..00000000 --- a/tmp.txt +++ /dev/null @@ -1 +0,0 @@ -226.867688ms \ No newline at end of file From 346753b673e0ee0217fe40fcc0ebb21d9c0b0cfc Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Mon, 13 Apr 2026 18:51:38 +0200 Subject: [PATCH 53/62] Make starting fireworks more fun :) --- exercises/07_structs/structs3.rs | 21 +++++++-------------- solutions/07_structs/structs3.rs | 21 +++++++-------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index b5457de6..d42729e1 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -22,13 +22,7 @@ fn add_rockets(fireworks: &mut Fireworks, rockets: usize) { // TODO: Turn this function into a method on `Fireworks`. fn start(fireworks: Fireworks) -> String { - if fireworks.rockets < 5 { - String::from("small") - } else if fireworks.rockets < 20 { - String::from("medium") - } else { - String::from("big") - } + "🚀".repeat(fireworks.rockets) } fn main() { @@ -41,18 +35,17 @@ mod tests { #[test] fn start_some_fireworks() { + let f = Fireworks::new(); + assert_eq!(f.start(), ""); + let mut f = Fireworks::new(); f.add_rockets(3); - assert_eq!(f.start(), "small"); + assert_eq!(f.start(), "🚀🚀🚀"); let mut f = Fireworks::new(); - f.add_rockets(15); - assert_eq!(f.start(), "medium"); - - let mut f = Fireworks::new(); - f.add_rockets(100); + f.add_rockets(7); // We don't use method syntax in the last test to ensure the `start` // function takes ownership of the fireworks. - assert_eq!(Fireworks::start(f), "big"); + assert_eq!(Fireworks::start(f), "🚀🚀🚀🚀🚀🚀🚀"); } } diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs index a8928443..c672bb19 100644 --- a/solutions/07_structs/structs3.rs +++ b/solutions/07_structs/structs3.rs @@ -15,13 +15,7 @@ impl Fireworks { } fn start(self) -> String { - if self.rockets < 5 { - String::from("small") - } else if self.rockets < 20 { - String::from("medium") - } else { - String::from("big") - } + "🚀".repeat(self.rockets) } } @@ -35,18 +29,17 @@ mod tests { #[test] fn start_some_fireworks() { + let f = Fireworks::new(); + assert_eq!(f.start(), ""); + let mut f = Fireworks::new(); f.add_rockets(3); - assert_eq!(f.start(), "small"); + assert_eq!(f.start(), "🚀🚀🚀"); let mut f = Fireworks::new(); - f.add_rockets(15); - assert_eq!(f.start(), "medium"); - - let mut f = Fireworks::new(); - f.add_rockets(100); + f.add_rockets(7); // We don't use method syntax in the last test to ensure the `start` // function takes ownership of the fireworks. - assert_eq!(Fireworks::start(f), "big"); + assert_eq!(Fireworks::start(f), "🚀🚀🚀🚀🚀🚀🚀"); } } From 5464fcd7e60c3cfd255faef3165eb65081da7b00 Mon Sep 17 00:00:00 2001 From: Tyler Breisacher Date: Thu, 16 Apr 2026 19:21:08 -0700 Subject: [PATCH 54/62] Add a note about offline stdlib docs --- website/content/setup/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/content/setup/index.md b/website/content/setup/index.md index 54551ada..a67e2809 100644 --- a/website/content/setup/index.md +++ b/website/content/setup/index.md @@ -73,6 +73,10 @@ While working with Rustlings, please use a modern terminal for the best user exp The default terminal on Linux and Mac should be sufficient. On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). +### Offline documentation (optional) + +If you are going to be working on Rustlings while offline, you may want to run `rustup doc --std` to make the standard library documentation available on your machine. + ## Usage After being done with the setup, visit the [**usage**](@/usage/index.md) page for some info about using Rustlings 🚀 From 03c5baf35c4d36c4a587a3bdab19366604c95aed Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 17 Apr 2026 14:56:21 +0200 Subject: [PATCH 55/62] Emphasize hotkeys in footer with color --- src/list/state.rs | 53 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 4fcbd3c3..6e52eec1 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -233,15 +233,38 @@ impl<'a> ListState<'a> { )?; next_ln(stdout)?; + let hotkey = |writer: &mut MaxLenWriter, hotkey| -> io::Result<()> { + writer + .stdout + .queue(SetForegroundColor(Color::Yellow))? + .queue(SetAttribute(Attribute::Bold))?; + writer.write_ascii(hotkey)?; + writer.stdout.queue(ResetColor)?; + Ok(()) + }; + let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { // Help footer message if self.scroll_state.selected().is_some() { - writer.write_str("↓/j ↑/k home/g end/G | ontinue at | eset exercise")?; + writer.write_str("↓/")?; + hotkey(&mut writer, b"j")?; + writer.write_str(" ↑/")?; + hotkey(&mut writer, b"k")?; + writer.write_ascii(b" home/")?; + hotkey(&mut writer, b"g")?; + writer.write_ascii(b" end/")?; + hotkey(&mut writer, b"G")?; + writer.write_ascii(b" | ")?; + hotkey(&mut writer, b"c")?; + writer.write_ascii(b"ontinue at | ")?; + hotkey(&mut writer, b"r")?; + writer.write_ascii(b"eset exercise")?; next_ln(stdout)?; writer = MaxLenWriter::new(stdout, self.term_width as usize); - writer.write_ascii(b"earch | filter ")?; + hotkey(&mut writer, b"s")?; + writer.write_ascii(b"earch | filter ")?; } else { // Nothing selected (and nothing shown), so only display filter and quit. writer.write_ascii(b"filter ")?; @@ -249,27 +272,41 @@ impl<'a> ListState<'a> { match self.filter { Filter::Done => { + writer.stdout.queue(SetAttribute(Attribute::Underlined))?; + hotkey(&mut writer, b"d")?; writer .stdout .queue(SetForegroundColor(Color::Magenta))? .queue(SetAttribute(Attribute::Underlined))?; - writer.write_ascii(b"one")?; + writer.write_str("one")?; writer.stdout.queue(ResetColor)?; - writer.write_ascii(b"/

ending")?; + writer.write_ascii(b"/")?; + hotkey(&mut writer, b"p")?; + writer.write_ascii(b"ending")?; } Filter::Pending => { - writer.write_ascii(b"one/")?; + hotkey(&mut writer, b"d")?; + writer.write_ascii(b"one/")?; + writer.stdout.queue(SetAttribute(Attribute::Underlined))?; + hotkey(&mut writer, b"p")?; writer .stdout .queue(SetForegroundColor(Color::Magenta))? .queue(SetAttribute(Attribute::Underlined))?; - writer.write_ascii(b"

ending")?; + writer.write_ascii(b"ending")?; writer.stdout.queue(ResetColor)?; } - Filter::None => writer.write_ascii(b"one/

ending")?, + Filter::None => { + hotkey(&mut writer, b"d")?; + writer.write_ascii(b"one/")?; + hotkey(&mut writer, b"p")?; + writer.write_ascii(b"ending")?; + } } - writer.write_ascii(b" | uit list")?; + writer.write_ascii(b" | ")?; + hotkey(&mut writer, b"q")?; + writer.write_ascii(b"uit list")?; } else { writer.stdout.queue(SetForegroundColor(Color::Magenta))?; writer.write_str(&self.message)?; From 013a88a1e629213bf060c6acf798afd5e4f9b9f1 Mon Sep 17 00:00:00 2001 From: Tyler Breisacher Date: Fri, 17 Apr 2026 21:05:13 -0700 Subject: [PATCH 56/62] new wording from @senekor --- website/content/setup/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/setup/index.md b/website/content/setup/index.md index a67e2809..8147ff56 100644 --- a/website/content/setup/index.md +++ b/website/content/setup/index.md @@ -73,9 +73,9 @@ While working with Rustlings, please use a modern terminal for the best user exp The default terminal on Linux and Mac should be sufficient. On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). -### Offline documentation (optional) +### Offline documentation -If you are going to be working on Rustlings while offline, you may want to run `rustup doc --std` to make the standard library documentation available on your machine. +Whenever you're working on Rustlings offline, you can access a local copy of the standard library documentation by running `rustup doc --std`. ## Usage From 870776d03bc612f473c8dbf1b19d3d78200ae3e7 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 19 Apr 2026 00:37:10 +0200 Subject: [PATCH 57/62] Allow selecting next exercise with enter key --- src/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list.rs b/src/list.rs index c60a5299..db466c2f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -83,7 +83,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } } KeyCode::Char('r') => list_state.reset_selected()?, - KeyCode::Char('c') => { + KeyCode::Char('c') | KeyCode::Enter => { if list_state.selected_to_current_exercise()? { return Ok(()); } From c658a997f3dde7e5d3d15126428e0546370ef0d4 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Mon, 20 Apr 2026 13:49:17 +0200 Subject: [PATCH 58/62] Turn unnecessary closure into regular function --- src/list/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 6e52eec1..c31cf8ae 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -233,7 +233,7 @@ impl<'a> ListState<'a> { )?; next_ln(stdout)?; - let hotkey = |writer: &mut MaxLenWriter, hotkey| -> io::Result<()> { + fn hotkey(writer: &mut MaxLenWriter, hotkey: &[u8]) -> io::Result<()> { writer .stdout .queue(SetForegroundColor(Color::Yellow))? @@ -241,7 +241,7 @@ impl<'a> ListState<'a> { writer.write_ascii(hotkey)?; writer.stdout.queue(ResetColor)?; Ok(()) - }; + } let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { From b86a532e2848d71d103a5bc677eaf4b9d277ce54 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 25 Apr 2026 13:53:25 +0200 Subject: [PATCH 59/62] Document enter keybind in list footer --- src/list/state.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 4fcbd3c3..e9386b8e 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -237,7 +237,8 @@ impl<'a> ListState<'a> { if self.message.is_empty() { // Help footer message if self.scroll_state.selected().is_some() { - writer.write_str("↓/j ↑/k home/g end/G | ontinue at | eset exercise")?; + writer + .write_str("↓/j ↑/k home/g end/G | ↩️/ontinue at | eset exercise")?; next_ln(stdout)?; writer = MaxLenWriter::new(stdout, self.term_width as usize); From e0334f79fe6c8ee522841154a48c34932b4346cf Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 25 Apr 2026 14:04:29 +0200 Subject: [PATCH 60/62] Avoid nested function definition --- src/list/state.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index c31cf8ae..1bfdf58d 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -233,16 +233,6 @@ impl<'a> ListState<'a> { )?; next_ln(stdout)?; - fn hotkey(writer: &mut MaxLenWriter, hotkey: &[u8]) -> io::Result<()> { - writer - .stdout - .queue(SetForegroundColor(Color::Yellow))? - .queue(SetAttribute(Attribute::Bold))?; - writer.write_ascii(hotkey)?; - writer.stdout.queue(ResetColor)?; - Ok(()) - } - let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { // Help footer message @@ -446,3 +436,14 @@ impl<'a> ListState<'a> { Ok(true) } } + +/// Draw an emphasized hotkey in the list footer. +fn hotkey(writer: &mut MaxLenWriter, hotkey: &[u8]) -> io::Result<()> { + writer + .stdout + .queue(SetForegroundColor(Color::Yellow))? + .queue(SetAttribute(Attribute::Bold))?; + writer.write_ascii(hotkey)?; + writer.stdout.queue(ResetColor)?; + Ok(()) +} From f9f8a37bc72bd0ac6562545e966e4d8cb26bc4d1 Mon Sep 17 00:00:00 2001 From: Piotr Spieker Date: Thu, 30 Apr 2026 12:05:38 +0200 Subject: [PATCH 61/62] Improve the list of enum variants in enums2 hint --- rustlings-macros/info.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index dd1f04be..735d131d 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -440,7 +440,7 @@ dir = "08_enums" test = false hint = """ You can create enumerations that have different variants with different types -such as struct-like variants, regular structs, a single string, tuples, no data, etc.""" +such as struct-like, tuple-like and unit-only variants.""" [[exercises]] name = "enums3" From 124708acd93a4a48272e1f7a953fe62c9a2ffa09 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 3 May 2026 14:54:27 +0200 Subject: [PATCH 62/62] Use slice instead of array in iterator1 This avoids confusion between `.into_iter()` and `.iter()`. On a slice, both methods do the same (correct) thing. Using `.into_iter()` will result in a clippy warning about the slice not being consumed. --- exercises/18_iterators/iterators1.rs | 4 ++-- solutions/18_iterators/iterators1.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index ca937ed0..8014f0f1 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -10,9 +10,9 @@ fn main() { mod tests { #[test] fn iterators() { - let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + let my_fav_fruits = &["banana", "custard apple", "avocado", "peach", "raspberry"]; - // TODO: Create an iterator over the array. + // TODO: Create an iterator over the slice. let mut fav_fruits_iterator = todo!(); assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); diff --git a/solutions/18_iterators/iterators1.rs b/solutions/18_iterators/iterators1.rs index 93a6008a..79977fa4 100644 --- a/solutions/18_iterators/iterators1.rs +++ b/solutions/18_iterators/iterators1.rs @@ -10,9 +10,9 @@ fn main() { mod tests { #[test] fn iterators() { - let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + let my_fav_fruits = &["banana", "custard apple", "avocado", "peach", "raspberry"]; - // Create an iterator over the array. + // Create an iterator over the slice. let mut fav_fruits_iterator = my_fav_fruits.iter(); assert_eq!(fav_fruits_iterator.next(), Some(&"banana"));