Compare commits

...

24 commits

Author SHA1 Message Date
mo8it cf3f6fd6a1 Fix typo 2024-05-14 01:50:03 +02:00
mo8it c8481d35c1 Done documentation 2024-05-14 01:49:22 +02:00
mo8it 96a44f3dcf Make it more clear that only one char is expected 2024-05-14 01:23:58 +02:00
mo8it 0ae66d1860 Remove inline 2024-05-14 00:55:07 +02:00
mo8it 700605ff35 Document init 2024-05-14 00:35:12 +02:00
mo8it a67e63cce0 Document info_file 2024-05-13 22:02:45 +02:00
mo8it d48e86b154 Use public comments for public items 2024-05-13 21:40:40 +02:00
mo8it 39a19f9450 Document exercise 2024-05-13 21:36:20 +02:00
mo8it 2dfc7cdb1a Document embedded 2024-05-13 21:07:04 +02:00
mo8it 0add5ac240 chore: Release 2024-05-13 17:14:11 +02:00
mo8it 5a1d95028c Update version in README 2024-05-13 17:14:00 +02:00
mo8it e80e91faf2 Thanks Clippy :) 2024-05-13 17:12:58 +02:00
mo8it 4ae3fcc3ca Don't skip exercises on file changes 2024-05-13 17:06:11 +02:00
mo8it 17a2d42ffd Better variable naming 2024-05-13 16:44:48 +02:00
mo8it a7bc6d53a5 Only send Unrecognized on ENTER if the last input wasn't valid 2024-05-13 16:39:38 +02:00
mo8it 56eb4a5d65 chore: Release 2024-05-13 04:11:29 +02:00
mo8it f6cf6c611c Fix Windows terminal links 2024-05-13 04:11:11 +02:00
mo8it 7a74a72dc8 Update beta version in README 2024-05-13 02:48:42 +02:00
mo8it a4da216a5c chore: Release 2024-05-13 02:46:26 +02:00
mo8it 8b2d9ed503 Use PartialEq instead of matches! 2024-05-13 02:45:12 +02:00
mo8it d2b5906be2 No more word input 2024-05-13 02:37:32 +02:00
mo8it f9e35a4344 Improve input handling 2024-05-13 02:32:25 +02:00
mo8it 0525739046 Fix invisible input on Windows 2024-05-13 02:20:04 +02:00
mo8it 11fda5d70f Move info.toml to rustlings-macros/
This improves the experience for contributors on Windows becuase
Windows can't deal with git symbolic links out of the box…
2024-05-13 01:25:38 +02:00
24 changed files with 1525 additions and 1467 deletions

View file

