From e2492f65a061ab8978d40d32a03cf981544fdf03 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Jul 2024 12:51:44 +0200 Subject: [PATCH 001/211] Update deps --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3824f16..4ae4ad04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,9 +151,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.9" +version = "4.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" dependencies = [ "clap_builder", "clap_derive", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.9" +version = "4.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" dependencies = [ "anstream", "anstyle", @@ -1095,9 +1095,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374ec40a2d767a3c1b4972d9475ecd557356637be906f2cb3f7fe17a6eb5e22f" +checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index f5fe7380..17989238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ include = [ [dependencies] anyhow = "1.0.86" -clap = { version = "4.5.9", features = ["derive"] } +clap = { version = "4.5.10", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } From 3f49decce947ff7f1d30b53cf22fc2a0bce820f6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Jul 2024 14:34:43 +0200 Subject: [PATCH 002/211] Remove assert_cmd and predicates --- Cargo.lock | 147 -------------------------- Cargo.toml | 4 - tests/integration_tests.rs | 207 +++++++++++++++++++++++-------------- 3 files changed, 131 insertions(+), 227 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ae4ad04..5932578a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,15 +14,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "allocator-api2" version = "0.2.18" @@ -84,21 +75,6 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -[[package]] -name = "assert_cmd" -version = "2.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" -dependencies = [ - "anstyle", - "bstr", - "doc-comment", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - [[package]] name = "autocfg" version = "1.3.0" @@ -117,17 +93,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -[[package]] -name = "bstr" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - [[package]] name = "cassowary" version = "0.3.0" @@ -248,18 +213,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "either" version = "1.13.0" @@ -284,15 +237,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -438,12 +382,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - [[package]] name = "notify" version = "6.1.1" @@ -473,15 +411,6 @@ dependencies = [ "notify", ] -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -527,36 +456,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "predicates" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" -dependencies = [ - "anstyle", - "difflib", - "float-cmp", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" - -[[package]] -name = "predicates-tree" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" -dependencies = [ - "predicates-core", - "termtree", -] - [[package]] name = "proc-macro2" version = "1.0.86" @@ -614,47 +513,16 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "regex" -version = "1.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - [[package]] name = "rustlings" version = "6.1.0" dependencies = [ "anyhow", - "assert_cmd", "clap", "crossterm", "hashbrown", "notify-debouncer-mini", "os_pipe", - "predicates", "ratatui", "rustlings-macros", "serde", @@ -829,12 +697,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termtree" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" - [[package]] name = "toml_datetime" version = "0.6.6" @@ -898,15 +760,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 17989238..ddf3c703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,10 +58,6 @@ serde_json = "1.0.120" serde.workspace = true toml_edit.workspace = true -[dev-dependencies] -assert_cmd = "2.0.14" -predicates = "3.1.0" - [profile.release] panic = "abort" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 7d30467b..d5afd2ca 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,134 +1,189 @@ -use assert_cmd::prelude::*; -use std::process::Command; +use std::{ + env::{self, consts::EXE_SUFFIX}, + process::{Command, Stdio}, + str::from_utf8, +}; + +#[derive(Default)] +struct Cmd<'a> { + current_dir: Option<&'a str>, + args: &'a [&'a str], + stdout: Option<&'a str>, + full_stdout: bool, +} + +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 stdout(&mut self, stdout: &'a str) -> &mut Self { + self.stdout = Some(stdout); + self + } + + #[inline] + fn full_stdout(&mut self) -> &mut Self { + self.full_stdout = true; + self + } + + 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); + + if let Some(current_dir) = self.current_dir { + cmd.current_dir(current_dir); + } + + cmd.args(self.args) + .stdin(Stdio::null()) + .stderr(Stdio::null()); + + let status = if let Some(expected_stdout) = self.stdout { + let output = cmd.output().unwrap(); + let stdout = from_utf8(&output.stdout).unwrap(); + + if self.full_stdout { + assert_eq!(stdout, expected_stdout); + } else { + assert!(stdout.contains(expected_stdout)); + } + + output.status + } else { + cmd.stdout(Stdio::null()).status().unwrap() + }; + + assert_eq!(status.success(), success); + } + + #[inline] + fn success(&self) { + self.assert(true); + } + + #[inline] + fn fail(&self) { + self.assert(false); + } +} #[test] fn fails_when_in_wrong_dir() { - Command::cargo_bin("rustlings") - .unwrap() - .current_dir("tests/") - .assert() - .code(1); + Cmd::default().current_dir("tests").fail(); } #[test] fn run_single_compile_success() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "compSuccess"]) - .current_dir("tests/fixture/success/") - .assert() + Cmd::default() + .current_dir("tests/fixture/success") + .args(&["run", "compSuccess"]) .success(); } #[test] fn run_single_compile_failure() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "compFailure"]) - .current_dir("tests/fixture/failure/") - .assert() - .code(1); + Cmd::default() + .current_dir("tests/fixture/failure") + .args(&["run", "compFailure"]) + .fail(); } #[test] fn run_single_test_success() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "testSuccess"]) - .current_dir("tests/fixture/success/") - .assert() + Cmd::default() + .current_dir("tests/fixture/success") + .args(&["run", "testSuccess"]) .success(); } #[test] fn run_single_test_failure() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "testFailure"]) - .current_dir("tests/fixture/failure/") - .assert() - .code(1); + Cmd::default() + .current_dir("tests/fixture/failure") + .args(&["run", "testFailure"]) + .fail(); } #[test] fn run_single_test_not_passed() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "testNotPassed.rs"]) - .current_dir("tests/fixture/failure/") - .assert() - .code(1); + Cmd::default() + .current_dir("tests/fixture/failure") + .args(&["run", "testNotPassed.rs"]) + .fail(); } #[test] fn run_single_test_no_exercise() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "compNoExercise.rs"]) + Cmd::default() .current_dir("tests/fixture/failure") - .assert() - .code(1); + .args(&["run", "compNoExercise.rs"]) + .fail(); } #[test] fn reset_single_exercise() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["reset", "intro1"]) - .assert() - .code(0); + Cmd::default().args(&["reset", "intro1"]).success(); } #[test] fn reset_no_exercise() { - Command::cargo_bin("rustlings") - .unwrap() - .arg("reset") - .assert() - .code(2) - .stderr(predicates::str::contains( - "required arguments were not provided", - )); + Cmd::default().args(&["reset"]).fail(); } #[test] fn get_hint_for_single_test() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["hint", "testFailure"]) + Cmd::default() .current_dir("tests/fixture/failure") - .assert() - .code(0) - .stdout("Hello!\n"); + .args(&["hint", "testFailure"]) + .stdout("Hello!\n") + .full_stdout() + .success(); } #[test] fn run_compile_exercise_does_not_prompt() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "pending_exercise"]) + Cmd::default() .current_dir("tests/fixture/state") - .assert() - .code(0); + .args(&["run", "pending_exercise"]) + .success(); } #[test] fn run_test_exercise_does_not_prompt() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "pending_test_exercise"]) + Cmd::default() .current_dir("tests/fixture/state") - .assert() - .code(0); + .args(&["run", "pending_test_exercise"]) + .success(); } #[test] fn run_single_test_success_with_output() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "testSuccess"]) - .current_dir("tests/fixture/success/") - .assert() - .code(0) - .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS")); + Cmd::default() + .current_dir("tests/fixture/success") + .args(&["run", "testSuccess"]) + .stdout("\nTHIS TEST TOO SHALL PASS\n") + .success(); } From 8fec5155c735efe791ba2fdbaa7a562bf3e0ddea Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Jul 2024 15:12:14 +0200 Subject: [PATCH 003/211] Clean up tests --- Cargo.toml | 4 +- tests/fixture/failure/Cargo.toml | 20 ----- .../fixture/failure/exercises/testFailure.rs | 6 -- .../failure/exercises/testNotPassed.rs | 6 -- tests/fixture/failure/info.toml | 10 --- tests/fixture/state/Cargo.toml | 16 ---- .../state/exercises/pending_exercise.rs | 1 - .../state/exercises/pending_test_exercise.rs | 4 - tests/fixture/state/info.toml | 15 ---- tests/fixture/success/Cargo.toml | 12 --- .../fixture/success/exercises/compSuccess.rs | 1 - .../fixture/success/exercises/testSuccess.rs | 7 -- tests/fixture/success/info.toml | 10 --- tests/integration_tests.rs | 81 +++++-------------- tests/test_exercises/Cargo.toml | 11 +++ .../exercises/compilation_failure.rs} | 0 .../exercises/compilation_success.rs} | 0 .../exercises/not_in_info.rs} | 0 .../test_exercises/exercises/test_failure.rs | 9 +++ .../test_exercises/exercises/test_success.rs | 9 +++ tests/test_exercises/info.toml | 19 +++++ 21 files changed, 71 insertions(+), 170 deletions(-) delete mode 100644 tests/fixture/failure/Cargo.toml delete mode 100644 tests/fixture/failure/exercises/testFailure.rs delete mode 100644 tests/fixture/failure/exercises/testNotPassed.rs delete mode 100644 tests/fixture/failure/info.toml delete mode 100644 tests/fixture/state/Cargo.toml delete mode 100644 tests/fixture/state/exercises/pending_exercise.rs delete mode 100644 tests/fixture/state/exercises/pending_test_exercise.rs delete mode 100644 tests/fixture/state/info.toml delete mode 100644 tests/fixture/success/Cargo.toml delete mode 100644 tests/fixture/success/exercises/compSuccess.rs delete mode 100644 tests/fixture/success/exercises/testSuccess.rs delete mode 100644 tests/fixture/success/info.toml create mode 100644 tests/test_exercises/Cargo.toml rename tests/{fixture/failure/exercises/compFailure.rs => test_exercises/exercises/compilation_failure.rs} (100%) rename tests/{fixture/failure/exercises/compNoExercise.rs => test_exercises/exercises/compilation_success.rs} (100%) rename tests/{fixture/state/exercises/finished_exercise.rs => test_exercises/exercises/not_in_info.rs} (100%) create mode 100644 tests/test_exercises/exercises/test_failure.rs create mode 100644 tests/test_exercises/exercises/test_success.rs create mode 100644 tests/test_exercises/info.toml diff --git a/Cargo.toml b/Cargo.toml index ddf3c703..685e8ac8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,7 @@ [workspace] resolver = "2" exclude = [ - "tests/fixture/failure", - "tests/fixture/state", - "tests/fixture/success", + "tests/test_exercises", "dev", ] diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml deleted file mode 100644 index 7ee2f068..00000000 --- a/tests/fixture/failure/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "failure" -edition = "2021" -publish = false - -[[bin]] -name = "compFailure" -path = "exercises/compFailure.rs" - -[[bin]] -name = "compNoExercise" -path = "exercises/compNoExercise.rs" - -[[bin]] -name = "testFailure" -path = "exercises/testFailure.rs" - -[[bin]] -name = "testNotPassed" -path = "exercises/testNotPassed.rs" diff --git a/tests/fixture/failure/exercises/testFailure.rs b/tests/fixture/failure/exercises/testFailure.rs deleted file mode 100644 index fcbcf90a..00000000 --- a/tests/fixture/failure/exercises/testFailure.rs +++ /dev/null @@ -1,6 +0,0 @@ -fn main() {} - -#[test] -fn passing() { - asset!(true); -} diff --git a/tests/fixture/failure/exercises/testNotPassed.rs b/tests/fixture/failure/exercises/testNotPassed.rs deleted file mode 100644 index de0d61c0..00000000 --- a/tests/fixture/failure/exercises/testNotPassed.rs +++ /dev/null @@ -1,6 +0,0 @@ -fn main() {} - -#[test] -fn not_passing() { - assert!(false); -} diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml deleted file mode 100644 index 554607a8..00000000 --- a/tests/fixture/failure/info.toml +++ /dev/null @@ -1,10 +0,0 @@ -format_version = 1 - -[[exercises]] -name = "compFailure" -test = false -hint = "" - -[[exercises]] -name = "testFailure" -hint = "Hello!" diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml deleted file mode 100644 index adbd8ab1..00000000 --- a/tests/fixture/state/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "state" -edition = "2021" -publish = false - -[[bin]] -name = "pending_exercise" -path = "exercises/pending_exercise.rs" - -[[bin]] -name = "pending_test_exercise" -path = "exercises/pending_test_exercise.rs" - -[[bin]] -name = "finished_exercise" -path = "exercises/finished_exercise.rs" diff --git a/tests/fixture/state/exercises/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs deleted file mode 100644 index f328e4d9..00000000 --- a/tests/fixture/state/exercises/pending_exercise.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/tests/fixture/state/exercises/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs deleted file mode 100644 index 718e1dbb..00000000 --- a/tests/fixture/state/exercises/pending_test_exercise.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() {} - -#[test] -fn it_works() {} diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml deleted file mode 100644 index ff0b932e..00000000 --- a/tests/fixture/state/info.toml +++ /dev/null @@ -1,15 +0,0 @@ -format_version = 1 - -[[exercises]] -name = "pending_exercise" -test = false -hint = """""" - -[[exercises]] -name = "pending_test_exercise" -hint = """""" - -[[exercises]] -name = "finished_exercise" -test = false -hint = """""" diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml deleted file mode 100644 index 028cf35a..00000000 --- a/tests/fixture/success/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "success" -edition = "2021" -publish = false - -[[bin]] -name = "compSuccess" -path = "exercises/compSuccess.rs" - -[[bin]] -name = "testSuccess" -path = "exercises/testSuccess.rs" diff --git a/tests/fixture/success/exercises/compSuccess.rs b/tests/fixture/success/exercises/compSuccess.rs deleted file mode 100644 index f328e4d9..00000000 --- a/tests/fixture/success/exercises/compSuccess.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/tests/fixture/success/exercises/testSuccess.rs b/tests/fixture/success/exercises/testSuccess.rs deleted file mode 100644 index 4296cf61..00000000 --- a/tests/fixture/success/exercises/testSuccess.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() {} - -#[test] -fn passing() { - println!("THIS TEST TOO SHALL PASS"); - assert!(true); -} diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml deleted file mode 100644 index d66d7d47..00000000 --- a/tests/fixture/success/info.toml +++ /dev/null @@ -1,10 +0,0 @@ -format_version = 1 - -[[exercises]] -name = "compSuccess" -test = false -hint = """""" - -[[exercises]] -name = "testSuccess" -hint = """""" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d5afd2ca..f21ee2f6 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -91,99 +91,62 @@ impl<'a> Cmd<'a> { } #[test] -fn fails_when_in_wrong_dir() { +fn wrong_dir() { Cmd::default().current_dir("tests").fail(); } #[test] -fn run_single_compile_success() { +fn run_compilation_success() { Cmd::default() - .current_dir("tests/fixture/success") - .args(&["run", "compSuccess"]) + .current_dir("tests/test_exercises") + .args(&["run", "compilation_success"]) .success(); } #[test] -fn run_single_compile_failure() { +fn run_compilation_failure() { Cmd::default() - .current_dir("tests/fixture/failure") - .args(&["run", "compFailure"]) + .current_dir("tests/test_exercises") + .args(&["run", "compilation_failure"]) .fail(); } #[test] -fn run_single_test_success() { +fn run_test_success() { Cmd::default() - .current_dir("tests/fixture/success") - .args(&["run", "testSuccess"]) + .current_dir("tests/test_exercises") + .args(&["run", "test_success"]) + .stdout("\nOutput from `main` function\n") .success(); } #[test] -fn run_single_test_failure() { +fn run_test_failure() { Cmd::default() - .current_dir("tests/fixture/failure") - .args(&["run", "testFailure"]) + .current_dir("tests/test_exercises") + .args(&["run", "test_failure"]) .fail(); } #[test] -fn run_single_test_not_passed() { +fn run_exercise_not_in_info() { Cmd::default() - .current_dir("tests/fixture/failure") - .args(&["run", "testNotPassed.rs"]) + .current_dir("tests/test_exercises") + .args(&["run", "not_in_info"]) .fail(); } #[test] -fn run_single_test_no_exercise() { - Cmd::default() - .current_dir("tests/fixture/failure") - .args(&["run", "compNoExercise.rs"]) - .fail(); -} - -#[test] -fn reset_single_exercise() { - Cmd::default().args(&["reset", "intro1"]).success(); -} - -#[test] -fn reset_no_exercise() { +fn reset_without_exercise_name() { Cmd::default().args(&["reset"]).fail(); } #[test] -fn get_hint_for_single_test() { +fn hint() { Cmd::default() - .current_dir("tests/fixture/failure") - .args(&["hint", "testFailure"]) - .stdout("Hello!\n") + .current_dir("tests/test_exercises") + .args(&["hint", "test_failure"]) + .stdout("The answer to everything: 42\n") .full_stdout() .success(); } - -#[test] -fn run_compile_exercise_does_not_prompt() { - Cmd::default() - .current_dir("tests/fixture/state") - .args(&["run", "pending_exercise"]) - .success(); -} - -#[test] -fn run_test_exercise_does_not_prompt() { - Cmd::default() - .current_dir("tests/fixture/state") - .args(&["run", "pending_test_exercise"]) - .success(); -} - -#[test] -fn run_single_test_success_with_output() { - Cmd::default() - .current_dir("tests/fixture/success") - .args(&["run", "testSuccess"]) - .stdout("\nTHIS TEST TOO SHALL PASS\n") - .success(); -} diff --git a/tests/test_exercises/Cargo.toml b/tests/test_exercises/Cargo.toml new file mode 100644 index 00000000..6b817518 --- /dev/null +++ b/tests/test_exercises/Cargo.toml @@ -0,0 +1,11 @@ +bin = [ + { name = "compilation_success", path = "exercises/compilation_success.rs" }, + { name = "compilation_failure", path = "exercises/compilation_failure.rs" }, + { name = "test_success", path = "exercises/test_success.rs" }, + { name = "test_failure", path = "exercises/test_failure.rs" }, +] + +[package] +name = "test_exercises" +edition = "2021" +publish = false diff --git a/tests/fixture/failure/exercises/compFailure.rs b/tests/test_exercises/exercises/compilation_failure.rs similarity index 100% rename from tests/fixture/failure/exercises/compFailure.rs rename to tests/test_exercises/exercises/compilation_failure.rs diff --git a/tests/fixture/failure/exercises/compNoExercise.rs b/tests/test_exercises/exercises/compilation_success.rs similarity index 100% rename from tests/fixture/failure/exercises/compNoExercise.rs rename to tests/test_exercises/exercises/compilation_success.rs diff --git a/tests/fixture/state/exercises/finished_exercise.rs b/tests/test_exercises/exercises/not_in_info.rs similarity index 100% rename from tests/fixture/state/exercises/finished_exercise.rs rename to tests/test_exercises/exercises/not_in_info.rs diff --git a/tests/test_exercises/exercises/test_failure.rs b/tests/test_exercises/exercises/test_failure.rs new file mode 100644 index 00000000..9dc142a4 --- /dev/null +++ b/tests/test_exercises/exercises/test_failure.rs @@ -0,0 +1,9 @@ +fn main() {} + +#[cfg(test)] +mod tests { + #[test] + fn fails() { + asset!(false); + } +} diff --git a/tests/test_exercises/exercises/test_success.rs b/tests/test_exercises/exercises/test_success.rs new file mode 100644 index 00000000..8c8a3c61 --- /dev/null +++ b/tests/test_exercises/exercises/test_success.rs @@ -0,0 +1,9 @@ +fn main() { + println!("Output from `main` function"); +} + +#[cfg(test)] +mod tests { + #[test] + fn passes() {} +} diff --git a/tests/test_exercises/info.toml b/tests/test_exercises/info.toml new file mode 100644 index 00000000..d91094c0 --- /dev/null +++ b/tests/test_exercises/info.toml @@ -0,0 +1,19 @@ +format_version = 1 + +[[exercises]] +name = "compilation_success" +test = false +hint = "" + +[[exercises]] +name = "compilation_failure" +test = false +hint = "" + +[[exercises]] +name = "test_success" +hint = "" + +[[exercises]] +name = "test_failure" +hint = "The answer to everything: 42" From 8beb2908420b0225aabe983e3425055db761b356 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Jul 2024 16:14:38 +0200 Subject: [PATCH 004/211] Test initialization --- src/main.rs | 4 - tests/integration_tests.rs | 98 ++++++++++++------- .../test_exercises/exercises/test_failure.rs | 2 +- 3 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3c96d1a9..658d551d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,10 +95,6 @@ fn main() -> Result<()> { match args.command { Some(Subcommands::Init) => { - if DEBUG_PROFILE { - bail!("Disabled in the debug build"); - } - { let mut stdout = io::stdout().lock(); stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index f21ee2f6..3ab54f97 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,15 +1,23 @@ use std::{ env::{self, consts::EXE_SUFFIX}, + fs, process::{Command, Stdio}, str::from_utf8, }; +enum Output<'a> { + FullStdout(&'a str), + PartialStdout(&'a str), + PartialStderr(&'a str), +} + +use Output::*; + #[derive(Default)] struct Cmd<'a> { current_dir: Option<&'a str>, args: &'a [&'a str], - stdout: Option<&'a str>, - full_stdout: bool, + output: Option>, } impl<'a> Cmd<'a> { @@ -26,14 +34,8 @@ impl<'a> Cmd<'a> { } #[inline] - fn stdout(&mut self, stdout: &'a str) -> &mut Self { - self.stdout = Some(stdout); - self - } - - #[inline] - fn full_stdout(&mut self) -> &mut Self { - self.full_stdout = true; + fn output(&mut self, output: Output<'a>) -> &mut Self { + self.output = Some(output); self } @@ -57,26 +59,32 @@ impl<'a> Cmd<'a> { cmd.current_dir(current_dir); } - cmd.args(self.args) - .stdin(Stdio::null()) - .stderr(Stdio::null()); + cmd.args(self.args).stdin(Stdio::null()); - let status = if let Some(expected_stdout) = self.stdout { - let output = cmd.output().unwrap(); - let stdout = from_utf8(&output.stdout).unwrap(); - - if self.full_stdout { - assert_eq!(stdout, expected_stdout); - } else { - assert!(stdout.contains(expected_stdout)); + let status = match self.output { + None => cmd + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .unwrap(), + Some(FullStdout(stdout)) => { + let output = cmd.stderr(Stdio::null()).output().unwrap(); + assert_eq!(from_utf8(&output.stdout).unwrap(), stdout); + output.status + } + Some(PartialStdout(stdout)) => { + let output = cmd.stderr(Stdio::null()).output().unwrap(); + assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); + output.status + } + Some(PartialStderr(stderr)) => { + let output = cmd.stdout(Stdio::null()).output().unwrap(); + assert!(from_utf8(&output.stderr).unwrap().contains(stderr)); + output.status } - - output.status - } else { - cmd.stdout(Stdio::null()).status().unwrap() }; - assert_eq!(status.success(), success); + assert_eq!(status.success(), success, "{cmd:?}"); } #[inline] @@ -90,11 +98,6 @@ impl<'a> Cmd<'a> { } } -#[test] -fn wrong_dir() { - Cmd::default().current_dir("tests").fail(); -} - #[test] fn run_compilation_success() { Cmd::default() @@ -116,7 +119,7 @@ fn run_test_success() { Cmd::default() .current_dir("tests/test_exercises") .args(&["run", "test_success"]) - .stdout("\nOutput from `main` function\n") + .output(PartialStdout("\nOutput from `main` function\n")) .success(); } @@ -146,7 +149,34 @@ fn hint() { Cmd::default() .current_dir("tests/test_exercises") .args(&["hint", "test_failure"]) - .stdout("The answer to everything: 42\n") - .full_stdout() + .output(FullStdout("The answer to everything: 42\n")) .success(); } + +#[test] +fn init() { + let _ = fs::remove_dir_all("tests/rustlings"); + + Cmd::default().current_dir("tests").fail(); + + Cmd::default() + .current_dir("tests") + .args(&["init"]) + .success(); + + // Running `init` after a successful initialization. + Cmd::default() + .current_dir("tests") + .args(&["init"]) + .output(PartialStderr("`cd rustlings`")) + .fail(); + + // Running `init` in the initialized directory. + Cmd::default() + .current_dir("tests/rustlings") + .args(&["init"]) + .output(PartialStderr("already initialized")) + .fail(); + + fs::remove_dir_all("tests/rustlings").unwrap(); +} diff --git a/tests/test_exercises/exercises/test_failure.rs b/tests/test_exercises/exercises/test_failure.rs index 9dc142a4..8c8d59d8 100644 --- a/tests/test_exercises/exercises/test_failure.rs +++ b/tests/test_exercises/exercises/test_failure.rs @@ -4,6 +4,6 @@ fn main() {} mod tests { #[test] fn fails() { - asset!(false); + assert!(false); } } From 1937b4bf664568afcdb2c73247b593299d3fca6d Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Jul 2024 16:26:48 +0200 Subject: [PATCH 005/211] Use the rexported crossterm from ratatui --- Cargo.lock | 45 ++++++++++++++++++------------------- Cargo.toml | 3 +-- src/app_state.rs | 2 +- src/exercise.rs | 2 +- src/init.rs | 2 +- src/list.rs | 13 ++++++----- src/progress_bar.rs | 2 +- src/run.rs | 2 +- src/watch/state.rs | 2 +- src/watch/terminal_event.rs | 2 +- 10 files changed, 38 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5932578a..ccd90130 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -37,33 +37,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -116,9 +116,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.10" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" dependencies = [ "clap_builder", "clap_derive", @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.10" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" dependencies = [ "anstream", "anstyle", @@ -138,9 +138,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" dependencies = [ "heck", "proc-macro2", @@ -150,15 +150,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "compact_str" @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -519,7 +519,6 @@ version = "6.1.0" dependencies = [ "anyhow", "clap", - "crossterm", "hashbrown", "notify-debouncer-mini", "os_pipe", diff --git a/Cargo.toml b/Cargo.toml index 685e8ac8..01e679ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,8 +45,7 @@ include = [ [dependencies] anyhow = "1.0.86" -clap = { version = "4.5.10", features = ["derive"] } -crossterm = "0.27.0" +clap = { version = "4.5.11", features = ["derive"] } hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.0" diff --git a/src/app_state.rs b/src/app_state.rs index e08f94c6..8995e81c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use crossterm::style::Stylize; +use ratatui::crossterm::style::Stylize; use serde::Deserialize; use std::{ fs::{self, File}, diff --git a/src/exercise.rs b/src/exercise.rs index b6adc141..960eec0e 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use crossterm::style::{style, StyledContent, Stylize}; +use ratatui::crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, io::Write, diff --git a/src/init.rs b/src/init.rs index 4063ca75..bfa0ab86 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use crossterm::style::Stylize; +use ratatui::crossterm::style::Stylize; use std::{ env::set_current_dir, fs::{self, create_dir}, diff --git a/src/list.rs b/src/list.rs index 790c02fe..15836a44 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,10 +1,13 @@ use anyhow::Result; -use crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, +use ratatui::{ + backend::CrosstermBackend, + crossterm::{ + event::{self, Event, KeyCode, KeyEventKind}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, + }, + Terminal, }; -use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; use crate::app_state::AppState; diff --git a/src/progress_bar.rs b/src/progress_bar.rs index 4a54170a..7f07ad59 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -14,7 +14,7 @@ const PROGRESS_EXCEEDS_MAX_ERR: &str = /// Terminal progress bar to be used when not using Ratataui. pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result { - use crossterm::style::Stylize; + use ratatui::crossterm::style::Stylize; if progress > total { bail!(PROGRESS_EXCEEDS_MAX_ERR); diff --git a/src/run.rs b/src/run.rs index 899d0a94..3965a0d0 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Result}; -use crossterm::style::{style, Stylize}; +use ratatui::crossterm::style::{style, Stylize}; use std::io::{self, Write}; use crate::{ diff --git a/src/watch/state.rs b/src/watch/state.rs index 78af30a4..26ff4118 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use crossterm::{ +use ratatui::crossterm::{ style::{style, Stylize}, terminal, }; diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index f54af17a..3a1762d3 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,4 +1,4 @@ -use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use std::sync::mpsc::Sender; use super::WatchEvent; From 2ae9f3555b9a4065eeb14f43e756f617ca8cf0ea Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 28 Jul 2024 13:30:31 +0200 Subject: [PATCH 006/211] Update deps --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccd90130..afb98cd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -598,9 +598,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -698,18 +698,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" dependencies = [ "indexmap", "serde", @@ -755,9 +755,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -947,9 +947,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.15" +version = "0.6.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 01e679ee..4aad110f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ edition = "2021" [workspace.dependencies] serde = { version = "1.0.204", features = ["derive"] } -toml_edit = { version = "0.22.16", default-features = false, features = ["parse", "serde"] } +toml_edit = { version = "0.22.17", default-features = false, features = ["parse", "serde"] } [package] name = "rustlings" From 3a99542f7346a639115abd65ea277f32ecd3e5a1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 28 Jul 2024 17:39:46 +0200 Subject: [PATCH 007/211] Run the final check in parallel --- CHANGELOG.md | 7 ++++++ src/app_state.rs | 57 ++++++++++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e000a1..01a2fb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ + + +## 6.1.1 (UNRELEASED) + +- Run the final check of all exercises in parallel. +- Small exercise improvements. + ## 6.1.0 (2024-07-10) diff --git a/src/app_state.rs b/src/app_state.rs index 8995e81c..40c91308 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,11 +1,11 @@ -use anyhow::{bail, Context, Result}; -use ratatui::crossterm::style::Stylize; +use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; use std::{ fs::{self, File}, io::{Read, StdoutLock, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, + thread, }; use crate::{ @@ -373,34 +373,50 @@ impl AppState { if let Some(ind) = self.next_pending_exercise_ind() { self.set_current_exercise_ind(ind)?; - return Ok(ExercisesProgress::NewPending); } writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; - let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - for (exercise_ind, exercise) in self.exercises().iter().enumerate() { - write!(writer, "Running {exercise} ... ")?; - writer.flush()?; + let n_exercises = self.exercises.len(); - let success = exercise.run_exercise(&mut output, &self.target_dir)?; - if !success { - writeln!(writer, "{}\n", "FAILED".red())?; + let pending_exercise_ind = thread::scope(|s| { + let handles = self + .exercises + .iter_mut() + .map(|exercise| { + s.spawn(|| { + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + let success = exercise.run_exercise(&mut output, &self.target_dir)?; + exercise.done = success; + Ok::<_, Error>(success) + }) + }) + .collect::>(); - self.current_exercise_ind = exercise_ind; + for (exercise_ind, handle) in handles.into_iter().enumerate() { + write!(writer, "\rProgress: {exercise_ind}/{n_exercises}")?; + writer.flush()?; - // No check if the exercise is done before setting it to pending - // because no pending exercise was found. - self.exercises[exercise_ind].done = false; - self.n_done -= 1; - - self.write()?; - - return Ok(ExercisesProgress::NewPending); + let success = handle.join().unwrap()?; + if !success { + writer.write_all(b"\n\n")?; + return Ok(Some(exercise_ind)); + } } - writeln!(writer, "{}", "ok".green())?; + Ok::<_, Error>(None) + })?; + + if let Some(pending_exercise_ind) = pending_exercise_ind { + self.current_exercise_ind = pending_exercise_ind; + self.n_done = self + .exercises + .iter() + .filter(|exercise| exercise.done) + .count() as u16; + self.write()?; + return Ok(ExercisesProgress::NewPending); } // Write that the last exercise is done. @@ -426,7 +442,6 @@ Try running `cargo --version` to diagnose the problem."; const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. - "; const FENISH_LINE: &str = "+----------------------------------------------------+ From 74fab994e2133cc40718abe923645922785c2a57 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 28 Jul 2024 20:30:23 +0200 Subject: [PATCH 008/211] Make the output optional --- Cargo.toml | 4 +++ src/app_state.rs | 5 ++-- src/cmd.rs | 54 +++++++++++++++++++++++--------------- src/dev/check.rs | 5 ++-- src/exercise.rs | 65 +++++++++++++++++++++++++++++----------------- src/run.rs | 2 +- src/watch/state.rs | 2 +- 7 files changed, 84 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4aad110f..0577a6e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,3 +63,7 @@ panic = "abort" [package.metadata.release] pre-release-hook = ["./release-hook.sh"] + +# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102 +[lints.clippy] +needless_option_as_deref = "allow" diff --git a/src/app_state.rs b/src/app_state.rs index 40c91308..537732bc 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ clear_terminal, embedded::EMBEDDED_FILES, - exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY}, + exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, DEBUG_PROFILE, }; @@ -386,8 +386,7 @@ impl AppState { .iter_mut() .map(|exercise| { s.spawn(|| { - let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let success = exercise.run_exercise(&mut output, &self.target_dir)?; + let success = exercise.run_exercise(None, &self.target_dir)?; exercise.done = success; Ok::<_, Error>(success) }) diff --git a/src/cmd.rs b/src/cmd.rs index 6092f531..efeb5988 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,30 +1,42 @@ use anyhow::{Context, Result}; -use std::{io::Read, path::Path, process::Command}; +use std::{ + io::Read, + path::Path, + process::{Command, Stdio}, +}; /// Run a command with a description for a possible error and append the merged stdout and stderr. /// The boolean in the returned `Result` is true if the command's exit status is success. -pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Result { - let (mut reader, writer) = os_pipe::pipe() - .with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; +pub fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec>) -> Result { + let spawn = |mut cmd: Command| { + // NOTE: The closure drops `cmd` which prevents a pipe deadlock. + cmd.spawn() + .with_context(|| format!("Failed to run the command `{description}`")) + }; - let writer_clone = writer.try_clone().with_context(|| { - format!("Failed to clone the pipe writer for the command `{description}`") - })?; + let mut handle = if let Some(output) = output { + let (mut reader, writer) = os_pipe::pipe().with_context(|| { + format!("Failed to create a pipe to run the command `{description}``") + })?; - let mut handle = cmd - .stdout(writer_clone) - .stderr(writer) - .spawn() - .with_context(|| format!("Failed to run the command `{description}`"))?; + let writer_clone = writer.try_clone().with_context(|| { + format!("Failed to clone the pipe writer for the command `{description}`") + })?; - // Prevent pipe deadlock. - drop(cmd); + cmd.stdout(writer_clone).stderr(writer); + let handle = spawn(cmd)?; - reader - .read_to_end(output) - .with_context(|| format!("Failed to read the output of the command `{description}`"))?; + reader + .read_to_end(output) + .with_context(|| format!("Failed to read the output of the command `{description}`"))?; - output.push(b'\n'); + output.push(b'\n'); + + handle + } else { + cmd.stdout(Stdio::null()).stderr(Stdio::null()); + spawn(cmd)? + }; handle .wait() @@ -42,14 +54,14 @@ pub struct CargoCmd<'a> { /// Added as `--target-dir` if `Self::dev` is true. pub target_dir: &'a Path, /// The output buffer to append the merged stdout and stderr. - pub output: &'a mut Vec, + pub output: Option<&'a mut Vec>, /// true while developing Rustlings. pub dev: bool, } impl<'a> CargoCmd<'a> { /// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`. - pub fn run(&mut self) -> Result { + pub fn run(self) -> Result { let mut cmd = Command::new("cargo"); cmd.arg(self.subcommand); @@ -86,7 +98,7 @@ mod tests { cmd.arg("Hello"); let mut output = Vec::with_capacity(8); - run_cmd(cmd, "echo …", &mut output).unwrap(); + run_cmd(cmd, "echo …", Some(&mut output)).unwrap(); assert_eq!(output, b"Hello\n\n"); } diff --git a/src/dev/check.rs b/src/dev/check.rs index 5c35462c..2090dab8 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -184,8 +184,7 @@ fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<( error_occurred.store(true, atomic::Ordering::Relaxed); }; - let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - match exercise_info.run_exercise(&mut output, target_dir) { + match exercise_info.run_exercise(None, target_dir) { Ok(true) => error(b"Already solved!"), Ok(false) => (), Err(e) => error(e.to_string().as_bytes()), @@ -244,7 +243,7 @@ fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &P } let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - match exercise_info.run_solution(&mut output, target_dir) { + match exercise_info.run_solution(Some(&mut output), target_dir) { Ok(true) => { paths.lock().unwrap().insert(PathBuf::from(path)); } diff --git a/src/exercise.rs b/src/exercise.rs index 960eec0e..5cb434bf 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -19,8 +19,10 @@ pub const OUTPUT_CAPACITY: usize = 1 << 14; // Run an exercise binary and append its output to the `output` buffer. // Compilation must be done before calling this method. -fn run_bin(bin_name: &str, output: &mut Vec, target_dir: &Path) -> Result { - writeln!(output, "{}", "Output".underlined())?; +fn run_bin(bin_name: &str, mut output: Option<&mut Vec>, target_dir: &Path) -> Result { + if let Some(output) = output.as_deref_mut() { + writeln!(output, "{}", "Output".underlined())?; + } // 7 = "/debug/".len() let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len()); @@ -28,19 +30,25 @@ fn run_bin(bin_name: &str, output: &mut Vec, target_dir: &Path) -> Result, target_dir: &Path) -> Result { - output.clear(); + fn run( + &self, + bin_name: &str, + mut output: Option<&mut Vec>, + target_dir: &Path, + ) -> Result { + if let Some(output) = output.as_deref_mut() { + output.clear(); + } // Developing the official Rustlings. let dev = DEBUG_PROFILE && in_official_repo(); @@ -90,7 +105,7 @@ pub trait RunnableExercise { description: "cargo build …", hide_warnings: false, target_dir, - output, + output: output.as_deref_mut(), dev, } .run()?; @@ -99,7 +114,9 @@ pub trait RunnableExercise { } // Discard the output of `cargo build` because it will be shown again by Clippy. - output.clear(); + if let Some(output) = output.as_deref_mut() { + output.clear(); + } // `--profile test` is required to also check code with `[cfg(test)]`. let clippy_args: &[&str] = if self.strict_clippy() { @@ -114,7 +131,7 @@ pub trait RunnableExercise { description: "cargo clippy …", hide_warnings: false, target_dir, - output, + output: output.as_deref_mut(), dev, } .run()?; @@ -123,7 +140,7 @@ pub trait RunnableExercise { } if !self.test() { - return run_bin(bin_name, output, target_dir); + return run_bin(bin_name, output.as_deref_mut(), target_dir); } let test_success = CargoCmd { @@ -134,12 +151,12 @@ pub trait RunnableExercise { // Hide warnings because they are shown by Clippy. hide_warnings: true, target_dir, - output, + output: output.as_deref_mut(), dev, } .run()?; - let run_success = run_bin(bin_name, output, target_dir)?; + let run_success = run_bin(bin_name, output.as_deref_mut(), target_dir)?; Ok(test_success && run_success) } @@ -147,13 +164,13 @@ 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: &mut Vec, target_dir: &Path) -> Result { + fn run_exercise(&self, output: Option<&mut Vec>, target_dir: &Path) -> Result { self.run(self.name(), output, target_dir) } /// Compile, check and run the exercise's solution. /// The output is written to the `output` buffer after clearing it. - fn run_solution(&self, output: &mut Vec, target_dir: &Path) -> Result { + fn run_solution(&self, output: Option<&mut Vec>, target_dir: &Path) -> Result { let name = self.name(); let mut bin_name = String::with_capacity(name.len()); bin_name.push_str(name); diff --git a/src/run.rs b/src/run.rs index 3965a0d0..606f0a43 100644 --- a/src/run.rs +++ b/src/run.rs @@ -11,7 +11,7 @@ use crate::{ pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let success = exercise.run_exercise(&mut output, app_state.target_dir())?; + let success = exercise.run_exercise(Some(&mut output), app_state.target_dir())?; let mut stdout = io::stdout().lock(); stdout.write_all(&output)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 26ff4118..8f01db7d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -54,7 +54,7 @@ impl<'a> WatchState<'a> { let success = self .app_state .current_exercise() - .run_exercise(&mut self.output, self.app_state.target_dir())?; + .run_exercise(Some(&mut self.output), self.app_state.target_dir())?; if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { From c8fddd8f62302395900ae6038a45653f22c994de Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 31 Jul 2024 18:53:25 +0200 Subject: [PATCH 009/211] Add Github profile links for every author --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0577a6e8..1e0228da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,10 @@ exclude = [ [workspace.package] version = "6.1.0" authors = [ - "Liv ", - "Mo Bitar ", + "Mo Bitar ", # https://github.com/mo8it + "Liv ", # https://github.com/shadows-withal # Alumni - "Carol (Nichols || Goulding) ", + "Carol (Nichols || Goulding) ", # https://github.com/carols10cents ] repository = "https://github.com/rust-lang/rustlings" license = "MIT" From 2ad408f2b8eb357a3f2b44b7facb9e3bfe4f35a2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 31 Jul 2024 18:54:24 +0200 Subject: [PATCH 010/211] Update deps --- Cargo.lock | 21 +++++++++++---------- Cargo.toml | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afb98cd9..fab8e584 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,9 +357,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ "hashbrown", ] @@ -587,11 +587,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -617,9 +618,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", @@ -698,18 +699,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.17" +version = "0.22.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" +checksum = "1490595c74d930da779e944f5ba2ecdf538af67df1a9848cbd156af43c1b7cf0" dependencies = [ "indexmap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1e0228da..ad9164d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ edition = "2021" [workspace.dependencies] serde = { version = "1.0.204", features = ["derive"] } -toml_edit = { version = "0.22.17", default-features = false, features = ["parse", "serde"] } +toml_edit = { version = "0.22.18", default-features = false, features = ["parse", "serde"] } [package] name = "rustlings" @@ -51,7 +51,7 @@ notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.0" ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" } -serde_json = "1.0.120" +serde_json = "1.0.121" serde.workspace = true toml_edit.workspace = true From 802b97b2edb142ad6bc4ee10ccc16ece7c6dc346 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 01:07:31 +0200 Subject: [PATCH 011/211] Set stdin to null when running the binary of an exercise --- src/cmd.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cmd.rs b/src/cmd.rs index efeb5988..d158bfb9 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -10,7 +10,8 @@ use std::{ pub fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec>) -> Result { let spawn = |mut cmd: Command| { // NOTE: The closure drops `cmd` which prevents a pipe deadlock. - cmd.spawn() + cmd.stdin(Stdio::null()) + .spawn() .with_context(|| format!("Failed to run the command `{description}`")) }; From 766f3c50ec20c9b3fbf95bddf9fc095ce65cef78 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 01:07:56 +0200 Subject: [PATCH 012/211] Add hint to run `dev check` again after `dev update` --- src/dev/check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 2090dab8..1087138c 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -41,7 +41,7 @@ fn check_cargo_toml( bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); } - bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it"); + bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again"); } Ok(()) From dacdce1ea245523bf7bf380f91bff76e7f867315 Mon Sep 17 00:00:00 2001 From: Yudai Kawabuchi Date: Thu, 1 Aug 2024 09:47:50 +0900 Subject: [PATCH 013/211] fix: update struct name in hashmap3 --- exercises/11_hashmaps/hashmaps3.rs | 4 ++-- solutions/11_hashmaps/hashmaps3.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs index 9f8fdd78..7e9584d1 100644 --- a/exercises/11_hashmaps/hashmaps3.rs +++ b/exercises/11_hashmaps/hashmaps3.rs @@ -10,12 +10,12 @@ use std::collections::HashMap; // A structure to store the goal details of a team. #[derive(Default)] -struct Team { +struct TeamScores { goals_scored: u8, goals_conceded: u8, } -fn build_scores_table(results: &str) -> HashMap<&str, Team> { +fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { // The name of the team is the key and its associated struct is the value. let mut scores = HashMap::new(); diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs index 54f480b9..c075b0f6 100644 --- a/solutions/11_hashmaps/hashmaps3.rs +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -10,12 +10,12 @@ use std::collections::HashMap; // A structure to store the goal details of a team. #[derive(Default)] -struct Team { +struct TeamScores { goals_scored: u8, goals_conceded: u8, } -fn build_scores_table(results: &str) -> HashMap<&str, Team> { +fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { // The name of the team is the key and its associated struct is the value. let mut scores = HashMap::new(); @@ -28,13 +28,13 @@ fn build_scores_table(results: &str) -> HashMap<&str, Team> { let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); // Insert the default with zeros if a team doesn't exist yet. - let team_1 = scores.entry(team_1_name).or_insert_with(Team::default); + let team_1 = scores.entry(team_1_name).or_insert_with(TeamScores::default); // Update the values. team_1.goals_scored += team_1_score; team_1.goals_conceded += team_2_score; // Similarely for the second team. - let team_2 = scores.entry(team_2_name).or_insert_with(Team::default); + let team_2 = scores.entry(team_2_name).or_insert_with(TeamScores::default); team_2.goals_scored += team_2_score; team_2.goals_conceded += team_1_score; } From e65ae09789410c230a863ad219b90c434adf5e4f Mon Sep 17 00:00:00 2001 From: Yudai Kawabuchi Date: Thu, 1 Aug 2024 09:55:25 +0900 Subject: [PATCH 014/211] fix format --- solutions/11_hashmaps/hashmaps3.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs index c075b0f6..9c58b2d3 100644 --- a/solutions/11_hashmaps/hashmaps3.rs +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -28,13 +28,17 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); // Insert the default with zeros if a team doesn't exist yet. - let team_1 = scores.entry(team_1_name).or_insert_with(TeamScores::default); + let team_1 = scores + .entry(team_1_name) + .or_insert_with(TeamScores::default); // Update the values. team_1.goals_scored += team_1_score; team_1.goals_conceded += team_2_score; // Similarely for the second team. - let team_2 = scores.entry(team_2_name).or_insert_with(TeamScores::default); + let team_2 = scores + .entry(team_2_name) + .or_insert_with(TeamScores::default); team_2.goals_scored += team_2_score; team_2.goals_conceded += team_1_score; } From 455d87caddc69d1b2589cd11638a341e136c891b Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 11:26:30 +0200 Subject: [PATCH 015/211] Fix capacity --- src/exercise.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index 5cb434bf..605d5f10 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -172,7 +172,7 @@ pub trait RunnableExercise { /// The output is written to the `output` buffer after clearing it. fn run_solution(&self, output: Option<&mut Vec>, target_dir: &Path) -> Result { let name = self.name(); - let mut bin_name = String::with_capacity(name.len()); + let mut bin_name = String::with_capacity(name.len() + 4); bin_name.push_str(name); bin_name.push_str("_sol"); From 33a56803281ec4ec84fbe61919e9c825f1f446f7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 11:28:26 +0200 Subject: [PATCH 016/211] Hide `cargo build` warnings if there is no output --- src/exercise.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index 605d5f10..8a040613 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -103,7 +103,7 @@ pub trait RunnableExercise { args: &[], bin_name, description: "cargo build …", - hide_warnings: false, + hide_warnings: output.is_none(), target_dir, output: output.as_deref_mut(), dev, From c7590dd752ab35d06a85f016e88921f10934e6aa Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 15:23:54 +0200 Subject: [PATCH 017/211] Improve the runner --- src/app_state.rs | 51 ++++-------------- src/cmd.rs | 126 +++++++++++++++++++++++++++++++++------------ src/dev.rs | 4 +- src/dev/check.rs | 34 ++++++------ src/dev/update.rs | 3 +- src/exercise.rs | 103 +++++++++++++----------------------- src/main.rs | 18 +------ src/run.rs | 2 +- src/watch/state.rs | 2 +- 9 files changed, 162 insertions(+), 181 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 537732bc..ea99746b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,19 +1,18 @@ use anyhow::{bail, Context, Error, Result}; -use serde::Deserialize; use std::{ fs::{self, File}, io::{Read, StdoutLock, Write}, - path::{Path, PathBuf}, + path::Path, process::{Command, Stdio}, thread, }; use crate::{ clear_terminal, + cmd::CmdRunner, embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, - DEBUG_PROFILE, }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; @@ -34,31 +33,6 @@ pub enum StateFileStatus { NotRead, } -// Parses parts of the output of `cargo metadata`. -#[derive(Deserialize)] -struct CargoMetadata { - target_directory: PathBuf, -} - -pub fn parse_target_dir() -> Result { - // Get the target directory from Cargo. - let metadata_output = Command::new("cargo") - .arg("metadata") - .arg("-q") - .arg("--format-version") - .arg("1") - .arg("--no-deps") - .stdin(Stdio::null()) - .stderr(Stdio::inherit()) - .output() - .context(CARGO_METADATA_ERR)? - .stdout; - - serde_json::de::from_slice::(&metadata_output) - .context("Failed to read the field `target_directory` from the `cargo metadata` output") - .map(|metadata| metadata.target_directory) -} - pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -68,8 +42,7 @@ pub struct AppState { // Preallocated buffer for reading and writing the state file. file_buf: Vec, official_exercises: bool, - // Cargo's target directory. - target_dir: PathBuf, + cmd_runner: CmdRunner, } impl AppState { @@ -123,7 +96,7 @@ impl AppState { exercise_infos: Vec, final_message: String, ) -> Result<(Self, StateFileStatus)> { - let target_dir = parse_target_dir()?; + let cmd_runner = CmdRunner::build()?; let exercises = exercise_infos .into_iter() @@ -157,7 +130,7 @@ impl AppState { final_message, file_buf: Vec::with_capacity(2048), official_exercises: !Path::new("info.toml").exists(), - target_dir, + cmd_runner, }; let state_file_status = slf.update_from_file(); @@ -186,8 +159,8 @@ impl AppState { } #[inline] - pub fn target_dir(&self) -> &Path { - &self.target_dir + pub fn cmd_runner(&self) -> &CmdRunner { + &self.cmd_runner } // Write the state file. @@ -336,7 +309,7 @@ impl AppState { /// Official exercises: Dump the solution file form the binary and return its path. /// Third-party exercises: Check if a solution file exists and return its path in that case. pub fn current_solution_path(&self) -> Result> { - if DEBUG_PROFILE { + if cfg!(debug_assertions) { return Ok(None); } @@ -386,7 +359,7 @@ impl AppState { .iter_mut() .map(|exercise| { s.spawn(|| { - let success = exercise.run_exercise(None, &self.target_dir)?; + let success = exercise.run_exercise(None, &self.cmd_runner)?; exercise.done = success; Ok::<_, Error>(success) }) @@ -434,10 +407,6 @@ impl AppState { } } -const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …` -Did you already install Rust? -Try running `cargo --version` to diagnose the problem."; - const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. @@ -490,7 +459,7 @@ mod tests { final_message: String::new(), file_buf: Vec::new(), official_exercises: true, - target_dir: PathBuf::new(), + cmd_runner: CmdRunner::build().unwrap(), }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/cmd.rs b/src/cmd.rs index d158bfb9..1891a283 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,13 +1,14 @@ use anyhow::{Context, Result}; +use serde::Deserialize; use std::{ io::Read, - path::Path, + path::PathBuf, process::{Command, Stdio}, }; /// Run a command with a description for a possible error and append the merged stdout and stderr. /// The boolean in the returned `Result` is true if the command's exit status is success. -pub fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec>) -> Result { +fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec>) -> Result { let spawn = |mut cmd: Command| { // NOTE: The closure drops `cmd` which prevents a pipe deadlock. cmd.stdin(Stdio::null()) @@ -45,50 +46,107 @@ pub fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec> .map(|status| status.success()) } -pub struct CargoCmd<'a> { - pub subcommand: &'a str, - pub args: &'a [&'a str], - pub bin_name: &'a str, - pub description: &'a str, - /// RUSTFLAGS="-A warnings" - pub hide_warnings: bool, - /// Added as `--target-dir` if `Self::dev` is true. - pub target_dir: &'a Path, - /// The output buffer to append the merged stdout and stderr. - pub output: Option<&'a mut Vec>, - /// true while developing Rustlings. - pub dev: bool, +// Parses parts of the output of `cargo metadata`. +#[derive(Deserialize)] +struct CargoMetadata { + target_directory: PathBuf, } -impl<'a> CargoCmd<'a> { - /// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`. - pub fn run(self) -> Result { +pub struct CmdRunner { + target_dir: PathBuf, +} + +impl CmdRunner { + pub fn build() -> Result { + // Get the target directory from Cargo. + let metadata_output = Command::new("cargo") + .arg("metadata") + .arg("-q") + .arg("--format-version") + .arg("1") + .arg("--no-deps") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_METADATA_ERR)? + .stdout; + + let target_dir = serde_json::de::from_slice::(&metadata_output) + .context("Failed to read the field `target_directory` from the `cargo metadata` output") + .map(|metadata| metadata.target_directory)?; + + Ok(Self { target_dir }) + } + + pub fn cargo<'out>( + &self, + subcommand: &str, + bin_name: &str, + output: Option<&'out mut Vec>, + ) -> CargoSubcommand<'out> { let mut cmd = Command::new("cargo"); - cmd.arg(self.subcommand); + cmd.arg(subcommand).arg("-q").arg("--bin").arg(bin_name); // A hack to make `cargo run` work when developing Rustlings. - if self.dev { - cmd.arg("--manifest-path") - .arg("dev/Cargo.toml") - .arg("--target-dir") - .arg(self.target_dir); + #[cfg(debug_assertions)] + cmd.arg("--manifest-path") + .arg("dev/Cargo.toml") + .arg("--target-dir") + .arg(&self.target_dir); + + if output.is_some() { + cmd.arg("--color").arg("always"); } - cmd.arg("--color") - .arg("always") - .arg("-q") - .arg("--bin") - .arg(self.bin_name) - .args(self.args); + CargoSubcommand { cmd, output } + } - if self.hide_warnings { - cmd.env("RUSTFLAGS", "-A warnings"); - } + /// The boolean in the returned `Result` is true if the command's exit status is success. + pub fn run_debug_bin(&self, bin_name: &str, output: Option<&mut Vec>) -> Result { + // 7 = "/debug/".len() + let mut bin_path = + PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len()); + bin_path.push(&self.target_dir); + bin_path.push("debug"); + bin_path.push(bin_name); - run_cmd(cmd, self.description, self.output) + run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output) } } +pub struct CargoSubcommand<'out> { + cmd: Command, + output: Option<&'out mut Vec>, +} + +impl<'out> CargoSubcommand<'out> { + #[inline] + pub fn args<'arg, I>(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + { + self.cmd.args(args); + self + } + + /// RUSTFLAGS="-A warnings" + #[inline] + pub fn hide_warnings(&mut self) -> &mut Self { + self.cmd.env("RUSTFLAGS", "-A warnings"); + self + } + + /// 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) + } +} + +const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …` +Did you already install Rust? +Try running `cargo --version` to diagnose the problem."; + #[cfg(test)] mod tests { use super::*; diff --git a/src/dev.rs b/src/dev.rs index 5f7e64c8..8af40d69 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -2,8 +2,6 @@ use anyhow::{bail, Context, Result}; use clap::Subcommand; use std::path::PathBuf; -use crate::DEBUG_PROFILE; - mod check; mod new; mod update; @@ -32,7 +30,7 @@ impl DevCommands { pub fn run(self) -> Result<()> { match self { Self::New { path, no_git } => { - if DEBUG_PROFILE { + if cfg!(debug_assertions) { bail!("Disabled in the debug build"); } diff --git a/src/dev/check.rs b/src/dev/check.rs index 1087138c..db5b21fc 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -12,11 +12,11 @@ use std::{ }; use crate::{ - app_state::parse_target_dir, cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, + cmd::CmdRunner, exercise::{RunnableExercise, OUTPUT_CAPACITY}, info_file::{ExerciseInfo, InfoFile}, - CURRENT_FORMAT_VERSION, DEBUG_PROFILE, + CURRENT_FORMAT_VERSION, }; // Find a char that isn't allowed in the exercise's `name` or `dir`. @@ -37,8 +37,8 @@ fn check_cargo_toml( append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); if old_bins != new_bins { - if DEBUG_PROFILE { - bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); + if cfg!(debug_assertions) { + bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again"); } bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again"); @@ -162,7 +162,7 @@ fn check_unexpected_files( Ok(()) } -fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<()> { +fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { let error_occurred = AtomicBool::new(false); println!( @@ -184,7 +184,7 @@ fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<( error_occurred.store(true, atomic::Ordering::Relaxed); }; - match exercise_info.run_exercise(None, target_dir) { + match exercise_info.run_exercise(None, cmd_runner) { Ok(true) => error(b"Already solved!"), Ok(false) => (), Err(e) => error(e.to_string().as_bytes()), @@ -200,7 +200,7 @@ fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<( Ok(()) } -fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> { +fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), @@ -210,10 +210,14 @@ fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> { let info_file_paths = check_info_file_exercises(info_file)?; check_unexpected_files("exercises", &info_file_paths)?; - check_exercises_unsolved(info_file, target_dir) + check_exercises_unsolved(info_file, cmd_runner) } -fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &Path) -> Result<()> { +fn check_solutions( + require_solutions: bool, + info_file: &InfoFile, + cmd_runner: &CmdRunner, +) -> Result<()> { let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len())); let error_occurred = AtomicBool::new(false); @@ -243,7 +247,7 @@ fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &P } let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - match exercise_info.run_solution(Some(&mut output), target_dir) { + match exercise_info.run_solution(Some(&mut output), cmd_runner) { Ok(true) => { paths.lock().unwrap().insert(PathBuf::from(path)); } @@ -266,8 +270,8 @@ fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &P pub fn check(require_solutions: bool) -> Result<()> { let info_file = InfoFile::parse()?; - // A hack to make `cargo run -- dev check` work when developing Rustlings. - if DEBUG_PROFILE { + if cfg!(debug_assertions) { + // A hack to make `cargo run -- dev check` work when developing Rustlings. check_cargo_toml( &info_file.exercises, include_str!("../../dev-Cargo.toml"), @@ -279,9 +283,9 @@ pub fn check(require_solutions: bool) -> Result<()> { check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?; } - let target_dir = parse_target_dir()?; - check_exercises(&info_file, &target_dir)?; - check_solutions(require_solutions, &info_file, &target_dir)?; + let cmd_runner = CmdRunner::build()?; + check_exercises(&info_file, &cmd_runner)?; + check_solutions(require_solutions, &info_file, &cmd_runner)?; println!("\nEverything looks fine!"); diff --git a/src/dev/update.rs b/src/dev/update.rs index 66efe3d0..680d302f 100644 --- a/src/dev/update.rs +++ b/src/dev/update.rs @@ -4,7 +4,6 @@ use std::fs; use crate::{ cargo_toml::updated_cargo_toml, info_file::{ExerciseInfo, InfoFile}, - DEBUG_PROFILE, }; // Update the `Cargo.toml` file. @@ -27,7 +26,7 @@ pub fn update() -> Result<()> { let info_file = InfoFile::parse()?; // A hack to make `cargo run -- dev update` work when developing Rustlings. - if DEBUG_PROFILE { + if cfg!(debug_assertions) { update_cargo_toml( &info_file.exercises, include_str!("../../dev-Cargo.toml"), diff --git a/src/exercise.rs b/src/exercise.rs index 8a040613..48b98896 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -3,38 +3,25 @@ use ratatui::crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, io::Write, - path::{Path, PathBuf}, - process::Command, }; -use crate::{ - cmd::{run_cmd, CargoCmd}, - in_official_repo, - terminal_link::TerminalFileLink, - DEBUG_PROFILE, -}; +use crate::{cmd::CmdRunner, terminal_link::TerminalFileLink}; /// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; // Run an exercise binary and append its output to the `output` buffer. // Compilation must be done before calling this method. -fn run_bin(bin_name: &str, mut output: Option<&mut Vec>, target_dir: &Path) -> Result { +fn run_bin( + bin_name: &str, + mut output: Option<&mut Vec>, + cmd_runner: &CmdRunner, +) -> Result { if let Some(output) = output.as_deref_mut() { writeln!(output, "{}", "Output".underlined())?; } - // 7 = "/debug/".len() - let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len()); - bin_path.push(target_dir); - bin_path.push("debug"); - bin_path.push(bin_name); - - let success = run_cmd( - Command::new(&bin_path), - &bin_path.to_string_lossy(), - output.as_deref_mut(), - )?; + let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?; if let Some(output) = output { if !success { @@ -89,26 +76,20 @@ pub trait RunnableExercise { &self, bin_name: &str, mut output: Option<&mut Vec>, - target_dir: &Path, + cmd_runner: &CmdRunner, ) -> Result { - if let Some(output) = output.as_deref_mut() { + let output_is_none = if let Some(output) = output.as_deref_mut() { output.clear(); - } + false + } else { + true + }; - // Developing the official Rustlings. - let dev = DEBUG_PROFILE && in_official_repo(); - - let build_success = CargoCmd { - subcommand: "build", - args: &[], - bin_name, - description: "cargo build …", - hide_warnings: output.is_none(), - target_dir, - output: output.as_deref_mut(), - dev, + let mut build_cmd = cmd_runner.cargo("build", bin_name, output.as_deref_mut()); + if output_is_none { + build_cmd.hide_warnings(); } - .run()?; + let build_success = build_cmd.run("cargo build …")?; if !build_success { return Ok(false); } @@ -118,45 +99,33 @@ pub trait RunnableExercise { output.clear(); } + let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut()); + // `--profile test` is required to also check code with `[cfg(test)]`. - let clippy_args: &[&str] = if self.strict_clippy() { - &["--profile", "test", "--", "-D", "warnings"] + if self.strict_clippy() { + clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]); } else { - &["--profile", "test"] - }; - let clippy_success = CargoCmd { - subcommand: "clippy", - args: clippy_args, - bin_name, - description: "cargo clippy …", - hide_warnings: false, - target_dir, - output: output.as_deref_mut(), - dev, + clippy_cmd.args(["--profile", "test"]); } - .run()?; + + let clippy_success = clippy_cmd.run("cargo clippy …")?; if !clippy_success { return Ok(false); } if !self.test() { - return run_bin(bin_name, output.as_deref_mut(), target_dir); + return run_bin(bin_name, output.as_deref_mut(), cmd_runner); } - let test_success = CargoCmd { - subcommand: "test", - args: &["--", "--color", "always", "--show-output"], - bin_name, - description: "cargo test …", - // Hide warnings because they are shown by Clippy. - hide_warnings: true, - target_dir, - output: output.as_deref_mut(), - dev, + let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut()); + if !output_is_none { + test_cmd.args(["--", "--color", "always", "--show-output"]); } - .run()?; + // Hide warnings because they are shown by Clippy. + test_cmd.hide_warnings(); + let test_success = test_cmd.run("cargo test …")?; - let run_success = run_bin(bin_name, output.as_deref_mut(), target_dir)?; + let run_success = run_bin(bin_name, output, cmd_runner)?; Ok(test_success && run_success) } @@ -164,19 +133,19 @@ 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>, target_dir: &Path) -> Result { - self.run(self.name(), output, target_dir) + fn run_exercise(&self, output: Option<&mut Vec>, cmd_runner: &CmdRunner) -> Result { + self.run(self.name(), output, cmd_runner) } /// Compile, check and run the exercise's solution. /// The output is written to the `output` buffer after clearing it. - fn run_solution(&self, output: Option<&mut Vec>, target_dir: &Path) -> Result { + fn run_solution(&self, output: Option<&mut Vec>, cmd_runner: &CmdRunner) -> Result { let name = self.name(); let mut bin_name = String::with_capacity(name.len() + 4); bin_name.push_str(name); bin_name.push_str("_sol"); - self.run(&bin_name, output, target_dir) + self.run(&bin_name, output, cmd_runner) } } diff --git a/src/main.rs b/src/main.rs index 658d551d..1f0afdec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,22 +24,6 @@ mod terminal_link; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; -const DEBUG_PROFILE: bool = { - #[allow(unused_assignments, unused_mut)] - let mut debug_profile = false; - - #[cfg(debug_assertions)] - { - debug_profile = true; - } - - debug_profile -}; - -// The current directory is the official Rustligns repository. -fn in_official_repo() -> bool { - Path::new("dev/rustlings-repo.txt").exists() -} fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") @@ -89,7 +73,7 @@ enum Subcommands { fn main() -> Result<()> { let args = Args::parse(); - if !DEBUG_PROFILE && in_official_repo() { + if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() { bail!("{OLD_METHOD_ERR}"); } diff --git a/src/run.rs b/src/run.rs index 606f0a43..964e13b8 100644 --- a/src/run.rs +++ b/src/run.rs @@ -11,7 +11,7 @@ use crate::{ pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let success = exercise.run_exercise(Some(&mut output), app_state.target_dir())?; + let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?; let mut stdout = io::stdout().lock(); stdout.write_all(&output)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 8f01db7d..46f48d9f 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -54,7 +54,7 @@ impl<'a> WatchState<'a> { let success = self .app_state .current_exercise() - .run_exercise(Some(&mut self.output), self.app_state.target_dir())?; + .run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?; if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { From e0f0944bffe607af9e6059df7a65d4b9b0b99e4f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 15:53:32 +0200 Subject: [PATCH 018/211] Refactor check_solutions --- src/dev/check.rs | 99 ++++++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index db5b21fc..0d1e5878 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,13 +1,10 @@ -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Error, Result}; use std::{ cmp::Ordering, fs::{self, read_dir, OpenOptions}, io::{self, Read, Write}, path::{Path, PathBuf}, - sync::{ - atomic::{self, AtomicBool}, - Mutex, - }, + sync::atomic::{self, AtomicBool}, thread, }; @@ -213,56 +210,76 @@ fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { check_exercises_unsolved(info_file, cmd_runner) } +enum SolutionCheck { + Success { sol_path: String }, + MissingRequired, + MissingOptional, + RunFailure { output: Vec }, + Err(Error), +} + fn check_solutions( require_solutions: bool, info_file: &InfoFile, cmd_runner: &CmdRunner, ) -> Result<()> { - let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len())); - let error_occurred = AtomicBool::new(false); - println!("Running all solutions. This may take a while…\n"); - thread::scope(|s| { - for exercise_info in &info_file.exercises { - s.spawn(|| { - let error = |e| { - let mut stderr = io::stderr().lock(); - stderr.write_all(e).unwrap(); - stderr - .write_all(b"\nFailed to run the solution of the exercise ") - .unwrap(); - stderr.write_all(exercise_info.name.as_bytes()).unwrap(); - stderr.write_all(SEPARATOR).unwrap(); - error_occurred.store(true, atomic::Ordering::Relaxed); - }; + let sol_paths = thread::scope(|s| { + let handles = info_file + .exercises + .iter() + .map(|exercise_info| { + s.spawn(|| { + let sol_path = exercise_info.sol_path(); + if !Path::new(&sol_path).exists() { + if require_solutions { + return SolutionCheck::MissingRequired; + } - let path = exercise_info.sol_path(); - if !Path::new(&path).exists() { - if require_solutions { - error(b"Solution missing"); + return SolutionCheck::MissingOptional; } - // No solution to check. - return; - } - - let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - match exercise_info.run_solution(Some(&mut output), cmd_runner) { - Ok(true) => { - paths.lock().unwrap().insert(PathBuf::from(path)); + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + match exercise_info.run_solution(Some(&mut output), cmd_runner) { + Ok(true) => SolutionCheck::Success { sol_path }, + Ok(false) => SolutionCheck::RunFailure { output }, + Err(e) => SolutionCheck::Err(e), } - Ok(false) => error(&output), - Err(e) => error(e.to_string().as_bytes()), + }) + }) + .collect::>(); + + let mut sol_paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + + for (exercise_name, handle) in info_file + .exercises + .iter() + .map(|exercise_info| &exercise_info.name) + .zip(handles) + { + match handle.join() { + Ok(SolutionCheck::Success { sol_path }) => { + sol_paths.insert(PathBuf::from(sol_path)); } - }); + Ok(SolutionCheck::MissingRequired) => { + bail!("The solution of the exercise {exercise_name} is missing"); + } + Ok(SolutionCheck::MissingOptional) => (), + Ok(SolutionCheck::RunFailure { output }) => { + io::stderr().lock().write_all(&output)?; + bail!("Running the solution of the exercise {exercise_name} failed with the error above"); + } + Ok(SolutionCheck::Err(e)) => return Err(e), + Err(_) => { + bail!("Panic while trying to run the solution of the exericse {exercise_name}"); + } + } } - }); - if error_occurred.load(atomic::Ordering::Relaxed) { - bail!("At least one solution failed. See the output above."); - } + Ok(sol_paths) + })?; - check_unexpected_files("solutions", &paths.into_inner().unwrap())?; + check_unexpected_files("solutions", &sol_paths)?; Ok(()) } From 65a8f6bb4b542a24bf80265cf5080b5c8f51fb7e Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 19:14:09 +0200 Subject: [PATCH 019/211] Run rustfmt on solutions in `dev check` --- .github/workflows/rust.yml | 2 -- CHANGELOG.md | 1 + Cargo.toml | 2 +- src/dev/check.rs | 26 +++++++++++++++++++++----- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5c423e24..80f052d6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -24,8 +24,6 @@ jobs: globs: "exercises/**/*.md" - name: Run cargo fmt run: cargo fmt --all --check - - name: Run rustfmt on solutions - run: rustfmt --check --edition 2021 --color always solutions/**/*.rs test: runs-on: ${{ matrix.os }} strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a2fb24..846c67f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Run the final check of all exercises in parallel. - Small exercise improvements. +- `dev check`: Check that all solutions are formatted with `rustfmt`. diff --git a/Cargo.toml b/Cargo.toml index ad9164d8..d4466ce2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ authors = [ ] repository = "https://github.com/rust-lang/rustlings" license = "MIT" -edition = "2021" +edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions. [workspace.dependencies] serde = { version = "1.0.204", features = ["derive"] } diff --git a/src/dev/check.rs b/src/dev/check.rs index 0d1e5878..cf1d9760 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -4,6 +4,7 @@ use std::{ fs::{self, read_dir, OpenOptions}, io::{self, Read, Write}, path::{Path, PathBuf}, + process::{Command, Stdio}, sync::atomic::{self, AtomicBool}, thread, }; @@ -224,7 +225,7 @@ fn check_solutions( cmd_runner: &CmdRunner, ) -> Result<()> { println!("Running all solutions. This may take a while…\n"); - let sol_paths = thread::scope(|s| { + thread::scope(|s| { let handles = info_file .exercises .iter() @@ -250,6 +251,14 @@ fn check_solutions( .collect::>(); let mut sol_paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + let mut fmt_cmd = Command::new("rustfmt"); + fmt_cmd + .arg("--check") + .arg("--edition") + .arg("2021") + .arg("--color") + .arg("--always") + .stdin(Stdio::null()); for (exercise_name, handle) in info_file .exercises @@ -259,6 +268,7 @@ fn check_solutions( { match handle.join() { Ok(SolutionCheck::Success { sol_path }) => { + fmt_cmd.arg(&sol_path); sol_paths.insert(PathBuf::from(sol_path)); } Ok(SolutionCheck::MissingRequired) => { @@ -276,12 +286,18 @@ fn check_solutions( } } - Ok(sol_paths) - })?; + let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths)); - check_unexpected_files("solutions", &sol_paths)?; + if !fmt_cmd + .status() + .context("Failed to run `rustfmt` on all solution files")? + .success() + { + bail!("Some solutions aren't formatted. Run `rustfmt` on them"); + } - Ok(()) + handle.join().unwrap() + }) } pub fn check(require_solutions: bool) -> Result<()> { From 3fc462f90fec10cfacd3d81e944b11bb776a2941 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 19:17:40 +0200 Subject: [PATCH 020/211] Fix tests --- src/dev/check.rs | 17 +++++++---------- src/dev/update.rs | 23 +++++++++-------------- tests/test_exercises/Cargo.toml | 11 ----------- tests/test_exercises/dev/Cargo.toml | 11 +++++++++++ 4 files changed, 27 insertions(+), 35 deletions(-) delete mode 100644 tests/test_exercises/Cargo.toml create mode 100644 tests/test_exercises/dev/Cargo.toml diff --git a/src/dev/check.rs b/src/dev/check.rs index cf1d9760..0b243b2f 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -25,10 +25,13 @@ fn forbidden_char(input: &str) -> Option { // Check that the Cargo.toml file is up-to-date. fn check_cargo_toml( exercise_infos: &[ExerciseInfo], - current_cargo_toml: &str, + cargo_toml_path: &str, exercise_path_prefix: &[u8], ) -> Result<()> { - let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + let current_cargo_toml = fs::read_to_string(cargo_toml_path) + .with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?; + + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(¤t_cargo_toml)?; let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY); @@ -305,15 +308,9 @@ pub fn check(require_solutions: bool) -> Result<()> { if cfg!(debug_assertions) { // A hack to make `cargo run -- dev check` work when developing Rustlings. - check_cargo_toml( - &info_file.exercises, - include_str!("../../dev-Cargo.toml"), - b"../", - )?; + check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?; } else { - let current_cargo_toml = - fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; - check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?; + check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?; } let cmd_runner = CmdRunner::build()?; diff --git a/src/dev/update.rs b/src/dev/update.rs index 680d302f..6de3c8f7 100644 --- a/src/dev/update.rs +++ b/src/dev/update.rs @@ -9,12 +9,14 @@ use crate::{ // Update the `Cargo.toml` file. fn update_cargo_toml( exercise_infos: &[ExerciseInfo], - current_cargo_toml: &str, - exercise_path_prefix: &[u8], cargo_toml_path: &str, + exercise_path_prefix: &[u8], ) -> Result<()> { + let current_cargo_toml = fs::read_to_string(cargo_toml_path) + .with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?; + let updated_cargo_toml = - updated_cargo_toml(exercise_infos, current_cargo_toml, exercise_path_prefix)?; + updated_cargo_toml(exercise_infos, ¤t_cargo_toml, exercise_path_prefix)?; fs::write(cargo_toml_path, updated_cargo_toml) .context("Failed to write the `Cargo.toml` file")?; @@ -25,21 +27,14 @@ fn update_cargo_toml( pub fn update() -> Result<()> { let info_file = InfoFile::parse()?; - // A hack to make `cargo run -- dev update` work when developing Rustlings. if cfg!(debug_assertions) { - update_cargo_toml( - &info_file.exercises, - include_str!("../../dev-Cargo.toml"), - b"../", - "dev/Cargo.toml", - ) - .context("Failed to update the file `dev/Cargo.toml`")?; + // A hack to make `cargo run -- dev update` work when developing Rustlings. + update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../") + .context("Failed to update the file `dev/Cargo.toml`")?; println!("Updated `dev/Cargo.toml`"); } else { - let current_cargo_toml = - fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; - update_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"", "Cargo.toml") + update_cargo_toml(&info_file.exercises, "Cargo.toml", &[]) .context("Failed to update the file `Cargo.toml`")?; println!("Updated `Cargo.toml`"); diff --git a/tests/test_exercises/Cargo.toml b/tests/test_exercises/Cargo.toml deleted file mode 100644 index 6b817518..00000000 --- a/tests/test_exercises/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -bin = [ - { name = "compilation_success", path = "exercises/compilation_success.rs" }, - { name = "compilation_failure", path = "exercises/compilation_failure.rs" }, - { name = "test_success", path = "exercises/test_success.rs" }, - { name = "test_failure", path = "exercises/test_failure.rs" }, -] - -[package] -name = "test_exercises" -edition = "2021" -publish = false diff --git a/tests/test_exercises/dev/Cargo.toml b/tests/test_exercises/dev/Cargo.toml new file mode 100644 index 00000000..01fe7c10 --- /dev/null +++ b/tests/test_exercises/dev/Cargo.toml @@ -0,0 +1,11 @@ +bin = [ + { name = "compilation_success", path = "../exercises/compilation_success.rs" }, + { name = "compilation_failure", path = "../exercises/compilation_failure.rs" }, + { name = "test_success", path = "../exercises/test_success.rs" }, + { name = "test_failure", path = "../exercises/test_failure.rs" }, +] + +[package] +name = "test_exercises" +edition = "2021" +publish = false From 700a065abd4d9536ca8f12fa18975025fc2bc1ac Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 19:19:14 +0200 Subject: [PATCH 021/211] Fix rustfmt option --- src/dev/check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 0b243b2f..c89eb35d 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -260,7 +260,7 @@ fn check_solutions( .arg("--edition") .arg("2021") .arg("--color") - .arg("--always") + .arg("always") .stdin(Stdio::null()); for (exercise_name, handle) in info_file From d1ff4b5cf069226d0852a2d999f71653897fd0e1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 1 Aug 2024 19:19:25 +0200 Subject: [PATCH 022/211] Remove newline --- src/dev/check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index c89eb35d..3f6b4407 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -317,7 +317,7 @@ pub fn check(require_solutions: bool) -> Result<()> { check_exercises(&info_file, &cmd_runner)?; check_solutions(require_solutions, &info_file, &cmd_runner)?; - println!("\nEverything looks fine!"); + println!("Everything looks fine!"); Ok(()) } From 14682060522371a358c2054fd2cc5cfdd1786078 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 2 Aug 2024 15:54:14 +0200 Subject: [PATCH 023/211] Stop on first exercise solved --- src/dev/check.rs | 110 ++++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 3f6b4407..f01374d3 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -5,7 +5,6 @@ use std::{ io::{self, Read, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, - sync::atomic::{self, AtomicBool}, thread, }; @@ -22,7 +21,7 @@ fn forbidden_char(input: &str) -> Option { input.chars().find(|c| !c.is_alphanumeric() && *c != '_') } -// Check that the Cargo.toml file is up-to-date. +// Check that the `Cargo.toml` file is up-to-date. fn check_cargo_toml( exercise_infos: &[ExerciseInfo], cargo_toml_path: &str, @@ -164,41 +163,42 @@ fn check_unexpected_files( } fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { - let error_occurred = AtomicBool::new(false); - println!( "Running all exercises to check that they aren't already solved. This may take a while…\n", ); thread::scope(|s| { - for exercise_info in &info_file.exercises { - if exercise_info.skip_check_unsolved { - continue; - } - - s.spawn(|| { - let error = |e| { - let mut stderr = io::stderr().lock(); - stderr.write_all(e).unwrap(); - stderr.write_all(b"\nProblem with the exercise ").unwrap(); - stderr.write_all(exercise_info.name.as_bytes()).unwrap(); - stderr.write_all(SEPARATOR).unwrap(); - error_occurred.store(true, atomic::Ordering::Relaxed); - }; - - match exercise_info.run_exercise(None, cmd_runner) { - Ok(true) => error(b"Already solved!"), - Ok(false) => (), - Err(e) => error(e.to_string().as_bytes()), + let handles = info_file + .exercises + .iter() + .filter_map(|exercise_info| { + if exercise_info.skip_check_unsolved { + return None; } - }); + + Some(s.spawn(|| exercise_info.run_exercise(None, cmd_runner))) + }) + .collect::>(); + + for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { + let Ok(result) = handle.join() else { + bail!( + "Panic while trying to run the exericse {}", + exercise_info.name, + ); + }; + + match result { + Ok(true) => bail!( + "The exercise {} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}", + exercise_info.name, + ), + Ok(false) => (), + Err(e) => return Err(e), + } } - }); - if error_occurred.load(atomic::Ordering::Relaxed) { - bail!(CHECK_EXERCISES_UNSOLVED_ERR); - } - - Ok(()) + Ok(()) + }) } fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { @@ -209,9 +209,10 @@ fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { } let info_file_paths = check_info_file_exercises(info_file)?; - check_unexpected_files("exercises", &info_file_paths)?; + let handle = thread::spawn(move || check_unexpected_files("exercises", &info_file_paths)); - check_exercises_unsolved(info_file, cmd_runner) + check_exercises_unsolved(info_file, cmd_runner)?; + handle.join().unwrap() } enum SolutionCheck { @@ -263,29 +264,34 @@ fn check_solutions( .arg("always") .stdin(Stdio::null()); - for (exercise_name, handle) in info_file - .exercises - .iter() - .map(|exercise_info| &exercise_info.name) - .zip(handles) - { - match handle.join() { - Ok(SolutionCheck::Success { sol_path }) => { + for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { + let Ok(check_result) = handle.join() else { + bail!( + "Panic while trying to run the solution of the exericse {}", + exercise_info.name, + ); + }; + + match check_result { + SolutionCheck::Success { sol_path } => { fmt_cmd.arg(&sol_path); sol_paths.insert(PathBuf::from(sol_path)); } - Ok(SolutionCheck::MissingRequired) => { - bail!("The solution of the exercise {exercise_name} is missing"); + SolutionCheck::MissingRequired => { + bail!( + "The solution of the exercise {} is missing", + exercise_info.name, + ); } - Ok(SolutionCheck::MissingOptional) => (), - Ok(SolutionCheck::RunFailure { output }) => { + SolutionCheck::MissingOptional => (), + SolutionCheck::RunFailure { output } => { io::stderr().lock().write_all(&output)?; - bail!("Running the solution of the exercise {exercise_name} failed with the error above"); - } - Ok(SolutionCheck::Err(e)) => return Err(e), - Err(_) => { - bail!("Panic while trying to run the solution of the exericse {exercise_name}"); + bail!( + "Running the solution of the exercise {} failed with the error above", + exercise_info.name, + ); } + SolutionCheck::Err(e) => return Err(e), } } @@ -322,8 +328,4 @@ pub fn check(require_solutions: bool) -> Result<()> { Ok(()) } -const SEPARATOR: &[u8] = - b"\n========================================================================================\n"; - -const CHECK_EXERCISES_UNSOLVED_ERR: &str = "At least one exercise is already solved or failed to run. See the output above. -If this is an intro exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file."; +const SKIP_CHECK_UNSOLVED_HINT: &str = "If this is an introduction exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file"; From 5016c7cf7c846cc4d271fa06d8d7debc7604ae5c Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 2 Aug 2024 16:28:05 +0200 Subject: [PATCH 024/211] Use `trim_ascii` instead of `trim` --- src/app_state.rs | 7 +++---- src/dev/check.rs | 2 +- src/exercise.rs | 2 +- src/main.rs | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index ea99746b..8e43c57c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -107,8 +107,7 @@ impl AppState { 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.trim().to_owned(); + let hint = exercise_info.hint.leak().trim_ascii(); Exercise { dir, @@ -397,7 +396,7 @@ impl AppState { clear_terminal(writer)?; writer.write_all(FENISH_LINE.as_bytes())?; - let final_message = self.final_message.trim(); + let final_message = self.final_message.trim_ascii(); if !final_message.is_empty() { writer.write_all(final_message.as_bytes())?; writer.write_all(b"\n")?; @@ -445,7 +444,7 @@ mod tests { path: "exercises/0.rs", test: false, strict_clippy: false, - hint: String::new(), + hint: "", done: false, } } diff --git a/src/dev/check.rs b/src/dev/check.rs index f01374d3..e1e716c1 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -71,7 +71,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result Result<()> { let mut stdout = io::stdout().lock(); clear_terminal(&mut stdout)?; - let welcome_message = welcome_message.trim(); + let welcome_message = welcome_message.trim_ascii(); write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; stdout.flush()?; press_enter_prompt()?; From 175294fa5dda30ed313050a4837631575dc8a232 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 2 Aug 2024 16:40:06 +0200 Subject: [PATCH 025/211] Add `rust-version` --- CHANGELOG.md | 1 + Cargo.toml | 2 ++ README.md | 2 +- rustlings-macros/Cargo.toml | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 846c67f0..cf036eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 6.1.1 (UNRELEASED) +- Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. - Run the final check of all exercises in parallel. - Small exercise improvements. - `dev check`: Check that all solutions are formatted with `rustfmt`. diff --git a/Cargo.toml b/Cargo.toml index d4466ce2..58955791 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ authors = [ repository = "https://github.com/rust-lang/rustlings" license = "MIT" edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions. +rust-version = "1.80" [workspace.dependencies] serde = { version = "1.0.204", features = ["derive"] } @@ -29,6 +30,7 @@ authors.workspace = true repository.workspace = true license.workspace = true edition.workspace = true +rust-version.workspace = true keywords = [ "exercise", "learning", diff --git a/README.md b/README.md index a7f81c12..bfffad81 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ It contains code examples and exercises similar to Rustlings, but online. ### Installing Rust -Before installing Rustlings, you need to have _Rust installed_. +Before installing Rustlings, you need to have the **latest version of Rust** installed. Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust. This will also install _Cargo_, Rust's package/project manager. diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index 20d6776e..f5ecfcc2 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -6,6 +6,7 @@ authors.workspace = true repository.workspace = true license.workspace = true edition.workspace = true +rust-version.workspace = true include = [ "/src/", "/info.toml", From 2128be8b2855f2640137491cb1ed0c9d89721603 Mon Sep 17 00:00:00 2001 From: Matt Nield <64328730+matthewjnield@users.noreply.github.com> Date: Sun, 4 Aug 2024 02:36:45 -0400 Subject: [PATCH 026/211] chore: Fix snakecase convention in errors6.rs Exercise errors6.rs prompts the user to add a method named `from_parseint`. This commit changes the method name to the corrected snakecase format, `from_parse_int`. --- exercises/13_error_handling/errors6.rs | 2 +- solutions/13_error_handling/errors6.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index b656c617..b1995e03 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -25,7 +25,7 @@ impl ParsePosNonzeroError { } // TODO: Add another error conversion function here. - // fn from_parseint(???) -> Self { ??? } + // fn from_parse_int(???) -> Self { ??? } } #[derive(PartialEq, Debug)] diff --git a/solutions/13_error_handling/errors6.rs b/solutions/13_error_handling/errors6.rs index 429d3ea3..86793619 100644 --- a/solutions/13_error_handling/errors6.rs +++ b/solutions/13_error_handling/errors6.rs @@ -24,7 +24,7 @@ impl ParsePosNonzeroError { Self::Creation(err) } - fn from_parseint(err: ParseIntError) -> Self { + fn from_parse_int(err: ParseIntError) -> Self { Self::ParseInt(err) } } @@ -44,7 +44,7 @@ impl PositiveNonzeroInteger { fn parse(s: &str) -> Result { // Return an appropriate error instead of panicking when `parse()` // returns an error. - let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?; + let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?; // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Self::new(x).map_err(ParsePosNonzeroError::from_creation) } From 13124aafe3fd0fcd5efad12419ea5cc5a3b8ceef Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 5 Aug 2024 03:15:43 +0200 Subject: [PATCH 027/211] Update deps --- Cargo.lock | 49 +++++++++++++++++++++++++++++-------------------- Cargo.toml | 8 ++++---- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fab8e584..ac915aac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,9 +116,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" dependencies = [ "clap_builder", "clap_derive", @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ "anstream", "anstyle", @@ -138,9 +138,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", "proc-macro2", @@ -264,9 +264,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown", @@ -419,12 +419,12 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "os_pipe" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -587,9 +587,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", "memchr", @@ -708,9 +708,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.18" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1490595c74d930da779e944f5ba2ecdf538af67df1a9848cbd156af43c1b7cf0" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", @@ -794,11 +794,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -825,6 +825,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -948,9 +957,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.16" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 58955791..e76077d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ rust-version = "1.80" [workspace.dependencies] serde = { version = "1.0.204", features = ["derive"] } -toml_edit = { version = "0.22.18", default-features = false, features = ["parse", "serde"] } +toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } [package] name = "rustlings" @@ -47,13 +47,13 @@ include = [ [dependencies] anyhow = "1.0.86" -clap = { version = "4.5.11", features = ["derive"] } +clap = { version = "4.5.13", features = ["derive"] } hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } -os_pipe = "1.2.0" +os_pipe = "1.2.1" ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" } -serde_json = "1.0.121" +serde_json = "1.0.122" serde.workspace = true toml_edit.workspace = true From bdf4960b6a9626c83281ae2fb9cbccda676dffcf Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 7 Aug 2024 23:25:22 +0200 Subject: [PATCH 028/211] Fix exercise name shift in exercise check --- src/dev/check.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index e1e716c1..202e6292 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -175,22 +175,21 @@ fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Res return None; } - Some(s.spawn(|| exercise_info.run_exercise(None, cmd_runner))) + Some(( + exercise_info.name.as_str(), + s.spawn(|| exercise_info.run_exercise(None, cmd_runner)), + )) }) .collect::>(); - for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { + for (exercise_name, handle) in handles { let Ok(result) = handle.join() else { - bail!( - "Panic while trying to run the exericse {}", - exercise_info.name, - ); + bail!("Panic while trying to run the exericse {exercise_name}"); }; match result { Ok(true) => bail!( - "The exercise {} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}", - exercise_info.name, + "The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}", ), Ok(false) => (), Err(e) => return Err(e), From 286a455fa94bc638e6418d75634e28a78275b033 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 7 Aug 2024 23:35:50 +0200 Subject: [PATCH 029/211] Avoid using `RUSTFLAGS` to not trigger rebuilding, especially in rust-analyzer --- src/cmd.rs | 7 ------- src/exercise.rs | 53 +++++++++++++++++++++++-------------------------- 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/src/cmd.rs b/src/cmd.rs index 1891a283..ba6ec894 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -129,13 +129,6 @@ impl<'out> CargoSubcommand<'out> { self } - /// RUSTFLAGS="-A warnings" - #[inline] - pub fn hide_warnings(&mut self) -> &mut Self { - self.cmd.env("RUSTFLAGS", "-A warnings"); - self - } - /// The boolean in the returned `Result` is true if the command's exit status is success. #[inline] pub fn run(self, description: &str) -> Result { diff --git a/src/exercise.rs b/src/exercise.rs index 7a383bb6..500d1194 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -78,27 +78,40 @@ pub trait RunnableExercise { mut output: Option<&mut Vec>, cmd_runner: &CmdRunner, ) -> Result { - let output_is_none = if let Some(output) = output.as_deref_mut() { + if let Some(output) = output.as_deref_mut() { output.clear(); - false - } else { - true - }; - - let mut build_cmd = cmd_runner.cargo("build", bin_name, output.as_deref_mut()); - if output_is_none { - build_cmd.hide_warnings(); } - let build_success = build_cmd.run("cargo build …")?; + + let build_success = cmd_runner + .cargo("build", bin_name, output.as_deref_mut()) + .run("cargo build …")?; if !build_success { return Ok(false); } - // Discard the output of `cargo build` because it will be shown again by Clippy. + // Discard the compiler output because it will be shown again by `cargo test` or Clippy. if let Some(output) = output.as_deref_mut() { output.clear(); } + if self.test() { + let output_is_some = output.is_some(); + let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut()); + if output_is_some { + test_cmd.args(["--", "--color", "always", "--show-output"]); + } + let test_success = test_cmd.run("cargo test …")?; + if !test_success { + run_bin(bin_name, output, cmd_runner)?; + return Ok(false); + } + + // Discard the compiler output because it will be shown again by Clippy. + if let Some(output) = output.as_deref_mut() { + output.clear(); + } + } + let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut()); // `--profile test` is required to also check code with `[cfg(test)]`. @@ -109,25 +122,9 @@ pub trait RunnableExercise { } let clippy_success = clippy_cmd.run("cargo clippy …")?; - if !clippy_success { - return Ok(false); - } - - if !self.test() { - return run_bin(bin_name, output.as_deref_mut(), cmd_runner); - } - - let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut()); - if !output_is_none { - test_cmd.args(["--", "--color", "always", "--show-output"]); - } - // Hide warnings because they are shown by Clippy. - test_cmd.hide_warnings(); - let test_success = test_cmd.run("cargo test …")?; - let run_success = run_bin(bin_name, output, cmd_runner)?; - Ok(test_success && run_success) + Ok(clippy_success && run_success) } /// Compile, check and run the exercise. From 24aed1b14e5d2ea40029cc0ae469bcb32332c88a Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 7 Aug 2024 23:45:58 +0200 Subject: [PATCH 030/211] Update CHANGELOG --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf036eeb..155ad843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ -## 6.1.1 (UNRELEASED) +## 6.1.1 (2024-08-08) +It is recommended to update to this version to fix issues with the language server `rust-analyzer`. +You can update using the following two commands: + +```bash +rustup update +cargo install rustlings +``` + +- Fix `rust-analyzer` rebuilding all exercises after changing one file. - Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. - Run the final check of all exercises in parallel. - Small exercise improvements. From 81bf0a64300a2dd1f05b71e2674cc927385df410 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 7 Aug 2024 23:46:11 +0200 Subject: [PATCH 031/211] Remove redundant rustfmt check for solutions --- release-hook.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/release-hook.sh b/release-hook.sh index f021f193..d5954ca8 100755 --- a/release-hook.sh +++ b/release-hook.sh @@ -9,6 +9,5 @@ cargo upgrades # Similar to CI cargo clippy -- --deny warnings cargo fmt --all --check -rustfmt --check --edition 2021 solutions/**/*.rs cargo test --workspace --all-targets cargo run -- dev check --require-solutions From 4933ace50b5e3fc6512f5129280425c5ba297b72 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 7 Aug 2024 23:54:02 +0200 Subject: [PATCH 032/211] Add `panic = "abort"` for exercises --- dev/Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 7f3acb51..d814ba2b 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -195,3 +195,9 @@ name = "exercises" edition = "2021" # Don't publish the exercises on crates.io! publish = false + +[profile.release] +panic = "abort" + +[profile.dev] +panic = "abort" From 97719fe8da82a91a1919e55e1950d0997acca574 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 00:20:04 +0200 Subject: [PATCH 033/211] Remove state file and solutions dir from .gitignore --- CHANGELOG.md | 5 +++-- src/dev/new.rs | 4 ++-- src/init.rs | 8 +++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 155ad843..c0143066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ - + -## 6.1.1 (2024-08-08) +## 6.2.0 (2024-08-08) It is recommended to update to this version to fix issues with the language server `rust-analyzer`. You can update using the following two commands: @@ -12,6 +12,7 @@ cargo install rustlings - Fix `rust-analyzer` rebuilding all exercises after changing one file. - Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. +- Remove the state file and the solutions directory from the generated `.gitignore` file. - Run the final check of all exercises in parallel. - Small exercise improvements. - `dev check`: Check that all solutions are formatted with `rustfmt`. diff --git a/src/dev/new.rs b/src/dev/new.rs index 55d5f141..c7650465 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -76,8 +76,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> { pub const GITIGNORE: &[u8] = b".rustlings-state.txt Cargo.lock -target -.vscode +target/ +.vscode/ !.vscode/extensions.json "; diff --git a/src/init.rs b/src/init.rs index bfa0ab86..9c7d10f4 100644 --- a/src/init.rs +++ b/src/init.rs @@ -92,11 +92,9 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() { } "; -const GITIGNORE: &[u8] = b".rustlings-state.txt -solutions -Cargo.lock -target -.vscode +const GITIGNORE: &[u8] = b"Cargo.lock +target/ +.vscode/ "; pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; From 693bb708b2af786b942e172d3aed104c0abd252e Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 00:20:20 +0200 Subject: [PATCH 034/211] Add README to the solutions dir --- CHANGELOG.md | 1 + solutions/README.md | 6 ++++++ src/init.rs | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 solutions/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c0143066..4b753d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ cargo install rustlings - Fix `rust-analyzer` rebuilding all exercises after changing one file. - Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. - Remove the state file and the solutions directory from the generated `.gitignore` file. +- Add a `README.md` file to the `solutions/` directory. - Run the final check of all exercises in parallel. - Small exercise improvements. - `dev check`: Check that all solutions are formatted with `rustfmt`. diff --git a/solutions/README.md b/solutions/README.md new file mode 100644 index 00000000..6a217fa4 --- /dev/null +++ b/solutions/README.md @@ -0,0 +1,6 @@ +# Official Rustlings solutions + +Before you finish an exercise, its solution file will only contain an empty `main` function. +The content of this file will be automatically replaced by the actual solution once you finish the exercise. + +Note that these solution are often only _one possibility_ to solve an exercise. diff --git a/src/init.rs b/src/init.rs index 9c7d10f4..3970bb2f 100644 --- a/src/init.rs +++ b/src/init.rs @@ -35,6 +35,11 @@ pub fn init() -> Result<()> { .context("Failed to initialize the `rustlings/exercises` directory")?; create_dir("solutions").context("Failed to create the `solutions/` directory")?; + fs::write( + "solutions/README.md", + include_bytes!("../solutions/README.md"), + ) + .context("Failed to create the file rustlings/solutions/README.md")?; for dir in EMBEDDED_FILES.exercise_dirs { let mut dir_path = String::with_capacity(10 + dir.name.len()); dir_path.push_str("solutions/"); From 11fc3f1e56b1b248465039db4c736bb7186e4f47 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 00:35:29 +0200 Subject: [PATCH 035/211] Fix errors not being shown after the welcome message --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index 2d1d5455..12786d01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,6 +121,8 @@ fn main() -> Result<()> { stdout.flush()?; press_enter_prompt()?; clear_terminal(&mut stdout)?; + // Flush to be able to show errors occuring before printing a newline to stdout. + stdout.flush()?; } StateFileStatus::Read => (), } From fd97470f3551e5c068dc796f25320ebb5b93a08c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 00:42:26 +0200 Subject: [PATCH 036/211] Adapt type name in 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 2ecb2264..504bfd94 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -571,7 +571,7 @@ name = "hashmaps3" dir = "11_hashmaps" hint = """ Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of - `HashMap` to insert the default value of `Team` if a team doesn't + `HashMap` to insert the default value of `TeamScores` if a team doesn't exist in the table yet. Learn more in The Book: From 06a0f278e5a5d9235c0bf97d2334bd33c432fd02 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 01:35:47 +0200 Subject: [PATCH 037/211] Don't recommend the builtin VS-Code terminal because it can't clear scrollback --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index bfffad81..91ca564c 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,6 @@ 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). -If you use VS Code, the builtin terminal should also be fine. - ## Doing exercises The exercises are sorted by topic and can be found in the subdirectory `exercises/`. From 39580381fa9cd52cf8026ad0360e077ee25d1aa0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 01:48:57 +0200 Subject: [PATCH 038/211] rust-analyzer problem isn't fixed :( --- CHANGELOG.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b753d28..19c9a516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,6 @@ ## 6.2.0 (2024-08-08) -It is recommended to update to this version to fix issues with the language server `rust-analyzer`. -You can update using the following two commands: - -```bash -rustup update -cargo install rustlings -``` - -- Fix `rust-analyzer` rebuilding all exercises after changing one file. - Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. - Remove the state file and the solutions directory from the generated `.gitignore` file. - Add a `README.md` file to the `solutions/` directory. From 8df66f79918168617da9709c0edcfeb3ca0e53c8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 02:45:18 +0200 Subject: [PATCH 039/211] Allow initialization in a workspace --- src/init.rs | 73 ++++++++++++++++++++++++++++++++++++----------------- src/main.rs | 24 +++--------------- src/term.rs | 12 +++++++++ 3 files changed, 66 insertions(+), 43 deletions(-) create mode 100644 src/term.rs diff --git a/src/init.rs b/src/init.rs index 3970bb2f..dc23cbbf 100644 --- a/src/init.rs +++ b/src/init.rs @@ -3,30 +3,40 @@ use ratatui::crossterm::style::Stylize; use std::{ env::set_current_dir, fs::{self, create_dir}, - io::ErrorKind, + io::{self, Write}, path::Path, process::{Command, Stdio}, }; -use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile}; +use crate::{ + cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile, + term::press_enter_prompt, +}; pub fn init() -> Result<()> { - // Prevent initialization in a directory that contains the file `Cargo.toml`. - // This can mean that Rustlings was already initialized in this directory. - // Otherwise, this can cause problems with Cargo workspaces. + let rustlings_dir = Path::new("rustlings"); + if rustlings_dir.exists() { + bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); + } + + let mut stdout = io::stdout().lock(); + let mut init_git = true; + if Path::new("Cargo.toml").exists() { - bail!(CARGO_TOML_EXISTS_ERR); - } - - let rustlings_path = Path::new("rustlings"); - if let Err(e) = create_dir(rustlings_path) { - if e.kind() == ErrorKind::AlreadyExists { - bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); + if Path::new("exercises").exists() && Path::new("solutions").exists() { + bail!(IN_INITIALIZED_DIR_ERR); } - return Err(e.into()); + + stdout.write_all(CARGO_TOML_EXISTS_PROMPT_MSG)?; + press_enter_prompt(&mut stdout)?; + init_git = false; } - set_current_dir("rustlings") + stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; + press_enter_prompt(&mut stdout)?; + + create_dir(rustlings_dir).context("Failed to create the `rustlings/` directory")?; + set_current_dir(rustlings_dir) .context("Failed to change the current directory to `rustlings/`")?; let info_file = InfoFile::parse()?; @@ -75,18 +85,21 @@ pub fn init() -> Result<()> { fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; - // Ignore any Git error because Git initialization is not required. - let _ = Command::new("git") - .arg("init") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .status(); + if init_git { + // Ignore any Git error because Git initialization is not required. + let _ = Command::new("git") + .arg("init") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } - println!( + writeln!( + stdout, "\n{}\n\n{}", "Initialization done ✓".green(), POST_INIT_MSG.bold(), - ); + )?; Ok(()) } @@ -104,7 +117,7 @@ target/ pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; -const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`. +const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory. If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises. Otherwise, please run `rustlings init` again in another directory."; @@ -115,5 +128,19 @@ You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again"; +const CARGO_TOML_EXISTS_PROMPT_MSG: &[u8] = br#"You are about to initialize Rustlings in a directory that already contains a `Cargo.toml` file! + + => It is recommended to abort with CTRL+C and initialize Rustlings in another directory <= + +If you know what you are doing and want to initialize Rustlings in a Cargo workspace, +then you need to add its directory to `members` in the `workspace` section of the `Cargo.toml` file: + +```toml +[workspace] +members = ["rustlings"] +``` + +Press ENTER if you are sure that you want to continue after reading the warning above "#; + const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. Then run `rustlings` to get started."; diff --git a/src/main.rs b/src/main.rs index 12786d01..edb3e146 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,11 @@ use anyhow::{bail, Context, Result}; use app_state::StateFileStatus; use clap::{Parser, Subcommand}; use std::{ - io::{self, BufRead, IsTerminal, StdoutLock, Write}, + io::{self, IsTerminal, Write}, path::Path, process::exit, }; +use term::{clear_terminal, press_enter_prompt}; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; @@ -20,20 +21,12 @@ mod init; mod list; mod progress_bar; mod run; +mod term; mod terminal_link; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; -fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { - stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") -} - -fn press_enter_prompt() -> io::Result<()> { - io::stdin().lock().read_until(b'\n', &mut Vec::new())?; - Ok(()) -} - /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -79,14 +72,6 @@ fn main() -> Result<()> { match args.command { Some(Subcommands::Init) => { - { - let mut stdout = io::stdout().lock(); - stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; - stdout.flush()?; - press_enter_prompt()?; - stdout.write_all(b"\n")?; - } - return init::init().context("Initialization failed"); } Some(Subcommands::Dev(dev_command)) => return dev_command.run(), @@ -118,8 +103,7 @@ fn main() -> Result<()> { let welcome_message = welcome_message.trim_ascii(); write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; - stdout.flush()?; - press_enter_prompt()?; + press_enter_prompt(&mut stdout)?; clear_terminal(&mut stdout)?; // Flush to be able to show errors occuring before printing a newline to stdout. stdout.flush()?; diff --git a/src/term.rs b/src/term.rs new file mode 100644 index 00000000..e1ac3da9 --- /dev/null +++ b/src/term.rs @@ -0,0 +1,12 @@ +use std::io::{self, BufRead, StdoutLock, Write}; + +pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { + stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") +} + +pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { + stdout.flush()?; + io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + stdout.write_all(b"\n")?; + Ok(()) +} From dc0ffbe16eb5ecc591422fe225ebb58f17b0e231 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 8 Aug 2024 01:23:58 +0200 Subject: [PATCH 040/211] Replace hashbrown with ahash hashbrown is already used in the standard library, but we want the improved performance of the different hash algorithm. Using ahash directly conveys this intent more clearly. --- Cargo.lock | 14 +++++++++++++- Cargo.toml | 2 +- src/app_state.rs | 3 ++- src/dev/check.rs | 14 ++++++-------- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac915aac..22aa2528 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -246,6 +247,17 @@ dependencies = [ "libc", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -517,9 +529,9 @@ dependencies = [ name = "rustlings" version = "6.1.0" dependencies = [ + "ahash", "anyhow", "clap", - "hashbrown", "notify-debouncer-mini", "os_pipe", "ratatui", diff --git a/Cargo.toml b/Cargo.toml index e76077d9..47e15301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,9 +46,9 @@ include = [ ] [dependencies] +ahash = "0.8.11" anyhow = "1.0.86" clap = { version = "4.5.13", features = ["derive"] } -hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } diff --git a/src/app_state.rs b/src/app_state.rs index 8e43c57c..ac45bfc6 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,3 +1,4 @@ +use ahash::{HashSet, HashSetExt}; use anyhow::{bail, Context, Error, Result}; use std::{ fs::{self, File}, @@ -69,7 +70,7 @@ impl AppState { return StateFileStatus::NotRead; } - let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len()); + let mut done_exercises = HashSet::with_capacity(self.exercises.len()); for done_exerise_name in lines { if done_exerise_name.is_empty() { diff --git a/src/dev/check.rs b/src/dev/check.rs index 202e6292..7b172749 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,3 +1,4 @@ +use ahash::{HashSet, HashSetExt}; use anyhow::{anyhow, bail, Context, Error, Result}; use std::{ cmp::Ordering, @@ -48,9 +49,9 @@ fn check_cargo_toml( } // Check the info of all exercises and return their paths in a set. -fn check_info_file_exercises(info_file: &InfoFile) -> Result> { - let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); - let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); +fn check_info_file_exercises(info_file: &InfoFile) -> Result> { + let mut names = HashSet::with_capacity(info_file.exercises.len()); + let mut paths = HashSet::with_capacity(info_file.exercises.len()); let mut file_buf = String::with_capacity(1 << 14); for exercise_info in &info_file.exercises { @@ -111,10 +112,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result, -) -> Result<()> { +fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> Result<()> { let unexpected_file = |path: &Path| { anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory", path.display()) }; @@ -253,7 +251,7 @@ fn check_solutions( }) .collect::>(); - let mut sol_paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + let mut sol_paths = HashSet::with_capacity(info_file.exercises.len()); let mut fmt_cmd = Command::new("rustfmt"); fmt_cmd .arg("--check") From dc086c6bf1e678a1886e0a2bb78627fac076402d Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 8 Aug 2024 12:51:27 +0200 Subject: [PATCH 041/211] Improve initialization in workspace - Detect if we are in a cargo project more reliably. (e.g. if `rustlings init` is run in the `src/` directory) - Refuse to initialize rustlings in a non-workspace cargo project. - Automatically populate the `workspace.members` field if `rustlings init` is run in a workspace. This may be considered risky, as there is no guarantee that's what the user wanted to do. However, it is consistent with the behavior of `cargo new`. Also, newcomers to Rust are unlikely to accidentally be in a cargo workspace, as they won't know how to create one in the first place. The use case for initialization in a workspace is when a workshop organizer wants to use rustlings alongside other exerices and provide a single repository with everything in one place. --- src/init.rs | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/init.rs b/src/init.rs index dc23cbbf..94551c4c 100644 --- a/src/init.rs +++ b/src/init.rs @@ -4,7 +4,7 @@ use std::{ env::set_current_dir, fs::{self, create_dir}, io::{self, Write}, - path::Path, + path::{Path, PathBuf}, process::{Command, Stdio}, }; @@ -22,14 +22,27 @@ pub fn init() -> Result<()> { let mut stdout = io::stdout().lock(); let mut init_git = true; - if Path::new("Cargo.toml").exists() { + let manifest_path = Command::new("cargo") + .args(["locate-project", "--message-format=plain"]) + .output()?; + if manifest_path.status.success() { + let manifest_path: PathBuf = String::from_utf8_lossy(&manifest_path.stdout).trim().into(); + if Path::new("exercises").exists() && Path::new("solutions").exists() { bail!(IN_INITIALIZED_DIR_ERR); } - - stdout.write_all(CARGO_TOML_EXISTS_PROMPT_MSG)?; - press_enter_prompt(&mut stdout)?; - init_git = false; + if fs::read_to_string(manifest_path)?.contains("[workspace]") { + // make sure "rustlings" is added to `workspace.members` by making + // cargo initialize a new project + let output = Command::new("cargo").args(["new", "rustlings"]).output()?; + if !output.status.success() { + bail!("Failed to initilize new workspace member"); + } + fs::remove_dir_all("rustlings")?; + init_git = false; + } else { + bail!(IN_NON_WORKSPACE_CARGO_PROJECT_ERR); + } } stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; @@ -128,19 +141,9 @@ You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again"; -const CARGO_TOML_EXISTS_PROMPT_MSG: &[u8] = br#"You are about to initialize Rustlings in a directory that already contains a `Cargo.toml` file! - - => It is recommended to abort with CTRL+C and initialize Rustlings in another directory <= - -If you know what you are doing and want to initialize Rustlings in a Cargo workspace, -then you need to add its directory to `members` in the `workspace` section of the `Cargo.toml` file: - -```toml -[workspace] -members = ["rustlings"] -``` - -Press ENTER if you are sure that you want to continue after reading the warning above "#; +const IN_NON_WORKSPACE_CARGO_PROJECT_ERR: &str = "\ +The current directory is already part of a cargo project. +Please initialize rustlings in a different directory."; const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. Then run `rustlings` to get started."; From 8b43d7925761edcd6ca8bacf382e82a05aa5c0e7 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 8 Aug 2024 14:04:43 +0200 Subject: [PATCH 042/211] Fix integration tests --- Cargo.lock | 49 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +++ tests/integration_tests.rs | 14 +++++------ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac915aac..e6170256 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "filetime" version = "0.2.23" @@ -339,6 +355,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.12" @@ -513,6 +535,19 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustlings" version = "6.1.0" @@ -526,6 +561,7 @@ dependencies = [ "rustlings-macros", "serde", "serde_json", + "tempfile", "toml_edit", ] @@ -697,6 +733,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "toml_datetime" version = "0.6.8" diff --git a/Cargo.toml b/Cargo.toml index e76077d9..316879ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,9 @@ serde_json = "1.0.122" serde.workspace = true toml_edit.workspace = true +[dev-dependencies] +tempfile = "3.12.0" + [profile.release] panic = "abort" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 3ab54f97..d821e20a 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -155,28 +155,28 @@ fn hint() { #[test] fn init() { - let _ = fs::remove_dir_all("tests/rustlings"); + let test_dir = tempfile::TempDir::new().unwrap(); + let initialized_dir = test_dir.path().join("rustlings"); + let test_dir = test_dir.path().to_str().unwrap(); - Cmd::default().current_dir("tests").fail(); + Cmd::default().current_dir(test_dir).fail(); Cmd::default() - .current_dir("tests") + .current_dir(test_dir) .args(&["init"]) .success(); // Running `init` after a successful initialization. Cmd::default() - .current_dir("tests") + .current_dir(test_dir) .args(&["init"]) .output(PartialStderr("`cd rustlings`")) .fail(); // Running `init` in the initialized directory. Cmd::default() - .current_dir("tests/rustlings") + .current_dir(initialized_dir.to_str().unwrap()) .args(&["init"]) .output(PartialStderr("already initialized")) .fail(); - - fs::remove_dir_all("tests/rustlings").unwrap(); } From 34f02cf83d155fc5efee9970994d3a83ab58c284 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 22:37:56 +0200 Subject: [PATCH 043/211] Attach error message as context --- src/watch.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 88a12301..c6690304 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -102,8 +102,7 @@ pub fn watch( watch_state.render()?; } WatchEvent::NotifyErr(e) => { - watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?; - return Err(Error::from(e)); + return Err(Error::from(e).context(NOTIFY_ERR)); } WatchEvent::TerminalEventErr(e) => { return Err(Error::from(e).context("Terminal event listener failed")); From 0785b2419277bc1cbc7f55c123f8e248759f4766 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 22:41:41 +0200 Subject: [PATCH 044/211] Show a message before running the exercise --- src/watch/state.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/watch/state.rs b/src/watch/state.rs index 46f48d9f..45fbd9e0 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -51,6 +51,8 @@ impl<'a> WatchState<'a> { pub fn run_current_exercise(&mut self) -> Result<()> { self.show_hint = false; + self.writer + .write_all(b"\nChecking the exercise, please wait...")?; let success = self .app_state .current_exercise() From 4ce8667b9d878dc48fafb665699a5fc71c190972 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 22:48:53 +0200 Subject: [PATCH 045/211] Show the exercise name in the waiting message --- src/watch/state.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index 45fbd9e0..abfff7ac 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -51,8 +51,11 @@ impl<'a> WatchState<'a> { pub fn run_current_exercise(&mut self) -> Result<()> { self.show_hint = false; - self.writer - .write_all(b"\nChecking the exercise, please wait...")?; + writeln!( + self.writer, + "\nChecking the exercise `{}`. Please wait…", + self.app_state.current_exercise().name, + )?; let success = self .app_state .current_exercise() From 1b9faa4d61665074fe450277644974dd0167e6e9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 23:13:49 +0200 Subject: [PATCH 046/211] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c9a516..1529ba00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 6.2.0 (2024-08-08) +- Show a message before checking and running an exercise. This gives the user instant feedback and avoids confusion if the checks take too long. - Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. - Remove the state file and the solutions directory from the generated `.gitignore` file. - Add a `README.md` file to the `solutions/` directory. From e41c3a7c925387ca2c2441b4f41c963b95bc828d Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 8 Aug 2024 23:46:21 +0200 Subject: [PATCH 047/211] Use fixed seeds with ahash --- Cargo.lock | 12 ------------ Cargo.toml | 2 +- src/app_state.rs | 4 ++-- src/collections.rs | 10 ++++++++++ src/dev/check.rs | 8 ++++---- src/main.rs | 1 + 6 files changed, 18 insertions(+), 19 deletions(-) create mode 100644 src/collections.rs diff --git a/Cargo.lock b/Cargo.lock index 86d35c5d..a61ba597 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", "once_cell", "version_check", "zerocopy", @@ -263,17 +262,6 @@ dependencies = [ "libc", ] -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "hashbrown" version = "0.14.5" diff --git a/Cargo.toml b/Cargo.toml index 456f738d..2cd2ebad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ include = [ ] [dependencies] -ahash = "0.8.11" +ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.86" clap = { version = "4.5.13", features = ["derive"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } diff --git a/src/app_state.rs b/src/app_state.rs index ac45bfc6..b72469c4 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,4 +1,3 @@ -use ahash::{HashSet, HashSetExt}; use anyhow::{bail, Context, Error, Result}; use std::{ fs::{self, File}, @@ -11,6 +10,7 @@ use std::{ use crate::{ clear_terminal, cmd::CmdRunner, + collections::hash_set_with_capacity, embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, @@ -70,7 +70,7 @@ impl AppState { return StateFileStatus::NotRead; } - let mut done_exercises = HashSet::with_capacity(self.exercises.len()); + let mut done_exercises = hash_set_with_capacity(self.exercises.len()); for done_exerise_name in lines { if done_exerise_name.is_empty() { diff --git a/src/collections.rs b/src/collections.rs new file mode 100644 index 00000000..fa9e3fa7 --- /dev/null +++ b/src/collections.rs @@ -0,0 +1,10 @@ +use ahash::AHasher; +use std::hash::BuildHasherDefault; + +/// DOS attacks aren't a concern for Rustlings. Therefore, we use `ahash` with fixed seeds. +pub type HashSet = std::collections::HashSet>; + +#[inline] +pub fn hash_set_with_capacity(capacity: usize) -> HashSet { + HashSet::with_capacity_and_hasher(capacity, BuildHasherDefault::::default()) +} diff --git a/src/dev/check.rs b/src/dev/check.rs index 7b172749..ca1b30c9 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,4 +1,3 @@ -use ahash::{HashSet, HashSetExt}; use anyhow::{anyhow, bail, Context, Error, Result}; use std::{ cmp::Ordering, @@ -12,6 +11,7 @@ use std::{ use crate::{ cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, cmd::CmdRunner, + collections::{hash_set_with_capacity, HashSet}, exercise::{RunnableExercise, OUTPUT_CAPACITY}, info_file::{ExerciseInfo, InfoFile}, CURRENT_FORMAT_VERSION, @@ -50,8 +50,8 @@ fn check_cargo_toml( // Check the info of all exercises and return their paths in a set. fn check_info_file_exercises(info_file: &InfoFile) -> Result> { - let mut names = HashSet::with_capacity(info_file.exercises.len()); - let mut paths = HashSet::with_capacity(info_file.exercises.len()); + let mut names = hash_set_with_capacity(info_file.exercises.len()); + let mut paths = hash_set_with_capacity(info_file.exercises.len()); let mut file_buf = String::with_capacity(1 << 14); for exercise_info in &info_file.exercises { @@ -251,7 +251,7 @@ fn check_solutions( }) .collect::>(); - let mut sol_paths = HashSet::with_capacity(info_file.exercises.len()); + let mut sol_paths = hash_set_with_capacity(info_file.exercises.len()); let mut fmt_cmd = Command::new("rustfmt"); fmt_cmd .arg("--check") diff --git a/src/main.rs b/src/main.rs index edb3e146..58cd8ff6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::Wa mod app_state; mod cargo_toml; mod cmd; +mod collections; mod dev; mod embedded; mod exercise; From 337460d299e59552620d4a9cc3de3a8cf067a4f8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 00:12:49 +0200 Subject: [PATCH 048/211] Check the status of the `cargo metadata` command --- src/cmd.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/cmd.rs b/src/cmd.rs index ba6ec894..a10a7eaa 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::{ io::Read, @@ -68,12 +68,16 @@ impl CmdRunner { .stdin(Stdio::null()) .stderr(Stdio::inherit()) .output() - .context(CARGO_METADATA_ERR)? - .stdout; + .context(CARGO_METADATA_ERR)?; - let target_dir = serde_json::de::from_slice::(&metadata_output) - .context("Failed to read the field `target_directory` from the `cargo metadata` output") - .map(|metadata| metadata.target_directory)?; + if !metadata_output.status.success() { + bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?"); + } + + let target_dir = serde_json::de::from_slice::(&metadata_output.stdout) + .context( + "Failed to read the field `target_directory` from the output of the command `cargo metadata …`", + )?.target_directory; Ok(Self { target_dir }) } From 140c4e4812eff982cc3e0c9df6fa5076d7b56633 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 00:49:30 +0200 Subject: [PATCH 049/211] Improve initialization in a Cargo workspace --- src/init.rs | 77 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/src/init.rs b/src/init.rs index 94551c4c..3a0e11d2 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,5 +1,6 @@ use anyhow::{bail, Context, Result}; use ratatui::crossterm::style::Stylize; +use serde::Deserialize; use std::{ env::set_current_dir, fs::{self, create_dir}, @@ -13,36 +14,68 @@ use crate::{ term::press_enter_prompt, }; +#[derive(Deserialize)] +struct CargoLocateProject { + root: PathBuf, +} + pub fn init() -> Result<()> { let rustlings_dir = Path::new("rustlings"); if rustlings_dir.exists() { bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); } + let locate_project_output = Command::new("cargo") + .arg("locate-project") + .arg("-q") + .arg("--workspace") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_LOCATE_PROJECT_ERR)?; + let mut stdout = io::stdout().lock(); let mut init_git = true; - let manifest_path = Command::new("cargo") - .args(["locate-project", "--message-format=plain"]) - .output()?; - if manifest_path.status.success() { - let manifest_path: PathBuf = String::from_utf8_lossy(&manifest_path.stdout).trim().into(); - + if locate_project_output.status.success() { if Path::new("exercises").exists() && Path::new("solutions").exists() { bail!(IN_INITIALIZED_DIR_ERR); } - if fs::read_to_string(manifest_path)?.contains("[workspace]") { - // make sure "rustlings" is added to `workspace.members` by making - // cargo initialize a new project - let output = Command::new("cargo").args(["new", "rustlings"]).output()?; - if !output.status.success() { - bail!("Failed to initilize new workspace member"); - } - fs::remove_dir_all("rustlings")?; - init_git = false; - } else { - bail!(IN_NON_WORKSPACE_CARGO_PROJECT_ERR); + + let workspace_manifest = + serde_json::de::from_slice::(&locate_project_output.stdout) + .context( + "Failed to read the field `root` from the output of `cargo locate-project …`", + )? + .root; + + 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") + && !workspace_manifest_content.contains("workspace.") + { + bail!("The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory"); } + + // Make sure "rustlings" is added to `workspace.members` by making + // Cargo initialize a new project. + let status = Command::new("cargo") + .arg("new") + .arg("-q") + .arg("--vcs") + .arg("none") + .arg("rustlings") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .status()?; + if !status.success() { + bail!("Failed to initilize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory"); + } + + stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the Cargo.toml file of this Cargo workspace.\n")?; + fs::remove_dir_all("rustlings") + .context("Failed to remove the temporary directory `rustlings/`")?; + init_git = false; } stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; @@ -117,6 +150,10 @@ pub fn init() -> Result<()> { Ok(()) } +const CARGO_LOCATE_PROJECT_ERR: &str = "Failed to run the command `cargo locate-project …` +Did you already install Rust? +Try running `cargo --version` to diagnose the problem."; + const INIT_SOLUTION_FILE: &[u8] = b"fn main() { // DON'T EDIT THIS SOLUTION FILE! // It will be automatically filled after you finish the exercise. @@ -133,7 +170,7 @@ pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.ru const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory. If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises. -Otherwise, please run `rustlings init` again in another directory."; +Otherwise, please run `rustlings init` again in a different directory."; const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str = "A directory with the name `rustlings` already exists in the current directory. @@ -141,9 +178,5 @@ You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again"; -const IN_NON_WORKSPACE_CARGO_PROJECT_ERR: &str = "\ -The current directory is already part of a cargo project. -Please initialize rustlings in a different directory."; - const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. Then run `rustlings` to get started."; From 479f45da9b18372d46c3f3ba7243c68f2bab09ae Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 01:05:44 +0200 Subject: [PATCH 050/211] test_dir is a str anyway --- tests/integration_tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d821e20a..bb3a084b 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,6 +1,5 @@ use std::{ env::{self, consts::EXE_SUFFIX}, - fs, process::{Command, Stdio}, str::from_utf8, }; @@ -156,7 +155,6 @@ fn hint() { #[test] fn init() { let test_dir = tempfile::TempDir::new().unwrap(); - let initialized_dir = test_dir.path().join("rustlings"); let test_dir = test_dir.path().to_str().unwrap(); Cmd::default().current_dir(test_dir).fail(); @@ -173,9 +171,11 @@ fn init() { .output(PartialStderr("`cd rustlings`")) .fail(); + let initialized_dir = format!("{test_dir}/rustlings"); + // Running `init` in the initialized directory. Cmd::default() - .current_dir(initialized_dir.to_str().unwrap()) + .current_dir(&initialized_dir) .args(&["init"]) .output(PartialStderr("already initialized")) .fail(); From 55e68d2c632b30733ec3d16b4039a9cd9b39823f Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 01:06:27 +0200 Subject: [PATCH 051/211] Update deps --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a61ba597..d3db242e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,9 +116,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.13" +version = "4.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +checksum = "c937d4061031a6d0c8da4b9a4f98a172fc2976dfb1c19213a9cf7d0d3c837e36" dependencies = [ "clap_builder", "clap_derive", @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.13" +version = "4.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +checksum = "85379ba512b21a328adf887e85f7742d12e96eb31f3ef077df4ffc26b506ffed" dependencies = [ "anstream", "anstyle", @@ -603,18 +603,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2cd2ebad..56b6f967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ edition = "2021" # On Update: Update the edition of the `rustfmt` command that c rust-version = "1.80" [workspace.dependencies] -serde = { version = "1.0.204", features = ["derive"] } +serde = { version = "1.0.205", features = ["derive"] } toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } [package] @@ -48,7 +48,7 @@ include = [ [dependencies] ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.86" -clap = { version = "4.5.13", features = ["derive"] } +clap = { version = "4.5.14", features = ["derive"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } From f5737b5a49f6dfafbefda74df05ca1a93cdec94a Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 01:08:52 +0200 Subject: [PATCH 052/211] Fix typos --- src/init.rs | 2 +- src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/init.rs b/src/init.rs index 3a0e11d2..62a670db 100644 --- a/src/init.rs +++ b/src/init.rs @@ -69,7 +69,7 @@ pub fn init() -> Result<()> { .stdout(Stdio::null()) .status()?; if !status.success() { - bail!("Failed to initilize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory"); + bail!("Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory"); } stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the Cargo.toml file of this Cargo workspace.\n")?; diff --git a/src/main.rs b/src/main.rs index 58cd8ff6..0855d435 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,7 +106,7 @@ fn main() -> Result<()> { write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; press_enter_prompt(&mut stdout)?; clear_terminal(&mut stdout)?; - // Flush to be able to show errors occuring before printing a newline to stdout. + // Flush to be able to show errors occurring before printing a newline to stdout. stdout.flush()?; } StateFileStatus::Read => (), From 82ebd29ff603b1f62c4e17f0f0c85cfcf05e70a0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 01:14:08 +0200 Subject: [PATCH 053/211] Add a special confirmation for initialization in a workspace --- src/init.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/init.rs b/src/init.rs index 62a670db..5e876d62 100644 --- a/src/init.rs +++ b/src/init.rs @@ -57,6 +57,9 @@ pub fn init() -> Result<()> { bail!("The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory"); } + stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\nPress ENTER to continue ")?; + press_enter_prompt(&mut stdout)?; + // Make sure "rustlings" is added to `workspace.members` by making // Cargo initialize a new project. let status = Command::new("cargo") @@ -76,11 +79,11 @@ pub fn init() -> Result<()> { fs::remove_dir_all("rustlings") .context("Failed to remove the temporary directory `rustlings/`")?; init_git = false; + } else { + stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; + press_enter_prompt(&mut stdout)?; } - stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; - press_enter_prompt(&mut stdout)?; - create_dir(rustlings_dir).context("Failed to create the `rustlings/` directory")?; set_current_dir(rustlings_dir) .context("Failed to change the current directory to `rustlings/`")?; From fc141b8dfc8326c35ad51f77aad4aef41cd62ee9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 01:16:45 +0200 Subject: [PATCH 054/211] Put Cargo.toml in `` --- src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 5e876d62..26fe09c5 100644 --- a/src/init.rs +++ b/src/init.rs @@ -75,7 +75,7 @@ pub fn init() -> Result<()> { bail!("Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory"); } - stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the Cargo.toml file of this Cargo workspace.\n")?; + 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") .context("Failed to remove the temporary directory `rustlings/`")?; init_git = false; From 16af9817721ac1855c66f2dd67627c820b91be5f Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 01:27:31 +0200 Subject: [PATCH 055/211] Hide stderr of `cargo locate-project` --- src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 26fe09c5..3a7ccf4d 100644 --- a/src/init.rs +++ b/src/init.rs @@ -30,7 +30,7 @@ pub fn init() -> Result<()> { .arg("-q") .arg("--workspace") .stdin(Stdio::null()) - .stderr(Stdio::inherit()) + .stderr(Stdio::null()) .output() .context(CARGO_LOCATE_PROJECT_ERR)?; From 52a231ce2f6c3853fc11a34a3935366f27e299f4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 02:17:01 +0200 Subject: [PATCH 056/211] Update Ratatui --- Cargo.lock | 62 +++++++++++++++++++++++++++++++---------------- Cargo.toml | 2 +- src/list.rs | 2 +- src/list/state.rs | 2 +- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3db242e..504113f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,13 +162,14 @@ checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "compact_str" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" dependencies = [ "castaway", "cfg-if", "itoa", + "rustversion", "ryu", "static_assertions", ] @@ -190,15 +191,15 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.6.0", "crossterm_winapi", - "libc", - "mio", + "mio 1.0.1", "parking_lot", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -278,6 +279,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "indexmap" version = "2.3.0" @@ -308,6 +315,16 @@ dependencies = [ "libc", ] +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -404,6 +421,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "notify" version = "6.1.1" @@ -418,7 +448,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -498,18 +528,18 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" +checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" dependencies = [ "bitflags 2.6.0", "cassowary", "compact_str", "crossterm", + "instability", "itertools", "lru", "paste", - "stability", "strum", "strum_macros", "unicode-segmentation", @@ -659,7 +689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 1.0.1", "signal-hook", ] @@ -678,16 +708,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "stability" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 56b6f967..1dcc55ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ anyhow = "1.0.86" clap = { version = "4.5.14", features = ["derive"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" -ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } +ratatui = { version = "0.28.0", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" } serde_json = "1.0.122" serde.workspace = true diff --git a/src/list.rs b/src/list.rs index 15836a44..a246ebc0 100644 --- a/src/list.rs +++ b/src/list.rs @@ -27,7 +27,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> { let mut ui_state = UiState::new(app_state); 'outer: loop { - terminal.draw(|frame| ui_state.draw(frame).unwrap())?; + terminal.try_draw(|frame| ui_state.draw(frame).map_err(io::Error::other))?; let key = loop { match event::read()? { diff --git a/src/list/state.rs b/src/list/state.rs index d6df6344..7bb95ffb 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -161,7 +161,7 @@ impl<'a> UiState<'a> { } pub fn draw(&mut self, frame: &mut Frame) -> Result<()> { - let area = frame.size(); + let area = frame.area(); frame.render_stateful_widget( &self.table, From a1d5702ba099263c42c73201d4eb44b4ad5785d5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 11:51:56 +0200 Subject: [PATCH 057/211] Ready for publish --- CHANGELOG.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1529ba00..391d9c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,20 @@ -## 6.2.0 (2024-08-08) +## 6.2.0 (2024-08-09) + +### Added - Show a message before checking and running an exercise. This gives the user instant feedback and avoids confusion if the checks take too long. - Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. -- Remove the state file and the solutions directory from the generated `.gitignore` file. - Add a `README.md` file to the `solutions/` directory. +- Allow initializing Rustlings in a Cargo workspace. +- `dev check`: Check that all solutions are formatted with `rustfmt`. + +### Changed + +- Remove the state file and the solutions directory from the generated `.gitignore` file. - Run the final check of all exercises in parallel. - Small exercise improvements. -- `dev check`: Check that all solutions are formatted with `rustfmt`. From 4472d50eba291a90017fa4c1974682c4392bf8b8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 11:52:31 +0200 Subject: [PATCH 058/211] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 504113f5..dfda6124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,7 +580,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.1.0" +version = "6.2.0" dependencies = [ "ahash", "anyhow", @@ -597,7 +597,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.1.0" +version = "6.2.0" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1dcc55ac..4ce639b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ exclude = [ ] [workspace.package] -version = "6.1.0" +version = "6.2.0" authors = [ "Mo Bitar ", # https://github.com/mo8it "Liv ", # https://github.com/shadows-withal @@ -52,7 +52,7 @@ clap = { version = "4.5.14", features = ["derive"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" ratatui = { version = "0.28.0", default-features = false, features = ["crossterm"] } -rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" } +rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" } serde_json = "1.0.122" serde.workspace = true toml_edit.workspace = true From ce3dcc98560a7555386556ba13d5e901bb27e2ed Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 9 Aug 2024 12:47:32 +0200 Subject: [PATCH 059/211] Fix typo --- solutions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/README.md b/solutions/README.md index 6a217fa4..5b5176b6 100644 --- a/solutions/README.md +++ b/solutions/README.md @@ -3,4 +3,4 @@ Before you finish an exercise, its solution file will only contain an empty `main` function. The content of this file will be automatically replaced by the actual solution once you finish the exercise. -Note that these solution are often only _one possibility_ to solve an exercise. +Note that these solutions are often only _one possibility_ to solve an exercise. From ed9740b72cbea165e030507ad5212e91d834d466 Mon Sep 17 00:00:00 2001 From: Chad Dougherty Date: Thu, 15 Aug 2024 14:21:27 -0400 Subject: [PATCH 060/211] fix typo Similarely -> Similarly in comment --- solutions/11_hashmaps/hashmaps3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs index 9c58b2d3..8a5d30b6 100644 --- a/solutions/11_hashmaps/hashmaps3.rs +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -35,7 +35,7 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { team_1.goals_scored += team_1_score; team_1.goals_conceded += team_2_score; - // Similarely for the second team. + // Similarly for the second team. let team_2 = scores .entry(team_2_name) .or_insert_with(TeamScores::default); From c903db5c533b4c047bb47740deb85ebfd467bdcc Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 16 Aug 2024 00:15:33 +0200 Subject: [PATCH 061/211] Add project lints --- Cargo.toml | 14 +++++++++++++- rustlings-macros/Cargo.toml | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4ce639b6..7e353d6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,18 @@ panic = "abort" [package.metadata.release] pre-release-hook = ["./release-hook.sh"] +[workspace.lints.rust] +unsafe_code = "forbid" +unstable_features = "forbid" + +[workspace.lints.clippy] +empty_loop = "forbid" +infinite_loop = "deny" +mem_forget = "deny" +dbg_macro = "warn" +todo = "warn" # TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102 -[lints.clippy] needless_option_as_deref = "allow" + +[lints] +workspace = true diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index f5ecfcc2..8a852018 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -19,3 +19,6 @@ proc-macro = true quote = "1.0.36" serde.workspace = true toml_edit.workspace = true + +[lints] +workspace = true From 0b3ad9141bc6a04d5216f8dec0163f92bcee4804 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 16 Aug 2024 00:24:38 +0200 Subject: [PATCH 062/211] Add exercise lints --- dev/Cargo.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index d814ba2b..7bde359c 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -201,3 +201,19 @@ panic = "abort" [profile.dev] panic = "abort" + +[lints.rust] +# You shouldn't write unsafe code in Rustlings +unsafe_code = "forbid" +# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust +unstable_features = "forbid" + +[lints.clippy] +# You forgot a `todo!()` +todo = "forbid" +# This can only happen by mistake in Rustlings +empty_loop = "forbid" +# No infinite loops are needed in Rustlings +infinite_loop = "deny" +# You shouldn't leak memory while still learning Rust +mem_forget = "deny" From 6ce31defb6386541e015c7905add3a6c138b35c6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 14:40:09 +0200 Subject: [PATCH 063/211] Ignore stdout of git init --- src/init.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 3a7ccf4d..95b04e9f 100644 --- a/src/init.rs +++ b/src/init.rs @@ -139,13 +139,14 @@ pub fn init() -> Result<()> { let _ = Command::new("git") .arg("init") .stdin(Stdio::null()) + .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); } writeln!( stdout, - "\n{}\n\n{}", + "{}\n\n{}", "Initialization done ✓".green(), POST_INIT_MSG.bold(), )?; From 8ef2ff12576ca3c3db443ad99475db3e9d656ab1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 14:54:13 +0200 Subject: [PATCH 064/211] Remove "Hello and" --- exercises/00_intro/intro1.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 7b8baa22..8bcaf683 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -6,8 +6,7 @@ // Try adding a new `println!` and check the updated output in the terminal. fn main() { - println!("Hello and"); - println!(r#" welcome to... "#); + println!(r#" Welcome to... "#); println!(r#" _ _ _ "#); println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#); println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#); From 8016f5ca2db2d790c1046b2c57a1add3bcf9cf64 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 14:55:58 +0200 Subject: [PATCH 065/211] Remove unneeded comma --- exercises/00_intro/intro1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 8bcaf683..172a6ab1 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,4 +1,4 @@ -// TODO: We sometimes encourage you to keep trying things on a given exercise, +// TODO: We sometimes encourage you to keep trying things on a given exercise // even after you already figured it out. If you got everything working and feel // ready for the next exercise, enter `n` in the terminal. // From 36f315c344a1c7cd8577d68d899a99f93d245bbd Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 14:56:52 +0200 Subject: [PATCH 066/211] Add "the" --- exercises/01_variables/variables1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/01_variables/variables1.rs b/exercises/01_variables/variables1.rs index 0a9e5548..f83b44d4 100644 --- a/exercises/01_variables/variables1.rs +++ b/exercises/01_variables/variables1.rs @@ -1,5 +1,5 @@ fn main() { - // TODO: Add missing keyword. + // TODO: Add the missing keyword. x = 5; println!("x has the value {x}"); From 69b4fd49fcfad1f71df4627c6fa7f1aa505d0778 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 14:59:00 +0200 Subject: [PATCH 067/211] Only take a u8 to avoid huge output --- exercises/02_functions/functions3.rs | 2 +- solutions/02_functions/functions3.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/02_functions/functions3.rs b/exercises/02_functions/functions3.rs index 5d5122af..8d654772 100644 --- a/exercises/02_functions/functions3.rs +++ b/exercises/02_functions/functions3.rs @@ -1,4 +1,4 @@ -fn call_me(num: u32) { +fn call_me(num: u8) { for i in 0..num { println!("Ring! Call number {}", i + 1); } diff --git a/solutions/02_functions/functions3.rs b/solutions/02_functions/functions3.rs index c581c425..ce5fe8eb 100644 --- a/solutions/02_functions/functions3.rs +++ b/solutions/02_functions/functions3.rs @@ -1,4 +1,4 @@ -fn call_me(num: u32) { +fn call_me(num: u8) { for i in 0..num { println!("Ring! Call number {}", i + 1); } From ca5d5f0a4909fbf53ed08e759a5f480648c88bb6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 15:45:02 +0200 Subject: [PATCH 068/211] Remove dot for copy-pasta --- exercises/13_error_handling/errors1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index ec7cb3cb..e07fddc3 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -6,7 +6,7 @@ // of `Option`. fn generate_nametag_text(name: String) -> Option { if name.is_empty() { - // Empty names aren't allowed. + // Empty names aren't allowed None } else { Some(format!("Hi! My name is {name}")) From e760f0776753a9a48f9c67634aa0814ec4f64cbb Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 15:53:24 +0200 Subject: [PATCH 069/211] Make it clear that reset only resets one exercise --- src/list/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 7bb95ffb..e4b28317 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -193,7 +193,7 @@ impl<'a> UiState<'a> { // Help footer. let mut spans = Vec::with_capacity(4); spans.push(Span::raw( - "↓/j ↑/k home/g end/G │ ontinue at │ eset │ filter ", + "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │ filter ", )); match self.filter { Filter::Done => { From 2baa140615f1cca592ab6f7b9b2af192571c36ec Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 15:53:34 +0200 Subject: [PATCH 070/211] q only quits the list --- src/list/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index e4b28317..b73b54e2 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -206,7 +206,7 @@ impl<'a> UiState<'a> { } Filter::None => spans.push(Span::raw("one/

ending")), } - spans.push(Span::raw(" │ uit")); + spans.push(Span::raw(" │ uit list")); Line::from(spans) } else { Line::from(self.message.as_str().light_blue()) From b678bd8ed2659d699bc2b73aee23162e9941c2d8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 16:34:43 +0200 Subject: [PATCH 071/211] Disable mouse in the list --- src/list.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/list.rs b/src/list.rs index a246ebc0..74ae0a26 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,14 +1,14 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use ratatui::{ backend::CrosstermBackend, crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, + QueueableCommand, }, Terminal, }; -use std::io; +use std::io::{self, Write}; use crate::app_state::AppState; @@ -18,7 +18,10 @@ mod state; pub fn list(app_state: &mut AppState) -> Result<()> { let mut stdout = io::stdout().lock(); - stdout.execute(EnterAlternateScreen)?; + stdout + .queue(EnterAlternateScreen)? + .queue(EnableMouseCapture)? + .flush()?; enable_raw_mode()?; let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; @@ -30,7 +33,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> { terminal.try_draw(|frame| ui_state.draw(frame).map_err(io::Error::other))?; let key = loop { - match event::read()? { + match event::read().context("Failed to read terminal event")? { Event::Key(key) => match key.kind { KeyEventKind::Press | KeyEventKind::Repeat => break key, KeyEventKind::Release => (), @@ -86,7 +89,10 @@ pub fn list(app_state: &mut AppState) -> Result<()> { } drop(terminal); - stdout.execute(LeaveAlternateScreen)?; + stdout + .queue(LeaveAlternateScreen)? + .queue(DisableMouseCapture)? + .flush()?; disable_raw_mode()?; Ok(()) From 3eaccbb61a730e0735ac151266e15fae79353f9d Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 16:49:07 +0200 Subject: [PATCH 072/211] Restore the terminal after an error in the list --- src/list.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/list.rs b/src/list.rs index 74ae0a26..f28230e4 100644 --- a/src/list.rs +++ b/src/list.rs @@ -8,7 +8,7 @@ use ratatui::{ }, Terminal, }; -use std::io::{self, Write}; +use std::io::{self, StdoutLock, Write}; use crate::app_state::AppState; @@ -16,15 +16,8 @@ use self::state::{Filter, UiState}; mod state; -pub fn list(app_state: &mut AppState) -> Result<()> { - let mut stdout = io::stdout().lock(); - stdout - .queue(EnterAlternateScreen)? - .queue(EnableMouseCapture)? - .flush()?; - enable_raw_mode()?; - - let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; +fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> { + let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?; terminal.clear()?; let mut ui_state = UiState::new(app_state); @@ -88,12 +81,25 @@ pub fn list(app_state: &mut AppState) -> Result<()> { } } - drop(terminal); + Ok(()) +} + +pub fn list(app_state: &mut AppState) -> Result<()> { + let mut stdout = io::stdout().lock(); + stdout + .queue(EnterAlternateScreen)? + .queue(EnableMouseCapture)? + .flush()?; + enable_raw_mode()?; + + let res = handle_list(app_state, &mut stdout); + + // Restore the terminal even if we got an error. stdout .queue(LeaveAlternateScreen)? .queue(DisableMouseCapture)? .flush()?; disable_raw_mode()?; - Ok(()) + res } From 72e557b3a9c2a802d81a56b6c08e69cda450f2c0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 16:54:44 +0200 Subject: [PATCH 073/211] Break help footer on narrow terminals --- src/list/state.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index b73b54e2..48a404f3 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -2,11 +2,11 @@ use anyhow::{Context, Result}; use ratatui::{ layout::{Constraint, Rect}, style::{Style, Stylize}, - text::{Line, Span}, + text::{Span, Text}, widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, Frame, }; -use std::fmt::Write; +use std::{fmt::Write, mem}; use crate::{app_state::AppState, progress_bar::progress_bar_ratatui}; @@ -162,6 +162,9 @@ impl<'a> UiState<'a> { pub fn draw(&mut self, frame: &mut Frame) -> Result<()> { let area = frame.area(); + let narrow = area.width < 95; + let narrow_u16 = u16::from(narrow); + let table_height = area.height - 3 - narrow_u16; frame.render_stateful_widget( &self.table, @@ -169,7 +172,7 @@ impl<'a> UiState<'a> { x: 0, y: 0, width: area.width, - height: area.height - 3, + height: table_height, }, &mut self.table_state, ); @@ -183,7 +186,7 @@ impl<'a> UiState<'a> { .block(Block::default().borders(Borders::BOTTOM)), Rect { x: 0, - y: area.height - 3, + y: table_height, width: area.width, height: 2, }, @@ -191,10 +194,19 @@ impl<'a> UiState<'a> { let message = if self.message.is_empty() { // Help footer. + let mut text = Text::default(); let mut spans = Vec::with_capacity(4); spans.push(Span::raw( - "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │ filter ", + "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │", )); + + if narrow { + text.push_line(mem::take(&mut spans)); + spans.push(Span::raw("filter ")); + } else { + spans.push(Span::raw(" filter ")); + } + match self.filter { Filter::Done => { spans.push("one".underlined().magenta()); @@ -206,18 +218,20 @@ impl<'a> UiState<'a> { } Filter::None => spans.push(Span::raw("one/

ending")), } + spans.push(Span::raw(" │ uit list")); - Line::from(spans) + text.push_line(spans); + text } else { - Line::from(self.message.as_str().light_blue()) + Text::from(self.message.as_str().light_blue()) }; frame.render_widget( message, Rect { x: 0, - y: area.height - 1, + y: table_height + 2, width: area.width, - height: 1, + height: 1 + narrow_u16, }, ); From 71f31d74bce5c657ab8a583c0a12a205e50e32cb Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 17 Aug 2024 16:57:58 +0200 Subject: [PATCH 074/211] Update deps --- Cargo.lock | 70 ++++++++++++++++++++++++++++-------------------------- Cargo.toml | 6 ++--- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfda6124..8a38321f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,9 +116,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.14" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c937d4061031a6d0c8da4b9a4f98a172fc2976dfb1c19213a9cf7d0d3c837e36" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.14" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85379ba512b21a328adf887e85f7742d12e96eb31f3ef077df4ffc26b506ffed" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", @@ -197,7 +197,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.6.0", "crossterm_winapi", - "mio 1.0.1", + "mio 1.0.2", "parking_lot", "rustix", "signal-hook", @@ -244,14 +244,14 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.59.0", ] [[package]] @@ -287,9 +287,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "indexmap" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown", @@ -368,9 +368,20 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.156" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall", +] [[package]] name = "linux-raw-sys" @@ -423,9 +434,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", @@ -497,7 +508,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -547,15 +558,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.3" @@ -633,18 +635,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.205" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.205" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", @@ -653,9 +655,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", "memchr", @@ -689,7 +691,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 1.0.1", + "mio 1.0.2", "signal-hook", ] @@ -744,9 +746,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7e353d6e..a8b81eb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ edition = "2021" # On Update: Update the edition of the `rustfmt` command that c rust-version = "1.80" [workspace.dependencies] -serde = { version = "1.0.205", features = ["derive"] } +serde = { version = "1.0.208", features = ["derive"] } toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } [package] @@ -48,12 +48,12 @@ include = [ [dependencies] ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.86" -clap = { version = "4.5.14", features = ["derive"] } +clap = { version = "4.5.16", features = ["derive"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" ratatui = { version = "0.28.0", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" } -serde_json = "1.0.122" +serde_json = "1.0.125" serde.workspace = true toml_edit.workspace = true From b70c1abd7c4dd4e7d4c95fd992a60dfcf9ce42fe Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 19 Aug 2024 23:28:53 +0200 Subject: [PATCH 075/211] Update deps --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a38321f..a66f4ba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,9 +368,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.156" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libredox" @@ -746,9 +746,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.74" +version = "2.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" dependencies = [ "proc-macro2", "quote", From 78a8553f1cfa7b9ae63d5d92702d68035f937041 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 19 Aug 2024 23:29:17 +0200 Subject: [PATCH 076/211] "Continue at" quits the list --- src/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list.rs b/src/list.rs index f28230e4..6ff69596 100644 --- a/src/list.rs +++ b/src/list.rs @@ -75,7 +75,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } KeyCode::Char('c') => { ui_state.selected_to_current_exercise()?; - ui_state = ui_state.with_updated_rows(); + break; } _ => (), } From b01fddef8b3bbf7805a5f767ac30b7c84fc8630f Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 19 Aug 2024 23:52:22 +0200 Subject: [PATCH 077/211] Show progress of `dev check` --- src/dev/check.rs | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index ca1b30c9..626a9c59 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -161,9 +161,9 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> R } fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { - println!( - "Running all exercises to check that they aren't already solved. This may take a while…\n", - ); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?; + thread::scope(|s| { let handles = info_file .exercises @@ -180,6 +180,11 @@ fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Res }) .collect::>(); + let n_handles = handles.len(); + write!(stdout, "Progress: 0/{n_handles}")?; + stdout.flush()?; + let mut handle_num = 1; + for (exercise_name, handle) in handles { let Ok(result) = handle.join() else { bail!("Panic while trying to run the exericse {exercise_name}"); @@ -192,7 +197,12 @@ fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Res Ok(false) => (), Err(e) => return Err(e), } + + write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; + stdout.flush()?; + handle_num += 1; } + stdout.write_all(b"\n")?; Ok(()) }) @@ -225,7 +235,9 @@ fn check_solutions( info_file: &InfoFile, cmd_runner: &CmdRunner, ) -> Result<()> { - println!("Running all solutions. This may take a while…\n"); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"Running all solutions...\n")?; + thread::scope(|s| { let handles = info_file .exercises @@ -261,6 +273,11 @@ fn check_solutions( .arg("always") .stdin(Stdio::null()); + let n_handles = handles.len(); + write!(stdout, "Progress: 0/{n_handles}")?; + stdout.flush()?; + let mut handle_num = 1; + for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { let Ok(check_result) = handle.join() else { bail!( @@ -282,7 +299,8 @@ fn check_solutions( } SolutionCheck::MissingOptional => (), SolutionCheck::RunFailure { output } => { - io::stderr().lock().write_all(&output)?; + stdout.write_all(b"\n\n")?; + stdout.write_all(&output)?; bail!( "Running the solution of the exercise {} failed with the error above", exercise_info.name, @@ -290,7 +308,12 @@ fn check_solutions( } SolutionCheck::Err(e) => return Err(e), } + + write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; + stdout.flush()?; + handle_num += 1; } + stdout.write_all(b"\n")?; let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths)); From 631f44331ea02abaac6f053fa9dfccb96fb8646f Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 13:08:15 +0200 Subject: [PATCH 078/211] Remove `--show-output` for tests and use `--format pretty` --- src/exercise.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index 500d1194..cdac2e2d 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -98,7 +98,7 @@ pub trait RunnableExercise { let output_is_some = output.is_some(); let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut()); if output_is_some { - test_cmd.args(["--", "--color", "always", "--show-output"]); + test_cmd.args(["--", "--color", "always", "--format", "pretty"]); } let test_success = test_cmd.run("cargo test …")?; if !test_success { From d141a73493ede10cb5d97cdd16b08560fa44de25 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 13:35:07 +0200 Subject: [PATCH 079/211] threads3: Improve the test --- exercises/20_threads/threads3.rs | 14 +++++--------- solutions/20_threads/threads3.rs | 14 +++++--------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs index 8aa7291f..6d16bd9f 100644 --- a/exercises/20_threads/threads3.rs +++ b/exercises/20_threads/threads3.rs @@ -1,7 +1,6 @@ use std::{sync::mpsc, thread, time::Duration}; struct Queue { - length: u32, first_half: Vec, second_half: Vec, } @@ -9,7 +8,6 @@ struct Queue { impl Queue { fn new() -> Self { Self { - length: 10, first_half: vec![1, 2, 3, 4, 5], second_half: vec![6, 7, 8, 9, 10], } @@ -48,17 +46,15 @@ mod tests { fn threads3() { let (tx, rx) = mpsc::channel(); let queue = Queue::new(); - let queue_length = queue.length; send_tx(queue, tx); - let mut total_received: u32 = 0; - for received in rx { - println!("Got: {received}"); - total_received += 1; + let mut received = Vec::with_capacity(10); + for value in rx { + received.push(value); } - println!("Number of received values: {total_received}"); - assert_eq!(total_received, queue_length); + received.sort(); + assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); } } diff --git a/solutions/20_threads/threads3.rs b/solutions/20_threads/threads3.rs index cd2dfbe3..7ceefea0 100644 --- a/solutions/20_threads/threads3.rs +++ b/solutions/20_threads/threads3.rs @@ -1,7 +1,6 @@ use std::{sync::mpsc, thread, time::Duration}; struct Queue { - length: u32, first_half: Vec, second_half: Vec, } @@ -9,7 +8,6 @@ struct Queue { impl Queue { fn new() -> Self { Self { - length: 10, first_half: vec![1, 2, 3, 4, 5], second_half: vec![6, 7, 8, 9, 10], } @@ -50,17 +48,15 @@ mod tests { fn threads3() { let (tx, rx) = mpsc::channel(); let queue = Queue::new(); - let queue_length = queue.length; send_tx(queue, tx); - let mut total_received: u32 = 0; - for received in rx { - println!("Got: {received}"); - total_received += 1; + let mut received = Vec::with_capacity(10); + for value in rx { + received.push(value); } - println!("Number of received values: {total_received}"); - assert_eq!(total_received, queue_length); + received.sort(); + assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); } } From e74f2a4274e22faf050a1229b4526ceb53b55479 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 13:39:14 +0200 Subject: [PATCH 080/211] Check for `#[test]` with newline at the end --- src/dev/check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 626a9c59..6cc24856 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -97,7 +97,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user."); } - if !exercise_info.test && file_buf.contains("#[test]") { + if !exercise_info.test && file_buf.contains("#[test]\n") { bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file"); } From 27999f2d2678607538d887032e38774a35d438b8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 13:49:48 +0200 Subject: [PATCH 081/211] Check if exercise doesn't contain tests --- src/dev/check.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 6cc24856..6f16e0bf 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -97,7 +97,12 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user."); } - if !exercise_info.test && file_buf.contains("#[test]\n") { + let contains_tests = file_buf.contains("#[test]\n"); + if exercise_info.test { + if !contains_tests { + bail!("The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file"); + } + } else if contains_tests { bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file"); } From 5b7368c46d9369a58075000b03be7f171f230f5c Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 13:54:20 +0200 Subject: [PATCH 082/211] Improve error message if no exercise exists --- src/info_file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/info_file.rs b/src/info_file.rs index f27d0185..d4e46110 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -135,4 +135,4 @@ impl InfoFile { } const NO_EXERCISES_ERR: &str = "There are no exercises yet! -If you are developing third-party exercises, add at least one exercise before testing."; +Add at least one exercise before testing."; From 13cc3acdfdcff91c059f4153c694464750a67b82 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 13:56:52 +0200 Subject: [PATCH 083/211] Improve readability --- src/cmd.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cmd.rs b/src/cmd.rs index a10a7eaa..4a93312a 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -74,12 +74,14 @@ impl CmdRunner { bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?"); } - let target_dir = serde_json::de::from_slice::(&metadata_output.stdout) + let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout) .context( "Failed to read the field `target_directory` from the output of the command `cargo metadata …`", - )?.target_directory; + )?; - Ok(Self { target_dir }) + Ok(Self { + target_dir: metadata.target_directory, + }) } pub fn cargo<'out>( From 8854f0a5ed2a0a3cd26ba1959ee8d0ec41798143 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 14:32:47 +0200 Subject: [PATCH 084/211] Use anyhow! --- src/dev/check.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 6f16e0bf..6ad19815 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -229,7 +229,6 @@ fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { enum SolutionCheck { Success { sol_path: String }, - MissingRequired, MissingOptional, RunFailure { output: Vec }, Err(Error), @@ -252,7 +251,10 @@ fn check_solutions( let sol_path = exercise_info.sol_path(); if !Path::new(&sol_path).exists() { if require_solutions { - return SolutionCheck::MissingRequired; + return SolutionCheck::Err(anyhow!( + "The solution of the exercise {} is missing", + exercise_info.name, + )); } return SolutionCheck::MissingOptional; @@ -296,12 +298,6 @@ fn check_solutions( fmt_cmd.arg(&sol_path); sol_paths.insert(PathBuf::from(sol_path)); } - SolutionCheck::MissingRequired => { - bail!( - "The solution of the exercise {} is missing", - exercise_info.name, - ); - } SolutionCheck::MissingOptional => (), SolutionCheck::RunFailure { output } => { stdout.write_all(b"\n\n")?; From 50f6e5232e342931beec67a7edae191feadf4d75 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 14:47:08 +0200 Subject: [PATCH 085/211] Leak info_file and cmd_runner in `dev check` --- src/dev/check.rs | 249 ++++++++++++++++++++++++----------------------- 1 file changed, 126 insertions(+), 123 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 6ad19815..e00d4cc1 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -165,65 +165,67 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> R Ok(()) } -fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { +fn check_exercises_unsolved( + info_file: &'static InfoFile, + cmd_runner: &'static CmdRunner, +) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?; - thread::scope(|s| { - let handles = info_file - .exercises - .iter() - .filter_map(|exercise_info| { - if exercise_info.skip_check_unsolved { - return None; - } - - Some(( - exercise_info.name.as_str(), - s.spawn(|| exercise_info.run_exercise(None, cmd_runner)), - )) - }) - .collect::>(); - - let n_handles = handles.len(); - write!(stdout, "Progress: 0/{n_handles}")?; - stdout.flush()?; - let mut handle_num = 1; - - for (exercise_name, handle) in handles { - let Ok(result) = handle.join() else { - bail!("Panic while trying to run the exericse {exercise_name}"); - }; - - match result { - Ok(true) => bail!( - "The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}", - ), - Ok(false) => (), - Err(e) => return Err(e), + let handles = info_file + .exercises + .iter() + .filter_map(|exercise_info| { + if exercise_info.skip_check_unsolved { + return None; } - write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; - stdout.flush()?; - handle_num += 1; - } - stdout.write_all(b"\n")?; + Some(( + exercise_info.name.as_str(), + thread::spawn(|| exercise_info.run_exercise(None, cmd_runner)), + )) + }) + .collect::>(); - Ok(()) - }) + let n_handles = handles.len(); + write!(stdout, "Progress: 0/{n_handles}")?; + stdout.flush()?; + let mut handle_num = 1; + + for (exercise_name, handle) in handles { + let Ok(result) = handle.join() else { + bail!("Panic while trying to run the exericse {exercise_name}"); + }; + + match result { + Ok(true) => { + bail!("The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",) + } + Ok(false) => (), + Err(e) => return Err(e), + } + + write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; + stdout.flush()?; + handle_num += 1; + } + stdout.write_all(b"\n")?; + + Ok(()) } -fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { +fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> { match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), Ordering::Equal => (), } - let info_file_paths = check_info_file_exercises(info_file)?; - let handle = thread::spawn(move || check_unexpected_files("exercises", &info_file_paths)); + let handle = thread::spawn(move || check_exercises_unsolved(info_file, cmd_runner)); + + let info_file_paths = check_info_file_exercises(info_file)?; + check_unexpected_files("exercises", &info_file_paths)?; - check_exercises_unsolved(info_file, cmd_runner)?; handle.join().unwrap() } @@ -236,98 +238,96 @@ enum SolutionCheck { fn check_solutions( require_solutions: bool, - info_file: &InfoFile, - cmd_runner: &CmdRunner, + info_file: &'static InfoFile, + cmd_runner: &'static CmdRunner, ) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.write_all(b"Running all solutions...\n")?; - thread::scope(|s| { - let handles = info_file - .exercises - .iter() - .map(|exercise_info| { - s.spawn(|| { - let sol_path = exercise_info.sol_path(); - if !Path::new(&sol_path).exists() { - if require_solutions { - return SolutionCheck::Err(anyhow!( - "The solution of the exercise {} is missing", - exercise_info.name, - )); - } - - return SolutionCheck::MissingOptional; + let handles = info_file + .exercises + .iter() + .map(|exercise_info| { + thread::spawn(move || { + let sol_path = exercise_info.sol_path(); + if !Path::new(&sol_path).exists() { + if require_solutions { + return SolutionCheck::Err(anyhow!( + "The solution of the exercise {} is missing", + exercise_info.name, + )); } - let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - match exercise_info.run_solution(Some(&mut output), cmd_runner) { - Ok(true) => SolutionCheck::Success { sol_path }, - Ok(false) => SolutionCheck::RunFailure { output }, - Err(e) => SolutionCheck::Err(e), - } - }) + return SolutionCheck::MissingOptional; + } + + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + match exercise_info.run_solution(Some(&mut output), cmd_runner) { + Ok(true) => SolutionCheck::Success { sol_path }, + Ok(false) => SolutionCheck::RunFailure { output }, + Err(e) => SolutionCheck::Err(e), + } }) - .collect::>(); + }) + .collect::>(); - let mut sol_paths = hash_set_with_capacity(info_file.exercises.len()); - let mut fmt_cmd = Command::new("rustfmt"); - fmt_cmd - .arg("--check") - .arg("--edition") - .arg("2021") - .arg("--color") - .arg("always") - .stdin(Stdio::null()); + let mut sol_paths = hash_set_with_capacity(info_file.exercises.len()); + let mut fmt_cmd = Command::new("rustfmt"); + fmt_cmd + .arg("--check") + .arg("--edition") + .arg("2021") + .arg("--color") + .arg("always") + .stdin(Stdio::null()); - let n_handles = handles.len(); - write!(stdout, "Progress: 0/{n_handles}")?; - stdout.flush()?; - let mut handle_num = 1; + let n_handles = handles.len(); + write!(stdout, "Progress: 0/{n_handles}")?; + stdout.flush()?; + let mut handle_num = 1; - for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { - let Ok(check_result) = handle.join() else { + for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { + let Ok(check_result) = handle.join() else { + bail!( + "Panic while trying to run the solution of the exericse {}", + exercise_info.name, + ); + }; + + match check_result { + SolutionCheck::Success { sol_path } => { + fmt_cmd.arg(&sol_path); + sol_paths.insert(PathBuf::from(sol_path)); + } + SolutionCheck::MissingOptional => (), + SolutionCheck::RunFailure { output } => { + stdout.write_all(b"\n\n")?; + stdout.write_all(&output)?; bail!( - "Panic while trying to run the solution of the exericse {}", + "Running the solution of the exercise {} failed with the error above", exercise_info.name, ); - }; - - match check_result { - SolutionCheck::Success { sol_path } => { - fmt_cmd.arg(&sol_path); - sol_paths.insert(PathBuf::from(sol_path)); - } - SolutionCheck::MissingOptional => (), - SolutionCheck::RunFailure { output } => { - stdout.write_all(b"\n\n")?; - stdout.write_all(&output)?; - bail!( - "Running the solution of the exercise {} failed with the error above", - exercise_info.name, - ); - } - SolutionCheck::Err(e) => return Err(e), } - - write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; - stdout.flush()?; - handle_num += 1; - } - stdout.write_all(b"\n")?; - - let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths)); - - if !fmt_cmd - .status() - .context("Failed to run `rustfmt` on all solution files")? - .success() - { - bail!("Some solutions aren't formatted. Run `rustfmt` on them"); + SolutionCheck::Err(e) => return Err(e), } - handle.join().unwrap() - }) + write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; + stdout.flush()?; + handle_num += 1; + } + stdout.write_all(b"\n")?; + + let handle = thread::spawn(move || check_unexpected_files("solutions", &sol_paths)); + + if !fmt_cmd + .status() + .context("Failed to run `rustfmt` on all solution files")? + .success() + { + bail!("Some solutions aren't formatted. Run `rustfmt` on them"); + } + + handle.join().unwrap() } pub fn check(require_solutions: bool) -> Result<()> { @@ -340,9 +340,12 @@ pub fn check(require_solutions: bool) -> Result<()> { check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?; } - let cmd_runner = CmdRunner::build()?; - check_exercises(&info_file, &cmd_runner)?; - check_solutions(require_solutions, &info_file, &cmd_runner)?; + // Leaking is fine since they are used until the end of the program. + let cmd_runner = Box::leak(Box::new(CmdRunner::build()?)); + let info_file = Box::leak(Box::new(info_file)); + + check_exercises(info_file, cmd_runner)?; + check_solutions(require_solutions, info_file, cmd_runner)?; println!("Everything looks fine!"); From e7ba88f90594962795bd028a8537efd6b1eedf6e Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 16:04:29 +0200 Subject: [PATCH 086/211] Highlight the solution file --- src/run.rs | 15 +++++++++------ src/watch/state.rs | 23 +++++++++++------------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/run.rs b/src/run.rs index 964e13b8..0bc965cd 100644 --- a/src/run.rs +++ b/src/run.rs @@ -33,18 +33,21 @@ pub fn run(app_state: &mut AppState) -> Result<()> { )?; if let Some(solution_path) = app_state.current_solution_path()? { - println!( - "\nA solution file can be found at {}\n", - style(TerminalFileLink(&solution_path)).underlined().green(), - ); + writeln!( + stdout, + "\n{} for comparison: {}\n", + "Solution".bold(), + style(TerminalFileLink(&solution_path)).underlined().cyan(), + )?; } match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!( + ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => writeln!( + stdout, "Next exercise: {}", app_state.current_exercise().terminal_link(), - ), + )?, } Ok(()) diff --git a/src/watch/state.rs b/src/watch/state.rs index abfff7ac..fe972faf 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -137,21 +137,20 @@ impl<'a> WatchState<'a> { } if self.done_status != DoneStatus::Pending { - writeln!( - self.writer, - "{}\n", - "Exercise done ✓ -When you are done experimenting, enter `n` to move on to the next exercise 🦀" - .bold() - .green(), - )?; - } + writeln!(self.writer, "{}", "Exercise done ✓".bold().green())?; + + if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { + writeln!( + self.writer, + "{} for comparison: {}", + "Solution".bold(), + style(TerminalFileLink(solution_path)).underlined().cyan(), + )?; + } - if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { writeln!( self.writer, - "A solution file can be found at {}\n", - style(TerminalFileLink(solution_path)).underlined().green(), + "When done experimenting, enter `n` to move on to the next exercise 🦀\n", )?; } From a2d1cb3b2246f50ea36408f2be1962f5dc69d09b Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 20 Aug 2024 16:05:52 +0200 Subject: [PATCH 087/211] Push newline after running an exercise instead on each rendering --- src/watch/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index fe972faf..6bf8e692 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -56,10 +56,12 @@ impl<'a> WatchState<'a> { "\nChecking the exercise `{}`. Please wait…", self.app_state.current_exercise().name, )?; + let success = self .app_state .current_exercise() .run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?; + self.output.push(b'\n'); if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { @@ -121,11 +123,9 @@ impl<'a> WatchState<'a> { pub fn render(&mut self) -> Result<()> { // Prevent having the first line shifted if clearing wasn't successful. self.writer.write_all(b"\n")?; - clear_terminal(&mut self.writer)?; self.writer.write_all(&self.output)?; - self.writer.write_all(b"\n")?; if self.show_hint { writeln!( From bedf0789f2129f333cc1af14775c40d7312297f5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 22 Aug 2024 14:25:14 +0200 Subject: [PATCH 088/211] Always use strict Clippy when checking solutions --- src/exercise.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index cdac2e2d..5318b9a5 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -72,7 +72,7 @@ pub trait RunnableExercise { // Compile, check and run the exercise or its solution (depending on `bin_name´). // The output is written to the `output` buffer after clearing it. - fn run( + fn run( &self, bin_name: &str, mut output: Option<&mut Vec>, @@ -115,7 +115,7 @@ pub trait RunnableExercise { let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut()); // `--profile test` is required to also check code with `[cfg(test)]`. - if self.strict_clippy() { + if FORCE_STRICT_CLIPPY || self.strict_clippy() { clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]); } else { clippy_cmd.args(["--profile", "test"]); @@ -131,7 +131,7 @@ pub trait RunnableExercise { /// 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) + self.run::(self.name(), output, cmd_runner) } /// Compile, check and run the exercise's solution. @@ -142,7 +142,7 @@ pub trait RunnableExercise { bin_name.push_str(name); bin_name.push_str("_sol"); - self.run(&bin_name, output, cmd_runner) + self.run::(&bin_name, output, cmd_runner) } } From 423b50b068f7cb489e4c5f241b696491419620c1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 22 Aug 2024 14:37:47 +0200 Subject: [PATCH 089/211] Use match instead of comparison chain --- exercises/13_error_handling/errors4.rs | 2 -- solutions/13_error_handling/errors4.rs | 12 ++++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index e41d5945..ba01e54b 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -1,5 +1,3 @@ -#![allow(clippy::comparison_chain)] - #[derive(PartialEq, Debug)] enum CreationError { Negative, diff --git a/solutions/13_error_handling/errors4.rs b/solutions/13_error_handling/errors4.rs index f4d39bf9..7f176cfc 100644 --- a/solutions/13_error_handling/errors4.rs +++ b/solutions/13_error_handling/errors4.rs @@ -1,5 +1,3 @@ -#![allow(clippy::comparison_chain)] - #[derive(PartialEq, Debug)] enum CreationError { Negative, @@ -11,12 +9,10 @@ struct PositiveNonzeroInteger(u64); impl PositiveNonzeroInteger { fn new(value: i64) -> Result { - if value == 0 { - Err(CreationError::Zero) - } else if value < 0 { - Err(CreationError::Negative) - } else { - Ok(Self(value as u64)) + match value.cmp(&0) { + Ordering::Less => Err(CreationError::Negative), + Ordering::Equal => Err(CreationError::Zero), + Ordering::Greater => Ok(Self(value as u64)), } } } From f1abd8577c824eac4eb152a4b0789ce23642ba62 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 22 Aug 2024 14:41:25 +0200 Subject: [PATCH 090/211] Add missing Clippy allows to solutions --- solutions/09_strings/strings4.rs | 7 +++---- solutions/18_iterators/iterators4.rs | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs index fe4733e3..3c69b976 100644 --- a/solutions/09_strings/strings4.rs +++ b/solutions/09_strings/strings4.rs @@ -18,12 +18,11 @@ fn main() { // Here, both answers work. // `.into()` converts a type into an expected type. // If it is called where `String` is expected, it will convert `&str` to `String`. - // But if is called where `&str` is expected, then `&str` is kept `&str` since no - // conversion is needed. string("nice weather".into()); + // But if it is called where `&str` is expected, then `&str` is kept `&str` since no conversion is needed. + // If you remove the `#[allow(…)]` line, then Clippy will tell you to remove `.into()` below since it is a useless conversion. + #[allow(clippy::useless_conversion)] string_slice("nice weather".into()); - // ^^^^^^^ the compiler recommends removing the `.into()` - // call because it is a useless conversion. string(format!("Interpolation {}", "Station")); diff --git a/solutions/18_iterators/iterators4.rs b/solutions/18_iterators/iterators4.rs index 4c3c49d9..4168835a 100644 --- a/solutions/18_iterators/iterators4.rs +++ b/solutions/18_iterators/iterators4.rs @@ -25,6 +25,7 @@ fn factorial_fold(num: u64) -> u64 { // -> 1 * 2 is calculated, then the result 2 is multiplied by // the second element 3 so the result 6 is returned. // And so on… + #[allow(clippy::unnecessary_fold)] (2..=num).fold(1, |acc, x| acc * x) } From 47976caa69e24ea9ee5d38918a0abea89ff10983 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 22 Aug 2024 14:42:17 +0200 Subject: [PATCH 091/211] Import Ordering --- solutions/13_error_handling/errors4.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solutions/13_error_handling/errors4.rs b/solutions/13_error_handling/errors4.rs index 7f176cfc..70c5f1ca 100644 --- a/solutions/13_error_handling/errors4.rs +++ b/solutions/13_error_handling/errors4.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + #[derive(PartialEq, Debug)] enum CreationError { Negative, From 570bc9f32d7ef0bf741fab44d15f7cd54a1f3fc1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 24 Aug 2024 00:14:12 +0200 Subject: [PATCH 092/211] Start list without Ratatui --- .typos.toml | 3 - Cargo.lock | 163 +--------------- Cargo.toml | 2 +- rustlings-macros/Cargo.toml | 2 +- src/exercise.rs | 2 +- src/init.rs | 2 +- src/list.rs | 142 +++++++------- src/list/state.rs | 361 +++++++++++++++++------------------- src/main.rs | 1 + src/progress_bar.rs | 93 +++------- src/run.rs | 2 +- src/term.rs | 12 +- src/watch/state.rs | 14 +- src/watch/terminal_event.rs | 4 +- 14 files changed, 303 insertions(+), 500 deletions(-) diff --git a/.typos.toml b/.typos.toml index a74498ab..2de6d580 100644 --- a/.typos.toml +++ b/.typos.toml @@ -2,6 +2,3 @@ extend-exclude = [ "CHANGELOG.md", ] - -[default.extend-words] -"ratatui" = "ratatui" diff --git a/Cargo.lock b/Cargo.lock index a66f4ba3..93b2051b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - [[package]] name = "anstream" version = "0.6.15" @@ -93,21 +87,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - -[[package]] -name = "castaway" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" -dependencies = [ - "rustversion", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -160,20 +139,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" -[[package]] -name = "compact_str" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -214,12 +179,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - [[package]] name = "equivalent" version = "1.0.1" @@ -268,10 +227,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "heck" @@ -315,31 +270,12 @@ dependencies = [ "libc", ] -[[package]] -name = "instability" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -405,15 +341,6 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "lru" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" -dependencies = [ - "hashbrown", -] - [[package]] name = "memchr" version = "2.7.4" @@ -513,12 +440,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "proc-macro2" version = "1.0.86" @@ -530,34 +451,13 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] -[[package]] -name = "ratatui" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" -dependencies = [ - "bitflags 2.6.0", - "cassowary", - "compact_str", - "crossterm", - "instability", - "itertools", - "lru", - "paste", - "strum", - "strum_macros", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - [[package]] name = "redox_syscall" version = "0.5.3" @@ -587,9 +487,9 @@ dependencies = [ "ahash", "anyhow", "clap", + "crossterm", "notify-debouncer-mini", "os_pipe", - "ratatui", "rustlings-macros", "serde", "serde_json", @@ -606,12 +506,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "rustversion" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - [[package]] name = "ryu" version = "1.0.18" @@ -710,40 +604,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "syn" version = "2.0.75" @@ -796,29 +662,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "unicode-width" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" - [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index a8b81eb2..4b3e98c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +49,9 @@ include = [ ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.86" clap = { version = "4.5.16", features = ["derive"] } +crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" -ratatui = { version = "0.28.0", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" } serde_json = "1.0.125" serde.workspace = true diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index 8a852018..3ed56a18 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -16,7 +16,7 @@ include = [ proc-macro = true [dependencies] -quote = "1.0.36" +quote = "1.0.37" serde.workspace = true toml_edit.workspace = true diff --git a/src/exercise.rs b/src/exercise.rs index 5318b9a5..ac5c6e6e 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use ratatui::crossterm::style::{style, StyledContent, Stylize}; +use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, io::Write, diff --git a/src/init.rs b/src/init.rs index 95b04e9f..2c172dca 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use ratatui::crossterm::style::Stylize; +use crossterm::style::Stylize; use serde::Deserialize; use std::{ env::set_current_dir, diff --git a/src/list.rs b/src/list.rs index 6ff69596..754c5e2f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,95 +1,101 @@ use anyhow::{Context, Result}; -use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - QueueableCommand, +use crossterm::{ + cursor, + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind, }, - Terminal, + terminal::{ + disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen, + LeaveAlternateScreen, + }, + QueueableCommand, }; use std::io::{self, StdoutLock, Write}; use crate::app_state::AppState; -use self::state::{Filter, UiState}; +use self::state::{Filter, ListState}; mod state; fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> { - let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?; - terminal.clear()?; + let mut list_state = ListState::new(app_state, stdout)?; - let mut ui_state = UiState::new(app_state); + loop { + match event::read().context("Failed to read terminal event")? { + Event::Key(key) => { + match key.kind { + KeyEventKind::Release => continue, + KeyEventKind::Press | KeyEventKind::Repeat => (), + } - 'outer: loop { - terminal.try_draw(|frame| ui_state.draw(frame).map_err(io::Error::other))?; + list_state.message.clear(); - let key = loop { - match event::read().context("Failed to read terminal event")? { - Event::Key(key) => match key.kind { - KeyEventKind::Press | KeyEventKind::Repeat => break key, - KeyEventKind::Release => (), - }, - // Redraw - Event::Resize(_, _) => continue 'outer, - // Ignore - Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (), + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Down | KeyCode::Char('j') => list_state.select_next(), + KeyCode::Up | KeyCode::Char('k') => list_state.select_previous(), + KeyCode::Home | KeyCode::Char('g') => list_state.select_first(), + KeyCode::End | KeyCode::Char('G') => list_state.select_last(), + KeyCode::Char('d') => { + let message = if list_state.filter() == Filter::Done { + list_state.set_filter(Filter::None); + "Disabled filter DONE" + } else { + list_state.set_filter(Filter::Done); + "Enabled filter DONE │ Press d again to disable the filter" + }; + + list_state.message.push_str(message); + } + KeyCode::Char('p') => { + let message = if list_state.filter() == Filter::Pending { + list_state.set_filter(Filter::None); + "Disabled filter PENDING" + } else { + list_state.set_filter(Filter::Pending); + "Enabled filter PENDING │ Press p again to disable the filter" + }; + + list_state.message.push_str(message); + } + KeyCode::Char('r') => { + list_state.reset_selected()?; + } + KeyCode::Char('c') => { + return list_state.selected_to_current_exercise(); + } + // Redraw to remove the message. + KeyCode::Esc => (), + _ => continue, + } + + list_state.redraw(stdout)?; } - }; + Event::Mouse(event) => { + match event.kind { + MouseEventKind::ScrollDown => list_state.select_next(), + MouseEventKind::ScrollUp => list_state.select_previous(), + _ => continue, + } - ui_state.message.clear(); - - match key.code { - KeyCode::Char('q') => break, - KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(), - KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(), - KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), - KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), - KeyCode::Char('d') => { - let message = if ui_state.filter == Filter::Done { - ui_state.filter = Filter::None; - "Disabled filter DONE" - } else { - ui_state.filter = Filter::Done; - "Enabled filter DONE │ Press d again to disable the filter" - }; - - ui_state = ui_state.with_updated_rows(); - ui_state.message.push_str(message); + list_state.redraw(stdout)?; } - KeyCode::Char('p') => { - let message = if ui_state.filter == Filter::Pending { - ui_state.filter = Filter::None; - "Disabled filter PENDING" - } else { - ui_state.filter = Filter::Pending; - "Enabled filter PENDING │ Press p again to disable the filter" - }; - - ui_state = ui_state.with_updated_rows(); - ui_state.message.push_str(message); - } - KeyCode::Char('r') => { - ui_state = ui_state.with_reset_selected()?; - } - KeyCode::Char('c') => { - ui_state.selected_to_current_exercise()?; - break; - } - _ => (), + // Redraw + Event::Resize(_, _) => list_state.redraw(stdout)?, + // Ignore + Event::FocusGained | Event::FocusLost => (), } } - - Ok(()) } pub fn list(app_state: &mut AppState) -> Result<()> { let mut stdout = io::stdout().lock(); stdout .queue(EnterAlternateScreen)? - .queue(EnableMouseCapture)? - .flush()?; + .queue(cursor::Hide)? + .queue(DisableLineWrap)? + .queue(EnableMouseCapture)?; enable_raw_mode()?; let res = handle_list(app_state, &mut stdout); @@ -97,6 +103,8 @@ pub fn list(app_state: &mut AppState) -> Result<()> { // Restore the terminal even if we got an error. stdout .queue(LeaveAlternateScreen)? + .queue(cursor::Show)? + .queue(EnableLineWrap)? .queue(DisableMouseCapture)? .flush()?; disable_raw_mode()?; diff --git a/src/list/state.rs b/src/list/state.rs index 48a404f3..cf147b44 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -1,14 +1,19 @@ use anyhow::{Context, Result}; -use ratatui::{ - layout::{Constraint, Rect}, - style::{Style, Stylize}, - text::{Span, Text}, - widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, - Frame, +use crossterm::{ + cursor::{MoveDown, MoveTo}, + style::{Color, ResetColor, SetForegroundColor}, + terminal::{self, BeginSynchronizedUpdate, EndSynchronizedUpdate}, + QueueableCommand, +}; +use std::{ + fmt::Write as _, + io::{self, StdoutLock, Write as _}, }; -use std::{fmt::Write, mem}; -use crate::{app_state::AppState, progress_bar::progress_bar_ratatui}; +use crate::{app_state::AppState, term::clear_terminal, MAX_EXERCISE_NAME_LEN}; + +// +1 for padding. +const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { @@ -17,230 +22,213 @@ pub enum Filter { None, } -pub struct UiState<'a> { - pub table: Table<'static>, +pub struct ListState<'a> { pub message: String, - pub filter: Filter, + filter: Filter, app_state: &'a mut AppState, - table_state: TableState, - n_rows: usize, + n_rows_with_filter: usize, + name_col_width: usize, + offset: usize, + selected: Option, } -impl<'a> UiState<'a> { - pub fn with_updated_rows(mut self) -> Self { - let current_exercise_ind = self.app_state.current_exercise_ind(); - - self.n_rows = 0; - let rows = self - .app_state - .exercises() - .iter() - .enumerate() - .filter_map(|(ind, exercise)| { - let exercise_state = if exercise.done { - if self.filter == Filter::Pending { - return None; - } - - "DONE".green() - } else { - if self.filter == Filter::Done { - return None; - } - - "PENDING".yellow() - }; - - self.n_rows += 1; - - let next = if ind == current_exercise_ind { - ">>>>".bold().red() - } else { - Span::default() - }; - - Some(Row::new([ - next, - exercise_state, - Span::raw(exercise.name), - Span::raw(exercise.path), - ])) - }); - - self.table = self.table.rows(rows); - - if self.n_rows == 0 { - self.table_state.select(None); - } else { - self.table_state.select(Some( - self.table_state - .selected() - .map_or(0, |selected| selected.min(self.n_rows - 1)), - )); - } - - self - } - - pub fn new(app_state: &'a mut AppState) -> Self { - let header = Row::new(["Next", "State", "Name", "Path"]); - - let max_name_len = app_state +impl<'a> ListState<'a> { + pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result { + let name_col_width = app_state .exercises() .iter() .map(|exercise| exercise.name.len()) .max() - .unwrap_or(4) as u16; + .map_or(4, |max| max.max(4)); - let widths = [ - Constraint::Length(4), - Constraint::Length(7), - Constraint::Length(max_name_len), - Constraint::Fill(1), - ]; - - let table = Table::default() - .widths(widths) - .header(header) - .column_spacing(2) - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) - .highlight_symbol("🦀") - .block(Block::default().borders(Borders::BOTTOM)); + clear_terminal(stdout)?; + stdout.write_all(b" Current State Name ")?; + stdout.write_all(&SPACE[..name_col_width - 4])?; + stdout.write_all(b"Path\r\n")?; let selected = app_state.current_exercise_ind(); - let table_state = TableState::default() - .with_offset(selected.saturating_sub(10)) - .with_selected(Some(selected)); + let n_rows_with_filter = app_state.exercises().len(); - let filter = Filter::None; - let n_rows = app_state.exercises().len(); - - let slf = Self { - table, + let mut slf = Self { message: String::with_capacity(128), - filter, + filter: Filter::None, app_state, - table_state, - n_rows, + n_rows_with_filter, + name_col_width, + offset: selected.saturating_sub(10), + selected: Some(selected), }; - slf.with_updated_rows() + slf.redraw(stdout)?; + + Ok(slf) + } + + #[inline] + pub fn filter(&self) -> Filter { + self.filter + } + + pub fn set_filter(&mut self, filter: Filter) { + self.filter = filter; + self.n_rows_with_filter = match filter { + Filter::Done => self + .app_state + .exercises() + .iter() + .filter(|exercise| !exercise.done) + .count(), + Filter::Pending => self + .app_state + .exercises() + .iter() + .filter(|exercise| exercise.done) + .count(), + Filter::None => self.app_state.exercises().len(), + }; + + if self.n_rows_with_filter == 0 { + self.selected = None; + } else { + self.selected = Some( + self.selected + .map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)), + ); + } } pub fn select_next(&mut self) { - if self.n_rows > 0 { - let next = self - .table_state - .selected() - .map_or(0, |selected| (selected + 1).min(self.n_rows - 1)); - self.table_state.select(Some(next)); + if self.n_rows_with_filter > 0 { + let next = self.selected.map_or(0, |selected| { + (selected + 1).min(self.n_rows_with_filter - 1) + }); + self.selected = Some(next); } } pub fn select_previous(&mut self) { - if self.n_rows > 0 { + if self.n_rows_with_filter > 0 { let previous = self - .table_state - .selected() + .selected .map_or(0, |selected| selected.saturating_sub(1)); - self.table_state.select(Some(previous)); + self.selected = Some(previous); } } pub fn select_first(&mut self) { - if self.n_rows > 0 { - self.table_state.select(Some(0)); + if self.n_rows_with_filter > 0 { + self.selected = Some(0); } } pub fn select_last(&mut self) { - if self.n_rows > 0 { - self.table_state.select(Some(self.n_rows - 1)); + if self.n_rows_with_filter > 0 { + self.selected = Some(self.n_rows_with_filter - 1); } } - pub fn draw(&mut self, frame: &mut Frame) -> Result<()> { - let area = frame.area(); - let narrow = area.width < 95; + pub fn redraw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { + stdout.queue(BeginSynchronizedUpdate)?; + stdout.queue(MoveTo(0, 1))?; + let (width, height) = terminal::size()?; + let narrow = width < 95; let narrow_u16 = u16::from(narrow); - let table_height = area.height - 3 - narrow_u16; + let max_n_rows_to_display = height.saturating_sub(narrow_u16 + 4); - frame.render_stateful_widget( - &self.table, - Rect { - x: 0, - y: 0, - width: area.width, - height: table_height, - }, - &mut self.table_state, - ); + let displayed_exercises = self + .app_state + .exercises() + .iter() + .enumerate() + .filter(|(_, exercise)| match self.filter { + Filter::Done => exercise.done, + Filter::Pending => !exercise.done, + Filter::None => true, + }) + .skip(self.offset) + .take(max_n_rows_to_display as usize); - frame.render_widget( - Paragraph::new(progress_bar_ratatui( - self.app_state.n_done(), - self.app_state.exercises().len() as u16, - area.width, - )?) - .block(Block::default().borders(Borders::BOTTOM)), - Rect { - x: 0, - y: table_height, - width: area.width, - height: 2, - }, - ); - - let message = if self.message.is_empty() { - // Help footer. - let mut text = Text::default(); - let mut spans = Vec::with_capacity(4); - spans.push(Span::raw( - "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │", - )); - - if narrow { - text.push_line(mem::take(&mut spans)); - spans.push(Span::raw("filter ")); + let mut n_displayed_rows: u16 = 0; + let current_exercise_ind = self.app_state.current_exercise_ind(); + for (ind, exercise) in displayed_exercises { + if self.selected == Some(n_displayed_rows as usize) { + write!(stdout, "🦀")?; } else { - spans.push(Span::raw(" filter ")); + stdout.write_all(b" ")?; } - match self.filter { - Filter::Done => { - spans.push("one".underlined().magenta()); - spans.push(Span::raw("/

ending")); - } - Filter::Pending => { - spans.push(Span::raw("one/")); - spans.push("

ending".underlined().magenta()); - } - Filter::None => spans.push(Span::raw("one/

ending")), + if ind == current_exercise_ind { + stdout.queue(SetForegroundColor(Color::Red))?; + stdout.write_all(b">>>>>>> ")?; + } else { + stdout.write_all(b" ")?; } - spans.push(Span::raw(" │ uit list")); - text.push_line(spans); - text - } else { - Text::from(self.message.as_str().light_blue()) - }; - frame.render_widget( - message, - Rect { - x: 0, - y: table_height + 2, - width: area.width, - height: 1 + narrow_u16, - }, - ); + if exercise.done { + stdout.queue(SetForegroundColor(Color::Yellow))?; + stdout.write_all(b"DONE ")?; + } else { + stdout.queue(SetForegroundColor(Color::Green))?; + stdout.write_all(b"PENDING ")?; + } + + stdout.queue(ResetColor)?; + + stdout.write_all(exercise.name.as_bytes())?; + stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; + + stdout.write_all(exercise.path.as_bytes())?; + stdout.write_all(b"\r\n")?; + + n_displayed_rows += 1; + } + + stdout.queue(MoveDown(max_n_rows_to_display - n_displayed_rows))?; + + // TODO + // let message = if self.message.is_empty() { + // // Help footer. + // let mut text = Text::default(); + // let mut spans = Vec::with_capacity(4); + // spans.push(Span::raw( + // "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │", + // )); + + // if narrow { + // text.push_line(mem::take(&mut spans)); + // spans.push(Span::raw("filter ")); + // } else { + // spans.push(Span::raw(" filter ")); + // } + + // match self.filter { + // Filter::Done => { + // spans.push("one".underlined().magenta()); + // spans.push(Span::raw("/

ending")); + // } + // Filter::Pending => { + // spans.push(Span::raw("one/")); + // spans.push("

ending".underlined().magenta()); + // } + // Filter::None => spans.push(Span::raw("one/

ending")), + // } + + // spans.push(Span::raw(" │ uit list")); + // text.push_line(spans); + // text + // } else { + // Text::from(self.message.as_str().light_blue()) + // }; + + stdout.queue(EndSynchronizedUpdate)?; + stdout.flush()?; Ok(()) } - pub fn with_reset_selected(mut self) -> Result { - let Some(selected) = self.table_state.selected() else { - return Ok(self); + pub fn reset_selected(&mut self) -> Result<()> { + let Some(selected) = self.selected else { + return Ok(()); }; let ind = self @@ -259,11 +247,12 @@ impl<'a> UiState<'a> { let exercise_path = self.app_state.reset_exercise_by_ind(ind)?; write!(self.message, "The exercise {exercise_path} has been reset")?; - Ok(self.with_updated_rows()) + Ok(()) } pub fn selected_to_current_exercise(&mut self) -> Result<()> { - let Some(selected) = self.table_state.selected() else { + let Some(selected) = self.selected else { + // TODO: Don't exit list return Ok(()); }; diff --git a/src/main.rs b/src/main.rs index 0855d435..59513671 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ mod terminal_link; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; +const MAX_EXERCISE_NAME_LEN: usize = 32; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] diff --git a/src/progress_bar.rs b/src/progress_bar.rs index 7f07ad59..837c4c78 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -1,100 +1,53 @@ -use anyhow::{bail, Result}; -use ratatui::text::{Line, Span}; -use std::fmt::Write; +use std::io::{self, StdoutLock, Write}; -const PREFIX: &str = "Progress: ["; +use crossterm::{ + style::{Color, ResetColor, SetForegroundColor}, + QueueableCommand, +}; + +const PREFIX: &[u8] = b"Progress: ["; const PREFIX_WIDTH: u16 = PREFIX.len() as u16; // Leaving the last char empty (_) for `total` > 99. const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16; const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; -const PROGRESS_EXCEEDS_MAX_ERR: &str = - "The progress of the progress bar is higher than the maximum"; - /// Terminal progress bar to be used when not using Ratataui. -pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result { - use ratatui::crossterm::style::Stylize; - - if progress > total { - bail!(PROGRESS_EXCEEDS_MAX_ERR); - } +pub fn progress_bar( + stdout: &mut StdoutLock, + progress: u16, + total: u16, + line_width: u16, +) -> io::Result<()> { + debug_assert!(progress <= total); if line_width < MIN_LINE_WIDTH { - return Ok(format!("Progress: {progress}/{total} exercises")); + return write!(stdout, "Progress: {progress}/{total} exercises"); } - let mut line = String::with_capacity(usize::from(line_width)); - line.push_str(PREFIX); + stdout.write_all(PREFIX)?; let width = line_width - WRAPPER_WIDTH; let filled = (width * progress) / total; - let mut green_part = String::with_capacity(usize::from(filled + 1)); + stdout.queue(SetForegroundColor(Color::Green))?; for _ in 0..filled { - green_part.push('#'); + stdout.write_all(b"#")?; } if filled < width { - green_part.push('>'); + stdout.write_all(b">")?; } - write!(line, "{}", green_part.green()).unwrap(); let width_minus_filled = width - filled; if width_minus_filled > 1 { let red_part_width = width_minus_filled - 1; - let mut red_part = String::with_capacity(usize::from(red_part_width)); + stdout.queue(SetForegroundColor(Color::Red))?; for _ in 0..red_part_width { - red_part.push('-'); + stdout.write_all(b"-")?; } - write!(line, "{}", red_part.red()).unwrap(); } - writeln!(line, "] {progress:>3}/{total} exercises").unwrap(); - - Ok(line) -} - -/// Progress bar to be used with Ratataui. -// Not using Ratatui's Gauge widget to keep the progress bar consistent. -pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result> { - use ratatui::style::Stylize; - - if progress > total { - bail!(PROGRESS_EXCEEDS_MAX_ERR); - } - - if line_width < MIN_LINE_WIDTH { - return Ok(Line::raw(format!("Progress: {progress}/{total} exercises"))); - } - - let mut spans = Vec::with_capacity(4); - spans.push(Span::raw(PREFIX)); - - let width = line_width - WRAPPER_WIDTH; - let filled = (width * progress) / total; - - let mut green_part = String::with_capacity(usize::from(filled + 1)); - for _ in 0..filled { - green_part.push('#'); - } - - if filled < width { - green_part.push('>'); - } - spans.push(green_part.green()); - - let width_minus_filled = width - filled; - if width_minus_filled > 1 { - let red_part_width = width_minus_filled - 1; - let mut red_part = String::with_capacity(usize::from(red_part_width)); - for _ in 0..red_part_width { - red_part.push('-'); - } - spans.push(red_part.red()); - } - - spans.push(Span::raw(format!("] {progress:>3}/{total} exercises"))); - - Ok(Line::from(spans)) + stdout.queue(ResetColor)?; + write!(stdout, "] {progress:>3}/{total} exercises") } diff --git a/src/run.rs b/src/run.rs index 0bc965cd..09e53ec9 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Result}; -use ratatui::crossterm::style::{style, Stylize}; +use crossterm::style::{style, Stylize}; use std::io::{self, Write}; use crate::{ diff --git a/src/term.rs b/src/term.rs index e1ac3da9..07edf900 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,7 +1,17 @@ use std::io::{self, BufRead, StdoutLock, Write}; +use crossterm::{ + cursor::MoveTo, + terminal::{Clear, ClearType}, + QueueableCommand, +}; + pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { - stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") + stdout + .queue(MoveTo(0, 0))? + .queue(Clear(ClearType::All))? + .queue(Clear(ClearType::Purge)) + .map(|_| ()) } pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { diff --git a/src/watch/state.rs b/src/watch/state.rs index 6bf8e692..26c83d50 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use ratatui::crossterm::{ +use crossterm::{ style::{style, Stylize}, terminal, }; @@ -76,7 +76,8 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } - self.render() + self.render()?; + Ok(()) } pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> { @@ -120,7 +121,7 @@ impl<'a> WatchState<'a> { self.writer.flush() } - pub fn render(&mut self) -> Result<()> { + pub fn render(&mut self) -> io::Result<()> { // Prevent having the first line shifted if clearing wasn't successful. self.writer.write_all(b"\n")?; clear_terminal(&mut self.writer)?; @@ -155,14 +156,15 @@ impl<'a> WatchState<'a> { } let line_width = terminal::size()?.0; - let progress_bar = progress_bar( + progress_bar( + &mut self.writer, self.app_state.n_done(), self.app_state.exercises().len() as u16, line_width, )?; writeln!( self.writer, - "{progress_bar}Current exercise: {}", + "\nCurrent exercise: {}", self.app_state.current_exercise().terminal_link(), )?; @@ -171,7 +173,7 @@ impl<'a> WatchState<'a> { Ok(()) } - pub fn show_hint(&mut self) -> Result<()> { + pub fn show_hint(&mut self) -> io::Result<()> { self.show_hint = true; self.render() } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 3a1762d3..3e8c272a 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,4 +1,4 @@ -use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; +use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use std::sync::mpsc::Sender; use super::WatchEvent; @@ -78,7 +78,7 @@ pub fn terminal_event_handler(tx: Sender, manual_run: bool) { return; } } - Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, + Event::FocusGained | Event::FocusLost | Event::Mouse(_) => continue, } }; From 4e12725616abe1918d6a4f21b23288dfac237cc4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 24 Aug 2024 00:23:45 +0200 Subject: [PATCH 093/211] Don't exit the list on "to current" if nothing is selected --- src/list.rs | 4 +++- src/list/state.rs | 20 +++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/list.rs b/src/list.rs index 754c5e2f..27a31d13 100644 --- a/src/list.rs +++ b/src/list.rs @@ -63,7 +63,9 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> list_state.reset_selected()?; } KeyCode::Char('c') => { - return list_state.selected_to_current_exercise(); + if list_state.selected_to_current_exercise()? { + return Ok(()); + } } // Redraw to remove the message. KeyCode::Esc => (), diff --git a/src/list/state.rs b/src/list/state.rs index cf147b44..645c768f 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -250,25 +250,27 @@ impl<'a> ListState<'a> { Ok(()) } - pub fn selected_to_current_exercise(&mut self) -> Result<()> { + // Return `true` if there was something to select. + pub fn selected_to_current_exercise(&mut self) -> Result { let Some(selected) = self.selected else { - // TODO: Don't exit list - return Ok(()); + self.message.push_str("Nothing selected to continue at!"); + return Ok(false); }; - let ind = self + let (ind, _) = self .app_state .exercises() .iter() .enumerate() - .filter_map(|(ind, exercise)| match self.filter { - Filter::Done => exercise.done.then_some(ind), - Filter::Pending => (!exercise.done).then_some(ind), - Filter::None => Some(ind), + .filter(|(_, exercise)| match self.filter { + Filter::Done => exercise.done, + Filter::Pending => !exercise.done, + Filter::None => true, }) .nth(selected) .context("Invalid selection index")?; - self.app_state.set_current_exercise_ind(ind) + self.app_state.set_current_exercise_ind(ind)?; + Ok(true) } } From b779c431268da50989257056d21a870a61a1702e Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 24 Aug 2024 17:17:56 +0200 Subject: [PATCH 094/211] Almost done with list display --- src/list.rs | 35 +++-- src/list/state.rs | 334 ++++++++++++++++++++++++++------------------ src/main.rs | 1 - src/progress_bar.rs | 53 ------- src/term.rs | 48 +++++++ src/watch/state.rs | 2 +- 6 files changed, 260 insertions(+), 213 deletions(-) delete mode 100644 src/progress_bar.rs diff --git a/src/list.rs b/src/list.rs index 27a31d13..a571eeec 100644 --- a/src/list.rs +++ b/src/list.rs @@ -38,15 +38,15 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> KeyCode::Home | KeyCode::Char('g') => list_state.select_first(), KeyCode::End | KeyCode::Char('G') => list_state.select_last(), KeyCode::Char('d') => { - let message = if list_state.filter() == Filter::Done { + if list_state.filter() == Filter::Done { list_state.set_filter(Filter::None); - "Disabled filter DONE" + list_state.message.push_str("Disabled filter DONE"); } else { list_state.set_filter(Filter::Done); - "Enabled filter DONE │ Press d again to disable the filter" - }; - - list_state.message.push_str(message); + list_state.message.push_str( + "Enabled filter DONE │ Press d again to disable the filter", + ); + } } KeyCode::Char('p') => { let message = if list_state.filter() == Filter::Pending { @@ -71,23 +71,20 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> KeyCode::Esc => (), _ => continue, } - - list_state.redraw(stdout)?; } - Event::Mouse(event) => { - match event.kind { - MouseEventKind::ScrollDown => list_state.select_next(), - MouseEventKind::ScrollUp => list_state.select_previous(), - _ => continue, - } - - list_state.redraw(stdout)?; + Event::Mouse(event) => match event.kind { + MouseEventKind::ScrollDown => list_state.select_next(), + MouseEventKind::ScrollUp => list_state.select_previous(), + _ => continue, + }, + Event::Resize(width, height) => { + list_state.set_term_size(width, height); } - // Redraw - Event::Resize(_, _) => list_state.redraw(stdout)?, // Ignore - Event::FocusGained | Event::FocusLost => (), + Event::FocusGained | Event::FocusLost => continue, } + + list_state.redraw(stdout)?; } } diff --git a/src/list/state.rs b/src/list/state.rs index 645c768f..d8744352 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -1,19 +1,30 @@ use anyhow::{Context, Result}; use crossterm::{ - cursor::{MoveDown, MoveTo}, - style::{Color, ResetColor, SetForegroundColor}, - terminal::{self, BeginSynchronizedUpdate, EndSynchronizedUpdate}, + cursor::{MoveTo, MoveToNextLine}, + style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, + terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate}, QueueableCommand, }; use std::{ fmt::Write as _, - io::{self, StdoutLock, Write as _}, + io::{self, StdoutLock, Write}, }; -use crate::{app_state::AppState, term::clear_terminal, MAX_EXERCISE_NAME_LEN}; +use crate::{app_state::AppState, term::progress_bar, MAX_EXERCISE_NAME_LEN}; -// +1 for padding. -const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; +fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> { + if CLEAR_LAST_CHAR { + // Avoids having the last written char as the last displayed one when + // the written width is higher than the terminal width. + // Happens on the Gnome terminal for example. + stdout.write_all(b" ")?; + } + + stdout + .queue(Clear(ClearType::UntilNewLine))? + .queue(MoveToNextLine(1))?; + Ok(()) +} #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { @@ -30,10 +41,16 @@ pub struct ListState<'a> { name_col_width: usize, offset: usize, selected: Option, + term_width: u16, + term_height: u16, + separator: Vec, } impl<'a> ListState<'a> { pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result { + let (term_width, term_height) = terminal::size()?; + stdout.queue(Clear(ClearType::All))?; + let name_col_width = app_state .exercises() .iter() @@ -41,13 +58,8 @@ impl<'a> ListState<'a> { .max() .map_or(4, |max| max.max(4)); - clear_terminal(stdout)?; - stdout.write_all(b" Current State Name ")?; - stdout.write_all(&SPACE[..name_col_width - 4])?; - stdout.write_all(b"Path\r\n")?; - - let selected = app_state.current_exercise_ind(); let n_rows_with_filter = app_state.exercises().len(); + let selected = app_state.current_exercise_ind(); let mut slf = Self { message: String::with_capacity(128), @@ -57,6 +69,9 @@ impl<'a> ListState<'a> { name_col_width, offset: selected.saturating_sub(10), selected: Some(selected), + term_width, + term_height, + separator: "─".as_bytes().repeat(term_width as usize), }; slf.redraw(stdout)?; @@ -64,6 +79,145 @@ impl<'a> ListState<'a> { Ok(slf) } + pub fn redraw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { + if self.term_height == 0 { + return Ok(()); + } + + stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?; + + // +1 for padding. + const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; + stdout.write_all(b" Current State Name")?; + stdout.write_all(&SPACE[..self.name_col_width - 2])?; + stdout.write_all(b"Path")?; + next_ln::(stdout)?; + + let narrow = self.term_width < 96; + let show_footer = self.term_height > 6; + let max_n_rows_to_display = + (self.term_height - 1 - u16::from(show_footer) * (4 + u16::from(narrow))) as usize; + + let displayed_exercises = self + .app_state + .exercises() + .iter() + .enumerate() + .filter(|(_, exercise)| match self.filter { + Filter::Done => exercise.done, + Filter::Pending => !exercise.done, + Filter::None => true, + }) + .skip(self.offset) + .take(max_n_rows_to_display); + + let current_exercise_ind = self.app_state.current_exercise_ind(); + let mut n_displayed_rows = 0; + for (exercise_ind, exercise) in displayed_exercises { + if self.selected == Some(n_displayed_rows) { + stdout.write_all("🦀".as_bytes())?; + } else { + stdout.write_all(b" ")?; + } + + if exercise_ind == current_exercise_ind { + stdout.queue(SetForegroundColor(Color::Red))?; + stdout.write_all(b">>>>>>> ")?; + } else { + stdout.write_all(b" ")?; + } + + if exercise.done { + stdout.queue(SetForegroundColor(Color::Yellow))?; + stdout.write_all(b"DONE ")?; + } else { + stdout.queue(SetForegroundColor(Color::Green))?; + stdout.write_all(b"PENDING ")?; + } + + stdout.queue(ResetColor)?; + + stdout.write_all(exercise.name.as_bytes())?; + stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; + + stdout.write_all(exercise.path.as_bytes())?; + + next_ln::(stdout)?; + n_displayed_rows += 1; + } + + for _ in 0..max_n_rows_to_display - n_displayed_rows { + next_ln::(stdout)?; + } + + if show_footer { + stdout.write_all(&self.separator)?; + next_ln::(stdout)?; + + progress_bar( + stdout, + self.app_state.n_done(), + self.app_state.exercises().len() as u16, + self.term_width, + )?; + next_ln::(stdout)?; + + stdout.write_all(&self.separator)?; + next_ln::(stdout)?; + + if self.message.is_empty() { + // Help footer. + stdout.write_all( + "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │".as_bytes(), + )?; + if narrow { + next_ln::(stdout)?; + stdout.write_all(b"filter ")?; + } else { + stdout.write_all(b" filter ")?; + } + + match self.filter { + Filter::Done => { + stdout + .queue(SetForegroundColor(Color::Magenta))? + .queue(SetAttribute(Attribute::Underlined))?; + stdout.write_all(b"one")?; + stdout.queue(ResetColor)?; + stdout.write_all(b"/

ending")?; + } + Filter::Pending => { + stdout.write_all(b"one/")?; + stdout + .queue(SetForegroundColor(Color::Magenta))? + .queue(SetAttribute(Attribute::Underlined))?; + stdout.write_all(b"

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

ending")?, + } + stdout.write_all(" │ uit list".as_bytes())?; + next_ln::(stdout)?; + } else { + stdout.queue(SetForegroundColor(Color::Magenta))?; + stdout.write_all(self.message.as_bytes())?; + stdout.queue(ResetColor)?; + next_ln::(stdout)?; + if narrow { + next_ln::(stdout)?; + } + } + } + + stdout.queue(EndSynchronizedUpdate)?.flush() + } + + pub fn set_term_size(&mut self, width: u16, height: u16) { + self.term_width = width; + self.term_height = height; + self.separator = "─".as_bytes().repeat(width as usize); + } + #[inline] pub fn filter(&self) -> Filter { self.filter @@ -76,13 +230,13 @@ impl<'a> ListState<'a> { .app_state .exercises() .iter() - .filter(|exercise| !exercise.done) + .filter(|exercise| exercise.done) .count(), Filter::Pending => self .app_state .exercises() .iter() - .filter(|exercise| exercise.done) + .filter(|exercise| !exercise.done) .count(), Filter::None => self.app_state.exercises().len(), }; @@ -127,124 +281,38 @@ impl<'a> ListState<'a> { } } - pub fn redraw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { - stdout.queue(BeginSynchronizedUpdate)?; - stdout.queue(MoveTo(0, 1))?; - let (width, height) = terminal::size()?; - let narrow = width < 95; - let narrow_u16 = u16::from(narrow); - let max_n_rows_to_display = height.saturating_sub(narrow_u16 + 4); - - let displayed_exercises = self - .app_state - .exercises() - .iter() - .enumerate() - .filter(|(_, exercise)| match self.filter { - Filter::Done => exercise.done, - Filter::Pending => !exercise.done, - Filter::None => true, - }) - .skip(self.offset) - .take(max_n_rows_to_display as usize); - - let mut n_displayed_rows: u16 = 0; - let current_exercise_ind = self.app_state.current_exercise_ind(); - for (ind, exercise) in displayed_exercises { - if self.selected == Some(n_displayed_rows as usize) { - write!(stdout, "🦀")?; - } else { - stdout.write_all(b" ")?; - } - - if ind == current_exercise_ind { - stdout.queue(SetForegroundColor(Color::Red))?; - stdout.write_all(b">>>>>>> ")?; - } else { - stdout.write_all(b" ")?; - } - - if exercise.done { - stdout.queue(SetForegroundColor(Color::Yellow))?; - stdout.write_all(b"DONE ")?; - } else { - stdout.queue(SetForegroundColor(Color::Green))?; - stdout.write_all(b"PENDING ")?; - } - - stdout.queue(ResetColor)?; - - stdout.write_all(exercise.name.as_bytes())?; - stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; - - stdout.write_all(exercise.path.as_bytes())?; - stdout.write_all(b"\r\n")?; - - n_displayed_rows += 1; + fn selected_to_exercise_ind(&self, selected: usize) -> Result { + match self.filter { + Filter::Done => self + .app_state + .exercises() + .iter() + .enumerate() + .filter(|(_, exercise)| exercise.done) + .nth(selected) + .context("Invalid selection index") + .map(|(ind, _)| ind), + Filter::Pending => self + .app_state + .exercises() + .iter() + .enumerate() + .filter(|(_, exercise)| !exercise.done) + .nth(selected) + .context("Invalid selection index") + .map(|(ind, _)| ind), + Filter::None => Ok(selected), } - - stdout.queue(MoveDown(max_n_rows_to_display - n_displayed_rows))?; - - // TODO - // let message = if self.message.is_empty() { - // // Help footer. - // let mut text = Text::default(); - // let mut spans = Vec::with_capacity(4); - // spans.push(Span::raw( - // "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │", - // )); - - // if narrow { - // text.push_line(mem::take(&mut spans)); - // spans.push(Span::raw("filter ")); - // } else { - // spans.push(Span::raw(" filter ")); - // } - - // match self.filter { - // Filter::Done => { - // spans.push("one".underlined().magenta()); - // spans.push(Span::raw("/

ending")); - // } - // Filter::Pending => { - // spans.push(Span::raw("one/")); - // spans.push("

ending".underlined().magenta()); - // } - // Filter::None => spans.push(Span::raw("one/

ending")), - // } - - // spans.push(Span::raw(" │ uit list")); - // text.push_line(spans); - // text - // } else { - // Text::from(self.message.as_str().light_blue()) - // }; - - stdout.queue(EndSynchronizedUpdate)?; - stdout.flush()?; - - Ok(()) } pub fn reset_selected(&mut self) -> Result<()> { let Some(selected) = self.selected else { + self.message.push_str("Nothing selected to reset!"); return Ok(()); }; - let ind = self - .app_state - .exercises() - .iter() - .enumerate() - .filter_map(|(ind, exercise)| match self.filter { - Filter::Done => exercise.done.then_some(ind), - Filter::Pending => (!exercise.done).then_some(ind), - Filter::None => Some(ind), - }) - .nth(selected) - .context("Invalid selection index")?; - - let exercise_path = self.app_state.reset_exercise_by_ind(ind)?; + let exercise_ind = self.selected_to_exercise_ind(selected)?; + let exercise_path = self.app_state.reset_exercise_by_ind(exercise_ind)?; write!(self.message, "The exercise {exercise_path} has been reset")?; Ok(()) @@ -257,20 +325,8 @@ impl<'a> ListState<'a> { return Ok(false); }; - let (ind, _) = self - .app_state - .exercises() - .iter() - .enumerate() - .filter(|(_, exercise)| match self.filter { - Filter::Done => exercise.done, - Filter::Pending => !exercise.done, - Filter::None => true, - }) - .nth(selected) - .context("Invalid selection index")?; - - self.app_state.set_current_exercise_ind(ind)?; + let exercise_ind = self.selected_to_exercise_ind(selected)?; + self.app_state.set_current_exercise_ind(exercise_ind)?; Ok(true) } } diff --git a/src/main.rs b/src/main.rs index 59513671..61dd8ea8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,6 @@ mod exercise; mod info_file; mod init; mod list; -mod progress_bar; mod run; mod term; mod terminal_link; diff --git a/src/progress_bar.rs b/src/progress_bar.rs deleted file mode 100644 index 837c4c78..00000000 --- a/src/progress_bar.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::io::{self, StdoutLock, Write}; - -use crossterm::{ - style::{Color, ResetColor, SetForegroundColor}, - QueueableCommand, -}; - -const PREFIX: &[u8] = b"Progress: ["; -const PREFIX_WIDTH: u16 = PREFIX.len() as u16; -// Leaving the last char empty (_) for `total` > 99. -const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16; -const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; -const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; - -/// Terminal progress bar to be used when not using Ratataui. -pub fn progress_bar( - stdout: &mut StdoutLock, - progress: u16, - total: u16, - line_width: u16, -) -> io::Result<()> { - debug_assert!(progress <= total); - - if line_width < MIN_LINE_WIDTH { - return write!(stdout, "Progress: {progress}/{total} exercises"); - } - - stdout.write_all(PREFIX)?; - - let width = line_width - WRAPPER_WIDTH; - let filled = (width * progress) / total; - - stdout.queue(SetForegroundColor(Color::Green))?; - for _ in 0..filled { - stdout.write_all(b"#")?; - } - - if filled < width { - stdout.write_all(b">")?; - } - - let width_minus_filled = width - filled; - if width_minus_filled > 1 { - let red_part_width = width_minus_filled - 1; - stdout.queue(SetForegroundColor(Color::Red))?; - for _ in 0..red_part_width { - stdout.write_all(b"-")?; - } - } - - stdout.queue(ResetColor)?; - write!(stdout, "] {progress:>3}/{total} exercises") -} diff --git a/src/term.rs b/src/term.rs index 07edf900..b993108e 100644 --- a/src/term.rs +++ b/src/term.rs @@ -2,10 +2,58 @@ use std::io::{self, BufRead, StdoutLock, Write}; use crossterm::{ cursor::MoveTo, + style::{Color, ResetColor, SetForegroundColor}, terminal::{Clear, ClearType}, QueueableCommand, }; +/// Terminal progress bar to be used when not using Ratataui. +pub fn progress_bar( + stdout: &mut StdoutLock, + progress: u16, + total: u16, + line_width: u16, +) -> io::Result<()> { + debug_assert!(progress <= total); + + const PREFIX: &[u8] = b"Progress: ["; + const PREFIX_WIDTH: u16 = PREFIX.len() as u16; + // Leaving the last char empty (_) for `total` > 99. + const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16; + const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; + const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; + + if line_width < MIN_LINE_WIDTH { + return write!(stdout, "Progress: {progress}/{total} exercises"); + } + + stdout.write_all(PREFIX)?; + + let width = line_width - WRAPPER_WIDTH; + let filled = (width * progress) / total; + + stdout.queue(SetForegroundColor(Color::Green))?; + for _ in 0..filled { + stdout.write_all(b"#")?; + } + + if filled < width { + stdout.write_all(b">")?; + } + + let width_minus_filled = width - filled; + if width_minus_filled > 1 { + let red_part_width = width_minus_filled - 1; + stdout.queue(SetForegroundColor(Color::Red))?; + for _ in 0..red_part_width { + stdout.write_all(b"-")?; + } + } + + stdout.queue(ResetColor)?; + write!(stdout, "] {progress:>3}/{total} exercises") +} + pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { stdout .queue(MoveTo(0, 0))? diff --git a/src/watch/state.rs b/src/watch/state.rs index 26c83d50..40e3d3ec 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -9,7 +9,7 @@ use crate::{ app_state::{AppState, ExercisesProgress}, clear_terminal, exercise::{RunnableExercise, OUTPUT_CAPACITY}, - progress_bar::progress_bar, + term::progress_bar, terminal_link::TerminalFileLink, }; From 28d0b0a21ec2d916309733dcce8aecdbdf305d46 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 24 Aug 2024 17:45:02 +0200 Subject: [PATCH 095/211] Highlight selected row --- src/list/state.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index d8744352..4ba3d4eb 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use crossterm::{ cursor::{MoveTo, MoveToNextLine}, - style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, + style::{Attribute, Color, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor}, terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate}, QueueableCommand, }; @@ -115,6 +115,11 @@ impl<'a> ListState<'a> { let mut n_displayed_rows = 0; for (exercise_ind, exercise) in displayed_exercises { if self.selected == Some(n_displayed_rows) { + stdout.queue(SetBackgroundColor(Color::Rgb { + r: 50, + g: 50, + b: 50, + }))?; stdout.write_all("🦀".as_bytes())?; } else { stdout.write_all(b" ")?; @@ -135,7 +140,7 @@ impl<'a> ListState<'a> { stdout.write_all(b"PENDING ")?; } - stdout.queue(ResetColor)?; + stdout.queue(SetForegroundColor(Color::Reset))?; stdout.write_all(exercise.name.as_bytes())?; stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; @@ -143,6 +148,7 @@ impl<'a> ListState<'a> { stdout.write_all(exercise.path.as_bytes())?; next_ln::(stdout)?; + stdout.queue(ResetColor)?; n_displayed_rows += 1; } From b6129ad0811e05a256713614db899a98308cb62c Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 24 Aug 2024 17:45:38 +0200 Subject: [PATCH 096/211] Use the full length for the wide footer --- src/list/state.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 4ba3d4eb..d4507416 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -93,7 +93,7 @@ impl<'a> ListState<'a> { stdout.write_all(b"Path")?; next_ln::(stdout)?; - let narrow = self.term_width < 96; + let narrow = self.term_width < 95; let show_footer = self.term_height > 6; let max_n_rows_to_display = (self.term_height - 1 - u16::from(show_footer) * (4 + u16::from(narrow))) as usize; @@ -203,7 +203,11 @@ impl<'a> ListState<'a> { Filter::None => stdout.write_all(b"one/

ending")?, } stdout.write_all(" │ uit list".as_bytes())?; - next_ln::(stdout)?; + if narrow { + next_ln::(stdout)?; + } else { + next_ln::(stdout)?; + } } else { stdout.queue(SetForegroundColor(Color::Magenta))?; stdout.write_all(self.message.as_bytes())?; From fd2a8c01cb35fcff4a5358cce9473ff91272c790 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 24 Aug 2024 19:18:13 +0200 Subject: [PATCH 097/211] Separate drawing rows --- src/list.rs | 2 +- src/list/state.rs | 90 ++++++++++++++++++++++++++++------------------- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/src/list.rs b/src/list.rs index a571eeec..e3601825 100644 --- a/src/list.rs +++ b/src/list.rs @@ -84,7 +84,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> Event::FocusGained | Event::FocusLost => continue, } - list_state.redraw(stdout)?; + list_state.draw(stdout)?; } } diff --git a/src/list/state.rs b/src/list/state.rs index d4507416..cbca1d9e 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -10,7 +10,10 @@ use std::{ io::{self, StdoutLock, Write}, }; -use crate::{app_state::AppState, term::progress_bar, MAX_EXERCISE_NAME_LEN}; +use crate::{app_state::AppState, exercise::Exercise, term::progress_bar, MAX_EXERCISE_NAME_LEN}; + +// +1 for padding. +const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> { if CLEAR_LAST_CHAR { @@ -74,46 +77,24 @@ impl<'a> ListState<'a> { separator: "─".as_bytes().repeat(term_width as usize), }; - slf.redraw(stdout)?; + slf.draw(stdout)?; Ok(slf) } - pub fn redraw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { - if self.term_height == 0 { - return Ok(()); - } - - stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?; - - // +1 for padding. - const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; - stdout.write_all(b" Current State Name")?; - stdout.write_all(&SPACE[..self.name_col_width - 2])?; - stdout.write_all(b"Path")?; - next_ln::(stdout)?; - - let narrow = self.term_width < 95; - let show_footer = self.term_height > 6; - let max_n_rows_to_display = - (self.term_height - 1 - u16::from(show_footer) * (4 + u16::from(narrow))) as usize; - - let displayed_exercises = self - .app_state - .exercises() - .iter() - .enumerate() - .filter(|(_, exercise)| match self.filter { - Filter::Done => exercise.done, - Filter::Pending => !exercise.done, - Filter::None => true, - }) - .skip(self.offset) - .take(max_n_rows_to_display); - + fn draw_rows( + &self, + stdout: &mut StdoutLock, + max_n_rows_to_display: usize, + filtered_exercises: impl Iterator, + ) -> io::Result { let current_exercise_ind = self.app_state.current_exercise_ind(); let mut n_displayed_rows = 0; - for (exercise_ind, exercise) in displayed_exercises { + + for (exercise_ind, exercise) in filtered_exercises + .skip(self.offset) + .take(max_n_rows_to_display) + { if self.selected == Some(n_displayed_rows) { stdout.queue(SetBackgroundColor(Color::Rgb { r: 50, @@ -152,6 +133,43 @@ impl<'a> ListState<'a> { n_displayed_rows += 1; } + Ok(n_displayed_rows) + } + + pub fn draw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { + if self.term_height == 0 { + return Ok(()); + } + + stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?; + + // Header + stdout.write_all(b" Current State Name")?; + stdout.write_all(&SPACE[..self.name_col_width - 2])?; + stdout.write_all(b"Path")?; + next_ln::(stdout)?; + + let narrow = self.term_width < 95; + let show_footer = self.term_height > 6; + let max_n_rows_to_display = + (self.term_height - 1 - u16::from(show_footer) * (4 + u16::from(narrow))) as usize; + + // Rows + let iter = self.app_state.exercises().iter().enumerate(); + let n_displayed_rows = match self.filter { + Filter::Done => self.draw_rows( + stdout, + max_n_rows_to_display, + iter.filter(|(_, exercise)| exercise.done), + )?, + Filter::Pending => self.draw_rows( + stdout, + max_n_rows_to_display, + iter.filter(|(_, exercise)| !exercise.done), + )?, + Filter::None => self.draw_rows(stdout, max_n_rows_to_display, iter)?, + }; + for _ in 0..max_n_rows_to_display - n_displayed_rows { next_ln::(stdout)?; } @@ -172,7 +190,7 @@ impl<'a> ListState<'a> { next_ln::(stdout)?; if self.message.is_empty() { - // Help footer. + // Help footer stdout.write_all( "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │".as_bytes(), )?; From 5f4875e2bae07d3c8ce6505abbc67bbe447b7aa6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 25 Aug 2024 19:24:12 +0200 Subject: [PATCH 098/211] Almost done with list --- src/list.rs | 8 +- src/list/state.rs | 230 +++++++++++++++++++++++++++------------------- 2 files changed, 135 insertions(+), 103 deletions(-) diff --git a/src/list.rs b/src/list.rs index e3601825..a8e52254 100644 --- a/src/list.rs +++ b/src/list.rs @@ -59,9 +59,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> list_state.message.push_str(message); } - KeyCode::Char('r') => { - list_state.reset_selected()?; - } + KeyCode::Char('r') => list_state.reset_selected()?, KeyCode::Char('c') => { if list_state.selected_to_current_exercise()? { return Ok(()); @@ -77,9 +75,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> MouseEventKind::ScrollUp => list_state.select_previous(), _ => continue, }, - Event::Resize(width, height) => { - list_state.set_term_size(width, height); - } + Event::Resize(width, height) => list_state.set_term_size(width, height), // Ignore Event::FocusGained | Event::FocusLost => continue, } diff --git a/src/list/state.rs b/src/list/state.rs index cbca1d9e..b8fdfcbc 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -15,20 +15,21 @@ use crate::{app_state::AppState, exercise::Exercise, term::progress_bar, MAX_EXE // +1 for padding. const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; -fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> { - if CLEAR_LAST_CHAR { - // Avoids having the last written char as the last displayed one when - // the written width is higher than the terminal width. - // Happens on the Gnome terminal for example. - stdout.write_all(b" ")?; - } - +fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> { stdout .queue(Clear(ClearType::UntilNewLine))? .queue(MoveToNextLine(1))?; Ok(()) } +// Avoids having the last written char as the last displayed one when the +// written width is higher than the terminal width. +// Happens on the Gnome terminal for example. +fn next_ln_overwrite(stdout: &mut StdoutLock) -> io::Result<()> { + stdout.write_all(b" ")?; + next_ln(stdout) +} + #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { Done, @@ -37,65 +38,111 @@ pub enum Filter { } pub struct ListState<'a> { + /// Footer message to be displayed if not empty. pub message: String, - filter: Filter, app_state: &'a mut AppState, - n_rows_with_filter: usize, name_col_width: usize, - offset: usize, - selected: Option, + filter: Filter, + n_rows_with_filter: usize, + /// Selected row out of the displayed ones. + selected_row: Option, term_width: u16, term_height: u16, - separator: Vec, + separator_line: Vec, + narrow_term: bool, + show_footer: bool, + max_n_rows_to_display: usize, + scroll_padding: usize, + row_offset: usize, } impl<'a> ListState<'a> { pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result { - let (term_width, term_height) = terminal::size()?; stdout.queue(Clear(ClearType::All))?; + let name_col_title_len = 4; let name_col_width = app_state .exercises() .iter() .map(|exercise| exercise.name.len()) .max() - .map_or(4, |max| max.max(4)); + .map_or(name_col_title_len, |max| max.max(name_col_title_len)); + let filter = Filter::None; let n_rows_with_filter = app_state.exercises().len(); - let selected = app_state.current_exercise_ind(); + let selected = Some(app_state.current_exercise_ind()); let mut slf = Self { message: String::with_capacity(128), - filter: Filter::None, app_state, - n_rows_with_filter, name_col_width, - offset: selected.saturating_sub(10), - selected: Some(selected), - term_width, - term_height, - separator: "─".as_bytes().repeat(term_width as usize), + filter, + n_rows_with_filter, + selected_row: selected, + // Set by `set_term_size` + term_width: 0, + term_height: 0, + separator_line: Vec::new(), + narrow_term: false, + show_footer: true, + max_n_rows_to_display: 0, + scroll_padding: 0, + // Updated by `draw` + row_offset: 0, }; + let (width, height) = terminal::size()?; + slf.set_term_size(width, height); slf.draw(stdout)?; Ok(slf) } + pub fn set_term_size(&mut self, width: u16, height: u16) { + self.term_width = width; + self.term_height = height; + + self.separator_line = "─".as_bytes().repeat(width as usize); + + self.narrow_term = width < 95 && self.selected_row.is_some(); + self.show_footer = height > 6; + self.max_n_rows_to_display = + (height - 1 - u16::from(self.show_footer) * (4 + u16::from(self.narrow_term))) as usize; + self.scroll_padding = (self.max_n_rows_to_display / 4).min(5); + } + + fn update_offset(&mut self) { + let Some(selected) = self.selected_row else { + return; + }; + + let min_offset = (selected + self.scroll_padding) + .saturating_sub(self.max_n_rows_to_display.saturating_sub(1)); + let max_offset = selected.saturating_sub(self.scroll_padding); + let global_max_offset = self + .n_rows_with_filter + .saturating_sub(self.max_n_rows_to_display); + + self.row_offset = self + .row_offset + .max(min_offset) + .min(max_offset) + .min(global_max_offset); + } + fn draw_rows( &self, stdout: &mut StdoutLock, - max_n_rows_to_display: usize, filtered_exercises: impl Iterator, ) -> io::Result { let current_exercise_ind = self.app_state.current_exercise_ind(); let mut n_displayed_rows = 0; for (exercise_ind, exercise) in filtered_exercises - .skip(self.offset) - .take(max_n_rows_to_display) + .skip(self.row_offset) + .take(self.max_n_rows_to_display) { - if self.selected == Some(n_displayed_rows) { + if self.selected_row == Some(self.row_offset + n_displayed_rows) { stdout.queue(SetBackgroundColor(Color::Rgb { r: 50, g: 50, @@ -128,7 +175,7 @@ impl<'a> ListState<'a> { stdout.write_all(exercise.path.as_bytes())?; - next_ln::(stdout)?; + next_ln_overwrite(stdout)?; stdout.queue(ResetColor)?; n_displayed_rows += 1; } @@ -147,36 +194,27 @@ impl<'a> ListState<'a> { stdout.write_all(b" Current State Name")?; stdout.write_all(&SPACE[..self.name_col_width - 2])?; stdout.write_all(b"Path")?; - next_ln::(stdout)?; + next_ln_overwrite(stdout)?; - let narrow = self.term_width < 95; - let show_footer = self.term_height > 6; - let max_n_rows_to_display = - (self.term_height - 1 - u16::from(show_footer) * (4 + u16::from(narrow))) as usize; + self.update_offset(); // Rows let iter = self.app_state.exercises().iter().enumerate(); let n_displayed_rows = match self.filter { - Filter::Done => self.draw_rows( - stdout, - max_n_rows_to_display, - iter.filter(|(_, exercise)| exercise.done), - )?, - Filter::Pending => self.draw_rows( - stdout, - max_n_rows_to_display, - iter.filter(|(_, exercise)| !exercise.done), - )?, - Filter::None => self.draw_rows(stdout, max_n_rows_to_display, iter)?, + Filter::Done => self.draw_rows(stdout, iter.filter(|(_, exercise)| exercise.done))?, + Filter::Pending => { + self.draw_rows(stdout, iter.filter(|(_, exercise)| !exercise.done))? + } + Filter::None => self.draw_rows(stdout, iter)?, }; - for _ in 0..max_n_rows_to_display - n_displayed_rows { - next_ln::(stdout)?; + for _ in 0..self.max_n_rows_to_display - n_displayed_rows { + next_ln(stdout)?; } - if show_footer { - stdout.write_all(&self.separator)?; - next_ln::(stdout)?; + if self.show_footer { + stdout.write_all(&self.separator_line)?; + next_ln(stdout)?; progress_bar( stdout, @@ -184,21 +222,25 @@ impl<'a> ListState<'a> { self.app_state.exercises().len() as u16, self.term_width, )?; - next_ln::(stdout)?; + next_ln(stdout)?; - stdout.write_all(&self.separator)?; - next_ln::(stdout)?; + stdout.write_all(&self.separator_line)?; + next_ln(stdout)?; if self.message.is_empty() { // Help footer - stdout.write_all( - "↓/j ↑/k home/g end/G │ ontinue at │ eset exercise │".as_bytes(), - )?; - if narrow { - next_ln::(stdout)?; - stdout.write_all(b"filter ")?; + if self.selected_row.is_some() { + stdout.write_all( + "↓/j ↑/k home/g end/G | ontinue at | eset exercise".as_bytes(), + )?; + if self.narrow_term { + next_ln_overwrite(stdout)?; + stdout.write_all(b"filter ")?; + } else { + stdout.write_all(b" | filter ")?; + } } else { - stdout.write_all(b" filter ")?; + stdout.write_all(b"filter ")?; } match self.filter { @@ -220,19 +262,19 @@ impl<'a> ListState<'a> { } Filter::None => stdout.write_all(b"one/

ending")?, } - stdout.write_all(" │ uit list".as_bytes())?; - if narrow { - next_ln::(stdout)?; + stdout.write_all(b" | uit list")?; + if self.narrow_term { + next_ln_overwrite(stdout)?; } else { - next_ln::(stdout)?; + next_ln(stdout)?; } } else { stdout.queue(SetForegroundColor(Color::Magenta))?; stdout.write_all(self.message.as_bytes())?; stdout.queue(ResetColor)?; - next_ln::(stdout)?; - if narrow { - next_ln::(stdout)?; + next_ln_overwrite(stdout)?; + if self.narrow_term { + next_ln(stdout)?; } } } @@ -240,20 +282,8 @@ impl<'a> ListState<'a> { stdout.queue(EndSynchronizedUpdate)?.flush() } - pub fn set_term_size(&mut self, width: u16, height: u16) { - self.term_width = width; - self.term_height = height; - self.separator = "─".as_bytes().repeat(width as usize); - } - - #[inline] - pub fn filter(&self) -> Filter { - self.filter - } - - pub fn set_filter(&mut self, filter: Filter) { - self.filter = filter; - self.n_rows_with_filter = match filter { + fn update_rows(&mut self) { + self.n_rows_with_filter = match self.filter { Filter::Done => self .app_state .exercises() @@ -270,42 +300,46 @@ impl<'a> ListState<'a> { }; if self.n_rows_with_filter == 0 { - self.selected = None; + self.selected_row = None; } else { - self.selected = Some( - self.selected + self.selected_row = Some( + self.selected_row .map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)), ); } } + #[inline] + pub fn filter(&self) -> Filter { + self.filter + } + + pub fn set_filter(&mut self, filter: Filter) { + self.filter = filter; + self.update_rows(); + } + pub fn select_next(&mut self) { - if self.n_rows_with_filter > 0 { - let next = self.selected.map_or(0, |selected| { - (selected + 1).min(self.n_rows_with_filter - 1) - }); - self.selected = Some(next); + if let Some(selected) = self.selected_row { + self.selected_row = Some((selected + 1).min(self.n_rows_with_filter - 1)); } } pub fn select_previous(&mut self) { - if self.n_rows_with_filter > 0 { - let previous = self - .selected - .map_or(0, |selected| selected.saturating_sub(1)); - self.selected = Some(previous); + if let Some(selected) = self.selected_row { + self.selected_row = Some(selected.saturating_sub(1)); } } pub fn select_first(&mut self) { if self.n_rows_with_filter > 0 { - self.selected = Some(0); + self.selected_row = Some(0); } } pub fn select_last(&mut self) { if self.n_rows_with_filter > 0 { - self.selected = Some(self.n_rows_with_filter - 1); + self.selected_row = Some(self.n_rows_with_filter - 1); } } @@ -334,13 +368,14 @@ impl<'a> ListState<'a> { } pub fn reset_selected(&mut self) -> Result<()> { - let Some(selected) = self.selected else { + let Some(selected) = self.selected_row else { self.message.push_str("Nothing selected to reset!"); return Ok(()); }; let exercise_ind = self.selected_to_exercise_ind(selected)?; let exercise_path = self.app_state.reset_exercise_by_ind(exercise_ind)?; + self.update_rows(); write!(self.message, "The exercise {exercise_path} has been reset")?; Ok(()) @@ -348,13 +383,14 @@ impl<'a> ListState<'a> { // Return `true` if there was something to select. pub fn selected_to_current_exercise(&mut self) -> Result { - let Some(selected) = self.selected else { + let Some(selected) = self.selected_row else { self.message.push_str("Nothing selected to continue at!"); return Ok(false); }; let exercise_ind = self.selected_to_exercise_ind(selected)?; self.app_state.set_current_exercise_ind(exercise_ind)?; + Ok(true) } } From 64772544fad6788fd3fce5db3f357dba6f2d8d23 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 25 Aug 2024 20:29:54 +0200 Subject: [PATCH 099/211] Final touches :D --- src/app_state.rs | 3 +- src/list/state.rs | 95 ++++++++++++++++++++++++++++++----------------- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index b72469c4..8fd8f3be 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -271,6 +271,7 @@ impl AppState { Ok(exercise.path) } + // Reset the exercise by index and return its name. pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> { if exercise_ind >= self.exercises.len() { bail!(BAD_INDEX_ERR); @@ -280,7 +281,7 @@ impl AppState { let exercise = &self.exercises[exercise_ind]; self.reset(exercise_ind, exercise.path)?; - Ok(exercise.path) + Ok(exercise.name) } // Return the index of the next pending exercise or `None` if all exercises are done. diff --git a/src/list/state.rs b/src/list/state.rs index b8fdfcbc..25ca1ded 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -12,7 +12,8 @@ use std::{ use crate::{app_state::AppState, exercise::Exercise, term::progress_bar, MAX_EXERCISE_NAME_LEN}; -// +1 for padding. +const MAX_SCROLL_PADDING: usize = 8; +// +1 for column padding. const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> { @@ -44,8 +45,9 @@ pub struct ListState<'a> { name_col_width: usize, filter: Filter, n_rows_with_filter: usize, - /// Selected row out of the displayed ones. + /// Selected row out of the filtered ones. selected_row: Option, + row_offset: usize, term_width: u16, term_height: u16, separator_line: Vec, @@ -53,7 +55,6 @@ pub struct ListState<'a> { show_footer: bool, max_n_rows_to_display: usize, scroll_padding: usize, - row_offset: usize, } impl<'a> ListState<'a> { @@ -70,7 +71,7 @@ impl<'a> ListState<'a> { let filter = Filter::None; let n_rows_with_filter = app_state.exercises().len(); - let selected = Some(app_state.current_exercise_ind()); + let selected = app_state.current_exercise_ind(); let mut slf = Self { message: String::with_capacity(128), @@ -78,7 +79,8 @@ impl<'a> ListState<'a> { name_col_width, filter, n_rows_with_filter, - selected_row: selected, + selected_row: Some(selected), + row_offset: selected.saturating_sub(MAX_SCROLL_PADDING), // Set by `set_term_size` term_width: 0, term_height: 0, @@ -87,8 +89,6 @@ impl<'a> ListState<'a> { show_footer: true, max_n_rows_to_display: 0, scroll_padding: 0, - // Updated by `draw` - row_offset: 0, }; let (width, height) = terminal::size()?; @@ -98,19 +98,6 @@ impl<'a> ListState<'a> { Ok(slf) } - pub fn set_term_size(&mut self, width: u16, height: u16) { - self.term_width = width; - self.term_height = height; - - self.separator_line = "─".as_bytes().repeat(width as usize); - - self.narrow_term = width < 95 && self.selected_row.is_some(); - self.show_footer = height > 6; - self.max_n_rows_to_display = - (height - 1 - u16::from(self.show_footer) * (4 + u16::from(self.narrow_term))) as usize; - self.scroll_padding = (self.max_n_rows_to_display / 4).min(5); - } - fn update_offset(&mut self) { let Some(selected) = self.selected_row else { return; @@ -130,6 +117,36 @@ impl<'a> ListState<'a> { .min(global_max_offset); } + pub fn set_term_size(&mut self, width: u16, height: u16) { + self.term_width = width; + self.term_height = height; + + if height == 0 { + return; + } + + let wide_help_footer_width = 95; + // The help footer is shorter when nothing is selected. + self.narrow_term = width < wide_help_footer_width && self.selected_row.is_some(); + + let header_height = 1; + // 2 separator, 1 progress bar, 1-2 footer message. + let footer_height = 4 + u16::from(self.narrow_term); + self.show_footer = height > header_height + footer_height; + + if self.show_footer { + self.separator_line = "─".as_bytes().repeat(width as usize); + } + + self.max_n_rows_to_display = height + .saturating_sub(header_height + u16::from(self.show_footer) * footer_height) + as usize; + + self.scroll_padding = (self.max_n_rows_to_display / 4).min(MAX_SCROLL_PADDING); + + self.update_offset(); + } + fn draw_rows( &self, stdout: &mut StdoutLock, @@ -196,8 +213,6 @@ impl<'a> ListState<'a> { stdout.write_all(b"Path")?; next_ln_overwrite(stdout)?; - self.update_offset(); - // Rows let iter = self.app_state.exercises().iter().enumerate(); let n_displayed_rows = match self.filter { @@ -228,7 +243,7 @@ impl<'a> ListState<'a> { next_ln(stdout)?; if self.message.is_empty() { - // Help footer + // Help footer message if self.selected_row.is_some() { stdout.write_all( "↓/j ↑/k home/g end/G | ontinue at | eset exercise".as_bytes(), @@ -240,6 +255,7 @@ impl<'a> ListState<'a> { stdout.write_all(b" | filter ")?; } } else { + // Nothing selected (and nothing shown), so only display filter and quit. stdout.write_all(b"filter ")?; } @@ -262,7 +278,9 @@ impl<'a> ListState<'a> { } Filter::None => stdout.write_all(b"one/

ending")?, } + stdout.write_all(b" | uit list")?; + if self.narrow_term { next_ln_overwrite(stdout)?; } else { @@ -282,6 +300,11 @@ impl<'a> ListState<'a> { stdout.queue(EndSynchronizedUpdate)?.flush() } + fn set_selected(&mut self, selected: usize) { + self.selected_row = Some(selected); + self.update_offset(); + } + fn update_rows(&mut self) { self.n_rows_with_filter = match self.filter { Filter::Done => self @@ -301,12 +324,13 @@ impl<'a> ListState<'a> { if self.n_rows_with_filter == 0 { self.selected_row = None; - } else { - self.selected_row = Some( - self.selected_row - .map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)), - ); + return; } + + self.set_selected( + self.selected_row + .map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)), + ); } #[inline] @@ -321,25 +345,25 @@ impl<'a> ListState<'a> { pub fn select_next(&mut self) { if let Some(selected) = self.selected_row { - self.selected_row = Some((selected + 1).min(self.n_rows_with_filter - 1)); + self.set_selected((selected + 1).min(self.n_rows_with_filter - 1)); } } pub fn select_previous(&mut self) { if let Some(selected) = self.selected_row { - self.selected_row = Some(selected.saturating_sub(1)); + self.set_selected(selected.saturating_sub(1)); } } pub fn select_first(&mut self) { if self.n_rows_with_filter > 0 { - self.selected_row = Some(0); + self.set_selected(0); } } pub fn select_last(&mut self) { if self.n_rows_with_filter > 0 { - self.selected_row = Some(self.n_rows_with_filter - 1); + self.set_selected(self.n_rows_with_filter - 1); } } @@ -374,9 +398,12 @@ impl<'a> ListState<'a> { }; let exercise_ind = self.selected_to_exercise_ind(selected)?; - let exercise_path = self.app_state.reset_exercise_by_ind(exercise_ind)?; + let exercise_name = self.app_state.reset_exercise_by_ind(exercise_ind)?; self.update_rows(); - write!(self.message, "The exercise {exercise_path} has been reset")?; + write!( + self.message, + "The exercise `{exercise_name}` has been reset", + )?; Ok(()) } From d29e9e7e07a16adda35aea9ce9dd120b6ecc9dfc Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 25 Aug 2024 20:42:13 +0200 Subject: [PATCH 100/211] Update deps --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93b2051b..048a82ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,9 +197,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "filetime" @@ -529,18 +529,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -549,9 +549,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", "memchr", @@ -612,9 +612,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.75" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 4b3e98c4..5eb25b49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ edition = "2021" # On Update: Update the edition of the `rustfmt` command that c rust-version = "1.80" [workspace.dependencies] -serde = { version = "1.0.208", features = ["derive"] } +serde = { version = "1.0.209", features = ["derive"] } toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } [package] @@ -53,7 +53,7 @@ crossterm = { version = "0.28.1", default-features = false, features = ["windows notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" } -serde_json = "1.0.125" +serde_json = "1.0.127" serde.workspace = true toml_edit.workspace = true From b1898f6d8b2c2ae45279ca4c67fa1b1a94acb936 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 25 Aug 2024 23:53:50 +0200 Subject: [PATCH 101/211] Use queue instead of Stylize --- src/app_state.rs | 18 ++++---- src/exercise.rs | 49 ++++++++++---------- src/init.rs | 23 +++++---- src/main.rs | 1 - src/run.rs | 54 +++++++++++----------- src/term.rs | 49 ++++++++++++++++++-- src/terminal_link.rs | 26 ----------- src/watch/state.rs | 108 +++++++++++++++++++++++++++---------------- 8 files changed, 189 insertions(+), 139 deletions(-) delete mode 100644 src/terminal_link.rs diff --git a/src/app_state.rs b/src/app_state.rs index 8fd8f3be..d7de1fdb 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -338,7 +338,7 @@ impl AppState { /// Mark the current exercise as done and move on to the next pending exercise if one exists. /// If all exercises are marked as done, run all of them to make sure that they are actually /// done. If an exercise which is marked as done fails, mark it as pending and continue on it. - pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { + pub fn done_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result { let exercise = &mut self.exercises[self.current_exercise_ind]; if !exercise.done { exercise.done = true; @@ -350,7 +350,7 @@ impl AppState { return Ok(ExercisesProgress::NewPending); } - writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?; let n_exercises = self.exercises.len(); @@ -368,12 +368,12 @@ impl AppState { .collect::>(); for (exercise_ind, handle) in handles.into_iter().enumerate() { - write!(writer, "\rProgress: {exercise_ind}/{n_exercises}")?; - writer.flush()?; + write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; + stdout.flush()?; let success = handle.join().unwrap()?; if !success { - writer.write_all(b"\n\n")?; + stdout.write_all(b"\n\n")?; return Ok(Some(exercise_ind)); } } @@ -395,13 +395,13 @@ impl AppState { // Write that the last exercise is done. self.write()?; - clear_terminal(writer)?; - writer.write_all(FENISH_LINE.as_bytes())?; + clear_terminal(stdout)?; + stdout.write_all(FENISH_LINE.as_bytes())?; let final_message = self.final_message.trim_ascii(); if !final_message.is_empty() { - writer.write_all(final_message.as_bytes())?; - writer.write_all(b"\n")?; + stdout.write_all(final_message.as_bytes())?; + stdout.write_all(b"\n")?; } Ok(ExercisesProgress::AllDone) diff --git a/src/exercise.rs b/src/exercise.rs index ac5c6e6e..462287db 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,15 +1,27 @@ use anyhow::Result; -use crossterm::style::{style, StyledContent, Stylize}; -use std::{ - fmt::{self, Display, Formatter}, - io::Write, +use crossterm::{ + style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, + QueueableCommand, }; +use std::io::{self, StdoutLock, Write}; -use crate::{cmd::CmdRunner, terminal_link::TerminalFileLink}; +use crate::{ + cmd::CmdRunner, + term::{terminal_file_link, write_ansi}, +}; /// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; +pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::Result<()> { + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"Solution")?; + stdout.queue(ResetColor)?; + stdout.write_all(b" for comparison: ")?; + terminal_file_link(stdout, solution_path, Color::Cyan)?; + stdout.write_all(b"\n") +} + // Run an exercise binary and append its output to the `output` buffer. // Compilation must be done before calling this method. fn run_bin( @@ -18,7 +30,9 @@ fn run_bin( cmd_runner: &CmdRunner, ) -> Result { if let Some(output) = output.as_deref_mut() { - writeln!(output, "{}", "Output".underlined())?; + write_ansi(output, SetAttribute(Attribute::Underlined)); + output.extend_from_slice(b"Output\n"); + write_ansi(output, ResetColor); } let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?; @@ -28,13 +42,10 @@ fn run_bin( // This output is important to show the user that something went wrong. // Otherwise, calling something like `exit(1)` in an exercise without further output // leaves the user confused about why the exercise isn't done yet. - writeln!( - output, - "{}", - "The exercise didn't run successfully (nonzero exit code)" - .bold() - .red(), - )?; + write_ansi(output, SetAttribute(Attribute::Bold)); + write_ansi(output, SetForegroundColor(Color::Red)); + output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)\n"); + write_ansi(output, ResetColor); } } @@ -53,18 +64,6 @@ pub struct Exercise { pub done: bool, } -impl Exercise { - pub fn terminal_link(&self) -> StyledContent> { - style(TerminalFileLink(self.path)).underlined().blue() - } -} - -impl Display for Exercise { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.path.fmt(f) - } -} - pub trait RunnableExercise { fn name(&self) -> &str; fn strict_clippy(&self) -> bool; diff --git a/src/init.rs b/src/init.rs index 2c172dca..40d9910d 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,5 +1,8 @@ use anyhow::{bail, Context, Result}; -use crossterm::style::Stylize; +use crossterm::{ + style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, + QueueableCommand, +}; use serde::Deserialize; use std::{ env::set_current_dir, @@ -144,12 +147,13 @@ pub fn init() -> Result<()> { .status(); } - writeln!( - stdout, - "{}\n\n{}", - "Initialization done ✓".green(), - POST_INIT_MSG.bold(), - )?; + stdout.queue(SetForegroundColor(Color::Green))?; + stdout.write_all("Initialization done ✓\n\n".as_bytes())?; + stdout + .queue(ResetColor)? + .queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(POST_INIT_MSG)?; + stdout.queue(ResetColor)?; Ok(()) } @@ -182,5 +186,6 @@ You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again"; -const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. -Then run `rustlings` to get started."; +const POST_INIT_MSG: &[u8] = b"Run `cd rustlings` to go into the generated directory. +Then run `rustlings` to get started. +"; diff --git a/src/main.rs b/src/main.rs index 61dd8ea8..998d3d11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,6 @@ mod init; mod list; mod run; mod term; -mod terminal_link; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; diff --git a/src/run.rs b/src/run.rs index 09e53ec9..929b4751 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,11 +1,17 @@ -use anyhow::{bail, Result}; -use crossterm::style::{style, Stylize}; -use std::io::{self, Write}; +use anyhow::Result; +use crossterm::{ + style::{Color, ResetColor, SetForegroundColor}, + QueueableCommand, +}; +use std::{ + io::{self, Write}, + process::exit, +}; use crate::{ app_state::{AppState, ExercisesProgress}, - exercise::{RunnableExercise, OUTPUT_CAPACITY}, - terminal_link::TerminalFileLink, + exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, + term::terminal_file_link, }; pub fn run(app_state: &mut AppState) -> Result<()> { @@ -19,35 +25,31 @@ pub fn run(app_state: &mut AppState) -> Result<()> { if !success { app_state.set_pending(app_state.current_exercise_ind())?; - bail!( - "Ran {} with errors", - app_state.current_exercise().terminal_link(), - ); + stdout.write_all(b"Ran ")?; + terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?; + stdout.write_all(b" with errors\n")?; + exit(1); } - writeln!( - stdout, - "{}{}", - "✓ Successfully ran ".green(), - exercise.path.green(), - )?; + stdout.queue(SetForegroundColor(Color::Green))?; + stdout.write_all("✓ Successfully ran ".as_bytes())?; + stdout.write_all(exercise.path.as_bytes())?; + stdout.queue(ResetColor)?; + stdout.write_all(b"\n")?; if let Some(solution_path) = app_state.current_solution_path()? { - writeln!( - stdout, - "\n{} for comparison: {}\n", - "Solution".bold(), - style(TerminalFileLink(&solution_path)).underlined().cyan(), - )?; + stdout.write_all(b"\n")?; + solution_link_line(&mut stdout, &solution_path)?; + stdout.write_all(b"\n")?; } match app_state.done_current_exercise(&mut stdout)? { + ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => { + stdout.write_all(b"Next exercise: ")?; + terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?; + stdout.write_all(b"\n")?; + } ExercisesProgress::AllDone => (), - ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => writeln!( - stdout, - "Next exercise: {}", - app_state.current_exercise().terminal_link(), - )?, } Ok(()) diff --git a/src/term.rs b/src/term.rs index b993108e..4c6ac904 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,10 +1,13 @@ -use std::io::{self, BufRead, StdoutLock, Write}; +use std::{ + fmt, fs, + io::{self, BufRead, StdoutLock, Write}, +}; use crossterm::{ cursor::MoveTo, - style::{Color, ResetColor, SetForegroundColor}, + style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, terminal::{Clear, ClearType}, - QueueableCommand, + Command, QueueableCommand, }; /// Terminal progress bar to be used when not using Ratataui. @@ -68,3 +71,43 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { stdout.write_all(b"\n")?; Ok(()) } + +pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) -> io::Result<()> { + let canonical_path = fs::canonicalize(path).ok(); + + let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else { + return stdout.write_all(path.as_bytes()); + }; + + // Windows itself can't handle its verbatim paths. + #[cfg(windows)] + let canonical_path = if canonical_path.len() > 5 && &canonical_path[0..4] == r"\\?\" { + &canonical_path[4..] + } else { + canonical_path + }; + + stdout + .queue(SetForegroundColor(color))? + .queue(SetAttribute(Attribute::Underlined))?; + write!( + stdout, + "\x1b]8;;file://{canonical_path}\x1b\\{path}\x1b]8;;\x1b\\", + )?; + stdout.queue(ResetColor)?; + + Ok(()) +} + +pub fn write_ansi(output: &mut Vec, command: impl Command) { + struct FmtWriter<'a>(&'a mut Vec); + + impl fmt::Write for FmtWriter<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.0.extend_from_slice(s.as_bytes()); + Ok(()) + } + } + + let _ = command.write_ansi(&mut FmtWriter(output)); +} diff --git a/src/terminal_link.rs b/src/terminal_link.rs deleted file mode 100644 index 9bea07d9..00000000 --- a/src/terminal_link.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::{ - fmt::{self, Display, Formatter}, - fs, -}; - -pub struct TerminalFileLink<'a>(pub &'a str); - -impl<'a> Display for TerminalFileLink<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let path = fs::canonicalize(self.0); - - if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) { - // Windows itself can't handle its verbatim paths. - #[cfg(windows)] - let path = if path.len() > 5 && &path[0..4] == r"\\?\" { - &path[4..] - } else { - path - }; - - write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0) - } else { - write!(f, "{}", self.0) - } - } -} diff --git a/src/watch/state.rs b/src/watch/state.rs index 40e3d3ec..f9fd1389 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,16 +1,17 @@ use anyhow::Result; use crossterm::{ - style::{style, Stylize}, - terminal, + style::{ + Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor, + }, + terminal, QueueableCommand, }; use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, clear_terminal, - exercise::{RunnableExercise, OUTPUT_CAPACITY}, - term::progress_bar, - terminal_link::TerminalFileLink, + exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, + term::{progress_bar, terminal_file_link}, }; #[derive(PartialEq, Eq)] @@ -21,7 +22,7 @@ enum DoneStatus { } pub struct WatchState<'a> { - writer: StdoutLock<'a>, + stdout: StdoutLock<'a>, app_state: &'a mut AppState, output: Vec, show_hint: bool, @@ -31,10 +32,11 @@ pub struct WatchState<'a> { impl<'a> WatchState<'a> { pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self { - let writer = io::stdout().lock(); + // TODO: Take stdout as arg. + let stdout = io::stdout().lock(); Self { - writer, + stdout, app_state, output: Vec::with_capacity(OUTPUT_CAPACITY), show_hint: false, @@ -45,14 +47,14 @@ impl<'a> WatchState<'a> { #[inline] pub fn into_writer(self) -> StdoutLock<'a> { - self.writer + self.stdout } pub fn run_current_exercise(&mut self) -> Result<()> { self.show_hint = false; writeln!( - self.writer, + self.stdout, "\nChecking the exercise `{}`. Please wait…", self.app_state.current_exercise().name, )?; @@ -98,75 +100,101 @@ impl<'a> WatchState<'a> { return Ok(ExercisesProgress::CurrentPending); } - self.app_state.done_current_exercise(&mut self.writer) + self.app_state.done_current_exercise(&mut self.stdout) } fn show_prompt(&mut self) -> io::Result<()> { - self.writer.write_all(b"\n")?; - if self.manual_run { - write!(self.writer, "{}:run / ", 'r'.bold())?; + self.stdout.queue(SetAttribute(Attribute::Bold))?; + self.stdout.write_all(b"r")?; + self.stdout.queue(ResetColor)?; + self.stdout.write_all(b":run / ")?; } if self.done_status != DoneStatus::Pending { - write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?; + self.stdout.queue(SetAttribute(Attribute::Bold))?; + self.stdout.write_all(b"n")?; + self.stdout.queue(ResetColor)?; + self.stdout.write_all(b":")?; + self.stdout.queue(SetAttribute(Attribute::Underlined))?; + self.stdout.write_all(b"next")?; + self.stdout.queue(ResetColor)?; + self.stdout.write_all(b" / ")?; } if !self.show_hint { - write!(self.writer, "{}:hint / ", 'h'.bold())?; + self.stdout.queue(SetAttribute(Attribute::Bold))?; + self.stdout.write_all(b"h")?; + self.stdout.queue(ResetColor)?; + self.stdout.write_all(b":hint / ")?; } - write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?; + self.stdout.queue(SetAttribute(Attribute::Bold))?; + self.stdout.write_all(b"l")?; + self.stdout.queue(ResetColor)?; + self.stdout.write_all(b":list / ")?; - self.writer.flush() + self.stdout.queue(SetAttribute(Attribute::Bold))?; + self.stdout.write_all(b"q")?; + self.stdout.queue(ResetColor)?; + self.stdout.write_all(b":quit ? ")?; + + self.stdout.flush() } pub fn render(&mut self) -> io::Result<()> { // Prevent having the first line shifted if clearing wasn't successful. - self.writer.write_all(b"\n")?; - clear_terminal(&mut self.writer)?; + self.stdout.write_all(b"\n")?; + clear_terminal(&mut self.stdout)?; - self.writer.write_all(&self.output)?; + self.stdout.write_all(&self.output)?; if self.show_hint { - writeln!( - self.writer, - "{}\n{}\n", - "Hint".bold().cyan().underlined(), - self.app_state.current_exercise().hint, - )?; + self.stdout + .queue(SetAttributes( + Attributes::from(Attribute::Bold).with(Attribute::Underlined), + ))? + .queue(SetForegroundColor(Color::Cyan))?; + self.stdout.write_all(b"Hint\n")?; + self.stdout.queue(ResetColor)?; + + self.stdout + .write_all(self.app_state.current_exercise().hint.as_bytes())?; + self.stdout.write_all(b"\n\n")?; } if self.done_status != DoneStatus::Pending { - writeln!(self.writer, "{}", "Exercise done ✓".bold().green())?; + self.stdout + .queue(SetAttribute(Attribute::Bold))? + .queue(SetForegroundColor(Color::Green))?; + self.stdout.write_all("Exercise done ✓\n".as_bytes())?; + self.stdout.queue(ResetColor)?; if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { - writeln!( - self.writer, - "{} for comparison: {}", - "Solution".bold(), - style(TerminalFileLink(solution_path)).underlined().cyan(), - )?; + solution_link_line(&mut self.stdout, solution_path)?; } writeln!( - self.writer, + self.stdout, "When done experimenting, enter `n` to move on to the next exercise 🦀\n", )?; } let line_width = terminal::size()?.0; progress_bar( - &mut self.writer, + &mut self.stdout, self.app_state.n_done(), self.app_state.exercises().len() as u16, line_width, )?; - writeln!( - self.writer, - "\nCurrent exercise: {}", - self.app_state.current_exercise().terminal_link(), + + self.stdout.write_all(b"\nCurrent exercise: ")?; + terminal_file_link( + &mut self.stdout, + self.app_state.current_exercise().path, + Color::Blue, )?; + self.stdout.write_all(b"\n\n")?; self.show_prompt()?; From a1f0eaab549300dbd7d2dbd85cf11aba34f57c2d Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 25 Aug 2024 23:54:04 +0200 Subject: [PATCH 102/211] Add disallowed types and methods in Clippy --- Cargo.toml | 2 ++ clippy.toml | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 clippy.toml diff --git a/Cargo.toml b/Cargo.toml index 5eb25b49..b332f40b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,8 @@ unstable_features = "forbid" [workspace.lints.clippy] empty_loop = "forbid" +disallowed-types = "deny" +disallowed-methods = "deny" infinite_loop = "deny" mem_forget = "deny" dbg_macro = "warn" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..81e372a7 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,13 @@ +disallowed-types = [ + # Inefficient. Use `.queue(…)` instead. + "crossterm::style::Stylize", + "crossterm::style::styled_content::StyledContent", +] + +disallowed-methods = [ + # We use `ahash` instead of the default hasher. + "std::collections::HashSet::new", + "std::collections::HashSet::with_capacity", + # Inefficient. Use `.queue(…)` instead. + "crossterm::style::style", +] From 631f2db1a31ce5a32bc954412a7cf42158046113 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 25 Aug 2024 23:54:18 +0200 Subject: [PATCH 103/211] Lower the maximum scroll padding --- src/list/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 25ca1ded..756814f6 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -12,7 +12,7 @@ use std::{ use crate::{app_state::AppState, exercise::Exercise, term::progress_bar, MAX_EXERCISE_NAME_LEN}; -const MAX_SCROLL_PADDING: usize = 8; +const MAX_SCROLL_PADDING: usize = 5; // +1 for column padding. const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; From 159273e53291cd72d27795cd2cfe0820587e3009 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 00:09:04 +0200 Subject: [PATCH 104/211] Take stdout as argument in watch mode --- src/watch.rs | 25 ++++----- src/watch/state.rs | 127 +++++++++++++++++++++------------------------ 2 files changed, 69 insertions(+), 83 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index c6690304..e14d3c57 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -72,35 +72,32 @@ pub fn watch( let mut watch_state = WatchState::new(app_state, manual_run); - watch_state.run_current_exercise()?; + let mut stdout = io::stdout().lock(); + watch_state.run_current_exercise(&mut stdout)?; thread::spawn(move || terminal_event_handler(tx, manual_run)); while let Ok(event) = rx.recv() { match event { - WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? { + WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? { ExercisesProgress::AllDone => break, - ExercisesProgress::CurrentPending => watch_state.render()?, - ExercisesProgress::NewPending => watch_state.run_current_exercise()?, + ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?, + ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?, }, - WatchEvent::Input(InputEvent::Hint) => { - watch_state.show_hint()?; - } + WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, WatchEvent::Input(InputEvent::List) => { return Ok(WatchExit::List); } WatchEvent::Input(InputEvent::Quit) => { - watch_state.into_writer().write_all(QUIT_MSG)?; + stdout.write_all(QUIT_MSG)?; break; } - WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, - WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?, + WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, + WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render(&mut stdout)?, WatchEvent::FileChange { exercise_ind } => { - watch_state.handle_file_change(exercise_ind)?; - } - WatchEvent::TerminalResize => { - watch_state.render()?; + watch_state.handle_file_change(exercise_ind, &mut stdout)?; } + WatchEvent::TerminalResize => watch_state.render(&mut stdout)?, WatchEvent::NotifyErr(e) => { return Err(Error::from(e).context(NOTIFY_ERR)); } diff --git a/src/watch/state.rs b/src/watch/state.rs index f9fd1389..47af9193 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -22,7 +22,6 @@ enum DoneStatus { } pub struct WatchState<'a> { - stdout: StdoutLock<'a>, app_state: &'a mut AppState, output: Vec, show_hint: bool, @@ -32,11 +31,7 @@ pub struct WatchState<'a> { impl<'a> WatchState<'a> { pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self { - // TODO: Take stdout as arg. - let stdout = io::stdout().lock(); - Self { - stdout, app_state, output: Vec::with_capacity(OUTPUT_CAPACITY), show_hint: false, @@ -45,16 +40,11 @@ impl<'a> WatchState<'a> { } } - #[inline] - pub fn into_writer(self) -> StdoutLock<'a> { - self.stdout - } - - pub fn run_current_exercise(&mut self) -> Result<()> { + pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> { self.show_hint = false; writeln!( - self.stdout, + stdout, "\nChecking the exercise `{}`. Please wait…", self.app_state.current_exercise().name, )?; @@ -78,11 +68,15 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } - self.render()?; + self.render(stdout)?; Ok(()) } - pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> { + pub fn handle_file_change( + &mut self, + exercise_ind: usize, + stdout: &mut StdoutLock, + ) -> Result<()> { // Don't skip exercises on file changes to avoid confusion from missing exercises. // Skipping exercises must be explicit in the interactive list. // But going back to an earlier exercise on file change is fine. @@ -91,118 +85,113 @@ impl<'a> WatchState<'a> { } self.app_state.set_current_exercise_ind(exercise_ind)?; - self.run_current_exercise() + self.run_current_exercise(stdout) } /// Move on to the next exercise if the current one is done. - pub fn next_exercise(&mut self) -> Result { + pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result { if self.done_status == DoneStatus::Pending { return Ok(ExercisesProgress::CurrentPending); } - self.app_state.done_current_exercise(&mut self.stdout) + self.app_state.done_current_exercise(stdout) } - fn show_prompt(&mut self) -> io::Result<()> { + fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> { if self.manual_run { - self.stdout.queue(SetAttribute(Attribute::Bold))?; - self.stdout.write_all(b"r")?; - self.stdout.queue(ResetColor)?; - self.stdout.write_all(b":run / ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"r")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":run / ")?; } if self.done_status != DoneStatus::Pending { - self.stdout.queue(SetAttribute(Attribute::Bold))?; - self.stdout.write_all(b"n")?; - self.stdout.queue(ResetColor)?; - self.stdout.write_all(b":")?; - self.stdout.queue(SetAttribute(Attribute::Underlined))?; - self.stdout.write_all(b"next")?; - self.stdout.queue(ResetColor)?; - self.stdout.write_all(b" / ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"n")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":")?; + stdout.queue(SetAttribute(Attribute::Underlined))?; + stdout.write_all(b"next")?; + stdout.queue(ResetColor)?; + stdout.write_all(b" / ")?; } if !self.show_hint { - self.stdout.queue(SetAttribute(Attribute::Bold))?; - self.stdout.write_all(b"h")?; - self.stdout.queue(ResetColor)?; - self.stdout.write_all(b":hint / ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"h")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":hint / ")?; } - self.stdout.queue(SetAttribute(Attribute::Bold))?; - self.stdout.write_all(b"l")?; - self.stdout.queue(ResetColor)?; - self.stdout.write_all(b":list / ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"l")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":list / ")?; - self.stdout.queue(SetAttribute(Attribute::Bold))?; - self.stdout.write_all(b"q")?; - self.stdout.queue(ResetColor)?; - self.stdout.write_all(b":quit ? ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"q")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":quit ? ")?; - self.stdout.flush() + stdout.flush() } - pub fn render(&mut self) -> io::Result<()> { + pub fn render(&self, stdout: &mut StdoutLock) -> io::Result<()> { // Prevent having the first line shifted if clearing wasn't successful. - self.stdout.write_all(b"\n")?; - clear_terminal(&mut self.stdout)?; + stdout.write_all(b"\n")?; + clear_terminal(stdout)?; - self.stdout.write_all(&self.output)?; + stdout.write_all(&self.output)?; if self.show_hint { - self.stdout + stdout .queue(SetAttributes( Attributes::from(Attribute::Bold).with(Attribute::Underlined), ))? .queue(SetForegroundColor(Color::Cyan))?; - self.stdout.write_all(b"Hint\n")?; - self.stdout.queue(ResetColor)?; + stdout.write_all(b"Hint\n")?; + stdout.queue(ResetColor)?; - self.stdout - .write_all(self.app_state.current_exercise().hint.as_bytes())?; - self.stdout.write_all(b"\n\n")?; + stdout.write_all(self.app_state.current_exercise().hint.as_bytes())?; + stdout.write_all(b"\n\n")?; } if self.done_status != DoneStatus::Pending { - self.stdout + stdout .queue(SetAttribute(Attribute::Bold))? .queue(SetForegroundColor(Color::Green))?; - self.stdout.write_all("Exercise done ✓\n".as_bytes())?; - self.stdout.queue(ResetColor)?; + stdout.write_all("Exercise done ✓\n".as_bytes())?; + stdout.queue(ResetColor)?; if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { - solution_link_line(&mut self.stdout, solution_path)?; + solution_link_line(stdout, solution_path)?; } writeln!( - self.stdout, + stdout, "When done experimenting, enter `n` to move on to the next exercise 🦀\n", )?; } let line_width = terminal::size()?.0; progress_bar( - &mut self.stdout, + stdout, self.app_state.n_done(), self.app_state.exercises().len() as u16, line_width, )?; - self.stdout.write_all(b"\nCurrent exercise: ")?; - terminal_file_link( - &mut self.stdout, - self.app_state.current_exercise().path, - Color::Blue, - )?; - self.stdout.write_all(b"\n\n")?; + stdout.write_all(b"\nCurrent exercise: ")?; + terminal_file_link(stdout, self.app_state.current_exercise().path, Color::Blue)?; + stdout.write_all(b"\n\n")?; - self.show_prompt()?; + self.show_prompt(stdout)?; Ok(()) } - pub fn show_hint(&mut self) -> io::Result<()> { + pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { self.show_hint = true; - self.render() + self.render(stdout) } } From 833e6e0c92c1d24948ffc086e4d1c69e90dd04c2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 00:24:39 +0200 Subject: [PATCH 105/211] Newline after resetting attributes --- src/exercise.rs | 6 ++++-- src/init.rs | 9 +++++---- src/watch/state.rs | 12 +++++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 462287db..ea15465c 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -31,8 +31,9 @@ fn run_bin( ) -> Result { if let Some(output) = output.as_deref_mut() { write_ansi(output, SetAttribute(Attribute::Underlined)); - output.extend_from_slice(b"Output\n"); + output.extend_from_slice(b"Output"); write_ansi(output, ResetColor); + output.push(b'\n'); } let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?; @@ -44,8 +45,9 @@ fn run_bin( // leaves the user confused about why the exercise isn't done yet. write_ansi(output, SetAttribute(Attribute::Bold)); write_ansi(output, SetForegroundColor(Color::Red)); - output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)\n"); + output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)"); write_ansi(output, ResetColor); + output.push(b'\n'); } } diff --git a/src/init.rs b/src/init.rs index 40d9910d..aecb2d8c 100644 --- a/src/init.rs +++ b/src/init.rs @@ -148,10 +148,11 @@ pub fn init() -> Result<()> { } stdout.queue(SetForegroundColor(Color::Green))?; - stdout.write_all("Initialization done ✓\n\n".as_bytes())?; - stdout - .queue(ResetColor)? - .queue(SetAttribute(Attribute::Bold))?; + stdout.write_all("Initialization done ✓".as_bytes())?; + stdout.queue(ResetColor)?; + stdout.write_all(b"\n\n")?; + + stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(POST_INIT_MSG)?; stdout.queue(ResetColor)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 47af9193..1c2e2a9a 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -149,8 +149,9 @@ impl<'a> WatchState<'a> { Attributes::from(Attribute::Bold).with(Attribute::Underlined), ))? .queue(SetForegroundColor(Color::Cyan))?; - stdout.write_all(b"Hint\n")?; + stdout.write_all(b"Hint")?; stdout.queue(ResetColor)?; + stdout.write_all(b"\n")?; stdout.write_all(self.app_state.current_exercise().hint.as_bytes())?; stdout.write_all(b"\n\n")?; @@ -160,16 +161,17 @@ impl<'a> WatchState<'a> { stdout .queue(SetAttribute(Attribute::Bold))? .queue(SetForegroundColor(Color::Green))?; - stdout.write_all("Exercise done ✓\n".as_bytes())?; + stdout.write_all("Exercise done ✓".as_bytes())?; stdout.queue(ResetColor)?; + stdout.write_all(b"\n")?; if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { solution_link_line(stdout, solution_path)?; } - writeln!( - stdout, - "When done experimenting, enter `n` to move on to the next exercise 🦀\n", + stdout.write_all( + "When done experimenting, enter `n` to move on to the next exercise 🦀\n\n" + .as_bytes(), )?; } From cb86b44dea79b538a9ce62fb230de4b74e95ccf4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 00:40:30 +0200 Subject: [PATCH 106/211] LOL, swapped colors --- 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 756814f6..eeda110b 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -178,10 +178,10 @@ impl<'a> ListState<'a> { } if exercise.done { - stdout.queue(SetForegroundColor(Color::Yellow))?; + stdout.queue(SetForegroundColor(Color::Green))?; stdout.write_all(b"DONE ")?; } else { - stdout.queue(SetForegroundColor(Color::Green))?; + stdout.queue(SetForegroundColor(Color::Yellow))?; stdout.write_all(b"PENDING ")?; } From d1571d18f915943418fb8d13a3997d0d7d384e77 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 00:48:12 +0200 Subject: [PATCH 107/211] Only reset color and underline after link --- src/term.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/term.rs b/src/term.rs index 4c6ac904..7b8642b9 100644 --- a/src/term.rs +++ b/src/term.rs @@ -94,7 +94,9 @@ pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) -> stdout, "\x1b]8;;file://{canonical_path}\x1b\\{path}\x1b]8;;\x1b\\", )?; - stdout.queue(ResetColor)?; + stdout + .queue(SetForegroundColor(Color::Reset))? + .queue(SetAttribute(Attribute::NoUnderline))?; Ok(()) } From 5c355468c1c0ef6561348591bb755ff67b561c30 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 00:49:56 +0200 Subject: [PATCH 108/211] File link in the list? No problem :D --- src/list/state.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index eeda110b..c1c75d79 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -10,7 +10,12 @@ use std::{ io::{self, StdoutLock, Write}, }; -use crate::{app_state::AppState, exercise::Exercise, term::progress_bar, MAX_EXERCISE_NAME_LEN}; +use crate::{ + app_state::AppState, + exercise::Exercise, + term::{progress_bar, terminal_file_link}, + MAX_EXERCISE_NAME_LEN, +}; const MAX_SCROLL_PADDING: usize = 5; // +1 for column padding. @@ -190,7 +195,7 @@ impl<'a> ListState<'a> { stdout.write_all(exercise.name.as_bytes())?; stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; - stdout.write_all(exercise.path.as_bytes())?; + terminal_file_link(stdout, exercise.path, Color::Blue)?; next_ln_overwrite(stdout)?; stdout.queue(ResetColor)?; From 594e212b8a49cae001c0a45818debaceeda3b9a3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 00:53:42 +0200 Subject: [PATCH 109/211] Darker highlighting in the list --- src/list/state.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index c1c75d79..cc563462 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -166,9 +166,9 @@ impl<'a> ListState<'a> { { if self.selected_row == Some(self.row_offset + n_displayed_rows) { stdout.queue(SetBackgroundColor(Color::Rgb { - r: 50, - g: 50, - b: 50, + r: 40, + g: 40, + b: 40, }))?; stdout.write_all("🦀".as_bytes())?; } else { From ee25a7d45805def5cb6516dec6c9edf54fad5e48 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 02:41:22 +0200 Subject: [PATCH 110/211] Disable terminal links in VS-Code --- src/term.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/term.rs b/src/term.rs index 7b8642b9..6efe4d51 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,5 +1,6 @@ use std::{ - fmt, fs, + cell::Cell, + env, fmt, fs, io::{self, BufRead, StdoutLock, Write}, }; @@ -10,6 +11,10 @@ use crossterm::{ Command, QueueableCommand, }; +thread_local! { + static VS_CODE: Cell = Cell::new(env::var_os("TERM").is_some_and(|v| v == "vscode")); +} + /// Terminal progress bar to be used when not using Ratataui. pub fn progress_bar( stdout: &mut StdoutLock, @@ -73,6 +78,11 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { } pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) -> io::Result<()> { + // VS Code shows its own links. This also avoids some issues, especially on Windows. + if VS_CODE.get() { + return stdout.write_all(path.as_bytes()); + } + let canonical_path = fs::canonicalize(path).ok(); let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else { From f22700a4eca613f1b3cbbd6f8b3bd4fc37569039 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 02:43:08 +0200 Subject: [PATCH 111/211] Use the correct environment variable --- src/term.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/term.rs b/src/term.rs index 6efe4d51..92429197 100644 --- a/src/term.rs +++ b/src/term.rs @@ -12,7 +12,7 @@ use crossterm::{ }; thread_local! { - static VS_CODE: Cell = Cell::new(env::var_os("TERM").is_some_and(|v| v == "vscode")); + static VS_CODE: Cell = Cell::new(env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode")); } /// Terminal progress bar to be used when not using Ratataui. From e811dd15b56d839b0e43e51eeaea1a2a700c0ebb Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 04:29:58 +0200 Subject: [PATCH 112/211] Fix list on terminals that don't disable line wrapping --- src/list/state.rs | 109 +++++++++++++++++++++------------------------ src/term.rs | 110 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 70 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index cc563462..3876884b 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -13,7 +13,7 @@ use std::{ use crate::{ app_state::AppState, exercise::Exercise, - term::{progress_bar, terminal_file_link}, + term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter}, MAX_EXERCISE_NAME_LEN, }; @@ -28,14 +28,6 @@ fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> { Ok(()) } -// Avoids having the last written char as the last displayed one when the -// written width is higher than the terminal width. -// Happens on the Gnome terminal for example. -fn next_ln_overwrite(stdout: &mut StdoutLock) -> io::Result<()> { - stdout.write_all(b" ")?; - next_ln(stdout) -} - #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { Done, @@ -164,40 +156,44 @@ impl<'a> ListState<'a> { .skip(self.row_offset) .take(self.max_n_rows_to_display) { + let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); + if self.selected_row == Some(self.row_offset + n_displayed_rows) { - stdout.queue(SetBackgroundColor(Color::Rgb { + writer.stdout.queue(SetBackgroundColor(Color::Rgb { r: 40, g: 40, b: 40, }))?; - stdout.write_all("🦀".as_bytes())?; + // The crab emoji has the width of two ascii chars. + writer.add_to_len(2); + writer.stdout.write_all("🦀".as_bytes())?; } else { - stdout.write_all(b" ")?; + writer.write_ascii(b" ")?; } if exercise_ind == current_exercise_ind { - stdout.queue(SetForegroundColor(Color::Red))?; - stdout.write_all(b">>>>>>> ")?; + writer.stdout.queue(SetForegroundColor(Color::Red))?; + writer.write_ascii(b">>>>>>> ")?; } else { - stdout.write_all(b" ")?; + writer.write_ascii(b" ")?; } if exercise.done { - stdout.queue(SetForegroundColor(Color::Green))?; - stdout.write_all(b"DONE ")?; + writer.stdout.queue(SetForegroundColor(Color::Green))?; + writer.write_ascii(b"DONE ")?; } else { - stdout.queue(SetForegroundColor(Color::Yellow))?; - stdout.write_all(b"PENDING ")?; + writer.stdout.queue(SetForegroundColor(Color::Yellow))?; + writer.write_ascii(b"PENDING ")?; } - stdout.queue(SetForegroundColor(Color::Reset))?; + writer.stdout.queue(SetForegroundColor(Color::Reset))?; - stdout.write_all(exercise.name.as_bytes())?; - stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; + writer.write_str(exercise.name)?; + writer.write_ascii(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; - terminal_file_link(stdout, exercise.path, Color::Blue)?; + terminal_file_link(&mut writer, exercise.path, Color::Blue)?; - next_ln_overwrite(stdout)?; + next_ln(stdout)?; stdout.queue(ResetColor)?; n_displayed_rows += 1; } @@ -213,10 +209,11 @@ impl<'a> ListState<'a> { stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?; // Header - stdout.write_all(b" Current State Name")?; - stdout.write_all(&SPACE[..self.name_col_width - 2])?; - stdout.write_all(b"Path")?; - next_ln_overwrite(stdout)?; + let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); + writer.write_ascii(b" Current State Name")?; + writer.write_ascii(&SPACE[..self.name_col_width - 2])?; + writer.write_ascii(b"Path")?; + next_ln(stdout)?; // Rows let iter = self.app_state.exercises().iter().enumerate(); @@ -237,7 +234,7 @@ impl<'a> ListState<'a> { next_ln(stdout)?; progress_bar( - stdout, + &mut MaxLenWriter::new(stdout, self.term_width as usize), self.app_state.n_done(), self.app_state.exercises().len() as u16, self.term_width, @@ -247,59 +244,55 @@ impl<'a> ListState<'a> { stdout.write_all(&self.separator_line)?; next_ln(stdout)?; + let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { // Help footer message if self.selected_row.is_some() { - stdout.write_all( - "↓/j ↑/k home/g end/G | ontinue at | eset exercise".as_bytes(), - )?; + writer.write_str("↓/j ↑/k home/g end/G | ontinue at | eset exercise")?; if self.narrow_term { - next_ln_overwrite(stdout)?; - stdout.write_all(b"filter ")?; + next_ln(stdout)?; + writer = MaxLenWriter::new(stdout, self.term_width as usize); + + writer.write_ascii(b"filter ")?; } else { - stdout.write_all(b" | filter ")?; + writer.write_ascii(b" | filter ")?; } } else { // Nothing selected (and nothing shown), so only display filter and quit. - stdout.write_all(b"filter ")?; + writer.write_ascii(b"filter ")?; } match self.filter { Filter::Done => { - stdout + writer + .stdout .queue(SetForegroundColor(Color::Magenta))? .queue(SetAttribute(Attribute::Underlined))?; - stdout.write_all(b"one")?; - stdout.queue(ResetColor)?; - stdout.write_all(b"/

ending")?; + writer.write_ascii(b"one")?; + writer.stdout.queue(ResetColor)?; + writer.write_ascii(b"/

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

ending")?; - stdout.queue(ResetColor)?; + writer.write_ascii(b"

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

ending")?, + Filter::None => writer.write_ascii(b"one/

ending")?, } - stdout.write_all(b" | uit list")?; - - if self.narrow_term { - next_ln_overwrite(stdout)?; - } else { - next_ln(stdout)?; - } + writer.write_ascii(b" | uit list")?; } else { - stdout.queue(SetForegroundColor(Color::Magenta))?; - stdout.write_all(self.message.as_bytes())?; + writer.stdout.queue(SetForegroundColor(Color::Magenta))?; + writer.write_str(&self.message)?; stdout.queue(ResetColor)?; - next_ln_overwrite(stdout)?; - if self.narrow_term { - next_ln(stdout)?; - } + next_ln(stdout)?; } + + next_ln(stdout)?; } stdout.queue(EndSynchronizedUpdate)?.flush() diff --git a/src/term.rs b/src/term.rs index 92429197..51fcad10 100644 --- a/src/term.rs +++ b/src/term.rs @@ -15,9 +15,83 @@ thread_local! { static VS_CODE: Cell = Cell::new(env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode")); } +pub struct MaxLenWriter<'a, 'b> { + pub stdout: &'a mut StdoutLock<'b>, + len: usize, + max_len: usize, +} + +impl<'a, 'b> MaxLenWriter<'a, 'b> { + #[inline] + pub fn new(stdout: &'a mut StdoutLock<'b>, max_len: usize) -> Self { + Self { + stdout, + len: 0, + max_len, + } + } + + // Additional is for emojis that take more space. + #[inline] + pub fn add_to_len(&mut self, additional: usize) { + self.len += additional; + } +} + +pub trait CountedWrite<'a> { + fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>; + fn write_str(&mut self, unicode: &str) -> io::Result<()>; + fn stdout(&mut self) -> &mut StdoutLock<'a>; +} + +impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> { + fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> { + let n = ascii.len().min(self.max_len.saturating_sub(self.len)); + self.stdout.write_all(&ascii[..n])?; + self.len += n; + Ok(()) + } + + fn write_str(&mut self, unicode: &str) -> io::Result<()> { + if let Some((ind, c)) = unicode + .char_indices() + .take(self.max_len.saturating_sub(self.len)) + .last() + { + self.stdout + .write_all(&unicode.as_bytes()[..ind + c.len_utf8()])?; + self.len += ind + 1; + } + + Ok(()) + } + + #[inline] + fn stdout(&mut self) -> &mut StdoutLock<'b> { + 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 + } +} + /// Terminal progress bar to be used when not using Ratataui. -pub fn progress_bar( - stdout: &mut StdoutLock, +pub fn progress_bar<'a>( + writer: &mut impl CountedWrite<'a>, progress: u16, total: u16, line_width: u16, @@ -32,9 +106,13 @@ pub fn progress_bar( const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; if line_width < MIN_LINE_WIDTH { - return write!(stdout, "Progress: {progress}/{total} exercises"); + writer.write_ascii(b"Progress: ")?; + // Integers are in ASCII. + writer.write_ascii(format!("{progress}/{total}").as_bytes())?; + return writer.write_ascii(b" exercises"); } + let stdout = writer.stdout(); stdout.write_all(PREFIX)?; let width = line_width - WRAPPER_WIDTH; @@ -77,16 +155,20 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { Ok(()) } -pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) -> io::Result<()> { +pub fn terminal_file_link<'a>( + writer: &mut impl CountedWrite<'a>, + path: &str, + color: Color, +) -> io::Result<()> { // VS Code shows its own links. This also avoids some issues, especially on Windows. if VS_CODE.get() { - return stdout.write_all(path.as_bytes()); + return writer.write_str(path); } let canonical_path = fs::canonicalize(path).ok(); let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else { - return stdout.write_all(path.as_bytes()); + return writer.write_str(path); }; // Windows itself can't handle its verbatim paths. @@ -97,14 +179,18 @@ pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) -> canonical_path }; - stdout + writer + .stdout() .queue(SetForegroundColor(color))? .queue(SetAttribute(Attribute::Underlined))?; - write!( - stdout, - "\x1b]8;;file://{canonical_path}\x1b\\{path}\x1b]8;;\x1b\\", - )?; - stdout + writer.stdout().write_all(b"\x1b]8;;file://")?; + writer.stdout().write_all(canonical_path.as_bytes())?; + writer.stdout().write_all(b"\x1b\\")?; + // Only this part is visible. + writer.write_str(path)?; + writer.stdout().write_all(b"\x1b]8;;\x1b\\")?; + writer + .stdout() .queue(SetForegroundColor(Color::Reset))? .queue(SetAttribute(Attribute::NoUnderline))?; From 74388d4bf44cdfebc0d6dc8e5faa81bffe71ddd6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 04:41:26 +0200 Subject: [PATCH 113/211] Only trigger write when needed --- src/term.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/term.rs b/src/term.rs index 51fcad10..fa71ac65 100644 --- a/src/term.rs +++ b/src/term.rs @@ -47,8 +47,10 @@ pub trait CountedWrite<'a> { impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> { fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> { let n = ascii.len().min(self.max_len.saturating_sub(self.len)); - self.stdout.write_all(&ascii[..n])?; - self.len += n; + if n > 0 { + self.stdout.write_all(&ascii[..n])?; + self.len += n; + } Ok(()) } From 0f71a150ff292b1f18b30c7aa75dc8b3d48d2b8e Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 26 Aug 2024 22:03:09 +0200 Subject: [PATCH 114/211] Making code prettier :P --- src/main.rs | 4 +--- src/term.rs | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 998d3d11..e8f274b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,9 +71,7 @@ fn main() -> Result<()> { } match args.command { - Some(Subcommands::Init) => { - return init::init().context("Initialization failed"); - } + Some(Subcommands::Init) => return init::init().context("Initialization failed"), Some(Subcommands::Dev(dev_command)) => return dev_command.run(), _ => (), } diff --git a/src/term.rs b/src/term.rs index fa71ac65..0416c30e 100644 --- a/src/term.rs +++ b/src/term.rs @@ -153,8 +153,7 @@ pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { stdout.flush()?; io::stdin().lock().read_until(b'\n', &mut Vec::new())?; - stdout.write_all(b"\n")?; - Ok(()) + stdout.write_all(b"\n") } pub fn terminal_file_link<'a>( From dd52e9cd7239276745c2fbad02a63931327a8e48 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 27 Aug 2024 00:03:50 +0200 Subject: [PATCH 115/211] Separate the scroll state --- src/list.rs | 1 + src/list/scroll_state.rs | 104 +++++++++++++++++++++++++++++++++++++++ src/list/state.rs | 104 +++++++++++---------------------------- 3 files changed, 135 insertions(+), 74 deletions(-) create mode 100644 src/list/scroll_state.rs diff --git a/src/list.rs b/src/list.rs index a8e52254..481fb2f4 100644 --- a/src/list.rs +++ b/src/list.rs @@ -16,6 +16,7 @@ use crate::app_state::AppState; use self::state::{Filter, ListState}; +mod scroll_state; mod state; fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> { diff --git a/src/list/scroll_state.rs b/src/list/scroll_state.rs new file mode 100644 index 00000000..25a73736 --- /dev/null +++ b/src/list/scroll_state.rs @@ -0,0 +1,104 @@ +pub struct ScrollState { + n_rows: usize, + max_n_rows_to_display: usize, + selected: Option, + offset: usize, + scroll_padding: usize, + max_scroll_padding: usize, +} + +impl ScrollState { + pub fn new(n_rows: usize, selected: Option, max_scroll_padding: usize) -> Self { + Self { + n_rows, + max_n_rows_to_display: 0, + selected, + offset: selected.map_or(0, |selected| selected.saturating_sub(max_scroll_padding)), + scroll_padding: 0, + max_scroll_padding, + } + } + + #[inline] + pub fn offset(&self) -> usize { + self.offset + } + + fn update_offset(&mut self) { + let Some(selected) = self.selected else { + return; + }; + + let min_offset = (selected + self.scroll_padding) + .saturating_sub(self.max_n_rows_to_display.saturating_sub(1)); + let max_offset = selected.saturating_sub(self.scroll_padding); + let global_max_offset = self.n_rows.saturating_sub(self.max_n_rows_to_display); + + self.offset = self + .offset + .max(min_offset) + .min(max_offset) + .min(global_max_offset); + } + + #[inline] + pub fn selected(&self) -> Option { + self.selected + } + + fn set_selected(&mut self, selected: usize) { + self.selected = Some(selected); + self.update_offset(); + } + + pub fn select_next(&mut self) { + if let Some(selected) = self.selected { + self.set_selected((selected + 1).min(self.n_rows - 1)); + } + } + + pub fn select_previous(&mut self) { + if let Some(selected) = self.selected { + self.set_selected(selected.saturating_sub(1)); + } + } + + pub fn select_first(&mut self) { + if self.n_rows > 0 { + self.set_selected(0); + } + } + + pub fn select_last(&mut self) { + if self.n_rows > 0 { + self.set_selected(self.n_rows - 1); + } + } + + pub fn set_n_rows(&mut self, n_rows: usize) { + self.n_rows = n_rows; + + if self.n_rows == 0 { + self.selected = None; + return; + } + + self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1))); + } + + #[inline] + fn update_scroll_padding(&mut self) { + self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding); + } + + #[inline] + pub fn max_n_rows_to_display(&self) -> usize { + self.max_n_rows_to_display + } + + pub fn set_max_n_rows_to_display(&mut self, max_n_rows_to_display: usize) { + self.max_n_rows_to_display = max_n_rows_to_display; + self.update_scroll_padding(); + self.update_offset(); + } +} diff --git a/src/list/state.rs b/src/list/state.rs index 3876884b..e263b7ed 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -17,7 +17,8 @@ use crate::{ MAX_EXERCISE_NAME_LEN, }; -const MAX_SCROLL_PADDING: usize = 5; +use super::scroll_state::ScrollState; + // +1 for column padding. const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; @@ -39,19 +40,14 @@ pub struct ListState<'a> { /// Footer message to be displayed if not empty. pub message: String, app_state: &'a mut AppState, + scroll_state: ScrollState, name_col_width: usize, filter: Filter, - n_rows_with_filter: usize, - /// Selected row out of the filtered ones. - selected_row: Option, - row_offset: usize, term_width: u16, term_height: u16, separator_line: Vec, narrow_term: bool, show_footer: bool, - max_n_rows_to_display: usize, - scroll_padding: usize, } impl<'a> ListState<'a> { @@ -70,50 +66,29 @@ impl<'a> ListState<'a> { let n_rows_with_filter = app_state.exercises().len(); let selected = app_state.current_exercise_ind(); + let (width, height) = terminal::size()?; + let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5); + let mut slf = Self { message: String::with_capacity(128), app_state, + scroll_state, name_col_width, filter, - n_rows_with_filter, - selected_row: Some(selected), - row_offset: selected.saturating_sub(MAX_SCROLL_PADDING), // Set by `set_term_size` term_width: 0, term_height: 0, separator_line: Vec::new(), narrow_term: false, show_footer: true, - max_n_rows_to_display: 0, - scroll_padding: 0, }; - let (width, height) = terminal::size()?; slf.set_term_size(width, height); slf.draw(stdout)?; Ok(slf) } - fn update_offset(&mut self) { - let Some(selected) = self.selected_row else { - return; - }; - - let min_offset = (selected + self.scroll_padding) - .saturating_sub(self.max_n_rows_to_display.saturating_sub(1)); - let max_offset = selected.saturating_sub(self.scroll_padding); - let global_max_offset = self - .n_rows_with_filter - .saturating_sub(self.max_n_rows_to_display); - - self.row_offset = self - .row_offset - .max(min_offset) - .min(max_offset) - .min(global_max_offset); - } - pub fn set_term_size(&mut self, width: u16, height: u16) { self.term_width = width; self.term_height = height; @@ -124,7 +99,7 @@ impl<'a> ListState<'a> { let wide_help_footer_width = 95; // The help footer is shorter when nothing is selected. - self.narrow_term = width < wide_help_footer_width && self.selected_row.is_some(); + self.narrow_term = width < wide_help_footer_width && self.scroll_state.selected().is_some(); let header_height = 1; // 2 separator, 1 progress bar, 1-2 footer message. @@ -135,13 +110,10 @@ impl<'a> ListState<'a> { self.separator_line = "─".as_bytes().repeat(width as usize); } - self.max_n_rows_to_display = height - .saturating_sub(header_height + u16::from(self.show_footer) * footer_height) - as usize; - - self.scroll_padding = (self.max_n_rows_to_display / 4).min(MAX_SCROLL_PADDING); - - self.update_offset(); + self.scroll_state.set_max_n_rows_to_display( + height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height) + as usize, + ); } fn draw_rows( @@ -150,15 +122,16 @@ impl<'a> ListState<'a> { filtered_exercises: impl Iterator, ) -> io::Result { let current_exercise_ind = self.app_state.current_exercise_ind(); + let row_offset = self.scroll_state.offset(); let mut n_displayed_rows = 0; for (exercise_ind, exercise) in filtered_exercises - .skip(self.row_offset) - .take(self.max_n_rows_to_display) + .skip(row_offset) + .take(self.scroll_state.max_n_rows_to_display()) { let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); - if self.selected_row == Some(self.row_offset + n_displayed_rows) { + if self.scroll_state.selected() == Some(row_offset + n_displayed_rows) { writer.stdout.queue(SetBackgroundColor(Color::Rgb { r: 40, g: 40, @@ -225,7 +198,7 @@ impl<'a> ListState<'a> { Filter::None => self.draw_rows(stdout, iter)?, }; - for _ in 0..self.max_n_rows_to_display - n_displayed_rows { + for _ in 0..self.scroll_state.max_n_rows_to_display() - n_displayed_rows { next_ln(stdout)?; } @@ -247,7 +220,7 @@ impl<'a> ListState<'a> { let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { // Help footer message - if self.selected_row.is_some() { + if self.scroll_state.selected().is_some() { writer.write_str("↓/j ↑/k home/g end/G | ontinue at | eset exercise")?; if self.narrow_term { next_ln(stdout)?; @@ -298,13 +271,8 @@ impl<'a> ListState<'a> { stdout.queue(EndSynchronizedUpdate)?.flush() } - fn set_selected(&mut self, selected: usize) { - self.selected_row = Some(selected); - self.update_offset(); - } - fn update_rows(&mut self) { - self.n_rows_with_filter = match self.filter { + let n_rows = match self.filter { Filter::Done => self .app_state .exercises() @@ -320,15 +288,7 @@ impl<'a> ListState<'a> { Filter::None => self.app_state.exercises().len(), }; - if self.n_rows_with_filter == 0 { - self.selected_row = None; - return; - } - - self.set_selected( - self.selected_row - .map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)), - ); + self.scroll_state.set_n_rows(n_rows); } #[inline] @@ -341,28 +301,24 @@ impl<'a> ListState<'a> { self.update_rows(); } + #[inline] pub fn select_next(&mut self) { - if let Some(selected) = self.selected_row { - self.set_selected((selected + 1).min(self.n_rows_with_filter - 1)); - } + self.scroll_state.select_next(); } + #[inline] pub fn select_previous(&mut self) { - if let Some(selected) = self.selected_row { - self.set_selected(selected.saturating_sub(1)); - } + self.scroll_state.select_previous(); } + #[inline] pub fn select_first(&mut self) { - if self.n_rows_with_filter > 0 { - self.set_selected(0); - } + self.scroll_state.select_first(); } + #[inline] pub fn select_last(&mut self) { - if self.n_rows_with_filter > 0 { - self.set_selected(self.n_rows_with_filter - 1); - } + self.scroll_state.select_last(); } fn selected_to_exercise_ind(&self, selected: usize) -> Result { @@ -390,7 +346,7 @@ impl<'a> ListState<'a> { } pub fn reset_selected(&mut self) -> Result<()> { - let Some(selected) = self.selected_row else { + let Some(selected) = self.scroll_state.selected() else { self.message.push_str("Nothing selected to reset!"); return Ok(()); }; @@ -408,7 +364,7 @@ impl<'a> ListState<'a> { // Return `true` if there was something to select. pub fn selected_to_current_exercise(&mut self) -> Result { - let Some(selected) = self.selected_row else { + let Some(selected) = self.scroll_state.selected() else { self.message.push_str("Nothing selected to continue at!"); return Ok(false); }; From c209c874a9b0aad4a311ef9947c734e086f83a1c Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 28 Aug 2024 00:34:24 +0200 Subject: [PATCH 116/211] Check the exercise name length --- src/dev/check.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index e00d4cc1..4c5e0728 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -14,7 +14,7 @@ use crate::{ collections::{hash_set_with_capacity, HashSet}, exercise::{RunnableExercise, OUTPUT_CAPACITY}, info_file::{ExerciseInfo, InfoFile}, - CURRENT_FORMAT_VERSION, + CURRENT_FORMAT_VERSION, MAX_EXERCISE_NAME_LEN, }; // Find a char that isn't allowed in the exercise's `name` or `dir`. @@ -59,6 +59,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { if name.is_empty() { bail!("Found an empty exercise name in `info.toml`"); } + if name.len() > MAX_EXERCISE_NAME_LEN { + bail!("The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN}"); + } if let Some(c) = forbidden_char(name) { bail!("Char `{c}` in the exercise name `{name}` is not allowed"); } From 7d2bc1c7a4333de5460cb86a8dca5e5ecad2a643 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 28 Aug 2024 00:56:22 +0200 Subject: [PATCH 117/211] Use a Vec for the name col padding --- src/dev/check.rs | 4 +++- src/list/state.rs | 13 ++++++------- src/main.rs | 1 - 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 4c5e0728..a6db3c21 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -14,9 +14,11 @@ use crate::{ collections::{hash_set_with_capacity, HashSet}, exercise::{RunnableExercise, OUTPUT_CAPACITY}, info_file::{ExerciseInfo, InfoFile}, - CURRENT_FORMAT_VERSION, MAX_EXERCISE_NAME_LEN, + CURRENT_FORMAT_VERSION, }; +const MAX_EXERCISE_NAME_LEN: usize = 32; + // Find a char that isn't allowed in the exercise's `name` or `dir`. fn forbidden_char(input: &str) -> Option { input.chars().find(|c| !c.is_alphanumeric() && *c != '_') diff --git a/src/list/state.rs b/src/list/state.rs index e263b7ed..51e4cfa9 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -14,13 +14,11 @@ use crate::{ app_state::AppState, exercise::Exercise, term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter}, - MAX_EXERCISE_NAME_LEN, }; use super::scroll_state::ScrollState; -// +1 for column padding. -const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; +const COL_SPACING: usize = 2; fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> { stdout @@ -41,7 +39,7 @@ pub struct ListState<'a> { pub message: String, app_state: &'a mut AppState, scroll_state: ScrollState, - name_col_width: usize, + name_col_padding: Vec, filter: Filter, term_width: u16, term_height: u16, @@ -61,6 +59,7 @@ impl<'a> ListState<'a> { .map(|exercise| exercise.name.len()) .max() .map_or(name_col_title_len, |max| max.max(name_col_title_len)); + let name_col_padding = vec![b' '; name_col_width + COL_SPACING]; let filter = Filter::None; let n_rows_with_filter = app_state.exercises().len(); @@ -73,7 +72,7 @@ impl<'a> ListState<'a> { message: String::with_capacity(128), app_state, scroll_state, - name_col_width, + name_col_padding, filter, // Set by `set_term_size` term_width: 0, @@ -162,7 +161,7 @@ impl<'a> ListState<'a> { writer.stdout.queue(SetForegroundColor(Color::Reset))?; writer.write_str(exercise.name)?; - writer.write_ascii(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; + writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?; terminal_file_link(&mut writer, exercise.path, Color::Blue)?; @@ -184,7 +183,7 @@ impl<'a> ListState<'a> { // Header let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); writer.write_ascii(b" Current State Name")?; - writer.write_ascii(&SPACE[..self.name_col_width - 2])?; + writer.write_ascii(&self.name_col_padding[2..])?; writer.write_ascii(b"Path")?; next_ln(stdout)?; diff --git a/src/main.rs b/src/main.rs index e8f274b0..e53cd5a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,6 @@ mod term; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; -const MAX_EXERCISE_NAME_LEN: usize = 32; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] From 5556d42b46e3bfe281343d69da588378c728c089 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 28 Aug 2024 01:10:19 +0200 Subject: [PATCH 118/211] Use sol_path --- src/app_state.rs | 10 +++------- src/cargo_toml.rs | 2 +- src/exercise.rs | 31 +++++++++++++++++++++++++++++++ src/info_file.rs | 29 +++++------------------------ src/init.rs | 4 ++-- 5 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index d7de1fdb..1000047d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -321,14 +321,10 @@ impl AppState { .write_solution_to_disk(self.current_exercise_ind, current_exercise.name) .map(Some) } else { - let solution_path = if let Some(dir) = current_exercise.dir { - format!("solutions/{dir}/{}.rs", current_exercise.name) - } else { - format!("solutions/{}.rs", current_exercise.name) - }; + let sol_path = current_exercise.sol_path(); - if Path::new(&solution_path).exists() { - return Ok(Some(solution_path)); + if Path::new(&sol_path).exists() { + return Ok(Some(sol_path)); } Ok(None) diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index 445b6b57..8d417ffa 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use std::path::Path; -use crate::info_file::ExerciseInfo; +use crate::{exercise::RunnableExercise, info_file::ExerciseInfo}; /// Initial capacity of the bins buffer. pub const BINS_BUFFER_CAPACITY: usize = 1 << 14; diff --git a/src/exercise.rs b/src/exercise.rs index ea15465c..11eea638 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -68,6 +68,7 @@ pub struct Exercise { pub trait RunnableExercise { fn name(&self) -> &str; + fn dir(&self) -> Option<&str>; fn strict_clippy(&self) -> bool; fn test(&self) -> bool; @@ -145,6 +146,31 @@ pub trait RunnableExercise { self.run::(&bin_name, output, cmd_runner) } + + fn sol_path(&self) -> String { + let name = self.name(); + + let mut path = if let Some(dir) = self.dir() { + // 14 = 10 + 1 + 3 + // solutions/ + / + .rs + let mut path = String::with_capacity(14 + dir.len() + name.len()); + path.push_str("solutions/"); + path.push_str(dir); + path.push('/'); + path + } else { + // 13 = 10 + 3 + // solutions/ + .rs + let mut path = String::with_capacity(13 + name.len()); + path.push_str("solutions/"); + path + }; + + path.push_str(name); + path.push_str(".rs"); + + path + } } impl RunnableExercise for Exercise { @@ -153,6 +179,11 @@ impl RunnableExercise for Exercise { self.name } + #[inline] + fn dir(&self) -> Option<&str> { + self.dir + } + #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy diff --git a/src/info_file.rs b/src/info_file.rs index d4e46110..fdc8f0f3 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -52,30 +52,6 @@ impl ExerciseInfo { path } - - /// Path to the solution file starting with the `solutions/` directory. - pub fn sol_path(&self) -> String { - let mut path = if let Some(dir) = &self.dir { - // 14 = 10 + 1 + 3 - // solutions/ + / + .rs - let mut path = String::with_capacity(14 + dir.len() + self.name.len()); - path.push_str("solutions/"); - path.push_str(dir); - path.push('/'); - path - } else { - // 13 = 10 + 3 - // solutions/ + .rs - let mut path = String::with_capacity(13 + self.name.len()); - path.push_str("solutions/"); - path - }; - - path.push_str(&self.name); - path.push_str(".rs"); - - path - } } impl RunnableExercise for ExerciseInfo { @@ -84,6 +60,11 @@ impl RunnableExercise for ExerciseInfo { &self.name } + #[inline] + fn dir(&self) -> Option<&str> { + self.dir.as_deref() + } + #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy diff --git a/src/init.rs b/src/init.rs index aecb2d8c..332bf52e 100644 --- a/src/init.rs +++ b/src/init.rs @@ -13,8 +13,8 @@ use std::{ }; use crate::{ - cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile, - term::press_enter_prompt, + cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, exercise::RunnableExercise, + info_file::InfoFile, term::press_enter_prompt, }; #[derive(Deserialize)] From cba4a6f9c8f3b76ccfbf8c4c2aab6adda649df64 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 28 Aug 2024 01:19:53 +0200 Subject: [PATCH 119/211] Only disable links in VS code in the list --- src/app_state.rs | 10 ++++++++++ src/list/state.rs | 8 +++++++- src/term.rs | 12 +----------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 1000047d..b88c1257 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,5 +1,6 @@ use anyhow::{bail, Context, Error, Result}; use std::{ + env, fs::{self, File}, io::{Read, StdoutLock, Write}, path::Path, @@ -44,6 +45,8 @@ pub struct AppState { file_buf: Vec, official_exercises: bool, cmd_runner: CmdRunner, + // Running in VS Code. + vs_code: bool, } impl AppState { @@ -131,6 +134,7 @@ impl AppState { file_buf: Vec::with_capacity(2048), official_exercises: !Path::new("info.toml").exists(), cmd_runner, + vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"), }; let state_file_status = slf.update_from_file(); @@ -163,6 +167,11 @@ impl AppState { &self.cmd_runner } + #[inline] + pub fn vs_code(&self) -> bool { + self.vs_code + } + // Write the state file. // The file's format is very simple: // - The first line is a comment. @@ -457,6 +466,7 @@ mod tests { file_buf: Vec::new(), official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), + vs_code: false, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/list/state.rs b/src/list/state.rs index 51e4cfa9..5f0cda37 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -163,7 +163,13 @@ impl<'a> ListState<'a> { writer.write_str(exercise.name)?; writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?; - terminal_file_link(&mut writer, exercise.path, Color::Blue)?; + // The list links aren't shown correctly in VS Code on Windows. + // But VS Code shows its own links anyway. + if self.app_state.vs_code() { + writer.write_str(exercise.path)?; + } else { + terminal_file_link(&mut writer, exercise.path, Color::Blue)?; + } next_ln(stdout)?; stdout.queue(ResetColor)?; diff --git a/src/term.rs b/src/term.rs index 0416c30e..ee8dbf86 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,6 +1,5 @@ use std::{ - cell::Cell, - env, fmt, fs, + fmt, fs, io::{self, BufRead, StdoutLock, Write}, }; @@ -11,10 +10,6 @@ use crossterm::{ Command, QueueableCommand, }; -thread_local! { - static VS_CODE: Cell = Cell::new(env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode")); -} - pub struct MaxLenWriter<'a, 'b> { pub stdout: &'a mut StdoutLock<'b>, len: usize, @@ -161,11 +156,6 @@ pub fn terminal_file_link<'a>( path: &str, color: Color, ) -> io::Result<()> { - // VS Code shows its own links. This also avoids some issues, especially on Windows. - if VS_CODE.get() { - return writer.write_str(path); - } - let canonical_path = fs::canonicalize(path).ok(); let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else { From afc320bed4ca57d19b66f9d1d33d71806f333e27 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 00:17:22 +0200 Subject: [PATCH 120/211] Fix error about too many open files during the final check --- src/app_state.rs | 103 ++++++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 38 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index b88c1257..cc77711a 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,8 +1,8 @@ -use anyhow::{bail, Context, Error, Result}; +use anyhow::{bail, Context, Result}; use std::{ env, fs::{self, File}, - io::{Read, StdoutLock, Write}, + io::{self, Read, StdoutLock, Write}, path::Path, process::{Command, Stdio}, thread, @@ -35,6 +35,12 @@ pub enum StateFileStatus { NotRead, } +enum AllExercisesCheck { + Pending(usize), + AllDone, + CheckedUntil(usize), +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -340,6 +346,58 @@ impl AppState { } } + // Return the exercise index of the first pending exercise found. + fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result> { + stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + let n_exercises = self.exercises.len(); + + let status = thread::scope(|s| { + let handles = self + .exercises + .iter() + .map(|exercise| s.spawn(|| exercise.run_exercise(None, &self.cmd_runner))) + .collect::>(); + + for (exercise_ind, handle) in handles.into_iter().enumerate() { + write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; + stdout.flush()?; + + let Ok(success) = handle.join().unwrap() else { + return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); + }; + + if !success { + return Ok(AllExercisesCheck::Pending(exercise_ind)); + } + } + + Ok::<_, io::Error>(AllExercisesCheck::AllDone) + })?; + + let mut exercise_ind = match status { + AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)), + AllExercisesCheck::AllDone => return Ok(None), + AllExercisesCheck::CheckedUntil(ind) => ind, + }; + + // We got an error while checking all exercises in parallel. + // This could be because we exceeded the limit of open file descriptors. + // Therefore, try to continue the check sequentially. + for exercise in &self.exercises[exercise_ind..] { + write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; + stdout.flush()?; + + let success = exercise.run_exercise(None, &self.cmd_runner)?; + if !success { + return Ok(Some(exercise_ind)); + } + + exercise_ind += 1; + } + + Ok(None) + } + /// Mark the current exercise as done and move on to the next pending exercise if one exists. /// If all exercises are marked as done, run all of them to make sure that they are actually /// done. If an exercise which is marked as done fails, mark it as pending and continue on it. @@ -355,44 +413,13 @@ impl AppState { return Ok(ExercisesProgress::NewPending); } - stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? { + stdout.write_all(b"\n\n")?; - let n_exercises = self.exercises.len(); - - let pending_exercise_ind = thread::scope(|s| { - let handles = self - .exercises - .iter_mut() - .map(|exercise| { - s.spawn(|| { - let success = exercise.run_exercise(None, &self.cmd_runner)?; - exercise.done = success; - Ok::<_, Error>(success) - }) - }) - .collect::>(); - - for (exercise_ind, handle) in handles.into_iter().enumerate() { - write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; - stdout.flush()?; - - let success = handle.join().unwrap()?; - if !success { - stdout.write_all(b"\n\n")?; - return Ok(Some(exercise_ind)); - } - } - - Ok::<_, Error>(None) - })?; - - if let Some(pending_exercise_ind) = pending_exercise_ind { self.current_exercise_ind = pending_exercise_ind; - self.n_done = self - .exercises - .iter() - .filter(|exercise| exercise.done) - .count() as u16; + self.exercises[pending_exercise_ind].done = false; + // All exercises were marked as done. + self.n_done -= 1; self.write()?; return Ok(ExercisesProgress::NewPending); } From 789492d1a9bb686e593b08dd8c4ca3af26652bee Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 00:32:58 +0200 Subject: [PATCH 121/211] The number of exercises can't be zero, but still --- src/app_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app_state.rs b/src/app_state.rs index cc77711a..2a205682 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -301,7 +301,7 @@ impl AppState { // Return the index of the next pending exercise or `None` if all exercises are done. fn next_pending_exercise_ind(&self) -> Option { - if self.current_exercise_ind == self.exercises.len() - 1 { + if self.current_exercise_ind + 1 == self.exercises.len() { // The last exercise is done. // Search for exercises not done from the start. return self.exercises[..self.current_exercise_ind] From fc1f9f012431b129dea850443b6b3b8a760a45e1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 01:56:45 +0200 Subject: [PATCH 122/211] Optimize reading and writing the state file --- src/app_state.rs | 138 ++++++++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 62 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 2a205682..ef2f8741 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,8 +1,8 @@ use anyhow::{bail, Context, Result}; use std::{ env, - fs::{self, File}, - io::{self, Read, StdoutLock, Write}, + fs::{File, OpenOptions}, + io::{self, Read, Seek, StdoutLock, Write}, path::Path, process::{Command, Stdio}, thread, @@ -18,7 +18,6 @@ use crate::{ }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; -const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; #[must_use] pub enum ExercisesProgress { @@ -47,6 +46,7 @@ pub struct AppState { // Caches the number of done exercises to avoid iterating over all exercises every time. n_done: u16, final_message: String, + state_file: File, // Preallocated buffer for reading and writing the state file. file_buf: Vec, official_exercises: bool, @@ -56,59 +56,22 @@ pub struct AppState { } impl AppState { - // Update the app state from the state file. - fn update_from_file(&mut self) -> StateFileStatus { - self.file_buf.clear(); - self.n_done = 0; - - if File::open(STATE_FILE_NAME) - .and_then(|mut file| file.read_to_end(&mut self.file_buf)) - .is_err() - { - return StateFileStatus::NotRead; - } - - // See `Self::write` for more information about the file format. - let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2); - - let Some(current_exercise_name) = lines.next() else { - return StateFileStatus::NotRead; - }; - - if current_exercise_name.is_empty() || lines.next().is_none() { - return StateFileStatus::NotRead; - } - - let mut done_exercises = hash_set_with_capacity(self.exercises.len()); - - for done_exerise_name in lines { - if done_exerise_name.is_empty() { - break; - } - done_exercises.insert(done_exerise_name); - } - - for (ind, exercise) in self.exercises.iter_mut().enumerate() { - if done_exercises.contains(exercise.name.as_bytes()) { - exercise.done = true; - self.n_done += 1; - } - - if exercise.name.as_bytes() == current_exercise_name { - self.current_exercise_ind = ind; - } - } - - StateFileStatus::Read - } - pub fn new( exercise_infos: Vec, final_message: String, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; + let mut state_file = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(false) + .open(STATE_FILE_NAME) + .with_context(|| { + format!("Failed to open or create the state file {STATE_FILE_NAME}") + })?; - let exercises = exercise_infos + let mut exercises = exercise_infos .into_iter() .map(|exercise_info| { // Leaking to be able to borrow in the watch mode `Table`. @@ -126,25 +89,69 @@ impl AppState { test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, hint, - // Updated in `Self::update_from_file`. + // Updated below. done: false, } }) .collect::>(); - let mut slf = Self { - current_exercise_ind: 0, + let mut current_exercise_ind = 0; + let mut n_done = 0; + let mut file_buf = Vec::with_capacity(2048); + let state_file_status = 'block: { + if state_file.read_to_end(&mut file_buf).is_err() { + break 'block StateFileStatus::NotRead; + } + + // See `Self::write` for more information about the file format. + let mut lines = file_buf.split(|c| *c == b'\n').skip(2); + + let Some(current_exercise_name) = lines.next() else { + break 'block StateFileStatus::NotRead; + }; + + if current_exercise_name.is_empty() || lines.next().is_none() { + break 'block StateFileStatus::NotRead; + } + + let mut done_exercises = hash_set_with_capacity(exercises.len()); + + for done_exerise_name in lines { + if done_exerise_name.is_empty() { + break; + } + done_exercises.insert(done_exerise_name); + } + + for (ind, exercise) in exercises.iter_mut().enumerate() { + if done_exercises.contains(exercise.name.as_bytes()) { + exercise.done = true; + n_done += 1; + } + + if exercise.name.as_bytes() == current_exercise_name { + current_exercise_ind = ind; + } + } + + StateFileStatus::Read + }; + + file_buf.clear(); + file_buf.extend_from_slice(STATE_FILE_HEADER); + + let slf = Self { + current_exercise_ind, exercises, - n_done: 0, + n_done, final_message, - file_buf: Vec::with_capacity(2048), + state_file, + file_buf, official_exercises: !Path::new("info.toml").exists(), cmd_runner, vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"), }; - let state_file_status = slf.update_from_file(); - Ok((slf, state_file_status)) } @@ -187,10 +194,8 @@ impl AppState { // - The fourth line is an empty line. // - All remaining lines are the names of done exercises. fn write(&mut self) -> Result<()> { - self.file_buf.clear(); + self.file_buf.truncate(STATE_FILE_HEADER.len()); - self.file_buf - .extend_from_slice(b"DON'T EDIT THIS FILE!\n\n"); self.file_buf .extend_from_slice(self.current_exercise().name.as_bytes()); self.file_buf.push(b'\n'); @@ -202,7 +207,14 @@ impl AppState { } } - fs::write(STATE_FILE_NAME, &self.file_buf) + self.state_file + .rewind() + .with_context(|| format!("Failed to rewind the state file {STATE_FILE_NAME}"))?; + self.state_file + .set_len(0) + .with_context(|| format!("Failed to truncate the state file {STATE_FILE_NAME}"))?; + self.state_file + .write_all(&self.file_buf) .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?; Ok(()) @@ -440,11 +452,12 @@ impl AppState { } } +const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; +const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. "; - const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | +-------------------------- ------------------------+ @@ -490,6 +503,7 @@ mod tests { exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], n_done: 0, final_message: String::new(), + state_file: tempfile::tempfile().unwrap(), file_buf: Vec::new(), official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), From fd2bf9f6f66f9ff680925cf0bea86c14c6da07c9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 01:59:04 +0200 Subject: [PATCH 123/211] Simplify next_pending_exercise_ind --- src/app_state.rs | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index ef2f8741..058352a7 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -313,25 +313,22 @@ impl AppState { // Return the index of the next pending exercise or `None` if all exercises are done. fn next_pending_exercise_ind(&self) -> Option { - if self.current_exercise_ind + 1 == self.exercises.len() { - // The last exercise is done. - // Search for exercises not done from the start. - return self.exercises[..self.current_exercise_ind] - .iter() - .position(|exercise| !exercise.done); - } - - // The done exercise isn't the last one. - // Search for a pending exercise after the current one and then from the start. - match self.exercises[self.current_exercise_ind + 1..] - .iter() - .position(|exercise| !exercise.done) - { - Some(ind) => Some(self.current_exercise_ind + 1 + ind), - None => self.exercises[..self.current_exercise_ind] - .iter() - .position(|exercise| !exercise.done), - } + let next_ind = self.current_exercise_ind + 1; + self.exercises + // If the exercise done isn't the last, search for pending exercises after it. + .get(next_ind..) + .and_then(|later_exercises| { + later_exercises + .iter() + .position(|exercise| !exercise.done) + .map(|ind| next_ind + ind) + }) + // Search from the start. + .or_else(|| { + self.exercises[..self.current_exercise_ind] + .iter() + .position(|exercise| !exercise.done) + }) } /// Official exercises: Dump the solution file form the binary and return its path. From 10eb1a3aeecdb9329323228c5e697ccbe1c57508 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 16:01:41 +0200 Subject: [PATCH 124/211] Fix header padding --- src/list/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 5f0cda37..b030aac8 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -189,7 +189,7 @@ impl<'a> ListState<'a> { // Header let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); writer.write_ascii(b" Current State Name")?; - writer.write_ascii(&self.name_col_padding[2..])?; + writer.write_ascii(&self.name_col_padding[4..])?; writer.write_ascii(b"Path")?; next_ln(stdout)?; From bfa00ffbdc8a5799d7ec504bed1d5d8b90388b4c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 16:40:40 +0200 Subject: [PATCH 125/211] Update deps --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 048a82ba..fdb4e45d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,9 +203,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "filetime" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", @@ -469,9 +469,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" dependencies = [ "bitflags 2.6.0", "errno", From dbbeb7d4ede2c92dc7dd5027a48e7aae19622e26 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 17:06:37 +0200 Subject: [PATCH 126/211] Fix displaying the list message in narrow mode --- src/list/state.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index b030aac8..bc1ac729 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -263,14 +263,17 @@ impl<'a> ListState<'a> { } writer.write_ascii(b" | uit list")?; + next_ln(stdout)?; } else { writer.stdout.queue(SetForegroundColor(Color::Magenta))?; writer.write_str(&self.message)?; stdout.queue(ResetColor)?; next_ln(stdout)?; - } - next_ln(stdout)?; + if self.narrow_term { + next_ln(stdout)?; + } + } } stdout.queue(EndSynchronizedUpdate)?.flush() From ab2eb3442ec21b53be36686b37c7af897a03298f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 17:10:39 +0200 Subject: [PATCH 127/211] Update changelog --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 391d9c6c..19bb8fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ + + +## 6.3.0 (2024-08-29) + +### Added + +- Add the following exercise lints: + - `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings. + - `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust. + - `forbid(todo)`: You forgot a `todo!()`. + - `forbid(empty_loop)`: This can only happen by mistake in Rustlings. + - `deny(infinite_loop)`: No infinite loops are needed in Rustlings. + - `deny(mem_forget)`: You shouldn't leak memory while still learning Rust. +- Show a link to every exercise file in the list. +- Add scroll padding in the list. +- Break the help footer of the list into two lines when the terminal width isn't big enough. +- Enable scrolling with the mouse in the list. +- `dev check`: Show the progress of checks. +- `dev check`: Check that the length of all exercise names is lower than 32. +- `dev check`: Check if exercise contains no tests and isn't marked with `test = false`. + +### Changed + +- The compilation time when installing Rustlings is reduced. +- Pressing `c` in the list for "continue on" now quits the list after setting the selected exercise as the current one. +- Better highlighting of the solution file after an exercise is done. +- Don't show the output of successful tests anymore. Instead, show the pretty output for tests. +- Be explicit about `q` only quitting the list and not the whole program in the list. +- Be explicit about `r` only resetting one exercise (the selected one) in the list. +- Ignore the standard output of `git init`. +- `threads3`: Remove the queue length and improve tests. +- `errors4`: Use match instead of a comparison chain in the solution. +- `functions3`: Only take `u8` to avoid using a too high number of iterations by mistake. +- `dev check`: Always check with strict Clippy (warnings to errors) when checking the solutions. + +### Fixed + +- Fix the error on some systems about too many open files during the final check of all exercises. +- Fix the list when the terminal height is too low. +- Restore the terminal after an error in the list. + ## 6.2.0 (2024-08-09) From c8d1d9c51fa218ad7e1afccab95c3a05ba0ba7cb Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 29 Aug 2024 17:20:17 +0200 Subject: [PATCH 128/211] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdb4e45d..e1048d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,7 +482,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.2.0" +version = "6.3.0" dependencies = [ "ahash", "anyhow", @@ -499,7 +499,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.2.0" +version = "6.3.0" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index b332f40b..d22aa60c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ exclude = [ ] [workspace.package] -version = "6.2.0" +version = "6.3.0" authors = [ "Mo Bitar ", # https://github.com/mo8it "Liv ", # https://github.com/shadows-withal @@ -52,7 +52,7 @@ clap = { version = "4.5.16", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" -rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" } +rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } serde_json = "1.0.127" serde.workspace = true toml_edit.workspace = true From 7d4100ed8a1fb725a2592465f18147828fa9ff75 Mon Sep 17 00:00:00 2001 From: William Ugalde Gamboa Date: Fri, 30 Aug 2024 20:27:26 -0600 Subject: [PATCH 129/211] Fix example in 'primitive_types3' 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 504bfd94..fd4d9405 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -253,7 +253,7 @@ require you to type in 100 items (but you certainly can if you want!). For example, you can do: ``` -let array = ["Are we there yet?"; 10]; +let array = ["Are we there yet?"; 100]; ``` Bonus: what are some other things you could have that would return `true` From ac62a3713c7a86172e5ff7d1e5b37f3960aecc35 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 1 Sep 2024 20:31:09 +0200 Subject: [PATCH 130/211] Fix typo --- src/app_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app_state.rs b/src/app_state.rs index 058352a7..381aaf8a 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -331,7 +331,7 @@ impl AppState { }) } - /// Official exercises: Dump the solution file form the binary and return its path. + /// Official exercises: Dump the solution file from the binary and return its path. /// Third-party exercises: Check if a solution file exists and return its path in that case. pub fn current_solution_path(&self) -> Result> { if cfg!(debug_assertions) { From 75a38fa38b65c075f34233f4745eb6d1d7405a39 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 1 Sep 2024 20:44:19 +0200 Subject: [PATCH 131/211] Add search to the help footer --- src/list/state.rs | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index bc1ac729..49b6d5d8 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -44,7 +44,6 @@ pub struct ListState<'a> { term_width: u16, term_height: u16, separator_line: Vec, - narrow_term: bool, show_footer: bool, } @@ -78,7 +77,6 @@ impl<'a> ListState<'a> { term_width: 0, term_height: 0, separator_line: Vec::new(), - narrow_term: false, show_footer: true, }; @@ -96,13 +94,9 @@ impl<'a> ListState<'a> { return; } - let wide_help_footer_width = 95; - // The help footer is shorter when nothing is selected. - self.narrow_term = width < wide_help_footer_width && self.scroll_state.selected().is_some(); - let header_height = 1; - // 2 separator, 1 progress bar, 1-2 footer message. - let footer_height = 4 + u16::from(self.narrow_term); + // 2 separators, 1 progress bar, 2 footer message lines. + let footer_height = 5; self.show_footer = height > header_height + footer_height; if self.show_footer { @@ -227,14 +221,10 @@ impl<'a> ListState<'a> { // Help footer message if self.scroll_state.selected().is_some() { writer.write_str("↓/j ↑/k home/g end/G | ontinue at | eset exercise")?; - if self.narrow_term { - next_ln(stdout)?; - writer = MaxLenWriter::new(stdout, self.term_width as usize); + next_ln(stdout)?; + writer = MaxLenWriter::new(stdout, self.term_width as usize); - writer.write_ascii(b"filter ")?; - } else { - writer.write_ascii(b" | filter ")?; - } + writer.write_ascii(b"earch | filter ")?; } else { // Nothing selected (and nothing shown), so only display filter and quit. writer.write_ascii(b"filter ")?; @@ -263,17 +253,14 @@ impl<'a> ListState<'a> { } writer.write_ascii(b" | uit list")?; - next_ln(stdout)?; } else { writer.stdout.queue(SetForegroundColor(Color::Magenta))?; writer.write_str(&self.message)?; stdout.queue(ResetColor)?; next_ln(stdout)?; - - if self.narrow_term { - next_ln(stdout)?; - } } + + next_ln(stdout)?; } stdout.queue(EndSynchronizedUpdate)?.flush() From f82e47f2afad97d47173a8c4ee60b033df9e12f4 Mon Sep 17 00:00:00 2001 From: Jesse Jackson Date: Sun, 1 Sep 2024 14:48:28 -0500 Subject: [PATCH 132/211] style: reduce pre-formatted message line lengths to 80 columns --- rustlings-macros/info.toml | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index fd4d9405..0fe83439 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1,6 +1,7 @@ format_version = 1 -welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners! +welcome_message = """ +Is this your first time? Don't worry, Rustlings is made for beginners! We are going to teach you a lot of things about Rust, but before we can get started, here are some notes about how Rustlings operates: @@ -10,15 +11,16 @@ get started, here are some notes about how Rustlings operates: and fix them! 2. Make sure to have your editor open in the `rustlings/` directory. Rustlings will show you the path of the current exercise under the progress bar. Open - the exercise file in your editor, fix errors and save the file. Rustlings will - automatically detect the file change and rerun the exercise. If all errors are - fixed, Rustlings will ask you to move on to the next exercise. + the exercise file in your editor, fix errors and save the file. Rustlings + will automatically detect the file change and rerun the exercise. If all + errors are fixed, Rustlings will ask you to move on to the next exercise. 3. If you're stuck on an exercise, enter `h` to show a hint. -4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! - (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, - other learners do too so you can help each other out!""" +4. If an exercise doesn't make sense to you, feel free to open an issue on + GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and + sometimes, other learners do too so you can help each other out!""" -final_message = """We hope you enjoyed learning about the various aspects of Rust! +final_message = """ +We hope you enjoyed learning about the various aspects of Rust! If you noticed any issues, don't hesitate to report them on Github. You can also contribute your own exercises to help the greater community! @@ -122,8 +124,8 @@ hint = """ We know about variables and mutability, but there is another important type of variables available: constants. -Constants are always immutable. They are declared with the keyword `const` instead -of `let`. +Constants are always immutable. They are declared with the keyword `const` +instead of `let`. The type of Constants must always be annotated. @@ -319,7 +321,8 @@ hint = """ In the first function, we create an empty vector and want to push new elements to it. -In the second function, we map the values of the input and collect them into a vector. +In the second function, we map the values of the input and collect them into +a vector. After you've completed both functions, decide for yourself which approach you like better. @@ -332,8 +335,8 @@ What do you think is the more commonly used pattern under Rust developers?""" name = "move_semantics1" dir = "06_move_semantics" hint = """ -So you've got the "cannot borrow `vec` as mutable, as it is not declared as mutable" -error on the line where we push an element to the vector, right? +So you've got the "cannot borrow `vec` as mutable, as it is not declared as +mutable" error on the line where we push an element to the vector, right? The fix for this is going to be adding one keyword, and the addition is NOT on the line where we push to the vector (where the error is). @@ -369,7 +372,8 @@ hint = """ Carefully reason about the range in which each mutable reference is in scope. Does it help to update the value of `x` immediately after the mutable reference is taken? -Read more about 'Mutable References' in the book's section 'References and Borrowing': +Read more about 'Mutable References' in the book's section 'References and +Borrowing': https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references.""" [[exercises]] @@ -508,7 +512,8 @@ name = "strings4" dir = "09_strings" test = false hint = """ -Replace `placeholder` with either `string` or `string_slice` in the `main` function. +Replace `placeholder` with either `string` or `string_slice` in the `main` +function. Example: `placeholder("blue");` @@ -1200,7 +1205,8 @@ hint = """ Is there an implementation of `TryFrom` in the standard library that can both do the required integer conversion and check the range of the input? -Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" +Challenge: Can you make the `TryFrom` implementations generic over many integer +types?""" [[exercises]] name = "as_ref_mut" From 86fc573d7a538539ea32fd84a1cd30c5533cacca Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 1 Sep 2024 22:02:07 +0200 Subject: [PATCH 133/211] Remove the footer separators --- src/list/state.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 49b6d5d8..7a2d3bf0 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -43,7 +43,6 @@ pub struct ListState<'a> { filter: Filter, term_width: u16, term_height: u16, - separator_line: Vec, show_footer: bool, } @@ -76,7 +75,6 @@ impl<'a> ListState<'a> { // Set by `set_term_size` term_width: 0, term_height: 0, - separator_line: Vec::new(), show_footer: true, }; @@ -95,14 +93,10 @@ impl<'a> ListState<'a> { } let header_height = 1; - // 2 separators, 1 progress bar, 2 footer message lines. - let footer_height = 5; + // 1 progress bar, 2 footer message lines. + let footer_height = 3; self.show_footer = height > header_height + footer_height; - if self.show_footer { - self.separator_line = "─".as_bytes().repeat(width as usize); - } - self.scroll_state.set_max_n_rows_to_display( height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height) as usize, @@ -202,9 +196,6 @@ impl<'a> ListState<'a> { } if self.show_footer { - stdout.write_all(&self.separator_line)?; - next_ln(stdout)?; - progress_bar( &mut MaxLenWriter::new(stdout, self.term_width as usize), self.app_state.n_done(), @@ -213,9 +204,6 @@ impl<'a> ListState<'a> { )?; next_ln(stdout)?; - stdout.write_all(&self.separator_line)?; - next_ln(stdout)?; - let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { // Help footer message From a8b13f5a821d40f11e206922d990aa7b2e66801f Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 1 Sep 2024 22:04:09 +0200 Subject: [PATCH 134/211] Remove "exercises" from the end of the progress bar --- src/term.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/term.rs b/src/term.rs index ee8dbf86..489d6585 100644 --- a/src/term.rs +++ b/src/term.rs @@ -5,7 +5,7 @@ use std::{ use crossterm::{ cursor::MoveTo, - style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, + style::{Attribute, Color, SetAttribute, SetForegroundColor}, terminal::{Clear, ClearType}, Command, QueueableCommand, }; @@ -93,20 +93,19 @@ pub fn progress_bar<'a>( total: u16, line_width: u16, ) -> io::Result<()> { + debug_assert!(total < 1000); debug_assert!(progress <= total); const PREFIX: &[u8] = b"Progress: ["; const PREFIX_WIDTH: u16 = PREFIX.len() as u16; - // Leaving the last char empty (_) for `total` > 99. - const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16; + const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16; const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; if line_width < MIN_LINE_WIDTH { writer.write_ascii(b"Progress: ")?; // Integers are in ASCII. - writer.write_ascii(format!("{progress}/{total}").as_bytes())?; - return writer.write_ascii(b" exercises"); + return writer.write_ascii(format!("{progress}/{total}").as_bytes()); } let stdout = writer.stdout(); @@ -133,8 +132,9 @@ pub fn progress_bar<'a>( } } - stdout.queue(ResetColor)?; - write!(stdout, "] {progress:>3}/{total} exercises") + stdout.queue(SetForegroundColor(Color::Reset))?; + + write!(stdout, "] {progress:>3}/{total}") } pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { From c4fd29541b049f38a9a898974b0b098f6bffe777 Mon Sep 17 00:00:00 2001 From: Adhyan Date: Sun, 1 Sep 2024 18:52:26 -0600 Subject: [PATCH 135/211] added a way to search through list, ref #2093 --- src/exercise.rs | 1 + src/list.rs | 55 +++++++++++++++++++++++++++++++++++++--- src/list/scroll_state.rs | 2 +- src/list/state.rs | 37 +++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 11eea638..68068076 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -55,6 +55,7 @@ fn run_bin( } /// See `info_file::ExerciseInfo` +#[derive(Debug)] pub struct Exercise { pub dir: Option<&'static str>, pub name: &'static str, diff --git a/src/list.rs b/src/list.rs index 481fb2f4..8364da7d 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{Context, Ok, Result}; use crossterm::{ cursor, event::{ @@ -21,6 +21,7 @@ mod state; fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> { let mut list_state = ListState::new(app_state, stdout)?; + let mut is_searching = false; loop { match event::read().context("Failed to read terminal event")? { @@ -31,9 +32,50 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } list_state.message.clear(); + + let curr_key = key.code; + + if is_searching { + match curr_key { + KeyCode::Esc | KeyCode::Enter => { + is_searching = false; // not sure why rust analyzer thinks this is unused + list_state.search_query.clear(); + return Ok(()); + } + KeyCode::Char(k) => { + eprintln!("pressed while searching {:?}", curr_key); + + list_state.search_query.push(k); + list_state.message.push_str("search:"); + list_state.message.push_str(&list_state.search_query); + list_state.message.push_str("|"); + + list_state.select_if_matches_search_query(); + + list_state.draw(stdout)?; + continue; + } + KeyCode::Backspace => { + list_state.search_query.pop(); + list_state.message.push_str("search:"); + list_state.message.push_str(&list_state.search_query); + list_state.message.push_str("|"); + + list_state.select_if_matches_search_query(); + + list_state.draw(stdout)?; + continue; + } + _ => { + continue; + } + } + } match key.code { - KeyCode::Char('q') => return Ok(()), + KeyCode::Char('q') => { + return Ok(()); + } KeyCode::Down | KeyCode::Char('j') => list_state.select_next(), KeyCode::Up | KeyCode::Char('k') => list_state.select_previous(), KeyCode::Home | KeyCode::Char('g') => list_state.select_first(), @@ -66,9 +108,16 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> return Ok(()); } } + KeyCode::Char('s') | KeyCode::Char('/') => { + eprintln!("starting search"); + list_state.message.push_str("search:|"); + is_searching = true; + } // Redraw to remove the message. KeyCode::Esc => (), - _ => continue, + _ => { + continue; + } } } Event::Mouse(event) => match event.kind { diff --git a/src/list/scroll_state.rs b/src/list/scroll_state.rs index 25a73736..2c02ed4f 100644 --- a/src/list/scroll_state.rs +++ b/src/list/scroll_state.rs @@ -46,7 +46,7 @@ impl ScrollState { self.selected } - fn set_selected(&mut self, selected: usize) { + pub fn set_selected(&mut self, selected: usize) { self.selected = Some(selected); self.update_offset(); } diff --git a/src/list/state.rs b/src/list/state.rs index 49b6d5d8..117740cf 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -45,6 +45,7 @@ pub struct ListState<'a> { term_height: u16, separator_line: Vec, show_footer: bool, + pub search_query: String, } impl<'a> ListState<'a> { @@ -78,6 +79,7 @@ impl<'a> ListState<'a> { term_height: 0, separator_line: Vec::new(), show_footer: true, + search_query: String::new(), }; slf.set_term_size(width, height); @@ -356,6 +358,41 @@ impl<'a> ListState<'a> { Ok(()) } + + pub fn select_if_matches_search_query(&mut self) { + eprintln!("search query: {:?}", self.search_query); + + let idx = self + .app_state + .exercises() + .iter() + .enumerate() + .find_map(|(i, s)| { + if s.name.contains(self.search_query.as_str()) { + Some(i) + } else { + None + } + }); + eprintln!("idx: {:?}", idx); + + match idx { + Some(i) => { + // ? do we need this function call? + // let exercise_ind = self.selected_to_exercise_ind(i).unwrap(); + let exercise_ind = i; + self.scroll_state.set_selected(exercise_ind); + eprintln!("exercise_ind: {:?}", exercise_ind); + self.update_rows(); + } + None => { + let msg = String::from("[NOT FOUND]") + &self.message.clone(); + self.message.clear(); + self.message.push_str(&msg); + } + } + + } // Return `true` if there was something to select. pub fn selected_to_current_exercise(&mut self) -> Result { From 388f8da97f2ff47011d1bebbf0d153ea85741562 Mon Sep 17 00:00:00 2001 From: Adhyan Date: Sun, 1 Sep 2024 19:03:33 -0600 Subject: [PATCH 136/211] removed debug statements --- src/exercise.rs | 1 - src/list.rs | 7 +------ src/list/state.rs | 4 ---- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 68068076..11eea638 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -55,7 +55,6 @@ fn run_bin( } /// See `info_file::ExerciseInfo` -#[derive(Debug)] pub struct Exercise { pub dir: Option<&'static str>, pub name: &'static str, diff --git a/src/list.rs b/src/list.rs index 8364da7d..351b0b5a 100644 --- a/src/list.rs +++ b/src/list.rs @@ -43,8 +43,6 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> return Ok(()); } KeyCode::Char(k) => { - eprintln!("pressed while searching {:?}", curr_key); - list_state.search_query.push(k); list_state.message.push_str("search:"); list_state.message.push_str(&list_state.search_query); @@ -109,15 +107,12 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } } KeyCode::Char('s') | KeyCode::Char('/') => { - eprintln!("starting search"); list_state.message.push_str("search:|"); is_searching = true; } // Redraw to remove the message. KeyCode::Esc => (), - _ => { - continue; - } + _ => continue, } } Event::Mouse(event) => match event.kind { diff --git a/src/list/state.rs b/src/list/state.rs index 53fed9a0..7dfd6b5a 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -348,8 +348,6 @@ impl<'a> ListState<'a> { } pub fn select_if_matches_search_query(&mut self) { - eprintln!("search query: {:?}", self.search_query); - let idx = self .app_state .exercises() @@ -362,7 +360,6 @@ impl<'a> ListState<'a> { None } }); - eprintln!("idx: {:?}", idx); match idx { Some(i) => { @@ -370,7 +367,6 @@ impl<'a> ListState<'a> { // let exercise_ind = self.selected_to_exercise_ind(i).unwrap(); let exercise_ind = i; self.scroll_state.set_selected(exercise_ind); - eprintln!("exercise_ind: {:?}", exercise_ind); self.update_rows(); } None => { From 92a1214dcdbead48f520cb4f1d8e53f59b5619e1 Mon Sep 17 00:00:00 2001 From: Adhyan Date: Sun, 1 Sep 2024 19:05:23 -0600 Subject: [PATCH 137/211] passes clippy lints --- src/list.rs | 12 ++++++------ src/list/state.rs | 5 ++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/list.rs b/src/list.rs index 351b0b5a..0f0643c6 100644 --- a/src/list.rs +++ b/src/list.rs @@ -32,9 +32,9 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } list_state.message.clear(); - + let curr_key = key.code; - + if is_searching { match curr_key { KeyCode::Esc | KeyCode::Enter => { @@ -46,10 +46,10 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> list_state.search_query.push(k); list_state.message.push_str("search:"); list_state.message.push_str(&list_state.search_query); - list_state.message.push_str("|"); - + list_state.message.push('|'); + list_state.select_if_matches_search_query(); - + list_state.draw(stdout)?; continue; } @@ -57,7 +57,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> list_state.search_query.pop(); list_state.message.push_str("search:"); list_state.message.push_str(&list_state.search_query); - list_state.message.push_str("|"); + list_state.message.push('|'); list_state.select_if_matches_search_query(); diff --git a/src/list/state.rs b/src/list/state.rs index 7dfd6b5a..be05f3b7 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -346,7 +346,7 @@ impl<'a> ListState<'a> { Ok(()) } - + pub fn select_if_matches_search_query(&mut self) { let idx = self .app_state @@ -360,7 +360,7 @@ impl<'a> ListState<'a> { None } }); - + match idx { Some(i) => { // ? do we need this function call? @@ -375,7 +375,6 @@ impl<'a> ListState<'a> { self.message.push_str(&msg); } } - } // Return `true` if there was something to select. From 547a9d947bf62052c263a6ee935b5451d3c0dbf1 Mon Sep 17 00:00:00 2001 From: Adhyan Date: Mon, 2 Sep 2024 10:45:45 -0600 Subject: [PATCH 138/211] escape/enter no longer exits the list, exits only the search --- src/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list.rs b/src/list.rs index 0f0643c6..b41c8913 100644 --- a/src/list.rs +++ b/src/list.rs @@ -40,7 +40,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> KeyCode::Esc | KeyCode::Enter => { is_searching = false; // not sure why rust analyzer thinks this is unused list_state.search_query.clear(); - return Ok(()); + continue; } KeyCode::Char(k) => { list_state.search_query.push(k); From abf1228a0a837e71d744f5f2881f386387802cc7 Mon Sep 17 00:00:00 2001 From: Adhyan Date: Mon, 2 Sep 2024 10:59:23 -0600 Subject: [PATCH 139/211] search now filters the list first --- src/list.rs | 2 +- src/list/state.rs | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/list.rs b/src/list.rs index b41c8913..069cdda0 100644 --- a/src/list.rs +++ b/src/list.rs @@ -38,7 +38,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> if is_searching { match curr_key { KeyCode::Esc | KeyCode::Enter => { - is_searching = false; // not sure why rust analyzer thinks this is unused + is_searching = false; list_state.search_query.clear(); continue; } diff --git a/src/list/state.rs b/src/list/state.rs index be05f3b7..8d5bf5b8 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -352,6 +352,27 @@ impl<'a> ListState<'a> { .app_state .exercises() .iter() + .filter_map(|exercise| { + match self.filter() { + Filter::None => { + Some(exercise) + }, + Filter::Done => { + if exercise.done { + Some(exercise) + } else { + None + } + }, + Filter::Pending => { + if !exercise.done { + Some(exercise) + } else { + None + } + } + } + }) .enumerate() .find_map(|(i, s)| { if s.name.contains(self.search_query.as_str()) { @@ -363,8 +384,6 @@ impl<'a> ListState<'a> { match idx { Some(i) => { - // ? do we need this function call? - // let exercise_ind = self.selected_to_exercise_ind(i).unwrap(); let exercise_ind = i; self.scroll_state.set_selected(exercise_ind); self.update_rows(); From 71494264ca7303071cfeafbdbf137e11e653190f Mon Sep 17 00:00:00 2001 From: Adhyan Date: Mon, 2 Sep 2024 11:02:17 -0600 Subject: [PATCH 140/211] fixed clippy lints --- src/list/state.rs | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 8d5bf5b8..9e813a0f 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -352,26 +352,10 @@ impl<'a> ListState<'a> { .app_state .exercises() .iter() - .filter_map(|exercise| { - match self.filter() { - Filter::None => { - Some(exercise) - }, - Filter::Done => { - if exercise.done { - Some(exercise) - } else { - None - } - }, - Filter::Pending => { - if !exercise.done { - Some(exercise) - } else { - None - } - } - } + .filter(|exercise| match self.filter() { + Filter::None => true, + Filter::Done => exercise.done, + Filter::Pending => !exercise.done, }) .enumerate() .find_map(|(i, s)| { From 948e16e3c783bff20736b356d9f961af3bb00784 Mon Sep 17 00:00:00 2001 From: Adhyan Date: Tue, 3 Sep 2024 14:40:24 -0600 Subject: [PATCH 141/211] moved continue to end of if-block --- src/list.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/list.rs b/src/list.rs index 069cdda0..dc224f00 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Ok, Result}; +use anyhow::{Context, Result}; use crossterm::{ cursor, event::{ @@ -40,7 +40,6 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> KeyCode::Esc | KeyCode::Enter => { is_searching = false; list_state.search_query.clear(); - continue; } KeyCode::Char(k) => { list_state.search_query.push(k); @@ -51,7 +50,6 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> list_state.select_if_matches_search_query(); list_state.draw(stdout)?; - continue; } KeyCode::Backspace => { list_state.search_query.pop(); @@ -62,12 +60,10 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> list_state.select_if_matches_search_query(); list_state.draw(stdout)?; - continue; - } - _ => { - continue; } + _ => {} } + continue; } match key.code { From fea917c8f2a8e9464ab5625f29f06fd622a26a04 Mon Sep 17 00:00:00 2001 From: Adhyan Date: Tue, 3 Sep 2024 14:52:09 -0600 Subject: [PATCH 142/211] removed unnecessary update_rows() call and minor refactoring --- src/list.rs | 6 ++---- src/list/state.rs | 14 ++++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/list.rs b/src/list.rs index dc224f00..857d0ce9 100644 --- a/src/list.rs +++ b/src/list.rs @@ -67,9 +67,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } match key.code { - KeyCode::Char('q') => { - return Ok(()); - } + KeyCode::Char('q') => return Ok(()), KeyCode::Down | KeyCode::Char('j') => list_state.select_next(), KeyCode::Up | KeyCode::Char('k') => list_state.select_previous(), KeyCode::Home | KeyCode::Char('g') => list_state.select_first(), @@ -102,7 +100,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> return Ok(()); } } - KeyCode::Char('s') | KeyCode::Char('/') => { + KeyCode::Char('s' | '/') => { list_state.message.push_str("search:|"); is_searching = true; } diff --git a/src/list/state.rs b/src/list/state.rs index 8d5bf5b8..f1507667 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -374,23 +374,21 @@ impl<'a> ListState<'a> { } }) .enumerate() - .find_map(|(i, s)| { - if s.name.contains(self.search_query.as_str()) { - Some(i) + .find_map(|(idx, exercise)| { + if exercise.name.contains(self.search_query.as_str()) { + Some(idx) } else { None } }); match idx { - Some(i) => { - let exercise_ind = i; + Some(x) => { + let exercise_ind = x; self.scroll_state.set_selected(exercise_ind); - self.update_rows(); } None => { - let msg = String::from("[NOT FOUND]") + &self.message.clone(); - self.message.clear(); + let msg = String::from(" (not found)"); self.message.push_str(&msg); } } From 47148e78a32dc39001ec69642eeb2d87f4b4e1ad Mon Sep 17 00:00:00 2001 From: Adhyan Date: Tue, 3 Sep 2024 15:03:25 -0600 Subject: [PATCH 143/211] replaced enumerate() with position(); converted select_if_matches_search_query to apply_search_query --- src/list.rs | 14 ++--------- src/list/state.rs | 62 ++++++++++++++++++++++------------------------- 2 files changed, 31 insertions(+), 45 deletions(-) diff --git a/src/list.rs b/src/list.rs index 857d0ce9..5d7c8dd9 100644 --- a/src/list.rs +++ b/src/list.rs @@ -43,22 +43,12 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } KeyCode::Char(k) => { list_state.search_query.push(k); - list_state.message.push_str("search:"); - list_state.message.push_str(&list_state.search_query); - list_state.message.push('|'); - - list_state.select_if_matches_search_query(); - + list_state.apply_search_query(); list_state.draw(stdout)?; } KeyCode::Backspace => { list_state.search_query.pop(); - list_state.message.push_str("search:"); - list_state.message.push_str(&list_state.search_query); - list_state.message.push('|'); - - list_state.select_if_matches_search_query(); - + list_state.apply_search_query(); list_state.draw(stdout)?; } _ => {} diff --git a/src/list/state.rs b/src/list/state.rs index f1507667..be6c42e5 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -347,40 +347,36 @@ impl<'a> ListState<'a> { Ok(()) } - pub fn select_if_matches_search_query(&mut self) { + pub fn apply_search_query(&mut self) { + self.message.push_str("search:"); + self.message.push_str(&self.search_query); + self.message.push('|'); + + if self.search_query.is_empty() { return; } + let idx = self - .app_state - .exercises() - .iter() - .filter_map(|exercise| { - match self.filter() { - Filter::None => { - Some(exercise) - }, - Filter::Done => { - if exercise.done { - Some(exercise) - } else { - None - } - }, - Filter::Pending => { - if !exercise.done { - Some(exercise) - } else { - None - } - } - } - }) - .enumerate() - .find_map(|(idx, exercise)| { - if exercise.name.contains(self.search_query.as_str()) { - Some(idx) - } else { - None - } - }); + .app_state + .exercises() + .iter() + .filter_map(|exercise| { + match self.filter() { + Filter::None => Some(exercise), + Filter::Done if exercise.done => Some(exercise), + Filter::Pending if !exercise.done => Some(exercise), + _ => None, + } + }) + .position(|exercise| exercise.name.contains(self.search_query.as_str())); + + match idx { + Some(exercise_ind) => { + self.scroll_state.set_selected(exercise_ind); + } + None => { + let msg = String::from(" (not found)"); + self.message.push_str(&msg); + } + } match idx { Some(x) => { From f463cf86627411696922bd703e8c875eec7b367b Mon Sep 17 00:00:00 2001 From: Adhyan Date: Tue, 3 Sep 2024 15:10:44 -0600 Subject: [PATCH 144/211] passes clippy lints and removed extra code from the merge --- src/list/state.rs | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index be6c42e5..60077c78 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -352,21 +352,20 @@ impl<'a> ListState<'a> { self.message.push_str(&self.search_query); self.message.push('|'); - if self.search_query.is_empty() { return; } + if self.search_query.is_empty() { + return; + } let idx = self - .app_state - .exercises() - .iter() - .filter_map(|exercise| { - match self.filter() { - Filter::None => Some(exercise), - Filter::Done if exercise.done => Some(exercise), - Filter::Pending if !exercise.done => Some(exercise), - _ => None, - } - }) - .position(|exercise| exercise.name.contains(self.search_query.as_str())); + .app_state + .exercises() + .iter() + .filter(|exercise| match self.filter() { + Filter::None => true, + Filter::Done => exercise.done, + Filter::Pending => !exercise.done, + }) + .position(|exercise| exercise.name.contains(self.search_query.as_str())); match idx { Some(exercise_ind) => { @@ -377,17 +376,6 @@ impl<'a> ListState<'a> { self.message.push_str(&msg); } } - - match idx { - Some(x) => { - let exercise_ind = x; - self.scroll_state.set_selected(exercise_ind); - } - None => { - let msg = String::from(" (not found)"); - self.message.push_str(&msg); - } - } } // Return `true` if there was something to select. From da8b3d143a5b7462baf912c58cc768f7cd210ab2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 4 Sep 2024 01:05:30 +0200 Subject: [PATCH 145/211] Final touches to searching --- src/list.rs | 16 +++++++--------- src/list/state.rs | 19 +++++++------------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/list.rs b/src/list.rs index 5d7c8dd9..5e303848 100644 --- a/src/list.rs +++ b/src/list.rs @@ -33,26 +33,24 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> list_state.message.clear(); - let curr_key = key.code; - if is_searching { - match curr_key { + match key.code { KeyCode::Esc | KeyCode::Enter => { is_searching = false; list_state.search_query.clear(); } - KeyCode::Char(k) => { - list_state.search_query.push(k); + KeyCode::Char(c) => { + list_state.search_query.push(c); list_state.apply_search_query(); - list_state.draw(stdout)?; } KeyCode::Backspace => { list_state.search_query.pop(); list_state.apply_search_query(); - list_state.draw(stdout)?; } - _ => {} + _ => continue, } + + list_state.draw(stdout)?; continue; } @@ -91,8 +89,8 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } } KeyCode::Char('s' | '/') => { - list_state.message.push_str("search:|"); is_searching = true; + list_state.apply_search_query(); } // Redraw to remove the message. KeyCode::Esc => (), diff --git a/src/list/state.rs b/src/list/state.rs index 60077c78..f932fab4 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -37,6 +37,7 @@ pub enum Filter { pub struct ListState<'a> { /// Footer message to be displayed if not empty. pub message: String, + pub search_query: String, app_state: &'a mut AppState, scroll_state: ScrollState, name_col_padding: Vec, @@ -44,7 +45,6 @@ pub struct ListState<'a> { term_width: u16, term_height: u16, show_footer: bool, - pub search_query: String, } impl<'a> ListState<'a> { @@ -69,6 +69,7 @@ impl<'a> ListState<'a> { let mut slf = Self { message: String::with_capacity(128), + search_query: String::new(), app_state, scroll_state, name_col_padding, @@ -77,7 +78,6 @@ impl<'a> ListState<'a> { term_width: 0, term_height: 0, show_footer: true, - search_query: String::new(), }; slf.set_term_size(width, height); @@ -356,25 +356,20 @@ impl<'a> ListState<'a> { return; } - let idx = self + let ind = self .app_state .exercises() .iter() - .filter(|exercise| match self.filter() { + .filter(|exercise| match self.filter { Filter::None => true, Filter::Done => exercise.done, Filter::Pending => !exercise.done, }) .position(|exercise| exercise.name.contains(self.search_query.as_str())); - match idx { - Some(exercise_ind) => { - self.scroll_state.set_selected(exercise_ind); - } - None => { - let msg = String::from(" (not found)"); - self.message.push_str(&msg); - } + match ind { + Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind), + None => self.message.push_str(" (not found)"), } } From 03baa471d94f9dfe7575b985e227df1d4e7803d4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 4 Sep 2024 01:07:08 +0200 Subject: [PATCH 146/211] Simplify handling `p` in list --- src/list.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/list.rs b/src/list.rs index 5e303848..cfd3720c 100644 --- a/src/list.rs +++ b/src/list.rs @@ -72,15 +72,15 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } } KeyCode::Char('p') => { - let message = if list_state.filter() == Filter::Pending { + if list_state.filter() == Filter::Pending { list_state.set_filter(Filter::None); - "Disabled filter PENDING" + list_state.message.push_str("Disabled filter PENDING"); } else { list_state.set_filter(Filter::Pending); - "Enabled filter PENDING │ Press p again to disable the filter" - }; - - list_state.message.push_str(message); + list_state.message.push_str( + "Enabled filter PENDING │ Press p again to disable the filter", + ); + } } KeyCode::Char('r') => list_state.reset_selected()?, KeyCode::Char('c') => { From e5ed11528855f6dddc5759df3426ff1296aba87e Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 4 Sep 2024 01:20:48 +0200 Subject: [PATCH 147/211] Match filter once --- 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 f932fab4..468049ab 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -356,16 +356,17 @@ impl<'a> ListState<'a> { return; } - let ind = self - .app_state - .exercises() - .iter() - .filter(|exercise| match self.filter { - Filter::None => true, - Filter::Done => exercise.done, - Filter::Pending => !exercise.done, - }) - .position(|exercise| exercise.name.contains(self.search_query.as_str())); + let is_search_result = |exercise: &Exercise| exercise.name.contains(&self.search_query); + let mut iter = self.app_state.exercises().iter(); + let ind = match self.filter { + Filter::None => iter.position(is_search_result), + Filter::Done => iter + .filter(|exercise| exercise.done) + .position(is_search_result), + Filter::Pending => iter + .filter(|exercise| !exercise.done) + .position(is_search_result), + }; match ind { Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind), From 247bd19f93e11fb037c945ff1dc464a1d1713471 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 4 Sep 2024 02:19:45 +0200 Subject: [PATCH 148/211] Canonicalize exercise paths only once --- src/app_state.rs | 27 ++++++++++++++++++++++++++- src/exercise.rs | 19 +++++++++++++++++-- src/list/state.rs | 4 ++-- src/run.rs | 9 ++++++--- src/term.rs | 41 ++++++++++++++++++++++------------------- src/watch/state.rs | 6 ++++-- 6 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 381aaf8a..7123d11a 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -3,7 +3,7 @@ use std::{ env, fs::{File, OpenOptions}, io::{self, Read, Seek, StdoutLock, Write}, - path::Path, + path::{Path, MAIN_SEPARATOR_STR}, process::{Command, Stdio}, thread, }; @@ -15,6 +15,7 @@ use crate::{ embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, + term, }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; @@ -71,6 +72,7 @@ impl AppState { format!("Failed to open or create the state file {STATE_FILE_NAME}") })?; + let dir_canonical_path = term::canonicalize("exercises"); let mut exercises = exercise_infos .into_iter() .map(|exercise_info| { @@ -82,10 +84,32 @@ impl AppState { let dir = exercise_info.dir.map(|dir| &*dir.leak()); let hint = exercise_info.hint.leak().trim_ascii(); + let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { + let mut canonical_path; + if let Some(dir) = dir { + canonical_path = String::with_capacity( + 2 + dir_canonical_path.len() + dir.len() + name.len(), + ); + canonical_path.push_str(dir_canonical_path); + canonical_path.push_str(MAIN_SEPARATOR_STR); + canonical_path.push_str(dir); + } else { + canonical_path = + String::with_capacity(1 + dir_canonical_path.len() + name.len()); + canonical_path.push_str(dir_canonical_path); + } + + canonical_path.push_str(MAIN_SEPARATOR_STR); + canonical_path.push_str(name); + canonical_path.push_str(".rs"); + canonical_path + }); + Exercise { dir, name, path, + canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, hint, @@ -486,6 +510,7 @@ mod tests { dir: None, name: "0", path: "exercises/0.rs", + canonical_path: None, test: false, strict_clippy: false, hint: "", diff --git a/src/exercise.rs b/src/exercise.rs index 11eea638..7fb2343c 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -7,7 +7,7 @@ use std::io::{self, StdoutLock, Write}; use crate::{ cmd::CmdRunner, - term::{terminal_file_link, write_ansi}, + term::{self, terminal_file_link, write_ansi, CountedWrite}, }; /// The initial capacity of the output buffer. @@ -18,7 +18,11 @@ pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::R stdout.write_all(b"Solution")?; stdout.queue(ResetColor)?; stdout.write_all(b" for comparison: ")?; - terminal_file_link(stdout, solution_path, Color::Cyan)?; + if let Some(canonical_path) = term::canonicalize(solution_path) { + terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?; + } else { + stdout.write_all(solution_path.as_bytes())?; + } stdout.write_all(b"\n") } @@ -60,12 +64,23 @@ pub struct Exercise { pub name: &'static str, /// Path of the exercise file starting with the `exercises/` directory. pub path: &'static str, + pub canonical_path: Option, pub test: bool, pub strict_clippy: bool, pub hint: &'static str, pub done: bool, } +impl Exercise { + pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> { + if let Some(canonical_path) = self.canonical_path.as_deref() { + return terminal_file_link(writer, self.path, canonical_path, Color::Blue); + } + + writer.write_str(self.path) + } +} + pub trait RunnableExercise { fn name(&self) -> &str; fn dir(&self) -> Option<&str>; diff --git a/src/list/state.rs b/src/list/state.rs index 468049ab..ed7c71f6 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -13,7 +13,7 @@ use std::{ use crate::{ app_state::AppState, exercise::Exercise, - term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter}, + term::{progress_bar, CountedWrite, MaxLenWriter}, }; use super::scroll_state::ScrollState; @@ -158,7 +158,7 @@ impl<'a> ListState<'a> { if self.app_state.vs_code() { writer.write_str(exercise.path)?; } else { - terminal_file_link(&mut writer, exercise.path, Color::Blue)?; + exercise.terminal_file_link(&mut writer)?; } next_ln(stdout)?; diff --git a/src/run.rs b/src/run.rs index 929b4751..f0faa69c 100644 --- a/src/run.rs +++ b/src/run.rs @@ -11,7 +11,6 @@ use std::{ use crate::{ app_state::{AppState, ExercisesProgress}, exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, - term::terminal_file_link, }; pub fn run(app_state: &mut AppState) -> Result<()> { @@ -26,7 +25,9 @@ pub fn run(app_state: &mut AppState) -> Result<()> { app_state.set_pending(app_state.current_exercise_ind())?; stdout.write_all(b"Ran ")?; - terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?; + app_state + .current_exercise() + .terminal_file_link(&mut stdout)?; stdout.write_all(b" with errors\n")?; exit(1); } @@ -46,7 +47,9 @@ pub fn run(app_state: &mut AppState) -> Result<()> { match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => { stdout.write_all(b"Next exercise: ")?; - terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?; + app_state + .current_exercise() + .terminal_file_link(&mut stdout)?; stdout.write_all(b"\n")?; } ExercisesProgress::AllDone => (), diff --git a/src/term.rs b/src/term.rs index 489d6585..5b557ecf 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,14 +1,13 @@ -use std::{ - fmt, fs, - io::{self, BufRead, StdoutLock, Write}, -}; - use crossterm::{ cursor::MoveTo, style::{Attribute, Color, SetAttribute, SetForegroundColor}, terminal::{Clear, ClearType}, Command, QueueableCommand, }; +use std::{ + fmt, fs, + io::{self, BufRead, StdoutLock, Write}, +}; pub struct MaxLenWriter<'a, 'b> { pub stdout: &'a mut StdoutLock<'b>, @@ -151,25 +150,29 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { stdout.write_all(b"\n") } +/// Canonicalize, convert to string and remove verbatim part on Windows. +pub fn canonicalize(path: &str) -> Option { + fs::canonicalize(path) + .ok()? + .into_os_string() + .into_string() + .ok() + .map(|mut path| { + // Windows itself can't handle its verbatim paths. + if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") { + path.drain(..4); + } + + path + }) +} + pub fn terminal_file_link<'a>( writer: &mut impl CountedWrite<'a>, path: &str, + canonical_path: &str, color: Color, ) -> io::Result<()> { - let canonical_path = fs::canonicalize(path).ok(); - - let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else { - return writer.write_str(path); - }; - - // Windows itself can't handle its verbatim paths. - #[cfg(windows)] - let canonical_path = if canonical_path.len() > 5 && &canonical_path[0..4] == r"\\?\" { - &canonical_path[4..] - } else { - canonical_path - }; - writer .stdout() .queue(SetForegroundColor(color))? diff --git a/src/watch/state.rs b/src/watch/state.rs index 1c2e2a9a..fe9e2748 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -11,7 +11,7 @@ use crate::{ app_state::{AppState, ExercisesProgress}, clear_terminal, exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, - term::{progress_bar, terminal_file_link}, + term::progress_bar, }; #[derive(PartialEq, Eq)] @@ -184,7 +184,9 @@ impl<'a> WatchState<'a> { )?; stdout.write_all(b"\nCurrent exercise: ")?; - terminal_file_link(stdout, self.app_state.current_exercise().path, Color::Blue)?; + self.app_state + .current_exercise() + .terminal_file_link(stdout)?; stdout.write_all(b"\n\n")?; self.show_prompt(stdout)?; From 5eb3dee59c57e320f87b2101bbfb2e4aac1b153b Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 5 Sep 2024 00:21:24 +0200 Subject: [PATCH 149/211] Create solution even if the solution's directory is missing --- src/embedded.rs | 88 ++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index 1dce46c5..51a14b6a 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Error, Result}; use std::{ - fs::{create_dir, OpenOptions}, - io::{self, Write}, + fs::{self, create_dir}, + io, }; use crate::info_file::ExerciseInfo; @@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo; /// Contains all embedded files. pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); -#[derive(Clone, Copy)] -pub enum WriteStrategy { - IfNotExists, - Overwrite, -} - -impl WriteStrategy { - fn write(self, path: &str, content: &[u8]) -> Result<()> { - let file = match self { - Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path), - Self::Overwrite => OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path), - }; - - file.with_context(|| format!("Failed to open the file `{path}` in write mode"))? - .write_all(content) - .with_context(|| format!("Failed to write the file {path}")) - } -} - // Files related to one exercise. struct ExerciseFiles { // The content of the exercise file. @@ -42,6 +19,16 @@ struct ExerciseFiles { dir_ind: usize, } +fn create_dir_if_not_exists(path: &str) -> Result<()> { + if let Err(e) = create_dir(path) { + if e.kind() != io::ErrorKind::AlreadyExists { + return Err(Error::from(e).context(format!("Failed to create the directory {path}"))); + } + } + + Ok(()) +} + // A directory in the `exercises/` directory. pub struct ExerciseDir { pub name: &'static str, @@ -55,21 +42,13 @@ impl ExerciseDir { let mut dir_path = String::with_capacity(20 + self.name.len()); dir_path.push_str("exercises/"); dir_path.push_str(self.name); - - if let Err(e) = create_dir(&dir_path) { - if e.kind() == io::ErrorKind::AlreadyExists { - return Ok(()); - } - - return Err( - Error::from(e).context(format!("Failed to create the directory {dir_path}")) - ); - } + create_dir_if_not_exists(&dir_path)?; let mut readme_path = dir_path; readme_path.push_str("/README.md"); - WriteStrategy::Overwrite.write(&readme_path, self.readme) + fs::write(&readme_path, self.readme) + .with_context(|| format!("Failed to write the file {readme_path}")) } } @@ -86,17 +65,31 @@ impl EmbeddedFiles { pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { create_dir("exercises").context("Failed to create the directory `exercises`")?; - WriteStrategy::IfNotExists.write( + fs::write( "exercises/README.md", include_bytes!("../exercises/README.md"), - )?; + ) + .context("Failed to write the file exercises/README.md")?; for dir in self.exercise_dirs { dir.init_on_disk()?; } + let mut exercise_path = String::with_capacity(64); + let prefix = "exercises/"; + exercise_path.push_str(prefix); + for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) { - WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?; + let dir = &self.exercise_dirs[exercise_files.dir_ind]; + + exercise_path.truncate(prefix.len()); + exercise_path.push_str(dir.name); + exercise_path.push('/'); + exercise_path.push_str(&exercise_info.name); + exercise_path.push_str(".rs"); + + fs::write(&exercise_path, exercise_files.exercise) + .with_context(|| format!("Failed to write the exercise file {exercise_path}"))?; } Ok(()) @@ -107,7 +100,8 @@ impl EmbeddedFiles { let dir = &self.exercise_dirs[exercise_files.dir_ind]; dir.init_on_disk()?; - WriteStrategy::Overwrite.write(path, exercise_files.exercise) + fs::write(path, exercise_files.exercise) + .with_context(|| format!("Failed to write the exercise file {path}")) } /// Write the solution file to disk and return its path. @@ -116,19 +110,25 @@ impl EmbeddedFiles { exercise_ind: usize, exercise_name: &str, ) -> Result { + create_dir_if_not_exists("solutions")?; + let exercise_files = &self.exercise_files[exercise_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind]; // 14 = 10 + 1 + 3 // solutions/ + / + .rs - let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); - solution_path.push_str("solutions/"); - solution_path.push_str(dir.name); + let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); + dir_path.push_str("solutions/"); + dir_path.push_str(dir.name); + create_dir_if_not_exists(&dir_path)?; + + let mut solution_path = dir_path; solution_path.push('/'); solution_path.push_str(exercise_name); solution_path.push_str(".rs"); - WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?; + fs::write(&solution_path, exercise_files.solution) + .with_context(|| format!("Failed to write the solution file {solution_path}"))?; Ok(solution_path) } From 17877366b7d563077c1457d42cd500b5c2eefc7c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 5 Sep 2024 01:55:31 +0200 Subject: [PATCH 150/211] Update deps --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1048d4f..6f78f3ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,9 +95,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.16" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -105,9 +105,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -242,9 +242,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -549,9 +549,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", "memchr", @@ -612,9 +612,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.76" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d22aa60c..15e2d1c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,12 +48,12 @@ include = [ [dependencies] ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.86" -clap = { version = "4.5.16", features = ["derive"] } +clap = { version = "4.5.17", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.1" rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } -serde_json = "1.0.127" +serde_json = "1.0.128" serde.workspace = true toml_edit.workspace = true From 2d0860fe1bd0aef512313617d8a26e9f118d2cd2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 5 Sep 2024 02:11:19 +0200 Subject: [PATCH 151/211] Hide input and disable its line buffering --- Cargo.lock | 1 + Cargo.toml | 3 +++ src/main.rs | 12 ++---------- src/watch.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f78f3ef..25949c4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,7 @@ dependencies = [ "crossterm", "notify-debouncer-mini", "os_pipe", + "rustix", "rustlings-macros", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 15e2d1c6..da4fc4d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,9 @@ serde_json = "1.0.128" serde.workspace = true toml_edit.workspace = true +[target.'cfg(not(windows))'.dependencies] +rustix = { version = "0.38.35", default-features = false, features = ["std", "stdio", "termios"] } + [dev-dependencies] tempfile = "3.12.0" diff --git a/src/main.rs b/src/main.rs index e53cd5a7..fe4b3dcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use std::{ }; use term::{clear_terminal, press_enter_prompt}; -use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; +use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile}; mod app_state; mod cargo_toml; @@ -130,15 +130,7 @@ fn main() -> Result<()> { ) }; - loop { - match watch::watch(&mut app_state, notify_exercise_names)? { - WatchExit::Shutdown => break, - // It is much easier to exit the watch mode, launch the list mode and then restart - // the watch mode instead of trying to pause the watch threads and correct the - // watch state. - WatchExit::List => list::list(&mut app_state)?, - } - } + watch::watch(&mut app_state, notify_exercise_names)?; } Some(Subcommands::Run { name }) => { if let Some(name) = name { diff --git a/src/watch.rs b/src/watch.rs index e14d3c57..be8409f7 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -11,7 +11,10 @@ use std::{ time::Duration, }; -use crate::app_state::{AppState, ExercisesProgress}; +use crate::{ + app_state::{AppState, ExercisesProgress}, + list, +}; use self::{ notify_event::NotifyEventHandler, @@ -33,15 +36,14 @@ enum WatchEvent { /// Returned by the watch mode to indicate what to do afterwards. #[must_use] -pub enum WatchExit { +enum WatchExit { /// Exit the program. Shutdown, /// Enter the list mode and restart the watch mode afterwards. List, } -/// `notify_exercise_names` as None activates the manual run mode. -pub fn watch( +fn run_watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, ) -> Result { @@ -110,6 +112,48 @@ pub fn watch( Ok(WatchExit::Shutdown) } +fn watch_list_loop( + app_state: &mut AppState, + notify_exercise_names: Option<&'static [&'static [u8]]>, +) -> Result<()> { + loop { + match run_watch(app_state, notify_exercise_names)? { + WatchExit::Shutdown => break Ok(()), + // It is much easier to exit the watch mode, launch the list mode and then restart + // the watch mode instead of trying to pause the watch threads and correct the + // watch state. + WatchExit::List => list::list(app_state)?, + } + } +} + +/// `notify_exercise_names` as None activates the manual run mode. +pub fn watch( + app_state: &mut AppState, + notify_exercise_names: Option<&'static [&'static [u8]]>, +) -> Result<()> { + #[cfg(not(windows))] + { + let stdin_fd = rustix::stdio::stdin(); + let mut termios = rustix::termios::tcgetattr(stdin_fd)?; + let original_local_modes = termios.local_modes; + // Disable stdin line buffering and hide input. + termios.local_modes -= + rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO; + rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?; + + let res = watch_list_loop(app_state, notify_exercise_names); + + termios.local_modes = original_local_modes; + rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?; + + res + } + + #[cfg(windows)] + watch_list_loop(app_state, notify_exercise_names) +} + const QUIT_MSG: &[u8] = b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. From aa3eda70e5727f9665a97fadcce081a96909e9b1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 5 Sep 2024 17:12:26 +0200 Subject: [PATCH 152/211] Simplify handling terminal events for unbuffered stdin --- src/watch.rs | 1 - src/watch/terminal_event.rs | 42 ++++++------------------------------- 2 files changed, 6 insertions(+), 37 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index be8409f7..ee5dd74d 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -95,7 +95,6 @@ fn run_watch( break; } WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, - WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render(&mut stdout)?, WatchEvent::FileChange { exercise_ind } => { watch_state.handle_file_change(exercise_ind, &mut stdout)?; } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 3e8c272a..8e2e2417 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -9,13 +9,9 @@ pub enum InputEvent { Hint, List, Quit, - Unrecognized, } pub fn terminal_event_handler(tx: Sender, manual_run: bool) { - // Only send `Unrecognized` on ENTER if the last input wasn't valid. - let mut last_input_valid = false; - let last_input_event = loop { let terminal_event = match event::read() { Ok(v) => v, @@ -34,39 +30,13 @@ pub fn terminal_event_handler(tx: Sender, manual_run: bool) { KeyEventKind::Press => (), } - if key.modifiers != KeyModifiers::NONE { - last_input_valid = false; - continue; - } - let input_event = match key.code { - KeyCode::Enter => { - if last_input_valid { - continue; - } - - InputEvent::Unrecognized - } - KeyCode::Char(c) => { - let input_event = match c { - 'n' => InputEvent::Next, - 'h' => InputEvent::Hint, - 'l' => break InputEvent::List, - 'q' => break InputEvent::Quit, - 'r' if manual_run => InputEvent::Run, - _ => { - last_input_valid = false; - continue; - } - }; - - last_input_valid = true; - input_event - } - _ => { - last_input_valid = false; - continue; - } + KeyCode::Char('n') => InputEvent::Next, + KeyCode::Char('h') => InputEvent::Hint, + KeyCode::Char('l') => break InputEvent::List, + KeyCode::Char('q') => break InputEvent::Quit, + KeyCode::Char('r') if manual_run => InputEvent::Run, + _ => continue, }; if tx.send(WatchEvent::Input(input_event)).is_err() { From 51b8d2ab2542eb6115bbfdbe7a404993dfcd0749 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 5 Sep 2024 17:23:56 +0200 Subject: [PATCH 153/211] Remove unused import --- src/watch/terminal_event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 8e2e2417..07621519 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,4 +1,4 @@ -use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use std::sync::mpsc::Sender; use super::WatchEvent; From dcad002057acfb1a41513fb421275116ea946ca3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 5 Sep 2024 17:32:59 +0200 Subject: [PATCH 154/211] Only render when needed --- src/app_state.rs | 4 ++-- src/run.rs | 2 +- src/watch.rs | 10 +++------- src/watch/state.rs | 8 ++++++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 7123d11a..ed723c22 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -24,10 +24,10 @@ const STATE_FILE_NAME: &str = ".rustlings-state.txt"; pub enum ExercisesProgress { // All exercises are done. AllDone, - // The current exercise failed and is still pending. - CurrentPending, // A new exercise is now pending. NewPending, + // The current exercise is still pending. + CurrentPending, } pub enum StateFileStatus { diff --git a/src/run.rs b/src/run.rs index f0faa69c..a969164c 100644 --- a/src/run.rs +++ b/src/run.rs @@ -45,7 +45,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { } match app_state.done_current_exercise(&mut stdout)? { - ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => { + ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => { stdout.write_all(b"Next exercise: ")?; app_state .current_exercise() diff --git a/src/watch.rs b/src/watch.rs index ee5dd74d..bca38321 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -83,13 +83,11 @@ fn run_watch( match event { WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? { ExercisesProgress::AllDone => break, - ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?, ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?, + ExercisesProgress::CurrentPending => (), }, WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, - WatchEvent::Input(InputEvent::List) => { - return Ok(WatchExit::List); - } + WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List), WatchEvent::Input(InputEvent::Quit) => { stdout.write_all(QUIT_MSG)?; break; @@ -99,9 +97,7 @@ fn run_watch( watch_state.handle_file_change(exercise_ind, &mut stdout)?; } WatchEvent::TerminalResize => watch_state.render(&mut stdout)?, - WatchEvent::NotifyErr(e) => { - return Err(Error::from(e).context(NOTIFY_ERR)); - } + WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)), WatchEvent::TerminalEventErr(e) => { return Err(Error::from(e).context("Terminal event listener failed")); } diff --git a/src/watch/state.rs b/src/watch/state.rs index fe9e2748..75a0c9e1 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -195,7 +195,11 @@ impl<'a> WatchState<'a> { } pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { - self.show_hint = true; - self.render(stdout) + if !self.show_hint { + self.show_hint = true; + self.render(stdout)?; + } + + Ok(()) } } From bcc2a136c8b086a660b8e656c2cd9398f47435f4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 5 Sep 2024 17:37:34 +0200 Subject: [PATCH 155/211] Add error message when unable to get terminal size --- src/list.rs | 2 +- src/list/state.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/list.rs b/src/list.rs index cfd3720c..9f243a17 100644 --- a/src/list.rs +++ b/src/list.rs @@ -20,7 +20,7 @@ mod scroll_state; mod state; fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> { - let mut list_state = ListState::new(app_state, stdout)?; + let mut list_state = ListState::build(app_state, stdout)?; let mut is_searching = false; loop { diff --git a/src/list/state.rs b/src/list/state.rs index ed7c71f6..5bdbca77 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -48,7 +48,7 @@ pub struct ListState<'a> { } impl<'a> ListState<'a> { - pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result { + pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result { stdout.queue(Clear(ClearType::All))?; let name_col_title_len = 4; @@ -64,7 +64,7 @@ impl<'a> ListState<'a> { let n_rows_with_filter = app_state.exercises().len(); let selected = app_state.current_exercise_ind(); - let (width, height) = terminal::size()?; + let (width, height) = terminal::size().context("Failed to get the terminal size")?; let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5); let mut slf = Self { From 9faa5d3aa48f7a94ed87e61ad6ea659579f1311a Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 5 Sep 2024 17:45:27 +0200 Subject: [PATCH 156/211] Avoid asking for terminal size on each rendering --- src/watch.rs | 8 +++++--- src/watch/state.rs | 26 ++++++++++++++++++++------ src/watch/terminal_event.rs | 4 ++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index bca38321..900eba7c 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -29,7 +29,7 @@ mod terminal_event; enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, - TerminalResize, + TerminalResize { width: u16 }, NotifyErr(notify::Error), TerminalEventErr(io::Error), } @@ -72,7 +72,7 @@ fn run_watch( None }; - let mut watch_state = WatchState::new(app_state, manual_run); + let mut watch_state = WatchState::build(app_state, manual_run)?; let mut stdout = io::stdout().lock(); watch_state.run_current_exercise(&mut stdout)?; @@ -96,7 +96,9 @@ fn run_watch( WatchEvent::FileChange { exercise_ind } => { watch_state.handle_file_change(exercise_ind, &mut stdout)?; } - WatchEvent::TerminalResize => watch_state.render(&mut stdout)?, + WatchEvent::TerminalResize { width } => { + watch_state.update_term_width(width, &mut stdout)?; + } WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)), WatchEvent::TerminalEventErr(e) => { return Err(Error::from(e).context("Terminal event listener failed")); diff --git a/src/watch/state.rs b/src/watch/state.rs index 75a0c9e1..e66cbeeb 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use crossterm::{ style::{ Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor, @@ -27,17 +27,23 @@ pub struct WatchState<'a> { show_hint: bool, done_status: DoneStatus, manual_run: bool, + term_width: u16, } impl<'a> WatchState<'a> { - pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self { - Self { + pub fn build(app_state: &'a mut AppState, manual_run: bool) -> Result { + let term_width = terminal::size() + .context("Failed to get the terminal size")? + .0; + + Ok(Self { app_state, output: Vec::with_capacity(OUTPUT_CAPACITY), show_hint: false, done_status: DoneStatus::Pending, manual_run, - } + term_width, + }) } pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> { @@ -175,12 +181,11 @@ impl<'a> WatchState<'a> { )?; } - let line_width = terminal::size()?.0; progress_bar( stdout, self.app_state.n_done(), self.app_state.exercises().len() as u16, - line_width, + self.term_width, )?; stdout.write_all(b"\nCurrent exercise: ")?; @@ -202,4 +207,13 @@ impl<'a> WatchState<'a> { Ok(()) } + + pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> { + if self.term_width != width { + self.term_width = width; + self.render(stdout)?; + } + + Ok(()) + } } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 07621519..ca3a8464 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -43,8 +43,8 @@ pub fn terminal_event_handler(tx: Sender, manual_run: bool) { return; } } - Event::Resize(_, _) => { - if tx.send(WatchEvent::TerminalResize).is_err() { + Event::Resize(width, _) => { + if tx.send(WatchEvent::TerminalResize { width }).is_err() { return; } } From 2d26358602fc1cd0a026f634b38c34e7b4618cc9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 6 Sep 2024 15:40:25 +0200 Subject: [PATCH 157/211] Use the thread builder and handle the spawn error --- clippy.toml | 3 +++ src/app_state.rs | 11 +++++++++-- src/dev/check.rs | 27 ++++++++++++++++++--------- src/watch.rs | 6 ++++-- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/clippy.toml b/clippy.toml index 81e372a7..4a5dd06a 100644 --- a/clippy.toml +++ b/clippy.toml @@ -10,4 +10,7 @@ disallowed-methods = [ "std::collections::HashSet::with_capacity", # Inefficient. Use `.queue(…)` instead. "crossterm::style::style", + # Use `thread::Builder::spawn` instead and handle the error. + "std::thread::spawn", + "std::thread::Scope::spawn", ] diff --git a/src/app_state.rs b/src/app_state.rs index ed723c22..ecb46898 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -388,13 +388,20 @@ impl AppState { let handles = self .exercises .iter() - .map(|exercise| s.spawn(|| exercise.run_exercise(None, &self.cmd_runner))) + .map(|exercise| { + thread::Builder::new() + .spawn_scoped(s, || exercise.run_exercise(None, &self.cmd_runner)) + }) .collect::>(); - for (exercise_ind, handle) in handles.into_iter().enumerate() { + for (exercise_ind, spawn_res) in handles.into_iter().enumerate() { write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; stdout.flush()?; + let Ok(handle) = spawn_res else { + return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); + }; + let Ok(success) = handle.join().unwrap() else { return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); }; diff --git a/src/dev/check.rs b/src/dev/check.rs index a6db3c21..b7e23dca 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -185,12 +185,14 @@ fn check_exercises_unsolved( return None; } - Some(( - exercise_info.name.as_str(), - thread::spawn(|| exercise_info.run_exercise(None, cmd_runner)), - )) + Some( + thread::Builder::new() + .spawn(|| exercise_info.run_exercise(None, cmd_runner)) + .map(|handle| (exercise_info.name.as_str(), handle)), + ) }) - .collect::>(); + .collect::, _>>() + .context("Failed to spawn a thread to check if an exercise is already solved")?; let n_handles = handles.len(); write!(stdout, "Progress: 0/{n_handles}")?; @@ -226,7 +228,9 @@ fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) Ordering::Equal => (), } - let handle = thread::spawn(move || check_exercises_unsolved(info_file, cmd_runner)); + let handle = thread::Builder::new() + .spawn(move || check_exercises_unsolved(info_file, cmd_runner)) + .context("Failed to spawn a thread to check if any exercise is already solved")?; let info_file_paths = check_info_file_exercises(info_file)?; check_unexpected_files("exercises", &info_file_paths)?; @@ -253,7 +257,7 @@ fn check_solutions( .exercises .iter() .map(|exercise_info| { - thread::spawn(move || { + thread::Builder::new().spawn(move || { let sol_path = exercise_info.sol_path(); if !Path::new(&sol_path).exists() { if require_solutions { @@ -274,7 +278,8 @@ fn check_solutions( } }) }) - .collect::>(); + .collect::, _>>() + .context("Failed to spawn a thread to check a solution")?; let mut sol_paths = hash_set_with_capacity(info_file.exercises.len()); let mut fmt_cmd = Command::new("rustfmt"); @@ -322,7 +327,11 @@ fn check_solutions( } stdout.write_all(b"\n")?; - let handle = thread::spawn(move || check_unexpected_files("solutions", &sol_paths)); + let handle = thread::Builder::new() + .spawn(move || check_unexpected_files("solutions", &sol_paths)) + .context( + "Failed to spawn a thread to check for unexpected files in the solutions directory", + )?; if !fmt_cmd .status() diff --git a/src/watch.rs b/src/watch.rs index 900eba7c..a44b5656 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,4 +1,4 @@ -use anyhow::{Error, Result}; +use anyhow::{Context, Error, Result}; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, @@ -77,7 +77,9 @@ fn run_watch( let mut stdout = io::stdout().lock(); watch_state.run_current_exercise(&mut stdout)?; - thread::spawn(move || terminal_event_handler(tx, manual_run)); + thread::Builder::new() + .spawn(move || terminal_event_handler(tx, manual_run)) + .context("Failed to spawn a thread to handle terminal events")?; while let Ok(event) = rx.recv() { match event { From 938500fd2fddcec191ed896b42756e34dd29b9a3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 6 Sep 2024 16:35:12 +0200 Subject: [PATCH 158/211] Fix dev check in official repo --- dev/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 7bde359c..2accf3a1 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -217,3 +217,5 @@ empty_loop = "forbid" infinite_loop = "deny" # You shouldn't leak memory while still learning Rust mem_forget = "deny" +# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings. +disallowed_methods = "allow" From 2b7caf6fcb7fc128bc5c0be93913d3482c87b35b Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 6 Sep 2024 16:36:36 +0200 Subject: [PATCH 159/211] Too polite :P --- src/dev/check.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index b7e23dca..5a7aaed4 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -41,10 +41,10 @@ fn check_cargo_toml( if old_bins != new_bins { if cfg!(debug_assertions) { - bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again"); + bail!("The file `dev/Cargo.toml` is outdated. Run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again"); } - bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again"); + bail!("The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again"); } Ok(()) From 9a25309c1c86b1e27938446b7c4b2de024514ea4 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Wed, 11 Sep 2024 16:47:27 +0200 Subject: [PATCH 160/211] Remove redundant enum definition task The exercise enums2.rs already contains a task where an identical enum has to be defined. --- exercises/08_enums/enums3.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs index 66c4675f..cb05f657 100644 --- a/exercises/08_enums/enums3.rs +++ b/exercises/08_enums/enums3.rs @@ -4,7 +4,11 @@ struct Point { } enum Message { - // TODO: Implement the message variant types based on their usage below. + Resize { width: u64, height: u64 }, + Move(Point), + Echo(String), + ChangeColor(u8, u8, u8), + Quit, } struct State { From 1f624d4c2a4ee0441ffce842591a8e37e329d309 Mon Sep 17 00:00:00 2001 From: Ali Bektas Date: Thu, 12 Sep 2024 15:26:40 +0200 Subject: [PATCH 161/211] Add rust-analyzer.toml file --- dev-rust-analyzer.toml | 4 ++++ src/init.rs | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 dev-rust-analyzer.toml diff --git a/dev-rust-analyzer.toml b/dev-rust-analyzer.toml new file mode 100644 index 00000000..b0b88fae --- /dev/null +++ b/dev-rust-analyzer.toml @@ -0,0 +1,4 @@ +# rust-analyzer configuration file +# DO NOT edit what is already defined. +# You may add new configurations as needed. +check.extraArgs = ["--profile", "test"] diff --git a/src/init.rs b/src/init.rs index 332bf52e..24dcfa68 100644 --- a/src/init.rs +++ b/src/init.rs @@ -130,6 +130,10 @@ pub fn init() -> Result<()> { fs::write("Cargo.toml", updated_cargo_toml) .context("Failed to create the file `rustlings/Cargo.toml`")?; + let ra_toml = include_str!("../dev-rust-analyzer.toml"); + fs::write("rust-analyzer.toml", ra_toml) + .context("Failed to create the file `rustlings/rust-analyzer.toml`")?; + fs::write(".gitignore", GITIGNORE) .context("Failed to create the file `rustlings/.gitignore`")?; From 88e10a9e54eb06217a3c45d330b6a3ba66577b01 Mon Sep 17 00:00:00 2001 From: Ali Bektas Date: Thu, 12 Sep 2024 15:46:09 +0200 Subject: [PATCH 162/211] hardcode ratoml in init.rs --- dev-rust-analyzer.toml | 4 ---- src/init.rs | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 dev-rust-analyzer.toml diff --git a/dev-rust-analyzer.toml b/dev-rust-analyzer.toml deleted file mode 100644 index b0b88fae..00000000 --- a/dev-rust-analyzer.toml +++ /dev/null @@ -1,4 +0,0 @@ -# rust-analyzer configuration file -# DO NOT edit what is already defined. -# You may add new configurations as needed. -check.extraArgs = ["--profile", "test"] diff --git a/src/init.rs b/src/init.rs index 24dcfa68..63224325 100644 --- a/src/init.rs +++ b/src/init.rs @@ -130,8 +130,7 @@ pub fn init() -> Result<()> { fs::write("Cargo.toml", updated_cargo_toml) .context("Failed to create the file `rustlings/Cargo.toml`")?; - let ra_toml = include_str!("../dev-rust-analyzer.toml"); - fs::write("rust-analyzer.toml", ra_toml) + fs::write("rust-analyzer.toml", RATOML) .context("Failed to create the file `rustlings/rust-analyzer.toml`")?; fs::write(".gitignore", GITIGNORE) @@ -173,6 +172,11 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() { } "; +const RATOML: &[u8] = br#"# rust-analyzer configuration file +# DO NOT edit what is already defined. +# You may add new configurations as needed. +check.extraArgs = ["--profile", "test"]"#; + const GITIGNORE: &[u8] = b"Cargo.lock target/ .vscode/ From 83d1275d721303439ad436465995fb2bd699d4fd Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 12 Sep 2024 14:10:50 +0200 Subject: [PATCH 163/211] Add missing # in comment --- src/exercise.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index 7fb2343c..84908284 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -131,7 +131,7 @@ pub trait RunnableExercise { let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut()); - // `--profile test` is required to also check code with `[cfg(test)]`. + // `--profile test` is required to also check code with `#[cfg(test)]`. if FORCE_STRICT_CLIPPY || self.strict_clippy() { clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]); } else { From 234a61a3eeb1c10b0bd6ac5bd1dfe3d09438c04f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 12 Sep 2024 15:40:21 +0200 Subject: [PATCH 164/211] Update deps --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25949c4f..7d84cc58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" [[package]] name = "autocfg" @@ -460,18 +460,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "rustix" -version = "0.38.35" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", @@ -530,18 +530,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -659,9 +659,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "utf8parse" diff --git a/Cargo.toml b/Cargo.toml index da4fc4d4..5b554a6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ edition = "2021" # On Update: Update the edition of the `rustfmt` command that c rust-version = "1.80" [workspace.dependencies] -serde = { version = "1.0.209", features = ["derive"] } +serde = { version = "1.0.210", features = ["derive"] } toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } [package] @@ -47,7 +47,7 @@ include = [ [dependencies] ahash = { version = "0.8.11", default-features = false } -anyhow = "1.0.86" +anyhow = "1.0.88" clap = { version = "4.5.17", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } @@ -58,7 +58,7 @@ serde.workspace = true toml_edit.workspace = true [target.'cfg(not(windows))'.dependencies] -rustix = { version = "0.38.35", default-features = false, features = ["std", "stdio", "termios"] } +rustix = { version = "0.38.37", default-features = false, features = ["std", "stdio", "termios"] } [dev-dependencies] tempfile = "3.12.0" From 664228ef8b910640b353acd7445fa14b9d16ad9f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 12 Sep 2024 16:34:33 +0200 Subject: [PATCH 165/211] Improve quit message --- src/watch.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/watch.rs b/src/watch.rs index a44b5656..e910fb71 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -154,8 +154,9 @@ pub fn watch( } const QUIT_MSG: &[u8] = b" + We hope you're enjoying learning Rust! -If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. +If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory. "; const NOTIFY_ERR: &str = " From 3947c4de284cb82945055a0fe802c2755e951bb9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 12 Sep 2024 17:45:42 +0200 Subject: [PATCH 166/211] Pause input while running an exercise --- src/watch.rs | 23 +++++--------- src/watch/notify_event.rs | 4 +-- src/watch/state.rs | 24 ++++++++++++-- src/watch/terminal_event.rs | 63 ++++++++++++++++++++++++------------- 4 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index e910fb71..c937bfbe 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Error, Result}; +use anyhow::{Error, Result}; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, @@ -7,7 +7,6 @@ use std::{ io::{self, Write}, path::Path, sync::mpsc::channel, - thread, time::Duration, }; @@ -16,11 +15,7 @@ use crate::{ list, }; -use self::{ - notify_event::NotifyEventHandler, - state::WatchState, - terminal_event::{terminal_event_handler, InputEvent}, -}; +use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}; mod notify_event; mod state; @@ -47,7 +42,7 @@ fn run_watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, ) -> Result { - let (tx, rx) = channel(); + let (watch_event_sender, watch_event_receiver) = channel(); let mut manual_run = false; // Prevent dropping the guard until the end of the function. @@ -56,7 +51,7 @@ fn run_watch( let mut debouncer = new_debouncer( Duration::from_millis(200), NotifyEventHandler { - tx: tx.clone(), + sender: watch_event_sender.clone(), exercise_names, }, ) @@ -72,16 +67,12 @@ fn run_watch( None }; - let mut watch_state = WatchState::build(app_state, manual_run)?; - + let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run)?; let mut stdout = io::stdout().lock(); + watch_state.run_current_exercise(&mut stdout)?; - thread::Builder::new() - .spawn(move || terminal_event_handler(tx, manual_run)) - .context("Failed to spawn a thread to handle terminal events")?; - - while let Ok(event) = rx.recv() { + while let Ok(event) = watch_event_receiver.recv() { match event { WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? { ExercisesProgress::AllDone => break, diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 74716409..9b235259 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -4,7 +4,7 @@ use std::sync::mpsc::Sender; use super::WatchEvent; pub struct NotifyEventHandler { - pub tx: Sender, + pub sender: Sender, /// Used to report which exercise was modified. pub exercise_names: &'static [&'static [u8]], } @@ -47,6 +47,6 @@ impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { // An error occurs when the receiver is dropped. // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.tx.send(output_event); + let _ = self.sender.send(output_event); } } diff --git a/src/watch/state.rs b/src/watch/state.rs index e66cbeeb..6e760018 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -5,7 +5,11 @@ use crossterm::{ }, terminal, QueueableCommand, }; -use std::io::{self, StdoutLock, Write}; +use std::{ + io::{self, StdoutLock, Write}, + sync::mpsc::Sender, + thread, +}; use crate::{ app_state::{AppState, ExercisesProgress}, @@ -14,6 +18,11 @@ use crate::{ term::progress_bar, }; +use super::{ + terminal_event::{terminal_event_handler, InputPauseGuard}, + WatchEvent, +}; + #[derive(PartialEq, Eq)] enum DoneStatus { DoneWithSolution(String), @@ -31,11 +40,19 @@ pub struct WatchState<'a> { } impl<'a> WatchState<'a> { - pub fn build(app_state: &'a mut AppState, manual_run: bool) -> Result { + pub fn build( + app_state: &'a mut AppState, + watch_event_sender: Sender, + manual_run: bool, + ) -> Result { let term_width = terminal::size() .context("Failed to get the terminal size")? .0; + thread::Builder::new() + .spawn(move || terminal_event_handler(watch_event_sender, manual_run)) + .context("Failed to spawn a thread to handle terminal events")?; + Ok(Self { app_state, output: Vec::with_capacity(OUTPUT_CAPACITY), @@ -47,6 +64,9 @@ impl<'a> WatchState<'a> { } pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> { + // Ignore any input until running the exercise is done. + let _input_pause_guard = InputPauseGuard::scoped_pause(); + self.show_hint = false; writeln!( diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index ca3a8464..2a1dfdcf 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,8 +1,32 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use std::sync::mpsc::Sender; +use std::sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + mpsc::Sender, +}; use super::WatchEvent; +static INPUT_PAUSED: AtomicBool = AtomicBool::new(false); + +// Private unit type to force using the constructor function. +#[must_use = "When the guard is dropped, the input is unpaused"] +pub struct InputPauseGuard(()); + +impl InputPauseGuard { + #[inline] + pub fn scoped_pause() -> Self { + INPUT_PAUSED.store(true, Relaxed); + Self(()) + } +} + +impl Drop for InputPauseGuard { + #[inline] + fn drop(&mut self) { + INPUT_PAUSED.store(false, Relaxed); + } +} + pub enum InputEvent { Run, Next, @@ -11,46 +35,41 @@ pub enum InputEvent { Quit, } -pub fn terminal_event_handler(tx: Sender, manual_run: bool) { - let last_input_event = loop { - let terminal_event = match event::read() { - Ok(v) => v, - Err(e) => { - // If `send` returns an error, then the receiver is dropped and - // a shutdown has been already initialized. - let _ = tx.send(WatchEvent::TerminalEventErr(e)); - return; - } - }; - - match terminal_event { - Event::Key(key) => { +pub fn terminal_event_handler(sender: Sender, manual_run: bool) { + let last_watch_event = loop { + match event::read() { + Ok(Event::Key(key)) => { match key.kind { KeyEventKind::Release | KeyEventKind::Repeat => continue, KeyEventKind::Press => (), } + if INPUT_PAUSED.load(Relaxed) { + continue; + } + let input_event = match key.code { KeyCode::Char('n') => InputEvent::Next, KeyCode::Char('h') => InputEvent::Hint, - KeyCode::Char('l') => break InputEvent::List, - KeyCode::Char('q') => break InputEvent::Quit, + KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), + KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit), KeyCode::Char('r') if manual_run => InputEvent::Run, _ => continue, }; - if tx.send(WatchEvent::Input(input_event)).is_err() { + if sender.send(WatchEvent::Input(input_event)).is_err() { return; } } - Event::Resize(width, _) => { - if tx.send(WatchEvent::TerminalResize { width }).is_err() { + Ok(Event::Resize(width, _)) => { + if sender.send(WatchEvent::TerminalResize { width }).is_err() { return; } } - Event::FocusGained | Event::FocusLost | Event::Mouse(_) => continue, + Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue, + Err(e) => break WatchEvent::TerminalEventErr(e), } }; - let _ = tx.send(WatchEvent::Input(last_input_event)); + let _ = sender.send(last_watch_event); } From 0513660b05e8dd45ba7bb25fff89b4fd089b14ea Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 13 Sep 2024 14:56:46 +0200 Subject: [PATCH 167/211] Allow dead code for all exercises and solutions --- dev/Cargo.toml | 14 ++++++++------ exercises/08_enums/enums2.rs | 2 -- exercises/10_modules/modules2.rs | 1 - exercises/15_traits/traits3.rs | 2 -- exercises/19_smart_pointers/rc1.rs | 1 - solutions/08_enums/enums2.rs | 2 -- solutions/10_modules/modules2.rs | 1 - solutions/15_traits/traits3.rs | 2 -- solutions/19_smart_pointers/rc1.rs | 1 - 9 files changed, 8 insertions(+), 18 deletions(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 2accf3a1..fbb093d1 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -203,19 +203,21 @@ panic = "abort" panic = "abort" [lints.rust] -# You shouldn't write unsafe code in Rustlings +# You shouldn't write unsafe code in Rustlings! unsafe_code = "forbid" -# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust +# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust. unstable_features = "forbid" [lints.clippy] -# You forgot a `todo!()` +# You forgot a `todo!()`! todo = "forbid" -# This can only happen by mistake in Rustlings +# This can only happen by mistake in Rustlings. empty_loop = "forbid" -# No infinite loops are needed in Rustlings +# No infinite loops are needed in Rustlings. infinite_loop = "deny" -# You shouldn't leak memory while still learning Rust +# You shouldn't leak memory while still learning Rust! mem_forget = "deny" +# Dead code warnings can't be avoided in some exercises and might distract while learning. +dead_code = "allow" # Currently, there are no disallowed methods. This line avoids problems when developing Rustlings. disallowed_methods = "allow" diff --git a/exercises/08_enums/enums2.rs b/exercises/08_enums/enums2.rs index 29ed1b6f..d70f6398 100644 --- a/exercises/08_enums/enums2.rs +++ b/exercises/08_enums/enums2.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - #[derive(Debug)] struct Point { x: u64, diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs index 02eb80a9..782a70ea 100644 --- a/exercises/10_modules/modules2.rs +++ b/exercises/10_modules/modules2.rs @@ -1,7 +1,6 @@ // You can bring module paths into scopes and provide new names for them with // the `use` and `as` keywords. -#[allow(dead_code)] mod delicious_snacks { // TODO: Add the following two `use` statements after fixing them. // use self::fruits::PEAR as ???; diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs index 2e8969eb..c244650d 100644 --- a/exercises/15_traits/traits3.rs +++ b/exercises/15_traits/traits3.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - trait Licensed { // TODO: Add a default implementation for `licensing_info` so that // implementors like the two structs below can share that default behavior diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/rc1.rs index 48e19dc0..ecd34387 100644 --- a/exercises/19_smart_pointers/rc1.rs +++ b/exercises/19_smart_pointers/rc1.rs @@ -8,7 +8,6 @@ use std::rc::Rc; #[derive(Debug)] struct Sun; -#[allow(dead_code)] #[derive(Debug)] enum Planet { Mercury(Rc), diff --git a/solutions/08_enums/enums2.rs b/solutions/08_enums/enums2.rs index 2ee0553a..07aee262 100644 --- a/solutions/08_enums/enums2.rs +++ b/solutions/08_enums/enums2.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - #[derive(Debug)] struct Point { x: u64, diff --git a/solutions/10_modules/modules2.rs b/solutions/10_modules/modules2.rs index 298d76eb..55c316d7 100644 --- a/solutions/10_modules/modules2.rs +++ b/solutions/10_modules/modules2.rs @@ -1,4 +1,3 @@ -#[allow(dead_code)] mod delicious_snacks { // Added `pub` and used the expected alias after `as`. pub use self::fruits::PEAR as fruit; diff --git a/solutions/15_traits/traits3.rs b/solutions/15_traits/traits3.rs index 747d9190..3d8ec85e 100644 --- a/solutions/15_traits/traits3.rs +++ b/solutions/15_traits/traits3.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - trait Licensed { fn licensing_info(&self) -> String { "Default license".to_string() diff --git a/solutions/19_smart_pointers/rc1.rs b/solutions/19_smart_pointers/rc1.rs index 512eb9ce..c0a41abf 100644 --- a/solutions/19_smart_pointers/rc1.rs +++ b/solutions/19_smart_pointers/rc1.rs @@ -8,7 +8,6 @@ use std::rc::Rc; #[derive(Debug)] struct Sun; -#[allow(dead_code)] #[derive(Debug)] enum Planet { Mercury(Rc), From 4ffce1c2971d127149e5530a94504664d9d5bc50 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 13 Sep 2024 14:59:34 +0200 Subject: [PATCH 168/211] Move lint to Rust lints --- dev/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index fbb093d1..29a557a0 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -207,6 +207,8 @@ panic = "abort" unsafe_code = "forbid" # You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust. unstable_features = "forbid" +# Dead code warnings can't be avoided in some exercises and might distract while learning. +dead_code = "allow" [lints.clippy] # You forgot a `todo!()`! @@ -217,7 +219,5 @@ empty_loop = "forbid" infinite_loop = "deny" # You shouldn't leak memory while still learning Rust! mem_forget = "deny" -# Dead code warnings can't be avoided in some exercises and might distract while learning. -dead_code = "allow" # Currently, there are no disallowed methods. This line avoids problems when developing Rustlings. disallowed_methods = "allow" From 5aaa8924a659051f474584c8b7065a5c3afd9b00 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 13 Sep 2024 15:07:53 +0200 Subject: [PATCH 169/211] earch isn't a typo --- .typos.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.typos.toml b/.typos.toml index 2de6d580..743c8741 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,3 +1,6 @@ +[default.extend-words] +"earch" = "earch" # Because of earch in the list footer + [files] extend-exclude = [ "CHANGELOG.md", From 9459eef03214e05c4a50de3cffe250ecef095917 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 13 Sep 2024 16:38:53 +0200 Subject: [PATCH 170/211] Use Clippy with Rust-Analyzer --- src/init.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/init.rs b/src/init.rs index 63224325..ce49bb65 100644 --- a/src/init.rs +++ b/src/init.rs @@ -130,7 +130,7 @@ pub fn init() -> Result<()> { fs::write("Cargo.toml", updated_cargo_toml) .context("Failed to create the file `rustlings/Cargo.toml`")?; - fs::write("rust-analyzer.toml", RATOML) + fs::write("rust-analyzer.toml", RUST_ANALYZER_TOML) .context("Failed to create the file `rustlings/rust-analyzer.toml`")?; fs::write(".gitignore", GITIGNORE) @@ -172,10 +172,9 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() { } "; -const RATOML: &[u8] = br#"# rust-analyzer configuration file -# DO NOT edit what is already defined. -# You may add new configurations as needed. -check.extraArgs = ["--profile", "test"]"#; +pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy" +check.extraArgs = ["--profile", "test"] +"#; const GITIGNORE: &[u8] = b"Cargo.lock target/ From 47f8a0cbe502654ec09a6865cc82fe9330580ce1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 13 Sep 2024 16:39:28 +0200 Subject: [PATCH 171/211] Add rust-analyzer.toml on `dev new` --- src/dev/new.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dev/new.rs b/src/dev/new.rs index c7650465..154cd224 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -6,7 +6,7 @@ use std::{ process::Command, }; -use crate::CURRENT_FORMAT_VERSION; +use crate::{init::RUST_ANALYZER_TOML, CURRENT_FORMAT_VERSION}; // Create a directory relative to the current directory and print its path. fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> { @@ -62,6 +62,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> { write_rel_file("README.md", &dir_path_str, README)?; + write_rel_file("rust-analyzer.toml", &dir_path_str, RUST_ANALYZER_TOML)?; + create_rel_dir(".vscode", &dir_path_str)?; write_rel_file( ".vscode/extensions.json", From 8b476e678a8b28bb1885671375a4a7a1b330b16b Mon Sep 17 00:00:00 2001 From: bri-rose Date: Fri, 13 Sep 2024 10:23:05 -0500 Subject: [PATCH 172/211] Update info.toml Fixed grammatical error, subject/verb agreement at line 124-125. --- 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 0fe83439..2d420457 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -122,7 +122,7 @@ dir = "01_variables" test = false hint = """ We know about variables and mutability, but there is another important type of -variables available: constants. +variable available: constants. Constants are always immutable. They are declared with the keyword `const` instead of `let`. From b540c6df253c1f528486bf4245da8eec66710684 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 14 Sep 2024 09:48:26 +0200 Subject: [PATCH 173/211] Make if2 less confusing Some people would get stuck on this exercise, trying to understand the meaning behind foo, fuzz, baz etc. Making the theme of the code make a little more sense to humans should hopefully prevent people from getting confused by abstract and non-sensical tests. --- exercises/03_if/if2.rs | 22 ++++++++++++---------- solutions/03_if/if2.rs | 26 ++++++++++++++------------ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index 593a77a7..10037f26 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -1,7 +1,7 @@ // TODO: Fix the compiler error on this function. -fn foo_if_fizz(fizzish: &str) -> &str { - if fizzish == "fizz" { - "foo" +fn picky_eater(food: &str) -> &str { + if food == "strawberry" { + "Yummy!" } else { 1 } @@ -18,18 +18,20 @@ mod tests { use super::*; #[test] - fn foo_for_fizz() { - // This means that calling `foo_if_fizz` with the argument "fizz" should return "foo". - assert_eq!(foo_if_fizz("fizz"), "foo"); + fn yummy_food() { + // This means that calling `picky_eater` with the argument "food" should return "Yummy!". + assert_eq!(picky_eater("strawberry"), "Yummy!"); } #[test] - fn bar_for_fuzz() { - assert_eq!(foo_if_fizz("fuzz"), "bar"); + fn neutral_food() { + assert_eq!(picky_eater("potato"), "I guess I can eat that."); } #[test] - fn default_to_baz() { - assert_eq!(foo_if_fizz("literally anything"), "baz"); + fn default_disliked_food() { + assert_eq!(picky_eater("broccoli"), "No thanks!"); + assert_eq!(picky_eater("gummy bears"), "No thanks!"); + assert_eq!(picky_eater("literally anything"), "No thanks!"); } } diff --git a/solutions/03_if/if2.rs b/solutions/03_if/if2.rs index 440bba05..21c0dcd3 100644 --- a/solutions/03_if/if2.rs +++ b/solutions/03_if/if2.rs @@ -1,10 +1,10 @@ -fn foo_if_fizz(fizzish: &str) -> &str { - if fizzish == "fizz" { - "foo" - } else if fizzish == "fuzz" { - "bar" +fn picky_eater(food: &str) -> &str { + if food == "strawberry" { + "Yummy!" + } else if food == "potato" { + "I guess I can eat that." } else { - "baz" + "No thanks!" } } @@ -17,17 +17,19 @@ mod tests { use super::*; #[test] - fn foo_for_fizz() { - assert_eq!(foo_if_fizz("fizz"), "foo"); + fn yummy_food() { + assert_eq!(picky_eater("strawberry"), "Yummy!"); } #[test] - fn bar_for_fuzz() { - assert_eq!(foo_if_fizz("fuzz"), "bar"); + fn neutral_food() { + assert_eq!(picky_eater("potato"), "I guess I can eat that."); } #[test] - fn default_to_baz() { - assert_eq!(foo_if_fizz("literally anything"), "baz"); + fn default_disliked_food() { + assert_eq!(picky_eater("broccoli"), "No thanks!"); + assert_eq!(picky_eater("gummy bears"), "No thanks!"); + assert_eq!(picky_eater("literally anything"), "No thanks!"); } } From e56ae6d65144d0a0bc8cc6759c89e59658a71497 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 17 Sep 2024 23:33:48 +0200 Subject: [PATCH 174/211] Update deps --- Cargo.lock | 8 ++++---- Cargo.toml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d84cc58..bededeb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "autocfg" @@ -646,9 +646,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ "indexmap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5b554a6a..23dc4f65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ rust-version = "1.80" [workspace.dependencies] serde = { version = "1.0.210", features = ["derive"] } -toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } +toml_edit = { version = "0.22.21", default-features = false, features = ["parse", "serde"] } [package] name = "rustlings" @@ -47,7 +47,7 @@ include = [ [dependencies] ahash = { version = "0.8.11", default-features = false } -anyhow = "1.0.88" +anyhow = "1.0.89" clap = { version = "4.5.17", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } notify-debouncer-mini = { version = "0.4.1", default-features = false } From 89c40ba25672b2da17e2fcc5bc742462699d54dd Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 18 Sep 2024 01:43:48 +0200 Subject: [PATCH 175/211] Optimize the file watcher --- Cargo.lock | 28 +----------- Cargo.toml | 2 +- src/watch.rs | 43 +++++++++++++----- src/watch/notify_event.rs | 91 +++++++++++++++++++++---------------- src/watch/state.rs | 5 +- src/watch/terminal_event.rs | 30 ++---------- 6 files changed, 91 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bededeb2..adc31129 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,21 +139,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - [[package]] name = "crossterm" version = "0.28.1" @@ -379,7 +364,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ "bitflags 2.6.0", - "crossbeam-channel", "filetime", "fsevent-sys", "inotify", @@ -391,16 +375,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "notify-debouncer-mini" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" -dependencies = [ - "log", - "notify", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -488,7 +462,7 @@ dependencies = [ "anyhow", "clap", "crossterm", - "notify-debouncer-mini", + "notify", "os_pipe", "rustix", "rustlings-macros", diff --git a/Cargo.toml b/Cargo.toml index 23dc4f65..f14b47a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.89" clap = { version = "4.5.17", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } -notify-debouncer-mini = { version = "0.4.1", default-features = false } +notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] } os_pipe = "1.2.1" rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } serde_json = "1.0.128" diff --git a/src/watch.rs b/src/watch.rs index c937bfbe..fd89b29e 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,12 +1,12 @@ use anyhow::{Error, Result}; -use notify_debouncer_mini::{ - new_debouncer, - notify::{self, RecursiveMode}, -}; +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; use std::{ io::{self, Write}, path::Path, - sync::mpsc::channel, + sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + mpsc::channel, + }, time::Duration, }; @@ -21,6 +21,27 @@ mod notify_event; mod state; mod terminal_event; +static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false); + +// Private unit type to force using the constructor function. +#[must_use = "When the guard is dropped, the input is unpaused"] +pub struct InputPauseGuard(()); + +impl InputPauseGuard { + #[inline] + pub fn scoped_pause() -> Self { + EXERCISE_RUNNING.store(true, Relaxed); + Self(()) + } +} + +impl Drop for InputPauseGuard { + #[inline] + fn drop(&mut self) { + EXERCISE_RUNNING.store(false, Relaxed); + } +} + enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, @@ -47,21 +68,21 @@ fn run_watch( let mut manual_run = false; // Prevent dropping the guard until the end of the function. // Otherwise, the file watcher exits. - let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { - let mut debouncer = new_debouncer( - Duration::from_millis(200), + let _watcher_guard = if let Some(exercise_names) = notify_exercise_names { + let mut watcher = RecommendedWatcher::new( NotifyEventHandler { sender: watch_event_sender.clone(), exercise_names, }, + Config::default().with_poll_interval(Duration::from_secs(1)), ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; - debouncer - .watcher() + + watcher .watch(Path::new("exercises"), RecursiveMode::Recursive) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; - Some(debouncer) + Some(watcher) } else { manual_run = true; None diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 9b235259..5ed8fd18 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,7 +1,10 @@ -use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; -use std::sync::mpsc::Sender; +use notify::{ + event::{MetadataKind, ModifyKind}, + Event, EventKind, +}; +use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; -use super::WatchEvent; +use super::{WatchEvent, EXERCISE_RUNNING}; pub struct NotifyEventHandler { pub sender: Sender, @@ -9,44 +12,56 @@ pub struct NotifyEventHandler { pub exercise_names: &'static [&'static [u8]], } -impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { - fn handle_event(&mut self, input_event: DebounceEventResult) { - let output_event = match input_event { - Ok(input_event) => { - let Some(exercise_ind) = input_event - .iter() - .filter_map(|input_event| { - if input_event.kind != DebouncedEventKind::Any { - return None; - } +impl notify::EventHandler for NotifyEventHandler { + fn handle_event(&mut self, input_event: notify::Result) { + if EXERCISE_RUNNING.load(Relaxed) { + return; + } - let file_name = input_event.path.file_name()?.to_str()?.as_bytes(); - - if file_name.len() < 4 { - return None; - } - let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3); - - if ext != b".rs" { - return None; - } - - self.exercise_names - .iter() - .position(|exercise_name| *exercise_name == file_name_without_ext) - }) - .min() - else { - return; - }; - - WatchEvent::FileChange { exercise_ind } + let input_event = match input_event { + Ok(v) => v, + Err(e) => { + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. + let _ = self.sender.send(WatchEvent::NotifyErr(e)); + return; } - Err(e) => WatchEvent::NotifyErr(e), }; - // An error occurs when the receiver is dropped. - // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.sender.send(output_event); + match input_event.kind { + EventKind::Any => (), + EventKind::Modify(modify_kind) => match modify_kind { + ModifyKind::Any | ModifyKind::Data(_) => (), + ModifyKind::Metadata(metadata_kind) => match metadata_kind { + MetadataKind::Any | MetadataKind::WriteTime => (), + MetadataKind::AccessTime + | MetadataKind::Permissions + | MetadataKind::Ownership + | MetadataKind::Extended + | MetadataKind::Other => return, + }, + ModifyKind::Name(_) | ModifyKind::Other => return, + }, + EventKind::Access(_) + | EventKind::Create(_) + | EventKind::Remove(_) + | EventKind::Other => return, + } + + let _ = input_event + .paths + .into_iter() + .filter_map(|path| { + let file_name = path.file_name()?.to_str()?.as_bytes(); + + let [file_name_without_ext @ .., b'.', b'r', b's'] = file_name else { + return None; + }; + + self.exercise_names + .iter() + .position(|exercise_name| *exercise_name == file_name_without_ext) + }) + .try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind })); } } diff --git a/src/watch/state.rs b/src/watch/state.rs index 6e760018..8cccb40d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -18,10 +18,7 @@ use crate::{ term::progress_bar, }; -use super::{ - terminal_event::{terminal_event_handler, InputPauseGuard}, - WatchEvent, -}; +use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent}; #[derive(PartialEq, Eq)] enum DoneStatus { diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 2a1dfdcf..050c4acb 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,31 +1,7 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use std::sync::{ - atomic::{AtomicBool, Ordering::Relaxed}, - mpsc::Sender, -}; +use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; -use super::WatchEvent; - -static INPUT_PAUSED: AtomicBool = AtomicBool::new(false); - -// Private unit type to force using the constructor function. -#[must_use = "When the guard is dropped, the input is unpaused"] -pub struct InputPauseGuard(()); - -impl InputPauseGuard { - #[inline] - pub fn scoped_pause() -> Self { - INPUT_PAUSED.store(true, Relaxed); - Self(()) - } -} - -impl Drop for InputPauseGuard { - #[inline] - fn drop(&mut self) { - INPUT_PAUSED.store(false, Relaxed); - } -} +use super::{WatchEvent, EXERCISE_RUNNING}; pub enum InputEvent { Run, @@ -44,7 +20,7 @@ pub fn terminal_event_handler(sender: Sender, manual_run: bool) { KeyEventKind::Press => (), } - if INPUT_PAUSED.load(Relaxed) { + if EXERCISE_RUNNING.load(Relaxed) { continue; } From 4e4b65711a20ae3d02baa79d8295da2b30ec7dd2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 18 Sep 2024 01:44:13 +0200 Subject: [PATCH 176/211] Only handle file changes for the current exercise, no jumping back --- src/watch/state.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index 8cccb40d..cb79b356 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -100,14 +100,10 @@ impl<'a> WatchState<'a> { exercise_ind: usize, stdout: &mut StdoutLock, ) -> Result<()> { - // Don't skip exercises on file changes to avoid confusion from missing exercises. - // Skipping exercises must be explicit in the interactive list. - // But going back to an earlier exercise on file change is fine. - if self.app_state.current_exercise_ind() < exercise_ind { + if self.app_state.current_exercise_ind() != exercise_ind { return Ok(()); } - self.app_state.set_current_exercise_ind(exercise_ind)?; self.run_current_exercise(stdout) } From 2653c3c4d448e4c3b0def82597796fa749f6f373 Mon Sep 17 00:00:00 2001 From: Samuel Tardieu Date: Sun, 22 Sep 2024 10:48:14 +0200 Subject: [PATCH 177/211] Do not use `.as_bytes().len()` on strings --- exercises/23_conversions/as_ref_mut.rs | 5 +++-- solutions/23_conversions/as_ref_mut.rs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs index 54f0cd11..d7892dd4 100644 --- a/exercises/23_conversions/as_ref_mut.rs +++ b/exercises/23_conversions/as_ref_mut.rs @@ -2,10 +2,11 @@ // about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. -// Obtain the number of bytes (not characters) in the given argument. +// Obtain the number of bytes (not characters) in the given argument +// (`.len()` returns the number of bytes in a string). // TODO: Add the `AsRef` trait appropriately as a trait bound. fn byte_counter(arg: T) -> usize { - arg.as_ref().as_bytes().len() + arg.as_ref().len() } // Obtain the number of characters (not bytes) in the given argument. diff --git a/solutions/23_conversions/as_ref_mut.rs b/solutions/23_conversions/as_ref_mut.rs index af62e2d8..a5d2d4fa 100644 --- a/solutions/23_conversions/as_ref_mut.rs +++ b/solutions/23_conversions/as_ref_mut.rs @@ -2,9 +2,10 @@ // about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. -// Obtain the number of bytes (not characters) in the given argument. +// Obtain the number of bytes (not characters) in the given argument +// (`.len()` returns the number of bytes in a string). fn byte_counter>(arg: T) -> usize { - arg.as_ref().as_bytes().len() + arg.as_ref().len() } // Obtain the number of characters (not bytes) in the given argument. From e3ec0abca464f337515f79d256c26b208d08b4a5 Mon Sep 17 00:00:00 2001 From: "sota.n" Date: Tue, 24 Sep 2024 16:58:37 +0900 Subject: [PATCH 178/211] add Third-Party List about rustlings-jp on README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 91ca564c..38d330f6 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,10 @@ Do you want to create your own set of Rustlings exercises to focus on some speci Or do you want to translate the original Rustlings exercises? Then follow the link to the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)! +### Third-Party List + +- [日本語版 Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercise. + ## Uninstalling Rustlings If you want to remove Rustlings from your system, run the following command: From 554301b8e9a1c3f8eda05d616fbdb909e2ebf640 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 24 Sep 2024 16:12:44 +0200 Subject: [PATCH 179/211] Clear terminal before final check in watch mode --- src/app_state.rs | 16 ++++++++++++---- src/run.rs | 2 +- src/watch/state.rs | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index ecb46898..c879955f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -381,7 +381,7 @@ impl AppState { // Return the exercise index of the first pending exercise found. fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result> { - stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + stdout.write_all(FINAL_CHECK_MSG)?; let n_exercises = self.exercises.len(); let status = thread::scope(|s| { @@ -441,7 +441,10 @@ impl AppState { /// Mark the current exercise as done and move on to the next pending exercise if one exists. /// If all exercises are marked as done, run all of them to make sure that they are actually /// done. If an exercise which is marked as done fails, mark it as pending and continue on it. - pub fn done_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result { + pub fn done_current_exercise( + &mut self, + stdout: &mut StdoutLock, + ) -> Result { let exercise = &mut self.exercises[self.current_exercise_ind]; if !exercise.done { exercise.done = true; @@ -453,6 +456,12 @@ impl AppState { return Ok(ExercisesProgress::NewPending); } + if CLEAR_BEFORE_FINAL_CHECK { + clear_terminal(stdout)?; + } else { + stdout.write_all(b"\n")?; + } + if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? { stdout.write_all(b"\n\n")?; @@ -482,8 +491,7 @@ impl AppState { const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; -const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" -All exercises seem to be done. +const FINAL_CHECK_MSG: &[u8] = b"All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. "; const FENISH_LINE: &str = "+----------------------------------------------------+ diff --git a/src/run.rs b/src/run.rs index a969164c..3fddcf22 100644 --- a/src/run.rs +++ b/src/run.rs @@ -44,7 +44,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { stdout.write_all(b"\n")?; } - match app_state.done_current_exercise(&mut stdout)? { + match app_state.done_current_exercise::(&mut stdout)? { ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => { stdout.write_all(b"Next exercise: ")?; app_state diff --git a/src/watch/state.rs b/src/watch/state.rs index cb79b356..d6c3eb29 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -113,7 +113,7 @@ impl<'a> WatchState<'a> { return Ok(ExercisesProgress::CurrentPending); } - self.app_state.done_current_exercise(stdout) + self.app_state.done_current_exercise::(stdout) } fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> { From d4fa61e4358bdc83173c346121c992836f1b7152 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Sep 2024 12:26:24 +0200 Subject: [PATCH 180/211] Debounce file change events --- src/watch.rs | 8 ++-- src/watch/notify_event.rs | 91 +++++++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index fd89b29e..11450b49 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -69,11 +69,11 @@ fn run_watch( // Prevent dropping the guard until the end of the function. // Otherwise, the file watcher exits. let _watcher_guard = if let Some(exercise_names) = notify_exercise_names { + let notify_event_handler = + NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?; + let mut watcher = RecommendedWatcher::new( - NotifyEventHandler { - sender: watch_event_sender.clone(), - exercise_names, - }, + notify_event_handler, Config::default().with_poll_interval(Duration::from_secs(1)), ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 5ed8fd18..2051e544 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,15 +1,71 @@ +use anyhow::{Context, Result}; use notify::{ - event::{MetadataKind, ModifyKind}, + event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode}, Event, EventKind, }; -use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; +use std::{ + sync::{ + atomic::Ordering::Relaxed, + mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender}, + }, + thread, + time::Duration, +}; use super::{WatchEvent, EXERCISE_RUNNING}; +const DEBOUNCE_DURATION: Duration = Duration::from_millis(200); + pub struct NotifyEventHandler { - pub sender: Sender, - /// Used to report which exercise was modified. - pub exercise_names: &'static [&'static [u8]], + error_sender: Sender, + // Sends the index of the updated exercise. + update_sender: SyncSender, + // Used to report which exercise was modified. + exercise_names: &'static [&'static [u8]], +} + +impl NotifyEventHandler { + pub fn build( + watch_event_sender: Sender, + exercise_names: &'static [&'static [u8]], + ) -> Result { + let (update_sender, update_receiver) = sync_channel(0); + let error_sender = watch_event_sender.clone(); + + // Debouncer + thread::Builder::new() + .spawn(move || { + let mut exercise_updated = vec![false; exercise_names.len()]; + + loop { + match update_receiver.recv_timeout(DEBOUNCE_DURATION) { + Ok(exercise_ind) => exercise_updated[exercise_ind] = true, + Err(RecvTimeoutError::Timeout) => { + for (exercise_ind, updated) in exercise_updated.iter_mut().enumerate() { + if *updated { + if watch_event_sender + .send(WatchEvent::FileChange { exercise_ind }) + .is_err() + { + break; + } + + *updated = false; + } + } + } + Err(RecvTimeoutError::Disconnected) => break, + } + } + }) + .context("Failed to spawn a thread to debounce file changes")?; + + Ok(Self { + error_sender, + update_sender, + exercise_names, + }) + } } impl notify::EventHandler for NotifyEventHandler { @@ -22,8 +78,8 @@ impl notify::EventHandler for NotifyEventHandler { Ok(v) => v, Err(e) => { // An error occurs when the receiver is dropped. - // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.sender.send(WatchEvent::NotifyErr(e)); + // After dropping the receiver, the watcher guard should also be dropped. + let _ = self.error_sender.send(WatchEvent::NotifyErr(e)); return; } }; @@ -32,6 +88,10 @@ impl notify::EventHandler for NotifyEventHandler { EventKind::Any => (), EventKind::Modify(modify_kind) => match modify_kind { ModifyKind::Any | ModifyKind::Data(_) => (), + ModifyKind::Name(rename_mode) => match rename_mode { + RenameMode::Any | RenameMode::To => (), + RenameMode::From | RenameMode::Both | RenameMode::Other => return, + }, ModifyKind::Metadata(metadata_kind) => match metadata_kind { MetadataKind::Any | MetadataKind::WriteTime => (), MetadataKind::AccessTime @@ -40,12 +100,17 @@ impl notify::EventHandler for NotifyEventHandler { | MetadataKind::Extended | MetadataKind::Other => return, }, - ModifyKind::Name(_) | ModifyKind::Other => return, + ModifyKind::Other => return, }, - EventKind::Access(_) - | EventKind::Create(_) - | EventKind::Remove(_) - | EventKind::Other => return, + EventKind::Access(access_kind) => match access_kind { + AccessKind::Any => (), + AccessKind::Close(access_mode) => match access_mode { + AccessMode::Any | AccessMode::Write => (), + AccessMode::Execute | AccessMode::Read | AccessMode::Other => return, + }, + AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return, + }, + EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return, } let _ = input_event @@ -62,6 +127,6 @@ impl notify::EventHandler for NotifyEventHandler { .iter() .position(|exercise_name| *exercise_name == file_name_without_ext) }) - .try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind })); + .try_for_each(|exercise_ind| self.update_sender.send(exercise_ind)); } } From 0d258b9e9691468fd1c9a46378446467a14be765 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Sep 2024 12:28:48 +0200 Subject: [PATCH 181/211] Update deps --- Cargo.lock | 32 ++++++++++++++++---------------- Cargo.toml | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index adc31129..1a541c67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,9 +95,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -105,9 +105,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -289,9 +289,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libredox" @@ -434,9 +434,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" dependencies = [ "bitflags 2.6.0", ] @@ -536,9 +536,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -620,9 +620,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.21" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -846,9 +846,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index f14b47a5..d4134fd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ rust-version = "1.80" [workspace.dependencies] serde = { version = "1.0.210", features = ["derive"] } -toml_edit = { version = "0.22.21", default-features = false, features = ["parse", "serde"] } +toml_edit = { version = "0.22.22", default-features = false, features = ["parse", "serde"] } [package] name = "rustlings" @@ -48,7 +48,7 @@ include = [ [dependencies] ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.89" -clap = { version = "4.5.17", features = ["derive"] } +clap = { version = "4.5.18", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] } os_pipe = "1.2.1" From 0e9eb9e87e21b1f95d2fbd8ad24ba8a197172318 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Sep 2024 18:05:05 +0200 Subject: [PATCH 182/211] Replace three dots with dot in 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 2d420457..c1342d68 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1144,7 +1144,7 @@ constants, but clippy recognizes those imprecise mathematical constants as a source of potential error. See the suggestions of the Clippy warning in the compile output and use the -appropriate replacement constant from `std::f32::consts`...""" +appropriate replacement constant from `std::f32::consts`.""" [[exercises]] name = "clippy2" From 0c79f2ea3e1fd2db54ebe8fba8ed17369a15b365 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Sep 2024 18:15:45 +0200 Subject: [PATCH 183/211] Reset in prompt with confirmation --- src/watch.rs | 3 +- src/watch/state.rs | 73 ++++++++++++++++++++++++++++++++----- src/watch/terminal_event.rs | 28 ++++++++++++-- 3 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 11450b49..35533b02 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -100,13 +100,14 @@ fn run_watch( ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?, ExercisesProgress::CurrentPending => (), }, + WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List), + WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Quit) => { stdout.write_all(QUIT_MSG)?; break; } - WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, WatchEvent::FileChange { exercise_ind } => { watch_state.handle_file_change(exercise_ind, &mut stdout)?; } diff --git a/src/watch/state.rs b/src/watch/state.rs index d6c3eb29..19910f03 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -6,8 +6,8 @@ use crossterm::{ terminal, QueueableCommand, }; use std::{ - io::{self, StdoutLock, Write}, - sync::mpsc::Sender, + io::{self, Read, StdoutLock, Write}, + sync::mpsc::{sync_channel, Sender, SyncSender}, thread, }; @@ -34,6 +34,7 @@ pub struct WatchState<'a> { done_status: DoneStatus, manual_run: bool, term_width: u16, + terminal_event_unpause_sender: SyncSender<()>, } impl<'a> WatchState<'a> { @@ -46,8 +47,16 @@ impl<'a> WatchState<'a> { .context("Failed to get the terminal size")? .0; + let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0); + thread::Builder::new() - .spawn(move || terminal_event_handler(watch_event_sender, manual_run)) + .spawn(move || { + terminal_event_handler( + watch_event_sender, + terminal_event_unpause_receiver, + manual_run, + ) + }) .context("Failed to spawn a thread to handle terminal events")?; Ok(Self { @@ -57,6 +66,7 @@ impl<'a> WatchState<'a> { done_status: DoneStatus::Pending, manual_run, term_width, + terminal_event_unpause_sender, }) } @@ -95,6 +105,44 @@ impl<'a> WatchState<'a> { Ok(()) } + pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> { + clear_terminal(stdout)?; + + stdout.write_all(b"Resetting will undo all your changes to the file ")?; + stdout.write_all(self.app_state.current_exercise().path.as_bytes())?; + stdout.write_all(b"\nReset (y/n)? ")?; + stdout.flush()?; + + { + let mut stdin = io::stdin().lock(); + let mut answer = [0]; + loop { + stdin + .read_exact(&mut answer) + .context("Failed to read the user's input")?; + + match answer[0] { + b'y' | b'Y' => { + self.app_state.reset_current_exercise()?; + + // The file watcher reruns the exercise otherwise. + if self.manual_run { + self.run_current_exercise(stdout)?; + } + } + b'n' | b'N' => self.render(stdout)?, + _ => continue, + } + + break; + } + } + + self.terminal_event_unpause_sender.send(())?; + + Ok(()) + } + pub fn handle_file_change( &mut self, exercise_ind: usize, @@ -117,13 +165,6 @@ impl<'a> WatchState<'a> { } fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> { - if self.manual_run { - stdout.queue(SetAttribute(Attribute::Bold))?; - stdout.write_all(b"r")?; - stdout.queue(ResetColor)?; - stdout.write_all(b":run / ")?; - } - if self.done_status != DoneStatus::Pending { stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"n")?; @@ -135,6 +176,13 @@ impl<'a> WatchState<'a> { stdout.write_all(b" / ")?; } + if self.manual_run { + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"r")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":run / ")?; + } + if !self.show_hint { stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"h")?; @@ -147,6 +195,11 @@ impl<'a> WatchState<'a> { stdout.queue(ResetColor)?; stdout.write_all(b":list / ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"x")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":reset / ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"q")?; stdout.queue(ResetColor)?; diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 050c4acb..1ed681dc 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,17 +1,25 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; +use std::sync::{ + atomic::Ordering::Relaxed, + mpsc::{Receiver, Sender}, +}; use super::{WatchEvent, EXERCISE_RUNNING}; pub enum InputEvent { - Run, Next, + Run, Hint, List, + Reset, Quit, } -pub fn terminal_event_handler(sender: Sender, manual_run: bool) { +pub fn terminal_event_handler( + sender: Sender, + unpause_receiver: Receiver<()>, + manual_run: bool, +) { let last_watch_event = loop { match event::read() { Ok(Event::Key(key)) => { @@ -26,10 +34,22 @@ pub fn terminal_event_handler(sender: Sender, manual_run: bool) { let input_event = match key.code { KeyCode::Char('n') => InputEvent::Next, + KeyCode::Char('r') if manual_run => InputEvent::Run, KeyCode::Char('h') => InputEvent::Hint, KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), + KeyCode::Char('x') => { + if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() { + return; + } + + // Pause input until quitting the confirmation prompt. + if unpause_receiver.recv().is_err() { + return; + }; + + continue; + } KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit), - KeyCode::Char('r') if manual_run => InputEvent::Run, _ => continue, }; From 26fd97a209d936755aa653ee0110d17d27e47306 Mon Sep 17 00:00:00 2001 From: Nahor Date: Wed, 2 Oct 2024 11:45:55 -0700 Subject: [PATCH 184/211] Update all exercises during the final check The previous code run the check on all exercises but only updates one exercise (the first that failed) even if multiple failed. The user won't be able to see all the failed exercises when viewing the list, and will have to run check_all after each fixed exercise. This change will update all the exercises so the user can see all that failed, fix them all, and only then need run check_all again. --- src/app_state.rs | 143 +++++++++++++++++++++++++++++++---------------- 1 file changed, 96 insertions(+), 47 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index c879955f..de5a3828 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -35,10 +35,12 @@ pub enum StateFileStatus { NotRead, } -enum AllExercisesCheck { - Pending(usize), - AllDone, - CheckedUntil(usize), +#[derive(Clone, Copy, PartialEq)] +enum AllExercisesResult { + Pending, + Success, + Failed, + Error, } pub struct AppState { @@ -270,18 +272,32 @@ impl AppState { self.write() } - pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> { + // Set the status of an exercise without saving. Returns `true` if the + // status actually changed (and thus needs saving later) + pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result { let exercise = self .exercises .get_mut(exercise_ind) .context(BAD_INDEX_ERR)?; - if exercise.done { - exercise.done = false; - self.n_done -= 1; + if exercise.done == done { + Ok(false) + } else { + exercise.done = done; + if done { + self.n_done += 1; + } else { + self.n_done -= 1; + } + Ok(true) + } + } + + // Set the status of an exercise to "pending" and save + pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> { + if self.set_status(exercise_ind, false)? { self.write()?; } - Ok(()) } @@ -380,11 +396,11 @@ impl AppState { } // Return the exercise index of the first pending exercise found. - fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result> { + fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result> { stdout.write_all(FINAL_CHECK_MSG)?; let n_exercises = self.exercises.len(); - let status = thread::scope(|s| { + let (mut checked_count, mut results) = thread::scope(|s| { let handles = self .exercises .iter() @@ -394,48 +410,83 @@ impl AppState { }) .collect::>(); + let mut results = vec![AllExercisesResult::Pending; n_exercises]; + let mut checked_count = 0; for (exercise_ind, spawn_res) in handles.into_iter().enumerate() { - write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; + write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?; stdout.flush()?; - let Ok(handle) = spawn_res else { - return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); - }; - - let Ok(success) = handle.join().unwrap() else { - return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); - }; - - if !success { - return Ok(AllExercisesCheck::Pending(exercise_ind)); - } + results[exercise_ind] = spawn_res + .context("Spawn error") + .and_then(|handle| handle.join().unwrap()) + .map_or_else( + |_| AllExercisesResult::Error, + |success| { + checked_count += 1; + if success { + AllExercisesResult::Success + } else { + AllExercisesResult::Failed + } + }, + ); } - Ok::<_, io::Error>(AllExercisesCheck::AllDone) + Ok::<_, io::Error>((checked_count, results)) })?; - let mut exercise_ind = match status { - AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)), - AllExercisesCheck::AllDone => return Ok(None), - AllExercisesCheck::CheckedUntil(ind) => ind, - }; + // If we got an error while checking all exercises in parallel, + // it could be because we exceeded the limit of open file descriptors. + // Therefore, re-try those one at a time (i.e. sequentially). + results + .iter_mut() + .enumerate() + .filter(|(_, result)| { + **result == AllExercisesResult::Pending || **result == AllExercisesResult::Error + }) + .try_for_each(|(exercise_ind, result)| { + let exercise = self.exercises.get(exercise_ind).context(BAD_INDEX_ERR)?; + *result = match exercise + .run_exercise(None, &self.cmd_runner) + .context("Sequential retry") + { + Ok(true) => AllExercisesResult::Success, + Ok(false) => AllExercisesResult::Failed, + Err(err) => bail!(err), + }; + checked_count += 1; + write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?; + stdout.flush()?; + Ok(()) + })?; - // We got an error while checking all exercises in parallel. - // This could be because we exceeded the limit of open file descriptors. - // Therefore, try to continue the check sequentially. - for exercise in &self.exercises[exercise_ind..] { - write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; - stdout.flush()?; + // Update the state of each exercise and return the first that failed + let first_fail = results + .iter() + .enumerate() + .filter_map(|(exercise_ind, result)| { + match result { + AllExercisesResult::Success => self + .set_status(exercise_ind, true) + .map_or_else(|err| Some(Err(err)), |_| None), + AllExercisesResult::Failed => self + .set_status(exercise_ind, false) + .map_or_else(|err| Some(Err(err)), |_| Some(Ok(exercise_ind))), + // The sequential check done earlier will have converted all + // exercises to Success/Failed, or bailed, so those are unreachable + AllExercisesResult::Pending | AllExercisesResult::Error => unreachable!(), + } + }) + .try_fold(None::, |current_min, index| { + match (current_min, index) { + (_, Err(err)) => Err(err), + (None, Ok(index)) => Ok(Some(index)), + (Some(current_min), Ok(index)) => Ok(Some(current_min.min(index))), + } + })?; + self.write()?; - let success = exercise.run_exercise(None, &self.cmd_runner)?; - if !success { - return Ok(Some(exercise_ind)); - } - - exercise_ind += 1; - } - - Ok(None) + Ok(first_fail) } /// Mark the current exercise as done and move on to the next pending exercise if one exists. @@ -467,9 +518,7 @@ impl AppState { self.current_exercise_ind = pending_exercise_ind; self.exercises[pending_exercise_ind].done = false; - // All exercises were marked as done. - self.n_done -= 1; - self.write()?; + return Ok(ExercisesProgress::NewPending); } From c52867eb8bf69f67e702b87dd2bf12125aa7ab12 Mon Sep 17 00:00:00 2001 From: Nahor Date: Wed, 2 Oct 2024 13:40:32 -0700 Subject: [PATCH 185/211] Add command to check all the exercises This allows for skipping repeating "next" when multiple exercises are done at once, or when earlier exercises have been updated/changed (and thus must be redone) while still working of the whole set (i.e. the final check_all is not yet available to flag those undone exercises) --- src/app_state.rs | 24 ++++++++++++++++++++---- src/watch.rs | 7 +++++++ src/watch/state.rs | 22 ++++++++++++++++++++++ src/watch/terminal_event.rs | 2 ++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index de5a3828..99772b7b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -396,8 +396,16 @@ impl AppState { } // Return the exercise index of the first pending exercise found. - fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result> { - stdout.write_all(FINAL_CHECK_MSG)?; + pub fn check_all_exercises( + &mut self, + stdout: &mut StdoutLock, + final_check: bool, + ) -> Result> { + if !final_check { + stdout.write_all(INTERMEDIATE_CHECK_MSG)?; + } else { + stdout.write_all(FINAL_CHECK_MSG)?; + } let n_exercises = self.exercises.len(); let (mut checked_count, mut results) = thread::scope(|s| { @@ -513,7 +521,7 @@ impl AppState { stdout.write_all(b"\n")?; } - if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? { + if let Some(pending_exercise_ind) = self.check_all_exercises(stdout, true)? { stdout.write_all(b"\n\n")?; self.current_exercise_ind = pending_exercise_ind; @@ -525,6 +533,12 @@ impl AppState { // Write that the last exercise is done. self.write()?; + self.render_final_message(stdout)?; + + Ok(ExercisesProgress::AllDone) + } + + pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> { clear_terminal(stdout)?; stdout.write_all(FENISH_LINE.as_bytes())?; @@ -534,12 +548,14 @@ impl AppState { stdout.write_all(b"\n")?; } - Ok(ExercisesProgress::AllDone) + Ok(()) } } const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; +const INTERMEDIATE_CHECK_MSG: &[u8] = b"Checking all exercises +"; const FINAL_CHECK_MSG: &[u8] = b"All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. "; diff --git a/src/watch.rs b/src/watch.rs index 35533b02..b9846757 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -103,6 +103,13 @@ fn run_watch( WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List), + WatchEvent::Input(InputEvent::CheckAll) => match watch_state + .check_all_exercises(&mut stdout)? + { + ExercisesProgress::AllDone => break, + ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?, + ExercisesProgress::CurrentPending => (), + }, WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Quit) => { stdout.write_all(QUIT_MSG)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 19910f03..67a63579 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -195,6 +195,11 @@ impl<'a> WatchState<'a> { stdout.queue(ResetColor)?; stdout.write_all(b":list / ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; + stdout.write_all(b"c")?; + stdout.queue(ResetColor)?; + stdout.write_all(b":check all / ")?; + stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"x")?; stdout.queue(ResetColor)?; @@ -274,6 +279,23 @@ impl<'a> WatchState<'a> { Ok(()) } + pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result { + stdout.write_all(b"\n")?; + + if let Some(first_fail) = self.app_state.check_all_exercises(stdout, false)? { + // Only change exercise if the current one is done... + if self.app_state.current_exercise().done { + self.app_state.set_current_exercise_ind(first_fail)?; + } + // ...but always pretend it's a "new" anyway because that refreshes + // the display + Ok(ExercisesProgress::NewPending) + } else { + self.app_state.render_final_message(stdout)?; + Ok(ExercisesProgress::AllDone) + } + } + pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> { if self.term_width != width { self.term_width = width; diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 1ed681dc..48411db0 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -11,6 +11,7 @@ pub enum InputEvent { Run, Hint, List, + CheckAll, Reset, Quit, } @@ -37,6 +38,7 @@ pub fn terminal_event_handler( KeyCode::Char('r') if manual_run => InputEvent::Run, KeyCode::Char('h') => InputEvent::Hint, KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), + KeyCode::Char('c') => InputEvent::CheckAll, KeyCode::Char('x') => { if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() { return; From 5c17abd1bf52d1222f6574d38a56a29ca0e7696f Mon Sep 17 00:00:00 2001 From: Nahor Date: Wed, 2 Oct 2024 14:10:26 -0700 Subject: [PATCH 186/211] Use a channel to update the check_all progress The previous code was checking the threads in the order they were created. So the progress update would be blocked on an earlier thread even if later thread were already done. Add to that that multiple instances of `cargo build` cannot run in parallel, they will be serialized instead. So if the exercises needs to be recompiled, depending on the order those `cargo build` are run, the first update can be a long time coming. So instead of relying on the thread terminating, use a channel to get notified when an exercise check is done, regardless of the order they finish in. --- src/app_state.rs | 55 ++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 99772b7b..28226d64 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -5,6 +5,7 @@ use std::{ io::{self, Read, Seek, StdoutLock, Write}, path::{Path, MAIN_SEPARATOR_STR}, process::{Command, Stdio}, + sync::mpsc, thread, }; @@ -409,35 +410,43 @@ impl AppState { let n_exercises = self.exercises.len(); let (mut checked_count, mut results) = thread::scope(|s| { - let handles = self - .exercises + let (tx, rx) = mpsc::channel(); + + self.exercises .iter() - .map(|exercise| { - thread::Builder::new() - .spawn_scoped(s, || exercise.run_exercise(None, &self.cmd_runner)) - }) - .collect::>(); + .enumerate() + .for_each(|(index, exercise)| { + let tx = tx.clone(); + let cmd_runner = &self.cmd_runner; + let _ = thread::Builder::new().spawn_scoped(s, move || { + tx.send((index, exercise.run_exercise(None, cmd_runner))) + }); + }); + + // Drop this `tx`, since the `rx` loop will not stop while there is + // at least one tx alive (i.e. we want the loop to block only while + // there are `tx` clones, i.e. threads) + drop(tx); let mut results = vec![AllExercisesResult::Pending; n_exercises]; let mut checked_count = 0; - for (exercise_ind, spawn_res) in handles.into_iter().enumerate() { + write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?; + stdout.flush()?; + while let Ok((exercise_ind, result)) = rx.recv() { + results[exercise_ind] = result.map_or_else( + |_| AllExercisesResult::Error, + |success| { + checked_count += 1; + if success { + AllExercisesResult::Success + } else { + AllExercisesResult::Failed + } + }, + ); + write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?; stdout.flush()?; - - results[exercise_ind] = spawn_res - .context("Spawn error") - .and_then(|handle| handle.join().unwrap()) - .map_or_else( - |_| AllExercisesResult::Error, - |success| { - checked_count += 1; - if success { - AllExercisesResult::Success - } else { - AllExercisesResult::Failed - } - }, - ); } Ok::<_, io::Error>((checked_count, results)) From e2f7734f37394097f330c4073bf7784500afdc9d Mon Sep 17 00:00:00 2001 From: Nahor Date: Wed, 2 Oct 2024 14:42:50 -0700 Subject: [PATCH 187/211] Limit the amount of parallelism in check_all Don't create more threads than there are CPU cores. --- src/app_state.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 28226d64..ec791883 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -5,7 +5,7 @@ use std::{ io::{self, Read, Seek, StdoutLock, Write}, path::{Path, MAIN_SEPARATOR_STR}, process::{Command, Stdio}, - sync::mpsc, + sync::{atomic::AtomicUsize, mpsc, Arc}, thread, }; @@ -20,6 +20,7 @@ use crate::{ }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; +const DEFAULT_CHECK_PARALLELISM: usize = 8; #[must_use] pub enum ExercisesProgress { @@ -411,17 +412,31 @@ impl AppState { let (mut checked_count, mut results) = thread::scope(|s| { let (tx, rx) = mpsc::channel(); + let exercise_ind = Arc::new(AtomicUsize::default()); - self.exercises - .iter() - .enumerate() - .for_each(|(index, exercise)| { - let tx = tx.clone(); - let cmd_runner = &self.cmd_runner; - let _ = thread::Builder::new().spawn_scoped(s, move || { - tx.send((index, exercise.run_exercise(None, cmd_runner))) - }); + let num_core = thread::available_parallelism() + .map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get()); + (0..num_core).for_each(|_| { + let tx = tx.clone(); + let exercise_ind = exercise_ind.clone(); + let this = &self; + let _ = thread::Builder::new().spawn_scoped(s, move || { + loop { + let exercise_ind = + exercise_ind.fetch_add(1, std::sync::atomic::Ordering::AcqRel); + let Some(exercise) = this.exercises.get(exercise_ind) else { + // No more exercises + break; + }; + if tx + .send((exercise_ind, exercise.run_exercise(None, &this.cmd_runner))) + .is_err() + { + break; + } + } }); + }); // Drop this `tx`, since the `rx` loop will not stop while there is // at least one tx alive (i.e. we want the loop to block only while From aa83fd6bc46167477447ee9b95d21e551e163411 Mon Sep 17 00:00:00 2001 From: Nahor Date: Wed, 2 Oct 2024 15:28:42 -0700 Subject: [PATCH 188/211] Show a progress bar when running check_all Replace the "Progress: xxx/yyy" with a progress bar when checking all the exercises --- src/app_state.rs | 97 ++++++++++++++++++++++++++++++++++++------------ src/term.rs | 75 ++++++++++++++++++++++++++++++++----- 2 files changed, 140 insertions(+), 32 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index ec791883..f4cc1804 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,4 +1,9 @@ use anyhow::{bail, Context, Result}; +use crossterm::{ + queue, + style::{Print, ResetColor, SetForegroundColor}, + terminal, +}; use std::{ env, fs::{File, OpenOptions}, @@ -16,7 +21,7 @@ use crate::{ embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, - term, + term::{self, progress_bar_with_success}, }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; @@ -428,10 +433,16 @@ impl AppState { // No more exercises break; }; - if tx - .send((exercise_ind, exercise.run_exercise(None, &this.cmd_runner))) - .is_err() - { + + // Notify the progress bar that this exercise is pending + if tx.send((exercise_ind, None)).is_err() { + break; + }; + + let result = exercise.run_exercise(None, &this.cmd_runner); + + // Notify the progress bar that this exercise is done + if tx.send((exercise_ind, Some(result))).is_err() { break; } } @@ -443,28 +454,68 @@ impl AppState { // there are `tx` clones, i.e. threads) drop(tx); - let mut results = vec![AllExercisesResult::Pending; n_exercises]; - let mut checked_count = 0; - write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?; - stdout.flush()?; - while let Ok((exercise_ind, result)) = rx.recv() { - results[exercise_ind] = result.map_or_else( - |_| AllExercisesResult::Error, - |success| { - checked_count += 1; - if success { - AllExercisesResult::Success - } else { - AllExercisesResult::Failed - } - }, - ); + // Print the legend + queue!( + stdout, + Print("Color legend: "), + SetForegroundColor(term::PROGRESS_FAILED_COLOR), + Print("Failure"), + ResetColor, + Print(" - "), + SetForegroundColor(term::PROGRESS_SUCCESS_COLOR), + Print("Success"), + ResetColor, + Print(" - "), + SetForegroundColor(term::PROGRESS_PENDING_COLOR), + Print("Checking"), + ResetColor, + Print("\n"), + ) + .unwrap(); + // We expect at least a few "pending" notifications shortly, so don't + // bother printing the initial state of the progress bar and flushing + // stdout - write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?; + let line_width = terminal::size().unwrap().0; + let mut results = vec![AllExercisesResult::Pending; n_exercises]; + let mut pending = 0; + let mut success = 0; + let mut failed = 0; + + while let Ok((exercise_ind, result)) = rx.recv() { + match result { + None => { + pending += 1; + } + Some(Err(_)) => { + results[exercise_ind] = AllExercisesResult::Error; + } + Some(Ok(true)) => { + results[exercise_ind] = AllExercisesResult::Success; + pending -= 1; + success += 1; + } + Some(Ok(false)) => { + results[exercise_ind] = AllExercisesResult::Failed; + pending -= 1; + failed += 1; + } + } + + write!(stdout, "\r").unwrap(); + progress_bar_with_success( + stdout, + pending, + failed, + success, + n_exercises as u16, + line_width, + ) + .unwrap(); stdout.flush()?; } - Ok::<_, io::Error>((checked_count, results)) + Ok::<_, io::Error>((success, results)) })?; // If we got an error while checking all exercises in parallel, diff --git a/src/term.rs b/src/term.rs index 5b557ecf..67ace4b8 100644 --- a/src/term.rs +++ b/src/term.rs @@ -9,6 +9,10 @@ use std::{ io::{self, BufRead, StdoutLock, Write}, }; +pub const PROGRESS_FAILED_COLOR: Color = Color::Red; +pub const PROGRESS_SUCCESS_COLOR: Color = Color::Green; +pub const PROGRESS_PENDING_COLOR: Color = Color::Blue; + pub struct MaxLenWriter<'a, 'b> { pub stdout: &'a mut StdoutLock<'b>, len: usize, @@ -85,15 +89,26 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> { } } -/// Terminal progress bar to be used when not using Ratataui. +/// Simple terminal progress bar pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, progress: u16, total: u16, line_width: u16, +) -> io::Result<()> { + progress_bar_with_success(writer, 0, 0, progress, total, line_width) +} +/// Terminal progress bar with three states (pending + failed + success) +pub fn progress_bar_with_success<'a>( + writer: &mut impl CountedWrite<'a>, + pending: u16, + failed: u16, + success: u16, + total: u16, + line_width: u16, ) -> io::Result<()> { debug_assert!(total < 1000); - debug_assert!(progress <= total); + debug_assert!((pending + failed + success) <= total); const PREFIX: &[u8] = b"Progress: ["; const PREFIX_WIDTH: u16 = PREFIX.len() as u16; @@ -104,25 +119,67 @@ pub fn progress_bar<'a>( if line_width < MIN_LINE_WIDTH { writer.write_ascii(b"Progress: ")?; // Integers are in ASCII. - return writer.write_ascii(format!("{progress}/{total}").as_bytes()); + return writer.write_ascii(format!("{}/{total}", failed + success).as_bytes()); } let stdout = writer.stdout(); stdout.write_all(PREFIX)?; let width = line_width - WRAPPER_WIDTH; - let filled = (width * progress) / total; + let mut failed_end = (width * failed) / total; + let mut success_end = (width * (failed + success)) / total; + let mut pending_end = (width * (failed + success + pending)) / total; - stdout.queue(SetForegroundColor(Color::Green))?; - for _ in 0..filled { + // In case the range boundaries overlap, "pending" has priority over both + // "failed" and "success" (don't show the bar as "complete" when we are + // still checking some things). + // "Failed" has priority over "success" (don't show 100% success if we + // have some failures, at the risk of showing 100% failures even with + // a few successes). + // + // "Failed" already has priority over "success" because it's displayed + // first. But "pending" is last so we need to fix "success"/"failed". + if pending > 0 { + pending_end = pending_end.max(1); + if pending_end == success_end { + success_end -= 1; + } + if pending_end == failed_end { + failed_end -= 1; + } + + // This will replace the last character of the "pending" range with + // the arrow char ('>'). This ensures that even if the progress bar + // is filled (everything either done or pending), we'll still see + // the '>' as long as we are not fully done. + pending_end -= 1; + } + + if failed > 0 { + stdout.queue(SetForegroundColor(PROGRESS_FAILED_COLOR))?; + for _ in 0..failed_end { + stdout.write_all(b"#")?; + } + } + + stdout.queue(SetForegroundColor(PROGRESS_SUCCESS_COLOR))?; + for _ in failed_end..success_end { stdout.write_all(b"#")?; } - if filled < width { + if pending > 0 { + stdout.queue(SetForegroundColor(PROGRESS_PENDING_COLOR))?; + + for _ in success_end..pending_end { + stdout.write_all(b"#")?; + } + } + + if pending_end < width { stdout.write_all(b">")?; } - let width_minus_filled = width - filled; + let width_minus_filled = width - pending_end; if width_minus_filled > 1 { let red_part_width = width_minus_filled - 1; stdout.queue(SetForegroundColor(Color::Red))?; @@ -133,7 +190,7 @@ pub fn progress_bar<'a>( stdout.queue(SetForegroundColor(Color::Reset))?; - write!(stdout, "] {progress:>3}/{total}") + write!(stdout, "] {:>3}/{}", failed + success, total) } pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { From d3f819f86f0fd7e67e9b995034947a65961cab34 Mon Sep 17 00:00:00 2001 From: Nahor Date: Fri, 4 Oct 2024 14:36:36 -0700 Subject: [PATCH 189/211] Add command line command to check all exercises --- src/main.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main.rs b/src/main.rs index fe4b3dcd..d257b408 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,10 @@ use anyhow::{bail, Context, Result}; use app_state::StateFileStatus; use clap::{Parser, Subcommand}; +use crossterm::{ + style::{Color, Print, ResetColor, SetForegroundColor}, + QueueableCommand, +}; use std::{ io::{self, IsTerminal, Write}, path::Path, @@ -47,6 +51,8 @@ enum Subcommands { /// The name of the exercise name: Option, }, + /// Run all the exercises, marking them as done or pending accordingly. + RunAll, /// Reset a single exercise Reset { /// The name of the exercise @@ -138,6 +144,36 @@ fn main() -> Result<()> { } run::run(&mut app_state)?; } + Some(Subcommands::RunAll) => { + let mut stdout = io::stdout().lock(); + if let Some(first_fail) = app_state.check_all_exercises(&mut stdout, false)? { + let pending = app_state + .exercises() + .iter() + .filter(|exercise| !exercise.done) + .count(); + if app_state.current_exercise().done { + app_state.set_current_exercise_ind(first_fail)?; + } + stdout + .queue(Print("\n"))? + .queue(SetForegroundColor(Color::Red))? + .queue(Print(format!("{pending}")))? + .queue(ResetColor)?; + if pending == 1 { + stdout.queue(Print(" exercise has some errors: "))?; + } else { + stdout.queue(Print(" exercises have errors, including "))?; + } + app_state + .current_exercise() + .terminal_file_link(&mut stdout)?; + stdout.write_all(b".\n")?; + exit(1); + } else { + app_state.render_final_message(&mut stdout)?; + } + } Some(Subcommands::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; let exercise_path = app_state.reset_current_exercise()?; From bf7d171915d2a720f10ccacb305993b917798419 Mon Sep 17 00:00:00 2001 From: Polycarbohydrate Date: Sat, 5 Oct 2024 16:05:35 -0400 Subject: [PATCH 190/211] Update from_str.rs --- exercises/23_conversions/from_str.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs index 4b1aaa28..ec6d3fd1 100644 --- a/exercises/23_conversions/from_str.rs +++ b/exercises/23_conversions/from_str.rs @@ -25,7 +25,7 @@ enum ParsePersonError { ParseInt(ParseIntError), } -// TODO: Complete this `From` implementation to be able to parse a `Person` +// TODO: Complete this `FromStr` implementation to be able to parse a `Person` // out of a string in the form of "Mark,20". // Note that you'll need to parse the age component into a `u8` with something // like `"4".parse::()`. From f516da4138111aa6eff0970471b8a37182c09fad Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 9 Oct 2024 15:27:36 +0200 Subject: [PATCH 191/211] Avoid single char variables --- solutions/08_enums/enums3.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solutions/08_enums/enums3.rs b/solutions/08_enums/enums3.rs index 4bc26b7c..94cf250f 100644 --- a/solutions/08_enums/enums3.rs +++ b/solutions/08_enums/enums3.rs @@ -46,8 +46,8 @@ impl State { match message { Message::Resize { width, height } => self.resize(width, height), Message::Move(point) => self.move_position(point), - Message::Echo(s) => self.echo(s), - Message::ChangeColor(r, g, b) => self.change_color(r, g, b), + Message::Echo(string) => self.echo(string), + Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue), Message::Quit => self.quit(), } } From 84a42a2b24687ed11f4d2a5c9b624d00b74de916 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 9 Oct 2024 15:42:16 +0200 Subject: [PATCH 192/211] Update third-party exercises section --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 38d330f6..b65920f2 100644 --- a/README.md +++ b/README.md @@ -135,13 +135,14 @@ Continue practicing your Rust skills by building your own projects, contributing ## Third-Party Exercises +Third-party exercises are a set of exercises maintained by the community. +You can use the same `rustlings` program that you installed with `cargo install rustlings` to run them: + +- [日本語版 Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercises. + Do you want to create your own set of Rustlings exercises to focus on some specific topic? Or do you want to translate the original Rustlings exercises? -Then follow the link to the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)! - -### Third-Party List - -- [日本語版 Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercise. +Then follow the the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)! ## Uninstalling Rustlings From 685e069c58ef02dae65381974722315ee8c84e8b Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 10 Oct 2024 19:43:35 +0200 Subject: [PATCH 193/211] First PR review changes --- src/app_state.rs | 318 +++++++++++++++++++++------------------------ src/main.rs | 3 +- src/term.rs | 19 +-- src/watch.rs | 2 +- src/watch/state.rs | 18 +-- 5 files changed, 171 insertions(+), 189 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index f4cc1804..7540181c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,16 +1,18 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Context, Error, Result}; use crossterm::{ - queue, - style::{Print, ResetColor, SetForegroundColor}, - terminal, + style::{ResetColor, SetForegroundColor}, + terminal, QueueableCommand, }; use std::{ env, fs::{File, OpenOptions}, - io::{self, Read, Seek, StdoutLock, Write}, + io::{Read, Seek, StdoutLock, Write}, path::{Path, MAIN_SEPARATOR_STR}, process::{Command, Stdio}, - sync::{atomic::AtomicUsize, mpsc, Arc}, + sync::{ + atomic::{AtomicUsize, Ordering::Relaxed}, + mpsc, + }, thread, }; @@ -42,11 +44,17 @@ pub enum StateFileStatus { NotRead, } -#[derive(Clone, Copy, PartialEq)] -enum AllExercisesResult { +enum ExerciseCheckProgress { + Checking, + Done, + Pending, + Error, +} + +#[derive(Clone, Copy)] +enum ExerciseCheckResult { + Done, Pending, - Success, - Failed, Error, } @@ -280,7 +288,7 @@ impl AppState { } // Set the status of an exercise without saving. Returns `true` if the - // status actually changed (and thus needs saving later) + // status actually changed (and thus needs saving later). pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result { let exercise = self .exercises @@ -288,23 +296,25 @@ impl AppState { .context(BAD_INDEX_ERR)?; if exercise.done == done { - Ok(false) - } else { - exercise.done = done; - if done { - self.n_done += 1; - } else { - self.n_done -= 1; - } - Ok(true) + return Ok(false); } + + exercise.done = done; + if done { + self.n_done += 1; + } else { + self.n_done -= 1; + } + + Ok(true) } - // Set the status of an exercise to "pending" and save + // Set the status of an exercise to "pending" and save. pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> { if self.set_status(exercise_ind, false)? { self.write()?; } + Ok(()) } @@ -403,173 +413,154 @@ impl AppState { } // Return the exercise index of the first pending exercise found. - pub fn check_all_exercises( - &mut self, - stdout: &mut StdoutLock, - final_check: bool, - ) -> Result> { - if !final_check { - stdout.write_all(INTERMEDIATE_CHECK_MSG)?; - } else { - stdout.write_all(FINAL_CHECK_MSG)?; - } - let n_exercises = self.exercises.len(); + pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result> { + stdout.write_all("Checking all exercises…\n".as_bytes())?; + let n_exercises = self.exercises.len() as u16; + let next_exercise_ind = AtomicUsize::new(0); + let term_width = terminal::size() + .context("Failed to get the terminal size")? + .0; - let (mut checked_count, mut results) = thread::scope(|s| { - let (tx, rx) = mpsc::channel(); - let exercise_ind = Arc::new(AtomicUsize::default()); + let mut results = vec![ExerciseCheckResult::Error; self.exercises.len()]; + let mut done = 0; + let mut pending = 0; - let num_core = thread::available_parallelism() + thread::scope(|s| { + let mut checking = 0; + let (exercise_result_sender, exercise_result_receiver) = mpsc::channel(); + let n_threads = thread::available_parallelism() .map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get()); - (0..num_core).for_each(|_| { - let tx = tx.clone(); - let exercise_ind = exercise_ind.clone(); - let this = &self; - let _ = thread::Builder::new().spawn_scoped(s, move || { - loop { - let exercise_ind = - exercise_ind.fetch_add(1, std::sync::atomic::Ordering::AcqRel); - let Some(exercise) = this.exercises.get(exercise_ind) else { - // No more exercises + + for _ in 0..n_threads { + let exercise_result_sender = exercise_result_sender.clone(); + let next_exercise_ind = &next_exercise_ind; + let slf = &self; + thread::Builder::new() + .spawn_scoped(s, move || loop { + let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed); + let Some(exercise) = slf.exercises.get(exercise_ind) else { + // No more exercises. break; }; - // Notify the progress bar that this exercise is pending - if tx.send((exercise_ind, None)).is_err() { + // Notify the progress bar that this exercise is pending. + if exercise_result_sender + .send((exercise_ind, ExerciseCheckProgress::Checking)) + .is_err() + { break; }; - let result = exercise.run_exercise(None, &this.cmd_runner); + let success = exercise.run_exercise(None, &slf.cmd_runner); + let result = match success { + Ok(true) => ExerciseCheckProgress::Done, + Ok(false) => ExerciseCheckProgress::Pending, + Err(_) => ExerciseCheckProgress::Error, + }; - // Notify the progress bar that this exercise is done - if tx.send((exercise_ind, Some(result))).is_err() { + // Notify the progress bar that this exercise is done. + if exercise_result_sender.send((exercise_ind, result)).is_err() { break; } - } - }); - }); + }) + .context("Failed to spawn a thread to check all exercises")?; + } - // Drop this `tx`, since the `rx` loop will not stop while there is - // at least one tx alive (i.e. we want the loop to block only while - // there are `tx` clones, i.e. threads) - drop(tx); + // Drop this sender to detect when the last thread is done. + drop(exercise_result_sender); - // Print the legend - queue!( - stdout, - Print("Color legend: "), - SetForegroundColor(term::PROGRESS_FAILED_COLOR), - Print("Failure"), - ResetColor, - Print(" - "), - SetForegroundColor(term::PROGRESS_SUCCESS_COLOR), - Print("Success"), - ResetColor, - Print(" - "), - SetForegroundColor(term::PROGRESS_PENDING_COLOR), - Print("Checking"), - ResetColor, - Print("\n"), - ) - .unwrap(); - // We expect at least a few "pending" notifications shortly, so don't - // bother printing the initial state of the progress bar and flushing - // stdout + // Print the legend. + stdout.write_all(b"Color legend: ")?; + stdout.queue(SetForegroundColor(term::PROGRESS_FAILED_COLOR))?; + stdout.write_all(b"Pending")?; + stdout.queue(ResetColor)?; + stdout.write_all(b" - ")?; + stdout.queue(SetForegroundColor(term::PROGRESS_SUCCESS_COLOR))?; + stdout.write_all(b"Done")?; + stdout.queue(ResetColor)?; + stdout.write_all(b" - ")?; + stdout.queue(SetForegroundColor(term::PROGRESS_PENDING_COLOR))?; + stdout.write_all(b"Checking")?; + stdout.queue(ResetColor)?; + stdout.write_all(b"\n")?; - let line_width = terminal::size().unwrap().0; - let mut results = vec![AllExercisesResult::Pending; n_exercises]; - let mut pending = 0; - let mut success = 0; - let mut failed = 0; - - while let Ok((exercise_ind, result)) = rx.recv() { + while let Ok((exercise_ind, result)) = exercise_result_receiver.recv() { match result { - None => { + ExerciseCheckProgress::Checking => checking += 1, + ExerciseCheckProgress::Done => { + results[exercise_ind] = ExerciseCheckResult::Done; + checking -= 1; + done += 1; + } + ExerciseCheckProgress::Pending => { + results[exercise_ind] = ExerciseCheckResult::Pending; + checking -= 1; pending += 1; } - Some(Err(_)) => { - results[exercise_ind] = AllExercisesResult::Error; - } - Some(Ok(true)) => { - results[exercise_ind] = AllExercisesResult::Success; - pending -= 1; - success += 1; - } - Some(Ok(false)) => { - results[exercise_ind] = AllExercisesResult::Failed; - pending -= 1; - failed += 1; - } + ExerciseCheckProgress::Error => checking -= 1, } - write!(stdout, "\r").unwrap(); + stdout.write_all(b"\r")?; progress_bar_with_success( stdout, + checking, pending, - failed, - success, - n_exercises as u16, - line_width, - ) - .unwrap(); + done, + n_exercises, + term_width, + )?; stdout.flush()?; } - Ok::<_, io::Error>((success, results)) + Ok::<_, Error>(()) })?; - // If we got an error while checking all exercises in parallel, - // it could be because we exceeded the limit of open file descriptors. - // Therefore, re-try those one at a time (i.e. sequentially). - results - .iter_mut() - .enumerate() - .filter(|(_, result)| { - **result == AllExercisesResult::Pending || **result == AllExercisesResult::Error - }) - .try_for_each(|(exercise_ind, result)| { - let exercise = self.exercises.get(exercise_ind).context(BAD_INDEX_ERR)?; - *result = match exercise - .run_exercise(None, &self.cmd_runner) - .context("Sequential retry") - { - Ok(true) => AllExercisesResult::Success, - Ok(false) => AllExercisesResult::Failed, - Err(err) => bail!(err), - }; - checked_count += 1; - write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?; - stdout.flush()?; - Ok(()) - })?; + let mut first_pending_exercise_ind = None; + for (exercise_ind, result) in results.into_iter().enumerate() { + match result { + ExerciseCheckResult::Done => { + self.set_status(exercise_ind, true)?; + } + ExerciseCheckResult::Pending => { + self.set_status(exercise_ind, false)?; + if first_pending_exercise_ind.is_none() { + first_pending_exercise_ind = Some(exercise_ind); + } + } + ExerciseCheckResult::Error => { + // If we got an error while checking all exercises in parallel, + // it could be because we exceeded the limit of open file descriptors. + // Therefore, try running exercises with errors sequentially. + let exercise = &self.exercises[exercise_ind]; + let success = exercise.run_exercise(None, &self.cmd_runner)?; + if success { + done += 1; + } else { + pending += 1; + if first_pending_exercise_ind.is_none() { + first_pending_exercise_ind = Some(exercise_ind); + } + } + self.set_status(exercise_ind, success)?; - // Update the state of each exercise and return the first that failed - let first_fail = results - .iter() - .enumerate() - .filter_map(|(exercise_ind, result)| { - match result { - AllExercisesResult::Success => self - .set_status(exercise_ind, true) - .map_or_else(|err| Some(Err(err)), |_| None), - AllExercisesResult::Failed => self - .set_status(exercise_ind, false) - .map_or_else(|err| Some(Err(err)), |_| Some(Ok(exercise_ind))), - // The sequential check done earlier will have converted all - // exercises to Success/Failed, or bailed, so those are unreachable - AllExercisesResult::Pending | AllExercisesResult::Error => unreachable!(), + stdout.write_all(b"\r")?; + progress_bar_with_success( + stdout, + u16::from(pending + done < n_exercises), + pending, + done, + n_exercises, + term_width, + )?; + stdout.flush()?; } - }) - .try_fold(None::, |current_min, index| { - match (current_min, index) { - (_, Err(err)) => Err(err), - (None, Ok(index)) => Ok(Some(index)), - (Some(current_min), Ok(index)) => Ok(Some(current_min.min(index))), - } - })?; + } + } + self.write()?; + stdout.write_all(b"\n\n")?; - Ok(first_fail) + Ok(first_pending_exercise_ind) } /// Mark the current exercise as done and move on to the next pending exercise if one exists. @@ -596,18 +587,12 @@ impl AppState { stdout.write_all(b"\n")?; } - if let Some(pending_exercise_ind) = self.check_all_exercises(stdout, true)? { - stdout.write_all(b"\n\n")?; - - self.current_exercise_ind = pending_exercise_ind; - self.exercises[pending_exercise_ind].done = false; + if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? { + self.set_current_exercise_ind(first_pending_exercise_ind)?; return Ok(ExercisesProgress::NewPending); } - // Write that the last exercise is done. - self.write()?; - self.render_final_message(stdout)?; Ok(ExercisesProgress::AllDone) @@ -629,11 +614,6 @@ impl AppState { const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; -const INTERMEDIATE_CHECK_MSG: &[u8] = b"Checking all exercises -"; -const FINAL_CHECK_MSG: &[u8] = b"All exercises seem to be done. -Recompiling and running all exercises to make sure that all of them are actually done. -"; const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | +-------------------------- ------------------------+ diff --git a/src/main.rs b/src/main.rs index d257b408..64b72bde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -146,7 +146,7 @@ fn main() -> Result<()> { } Some(Subcommands::RunAll) => { let mut stdout = io::stdout().lock(); - if let Some(first_fail) = app_state.check_all_exercises(&mut stdout, false)? { + if let Some(first_fail) = app_state.check_all_exercises(&mut stdout)? { let pending = app_state .exercises() .iter() @@ -156,7 +156,6 @@ fn main() -> Result<()> { app_state.set_current_exercise_ind(first_fail)?; } stdout - .queue(Print("\n"))? .queue(SetForegroundColor(Color::Red))? .queue(Print(format!("{pending}")))? .queue(ResetColor)?; diff --git a/src/term.rs b/src/term.rs index 67ace4b8..31a951db 100644 --- a/src/term.rs +++ b/src/term.rs @@ -89,34 +89,35 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> { } } -/// Simple terminal progress bar +/// Simple terminal progress bar. pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, progress: u16, total: u16, - line_width: u16, + term_width: u16, ) -> io::Result<()> { - progress_bar_with_success(writer, 0, 0, progress, total, line_width) + progress_bar_with_success(writer, 0, 0, progress, total, term_width) } -/// Terminal progress bar with three states (pending + failed + success) + +/// Terminal progress bar with three states (pending + failed + success). pub fn progress_bar_with_success<'a>( writer: &mut impl CountedWrite<'a>, pending: u16, failed: u16, success: u16, total: u16, - line_width: u16, + term_width: u16, ) -> io::Result<()> { debug_assert!(total < 1000); - debug_assert!((pending + failed + success) <= total); + debug_assert!(pending + failed + success <= 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; + const MIN_TERM_WIDTH: u16 = WRAPPER_WIDTH + 4; - if line_width < MIN_LINE_WIDTH { + if term_width < MIN_TERM_WIDTH { writer.write_ascii(b"Progress: ")?; // Integers are in ASCII. return writer.write_ascii(format!("{}/{total}", failed + success).as_bytes()); @@ -125,7 +126,7 @@ pub fn progress_bar_with_success<'a>( let stdout = writer.stdout(); stdout.write_all(PREFIX)?; - let width = line_width - WRAPPER_WIDTH; + let width = term_width - WRAPPER_WIDTH; let mut failed_end = (width * failed) / total; let mut success_end = (width * (failed + success)) / total; let mut pending_end = (width * (failed + success + pending)) / total; diff --git a/src/watch.rs b/src/watch.rs index b9846757..6259c9df 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -108,7 +108,7 @@ fn run_watch( { ExercisesProgress::AllDone => break, ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?, - ExercisesProgress::CurrentPending => (), + ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?, }, WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Quit) => { diff --git a/src/watch/state.rs b/src/watch/state.rs index 67a63579..8b58e311 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -157,8 +157,9 @@ impl<'a> WatchState<'a> { /// Move on to the next exercise if the current one is done. pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result { - if self.done_status == DoneStatus::Pending { - return Ok(ExercisesProgress::CurrentPending); + match self.done_status { + DoneStatus::DoneWithSolution(_) | DoneStatus::DoneWithoutSolution => (), + DoneStatus::Pending => return Ok(ExercisesProgress::CurrentPending), } self.app_state.done_current_exercise::(stdout) @@ -282,14 +283,15 @@ impl<'a> WatchState<'a> { pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result { stdout.write_all(b"\n")?; - if let Some(first_fail) = self.app_state.check_all_exercises(stdout, false)? { - // Only change exercise if the current one is done... + if let Some(first_pending_exercise_ind) = self.app_state.check_all_exercises(stdout)? { + // Only change exercise if the current one is done. if self.app_state.current_exercise().done { - self.app_state.set_current_exercise_ind(first_fail)?; + self.app_state + .set_current_exercise_ind(first_pending_exercise_ind)?; + Ok(ExercisesProgress::NewPending) + } else { + Ok(ExercisesProgress::CurrentPending) } - // ...but always pretend it's a "new" anyway because that refreshes - // the display - Ok(ExercisesProgress::NewPending) } else { self.app_state.render_final_message(stdout)?; Ok(ExercisesProgress::AllDone) From 326169a7fabacda9a21377b110371f91b32e8fd3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 13 Oct 2024 22:02:41 +0200 Subject: [PATCH 194/211] Improve check-all command --- clippy.toml | 2 ++ src/app_state.rs | 5 +++++ src/main.rs | 54 ++++++++++++++++++++++++------------------------ src/run.rs | 8 +++---- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/clippy.toml b/clippy.toml index 4a5dd06a..11ec6cc3 100644 --- a/clippy.toml +++ b/clippy.toml @@ -13,4 +13,6 @@ disallowed-methods = [ # Use `thread::Builder::spawn` instead and handle the error. "std::thread::spawn", "std::thread::Scope::spawn", + # Return `ExitCode` instead. + "std::process::exit", ] diff --git a/src/app_state.rs b/src/app_state.rs index 7540181c..c3998422 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -211,6 +211,11 @@ impl AppState { self.n_done } + #[inline] + pub fn n_pending(&self) -> u16 { + self.exercises.len() as u16 - self.n_done + } + #[inline] pub fn current_exercise(&self) -> &Exercise { &self.exercises[self.current_exercise_ind] diff --git a/src/main.rs b/src/main.rs index 64b72bde..f40bb89a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use crossterm::{ use std::{ io::{self, IsTerminal, Write}, path::Path, - process::exit, + process::ExitCode, }; use term::{clear_terminal, press_enter_prompt}; @@ -51,8 +51,8 @@ enum Subcommands { /// The name of the exercise name: Option, }, - /// Run all the exercises, marking them as done or pending accordingly. - RunAll, + /// Check all the exercises, marking them as done or pending accordingly. + CheckAll, /// Reset a single exercise Reset { /// The name of the exercise @@ -68,22 +68,26 @@ enum Subcommands { Dev(DevCommands), } -fn main() -> Result<()> { +fn main() -> Result { let args = Args::parse(); if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() { bail!("{OLD_METHOD_ERR}"); } - match args.command { - Some(Subcommands::Init) => return init::init().context("Initialization failed"), - Some(Subcommands::Dev(dev_command)) => return dev_command.run(), - _ => (), + 'priority_cmd: { + match args.command { + Some(Subcommands::Init) => init::init().context("Initialization failed")?, + Some(Subcommands::Dev(dev_command)) => dev_command.run()?, + _ => break 'priority_cmd, + } + + return Ok(ExitCode::SUCCESS); } if !Path::new("exercises").is_dir() { println!("{PRE_INIT_MSG}"); - exit(1); + return Ok(ExitCode::FAILURE); } let info_file = InfoFile::parse()?; @@ -142,33 +146,29 @@ fn main() -> Result<()> { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } - run::run(&mut app_state)?; + return run::run(&mut app_state); } - Some(Subcommands::RunAll) => { + Some(Subcommands::CheckAll) => { let mut stdout = io::stdout().lock(); - if let Some(first_fail) = app_state.check_all_exercises(&mut stdout)? { - let pending = app_state - .exercises() - .iter() - .filter(|exercise| !exercise.done) - .count(); + if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? { if app_state.current_exercise().done { - app_state.set_current_exercise_ind(first_fail)?; + app_state.set_current_exercise_ind(first_pending_exercise_ind)?; } - stdout - .queue(SetForegroundColor(Color::Red))? - .queue(Print(format!("{pending}")))? - .queue(ResetColor)?; + + let pending = app_state.n_pending(); if pending == 1 { - stdout.queue(Print(" exercise has some errors: "))?; + stdout.queue(Print("One exercise pending: "))?; } else { - stdout.queue(Print(" exercises have errors, including "))?; + stdout.queue(SetForegroundColor(Color::Red))?; + write!(stdout, "{pending}")?; + stdout.queue(ResetColor)?; + stdout.queue(Print(" exercises are pending. The first: "))?; } app_state .current_exercise() .terminal_file_link(&mut stdout)?; - stdout.write_all(b".\n")?; - exit(1); + stdout.write_all(b"\n")?; + return Ok(ExitCode::FAILURE); } else { app_state.render_final_message(&mut stdout)?; } @@ -188,7 +188,7 @@ fn main() -> Result<()> { Some(Subcommands::Init | Subcommands::Dev(_)) => (), } - Ok(()) + Ok(ExitCode::SUCCESS) } const OLD_METHOD_ERR: &str = diff --git a/src/run.rs b/src/run.rs index 3fddcf22..f259f52c 100644 --- a/src/run.rs +++ b/src/run.rs @@ -5,7 +5,7 @@ use crossterm::{ }; use std::{ io::{self, Write}, - process::exit, + process::ExitCode, }; use crate::{ @@ -13,7 +13,7 @@ use crate::{ exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, }; -pub fn run(app_state: &mut AppState) -> Result<()> { +pub fn run(app_state: &mut AppState) -> Result { let exercise = app_state.current_exercise(); let mut output = Vec::with_capacity(OUTPUT_CAPACITY); let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?; @@ -29,7 +29,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { .current_exercise() .terminal_file_link(&mut stdout)?; stdout.write_all(b" with errors\n")?; - exit(1); + return Ok(ExitCode::FAILURE); } stdout.queue(SetForegroundColor(Color::Green))?; @@ -55,5 +55,5 @@ pub fn run(app_state: &mut AppState) -> Result<()> { ExercisesProgress::AllDone => (), } - Ok(()) + Ok(ExitCode::SUCCESS) } From 396ee4d618bc5e1cd5c84495f571f9d3f79774c8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 13 Oct 2024 23:28:17 +0200 Subject: [PATCH 195/211] Show progress with exercise numbers --- src/app_state.rs | 126 +++++++++++++++++---------------------------- src/main.rs | 15 +++--- src/term.rs | 130 ++++++++++++++++++++++------------------------- 3 files changed, 113 insertions(+), 158 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index c3998422..db9d1f10 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,8 +1,5 @@ use anyhow::{bail, Context, Error, Result}; -use crossterm::{ - style::{ResetColor, SetForegroundColor}, - terminal, QueueableCommand, -}; +use crossterm::{cursor, terminal, QueueableCommand}; use std::{ env, fs::{File, OpenOptions}, @@ -23,7 +20,7 @@ use crate::{ embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, - term::{self, progress_bar_with_success}, + term::{self, show_exercises_check_progress}, }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; @@ -44,18 +41,12 @@ pub enum StateFileStatus { NotRead, } -enum ExerciseCheckProgress { +#[derive(Clone, Copy)] +pub enum ExerciseCheckProgress { + None, Checking, Done, Pending, - Error, -} - -#[derive(Clone, Copy)] -enum ExerciseCheckResult { - Done, - Pending, - Error, } pub struct AppState { @@ -417,27 +408,25 @@ impl AppState { } } - // Return the exercise index of the first pending exercise found. - pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result> { + fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result> { stdout.write_all("Checking all exercises…\n".as_bytes())?; - let n_exercises = self.exercises.len() as u16; let next_exercise_ind = AtomicUsize::new(0); let term_width = terminal::size() .context("Failed to get the terminal size")? .0; + clear_terminal(stdout)?; - let mut results = vec![ExerciseCheckResult::Error; self.exercises.len()]; + let mut progresses = vec![ExerciseCheckProgress::None; self.exercises.len()]; let mut done = 0; let mut pending = 0; thread::scope(|s| { - let mut checking = 0; - let (exercise_result_sender, exercise_result_receiver) = mpsc::channel(); + let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel(); let n_threads = thread::available_parallelism() .map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get()); for _ in 0..n_threads { - let exercise_result_sender = exercise_result_sender.clone(); + let exercise_progress_sender = exercise_progress_sender.clone(); let next_exercise_ind = &next_exercise_ind; let slf = &self; thread::Builder::new() @@ -449,7 +438,7 @@ impl AppState { }; // Notify the progress bar that this exercise is pending. - if exercise_result_sender + if exercise_progress_sender .send((exercise_ind, ExerciseCheckProgress::Checking)) .is_err() { @@ -457,14 +446,17 @@ impl AppState { }; let success = exercise.run_exercise(None, &slf.cmd_runner); - let result = match success { + let progress = match success { Ok(true) => ExerciseCheckProgress::Done, Ok(false) => ExerciseCheckProgress::Pending, - Err(_) => ExerciseCheckProgress::Error, + Err(_) => ExerciseCheckProgress::None, }; // Notify the progress bar that this exercise is done. - if exercise_result_sender.send((exercise_ind, result)).is_err() { + if exercise_progress_sender + .send((exercise_ind, progress)) + .is_err() + { break; } }) @@ -472,102 +464,76 @@ impl AppState { } // Drop this sender to detect when the last thread is done. - drop(exercise_result_sender); + drop(exercise_progress_sender); - // Print the legend. - stdout.write_all(b"Color legend: ")?; - stdout.queue(SetForegroundColor(term::PROGRESS_FAILED_COLOR))?; - stdout.write_all(b"Pending")?; - stdout.queue(ResetColor)?; - stdout.write_all(b" - ")?; - stdout.queue(SetForegroundColor(term::PROGRESS_SUCCESS_COLOR))?; - stdout.write_all(b"Done")?; - stdout.queue(ResetColor)?; - stdout.write_all(b" - ")?; - stdout.queue(SetForegroundColor(term::PROGRESS_PENDING_COLOR))?; - stdout.write_all(b"Checking")?; - stdout.queue(ResetColor)?; - stdout.write_all(b"\n")?; + while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() { + progresses[exercise_ind] = progress; - while let Ok((exercise_ind, result)) = exercise_result_receiver.recv() { - match result { - ExerciseCheckProgress::Checking => checking += 1, - ExerciseCheckProgress::Done => { - results[exercise_ind] = ExerciseCheckResult::Done; - checking -= 1; - done += 1; - } - ExerciseCheckProgress::Pending => { - results[exercise_ind] = ExerciseCheckResult::Pending; - checking -= 1; - pending += 1; - } - ExerciseCheckProgress::Error => checking -= 1, + match progress { + ExerciseCheckProgress::None | ExerciseCheckProgress::Checking => (), + ExerciseCheckProgress::Done => done += 1, + ExerciseCheckProgress::Pending => pending += 1, } - stdout.write_all(b"\r")?; - progress_bar_with_success( - stdout, - checking, - pending, - done, - n_exercises, - term_width, - )?; - stdout.flush()?; + show_exercises_check_progress(stdout, &progresses, term_width)?; } Ok::<_, Error>(()) })?; let mut first_pending_exercise_ind = None; - for (exercise_ind, result) in results.into_iter().enumerate() { - match result { - ExerciseCheckResult::Done => { + for exercise_ind in 0..progresses.len() { + match progresses[exercise_ind] { + ExerciseCheckProgress::Done => { self.set_status(exercise_ind, true)?; } - ExerciseCheckResult::Pending => { + ExerciseCheckProgress::Pending => { self.set_status(exercise_ind, false)?; if first_pending_exercise_ind.is_none() { first_pending_exercise_ind = Some(exercise_ind); } } - ExerciseCheckResult::Error => { + ExerciseCheckProgress::None | ExerciseCheckProgress::Checking => { // If we got an error while checking all exercises in parallel, // it could be because we exceeded the limit of open file descriptors. // Therefore, try running exercises with errors sequentially. + progresses[exercise_ind] = ExerciseCheckProgress::Checking; + show_exercises_check_progress(stdout, &progresses, term_width)?; + let exercise = &self.exercises[exercise_ind]; let success = exercise.run_exercise(None, &self.cmd_runner)?; if success { done += 1; + progresses[exercise_ind] = ExerciseCheckProgress::Done; } else { pending += 1; if first_pending_exercise_ind.is_none() { first_pending_exercise_ind = Some(exercise_ind); } + progresses[exercise_ind] = ExerciseCheckProgress::Pending; } self.set_status(exercise_ind, success)?; - stdout.write_all(b"\r")?; - progress_bar_with_success( - stdout, - u16::from(pending + done < n_exercises), - pending, - done, - n_exercises, - term_width, - )?; - stdout.flush()?; + show_exercises_check_progress(stdout, &progresses, term_width)?; } } } self.write()?; - stdout.write_all(b"\n\n")?; + stdout.write_all(b"\n")?; Ok(first_pending_exercise_ind) } + // Return the exercise index of the first pending exercise found. + pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result> { + stdout.queue(cursor::Hide)?; + let res = self.check_all_exercises_impl(stdout); + stdout.queue(cursor::Show)?; + + res + } + /// Mark the current exercise as done and move on to the next pending exercise if one exists. /// If all exercises are marked as done, run all of them to make sure that they are actually /// done. If an exercise which is marked as done fails, mark it as pending and continue on it. diff --git a/src/main.rs b/src/main.rs index f40bb89a..075e7265 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,6 @@ use anyhow::{bail, Context, Result}; use app_state::StateFileStatus; use clap::{Parser, Subcommand}; -use crossterm::{ - style::{Color, Print, ResetColor, SetForegroundColor}, - QueueableCommand, -}; use std::{ io::{self, IsTerminal, Write}, path::Path, @@ -157,12 +153,13 @@ fn main() -> Result { let pending = app_state.n_pending(); if pending == 1 { - stdout.queue(Print("One exercise pending: "))?; + stdout.write_all(b"One exercise pending: ")?; } else { - stdout.queue(SetForegroundColor(Color::Red))?; - write!(stdout, "{pending}")?; - stdout.queue(ResetColor)?; - stdout.queue(Print(" exercises are pending. The first: "))?; + write!( + stdout, + "{pending}/{} exercises are pending. The first: ", + app_state.exercises().len(), + )?; } app_state .current_exercise() diff --git a/src/term.rs b/src/term.rs index 31a951db..0294017b 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,6 +1,6 @@ use crossterm::{ cursor::MoveTo, - style::{Attribute, Color, SetAttribute, SetForegroundColor}, + style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, terminal::{Clear, ClearType}, Command, QueueableCommand, }; @@ -9,9 +9,7 @@ use std::{ io::{self, BufRead, StdoutLock, Write}, }; -pub const PROGRESS_FAILED_COLOR: Color = Color::Red; -pub const PROGRESS_SUCCESS_COLOR: Color = Color::Green; -pub const PROGRESS_PENDING_COLOR: Color = Color::Blue; +use crate::app_state::ExerciseCheckProgress; pub struct MaxLenWriter<'a, 'b> { pub stdout: &'a mut StdoutLock<'b>, @@ -89,98 +87,43 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> { } } -/// Simple terminal progress bar. pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, progress: u16, total: u16, term_width: u16, -) -> io::Result<()> { - progress_bar_with_success(writer, 0, 0, progress, total, term_width) -} - -/// Terminal progress bar with three states (pending + failed + success). -pub fn progress_bar_with_success<'a>( - writer: &mut impl CountedWrite<'a>, - pending: u16, - failed: u16, - success: u16, - total: u16, - term_width: u16, ) -> io::Result<()> { debug_assert!(total < 1000); - debug_assert!(pending + failed + success <= total); + 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_TERM_WIDTH: u16 = WRAPPER_WIDTH + 4; + const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; - if term_width < MIN_TERM_WIDTH { + if term_width < MIN_LINE_WIDTH { writer.write_ascii(b"Progress: ")?; // Integers are in ASCII. - return writer.write_ascii(format!("{}/{total}", failed + success).as_bytes()); + return writer.write_ascii(format!("{progress}/{total}").as_bytes()); } let stdout = writer.stdout(); stdout.write_all(PREFIX)?; let width = term_width - WRAPPER_WIDTH; - let mut failed_end = (width * failed) / total; - let mut success_end = (width * (failed + success)) / total; - let mut pending_end = (width * (failed + success + pending)) / total; + let filled = (width * progress) / total; - // In case the range boundaries overlap, "pending" has priority over both - // "failed" and "success" (don't show the bar as "complete" when we are - // still checking some things). - // "Failed" has priority over "success" (don't show 100% success if we - // have some failures, at the risk of showing 100% failures even with - // a few successes). - // - // "Failed" already has priority over "success" because it's displayed - // first. But "pending" is last so we need to fix "success"/"failed". - if pending > 0 { - pending_end = pending_end.max(1); - if pending_end == success_end { - success_end -= 1; - } - if pending_end == failed_end { - failed_end -= 1; - } - - // This will replace the last character of the "pending" range with - // the arrow char ('>'). This ensures that even if the progress bar - // is filled (everything either done or pending), we'll still see - // the '>' as long as we are not fully done. - pending_end -= 1; - } - - if failed > 0 { - stdout.queue(SetForegroundColor(PROGRESS_FAILED_COLOR))?; - for _ in 0..failed_end { - stdout.write_all(b"#")?; - } - } - - stdout.queue(SetForegroundColor(PROGRESS_SUCCESS_COLOR))?; - for _ in failed_end..success_end { + stdout.queue(SetForegroundColor(Color::Green))?; + for _ in 0..filled { stdout.write_all(b"#")?; } - if pending > 0 { - stdout.queue(SetForegroundColor(PROGRESS_PENDING_COLOR))?; - - for _ in success_end..pending_end { - stdout.write_all(b"#")?; - } - } - - if pending_end < width { + if filled < width { stdout.write_all(b">")?; } - let width_minus_filled = width - pending_end; + let width_minus_filled = width - filled; if width_minus_filled > 1 { let red_part_width = width_minus_filled - 1; stdout.queue(SetForegroundColor(Color::Red))?; @@ -191,7 +134,56 @@ pub fn progress_bar_with_success<'a>( stdout.queue(SetForegroundColor(Color::Reset))?; - write!(stdout, "] {:>3}/{}", failed + success, total) + write!(stdout, "] {progress:>3}/{total}") +} + +pub fn show_exercises_check_progress( + stdout: &mut StdoutLock, + progresses: &[ExerciseCheckProgress], + term_width: u16, +) -> io::Result<()> { + stdout.queue(MoveTo(0, 0))?; + + // Legend + stdout.write_all(b"Color of exercise number: ")?; + stdout.queue(SetForegroundColor(Color::Blue))?; + stdout.write_all(b"Checking")?; + stdout.queue(ResetColor)?; + stdout.write_all(b" - ")?; + stdout.queue(SetForegroundColor(Color::Green))?; + stdout.write_all(b"Done")?; + stdout.queue(ResetColor)?; + stdout.write_all(b" - ")?; + stdout.queue(SetForegroundColor(Color::Red))?; + stdout.write_all(b"Pending")?; + stdout.queue(ResetColor)?; + stdout.write_all(b"\n")?; + + // Exercise numbers with up to 3 digits. + let n_cols = usize::from(term_width + 1) / 4; + + let mut exercise_num = 1; + for exercise_progress in progresses { + let color = match exercise_progress { + ExerciseCheckProgress::None => Color::Reset, + ExerciseCheckProgress::Checking => Color::Blue, + ExerciseCheckProgress::Done => Color::Green, + ExerciseCheckProgress::Pending => Color::Red, + }; + + stdout.queue(SetForegroundColor(color))?; + write!(stdout, "{exercise_num:<3}")?; + + if exercise_num % n_cols == 0 { + stdout.write_all(b"\n")?; + } else { + stdout.write_all(b" ")?; + } + + exercise_num += 1; + } + + stdout.queue(ResetColor)?.flush() } pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { From 8cac21511cbcc148ea7a4c8c6d196c9c0bf17255 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 14 Oct 2024 00:42:49 +0200 Subject: [PATCH 196/211] Small improvements to showing progress --- src/app_state.rs | 1 - src/main.rs | 4 +++- src/term.rs | 40 ++++++++++++++++++++++++---------------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index db9d1f10..41231ef9 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -520,7 +520,6 @@ impl AppState { } self.write()?; - stdout.write_all(b"\n")?; Ok(first_pending_exercise_ind) } diff --git a/src/main.rs b/src/main.rs index 075e7265..5616d264 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,13 +151,15 @@ fn main() -> Result { app_state.set_current_exercise_ind(first_pending_exercise_ind)?; } + stdout.write_all(b"\n\n")?; + let pending = app_state.n_pending(); if pending == 1 { stdout.write_all(b"One exercise pending: ")?; } else { write!( stdout, - "{pending}/{} exercises are pending. The first: ", + "{pending}/{} exercises pending. The first: ", app_state.exercises().len(), )?; } diff --git a/src/term.rs b/src/term.rs index 0294017b..8a2f8c59 100644 --- a/src/term.rs +++ b/src/term.rs @@ -164,26 +164,34 @@ pub fn show_exercises_check_progress( let mut exercise_num = 1; for exercise_progress in progresses { - let color = match exercise_progress { - ExerciseCheckProgress::None => Color::Reset, - ExerciseCheckProgress::Checking => Color::Blue, - ExerciseCheckProgress::Done => Color::Green, - ExerciseCheckProgress::Pending => Color::Red, - }; - - stdout.queue(SetForegroundColor(color))?; - write!(stdout, "{exercise_num:<3}")?; - - if exercise_num % n_cols == 0 { - stdout.write_all(b"\n")?; - } else { - stdout.write_all(b" ")?; + match exercise_progress { + ExerciseCheckProgress::None => (), + ExerciseCheckProgress::Checking => { + stdout.queue(SetForegroundColor(Color::Blue))?; + } + ExerciseCheckProgress::Done => { + stdout.queue(SetForegroundColor(Color::Green))?; + } + ExerciseCheckProgress::Pending => { + stdout.queue(SetForegroundColor(Color::Red))?; + } } - exercise_num += 1; + write!(stdout, "{exercise_num:<3}")?; + stdout.queue(ResetColor)?; + + if exercise_num != progresses.len() { + if exercise_num % n_cols == 0 { + stdout.write_all(b"\n")?; + } else { + stdout.write_all(b" ")?; + } + + exercise_num += 1; + } } - stdout.queue(ResetColor)?.flush() + stdout.flush() } pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { From 9705c161b4d9b7fc8b071978f57b35a1b0c69819 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 14 Oct 2024 00:45:41 +0200 Subject: [PATCH 197/211] Remove the tracking of done and pending --- src/app_state.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 41231ef9..d2dd87be 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -417,8 +417,6 @@ impl AppState { clear_terminal(stdout)?; let mut progresses = vec![ExerciseCheckProgress::None; self.exercises.len()]; - let mut done = 0; - let mut pending = 0; thread::scope(|s| { let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel(); @@ -468,13 +466,6 @@ impl AppState { while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() { progresses[exercise_ind] = progress; - - match progress { - ExerciseCheckProgress::None | ExerciseCheckProgress::Checking => (), - ExerciseCheckProgress::Done => done += 1, - ExerciseCheckProgress::Pending => pending += 1, - } - show_exercises_check_progress(stdout, &progresses, term_width)?; } @@ -503,10 +494,8 @@ impl AppState { let exercise = &self.exercises[exercise_ind]; let success = exercise.run_exercise(None, &self.cmd_runner)?; if success { - done += 1; progresses[exercise_ind] = ExerciseCheckProgress::Done; } else { - pending += 1; if first_pending_exercise_ind.is_none() { first_pending_exercise_ind = Some(exercise_ind); } From fc5fc0920f3590d1b1e8c8186309ac1c5ec6fba5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 14 Oct 2024 00:48:12 +0200 Subject: [PATCH 198/211] Remove outdated comments --- src/app_state.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index d2dd87be..76a4c454 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -435,7 +435,6 @@ impl AppState { break; }; - // Notify the progress bar that this exercise is pending. if exercise_progress_sender .send((exercise_ind, ExerciseCheckProgress::Checking)) .is_err() @@ -450,7 +449,6 @@ impl AppState { Err(_) => ExerciseCheckProgress::None, }; - // Notify the progress bar that this exercise is done. if exercise_progress_sender .send((exercise_ind, progress)) .is_err() From ea73af9ba37bc1f6155a910c72f2ded8a0b64805 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 14 Oct 2024 01:06:11 +0200 Subject: [PATCH 199/211] Separate initialization with a struct --- src/app_state.rs | 13 +++-- src/term.rs | 125 ++++++++++++++++++++++++++--------------------- 2 files changed, 74 insertions(+), 64 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 76a4c454..57ffea85 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -20,7 +20,7 @@ use crate::{ embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, - term::{self, show_exercises_check_progress}, + term::{self, ExercisesCheckProgressVisualizer}, }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; @@ -409,13 +409,12 @@ impl AppState { } fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result> { - stdout.write_all("Checking all exercises…\n".as_bytes())?; - let next_exercise_ind = AtomicUsize::new(0); let term_width = terminal::size() .context("Failed to get the terminal size")? .0; - clear_terminal(stdout)?; + let mut progress_visualizer = ExercisesCheckProgressVisualizer::build(stdout, term_width)?; + let next_exercise_ind = AtomicUsize::new(0); let mut progresses = vec![ExerciseCheckProgress::None; self.exercises.len()]; thread::scope(|s| { @@ -464,7 +463,7 @@ impl AppState { while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() { progresses[exercise_ind] = progress; - show_exercises_check_progress(stdout, &progresses, term_width)?; + progress_visualizer.update(&progresses)?; } Ok::<_, Error>(()) @@ -487,7 +486,7 @@ impl AppState { // it could be because we exceeded the limit of open file descriptors. // Therefore, try running exercises with errors sequentially. progresses[exercise_ind] = ExerciseCheckProgress::Checking; - show_exercises_check_progress(stdout, &progresses, term_width)?; + progress_visualizer.update(&progresses)?; let exercise = &self.exercises[exercise_ind]; let success = exercise.run_exercise(None, &self.cmd_runner)?; @@ -501,7 +500,7 @@ impl AppState { } self.set_status(exercise_ind, success)?; - show_exercises_check_progress(stdout, &progresses, term_width)?; + progress_visualizer.update(&progresses)?; } } } diff --git a/src/term.rs b/src/term.rs index 8a2f8c59..13d5657b 100644 --- a/src/term.rs +++ b/src/term.rs @@ -87,6 +87,74 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> { } } +pub struct ExercisesCheckProgressVisualizer<'a, 'b> { + stdout: &'a mut StdoutLock<'b>, + n_cols: usize, +} + +impl<'a, 'b> ExercisesCheckProgressVisualizer<'a, 'b> { + pub fn build(stdout: &'a mut StdoutLock<'b>, term_width: u16) -> io::Result { + clear_terminal(stdout)?; + stdout.write_all("Checking all exercises…\n".as_bytes())?; + + // Legend + stdout.write_all(b"Color of exercise number: ")?; + stdout.queue(SetForegroundColor(Color::Blue))?; + stdout.write_all(b"Checking")?; + stdout.queue(ResetColor)?; + stdout.write_all(b" - ")?; + stdout.queue(SetForegroundColor(Color::Green))?; + stdout.write_all(b"Done")?; + stdout.queue(ResetColor)?; + stdout.write_all(b" - ")?; + stdout.queue(SetForegroundColor(Color::Red))?; + stdout.write_all(b"Pending")?; + stdout.queue(ResetColor)?; + stdout.write_all(b"\n")?; + + // Exercise numbers with up to 3 digits. + // +1 because the last column doesn't end with a whitespace. + let n_cols = usize::from(term_width + 1) / 4; + + Ok(Self { stdout, n_cols }) + } + + pub fn update(&mut self, progresses: &[ExerciseCheckProgress]) -> io::Result<()> { + self.stdout.queue(MoveTo(0, 2))?; + + let mut exercise_num = 1; + for exercise_progress in progresses { + match exercise_progress { + ExerciseCheckProgress::None => (), + ExerciseCheckProgress::Checking => { + self.stdout.queue(SetForegroundColor(Color::Blue))?; + } + ExerciseCheckProgress::Done => { + self.stdout.queue(SetForegroundColor(Color::Green))?; + } + ExerciseCheckProgress::Pending => { + self.stdout.queue(SetForegroundColor(Color::Red))?; + } + } + + write!(self.stdout, "{exercise_num:<3}")?; + self.stdout.queue(ResetColor)?; + + if exercise_num != progresses.len() { + if exercise_num % self.n_cols == 0 { + self.stdout.write_all(b"\n")?; + } else { + self.stdout.write_all(b" ")?; + } + + exercise_num += 1; + } + } + + self.stdout.flush() + } +} + pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, progress: u16, @@ -137,63 +205,6 @@ pub fn progress_bar<'a>( write!(stdout, "] {progress:>3}/{total}") } -pub fn show_exercises_check_progress( - stdout: &mut StdoutLock, - progresses: &[ExerciseCheckProgress], - term_width: u16, -) -> io::Result<()> { - stdout.queue(MoveTo(0, 0))?; - - // Legend - stdout.write_all(b"Color of exercise number: ")?; - stdout.queue(SetForegroundColor(Color::Blue))?; - stdout.write_all(b"Checking")?; - stdout.queue(ResetColor)?; - stdout.write_all(b" - ")?; - stdout.queue(SetForegroundColor(Color::Green))?; - stdout.write_all(b"Done")?; - stdout.queue(ResetColor)?; - stdout.write_all(b" - ")?; - stdout.queue(SetForegroundColor(Color::Red))?; - stdout.write_all(b"Pending")?; - stdout.queue(ResetColor)?; - stdout.write_all(b"\n")?; - - // Exercise numbers with up to 3 digits. - let n_cols = usize::from(term_width + 1) / 4; - - let mut exercise_num = 1; - for exercise_progress in progresses { - match exercise_progress { - ExerciseCheckProgress::None => (), - ExerciseCheckProgress::Checking => { - stdout.queue(SetForegroundColor(Color::Blue))?; - } - ExerciseCheckProgress::Done => { - stdout.queue(SetForegroundColor(Color::Green))?; - } - ExerciseCheckProgress::Pending => { - stdout.queue(SetForegroundColor(Color::Red))?; - } - } - - write!(stdout, "{exercise_num:<3}")?; - stdout.queue(ResetColor)?; - - if exercise_num != progresses.len() { - if exercise_num % n_cols == 0 { - stdout.write_all(b"\n")?; - } else { - stdout.write_all(b" ")?; - } - - exercise_num += 1; - } - } - - stdout.flush() -} - pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { stdout .queue(MoveTo(0, 0))? From bdc6dad8de2d3b47e33098fd55956ba03b131b27 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 14 Oct 2024 01:28:12 +0200 Subject: [PATCH 200/211] Update names --- src/app_state.rs | 29 ++++++++++++++--------------- src/term.rs | 33 +++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 57ffea85..5f84d35d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -20,7 +20,7 @@ use crate::{ embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, - term::{self, ExercisesCheckProgressVisualizer}, + term::{self, CheckProgressVisualizer}, }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; @@ -42,7 +42,7 @@ pub enum StateFileStatus { } #[derive(Clone, Copy)] -pub enum ExerciseCheckProgress { +pub enum CheckProgress { None, Checking, Done, @@ -412,10 +412,10 @@ impl AppState { let term_width = terminal::size() .context("Failed to get the terminal size")? .0; - let mut progress_visualizer = ExercisesCheckProgressVisualizer::build(stdout, term_width)?; + let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?; let next_exercise_ind = AtomicUsize::new(0); - let mut progresses = vec![ExerciseCheckProgress::None; self.exercises.len()]; + let mut progresses = vec![CheckProgress::None; self.exercises.len()]; thread::scope(|s| { let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel(); @@ -435,7 +435,7 @@ impl AppState { }; if exercise_progress_sender - .send((exercise_ind, ExerciseCheckProgress::Checking)) + .send((exercise_ind, CheckProgress::Checking)) .is_err() { break; @@ -443,9 +443,9 @@ impl AppState { let success = exercise.run_exercise(None, &slf.cmd_runner); let progress = match success { - Ok(true) => ExerciseCheckProgress::Done, - Ok(false) => ExerciseCheckProgress::Pending, - Err(_) => ExerciseCheckProgress::None, + Ok(true) => CheckProgress::Done, + Ok(false) => CheckProgress::Pending, + Err(_) => CheckProgress::None, }; if exercise_progress_sender @@ -472,34 +472,33 @@ impl AppState { let mut first_pending_exercise_ind = None; for exercise_ind in 0..progresses.len() { match progresses[exercise_ind] { - ExerciseCheckProgress::Done => { + CheckProgress::Done => { self.set_status(exercise_ind, true)?; } - ExerciseCheckProgress::Pending => { + CheckProgress::Pending => { self.set_status(exercise_ind, false)?; if first_pending_exercise_ind.is_none() { first_pending_exercise_ind = Some(exercise_ind); } } - ExerciseCheckProgress::None | ExerciseCheckProgress::Checking => { + CheckProgress::None | CheckProgress::Checking => { // If we got an error while checking all exercises in parallel, // it could be because we exceeded the limit of open file descriptors. // Therefore, try running exercises with errors sequentially. - progresses[exercise_ind] = ExerciseCheckProgress::Checking; + progresses[exercise_ind] = CheckProgress::Checking; progress_visualizer.update(&progresses)?; let exercise = &self.exercises[exercise_ind]; let success = exercise.run_exercise(None, &self.cmd_runner)?; if success { - progresses[exercise_ind] = ExerciseCheckProgress::Done; + progresses[exercise_ind] = CheckProgress::Done; } else { + progresses[exercise_ind] = CheckProgress::Pending; if first_pending_exercise_ind.is_none() { first_pending_exercise_ind = Some(exercise_ind); } - progresses[exercise_ind] = ExerciseCheckProgress::Pending; } self.set_status(exercise_ind, success)?; - progress_visualizer.update(&progresses)?; } } diff --git a/src/term.rs b/src/term.rs index 13d5657b..86909f08 100644 --- a/src/term.rs +++ b/src/term.rs @@ -9,7 +9,7 @@ use std::{ io::{self, BufRead, StdoutLock, Write}, }; -use crate::app_state::ExerciseCheckProgress; +use crate::app_state::CheckProgress; pub struct MaxLenWriter<'a, 'b> { pub stdout: &'a mut StdoutLock<'b>, @@ -87,27 +87,31 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> { } } -pub struct ExercisesCheckProgressVisualizer<'a, 'b> { +pub struct CheckProgressVisualizer<'a, 'b> { stdout: &'a mut StdoutLock<'b>, n_cols: usize, } -impl<'a, 'b> ExercisesCheckProgressVisualizer<'a, 'b> { +impl<'a, 'b> CheckProgressVisualizer<'a, 'b> { + const CHECKING_COLOR: Color = Color::Blue; + const DONE_COLOR: Color = Color::Green; + const PENDING_COLOR: Color = Color::Red; + pub fn build(stdout: &'a mut StdoutLock<'b>, term_width: u16) -> io::Result { clear_terminal(stdout)?; stdout.write_all("Checking all exercises…\n".as_bytes())?; // Legend stdout.write_all(b"Color of exercise number: ")?; - stdout.queue(SetForegroundColor(Color::Blue))?; + stdout.queue(SetForegroundColor(Self::CHECKING_COLOR))?; stdout.write_all(b"Checking")?; stdout.queue(ResetColor)?; stdout.write_all(b" - ")?; - stdout.queue(SetForegroundColor(Color::Green))?; + stdout.queue(SetForegroundColor(Self::DONE_COLOR))?; stdout.write_all(b"Done")?; stdout.queue(ResetColor)?; stdout.write_all(b" - ")?; - stdout.queue(SetForegroundColor(Color::Red))?; + stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?; stdout.write_all(b"Pending")?; stdout.queue(ResetColor)?; stdout.write_all(b"\n")?; @@ -119,21 +123,22 @@ impl<'a, 'b> ExercisesCheckProgressVisualizer<'a, 'b> { Ok(Self { stdout, n_cols }) } - pub fn update(&mut self, progresses: &[ExerciseCheckProgress]) -> io::Result<()> { + pub fn update(&mut self, progresses: &[CheckProgress]) -> io::Result<()> { self.stdout.queue(MoveTo(0, 2))?; let mut exercise_num = 1; for exercise_progress in progresses { match exercise_progress { - ExerciseCheckProgress::None => (), - ExerciseCheckProgress::Checking => { - self.stdout.queue(SetForegroundColor(Color::Blue))?; + CheckProgress::None => (), + CheckProgress::Checking => { + self.stdout + .queue(SetForegroundColor(Self::CHECKING_COLOR))?; } - ExerciseCheckProgress::Done => { - self.stdout.queue(SetForegroundColor(Color::Green))?; + CheckProgress::Done => { + self.stdout.queue(SetForegroundColor(Self::DONE_COLOR))?; } - ExerciseCheckProgress::Pending => { - self.stdout.queue(SetForegroundColor(Color::Red))?; + CheckProgress::Pending => { + self.stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?; } } From 932bc25d8824e18debc91e5f25f022e8d066bcf8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 14 Oct 2024 01:28:34 +0200 Subject: [PATCH 201/211] Remove unneeded line --- src/main.rs | 2 +- src/run.rs | 1 + src/watch/state.rs | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5616d264..c8bcd2e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,7 +152,6 @@ fn main() -> Result { } stdout.write_all(b"\n\n")?; - let pending = app_state.n_pending(); if pending == 1 { stdout.write_all(b"One exercise pending: ")?; @@ -167,6 +166,7 @@ fn main() -> Result { .current_exercise() .terminal_file_link(&mut stdout)?; stdout.write_all(b"\n")?; + return Ok(ExitCode::FAILURE); } else { app_state.render_final_message(&mut stdout)?; diff --git a/src/run.rs b/src/run.rs index f259f52c..ac8b26ad 100644 --- a/src/run.rs +++ b/src/run.rs @@ -29,6 +29,7 @@ pub fn run(app_state: &mut AppState) -> Result { .current_exercise() .terminal_file_link(&mut stdout)?; stdout.write_all(b" with errors\n")?; + return Ok(ExitCode::FAILURE); } diff --git a/src/watch/state.rs b/src/watch/state.rs index 8b58e311..0ac758ce 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -281,8 +281,6 @@ impl<'a> WatchState<'a> { } pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result { - stdout.write_all(b"\n")?; - if let Some(first_pending_exercise_ind) = self.app_state.check_all_exercises(stdout)? { // Only change exercise if the current one is done. if self.app_state.current_exercise().done { From a675cb5754309ba9997fd2344ab0a364688de430 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 14 Oct 2024 15:24:42 +0200 Subject: [PATCH 202/211] Replace ahash with foldhash --- Cargo.lock | 86 +++++++++++++++------------------------------- Cargo.toml | 6 ++-- clippy.toml | 2 +- src/collections.rs | 9 +++-- 4 files changed, 35 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a541c67..f89c139f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "anstream" version = "0.6.15" @@ -71,9 +59,9 @@ checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" @@ -95,9 +83,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -105,9 +93,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -198,6 +186,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -209,9 +203,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heck" @@ -227,9 +221,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", @@ -377,9 +371,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "os_pipe" @@ -416,9 +410,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -434,9 +428,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -458,10 +452,10 @@ dependencies = [ name = "rustlings" version = "6.3.0" dependencies = [ - "ahash", "anyhow", "clap", "crossterm", + "foldhash", "notify", "os_pipe", "rustix", @@ -587,9 +581,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -598,9 +592,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -643,12 +637,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -852,23 +840,3 @@ checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index d4134fd8..eb22cfee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,10 +46,10 @@ include = [ ] [dependencies] -ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.89" -clap = { version = "4.5.18", features = ["derive"] } +clap = { version = "4.5.20", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } +foldhash = "0.1.3" notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] } os_pipe = "1.2.1" rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } @@ -61,7 +61,7 @@ toml_edit.workspace = true rustix = { version = "0.38.37", default-features = false, features = ["std", "stdio", "termios"] } [dev-dependencies] -tempfile = "3.12.0" +tempfile = "3.13.0" [profile.release] panic = "abort" diff --git a/clippy.toml b/clippy.toml index 11ec6cc3..2a981849 100644 --- a/clippy.toml +++ b/clippy.toml @@ -5,7 +5,7 @@ disallowed-types = [ ] disallowed-methods = [ - # We use `ahash` instead of the default hasher. + # We use `foldhash` instead of the default hasher. "std::collections::HashSet::new", "std::collections::HashSet::with_capacity", # Inefficient. Use `.queue(…)` instead. diff --git a/src/collections.rs b/src/collections.rs index fa9e3fa7..3f2841e0 100644 --- a/src/collections.rs +++ b/src/collections.rs @@ -1,10 +1,9 @@ -use ahash::AHasher; -use std::hash::BuildHasherDefault; +use foldhash::fast::FixedState; -/// DOS attacks aren't a concern for Rustlings. Therefore, we use `ahash` with fixed seeds. -pub type HashSet = std::collections::HashSet>; +/// DOS attacks aren't a concern for Rustlings. Therefore, we use `foldhash` with a fixed state. +pub type HashSet = std::collections::HashSet; #[inline] pub fn hash_set_with_capacity(capacity: usize) -> HashSet { - HashSet::with_capacity_and_hasher(capacity, BuildHasherDefault::::default()) + HashSet::with_capacity_and_hasher(capacity, FixedState::default()) } From 990a722852ab22b55db342f93ebe03e6ed122f7f Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 14 Oct 2024 15:57:44 +0200 Subject: [PATCH 203/211] Limit the maximum number of exercises to 999 --- src/dev/check.rs | 5 +++++ src/term.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 5a7aaed4..bd73ec8c 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -17,6 +17,7 @@ use crate::{ CURRENT_FORMAT_VERSION, }; +const MAX_N_EXERCISES: usize = 999; const MAX_EXERCISE_NAME_LEN: usize = 32; // Find a char that isn't allowed in the exercise's `name` or `dir`. @@ -347,6 +348,10 @@ fn check_solutions( pub fn check(require_solutions: bool) -> Result<()> { let info_file = InfoFile::parse()?; + if info_file.exercises.len() > MAX_N_EXERCISES { + bail!("The maximum number of exercises is {MAX_N_EXERCISES}"); + } + if cfg!(debug_assertions) { // A hack to make `cargo run -- dev check` work when developing Rustlings. check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?; diff --git a/src/term.rs b/src/term.rs index 86909f08..e3bfd7bb 100644 --- a/src/term.rs +++ b/src/term.rs @@ -166,7 +166,7 @@ pub fn progress_bar<'a>( total: u16, term_width: u16, ) -> io::Result<()> { - debug_assert!(total < 1000); + debug_assert!(total <= 999); debug_assert!(progress <= total); const PREFIX: &[u8] = b"Progress: ["; From f33ba139b460ee433c6738ab050534b951436d6d Mon Sep 17 00:00:00 2001 From: Nahor Date: Mon, 14 Oct 2024 10:13:30 -0700 Subject: [PATCH 204/211] Fix typos --- src/app_state.rs | 6 +++--- src/dev/check.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 5f84d35d..4007fbc3 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -148,11 +148,11 @@ impl AppState { let mut done_exercises = hash_set_with_capacity(exercises.len()); - for done_exerise_name in lines { - if done_exerise_name.is_empty() { + for done_exercise_name in lines { + if done_exercise_name.is_empty() { break; } - done_exercises.insert(done_exerise_name); + done_exercises.insert(done_exercise_name); } for (ind, exercise) in exercises.iter_mut().enumerate() { diff --git a/src/dev/check.rs b/src/dev/check.rs index bd73ec8c..119fed5f 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -202,7 +202,7 @@ fn check_exercises_unsolved( for (exercise_name, handle) in handles { let Ok(result) = handle.join() else { - bail!("Panic while trying to run the exericse {exercise_name}"); + bail!("Panic while trying to run the exercise {exercise_name}"); }; match result { @@ -300,7 +300,7 @@ fn check_solutions( for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { let Ok(check_result) = handle.join() else { bail!( - "Panic while trying to run the solution of the exericse {}", + "Panic while trying to run the solution of the exercise {}", exercise_info.name, ); }; From f146553dead78357cd44736dfca97b1349418fa2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 17 Oct 2024 14:37:47 +0200 Subject: [PATCH 205/211] hashmap3: Use `or_default` --- rustlings-macros/info.toml | 8 ++------ solutions/11_hashmaps/hashmaps3.rs | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index c1342d68..e7055981 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -575,12 +575,8 @@ https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if- name = "hashmaps3" dir = "11_hashmaps" hint = """ -Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of - `HashMap` to insert the default value of `TeamScores` if a team doesn't - exist in the table yet. - -Learn more in The Book: -https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value +Hint 1: Use the `entry()` and `or_default()` methods of `HashMap` to insert the + default value of `TeamScores` if a team doesn't exist in the table yet. Hint 2: If there is already an entry for a given key, the value returned by `entry()` can be updated based on the existing value. diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs index 8a5d30b6..41da784b 100644 --- a/solutions/11_hashmaps/hashmaps3.rs +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -28,17 +28,13 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); // Insert the default with zeros if a team doesn't exist yet. - let team_1 = scores - .entry(team_1_name) - .or_insert_with(TeamScores::default); + let team_1 = scores.entry(team_1_name).or_default(); // Update the values. team_1.goals_scored += team_1_score; team_1.goals_conceded += team_2_score; // Similarly for the second team. - let team_2 = scores - .entry(team_2_name) - .or_insert_with(TeamScores::default); + let team_2 = scores.entry(team_2_name).or_default(); team_2.goals_scored += team_2_score; team_2.goals_conceded += team_1_score; } From 99496706c5041affdc252649bfc74d50a2187271 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 17 Oct 2024 14:44:48 +0200 Subject: [PATCH 206/211] Apply new Clippy lints --- src/cmd.rs | 2 +- src/term.rs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/cmd.rs b/src/cmd.rs index 4a93312a..30f988a6 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -125,7 +125,7 @@ pub struct CargoSubcommand<'out> { output: Option<&'out mut Vec>, } -impl<'out> CargoSubcommand<'out> { +impl CargoSubcommand<'_> { #[inline] pub fn args<'arg, I>(&mut self, args: I) -> &mut Self where diff --git a/src/term.rs b/src/term.rs index e3bfd7bb..cb0a07ce 100644 --- a/src/term.rs +++ b/src/term.rs @@ -11,15 +11,15 @@ use std::{ use crate::app_state::CheckProgress; -pub struct MaxLenWriter<'a, 'b> { - pub stdout: &'a mut StdoutLock<'b>, +pub struct MaxLenWriter<'a, 'lock> { + pub stdout: &'a mut StdoutLock<'lock>, len: usize, max_len: usize, } -impl<'a, 'b> MaxLenWriter<'a, 'b> { +impl<'a, 'lock> MaxLenWriter<'a, 'lock> { #[inline] - pub fn new(stdout: &'a mut StdoutLock<'b>, max_len: usize) -> Self { + pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self { Self { stdout, len: 0, @@ -34,13 +34,13 @@ impl<'a, 'b> MaxLenWriter<'a, 'b> { } } -pub trait CountedWrite<'a> { +pub trait CountedWrite<'lock> { fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>; fn write_str(&mut self, unicode: &str) -> io::Result<()>; - fn stdout(&mut self) -> &mut StdoutLock<'a>; + fn stdout(&mut self) -> &mut StdoutLock<'lock>; } -impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> { +impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> { fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> { let n = ascii.len().min(self.max_len.saturating_sub(self.len)); if n > 0 { @@ -65,7 +65,7 @@ impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> { } #[inline] - fn stdout(&mut self) -> &mut StdoutLock<'b> { + fn stdout(&mut self) -> &mut StdoutLock<'lock> { self.stdout } } @@ -87,17 +87,17 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> { } } -pub struct CheckProgressVisualizer<'a, 'b> { - stdout: &'a mut StdoutLock<'b>, +pub struct CheckProgressVisualizer<'a, 'lock> { + stdout: &'a mut StdoutLock<'lock>, n_cols: usize, } -impl<'a, 'b> CheckProgressVisualizer<'a, 'b> { +impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> { const CHECKING_COLOR: Color = Color::Blue; const DONE_COLOR: Color = Color::Green; const PENDING_COLOR: Color = Color::Red; - pub fn build(stdout: &'a mut StdoutLock<'b>, term_width: u16) -> io::Result { + pub fn build(stdout: &'a mut StdoutLock<'lock>, term_width: u16) -> io::Result { clear_terminal(stdout)?; stdout.write_all("Checking all exercises…\n".as_bytes())?; From 0e090ae11244ee8e0cdb50501b34c36a7112fd0c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 17 Oct 2024 14:48:56 +0200 Subject: [PATCH 207/211] Add required type annotation --- exercises/11_hashmaps/hashmaps3.rs | 2 +- solutions/11_hashmaps/hashmaps3.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs index 7e9584d1..5b390ab9 100644 --- a/exercises/11_hashmaps/hashmaps3.rs +++ b/exercises/11_hashmaps/hashmaps3.rs @@ -17,7 +17,7 @@ struct TeamScores { fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { // The name of the team is the key and its associated struct is the value. - let mut scores = HashMap::new(); + let mut scores = HashMap::<&str, TeamScores>::new(); for line in results.lines() { let mut split_iterator = line.split(','); diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs index 41da784b..433b16c3 100644 --- a/solutions/11_hashmaps/hashmaps3.rs +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -17,7 +17,7 @@ struct TeamScores { fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { // The name of the team is the key and its associated struct is the value. - let mut scores = HashMap::new(); + let mut scores = HashMap::<&str, TeamScores>::new(); for line in results.lines() { let mut split_iterator = line.split(','); From e90f5f03f3da639bf3157aec12ebf0cec62ac7ae Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 17 Oct 2024 14:59:37 +0200 Subject: [PATCH 208/211] Mention the Q&A category --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b65920f2..9a223670 100644 --- a/README.md +++ b/README.md @@ -124,14 +124,13 @@ The list allows you to… - See the status of all exercises (done or pending) - `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one) -- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards) +- `r`: Reset status and file of the selected exercise (you need to _reload/reopen_ its file in your editor afterwards) See the footer of the list for all possible keys. -## Continuing On +## Questions? -Once you've completed Rustlings, put your new knowledge to good use! -Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. +If you need any help while doing the exercises and the builtin-hints aren't helpful, feel free to ask in the [_Q&A_ category of the discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question wasn't asked yet 💡 ## Third-Party Exercises @@ -144,6 +143,11 @@ Do you want to create your own set of Rustlings exercises to focus on some speci Or do you want to translate the original Rustlings exercises? Then follow the the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)! +## Continuing On + +Once you've completed Rustlings, put your new knowledge to good use! +Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. + ## Uninstalling Rustlings If you want to remove Rustlings from your system, run the following command: From 7e2f56f41a89213d3ae60a069402a25b570f0cca Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 17 Oct 2024 15:03:43 +0200 Subject: [PATCH 209/211] Use the default hasher --- Cargo.lock | 15 ++++----------- Cargo.toml | 1 - clippy.toml | 3 --- src/app_state.rs | 4 ++-- src/collections.rs | 9 --------- src/dev/check.rs | 8 ++++---- src/main.rs | 1 - 7 files changed, 10 insertions(+), 31 deletions(-) delete mode 100644 src/collections.rs diff --git a/Cargo.lock b/Cargo.lock index f89c139f..1ac56b40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,12 +186,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "foldhash" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -283,9 +277,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.159" +version = "0.2.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f" [[package]] name = "libredox" @@ -410,9 +404,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ "unicode-ident", ] @@ -455,7 +449,6 @@ dependencies = [ "anyhow", "clap", "crossterm", - "foldhash", "notify", "os_pipe", "rustix", diff --git a/Cargo.toml b/Cargo.toml index eb22cfee..4dbcb5fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ include = [ anyhow = "1.0.89" clap = { version = "4.5.20", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } -foldhash = "0.1.3" notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] } os_pipe = "1.2.1" rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } diff --git a/clippy.toml b/clippy.toml index 2a981849..afc9253a 100644 --- a/clippy.toml +++ b/clippy.toml @@ -5,9 +5,6 @@ disallowed-types = [ ] disallowed-methods = [ - # We use `foldhash` instead of the default hasher. - "std::collections::HashSet::new", - "std::collections::HashSet::with_capacity", # Inefficient. Use `.queue(…)` instead. "crossterm::style::style", # Use `thread::Builder::spawn` instead and handle the error. diff --git a/src/app_state.rs b/src/app_state.rs index 4007fbc3..5979150f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,6 +1,7 @@ use anyhow::{bail, Context, Error, Result}; use crossterm::{cursor, terminal, QueueableCommand}; use std::{ + collections::HashSet, env, fs::{File, OpenOptions}, io::{Read, Seek, StdoutLock, Write}, @@ -16,7 +17,6 @@ use std::{ use crate::{ clear_terminal, cmd::CmdRunner, - collections::hash_set_with_capacity, embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, @@ -146,7 +146,7 @@ impl AppState { break 'block StateFileStatus::NotRead; } - let mut done_exercises = hash_set_with_capacity(exercises.len()); + let mut done_exercises = HashSet::with_capacity(exercises.len()); for done_exercise_name in lines { if done_exercise_name.is_empty() { diff --git a/src/collections.rs b/src/collections.rs deleted file mode 100644 index 3f2841e0..00000000 --- a/src/collections.rs +++ /dev/null @@ -1,9 +0,0 @@ -use foldhash::fast::FixedState; - -/// DOS attacks aren't a concern for Rustlings. Therefore, we use `foldhash` with a fixed state. -pub type HashSet = std::collections::HashSet; - -#[inline] -pub fn hash_set_with_capacity(capacity: usize) -> HashSet { - HashSet::with_capacity_and_hasher(capacity, FixedState::default()) -} diff --git a/src/dev/check.rs b/src/dev/check.rs index 119fed5f..956c2be2 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, bail, Context, Error, Result}; use std::{ cmp::Ordering, + collections::HashSet, fs::{self, read_dir, OpenOptions}, io::{self, Read, Write}, path::{Path, PathBuf}, @@ -11,7 +12,6 @@ use std::{ use crate::{ cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, cmd::CmdRunner, - collections::{hash_set_with_capacity, HashSet}, exercise::{RunnableExercise, OUTPUT_CAPACITY}, info_file::{ExerciseInfo, InfoFile}, CURRENT_FORMAT_VERSION, @@ -53,8 +53,8 @@ fn check_cargo_toml( // Check the info of all exercises and return their paths in a set. fn check_info_file_exercises(info_file: &InfoFile) -> Result> { - let mut names = hash_set_with_capacity(info_file.exercises.len()); - let mut paths = hash_set_with_capacity(info_file.exercises.len()); + let mut names = HashSet::with_capacity(info_file.exercises.len()); + let mut paths = HashSet::with_capacity(info_file.exercises.len()); let mut file_buf = String::with_capacity(1 << 14); for exercise_info in &info_file.exercises { @@ -282,7 +282,7 @@ fn check_solutions( .collect::, _>>() .context("Failed to spawn a thread to check a solution")?; - let mut sol_paths = hash_set_with_capacity(info_file.exercises.len()); + let mut sol_paths = HashSet::with_capacity(info_file.exercises.len()); let mut fmt_cmd = Command::new("rustfmt"); fmt_cmd .arg("--check") diff --git a/src/main.rs b/src/main.rs index c8bcd2e5..eeb1883e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,6 @@ use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile}; mod app_state; mod cargo_toml; mod cmd; -mod collections; mod dev; mod embedded; mod exercise; From 930a0ea73b74921d687f3389f8dfb99f8fda8cea Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 17 Oct 2024 16:00:10 +0200 Subject: [PATCH 210/211] list: Highlight search match in exercise names --- src/list/state.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 5bdbca77..53fe07c4 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -105,6 +105,28 @@ impl<'a> ListState<'a> { ); } + fn draw_exericse_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> { + if !self.search_query.is_empty() { + if let Some((pre_highlight, highlight, post_highlight)) = exercise + .name + .find(&self.search_query) + .and_then(|ind| exercise.name.split_at_checked(ind)) + .and_then(|(pre_highlight, rest)| { + rest.split_at_checked(self.search_query.len()) + .map(|x| (pre_highlight, x.0, x.1)) + }) + { + writer.write_str(pre_highlight)?; + writer.stdout.queue(SetForegroundColor(Color::Magenta))?; + writer.write_str(highlight)?; + writer.stdout.queue(ResetColor)?; + return writer.write_str(post_highlight); + } + } + + writer.write_str(exercise.name) + } + fn draw_rows( &self, stdout: &mut StdoutLock, @@ -147,10 +169,10 @@ impl<'a> ListState<'a> { writer.stdout.queue(SetForegroundColor(Color::Yellow))?; writer.write_ascii(b"PENDING ")?; } - writer.stdout.queue(SetForegroundColor(Color::Reset))?; - writer.write_str(exercise.name)?; + self.draw_exericse_name(&mut writer, exercise)?; + writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?; // The list links aren't shown correctly in VS Code on Windows. From 6bec6f92c4f0fe14ed56ad646514e89f6d0ee7cc Mon Sep 17 00:00:00 2001 From: Vincent Ging Ho Yim Date: Tue, 22 Oct 2024 16:49:44 +1100 Subject: [PATCH 211/211] threads1: Fix typos in description --- exercises/20_threads/threads1.rs | 4 ++-- solutions/20_threads/threads1.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/20_threads/threads1.rs b/exercises/20_threads/threads1.rs index 01f9ff44..dbc64b16 100644 --- a/exercises/20_threads/threads1.rs +++ b/exercises/20_threads/threads1.rs @@ -1,5 +1,5 @@ -// This program spawns multiple threads that each run for at least 250ms, and -// each thread returns how much time they took to complete. The program should +// This program spawns multiple threads that each runs for at least 250ms, and +// each thread returns how much time it took to complete. The program should // wait until all the spawned threads have finished and should collect their // return values into a vector. diff --git a/solutions/20_threads/threads1.rs b/solutions/20_threads/threads1.rs index 7f3dd29a..1fc5bc9c 100644 --- a/solutions/20_threads/threads1.rs +++ b/solutions/20_threads/threads1.rs @@ -1,5 +1,5 @@ -// This program spawns multiple threads that each run for at least 250ms, and -// each thread returns how much time they took to complete. The program should +// This program spawns multiple threads that each runs for at least 250ms, and +// each thread returns how much time it took to complete. The program should // wait until all the spawned threads have finished and should collect their // return values into a vector.