From 2b6f9fb6a7a33f074aa609b2da1ac084bc3ecd6b Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 4 Apr 2024 20:21:55 +0200
Subject: [PATCH 001/109] Add Ratatui

---
 Cargo.lock | 323 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 Cargo.toml |   2 +
 2 files changed, 314 insertions(+), 11 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index d8e5b723..38f8170c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,18 @@
 # 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 = "aho-corasick"
 version = "1.1.3"
@@ -11,6 +23,12 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+
 [[package]]
 name = "anstream"
 version = "0.6.13"
@@ -109,6 +127,21 @@ dependencies = [
  "serde",
 ]
 
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
+dependencies = [
+ "rustversion",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -143,10 +176,10 @@ version = "4.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
 dependencies = [
- "heck",
+ "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.58",
 ]
 
 [[package]]
@@ -161,6 +194,19 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "compact_str"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "ryu",
+ "static_assertions",
+]
+
 [[package]]
 name = "console"
 version = "0.15.8"
@@ -189,6 +235,31 @@ version = "0.8.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
 
+[[package]]
+name = "crossterm"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
+dependencies = [
+ "bitflags 2.5.0",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "difflib"
 version = "0.4.0"
@@ -270,6 +341,16 @@ name = "hashbrown"
 version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
 name = "heck"
@@ -309,6 +390,12 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "indoc"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
+
 [[package]]
 name = "inotify"
 version = "0.9.6"
@@ -338,6 +425,15 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.11"
@@ -382,6 +478,16 @@ version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
 
+[[package]]
+name = "lock_api"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
 [[package]]
 name = "log"
 version = "0.4.21"
@@ -389,10 +495,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
 
 [[package]]
-name = "memchr"
-version = "2.7.1"
+name = "lru"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
 
 [[package]]
 name = "mio"
@@ -457,6 +572,41 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
 
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
 [[package]]
 name = "portable-atomic"
 version = "1.6.0"
@@ -511,6 +661,26 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "ratatui"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8"
+dependencies = [
+ "bitflags 2.5.0",
+ "cassowary",
+ "compact_str",
+ "crossterm",
+ "indoc",
+ "itertools",
+ "lru",
+ "paste",
+ "stability",
+ "strum",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.4.1"
@@ -570,10 +740,12 @@ dependencies = [
  "assert_cmd",
  "clap",
  "console",
+ "crossterm",
  "glob",
  "indicatif",
  "notify-debouncer-mini",
  "predicates",
+ "ratatui",
  "rustlings-macros",
  "serde",
  "serde_json",
@@ -590,6 +762,12 @@ dependencies = [
  "quote",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
 [[package]]
 name = "ryu"
 version = "1.0.17"
@@ -605,6 +783,12 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
 [[package]]
 name = "serde"
 version = "1.0.197"
@@ -622,7 +806,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.58",
 ]
 
 [[package]]
@@ -652,16 +836,101 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
-name = "strsim"
-version = "0.11.0"
+name = "signal-hook"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "stability"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.58",
+]
 
 [[package]]
 name = "syn"
-version = "2.0.55"
+version = "1.0.109"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -702,6 +971,12 @@ 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-width"
 version = "0.1.11"
@@ -714,6 +989,12 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
 
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
 [[package]]
 name = "wait-timeout"
 version = "0.2.0"
@@ -928,3 +1209,23 @@ name = "winsafe"
 version = "0.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.58",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 86187b4b..2a22fce1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,8 +29,10 @@ edition.workspace = true
 anyhow = "1.0.81"
 clap = { version = "4.5.4", features = ["derive"] }
 console = "0.15.8"
+crossterm = "0.27.0"
 indicatif = "0.17.8"
 notify-debouncer-mini = "0.4.1"
+ratatui = "0.26.1"
 rustlings-macros = { path = "rustlings-macros" }
 serde_json = "1.0.115"
 serde = { version = "1.0.197", features = ["derive"] }

From 9ea744a7104f441ef505db0a96e852f93d8c0bf4 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 4 Apr 2024 20:27:30 +0200
Subject: [PATCH 002/109] Remove deps not needed in the TUI

---
 Cargo.lock | 42 ------------------------------------------
 Cargo.toml |  2 --
 2 files changed, 44 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 38f8170c..4aaec38c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -377,19 +377,6 @@ dependencies = [
  "hashbrown",
 ]
 
-[[package]]
-name = "indicatif"
-version = "0.17.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
-dependencies = [
- "console",
- "instant",
- "number_prefix",
- "portable-atomic",
- "unicode-width",
-]
-
 [[package]]
 name = "indoc"
 version = "2.0.5"
@@ -416,15 +403,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "instant"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
-dependencies = [
- "cfg-if",
-]
-
 [[package]]
 name = "itertools"
 version = "0.12.1"
@@ -566,12 +544,6 @@ dependencies = [
  "autocfg",
 ]
 
-[[package]]
-name = "number_prefix"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
-
 [[package]]
 name = "once_cell"
 version = "1.19.0"
@@ -607,12 +579,6 @@ version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
 
-[[package]]
-name = "portable-atomic"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
-
 [[package]]
 name = "predicates"
 version = "3.1.0"
@@ -742,14 +708,12 @@ dependencies = [
  "console",
  "crossterm",
  "glob",
- "indicatif",
  "notify-debouncer-mini",
  "predicates",
  "ratatui",
  "rustlings-macros",
  "serde",
  "serde_json",
- "shlex",
  "toml_edit",
  "which",
  "winnow",
@@ -829,12 +793,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "shlex"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
 [[package]]
 name = "signal-hook"
 version = "0.3.17"
diff --git a/Cargo.toml b/Cargo.toml
index 2a22fce1..3c187417 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,13 +30,11 @@ anyhow = "1.0.81"
 clap = { version = "4.5.4", features = ["derive"] }
 console = "0.15.8"
 crossterm = "0.27.0"
-indicatif = "0.17.8"
 notify-debouncer-mini = "0.4.1"
 ratatui = "0.26.1"
 rustlings-macros = { path = "rustlings-macros" }
 serde_json = "1.0.115"
 serde = { version = "1.0.197", features = ["derive"] }
-shlex = "1.3.0"
 toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] }
 which = "6.0.1"
 winnow = "0.6.5"

From 34375b2ebfbdb0b6504a56c82635c8c9d3d6ce59 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 4 Apr 2024 21:06:11 +0200
Subject: [PATCH 003/109] Clean up as a preparation for the TUI

---
 src/main.rs   |  44 ++-------
 src/run.rs    |  40 +++-----
 src/verify.rs | 249 +++++++++++---------------------------------------
 3 files changed, 77 insertions(+), 256 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index c8c65848..20ec290f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,10 +7,9 @@ use clap::{Parser, Subcommand};
 use console::Emoji;
 use notify_debouncer_mini::notify::RecursiveMode;
 use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
-use shlex::Shlex;
 use std::io::{BufRead, Write};
 use std::path::Path;
-use std::process::{exit, Command};
+use std::process::exit;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::mpsc::{channel, RecvTimeoutError};
 use std::sync::{Arc, Mutex};
@@ -31,9 +30,6 @@ mod verify;
 #[derive(Parser)]
 #[command(version)]
 struct Args {
-    /// Show outputs from the test exercises
-    #[arg(long)]
-    nocapture: bool,
     #[command(subcommand)]
     command: Option<Subcommands>,
 }
@@ -45,11 +41,7 @@ enum Subcommands {
     /// Verify all exercises according to the recommended order
     Verify,
     /// Rerun `verify` when files were edited
-    Watch {
-        /// Show hints on success
-        #[arg(long)]
-        success_hints: bool,
-    },
+    Watch,
     /// Run/Test a single exercise
     Run {
         /// The name of the exercise
@@ -117,7 +109,6 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         exit(1);
     }
 
-    let verbose = args.nocapture;
     let command = args.command.unwrap_or_else(|| {
         println!("{DEFAULT_OUT}\n");
         exit(0);
@@ -203,7 +194,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
 
         Subcommands::Run { name } => {
             let exercise = find_exercise(&name, &exercises)?;
-            run(exercise, verbose).unwrap_or_else(|_| exit(1));
+            run(exercise).unwrap_or_else(|_| exit(1));
         }
 
         Subcommands::Reset { name } => {
@@ -219,12 +210,12 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
             println!("{}", exercise.hint);
         }
 
-        Subcommands::Verify => match verify(&exercises, (0, exercises.len()), verbose, false)? {
+        Subcommands::Verify => match verify(&exercises, (0, exercises.len()))? {
             VerifyState::AllExercisesDone => println!("All exercises done!"),
             VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
         },
 
-        Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) {
+        Subcommands::Watch => match watch(&exercises) {
             Err(e) => {
                 println!("Error: Could not watch your progress. Error message was {e:?}.");
                 println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
@@ -277,17 +268,6 @@ fn spawn_watch_shell(
                 println!("Bye!");
             } else if input == "help" {
                 println!("{WATCH_MODE_HELP_MESSAGE}");
-            } else if let Some(cmd) = input.strip_prefix('!') {
-                let mut parts = Shlex::new(cmd);
-
-                let Some(program) = parts.next() else {
-                    println!("no command provided");
-                    continue;
-                };
-
-                if let Err(e) = Command::new(program).args(parts).status() {
-                    println!("failed to execute command `{cmd}`: {e}");
-                }
             } else {
                 println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}");
             }
@@ -319,7 +299,7 @@ enum WatchStatus {
     Unfinished,
 }
 
-fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<WatchStatus> {
+fn watch(exercises: &[Exercise]) -> Result<WatchStatus> {
     /* Clears the terminal with an ANSI escape code.
     Works in UNIX and newer Windows terminals. */
     fn clear_screen() {
@@ -336,11 +316,10 @@ fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<W
 
     clear_screen();
 
-    let failed_exercise_hint =
-        match verify(exercises, (0, exercises.len()), verbose, success_hints)? {
-            VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
-            VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
-        };
+    let failed_exercise_hint = match verify(exercises, (0, exercises.len()))? {
+        VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
+        VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
+    };
 
     spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit));
 
@@ -364,8 +343,6 @@ fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<W
                             match verify(
                                 pending_exercises.iter().copied(),
                                 (num_done, exercises.len()),
-                                verbose,
-                                success_hints,
                             )? {
                                 VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
                                 VerifyState::Failed(exercise) => {
@@ -429,7 +406,6 @@ const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode:
   hint   - prints the current exercise's hint
   clear  - clears the screen
   quit   - quits watch mode
-  !<cmd> - executes a command, like `!rustc --explain E0381`
   help   - displays this help message
 
 Watch mode automatically re-evaluates the current exercise
diff --git a/src/run.rs b/src/run.rs
index 3f93f146..0a09eccf 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -1,39 +1,27 @@
-use anyhow::{bail, Result};
+use anyhow::Result;
 use std::io::{stdout, Write};
-use std::time::Duration;
 
-use crate::exercise::{Exercise, Mode};
-use crate::verify::test;
-use indicatif::ProgressBar;
+use crate::exercise::Exercise;
 
 // Invoke the rust compiler on the path of the given exercise,
 // and run the ensuing binary.
 // The verbose argument helps determine whether or not to show
 // the output from the test harnesses (if the mode of the exercise is test)
-pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> {
-    match exercise.mode {
-        Mode::Test => test(exercise, verbose),
-        Mode::Compile | Mode::Clippy => compile_and_run(exercise),
-    }
-}
-
-// Compile and run an exercise.
-// This is strictly for non-test binaries, so output is displayed
-fn compile_and_run(exercise: &Exercise) -> Result<()> {
-    let progress_bar = ProgressBar::new_spinner();
-    progress_bar.set_message(format!("Running {exercise}..."));
-    progress_bar.enable_steady_tick(Duration::from_millis(100));
-
+pub fn run(exercise: &Exercise) -> Result<()> {
     let output = exercise.run()?;
-    progress_bar.finish_and_clear();
 
-    stdout().write_all(&output.stdout)?;
-    if !output.status.success() {
-        stdout().write_all(&output.stderr)?;
-        warn!("Ran {} with errors", exercise);
-        bail!("TODO");
+    {
+        let mut stdout = stdout().lock();
+        stdout.write_all(&output.stdout)?;
+        stdout.write_all(&output.stderr)?;
+        stdout.flush()?;
+    }
+
+    if output.status.success() {
+        success!("Successfully ran {}", exercise);
+    } else {
+        warn!("Ran {} with errors", exercise);
     }
 
-    success!("Successfully ran {}", exercise);
     Ok(())
 }
diff --git a/src/verify.rs b/src/verify.rs
index ef966f60..5b053940 100644
--- a/src/verify.rs
+++ b/src/verify.rs
@@ -1,12 +1,6 @@
-use anyhow::{bail, Result};
+use anyhow::Result;
 use console::style;
-use indicatif::{ProgressBar, ProgressStyle};
-use std::{
-    env,
-    io::{stdout, Write},
-    process::Output,
-    time::Duration,
-};
+use std::io::{stdout, Write};
 
 use crate::exercise::{Exercise, Mode, State};
 
@@ -23,201 +17,64 @@ pub enum VerifyState<'a> {
 pub fn verify<'a>(
     pending_exercises: impl IntoIterator<Item = &'a Exercise>,
     progress: (usize, usize),
-    verbose: bool,
-    success_hints: bool,
 ) -> Result<VerifyState<'a>> {
-    let (num_done, total) = progress;
-    let bar = ProgressBar::new(total as u64);
-    let mut percentage = num_done as f32 / total as f32 * 100.0;
-    bar.set_style(
-        ProgressStyle::default_bar()
-            .template("Progress: [{bar:60.green/red}] {pos}/{len} {msg}")
-            .expect("Progressbar template should be valid!")
-            .progress_chars("#>-"),
+    let (mut num_done, total) = progress;
+    println!(
+        "Progress: {num_done}/{total} ({:.1}%)\n",
+        num_done as f32 / total as f32 * 100.0,
     );
-    bar.set_position(num_done as u64);
-    bar.set_message(format!("({percentage:.1} %)"));
 
     for exercise in pending_exercises {
-        let compile_result = match exercise.mode {
-            Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints)?,
-            Mode::Compile => compile_and_run_interactively(exercise, success_hints)?,
-            Mode::Clippy => compile_only(exercise, success_hints)?,
-        };
-        if !compile_result {
+        let output = exercise.run()?;
+
+        {
+            let mut stdout = stdout().lock();
+            stdout.write_all(&output.stdout)?;
+            stdout.write_all(&output.stderr)?;
+            stdout.flush()?;
+        }
+
+        if !output.status.success() {
             return Ok(VerifyState::Failed(exercise));
         }
-        percentage += 100.0 / total as f32;
-        bar.inc(1);
-        bar.set_message(format!("({percentage:.1} %)"));
-    }
 
-    bar.finish();
-    println!("You completed all exercises!");
+        println!();
+        match exercise.mode {
+            Mode::Compile => success!("Successfully ran {}!", exercise),
+            Mode::Test => success!("Successfully tested {}!", exercise),
+            Mode::Clippy => success!("Successfully checked {}!", exercise),
+        }
+
+        if let State::Pending(context) = exercise.state()? {
+            println!(
+                "\nYou can keep working on this exercise,
+or jump into the next one by removing the {} comment:\n",
+                style("`I AM NOT DONE`").bold()
+            );
+
+            for context_line in context {
+                let formatted_line = if context_line.important {
+                    format!("{}", style(context_line.line).bold())
+                } else {
+                    context_line.line
+                };
+
+                println!(
+                    "{:>2} {}  {}",
+                    style(context_line.number).blue().bold(),
+                    style("|").blue(),
+                    formatted_line,
+                );
+            }
+            return Ok(VerifyState::Failed(exercise));
+        }
+
+        num_done += 1;
+        println!(
+            "Progress: {num_done}/{total} ({:.1}%)\n",
+            num_done as f32 / total as f32 * 100.0,
+        );
+    }
 
     Ok(VerifyState::AllExercisesDone)
 }
-
-#[derive(PartialEq, Eq)]
-enum RunMode {
-    Interactive,
-    NonInteractive,
-}
-
-// Compile and run the resulting test harness of the given Exercise
-pub fn test(exercise: &Exercise, verbose: bool) -> Result<()> {
-    compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?;
-    Ok(())
-}
-
-// Invoke the rust compiler without running the resulting binary
-fn compile_only(exercise: &Exercise, success_hints: bool) -> Result<bool> {
-    let progress_bar = ProgressBar::new_spinner();
-    progress_bar.set_message(format!("Compiling {exercise}..."));
-    progress_bar.enable_steady_tick(Duration::from_millis(100));
-
-    let _ = exercise.run()?;
-    progress_bar.finish_and_clear();
-
-    prompt_for_completion(exercise, None, success_hints)
-}
-
-// Compile the given Exercise and run the resulting binary in an interactive mode
-fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result<bool> {
-    let progress_bar = ProgressBar::new_spinner();
-    progress_bar.set_message(format!("Running {exercise}..."));
-    progress_bar.enable_steady_tick(Duration::from_millis(100));
-
-    let output = exercise.run()?;
-    progress_bar.finish_and_clear();
-
-    if !output.status.success() {
-        warn!("Ran {} with errors", exercise);
-        {
-            let mut stdout = stdout().lock();
-            stdout.write_all(&output.stdout)?;
-            stdout.write_all(&output.stderr)?;
-            stdout.flush()?;
-        }
-        bail!("TODO");
-    }
-
-    prompt_for_completion(exercise, Some(output), success_hints)
-}
-
-// Compile the given Exercise as a test harness and display
-// the output if verbose is set to true
-fn compile_and_test(
-    exercise: &Exercise,
-    run_mode: RunMode,
-    verbose: bool,
-    success_hints: bool,
-) -> Result<bool> {
-    let progress_bar = ProgressBar::new_spinner();
-    progress_bar.set_message(format!("Testing {exercise}..."));
-    progress_bar.enable_steady_tick(Duration::from_millis(100));
-
-    let output = exercise.run()?;
-    progress_bar.finish_and_clear();
-
-    if !output.status.success() {
-        warn!(
-            "Testing of {} failed! Please try again. Here's the output:",
-            exercise
-        );
-        {
-            let mut stdout = stdout().lock();
-            stdout.write_all(&output.stdout)?;
-            stdout.write_all(&output.stderr)?;
-            stdout.flush()?;
-        }
-        bail!("TODO");
-    }
-
-    if verbose {
-        stdout().write_all(&output.stdout)?;
-    }
-
-    if run_mode == RunMode::Interactive {
-        prompt_for_completion(exercise, None, success_hints)
-    } else {
-        Ok(true)
-    }
-}
-
-fn prompt_for_completion(
-    exercise: &Exercise,
-    prompt_output: Option<Output>,
-    success_hints: bool,
-) -> Result<bool> {
-    let context = match exercise.state()? {
-        State::Done => return Ok(true),
-        State::Pending(context) => context,
-    };
-    match exercise.mode {
-        Mode::Compile => success!("Successfully ran {}!", exercise),
-        Mode::Test => success!("Successfully tested {}!", exercise),
-        Mode::Clippy => success!("Successfully compiled {}!", exercise),
-    }
-
-    let no_emoji = env::var("NO_EMOJI").is_ok();
-
-    let clippy_success_msg = if no_emoji {
-        "The code is compiling, and Clippy is happy!"
-    } else {
-        "The code is compiling, and šŸ“Ž Clippy šŸ“Ž is happy!"
-    };
-
-    let success_msg = match exercise.mode {
-        Mode::Compile => "The code is compiling!",
-        Mode::Test => "The code is compiling, and the tests pass!",
-        Mode::Clippy => clippy_success_msg,
-    };
-
-    if no_emoji {
-        println!("\n~*~ {success_msg} ~*~\n");
-    } else {
-        println!("\nšŸŽ‰ šŸŽ‰ {success_msg} šŸŽ‰ šŸŽ‰\n");
-    }
-
-    if let Some(output) = prompt_output {
-        let separator = separator();
-        println!("Output:\n{separator}");
-        stdout().write_all(&output.stdout).unwrap();
-        println!("\n{separator}\n");
-    }
-    if success_hints {
-        println!(
-            "Hints:\n{separator}\n{}\n{separator}\n",
-            exercise.hint,
-            separator = separator(),
-        );
-    }
-
-    println!("You can keep working on this exercise,");
-    println!(
-        "or jump into the next one by removing the {} comment:",
-        style("`I AM NOT DONE`").bold()
-    );
-    println!();
-    for context_line in context {
-        let formatted_line = if context_line.important {
-            format!("{}", style(context_line.line).bold())
-        } else {
-            context_line.line
-        };
-
-        println!(
-            "{:>2} {}  {}",
-            style(context_line.number).blue().bold(),
-            style("|").blue(),
-            formatted_line,
-        );
-    }
-
-    Ok(false)
-}
-
-fn separator() -> console::StyledObject<&'static str> {
-    style("====================").bold()
-}

From 445441ce25ec8658bcdec6b2038d17e893a5903f Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 4 Apr 2024 23:16:57 +0200
Subject: [PATCH 004/109] Make gen-dev-cargo-toml a separate package

so that `cargo install` only installs `rustlings`
---
 Cargo.lock                                         |  9 +++++++++
 Cargo.toml                                         | 14 +++++++++++---
 dev/Cargo.toml                                     |  4 ++--
 gen-dev-cargo-toml/Cargo.toml                      | 10 ++++++++++
 .../src/main.rs                                    |  6 +++---
 tests/dev_cargo_bins.rs                            |  2 +-
 6 files changed, 36 insertions(+), 9 deletions(-)
 create mode 100644 gen-dev-cargo-toml/Cargo.toml
 rename src/bin/gen-dev-cargo-toml.rs => gen-dev-cargo-toml/src/main.rs (86%)

diff --git a/Cargo.lock b/Cargo.lock
index 4aaec38c..e03980ca 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -330,6 +330,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "gen-dev-cargo-toml"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "serde",
+ "toml_edit",
+]
+
 [[package]]
 name = "glob"
 version = "0.3.1"
diff --git a/Cargo.toml b/Cargo.toml
index 3c187417..d80550a0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,9 @@ exclude = [
   "tests/fixture/success",
   "dev",
 ]
+members = [
+  "gen-dev-cargo-toml",
+]
 
 [workspace.package]
 version = "6.0.0"
@@ -16,6 +19,11 @@ authors = [
 license = "MIT"
 edition = "2021"
 
+[workspace.dependencies]
+anyhow = "1.0.81"
+serde = { version = "1.0.197", features = ["derive"] }
+toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] }
+
 [package]
 name = "rustlings"
 description = "Small exercises to get you used to reading and writing Rust code!"
@@ -26,7 +34,7 @@ license.workspace = true
 edition.workspace = true
 
 [dependencies]
-anyhow = "1.0.81"
+anyhow.workspace = true
 clap = { version = "4.5.4", features = ["derive"] }
 console = "0.15.8"
 crossterm = "0.27.0"
@@ -34,8 +42,8 @@ notify-debouncer-mini = "0.4.1"
 ratatui = "0.26.1"
 rustlings-macros = { path = "rustlings-macros" }
 serde_json = "1.0.115"
-serde = { version = "1.0.197", features = ["derive"] }
-toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] }
+serde.workspace = true
+toml_edit.workspace = true
 which = "6.0.1"
 winnow = "0.6.5"
 
diff --git a/dev/Cargo.toml b/dev/Cargo.toml
index 7868b97c..ed9b3ed3 100644
--- a/dev/Cargo.toml
+++ b/dev/Cargo.toml
@@ -1,5 +1,5 @@
-# This file is a hack to allow using `cargo r` to test `rustlings` during development.
-# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`.
+# This file is a hack to allow using `cargo run` to test `rustlings` during development.
+# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`.
 
 bin = [
   { name = "intro1", path = "../exercises/00_intro/intro1.rs" },
diff --git a/gen-dev-cargo-toml/Cargo.toml b/gen-dev-cargo-toml/Cargo.toml
new file mode 100644
index 00000000..8922ae8c
--- /dev/null
+++ b/gen-dev-cargo-toml/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "gen-dev-cargo-toml"
+publish = false
+license.workspace = true
+edition.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+serde.workspace = true
+toml_edit.workspace = true
diff --git a/src/bin/gen-dev-cargo-toml.rs b/gen-dev-cargo-toml/src/main.rs
similarity index 86%
rename from src/bin/gen-dev-cargo-toml.rs
rename to gen-dev-cargo-toml/src/main.rs
index ff8f31db..622762ad 100644
--- a/src/bin/gen-dev-cargo-toml.rs
+++ b/gen-dev-cargo-toml/src/main.rs
@@ -1,5 +1,5 @@
 // Generates `dev/Cargo.toml` such that it is synced with `info.toml`.
-// `dev/Cargo.toml` is a hack to allow using `cargo r` to test `rustlings`
+// `dev/Cargo.toml` is a hack to allow using `cargo run` to test `rustlings`
 // during development.
 
 use anyhow::{bail, Context, Result};
@@ -30,8 +30,8 @@ fn main() -> Result<()> {
     let mut buf = Vec::with_capacity(1 << 14);
 
     buf.extend_from_slice(
-        b"# This file is a hack to allow using `cargo r` to test `rustlings` during development.
-# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`.
+        b"# This file is a hack to allow using `cargo run` to test `rustlings` during development.
+# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`.
 
 bin = [\n",
     );
diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs
index 7f1771b6..ad4832f8 100644
--- a/tests/dev_cargo_bins.rs
+++ b/tests/dev_cargo_bins.rs
@@ -1,5 +1,5 @@
 // Makes sure that `dev/Cargo.toml` is synced with `info.toml`.
-// When this test fails, you just need to run `cargo run --bin gen-dev-cargo-toml`.
+// When this test fails, you just need to run `cargo run -p gen-dev-cargo-toml`.
 
 use serde::Deserialize;
 use std::fs;

From 919ba88413fcc495ebde288960079f6f627eb5b7 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 5 Apr 2024 00:43:36 +0200
Subject: [PATCH 005/109] Use the pretty format when testing even with -q

---
 src/exercise.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/exercise.rs b/src/exercise.rs
index 450acf45..d5ca254e 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -114,7 +114,7 @@ impl Exercise {
     pub fn run(&self) -> Result<Output> {
         match self.mode {
             Mode::Compile => self.cargo_cmd("run", &[]),
-            Mode::Test => self.cargo_cmd("test", &["--", "--nocapture"]),
+            Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]),
             Mode::Clippy => self.cargo_cmd(
                 "clippy",
                 &["--", "-D", "warnings", "-D", "clippy::float_cmp"],

From 5a233398ebe7078767404bd05ca06e08b37fb3d4 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 5 Apr 2024 00:44:43 +0200
Subject: [PATCH 006/109] Fix tests

---
 src/run.rs                 | 10 +++++-----
 tests/dev_cargo_bins.rs    |  2 +-
 tests/integration_tests.rs | 13 +------------
 3 files changed, 7 insertions(+), 18 deletions(-)

diff --git a/src/run.rs b/src/run.rs
index 0a09eccf..ee2d3b4f 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -1,4 +1,4 @@
-use anyhow::Result;
+use anyhow::{bail, Result};
 use std::io::{stdout, Write};
 
 use crate::exercise::Exercise;
@@ -17,11 +17,11 @@ pub fn run(exercise: &Exercise) -> Result<()> {
         stdout.flush()?;
     }
 
-    if output.status.success() {
-        success!("Successfully ran {}", exercise);
-    } else {
-        warn!("Ran {} with errors", exercise);
+    if !output.status.success() {
+        bail!("Ran {exercise} with errors");
     }
 
+    success!("Successfully ran {}", exercise);
+
     Ok(())
 }
diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs
index ad4832f8..c3faea92 100644
--- a/tests/dev_cargo_bins.rs
+++ b/tests/dev_cargo_bins.rs
@@ -17,7 +17,7 @@ struct InfoToml {
 
 #[test]
 fn dev_cargo_bins() {
-    let content = fs::read_to_string("exercises/Cargo.toml").unwrap();
+    let content = fs::read_to_string("dev/Cargo.toml").unwrap();
 
     let exercises = toml_edit::de::from_str::<InfoToml>(&fs::read_to_string("info.toml").unwrap())
         .unwrap()
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index d1694a39..d853521f 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -194,24 +194,13 @@ fn run_test_exercise_does_not_prompt() {
 
 #[test]
 fn run_single_test_success_with_output() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .args(["--nocapture", "run", "testSuccess"])
-        .current_dir("tests/fixture/success/")
-        .assert()
-        .code(0)
-        .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS"));
-}
-
-#[test]
-fn run_single_test_success_without_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").not());
+        .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS"));
 }
 
 #[test]

From 157fe016e5f335e04b4dd322623d35a244faa2ab Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 5 Apr 2024 00:49:22 +0200
Subject: [PATCH 007/109] Remove ui.rs

---
 src/main.rs   |  3 ---
 src/run.rs    |  3 ++-
 src/ui.rs     | 28 ----------------------------
 src/verify.rs |  7 ++++---
 4 files changed, 6 insertions(+), 35 deletions(-)
 delete mode 100644 src/ui.rs

diff --git a/src/main.rs b/src/main.rs
index 20ec290f..c62837d3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -17,9 +17,6 @@ use std::time::Duration;
 use std::{io, thread};
 use verify::VerifyState;
 
-#[macro_use]
-mod ui;
-
 mod embedded;
 mod exercise;
 mod init;
diff --git a/src/run.rs b/src/run.rs
index ee2d3b4f..38f4e0e2 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -21,7 +21,8 @@ pub fn run(exercise: &Exercise) -> Result<()> {
         bail!("Ran {exercise} with errors");
     }
 
-    success!("Successfully ran {}", exercise);
+    // TODO: Color
+    println!("Successfully ran {exercise}");
 
     Ok(())
 }
diff --git a/src/ui.rs b/src/ui.rs
deleted file mode 100644
index 22d60d96..00000000
--- a/src/ui.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-macro_rules! print_emoji {
-    ($emoji:expr, $sign:expr, $color: ident, $fmt:literal, $ex:expr) => {{
-        use console::{style, Emoji};
-        use std::env;
-        let formatstr = format!($fmt, $ex);
-        if env::var_os("NO_EMOJI").is_some() {
-            println!("{} {}", style($sign).$color(), style(formatstr).$color());
-        } else {
-            println!(
-                "{} {}",
-                style(Emoji($emoji, $sign)).$color(),
-                style(formatstr).$color()
-            );
-        }
-    }};
-}
-
-macro_rules! warn {
-    ($fmt:literal, $ex:expr) => {{
-        print_emoji!("āš ļø ", "!", red, $fmt, $ex);
-    }};
-}
-
-macro_rules! success {
-    ($fmt:literal, $ex:expr) => {{
-        print_emoji!("āœ… ", "āœ“", green, $fmt, $ex);
-    }};
-}
diff --git a/src/verify.rs b/src/verify.rs
index 5b053940..5beb2069 100644
--- a/src/verify.rs
+++ b/src/verify.rs
@@ -39,10 +39,11 @@ pub fn verify<'a>(
         }
 
         println!();
+        // TODO: Color
         match exercise.mode {
-            Mode::Compile => success!("Successfully ran {}!", exercise),
-            Mode::Test => success!("Successfully tested {}!", exercise),
-            Mode::Clippy => success!("Successfully checked {}!", exercise),
+            Mode::Compile => println!("Successfully ran {exercise}!"),
+            Mode::Test => println!("Successfully tested {exercise}!"),
+            Mode::Clippy => println!("Successfully checked {exercise}!"),
         }
 
         if let State::Pending(context) = exercise.state()? {

From 1d2c2cffd2f5a85714c3902bec6e8b198fede12f Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 5 Apr 2024 00:59:13 +0200
Subject: [PATCH 008/109] Remove .gitattributes

---
 .gitattributes | 2 --
 1 file changed, 2 deletions(-)
 delete mode 100644 .gitattributes

diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index efdba876..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1,2 +0,0 @@
-* text=auto
-*.sh text eol=lf

From 0bf51c6a0de117d7f28ddf4a253bfc0306f2e78b Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 5 Apr 2024 00:59:21 +0200
Subject: [PATCH 009/109] Ignore .ignore

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index 0bbbc542..0ea1fb6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ target/
 *.o
 public/
 .direnv/
+.ignore
 
 # Local Netlify folder
 .netlify

From b0f19fd862d659d2d4b01f2faa6b006fe2c60561 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 5 Apr 2024 03:04:53 +0200
Subject: [PATCH 010/109] Start with the TUI

---
 Cargo.lock    |  26 ------
 Cargo.toml    |   1 -
 src/consts.rs |  59 ++++++++++++
 src/main.rs   | 245 ++++----------------------------------------------
 src/tui.rs    |  92 +++++++++++++++++++
 src/verify.rs |  16 ++--
 6 files changed, 180 insertions(+), 259 deletions(-)
 create mode 100644 src/consts.rs
 create mode 100644 src/tui.rs

diff --git a/Cargo.lock b/Cargo.lock
index e03980ca..33d3030a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -207,19 +207,6 @@ dependencies = [
  "static_assertions",
 ]
 
-[[package]]
-name = "console"
-version = "0.15.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
-dependencies = [
- "encode_unicode",
- "lazy_static",
- "libc",
- "unicode-width",
- "windows-sys 0.52.0",
-]
-
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.12"
@@ -278,12 +265,6 @@ version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
 
-[[package]]
-name = "encode_unicode"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
-
 [[package]]
 name = "equivalent"
 version = "1.0.1"
@@ -447,12 +428,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-
 [[package]]
 name = "libc"
 version = "0.2.153"
@@ -714,7 +689,6 @@ dependencies = [
  "anyhow",
  "assert_cmd",
  "clap",
- "console",
  "crossterm",
  "glob",
  "notify-debouncer-mini",
diff --git a/Cargo.toml b/Cargo.toml
index d80550a0..da09ba18 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,7 +36,6 @@ edition.workspace = true
 [dependencies]
 anyhow.workspace = true
 clap = { version = "4.5.4", features = ["derive"] }
-console = "0.15.8"
 crossterm = "0.27.0"
 notify-debouncer-mini = "0.4.1"
 ratatui = "0.26.1"
diff --git a/src/consts.rs b/src/consts.rs
new file mode 100644
index 00000000..40bf150f
--- /dev/null
+++ b/src/consts.rs
@@ -0,0 +1,59 @@
+pub const WELCOME: &str = r"       welcome to...
+                 _   _ _
+  _ __ _   _ ___| |_| (_)_ __   __ _ ___
+ | '__| | | / __| __| | | '_ \ / _` / __|
+ | |  | |_| \__ \ |_| | | | | | (_| \__ \
+ |_|   \__,_|___/\__|_|_|_| |_|\__, |___/
+                               |___/";
+
+pub const DEFAULT_OUT: &str =
+    "Is this your first time? Don't worry, Rustlings was made for beginners! We are
+going to teach you a lot of things about Rust, but before we can get
+started, here's a couple of notes about how Rustlings operates:
+
+1. The central concept behind Rustlings is that you solve exercises. These
+   exercises usually have some sort of syntax error in them, which will cause
+   them to fail compilation or testing. Sometimes there's a logic error instead
+   of a syntax error. No matter what error, it's your job to find it and fix it!
+   You'll know when you fixed it because then, the exercise will compile and
+   Rustlings will be able to move on to the next exercise.
+2. If you run Rustlings in watch mode (which we recommend), it'll automatically
+   start with the first exercise. Don't get confused by an error message popping
+   up as soon as you run Rustlings! This is part of the exercise that you're
+   supposed to solve, so open the exercise file in an editor and start your
+   detective work!
+3. If you're stuck on an exercise, there is a helpful hint you can view by typing
+   'hint' (in watch mode), or running `rustlings hint exercise_name`.
+4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
+   (https://github.com/rust-lang/rustlings/issues/new). We look at every issue,
+   and sometimes, other learners do too so you can help each other out!
+
+Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise.
+Make sure to have your editor open in the `rustlings` directory!";
+
+pub const FENISH_LINE: &str = "+----------------------------------------------------+
+|          You made it to the Fe-nish line!          |
++--------------------------  ------------------------+
+                           \\/\x1b[31m
+     ā–’ā–’          ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’      ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’          ā–’ā–’
+   ā–’ā–’ā–’ā–’  ā–’ā–’    ā–’ā–’        ā–’ā–’  ā–’ā–’        ā–’ā–’    ā–’ā–’  ā–’ā–’ā–’ā–’
+   ā–’ā–’ā–’ā–’  ā–’ā–’  ā–’ā–’            ā–’ā–’            ā–’ā–’  ā–’ā–’  ā–’ā–’ā–’ā–’
+ ā–‘ā–‘ā–’ā–’ā–’ā–’ā–‘ā–‘ā–’ā–’  ā–’ā–’            ā–’ā–’            ā–’ā–’  ā–’ā–’ā–‘ā–‘ā–’ā–’ā–’ā–’
+   ā–“ā–“ā–“ā–“ā–“ā–“ā–“ā–“  ā–“ā–“      ā–“ā–“ā–ˆā–ˆ  ā–“ā–“  ā–“ā–“ā–ˆā–ˆ      ā–“ā–“  ā–“ā–“ā–“ā–“ā–“ā–“ā–“ā–“
+     ā–’ā–’ā–’ā–’    ā–’ā–’      ā–ˆā–ˆā–ˆā–ˆ  ā–’ā–’  ā–ˆā–ˆā–ˆā–ˆ      ā–’ā–’ā–‘ā–‘  ā–’ā–’ā–’ā–’
+       ā–’ā–’  ā–’ā–’ā–’ā–’ā–’ā–’        ā–’ā–’ā–’ā–’ā–’ā–’        ā–’ā–’ā–’ā–’ā–’ā–’  ā–’ā–’
+         ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–“ā–“ā–“ā–“ā–“ā–“ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–“ā–“ā–’ā–’ā–“ā–“ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
+           ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
+             ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
+           ā–’ā–’  ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’  ā–’ā–’
+         ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’
+       ā–’ā–’    ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’    ā–’ā–’
+       ā–’ā–’  ā–’ā–’    ā–’ā–’                  ā–’ā–’    ā–’ā–’  ā–’ā–’
+           ā–’ā–’  ā–’ā–’                      ā–’ā–’  ā–’ā–’\x1b[0m
+
+We hope you enjoyed learning about the various aspects of Rust!
+If you noticed any issues, please don't hesitate to report them to our repo.
+You can also contribute your own exercises to help the greater community!
+
+Before reporting an issue or contributing, please read our guidelines:
+https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md";
diff --git a/src/main.rs b/src/main.rs
index c62837d3..47afd019 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,26 +1,22 @@
+use crate::consts::{DEFAULT_OUT, WELCOME};
 use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
 use crate::exercise::{Exercise, ExerciseList};
 use crate::run::run;
+use crate::tui::tui;
 use crate::verify::verify;
 use anyhow::{bail, Context, Result};
 use clap::{Parser, Subcommand};
-use console::Emoji;
-use notify_debouncer_mini::notify::RecursiveMode;
-use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
-use std::io::{BufRead, Write};
+use std::io::Write;
 use std::path::Path;
 use std::process::exit;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::mpsc::{channel, RecvTimeoutError};
-use std::sync::{Arc, Mutex};
-use std::time::Duration;
-use std::{io, thread};
 use verify::VerifyState;
 
+mod consts;
 mod embedded;
 mod exercise;
 mod init;
 mod run;
+mod tui;
 mod verify;
 
 /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
@@ -37,7 +33,7 @@ enum Subcommands {
     Init,
     /// Verify all exercises according to the recommended order
     Verify,
-    /// Rerun `verify` when files were edited
+    /// Same as just running `rustlings` without a subcommand.
     Watch,
     /// Run/Test a single exercise
     Run {
@@ -106,21 +102,20 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         exit(1);
     }
 
-    let command = args.command.unwrap_or_else(|| {
-        println!("{DEFAULT_OUT}\n");
-        exit(0);
-    });
-
-    match command {
+    match args.command {
+        None | Some(Subcommands::Watch) => {
+            println!("{DEFAULT_OUT}\n");
+            tui(&exercises)?;
+        }
         // `Init` is handled above.
-        Subcommands::Init => (),
-        Subcommands::List {
+        Some(Subcommands::Init) => (),
+        Some(Subcommands::List {
             paths,
             names,
             filter,
             unsolved,
             solved,
-        } => {
+        }) => {
             if !paths && !names {
                 println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status");
             }
@@ -188,90 +183,30 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
             );
             exit(0);
         }
-
-        Subcommands::Run { name } => {
+        Some(Subcommands::Run { name }) => {
             let exercise = find_exercise(&name, &exercises)?;
             run(exercise).unwrap_or_else(|_| exit(1));
         }
-
-        Subcommands::Reset { name } => {
+        Some(Subcommands::Reset { name }) => {
             let exercise = find_exercise(&name, &exercises)?;
             EMBEDDED_FILES
                 .write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite)
                 .with_context(|| format!("Failed to reset the exercise {exercise}"))?;
             println!("The file {} has been reset!", exercise.path.display());
         }
-
-        Subcommands::Hint { name } => {
+        Some(Subcommands::Hint { name }) => {
             let exercise = find_exercise(&name, &exercises)?;
             println!("{}", exercise.hint);
         }
-
-        Subcommands::Verify => match verify(&exercises, (0, exercises.len()))? {
+        Some(Subcommands::Verify) => match verify(&exercises, (0, exercises.len()))? {
             VerifyState::AllExercisesDone => println!("All exercises done!"),
             VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
         },
-
-        Subcommands::Watch => match watch(&exercises) {
-            Err(e) => {
-                println!("Error: Could not watch your progress. Error message was {e:?}.");
-                println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
-                exit(1);
-            }
-            Ok(WatchStatus::Finished) => {
-                println!(
-                    "{emoji} All exercises completed! {emoji}",
-                    emoji = Emoji("šŸŽ‰", "ā˜…")
-                );
-                println!("\n{FENISH_LINE}\n");
-            }
-            Ok(WatchStatus::Unfinished) => {
-                println!("We hope you're enjoying learning about Rust!");
-                println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again");
-            }
-        },
     }
 
     Ok(())
 }
 
-fn spawn_watch_shell(
-    failed_exercise_hint: Arc<Mutex<Option<String>>>,
-    should_quit: Arc<AtomicBool>,
-) {
-    println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here.");
-
-    thread::spawn(move || {
-        let mut input = String::with_capacity(32);
-        let mut stdin = io::stdin().lock();
-
-        loop {
-            // Recycle input buffer.
-            input.clear();
-
-            if let Err(e) = stdin.read_line(&mut input) {
-                println!("error reading command: {e}");
-            }
-
-            let input = input.trim();
-            if input == "hint" {
-                if let Some(hint) = &*failed_exercise_hint.lock().unwrap() {
-                    println!("{hint}");
-                }
-            } else if input == "clear" {
-                println!("\x1B[2J\x1B[1;1H");
-            } else if input == "quit" {
-                should_quit.store(true, Ordering::SeqCst);
-                println!("Bye!");
-            } else if input == "help" {
-                println!("{WATCH_MODE_HELP_MESSAGE}");
-            } else {
-                println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}");
-            }
-        }
-    });
-}
-
 fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> {
     if name == "next" {
         for exercise in exercises {
@@ -290,147 +225,3 @@ fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exerci
         .find(|e| e.name == name)
         .with_context(|| format!("No exercise found for '{name}'!"))
 }
-
-enum WatchStatus {
-    Finished,
-    Unfinished,
-}
-
-fn watch(exercises: &[Exercise]) -> Result<WatchStatus> {
-    /* Clears the terminal with an ANSI escape code.
-    Works in UNIX and newer Windows terminals. */
-    fn clear_screen() {
-        println!("\x1Bc");
-    }
-
-    let (tx, rx) = channel();
-    let should_quit = Arc::new(AtomicBool::new(false));
-
-    let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
-    debouncer
-        .watcher()
-        .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
-
-    clear_screen();
-
-    let failed_exercise_hint = match verify(exercises, (0, exercises.len()))? {
-        VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
-        VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
-    };
-
-    spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit));
-
-    let mut pending_exercises = Vec::with_capacity(exercises.len());
-    loop {
-        match rx.recv_timeout(Duration::from_secs(1)) {
-            Ok(event) => match event {
-                Ok(events) => {
-                    for event in events {
-                        if event.kind == DebouncedEventKind::Any
-                            && event.path.extension().is_some_and(|ext| ext == "rs")
-                        {
-                            pending_exercises.extend(exercises.iter().filter(|exercise| {
-                                !exercise.looks_done().unwrap_or(false)
-                                    || event.path.ends_with(&exercise.path)
-                            }));
-                            let num_done = exercises.len() - pending_exercises.len();
-
-                            clear_screen();
-
-                            match verify(
-                                pending_exercises.iter().copied(),
-                                (num_done, exercises.len()),
-                            )? {
-                                VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
-                                VerifyState::Failed(exercise) => {
-                                    let hint = exercise.hint.clone();
-                                    *failed_exercise_hint.lock().unwrap() = Some(hint);
-                                }
-                            }
-
-                            pending_exercises.clear();
-                        }
-                    }
-                }
-                Err(e) => println!("watch error: {e:?}"),
-            },
-            Err(RecvTimeoutError::Timeout) => {
-                // the timeout expired, just check the `should_quit` variable below then loop again
-            }
-            Err(e) => println!("watch error: {e:?}"),
-        }
-        // Check if we need to exit
-        if should_quit.load(Ordering::SeqCst) {
-            return Ok(WatchStatus::Unfinished);
-        }
-    }
-}
-
-const WELCOME: &str = r"       welcome to...
-                 _   _ _
-  _ __ _   _ ___| |_| (_)_ __   __ _ ___
- | '__| | | / __| __| | | '_ \ / _` / __|
- | |  | |_| \__ \ |_| | | | | | (_| \__ \
- |_|   \__,_|___/\__|_|_|_| |_|\__, |___/
-                               |___/";
-
-const DEFAULT_OUT: &str =
-    "Is this your first time? Don't worry, Rustlings was made for beginners! We are
-going to teach you a lot of things about Rust, but before we can get
-started, here's a couple of notes about how Rustlings operates:
-
-1. The central concept behind Rustlings is that you solve exercises. These
-   exercises usually have some sort of syntax error in them, which will cause
-   them to fail compilation or testing. Sometimes there's a logic error instead
-   of a syntax error. No matter what error, it's your job to find it and fix it!
-   You'll know when you fixed it because then, the exercise will compile and
-   Rustlings will be able to move on to the next exercise.
-2. If you run Rustlings in watch mode (which we recommend), it'll automatically
-   start with the first exercise. Don't get confused by an error message popping
-   up as soon as you run Rustlings! This is part of the exercise that you're
-   supposed to solve, so open the exercise file in an editor and start your
-   detective work!
-3. If you're stuck on an exercise, there is a helpful hint you can view by typing
-   'hint' (in watch mode), or running `rustlings hint exercise_name`.
-4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
-   (https://github.com/rust-lang/rustlings/issues/new). We look at every issue,
-   and sometimes, other learners do too so you can help each other out!
-
-Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise.
-Make sure to have your editor open in the `rustlings` directory!";
-
-const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode:
-  hint   - prints the current exercise's hint
-  clear  - clears the screen
-  quit   - quits watch mode
-  help   - displays this help message
-
-Watch mode automatically re-evaluates the current exercise
-when you edit a file's contents.";
-
-const FENISH_LINE: &str = "+----------------------------------------------------+
-|          You made it to the Fe-nish line!          |
-+--------------------------  ------------------------+
-                           \\/\x1b[31m
-     ā–’ā–’          ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’      ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’          ā–’ā–’
-   ā–’ā–’ā–’ā–’  ā–’ā–’    ā–’ā–’        ā–’ā–’  ā–’ā–’        ā–’ā–’    ā–’ā–’  ā–’ā–’ā–’ā–’
-   ā–’ā–’ā–’ā–’  ā–’ā–’  ā–’ā–’            ā–’ā–’            ā–’ā–’  ā–’ā–’  ā–’ā–’ā–’ā–’
- ā–‘ā–‘ā–’ā–’ā–’ā–’ā–‘ā–‘ā–’ā–’  ā–’ā–’            ā–’ā–’            ā–’ā–’  ā–’ā–’ā–‘ā–‘ā–’ā–’ā–’ā–’
-   ā–“ā–“ā–“ā–“ā–“ā–“ā–“ā–“  ā–“ā–“      ā–“ā–“ā–ˆā–ˆ  ā–“ā–“  ā–“ā–“ā–ˆā–ˆ      ā–“ā–“  ā–“ā–“ā–“ā–“ā–“ā–“ā–“ā–“
-     ā–’ā–’ā–’ā–’    ā–’ā–’      ā–ˆā–ˆā–ˆā–ˆ  ā–’ā–’  ā–ˆā–ˆā–ˆā–ˆ      ā–’ā–’ā–‘ā–‘  ā–’ā–’ā–’ā–’
-       ā–’ā–’  ā–’ā–’ā–’ā–’ā–’ā–’        ā–’ā–’ā–’ā–’ā–’ā–’        ā–’ā–’ā–’ā–’ā–’ā–’  ā–’ā–’
-         ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–“ā–“ā–“ā–“ā–“ā–“ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–“ā–“ā–’ā–’ā–“ā–“ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
-           ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
-             ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
-           ā–’ā–’  ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’  ā–’ā–’
-         ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’
-       ā–’ā–’    ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’    ā–’ā–’
-       ā–’ā–’  ā–’ā–’    ā–’ā–’                  ā–’ā–’    ā–’ā–’  ā–’ā–’
-           ā–’ā–’  ā–’ā–’                      ā–’ā–’  ā–’ā–’\x1b[0m
-
-We hope you enjoyed learning about the various aspects of Rust!
-If you noticed any issues, please don't hesitate to report them to our repo.
-You can also contribute your own exercises to help the greater community!
-
-Before reporting an issue or contributing, please read our guidelines:
-https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md";
diff --git a/src/tui.rs b/src/tui.rs
new file mode 100644
index 00000000..bb873652
--- /dev/null
+++ b/src/tui.rs
@@ -0,0 +1,92 @@
+use anyhow::Result;
+use crossterm::{
+    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+    ExecutableCommand,
+};
+use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode, DebouncedEventKind};
+use ratatui::{backend::CrosstermBackend, Terminal};
+use std::{
+    io::stdout,
+    path::Path,
+    sync::mpsc::{channel, RecvTimeoutError},
+    time::Duration,
+};
+
+use crate::{
+    exercise::Exercise,
+    verify::{verify, VerifyState},
+};
+
+fn watch(exercises: &[Exercise]) -> Result<()> {
+    let (tx, rx) = channel();
+
+    let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
+    debouncer
+        .watcher()
+        .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
+
+    let mut failed_exercise_hint = match verify(exercises, (0, exercises.len()))? {
+        VerifyState::AllExercisesDone => return Ok(()),
+        VerifyState::Failed(exercise) => Some(&exercise.hint),
+    };
+
+    let mut pending_exercises = Vec::with_capacity(exercises.len());
+    loop {
+        match rx.recv_timeout(Duration::from_secs(1)) {
+            Ok(event) => match event {
+                Ok(events) => {
+                    for event in events {
+                        if event.kind == DebouncedEventKind::Any
+                            && event.path.extension().is_some_and(|ext| ext == "rs")
+                        {
+                            pending_exercises.extend(exercises.iter().filter(|exercise| {
+                                !exercise.looks_done().unwrap_or(false)
+                                    || event.path.ends_with(&exercise.path)
+                            }));
+                            let num_done = exercises.len() - pending_exercises.len();
+
+                            match verify(
+                                pending_exercises.iter().copied(),
+                                (num_done, exercises.len()),
+                            )? {
+                                VerifyState::AllExercisesDone => return Ok(()),
+                                VerifyState::Failed(exercise) => {
+                                    failed_exercise_hint = Some(&exercise.hint);
+                                }
+                            }
+
+                            pending_exercises.clear();
+                        }
+                    }
+                }
+                Err(e) => println!("watch error: {e:?}"),
+            },
+            Err(RecvTimeoutError::Timeout) => {
+                // the timeout expired, just check the `should_quit` variable below then loop again
+            }
+            Err(e) => println!("watch error: {e:?}"),
+        }
+
+        // TODO: Check if we need to exit
+    }
+}
+
+pub fn tui(exercises: &[Exercise]) -> Result<()> {
+    let mut stdout = stdout().lock();
+    stdout.execute(EnterAlternateScreen)?;
+    enable_raw_mode()?;
+    let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
+    terminal.clear()?;
+
+    watch(exercises)?;
+
+    drop(terminal);
+    stdout.execute(LeaveAlternateScreen)?;
+    disable_raw_mode()?;
+
+    // TODO
+    println!("We hope you're enjoying learning about Rust!");
+    println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again");
+
+    Ok(())
+}
diff --git a/src/verify.rs b/src/verify.rs
index 5beb2069..aec2185c 100644
--- a/src/verify.rs
+++ b/src/verify.rs
@@ -1,5 +1,5 @@
 use anyhow::Result;
-use console::style;
+use crossterm::style::{Attribute, ContentStyle, Stylize};
 use std::io::{stdout, Write};
 
 use crate::exercise::{Exercise, Mode, State};
@@ -50,20 +50,26 @@ pub fn verify<'a>(
             println!(
                 "\nYou can keep working on this exercise,
 or jump into the next one by removing the {} comment:\n",
-                style("`I AM NOT DONE`").bold()
+                "`I AM NOT DONE`".bold()
             );
 
             for context_line in context {
                 let formatted_line = if context_line.important {
-                    format!("{}", style(context_line.line).bold())
+                    format!("{}", context_line.line.bold())
                 } else {
                     context_line.line
                 };
 
                 println!(
                     "{:>2} {}  {}",
-                    style(context_line.number).blue().bold(),
-                    style("|").blue(),
+                    ContentStyle {
+                        foreground_color: Some(crossterm::style::Color::Blue),
+                        background_color: None,
+                        underline_color: None,
+                        attributes: Attribute::Bold.into()
+                    }
+                    .apply(context_line.number),
+                    "|".blue(),
                     formatted_line,
                 );
             }

From 3f2d41de9ecd174ff2b099d3000bf7eca781779d Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 5 Apr 2024 03:05:07 +0200
Subject: [PATCH 011/109] Start with the state

---
 src/main.rs  |  1 +
 src/state.rs | 32 ++++++++++++++++++++++++++++++++
 2 files changed, 33 insertions(+)
 create mode 100644 src/state.rs

diff --git a/src/main.rs b/src/main.rs
index 47afd019..50517850 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,6 +16,7 @@ mod embedded;
 mod exercise;
 mod init;
 mod run;
+mod state;
 mod tui;
 mod verify;
 
diff --git a/src/state.rs b/src/state.rs
new file mode 100644
index 00000000..e3e32990
--- /dev/null
+++ b/src/state.rs
@@ -0,0 +1,32 @@
+use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+use std::{fs, io, path::PathBuf};
+
+#[derive(Serialize, Deserialize)]
+pub struct ExerciseState {
+    pub path: PathBuf,
+    pub done: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct State {
+    pub progress: Vec<ExerciseState>,
+}
+
+impl State {
+    pub fn read() -> Result<Self> {
+        let file_content =
+            fs::read(".rustlings.json").context("Failed to read the file `.rustlings.json`")?;
+
+        serde_json::de::from_slice(&file_content)
+            .context("Failed to deserialize the file `.rustlings.json`")
+    }
+
+    pub fn write(&self) -> io::Result<()> {
+        // TODO: Capacity
+        let mut buf = Vec::with_capacity(1 << 12);
+        serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state");
+        dbg!(buf.len());
+        Ok(())
+    }
+}

From 60155294e94acd661e4fe20cf8b72412167c772d Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sat, 6 Apr 2024 01:45:54 +0200
Subject: [PATCH 012/109] Rename packages

---
 dev/Cargo.toml                   | 2 +-
 gen-dev-cargo-toml/src/main.rs   | 2 +-
 tests/fixture/failure/Cargo.toml | 2 +-
 tests/fixture/state/Cargo.toml   | 2 +-
 tests/fixture/success/Cargo.toml | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/dev/Cargo.toml b/dev/Cargo.toml
index ed9b3ed3..1d230ebb 100644
--- a/dev/Cargo.toml
+++ b/dev/Cargo.toml
@@ -101,6 +101,6 @@ bin = [
 ]
 
 [package]
-name = "rustlings"
+name = "rustlings-dev"
 edition = "2021"
 publish = false
diff --git a/gen-dev-cargo-toml/src/main.rs b/gen-dev-cargo-toml/src/main.rs
index 622762ad..9a7c1bbd 100644
--- a/gen-dev-cargo-toml/src/main.rs
+++ b/gen-dev-cargo-toml/src/main.rs
@@ -48,7 +48,7 @@ bin = [\n",
         br#"]
 
 [package]
-name = "rustlings"
+name = "rustlings-dev"
 edition = "2021"
 publish = false
 "#,
diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml
index e111cf2b..7ee2f068 100644
--- a/tests/fixture/failure/Cargo.toml
+++ b/tests/fixture/failure/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "tests"
+name = "failure"
 edition = "2021"
 publish = false
 
diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml
index c8d74e47..adbd8ab1 100644
--- a/tests/fixture/state/Cargo.toml
+++ b/tests/fixture/state/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "tests"
+name = "state"
 edition = "2021"
 publish = false
 
diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml
index f26a44f1..028cf35a 100644
--- a/tests/fixture/success/Cargo.toml
+++ b/tests/fixture/success/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "tests"
+name = "success"
 edition = "2021"
 publish = false
 

From 06e7216c833f46299c0314bbab47f8df9fc355a3 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sat, 6 Apr 2024 01:46:09 +0200
Subject: [PATCH 013/109] Elimintate an itermediate variable

---
 tests/integration_tests.rs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index d853521f..ccdd910e 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -7,8 +7,7 @@ use std::process::Command;
 
 #[test]
 fn runs_without_arguments() {
-    let mut cmd = Command::cargo_bin("rustlings").unwrap();
-    cmd.assert().success();
+    Command::cargo_bin("rustlings").unwrap().assert().success();
 }
 
 #[test]

From de9a0ed5221934b43a27921455f484e006c3ec20 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sat, 6 Apr 2024 01:46:22 +0200
Subject: [PATCH 014/109] Update state

---
 src/state.rs | 34 ++++++++++++++++++++--------------
 1 file changed, 20 insertions(+), 14 deletions(-)

diff --git a/src/state.rs b/src/state.rs
index e3e32990..60f6a379 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -1,31 +1,37 @@
 use anyhow::{Context, Result};
 use serde::{Deserialize, Serialize};
-use std::{fs, io, path::PathBuf};
+use std::fs;
 
-#[derive(Serialize, Deserialize)]
-pub struct ExerciseState {
-    pub path: PathBuf,
-    pub done: bool,
-}
+use crate::exercise::Exercise;
 
 #[derive(Serialize, Deserialize)]
 pub struct State {
-    pub progress: Vec<ExerciseState>,
+    pub progress: Vec<bool>,
 }
 
 impl State {
-    pub fn read() -> Result<Self> {
-        let file_content =
-            fs::read(".rustlings.json").context("Failed to read the file `.rustlings.json`")?;
+    fn read(exercises: &[Exercise]) -> Option<Self> {
+        let file_content = fs::read(".rustlings.json").ok()?;
 
-        serde_json::de::from_slice(&file_content)
-            .context("Failed to deserialize the file `.rustlings.json`")
+        let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
+
+        if slf.progress.len() != exercises.len() {
+            return None;
+        }
+
+        Some(slf)
     }
 
-    pub fn write(&self) -> io::Result<()> {
+    pub fn read_or_default(exercises: &[Exercise]) -> Self {
+        Self::read(exercises).unwrap_or_else(|| Self {
+            progress: vec![false; exercises.len()],
+        })
+    }
+
+    pub fn write(&self) -> Result<()> {
         // TODO: Capacity
         let mut buf = Vec::with_capacity(1 << 12);
-        serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state");
+        serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?;
         dbg!(buf.len());
         Ok(())
     }

From c2daad8340c04eaa84525f6ee832972667068fd6 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 01:15:47 +0200
Subject: [PATCH 015/109] Return an error instead of exiting

---
 src/exercise.rs | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/src/exercise.rs b/src/exercise.rs
index d5ca254e..d01d427a 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Display, Formatter};
 use std::fs::{self, File};
 use std::io::{self, BufRead, BufReader};
 use std::path::PathBuf;
-use std::process::{exit, Command, Output};
+use std::process::{Command, Output};
 use std::{array, mem};
 use winnow::ascii::{space0, Caseless};
 use winnow::combinator::opt;
@@ -145,13 +145,9 @@ impl Exercise {
         let mut line = String::with_capacity(256);
 
         loop {
-            let n = read_line(&mut line).unwrap_or_else(|e| {
-                println!(
-                    "Failed to read the exercise file {}: {e}",
-                    self.path.display(),
-                );
-                exit(1);
-            });
+            let n = read_line(&mut line).with_context(|| {
+                format!("Failed to read the exercise file {}", self.path.display())
+            })?;
 
             // Reached the end of the file and didn't find the comment.
             if n == 0 {

From 18342b3aa3bd43c2c013614935f45e7d6bbaea8f Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 01:16:56 +0200
Subject: [PATCH 016/109] Verify starting with some index

---
 src/verify.rs | 25 ++++++++++---------------
 1 file changed, 10 insertions(+), 15 deletions(-)

diff --git a/src/verify.rs b/src/verify.rs
index aec2185c..c4368cc7 100644
--- a/src/verify.rs
+++ b/src/verify.rs
@@ -14,17 +14,16 @@ pub enum VerifyState<'a> {
 // Any such failures will be reported to the end user.
 // If the Exercise being verified is a test, the verbose boolean
 // determines whether or not the test harness outputs are displayed.
-pub fn verify<'a>(
-    pending_exercises: impl IntoIterator<Item = &'a Exercise>,
-    progress: (usize, usize),
-) -> Result<VerifyState<'a>> {
-    let (mut num_done, total) = progress;
-    println!(
-        "Progress: {num_done}/{total} ({:.1}%)\n",
-        num_done as f32 / total as f32 * 100.0,
-    );
+pub fn verify(exercises: &[Exercise], mut current_exercise_ind: usize) -> Result<VerifyState<'_>> {
+    while current_exercise_ind < exercises.len() {
+        let exercise = &exercises[current_exercise_ind];
+
+        println!(
+            "Progress: {current_exercise_ind}/{} ({:.1}%)\n",
+            exercises.len(),
+            current_exercise_ind as f32 / exercises.len() as f32 * 100.0,
+        );
 
-    for exercise in pending_exercises {
         let output = exercise.run()?;
 
         {
@@ -76,11 +75,7 @@ or jump into the next one by removing the {} comment:\n",
             return Ok(VerifyState::Failed(exercise));
         }
 
-        num_done += 1;
-        println!(
-            "Progress: {num_done}/{total} ({:.1}%)\n",
-            num_done as f32 / total as f32 * 100.0,
-        );
+        current_exercise_ind += 1;
     }
 
     Ok(VerifyState::AllExercisesDone)

From 0819bbe21fc86315d3acdcdb2bc14b21f3acb788 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 01:17:53 +0200
Subject: [PATCH 017/109] Can't use Ratatui for the watch mode :(

---
 src/main.rs  |  22 ++---
 src/tui.rs   |  92 --------------------
 src/watch.rs | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 251 insertions(+), 103 deletions(-)
 delete mode 100644 src/tui.rs
 create mode 100644 src/watch.rs

diff --git a/src/main.rs b/src/main.rs
index 50517850..e8218efe 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,11 +1,11 @@
-use crate::consts::{DEFAULT_OUT, WELCOME};
+use crate::consts::WELCOME;
 use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
 use crate::exercise::{Exercise, ExerciseList};
 use crate::run::run;
-use crate::tui::tui;
 use crate::verify::verify;
 use anyhow::{bail, Context, Result};
 use clap::{Parser, Subcommand};
+use state::State;
 use std::io::Write;
 use std::path::Path;
 use std::process::exit;
@@ -17,8 +17,8 @@ mod exercise;
 mod init;
 mod run;
 mod state;
-mod tui;
 mod verify;
+mod watch;
 
 /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
 #[derive(Parser)]
@@ -75,10 +75,6 @@ enum Subcommands {
 fn main() -> Result<()> {
     let args = Args::parse();
 
-    if args.command.is_none() {
-        println!("\n{WELCOME}\n");
-    }
-
     which::which("cargo").context(
         "Failed to find `cargo`.
 Did you already install Rust?
@@ -97,16 +93,20 @@ Then run `rustlings` for further instructions on getting started."
         return Ok(());
     } else if !Path::new("exercises").is_dir() {
         println!(
-            "\nThe `exercises` directory wasn't found in the current directory.
+            "
+{WELCOME}
+
+The `exercises` directory wasn't found in the current directory.
 If you are just starting with Rustlings, run the command `rustlings init` to initialize it."
         );
         exit(1);
     }
 
+    let state = State::read_or_default(&exercises);
+
     match args.command {
         None | Some(Subcommands::Watch) => {
-            println!("{DEFAULT_OUT}\n");
-            tui(&exercises)?;
+            watch::watch(&state, &exercises)?;
         }
         // `Init` is handled above.
         Some(Subcommands::Init) => (),
@@ -199,7 +199,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
             let exercise = find_exercise(&name, &exercises)?;
             println!("{}", exercise.hint);
         }
-        Some(Subcommands::Verify) => match verify(&exercises, (0, exercises.len()))? {
+        Some(Subcommands::Verify) => match verify(&exercises, 0)? {
             VerifyState::AllExercisesDone => println!("All exercises done!"),
             VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
         },
diff --git a/src/tui.rs b/src/tui.rs
deleted file mode 100644
index bb873652..00000000
--- a/src/tui.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use anyhow::Result;
-use crossterm::{
-    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
-    ExecutableCommand,
-};
-use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode, DebouncedEventKind};
-use ratatui::{backend::CrosstermBackend, Terminal};
-use std::{
-    io::stdout,
-    path::Path,
-    sync::mpsc::{channel, RecvTimeoutError},
-    time::Duration,
-};
-
-use crate::{
-    exercise::Exercise,
-    verify::{verify, VerifyState},
-};
-
-fn watch(exercises: &[Exercise]) -> Result<()> {
-    let (tx, rx) = channel();
-
-    let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
-    debouncer
-        .watcher()
-        .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
-
-    let mut failed_exercise_hint = match verify(exercises, (0, exercises.len()))? {
-        VerifyState::AllExercisesDone => return Ok(()),
-        VerifyState::Failed(exercise) => Some(&exercise.hint),
-    };
-
-    let mut pending_exercises = Vec::with_capacity(exercises.len());
-    loop {
-        match rx.recv_timeout(Duration::from_secs(1)) {
-            Ok(event) => match event {
-                Ok(events) => {
-                    for event in events {
-                        if event.kind == DebouncedEventKind::Any
-                            && event.path.extension().is_some_and(|ext| ext == "rs")
-                        {
-                            pending_exercises.extend(exercises.iter().filter(|exercise| {
-                                !exercise.looks_done().unwrap_or(false)
-                                    || event.path.ends_with(&exercise.path)
-                            }));
-                            let num_done = exercises.len() - pending_exercises.len();
-
-                            match verify(
-                                pending_exercises.iter().copied(),
-                                (num_done, exercises.len()),
-                            )? {
-                                VerifyState::AllExercisesDone => return Ok(()),
-                                VerifyState::Failed(exercise) => {
-                                    failed_exercise_hint = Some(&exercise.hint);
-                                }
-                            }
-
-                            pending_exercises.clear();
-                        }
-                    }
-                }
-                Err(e) => println!("watch error: {e:?}"),
-            },
-            Err(RecvTimeoutError::Timeout) => {
-                // the timeout expired, just check the `should_quit` variable below then loop again
-            }
-            Err(e) => println!("watch error: {e:?}"),
-        }
-
-        // TODO: Check if we need to exit
-    }
-}
-
-pub fn tui(exercises: &[Exercise]) -> Result<()> {
-    let mut stdout = stdout().lock();
-    stdout.execute(EnterAlternateScreen)?;
-    enable_raw_mode()?;
-    let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
-    terminal.clear()?;
-
-    watch(exercises)?;
-
-    drop(terminal);
-    stdout.execute(LeaveAlternateScreen)?;
-    disable_raw_mode()?;
-
-    // TODO
-    println!("We hope you're enjoying learning about Rust!");
-    println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again");
-
-    Ok(())
-}
diff --git a/src/watch.rs b/src/watch.rs
new file mode 100644
index 00000000..92da20dd
--- /dev/null
+++ b/src/watch.rs
@@ -0,0 +1,240 @@
+use anyhow::Result;
+use crossterm::{
+    style::{Attribute, ContentStyle, Stylize},
+    terminal::{Clear, ClearType},
+    ExecutableCommand,
+};
+use notify_debouncer_mini::{
+    new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind,
+};
+use std::{
+    fmt::Write as _,
+    io::{self, BufRead, StdoutLock, Write},
+    path::Path,
+    sync::mpsc::{channel, sync_channel, Receiver},
+    thread,
+    time::Duration,
+};
+
+use crate::{
+    exercise::{self, Exercise},
+    state::State,
+};
+
+enum Event {
+    Hint,
+    Clear,
+    Quit,
+}
+
+struct WatchState<'a> {
+    writer: StdoutLock<'a>,
+    rx: Receiver<DebounceEventResult>,
+    exercises: &'a [Exercise],
+    exercise: &'a Exercise,
+    current_exercise_ind: usize,
+    stdout: Option<Vec<u8>>,
+    stderr: Option<Vec<u8>>,
+    message: Option<String>,
+    prompt: Vec<u8>,
+}
+
+impl<'a> WatchState<'a> {
+    fn run_exercise(&mut self) -> Result<bool> {
+        let output = self.exercise.run()?;
+
+        if !output.status.success() {
+            self.stdout = Some(output.stdout);
+            self.stderr = Some(output.stderr);
+            return Ok(false);
+        }
+
+        if let exercise::State::Pending(context) = self.exercise.state()? {
+            let mut message = format!(
+                "
+You can keep working on this exercise or jump into the next one by removing the {} comment:
+
+",
+                "`I AM NOT DONE`".bold(),
+            );
+
+            for context_line in context {
+                let formatted_line = if context_line.important {
+                    context_line.line.bold()
+                } else {
+                    context_line.line.stylize()
+                };
+
+                writeln!(
+                    message,
+                    "{:>2} {}  {}",
+                    ContentStyle {
+                        foreground_color: Some(crossterm::style::Color::Blue),
+                        background_color: None,
+                        underline_color: None,
+                        attributes: Attribute::Bold.into()
+                    }
+                    .apply(context_line.number),
+                    "|".blue(),
+                    formatted_line,
+                )?;
+            }
+
+            self.stdout = Some(output.stdout);
+            self.message = Some(message);
+            return Ok(false);
+        }
+
+        Ok(true)
+    }
+
+    fn try_recv_event(&mut self) -> Result<()> {
+        let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else {
+            return Ok(());
+        };
+
+        if let Some(current_exercise_ind) = events?
+            .iter()
+            .filter_map(|event| {
+                if event.kind != DebouncedEventKind::Any
+                    || !event.path.extension().is_some_and(|ext| ext == "rs")
+                {
+                    return None;
+                }
+
+                self.exercises
+                    .iter()
+                    .position(|exercise| event.path.ends_with(&exercise.path))
+            })
+            .min()
+        {
+            self.current_exercise_ind = current_exercise_ind;
+        } else {
+            return Ok(());
+        };
+
+        while self.current_exercise_ind < self.exercises.len() {
+            self.exercise = &self.exercises[self.current_exercise_ind];
+            if !self.run_exercise()? {
+                break;
+            }
+
+            self.current_exercise_ind += 1;
+        }
+
+        Ok(())
+    }
+
+    fn prompt(&mut self) -> io::Result<()> {
+        self.writer.write_all(&self.prompt)?;
+        self.writer.flush()
+    }
+
+    fn render(&mut self) -> Result<()> {
+        self.writer.execute(Clear(ClearType::All))?;
+
+        if let Some(stdout) = &self.stdout {
+            self.writer.write_all(stdout)?;
+        }
+
+        if let Some(stderr) = &self.stderr {
+            self.writer.write_all(stderr)?;
+        }
+
+        if let Some(message) = &self.message {
+            self.writer.write_all(message.as_bytes())?;
+        }
+
+        self.prompt()?;
+
+        Ok(())
+    }
+}
+
+pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> {
+    let (tx, rx) = channel();
+    let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
+    debouncer
+        .watcher()
+        .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
+
+    let current_exercise_ind = state.progress.iter().position(|done| *done).unwrap_or(0);
+
+    let exercise = &exercises[current_exercise_ind];
+
+    let writer = io::stdout().lock();
+
+    let mut watch_state = WatchState {
+        writer,
+        rx,
+        exercises,
+        exercise,
+        current_exercise_ind,
+        stdout: None,
+        stderr: None,
+        message: None,
+        prompt: format!(
+            "\n\n{}int/{}lear/{}uit? ",
+            "h".bold(),
+            "c".bold(),
+            "q".bold()
+        )
+        .into_bytes(),
+    };
+
+    watch_state.run_exercise()?;
+    watch_state.render()?;
+
+    let (tx, rx) = sync_channel(0);
+    thread::spawn(move || {
+        let mut stdin = io::stdin().lock();
+        let mut stdin_buf = String::with_capacity(8);
+
+        loop {
+            stdin.read_line(&mut stdin_buf).unwrap();
+
+            let event = match stdin_buf.trim() {
+                "h" | "hint" => Some(Event::Hint),
+                "c" | "clear" => Some(Event::Clear),
+                "q" | "quit" => Some(Event::Quit),
+                _ => None,
+            };
+
+            stdin_buf.clear();
+
+            if tx.send(event).is_err() {
+                break;
+            };
+        }
+    });
+
+    loop {
+        watch_state.try_recv_event()?;
+
+        if let Ok(event) = rx.try_recv() {
+            match event {
+                Some(Event::Hint) => {
+                    watch_state
+                        .writer
+                        .write_all(watch_state.exercise.hint.as_bytes())?;
+                    watch_state.prompt()?;
+                }
+                Some(Event::Clear) => {
+                    watch_state.render()?;
+                }
+                Some(Event::Quit) => break,
+                None => {
+                    watch_state.writer.write_all(b"Invalid command")?;
+                    watch_state.prompt()?;
+                }
+            }
+        }
+    }
+
+    watch_state.writer.write_all(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.
+")?;
+
+    Ok(())
+}

From f6db88aca860b229e97712a612cee8ab4436b764 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 03:03:37 +0200
Subject: [PATCH 018/109] Started with list

---
 src/list.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main.rs | 96 +++--------------------------------------------------
 2 files changed, 97 insertions(+), 92 deletions(-)
 create mode 100644 src/list.rs

diff --git a/src/list.rs b/src/list.rs
new file mode 100644
index 00000000..f8713b07
--- /dev/null
+++ b/src/list.rs
@@ -0,0 +1,93 @@
+use std::{io, time::Duration};
+
+use anyhow::Result;
+use crossterm::{
+    event::{self, KeyCode, KeyEventKind},
+    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+    ExecutableCommand,
+};
+use ratatui::{
+    backend::CrosstermBackend,
+    layout::Constraint,
+    style::{Modifier, Style, Stylize},
+    text::Span,
+    widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState},
+    Terminal,
+};
+
+use crate::{exercise::Exercise, state::State};
+
+// 40 FPS.
+const UPDATE_INTERVAL: Duration = Duration::from_millis(25);
+
+pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
+    let mut stdout = io::stdout().lock();
+
+    stdout.execute(EnterAlternateScreen)?;
+    enable_raw_mode()?;
+
+    let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
+    terminal.clear()?;
+
+    let header = Row::new(["State", "Name", "Path"]);
+
+    let max_name_len = exercises
+        .iter()
+        .map(|exercise| exercise.name.len())
+        .max()
+        .unwrap_or(4) as u16;
+
+    let widths = [
+        Constraint::Length(7),
+        Constraint::Length(max_name_len),
+        Constraint::Fill(1),
+    ];
+
+    let rows = exercises
+        .iter()
+        .zip(&state.progress)
+        .map(|(exercise, done)| {
+            let state = if *done {
+                "DONE".green()
+            } else {
+                "PENDING".yellow()
+            };
+            Row::new([
+                state,
+                Span::raw(&exercise.name),
+                Span::raw(exercise.path.to_string_lossy()),
+            ])
+        })
+        .collect::<Vec<_>>();
+
+    let table = Table::new(rows, widths)
+        .header(header)
+        .column_spacing(2)
+        .highlight_spacing(HighlightSpacing::Always)
+        .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
+        .highlight_symbol("šŸ¦€");
+
+    let mut table_state = TableState::default().with_selected(Some(0));
+
+    loop {
+        terminal.draw(|frame| {
+            let area = frame.size();
+
+            frame.render_stateful_widget(&table, area, &mut table_state);
+        })?;
+
+        if event::poll(UPDATE_INTERVAL)? {
+            if let event::Event::Key(key) = event::read()? {
+                if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
+                    break;
+                }
+            }
+        }
+    }
+
+    drop(terminal);
+    stdout.execute(LeaveAlternateScreen)?;
+    disable_raw_mode()?;
+
+    Ok(())
+}
diff --git a/src/main.rs b/src/main.rs
index e8218efe..34d1784a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,7 +6,6 @@ use crate::verify::verify;
 use anyhow::{bail, Context, Result};
 use clap::{Parser, Subcommand};
 use state::State;
-use std::io::Write;
 use std::path::Path;
 use std::process::exit;
 use verify::VerifyState;
@@ -15,6 +14,7 @@ mod consts;
 mod embedded;
 mod exercise;
 mod init;
+mod list;
 mod run;
 mod state;
 mod verify;
@@ -52,24 +52,7 @@ enum Subcommands {
         name: String,
     },
     /// List the exercises available in Rustlings
-    List {
-        /// Show only the paths of the exercises
-        #[arg(short, long)]
-        paths: bool,
-        /// Show only the names of the exercises
-        #[arg(short, long)]
-        names: bool,
-        /// Provide a string to match exercise names.
-        /// Comma separated patterns are accepted
-        #[arg(short, long)]
-        filter: Option<String>,
-        /// Display only exercises not yet solved
-        #[arg(short, long)]
-        unsolved: bool,
-        /// Display only exercises that have been solved
-        #[arg(short, long)]
-        solved: bool,
-    },
+    List,
 }
 
 fn main() -> Result<()> {
@@ -110,79 +93,8 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         }
         // `Init` is handled above.
         Some(Subcommands::Init) => (),
-        Some(Subcommands::List {
-            paths,
-            names,
-            filter,
-            unsolved,
-            solved,
-        }) => {
-            if !paths && !names {
-                println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status");
-            }
-            let mut exercises_done: u16 = 0;
-            let lowercase_filter = filter
-                .as_ref()
-                .map(|s| s.to_lowercase())
-                .unwrap_or_default();
-            let filters = lowercase_filter
-                .split(',')
-                .filter_map(|f| {
-                    let f = f.trim();
-                    if f.is_empty() {
-                        None
-                    } else {
-                        Some(f)
-                    }
-                })
-                .collect::<Vec<_>>();
-
-            for exercise in &exercises {
-                let fname = exercise.path.to_string_lossy();
-                let filter_cond = filters
-                    .iter()
-                    .any(|f| exercise.name.contains(f) || fname.contains(f));
-                let looks_done = exercise.looks_done()?;
-                let status = if looks_done {
-                    exercises_done += 1;
-                    "Done"
-                } else {
-                    "Pending"
-                };
-                let solve_cond =
-                    (looks_done && solved) || (!looks_done && unsolved) || (!solved && !unsolved);
-                if solve_cond && (filter_cond || filter.is_none()) {
-                    let line = if paths {
-                        format!("{fname}\n")
-                    } else if names {
-                        format!("{}\n", exercise.name)
-                    } else {
-                        format!("{:<17}\t{fname:<46}\t{status:<7}\n", exercise.name)
-                    };
-                    // Somehow using println! leads to the binary panicking
-                    // when its output is piped.
-                    // So, we're handling a Broken Pipe error and exiting with 0 anyway
-                    let stdout = std::io::stdout();
-                    {
-                        let mut handle = stdout.lock();
-                        handle.write_all(line.as_bytes()).unwrap_or_else(|e| {
-                            match e.kind() {
-                                std::io::ErrorKind::BrokenPipe => exit(0),
-                                _ => exit(1),
-                            };
-                        });
-                    }
-                }
-            }
-
-            let percentage_progress = exercises_done as f32 / exercises.len() as f32 * 100.0;
-            println!(
-                "Progress: You completed {} / {} exercises ({:.1} %).",
-                exercises_done,
-                exercises.len(),
-                percentage_progress
-            );
-            exit(0);
+        Some(Subcommands::List) => {
+            list::list(&state, &exercises)?;
         }
         Some(Subcommands::Run { name }) => {
             let exercise = find_exercise(&name, &exercises)?;

From 729385362c06da0c90015bb2d4b6b341d2cd489b Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 03:03:59 +0200
Subject: [PATCH 019/109] Update deps

---
 Cargo.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 33d3030a..ee469437 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -711,9 +711,9 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
 
 [[package]]
 name = "ryu"

From 372290a796eb27b28edaf2475ebbb4e6e09090b3 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 03:38:18 +0200
Subject: [PATCH 020/109] Done navigation

---
 src/list.rs | 83 ++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 56 insertions(+), 27 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index f8713b07..82c3e465 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -1,34 +1,22 @@
-use std::{io, time::Duration};
-
 use anyhow::Result;
 use crossterm::{
-    event::{self, KeyCode, KeyEventKind},
+    event::{self, Event, KeyCode, KeyEventKind},
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
     ExecutableCommand,
 };
 use ratatui::{
     backend::CrosstermBackend,
     layout::Constraint,
-    style::{Modifier, Style, Stylize},
+    style::{Style, Stylize},
     text::Span,
-    widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState},
+    widgets::{HighlightSpacing, Row, Table, TableState},
     Terminal,
 };
+use std::io;
 
 use crate::{exercise::Exercise, state::State};
 
-// 40 FPS.
-const UPDATE_INTERVAL: Duration = Duration::from_millis(25);
-
-pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
-    let mut stdout = io::stdout().lock();
-
-    stdout.execute(EnterAlternateScreen)?;
-    enable_raw_mode()?;
-
-    let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
-    terminal.clear()?;
-
+fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
     let header = Row::new(["State", "Name", "Path"]);
 
     let max_name_len = exercises
@@ -60,28 +48,69 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
         })
         .collect::<Vec<_>>();
 
-    let table = Table::new(rows, widths)
+    Table::new(rows, widths)
         .header(header)
         .column_spacing(2)
         .highlight_spacing(HighlightSpacing::Always)
-        .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
-        .highlight_symbol("šŸ¦€");
+        .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
+        .highlight_symbol("šŸ¦€")
+}
 
-    let mut table_state = TableState::default().with_selected(Some(0));
+pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
+    let mut stdout = io::stdout().lock();
 
-    loop {
+    stdout.execute(EnterAlternateScreen)?;
+    enable_raw_mode()?;
+
+    let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
+    terminal.clear()?;
+
+    let table = table(state, exercises);
+
+    let last_ind = exercises.len() - 1;
+    let mut selected = 0;
+    let mut table_state = TableState::default().with_selected(Some(selected));
+
+    'outer: loop {
         terminal.draw(|frame| {
             let area = frame.size();
 
             frame.render_stateful_widget(&table, area, &mut table_state);
         })?;
 
-        if event::poll(UPDATE_INTERVAL)? {
-            if let event::Event::Key(key) = event::read()? {
-                if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
-                    break;
-                }
+        let key = loop {
+            match event::read()? {
+                Event::Key(key) => break key,
+                // Redraw
+                Event::Resize(_, _) => continue 'outer,
+                // Ignore
+                Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (),
             }
+        };
+
+        if key.kind != KeyEventKind::Press {
+            continue;
+        }
+
+        match key.code {
+            KeyCode::Char('q') => break,
+            KeyCode::Down | KeyCode::Char('j') => {
+                selected = selected.saturating_add(1).min(last_ind);
+                table_state.select(Some(selected));
+            }
+            KeyCode::Up | KeyCode::Char('k') => {
+                selected = selected.saturating_sub(1).max(0);
+                table_state.select(Some(selected));
+            }
+            KeyCode::Home | KeyCode::Char('g') => {
+                selected = 0;
+                table_state.select(Some(selected));
+            }
+            KeyCode::End | KeyCode::Char('G') => {
+                selected = last_ind;
+                table_state.select(Some(selected));
+            }
+            _ => (),
         }
     }
 

From c4897139aeff2316d2b737a4e03b7491b696ce3b Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 03:41:23 +0200
Subject: [PATCH 021/109] Prevent unneeded redraws

---
 src/list.rs | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index 82c3e465..b8ea27bb 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -80,7 +80,13 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
 
         let key = loop {
             match event::read()? {
-                Event::Key(key) => break key,
+                Event::Key(key) => {
+                    if key.kind != KeyEventKind::Press {
+                        continue;
+                    }
+
+                    break key;
+                }
                 // Redraw
                 Event::Resize(_, _) => continue 'outer,
                 // Ignore
@@ -88,10 +94,6 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
             }
         };
 
-        if key.kind != KeyEventKind::Press {
-            continue;
-        }
-
         match key.code {
             KeyCode::Char('q') => break,
             KeyCode::Down | KeyCode::Char('j') => {

From 7f5a18fa3478596c3c1dbdc7eb92da99b0945886 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 04:19:50 +0200
Subject: [PATCH 022/109] Show help message

---
 src/list.rs | 27 +++++++++++++++++++++++----
 1 file changed, 23 insertions(+), 4 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index b8ea27bb..7329d2b8 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -6,10 +6,10 @@ use crossterm::{
 };
 use ratatui::{
     backend::CrosstermBackend,
-    layout::Constraint,
+    layout::{Constraint, Rect},
     style::{Style, Stylize},
-    text::Span,
-    widgets::{HighlightSpacing, Row, Table, TableState},
+    text::{Line, Span},
+    widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState},
     Terminal,
 };
 use std::io;
@@ -54,6 +54,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
         .highlight_spacing(HighlightSpacing::Always)
         .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
         .highlight_symbol("šŸ¦€")
+        .block(Block::default().borders(Borders::BOTTOM))
 }
 
 pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
@@ -75,7 +76,25 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
         terminal.draw(|frame| {
             let area = frame.size();
 
-            frame.render_stateful_widget(&table, area, &mut table_state);
+            frame.render_stateful_widget(
+                &table,
+                Rect {
+                    x: 0,
+                    y: 0,
+                    width: area.width,
+                    height: area.height - 1,
+                },
+                &mut table_state,
+            );
+            frame.render_widget(
+                Span::raw("Navi: ↓/j ↑/k home/g end/G │ Filter done/pending: d/p │ Reset: r │ Continue at: c │ Quit: q"),
+                Rect {
+                    x: 0,
+                    y: area.height - 1,
+                    width: area.width,
+                    height: 1,
+                },
+            );
         })?;
 
         let key = loop {

From e640b4a1ffec82cba6b34c0bd222f4ab65502daa Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 04:36:27 +0200
Subject: [PATCH 023/109] Add "Next" column

---
 src/list.rs  | 20 +++++++++++++++-----
 src/state.rs |  4 +++-
 2 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index 7329d2b8..ce809efe 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -8,7 +8,7 @@ use ratatui::{
     backend::CrosstermBackend,
     layout::{Constraint, Rect},
     style::{Style, Stylize},
-    text::{Line, Span},
+    text::Span,
     widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState},
     Terminal,
 };
@@ -17,7 +17,7 @@ use std::io;
 use crate::{exercise::Exercise, state::State};
 
 fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
-    let header = Row::new(["State", "Name", "Path"]);
+    let header = Row::new(["Next", "State", "Name", "Path"]);
 
     let max_name_len = exercises
         .iter()
@@ -26,6 +26,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
         .unwrap_or(4) as u16;
 
     let widths = [
+        Constraint::Length(4),
         Constraint::Length(7),
         Constraint::Length(max_name_len),
         Constraint::Fill(1),
@@ -34,14 +35,23 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
     let rows = exercises
         .iter()
         .zip(&state.progress)
-        .map(|(exercise, done)| {
-            let state = if *done {
+        .enumerate()
+        .map(|(ind, (exercise, done))| {
+            let exercise_state = if *done {
                 "DONE".green()
             } else {
                 "PENDING".yellow()
             };
+
+            let next = if ind == state.next_exercise_ind {
+                ">>>>".bold().red()
+            } else {
+                Span::default()
+            };
+
             Row::new([
-                state,
+                next,
+                exercise_state,
                 Span::raw(&exercise.name),
                 Span::raw(exercise.path.to_string_lossy()),
             ])
diff --git a/src/state.rs b/src/state.rs
index 60f6a379..f29dc135 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -6,6 +6,7 @@ use crate::exercise::Exercise;
 
 #[derive(Serialize, Deserialize)]
 pub struct State {
+    pub next_exercise_ind: usize,
     pub progress: Vec<bool>,
 }
 
@@ -15,7 +16,7 @@ impl State {
 
         let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
 
-        if slf.progress.len() != exercises.len() {
+        if slf.progress.len() != exercises.len() || slf.next_exercise_ind >= exercises.len() {
             return None;
         }
 
@@ -24,6 +25,7 @@ impl State {
 
     pub fn read_or_default(exercises: &[Exercise]) -> Self {
         Self::read(exercises).unwrap_or_else(|| Self {
+            next_exercise_ind: 0,
             progress: vec![false; exercises.len()],
         })
     }

From 4f69285375342951da36346f1a1b93f7903a362f Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 04:39:03 +0200
Subject: [PATCH 024/109] Shorten the help footer

---
 src/list.rs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/list.rs b/src/list.rs
index ce809efe..ff031cb0 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -96,8 +96,10 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
                 },
                 &mut table_state,
             );
+
+            // Help footer
             frame.render_widget(
-                Span::raw("Navi: ↓/j ↑/k home/g end/G │ Filter done/pending: d/p │ Reset: r │ Continue at: c │ Quit: q"),
+                Span::raw("↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit"),
                 Rect {
                     x: 0,
                     y: area.height - 1,

From b0a475062445705853b4f861ee9e3135065f0660 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 04:59:22 +0200
Subject: [PATCH 025/109] Implement "continue at"

---
 src/list.rs  | 66 +++++++++++++++++++++++++++++-----------------------
 src/main.rs  |  4 ++--
 src/state.rs | 31 +++++++++++++++++++-----
 src/watch.rs |  2 +-
 4 files changed, 65 insertions(+), 38 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index ff031cb0..bb5ba1c0 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -16,6 +16,36 @@ use std::io;
 
 use crate::{exercise::Exercise, state::State};
 
+fn rows<'s, 'e>(state: &'s State, exercises: &'e [Exercise]) -> impl Iterator<Item = Row<'e>> + 's
+where
+    'e: 's,
+{
+    exercises
+        .iter()
+        .zip(state.progress())
+        .enumerate()
+        .map(|(ind, (exercise, done))| {
+            let exercise_state = if *done {
+                "DONE".green()
+            } else {
+                "PENDING".yellow()
+            };
+
+            let next = if ind == state.next_exercise_ind() {
+                ">>>>".bold().red()
+            } else {
+                Span::default()
+            };
+
+            Row::new([
+                next,
+                exercise_state,
+                Span::raw(&exercise.name),
+                Span::raw(exercise.path.to_string_lossy()),
+            ])
+        })
+}
+
 fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
     let header = Row::new(["Next", "State", "Name", "Path"]);
 
@@ -32,33 +62,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
         Constraint::Fill(1),
     ];
 
-    let rows = exercises
-        .iter()
-        .zip(&state.progress)
-        .enumerate()
-        .map(|(ind, (exercise, done))| {
-            let exercise_state = if *done {
-                "DONE".green()
-            } else {
-                "PENDING".yellow()
-            };
-
-            let next = if ind == state.next_exercise_ind {
-                ">>>>".bold().red()
-            } else {
-                Span::default()
-            };
-
-            Row::new([
-                next,
-                exercise_state,
-                Span::raw(&exercise.name),
-                Span::raw(exercise.path.to_string_lossy()),
-            ])
-        })
-        .collect::<Vec<_>>();
-
-    Table::new(rows, widths)
+    Table::new(rows(state, exercises), widths)
         .header(header)
         .column_spacing(2)
         .highlight_spacing(HighlightSpacing::Always)
@@ -67,7 +71,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
         .block(Block::default().borders(Borders::BOTTOM))
 }
 
-pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
+pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> {
     let mut stdout = io::stdout().lock();
 
     stdout.execute(EnterAlternateScreen)?;
@@ -76,7 +80,7 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
     let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
     terminal.clear()?;
 
-    let table = table(state, exercises);
+    let mut table = table(state, exercises);
 
     let last_ind = exercises.len() - 1;
     let mut selected = 0;
@@ -143,6 +147,10 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> {
                 selected = last_ind;
                 table_state.select(Some(selected));
             }
+            KeyCode::Char('c') => {
+                state.set_next_exercise_ind(selected)?;
+                table = table.rows(rows(state, exercises));
+            }
             _ => (),
         }
     }
diff --git a/src/main.rs b/src/main.rs
index 34d1784a..e82fc808 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -85,7 +85,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         exit(1);
     }
 
-    let state = State::read_or_default(&exercises);
+    let mut state = State::read_or_default(&exercises);
 
     match args.command {
         None | Some(Subcommands::Watch) => {
@@ -94,7 +94,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         // `Init` is handled above.
         Some(Subcommands::Init) => (),
         Some(Subcommands::List) => {
-            list::list(&state, &exercises)?;
+            list::list(&mut state, &exercises)?;
         }
         Some(Subcommands::Run { name }) => {
             let exercise = find_exercise(&name, &exercises)?;
diff --git a/src/state.rs b/src/state.rs
index f29dc135..5a644873 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -1,4 +1,4 @@
-use anyhow::{Context, Result};
+use anyhow::{bail, Context, Result};
 use serde::{Deserialize, Serialize};
 use std::fs;
 
@@ -6,8 +6,8 @@ use crate::exercise::Exercise;
 
 #[derive(Serialize, Deserialize)]
 pub struct State {
-    pub next_exercise_ind: usize,
-    pub progress: Vec<bool>,
+    next_exercise_ind: usize,
+    progress: Vec<bool>,
 }
 
 impl State {
@@ -30,11 +30,30 @@ impl State {
         })
     }
 
-    pub fn write(&self) -> Result<()> {
+    fn write(&self) -> Result<()> {
         // TODO: Capacity
-        let mut buf = Vec::with_capacity(1 << 12);
+        let mut buf = Vec::with_capacity(1024);
         serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?;
-        dbg!(buf.len());
+
         Ok(())
     }
+
+    #[inline]
+    pub fn next_exercise_ind(&self) -> usize {
+        self.next_exercise_ind
+    }
+
+    pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> {
+        if ind >= self.progress.len() {
+            bail!("The next exercise index is higher than the number of exercises");
+        }
+
+        self.next_exercise_ind = ind;
+        self.write()
+    }
+
+    #[inline]
+    pub fn progress(&self) -> &[bool] {
+        &self.progress
+    }
 }
diff --git a/src/watch.rs b/src/watch.rs
index 92da20dd..cc9668d0 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -158,7 +158,7 @@ pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> {
         .watcher()
         .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
 
-    let current_exercise_ind = state.progress.iter().position(|done| *done).unwrap_or(0);
+    let current_exercise_ind = state.next_exercise_ind();
 
     let exercise = &exercises[current_exercise_ind];
 

From 2db86833a9f3fae4dc5410aac828b3071dda1984 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 13:12:40 +0200
Subject: [PATCH 026/109] Fix lifetimes

---
 src/list.rs | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index bb5ba1c0..5153e01f 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -16,27 +16,31 @@ use std::io;
 
 use crate::{exercise::Exercise, state::State};
 
-fn rows<'s, 'e>(state: &'s State, exercises: &'e [Exercise]) -> impl Iterator<Item = Row<'e>> + 's
+fn rows<'s, 'e, 'i>(
+    state: &'s State,
+    exercises: &'e [Exercise],
+) -> impl Iterator<Item = Row<'e>> + 'i
 where
-    'e: 's,
+    's: 'i,
+    'e: 'i,
 {
     exercises
         .iter()
         .zip(state.progress())
         .enumerate()
         .map(|(ind, (exercise, done))| {
-            let exercise_state = if *done {
-                "DONE".green()
-            } else {
-                "PENDING".yellow()
-            };
-
             let next = if ind == state.next_exercise_ind() {
                 ">>>>".bold().red()
             } else {
                 Span::default()
             };
 
+            let exercise_state = if *done {
+                "DONE".green()
+            } else {
+                "PENDING".yellow()
+            };
+
             Row::new([
                 next,
                 exercise_state,

From d988054ad851cb6ce67c77e2607322142d188804 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 16:33:00 +0200
Subject: [PATCH 027/109] Add UiState

---
 src/list.rs | 236 +++++++++++++++++++++++++++++-----------------------
 1 file changed, 134 insertions(+), 102 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index 5153e01f..dad21822 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -10,112 +10,156 @@ use ratatui::{
     style::{Style, Stylize},
     text::Span,
     widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState},
-    Terminal,
+    Frame, Terminal,
 };
 use std::io;
 
 use crate::{exercise::Exercise, state::State};
 
-fn rows<'s, 'e, 'i>(
-    state: &'s State,
-    exercises: &'e [Exercise],
-) -> impl Iterator<Item = Row<'e>> + 'i
-where
-    's: 'i,
-    'e: 'i,
-{
-    exercises
-        .iter()
-        .zip(state.progress())
-        .enumerate()
-        .map(|(ind, (exercise, done))| {
-            let next = if ind == state.next_exercise_ind() {
-                ">>>>".bold().red()
-            } else {
-                Span::default()
-            };
-
-            let exercise_state = if *done {
-                "DONE".green()
-            } else {
-                "PENDING".yellow()
-            };
-
-            Row::new([
-                next,
-                exercise_state,
-                Span::raw(&exercise.name),
-                Span::raw(exercise.path.to_string_lossy()),
-            ])
-        })
+struct UiState<'a> {
+    pub table: Table<'a>,
+    selected: usize,
+    table_state: TableState,
+    last_ind: usize,
 }
 
-fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> {
-    let header = Row::new(["Next", "State", "Name", "Path"]);
+impl<'a> UiState<'a> {
+    pub fn rows<'s, 'i>(
+        state: &'s State,
+        exercises: &'a [Exercise],
+    ) -> impl Iterator<Item = Row<'a>> + 'i
+    where
+        's: 'i,
+        'a: 'i,
+    {
+        exercises
+            .iter()
+            .zip(state.progress())
+            .enumerate()
+            .map(|(ind, (exercise, done))| {
+                let next = if ind == state.next_exercise_ind() {
+                    ">>>>".bold().red()
+                } else {
+                    Span::default()
+                };
 
-    let max_name_len = exercises
-        .iter()
-        .map(|exercise| exercise.name.len())
-        .max()
-        .unwrap_or(4) as u16;
+                let exercise_state = if *done {
+                    "DONE".green()
+                } else {
+                    "PENDING".yellow()
+                };
 
-    let widths = [
-        Constraint::Length(4),
-        Constraint::Length(7),
-        Constraint::Length(max_name_len),
-        Constraint::Fill(1),
-    ];
+                Row::new([
+                    next,
+                    exercise_state,
+                    Span::raw(&exercise.name),
+                    Span::raw(exercise.path.to_string_lossy()),
+                ])
+            })
+    }
 
-    Table::new(rows(state, exercises), 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))
+    pub fn new(state: &State, exercises: &'a [Exercise]) -> Self {
+        let header = Row::new(["Next", "State", "Name", "Path"]);
+
+        let max_name_len = exercises
+            .iter()
+            .map(|exercise| exercise.name.len())
+            .max()
+            .unwrap_or(4) as u16;
+
+        let widths = [
+            Constraint::Length(4),
+            Constraint::Length(7),
+            Constraint::Length(max_name_len),
+            Constraint::Fill(1),
+        ];
+
+        let rows = Self::rows(state, exercises);
+
+        let table = Table::new(rows, widths)
+            .header(header)
+            .column_spacing(2)
+            .highlight_spacing(HighlightSpacing::Always)
+            .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
+            .highlight_symbol("šŸ¦€")
+            .block(Block::default().borders(Borders::BOTTOM));
+
+        let selected = 0;
+        let table_state = TableState::default().with_selected(Some(selected));
+        let last_ind = exercises.len() - 1;
+
+        Self {
+            table,
+            selected,
+            table_state,
+            last_ind,
+        }
+    }
+
+    fn select(&mut self, ind: usize) {
+        self.selected = ind;
+        self.table_state.select(Some(ind));
+    }
+
+    pub fn select_next(&mut self) {
+        self.select(self.selected.saturating_add(1).min(self.last_ind));
+    }
+
+    pub fn select_previous(&mut self) {
+        self.select(self.selected.saturating_sub(1));
+    }
+
+    #[inline]
+    pub fn select_first(&mut self) {
+        self.select(0);
+    }
+
+    #[inline]
+    pub fn select_last(&mut self) {
+        self.select(self.last_ind);
+    }
+
+    pub fn draw(&mut self, frame: &mut Frame) {
+        let area = frame.size();
+
+        frame.render_stateful_widget(
+            &self.table,
+            Rect {
+                x: 0,
+                y: 0,
+                width: area.width,
+                height: area.height - 1,
+            },
+            &mut self.table_state,
+        );
+
+        // Help footer
+        let footer =
+            "↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit";
+        frame.render_widget(
+            Span::raw(footer),
+            Rect {
+                x: 0,
+                y: area.height - 1,
+                width: area.width,
+                height: 1,
+            },
+        );
+    }
 }
 
 pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> {
     let mut stdout = io::stdout().lock();
-
     stdout.execute(EnterAlternateScreen)?;
     enable_raw_mode()?;
 
     let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
     terminal.clear()?;
 
-    let mut table = table(state, exercises);
-
-    let last_ind = exercises.len() - 1;
-    let mut selected = 0;
-    let mut table_state = TableState::default().with_selected(Some(selected));
+    let mut ui_state = UiState::new(state, exercises);
 
     'outer: loop {
-        terminal.draw(|frame| {
-            let area = frame.size();
-
-            frame.render_stateful_widget(
-                &table,
-                Rect {
-                    x: 0,
-                    y: 0,
-                    width: area.width,
-                    height: area.height - 1,
-                },
-                &mut table_state,
-            );
-
-            // Help footer
-            frame.render_widget(
-                Span::raw("↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit"),
-                Rect {
-                    x: 0,
-                    y: area.height - 1,
-                    width: area.width,
-                    height: 1,
-                },
-            );
-        })?;
+        terminal.draw(|frame| ui_state.draw(frame))?;
 
         let key = loop {
             match event::read()? {
@@ -135,25 +179,13 @@ pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> {
 
         match key.code {
             KeyCode::Char('q') => break,
-            KeyCode::Down | KeyCode::Char('j') => {
-                selected = selected.saturating_add(1).min(last_ind);
-                table_state.select(Some(selected));
-            }
-            KeyCode::Up | KeyCode::Char('k') => {
-                selected = selected.saturating_sub(1).max(0);
-                table_state.select(Some(selected));
-            }
-            KeyCode::Home | KeyCode::Char('g') => {
-                selected = 0;
-                table_state.select(Some(selected));
-            }
-            KeyCode::End | KeyCode::Char('G') => {
-                selected = last_ind;
-                table_state.select(Some(selected));
-            }
+            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('c') => {
-                state.set_next_exercise_ind(selected)?;
-                table = table.rows(rows(state, exercises));
+                state.set_next_exercise_ind(ui_state.selected)?;
+                ui_state.table = ui_state.table.rows(UiState::rows(state, exercises));
             }
             _ => (),
         }

From 8c31d38fa17970d0d2dc696922eb8cb329a6fdb9 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 17:57:20 +0200
Subject: [PATCH 028/109] Better variable name

---
 src/list.rs | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index dad21822..cff0a3d8 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -133,11 +133,10 @@ impl<'a> UiState<'a> {
             &mut self.table_state,
         );
 
-        // Help footer
-        let footer =
+        let help_footer =
             "↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit";
         frame.render_widget(
-            Span::raw(footer),
+            Span::raw(help_footer),
             Rect {
                 x: 0,
                 y: area.height - 1,

From 3bd26c7a24a97f9b4b87c453fbdbb06fe9971920 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 19:01:08 +0200
Subject: [PATCH 029/109] State -> StateFile

---
 src/list.rs                     | 20 ++++++++++----------
 src/main.rs                     | 17 +++++++++--------
 src/{state.rs => state_file.rs} |  4 ++--
 src/watch.rs                    |  6 +++---
 4 files changed, 24 insertions(+), 23 deletions(-)
 rename src/{state.rs => state_file.rs} (97%)

diff --git a/src/list.rs b/src/list.rs
index cff0a3d8..c59b8d84 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -14,7 +14,7 @@ use ratatui::{
 };
 use std::io;
 
-use crate::{exercise::Exercise, state::State};
+use crate::{exercise::Exercise, state_file::StateFile};
 
 struct UiState<'a> {
     pub table: Table<'a>,
@@ -25,7 +25,7 @@ struct UiState<'a> {
 
 impl<'a> UiState<'a> {
     pub fn rows<'s, 'i>(
-        state: &'s State,
+        state_file: &'s StateFile,
         exercises: &'a [Exercise],
     ) -> impl Iterator<Item = Row<'a>> + 'i
     where
@@ -34,10 +34,10 @@ impl<'a> UiState<'a> {
     {
         exercises
             .iter()
-            .zip(state.progress())
+            .zip(state_file.progress())
             .enumerate()
             .map(|(ind, (exercise, done))| {
-                let next = if ind == state.next_exercise_ind() {
+                let next = if ind == state_file.next_exercise_ind() {
                     ">>>>".bold().red()
                 } else {
                     Span::default()
@@ -58,7 +58,7 @@ impl<'a> UiState<'a> {
             })
     }
 
-    pub fn new(state: &State, exercises: &'a [Exercise]) -> Self {
+    pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self {
         let header = Row::new(["Next", "State", "Name", "Path"]);
 
         let max_name_len = exercises
@@ -74,7 +74,7 @@ impl<'a> UiState<'a> {
             Constraint::Fill(1),
         ];
 
-        let rows = Self::rows(state, exercises);
+        let rows = Self::rows(state_file, exercises);
 
         let table = Table::new(rows, widths)
             .header(header)
@@ -147,7 +147,7 @@ impl<'a> UiState<'a> {
     }
 }
 
-pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> {
+pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
     let mut stdout = io::stdout().lock();
     stdout.execute(EnterAlternateScreen)?;
     enable_raw_mode()?;
@@ -155,7 +155,7 @@ pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> {
     let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
     terminal.clear()?;
 
-    let mut ui_state = UiState::new(state, exercises);
+    let mut ui_state = UiState::new(state_file, exercises);
 
     'outer: loop {
         terminal.draw(|frame| ui_state.draw(frame))?;
@@ -183,8 +183,8 @@ pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> {
             KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(),
             KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
             KeyCode::Char('c') => {
-                state.set_next_exercise_ind(ui_state.selected)?;
-                ui_state.table = ui_state.table.rows(UiState::rows(state, exercises));
+                state_file.set_next_exercise_ind(ui_state.selected)?;
+                ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises));
             }
             _ => (),
         }
diff --git a/src/main.rs b/src/main.rs
index e82fc808..3d691b08 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,11 +1,6 @@
-use crate::consts::WELCOME;
-use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
-use crate::exercise::{Exercise, ExerciseList};
-use crate::run::run;
-use crate::verify::verify;
 use anyhow::{bail, Context, Result};
 use clap::{Parser, Subcommand};
-use state::State;
+use state_file::StateFile;
 use std::path::Path;
 use std::process::exit;
 use verify::VerifyState;
@@ -16,10 +11,16 @@ mod exercise;
 mod init;
 mod list;
 mod run;
-mod state;
+mod state_file;
 mod verify;
 mod watch;
 
+use crate::consts::WELCOME;
+use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
+use crate::exercise::{Exercise, ExerciseList};
+use crate::run::run;
+use crate::verify::verify;
+
 /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
 #[derive(Parser)]
 #[command(version)]
@@ -85,7 +86,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         exit(1);
     }
 
-    let mut state = State::read_or_default(&exercises);
+    let mut state = StateFile::read_or_default(&exercises);
 
     match args.command {
         None | Some(Subcommands::Watch) => {
diff --git a/src/state.rs b/src/state_file.rs
similarity index 97%
rename from src/state.rs
rename to src/state_file.rs
index 5a644873..ca7ed342 100644
--- a/src/state.rs
+++ b/src/state_file.rs
@@ -5,12 +5,12 @@ use std::fs;
 use crate::exercise::Exercise;
 
 #[derive(Serialize, Deserialize)]
-pub struct State {
+pub struct StateFile {
     next_exercise_ind: usize,
     progress: Vec<bool>,
 }
 
-impl State {
+impl StateFile {
     fn read(exercises: &[Exercise]) -> Option<Self> {
         let file_content = fs::read(".rustlings.json").ok()?;
 
diff --git a/src/watch.rs b/src/watch.rs
index cc9668d0..1503fdfe 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -18,7 +18,7 @@ use std::{
 
 use crate::{
     exercise::{self, Exercise},
-    state::State,
+    state_file::StateFile,
 };
 
 enum Event {
@@ -151,14 +151,14 @@ You can keep working on this exercise or jump into the next one by removing the
     }
 }
 
-pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> {
+pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> {
     let (tx, rx) = channel();
     let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
     debouncer
         .watcher()
         .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
 
-    let current_exercise_ind = state.next_exercise_ind();
+    let current_exercise_ind = state_file.next_exercise_ind();
 
     let exercise = &exercises[current_exercise_ind];
 

From 0a674a158da0d519f03a88bfabf31d98c0e064c6 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 19:05:29 +0200
Subject: [PATCH 030/109] Separate UiState

---
 src/list.rs       | 144 ++-------------------------------------------
 src/list/state.rs | 145 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 150 insertions(+), 139 deletions(-)
 create mode 100644 src/list/state.rs

diff --git a/src/list.rs b/src/list.rs
index c59b8d84..4d26702d 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -4,148 +4,14 @@ use crossterm::{
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
     ExecutableCommand,
 };
-use ratatui::{
-    backend::CrosstermBackend,
-    layout::{Constraint, Rect},
-    style::{Style, Stylize},
-    text::Span,
-    widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState},
-    Frame, Terminal,
-};
+use ratatui::{backend::CrosstermBackend, Terminal};
 use std::io;
 
+mod state;
+
 use crate::{exercise::Exercise, state_file::StateFile};
 
-struct UiState<'a> {
-    pub table: Table<'a>,
-    selected: usize,
-    table_state: TableState,
-    last_ind: usize,
-}
-
-impl<'a> UiState<'a> {
-    pub fn rows<'s, 'i>(
-        state_file: &'s StateFile,
-        exercises: &'a [Exercise],
-    ) -> impl Iterator<Item = Row<'a>> + 'i
-    where
-        's: 'i,
-        'a: 'i,
-    {
-        exercises
-            .iter()
-            .zip(state_file.progress())
-            .enumerate()
-            .map(|(ind, (exercise, done))| {
-                let next = if ind == state_file.next_exercise_ind() {
-                    ">>>>".bold().red()
-                } else {
-                    Span::default()
-                };
-
-                let exercise_state = if *done {
-                    "DONE".green()
-                } else {
-                    "PENDING".yellow()
-                };
-
-                Row::new([
-                    next,
-                    exercise_state,
-                    Span::raw(&exercise.name),
-                    Span::raw(exercise.path.to_string_lossy()),
-                ])
-            })
-    }
-
-    pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self {
-        let header = Row::new(["Next", "State", "Name", "Path"]);
-
-        let max_name_len = exercises
-            .iter()
-            .map(|exercise| exercise.name.len())
-            .max()
-            .unwrap_or(4) as u16;
-
-        let widths = [
-            Constraint::Length(4),
-            Constraint::Length(7),
-            Constraint::Length(max_name_len),
-            Constraint::Fill(1),
-        ];
-
-        let rows = Self::rows(state_file, exercises);
-
-        let table = Table::new(rows, widths)
-            .header(header)
-            .column_spacing(2)
-            .highlight_spacing(HighlightSpacing::Always)
-            .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
-            .highlight_symbol("šŸ¦€")
-            .block(Block::default().borders(Borders::BOTTOM));
-
-        let selected = 0;
-        let table_state = TableState::default().with_selected(Some(selected));
-        let last_ind = exercises.len() - 1;
-
-        Self {
-            table,
-            selected,
-            table_state,
-            last_ind,
-        }
-    }
-
-    fn select(&mut self, ind: usize) {
-        self.selected = ind;
-        self.table_state.select(Some(ind));
-    }
-
-    pub fn select_next(&mut self) {
-        self.select(self.selected.saturating_add(1).min(self.last_ind));
-    }
-
-    pub fn select_previous(&mut self) {
-        self.select(self.selected.saturating_sub(1));
-    }
-
-    #[inline]
-    pub fn select_first(&mut self) {
-        self.select(0);
-    }
-
-    #[inline]
-    pub fn select_last(&mut self) {
-        self.select(self.last_ind);
-    }
-
-    pub fn draw(&mut self, frame: &mut Frame) {
-        let area = frame.size();
-
-        frame.render_stateful_widget(
-            &self.table,
-            Rect {
-                x: 0,
-                y: 0,
-                width: area.width,
-                height: area.height - 1,
-            },
-            &mut self.table_state,
-        );
-
-        let help_footer =
-            "↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit";
-        frame.render_widget(
-            Span::raw(help_footer),
-            Rect {
-                x: 0,
-                y: area.height - 1,
-                width: area.width,
-                height: 1,
-            },
-        );
-    }
-}
+use self::state::UiState;
 
 pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
     let mut stdout = io::stdout().lock();
@@ -183,7 +49,7 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
             KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(),
             KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
             KeyCode::Char('c') => {
-                state_file.set_next_exercise_ind(ui_state.selected)?;
+                state_file.set_next_exercise_ind(ui_state.selected())?;
                 ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises));
             }
             _ => (),
diff --git a/src/list/state.rs b/src/list/state.rs
new file mode 100644
index 00000000..3d2f0a62
--- /dev/null
+++ b/src/list/state.rs
@@ -0,0 +1,145 @@
+use ratatui::{
+    layout::{Constraint, Rect},
+    style::{Style, Stylize},
+    text::Span,
+    widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState},
+    Frame,
+};
+
+use crate::{exercise::Exercise, state_file::StateFile};
+
+pub struct UiState<'a> {
+    pub table: Table<'a>,
+    selected: usize,
+    table_state: TableState,
+    last_ind: usize,
+}
+
+impl<'a> UiState<'a> {
+    pub fn rows<'s, 'i>(
+        state_file: &'s StateFile,
+        exercises: &'a [Exercise],
+    ) -> impl Iterator<Item = Row<'a>> + 'i
+    where
+        's: 'i,
+        'a: 'i,
+    {
+        exercises
+            .iter()
+            .zip(state_file.progress())
+            .enumerate()
+            .map(|(ind, (exercise, done))| {
+                let next = if ind == state_file.next_exercise_ind() {
+                    ">>>>".bold().red()
+                } else {
+                    Span::default()
+                };
+
+                let exercise_state = if *done {
+                    "DONE".green()
+                } else {
+                    "PENDING".yellow()
+                };
+
+                Row::new([
+                    next,
+                    exercise_state,
+                    Span::raw(&exercise.name),
+                    Span::raw(exercise.path.to_string_lossy()),
+                ])
+            })
+    }
+
+    pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self {
+        let header = Row::new(["Next", "State", "Name", "Path"]);
+
+        let max_name_len = exercises
+            .iter()
+            .map(|exercise| exercise.name.len())
+            .max()
+            .unwrap_or(4) as u16;
+
+        let widths = [
+            Constraint::Length(4),
+            Constraint::Length(7),
+            Constraint::Length(max_name_len),
+            Constraint::Fill(1),
+        ];
+
+        let rows = Self::rows(state_file, exercises);
+
+        let table = Table::new(rows, widths)
+            .header(header)
+            .column_spacing(2)
+            .highlight_spacing(HighlightSpacing::Always)
+            .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
+            .highlight_symbol("šŸ¦€")
+            .block(Block::default().borders(Borders::BOTTOM));
+
+        let selected = 0;
+        let table_state = TableState::default().with_selected(Some(selected));
+        let last_ind = exercises.len() - 1;
+
+        Self {
+            table,
+            selected,
+            table_state,
+            last_ind,
+        }
+    }
+
+    #[inline]
+    pub fn selected(&self) -> usize {
+        self.selected
+    }
+
+    fn select(&mut self, ind: usize) {
+        self.selected = ind;
+        self.table_state.select(Some(ind));
+    }
+
+    pub fn select_next(&mut self) {
+        self.select(self.selected.saturating_add(1).min(self.last_ind));
+    }
+
+    pub fn select_previous(&mut self) {
+        self.select(self.selected.saturating_sub(1));
+    }
+
+    #[inline]
+    pub fn select_first(&mut self) {
+        self.select(0);
+    }
+
+    #[inline]
+    pub fn select_last(&mut self) {
+        self.select(self.last_ind);
+    }
+
+    pub fn draw(&mut self, frame: &mut Frame) {
+        let area = frame.size();
+
+        frame.render_stateful_widget(
+            &self.table,
+            Rect {
+                x: 0,
+                y: 0,
+                width: area.width,
+                height: area.height - 1,
+            },
+            &mut self.table_state,
+        );
+
+        let help_footer =
+            "↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit";
+        frame.render_widget(
+            Span::raw(help_footer),
+            Rect {
+                x: 0,
+                y: area.height - 1,
+                width: area.width,
+                height: 1,
+            },
+        );
+    }
+}

From 9a4ee47c527251fc3efacacc31bd0e73ef527969 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 19:29:16 +0200
Subject: [PATCH 031/109] Separate WatchState

---
 src/watch.rs       | 181 +++----------------------------------------
 src/watch/state.rs | 186 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 198 insertions(+), 169 deletions(-)
 create mode 100644 src/watch/state.rs

diff --git a/src/watch.rs b/src/watch.rs
index 1503fdfe..967f98c1 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -1,25 +1,18 @@
 use anyhow::Result;
-use crossterm::{
-    style::{Attribute, ContentStyle, Stylize},
-    terminal::{Clear, ClearType},
-    ExecutableCommand,
-};
-use notify_debouncer_mini::{
-    new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind,
-};
+use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode};
 use std::{
-    fmt::Write as _,
-    io::{self, BufRead, StdoutLock, Write},
+    io::{self, BufRead, Write},
     path::Path,
-    sync::mpsc::{channel, sync_channel, Receiver},
+    sync::mpsc::{channel, sync_channel},
     thread,
     time::Duration,
 };
 
-use crate::{
-    exercise::{self, Exercise},
-    state_file::StateFile,
-};
+mod state;
+
+use crate::{exercise::Exercise, state_file::StateFile};
+
+use self::state::WatchState;
 
 enum Event {
     Hint,
@@ -27,130 +20,6 @@ enum Event {
     Quit,
 }
 
-struct WatchState<'a> {
-    writer: StdoutLock<'a>,
-    rx: Receiver<DebounceEventResult>,
-    exercises: &'a [Exercise],
-    exercise: &'a Exercise,
-    current_exercise_ind: usize,
-    stdout: Option<Vec<u8>>,
-    stderr: Option<Vec<u8>>,
-    message: Option<String>,
-    prompt: Vec<u8>,
-}
-
-impl<'a> WatchState<'a> {
-    fn run_exercise(&mut self) -> Result<bool> {
-        let output = self.exercise.run()?;
-
-        if !output.status.success() {
-            self.stdout = Some(output.stdout);
-            self.stderr = Some(output.stderr);
-            return Ok(false);
-        }
-
-        if let exercise::State::Pending(context) = self.exercise.state()? {
-            let mut message = format!(
-                "
-You can keep working on this exercise or jump into the next one by removing the {} comment:
-
-",
-                "`I AM NOT DONE`".bold(),
-            );
-
-            for context_line in context {
-                let formatted_line = if context_line.important {
-                    context_line.line.bold()
-                } else {
-                    context_line.line.stylize()
-                };
-
-                writeln!(
-                    message,
-                    "{:>2} {}  {}",
-                    ContentStyle {
-                        foreground_color: Some(crossterm::style::Color::Blue),
-                        background_color: None,
-                        underline_color: None,
-                        attributes: Attribute::Bold.into()
-                    }
-                    .apply(context_line.number),
-                    "|".blue(),
-                    formatted_line,
-                )?;
-            }
-
-            self.stdout = Some(output.stdout);
-            self.message = Some(message);
-            return Ok(false);
-        }
-
-        Ok(true)
-    }
-
-    fn try_recv_event(&mut self) -> Result<()> {
-        let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else {
-            return Ok(());
-        };
-
-        if let Some(current_exercise_ind) = events?
-            .iter()
-            .filter_map(|event| {
-                if event.kind != DebouncedEventKind::Any
-                    || !event.path.extension().is_some_and(|ext| ext == "rs")
-                {
-                    return None;
-                }
-
-                self.exercises
-                    .iter()
-                    .position(|exercise| event.path.ends_with(&exercise.path))
-            })
-            .min()
-        {
-            self.current_exercise_ind = current_exercise_ind;
-        } else {
-            return Ok(());
-        };
-
-        while self.current_exercise_ind < self.exercises.len() {
-            self.exercise = &self.exercises[self.current_exercise_ind];
-            if !self.run_exercise()? {
-                break;
-            }
-
-            self.current_exercise_ind += 1;
-        }
-
-        Ok(())
-    }
-
-    fn prompt(&mut self) -> io::Result<()> {
-        self.writer.write_all(&self.prompt)?;
-        self.writer.flush()
-    }
-
-    fn render(&mut self) -> Result<()> {
-        self.writer.execute(Clear(ClearType::All))?;
-
-        if let Some(stdout) = &self.stdout {
-            self.writer.write_all(stdout)?;
-        }
-
-        if let Some(stderr) = &self.stderr {
-            self.writer.write_all(stderr)?;
-        }
-
-        if let Some(message) = &self.message {
-            self.writer.write_all(message.as_bytes())?;
-        }
-
-        self.prompt()?;
-
-        Ok(())
-    }
-}
-
 pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> {
     let (tx, rx) = channel();
     let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
@@ -158,29 +27,7 @@ pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> {
         .watcher()
         .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
 
-    let current_exercise_ind = state_file.next_exercise_ind();
-
-    let exercise = &exercises[current_exercise_ind];
-
-    let writer = io::stdout().lock();
-
-    let mut watch_state = WatchState {
-        writer,
-        rx,
-        exercises,
-        exercise,
-        current_exercise_ind,
-        stdout: None,
-        stderr: None,
-        message: None,
-        prompt: format!(
-            "\n\n{}int/{}lear/{}uit? ",
-            "h".bold(),
-            "c".bold(),
-            "q".bold()
-        )
-        .into_bytes(),
-    };
+    let mut watch_state = WatchState::new(state_file, exercises, rx);
 
     watch_state.run_exercise()?;
     watch_state.render()?;
@@ -214,24 +61,20 @@ pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> {
         if let Ok(event) = rx.try_recv() {
             match event {
                 Some(Event::Hint) => {
-                    watch_state
-                        .writer
-                        .write_all(watch_state.exercise.hint.as_bytes())?;
-                    watch_state.prompt()?;
+                    watch_state.show_hint()?;
                 }
                 Some(Event::Clear) => {
                     watch_state.render()?;
                 }
                 Some(Event::Quit) => break,
                 None => {
-                    watch_state.writer.write_all(b"Invalid command")?;
-                    watch_state.prompt()?;
+                    watch_state.handle_invalid_cmd()?;
                 }
             }
         }
     }
 
-    watch_state.writer.write_all(b"
+    watch_state.into_writer().write_all(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.
 ")?;
diff --git a/src/watch/state.rs b/src/watch/state.rs
new file mode 100644
index 00000000..40f48eff
--- /dev/null
+++ b/src/watch/state.rs
@@ -0,0 +1,186 @@
+use anyhow::Result;
+use crossterm::{
+    style::{Attribute, ContentStyle, Stylize},
+    terminal::{Clear, ClearType},
+    ExecutableCommand,
+};
+use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
+use std::{
+    fmt::Write as _,
+    io::{self, StdoutLock, Write as _},
+    sync::mpsc::Receiver,
+    time::Duration,
+};
+
+use crate::{
+    exercise::{Exercise, State},
+    state_file::StateFile,
+};
+
+pub struct WatchState<'a> {
+    writer: StdoutLock<'a>,
+    rx: Receiver<DebounceEventResult>,
+    exercises: &'a [Exercise],
+    exercise: &'a Exercise,
+    current_exercise_ind: usize,
+    stdout: Option<Vec<u8>>,
+    stderr: Option<Vec<u8>>,
+    message: Option<String>,
+    prompt: Vec<u8>,
+}
+
+impl<'a> WatchState<'a> {
+    pub fn new(
+        state_file: &StateFile,
+        exercises: &'a [Exercise],
+        rx: Receiver<DebounceEventResult>,
+    ) -> Self {
+        let current_exercise_ind = state_file.next_exercise_ind();
+        let exercise = &exercises[current_exercise_ind];
+
+        let writer = io::stdout().lock();
+
+        let prompt = format!(
+            "\n\n{}int/{}lear/{}uit? ",
+            "h".bold(),
+            "c".bold(),
+            "q".bold()
+        )
+        .into_bytes();
+
+        Self {
+            writer,
+            rx,
+            exercises,
+            exercise,
+            current_exercise_ind,
+            stdout: None,
+            stderr: None,
+            message: None,
+            prompt,
+        }
+    }
+
+    #[inline]
+    pub fn into_writer(self) -> StdoutLock<'a> {
+        self.writer
+    }
+
+    pub fn run_exercise(&mut self) -> Result<bool> {
+        let output = self.exercise.run()?;
+
+        if !output.status.success() {
+            self.stdout = Some(output.stdout);
+            self.stderr = Some(output.stderr);
+            return Ok(false);
+        }
+
+        if let State::Pending(context) = self.exercise.state()? {
+            let mut message = format!(
+                "
+You can keep working on this exercise or jump into the next one by removing the {} comment:
+
+",
+                "`I AM NOT DONE`".bold(),
+            );
+
+            for context_line in context {
+                let formatted_line = if context_line.important {
+                    context_line.line.bold()
+                } else {
+                    context_line.line.stylize()
+                };
+
+                writeln!(
+                    message,
+                    "{:>2} {}  {}",
+                    ContentStyle {
+                        foreground_color: Some(crossterm::style::Color::Blue),
+                        background_color: None,
+                        underline_color: None,
+                        attributes: Attribute::Bold.into()
+                    }
+                    .apply(context_line.number),
+                    "|".blue(),
+                    formatted_line,
+                )?;
+            }
+
+            self.stdout = Some(output.stdout);
+            self.message = Some(message);
+            return Ok(false);
+        }
+
+        Ok(true)
+    }
+
+    pub fn try_recv_event(&mut self) -> Result<()> {
+        let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else {
+            return Ok(());
+        };
+
+        if let Some(current_exercise_ind) = events?
+            .iter()
+            .filter_map(|event| {
+                if event.kind != DebouncedEventKind::Any
+                    || !event.path.extension().is_some_and(|ext| ext == "rs")
+                {
+                    return None;
+                }
+
+                self.exercises
+                    .iter()
+                    .position(|exercise| event.path.ends_with(&exercise.path))
+            })
+            .min()
+        {
+            self.current_exercise_ind = current_exercise_ind;
+        } else {
+            return Ok(());
+        };
+
+        while self.current_exercise_ind < self.exercises.len() {
+            self.exercise = &self.exercises[self.current_exercise_ind];
+            if !self.run_exercise()? {
+                break;
+            }
+
+            self.current_exercise_ind += 1;
+        }
+
+        Ok(())
+    }
+
+    pub fn show_prompt(&mut self) -> io::Result<()> {
+        self.writer.write_all(&self.prompt)?;
+        self.writer.flush()
+    }
+
+    pub fn render(&mut self) -> io::Result<()> {
+        self.writer.execute(Clear(ClearType::All))?;
+
+        if let Some(stdout) = &self.stdout {
+            self.writer.write_all(stdout)?;
+        }
+
+        if let Some(stderr) = &self.stderr {
+            self.writer.write_all(stderr)?;
+        }
+
+        if let Some(message) = &self.message {
+            self.writer.write_all(message.as_bytes())?;
+        }
+
+        self.show_prompt()
+    }
+
+    pub fn show_hint(&mut self) -> io::Result<()> {
+        self.writer.write_all(self.exercise.hint.as_bytes())?;
+        self.show_prompt()
+    }
+
+    pub fn handle_invalid_cmd(&mut self) -> io::Result<()> {
+        self.writer.write_all(b"Invalid command")?;
+        self.show_prompt()
+    }
+}

From db43efe3ec9d0bba5ee997923d68d2356b08a257 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 22:40:50 +0200
Subject: [PATCH 032/109] Update .gitignore

---
 .gitignore | 28 ++++++++++++++++++----------
 1 file changed, 18 insertions(+), 10 deletions(-)

diff --git a/.gitignore b/.gitignore
index 0ea1fb6d..2d4a04dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,19 +1,27 @@
+# Cargo
 target/
 /tests/fixture/*/Cargo.lock
 /dev/Cargo.lock
 
-*.swp
-**/*.rs.bk
+# State file
+.rustlings-state.json
+
+# oranda
+public/
+.netlify
+
+# OS
 .DS_Store
-*.pdb
+.direnv/
+
+# Editor
+*.swp
 .idea
+*.iml
+
+# VS Code extension recommendations
 .vscode/*
 !.vscode/extensions.json
-*.iml
-*.o
-public/
-.direnv/
-.ignore
 
-# Local Netlify folder
-.netlify
+# Ignore file for editors like Helix
+.ignore

From 99c9ab467b3e57f9dca080a6fe9c1dbd991a3fdb Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 22:43:59 +0200
Subject: [PATCH 033/109] Implement resetting

---
 src/exercise.rs   |  8 ++++++-
 src/list.rs       |  6 +++++
 src/main.rs       | 57 +++++++++++++++++++++++------------------------
 src/state_file.rs | 15 ++++++++++---
 4 files changed, 53 insertions(+), 33 deletions(-)

diff --git a/src/exercise.rs b/src/exercise.rs
index d01d427a..508f4776 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -10,7 +10,7 @@ use winnow::ascii::{space0, Caseless};
 use winnow::combinator::opt;
 use winnow::Parser;
 
-use crate::embedded::EMBEDDED_FILES;
+use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
 
 // The number of context lines above and below a highlighted line.
 const CONTEXT: usize = 2;
@@ -220,6 +220,12 @@ impl Exercise {
     pub fn looks_done(&self) -> Result<bool> {
         self.state().map(|state| state == State::Done)
     }
+
+    pub fn reset(&self) -> Result<()> {
+        EMBEDDED_FILES
+            .write_exercise_to_disk(&self.path, WriteStrategy::Overwrite)
+            .with_context(|| format!("Failed to reset the exercise {self}"))
+    }
 }
 
 impl Display for Exercise {
diff --git a/src/list.rs b/src/list.rs
index 4d26702d..e2af21d3 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -48,6 +48,12 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
             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('r') => {
+                let selected = ui_state.selected();
+                exercises[selected].reset()?;
+                state_file.reset(selected)?;
+                ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises));
+            }
             KeyCode::Char('c') => {
                 state_file.set_next_exercise_ind(ui_state.selected())?;
                 ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises));
diff --git a/src/main.rs b/src/main.rs
index 3d691b08..81f66175 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,7 +16,6 @@ mod verify;
 mod watch;
 
 use crate::consts::WELCOME;
-use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
 use crate::exercise::{Exercise, ExerciseList};
 use crate::run::run;
 use crate::verify::verify;
@@ -56,6 +55,26 @@ enum Subcommands {
     List,
 }
 
+fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<(usize, &'a Exercise)> {
+    if name == "next" {
+        for (ind, exercise) in exercises.iter().enumerate() {
+            if !exercise.looks_done()? {
+                return Ok((ind, exercise));
+            }
+        }
+
+        println!("šŸŽ‰ Congratulations! You have done all the exercises!");
+        println!("šŸ”š There are no more exercises to do next!");
+        exit(0);
+    }
+
+    exercises
+        .iter()
+        .enumerate()
+        .find(|(_, exercise)| exercise.name == name)
+        .with_context(|| format!("No exercise found for '{name}'!"))
+}
+
 fn main() -> Result<()> {
     let args = Args::parse();
 
@@ -86,30 +105,29 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         exit(1);
     }
 
-    let mut state = StateFile::read_or_default(&exercises);
+    let mut state_file = StateFile::read_or_default(&exercises);
 
     match args.command {
         None | Some(Subcommands::Watch) => {
-            watch::watch(&state, &exercises)?;
+            watch::watch(&state_file, &exercises)?;
         }
         // `Init` is handled above.
         Some(Subcommands::Init) => (),
         Some(Subcommands::List) => {
-            list::list(&mut state, &exercises)?;
+            list::list(&mut state_file, &exercises)?;
         }
         Some(Subcommands::Run { name }) => {
-            let exercise = find_exercise(&name, &exercises)?;
+            let (_, exercise) = find_exercise(&name, &exercises)?;
             run(exercise).unwrap_or_else(|_| exit(1));
         }
         Some(Subcommands::Reset { name }) => {
-            let exercise = find_exercise(&name, &exercises)?;
-            EMBEDDED_FILES
-                .write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite)
-                .with_context(|| format!("Failed to reset the exercise {exercise}"))?;
+            let (ind, exercise) = find_exercise(&name, &exercises)?;
+            exercise.reset()?;
+            state_file.reset(ind)?;
             println!("The file {} has been reset!", exercise.path.display());
         }
         Some(Subcommands::Hint { name }) => {
-            let exercise = find_exercise(&name, &exercises)?;
+            let (_, exercise) = find_exercise(&name, &exercises)?;
             println!("{}", exercise.hint);
         }
         Some(Subcommands::Verify) => match verify(&exercises, 0)? {
@@ -120,22 +138,3 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
 
     Ok(())
 }
-
-fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> {
-    if name == "next" {
-        for exercise in exercises {
-            if !exercise.looks_done()? {
-                return Ok(exercise);
-            }
-        }
-
-        println!("šŸŽ‰ Congratulations! You have done all the exercises!");
-        println!("šŸ”š There are no more exercises to do next!");
-        exit(0);
-    }
-
-    exercises
-        .iter()
-        .find(|e| e.name == name)
-        .with_context(|| format!("No exercise found for '{name}'!"))
-}
diff --git a/src/state_file.rs b/src/state_file.rs
index ca7ed342..693c78dc 100644
--- a/src/state_file.rs
+++ b/src/state_file.rs
@@ -10,9 +10,11 @@ pub struct StateFile {
     progress: Vec<bool>,
 }
 
+const BAD_INDEX_ERR: &str = "The next exercise index is higher than the number of exercises";
+
 impl StateFile {
     fn read(exercises: &[Exercise]) -> Option<Self> {
-        let file_content = fs::read(".rustlings.json").ok()?;
+        let file_content = fs::read(".rustlings-state.json").ok()?;
 
         let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
 
@@ -34,6 +36,8 @@ impl StateFile {
         // TODO: Capacity
         let mut buf = Vec::with_capacity(1024);
         serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?;
+        fs::write(".rustlings-state.json", buf)
+            .context("Failed to write the state file `.rustlings-state.json`")?;
 
         Ok(())
     }
@@ -45,9 +49,8 @@ impl StateFile {
 
     pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> {
         if ind >= self.progress.len() {
-            bail!("The next exercise index is higher than the number of exercises");
+            bail!(BAD_INDEX_ERR);
         }
-
         self.next_exercise_ind = ind;
         self.write()
     }
@@ -56,4 +59,10 @@ impl StateFile {
     pub fn progress(&self) -> &[bool] {
         &self.progress
     }
+
+    pub fn reset(&mut self, ind: usize) -> Result<()> {
+        let done = self.progress.get_mut(ind).context(BAD_INDEX_ERR)?;
+        *done = false;
+        self.write()
+    }
 }

From 93f8d1610d293e57fd5002a9755c1f91a31ba891 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 23:37:40 +0200
Subject: [PATCH 034/109] Some renamings

---
 src/exercise.rs | 4 ++--
 src/init.rs     | 2 +-
 src/main.rs     | 6 +++---
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/exercise.rs b/src/exercise.rs
index 508f4776..ae47d5e6 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -41,11 +41,11 @@ pub enum Mode {
 }
 
 #[derive(Deserialize)]
-pub struct ExerciseList {
+pub struct InfoFile {
     pub exercises: Vec<Exercise>,
 }
 
-impl ExerciseList {
+impl InfoFile {
     pub fn parse() -> Result<Self> {
         // Read a local `info.toml` if it exists.
         // Mainly to let the tests work for now.
diff --git a/src/init.rs b/src/init.rs
index 6af32351..df2d19d8 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -56,7 +56,7 @@ fn create_vscode_dir() -> Result<()> {
     Ok(())
 }
 
-pub fn init_rustlings(exercises: &[Exercise]) -> Result<()> {
+pub fn init(exercises: &[Exercise]) -> Result<()> {
     if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
         bail!(
             "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist
diff --git a/src/main.rs b/src/main.rs
index 81f66175..3f10a8bd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,7 +16,7 @@ mod verify;
 mod watch;
 
 use crate::consts::WELCOME;
-use crate::exercise::{Exercise, ExerciseList};
+use crate::exercise::{Exercise, InfoFile};
 use crate::run::run;
 use crate::verify::verify;
 
@@ -84,10 +84,10 @@ Did you already install Rust?
 Try running `cargo --version` to diagnose the problem.",
     )?;
 
-    let exercises = ExerciseList::parse()?.exercises;
+    let exercises = InfoFile::parse()?.exercises;
 
     if matches!(args.command, Some(Subcommands::Init)) {
-        init::init_rustlings(&exercises).context("Initialization failed")?;
+        init::init(&exercises).context("Initialization failed")?;
         println!(
             "\nDone initialization!\n
 Run `cd rustlings` to go into the generated directory.

From db25cc91576a05b02edd3754df85eb5668cec83f Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 23:54:32 +0200
Subject: [PATCH 035/109] Ignore .rustlings-state.json

---
 src/init.rs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/init.rs b/src/init.rs
index df2d19d8..bc561eaf 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -36,7 +36,8 @@ publish = false
 }
 
 fn create_gitignore() -> io::Result<()> {
-    let gitignore = b"/target";
+    let gitignore = b"/target
+/.rustlings-state.json";
     OpenOptions::new()
         .create_new(true)
         .write(true)

From 394ca402a8883581dc040546b4ca18b07d76a7f2 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 7 Apr 2024 23:57:54 +0200
Subject: [PATCH 036/109] Remove the info_toml_content field

---
 rustlings-macros/src/lib.rs | 1 -
 src/embedded.rs             | 1 -
 src/exercise.rs             | 2 +-
 3 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs
index 598b5c35..d8da666e 100644
--- a/rustlings-macros/src/lib.rs
+++ b/rustlings-macros/src/lib.rs
@@ -75,7 +75,6 @@ pub fn include_files(_: TokenStream) -> TokenStream {
 
     quote! {
         EmbeddedFiles {
-            info_toml_content: ::std::include_str!("../info.toml"),
             exercises_dir: ExercisesDir {
                 readme: EmbeddedFile {
                     path: "exercises/README.md",
diff --git a/src/embedded.rs b/src/embedded.rs
index 56b4b618..1e2d6770 100644
--- a/src/embedded.rs
+++ b/src/embedded.rs
@@ -65,7 +65,6 @@ struct ExercisesDir {
 }
 
 pub struct EmbeddedFiles {
-    pub info_toml_content: &'static str,
     exercises_dir: ExercisesDir,
 }
 
diff --git a/src/exercise.rs b/src/exercise.rs
index ae47d5e6..c9fb3312 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -52,7 +52,7 @@ impl InfoFile {
         if let Ok(file_content) = fs::read_to_string("info.toml") {
             toml_edit::de::from_str(&file_content)
         } else {
-            toml_edit::de::from_str(EMBEDDED_FILES.info_toml_content)
+            toml_edit::de::from_str(include_str!("../info.toml"))
         }
         .context("Failed to parse `info.toml`")
     }

From 3a4f2bebb487f3fef9ce222674eede86722824b3 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 00:35:51 +0200
Subject: [PATCH 037/109] Remove test because of defaulting to watch mode

---
 tests/integration_tests.rs | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index ccdd910e..2219fea4 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -1,14 +1,7 @@
 use assert_cmd::prelude::*;
 use glob::glob;
 use predicates::boolean::PredicateBooleanExt;
-use std::fs::File;
-use std::io::Read;
-use std::process::Command;
-
-#[test]
-fn runs_without_arguments() {
-    Command::cargo_bin("rustlings").unwrap().assert().success();
-}
+use std::{fs::File, io::Read, process::Command};
 
 #[test]
 fn fails_when_in_wrong_dir() {

From c2501ae733f27cf3d9f14cf1b14e437c8675d80c Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 00:36:10 +0200
Subject: [PATCH 038/109] Remove list tests because of the TUI

---
 tests/integration_tests.rs | 54 --------------------------------------
 1 file changed, 54 deletions(-)

diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 2219fea4..f8f4383f 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -194,57 +194,3 @@ fn run_single_test_success_with_output() {
         .code(0)
         .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS"));
 }
-
-#[test]
-fn run_rustlings_list() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .args(["list"])
-        .current_dir("tests/fixture/success")
-        .assert()
-        .success();
-}
-
-#[test]
-fn run_rustlings_list_no_pending() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .args(["list"])
-        .current_dir("tests/fixture/success")
-        .assert()
-        .success()
-        .stdout(predicates::str::contains("Pending").not());
-}
-
-#[test]
-fn run_rustlings_list_both_done_and_pending() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .args(["list"])
-        .current_dir("tests/fixture/state")
-        .assert()
-        .success()
-        .stdout(predicates::str::contains("Done").and(predicates::str::contains("Pending")));
-}
-
-#[test]
-fn run_rustlings_list_without_pending() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .args(["list", "--solved"])
-        .current_dir("tests/fixture/state")
-        .assert()
-        .success()
-        .stdout(predicates::str::contains("Pending").not());
-}
-
-#[test]
-fn run_rustlings_list_without_done() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .args(["list", "--unsolved"])
-        .current_dir("tests/fixture/state")
-        .assert()
-        .success()
-        .stdout(predicates::str::contains("Done").not());
-}

From 25e855a009c47d30bfa4da93a93d8390df20fe45 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 00:36:26 +0200
Subject: [PATCH 039/109] Merge imports

---
 src/exercise.rs | 23 ++++++++++++++---------
 src/main.rs     | 16 ++++++++--------
 2 files changed, 22 insertions(+), 17 deletions(-)

diff --git a/src/exercise.rs b/src/exercise.rs
index c9fb3312..232d7f95 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -1,14 +1,19 @@
 use anyhow::{Context, Result};
 use serde::Deserialize;
-use std::fmt::{self, Debug, Display, Formatter};
-use std::fs::{self, File};
-use std::io::{self, BufRead, BufReader};
-use std::path::PathBuf;
-use std::process::{Command, Output};
-use std::{array, mem};
-use winnow::ascii::{space0, Caseless};
-use winnow::combinator::opt;
-use winnow::Parser;
+use std::{
+    array,
+    fmt::{self, Debug, Display, Formatter},
+    fs::{self, File},
+    io::{self, BufRead, BufReader},
+    mem,
+    path::PathBuf,
+    process::{Command, Output},
+};
+use winnow::{
+    ascii::{space0, Caseless},
+    combinator::opt,
+    Parser,
+};
 
 use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
 
diff --git a/src/main.rs b/src/main.rs
index 3f10a8bd..cba525a1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,6 @@
 use anyhow::{bail, Context, Result};
 use clap::{Parser, Subcommand};
-use state_file::StateFile;
-use std::path::Path;
-use std::process::exit;
-use verify::VerifyState;
+use std::{path::Path, process::exit};
 
 mod consts;
 mod embedded;
@@ -15,10 +12,13 @@ mod state_file;
 mod verify;
 mod watch;
 
-use crate::consts::WELCOME;
-use crate::exercise::{Exercise, InfoFile};
-use crate::run::run;
-use crate::verify::verify;
+use self::{
+    consts::WELCOME,
+    exercise::{Exercise, InfoFile},
+    run::run,
+    state_file::StateFile,
+    verify::{verify, VerifyState},
+};
 
 /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
 #[derive(Parser)]

From bd5503a0d363384fb551f3e303d0376a08d50831 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 01:33:11 +0200
Subject: [PATCH 040/109] Show message on reset

---
 src/list.rs       | 11 +++++++++--
 src/list/state.rs | 18 +++++++++++-------
 src/main.rs       |  2 +-
 3 files changed, 21 insertions(+), 10 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index e2af21d3..3d91b8ae 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -5,7 +5,7 @@ use crossterm::{
     ExecutableCommand,
 };
 use ratatui::{backend::CrosstermBackend, Terminal};
-use std::io;
+use std::{fmt::Write, io};
 
 mod state;
 
@@ -42,6 +42,8 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
             }
         };
 
+        ui_state.message.clear();
+
         match key.code {
             KeyCode::Char('q') => break,
             KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(),
@@ -50,9 +52,14 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
             KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
             KeyCode::Char('r') => {
                 let selected = ui_state.selected();
-                exercises[selected].reset()?;
+                let exercise = &exercises[selected];
+                exercise.reset()?;
                 state_file.reset(selected)?;
+
                 ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises));
+                ui_state
+                    .message
+                    .write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
             }
             KeyCode::Char('c') => {
                 state_file.set_next_exercise_ind(ui_state.selected())?;
diff --git a/src/list/state.rs b/src/list/state.rs
index 3d2f0a62..534b5359 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -10,6 +10,7 @@ use crate::{exercise::Exercise, state_file::StateFile};
 
 pub struct UiState<'a> {
     pub table: Table<'a>,
+    pub message: String,
     selected: usize,
     table_state: TableState,
     last_ind: usize,
@@ -77,14 +78,13 @@ impl<'a> UiState<'a> {
             .block(Block::default().borders(Borders::BOTTOM));
 
         let selected = 0;
-        let table_state = TableState::default().with_selected(Some(selected));
-        let last_ind = exercises.len() - 1;
 
         Self {
             table,
             selected,
-            table_state,
-            last_ind,
+            table_state: TableState::default().with_selected(Some(selected)),
+            last_ind: exercises.len() - 1,
+            message: String::with_capacity(128),
         }
     }
 
@@ -130,10 +130,14 @@ impl<'a> UiState<'a> {
             &mut self.table_state,
         );
 
-        let help_footer =
-            "↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit";
+        let message = if self.message.is_empty() {
+            // Help footer.
+            "↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit"
+        } else {
+            &self.message
+        };
         frame.render_widget(
-            Span::raw(help_footer),
+            Span::raw(message),
             Rect {
                 x: 0,
                 y: area.height - 1,
diff --git a/src/main.rs b/src/main.rs
index cba525a1..f6c4c200 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -124,7 +124,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
             let (ind, exercise) = find_exercise(&name, &exercises)?;
             exercise.reset()?;
             state_file.reset(ind)?;
-            println!("The file {} has been reset!", exercise.path.display());
+            println!("The exercise {exercise} has been reset!");
         }
         Some(Subcommands::Hint { name }) => {
             let (_, exercise) = find_exercise(&name, &exercises)?;

From 0bf3f7e01f219372bea56e2c3e9144a1b76bd3af Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 01:34:41 +0200
Subject: [PATCH 041/109] Lowercase "filter" in help footer

---
 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 534b5359..35a906a3 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -132,7 +132,7 @@ impl<'a> UiState<'a> {
 
         let message = if self.message.is_empty() {
             // Help footer.
-            "↓/j ↑/k home/g end/G │ Filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit"
+            "↓/j ↑/k home/g end/G │ filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit"
         } else {
             &self.message
         };

From 05729b27a06d50d4d3516c1b62a2c7450e4ac12a Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 01:49:38 +0200
Subject: [PATCH 042/109] Set a list offset

---
 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 35a906a3..d2ade97e 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -77,12 +77,15 @@ impl<'a> UiState<'a> {
             .highlight_symbol("šŸ¦€")
             .block(Block::default().borders(Borders::BOTTOM));
 
-        let selected = 0;
+        let selected = state_file.next_exercise_ind();
+        let table_state = TableState::default()
+            .with_offset(selected.saturating_sub(3))
+            .with_selected(Some(selected));
 
         Self {
             table,
             selected,
-            table_state: TableState::default().with_selected(Some(selected)),
+            table_state,
             last_ind: exercises.len() - 1,
             message: String::with_capacity(128),
         }

From 7c4d33654fb37200905c06c198f427545fedd461 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 02:41:48 +0200
Subject: [PATCH 043/109] Implement done/pending filters

---
 src/list.rs       | 30 +++++++++++++++++++++++++++---
 src/list/state.rs | 40 ++++++++++++++++++++++++++++++++--------
 2 files changed, 59 insertions(+), 11 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index 3d91b8ae..d7fa05f1 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -11,7 +11,7 @@ mod state;
 
 use crate::{exercise::Exercise, state_file::StateFile};
 
-use self::state::UiState;
+use self::state::{Filter, UiState};
 
 pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
     let mut stdout = io::stdout().lock();
@@ -50,20 +50,44 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
             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(state_file);
+                ui_state.message.push_str(message);
+            }
+            KeyCode::Char('p') => {
+                let message = if ui_state.filter == Filter::Pending {
+                    ui_state.filter = Filter::None;
+                    "Disabled filter PENDING"
+                } else {
+                    ui_state.filter = Filter::Pending;
+                    "Enabled filter PENDING │ Press p again to disable the filter"
+                };
+
+                ui_state = ui_state.with_updated_rows(state_file);
+                ui_state.message.push_str(message);
+            }
             KeyCode::Char('r') => {
                 let selected = ui_state.selected();
                 let exercise = &exercises[selected];
                 exercise.reset()?;
                 state_file.reset(selected)?;
 
-                ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises));
+                ui_state = ui_state.with_updated_rows(state_file);
                 ui_state
                     .message
                     .write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
             }
             KeyCode::Char('c') => {
                 state_file.set_next_exercise_ind(ui_state.selected())?;
-                ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises));
+                ui_state = ui_state.with_updated_rows(state_file);
             }
             _ => (),
         }
diff --git a/src/list/state.rs b/src/list/state.rs
index d2ade97e..30567d1c 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -8,18 +8,28 @@ use ratatui::{
 
 use crate::{exercise::Exercise, state_file::StateFile};
 
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Filter {
+    Done,
+    Pending,
+    None,
+}
+
 pub struct UiState<'a> {
     pub table: Table<'a>,
     pub message: String,
+    pub filter: Filter,
+    exercises: &'a [Exercise],
     selected: usize,
     table_state: TableState,
     last_ind: usize,
 }
 
 impl<'a> UiState<'a> {
-    pub fn rows<'s, 'i>(
+    fn rows<'s, 'i>(
         state_file: &'s StateFile,
         exercises: &'a [Exercise],
+        filter: Filter,
     ) -> impl Iterator<Item = Row<'a>> + 'i
     where
         's: 'i,
@@ -27,30 +37,41 @@ impl<'a> UiState<'a> {
     {
         exercises
             .iter()
-            .zip(state_file.progress())
+            .zip(state_file.progress().iter().copied())
             .enumerate()
-            .map(|(ind, (exercise, done))| {
+            .filter_map(move |(ind, (exercise, done))| {
+                match (filter, done) {
+                    (Filter::Done, false) | (Filter::Pending, true) => return None,
+                    _ => (),
+                }
+
                 let next = if ind == state_file.next_exercise_ind() {
                     ">>>>".bold().red()
                 } else {
                     Span::default()
                 };
 
-                let exercise_state = if *done {
+                let exercise_state = if done {
                     "DONE".green()
                 } else {
                     "PENDING".yellow()
                 };
 
-                Row::new([
+                Some(Row::new([
                     next,
                     exercise_state,
                     Span::raw(&exercise.name),
                     Span::raw(exercise.path.to_string_lossy()),
-                ])
+                ]))
             })
     }
 
+    pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self {
+        let rows = Self::rows(state_file, self.exercises, self.filter);
+        self.table = self.table.rows(rows);
+        self
+    }
+
     pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self {
         let header = Row::new(["Next", "State", "Name", "Path"]);
 
@@ -67,7 +88,8 @@ impl<'a> UiState<'a> {
             Constraint::Fill(1),
         ];
 
-        let rows = Self::rows(state_file, exercises);
+        let filter = Filter::None;
+        let rows = Self::rows(state_file, exercises, filter);
 
         let table = Table::new(rows, widths)
             .header(header)
@@ -84,10 +106,12 @@ impl<'a> UiState<'a> {
 
         Self {
             table,
+            message: String::with_capacity(128),
+            filter,
+            exercises,
             selected,
             table_state,
             last_ind: exercises.len() - 1,
-            message: String::with_capacity(128),
         }
     }
 

From b5fc06bd56c6bf6a9b3d4e3dbcd4346c8256731c Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 02:46:35 +0200
Subject: [PATCH 044/109] Show more exercises before the selected one

---
 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 30567d1c..48c90d28 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -101,7 +101,7 @@ impl<'a> UiState<'a> {
 
         let selected = state_file.next_exercise_ind();
         let table_state = TableState::default()
-            .with_offset(selected.saturating_sub(3))
+            .with_offset(selected.saturating_sub(10))
             .with_selected(Some(selected));
 
         Self {

From 1db5de965305c0eb3f31e78217e8a52c61e15dd4 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 03:08:05 +0200
Subject: [PATCH 045/109] Fix selection after applying filters

---
 src/list/state.rs | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/src/list/state.rs b/src/list/state.rs
index 48c90d28..902e7a6a 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -26,14 +26,16 @@ pub struct UiState<'a> {
 }
 
 impl<'a> UiState<'a> {
-    fn rows<'s, 'i>(
+    fn rows<'s, 'c, 'i>(
         state_file: &'s StateFile,
         exercises: &'a [Exercise],
+        rows_counter: &'c mut usize,
         filter: Filter,
     ) -> impl Iterator<Item = Row<'a>> + 'i
     where
         's: 'i,
         'a: 'i,
+        'c: 'i,
     {
         exercises
             .iter()
@@ -45,6 +47,8 @@ impl<'a> UiState<'a> {
                     _ => (),
                 }
 
+                *rows_counter += 1;
+
                 let next = if ind == state_file.next_exercise_ind() {
                     ">>>>".bold().red()
                 } else {
@@ -67,8 +71,13 @@ impl<'a> UiState<'a> {
     }
 
     pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self {
-        let rows = Self::rows(state_file, self.exercises, self.filter);
+        let mut rows_counter = 0;
+        let rows = Self::rows(state_file, self.exercises, &mut rows_counter, self.filter);
         self.table = self.table.rows(rows);
+
+        self.last_ind = rows_counter.saturating_sub(1);
+        self.select(self.selected.min(self.last_ind));
+
         self
     }
 
@@ -89,7 +98,8 @@ impl<'a> UiState<'a> {
         ];
 
         let filter = Filter::None;
-        let rows = Self::rows(state_file, exercises, filter);
+        let mut rows_counter = 0;
+        let rows = Self::rows(state_file, exercises, &mut rows_counter, filter);
 
         let table = Table::new(rows, widths)
             .header(header)
@@ -111,7 +121,7 @@ impl<'a> UiState<'a> {
             exercises,
             selected,
             table_state,
-            last_ind: exercises.len() - 1,
+            last_ind: rows_counter.saturating_sub(1),
         }
     }
 

From 7c46e7ac697507ff1826bf5bf691a93898d4368d Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 03:16:38 +0200
Subject: [PATCH 046/109] Simplify building rows.

No more lifetimes championship :(
---
 src/list/state.rs | 45 ++++++++++++++++-----------------------------
 1 file changed, 16 insertions(+), 29 deletions(-)

diff --git a/src/list/state.rs b/src/list/state.rs
index 902e7a6a..b3dbafed 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -26,28 +26,20 @@ pub struct UiState<'a> {
 }
 
 impl<'a> UiState<'a> {
-    fn rows<'s, 'c, 'i>(
-        state_file: &'s StateFile,
-        exercises: &'a [Exercise],
-        rows_counter: &'c mut usize,
-        filter: Filter,
-    ) -> impl Iterator<Item = Row<'a>> + 'i
-    where
-        's: 'i,
-        'a: 'i,
-        'c: 'i,
-    {
-        exercises
+    pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self {
+        let mut rows_counter: usize = 0;
+        let rows = self
+            .exercises
             .iter()
             .zip(state_file.progress().iter().copied())
             .enumerate()
-            .filter_map(move |(ind, (exercise, done))| {
-                match (filter, done) {
+            .filter_map(|(ind, (exercise, done))| {
+                match (self.filter, done) {
                     (Filter::Done, false) | (Filter::Pending, true) => return None,
                     _ => (),
                 }
 
-                *rows_counter += 1;
+                rows_counter += 1;
 
                 let next = if ind == state_file.next_exercise_ind() {
                     ">>>>".bold().red()
@@ -67,12 +59,8 @@ impl<'a> UiState<'a> {
                     Span::raw(&exercise.name),
                     Span::raw(exercise.path.to_string_lossy()),
                 ]))
-            })
-    }
+            });
 
-    pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self {
-        let mut rows_counter = 0;
-        let rows = Self::rows(state_file, self.exercises, &mut rows_counter, self.filter);
         self.table = self.table.rows(rows);
 
         self.last_ind = rows_counter.saturating_sub(1);
@@ -97,11 +85,8 @@ impl<'a> UiState<'a> {
             Constraint::Fill(1),
         ];
 
-        let filter = Filter::None;
-        let mut rows_counter = 0;
-        let rows = Self::rows(state_file, exercises, &mut rows_counter, filter);
-
-        let table = Table::new(rows, widths)
+        let table = Table::default()
+            .widths(widths)
             .header(header)
             .column_spacing(2)
             .highlight_spacing(HighlightSpacing::Always)
@@ -114,15 +99,17 @@ impl<'a> UiState<'a> {
             .with_offset(selected.saturating_sub(10))
             .with_selected(Some(selected));
 
-        Self {
+        let slf = Self {
             table,
             message: String::with_capacity(128),
-            filter,
+            filter: Filter::None,
             exercises,
             selected,
             table_state,
-            last_ind: rows_counter.saturating_sub(1),
-        }
+            last_ind: 0,
+        };
+
+        slf.with_updated_rows(state_file)
     }
 
     #[inline]

From d0fcd8ae8aac43e0c0ac933bd810f11fa79d962e Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Mon, 8 Apr 2024 03:21:13 +0200
Subject: [PATCH 047/109] Use a color for the message

---
 src/list/state.rs | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/list/state.rs b/src/list/state.rs
index b3dbafed..dc9ff5fe 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -156,12 +156,14 @@ impl<'a> UiState<'a> {
 
         let message = if self.message.is_empty() {
             // Help footer.
-            "↓/j ↑/k home/g end/G │ filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit"
+            Span::raw(
+                "↓/j ↑/k home/g end/G │ filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit",
+            )
         } else {
-            &self.message
+            self.message.as_str().blue()
         };
         frame.render_widget(
-            Span::raw(message),
+            message,
             Rect {
                 x: 0,
                 y: area.height - 1,

From ee7d9762832241b34dc5533bad4ed151e21acab1 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 17:15:12 +0200
Subject: [PATCH 048/109] Use a green color on successful run

---
 src/run.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/run.rs b/src/run.rs
index 38f4e0e2..2fd6f407 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -1,4 +1,5 @@
 use anyhow::{bail, Result};
+use crossterm::style::Stylize;
 use std::io::{stdout, Write};
 
 use crate::exercise::Exercise;
@@ -21,8 +22,7 @@ pub fn run(exercise: &Exercise) -> Result<()> {
         bail!("Ran {exercise} with errors");
     }
 
-    // TODO: Color
-    println!("Successfully ran {exercise}");
+    println!("{}", "āœ“ Successfully ran {exercise}".green());
 
     Ok(())
 }

From 850c1d0234b2c1ae09a8f1c8f669e23a324fd644 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 19:37:39 +0200
Subject: [PATCH 049/109] Add progress bar to list

---
 src/list.rs         |  2 +-
 src/list/state.rs   | 56 +++++++++++++++++++++++++++++++++------------
 src/main.rs         |  1 +
 src/progress_bar.rs | 41 +++++++++++++++++++++++++++++++++
 4 files changed, 85 insertions(+), 15 deletions(-)
 create mode 100644 src/progress_bar.rs

diff --git a/src/list.rs b/src/list.rs
index d7fa05f1..db83ea4f 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -24,7 +24,7 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
     let mut ui_state = UiState::new(state_file, exercises);
 
     'outer: loop {
-        terminal.draw(|frame| ui_state.draw(frame))?;
+        terminal.draw(|frame| ui_state.draw(frame).unwrap())?;
 
         let key = loop {
             match event::read()? {
diff --git a/src/list/state.rs b/src/list/state.rs
index dc9ff5fe..7bfc163a 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -1,12 +1,13 @@
+use anyhow::Result;
 use ratatui::{
     layout::{Constraint, Rect},
     style::{Style, Stylize},
     text::Span,
-    widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState},
+    widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
     Frame,
 };
 
-use crate::{exercise::Exercise, state_file::StateFile};
+use crate::{exercise::Exercise, progress_bar::progress_bar, state_file::StateFile};
 
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub enum Filter {
@@ -20,6 +21,7 @@ pub struct UiState<'a> {
     pub message: String,
     pub filter: Filter,
     exercises: &'a [Exercise],
+    progress: u16,
     selected: usize,
     table_state: TableState,
     last_ind: usize,
@@ -28,16 +30,28 @@ pub struct UiState<'a> {
 impl<'a> UiState<'a> {
     pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self {
         let mut rows_counter: usize = 0;
+        let mut progress: u16 = 0;
         let rows = self
             .exercises
             .iter()
             .zip(state_file.progress().iter().copied())
             .enumerate()
             .filter_map(|(ind, (exercise, done))| {
-                match (self.filter, done) {
-                    (Filter::Done, false) | (Filter::Pending, true) => return None,
-                    _ => (),
-                }
+                let exercise_state = if done {
+                    progress += 1;
+
+                    if self.filter == Filter::Pending {
+                        return None;
+                    }
+
+                    "DONE".green()
+                } else {
+                    if self.filter == Filter::Done {
+                        return None;
+                    }
+
+                    "PENDING".yellow()
+                };
 
                 rows_counter += 1;
 
@@ -47,12 +61,6 @@ impl<'a> UiState<'a> {
                     Span::default()
                 };
 
-                let exercise_state = if done {
-                    "DONE".green()
-                } else {
-                    "PENDING".yellow()
-                };
-
                 Some(Row::new([
                     next,
                     exercise_state,
@@ -66,6 +74,8 @@ impl<'a> UiState<'a> {
         self.last_ind = rows_counter.saturating_sub(1);
         self.select(self.selected.min(self.last_ind));
 
+        self.progress = progress;
+
         self
     }
 
@@ -104,6 +114,7 @@ impl<'a> UiState<'a> {
             message: String::with_capacity(128),
             filter: Filter::None,
             exercises,
+            progress: 0,
             selected,
             table_state,
             last_ind: 0,
@@ -140,7 +151,7 @@ impl<'a> UiState<'a> {
         self.select(self.last_ind);
     }
 
-    pub fn draw(&mut self, frame: &mut Frame) {
+    pub fn draw(&mut self, frame: &mut Frame) -> Result<()> {
         let area = frame.size();
 
         frame.render_stateful_widget(
@@ -149,11 +160,26 @@ impl<'a> UiState<'a> {
                 x: 0,
                 y: 0,
                 width: area.width,
-                height: area.height - 1,
+                height: area.height - 3,
             },
             &mut self.table_state,
         );
 
+        frame.render_widget(
+            Paragraph::new(Span::raw(progress_bar(
+                self.progress,
+                self.exercises.len() as u16,
+                area.width,
+            )?))
+            .block(Block::default().borders(Borders::BOTTOM)),
+            Rect {
+                x: 0,
+                y: area.height - 3,
+                width: area.width,
+                height: 2,
+            },
+        );
+
         let message = if self.message.is_empty() {
             // Help footer.
             Span::raw(
@@ -171,5 +197,7 @@ impl<'a> UiState<'a> {
                 height: 1,
             },
         );
+
+        Ok(())
     }
 }
diff --git a/src/main.rs b/src/main.rs
index f6c4c200..356b77ca 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,6 +7,7 @@ mod embedded;
 mod exercise;
 mod init;
 mod list;
+mod progress_bar;
 mod run;
 mod state_file;
 mod verify;
diff --git a/src/progress_bar.rs b/src/progress_bar.rs
new file mode 100644
index 00000000..b4abbfc9
--- /dev/null
+++ b/src/progress_bar.rs
@@ -0,0 +1,41 @@
+use anyhow::{bail, Result};
+use std::fmt::Write;
+
+pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
+    if progress > total {
+        bail!("The progress of the progress bar is higher than the maximum");
+    }
+
+    // "Progress: [".len() == 11
+    // "] xxx/xxx".len() == 9
+    // 11 + 9 = 20
+    let wrapper_width = 20;
+
+    // If the line width is too low for a progress bar, just show the ratio.
+    if line_width < wrapper_width + 4 {
+        return Ok(format!("Progress: {progress}/{total}"));
+    }
+
+    let mut line = String::with_capacity(usize::from(line_width));
+    line.push_str("Progress: [");
+
+    let remaining_width = line_width.saturating_sub(wrapper_width);
+    let filled = (remaining_width * progress) / total;
+
+    for _ in 0..filled {
+        line.push('=');
+    }
+
+    if filled < remaining_width {
+        line.push('>');
+    }
+
+    for _ in 0..(remaining_width - filled).saturating_sub(1) {
+        line.push(' ');
+    }
+
+    line.write_fmt(format_args!("] {progress:>3}/{total:<3}"))
+        .unwrap();
+
+    Ok(line)
+}

From f0ce2c1afa21fdaa34aed8f21c1ef4d3c47cebdd Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 21:07:53 +0200
Subject: [PATCH 050/109] Improve event handling in the watch mode

---
 src/main.rs        |   5 +-
 src/watch.rs       | 150 ++++++++++++++++++++++++++++++++-------------
 src/watch/state.rs |  73 ++++++++--------------
 3 files changed, 133 insertions(+), 95 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 356b77ca..6af66bd7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -85,7 +85,8 @@ Did you already install Rust?
 Try running `cargo --version` to diagnose the problem.",
     )?;
 
-    let exercises = InfoFile::parse()?.exercises;
+    // Leaking is not a problem since the exercises are used until the end of the program.
+    let exercises = InfoFile::parse()?.exercises.leak();
 
     if matches!(args.command, Some(Subcommands::Init)) {
         init::init(&exercises).context("Initialization failed")?;
@@ -110,7 +111,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
 
     match args.command {
         None | Some(Subcommands::Watch) => {
-            watch::watch(&state_file, &exercises)?;
+            watch::watch(&state_file, exercises)?;
         }
         // `Init` is handled above.
         Some(Subcommands::Init) => (),
diff --git a/src/watch.rs b/src/watch.rs
index 967f98c1..abf40020 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -1,9 +1,11 @@
-use anyhow::Result;
-use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode};
+use anyhow::{bail, Context, Result};
+use notify_debouncer_mini::{
+    new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind,
+};
 use std::{
     io::{self, BufRead, Write},
     path::Path,
-    sync::mpsc::{channel, sync_channel},
+    sync::mpsc::{channel, Sender},
     thread,
     time::Duration,
 };
@@ -14,70 +16,130 @@ use crate::{exercise::Exercise, state_file::StateFile};
 
 use self::state::WatchState;
 
-enum Event {
+enum InputEvent {
     Hint,
     Clear,
     Quit,
+    Unrecognized,
 }
 
-pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> {
+enum WatchEvent {
+    Input(InputEvent),
+    FileChange { exercise_ind: usize },
+    TerminalResize,
+}
+
+struct DebouceEventHandler {
+    tx: Sender<WatchEvent>,
+    exercises: &'static [Exercise],
+}
+
+impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler {
+    fn handle_event(&mut self, event: DebounceEventResult) {
+        let Ok(event) = event else {
+            // TODO
+            return;
+        };
+
+        let Some(exercise_ind) = event
+            .iter()
+            .filter_map(|event| {
+                if event.kind != DebouncedEventKind::Any
+                    || !event.path.extension().is_some_and(|ext| ext == "rs")
+                {
+                    return None;
+                }
+
+                self.exercises
+                    .iter()
+                    .position(|exercise| event.path.ends_with(&exercise.path))
+            })
+            .min()
+        else {
+            return;
+        };
+
+        self.tx.send(WatchEvent::FileChange { exercise_ind });
+    }
+}
+
+fn input_handler(tx: Sender<WatchEvent>) -> Result<()> {
+    let mut stdin = io::stdin().lock();
+    let mut stdin_buf = String::with_capacity(8);
+
+    loop {
+        stdin
+            .read_line(&mut stdin_buf)
+            .context("Failed to read the user's input from stdin")?;
+
+        let event = match stdin_buf.trim() {
+            "h" | "hint" => InputEvent::Hint,
+            "c" | "clear" => InputEvent::Clear,
+            "q" | "quit" => InputEvent::Quit,
+            _ => InputEvent::Unrecognized,
+        };
+
+        stdin_buf.clear();
+
+        if tx.send(WatchEvent::Input(event)).is_err() {
+            return Ok(());
+        }
+    }
+}
+
+pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> {
     let (tx, rx) = channel();
-    let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
+    let mut debouncer = new_debouncer(
+        Duration::from_secs(1),
+        DebouceEventHandler {
+            tx: tx.clone(),
+            exercises,
+        },
+    )?;
     debouncer
         .watcher()
         .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
 
-    let mut watch_state = WatchState::new(state_file, exercises, rx);
+    let mut watch_state = WatchState::new(state_file, exercises);
 
+    // TODO: bool
     watch_state.run_exercise()?;
     watch_state.render()?;
 
-    let (tx, rx) = sync_channel(0);
-    thread::spawn(move || {
-        let mut stdin = io::stdin().lock();
-        let mut stdin_buf = String::with_capacity(8);
+    let input_thread = thread::spawn(move || input_handler(tx));
 
-        loop {
-            stdin.read_line(&mut stdin_buf).unwrap();
-
-            let event = match stdin_buf.trim() {
-                "h" | "hint" => Some(Event::Hint),
-                "c" | "clear" => Some(Event::Clear),
-                "q" | "quit" => Some(Event::Quit),
-                _ => None,
-            };
-
-            stdin_buf.clear();
-
-            if tx.send(event).is_err() {
-                break;
-            };
-        }
-    });
-
-    loop {
-        watch_state.try_recv_event()?;
-
-        if let Ok(event) = rx.try_recv() {
-            match event {
-                Some(Event::Hint) => {
-                    watch_state.show_hint()?;
-                }
-                Some(Event::Clear) => {
-                    watch_state.render()?;
-                }
-                Some(Event::Quit) => break,
-                None => {
-                    watch_state.handle_invalid_cmd()?;
-                }
+    while let Ok(event) = rx.recv() {
+        match event {
+            WatchEvent::Input(InputEvent::Hint) => {
+                watch_state.show_hint()?;
+            }
+            WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => {
+                watch_state.render()?;
+            }
+            WatchEvent::Input(InputEvent::Quit) => break,
+            WatchEvent::Input(InputEvent::Unrecognized) => {
+                watch_state.handle_invalid_cmd()?;
+            }
+            WatchEvent::FileChange { exercise_ind } => {
+                // TODO: bool
+                watch_state.run_exercise_with_ind(exercise_ind)?;
+                watch_state.render()?;
             }
         }
     }
 
+    // Drop the receiver for the sender threads to exit.
+    drop(rx);
+
     watch_state.into_writer().write_all(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.
 ")?;
 
+    match input_thread.join() {
+        Ok(res) => res?,
+        Err(_) => bail!("The input thread panicked"),
+    }
+
     Ok(())
 }
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 40f48eff..f614ae0d 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -1,26 +1,23 @@
-use anyhow::Result;
+use anyhow::{Context, Result};
 use crossterm::{
     style::{Attribute, ContentStyle, Stylize},
-    terminal::{Clear, ClearType},
+    terminal::{size, Clear, ClearType},
     ExecutableCommand,
 };
-use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
 use std::{
     fmt::Write as _,
     io::{self, StdoutLock, Write as _},
-    sync::mpsc::Receiver,
-    time::Duration,
 };
 
 use crate::{
     exercise::{Exercise, State},
+    progress_bar::progress_bar,
     state_file::StateFile,
 };
 
 pub struct WatchState<'a> {
     writer: StdoutLock<'a>,
-    rx: Receiver<DebounceEventResult>,
-    exercises: &'a [Exercise],
+    exercises: &'static [Exercise],
     exercise: &'a Exercise,
     current_exercise_ind: usize,
     stdout: Option<Vec<u8>>,
@@ -30,11 +27,7 @@ pub struct WatchState<'a> {
 }
 
 impl<'a> WatchState<'a> {
-    pub fn new(
-        state_file: &StateFile,
-        exercises: &'a [Exercise],
-        rx: Receiver<DebounceEventResult>,
-    ) -> Self {
+    pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self {
         let current_exercise_ind = state_file.next_exercise_ind();
         let exercise = &exercises[current_exercise_ind];
 
@@ -50,7 +43,6 @@ impl<'a> WatchState<'a> {
 
         Self {
             writer,
-            rx,
             exercises,
             exercise,
             current_exercise_ind,
@@ -114,41 +106,14 @@ You can keep working on this exercise or jump into the next one by removing the
         Ok(true)
     }
 
-    pub fn try_recv_event(&mut self) -> Result<()> {
-        let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else {
-            return Ok(());
-        };
+    pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<bool> {
+        self.exercise = self
+            .exercises
+            .get(exercise_ind)
+            .context("Invalid exercise index")?;
+        self.current_exercise_ind = exercise_ind;
 
-        if let Some(current_exercise_ind) = events?
-            .iter()
-            .filter_map(|event| {
-                if event.kind != DebouncedEventKind::Any
-                    || !event.path.extension().is_some_and(|ext| ext == "rs")
-                {
-                    return None;
-                }
-
-                self.exercises
-                    .iter()
-                    .position(|exercise| event.path.ends_with(&exercise.path))
-            })
-            .min()
-        {
-            self.current_exercise_ind = current_exercise_ind;
-        } else {
-            return Ok(());
-        };
-
-        while self.current_exercise_ind < self.exercises.len() {
-            self.exercise = &self.exercises[self.current_exercise_ind];
-            if !self.run_exercise()? {
-                break;
-            }
-
-            self.current_exercise_ind += 1;
-        }
-
-        Ok(())
+        self.run_exercise()
     }
 
     pub fn show_prompt(&mut self) -> io::Result<()> {
@@ -156,7 +121,7 @@ You can keep working on this exercise or jump into the next one by removing the
         self.writer.flush()
     }
 
-    pub fn render(&mut self) -> io::Result<()> {
+    pub fn render(&mut self) -> Result<()> {
         self.writer.execute(Clear(ClearType::All))?;
 
         if let Some(stdout) = &self.stdout {
@@ -171,7 +136,17 @@ You can keep working on this exercise or jump into the next one by removing the
             self.writer.write_all(message.as_bytes())?;
         }
 
-        self.show_prompt()
+        let line_width = size()?.0;
+        let progress_bar = progress_bar(
+            self.current_exercise_ind as u16,
+            self.exercises.len() as u16,
+            line_width,
+        )?;
+        self.writer.write_all(progress_bar.as_bytes())?;
+
+        self.show_prompt()?;
+
+        Ok(())
     }
 
     pub fn show_hint(&mut self) -> io::Result<()> {

From 787bec9875ec3e76d5870808cc7299da1d26dea6 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 21:16:27 +0200
Subject: [PATCH 051/109] Use exercises as leaked

---
 src/list.rs        |  2 +-
 src/list/state.rs  | 10 +++++-----
 src/main.rs        | 16 ++++++++--------
 src/verify.rs      |  9 ++++++---
 src/watch/state.rs |  2 +-
 5 files changed, 21 insertions(+), 18 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index db83ea4f..c92b3692 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -13,7 +13,7 @@ use crate::{exercise::Exercise, state_file::StateFile};
 
 use self::state::{Filter, UiState};
 
-pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> {
+pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<()> {
     let mut stdout = io::stdout().lock();
     stdout.execute(EnterAlternateScreen)?;
     enable_raw_mode()?;
diff --git a/src/list/state.rs b/src/list/state.rs
index 7bfc163a..b67c624b 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -16,18 +16,18 @@ pub enum Filter {
     None,
 }
 
-pub struct UiState<'a> {
-    pub table: Table<'a>,
+pub struct UiState {
+    pub table: Table<'static>,
     pub message: String,
     pub filter: Filter,
-    exercises: &'a [Exercise],
+    exercises: &'static [Exercise],
     progress: u16,
     selected: usize,
     table_state: TableState,
     last_ind: usize,
 }
 
-impl<'a> UiState<'a> {
+impl UiState {
     pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self {
         let mut rows_counter: usize = 0;
         let mut progress: u16 = 0;
@@ -79,7 +79,7 @@ impl<'a> UiState<'a> {
         self
     }
 
-    pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self {
+    pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self {
         let header = Row::new(["Next", "State", "Name", "Path"]);
 
         let max_name_len = exercises
diff --git a/src/main.rs b/src/main.rs
index 6af66bd7..62bfd98b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -56,7 +56,7 @@ enum Subcommands {
     List,
 }
 
-fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<(usize, &'a Exercise)> {
+fn find_exercise(name: &str, exercises: &'static [Exercise]) -> Result<(usize, &'static Exercise)> {
     if name == "next" {
         for (ind, exercise) in exercises.iter().enumerate() {
             if !exercise.looks_done()? {
@@ -89,7 +89,7 @@ Try running `cargo --version` to diagnose the problem.",
     let exercises = InfoFile::parse()?.exercises.leak();
 
     if matches!(args.command, Some(Subcommands::Init)) {
-        init::init(&exercises).context("Initialization failed")?;
+        init::init(exercises).context("Initialization failed")?;
         println!(
             "\nDone initialization!\n
 Run `cd rustlings` to go into the generated directory.
@@ -107,7 +107,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         exit(1);
     }
 
-    let mut state_file = StateFile::read_or_default(&exercises);
+    let mut state_file = StateFile::read_or_default(exercises);
 
     match args.command {
         None | Some(Subcommands::Watch) => {
@@ -116,23 +116,23 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         // `Init` is handled above.
         Some(Subcommands::Init) => (),
         Some(Subcommands::List) => {
-            list::list(&mut state_file, &exercises)?;
+            list::list(&mut state_file, exercises)?;
         }
         Some(Subcommands::Run { name }) => {
-            let (_, exercise) = find_exercise(&name, &exercises)?;
+            let (_, exercise) = find_exercise(&name, exercises)?;
             run(exercise).unwrap_or_else(|_| exit(1));
         }
         Some(Subcommands::Reset { name }) => {
-            let (ind, exercise) = find_exercise(&name, &exercises)?;
+            let (ind, exercise) = find_exercise(&name, exercises)?;
             exercise.reset()?;
             state_file.reset(ind)?;
             println!("The exercise {exercise} has been reset!");
         }
         Some(Subcommands::Hint { name }) => {
-            let (_, exercise) = find_exercise(&name, &exercises)?;
+            let (_, exercise) = find_exercise(&name, exercises)?;
             println!("{}", exercise.hint);
         }
-        Some(Subcommands::Verify) => match verify(&exercises, 0)? {
+        Some(Subcommands::Verify) => match verify(exercises, 0)? {
             VerifyState::AllExercisesDone => println!("All exercises done!"),
             VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
         },
diff --git a/src/verify.rs b/src/verify.rs
index c4368cc7..cea6bdf6 100644
--- a/src/verify.rs
+++ b/src/verify.rs
@@ -4,9 +4,9 @@ use std::io::{stdout, Write};
 
 use crate::exercise::{Exercise, Mode, State};
 
-pub enum VerifyState<'a> {
+pub enum VerifyState {
     AllExercisesDone,
-    Failed(&'a Exercise),
+    Failed(&'static Exercise),
 }
 
 // Verify that the provided container of Exercise objects
@@ -14,7 +14,10 @@ pub enum VerifyState<'a> {
 // Any such failures will be reported to the end user.
 // If the Exercise being verified is a test, the verbose boolean
 // determines whether or not the test harness outputs are displayed.
-pub fn verify(exercises: &[Exercise], mut current_exercise_ind: usize) -> Result<VerifyState<'_>> {
+pub fn verify(
+    exercises: &'static [Exercise],
+    mut current_exercise_ind: usize,
+) -> Result<VerifyState> {
     while current_exercise_ind < exercises.len() {
         let exercise = &exercises[current_exercise_ind];
 
diff --git a/src/watch/state.rs b/src/watch/state.rs
index f614ae0d..d8fed5b7 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -18,7 +18,7 @@ use crate::{
 pub struct WatchState<'a> {
     writer: StdoutLock<'a>,
     exercises: &'static [Exercise],
-    exercise: &'a Exercise,
+    exercise: &'static Exercise,
     current_exercise_ind: usize,
     stdout: Option<Vec<u8>>,
     stderr: Option<Vec<u8>>,

From b15e0a279b17d29a3fa6408b76da35f0b843ce21 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 21:23:02 +0200
Subject: [PATCH 052/109] Use shrink to fit before leaking the vector

---
 src/main.rs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 62bfd98b..504c02dc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -85,8 +85,10 @@ Did you already install Rust?
 Try running `cargo --version` to diagnose the problem.",
     )?;
 
-    // Leaking is not a problem since the exercises are used until the end of the program.
-    let exercises = InfoFile::parse()?.exercises.leak();
+    let mut info_file = InfoFile::parse()?;
+    info_file.exercises.shrink_to_fit();
+    // Leaking is not a problem since the exercises' slice is used until the end of the program.
+    let exercises = info_file.exercises.leak();
 
     if matches!(args.command, Some(Subcommands::Init)) {
         init::init(exercises).context("Initialization failed")?;

From 4110ae21afd2c026e49d330918e212f4ab0eb5cc Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 21:46:55 +0200
Subject: [PATCH 053/109] Handle notify errors

---
 src/watch.rs | 50 ++++++++++++++++++++++++++++----------------------
 1 file changed, 28 insertions(+), 22 deletions(-)

diff --git a/src/watch.rs b/src/watch.rs
index abf40020..5a1e38ab 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -1,6 +1,8 @@
 use anyhow::{bail, Context, Result};
 use notify_debouncer_mini::{
-    new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind,
+    new_debouncer,
+    notify::{self, RecursiveMode},
+    DebounceEventResult, DebouncedEventKind,
 };
 use std::{
     io::{self, BufRead, Write},
@@ -26,6 +28,7 @@ enum InputEvent {
 enum WatchEvent {
     Input(InputEvent),
     FileChange { exercise_ind: usize },
+    NotifyErr(notify::Error),
     TerminalResize,
 }
 
@@ -36,30 +39,32 @@ struct DebouceEventHandler {
 
 impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler {
     fn handle_event(&mut self, event: DebounceEventResult) {
-        let Ok(event) = event else {
-            // TODO
-            return;
-        };
-
-        let Some(exercise_ind) = event
-            .iter()
-            .filter_map(|event| {
-                if event.kind != DebouncedEventKind::Any
-                    || !event.path.extension().is_some_and(|ext| ext == "rs")
-                {
-                    return None;
-                }
-
-                self.exercises
+        let event = match event {
+            Ok(event) => {
+                let Some(exercise_ind) = event
                     .iter()
-                    .position(|exercise| event.path.ends_with(&exercise.path))
-            })
-            .min()
-        else {
-            return;
+                    .filter_map(|event| {
+                        if event.kind != DebouncedEventKind::Any
+                            || !event.path.extension().is_some_and(|ext| ext == "rs")
+                        {
+                            return None;
+                        }
+
+                        self.exercises
+                            .iter()
+                            .position(|exercise| event.path.ends_with(&exercise.path))
+                    })
+                    .min()
+                else {
+                    return;
+                };
+
+                WatchEvent::FileChange { exercise_ind }
+            }
+            Err(e) => WatchEvent::NotifyErr(e),
         };
 
-        self.tx.send(WatchEvent::FileChange { exercise_ind });
+        let _ = self.tx.send(event);
     }
 }
 
@@ -125,6 +130,7 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<(
                 watch_state.run_exercise_with_ind(exercise_ind)?;
                 watch_state.render()?;
             }
+            WatchEvent::NotifyErr(e) => return Err(e.into()),
         }
     }
 

From ff6c15f9c15ae80b48d3acd7091eb6328c931e7a Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 22:04:10 +0200
Subject: [PATCH 054/109] Don't try to join the input thread

---
 src/watch.rs | 30 +++++++++++++++---------------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/src/watch.rs b/src/watch.rs
index 5a1e38ab..6324eb36 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -1,4 +1,4 @@
-use anyhow::{bail, Context, Result};
+use anyhow::Result;
 use notify_debouncer_mini::{
     new_debouncer,
     notify::{self, RecursiveMode},
@@ -29,6 +29,7 @@ enum WatchEvent {
     Input(InputEvent),
     FileChange { exercise_ind: usize },
     NotifyErr(notify::Error),
+    StdinErr(io::Error),
     TerminalResize,
 }
 
@@ -64,18 +65,23 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler {
             Err(e) => WatchEvent::NotifyErr(e),
         };
 
+        // An error occurs when the receiver is dropped.
+        // After dropping the receiver, the debouncer guard should also be dropped.
         let _ = self.tx.send(event);
     }
 }
 
-fn input_handler(tx: Sender<WatchEvent>) -> Result<()> {
+fn input_handler(tx: Sender<WatchEvent>) {
     let mut stdin = io::stdin().lock();
     let mut stdin_buf = String::with_capacity(8);
 
     loop {
-        stdin
-            .read_line(&mut stdin_buf)
-            .context("Failed to read the user's input from stdin")?;
+        if let Err(e) = stdin.read_line(&mut stdin_buf) {
+            // If `send` returns an error, then the receiver is dropped and
+            // a shutdown has been already initialized.
+            let _ = tx.send(WatchEvent::StdinErr(e));
+            return;
+        }
 
         let event = match stdin_buf.trim() {
             "h" | "hint" => InputEvent::Hint,
@@ -87,7 +93,8 @@ fn input_handler(tx: Sender<WatchEvent>) -> Result<()> {
         stdin_buf.clear();
 
         if tx.send(WatchEvent::Input(event)).is_err() {
-            return Ok(());
+            // The receiver was dropped.
+            return;
         }
     }
 }
@@ -111,7 +118,7 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<(
     watch_state.run_exercise()?;
     watch_state.render()?;
 
-    let input_thread = thread::spawn(move || input_handler(tx));
+    thread::spawn(move || input_handler(tx));
 
     while let Ok(event) = rx.recv() {
         match event {
@@ -131,21 +138,14 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<(
                 watch_state.render()?;
             }
             WatchEvent::NotifyErr(e) => return Err(e.into()),
+            WatchEvent::StdinErr(e) => return Err(e.into()),
         }
     }
 
-    // Drop the receiver for the sender threads to exit.
-    drop(rx);
-
     watch_state.into_writer().write_all(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.
 ")?;
 
-    match input_thread.join() {
-        Ok(res) => res?,
-        Err(_) => bail!("The input thread panicked"),
-    }
-
     Ok(())
 }

From af85f2036cd545013225da04e67257fe4f6a4179 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 22:06:55 +0200
Subject: [PATCH 055/109] Print a newline before the progress bar

---
 src/watch/state.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/watch/state.rs b/src/watch/state.rs
index d8fed5b7..8fae7e87 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -136,6 +136,7 @@ You can keep working on this exercise or jump into the next one by removing the
             self.writer.write_all(message.as_bytes())?;
         }
 
+        self.writer.write_all(b"\n")?;
         let line_width = size()?.0;
         let progress_bar = progress_bar(
             self.current_exercise_ind as u16,

From a8ddc07a9aea5b2e3840a7b6e0eb20f2189bdd60 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 22:15:41 +0200
Subject: [PATCH 056/109] Add "exercises" to the end of the progress bar

---
 src/progress_bar.rs | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/progress_bar.rs b/src/progress_bar.rs
index b4abbfc9..ee55ba7b 100644
--- a/src/progress_bar.rs
+++ b/src/progress_bar.rs
@@ -7,13 +7,13 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String
     }
 
     // "Progress: [".len() == 11
-    // "] xxx/xxx".len() == 9
-    // 11 + 9 = 20
-    let wrapper_width = 20;
+    // "] xxx/xx exercises_".len() == 19 (leaving the last char empty for `total` > 99)
+    // 11 + 19 = 30
+    let wrapper_width = 30;
 
     // If the line width is too low for a progress bar, just show the ratio.
     if line_width < wrapper_width + 4 {
-        return Ok(format!("Progress: {progress}/{total}"));
+        return Ok(format!("Progress: {progress}/{total} exercises"));
     }
 
     let mut line = String::with_capacity(usize::from(line_width));
@@ -34,7 +34,7 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String
         line.push(' ');
     }
 
-    line.write_fmt(format_args!("] {progress:>3}/{total:<3}"))
+    line.write_fmt(format_args!("] {progress:>3}/{total} exercises"))
         .unwrap();
 
     Ok(line)

From c8d217ad50a7117fe35735b4083f2aa1e2b47d97 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Tue, 9 Apr 2024 22:20:12 +0200
Subject: [PATCH 057/109] Fix showing stdout and stderr

---
 src/watch/state.rs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/watch/state.rs b/src/watch/state.rs
index 8fae7e87..24978bb7 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -60,13 +60,15 @@ impl<'a> WatchState<'a> {
 
     pub fn run_exercise(&mut self) -> Result<bool> {
         let output = self.exercise.run()?;
+        self.stdout = Some(output.stdout);
 
         if !output.status.success() {
-            self.stdout = Some(output.stdout);
             self.stderr = Some(output.stderr);
             return Ok(false);
         }
 
+        self.stderr = None;
+
         if let State::Pending(context) = self.exercise.state()? {
             let mut message = format!(
                 "
@@ -98,7 +100,6 @@ You can keep working on this exercise or jump into the next one by removing the
                 )?;
             }
 
-            self.stdout = Some(output.stdout);
             self.message = Some(message);
             return Ok(false);
         }

From 4a80bf64411f228c35c173b6188df5114d4c52fa Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 00:42:32 +0200
Subject: [PATCH 058/109] Colorize the progress bar

---
 src/list/state.rs   |  6 +--
 src/progress_bar.rs | 96 +++++++++++++++++++++++++++++++++++----------
 2 files changed, 79 insertions(+), 23 deletions(-)

diff --git a/src/list/state.rs b/src/list/state.rs
index b67c624b..89189794 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -7,7 +7,7 @@ use ratatui::{
     Frame,
 };
 
-use crate::{exercise::Exercise, progress_bar::progress_bar, state_file::StateFile};
+use crate::{exercise::Exercise, progress_bar::progress_bar_ratatui, state_file::StateFile};
 
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub enum Filter {
@@ -166,11 +166,11 @@ impl UiState {
         );
 
         frame.render_widget(
-            Paragraph::new(Span::raw(progress_bar(
+            Paragraph::new(progress_bar_ratatui(
                 self.progress,
                 self.exercises.len() as u16,
                 area.width,
-            )?))
+            )?)
             .block(Block::default().borders(Borders::BOTTOM)),
             Rect {
                 x: 0,
diff --git a/src/progress_bar.rs b/src/progress_bar.rs
index ee55ba7b..97c8ad90 100644
--- a/src/progress_bar.rs
+++ b/src/progress_bar.rs
@@ -1,41 +1,97 @@
 use anyhow::{bail, Result};
+use ratatui::text::{Line, Span};
 use std::fmt::Write;
 
+const PREFIX: &str = "Progress: [";
+const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
+// Leaving the last char empty (_) for `total` > 99.
+const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
+const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
+const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
+
+const PROGRESS_EXCEEDS_MAX_ERR: &str =
+    "The progress of the progress bar is higher than the maximum";
+
 pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
+    use crossterm::style::Stylize;
+
     if progress > total {
-        bail!("The progress of the progress bar is higher than the maximum");
+        bail!(PROGRESS_EXCEEDS_MAX_ERR);
     }
 
-    // "Progress: [".len() == 11
-    // "] xxx/xx exercises_".len() == 19 (leaving the last char empty for `total` > 99)
-    // 11 + 19 = 30
-    let wrapper_width = 30;
-
-    // If the line width is too low for a progress bar, just show the ratio.
-    if line_width < wrapper_width + 4 {
+    if line_width < MIN_LINE_WIDTH {
         return Ok(format!("Progress: {progress}/{total} exercises"));
     }
 
     let mut line = String::with_capacity(usize::from(line_width));
-    line.push_str("Progress: [");
+    line.push_str(PREFIX);
 
-    let remaining_width = line_width.saturating_sub(wrapper_width);
-    let filled = (remaining_width * progress) / total;
+    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 {
-        line.push('=');
+        green_part.push('#');
     }
 
-    if filled < remaining_width {
-        line.push('>');
+    if filled < width {
+        green_part.push('>');
+    }
+    write!(line, "{}", green_part.green()).unwrap();
+
+    let width_minus_filled = width - filled;
+    if width_minus_filled > 1 {
+        let red_part_width = width_minus_filled - 1;
+        let mut red_part = String::with_capacity(usize::from(red_part_width));
+        for _ in 0..red_part_width {
+            red_part.push('-');
+        }
+        write!(line, "{}", red_part.red()).unwrap();
     }
 
-    for _ in 0..(remaining_width - filled).saturating_sub(1) {
-        line.push(' ');
-    }
-
-    line.write_fmt(format_args!("] {progress:>3}/{total} exercises"))
-        .unwrap();
+    write!(line, "] {progress:>3}/{total} exercises").unwrap();
 
     Ok(line)
 }
+
+pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
+    use ratatui::style::Stylize;
+
+    if progress > total {
+        bail!(PROGRESS_EXCEEDS_MAX_ERR);
+    }
+
+    if line_width < MIN_LINE_WIDTH {
+        return Ok(Line::raw(format!("Progress: {progress}/{total} exercises")));
+    }
+
+    let mut spans = Vec::with_capacity(4);
+    spans.push(Span::raw(PREFIX));
+
+    let width = line_width - WRAPPER_WIDTH;
+    let filled = (width * progress) / total;
+
+    let mut green_part = String::with_capacity(usize::from(filled + 1));
+    for _ in 0..filled {
+        green_part.push('#');
+    }
+
+    if filled < width {
+        green_part.push('>');
+    }
+    spans.push(green_part.green());
+
+    let width_minus_filled = width - filled;
+    if width_minus_filled > 1 {
+        let red_part_width = width_minus_filled - 1;
+        let mut red_part = String::with_capacity(usize::from(red_part_width));
+        for _ in 0..red_part_width {
+            red_part.push('-');
+        }
+        spans.push(red_part.red());
+    }
+
+    spans.push(Span::raw(format!("] {progress:>3}/{total} exercises")));
+
+    Ok(Line::from(spans))
+}

From 533a009257adba0714292d326f57671f77cffbd3 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 00:51:41 +0200
Subject: [PATCH 059/109] Show the progress in the progress bar, not the
 current exercise index

---
 src/watch/state.rs | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/watch/state.rs b/src/watch/state.rs
index 24978bb7..4db9440b 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -20,6 +20,7 @@ pub struct WatchState<'a> {
     exercises: &'static [Exercise],
     exercise: &'static Exercise,
     current_exercise_ind: usize,
+    progress: u16,
     stdout: Option<Vec<u8>>,
     stderr: Option<Vec<u8>>,
     message: Option<String>,
@@ -29,6 +30,7 @@ pub struct WatchState<'a> {
 impl<'a> WatchState<'a> {
     pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self {
         let current_exercise_ind = state_file.next_exercise_ind();
+        let progress = state_file.progress().iter().filter(|done| **done).count() as u16;
         let exercise = &exercises[current_exercise_ind];
 
         let writer = io::stdout().lock();
@@ -46,6 +48,7 @@ impl<'a> WatchState<'a> {
             exercises,
             exercise,
             current_exercise_ind,
+            progress,
             stdout: None,
             stderr: None,
             message: None,
@@ -139,11 +142,7 @@ You can keep working on this exercise or jump into the next one by removing the
 
         self.writer.write_all(b"\n")?;
         let line_width = size()?.0;
-        let progress_bar = progress_bar(
-            self.current_exercise_ind as u16,
-            self.exercises.len() as u16,
-            line_width,
-        )?;
+        let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?;
         self.writer.write_all(progress_bar.as_bytes())?;
 
         self.show_prompt()?;

From d1a965f019d0e8f22d5a57f0a7abd8cd4a8d0d0c Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 02:12:50 +0200
Subject: [PATCH 060/109] Make the list mode part of the watch mode

---
 src/main.rs        | 19 +++++++++++--------
 src/watch.rs       | 27 +++++++++++++++++++++++----
 src/watch/state.rs |  5 +++--
 3 files changed, 37 insertions(+), 14 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 504c02dc..fc83e0fd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,9 +16,11 @@ mod watch;
 use self::{
     consts::WELCOME,
     exercise::{Exercise, InfoFile},
+    list::list,
     run::run,
     state_file::StateFile,
     verify::{verify, VerifyState},
+    watch::{watch, WatchExit},
 };
 
 /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
@@ -52,8 +54,6 @@ enum Subcommands {
         /// The name of the exercise
         name: String,
     },
-    /// List the exercises available in Rustlings
-    List,
 }
 
 fn find_exercise(name: &str, exercises: &'static [Exercise]) -> Result<(usize, &'static Exercise)> {
@@ -112,14 +112,17 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
     let mut state_file = StateFile::read_or_default(exercises);
 
     match args.command {
-        None | Some(Subcommands::Watch) => {
-            watch::watch(&state_file, exercises)?;
-        }
+        None | Some(Subcommands::Watch) => loop {
+            match watch(&mut state_file, exercises)? {
+                WatchExit::Shutdown => break,
+                // It is much easier to exit the watch mode, launch the list mode and then restart
+                // the watch mode instead of trying to pause the watch threads and correct the
+                // watch state.
+                WatchExit::List => list(&mut state_file, exercises)?,
+            }
+        },
         // `Init` is handled above.
         Some(Subcommands::Init) => (),
-        Some(Subcommands::List) => {
-            list::list(&mut state_file, exercises)?;
-        }
         Some(Subcommands::Run { name }) => {
             let (_, exercise) = find_exercise(&name, exercises)?;
             run(exercise).unwrap_or_else(|_| exit(1));
diff --git a/src/watch.rs b/src/watch.rs
index 6324eb36..004a13f6 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -18,9 +18,19 @@ use crate::{exercise::Exercise, state_file::StateFile};
 
 use self::state::WatchState;
 
+/// Returned by the watch mode to indicate what to do afterwards.
+pub enum WatchExit {
+    /// Exit the program.
+    Shutdown,
+    /// Enter the list mode and restart the watch mode afterwards.
+    List,
+}
+
+#[derive(Copy, Clone)]
 enum InputEvent {
     Hint,
     Clear,
+    List,
     Quit,
     Unrecognized,
 }
@@ -86,20 +96,26 @@ fn input_handler(tx: Sender<WatchEvent>) {
         let event = match stdin_buf.trim() {
             "h" | "hint" => InputEvent::Hint,
             "c" | "clear" => InputEvent::Clear,
+            "l" | "list" => InputEvent::List,
             "q" | "quit" => InputEvent::Quit,
             _ => InputEvent::Unrecognized,
         };
 
-        stdin_buf.clear();
-
         if tx.send(WatchEvent::Input(event)).is_err() {
             // The receiver was dropped.
             return;
         }
+
+        match event {
+            InputEvent::List | InputEvent::Quit => return,
+            _ => (),
+        }
+
+        stdin_buf.clear();
     }
 }
 
-pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> {
+pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<WatchExit> {
     let (tx, rx) = channel();
     let mut debouncer = new_debouncer(
         Duration::from_secs(1),
@@ -125,6 +141,9 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<(
             WatchEvent::Input(InputEvent::Hint) => {
                 watch_state.show_hint()?;
             }
+            WatchEvent::Input(InputEvent::List) => {
+                return Ok(WatchExit::List);
+            }
             WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => {
                 watch_state.render()?;
             }
@@ -147,5 +166,5 @@ 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.
 ")?;
 
-    Ok(())
+    Ok(WatchExit::Shutdown)
 }
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 4db9440b..393ea02c 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -36,10 +36,11 @@ impl<'a> WatchState<'a> {
         let writer = io::stdout().lock();
 
         let prompt = format!(
-            "\n\n{}int/{}lear/{}uit? ",
+            "\n\n{}int/{}lear/{}ist/{}uit? ",
             "h".bold(),
             "c".bold(),
-            "q".bold()
+            "l".bold(),
+            "q".bold(),
         )
         .into_bytes();
 

From c9a5fa6097997e95bc415cd76ef931a1a4bb1510 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 02:19:14 +0200
Subject: [PATCH 061/109] Accept repeat keyboard events

---
 src/list.rs | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index c92b3692..560b85a8 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -28,13 +28,10 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul
 
         let key = loop {
             match event::read()? {
-                Event::Key(key) => {
-                    if key.kind != KeyEventKind::Press {
-                        continue;
-                    }
-
-                    break key;
-                }
+                Event::Key(key) => match key.kind {
+                    KeyEventKind::Press | KeyEventKind::Repeat => break key,
+                    KeyEventKind::Release => (),
+                },
                 // Redraw
                 Event::Resize(_, _) => continue 'outer,
                 // Ignore

From f034899c7f8de93ff572722b1cdf44f73c6452b5 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 03:54:48 +0200
Subject: [PATCH 062/109] Capture terminal resize events

---
 src/watch.rs | 91 ++++++++++++++++++++++++++++++++++------------------
 1 file changed, 60 insertions(+), 31 deletions(-)

diff --git a/src/watch.rs b/src/watch.rs
index 004a13f6..7b4a02dd 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -1,11 +1,12 @@
-use anyhow::Result;
+use anyhow::{Error, Result};
+use crossterm::event::{self, Event, KeyCode, KeyEventKind};
 use notify_debouncer_mini::{
     new_debouncer,
     notify::{self, RecursiveMode},
     DebounceEventResult, DebouncedEventKind,
 };
 use std::{
-    io::{self, BufRead, Write},
+    io::{self, Write},
     path::Path,
     sync::mpsc::{channel, Sender},
     thread,
@@ -39,7 +40,7 @@ enum WatchEvent {
     Input(InputEvent),
     FileChange { exercise_ind: usize },
     NotifyErr(notify::Error),
-    StdinErr(io::Error),
+    TerminalEventErr(io::Error),
     TerminalResize,
 }
 
@@ -81,37 +82,61 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler {
     }
 }
 
-fn input_handler(tx: Sender<WatchEvent>) {
-    let mut stdin = io::stdin().lock();
-    let mut stdin_buf = String::with_capacity(8);
+fn terminal_event_handler(tx: Sender<WatchEvent>) {
+    let mut input = String::with_capacity(8);
 
     loop {
-        if let Err(e) = stdin.read_line(&mut stdin_buf) {
-            // If `send` returns an error, then the receiver is dropped and
-            // a shutdown has been already initialized.
-            let _ = tx.send(WatchEvent::StdinErr(e));
-            return;
-        }
-
-        let event = match stdin_buf.trim() {
-            "h" | "hint" => InputEvent::Hint,
-            "c" | "clear" => InputEvent::Clear,
-            "l" | "list" => InputEvent::List,
-            "q" | "quit" => InputEvent::Quit,
-            _ => InputEvent::Unrecognized,
+        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;
+            }
         };
 
-        if tx.send(WatchEvent::Input(event)).is_err() {
-            // The receiver was dropped.
-            return;
-        }
+        match terminal_event {
+            Event::Key(key) => {
+                match key.kind {
+                    KeyEventKind::Release => continue,
+                    KeyEventKind::Press | KeyEventKind::Repeat => (),
+                }
 
-        match event {
-            InputEvent::List | InputEvent::Quit => return,
-            _ => (),
-        }
+                match key.code {
+                    KeyCode::Enter => {
+                        let input_event = match input.trim() {
+                            "h" | "hint" => InputEvent::Hint,
+                            "c" | "clear" => InputEvent::Clear,
+                            "l" | "list" => InputEvent::List,
+                            "q" | "quit" => InputEvent::Quit,
+                            _ => InputEvent::Unrecognized,
+                        };
 
-        stdin_buf.clear();
+                        if tx.send(WatchEvent::Input(input_event)).is_err() {
+                            return;
+                        }
+
+                        match input_event {
+                            InputEvent::List | InputEvent::Quit => return,
+                            _ => (),
+                        }
+
+                        input.clear();
+                    }
+                    KeyCode::Char(c) => {
+                        input.push(c);
+                    }
+                    _ => (),
+                }
+            }
+            Event::Resize(_, _) => {
+                if tx.send(WatchEvent::TerminalResize).is_err() {
+                    return;
+                }
+            }
+            Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
+        }
     }
 }
 
@@ -134,7 +159,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu
     watch_state.run_exercise()?;
     watch_state.render()?;
 
-    thread::spawn(move || input_handler(tx));
+    thread::spawn(move || terminal_event_handler(tx));
 
     while let Ok(event) = rx.recv() {
         match event {
@@ -156,8 +181,12 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu
                 watch_state.run_exercise_with_ind(exercise_ind)?;
                 watch_state.render()?;
             }
-            WatchEvent::NotifyErr(e) => return Err(e.into()),
-            WatchEvent::StdinErr(e) => return Err(e.into()),
+            WatchEvent::NotifyErr(e) => {
+                return Err(Error::from(e).context("Exercise file watcher failed"))
+            }
+            WatchEvent::TerminalEventErr(e) => {
+                return Err(Error::from(e).context("Terminal event listener failed"))
+            }
         }
     }
 

From a46d66134b26095e553f284c02de9a895e15f180 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 03:56:41 +0200
Subject: [PATCH 063/109] Fix shift of first output line

---
 src/watch/state.rs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/watch/state.rs b/src/watch/state.rs
index 393ea02c..08707a45 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -127,6 +127,9 @@ You can keep working on this exercise or jump into the next one by removing the
     }
 
     pub fn render(&mut self) -> Result<()> {
+        // Prevent having the first line shifted after clearing because of the prompt.
+        self.writer.write_all(b"\n")?;
+
         self.writer.execute(Clear(ClearType::All))?;
 
         if let Some(stdout) = &self.stdout {

From 6255efe8b2de9d8d7f69871584444ab34fae122d Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 04:08:40 +0200
Subject: [PATCH 064/109] Show the invalid command to avoid confusion after
 resizing the terminal

---
 src/watch.rs       | 24 ++++++++++--------------
 src/watch/state.rs |  9 +++++++--
 2 files changed, 17 insertions(+), 16 deletions(-)

diff --git a/src/watch.rs b/src/watch.rs
index 7b4a02dd..8b211035 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -27,13 +27,12 @@ pub enum WatchExit {
     List,
 }
 
-#[derive(Copy, Clone)]
 enum InputEvent {
     Hint,
     Clear,
     List,
     Quit,
-    Unrecognized,
+    Unrecognized(String),
 }
 
 enum WatchEvent {
@@ -85,7 +84,7 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler {
 fn terminal_event_handler(tx: Sender<WatchEvent>) {
     let mut input = String::with_capacity(8);
 
-    loop {
+    let last_input_event = loop {
         let terminal_event = match event::read() {
             Ok(v) => v,
             Err(e) => {
@@ -108,20 +107,15 @@ fn terminal_event_handler(tx: Sender<WatchEvent>) {
                         let input_event = match input.trim() {
                             "h" | "hint" => InputEvent::Hint,
                             "c" | "clear" => InputEvent::Clear,
-                            "l" | "list" => InputEvent::List,
-                            "q" | "quit" => InputEvent::Quit,
-                            _ => InputEvent::Unrecognized,
+                            "l" | "list" => break InputEvent::List,
+                            "q" | "quit" => break InputEvent::Quit,
+                            _ => InputEvent::Unrecognized(input.clone()),
                         };
 
                         if tx.send(WatchEvent::Input(input_event)).is_err() {
                             return;
                         }
 
-                        match input_event {
-                            InputEvent::List | InputEvent::Quit => return,
-                            _ => (),
-                        }
-
                         input.clear();
                     }
                     KeyCode::Char(c) => {
@@ -137,7 +131,9 @@ fn terminal_event_handler(tx: Sender<WatchEvent>) {
             }
             Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
         }
-    }
+    };
+
+    let _ = tx.send(WatchEvent::Input(last_input_event));
 }
 
 pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<WatchExit> {
@@ -173,8 +169,8 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu
                 watch_state.render()?;
             }
             WatchEvent::Input(InputEvent::Quit) => break,
-            WatchEvent::Input(InputEvent::Unrecognized) => {
-                watch_state.handle_invalid_cmd()?;
+            WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
+                watch_state.handle_invalid_cmd(&cmd)?;
             }
             WatchEvent::FileChange { exercise_ind } => {
                 // TODO: bool
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 08707a45..751285fc 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -159,8 +159,13 @@ You can keep working on this exercise or jump into the next one by removing the
         self.show_prompt()
     }
 
-    pub fn handle_invalid_cmd(&mut self) -> io::Result<()> {
-        self.writer.write_all(b"Invalid command")?;
+    pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> {
+        self.writer.write_all(b"Invalid command: ")?;
+        self.writer.write_all(cmd.as_bytes())?;
+        if cmd.len() > 1 {
+            self.writer
+                .write_all(b" (confusing input can occur after resizing the terminal)")?;
+        }
         self.show_prompt()
     }
 }

From 62e92476e6dad1fc191fd666eae2fccb263f5ff0 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 04:10:05 +0200
Subject: [PATCH 065/109] Fix typo

---
 src/watch.rs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/watch.rs b/src/watch.rs
index 8b211035..cf63627d 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -43,12 +43,12 @@ enum WatchEvent {
     TerminalResize,
 }
 
-struct DebouceEventHandler {
+struct DebounceEventHandler {
     tx: Sender<WatchEvent>,
     exercises: &'static [Exercise],
 }
 
-impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler {
+impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
     fn handle_event(&mut self, event: DebounceEventResult) {
         let event = match event {
             Ok(event) => {
@@ -140,7 +140,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu
     let (tx, rx) = channel();
     let mut debouncer = new_debouncer(
         Duration::from_secs(1),
-        DebouceEventHandler {
+        DebounceEventHandler {
             tx: tx.clone(),
             exercises,
         },

From a59acf88354c8dfba301e59173653bc9a5f4bfb2 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 14:29:31 +0200
Subject: [PATCH 066/109] Show the current exercise path

---
 src/progress_bar.rs | 2 +-
 src/watch/state.rs  | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/progress_bar.rs b/src/progress_bar.rs
index 97c8ad90..d6962b8c 100644
--- a/src/progress_bar.rs
+++ b/src/progress_bar.rs
@@ -49,7 +49,7 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String
         write!(line, "{}", red_part.red()).unwrap();
     }
 
-    write!(line, "] {progress:>3}/{total} exercises").unwrap();
+    writeln!(line, "] {progress:>3}/{total} exercises").unwrap();
 
     Ok(line)
 }
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 751285fc..da5ac3d7 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -149,6 +149,12 @@ You can keep working on this exercise or jump into the next one by removing the
         let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?;
         self.writer.write_all(progress_bar.as_bytes())?;
 
+        self.writer.write_all(b"Current exercise: ")?;
+        self.writer.write_fmt(format_args!(
+            "{}",
+            self.exercise.path.to_string_lossy().bold()
+        ))?;
+
         self.show_prompt()?;
 
         Ok(())

From 193e0a03b2cde094b2a668371b7ed94f81d33de7 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 14:31:08 +0200
Subject: [PATCH 067/109] Use light blue for the message

---
 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 89189794..209374b1 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -186,7 +186,7 @@ impl UiState {
                 "↓/j ↑/k home/g end/G │ filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit",
             )
         } else {
-            self.message.as_str().blue()
+            self.message.as_str().light_blue()
         };
         frame.render_widget(
             message,

From b3642b0219252e97213fd4348379f272a3002f39 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 14:35:42 +0200
Subject: [PATCH 068/109] Remove todo

---
 src/state_file.rs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/state_file.rs b/src/state_file.rs
index 693c78dc..583e0436 100644
--- a/src/state_file.rs
+++ b/src/state_file.rs
@@ -33,7 +33,6 @@ impl StateFile {
     }
 
     fn write(&self) -> Result<()> {
-        // TODO: Capacity
         let mut buf = Vec::with_capacity(1024);
         serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?;
         fs::write(".rustlings-state.json", buf)

From 27e95206658e8f86cad351ce163f03c0d36e05ea Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 14:40:49 +0200
Subject: [PATCH 069/109] Add deny_unknown_fields

---
 src/exercise.rs   | 2 ++
 src/state_file.rs | 1 +
 2 files changed, 3 insertions(+)

diff --git a/src/exercise.rs b/src/exercise.rs
index 232d7f95..ca47009d 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -46,6 +46,7 @@ pub enum Mode {
 }
 
 #[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
 pub struct InfoFile {
     pub exercises: Vec<Exercise>,
 }
@@ -65,6 +66,7 @@ impl InfoFile {
 
 // Deserialized from the `info.toml` file.
 #[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
 pub struct Exercise {
     // Name of the exercise
     pub name: String,
diff --git a/src/state_file.rs b/src/state_file.rs
index 583e0436..6b80354e 100644
--- a/src/state_file.rs
+++ b/src/state_file.rs
@@ -5,6 +5,7 @@ use std::fs;
 use crate::exercise::Exercise;
 
 #[derive(Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
 pub struct StateFile {
     next_exercise_ind: usize,
     progress: Vec<bool>,

From 256c4013b759368b97f08aeb38d1b03f2eb42d7a Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 15:56:38 +0200
Subject: [PATCH 070/109] Keep hint displayed after resizing the terminal

---
 src/watch.rs       |  4 +---
 src/watch/state.rs | 41 +++++++++++++++++++++++++----------------
 2 files changed, 26 insertions(+), 19 deletions(-)

diff --git a/src/watch.rs b/src/watch.rs
index cf63627d..6d791f4b 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -29,7 +29,6 @@ pub enum WatchExit {
 
 enum InputEvent {
     Hint,
-    Clear,
     List,
     Quit,
     Unrecognized(String),
@@ -106,7 +105,6 @@ fn terminal_event_handler(tx: Sender<WatchEvent>) {
                     KeyCode::Enter => {
                         let input_event = match input.trim() {
                             "h" | "hint" => InputEvent::Hint,
-                            "c" | "clear" => InputEvent::Clear,
                             "l" | "list" => break InputEvent::List,
                             "q" | "quit" => break InputEvent::Quit,
                             _ => InputEvent::Unrecognized(input.clone()),
@@ -165,7 +163,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu
             WatchEvent::Input(InputEvent::List) => {
                 return Ok(WatchExit::List);
             }
-            WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => {
+            WatchEvent::TerminalResize => {
                 watch_state.render()?;
             }
             WatchEvent::Input(InputEvent::Quit) => break,
diff --git a/src/watch/state.rs b/src/watch/state.rs
index da5ac3d7..6f6d2f10 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -6,7 +6,7 @@ use crossterm::{
 };
 use std::{
     fmt::Write as _,
-    io::{self, StdoutLock, Write as _},
+    io::{self, StdoutLock, Write},
 };
 
 use crate::{
@@ -24,7 +24,7 @@ pub struct WatchState<'a> {
     stdout: Option<Vec<u8>>,
     stderr: Option<Vec<u8>>,
     message: Option<String>,
-    prompt: Vec<u8>,
+    hint_displayed: bool,
 }
 
 impl<'a> WatchState<'a> {
@@ -35,15 +35,6 @@ impl<'a> WatchState<'a> {
 
         let writer = io::stdout().lock();
 
-        let prompt = format!(
-            "\n\n{}int/{}lear/{}ist/{}uit? ",
-            "h".bold(),
-            "c".bold(),
-            "l".bold(),
-            "q".bold(),
-        )
-        .into_bytes();
-
         Self {
             writer,
             exercises,
@@ -53,7 +44,7 @@ impl<'a> WatchState<'a> {
             stdout: None,
             stderr: None,
             message: None,
-            prompt,
+            hint_displayed: false,
         }
     }
 
@@ -122,7 +113,15 @@ You can keep working on this exercise or jump into the next one by removing the
     }
 
     pub fn show_prompt(&mut self) -> io::Result<()> {
-        self.writer.write_all(&self.prompt)?;
+        self.writer.write_all(b"\n\n")?;
+
+        if !self.hint_displayed {
+            self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?;
+        }
+
+        self.writer
+            .write_fmt(format_args!("{}ist/{}uit? ", 'l'.bold(), 'q'.bold()))?;
+
         self.writer.flush()
     }
 
@@ -134,10 +133,12 @@ You can keep working on this exercise or jump into the next one by removing the
 
         if let Some(stdout) = &self.stdout {
             self.writer.write_all(stdout)?;
+            self.writer.write_all(b"\n")?;
         }
 
         if let Some(stderr) = &self.stderr {
             self.writer.write_all(stderr)?;
+            self.writer.write_all(b"\n")?;
         }
 
         if let Some(message) = &self.message {
@@ -145,6 +146,14 @@ You can keep working on this exercise or jump into the next one by removing the
         }
 
         self.writer.write_all(b"\n")?;
+
+        if self.hint_displayed {
+            self.writer
+                .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?;
+            self.writer.write_all(self.exercise.hint.as_bytes())?;
+            self.writer.write_all(b"\n\n")?;
+        }
+
         let line_width = size()?.0;
         let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?;
         self.writer.write_all(progress_bar.as_bytes())?;
@@ -160,9 +169,9 @@ You can keep working on this exercise or jump into the next one by removing the
         Ok(())
     }
 
-    pub fn show_hint(&mut self) -> io::Result<()> {
-        self.writer.write_all(self.exercise.hint.as_bytes())?;
-        self.show_prompt()
+    pub fn show_hint(&mut self) -> Result<()> {
+        self.hint_displayed = true;
+        self.render()
     }
 
     pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> {

From 4bb6bda9f6416e30233342e73fc9a8486faa3f98 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Wed, 10 Apr 2024 16:02:12 +0200
Subject: [PATCH 071/109] Separate event handlers

---
 src/watch.rs                | 123 ++++--------------------------------
 src/watch/debounce_event.rs |  44 +++++++++++++
 src/watch/terminal_event.rs |  65 +++++++++++++++++++
 3 files changed, 123 insertions(+), 109 deletions(-)
 create mode 100644 src/watch/debounce_event.rs
 create mode 100644 src/watch/terminal_event.rs

diff --git a/src/watch.rs b/src/watch.rs
index 6d791f4b..b29169b3 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -1,38 +1,27 @@
 use anyhow::{Error, Result};
-use crossterm::event::{self, Event, KeyCode, KeyEventKind};
 use notify_debouncer_mini::{
     new_debouncer,
     notify::{self, RecursiveMode},
-    DebounceEventResult, DebouncedEventKind,
 };
 use std::{
     io::{self, Write},
     path::Path,
-    sync::mpsc::{channel, Sender},
+    sync::mpsc::channel,
     thread,
     time::Duration,
 };
 
+mod debounce_event;
 mod state;
+mod terminal_event;
 
 use crate::{exercise::Exercise, state_file::StateFile};
 
-use self::state::WatchState;
-
-/// Returned by the watch mode to indicate what to do afterwards.
-pub enum WatchExit {
-    /// Exit the program.
-    Shutdown,
-    /// Enter the list mode and restart the watch mode afterwards.
-    List,
-}
-
-enum InputEvent {
-    Hint,
-    List,
-    Quit,
-    Unrecognized(String),
-}
+use self::{
+    debounce_event::DebounceEventHandler,
+    state::WatchState,
+    terminal_event::{terminal_event_handler, InputEvent},
+};
 
 enum WatchEvent {
     Input(InputEvent),
@@ -42,96 +31,12 @@ enum WatchEvent {
     TerminalResize,
 }
 
-struct DebounceEventHandler {
-    tx: Sender<WatchEvent>,
-    exercises: &'static [Exercise],
-}
-
-impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
-    fn handle_event(&mut self, event: DebounceEventResult) {
-        let event = match event {
-            Ok(event) => {
-                let Some(exercise_ind) = event
-                    .iter()
-                    .filter_map(|event| {
-                        if event.kind != DebouncedEventKind::Any
-                            || !event.path.extension().is_some_and(|ext| ext == "rs")
-                        {
-                            return None;
-                        }
-
-                        self.exercises
-                            .iter()
-                            .position(|exercise| event.path.ends_with(&exercise.path))
-                    })
-                    .min()
-                else {
-                    return;
-                };
-
-                WatchEvent::FileChange { exercise_ind }
-            }
-            Err(e) => WatchEvent::NotifyErr(e),
-        };
-
-        // An error occurs when the receiver is dropped.
-        // After dropping the receiver, the debouncer guard should also be dropped.
-        let _ = self.tx.send(event);
-    }
-}
-
-fn terminal_event_handler(tx: Sender<WatchEvent>) {
-    let mut input = String::with_capacity(8);
-
-    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) => {
-                match key.kind {
-                    KeyEventKind::Release => continue,
-                    KeyEventKind::Press | KeyEventKind::Repeat => (),
-                }
-
-                match key.code {
-                    KeyCode::Enter => {
-                        let input_event = match input.trim() {
-                            "h" | "hint" => InputEvent::Hint,
-                            "l" | "list" => break InputEvent::List,
-                            "q" | "quit" => break InputEvent::Quit,
-                            _ => InputEvent::Unrecognized(input.clone()),
-                        };
-
-                        if tx.send(WatchEvent::Input(input_event)).is_err() {
-                            return;
-                        }
-
-                        input.clear();
-                    }
-                    KeyCode::Char(c) => {
-                        input.push(c);
-                    }
-                    _ => (),
-                }
-            }
-            Event::Resize(_, _) => {
-                if tx.send(WatchEvent::TerminalResize).is_err() {
-                    return;
-                }
-            }
-            Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
-        }
-    };
-
-    let _ = tx.send(WatchEvent::Input(last_input_event));
+/// Returned by the watch mode to indicate what to do afterwards.
+pub enum WatchExit {
+    /// Exit the program.
+    Shutdown,
+    /// Enter the list mode and restart the watch mode afterwards.
+    List,
 }
 
 pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<WatchExit> {
diff --git a/src/watch/debounce_event.rs b/src/watch/debounce_event.rs
new file mode 100644
index 00000000..1dc92cb4
--- /dev/null
+++ b/src/watch/debounce_event.rs
@@ -0,0 +1,44 @@
+use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
+use std::sync::mpsc::Sender;
+
+use crate::exercise::Exercise;
+
+use super::WatchEvent;
+
+pub struct DebounceEventHandler {
+    pub tx: Sender<WatchEvent>,
+    pub exercises: &'static [Exercise],
+}
+
+impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
+    fn handle_event(&mut self, event: DebounceEventResult) {
+        let event = match event {
+            Ok(event) => {
+                let Some(exercise_ind) = event
+                    .iter()
+                    .filter_map(|event| {
+                        if event.kind != DebouncedEventKind::Any
+                            || !event.path.extension().is_some_and(|ext| ext == "rs")
+                        {
+                            return None;
+                        }
+
+                        self.exercises
+                            .iter()
+                            .position(|exercise| event.path.ends_with(&exercise.path))
+                    })
+                    .min()
+                else {
+                    return;
+                };
+
+                WatchEvent::FileChange { exercise_ind }
+            }
+            Err(e) => WatchEvent::NotifyErr(e),
+        };
+
+        // An error occurs when the receiver is dropped.
+        // After dropping the receiver, the debouncer guard should also be dropped.
+        let _ = self.tx.send(event);
+    }
+}
diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs
new file mode 100644
index 00000000..7c85b5bd
--- /dev/null
+++ b/src/watch/terminal_event.rs
@@ -0,0 +1,65 @@
+use crossterm::event::{self, Event, KeyCode, KeyEventKind};
+use std::sync::mpsc::Sender;
+
+use super::WatchEvent;
+
+pub enum InputEvent {
+    Hint,
+    List,
+    Quit,
+    Unrecognized(String),
+}
+
+pub fn terminal_event_handler(tx: Sender<WatchEvent>) {
+    let mut input = String::with_capacity(8);
+
+    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) => {
+                match key.kind {
+                    KeyEventKind::Release => continue,
+                    KeyEventKind::Press | KeyEventKind::Repeat => (),
+                }
+
+                match key.code {
+                    KeyCode::Enter => {
+                        let input_event = match input.trim() {
+                            "h" | "hint" => InputEvent::Hint,
+                            "l" | "list" => break InputEvent::List,
+                            "q" | "quit" => break InputEvent::Quit,
+                            _ => InputEvent::Unrecognized(input.clone()),
+                        };
+
+                        if tx.send(WatchEvent::Input(input_event)).is_err() {
+                            return;
+                        }
+
+                        input.clear();
+                    }
+                    KeyCode::Char(c) => {
+                        input.push(c);
+                    }
+                    _ => (),
+                }
+            }
+            Event::Resize(_, _) => {
+                if tx.send(WatchEvent::TerminalResize).is_err() {
+                    return;
+                }
+            }
+            Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
+        }
+    };
+
+    let _ = tx.send(WatchEvent::Input(last_input_event));
+}

From fa1f239a702eb2c0b7e0115e986481156961bbc8 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 02:51:02 +0200
Subject: [PATCH 072/109] Remove "I AM NOT DONE" and the verify mode and add
 AppState

---
 Cargo.lock                                    |   1 -
 Cargo.toml                                    |   1 -
 README.md                                     |   8 +-
 exercises/00_intro/intro1.rs                  |   4 +-
 exercises/00_intro/intro2.rs                  |   2 -
 exercises/01_variables/variables1.rs          |   2 -
 exercises/01_variables/variables2.rs          |   2 -
 exercises/01_variables/variables3.rs          |   2 -
 exercises/01_variables/variables4.rs          |   2 -
 exercises/01_variables/variables5.rs          |   2 -
 exercises/01_variables/variables6.rs          |   2 -
 exercises/02_functions/functions1.rs          |   2 -
 exercises/02_functions/functions2.rs          |   2 -
 exercises/02_functions/functions3.rs          |   2 -
 exercises/02_functions/functions4.rs          |   2 -
 exercises/02_functions/functions5.rs          |   2 -
 exercises/03_if/if1.rs                        |   2 -
 exercises/03_if/if2.rs                        |   2 -
 exercises/03_if/if3.rs                        |   2 -
 .../04_primitive_types/primitive_types1.rs    |   2 -
 .../04_primitive_types/primitive_types2.rs    |   2 -
 .../04_primitive_types/primitive_types3.rs    |   2 -
 .../04_primitive_types/primitive_types4.rs    |   2 -
 .../04_primitive_types/primitive_types5.rs    |   2 -
 .../04_primitive_types/primitive_types6.rs    |   2 -
 exercises/05_vecs/vecs1.rs                    |   2 -
 exercises/05_vecs/vecs2.rs                    |   2 -
 .../06_move_semantics/move_semantics1.rs      |   2 -
 .../06_move_semantics/move_semantics2.rs      |   2 -
 .../06_move_semantics/move_semantics3.rs      |   2 -
 .../06_move_semantics/move_semantics4.rs      |   2 -
 .../06_move_semantics/move_semantics5.rs      |   2 -
 .../06_move_semantics/move_semantics6.rs      |   2 -
 exercises/07_structs/structs1.rs              |   2 -
 exercises/07_structs/structs2.rs              |   2 -
 exercises/07_structs/structs3.rs              |   2 -
 exercises/08_enums/enums1.rs                  |   2 -
 exercises/08_enums/enums2.rs                  |   2 -
 exercises/08_enums/enums3.rs                  |   2 -
 exercises/09_strings/strings1.rs              |   2 -
 exercises/09_strings/strings2.rs              |   2 -
 exercises/09_strings/strings3.rs              |   2 -
 exercises/09_strings/strings4.rs              |   2 -
 exercises/10_modules/modules1.rs              |   2 -
 exercises/10_modules/modules2.rs              |   2 -
 exercises/10_modules/modules3.rs              |   2 -
 exercises/11_hashmaps/hashmaps1.rs            |   2 -
 exercises/11_hashmaps/hashmaps2.rs            |   2 -
 exercises/11_hashmaps/hashmaps3.rs            |   2 -
 exercises/12_options/options1.rs              |   2 -
 exercises/12_options/options2.rs              |   2 -
 exercises/12_options/options3.rs              |   2 -
 exercises/13_error_handling/errors1.rs        |   2 -
 exercises/13_error_handling/errors2.rs        |   2 -
 exercises/13_error_handling/errors3.rs        |   2 -
 exercises/13_error_handling/errors4.rs        |   2 -
 exercises/13_error_handling/errors5.rs        |   2 -
 exercises/13_error_handling/errors6.rs        |   2 -
 exercises/14_generics/generics1.rs            |   2 -
 exercises/14_generics/generics2.rs            |   2 -
 exercises/15_traits/traits1.rs                |   2 -
 exercises/15_traits/traits2.rs                |   2 -
 exercises/15_traits/traits3.rs                |   2 -
 exercises/15_traits/traits4.rs                |   2 -
 exercises/15_traits/traits5.rs                |   2 -
 exercises/16_lifetimes/lifetimes1.rs          |   2 -
 exercises/16_lifetimes/lifetimes2.rs          |   2 -
 exercises/16_lifetimes/lifetimes3.rs          |   2 -
 exercises/17_tests/tests1.rs                  |   2 -
 exercises/17_tests/tests2.rs                  |   2 -
 exercises/17_tests/tests3.rs                  |   2 -
 exercises/17_tests/tests4.rs                  |   2 -
 exercises/18_iterators/iterators1.rs          |   2 -
 exercises/18_iterators/iterators2.rs          |   2 -
 exercises/18_iterators/iterators3.rs          |   2 -
 exercises/18_iterators/iterators4.rs          |   2 -
 exercises/18_iterators/iterators5.rs          |   2 -
 exercises/19_smart_pointers/arc1.rs           |   2 -
 exercises/19_smart_pointers/box1.rs           |   2 -
 exercises/19_smart_pointers/cow1.rs           |   2 -
 exercises/19_smart_pointers/rc1.rs            |   2 -
 exercises/20_threads/threads1.rs              |   2 -
 exercises/20_threads/threads2.rs              |   2 -
 exercises/20_threads/threads3.rs              |   2 -
 exercises/21_macros/macros1.rs                |   2 -
 exercises/21_macros/macros2.rs                |   2 -
 exercises/21_macros/macros3.rs                |   2 -
 exercises/21_macros/macros4.rs                |   2 -
 exercises/22_clippy/clippy1.rs                |   2 -
 exercises/22_clippy/clippy2.rs                |   2 -
 exercises/22_clippy/clippy3.rs                |   2 -
 exercises/23_conversions/as_ref_mut.rs        |   2 -
 exercises/23_conversions/from_into.rs         |   2 -
 exercises/23_conversions/from_str.rs          |   2 -
 exercises/23_conversions/try_from_into.rs     |   2 -
 exercises/23_conversions/using_as.rs          |   2 -
 exercises/quiz1.rs                            |   2 -
 exercises/quiz2.rs                            |   2 -
 exercises/quiz3.rs                            |   2 -
 info.toml                                     |   7 +-
 src/app_state.rs                              | 185 ++++++++++++++++
 src/exercise.rs                               | 206 +-----------------
 src/list.rs                                   |  21 +-
 src/list/state.rs                             |  71 +++---
 src/main.rs                                   |  67 ++----
 src/run.rs                                    |  23 +-
 src/state_file.rs                             |  68 ------
 src/verify.rs                                 |  85 --------
 src/watch.rs                                  |  10 +-
 src/watch/state.rs                            |  96 ++------
 .../state/exercises/pending_exercise.rs       |   2 -
 .../state/exercises/pending_test_exercise.rs  |   2 -
 tests/integration_tests.rs                    |  28 +--
 113 files changed, 306 insertions(+), 769 deletions(-)
 create mode 100644 src/app_state.rs
 delete mode 100644 src/state_file.rs
 delete mode 100644 src/verify.rs

diff --git a/Cargo.lock b/Cargo.lock
index ee469437..aeb6c61f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -699,7 +699,6 @@ dependencies = [
  "serde_json",
  "toml_edit",
  "which",
- "winnow",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index da09ba18..435dfd49 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,7 +44,6 @@ serde_json = "1.0.115"
 serde.workspace = true
 toml_edit.workspace = true
 which = "6.0.1"
-winnow = "0.6.5"
 
 [dev-dependencies]
 assert_cmd = "2.0.14"
diff --git a/README.md b/README.md
index 6b9c9833..fd76fdff 100644
--- a/README.md
+++ b/README.md
@@ -101,13 +101,7 @@ The task is simple. Most exercises contain an error that keeps them from compili
 rustlings watch
 ```
 
-This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory. If you want to only run it once, you can use:
-
-```bash
-rustlings verify
-```
-
-This will do the same as watch, but it'll quit after running.
+This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory.
 
 In case you want to go by your own order, or want to only verify a single exercise, you can run:
 
diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs
index 5dd18b45..aa505a13 100644
--- a/exercises/00_intro/intro1.rs
+++ b/exercises/00_intro/intro1.rs
@@ -1,6 +1,6 @@
 // intro1.rs
 //
-// About this `I AM NOT DONE` thing:
+// TODO: Update comment
 // We sometimes encourage you to keep trying things on a given exercise, even
 // after you already figured it out. If you got everything working and feel
 // ready for the next exercise, remove the `I AM NOT DONE` comment below.
@@ -13,8 +13,6 @@
 // Execute `rustlings hint intro1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     println!("Hello and");
     println!(r#"       welcome to...                      "#);
diff --git a/exercises/00_intro/intro2.rs b/exercises/00_intro/intro2.rs
index a28ad3dc..84e0d75c 100644
--- a/exercises/00_intro/intro2.rs
+++ b/exercises/00_intro/intro2.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint intro2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     printline!("Hello there!")
 }
diff --git a/exercises/01_variables/variables1.rs b/exercises/01_variables/variables1.rs
index b3e089a5..56408f35 100644
--- a/exercises/01_variables/variables1.rs
+++ b/exercises/01_variables/variables1.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint variables1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     x = 5;
     println!("x has the value {}", x);
diff --git a/exercises/01_variables/variables2.rs b/exercises/01_variables/variables2.rs
index e1c23edf..0f417e01 100644
--- a/exercises/01_variables/variables2.rs
+++ b/exercises/01_variables/variables2.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint variables2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let x;
     if x == 10 {
diff --git a/exercises/01_variables/variables3.rs b/exercises/01_variables/variables3.rs
index 86bed419..421c6b15 100644
--- a/exercises/01_variables/variables3.rs
+++ b/exercises/01_variables/variables3.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint variables3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let x: i32;
     println!("Number {}", x);
diff --git a/exercises/01_variables/variables4.rs b/exercises/01_variables/variables4.rs
index 5394f394..68f8f50b 100644
--- a/exercises/01_variables/variables4.rs
+++ b/exercises/01_variables/variables4.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint variables4` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let x = 3;
     println!("Number {}", x);
diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs
index a29b38be..7014c568 100644
--- a/exercises/01_variables/variables5.rs
+++ b/exercises/01_variables/variables5.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint variables5` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let number = "T-H-R-E-E"; // don't change this line
     println!("Spell a Number : {}", number);
diff --git a/exercises/01_variables/variables6.rs b/exercises/01_variables/variables6.rs
index 853183ba..9f476825 100644
--- a/exercises/01_variables/variables6.rs
+++ b/exercises/01_variables/variables6.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint variables6` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 const NUMBER = 3;
 fn main() {
     println!("Number {}", NUMBER);
diff --git a/exercises/02_functions/functions1.rs b/exercises/02_functions/functions1.rs
index 40ed9a07..2365f91b 100644
--- a/exercises/02_functions/functions1.rs
+++ b/exercises/02_functions/functions1.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint functions1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     call_me();
 }
diff --git a/exercises/02_functions/functions2.rs b/exercises/02_functions/functions2.rs
index 5154f34d..64dbd665 100644
--- a/exercises/02_functions/functions2.rs
+++ b/exercises/02_functions/functions2.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint functions2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     call_me(3);
 }
diff --git a/exercises/02_functions/functions3.rs b/exercises/02_functions/functions3.rs
index 74f44d6d..50371212 100644
--- a/exercises/02_functions/functions3.rs
+++ b/exercises/02_functions/functions3.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint functions3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     call_me();
 }
diff --git a/exercises/02_functions/functions4.rs b/exercises/02_functions/functions4.rs
index 77c4b2aa..6b449edf 100644
--- a/exercises/02_functions/functions4.rs
+++ b/exercises/02_functions/functions4.rs
@@ -8,8 +8,6 @@
 // Execute `rustlings hint functions4` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let original_price = 51;
     println!("Your sale price is {}", sale_price(original_price));
diff --git a/exercises/02_functions/functions5.rs b/exercises/02_functions/functions5.rs
index f1b63f48..0c963223 100644
--- a/exercises/02_functions/functions5.rs
+++ b/exercises/02_functions/functions5.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint functions5` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let answer = square(3);
     println!("The square of 3 is {}", answer);
diff --git a/exercises/03_if/if1.rs b/exercises/03_if/if1.rs
index d2afccf8..a1df66bb 100644
--- a/exercises/03_if/if1.rs
+++ b/exercises/03_if/if1.rs
@@ -2,8 +2,6 @@
 //
 // Execute `rustlings hint if1` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 pub fn bigger(a: i32, b: i32) -> i32 {
     // Complete this function to return the bigger number!
     // If both numbers are equal, any of them can be returned.
diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs
index f512f13f..7b9c05f6 100644
--- a/exercises/03_if/if2.rs
+++ b/exercises/03_if/if2.rs
@@ -5,8 +5,6 @@
 //
 // Execute `rustlings hint if2` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 pub fn foo_if_fizz(fizzish: &str) -> &str {
     if fizzish == "fizz" {
         "foo"
diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs
index 16962740..caba172b 100644
--- a/exercises/03_if/if3.rs
+++ b/exercises/03_if/if3.rs
@@ -2,8 +2,6 @@
 //
 // Execute `rustlings hint if3` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 pub fn animal_habitat(animal: &str) -> &'static str {
     let identifier = if animal == "crab" {
         1
diff --git a/exercises/04_primitive_types/primitive_types1.rs b/exercises/04_primitive_types/primitive_types1.rs
index 36633400..f9169c84 100644
--- a/exercises/04_primitive_types/primitive_types1.rs
+++ b/exercises/04_primitive_types/primitive_types1.rs
@@ -3,8 +3,6 @@
 // Fill in the rest of the line that has code missing! No hints, there's no
 // tricks, just get used to typing these :)
 
-// I AM NOT DONE
-
 fn main() {
     // Booleans (`bool`)
 
diff --git a/exercises/04_primitive_types/primitive_types2.rs b/exercises/04_primitive_types/primitive_types2.rs
index f1616ed3..1911b12a 100644
--- a/exercises/04_primitive_types/primitive_types2.rs
+++ b/exercises/04_primitive_types/primitive_types2.rs
@@ -3,8 +3,6 @@
 // Fill in the rest of the line that has code missing! No hints, there's no
 // tricks, just get used to typing these :)
 
-// I AM NOT DONE
-
 fn main() {
     // Characters (`char`)
 
diff --git a/exercises/04_primitive_types/primitive_types3.rs b/exercises/04_primitive_types/primitive_types3.rs
index 8b0de44e..70a8cc20 100644
--- a/exercises/04_primitive_types/primitive_types3.rs
+++ b/exercises/04_primitive_types/primitive_types3.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint primitive_types3` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 fn main() {
     let a = ???
 
diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs
index d44d8776..8ed0a82a 100644
--- a/exercises/04_primitive_types/primitive_types4.rs
+++ b/exercises/04_primitive_types/primitive_types4.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint primitive_types4` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 #[test]
 fn slice_out_of_array() {
     let a = [1, 2, 3, 4, 5];
diff --git a/exercises/04_primitive_types/primitive_types5.rs b/exercises/04_primitive_types/primitive_types5.rs
index f646986e..5754a3d8 100644
--- a/exercises/04_primitive_types/primitive_types5.rs
+++ b/exercises/04_primitive_types/primitive_types5.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint primitive_types5` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 fn main() {
     let cat = ("Furry McFurson", 3.5);
     let /* your pattern here */ = cat;
diff --git a/exercises/04_primitive_types/primitive_types6.rs b/exercises/04_primitive_types/primitive_types6.rs
index 07cc46c6..5f82f10f 100644
--- a/exercises/04_primitive_types/primitive_types6.rs
+++ b/exercises/04_primitive_types/primitive_types6.rs
@@ -6,8 +6,6 @@
 // Execute `rustlings hint primitive_types6` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 #[test]
 fn indexing_tuple() {
     let numbers = (1, 2, 3);
diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs
index 65b7a7f8..c64acbbd 100644
--- a/exercises/05_vecs/vecs1.rs
+++ b/exercises/05_vecs/vecs1.rs
@@ -7,8 +7,6 @@
 //
 // Execute `rustlings hint vecs1` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 fn array_and_vec() -> ([i32; 4], Vec<i32>) {
     let a = [10, 20, 30, 40]; // a plain array
     let v = // TODO: declare your vector here with the macro for vectors
diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs
index e92c970a..d64d3d16 100644
--- a/exercises/05_vecs/vecs2.rs
+++ b/exercises/05_vecs/vecs2.rs
@@ -7,8 +7,6 @@
 //
 // Execute `rustlings hint vecs2` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 fn vec_loop(mut v: Vec<i32>) -> Vec<i32> {
     for element in v.iter_mut() {
         // TODO: Fill this up so that each element in the Vec `v` is
diff --git a/exercises/06_move_semantics/move_semantics1.rs b/exercises/06_move_semantics/move_semantics1.rs
index e0639375..c612ba93 100644
--- a/exercises/06_move_semantics/move_semantics1.rs
+++ b/exercises/06_move_semantics/move_semantics1.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint move_semantics1` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 #[test]
 fn main() {
     let vec0 = vec![22, 44, 66];
diff --git a/exercises/06_move_semantics/move_semantics2.rs b/exercises/06_move_semantics/move_semantics2.rs
index dc58be50..3457d111 100644
--- a/exercises/06_move_semantics/move_semantics2.rs
+++ b/exercises/06_move_semantics/move_semantics2.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint move_semantics2` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 #[test]
 fn main() {
     let vec0 = vec![22, 44, 66];
diff --git a/exercises/06_move_semantics/move_semantics3.rs b/exercises/06_move_semantics/move_semantics3.rs
index 7152c716..9415eb15 100644
--- a/exercises/06_move_semantics/move_semantics3.rs
+++ b/exercises/06_move_semantics/move_semantics3.rs
@@ -6,8 +6,6 @@
 // Execute `rustlings hint move_semantics3` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 #[test]
 fn main() {
     let vec0 = vec![22, 44, 66];
diff --git a/exercises/06_move_semantics/move_semantics4.rs b/exercises/06_move_semantics/move_semantics4.rs
index bfc917fa..1509f5d2 100644
--- a/exercises/06_move_semantics/move_semantics4.rs
+++ b/exercises/06_move_semantics/move_semantics4.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint move_semantics4` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 #[test]
 fn main() {
     let vec0 = vec![22, 44, 66];
diff --git a/exercises/06_move_semantics/move_semantics5.rs b/exercises/06_move_semantics/move_semantics5.rs
index 267bdccc..c84d2fea 100644
--- a/exercises/06_move_semantics/move_semantics5.rs
+++ b/exercises/06_move_semantics/move_semantics5.rs
@@ -6,8 +6,6 @@
 // Execute `rustlings hint move_semantics5` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 #[test]
 fn main() {
     let mut x = 100;
diff --git a/exercises/06_move_semantics/move_semantics6.rs b/exercises/06_move_semantics/move_semantics6.rs
index cace4ca6..6059e61c 100644
--- a/exercises/06_move_semantics/move_semantics6.rs
+++ b/exercises/06_move_semantics/move_semantics6.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint move_semantics6` or use the `hint` watch subcommand
 // for a hint.
 
-// I AM NOT DONE
-
 fn main() {
     let data = "Rust is great!".to_string();
 
diff --git a/exercises/07_structs/structs1.rs b/exercises/07_structs/structs1.rs
index 5fa5821c..29781214 100644
--- a/exercises/07_structs/structs1.rs
+++ b/exercises/07_structs/structs1.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint structs1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 struct ColorClassicStruct {
     // TODO: Something goes here
 }
diff --git a/exercises/07_structs/structs2.rs b/exercises/07_structs/structs2.rs
index 328567f0..a7a2deca 100644
--- a/exercises/07_structs/structs2.rs
+++ b/exercises/07_structs/structs2.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint structs2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[derive(Debug)]
 struct Order {
     name: String,
diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs
index 7cda5af1..9835b811 100644
--- a/exercises/07_structs/structs3.rs
+++ b/exercises/07_structs/structs3.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint structs3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[derive(Debug)]
 struct Package {
     sender_country: String,
diff --git a/exercises/08_enums/enums1.rs b/exercises/08_enums/enums1.rs
index 25525b25..330269ca 100644
--- a/exercises/08_enums/enums1.rs
+++ b/exercises/08_enums/enums1.rs
@@ -2,8 +2,6 @@
 //
 // No hints this time! ;)
 
-// I AM NOT DONE
-
 #[derive(Debug)]
 enum Message {
     // TODO: define a few types of messages as used below
diff --git a/exercises/08_enums/enums2.rs b/exercises/08_enums/enums2.rs
index df93fe0f..f0e4e6d3 100644
--- a/exercises/08_enums/enums2.rs
+++ b/exercises/08_enums/enums2.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint enums2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[derive(Debug)]
 enum Message {
     // TODO: define the different variants used below
diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs
index 92d18c46..580a553e 100644
--- a/exercises/08_enums/enums3.rs
+++ b/exercises/08_enums/enums3.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint enums3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 enum Message {
     // TODO: implement the message variant types based on their usage below
 }
diff --git a/exercises/09_strings/strings1.rs b/exercises/09_strings/strings1.rs
index f50e1fa9..a1255a32 100644
--- a/exercises/09_strings/strings1.rs
+++ b/exercises/09_strings/strings1.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint strings1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let answer = current_favorite_color();
     println!("My current favorite color is {}", answer);
diff --git a/exercises/09_strings/strings2.rs b/exercises/09_strings/strings2.rs
index 4d95d16a..ba76fe65 100644
--- a/exercises/09_strings/strings2.rs
+++ b/exercises/09_strings/strings2.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint strings2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let word = String::from("green"); // Try not changing this line :)
     if is_a_color_word(word) {
diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs
index 384e7ce3..dedc081f 100644
--- a/exercises/09_strings/strings3.rs
+++ b/exercises/09_strings/strings3.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint strings3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn trim_me(input: &str) -> String {
     // TODO: Remove whitespace from both ends of a string!
     ???
diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs
index e8c54acc..a034aa49 100644
--- a/exercises/09_strings/strings4.rs
+++ b/exercises/09_strings/strings4.rs
@@ -7,8 +7,6 @@
 //
 // No hints this time!
 
-// I AM NOT DONE
-
 fn string_slice(arg: &str) {
     println!("{}", arg);
 }
diff --git a/exercises/10_modules/modules1.rs b/exercises/10_modules/modules1.rs
index 9eb5a48b..c750946c 100644
--- a/exercises/10_modules/modules1.rs
+++ b/exercises/10_modules/modules1.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint modules1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 mod sausage_factory {
     // Don't let anybody outside of this module see this!
     fn get_secret_recipe() -> String {
diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs
index 04154543..4d3106c4 100644
--- a/exercises/10_modules/modules2.rs
+++ b/exercises/10_modules/modules2.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint modules2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 mod delicious_snacks {
     // TODO: Fix these use statements
     use self::fruits::PEAR as ???
diff --git a/exercises/10_modules/modules3.rs b/exercises/10_modules/modules3.rs
index f2bb0503..c211a769 100644
--- a/exercises/10_modules/modules3.rs
+++ b/exercises/10_modules/modules3.rs
@@ -8,8 +8,6 @@
 // Execute `rustlings hint modules3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 // TODO: Complete this use statement
 use ???
 
diff --git a/exercises/11_hashmaps/hashmaps1.rs b/exercises/11_hashmaps/hashmaps1.rs
index 80829eaa..5a52f611 100644
--- a/exercises/11_hashmaps/hashmaps1.rs
+++ b/exercises/11_hashmaps/hashmaps1.rs
@@ -11,8 +11,6 @@
 // Execute `rustlings hint hashmaps1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::collections::HashMap;
 
 fn fruit_basket() -> HashMap<String, u32> {
diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs
index a5925690..27306439 100644
--- a/exercises/11_hashmaps/hashmaps2.rs
+++ b/exercises/11_hashmaps/hashmaps2.rs
@@ -14,8 +14,6 @@
 // Execute `rustlings hint hashmaps2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::collections::HashMap;
 
 #[derive(Hash, PartialEq, Eq)]
diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs
index 8d9236df..775a4014 100644
--- a/exercises/11_hashmaps/hashmaps3.rs
+++ b/exercises/11_hashmaps/hashmaps3.rs
@@ -15,8 +15,6 @@
 // Execute `rustlings hint hashmaps3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::collections::HashMap;
 
 // A structure to store the goal details of a team.
diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs
index 3cbfecd6..ba4b1cda 100644
--- a/exercises/12_options/options1.rs
+++ b/exercises/12_options/options1.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint options1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 // This function returns how much icecream there is left in the fridge.
 // If it's before 10PM, there's 5 scoops left. At 10PM, someone eats it
 // all, so there'll be no more left :(
diff --git a/exercises/12_options/options2.rs b/exercises/12_options/options2.rs
index 4d998e7d..73f707e3 100644
--- a/exercises/12_options/options2.rs
+++ b/exercises/12_options/options2.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint options2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[cfg(test)]
 mod tests {
     #[test]
diff --git a/exercises/12_options/options3.rs b/exercises/12_options/options3.rs
index 23c15eab..7922ef92 100644
--- a/exercises/12_options/options3.rs
+++ b/exercises/12_options/options3.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint options3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 struct Point {
     x: i32,
     y: i32,
diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs
index 0ba59a57..9767f2c8 100644
--- a/exercises/13_error_handling/errors1.rs
+++ b/exercises/13_error_handling/errors1.rs
@@ -9,8 +9,6 @@
 // Execute `rustlings hint errors1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 pub fn generate_nametag_text(name: String) -> Option<String> {
     if name.is_empty() {
         // Empty names aren't allowed.
diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs
index 631fe67f..88d1bf43 100644
--- a/exercises/13_error_handling/errors2.rs
+++ b/exercises/13_error_handling/errors2.rs
@@ -19,8 +19,6 @@
 // Execute `rustlings hint errors2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::num::ParseIntError;
 
 pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs
index d42d3b17..56bb31b1 100644
--- a/exercises/13_error_handling/errors3.rs
+++ b/exercises/13_error_handling/errors3.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint errors3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::num::ParseIntError;
 
 fn main() {
diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs
index d6d6fcb6..0e5c08bf 100644
--- a/exercises/13_error_handling/errors4.rs
+++ b/exercises/13_error_handling/errors4.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint errors4` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[derive(PartialEq, Debug)]
 struct PositiveNonzeroInteger(u64);
 
diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs
index 92461a7e..0bcb4b8c 100644
--- a/exercises/13_error_handling/errors5.rs
+++ b/exercises/13_error_handling/errors5.rs
@@ -22,8 +22,6 @@
 // Execute `rustlings hint errors5` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::error;
 use std::fmt;
 use std::num::ParseIntError;
diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs
index aaf0948e..de73a9a5 100644
--- a/exercises/13_error_handling/errors6.rs
+++ b/exercises/13_error_handling/errors6.rs
@@ -9,8 +9,6 @@
 // Execute `rustlings hint errors6` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::num::ParseIntError;
 
 // This is a custom error type that we will be using in `parse_pos_nonzero()`.
diff --git a/exercises/14_generics/generics1.rs b/exercises/14_generics/generics1.rs
index 35c1d2fe..545fd95c 100644
--- a/exercises/14_generics/generics1.rs
+++ b/exercises/14_generics/generics1.rs
@@ -6,8 +6,6 @@
 // Execute `rustlings hint generics1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let mut shopping_list: Vec<?> = Vec::new();
     shopping_list.push("milk");
diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs
index 074cd938..d50ed174 100644
--- a/exercises/14_generics/generics2.rs
+++ b/exercises/14_generics/generics2.rs
@@ -6,8 +6,6 @@
 // Execute `rustlings hint generics2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 struct Wrapper {
     value: u32,
 }
diff --git a/exercises/15_traits/traits1.rs b/exercises/15_traits/traits1.rs
index 37dfcbfe..c51d3b88 100644
--- a/exercises/15_traits/traits1.rs
+++ b/exercises/15_traits/traits1.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint traits1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 trait AppendBar {
     fn append_bar(self) -> Self;
 }
diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs
index 3e35f8e1..9a2bc07a 100644
--- a/exercises/15_traits/traits2.rs
+++ b/exercises/15_traits/traits2.rs
@@ -8,8 +8,6 @@
 //
 // Execute `rustlings hint traits2` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 trait AppendBar {
     fn append_bar(self) -> Self;
 }
diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs
index 4e2b06b0..357f1d7f 100644
--- a/exercises/15_traits/traits3.rs
+++ b/exercises/15_traits/traits3.rs
@@ -8,8 +8,6 @@
 // Execute `rustlings hint traits3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 pub trait Licensed {
     fn licensing_info(&self) -> String;
 }
diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs
index 4bda3e57..7242c483 100644
--- a/exercises/15_traits/traits4.rs
+++ b/exercises/15_traits/traits4.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint traits4` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 pub trait Licensed {
     fn licensing_info(&self) -> String {
         "some information".to_string()
diff --git a/exercises/15_traits/traits5.rs b/exercises/15_traits/traits5.rs
index df183805..f258d327 100644
--- a/exercises/15_traits/traits5.rs
+++ b/exercises/15_traits/traits5.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint traits5` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 pub trait SomeTrait {
     fn some_function(&self) -> bool {
         true
diff --git a/exercises/16_lifetimes/lifetimes1.rs b/exercises/16_lifetimes/lifetimes1.rs
index 87bde490..4f544b41 100644
--- a/exercises/16_lifetimes/lifetimes1.rs
+++ b/exercises/16_lifetimes/lifetimes1.rs
@@ -8,8 +8,6 @@
 // Execute `rustlings hint lifetimes1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn longest(x: &str, y: &str) -> &str {
     if x.len() > y.len() {
         x
diff --git a/exercises/16_lifetimes/lifetimes2.rs b/exercises/16_lifetimes/lifetimes2.rs
index 4f3d8c18..33b5565f 100644
--- a/exercises/16_lifetimes/lifetimes2.rs
+++ b/exercises/16_lifetimes/lifetimes2.rs
@@ -6,8 +6,6 @@
 // Execute `rustlings hint lifetimes2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
     if x.len() > y.len() {
         x
diff --git a/exercises/16_lifetimes/lifetimes3.rs b/exercises/16_lifetimes/lifetimes3.rs
index 9c59f9c0..de6005ec 100644
--- a/exercises/16_lifetimes/lifetimes3.rs
+++ b/exercises/16_lifetimes/lifetimes3.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint lifetimes3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 struct Book {
     author: &str,
     title: &str,
diff --git a/exercises/17_tests/tests1.rs b/exercises/17_tests/tests1.rs
index 810277ac..bde21083 100644
--- a/exercises/17_tests/tests1.rs
+++ b/exercises/17_tests/tests1.rs
@@ -10,8 +10,6 @@
 // Execute `rustlings hint tests1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[cfg(test)]
 mod tests {
     #[test]
diff --git a/exercises/17_tests/tests2.rs b/exercises/17_tests/tests2.rs
index f8024e9f..aea5c0e4 100644
--- a/exercises/17_tests/tests2.rs
+++ b/exercises/17_tests/tests2.rs
@@ -6,8 +6,6 @@
 // Execute `rustlings hint tests2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[cfg(test)]
 mod tests {
     #[test]
diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs
index 4013e384..d815e058 100644
--- a/exercises/17_tests/tests3.rs
+++ b/exercises/17_tests/tests3.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint tests3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 pub fn is_even(num: i32) -> bool {
     num % 2 == 0
 }
diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs
index 935d0db1..0972a5b4 100644
--- a/exercises/17_tests/tests4.rs
+++ b/exercises/17_tests/tests4.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint tests4` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 struct Rectangle {
     width: i32,
     height: i32
diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs
index 31076bb9..7ec7da2c 100644
--- a/exercises/18_iterators/iterators1.rs
+++ b/exercises/18_iterators/iterators1.rs
@@ -9,8 +9,6 @@
 // Execute `rustlings hint iterators1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[test]
 fn main() {
     let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"];
diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs
index dda82a08..4ca7742e 100644
--- a/exercises/18_iterators/iterators2.rs
+++ b/exercises/18_iterators/iterators2.rs
@@ -6,8 +6,6 @@
 // Execute `rustlings hint iterators2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 // Step 1.
 // Complete the `capitalize_first` function.
 // "hello" -> "Hello"
diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs
index 29fa23a3..f7da049c 100644
--- a/exercises/18_iterators/iterators3.rs
+++ b/exercises/18_iterators/iterators3.rs
@@ -9,8 +9,6 @@
 // Execute `rustlings hint iterators3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[derive(Debug, PartialEq, Eq)]
 pub enum DivisionError {
     NotDivisible(NotDivisibleError),
diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs
index 3c0724e9..af3958c8 100644
--- a/exercises/18_iterators/iterators4.rs
+++ b/exercises/18_iterators/iterators4.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint iterators4` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 pub fn factorial(num: u64) -> u64 {
     // Complete this function to return the factorial of num
     // Do not use:
diff --git a/exercises/18_iterators/iterators5.rs b/exercises/18_iterators/iterators5.rs
index a062ee4c..ceec5369 100644
--- a/exercises/18_iterators/iterators5.rs
+++ b/exercises/18_iterators/iterators5.rs
@@ -11,8 +11,6 @@
 // Execute `rustlings hint iterators5` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::collections::HashMap;
 
 #[derive(Clone, Copy, PartialEq, Eq)]
diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/arc1.rs
index 3526ddcb..0647eea7 100644
--- a/exercises/19_smart_pointers/arc1.rs
+++ b/exercises/19_smart_pointers/arc1.rs
@@ -21,8 +21,6 @@
 //
 // Execute `rustlings hint arc1` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 #![forbid(unused_imports)] // Do not change this, (or the next) line.
 use std::sync::Arc;
 use std::thread;
diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs
index 513e7daa..2abc0249 100644
--- a/exercises/19_smart_pointers/box1.rs
+++ b/exercises/19_smart_pointers/box1.rs
@@ -18,8 +18,6 @@
 //
 // Execute `rustlings hint box1` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 #[derive(PartialEq, Debug)]
 pub enum List {
     Cons(i32, List),
diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/cow1.rs
index fcd3e0bb..b24591b7 100644
--- a/exercises/19_smart_pointers/cow1.rs
+++ b/exercises/19_smart_pointers/cow1.rs
@@ -12,8 +12,6 @@
 //
 // Execute `rustlings hint cow1` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 use std::borrow::Cow;
 
 fn abs_all<'a, 'b>(input: &'a mut Cow<'b, [i32]>) -> &'a mut Cow<'b, [i32]> {
diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/rc1.rs
index 1b903469..e96e6255 100644
--- a/exercises/19_smart_pointers/rc1.rs
+++ b/exercises/19_smart_pointers/rc1.rs
@@ -10,8 +10,6 @@
 //
 // Execute `rustlings hint rc1` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 use std::rc::Rc;
 
 #[derive(Debug)]
diff --git a/exercises/20_threads/threads1.rs b/exercises/20_threads/threads1.rs
index 80b6def3..be1301d7 100644
--- a/exercises/20_threads/threads1.rs
+++ b/exercises/20_threads/threads1.rs
@@ -8,8 +8,6 @@
 // Execute `rustlings hint threads1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::thread;
 use std::time::{Duration, Instant};
 
diff --git a/exercises/20_threads/threads2.rs b/exercises/20_threads/threads2.rs
index 60d68241..13cb840e 100644
--- a/exercises/20_threads/threads2.rs
+++ b/exercises/20_threads/threads2.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint threads2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::sync::Arc;
 use std::thread;
 use std::time::Duration;
diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs
index acb97b4b..35b914ac 100644
--- a/exercises/20_threads/threads3.rs
+++ b/exercises/20_threads/threads3.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint threads3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::sync::mpsc;
 use std::sync::Arc;
 use std::thread;
diff --git a/exercises/21_macros/macros1.rs b/exercises/21_macros/macros1.rs
index 678de6ee..65986db0 100644
--- a/exercises/21_macros/macros1.rs
+++ b/exercises/21_macros/macros1.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint macros1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 macro_rules! my_macro {
     () => {
         println!("Check out my macro!");
diff --git a/exercises/21_macros/macros2.rs b/exercises/21_macros/macros2.rs
index 788fc16a..b7c37fd9 100644
--- a/exercises/21_macros/macros2.rs
+++ b/exercises/21_macros/macros2.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint macros2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     my_macro!();
 }
diff --git a/exercises/21_macros/macros3.rs b/exercises/21_macros/macros3.rs
index b795c149..92a19227 100644
--- a/exercises/21_macros/macros3.rs
+++ b/exercises/21_macros/macros3.rs
@@ -5,8 +5,6 @@
 // Execute `rustlings hint macros3` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 mod macros {
     macro_rules! my_macro {
         () => {
diff --git a/exercises/21_macros/macros4.rs b/exercises/21_macros/macros4.rs
index 71b45a09..83a6e44f 100644
--- a/exercises/21_macros/macros4.rs
+++ b/exercises/21_macros/macros4.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint macros4` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 #[rustfmt::skip]
 macro_rules! my_macro {
     () => {
diff --git a/exercises/22_clippy/clippy1.rs b/exercises/22_clippy/clippy1.rs
index e0c6ce7c4..1e0f42e2 100644
--- a/exercises/22_clippy/clippy1.rs
+++ b/exercises/22_clippy/clippy1.rs
@@ -9,8 +9,6 @@
 // Execute `rustlings hint clippy1` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 use std::f32;
 
 fn main() {
diff --git a/exercises/22_clippy/clippy2.rs b/exercises/22_clippy/clippy2.rs
index 9b87a0b7..37ac089e 100644
--- a/exercises/22_clippy/clippy2.rs
+++ b/exercises/22_clippy/clippy2.rs
@@ -3,8 +3,6 @@
 // Execute `rustlings hint clippy2` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn main() {
     let mut res = 42;
     let option = Some(12);
diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs
index 5a95f5b8..6a6a36b5 100644
--- a/exercises/22_clippy/clippy3.rs
+++ b/exercises/22_clippy/clippy3.rs
@@ -3,8 +3,6 @@
 // Here's a couple more easy Clippy fixes, so you can see its utility.
 // No hints.
 
-// I AM NOT DONE
-
 #[allow(unused_variables, unused_assignments)]
 fn main() {
     let my_option: Option<()> = None;
diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs
index 2ba9e3f0..cd2c93be 100644
--- a/exercises/23_conversions/as_ref_mut.rs
+++ b/exercises/23_conversions/as_ref_mut.rs
@@ -7,8 +7,6 @@
 // Execute `rustlings hint as_ref_mut` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 // Obtain the number of bytes (not characters) in the given argument.
 // TODO: Add the AsRef trait appropriately as a trait bound.
 fn byte_counter<T>(arg: T) -> usize {
diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/from_into.rs
index 11787c37..d2a1609e 100644
--- a/exercises/23_conversions/from_into.rs
+++ b/exercises/23_conversions/from_into.rs
@@ -41,8 +41,6 @@ impl Default for Person {
 // If while parsing the age, something goes wrong, then return the default of
 // Person Otherwise, then return an instantiated Person object with the results
 
-// I AM NOT DONE
-
 impl From<&str> for Person {
     fn from(s: &str) -> Person {}
 }
diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs
index e2093474..ed91ca5c 100644
--- a/exercises/23_conversions/from_str.rs
+++ b/exercises/23_conversions/from_str.rs
@@ -31,8 +31,6 @@ enum ParsePersonError {
     ParseInt(ParseIntError),
 }
 
-// I AM NOT DONE
-
 // Steps:
 // 1. If the length of the provided string is 0, an error should be returned
 // 2. Split the given string on the commas present in it
diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/try_from_into.rs
index 32d6ef39..23166555 100644
--- a/exercises/23_conversions/try_from_into.rs
+++ b/exercises/23_conversions/try_from_into.rs
@@ -27,8 +27,6 @@ enum IntoColorError {
     IntConversion,
 }
 
-// I AM NOT DONE
-
 // Your task is to complete this implementation and return an Ok result of inner
 // type Color. You need to create an implementation for a tuple of three
 // integers, an array of three integers, and a slice of integers.
diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/using_as.rs
index 414cef3a..9f617ec5 100644
--- a/exercises/23_conversions/using_as.rs
+++ b/exercises/23_conversions/using_as.rs
@@ -10,8 +10,6 @@
 // Execute `rustlings hint using_as` or use the `hint` watch subcommand for a
 // hint.
 
-// I AM NOT DONE
-
 fn average(values: &[f64]) -> f64 {
     let total = values.iter().sum::<f64>();
     total / values.len()
diff --git a/exercises/quiz1.rs b/exercises/quiz1.rs
index 4ee5ada7..b9e71f59 100644
--- a/exercises/quiz1.rs
+++ b/exercises/quiz1.rs
@@ -13,8 +13,6 @@
 //
 // No hints this time ;)
 
-// I AM NOT DONE
-
 // Put your function here!
 // fn calculate_price_of_apples {
 
diff --git a/exercises/quiz2.rs b/exercises/quiz2.rs
index 29925caf..8ace3fe0 100644
--- a/exercises/quiz2.rs
+++ b/exercises/quiz2.rs
@@ -20,8 +20,6 @@
 //
 // No hints this time!
 
-// I AM NOT DONE
-
 pub enum Command {
     Uppercase,
     Trim,
diff --git a/exercises/quiz3.rs b/exercises/quiz3.rs
index 3b01d313..24f70829 100644
--- a/exercises/quiz3.rs
+++ b/exercises/quiz3.rs
@@ -16,8 +16,6 @@
 //
 // Execute `rustlings hint quiz3` or use the `hint` watch subcommand for a hint.
 
-// I AM NOT DONE
-
 pub struct ReportCard {
     pub grade: f32,
     pub student_name: String,
diff --git a/info.toml b/info.toml
index 36629b38..c085e89c 100644
--- a/info.toml
+++ b/info.toml
@@ -4,6 +4,7 @@
 name = "intro1"
 path = "exercises/00_intro/intro1.rs"
 mode = "compile"
+# TODO: Fix hint
 hint = """
 Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file
 to move on to the next exercise."""
@@ -129,11 +130,7 @@ path = "exercises/02_functions/functions3.rs"
 mode = "compile"
 hint = """
 This time, the function *declaration* is okay, but there's something wrong
-with the place where we're calling the function.
-
-As a reminder, you can freely play around with different solutions in Rustlings!
-Watch mode will only jump to the next exercise if you remove the `I AM NOT
-DONE` comment."""
+with the place where we're calling the function."""
 
 [[exercises]]
 name = "functions4"
diff --git a/src/app_state.rs b/src/app_state.rs
new file mode 100644
index 00000000..4a0912e4
--- /dev/null
+++ b/src/app_state.rs
@@ -0,0 +1,185 @@
+use anyhow::{bail, Context, Result};
+use serde::{Deserialize, Serialize};
+use std::fs;
+
+use crate::exercise::Exercise;
+
+const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
+
+#[derive(Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct StateFile {
+    current_exercise_ind: usize,
+    progress: Vec<bool>,
+}
+
+impl StateFile {
+    fn read(exercises: &[Exercise]) -> Option<Self> {
+        let file_content = fs::read(".rustlings-state.json").ok()?;
+
+        let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
+
+        if slf.progress.len() != exercises.len() || slf.current_exercise_ind >= exercises.len() {
+            return None;
+        }
+
+        Some(slf)
+    }
+
+    fn read_or_default(exercises: &[Exercise]) -> Self {
+        Self::read(exercises).unwrap_or_else(|| Self {
+            current_exercise_ind: 0,
+            progress: vec![false; exercises.len()],
+        })
+    }
+
+    fn write(&self) -> Result<()> {
+        let mut buf = Vec::with_capacity(1024);
+        serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?;
+        fs::write(".rustlings-state.json", buf)
+            .context("Failed to write the state file `.rustlings-state.json`")?;
+
+        Ok(())
+    }
+}
+
+pub struct AppState {
+    state_file: StateFile,
+    exercises: &'static [Exercise],
+    n_done: u16,
+    current_exercise: &'static Exercise,
+}
+
+#[must_use]
+pub enum ExercisesProgress {
+    AllDone,
+    Pending,
+}
+
+impl AppState {
+    pub fn new(exercises: Vec<Exercise>) -> Self {
+        // Leaking for sending the exercises to the debounce event handler.
+        // Leaking is not a problem since the exercises' slice is used until the end of the program.
+        let exercises = exercises.leak();
+
+        let state_file = StateFile::read_or_default(exercises);
+        let n_done = state_file
+            .progress
+            .iter()
+            .fold(0, |acc, done| acc + u16::from(*done));
+        let current_exercise = &exercises[state_file.current_exercise_ind];
+
+        Self {
+            state_file,
+            exercises,
+            n_done,
+            current_exercise,
+        }
+    }
+
+    #[inline]
+    pub fn current_exercise_ind(&self) -> usize {
+        self.state_file.current_exercise_ind
+    }
+
+    #[inline]
+    pub fn progress(&self) -> &[bool] {
+        &self.state_file.progress
+    }
+
+    #[inline]
+    pub fn exercises(&self) -> &'static [Exercise] {
+        self.exercises
+    }
+
+    #[inline]
+    pub fn n_done(&self) -> u16 {
+        self.n_done
+    }
+
+    #[inline]
+    pub fn current_exercise(&self) -> &'static Exercise {
+        self.current_exercise
+    }
+
+    pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> {
+        if ind >= self.exercises.len() {
+            bail!(BAD_INDEX_ERR);
+        }
+
+        self.state_file.current_exercise_ind = ind;
+        self.current_exercise = &self.exercises[ind];
+
+        self.state_file.write()
+    }
+
+    pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
+        let (ind, exercise) = self
+            .exercises
+            .iter()
+            .enumerate()
+            .find(|(_, exercise)| exercise.name == name)
+            .with_context(|| format!("No exercise found for '{name}'!"))?;
+
+        self.state_file.current_exercise_ind = ind;
+        self.current_exercise = exercise;
+
+        self.state_file.write()
+    }
+
+    pub fn set_pending(&mut self, ind: usize) -> Result<()> {
+        let done = self
+            .state_file
+            .progress
+            .get_mut(ind)
+            .context(BAD_INDEX_ERR)?;
+
+        if *done {
+            *done = false;
+            self.n_done -= 1;
+            self.state_file.write()?;
+        }
+
+        Ok(())
+    }
+
+    fn next_exercise_ind(&self) -> Option<usize> {
+        let current_ind = self.state_file.current_exercise_ind;
+
+        if current_ind == self.state_file.progress.len() - 1 {
+            // The last exercise is done.
+            // Search for exercises not done from the start.
+            return self.state_file.progress[..current_ind]
+                .iter()
+                .position(|done| !done);
+        }
+
+        // The done exercise isn't the last one.
+        // Search for a pending exercise after the current one and then from the start.
+        match self.state_file.progress[current_ind + 1..]
+            .iter()
+            .position(|done| !done)
+        {
+            Some(ind) => Some(current_ind + 1 + ind),
+            None => self.state_file.progress[..current_ind]
+                .iter()
+                .position(|done| !done),
+        }
+    }
+
+    pub fn done_current_exercise(&mut self) -> Result<ExercisesProgress> {
+        let done = &mut self.state_file.progress[self.state_file.current_exercise_ind];
+        if !*done {
+            *done = true;
+            self.n_done += 1;
+        }
+
+        let Some(ind) = self.next_exercise_ind() else {
+            return Ok(ExercisesProgress::AllDone);
+        };
+
+        self.set_current_exercise_ind(ind)?;
+
+        Ok(ExercisesProgress::Pending)
+    }
+}
diff --git a/src/exercise.rs b/src/exercise.rs
index ca47009d..de435d13 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -1,38 +1,14 @@
 use anyhow::{Context, Result};
 use serde::Deserialize;
 use std::{
-    array,
     fmt::{self, Debug, Display, Formatter},
-    fs::{self, File},
-    io::{self, BufRead, BufReader},
-    mem,
+    fs::{self},
     path::PathBuf,
     process::{Command, Output},
 };
-use winnow::{
-    ascii::{space0, Caseless},
-    combinator::opt,
-    Parser,
-};
 
 use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
 
-// The number of context lines above and below a highlighted line.
-const CONTEXT: usize = 2;
-
-// Check if the line contains the "I AM NOT DONE" comment.
-fn contains_not_done_comment(input: &str) -> bool {
-    (
-        space0::<_, ()>,
-        "//",
-        opt('/'),
-        space0,
-        Caseless("I AM NOT DONE"),
-    )
-        .parse_next(&mut &*input)
-        .is_ok()
-}
-
 // The mode of the exercise.
 #[derive(Deserialize, Copy, Clone)]
 #[serde(rename_all = "lowercase")]
@@ -78,13 +54,6 @@ pub struct Exercise {
     pub hint: String,
 }
 
-// The state of an Exercise.
-#[derive(PartialEq, Eq, Debug)]
-pub enum State {
-    Done,
-    Pending(Vec<ContextLine>),
-}
-
 // The context information of a pending exercise.
 #[derive(PartialEq, Eq, Debug)]
 pub struct ContextLine {
@@ -129,105 +98,6 @@ impl Exercise {
         }
     }
 
-    pub fn state(&self) -> Result<State> {
-        let source_file = File::open(&self.path)
-            .with_context(|| format!("Failed to open the exercise file {}", self.path.display()))?;
-        let mut source_reader = BufReader::new(source_file);
-
-        // Read the next line into `buf` without the newline at the end.
-        let mut read_line = |buf: &mut String| -> io::Result<_> {
-            let n = source_reader.read_line(buf)?;
-            if buf.ends_with('\n') {
-                buf.pop();
-                if buf.ends_with('\r') {
-                    buf.pop();
-                }
-            }
-            Ok(n)
-        };
-
-        let mut current_line_number: usize = 1;
-        // Keep the last `CONTEXT` lines while iterating over the file lines.
-        let mut prev_lines: [_; CONTEXT] = array::from_fn(|_| String::with_capacity(256));
-        let mut line = String::with_capacity(256);
-
-        loop {
-            let n = read_line(&mut line).with_context(|| {
-                format!("Failed to read the exercise file {}", self.path.display())
-            })?;
-
-            // Reached the end of the file and didn't find the comment.
-            if n == 0 {
-                return Ok(State::Done);
-            }
-
-            if contains_not_done_comment(&line) {
-                let mut context = Vec::with_capacity(2 * CONTEXT + 1);
-                // Previous lines.
-                for (ind, prev_line) in prev_lines
-                    .into_iter()
-                    .take(current_line_number - 1)
-                    .enumerate()
-                    .rev()
-                {
-                    context.push(ContextLine {
-                        line: prev_line,
-                        number: current_line_number - 1 - ind,
-                        important: false,
-                    });
-                }
-
-                // Current line.
-                context.push(ContextLine {
-                    line,
-                    number: current_line_number,
-                    important: true,
-                });
-
-                // Next lines.
-                for ind in 0..CONTEXT {
-                    let mut next_line = String::with_capacity(256);
-                    let Ok(n) = read_line(&mut next_line) else {
-                        // If an error occurs, just ignore the next lines.
-                        break;
-                    };
-
-                    // Reached the end of the file.
-                    if n == 0 {
-                        break;
-                    }
-
-                    context.push(ContextLine {
-                        line: next_line,
-                        number: current_line_number + 1 + ind,
-                        important: false,
-                    });
-                }
-
-                return Ok(State::Pending(context));
-            }
-
-            current_line_number += 1;
-            // Add the current line as a previous line and shift the older lines by one.
-            for prev_line in &mut prev_lines {
-                mem::swap(&mut line, prev_line);
-            }
-            // The current line now contains the oldest previous line.
-            // Recycle it for reading the next line.
-            line.clear();
-        }
-    }
-
-    // Check that the exercise looks to be solved using self.state()
-    // This is not the best way to check since
-    // the user can just remove the "I AM NOT DONE" string from the file
-    // without actually having solved anything.
-    // The only other way to truly check this would to compile and run
-    // the exercise; which would be both costly and counterintuitive
-    pub fn looks_done(&self) -> Result<bool> {
-        self.state().map(|state| state == State::Done)
-    }
-
     pub fn reset(&self) -> Result<()> {
         EMBEDDED_FILES
             .write_exercise_to_disk(&self.path, WriteStrategy::Overwrite)
@@ -240,77 +110,3 @@ impl Display for Exercise {
         self.path.fmt(f)
     }
 }
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    #[test]
-    fn test_pending_state() {
-        let exercise = Exercise {
-            name: "pending_exercise".into(),
-            path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"),
-            mode: Mode::Compile,
-            hint: String::new(),
-        };
-
-        let state = exercise.state();
-        let expected = vec![
-            ContextLine {
-                line: "// fake_exercise".to_string(),
-                number: 1,
-                important: false,
-            },
-            ContextLine {
-                line: "".to_string(),
-                number: 2,
-                important: false,
-            },
-            ContextLine {
-                line: "// I AM NOT DONE".to_string(),
-                number: 3,
-                important: true,
-            },
-            ContextLine {
-                line: "".to_string(),
-                number: 4,
-                important: false,
-            },
-            ContextLine {
-                line: "fn main() {".to_string(),
-                number: 5,
-                important: false,
-            },
-        ];
-
-        assert_eq!(state.unwrap(), State::Pending(expected));
-    }
-
-    #[test]
-    fn test_finished_exercise() {
-        let exercise = Exercise {
-            name: "finished_exercise".into(),
-            path: PathBuf::from("tests/fixture/state/exercises/finished_exercise.rs"),
-            mode: Mode::Compile,
-            hint: String::new(),
-        };
-
-        assert_eq!(exercise.state().unwrap(), State::Done);
-    }
-
-    #[test]
-    fn test_not_done() {
-        assert!(contains_not_done_comment("// I AM NOT DONE"));
-        assert!(contains_not_done_comment("/// I AM NOT DONE"));
-        assert!(contains_not_done_comment("//  I AM NOT DONE"));
-        assert!(contains_not_done_comment("///  I AM NOT DONE"));
-        assert!(contains_not_done_comment("// I AM NOT DONE "));
-        assert!(contains_not_done_comment("// I AM NOT DONE!"));
-        assert!(contains_not_done_comment("// I am not done"));
-        assert!(contains_not_done_comment("// i am NOT done"));
-
-        assert!(!contains_not_done_comment("I AM NOT DONE"));
-        assert!(!contains_not_done_comment("// NOT DONE"));
-        assert!(!contains_not_done_comment("DONE"));
-    }
-}
diff --git a/src/list.rs b/src/list.rs
index 560b85a8..80b78e8d 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -9,11 +9,11 @@ use std::{fmt::Write, io};
 
 mod state;
 
-use crate::{exercise::Exercise, state_file::StateFile};
+use crate::app_state::AppState;
 
 use self::state::{Filter, UiState};
 
-pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<()> {
+pub fn list(app_state: &mut AppState) -> Result<()> {
     let mut stdout = io::stdout().lock();
     stdout.execute(EnterAlternateScreen)?;
     enable_raw_mode()?;
@@ -21,7 +21,7 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul
     let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
     terminal.clear()?;
 
-    let mut ui_state = UiState::new(state_file, exercises);
+    let mut ui_state = UiState::new(app_state);
 
     'outer: loop {
         terminal.draw(|frame| ui_state.draw(frame).unwrap())?;
@@ -56,7 +56,7 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul
                     "Enabled filter DONE │ Press d again to disable the filter"
                 };
 
-                ui_state = ui_state.with_updated_rows(state_file);
+                ui_state = ui_state.with_updated_rows();
                 ui_state.message.push_str(message);
             }
             KeyCode::Char('p') => {
@@ -68,23 +68,20 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul
                     "Enabled filter PENDING │ Press p again to disable the filter"
                 };
 
-                ui_state = ui_state.with_updated_rows(state_file);
+                ui_state = ui_state.with_updated_rows();
                 ui_state.message.push_str(message);
             }
             KeyCode::Char('r') => {
-                let selected = ui_state.selected();
-                let exercise = &exercises[selected];
-                exercise.reset()?;
-                state_file.reset(selected)?;
+                let exercise = ui_state.reset_selected()?;
 
-                ui_state = ui_state.with_updated_rows(state_file);
+                ui_state = ui_state.with_updated_rows();
                 ui_state
                     .message
                     .write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
             }
             KeyCode::Char('c') => {
-                state_file.set_next_exercise_ind(ui_state.selected())?;
-                ui_state = ui_state.with_updated_rows(state_file);
+                ui_state.selected_to_current_exercise()?;
+                ui_state = ui_state.with_updated_rows();
             }
             _ => (),
         }
diff --git a/src/list/state.rs b/src/list/state.rs
index 209374b1..7714268c 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -7,7 +7,7 @@ use ratatui::{
     Frame,
 };
 
-use crate::{exercise::Exercise, progress_bar::progress_bar_ratatui, state_file::StateFile};
+use crate::{app_state::AppState, exercise::Exercise, progress_bar::progress_bar_ratatui};
 
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub enum Filter {
@@ -16,30 +16,29 @@ pub enum Filter {
     None,
 }
 
-pub struct UiState {
+pub struct UiState<'a> {
     pub table: Table<'static>,
     pub message: String,
     pub filter: Filter,
-    exercises: &'static [Exercise],
-    progress: u16,
-    selected: usize,
+    app_state: &'a mut AppState,
     table_state: TableState,
+    selected: usize,
     last_ind: usize,
 }
 
-impl UiState {
-    pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self {
+impl<'a> UiState<'a> {
+    pub fn with_updated_rows(mut self) -> Self {
+        let current_exercise_ind = self.app_state.current_exercise_ind();
+
         let mut rows_counter: usize = 0;
-        let mut progress: u16 = 0;
         let rows = self
-            .exercises
+            .app_state
+            .exercises()
             .iter()
-            .zip(state_file.progress().iter().copied())
+            .zip(self.app_state.progress().iter().copied())
             .enumerate()
             .filter_map(|(ind, (exercise, done))| {
                 let exercise_state = if done {
-                    progress += 1;
-
                     if self.filter == Filter::Pending {
                         return None;
                     }
@@ -55,7 +54,7 @@ impl UiState {
 
                 rows_counter += 1;
 
-                let next = if ind == state_file.next_exercise_ind() {
+                let next = if ind == current_exercise_ind {
                     ">>>>".bold().red()
                 } else {
                     Span::default()
@@ -74,15 +73,14 @@ impl UiState {
         self.last_ind = rows_counter.saturating_sub(1);
         self.select(self.selected.min(self.last_ind));
 
-        self.progress = progress;
-
         self
     }
 
-    pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self {
+    pub fn new(app_state: &'a mut AppState) -> Self {
         let header = Row::new(["Next", "State", "Name", "Path"]);
 
-        let max_name_len = exercises
+        let max_name_len = app_state
+            .exercises()
             .iter()
             .map(|exercise| exercise.name.len())
             .max()
@@ -104,7 +102,7 @@ impl UiState {
             .highlight_symbol("šŸ¦€")
             .block(Block::default().borders(Borders::BOTTOM));
 
-        let selected = state_file.next_exercise_ind();
+        let selected = app_state.current_exercise_ind();
         let table_state = TableState::default()
             .with_offset(selected.saturating_sub(10))
             .with_selected(Some(selected));
@@ -113,19 +111,13 @@ impl UiState {
             table,
             message: String::with_capacity(128),
             filter: Filter::None,
-            exercises,
-            progress: 0,
-            selected,
+            app_state,
             table_state,
+            selected,
             last_ind: 0,
         };
 
-        slf.with_updated_rows(state_file)
-    }
-
-    #[inline]
-    pub fn selected(&self) -> usize {
-        self.selected
+        slf.with_updated_rows()
     }
 
     fn select(&mut self, ind: usize) {
@@ -134,11 +126,13 @@ impl UiState {
     }
 
     pub fn select_next(&mut self) {
-        self.select(self.selected.saturating_add(1).min(self.last_ind));
+        let next = (self.selected + 1).min(self.last_ind);
+        self.select(next);
     }
 
     pub fn select_previous(&mut self) {
-        self.select(self.selected.saturating_sub(1));
+        let previous = self.selected.saturating_sub(1);
+        self.select(previous);
     }
 
     #[inline]
@@ -167,8 +161,8 @@ impl UiState {
 
         frame.render_widget(
             Paragraph::new(progress_bar_ratatui(
-                self.progress,
-                self.exercises.len() as u16,
+                self.app_state.n_done(),
+                self.app_state.exercises().len() as u16,
                 area.width,
             )?)
             .block(Block::default().borders(Borders::BOTTOM)),
@@ -200,4 +194,19 @@ impl UiState {
 
         Ok(())
     }
+
+    pub fn reset_selected(&mut self) -> Result<&'static Exercise> {
+        self.app_state.set_pending(self.selected)?;
+        // TODO: Take care of filters!
+        let exercise = &self.app_state.exercises()[self.selected];
+        exercise.reset()?;
+
+        Ok(exercise)
+    }
+
+    #[inline]
+    pub fn selected_to_current_exercise(&mut self) -> Result<()> {
+        // TODO: Take care of filters!
+        self.app_state.set_current_exercise_ind(self.selected)
+    }
 }
diff --git a/src/main.rs b/src/main.rs
index fc83e0fd..926605c1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,8 @@
-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result};
 use clap::{Parser, Subcommand};
 use std::{path::Path, process::exit};
 
+mod app_state;
 mod consts;
 mod embedded;
 mod exercise;
@@ -9,17 +10,15 @@ mod init;
 mod list;
 mod progress_bar;
 mod run;
-mod state_file;
-mod verify;
 mod watch;
 
 use self::{
+    app_state::AppState,
     consts::WELCOME,
-    exercise::{Exercise, InfoFile},
+    exercise::InfoFile,
+    init::init,
     list::list,
     run::run,
-    state_file::StateFile,
-    verify::{verify, VerifyState},
     watch::{watch, WatchExit},
 };
 
@@ -35,14 +34,12 @@ struct Args {
 enum Subcommands {
     /// Initialize Rustlings
     Init,
-    /// Verify all exercises according to the recommended order
-    Verify,
     /// Same as just running `rustlings` without a subcommand.
     Watch,
-    /// Run/Test a single exercise
+    /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified.
     Run {
         /// The name of the exercise
-        name: String,
+        name: Option<String>,
     },
     /// Reset a single exercise
     Reset {
@@ -56,26 +53,6 @@ enum Subcommands {
     },
 }
 
-fn find_exercise(name: &str, exercises: &'static [Exercise]) -> Result<(usize, &'static Exercise)> {
-    if name == "next" {
-        for (ind, exercise) in exercises.iter().enumerate() {
-            if !exercise.looks_done()? {
-                return Ok((ind, exercise));
-            }
-        }
-
-        println!("šŸŽ‰ Congratulations! You have done all the exercises!");
-        println!("šŸ”š There are no more exercises to do next!");
-        exit(0);
-    }
-
-    exercises
-        .iter()
-        .enumerate()
-        .find(|(_, exercise)| exercise.name == name)
-        .with_context(|| format!("No exercise found for '{name}'!"))
-}
-
 fn main() -> Result<()> {
     let args = Args::parse();
 
@@ -87,11 +64,10 @@ Try running `cargo --version` to diagnose the problem.",
 
     let mut info_file = InfoFile::parse()?;
     info_file.exercises.shrink_to_fit();
-    // Leaking is not a problem since the exercises' slice is used until the end of the program.
-    let exercises = info_file.exercises.leak();
+    let exercises = info_file.exercises;
 
     if matches!(args.command, Some(Subcommands::Init)) {
-        init::init(exercises).context("Initialization failed")?;
+        init(&exercises).context("Initialization failed")?;
         println!(
             "\nDone initialization!\n
 Run `cd rustlings` to go into the generated directory.
@@ -109,38 +85,37 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
         exit(1);
     }
 
-    let mut state_file = StateFile::read_or_default(exercises);
+    let mut app_state = AppState::new(exercises);
 
     match args.command {
         None | Some(Subcommands::Watch) => loop {
-            match watch(&mut state_file, exercises)? {
+            match watch(&mut app_state)? {
                 WatchExit::Shutdown => break,
                 // It is much easier to exit the watch mode, launch the list mode and then restart
                 // the watch mode instead of trying to pause the watch threads and correct the
                 // watch state.
-                WatchExit::List => list(&mut state_file, exercises)?,
+                WatchExit::List => list(&mut app_state)?,
             }
         },
         // `Init` is handled above.
         Some(Subcommands::Init) => (),
         Some(Subcommands::Run { name }) => {
-            let (_, exercise) = find_exercise(&name, exercises)?;
-            run(exercise).unwrap_or_else(|_| exit(1));
+            if let Some(name) = name {
+                app_state.set_current_exercise_by_name(&name)?;
+            }
+            run(&mut app_state)?;
         }
         Some(Subcommands::Reset { name }) => {
-            let (ind, exercise) = find_exercise(&name, exercises)?;
+            app_state.set_current_exercise_by_name(&name)?;
+            app_state.set_pending(app_state.current_exercise_ind())?;
+            let exercise = app_state.current_exercise();
             exercise.reset()?;
-            state_file.reset(ind)?;
             println!("The exercise {exercise} has been reset!");
         }
         Some(Subcommands::Hint { name }) => {
-            let (_, exercise) = find_exercise(&name, exercises)?;
-            println!("{}", exercise.hint);
+            app_state.set_current_exercise_by_name(&name)?;
+            println!("{}", app_state.current_exercise().hint);
         }
-        Some(Subcommands::Verify) => match verify(exercises, 0)? {
-            VerifyState::AllExercisesDone => println!("All exercises done!"),
-            VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
-        },
     }
 
     Ok(())
diff --git a/src/run.rs b/src/run.rs
index 2fd6f407..18da193b 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -2,13 +2,10 @@ use anyhow::{bail, Result};
 use crossterm::style::Stylize;
 use std::io::{stdout, Write};
 
-use crate::exercise::Exercise;
+use crate::app_state::{AppState, ExercisesProgress};
 
-// Invoke the rust compiler on the path of the given exercise,
-// and run the ensuing binary.
-// The verbose argument helps determine whether or not to show
-// the output from the test harnesses (if the mode of the exercise is test)
-pub fn run(exercise: &Exercise) -> Result<()> {
+pub fn run(app_state: &mut AppState) -> Result<()> {
+    let exercise = app_state.current_exercise();
     let output = exercise.run()?;
 
     {
@@ -22,7 +19,19 @@ pub fn run(exercise: &Exercise) -> Result<()> {
         bail!("Ran {exercise} with errors");
     }
 
-    println!("{}", "āœ“ Successfully ran {exercise}".green());
+    println!(
+        "{}{}",
+        "āœ“ Successfully ran ".green(),
+        exercise.path.to_string_lossy().green(),
+    );
+
+    match app_state.done_current_exercise()? {
+        ExercisesProgress::AllDone => println!(
+            "šŸŽ‰ Congratulations! You have done all the exercises!
+šŸ”š There are no more exercises to do next!"
+        ),
+        ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()),
+    }
 
     Ok(())
 }
diff --git a/src/state_file.rs b/src/state_file.rs
deleted file mode 100644
index 6b80354e..00000000
--- a/src/state_file.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use anyhow::{bail, Context, Result};
-use serde::{Deserialize, Serialize};
-use std::fs;
-
-use crate::exercise::Exercise;
-
-#[derive(Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct StateFile {
-    next_exercise_ind: usize,
-    progress: Vec<bool>,
-}
-
-const BAD_INDEX_ERR: &str = "The next exercise index is higher than the number of exercises";
-
-impl StateFile {
-    fn read(exercises: &[Exercise]) -> Option<Self> {
-        let file_content = fs::read(".rustlings-state.json").ok()?;
-
-        let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
-
-        if slf.progress.len() != exercises.len() || slf.next_exercise_ind >= exercises.len() {
-            return None;
-        }
-
-        Some(slf)
-    }
-
-    pub fn read_or_default(exercises: &[Exercise]) -> Self {
-        Self::read(exercises).unwrap_or_else(|| Self {
-            next_exercise_ind: 0,
-            progress: vec![false; exercises.len()],
-        })
-    }
-
-    fn write(&self) -> Result<()> {
-        let mut buf = Vec::with_capacity(1024);
-        serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?;
-        fs::write(".rustlings-state.json", buf)
-            .context("Failed to write the state file `.rustlings-state.json`")?;
-
-        Ok(())
-    }
-
-    #[inline]
-    pub fn next_exercise_ind(&self) -> usize {
-        self.next_exercise_ind
-    }
-
-    pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> {
-        if ind >= self.progress.len() {
-            bail!(BAD_INDEX_ERR);
-        }
-        self.next_exercise_ind = ind;
-        self.write()
-    }
-
-    #[inline]
-    pub fn progress(&self) -> &[bool] {
-        &self.progress
-    }
-
-    pub fn reset(&mut self, ind: usize) -> Result<()> {
-        let done = self.progress.get_mut(ind).context(BAD_INDEX_ERR)?;
-        *done = false;
-        self.write()
-    }
-}
diff --git a/src/verify.rs b/src/verify.rs
deleted file mode 100644
index cea6bdf6..00000000
--- a/src/verify.rs
+++ /dev/null
@@ -1,85 +0,0 @@
-use anyhow::Result;
-use crossterm::style::{Attribute, ContentStyle, Stylize};
-use std::io::{stdout, Write};
-
-use crate::exercise::{Exercise, Mode, State};
-
-pub enum VerifyState {
-    AllExercisesDone,
-    Failed(&'static Exercise),
-}
-
-// Verify that the provided container of Exercise objects
-// can be compiled and run without any failures.
-// Any such failures will be reported to the end user.
-// If the Exercise being verified is a test, the verbose boolean
-// determines whether or not the test harness outputs are displayed.
-pub fn verify(
-    exercises: &'static [Exercise],
-    mut current_exercise_ind: usize,
-) -> Result<VerifyState> {
-    while current_exercise_ind < exercises.len() {
-        let exercise = &exercises[current_exercise_ind];
-
-        println!(
-            "Progress: {current_exercise_ind}/{} ({:.1}%)\n",
-            exercises.len(),
-            current_exercise_ind as f32 / exercises.len() as f32 * 100.0,
-        );
-
-        let output = exercise.run()?;
-
-        {
-            let mut stdout = stdout().lock();
-            stdout.write_all(&output.stdout)?;
-            stdout.write_all(&output.stderr)?;
-            stdout.flush()?;
-        }
-
-        if !output.status.success() {
-            return Ok(VerifyState::Failed(exercise));
-        }
-
-        println!();
-        // TODO: Color
-        match exercise.mode {
-            Mode::Compile => println!("Successfully ran {exercise}!"),
-            Mode::Test => println!("Successfully tested {exercise}!"),
-            Mode::Clippy => println!("Successfully checked {exercise}!"),
-        }
-
-        if let State::Pending(context) = exercise.state()? {
-            println!(
-                "\nYou can keep working on this exercise,
-or jump into the next one by removing the {} comment:\n",
-                "`I AM NOT DONE`".bold()
-            );
-
-            for context_line in context {
-                let formatted_line = if context_line.important {
-                    format!("{}", context_line.line.bold())
-                } else {
-                    context_line.line
-                };
-
-                println!(
-                    "{:>2} {}  {}",
-                    ContentStyle {
-                        foreground_color: Some(crossterm::style::Color::Blue),
-                        background_color: None,
-                        underline_color: None,
-                        attributes: Attribute::Bold.into()
-                    }
-                    .apply(context_line.number),
-                    "|".blue(),
-                    formatted_line,
-                );
-            }
-            return Ok(VerifyState::Failed(exercise));
-        }
-
-        current_exercise_ind += 1;
-    }
-
-    Ok(VerifyState::AllExercisesDone)
-}
diff --git a/src/watch.rs b/src/watch.rs
index b29169b3..929275f1 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -15,7 +15,7 @@ mod debounce_event;
 mod state;
 mod terminal_event;
 
-use crate::{exercise::Exercise, state_file::StateFile};
+use crate::app_state::AppState;
 
 use self::{
     debounce_event::DebounceEventHandler,
@@ -39,23 +39,23 @@ pub enum WatchExit {
     List,
 }
 
-pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<WatchExit> {
+pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
     let (tx, rx) = channel();
     let mut debouncer = new_debouncer(
         Duration::from_secs(1),
         DebounceEventHandler {
             tx: tx.clone(),
-            exercises,
+            exercises: app_state.exercises(),
         },
     )?;
     debouncer
         .watcher()
         .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
 
-    let mut watch_state = WatchState::new(state_file, exercises);
+    let mut watch_state = WatchState::new(app_state);
 
     // TODO: bool
-    watch_state.run_exercise()?;
+    watch_state.run_current_exercise()?;
     watch_state.render()?;
 
     thread::spawn(move || terminal_event_handler(tx));
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 6f6d2f10..a7647d8d 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -1,26 +1,16 @@
-use anyhow::{Context, Result};
+use anyhow::Result;
 use crossterm::{
-    style::{Attribute, ContentStyle, Stylize},
+    style::Stylize,
     terminal::{size, Clear, ClearType},
     ExecutableCommand,
 };
-use std::{
-    fmt::Write as _,
-    io::{self, StdoutLock, Write},
-};
+use std::io::{self, StdoutLock, Write};
 
-use crate::{
-    exercise::{Exercise, State},
-    progress_bar::progress_bar,
-    state_file::StateFile,
-};
+use crate::{app_state::AppState, progress_bar::progress_bar};
 
 pub struct WatchState<'a> {
     writer: StdoutLock<'a>,
-    exercises: &'static [Exercise],
-    exercise: &'static Exercise,
-    current_exercise_ind: usize,
-    progress: u16,
+    app_state: &'a mut AppState,
     stdout: Option<Vec<u8>>,
     stderr: Option<Vec<u8>>,
     message: Option<String>,
@@ -28,19 +18,12 @@ pub struct WatchState<'a> {
 }
 
 impl<'a> WatchState<'a> {
-    pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self {
-        let current_exercise_ind = state_file.next_exercise_ind();
-        let progress = state_file.progress().iter().filter(|done| **done).count() as u16;
-        let exercise = &exercises[current_exercise_ind];
-
+    pub fn new(app_state: &'a mut AppState) -> Self {
         let writer = io::stdout().lock();
 
         Self {
             writer,
-            exercises,
-            exercise,
-            current_exercise_ind,
-            progress,
+            app_state,
             stdout: None,
             stderr: None,
             message: None,
@@ -53,8 +36,8 @@ impl<'a> WatchState<'a> {
         self.writer
     }
 
-    pub fn run_exercise(&mut self) -> Result<bool> {
-        let output = self.exercise.run()?;
+    pub fn run_current_exercise(&mut self) -> Result<bool> {
+        let output = self.app_state.current_exercise().run()?;
         self.stdout = Some(output.stdout);
 
         if !output.status.success() {
@@ -64,55 +47,15 @@ impl<'a> WatchState<'a> {
 
         self.stderr = None;
 
-        if let State::Pending(context) = self.exercise.state()? {
-            let mut message = format!(
-                "
-You can keep working on this exercise or jump into the next one by removing the {} comment:
-
-",
-                "`I AM NOT DONE`".bold(),
-            );
-
-            for context_line in context {
-                let formatted_line = if context_line.important {
-                    context_line.line.bold()
-                } else {
-                    context_line.line.stylize()
-                };
-
-                writeln!(
-                    message,
-                    "{:>2} {}  {}",
-                    ContentStyle {
-                        foreground_color: Some(crossterm::style::Color::Blue),
-                        background_color: None,
-                        underline_color: None,
-                        attributes: Attribute::Bold.into()
-                    }
-                    .apply(context_line.number),
-                    "|".blue(),
-                    formatted_line,
-                )?;
-            }
-
-            self.message = Some(message);
-            return Ok(false);
-        }
-
         Ok(true)
     }
 
     pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<bool> {
-        self.exercise = self
-            .exercises
-            .get(exercise_ind)
-            .context("Invalid exercise index")?;
-        self.current_exercise_ind = exercise_ind;
-
-        self.run_exercise()
+        self.app_state.set_current_exercise_ind(exercise_ind)?;
+        self.run_current_exercise()
     }
 
-    pub fn show_prompt(&mut self) -> io::Result<()> {
+    fn show_prompt(&mut self) -> io::Result<()> {
         self.writer.write_all(b"\n\n")?;
 
         if !self.hint_displayed {
@@ -150,18 +93,27 @@ You can keep working on this exercise or jump into the next one by removing the
         if self.hint_displayed {
             self.writer
                 .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?;
-            self.writer.write_all(self.exercise.hint.as_bytes())?;
+            self.writer
+                .write_all(self.app_state.current_exercise().hint.as_bytes())?;
             self.writer.write_all(b"\n\n")?;
         }
 
         let line_width = size()?.0;
-        let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?;
+        let progress_bar = progress_bar(
+            self.app_state.n_done(),
+            self.app_state.exercises().len() as u16,
+            line_width,
+        )?;
         self.writer.write_all(progress_bar.as_bytes())?;
 
         self.writer.write_all(b"Current exercise: ")?;
         self.writer.write_fmt(format_args!(
             "{}",
-            self.exercise.path.to_string_lossy().bold()
+            self.app_state
+                .current_exercise()
+                .path
+                .to_string_lossy()
+                .bold(),
         ))?;
 
         self.show_prompt()?;
diff --git a/tests/fixture/state/exercises/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs
index f579d0b4..016b827c 100644
--- a/tests/fixture/state/exercises/pending_exercise.rs
+++ b/tests/fixture/state/exercises/pending_exercise.rs
@@ -1,7 +1,5 @@
 // fake_exercise
 
-// I AM NOT DONE
-
 fn main() {
 
 }
diff --git a/tests/fixture/state/exercises/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs
index 8756f02d..2002ef17 100644
--- a/tests/fixture/state/exercises/pending_test_exercise.rs
+++ b/tests/fixture/state/exercises/pending_test_exercise.rs
@@ -1,4 +1,2 @@
-// I AM NOT DONE
-
 #[test]
 fn it_works() {}
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index f8f4383f..51cdefb8 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -1,7 +1,6 @@
 use assert_cmd::prelude::*;
-use glob::glob;
 use predicates::boolean::PredicateBooleanExt;
-use std::{fs::File, io::Read, process::Command};
+use std::process::Command;
 
 #[test]
 fn fails_when_in_wrong_dir() {
@@ -137,31 +136,6 @@ fn get_hint_for_single_test() {
         .stdout("Hello!\n");
 }
 
-#[test]
-fn all_exercises_require_confirmation() {
-    for exercise in glob("exercises/**/*.rs").unwrap() {
-        let path = exercise.unwrap();
-        if path.file_name().unwrap() == "mod.rs" {
-            continue;
-        }
-        let source = {
-            let mut file = File::open(&path).unwrap();
-            let mut s = String::new();
-            file.read_to_string(&mut s).unwrap();
-            s
-        };
-        source
-            .matches("// I AM NOT DONE")
-            .next()
-            .unwrap_or_else(|| {
-                panic!(
-                    "There should be an `I AM NOT DONE` annotation in {:?}",
-                    path
-                )
-            });
-    }
-}
-
 #[test]
 fn run_compile_exercise_does_not_prompt() {
     Command::cargo_bin("rustlings")

From 65849629f5877a5d9f51accbb593d431938bd60c Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 02:51:23 +0200
Subject: [PATCH 073/109] Remove glob

---
 Cargo.lock | 7 -------
 Cargo.toml | 1 -
 2 files changed, 8 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index aeb6c61f..a8ffb8ec 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -320,12 +320,6 @@ dependencies = [
  "toml_edit",
 ]
 
-[[package]]
-name = "glob"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
-
 [[package]]
 name = "hashbrown"
 version = "0.14.3"
@@ -690,7 +684,6 @@ dependencies = [
  "assert_cmd",
  "clap",
  "crossterm",
- "glob",
  "notify-debouncer-mini",
  "predicates",
  "ratatui",
diff --git a/Cargo.toml b/Cargo.toml
index 435dfd49..83f01c25 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,7 +47,6 @@ which = "6.0.1"
 
 [dev-dependencies]
 assert_cmd = "2.0.14"
-glob = "0.3.0"
 predicates = "3.1.0"
 
 [profile.release]

From c3933904f643238eaafe42e7da967c8262fef22a Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 02:51:50 +0200
Subject: [PATCH 074/109] Update deps

---
 Cargo.lock                  | 8 ++++----
 Cargo.toml                  | 2 +-
 rustlings-macros/Cargo.toml | 2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index a8ffb8ec..554db288 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -79,9 +79,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.81"
+version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
 
 [[package]]
 name = "assert_cmd"
@@ -598,9 +598,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.35"
+version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
 dependencies = [
  "proc-macro2",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 83f01c25..285e7df6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,7 @@ license = "MIT"
 edition = "2021"
 
 [workspace.dependencies]
-anyhow = "1.0.81"
+anyhow = "1.0.82"
 serde = { version = "1.0.197", features = ["derive"] }
 toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] }
 
diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml
index 0114c8f0..79279f57 100644
--- a/rustlings-macros/Cargo.toml
+++ b/rustlings-macros/Cargo.toml
@@ -9,4 +9,4 @@ edition.workspace = true
 proc-macro = true
 
 [dependencies]
-quote = "1.0.35"
+quote = "1.0.36"

From 686143100fbb89e2a7ba4098134fe37bf0c69ad2 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 02:55:58 +0200
Subject: [PATCH 075/109] Update intro1

---
 exercises/00_intro/intro1.rs | 11 ++---------
 1 file changed, 2 insertions(+), 9 deletions(-)

diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs
index aa505a13..e4e0444a 100644
--- a/exercises/00_intro/intro1.rs
+++ b/exercises/00_intro/intro1.rs
@@ -27,13 +27,6 @@ fn main() {
     println!("or logic error. The central concept behind Rustlings is to fix these errors and");
     println!("solve the exercises. Good luck!");
     println!();
-    println!("The source for this exercise is in `exercises/00_intro/intro1.rs`. Have a look!");
-    println!(
-        "Going forward, the source of the exercises will always be in the success/failure output."
-    );
-    println!();
-    println!(
-        "If you want to use rust-analyzer, Rust's LSP implementation, make sure your editor is set"
-    );
-    println!("up, and then run `rustlings lsp` before continuing.")
+    println!("The file of this exercise is `exercises/00_intro/intro1.rs`. Have a look!");
+    println!("The current exercise path is shown under the progress bar in the watch mode.");
 }

From 470dc65956dae034f17deefbc0b45490e1ec1448 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 14:35:30 +0200
Subject: [PATCH 076/109] Fix selected when there are no rows

---
 src/list.rs       |  4 ++-
 src/list/state.rs | 75 +++++++++++++++++++++++++++++++----------------
 2 files changed, 53 insertions(+), 26 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index 80b78e8d..de120eaf 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -72,7 +72,9 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
                 ui_state.message.push_str(message);
             }
             KeyCode::Char('r') => {
-                let exercise = ui_state.reset_selected()?;
+                let Some(exercise) = ui_state.reset_selected()? else {
+                    continue;
+                };
 
                 ui_state = ui_state.with_updated_rows();
                 ui_state
diff --git a/src/list/state.rs b/src/list/state.rs
index 7714268c..3344fbbd 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -22,15 +22,14 @@ pub struct UiState<'a> {
     pub filter: Filter,
     app_state: &'a mut AppState,
     table_state: TableState,
-    selected: usize,
-    last_ind: usize,
+    n_rows: usize,
 }
 
 impl<'a> UiState<'a> {
     pub fn with_updated_rows(mut self) -> Self {
         let current_exercise_ind = self.app_state.current_exercise_ind();
 
-        let mut rows_counter: usize = 0;
+        self.n_rows = 0;
         let rows = self
             .app_state
             .exercises()
@@ -52,7 +51,7 @@ impl<'a> UiState<'a> {
                     "PENDING".yellow()
                 };
 
-                rows_counter += 1;
+                self.n_rows += 1;
 
                 let next = if ind == current_exercise_ind {
                     ">>>>".bold().red()
@@ -70,8 +69,15 @@ impl<'a> UiState<'a> {
 
         self.table = self.table.rows(rows);
 
-        self.last_ind = rows_counter.saturating_sub(1);
-        self.select(self.selected.min(self.last_ind));
+        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
     }
@@ -107,42 +113,53 @@ impl<'a> UiState<'a> {
             .with_offset(selected.saturating_sub(10))
             .with_selected(Some(selected));
 
+        let filter = Filter::None;
+        let n_rows = app_state.exercises().len();
+
         let slf = Self {
             table,
             message: String::with_capacity(128),
-            filter: Filter::None,
+            filter,
             app_state,
             table_state,
-            selected,
-            last_ind: 0,
+            n_rows,
         };
 
         slf.with_updated_rows()
     }
 
-    fn select(&mut self, ind: usize) {
-        self.selected = ind;
-        self.table_state.select(Some(ind));
-    }
-
     pub fn select_next(&mut self) {
-        let next = (self.selected + 1).min(self.last_ind);
-        self.select(next);
+        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));
+        }
     }
 
     pub fn select_previous(&mut self) {
-        let previous = self.selected.saturating_sub(1);
-        self.select(previous);
+        if self.n_rows > 0 {
+            let previous = self
+                .table_state
+                .selected()
+                .map_or(0, |selected| selected.saturating_sub(1));
+            self.table_state.select(Some(previous));
+        }
     }
 
     #[inline]
     pub fn select_first(&mut self) {
-        self.select(0);
+        if self.n_rows > 0 {
+            self.table_state.select(Some(0));
+        }
     }
 
     #[inline]
     pub fn select_last(&mut self) {
-        self.select(self.last_ind);
+        if self.n_rows > 0 {
+            self.table_state.select(Some(self.n_rows - 1));
+        }
     }
 
     pub fn draw(&mut self, frame: &mut Frame) -> Result<()> {
@@ -195,18 +212,26 @@ impl<'a> UiState<'a> {
         Ok(())
     }
 
-    pub fn reset_selected(&mut self) -> Result<&'static Exercise> {
-        self.app_state.set_pending(self.selected)?;
+    pub fn reset_selected(&mut self) -> Result<Option<&'static Exercise>> {
+        let Some(selected) = self.table_state.selected() else {
+            return Ok(None);
+        };
+
+        self.app_state.set_pending(selected)?;
         // TODO: Take care of filters!
-        let exercise = &self.app_state.exercises()[self.selected];
+        let exercise = &self.app_state.exercises()[selected];
         exercise.reset()?;
 
-        Ok(exercise)
+        Ok(Some(exercise))
     }
 
     #[inline]
     pub fn selected_to_current_exercise(&mut self) -> Result<()> {
+        let Some(selected) = self.table_state.selected() else {
+            return Ok(());
+        };
+
         // TODO: Take care of filters!
-        self.app_state.set_current_exercise_ind(self.selected)
+        self.app_state.set_current_exercise_ind(selected)
     }
 }

From f53a0e870045ac0ff1bb4a3be7fe125680d477a5 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 14:39:19 +0200
Subject: [PATCH 077/109] Panic if there are no exercises

---
 src/exercise.rs | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/src/exercise.rs b/src/exercise.rs
index de435d13..f01c6fcc 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -31,12 +31,21 @@ impl InfoFile {
     pub fn parse() -> Result<Self> {
         // Read a local `info.toml` if it exists.
         // Mainly to let the tests work for now.
-        if let Ok(file_content) = fs::read_to_string("info.toml") {
+        let slf: Self = if let Ok(file_content) = fs::read_to_string("info.toml") {
             toml_edit::de::from_str(&file_content)
         } else {
             toml_edit::de::from_str(include_str!("../info.toml"))
         }
-        .context("Failed to parse `info.toml`")
+        .context("Failed to parse `info.toml`")?;
+
+        if slf.exercises.is_empty() {
+            panic!(
+                "There are no exercises yet!
+If you are developing third-party exercises, add at least one exercise before testing."
+            );
+        }
+
+        Ok(slf)
     }
 }
 

From 2e1a87d7d3671c82932eb63b38ba383ce1fc7d53 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 14:58:56 +0200
Subject: [PATCH 078/109] Take care of filters when resolving the selected
 exercise

---
 src/list/state.rs | 37 ++++++++++++++++++++++++++++++-------
 1 file changed, 30 insertions(+), 7 deletions(-)

diff --git a/src/list/state.rs b/src/list/state.rs
index 3344fbbd..0dcfe88a 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -1,4 +1,4 @@
-use anyhow::Result;
+use anyhow::{Context, Result};
 use ratatui::{
     layout::{Constraint, Rect},
     style::{Style, Stylize},
@@ -217,21 +217,44 @@ impl<'a> UiState<'a> {
             return Ok(None);
         };
 
-        self.app_state.set_pending(selected)?;
-        // TODO: Take care of filters!
-        let exercise = &self.app_state.exercises()[selected];
+        let (ind, exercise) = self
+            .app_state
+            .exercises()
+            .iter()
+            .zip(self.app_state.progress())
+            .enumerate()
+            .filter_map(|(ind, (exercise, done))| match self.filter {
+                Filter::Done => done.then_some((ind, exercise)),
+                Filter::Pending => (!done).then_some((ind, exercise)),
+                Filter::None => Some((ind, exercise)),
+            })
+            .nth(selected)
+            .context("Invalid selection index")?;
+
+        self.app_state.set_pending(ind)?;
         exercise.reset()?;
 
         Ok(Some(exercise))
     }
 
-    #[inline]
     pub fn selected_to_current_exercise(&mut self) -> Result<()> {
         let Some(selected) = self.table_state.selected() else {
             return Ok(());
         };
 
-        // TODO: Take care of filters!
-        self.app_state.set_current_exercise_ind(selected)
+        let ind = self
+            .app_state
+            .progress()
+            .iter()
+            .enumerate()
+            .filter_map(|(ind, done)| match self.filter {
+                Filter::Done => done.then_some(ind),
+                Filter::Pending => (!done).then_some(ind),
+                Filter::None => Some(ind),
+            })
+            .nth(selected)
+            .context("Invalid selection index")?;
+
+        self.app_state.set_current_exercise_ind(ind)
     }
 }

From e79bc727f07bbe99092f30e66f4df845a2cd2ec5 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 15:08:46 +0200
Subject: [PATCH 079/109] Don't listen on keys with modifiers

---
 src/list.rs                 | 16 +++++++++++-----
 src/watch/terminal_event.rs |  6 +++++-
 2 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index de120eaf..2430ed73 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -1,6 +1,6 @@
 use anyhow::Result;
 use crossterm::{
-    event::{self, Event, KeyCode, KeyEventKind},
+    event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
     ExecutableCommand,
 };
@@ -28,10 +28,16 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
 
         let key = loop {
             match event::read()? {
-                Event::Key(key) => match key.kind {
-                    KeyEventKind::Press | KeyEventKind::Repeat => break key,
-                    KeyEventKind::Release => (),
-                },
+                Event::Key(key) => {
+                    if key.modifiers != KeyModifiers::NONE {
+                        continue;
+                    }
+
+                    match key.kind {
+                        KeyEventKind::Press | KeyEventKind::Repeat => break key,
+                        KeyEventKind::Release => (),
+                    }
+                }
                 // Redraw
                 Event::Resize(_, _) => continue 'outer,
                 // Ignore
diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs
index 7c85b5bd..faca8a2b 100644
--- a/src/watch/terminal_event.rs
+++ b/src/watch/terminal_event.rs
@@ -1,4 +1,4 @@
-use crossterm::event::{self, Event, KeyCode, KeyEventKind};
+use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
 use std::sync::mpsc::Sender;
 
 use super::WatchEvent;
@@ -26,6 +26,10 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>) {
 
         match terminal_event {
             Event::Key(key) => {
+                if key.modifiers != KeyModifiers::NONE {
+                    continue;
+                }
+
                 match key.kind {
                     KeyEventKind::Release => continue,
                     KeyEventKind::Press | KeyEventKind::Repeat => (),

From 864cfa725be9dc78b1b962f13c8b6a0bc971d4c4 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 15:10:15 +0200
Subject: [PATCH 080/109] Remove outdated tests

---
 tests/integration_tests.rs | 33 ---------------------------------
 1 file changed, 33 deletions(-)

diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 51cdefb8..f81cc94b 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -11,26 +11,6 @@ fn fails_when_in_wrong_dir() {
         .code(1);
 }
 
-#[test]
-fn verify_all_success() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .arg("verify")
-        .current_dir("tests/fixture/success")
-        .assert()
-        .success();
-}
-
-#[test]
-fn verify_fails_if_some_fails() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .arg("verify")
-        .current_dir("tests/fixture/failure")
-        .assert()
-        .code(1);
-}
-
 #[test]
 fn run_single_compile_success() {
     Command::cargo_bin("rustlings")
@@ -81,19 +61,6 @@ fn run_single_test_not_passed() {
         .code(1);
 }
 
-#[test]
-fn run_single_test_no_filename() {
-    Command::cargo_bin("rustlings")
-        .unwrap()
-        .arg("run")
-        .current_dir("tests/fixture/")
-        .assert()
-        .code(2)
-        .stderr(predicates::str::contains(
-            "required arguments were not provided",
-        ));
-}
-
 #[test]
 fn run_single_test_no_exercise() {
     Command::cargo_bin("rustlings")

From 6494a8c50be2e3b8fbd9bb0ae50d8dfbf0569e2a Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Thu, 11 Apr 2024 16:54:27 +0200
Subject: [PATCH 081/109] Remove the watch subcommand

---
 src/main.rs | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 926605c1..7bc10ac8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -34,8 +34,6 @@ struct Args {
 enum Subcommands {
     /// Initialize Rustlings
     Init,
-    /// Same as just running `rustlings` without a subcommand.
-    Watch,
     /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified.
     Run {
         /// The name of the exercise
@@ -88,7 +86,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
     let mut app_state = AppState::new(exercises);
 
     match args.command {
-        None | Some(Subcommands::Watch) => loop {
+        None => loop {
             match watch(&mut app_state)? {
                 WatchExit::Shutdown => break,
                 // It is much easier to exit the watch mode, launch the list mode and then restart

From d8160f9113ea4f896c0843a40b9444a6e175826f Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 00:56:40 +0200
Subject: [PATCH 082/109] Remove outdated installation methods

---
 .devcontainer/devcontainer.json |  8 ----
 .gitignore                      |  4 --
 .gitpod.yml                     |  7 ---
 .vscode/extensions.json         |  5 ---
 README.md                       | 77 +-------------------------------
 flake.lock                      | 78 ---------------------------------
 flake.nix                       | 78 ---------------------------------
 shell.nix                       |  6 ---
 8 files changed, 1 insertion(+), 262 deletions(-)
 delete mode 100644 .devcontainer/devcontainer.json
 delete mode 100644 .gitpod.yml
 delete mode 100644 .vscode/extensions.json
 delete mode 100644 flake.lock
 delete mode 100644 flake.nix
 delete mode 100644 shell.nix

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
deleted file mode 100644
index f25e8bd8..00000000
--- a/.devcontainer/devcontainer.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "image": "mcr.microsoft.com/devcontainers/rust:1",
-  "updateContentCommand": ["cargo", "build"],
-  "postAttachCommand": ["rustlings", "watch"],
-  "remoteEnv": {
-    "PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/target/debug"
-  }
-}
diff --git a/.gitignore b/.gitignore
index 2d4a04dc..c9172e01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,9 +19,5 @@ public/
 .idea
 *.iml
 
-# VS Code extension recommendations
-.vscode/*
-!.vscode/extensions.json
-
 # Ignore file for editors like Helix
 .ignore
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index 06919335..00000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-tasks:
-  - init: /workspace/rustlings/install.sh
-    command: /workspace/.cargo/bin/rustlings watch
-
-vscode:
-  extensions:
-    - rust-lang.rust-analyzer@0.3.1348
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index b85de749..00000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-    "recommendations": [
-        "rust-lang.rust-analyzer"
-    ]
-}
diff --git a/README.md b/README.md
index fd76fdff..96421ebd 100644
--- a/README.md
+++ b/README.md
@@ -18,78 +18,7 @@ _Note: If you're on Linux, make sure you've installed gcc. Deb: `sudo apt instal
 
 You will need to have Rust installed. You can get it by visiting <https://rustup.rs>. This'll also install Cargo, Rust's package/project manager.
 
-## MacOS/Linux
-
-Just run:
-
-```bash
-curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash
-```
-
-Or if you want it to be installed to a different path:
-
-```bash
-curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash -s mypath/
-```
-
-This will install Rustlings and give you access to the `rustlings` command. Run it to get started!
-
-### Nix
-
-Basically: Clone the repository at the latest tag, finally run `nix develop` or `nix-shell`.
-
-```bash
-# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1)
-git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings
-cd rustlings
-# if nix version > 2.3
-nix develop
-# if nix version <= 2.3
-nix-shell
-```
-
-## Windows
-
-In PowerShell (Run as Administrator), set `ExecutionPolicy` to `RemoteSigned`:
-
-```ps1
-Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
-```
-
-Then, you can run:
-
-```ps1
-Start-BitsTransfer -Source https://raw.githubusercontent.com/rust-lang/rustlings/main/install.ps1 -Destination $env:TMP/install_rustlings.ps1; Unblock-File $env:TMP/install_rustlings.ps1; Invoke-Expression $env:TMP/install_rustlings.ps1
-```
-
-To install Rustlings. Same as on MacOS/Linux, you will have access to the `rustlings` command after it. Keep in mind that this works best in PowerShell, and any other terminals may give you errors.
-
-If you get a permission denied message, you might have to exclude the directory where you cloned Rustlings in your antivirus.
-
-## Browser
-
-[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/rust-lang/rustlings)
-
-[![Open Rustlings On Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/?repo=rust-lang%2Frustlings&ref=main)
-
-## Manually
-
-Basically: Clone the repository at the latest tag, run `cargo install --path .`.
-
-```bash
-# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1)
-git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings
-cd rustlings
-cargo install --force --path .
-```
-
-If there are installation errors, ensure that your toolchain is up to date. For the latest, run:
-
-```bash
-rustup update
-```
-
-Then, same as above, run `rustlings` to get started.
+<!-- TODO: Installation with Cargo -->
 
 ## Doing exercises
 
@@ -138,10 +67,6 @@ rustlings list
 
 After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. These quizzes are found in `exercises/quizN.rs`.
 
-## Enabling `rust-analyzer`
-
-Run the command `rustlings lsp` which will generate a `rust-project.json` at the root of the project, this allows [rust-analyzer](https://rust-analyzer.github.io/) to parse each exercise.
-
 ## 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.
diff --git a/flake.lock b/flake.lock
deleted file mode 100644
index 15238981..00000000
--- a/flake.lock
+++ /dev/null
@@ -1,78 +0,0 @@
-{
-  "nodes": {
-    "flake-compat": {
-      "flake": false,
-      "locked": {
-        "lastModified": 1673956053,
-        "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
-        "owner": "edolstra",
-        "repo": "flake-compat",
-        "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
-        "type": "github"
-      },
-      "original": {
-        "owner": "edolstra",
-        "repo": "flake-compat",
-        "type": "github"
-      }
-    },
-    "flake-utils": {
-      "inputs": {
-        "systems": "systems"
-      },
-      "locked": {
-        "lastModified": 1692799911,
-        "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
-        "type": "github"
-      },
-      "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "type": "github"
-      }
-    },
-    "nixpkgs": {
-      "locked": {
-        "lastModified": 1694183432,
-        "narHash": "sha256-YyPGNapgZNNj51ylQMw9lAgvxtM2ai1HZVUu3GS8Fng=",
-        "owner": "nixos",
-        "repo": "nixpkgs",
-        "rev": "db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nixos",
-        "ref": "nixos-unstable",
-        "repo": "nixpkgs",
-        "type": "github"
-      }
-    },
-    "root": {
-      "inputs": {
-        "flake-compat": "flake-compat",
-        "flake-utils": "flake-utils",
-        "nixpkgs": "nixpkgs"
-      }
-    },
-    "systems": {
-      "locked": {
-        "lastModified": 1681028828,
-        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
-        "owner": "nix-systems",
-        "repo": "default",
-        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-systems",
-        "repo": "default",
-        "type": "github"
-      }
-    }
-  },
-  "root": "root",
-  "version": 7
-}
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index 152d38e6..00000000
--- a/flake.nix
+++ /dev/null
@@ -1,78 +0,0 @@
-{
-  description = "Small exercises to get you used to reading and writing Rust code";
-
-  inputs = {
-    flake-compat = {
-      url = "github:edolstra/flake-compat";
-      flake = false;
-    };
-    flake-utils.url = "github:numtide/flake-utils";
-    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
-  };
-
-  outputs = { self, flake-utils, nixpkgs, ... }:
-    flake-utils.lib.eachDefaultSystem (system:
-      let
-        pkgs = nixpkgs.legacyPackages.${system};
-
-        cargoBuildInputs = with pkgs; lib.optionals stdenv.isDarwin [
-          darwin.apple_sdk.frameworks.CoreServices
-        ];
-
-        rustlings =
-          pkgs.rustPlatform.buildRustPackage {
-            name = "rustlings";
-            version = "5.6.1";
-
-            buildInputs = cargoBuildInputs;
-            nativeBuildInputs = [pkgs.git];
-
-            src = with pkgs.lib; cleanSourceWith {
-              src = self;
-              # a function that returns a bool determining if the path should be included in the cleaned source
-              filter = path: type:
-                let
-                  # filename
-                  baseName = builtins.baseNameOf (toString path);
-                  # path from root directory
-                  path' = builtins.replaceStrings [ "${self}/" ] [ "" ] path;
-                  # checks if path is in the directory
-                  inDirectory = directory: hasPrefix directory path';
-                in
-                inDirectory "src" ||
-                inDirectory "tests" ||
-                hasPrefix "Cargo" baseName ||
-                baseName == "info.toml";
-            };
-
-            cargoLock.lockFile = ./Cargo.lock;
-          };
-      in
-      {
-        devShell = pkgs.mkShell {
-          RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
-
-          buildInputs = with pkgs; [
-            cargo
-            rustc
-            rust-analyzer
-            rustlings
-            rustfmt
-            clippy
-          ] ++ cargoBuildInputs;
-        };
-        apps = let
-          rustlings-app = {
-            type = "app";
-            program = "${rustlings}/bin/rustlings";
-          };
-        in {
-          default = rustlings-app;
-          rustlings = rustlings-app;
-        };
-        packages = {
-          inherit rustlings;
-          default = rustlings;
-        };
-      });
-}
diff --git a/shell.nix b/shell.nix
deleted file mode 100644
index fa2a56c7..00000000
--- a/shell.nix
+++ /dev/null
@@ -1,6 +0,0 @@
-(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
-in fetchTarball {
-  url =
-    "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
-  sha256 = lock.nodes.flake-compat.locked.narHash;
-}) { src = ./.; }).shellNix

From 1e3745ccdf5ca41ae47d4f4d8594e8070df200a5 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 00:58:26 +0200
Subject: [PATCH 083/109] Update winnow

---
 Cargo.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 554db288..a5ad8c9d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1130,9 +1130,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
 
 [[package]]
 name = "winnow"
-version = "0.6.5"
+version = "0.6.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
+checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
 dependencies = [
  "memchr",
 ]

From 2a95a3e96644a0f769019204a518816c9f2e2aee Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 01:24:01 +0200
Subject: [PATCH 084/109] Deal with long strings

---
 info.toml       | 32 ++++++++++++++++++++++++
 src/consts.rs   | 59 --------------------------------------------
 src/exercise.rs | 12 ++++++---
 src/init.rs     | 40 ++++++++++++++++--------------
 src/main.rs     | 65 ++++++++++++++++++++++++++++++++++---------------
 src/watch.rs    | 10 +++++---
 6 files changed, 114 insertions(+), 104 deletions(-)
 delete mode 100644 src/consts.rs

diff --git a/info.toml b/info.toml
index c085e89c..d35b5702 100644
--- a/info.toml
+++ b/info.toml
@@ -1,3 +1,35 @@
+welcome_message = """Is this your first time? Don't worry, Rustlings was made for beginners! We are
+going to teach you a lot of things about Rust, but before we can get
+started, here's a couple of notes about how Rustlings operates:
+
+1. The central concept behind Rustlings is that you solve exercises. These
+   exercises usually have some sort of syntax error in them, which will cause
+   them to fail compilation or testing. Sometimes there's a logic error instead
+   of a syntax error. No matter what error, it's your job to find it and fix it!
+   You'll know when you fixed it because then, the exercise will compile and
+   Rustlings will be able to move on to the next exercise.
+2. If you run Rustlings in watch mode (which we recommend), it'll automatically
+   start with the first exercise. Don't get confused by an error message popping
+   up as soon as you run Rustlings! This is part of the exercise that you're
+   supposed to solve, so open the exercise file in an editor and start your
+   detective work!
+3. If you're stuck on an exercise, there is a helpful hint you can view by typing
+   'hint' (in watch mode), or running `rustlings hint exercise_name`.
+4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
+   (https://github.com/rust-lang/rustlings/issues/new). We look at every issue,
+   and sometimes, other learners do too so you can help each other out!
+
+Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise.
+Make sure to have your editor open in the `rustlings` directory!"""
+
+final_message = """We hope you enjoyed learning about the various aspects of Rust!
+If you noticed any issues, please don't hesitate to report them to our repo.
+You can also contribute your own exercises to help the greater community!
+
+Before reporting an issue or contributing, please read our guidelines:
+https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md
+"""
+
 # INTRO
 
 [[exercises]]
diff --git a/src/consts.rs b/src/consts.rs
deleted file mode 100644
index 40bf150f..00000000
--- a/src/consts.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-pub const WELCOME: &str = r"       welcome to...
-                 _   _ _
-  _ __ _   _ ___| |_| (_)_ __   __ _ ___
- | '__| | | / __| __| | | '_ \ / _` / __|
- | |  | |_| \__ \ |_| | | | | | (_| \__ \
- |_|   \__,_|___/\__|_|_|_| |_|\__, |___/
-                               |___/";
-
-pub const DEFAULT_OUT: &str =
-    "Is this your first time? Don't worry, Rustlings was made for beginners! We are
-going to teach you a lot of things about Rust, but before we can get
-started, here's a couple of notes about how Rustlings operates:
-
-1. The central concept behind Rustlings is that you solve exercises. These
-   exercises usually have some sort of syntax error in them, which will cause
-   them to fail compilation or testing. Sometimes there's a logic error instead
-   of a syntax error. No matter what error, it's your job to find it and fix it!
-   You'll know when you fixed it because then, the exercise will compile and
-   Rustlings will be able to move on to the next exercise.
-2. If you run Rustlings in watch mode (which we recommend), it'll automatically
-   start with the first exercise. Don't get confused by an error message popping
-   up as soon as you run Rustlings! This is part of the exercise that you're
-   supposed to solve, so open the exercise file in an editor and start your
-   detective work!
-3. If you're stuck on an exercise, there is a helpful hint you can view by typing
-   'hint' (in watch mode), or running `rustlings hint exercise_name`.
-4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
-   (https://github.com/rust-lang/rustlings/issues/new). We look at every issue,
-   and sometimes, other learners do too so you can help each other out!
-
-Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise.
-Make sure to have your editor open in the `rustlings` directory!";
-
-pub const FENISH_LINE: &str = "+----------------------------------------------------+
-|          You made it to the Fe-nish line!          |
-+--------------------------  ------------------------+
-                           \\/\x1b[31m
-     ā–’ā–’          ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’      ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’          ā–’ā–’
-   ā–’ā–’ā–’ā–’  ā–’ā–’    ā–’ā–’        ā–’ā–’  ā–’ā–’        ā–’ā–’    ā–’ā–’  ā–’ā–’ā–’ā–’
-   ā–’ā–’ā–’ā–’  ā–’ā–’  ā–’ā–’            ā–’ā–’            ā–’ā–’  ā–’ā–’  ā–’ā–’ā–’ā–’
- ā–‘ā–‘ā–’ā–’ā–’ā–’ā–‘ā–‘ā–’ā–’  ā–’ā–’            ā–’ā–’            ā–’ā–’  ā–’ā–’ā–‘ā–‘ā–’ā–’ā–’ā–’
-   ā–“ā–“ā–“ā–“ā–“ā–“ā–“ā–“  ā–“ā–“      ā–“ā–“ā–ˆā–ˆ  ā–“ā–“  ā–“ā–“ā–ˆā–ˆ      ā–“ā–“  ā–“ā–“ā–“ā–“ā–“ā–“ā–“ā–“
-     ā–’ā–’ā–’ā–’    ā–’ā–’      ā–ˆā–ˆā–ˆā–ˆ  ā–’ā–’  ā–ˆā–ˆā–ˆā–ˆ      ā–’ā–’ā–‘ā–‘  ā–’ā–’ā–’ā–’
-       ā–’ā–’  ā–’ā–’ā–’ā–’ā–’ā–’        ā–’ā–’ā–’ā–’ā–’ā–’        ā–’ā–’ā–’ā–’ā–’ā–’  ā–’ā–’
-         ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–“ā–“ā–“ā–“ā–“ā–“ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–“ā–“ā–’ā–’ā–“ā–“ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
-           ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
-             ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
-           ā–’ā–’  ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’  ā–’ā–’
-         ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’
-       ā–’ā–’    ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’    ā–’ā–’
-       ā–’ā–’  ā–’ā–’    ā–’ā–’                  ā–’ā–’    ā–’ā–’  ā–’ā–’
-           ā–’ā–’  ā–’ā–’                      ā–’ā–’  ā–’ā–’\x1b[0m
-
-We hope you enjoyed learning about the various aspects of Rust!
-If you noticed any issues, please don't hesitate to report them to our repo.
-You can also contribute your own exercises to help the greater community!
-
-Before reporting an issue or contributing, please read our guidelines:
-https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md";
diff --git a/src/exercise.rs b/src/exercise.rs
index f01c6fcc..d28f4dbe 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -24,6 +24,10 @@ pub enum Mode {
 #[derive(Deserialize)]
 #[serde(deny_unknown_fields)]
 pub struct InfoFile {
+    // TODO
+    pub welcome_message: Option<String>,
+    // TODO
+    pub final_message: Option<String>,
     pub exercises: Vec<Exercise>,
 }
 
@@ -39,10 +43,7 @@ impl InfoFile {
         .context("Failed to parse `info.toml`")?;
 
         if slf.exercises.is_empty() {
-            panic!(
-                "There are no exercises yet!
-If you are developing third-party exercises, add at least one exercise before testing."
-            );
+            panic!("{NO_EXERCISES_ERR}");
         }
 
         Ok(slf)
@@ -119,3 +120,6 @@ impl Display for Exercise {
         self.path.fmt(f)
     }
 }
+
+const NO_EXERCISES_ERR: &str = "There are no exercises yet!
+If you are developing third-party exercises, add at least one exercise before testing.";
diff --git a/src/init.rs b/src/init.rs
index bc561eaf..44747438 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -36,47 +36,33 @@ publish = false
 }
 
 fn create_gitignore() -> io::Result<()> {
-    let gitignore = b"/target
-/.rustlings-state.json";
     OpenOptions::new()
         .create_new(true)
         .write(true)
         .open(".gitignore")?
-        .write_all(gitignore)
+        .write_all(GITIGNORE)
 }
 
 fn create_vscode_dir() -> Result<()> {
     create_dir(".vscode").context("Failed to create the directory `.vscode`")?;
-    let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
     OpenOptions::new()
         .create_new(true)
         .write(true)
         .open(".vscode/extensions.json")?
-        .write_all(vs_code_extensions_json)?;
+        .write_all(VS_CODE_EXTENSIONS_JSON)?;
 
     Ok(())
 }
 
 pub fn init(exercises: &[Exercise]) -> Result<()> {
     if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
-        bail!(
-            "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist
-in the current directory. It looks like Rustlings was already initialized here.
-Run `rustlings` for instructions on getting started with the exercises.
-
-If you didn't already initialize Rustlings, please initialize it in another directory."
-        );
+        bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
     }
 
     let rustlings_path = Path::new("rustlings");
     if let Err(e) = create_dir(rustlings_path) {
         if e.kind() == ErrorKind::AlreadyExists {
-            bail!(
-                "A directory with the name `rustlings` already exists in the current directory.
-You probably already initialized Rustlings.
-Run `cd rustlings`
-Then run `rustlings` again"
-            );
+            bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
         }
         return Err(e.into());
     }
@@ -96,3 +82,21 @@ Then run `rustlings` again"
 
     Ok(())
 }
+
+const GITIGNORE: &[u8] = b"/target
+/.rustlings-state.json";
+
+const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
+
+const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str =
+    "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist
+in the current directory. It looks like Rustlings was already initialized here.
+Run `rustlings` for instructions on getting started with the exercises.
+
+If you didn't already initialize Rustlings, please initialize it in another directory.";
+
+const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
+    "A directory with the name `rustlings` already exists in the current directory.
+You probably already initialized Rustlings.
+Run `cd rustlings`
+Then run `rustlings` again";
diff --git a/src/main.rs b/src/main.rs
index 7bc10ac8..fdbb710c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,7 +3,6 @@ use clap::{Parser, Subcommand};
 use std::{path::Path, process::exit};
 
 mod app_state;
-mod consts;
 mod embedded;
 mod exercise;
 mod init;
@@ -14,7 +13,6 @@ mod watch;
 
 use self::{
     app_state::AppState,
-    consts::WELCOME,
     exercise::InfoFile,
     init::init,
     list::list,
@@ -54,11 +52,7 @@ enum Subcommands {
 fn main() -> Result<()> {
     let args = Args::parse();
 
-    which::which("cargo").context(
-        "Failed to find `cargo`.
-Did you already install Rust?
-Try running `cargo --version` to diagnose the problem.",
-    )?;
+    which::which("cargo").context(CARGO_NOT_FOUND_ERR)?;
 
     let mut info_file = InfoFile::parse()?;
     info_file.exercises.shrink_to_fit();
@@ -66,20 +60,11 @@ Try running `cargo --version` to diagnose the problem.",
 
     if matches!(args.command, Some(Subcommands::Init)) {
         init(&exercises).context("Initialization failed")?;
-        println!(
-            "\nDone initialization!\n
-Run `cd rustlings` to go into the generated directory.
-Then run `rustlings` for further instructions on getting started."
-        );
+
+        println!("{POST_INIT_MSG}");
         return Ok(());
     } else if !Path::new("exercises").is_dir() {
-        println!(
-            "
-{WELCOME}
-
-The `exercises` directory wasn't found in the current directory.
-If you are just starting with Rustlings, run the command `rustlings init` to initialize it."
-        );
+        println!("{PRE_INIT_MSG}");
         exit(1);
     }
 
@@ -118,3 +103,45 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
 
     Ok(())
 }
+
+const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`.
+Did you already install Rust?
+Try running `cargo --version` to diagnose the problem.";
+
+const PRE_INIT_MSG: &str = r"
+       welcome to...
+                 _   _ _
+  _ __ _   _ ___| |_| (_)_ __   __ _ ___
+ | '__| | | / __| __| | | '_ \ / _` / __|
+ | |  | |_| \__ \ |_| | | | | | (_| \__ \
+ |_|   \__,_|___/\__|_|_|_| |_|\__, |___/
+                               |___/
+
+The `exercises` directory wasn't found in the current directory.
+If you are just starting with Rustlings, run the command `rustlings init` to initialize it.";
+
+const POST_INIT_MSG: &str = "
+Done initialization!
+
+Run `cd rustlings` to go into the generated directory.
+Then run `rustlings` for further instructions on getting started.";
+
+const FENISH_LINE: &str = "+----------------------------------------------------+
+|          You made it to the Fe-nish line!          |
++--------------------------  ------------------------+
+                           \\/\x1b[31m
+     ā–’ā–’          ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’      ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’          ā–’ā–’
+   ā–’ā–’ā–’ā–’  ā–’ā–’    ā–’ā–’        ā–’ā–’  ā–’ā–’        ā–’ā–’    ā–’ā–’  ā–’ā–’ā–’ā–’
+   ā–’ā–’ā–’ā–’  ā–’ā–’  ā–’ā–’            ā–’ā–’            ā–’ā–’  ā–’ā–’  ā–’ā–’ā–’ā–’
+ ā–‘ā–‘ā–’ā–’ā–’ā–’ā–‘ā–‘ā–’ā–’  ā–’ā–’            ā–’ā–’            ā–’ā–’  ā–’ā–’ā–‘ā–‘ā–’ā–’ā–’ā–’
+   ā–“ā–“ā–“ā–“ā–“ā–“ā–“ā–“  ā–“ā–“      ā–“ā–“ā–ˆā–ˆ  ā–“ā–“  ā–“ā–“ā–ˆā–ˆ      ā–“ā–“  ā–“ā–“ā–“ā–“ā–“ā–“ā–“ā–“
+     ā–’ā–’ā–’ā–’    ā–’ā–’      ā–ˆā–ˆā–ˆā–ˆ  ā–’ā–’  ā–ˆā–ˆā–ˆā–ˆ      ā–’ā–’ā–‘ā–‘  ā–’ā–’ā–’ā–’
+       ā–’ā–’  ā–’ā–’ā–’ā–’ā–’ā–’        ā–’ā–’ā–’ā–’ā–’ā–’        ā–’ā–’ā–’ā–’ā–’ā–’  ā–’ā–’
+         ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–“ā–“ā–“ā–“ā–“ā–“ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–“ā–“ā–“ā–“ā–“ā–“ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
+           ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
+             ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’
+           ā–’ā–’  ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’  ā–’ā–’
+         ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’
+       ā–’ā–’    ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’    ā–’ā–’
+       ā–’ā–’  ā–’ā–’    ā–’ā–’                  ā–’ā–’    ā–’ā–’  ā–’ā–’
+           ā–’ā–’  ā–’ā–’                      ā–’ā–’  ā–’ā–’\x1b[0m";
diff --git a/src/watch.rs b/src/watch.rs
index 929275f1..bfa0f88a 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -89,10 +89,12 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
         }
     }
 
-    watch_state.into_writer().write_all(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.
-")?;
+    watch_state.into_writer().write_all(QUIT_MSG)?;
 
     Ok(WatchExit::Shutdown)
 }
+
+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 6807e63c5f26ee01b60460355ce2c5411c603f16 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 02:45:54 +0200
Subject: [PATCH 085/109] Show done message

---
 src/watch.rs       |  4 ----
 src/watch/state.rs | 52 +++++++++++++++++++++++++++-------------------
 2 files changed, 31 insertions(+), 25 deletions(-)

diff --git a/src/watch.rs b/src/watch.rs
index bfa0f88a..928fc5fb 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -54,9 +54,7 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
 
     let mut watch_state = WatchState::new(app_state);
 
-    // TODO: bool
     watch_state.run_current_exercise()?;
-    watch_state.render()?;
 
     thread::spawn(move || terminal_event_handler(tx));
 
@@ -76,9 +74,7 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
                 watch_state.handle_invalid_cmd(&cmd)?;
             }
             WatchEvent::FileChange { exercise_ind } => {
-                // TODO: bool
                 watch_state.run_exercise_with_ind(exercise_ind)?;
-                watch_state.render()?;
             }
             WatchEvent::NotifyErr(e) => {
                 return Err(Error::from(e).context("Exercise file watcher failed"))
diff --git a/src/watch/state.rs b/src/watch/state.rs
index a7647d8d..5a5c0ca1 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -13,8 +13,8 @@ pub struct WatchState<'a> {
     app_state: &'a mut AppState,
     stdout: Option<Vec<u8>>,
     stderr: Option<Vec<u8>>,
-    message: Option<String>,
-    hint_displayed: bool,
+    show_hint: bool,
+    show_done: bool,
 }
 
 impl<'a> WatchState<'a> {
@@ -26,8 +26,8 @@ impl<'a> WatchState<'a> {
             app_state,
             stdout: None,
             stderr: None,
-            message: None,
-            hint_displayed: false,
+            show_hint: false,
+            show_done: false,
         }
     }
 
@@ -36,29 +36,32 @@ impl<'a> WatchState<'a> {
         self.writer
     }
 
-    pub fn run_current_exercise(&mut self) -> Result<bool> {
+    pub fn run_current_exercise(&mut self) -> Result<()> {
+        self.show_hint = false;
+
         let output = self.app_state.current_exercise().run()?;
         self.stdout = Some(output.stdout);
 
-        if !output.status.success() {
+        if output.status.success() {
+            self.stderr = None;
+            self.show_done = true;
+        } else {
             self.stderr = Some(output.stderr);
-            return Ok(false);
+            self.show_done = false;
         }
 
-        self.stderr = None;
-
-        Ok(true)
+        self.render()
     }
 
-    pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<bool> {
+    pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> {
         self.app_state.set_current_exercise_ind(exercise_ind)?;
         self.run_current_exercise()
     }
 
     fn show_prompt(&mut self) -> io::Result<()> {
-        self.writer.write_all(b"\n\n")?;
+        self.writer.write_all(b"\n")?;
 
-        if !self.hint_displayed {
+        if !self.show_hint {
             self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?;
         }
 
@@ -84,20 +87,26 @@ impl<'a> WatchState<'a> {
             self.writer.write_all(b"\n")?;
         }
 
-        if let Some(message) = &self.message {
-            self.writer.write_all(message.as_bytes())?;
-        }
-
         self.writer.write_all(b"\n")?;
 
-        if self.hint_displayed {
+        if self.show_hint {
             self.writer
-                .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?;
+                .write_fmt(format_args!("{}\n", "Hint".bold().cyan().underlined()))?;
             self.writer
                 .write_all(self.app_state.current_exercise().hint.as_bytes())?;
             self.writer.write_all(b"\n\n")?;
         }
 
+        if self.show_done {
+            self.writer.write_fmt(format_args!(
+                "{}\n\n",
+                "Exercise done āœ“
+When you are done experimenting, enter `n` or `next` to go to the next exercise šŸ¦€"
+                    .bold()
+                    .green(),
+            ))?;
+        }
+
         let line_width = size()?.0;
         let progress_bar = progress_bar(
             self.app_state.n_done(),
@@ -108,7 +117,7 @@ impl<'a> WatchState<'a> {
 
         self.writer.write_all(b"Current exercise: ")?;
         self.writer.write_fmt(format_args!(
-            "{}",
+            "{}\n",
             self.app_state
                 .current_exercise()
                 .path
@@ -122,7 +131,7 @@ impl<'a> WatchState<'a> {
     }
 
     pub fn show_hint(&mut self) -> Result<()> {
-        self.hint_displayed = true;
+        self.show_hint = true;
         self.render()
     }
 
@@ -133,6 +142,7 @@ impl<'a> WatchState<'a> {
             self.writer
                 .write_all(b" (confusing input can occur after resizing the terminal)")?;
         }
+        self.writer.write_all(b"\n")?;
         self.show_prompt()
     }
 }

From 98c5088a39439389a4e198839b47819bfa1b1712 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 14:52:50 +0200
Subject: [PATCH 086/109] Update deps

---
 Cargo.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index a5ad8c9d..6c646614 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -25,9 +25,9 @@ dependencies = [
 
 [[package]]
 name = "allocator-api2"
-version = "0.2.16"
+version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
 
 [[package]]
 name = "anstream"

From a534de0312ff47d5e87b3bf60d508bdaafb98fbc Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 15:27:29 +0200
Subject: [PATCH 087/109] Implement going to the next exercise

---
 src/watch.rs                | 11 +++++++----
 src/watch/state.rs          | 23 ++++++++++++++++++++++-
 src/watch/terminal_event.rs |  2 ++
 3 files changed, 31 insertions(+), 5 deletions(-)

diff --git a/src/watch.rs b/src/watch.rs
index 928fc5fb..357b5c71 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -26,9 +26,9 @@ use self::{
 enum WatchEvent {
     Input(InputEvent),
     FileChange { exercise_ind: usize },
+    TerminalResize,
     NotifyErr(notify::Error),
     TerminalEventErr(io::Error),
-    TerminalResize,
 }
 
 /// Returned by the watch mode to indicate what to do afterwards.
@@ -60,15 +60,15 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
 
     while let Ok(event) = rx.recv() {
         match event {
+            WatchEvent::Input(InputEvent::Next) => {
+                watch_state.next_exercise()?;
+            }
             WatchEvent::Input(InputEvent::Hint) => {
                 watch_state.show_hint()?;
             }
             WatchEvent::Input(InputEvent::List) => {
                 return Ok(WatchExit::List);
             }
-            WatchEvent::TerminalResize => {
-                watch_state.render()?;
-            }
             WatchEvent::Input(InputEvent::Quit) => break,
             WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
                 watch_state.handle_invalid_cmd(&cmd)?;
@@ -76,6 +76,9 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
             WatchEvent::FileChange { exercise_ind } => {
                 watch_state.run_exercise_with_ind(exercise_ind)?;
             }
+            WatchEvent::TerminalResize => {
+                watch_state.render()?;
+            }
             WatchEvent::NotifyErr(e) => {
                 return Err(Error::from(e).context("Exercise file watcher failed"))
             }
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 5a5c0ca1..462633d1 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -6,7 +6,10 @@ use crossterm::{
 };
 use std::io::{self, StdoutLock, Write};
 
-use crate::{app_state::AppState, progress_bar::progress_bar};
+use crate::{
+    app_state::{AppState, ExercisesProgress},
+    progress_bar::progress_bar,
+};
 
 pub struct WatchState<'a> {
     writer: StdoutLock<'a>,
@@ -58,9 +61,27 @@ impl<'a> WatchState<'a> {
         self.run_current_exercise()
     }
 
+    pub fn next_exercise(&mut self) -> Result<()> {
+        if !self.show_done {
+            self.writer
+                .write_all(b"The current exercise isn't done yet\n")?;
+            self.show_prompt()?;
+            return Ok(());
+        }
+
+        match self.app_state.done_current_exercise()? {
+            ExercisesProgress::AllDone => todo!(),
+            ExercisesProgress::Pending => self.run_current_exercise(),
+        }
+    }
+
     fn show_prompt(&mut self) -> io::Result<()> {
         self.writer.write_all(b"\n")?;
 
+        if self.show_done {
+            self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?;
+        }
+
         if !self.show_hint {
             self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?;
         }
diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs
index faca8a2b..7f7ebe06 100644
--- a/src/watch/terminal_event.rs
+++ b/src/watch/terminal_event.rs
@@ -4,6 +4,7 @@ use std::sync::mpsc::Sender;
 use super::WatchEvent;
 
 pub enum InputEvent {
+    Next,
     Hint,
     List,
     Quit,
@@ -38,6 +39,7 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>) {
                 match key.code {
                     KeyCode::Enter => {
                         let input_event = match input.trim() {
+                            "n" | "next" => InputEvent::Next,
                             "h" | "hint" => InputEvent::Hint,
                             "l" | "list" => break InputEvent::List,
                             "q" | "quit" => break InputEvent::Quit,

From d5a6dee1b329f68d00bee61c6b6c7a0adbf8bab5 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 18:57:04 +0200
Subject: [PATCH 088/109] Handle the case when all exercises are done

---
 src/app_state.rs   | 52 +++++++++++++++++++++++++++++++++++++++++-----
 src/run.rs         | 24 +++++++++------------
 src/watch.rs       | 17 ++++++++-------
 src/watch/state.rs | 34 +++++++++++++++---------------
 4 files changed, 84 insertions(+), 43 deletions(-)

diff --git a/src/app_state.rs b/src/app_state.rs
index 4a0912e4..b1440e8a 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -1,8 +1,16 @@
 use anyhow::{bail, Context, Result};
+use crossterm::{
+    style::Stylize,
+    terminal::{Clear, ClearType},
+    ExecutableCommand,
+};
 use serde::{Deserialize, Serialize};
-use std::fs;
+use std::{
+    fs,
+    io::{StdoutLock, Write},
+};
 
-use crate::exercise::Exercise;
+use crate::{exercise::Exercise, FENISH_LINE};
 
 const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
 
@@ -143,7 +151,7 @@ impl AppState {
         Ok(())
     }
 
-    fn next_exercise_ind(&self) -> Option<usize> {
+    fn next_pending_exercise_ind(&self) -> Option<usize> {
         let current_ind = self.state_file.current_exercise_ind;
 
         if current_ind == self.state_file.progress.len() - 1 {
@@ -167,14 +175,41 @@ impl AppState {
         }
     }
 
-    pub fn done_current_exercise(&mut self) -> Result<ExercisesProgress> {
+    pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
         let done = &mut self.state_file.progress[self.state_file.current_exercise_ind];
         if !*done {
             *done = true;
             self.n_done += 1;
         }
 
-        let Some(ind) = self.next_exercise_ind() else {
+        let Some(ind) = self.next_pending_exercise_ind() else {
+            writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
+
+            for (exercise_ind, exercise) in self.exercises().iter().enumerate() {
+                writer.write_fmt(format_args!("Running {exercise} ... "))?;
+                writer.flush()?;
+
+                if !exercise.run()?.status.success() {
+                    self.state_file.current_exercise_ind = exercise_ind;
+                    self.current_exercise = exercise;
+
+                    // No check if the exercise is done before setting it to pending
+                    // because no pending exercise was found.
+                    self.state_file.progress[exercise_ind] = false;
+                    self.n_done -= 1;
+
+                    self.state_file.write()?;
+
+                    return Ok(ExercisesProgress::Pending);
+                }
+
+                writer.write_fmt(format_args!("{}\n", "ok".green()))?;
+            }
+
+            writer.execute(Clear(ClearType::All))?;
+            writer.write_all(FENISH_LINE.as_bytes())?;
+            // TODO: Show final message.
+
             return Ok(ExercisesProgress::AllDone);
         };
 
@@ -183,3 +218,10 @@ impl AppState {
         Ok(ExercisesProgress::Pending)
     }
 }
+
+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.
+This might take some minutes.
+
+";
diff --git a/src/run.rs b/src/run.rs
index 18da193b..ea790e9a 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -1,6 +1,6 @@
 use anyhow::{bail, Result};
 use crossterm::style::Stylize;
-use std::io::{stdout, Write};
+use std::io::{self, Write};
 
 use crate::app_state::{AppState, ExercisesProgress};
 
@@ -8,28 +8,24 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
     let exercise = app_state.current_exercise();
     let output = exercise.run()?;
 
-    {
-        let mut stdout = stdout().lock();
-        stdout.write_all(&output.stdout)?;
-        stdout.write_all(&output.stderr)?;
-        stdout.flush()?;
-    }
+    let mut stdout = io::stdout().lock();
+    stdout.write_all(&output.stdout)?;
+    stdout.write_all(b"\n")?;
+    stdout.write_all(&output.stderr)?;
+    stdout.flush()?;
 
     if !output.status.success() {
         bail!("Ran {exercise} with errors");
     }
 
-    println!(
+    stdout.write_fmt(format_args!(
         "{}{}",
         "āœ“ Successfully ran ".green(),
         exercise.path.to_string_lossy().green(),
-    );
+    ))?;
 
-    match app_state.done_current_exercise()? {
-        ExercisesProgress::AllDone => println!(
-            "šŸŽ‰ Congratulations! You have done all the exercises!
-šŸ”š There are no more exercises to do next!"
-        ),
+    match app_state.done_current_exercise(&mut stdout)? {
+        ExercisesProgress::AllDone => (),
         ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()),
     }
 
diff --git a/src/watch.rs b/src/watch.rs
index 357b5c71..beb69b3d 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -15,7 +15,7 @@ mod debounce_event;
 mod state;
 mod terminal_event;
 
-use crate::app_state::AppState;
+use crate::app_state::{AppState, ExercisesProgress};
 
 use self::{
     debounce_event::DebounceEventHandler,
@@ -32,6 +32,7 @@ enum WatchEvent {
 }
 
 /// Returned by the watch mode to indicate what to do afterwards.
+#[must_use]
 pub enum WatchExit {
     /// Exit the program.
     Shutdown,
@@ -60,16 +61,20 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
 
     while let Ok(event) = rx.recv() {
         match event {
-            WatchEvent::Input(InputEvent::Next) => {
-                watch_state.next_exercise()?;
-            }
+            WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
+                ExercisesProgress::AllDone => break,
+                ExercisesProgress::Pending => watch_state.run_current_exercise()?,
+            },
             WatchEvent::Input(InputEvent::Hint) => {
                 watch_state.show_hint()?;
             }
             WatchEvent::Input(InputEvent::List) => {
                 return Ok(WatchExit::List);
             }
-            WatchEvent::Input(InputEvent::Quit) => break,
+            WatchEvent::Input(InputEvent::Quit) => {
+                watch_state.into_writer().write_all(QUIT_MSG)?;
+                break;
+            }
             WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
                 watch_state.handle_invalid_cmd(&cmd)?;
             }
@@ -88,8 +93,6 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
         }
     }
 
-    watch_state.into_writer().write_all(QUIT_MSG)?;
-
     Ok(WatchExit::Shutdown)
 }
 
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 462633d1..70b6ae48 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -4,7 +4,10 @@ use crossterm::{
     terminal::{size, Clear, ClearType},
     ExecutableCommand,
 };
-use std::io::{self, StdoutLock, Write};
+use std::{
+    io::{self, StdoutLock, Write},
+    process::Output,
+};
 
 use crate::{
     app_state::{AppState, ExercisesProgress},
@@ -49,6 +52,9 @@ impl<'a> WatchState<'a> {
             self.stderr = None;
             self.show_done = true;
         } else {
+            self.app_state
+                .set_pending(self.app_state.current_exercise_ind())?;
+
             self.stderr = Some(output.stderr);
             self.show_done = false;
         }
@@ -61,18 +67,15 @@ impl<'a> WatchState<'a> {
         self.run_current_exercise()
     }
 
-    pub fn next_exercise(&mut self) -> Result<()> {
+    pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
         if !self.show_done {
             self.writer
                 .write_all(b"The current exercise isn't done yet\n")?;
             self.show_prompt()?;
-            return Ok(());
+            return Ok(ExercisesProgress::Pending);
         }
 
-        match self.app_state.done_current_exercise()? {
-            ExercisesProgress::AllDone => todo!(),
-            ExercisesProgress::Pending => self.run_current_exercise(),
-        }
+        self.app_state.done_current_exercise(&mut self.writer)
     }
 
     fn show_prompt(&mut self) -> io::Result<()> {
@@ -93,7 +96,7 @@ impl<'a> WatchState<'a> {
     }
 
     pub fn render(&mut self) -> Result<()> {
-        // Prevent having the first line shifted after clearing because of the prompt.
+        // Prevent having the first line shifted.
         self.writer.write_all(b"\n")?;
 
         self.writer.execute(Clear(ClearType::All))?;
@@ -111,11 +114,11 @@ impl<'a> WatchState<'a> {
         self.writer.write_all(b"\n")?;
 
         if self.show_hint {
-            self.writer
-                .write_fmt(format_args!("{}\n", "Hint".bold().cyan().underlined()))?;
-            self.writer
-                .write_all(self.app_state.current_exercise().hint.as_bytes())?;
-            self.writer.write_all(b"\n\n")?;
+            self.writer.write_fmt(format_args!(
+                "{}\n{}\n\n",
+                "Hint".bold().cyan().underlined(),
+                self.app_state.current_exercise().hint,
+            ))?;
         }
 
         if self.show_done {
@@ -134,11 +137,8 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise
             self.app_state.exercises().len() as u16,
             line_width,
         )?;
-        self.writer.write_all(progress_bar.as_bytes())?;
-
-        self.writer.write_all(b"Current exercise: ")?;
         self.writer.write_fmt(format_args!(
-            "{}\n",
+            "{progress_bar}Current exercise: {}\n",
             self.app_state
                 .current_exercise()
                 .path

From 8bd03093eb314f799d7daafbd3f7dcea9a5ef148 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 18:57:39 +0200
Subject: [PATCH 089/109] Add newline at the end of the generated .gitignore

---
 src/init.rs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/init.rs b/src/init.rs
index 44747438..093610ab 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -84,7 +84,8 @@ pub fn init(exercises: &[Exercise]) -> Result<()> {
 }
 
 const GITIGNORE: &[u8] = b"/target
-/.rustlings-state.json";
+/.rustlings-state.json
+";
 
 const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
 

From 44824718b2155268c79d1ce216abc770df94d05d Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 18:58:01 +0200
Subject: [PATCH 090/109] Remove unused import

---
 src/watch/state.rs | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/watch/state.rs b/src/watch/state.rs
index 70b6ae48..6a97637b 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -4,10 +4,7 @@ use crossterm::{
     terminal::{size, Clear, ClearType},
     ExecutableCommand,
 };
-use std::{
-    io::{self, StdoutLock, Write},
-    process::Output,
-};
+use std::io::{self, StdoutLock, Write};
 
 use crate::{
     app_state::{AppState, ExercisesProgress},

From 9b0eeb815acd550d733a722c0563bfb703bb8513 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 19:07:17 +0200
Subject: [PATCH 091/109] Fix Display for Exercise

---
 src/exercise.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/exercise.rs b/src/exercise.rs
index d28f4dbe..a9dcce34 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -117,7 +117,7 @@ impl Exercise {
 
 impl Display for Exercise {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        self.path.fmt(f)
+        Display::fmt(&self.path.display(), f)
     }
 }
 

From 279ebdc1534d70d838110c16e46dce848a9de956 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 19:16:52 +0200
Subject: [PATCH 092/109] Remove the modifier filter in the list mode

---
 src/list.rs | 16 +++++-----------
 1 file changed, 5 insertions(+), 11 deletions(-)

diff --git a/src/list.rs b/src/list.rs
index 2430ed73..de120eaf 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -1,6 +1,6 @@
 use anyhow::Result;
 use crossterm::{
-    event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
+    event::{self, Event, KeyCode, KeyEventKind},
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
     ExecutableCommand,
 };
@@ -28,16 +28,10 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
 
         let key = loop {
             match event::read()? {
-                Event::Key(key) => {
-                    if key.modifiers != KeyModifiers::NONE {
-                        continue;
-                    }
-
-                    match key.kind {
-                        KeyEventKind::Press | KeyEventKind::Repeat => break key,
-                        KeyEventKind::Release => (),
-                    }
-                }
+                Event::Key(key) => match key.kind {
+                    KeyEventKind::Press | KeyEventKind::Repeat => break key,
+                    KeyEventKind::Release => (),
+                },
                 // Redraw
                 Event::Resize(_, _) => continue 'outer,
                 // Ignore

From 6e827da570278b6ff282f3b5c23e2ab95624117e Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 19:18:16 +0200
Subject: [PATCH 093/109] It doesn't take minutes :P

---
 src/app_state.rs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/app_state.rs b/src/app_state.rs
index b1440e8a..18d9e2ae 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -222,6 +222,5 @@ impl AppState {
 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.
-This might take some minutes.
 
 ";

From 06d1089714d77e8619fd0b5c34361eec5312363e Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 19:24:26 +0200
Subject: [PATCH 094/109] Set pending on fail in run mode

---
 src/run.rs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/run.rs b/src/run.rs
index ea790e9a..ebe4f965 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -15,6 +15,8 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
     stdout.flush()?;
 
     if !output.status.success() {
+        app_state.set_pending(app_state.current_exercise_ind())?;
+
         bail!("Ran {exercise} with errors");
     }
 

From ff4c7529846ba13ecb2e90616ff8fd7a9ee87164 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 19:30:29 +0200
Subject: [PATCH 095/109] Print FAILED

---
 src/app_state.rs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/app_state.rs b/src/app_state.rs
index 18d9e2ae..cb7debe1 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -190,6 +190,8 @@ impl AppState {
                 writer.flush()?;
 
                 if !exercise.run()?.status.success() {
+                    writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?;
+
                     self.state_file.current_exercise_ind = exercise_ind;
                     self.current_exercise = exercise;
 

From 757723a7e8db5822df3b7ca56012448ca292ce4f Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 19:30:36 +0200
Subject: [PATCH 096/109] Add missing newline

---
 src/run.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/run.rs b/src/run.rs
index ebe4f965..47485492 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -21,7 +21,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
     }
 
     stdout.write_fmt(format_args!(
-        "{}{}",
+        "{}{}\n",
         "āœ“ Successfully ran ".green(),
         exercise.path.to_string_lossy().green(),
     ))?;

From 24539666afb0e8c80fbccbca7ad212ba8fbd1189 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Fri, 12 Apr 2024 20:06:56 +0200
Subject: [PATCH 097/109] Show the final message

---
 info.toml        |  3 ++-
 src/app_state.rs | 29 ++++++++++++++++++-----------
 src/exercise.rs  |  1 -
 src/main.rs      |  6 ++++--
 4 files changed, 24 insertions(+), 15 deletions(-)

diff --git a/info.toml b/info.toml
index d35b5702..b6b68008 100644
--- a/info.toml
+++ b/info.toml
@@ -20,7 +20,8 @@ started, here's a couple of notes about how Rustlings operates:
    and sometimes, other learners do too so you can help each other out!
 
 Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise.
-Make sure to have your editor open in the `rustlings` directory!"""
+Make sure to have your editor open in the `rustlings` directory!
+"""
 
 final_message = """We hope you enjoyed learning about the various aspects of Rust!
 If you noticed any issues, please don't hesitate to report them to our repo.
diff --git a/src/app_state.rs b/src/app_state.rs
index cb7debe1..2ea3db42 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -51,24 +51,29 @@ impl StateFile {
     }
 }
 
-pub struct AppState {
-    state_file: StateFile,
-    exercises: &'static [Exercise],
-    n_done: u16,
-    current_exercise: &'static Exercise,
-}
-
 #[must_use]
 pub enum ExercisesProgress {
     AllDone,
     Pending,
 }
 
+pub struct AppState {
+    state_file: StateFile,
+    exercises: &'static [Exercise],
+    n_done: u16,
+    current_exercise: &'static Exercise,
+    final_message: &'static str,
+}
+
 impl AppState {
-    pub fn new(exercises: Vec<Exercise>) -> Self {
-        // Leaking for sending the exercises to the debounce event handler.
-        // Leaking is not a problem since the exercises' slice is used until the end of the program.
+    pub fn new(mut exercises: Vec<Exercise>, mut final_message: String) -> Self {
+        // Leaking especially for sending the exercises to the debounce event handler.
+        // Leaking is not a problem because the `AppState` instance lives until
+        // the end of the program.
+        exercises.shrink_to_fit();
         let exercises = exercises.leak();
+        final_message.shrink_to_fit();
+        let final_message = final_message.leak();
 
         let state_file = StateFile::read_or_default(exercises);
         let n_done = state_file
@@ -82,6 +87,7 @@ impl AppState {
             exercises,
             n_done,
             current_exercise,
+            final_message,
         }
     }
 
@@ -210,7 +216,8 @@ impl AppState {
 
             writer.execute(Clear(ClearType::All))?;
             writer.write_all(FENISH_LINE.as_bytes())?;
-            // TODO: Show final message.
+            writer.write_all(self.final_message.as_bytes())?;
+            writer.write_all(b"\n")?;
 
             return Ok(ExercisesProgress::AllDone);
         };
diff --git a/src/exercise.rs b/src/exercise.rs
index a9dcce34..a29b83aa 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -26,7 +26,6 @@ pub enum Mode {
 pub struct InfoFile {
     // TODO
     pub welcome_message: Option<String>,
-    // TODO
     pub final_message: Option<String>,
     pub exercises: Vec<Exercise>,
 }
diff --git a/src/main.rs b/src/main.rs
index fdbb710c..cdfa21f6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -68,7 +68,7 @@ fn main() -> Result<()> {
         exit(1);
     }
 
-    let mut app_state = AppState::new(exercises);
+    let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default());
 
     match args.command {
         None => loop {
@@ -144,4 +144,6 @@ const FENISH_LINE: &str = "+----------------------------------------------------
          ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’
        ā–’ā–’    ā–’ā–’    ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’ā–’    ā–’ā–’    ā–’ā–’
        ā–’ā–’  ā–’ā–’    ā–’ā–’                  ā–’ā–’    ā–’ā–’  ā–’ā–’
-           ā–’ā–’  ā–’ā–’                      ā–’ā–’  ā–’ā–’\x1b[0m";
+           ā–’ā–’  ā–’ā–’                      ā–’ā–’  ā–’ā–’\x1b[0m
+
+";

From 2a26dfcb005d2a9ee24e920462b37dfb6d235c32 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sat, 13 Apr 2024 15:30:35 +0200
Subject: [PATCH 098/109] Remove unused ContextLine

---
 src/exercise.rs | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/src/exercise.rs b/src/exercise.rs
index a29b83aa..6aa3b82e 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -63,17 +63,6 @@ pub struct Exercise {
     pub hint: String,
 }
 
-// The context information of a pending exercise.
-#[derive(PartialEq, Eq, Debug)]
-pub struct ContextLine {
-    // The source code line
-    pub line: String,
-    // The line number
-    pub number: usize,
-    // Whether this is important and should be highlighted
-    pub important: bool,
-}
-
 impl Exercise {
     fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result<Output> {
         let mut cmd = Command::new("cargo");

From 5c0073a9485c4226e58b657cb49628919a28a942 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 01:15:43 +0200
Subject: [PATCH 099/109] Tolerate changes in the state file

---
 Cargo.lock                                    |   1 +
 Cargo.toml                                    |   1 +
 exercises/00_intro/intro1.rs                  |   1 -
 info.toml                                     | 272 +++++++++---------
 src/app_state.rs                              | 205 +++++++------
 src/app_state/state_file.rs                   | 112 ++++++++
 src/exercise.rs                               |  72 +----
 src/info_file.rs                              |  81 ++++++
 src/init.rs                                   |  23 +-
 src/list.rs                                   |  11 +-
 src/list/state.rs                             |  35 +--
 src/main.rs                                   |  40 ++-
 src/run.rs                                    |   2 +-
 src/watch.rs                                  |  15 +-
 .../{debounce_event.rs => notify_event.rs}    |  10 +-
 15 files changed, 513 insertions(+), 368 deletions(-)
 create mode 100644 src/app_state/state_file.rs
 create mode 100644 src/info_file.rs
 rename src/watch/{debounce_event.rs => notify_event.rs} (84%)

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

From bee62c89de09fdd9823cba81e07f0f8528fe8ef9 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 02:41:19 +0200
Subject: [PATCH 100/109] Add terminal links

---
 src/app_state.rs            |  2 +-
 src/app_state/state_file.rs |  8 +++-----
 src/embedded.rs             |  7 ++++++-
 src/exercise.rs             | 34 +++++++++++++++++++++++++++++++---
 src/info_file.rs            | 10 ++++------
 src/list/state.rs           |  2 +-
 src/run.rs                  | 12 +++++++++---
 src/watch.rs                |  2 +-
 src/watch/notify_event.rs   |  4 ++--
 src/watch/state.rs          |  6 +-----
 10 files changed, 59 insertions(+), 28 deletions(-)

diff --git a/src/app_state.rs b/src/app_state.rs
index 1a051b97..98c63842 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -38,7 +38,7 @@ impl AppState {
                 // Leaking to be able to borrow in the watch mode `Table`.
                 // Leaking is not a problem because the `AppState` instance lives until
                 // the end of the program.
-                let path = Box::leak(exercise_info.path().into_boxed_path());
+                let path = exercise_info.path().leak();
 
                 exercise_info.name.shrink_to_fit();
                 let name = exercise_info.name.leak();
diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs
index 364a1fa3..4e4a0e15 100644
--- a/src/app_state/state_file.rs
+++ b/src/app_state/state_file.rs
@@ -59,7 +59,7 @@ pub fn write(app_state: &AppState) -> Result<()> {
         exercises: ExercisesStateSerializer(&app_state.exercises),
     };
 
-    let mut buf = Vec::with_capacity(1024);
+    let mut buf = Vec::with_capacity(4096);
     serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?;
     fs::write(STATE_FILE_NAME, buf)
         .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?;
@@ -69,8 +69,6 @@ pub fn write(app_state: &AppState) -> Result<()> {
 
 #[cfg(test)]
 mod tests {
-    use std::path::Path;
-
     use crate::info_file::Mode;
 
     use super::*;
@@ -81,14 +79,14 @@ mod tests {
         let exercises = [
             Exercise {
                 name: "1",
-                path: Path::new("exercises/1.rs"),
+                path: "exercises/1.rs",
                 mode: Mode::Run,
                 hint: String::new(),
                 done: true,
             },
             Exercise {
                 name: "2",
-                path: Path::new("exercises/2.rs"),
+                path: "exercises/2.rs",
                 mode: Mode::Test,
                 hint: String::new(),
                 done: false,
diff --git a/src/embedded.rs b/src/embedded.rs
index 1e2d6770..866b12b8 100644
--- a/src/embedded.rs
+++ b/src/embedded.rs
@@ -91,7 +91,12 @@ impl EmbeddedFiles {
         Ok(())
     }
 
-    pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> {
+    pub fn write_exercise_to_disk<P>(&self, path: P, strategy: WriteStrategy) -> io::Result<()>
+    where
+        P: AsRef<Path>,
+    {
+        let path = path.as_ref();
+
         if let Some(file) = self
             .exercises_dir
             .files
diff --git a/src/exercise.rs b/src/exercise.rs
index c5ece5f5..2ec8d979 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -1,7 +1,8 @@
 use anyhow::{Context, Result};
+use crossterm::style::{style, StyledContent, Stylize};
 use std::{
     fmt::{self, Display, Formatter},
-    path::Path,
+    fs,
     process::{Command, Output},
 };
 
@@ -10,11 +11,32 @@ use crate::{
     info_file::Mode,
 };
 
+pub struct TerminalFileLink<'a> {
+    path: &'a str,
+}
+
+impl<'a> Display for TerminalFileLink<'a> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        if let Ok(Some(canonical_path)) = fs::canonicalize(self.path)
+            .as_deref()
+            .map(|path| path.to_str())
+        {
+            write!(
+                f,
+                "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\",
+                canonical_path, self.path,
+            )
+        } else {
+            write!(f, "{}", self.path,)
+        }
+    }
+}
+
 pub struct Exercise {
     // Exercise's unique name
     pub name: &'static str,
     // Exercise's path
-    pub path: &'static Path,
+    pub path: &'static str,
     // The mode of the exercise
     pub mode: Mode,
     // The hint text associated with the exercise
@@ -60,10 +82,16 @@ impl Exercise {
             .write_exercise_to_disk(self.path, WriteStrategy::Overwrite)
             .with_context(|| format!("Failed to reset the exercise {self}"))
     }
+
+    pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
+        style(TerminalFileLink { path: self.path })
+            .underlined()
+            .blue()
+    }
 }
 
 impl Display for Exercise {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        Display::fmt(&self.path.display(), f)
+        self.path.fmt(f)
     }
 }
diff --git a/src/info_file.rs b/src/info_file.rs
index dc97b926..2a45e02d 100644
--- a/src/info_file.rs
+++ b/src/info_file.rs
@@ -1,6 +1,6 @@
 use anyhow::{bail, Context, Error, Result};
 use serde::Deserialize;
-use std::{fs, path::PathBuf};
+use std::fs;
 
 // The mode of the exercise.
 #[derive(Deserialize, Copy, Clone)]
@@ -28,14 +28,12 @@ pub struct ExerciseInfo {
 }
 
 impl ExerciseInfo {
-    pub fn path(&self) -> PathBuf {
-        let path = if let Some(dir) = &self.dir {
+    pub fn path(&self) -> String {
+        if let Some(dir) = &self.dir {
             format!("exercises/{dir}/{}.rs", self.name)
         } else {
             format!("exercises/{}.rs", self.name)
-        };
-
-        PathBuf::from(path)
+        }
     }
 }
 
diff --git a/src/list/state.rs b/src/list/state.rs
index 38391a49..2a1fef18 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -63,7 +63,7 @@ impl<'a> UiState<'a> {
                     next,
                     exercise_state,
                     Span::raw(exercise.name),
-                    Span::raw(exercise.path.to_string_lossy()),
+                    Span::raw(exercise.path),
                 ]))
             });
 
diff --git a/src/run.rs b/src/run.rs
index 9c504b53..863b584e 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -17,18 +17,24 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
     if !output.status.success() {
         app_state.set_pending(app_state.current_exercise_ind())?;
 
-        bail!("Ran {} with errors", app_state.current_exercise());
+        bail!(
+            "Ran {} with errors",
+            app_state.current_exercise().terminal_link(),
+        );
     }
 
     stdout.write_fmt(format_args!(
         "{}{}\n",
         "āœ“ Successfully ran ".green(),
-        exercise.path.to_string_lossy().green(),
+        exercise.path.green(),
     ))?;
 
     match app_state.done_current_exercise(&mut stdout)? {
         ExercisesProgress::AllDone => (),
-        ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()),
+        ExercisesProgress::Pending => println!(
+            "Next exercise: {}",
+            app_state.current_exercise().terminal_link(),
+        ),
     }
 
     Ok(())
diff --git a/src/watch.rs b/src/watch.rs
index 58e829f3..bab64ae1 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -42,7 +42,7 @@ pub enum WatchExit {
 
 pub fn watch(
     app_state: &mut AppState,
-    exercise_paths: &'static [&'static Path],
+    exercise_paths: &'static [&'static str],
 ) -> Result<WatchExit> {
     let (tx, rx) = channel();
     let mut debouncer = new_debouncer(
diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs
index 0c8d6692..fb9a8c05 100644
--- a/src/watch/notify_event.rs
+++ b/src/watch/notify_event.rs
@@ -1,11 +1,11 @@
 use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
-use std::{path::Path, sync::mpsc::Sender};
+use std::sync::mpsc::Sender;
 
 use super::WatchEvent;
 
 pub struct DebounceEventHandler {
     pub tx: Sender<WatchEvent>,
-    pub exercise_paths: &'static [&'static Path],
+    pub exercise_paths: &'static [&'static str],
 }
 
 impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 6a97637b..1a79573f 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -136,11 +136,7 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise
         )?;
         self.writer.write_fmt(format_args!(
             "{progress_bar}Current exercise: {}\n",
-            self.app_state
-                .current_exercise()
-                .path
-                .to_string_lossy()
-                .bold(),
+            self.app_state.current_exercise().terminal_link(),
         ))?;
 
         self.show_prompt()?;

From 9831cbb13975cd0f5ee4c295156102e3573ede1a Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 03:13:33 +0200
Subject: [PATCH 101/109] Fix tests

---
 gen-dev-cargo-toml/src/main.rs  | 24 ++++++++++++---------
 tests/dev_cargo_bins.rs         | 37 +++++++++++++++++++--------------
 tests/fixture/failure/info.toml |  4 +---
 tests/fixture/state/info.toml   |  7 ++-----
 tests/fixture/success/info.toml |  4 +---
 5 files changed, 39 insertions(+), 37 deletions(-)

diff --git a/gen-dev-cargo-toml/src/main.rs b/gen-dev-cargo-toml/src/main.rs
index 9a7c1bbd..792fe5f0 100644
--- a/gen-dev-cargo-toml/src/main.rs
+++ b/gen-dev-cargo-toml/src/main.rs
@@ -10,18 +10,18 @@ use std::{
 };
 
 #[derive(Deserialize)]
-struct Exercise {
+struct ExerciseInfo {
     name: String,
-    path: String,
+    dir: Option<String>,
 }
 
 #[derive(Deserialize)]
-struct InfoToml {
-    exercises: Vec<Exercise>,
+struct InfoFile {
+    exercises: Vec<ExerciseInfo>,
 }
 
 fn main() -> Result<()> {
-    let exercises = toml_edit::de::from_str::<InfoToml>(
+    let exercise_infos = toml_edit::de::from_str::<InfoFile>(
         &fs::read_to_string("info.toml").context("Failed to read `info.toml`")?,
     )
     .context("Failed to deserialize `info.toml`")?
@@ -36,12 +36,16 @@ fn main() -> Result<()> {
 bin = [\n",
     );
 
-    for exercise in exercises {
+    for exercise_info in exercise_infos {
         buf.extend_from_slice(b"  { name = \"");
-        buf.extend_from_slice(exercise.name.as_bytes());
-        buf.extend_from_slice(b"\", path = \"../");
-        buf.extend_from_slice(exercise.path.as_bytes());
-        buf.extend_from_slice(b"\" },\n");
+        buf.extend_from_slice(exercise_info.name.as_bytes());
+        buf.extend_from_slice(b"\", path = \"../exercises/");
+        if let Some(dir) = &exercise_info.dir {
+            buf.extend_from_slice(dir.as_bytes());
+            buf.extend_from_slice(b"/");
+        }
+        buf.extend_from_slice(exercise_info.name.as_bytes());
+        buf.extend_from_slice(b".rs\" },\n");
     }
 
     buf.extend_from_slice(
diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs
index c3faea92..81f48b1e 100644
--- a/tests/dev_cargo_bins.rs
+++ b/tests/dev_cargo_bins.rs
@@ -5,34 +5,39 @@ use serde::Deserialize;
 use std::fs;
 
 #[derive(Deserialize)]
-struct Exercise {
+struct ExerciseInfo {
     name: String,
-    path: String,
+    dir: Option<String>,
 }
 
 #[derive(Deserialize)]
-struct InfoToml {
-    exercises: Vec<Exercise>,
+struct InfoFile {
+    exercises: Vec<ExerciseInfo>,
 }
 
 #[test]
 fn dev_cargo_bins() {
-    let content = fs::read_to_string("dev/Cargo.toml").unwrap();
+    let cargo_toml = fs::read_to_string("dev/Cargo.toml").unwrap();
 
-    let exercises = toml_edit::de::from_str::<InfoToml>(&fs::read_to_string("info.toml").unwrap())
-        .unwrap()
-        .exercises;
+    let exercise_infos =
+        toml_edit::de::from_str::<InfoFile>(&fs::read_to_string("info.toml").unwrap())
+            .unwrap()
+            .exercises;
 
     let mut start_ind = 0;
-    for exercise in exercises {
-        let name_start = start_ind + content[start_ind..].find('"').unwrap() + 1;
-        let name_end = name_start + content[name_start..].find('"').unwrap();
-        assert_eq!(exercise.name, &content[name_start..name_end]);
+    for exercise_info in exercise_infos {
+        let name_start = start_ind + cargo_toml[start_ind..].find('"').unwrap() + 1;
+        let name_end = name_start + cargo_toml[name_start..].find('"').unwrap();
+        assert_eq!(exercise_info.name, &cargo_toml[name_start..name_end]);
 
-        // +3 to skip `../` at the begeinning of the path.
-        let path_start = name_end + content[name_end + 1..].find('"').unwrap() + 5;
-        let path_end = path_start + content[path_start..].find('"').unwrap();
-        assert_eq!(exercise.path, &content[path_start..path_end]);
+        let path_start = name_end + cargo_toml[name_end + 1..].find('"').unwrap() + 2;
+        let path_end = path_start + cargo_toml[path_start..].find('"').unwrap();
+        let expected_path = if let Some(dir) = exercise_info.dir {
+            format!("../exercises/{dir}/{}.rs", exercise_info.name)
+        } else {
+            format!("../exercises/{}.rs", exercise_info.name)
+        };
+        assert_eq!(expected_path, &cargo_toml[path_start..path_end]);
 
         start_ind = path_end + 1;
     }
diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml
index 9474ee3f..94ec6ead 100644
--- a/tests/fixture/failure/info.toml
+++ b/tests/fixture/failure/info.toml
@@ -1,11 +1,9 @@
 [[exercises]]
 name = "compFailure"
-path = "exercises/compFailure.rs"
-mode = "compile"
+mode = "run"
 hint = ""
 
 [[exercises]]
 name = "testFailure"
-path = "exercises/testFailure.rs"
 mode = "test"
 hint = "Hello!"
diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml
index 8de5d604..e5c4d8f4 100644
--- a/tests/fixture/state/info.toml
+++ b/tests/fixture/state/info.toml
@@ -1,17 +1,14 @@
 [[exercises]]
 name = "pending_exercise"
-path = "exercises/pending_exercise.rs"
-mode = "compile"
+mode = "run"
 hint = """"""
 
 [[exercises]]
 name = "pending_test_exercise"
-path = "exercises/pending_test_exercise.rs"
 mode = "test"
 hint = """"""
 
 [[exercises]]
 name = "finished_exercise"
-path = "exercises/finished_exercise.rs"
-mode = "compile"
+mode = "run"
 hint = """"""
diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml
index 17ed8c62..674ba264 100644
--- a/tests/fixture/success/info.toml
+++ b/tests/fixture/success/info.toml
@@ -1,11 +1,9 @@
 [[exercises]]
 name = "compSuccess"
-path = "exercises/compSuccess.rs"
-mode = "compile"
+mode = "run"
 hint = """"""
 
 [[exercises]]
 name = "testSuccess"
-path = "exercises/testSuccess.rs"
 mode = "test"
 hint = """"""

From 9dcc4b7df5f539b10117e97870a9f1cb01ca040d Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 05:13:27 +0200
Subject: [PATCH 102/109] Simplify the state file

---
 .gitignore                  |   2 +-
 Cargo.lock                  |  12 ----
 Cargo.toml                  |   1 -
 src/app_state.rs            | 128 +++++++++++++++++++++++-------------
 src/app_state/state_file.rs | 110 -------------------------------
 src/init.rs                 |   2 +-
 6 files changed, 86 insertions(+), 169 deletions(-)
 delete mode 100644 src/app_state/state_file.rs

diff --git a/.gitignore b/.gitignore
index c9172e01..80f9092a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@ target/
 /dev/Cargo.lock
 
 # State file
-.rustlings-state.json
+.rustlings-state.txt
 
 # oranda
 public/
diff --git a/Cargo.lock b/Cargo.lock
index dbf1923e..6bc68f0d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -690,7 +690,6 @@ dependencies = [
  "ratatui",
  "rustlings-macros",
  "serde",
- "serde_json",
  "toml_edit",
  "which",
 ]
@@ -749,17 +748,6 @@ dependencies = [
  "syn 2.0.58",
 ]
 
-[[package]]
-name = "serde_json"
-version = "1.0.115"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
-dependencies = [
- "itoa",
- "ryu",
- "serde",
-]
-
 [[package]]
 name = "serde_spanned"
 version = "0.6.5"
diff --git a/Cargo.toml b/Cargo.toml
index 14ae9a14..07865abc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,7 +41,6 @@ hashbrown = "0.14.3"
 notify-debouncer-mini = "0.4.1"
 ratatui = "0.26.1"
 rustlings-macros = { path = "rustlings-macros" }
-serde_json = "1.0.115"
 serde.workspace = true
 toml_edit.workspace = true
 which = "6.0.1"
diff --git a/src/app_state.rs b/src/app_state.rs
index 98c63842..9a378de6 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -4,15 +4,14 @@ use crossterm::{
     terminal::{Clear, ClearType},
     ExecutableCommand,
 };
-use std::io::{StdoutLock, Write};
-
-mod state_file;
+use std::{
+    fs::{self, File},
+    io::{Read, StdoutLock, Write},
+};
 
 use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE};
 
-use self::state_file::{write, StateFileDeser};
-
-const STATE_FILE_NAME: &str = ".rustlings-state.json";
+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]
@@ -27,11 +26,51 @@ pub struct AppState {
     n_done: u16,
     welcome_message: String,
     final_message: String,
+    file_buf: Vec<u8>,
 }
 
 impl AppState {
+    fn update_from_file(&mut self) {
+        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_ok()
+        {
+            let mut lines = self.file_buf.split(|c| *c == b'\n');
+            let Some(current_exercise_name) = lines.next() else {
+                return;
+            };
+
+            if lines.next().is_none() {
+                return;
+            }
+
+            let mut done_exercises = hashbrown::HashSet::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;
+                }
+            }
+        }
+    }
+
     pub fn new(info_file: InfoFile) -> Self {
-        let mut exercises = info_file
+        let exercises = info_file
             .exercises
             .into_iter()
             .map(|mut exercise_info| {
@@ -55,42 +94,18 @@ impl AppState {
             })
             .collect::<Vec<_>>();
 
-        let (current_exercise_ind, n_done) = StateFileDeser::read().map_or((0, 0), |state_file| {
-            let mut state_file_exercises =
-                hashbrown::HashMap::with_capacity(state_file.exercises.len());
-
-            for (ind, exercise_state) in state_file.exercises.into_iter().enumerate() {
-                state_file_exercises.insert(
-                    exercise_state.name,
-                    (ind == state_file.current_exercise_ind, exercise_state.done),
-                );
-            }
-
-            let mut current_exercise_ind = 0;
-            let mut n_done = 0;
-            for (ind, exercise) in exercises.iter_mut().enumerate() {
-                if let Some((current, done)) = state_file_exercises.get(exercise.name) {
-                    if *done {
-                        exercise.done = true;
-                        n_done += 1;
-                    }
-
-                    if *current {
-                        current_exercise_ind = ind;
-                    }
-                }
-            }
-
-            (current_exercise_ind, n_done)
-        });
-
-        Self {
-            current_exercise_ind,
+        let mut slf = Self {
+            current_exercise_ind: 0,
             exercises,
-            n_done,
+            n_done: 0,
             welcome_message: info_file.welcome_message.unwrap_or_default(),
             final_message: info_file.final_message.unwrap_or_default(),
-        }
+            file_buf: Vec::with_capacity(2048),
+        };
+
+        slf.update_from_file();
+
+        slf
     }
 
     #[inline]
@@ -120,7 +135,7 @@ impl AppState {
 
         self.current_exercise_ind = ind;
 
-        write(self)
+        self.write()
     }
 
     pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
@@ -132,7 +147,7 @@ impl AppState {
             .position(|exercise| exercise.name == name)
             .with_context(|| format!("No exercise found for '{name}'!"))?;
 
-        write(self)
+        self.write()
     }
 
     pub fn set_pending(&mut self, ind: usize) -> Result<()> {
@@ -141,7 +156,7 @@ impl AppState {
         if exercise.done {
             exercise.done = false;
             self.n_done -= 1;
-            write(self)?;
+            self.write()?;
         }
 
         Ok(())
@@ -193,7 +208,7 @@ impl AppState {
                     self.exercises[exercise_ind].done = false;
                     self.n_done -= 1;
 
-                    write(self)?;
+                    self.write()?;
 
                     return Ok(ExercisesProgress::Pending);
                 }
@@ -213,6 +228,31 @@ impl AppState {
 
         Ok(ExercisesProgress::Pending)
     }
+
+    // Write the state file.
+    // The file's format is very simple:
+    // - The first line is the name of the current exercise.
+    // - The second 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
+            .extend_from_slice(self.current_exercise().name.as_bytes());
+        self.file_buf.extend_from_slice(b"\n\n");
+
+        for exercise in &self.exercises {
+            if exercise.done {
+                self.file_buf.extend_from_slice(exercise.name.as_bytes());
+                self.file_buf.extend_from_slice(b"\n");
+            }
+        }
+
+        fs::write(STATE_FILE_NAME, &self.file_buf)
+            .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
+
+        Ok(())
+    }
 }
 
 const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs
deleted file mode 100644
index 4e4a0e15..00000000
--- a/src/app_state/state_file.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-use anyhow::{Context, Result};
-use serde::{Deserialize, Serialize};
-use std::fs;
-
-use crate::exercise::Exercise;
-
-use super::{AppState, STATE_FILE_NAME};
-
-#[derive(Deserialize)]
-pub struct ExerciseStateDeser {
-    pub name: String,
-    pub done: bool,
-}
-
-#[derive(Serialize)]
-struct ExerciseStateSer<'a> {
-    name: &'a str,
-    done: bool,
-}
-
-struct ExercisesStateSerializer<'a>(&'a [Exercise]);
-
-impl<'a> Serialize for ExercisesStateSerializer<'a> {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        let iter = self.0.iter().map(|exercise| ExerciseStateSer {
-            name: exercise.name,
-            done: exercise.done,
-        });
-
-        serializer.collect_seq(iter)
-    }
-}
-
-#[derive(Deserialize)]
-pub struct StateFileDeser {
-    pub current_exercise_ind: usize,
-    pub exercises: Vec<ExerciseStateDeser>,
-}
-
-#[derive(Serialize)]
-struct StateFileSer<'a> {
-    current_exercise_ind: usize,
-    exercises: ExercisesStateSerializer<'a>,
-}
-
-impl StateFileDeser {
-    pub fn read() -> Option<Self> {
-        let file_content = fs::read(STATE_FILE_NAME).ok()?;
-        serde_json::de::from_slice(&file_content).ok()
-    }
-}
-
-pub fn write(app_state: &AppState) -> Result<()> {
-    let content = StateFileSer {
-        current_exercise_ind: app_state.current_exercise_ind,
-        exercises: ExercisesStateSerializer(&app_state.exercises),
-    };
-
-    let mut buf = Vec::with_capacity(4096);
-    serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?;
-    fs::write(STATE_FILE_NAME, buf)
-        .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?;
-
-    Ok(())
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::info_file::Mode;
-
-    use super::*;
-
-    #[test]
-    fn ser_deser_sync() {
-        let current_exercise_ind = 1;
-        let exercises = [
-            Exercise {
-                name: "1",
-                path: "exercises/1.rs",
-                mode: Mode::Run,
-                hint: String::new(),
-                done: true,
-            },
-            Exercise {
-                name: "2",
-                path: "exercises/2.rs",
-                mode: Mode::Test,
-                hint: String::new(),
-                done: false,
-            },
-        ];
-
-        let ser = StateFileSer {
-            current_exercise_ind,
-            exercises: ExercisesStateSerializer(&exercises),
-        };
-        let deser: StateFileDeser =
-            serde_json::de::from_slice(&serde_json::ser::to_vec(&ser).unwrap()).unwrap();
-
-        assert_eq!(deser.current_exercise_ind, current_exercise_ind);
-        assert!(deser
-            .exercises
-            .iter()
-            .zip(exercises)
-            .all(|(deser, ser)| deser.name == ser.name && deser.done == ser.done));
-    }
-}
diff --git a/src/init.rs b/src/init.rs
index 2badf376..4ee503a2 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -89,7 +89,7 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> {
 }
 
 const GITIGNORE: &[u8] = b"/target
-/.rustlings-state.json
+/.rustlings-state.txt
 ";
 
 const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;

From 1c90575b9fe0f0fb32006e000aefff10d8a4a39c Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 05:13:50 +0200
Subject: [PATCH 103/109] Update deps

---
 Cargo.lock | 59 ++++++++++++++++++++++++++++++------------------------
 1 file changed, 33 insertions(+), 26 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 6bc68f0d..5cfebe60 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -261,9 +261,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
 [[package]]
 name = "either"
-version = "1.10.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
 
 [[package]]
 name = "equivalent"
@@ -1000,7 +1000,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -1020,17 +1020,18 @@ dependencies = [
 
 [[package]]
 name = "windows-targets"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.4",
- "windows_aarch64_msvc 0.52.4",
- "windows_i686_gnu 0.52.4",
- "windows_i686_msvc 0.52.4",
- "windows_x86_64_gnu 0.52.4",
- "windows_x86_64_gnullvm 0.52.4",
- "windows_x86_64_msvc 0.52.4",
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
 ]
 
 [[package]]
@@ -1041,9 +1042,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -1053,9 +1054,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -1065,9 +1066,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -1077,9 +1084,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -1089,9 +1096,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -1101,9 +1108,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -1113,9 +1120,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
 
 [[package]]
 name = "winnow"

From 3da860927d131eacc288764672ed8799a6a8cfca Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 14:53:32 +0200
Subject: [PATCH 104/109] Use push instead of extend_from_slice on chars

---
 gen-dev-cargo-toml/src/main.rs | 2 +-
 src/app_state.rs               | 2 +-
 src/init.rs                    | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/gen-dev-cargo-toml/src/main.rs b/gen-dev-cargo-toml/src/main.rs
index 792fe5f0..43b4ebd7 100644
--- a/gen-dev-cargo-toml/src/main.rs
+++ b/gen-dev-cargo-toml/src/main.rs
@@ -42,7 +42,7 @@ bin = [\n",
         buf.extend_from_slice(b"\", path = \"../exercises/");
         if let Some(dir) = &exercise_info.dir {
             buf.extend_from_slice(dir.as_bytes());
-            buf.extend_from_slice(b"/");
+            buf.push(b'/');
         }
         buf.extend_from_slice(exercise_info.name.as_bytes());
         buf.extend_from_slice(b".rs\" },\n");
diff --git a/src/app_state.rs b/src/app_state.rs
index 9a378de6..31cb2cbf 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -244,7 +244,7 @@ impl AppState {
         for exercise in &self.exercises {
             if exercise.done {
                 self.file_buf.extend_from_slice(exercise.name.as_bytes());
-                self.file_buf.extend_from_slice(b"\n");
+                self.file_buf.push(b'\n');
             }
         }
 
diff --git a/src/init.rs b/src/init.rs
index 4ee503a2..459519de 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -17,7 +17,7 @@ fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> {
         cargo_toml.extend_from_slice(b"\", path = \"exercises/");
         if let Some(dir) = &exercise_info.dir {
             cargo_toml.extend_from_slice(dir.as_bytes());
-            cargo_toml.extend_from_slice(b"/");
+            cargo_toml.push(b'/');
         }
         cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
         cargo_toml.extend_from_slice(b".rs\" },\n");

From 8aef915ee732af1480cd7b93818f7d71c3ba178c Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 16:03:49 +0200
Subject: [PATCH 105/109] Show the welcome message

---
 src/app_state.rs | 87 +++++++++++++++++++++++++++---------------------
 src/main.rs      | 32 ++++++++++++++++--
 2 files changed, 79 insertions(+), 40 deletions(-)

diff --git a/src/app_state.rs b/src/app_state.rs
index 31cb2cbf..fb4b92e7 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -9,7 +9,7 @@ use std::{
     io::{Read, StdoutLock, Write},
 };
 
-use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE};
+use crate::{exercise::Exercise, info_file::ExerciseInfo, FENISH_LINE};
 
 const STATE_FILE_NAME: &str = ".rustlings-state.txt";
 const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
@@ -20,58 +20,69 @@ pub enum ExercisesProgress {
     Pending,
 }
 
+pub enum StateFileStatus {
+    Read,
+    NotRead,
+}
+
 pub struct AppState {
     current_exercise_ind: usize,
     exercises: Vec<Exercise>,
     n_done: u16,
-    welcome_message: String,
     final_message: String,
     file_buf: Vec<u8>,
 }
 
 impl AppState {
-    fn update_from_file(&mut self) {
+    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_ok()
+            .is_err()
         {
-            let mut lines = self.file_buf.split(|c| *c == b'\n');
-            let Some(current_exercise_name) = lines.next() else {
-                return;
-            };
+            return StateFileStatus::NotRead;
+        }
 
-            if lines.next().is_none() {
-                return;
+        // See `Self::write` for more information about the file format.
+        let mut lines = self.file_buf.split(|c| *c == b'\n');
+        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 = hashbrown::HashSet::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;
             }
 
-            let mut done_exercises = hashbrown::HashSet::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;
-                }
+            if exercise.name.as_bytes() == current_exercise_name {
+                self.current_exercise_ind = ind;
             }
         }
+
+        StateFileStatus::Read
     }
 
-    pub fn new(info_file: InfoFile) -> Self {
-        let exercises = info_file
-            .exercises
+    pub fn new(
+        exercise_infos: Vec<ExerciseInfo>,
+        final_message: String,
+    ) -> (Self, StateFileStatus) {
+        let exercises = exercise_infos
             .into_iter()
             .map(|mut exercise_info| {
                 // Leaking to be able to borrow in the watch mode `Table`.
@@ -98,14 +109,13 @@ impl AppState {
             current_exercise_ind: 0,
             exercises,
             n_done: 0,
-            welcome_message: info_file.welcome_message.unwrap_or_default(),
-            final_message: info_file.final_message.unwrap_or_default(),
+            final_message,
             file_buf: Vec::with_capacity(2048),
         };
 
-        slf.update_from_file();
+        let state_file_status = slf.update_from_file();
 
-        slf
+        (slf, state_file_status)
     }
 
     #[inline]
@@ -231,7 +241,8 @@ impl AppState {
 
     // Write the state file.
     // The file's format is very simple:
-    // - The first line is the name of the current exercise.
+    // - The first line is the name of the current exercise. It must end with `\n` even if there
+    // are no done exercises.
     // - The second line is an empty line.
     // - All remaining lines are the names of done exercises.
     fn write(&mut self) -> Result<()> {
@@ -239,12 +250,12 @@ impl AppState {
 
         self.file_buf
             .extend_from_slice(self.current_exercise().name.as_bytes());
-        self.file_buf.extend_from_slice(b"\n\n");
+        self.file_buf.push(b'\n');
 
         for exercise in &self.exercises {
             if exercise.done {
-                self.file_buf.extend_from_slice(exercise.name.as_bytes());
                 self.file_buf.push(b'\n');
+                self.file_buf.extend_from_slice(exercise.name.as_bytes());
             }
         }
 
diff --git a/src/main.rs b/src/main.rs
index a96e3230..aeb94321 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,15 @@
 use anyhow::{Context, Result};
+use app_state::StateFileStatus;
 use clap::{Parser, Subcommand};
-use std::{path::Path, process::exit};
+use crossterm::{
+    terminal::{Clear, ClearType},
+    ExecutableCommand,
+};
+use std::{
+    io::{self, BufRead, Write},
+    path::Path,
+    process::exit,
+};
 
 mod app_state;
 mod embedded;
@@ -67,7 +76,26 @@ fn main() -> Result<()> {
         exit(1);
     }
 
-    let mut app_state = AppState::new(info_file);
+    let (mut app_state, state_file_status) = AppState::new(
+        info_file.exercises,
+        info_file.final_message.unwrap_or_default(),
+    );
+
+    if let Some(welcome_message) = info_file.welcome_message {
+        match state_file_status {
+            StateFileStatus::NotRead => {
+                let mut stdout = io::stdout().lock();
+                stdout.execute(Clear(ClearType::All))?;
+
+                let welcome_message = welcome_message.trim();
+                write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
+                stdout.flush()?;
+
+                io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
+            }
+            StateFileStatus::Read => (),
+        }
+    }
 
     match args.command {
         None => {

From 070a780d7f7ca4ef03ab29898ec553933994bfab Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 16:04:05 +0200
Subject: [PATCH 106/109] Trim the final message

---
 src/app_state.rs | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/app_state.rs b/src/app_state.rs
index fb4b92e7..432a9a27 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -228,8 +228,12 @@ impl AppState {
 
             writer.execute(Clear(ClearType::All))?;
             writer.write_all(FENISH_LINE.as_bytes())?;
-            writer.write_all(self.final_message.as_bytes())?;
-            writer.write_all(b"\n")?;
+
+            let final_message = self.final_message.trim();
+            if !final_message.is_empty() {
+                writer.write_all(self.final_message.as_bytes())?;
+                writer.write_all(b"\n")?;
+            }
 
             return Ok(ExercisesProgress::AllDone);
         };

From bd10b154fe558af693e9f8f57dbb3e43f0bd0ec8 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 16:07:17 +0200
Subject: [PATCH 107/109] Clear the terminal after showing the welcome message

---
 src/main.rs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main.rs b/src/main.rs
index aeb94321..67969215 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -92,6 +92,8 @@ fn main() -> Result<()> {
                 stdout.flush()?;
 
                 io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
+
+                stdout.execute(Clear(ClearType::All))?;
             }
             StateFileStatus::Read => (),
         }

From 1cbabc3d28a29a01caeffba969ed640e00e5f0be Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 17:10:53 +0200
Subject: [PATCH 108/109] Add the manual-run option

---
 src/main.rs                 | 28 +++++++++++++-------
 src/watch.rs                | 51 +++++++++++++++++++++++++++----------
 src/watch/state.rs          |  8 +++++-
 src/watch/terminal_event.rs |  4 ++-
 4 files changed, 66 insertions(+), 25 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 67969215..28a426b7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -36,6 +36,10 @@ use self::{
 struct Args {
     #[command(subcommand)]
     command: Option<Subcommands>,
+    /// Manually run the current exercise using `r` or `run` in the watch mode.
+    /// Only use this if Rustlings fails to detect exercise file changes.
+    #[arg(long)]
+    manual_run: bool,
 }
 
 #[derive(Subcommand)]
@@ -101,17 +105,23 @@ fn main() -> Result<()> {
 
     match args.command {
         None => {
-            // For the the notify event handler thread.
-            // Leaking is not a problem because the slice lives until the end of the program.
-            let exercise_paths = app_state
-                .exercises()
-                .iter()
-                .map(|exercise| exercise.path)
-                .collect::<Vec<_>>()
-                .leak();
+            let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run {
+                None
+            } else {
+                // For the the notify event handler thread.
+                // Leaking is not a problem because the slice lives until the end of the program.
+                Some(
+                    app_state
+                        .exercises()
+                        .iter()
+                        .map(|exercise| exercise.path)
+                        .collect::<Vec<_>>()
+                        .leak(),
+                )
+            };
 
             loop {
-                match watch(&mut app_state, exercise_paths)? {
+                match watch(&mut app_state, notify_exercise_paths)? {
                     WatchExit::Shutdown => break,
                     // It is much easier to exit the watch mode, launch the list mode and then restart
                     // the watch mode instead of trying to pause the watch threads and correct the
diff --git a/src/watch.rs b/src/watch.rs
index bab64ae1..d20e552e 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -42,25 +42,38 @@ pub enum WatchExit {
 
 pub fn watch(
     app_state: &mut AppState,
-    exercise_paths: &'static [&'static str],
+    notify_exercise_paths: Option<&'static [&'static str]>,
 ) -> Result<WatchExit> {
     let (tx, rx) = channel();
-    let mut debouncer = new_debouncer(
-        Duration::from_secs(1),
-        DebounceEventHandler {
-            tx: tx.clone(),
-            exercise_paths,
-        },
-    )?;
-    debouncer
-        .watcher()
-        .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
 
-    let mut watch_state = WatchState::new(app_state);
+    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_paths) = notify_exercise_paths {
+        let mut debouncer = new_debouncer(
+            Duration::from_secs(1),
+            DebounceEventHandler {
+                tx: tx.clone(),
+                exercise_paths,
+            },
+        )
+        .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
+        debouncer
+            .watcher()
+            .watch(Path::new("exercises"), RecursiveMode::Recursive)
+            .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
+
+        Some(debouncer)
+    } else {
+        manual_run = true;
+        None
+    };
+
+    let mut watch_state = WatchState::new(app_state, manual_run);
 
     watch_state.run_current_exercise()?;
 
-    thread::spawn(move || terminal_event_handler(tx));
+    thread::spawn(move || terminal_event_handler(tx, manual_run));
 
     while let Ok(event) = rx.recv() {
         match event {
@@ -78,6 +91,7 @@ pub fn watch(
                 watch_state.into_writer().write_all(QUIT_MSG)?;
                 break;
             }
+            WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
             WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
                 watch_state.handle_invalid_cmd(&cmd)?;
             }
@@ -88,7 +102,8 @@ pub fn watch(
                 watch_state.render()?;
             }
             WatchEvent::NotifyErr(e) => {
-                return Err(Error::from(e).context("Exercise file watcher failed"));
+                watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?;
+                return Err(Error::from(e));
             }
             WatchEvent::TerminalEventErr(e) => {
                 return Err(Error::from(e).context("Terminal event listener failed"));
@@ -103,3 +118,11 @@ 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.
 ";
+
+const NOTIFY_ERR: &str = "
+The automatic detection of exercise file changes failed :(
+Please try running `rustlings` again.
+
+If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher.
+You need to manually trigger running the current exercise using `r` or `run` then.
+";
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 1a79573f..c0f6c532 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -18,10 +18,11 @@ pub struct WatchState<'a> {
     stderr: Option<Vec<u8>>,
     show_hint: bool,
     show_done: bool,
+    manual_run: bool,
 }
 
 impl<'a> WatchState<'a> {
-    pub fn new(app_state: &'a mut AppState) -> Self {
+    pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
         let writer = io::stdout().lock();
 
         Self {
@@ -31,6 +32,7 @@ impl<'a> WatchState<'a> {
             stderr: None,
             show_hint: false,
             show_done: false,
+            manual_run,
         }
     }
 
@@ -78,6 +80,10 @@ impl<'a> WatchState<'a> {
     fn show_prompt(&mut self) -> io::Result<()> {
         self.writer.write_all(b"\n")?;
 
+        if self.manual_run {
+            self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?;
+        }
+
         if self.show_done {
             self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?;
         }
diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs
index 7f7ebe06..6d790b7c 100644
--- a/src/watch/terminal_event.rs
+++ b/src/watch/terminal_event.rs
@@ -4,6 +4,7 @@ use std::sync::mpsc::Sender;
 use super::WatchEvent;
 
 pub enum InputEvent {
+    Run,
     Next,
     Hint,
     List,
@@ -11,7 +12,7 @@ pub enum InputEvent {
     Unrecognized(String),
 }
 
-pub fn terminal_event_handler(tx: Sender<WatchEvent>) {
+pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
     let mut input = String::with_capacity(8);
 
     let last_input_event = loop {
@@ -43,6 +44,7 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>) {
                             "h" | "hint" => InputEvent::Hint,
                             "l" | "list" => break InputEvent::List,
                             "q" | "quit" => break InputEvent::Quit,
+                            "r" | "run" if manual_run => InputEvent::Run,
                             _ => InputEvent::Unrecognized(input.clone()),
                         };
 

From 7526c6b1f92626df6ab8b4853535b73711bfada4 Mon Sep 17 00:00:00 2001
From: mo8it <mo8it@proton.me>
Date: Sun, 14 Apr 2024 17:11:27 +0200
Subject: [PATCH 109/109]  Update POST_INIT_MSG

---
 src/main.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main.rs b/src/main.rs
index 28a426b7..ed5becf5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -174,7 +174,7 @@ const POST_INIT_MSG: &str = "
 Done initialization!
 
 Run `cd rustlings` to go into the generated directory.
-Then run `rustlings` for further instructions on getting started.";
+Then run `rustlings` to get started.";
 
 const FENISH_LINE: &str = "+----------------------------------------------------+
 |          You made it to the Fe-nish line!          |