@ -37,7 +37,7 @@ Please be patient 😇
- Name the file `exercises/yourTopic/yourTopicN.rs`. - Name the file `exercises/yourTopic/yourTopicN.rs`.
- Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`. - Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`.
- Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it. - Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it.
- Add the [metadata for your exercise](#exercise-metadata) in the `info.toml` file. - Add the [metadata for your exercise](#exercise-metadata) in the `rustlings-macros/info.toml` file.
- Make sure your exercise runs with `rustlings run yourTopicN`. - Make sure your exercise runs with `rustlings run yourTopicN`.
- [Open a pull request](#pull-requests). - [Open a pull request](#pull-requests).

4
Cargo.lock generated
View file

@ -656,7 +656,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]] [[package]]
name = "rustlings" name = "rustlings"
version = "6.0.0-beta.6" version = "6.0.0-beta.9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
@ -675,7 +675,7 @@ dependencies = [
[[package]] [[package]]
name = "rustlings-macros" name = "rustlings-macros"
version = "6.0.0-beta.6" version = "6.0.0-beta.9"
dependencies = [ dependencies = [
"quote", "quote",
"serde", "serde",

View file

@ -8,7 +8,7 @@ exclude = [
] ]
[workspace.package] [workspace.package]
version = "6.0.0-beta.6" version = "6.0.0-beta.9"
authors = [ authors = [
"Liv <mokou@fastmail.com>", "Liv <mokou@fastmail.com>",
"Mo Bitar <mo8it@proton.me>", "Mo Bitar <mo8it@proton.me>",
@ -39,7 +39,6 @@ include = [
"/src/", "/src/",
"/exercises/", "/exercises/",
"/solutions/", "/solutions/",
"/info.toml",
# A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded. # A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded.
"/dev-Cargo.toml", "/dev-Cargo.toml",
"/README.md", "/README.md",
@ -54,7 +53,7 @@ hashbrown = "0.14.5"
notify-debouncer-mini = { version = "0.4.1", default-features = false } notify-debouncer-mini = { version = "0.4.1", default-features = false }
os_pipe = "1.1.5" os_pipe = "1.1.5"
ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] }
rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.6" } rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" }
serde_json = "1.0.117" serde_json = "1.0.117"
serde.workspace = true serde.workspace = true
toml_edit.workspace = true toml_edit.workspace = true

View file

@ -35,7 +35,7 @@ The following command will download and compile Rustlings:
<!-- TODO: Remove @6.0.0-beta.x --> <!-- TODO: Remove @6.0.0-beta.x -->
```bash ```bash
cargo install rustlings@6.0.0-beta.6 cargo install rustlings@6.0.0-beta.9
``` ```
<details> <details>
@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.6
<!-- TODO: Remove @6.0.0-beta.x --> <!-- TODO: Remove @6.0.0-beta.x -->
- Make sure you have the latest Rust version by running `rustup update` - Make sure you have the latest Rust version by running `rustup update`
- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.6 --locked` - Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.9 --locked`
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new) - Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
</details> </details>
@ -98,7 +98,7 @@ It will rerun the current exercise automatically every time you change the exerc
<details> <details>
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary> <summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` (or `run`) in the watch mode. > You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
> >
> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL). > Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
@ -106,7 +106,7 @@ It will rerun the current exercise automatically every time you change the exerc
### Exercise List ### Exercise List
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` (or `list`) to open the interactive exercise list. In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
The list allows you to… The list allows you to…

View file

@ -1,6 +1,6 @@
// We sometimes encourage you to keep trying things on a given exercise, even // We sometimes encourage you to keep trying things on a given exercise, even
// after you already figured it out. If you got everything working and feel // after you already figured it out. If you got everything working and feel
// ready for the next exercise, enter `n` (or `next`) in the terminal. // ready for the next exercise, enter `n` in the terminal.
// //
// The exercise file will be reloaded when you change one of the lines below! // The exercise file will be reloaded when you change one of the lines below!
// Try adding a new `println!`. // Try adding a new `println!`.

1286
info.toml

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
../info.toml

1286
rustlings-macros/info.toml Normal file

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,8 @@ struct InfoFile {
#[proc_macro] #[proc_macro]
pub fn include_files(_: TokenStream) -> TokenStream { pub fn include_files(_: TokenStream) -> TokenStream {
let exercises = toml_edit::de::from_str::<InfoFile>(include_str!("../info.toml")) let info_file = include_str!("../info.toml");
let exercises = toml_edit::de::from_str::<InfoFile>(info_file)
.expect("Failed to parse `info.toml`") .expect("Failed to parse `info.toml`")
.exercises; .exercises;
@ -46,6 +47,7 @@ pub fn include_files(_: TokenStream) -> TokenStream {
quote! { quote! {
EmbeddedFiles { EmbeddedFiles {
info_file: #info_file,
exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*], exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*],
exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*] exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*]
} }

View file

@ -21,8 +21,12 @@ const BAD_INDEX_ERR: &str = "The current exercise index is higher than the numbe
#[must_use] #[must_use]
pub enum ExercisesProgress { pub enum ExercisesProgress {
// All exercises are done.
AllDone, AllDone,
Pending, // The current exercise failed and is still pending.
CurrentPending,
// A new exercise is now pending.
NewPending,
} }
pub enum StateFileStatus { pub enum StateFileStatus {
@ -120,7 +124,27 @@ impl AppState {
let exercises = exercise_infos let exercises = exercise_infos
.into_iter() .into_iter()
.map(Exercise::from) .map(|exercise_info| {
// Leaking to be able to borrow in the watch mode `Table`.
// Leaking is not a problem because the `AppState` instance lives until
// the end of the program.
let path = exercise_info.path().leak();
let name = exercise_info.name.leak();
let dir = exercise_info.dir.map(|dir| &*dir.leak());
let hint = exercise_info.hint.trim().to_owned();
Exercise {
dir,
name,
path,
test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy,
hint,
// Updated in `Self::update_from_file`.
done: false,
}
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut slf = Self { let mut slf = Self {
@ -194,6 +218,10 @@ impl AppState {
} }
pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> { pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> {
if exercise_ind == self.current_exercise_ind {
return Ok(());
}
if exercise_ind >= self.exercises.len() { if exercise_ind >= self.exercises.len() {
bail!(BAD_INDEX_ERR); bail!(BAD_INDEX_ERR);
} }
@ -302,8 +330,8 @@ impl AppState {
} }
} }
// Official exercises: Dump the solution file form the binary and return its path. /// Official exercises: Dump the solution file form the binary and return its path.
// Third-party exercises: Check if a solution file exists and return its path in that case. /// Third-party exercises: Check if a solution file exists and return its path in that case.
pub fn current_solution_path(&self) -> Result<Option<String>> { pub fn current_solution_path(&self) -> Result<Option<String>> {
if DEBUG_PROFILE { if DEBUG_PROFILE {
return Ok(None); return Ok(None);
@ -330,9 +358,9 @@ impl AppState {
} }
} }
// Mark the current exercise as done and move on to the next pending exercise if one exists. /// Mark the current exercise as done and move on to the next pending exercise if one exists.
// If all exercises are marked as done, run all of them to make sure that they are actually /// If all exercises are marked as done, run all of them to make sure that they are actually
// done. If an exercise which is marked as done fails, mark it as pending and continue on it. /// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> { pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
let exercise = &mut self.exercises[self.current_exercise_ind]; let exercise = &mut self.exercises[self.current_exercise_ind];
if !exercise.done { if !exercise.done {
@ -343,7 +371,7 @@ impl AppState {
if let Some(ind) = self.next_pending_exercise_ind() { if let Some(ind) = self.next_pending_exercise_ind() {
self.set_current_exercise_ind(ind)?; self.set_current_exercise_ind(ind)?;
return Ok(ExercisesProgress::Pending); return Ok(ExercisesProgress::NewPending);
} }
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
@ -366,7 +394,7 @@ impl AppState {
self.write()?; self.write()?;
return Ok(ExercisesProgress::Pending); return Ok(ExercisesProgress::NewPending);
} }
writeln!(writer, "{}", "ok".green())?; writeln!(writer, "{}", "ok".green())?;

View file

@ -2,10 +2,10 @@ use anyhow::{Context, Result};
use crate::info_file::ExerciseInfo; use crate::info_file::ExerciseInfo;
// Return the start and end index of the content of the list `bin = […]`. /// Return the start and end index of the content of the list `bin = […]`.
// bin = [xxxxxxxxxxxxxxxxx] /// bin = [xxxxxxxxxxxxxxxxx]
// |start_ind | /// |start_ind |
// |end_ind /// |end_ind
pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> {
let start_ind = cargo_toml let start_ind = cargo_toml
.find("bin = [") .find("bin = [")
@ -20,8 +20,8 @@ pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> {
Ok((start_ind, end_ind)) Ok((start_ind, end_ind))
} }
// Generate and append the content of the `bin` list in `Cargo.toml`. /// Generate and append the content of the `bin` list in `Cargo.toml`.
// The `exercise_path_prefix` is the prefix of the `path` field of every list entry. /// The `exercise_path_prefix` is the prefix of the `path` field of every list entry.
pub fn append_bins( pub fn append_bins(
buf: &mut Vec<u8>, buf: &mut Vec<u8>,
exercise_infos: &[ExerciseInfo], exercise_infos: &[ExerciseInfo],
@ -43,7 +43,7 @@ pub fn append_bins(
} }
} }
// Update the `bin` list and leave everything else unchanged. /// Update the `bin` list and leave everything else unchanged.
pub fn updated_cargo_toml( pub fn updated_cargo_toml(
exercise_infos: &[ExerciseInfo], exercise_infos: &[ExerciseInfo],
current_cargo_toml: &str, current_cargo_toml: &str,

View file

@ -1,8 +1,8 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::{io::Read, path::Path, process::Command}; use std::{io::Read, path::Path, process::Command};
// Run a command with a description for a possible error and append the merged stdout and stderr. /// Run a command with a description for a possible error and append the merged stdout and stderr.
// The boolean in the returned `Result` is true if the command's exit status is success. /// The boolean in the returned `Result` is true if the command's exit status is success.
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> { pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> {
let (mut reader, writer) = os_pipe::pipe() let (mut reader, writer) = os_pipe::pipe()
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; .with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?;
@ -37,18 +37,18 @@ pub struct CargoCmd<'a> {
pub args: &'a [&'a str], pub args: &'a [&'a str],
pub exercise_name: &'a str, pub exercise_name: &'a str,
pub description: &'a str, pub description: &'a str,
// RUSTFLAGS="-A warnings" /// RUSTFLAGS="-A warnings"
pub hide_warnings: bool, pub hide_warnings: bool,
// Added as `--target-dir` if `Self::dev` is true. /// Added as `--target-dir` if `Self::dev` is true.
pub target_dir: &'a Path, pub target_dir: &'a Path,
// The output buffer to append the merged stdout and stderr. /// The output buffer to append the merged stdout and stderr.
pub output: &'a mut Vec<u8>, pub output: &'a mut Vec<u8>,
// true while developing Rustlings. /// true while developing Rustlings.
pub dev: bool, pub dev: bool,
} }
impl<'a> CargoCmd<'a> { impl<'a> CargoCmd<'a> {
// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`. /// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
pub fn run(&mut self) -> Result<bool> { pub fn run(&mut self) -> Result<bool> {
let mut cmd = Command::new("cargo"); let mut cmd = Command::new("cargo");
cmd.arg(self.subcommand); cmd.arg(self.subcommand);

View file

@ -6,6 +6,7 @@ use std::{
use crate::info_file::ExerciseInfo; use crate::info_file::ExerciseInfo;
/// Contains all embedded files.
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -31,12 +32,17 @@ impl WriteStrategy {
} }
} }
// Files related to one exercise.
struct ExerciseFiles { struct ExerciseFiles {
// The content of the exercise file.
exercise: &'static [u8], exercise: &'static [u8],
// The content of the solution file.
solution: &'static [u8], solution: &'static [u8],
// Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`.
dir_ind: usize, dir_ind: usize,
} }
// A directory in the `exercises/` directory.
struct ExerciseDir { struct ExerciseDir {
name: &'static str, name: &'static str,
readme: &'static [u8], readme: &'static [u8],
@ -63,18 +69,20 @@ impl ExerciseDir {
let mut readme_path = dir_path; let mut readme_path = dir_path;
readme_path.push_str("/README.md"); readme_path.push_str("/README.md");
WriteStrategy::Overwrite.write(&readme_path, self.readme)?; WriteStrategy::Overwrite.write(&readme_path, self.readme)
Ok(())
} }
} }
/// All embedded files.
pub struct EmbeddedFiles { pub struct EmbeddedFiles {
/// The content of the `info.toml` file.
pub info_file: &'static str,
exercise_files: &'static [ExerciseFiles], exercise_files: &'static [ExerciseFiles],
exercise_dirs: &'static [ExerciseDir], exercise_dirs: &'static [ExerciseDir],
} }
impl EmbeddedFiles { impl EmbeddedFiles {
/// Dump all the embedded files of the `exercises/` directory.
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
create_dir("exercises").context("Failed to create the directory `exercises`")?; create_dir("exercises").context("Failed to create the directory `exercises`")?;
@ -95,21 +103,21 @@ impl EmbeddedFiles {
} }
pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> { pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> {
let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind]; let exercise_files = &self.exercise_files[exercise_ind];
let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind];
dir.init_on_disk()?; dir.init_on_disk()?;
WriteStrategy::Overwrite.write(path, exercise_files.exercise) WriteStrategy::Overwrite.write(path, exercise_files.exercise)
} }
// Write the solution file to disk and return its path. /// Write the solution file to disk and return its path.
pub fn write_solution_to_disk( pub fn write_solution_to_disk(
&self, &self,
exercise_ind: usize, exercise_ind: usize,
exercise_name: &str, exercise_name: &str,
) -> Result<String> { ) -> Result<String> {
let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind]; let exercise_files = &self.exercise_files[exercise_ind];
let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind];
// 14 = 10 + 1 + 3 // 14 = 10 + 1 + 3
// solutions/ + / + .rs // solutions/ + / + .rs
@ -148,7 +156,7 @@ mod tests {
#[test] #[test]
fn dirs() { fn dirs() {
let exercises = toml_edit::de::from_str::<InfoFile>(include_str!("../info.toml")) let exercises = toml_edit::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file)
.expect("Failed to parse `info.toml`") .expect("Failed to parse `info.toml`")
.exercises; .exercises;

View file

@ -10,30 +10,32 @@ use std::{
use crate::{ use crate::{
cmd::{run_cmd, CargoCmd}, cmd::{run_cmd, CargoCmd},
in_official_repo, in_official_repo,
info_file::ExerciseInfo,
terminal_link::TerminalFileLink, terminal_link::TerminalFileLink,
DEBUG_PROFILE, DEBUG_PROFILE,
}; };
/// The initial capacity of the output buffer.
pub const OUTPUT_CAPACITY: usize = 1 << 14; pub const OUTPUT_CAPACITY: usize = 1 << 14;
/// See `info_file::ExerciseInfo`
pub struct Exercise { pub struct Exercise {
pub dir: Option<&'static str>, pub dir: Option<&'static str>,
// Exercise's unique name
pub name: &'static str, pub name: &'static str,
// Exercise's path /// Path of the exercise file starting with the `exercises/` directory.
pub path: &'static str, pub path: &'static str,
pub test: bool, pub test: bool,
pub strict_clippy: bool, pub strict_clippy: bool,
// The hint text associated with the exercise
pub hint: String, pub hint: String,
pub done: bool, pub done: bool,
} }
impl Exercise { impl Exercise {
// Run the exercise's binary and append its output to the `output` buffer.
// Compilation should be done before calling this method.
fn run_bin(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> { fn run_bin(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
writeln!(output, "{}", "Output".underlined())?; writeln!(output, "{}", "Output".underlined())?;
// 7 = "/debug/".len()
let mut bin_path = let mut bin_path =
PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len()); PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len());
bin_path.push(target_dir); bin_path.push(target_dir);
@ -43,18 +45,23 @@ impl Exercise {
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?;
if !success { if !success {
// This output is important to show the user that something went wrong.
// Otherwise, calling something like `exit(1)` in an exercise without further output
// leaves the user confused about why the exercise isn't done yet.
writeln!( writeln!(
output, output,
"{}", "{}",
"The exercise didn't run successfully (nonzero exit code)" "The exercise didn't run successfully (nonzero exit code)"
.bold() .bold()
.red() .red(),
)?; )?;
} }
Ok(success) Ok(success)
} }
/// Compile, check and run the exercise.
/// The output is written to the `output` buffer after clearing it.
pub fn run(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> { pub fn run(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
output.clear(); output.clear();
@ -76,9 +83,10 @@ impl Exercise {
return Ok(false); return Ok(false);
} }
// Discard the output of `cargo build` because it will be shown again by the Cargo command. // Discard the output of `cargo build` because it will be shown again by Clippy.
output.clear(); output.clear();
// `--profile test` is required to also check code with `[cfg(test)]`.
let clippy_args: &[&str] = if self.strict_clippy { let clippy_args: &[&str] = if self.strict_clippy {
&["--profile", "test", "--", "-D", "warnings"] &["--profile", "test", "--", "-D", "warnings"]
} else { } else {
@ -126,34 +134,6 @@ impl Exercise {
} }
} }
impl From<ExerciseInfo> for Exercise {
fn from(mut exercise_info: ExerciseInfo) -> Self {
// Leaking to be able to borrow in the watch mode `Table`.
// Leaking is not a problem because the `AppState` instance lives until
// the end of the program.
let path = exercise_info.path().leak();
exercise_info.name.shrink_to_fit();
let name = exercise_info.name.leak();
let dir = exercise_info.dir.map(|mut dir| {
dir.shrink_to_fit();
&*dir.leak()
});
let hint = exercise_info.hint.trim().to_owned();
Exercise {
dir,
name,
path,
test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy,
hint,
done: false,
}
}
}
impl Display for Exercise { impl Display for Exercise {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.path.fmt(f) self.path.fmt(f)

View file

@ -2,44 +2,71 @@ use anyhow::{bail, Context, Error, Result};
use serde::Deserialize; use serde::Deserialize;
use std::{fs, io::ErrorKind}; use std::{fs, io::ErrorKind};
// Deserialized from the `info.toml` file. use crate::embedded::EMBEDDED_FILES;
/// Deserialized from the `info.toml` file.
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ExerciseInfo { pub struct ExerciseInfo {
// Name of the exercise /// Exercise's unique name.
pub name: String, pub name: String,
// The exercise's directory inside the `exercises` directory /// Exercise's directory name inside the `exercises/` directory.
pub dir: Option<String>, pub dir: Option<String>,
#[serde(default = "default_true")] #[serde(default = "default_true")]
/// Run `cargo test` on the exercise.
pub test: bool, pub test: bool,
/// Deny all Clippy warnings.
#[serde(default)] #[serde(default)]
pub strict_clippy: bool, pub strict_clippy: bool,
// The hint text associated with the exercise /// The exercise's hint to be shown to the user on request.
pub hint: String, pub hint: String,
} }
#[inline] #[inline(always)]
const fn default_true() -> bool { const fn default_true() -> bool {
true true
} }
impl ExerciseInfo { impl ExerciseInfo {
/// Path to the exercise file starting with the `exercises/` directory.
pub fn path(&self) -> String { pub fn path(&self) -> String {
if let Some(dir) = &self.dir { let mut path = if let Some(dir) = &self.dir {
format!("exercises/{dir}/{}.rs", self.name) // 14 = 10 + 1 + 3
// exercises/ + / + .rs
let mut path = String::with_capacity(14 + dir.len() + self.name.len());
path.push_str("exercises/");
path.push_str(dir);
path.push('/');
path
} else { } else {
format!("exercises/{}.rs", self.name) // 13 = 10 + 3
} // exercises/ + .rs
let mut path = String::with_capacity(13 + self.name.len());
path.push_str("exercises/");
path
};
path.push_str(&self.name);
path.push_str(".rs");
path
} }
} }
/// The deserialized `info.toml` file.
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct InfoFile { pub struct InfoFile {
/// For possible breaking changes in the future for third-party exercises.
pub format_version: u8, pub format_version: u8,
/// Shown to users when starting with the exercises.
pub welcome_message: Option<String>, pub welcome_message: Option<String>,
/// Shown to users after finishing all exercises.
pub final_message: Option<String>, pub final_message: Option<String>,
/// List of all exercises.
pub exercises: Vec<ExerciseInfo>, pub exercises: Vec<ExerciseInfo>,
} }
impl InfoFile { impl InfoFile {
/// Official exercises: Parse the embedded `info.toml` file.
/// Third-party exercises: Parse the `info.toml` file in the current directory.
pub fn parse() -> Result<Self> { pub fn parse() -> Result<Self> {
// Read a local `info.toml` if it exists. // Read a local `info.toml` if it exists.
let slf = match fs::read_to_string("info.toml") { let slf = match fs::read_to_string("info.toml") {
@ -47,7 +74,7 @@ impl InfoFile {
.context("Failed to parse the `info.toml` file")?, .context("Failed to parse the `info.toml` file")?,
Err(e) => { Err(e) => {
if e.kind() == ErrorKind::NotFound { if e.kind() == ErrorKind::NotFound {
return toml_edit::de::from_str(include_str!("../info.toml")) return toml_edit::de::from_str(EMBEDDED_FILES.info_file)
.context("Failed to parse the embedded `info.toml` file"); .context("Failed to parse the embedded `info.toml` file");
} }

View file

@ -11,8 +11,11 @@ use std::{
use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile}; use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile};
pub fn init() -> Result<()> { pub fn init() -> Result<()> {
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { // Prevent initialization in a directory that contains the file `Cargo.toml`.
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); // This can mean that Rustlings was already initialized in this directory.
// Otherwise, this can cause problems with Cargo workspaces.
if Path::new("Cargo.toml").exists() {
bail!(CARGO_TOML_EXISTS_ERR);
} }
let rustlings_path = Path::new("rustlings"); let rustlings_path = Path::new("rustlings");
@ -24,7 +27,7 @@ pub fn init() -> Result<()> {
} }
set_current_dir("rustlings") set_current_dir("rustlings")
.context("Failed to change the current directory to `rustlings`")?; .context("Failed to change the current directory to `rustlings/`")?;
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
EMBEDDED_FILES EMBEDDED_FILES
@ -37,9 +40,10 @@ pub fn init() -> Result<()> {
.as_bytes() .as_bytes()
.iter() .iter()
.position(|c| *c == b'\n') .position(|c| *c == b'\n')
.context("The embedded `Cargo.toml` is empty or contains only one line.")?; .context("The embedded `Cargo.toml` is empty or contains only one line")?;
let current_cargo_toml = let current_cargo_toml = current_cargo_toml
&current_cargo_toml[(newline_ind + 1).min(current_cargo_toml.len() - 1)..]; .get(newline_ind + 1..)
.context("The embedded `Cargo.toml` contains only one line")?;
let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"") let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"")
.context("Failed to generate `Cargo.toml`")?; .context("Failed to generate `Cargo.toml`")?;
fs::write("Cargo.toml", updated_cargo_toml) fs::write("Cargo.toml", updated_cargo_toml)
@ -77,12 +81,10 @@ target
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str = const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`.
"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."; If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
Otherwise, please run `rustlings init` again in another directory.";
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str = const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
"A directory with the name `rustlings` already exists in the current directory. "A directory with the name `rustlings` already exists in the current directory.

View file

@ -148,14 +148,12 @@ impl<'a> UiState<'a> {
} }
} }
#[inline]
pub fn select_first(&mut self) { pub fn select_first(&mut self) {
if self.n_rows > 0 { if self.n_rows > 0 {
self.table_state.select(Some(0)); self.table_state.select(Some(0));
} }
} }
#[inline]
pub fn select_last(&mut self) { pub fn select_last(&mut self) {
if self.n_rows > 0 { if self.n_rows > 0 {
self.table_state.select(Some(self.n_rows - 1)); self.table_state.select(Some(self.n_rows - 1));

View file

@ -56,7 +56,7 @@ fn press_enter_prompt() -> io::Result<()> {
struct Args { struct Args {
#[command(subcommand)] #[command(subcommand)]
command: Option<Subcommands>, command: Option<Subcommands>,
/// Manually run the current exercise using `r` or `run` in the watch mode. /// Manually run the current exercise using `r` in the watch mode.
/// Only use this if Rustlings fails to detect exercise file changes. /// Only use this if Rustlings fails to detect exercise file changes.
#[arg(long)] #[arg(long)]
manual_run: bool, manual_run: bool,

View file

@ -12,6 +12,7 @@ const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
const PROGRESS_EXCEEDS_MAX_ERR: &str = const PROGRESS_EXCEEDS_MAX_ERR: &str =
"The progress of the progress bar is higher than the maximum"; "The progress of the progress bar is higher than the maximum";
/// Terminal progress bar to be used when not using Ratataui.
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> { pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
use crossterm::style::Stylize; use crossterm::style::Stylize;
@ -54,6 +55,8 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String
Ok(line) Ok(line)
} }
/// Progress bar to be used with Ratataui.
// Not using Ratatui's Gauge widget to keep the progress bar consistent.
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> { pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
use ratatui::style::Stylize; use ratatui::style::Stylize;

View file

@ -41,7 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
match app_state.done_current_exercise(&mut stdout)? { match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::AllDone => (), ExercisesProgress::AllDone => (),
ExercisesProgress::Pending => println!( ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!(
"Next exercise: {}", "Next exercise: {}",
app_state.current_exercise().terminal_link(), app_state.current_exercise().terminal_link(),
), ),

View file

@ -7,15 +7,18 @@ pub struct TerminalFileLink<'a>(pub &'a str);
impl<'a> Display for TerminalFileLink<'a> { impl<'a> Display for TerminalFileLink<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Ok(Some(canonical_path)) = fs::canonicalize(self.0) let path = fs::canonicalize(self.0);
.as_deref()
.map(|path| path.to_str()) if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) {
{ // Windows itself can't handle its verbatim paths.
write!( #[cfg(windows)]
f, let path = if path.len() > 5 && &path[0..4] == r"\\?\" {
"\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", &path[4..]
canonical_path, self.0, } else {
) path
};
write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0)
} else { } else {
write!(f, "{}", self.0) write!(f, "{}", self.0)
} }

View file

@ -14,7 +14,7 @@ use std::{
use crate::app_state::{AppState, ExercisesProgress}; use crate::app_state::{AppState, ExercisesProgress};
use self::{ use self::{
notify_event::DebounceEventHandler, notify_event::NotifyEventHandler,
state::WatchState, state::WatchState,
terminal_event::{terminal_event_handler, InputEvent}, terminal_event::{terminal_event_handler, InputEvent},
}; };
@ -40,6 +40,7 @@ pub enum WatchExit {
List, List,
} }
/// `notify_exercise_names` as None activates the manual run mode.
pub fn watch( pub fn watch(
app_state: &mut AppState, app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>, notify_exercise_names: Option<&'static [&'static [u8]]>,
@ -52,7 +53,7 @@ pub fn watch(
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names {
let mut debouncer = new_debouncer( let mut debouncer = new_debouncer(
Duration::from_millis(200), Duration::from_millis(200),
DebounceEventHandler { NotifyEventHandler {
tx: tx.clone(), tx: tx.clone(),
exercise_names, exercise_names,
}, },
@ -79,7 +80,8 @@ pub fn watch(
match event { match event {
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? { WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
ExercisesProgress::AllDone => break, ExercisesProgress::AllDone => break,
ExercisesProgress::Pending => watch_state.run_current_exercise()?, ExercisesProgress::CurrentPending => watch_state.render()?,
ExercisesProgress::NewPending => watch_state.run_current_exercise()?,
}, },
WatchEvent::Input(InputEvent::Hint) => { WatchEvent::Input(InputEvent::Hint) => {
watch_state.show_hint()?; watch_state.show_hint()?;
@ -92,11 +94,9 @@ pub fn watch(
break; break;
} }
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?,
watch_state.handle_invalid_cmd(&cmd)?;
}
WatchEvent::FileChange { exercise_ind } => { WatchEvent::FileChange { exercise_ind } => {
watch_state.run_exercise_with_ind(exercise_ind)?; watch_state.handle_file_change(exercise_ind)?;
} }
WatchEvent::TerminalResize => { WatchEvent::TerminalResize => {
watch_state.render()?; watch_state.render()?;
@ -124,5 +124,5 @@ The automatic detection of exercise file changes failed :(
Please try running `rustlings` again. Please try running `rustlings` again.
If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher. 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. You need to manually trigger running the current exercise using `r` then.
"; ";

View file

@ -3,23 +3,24 @@ use std::sync::mpsc::Sender;
use super::WatchEvent; use super::WatchEvent;
pub struct DebounceEventHandler { pub struct NotifyEventHandler {
pub tx: Sender<WatchEvent>, pub tx: Sender<WatchEvent>,
/// Used to report which exercise was modified.
pub exercise_names: &'static [&'static [u8]], pub exercise_names: &'static [&'static [u8]],
} }
impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler {
fn handle_event(&mut self, event: DebounceEventResult) { fn handle_event(&mut self, input_event: DebounceEventResult) {
let event = match event { let output_event = match input_event {
Ok(event) => { Ok(input_event) => {
let Some(exercise_ind) = event let Some(exercise_ind) = input_event
.iter() .iter()
.filter_map(|event| { .filter_map(|input_event| {
if event.kind != DebouncedEventKind::Any { if input_event.kind != DebouncedEventKind::Any {
return None; return None;
} }
let file_name = event.path.file_name()?.to_str()?.as_bytes(); let file_name = input_event.path.file_name()?.to_str()?.as_bytes();
if file_name.len() < 4 { if file_name.len() < 4 {
return None; return None;
@ -46,6 +47,6 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
// An error occurs when the receiver is dropped. // An error occurs when the receiver is dropped.
// After dropping the receiver, the debouncer guard should also be dropped. // After dropping the receiver, the debouncer guard should also be dropped.
let _ = self.tx.send(event); let _ = self.tx.send(output_event);
} }
} }

View file

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use crossterm::{ use crossterm::{
style::{style, Stylize}, style::{style, Stylize},
terminal::size, terminal,
}; };
use std::io::{self, StdoutLock, Write}; use std::io::{self, StdoutLock, Write};
@ -13,6 +13,7 @@ use crate::{
terminal_link::TerminalFileLink, terminal_link::TerminalFileLink,
}; };
#[derive(PartialEq, Eq)]
enum DoneStatus { enum DoneStatus {
DoneWithSolution(String), DoneWithSolution(String),
DoneWithoutSolution, DoneWithoutSolution,
@ -71,17 +72,22 @@ impl<'a> WatchState<'a> {
self.render() self.render()
} }
pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> { pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> {
// Don't skip exercises on file changes to avoid confusion from missing exercises.
// Skipping exercises must be explicit in the interactive list.
// But going back to an earlier exercise on file change is fine.
if self.app_state.current_exercise_ind() < exercise_ind {
return Ok(());
}
self.app_state.set_current_exercise_ind(exercise_ind)?; self.app_state.set_current_exercise_ind(exercise_ind)?;
self.run_current_exercise() self.run_current_exercise()
} }
/// Move on to the next exercise if the current one is done.
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> { pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
if matches!(self.done_status, DoneStatus::Pending) { if self.done_status == DoneStatus::Pending {
self.writer return Ok(ExercisesProgress::CurrentPending);
.write_all(b"The current exercise isn't done yet\n")?;
self.show_prompt()?;
return Ok(ExercisesProgress::Pending);
} }
self.app_state.done_current_exercise(&mut self.writer) self.app_state.done_current_exercise(&mut self.writer)
@ -91,24 +97,24 @@ impl<'a> WatchState<'a> {
self.writer.write_all(b"\n")?; self.writer.write_all(b"\n")?;
if self.manual_run { if self.manual_run {
write!(self.writer, "{}un/", 'r'.bold())?; write!(self.writer, "{}:run / ", 'r'.bold())?;
} }
if !matches!(self.done_status, DoneStatus::Pending) { if self.done_status != DoneStatus::Pending {
write!(self.writer, "{}ext/", 'n'.bold())?; write!(self.writer, "{}:next / ", 'n'.bold())?;
} }
if !self.show_hint { if !self.show_hint {
write!(self.writer, "{}int/", 'h'.bold())?; write!(self.writer, "{}:hint / ", 'h'.bold())?;
} }
write!(self.writer, "{}ist/{}uit? ", 'l'.bold(), 'q'.bold())?; write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?;
self.writer.flush() self.writer.flush()
} }
pub fn render(&mut self) -> Result<()> { pub fn render(&mut self) -> Result<()> {
// Prevent having the first line shifted. // Prevent having the first line shifted if clearing wasn't successful.
self.writer.write_all(b"\n")?; self.writer.write_all(b"\n")?;
clear_terminal(&mut self.writer)?; clear_terminal(&mut self.writer)?;
@ -125,12 +131,12 @@ impl<'a> WatchState<'a> {
)?; )?;
} }
if !matches!(self.done_status, DoneStatus::Pending) { if self.done_status != DoneStatus::Pending {
writeln!( writeln!(
self.writer, self.writer,
"{}\n", "{}\n",
"Exercise done ✓ "Exercise done ✓
When you are done experimenting, enter `n` (or `next`) to move on to the next exercise 🦀" When you are done experimenting, enter `n` to move on to the next exercise 🦀"
.bold() .bold()
.green(), .green(),
)?; )?;
@ -140,11 +146,11 @@ When you are done experimenting, enter `n` (or `next`) to move on to the next ex
writeln!( writeln!(
self.writer, self.writer,
"A solution file can be found at {}\n", "A solution file can be found at {}\n",
style(TerminalFileLink(solution_path)).underlined().green() style(TerminalFileLink(solution_path)).underlined().green(),
)?; )?;
} }
let line_width = size()?.0; let line_width = terminal::size()?.0;
let progress_bar = progress_bar( let progress_bar = progress_bar(
self.app_state.n_done(), self.app_state.n_done(),
self.app_state.exercises().len() as u16, self.app_state.exercises().len() as u16,
@ -165,15 +171,4 @@ When you are done experimenting, enter `n` (or `next`) to move on to the next ex
self.show_hint = true; self.show_hint = true;
self.render() self.render()
} }
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.writer.write_all(b"\n")?;
self.show_prompt()
}
} }

View file

@ -9,11 +9,12 @@ pub enum InputEvent {
Hint, Hint,
List, List,
Quit, Quit,
Unrecognized(String), Unrecognized,
} }
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) { pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
let mut input = String::with_capacity(8); // Only send `Unrecognized` on ENTER if the last input wasn't valid.
let mut last_input_valid = false;
let last_input_event = loop { let last_input_event = loop {
let terminal_event = match event::read() { let terminal_event = match event::read() {
@ -28,37 +29,49 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
match terminal_event { match terminal_event {
Event::Key(key) => { Event::Key(key) => {
match key.kind {
KeyEventKind::Release | KeyEventKind::Repeat => continue,
KeyEventKind::Press => (),
}
if key.modifiers != KeyModifiers::NONE { if key.modifiers != KeyModifiers::NONE {
last_input_valid = false;
continue; continue;
} }
match key.kind { let input_event = match key.code {
KeyEventKind::Release => continue, KeyCode::Enter => {
KeyEventKind::Press | KeyEventKind::Repeat => (), if last_input_valid {
continue;
} }
match key.code { InputEvent::Unrecognized
KeyCode::Enter => { }
let input_event = match input.trim() { KeyCode::Char(c) => {
"n" | "next" => InputEvent::Next, let input_event = match c {
"h" | "hint" => InputEvent::Hint, 'n' => InputEvent::Next,
"l" | "list" => break InputEvent::List, 'h' => InputEvent::Hint,
"q" | "quit" => break InputEvent::Quit, 'l' => break InputEvent::List,
"r" | "run" if manual_run => InputEvent::Run, 'q' => break InputEvent::Quit,
_ => InputEvent::Unrecognized(input.clone()), 'r' if manual_run => InputEvent::Run,
_ => {
last_input_valid = false;
continue;
}
};
last_input_valid = true;
input_event
}
_ => {
last_input_valid = false;
continue;
}
}; };
if tx.send(WatchEvent::Input(input_event)).is_err() { if tx.send(WatchEvent::Input(input_event)).is_err() {
return; return;
} }
input.clear();
}
KeyCode::Char(c) => {
input.push(c);
}
_ => (),
}
} }
Event::Resize(_, _) => { Event::Resize(_, _) => {
if tx.send(WatchEvent::TerminalResize).is_err() { if tx.send(WatchEvent::TerminalResize).is_err() {