mirror of
https://github.com/rust-lang/rustlings.git
synced 2024-12-26 00:00:03 +03:00
Compare commits
17 commits
a23bd69750
...
9ad31aa30d
Author | SHA1 | Date | |
---|---|---|---|
9ad31aa30d | |||
2b7caf6fcb | |||
938500fd2f | |||
2d26358602 | |||
9faa5d3aa4 | |||
bcc2a136c8 | |||
dcad002057 | |||
51b8d2ab25 | |||
aa3eda70e5 | |||
2d0860fe1b | |||
17877366b7 | |||
5eb3dee59c | |||
59e8f70e55 | |||
4c8365fe88 | |||
52af0674c1 | |||
938b90e5f2 | |||
55cc8584bd |
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -95,9 +95,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.16"
|
version = "4.5.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
|
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
@ -105,9 +105,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.15"
|
version = "4.5.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
|
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
@ -242,9 +242,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
@ -490,6 +490,7 @@ dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
"os_pipe",
|
"os_pipe",
|
||||||
|
"rustix",
|
||||||
"rustlings-macros",
|
"rustlings-macros",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -549,9 +550,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.127"
|
version = "1.0.128"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
|
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -612,9 +613,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.76"
|
version = "2.0.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
|
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
@ -48,15 +48,18 @@ include = [
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ahash = { version = "0.8.11", default-features = false }
|
ahash = { version = "0.8.11", default-features = false }
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
clap = { version = "4.5.16", features = ["derive"] }
|
clap = { version = "4.5.17", features = ["derive"] }
|
||||||
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
|
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
|
||||||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
||||||
os_pipe = "1.2.1"
|
os_pipe = "1.2.1"
|
||||||
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
|
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
|
||||||
serde_json = "1.0.127"
|
serde_json = "1.0.128"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml_edit.workspace = true
|
toml_edit.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
|
rustix = { version = "0.38.35", default-features = false, features = ["std", "stdio", "termios"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.12.0"
|
tempfile = "3.12.0"
|
||||||
|
|
||||||
|
|
|
@ -10,4 +10,7 @@ disallowed-methods = [
|
||||||
"std::collections::HashSet::with_capacity",
|
"std::collections::HashSet::with_capacity",
|
||||||
# Inefficient. Use `.queue(…)` instead.
|
# Inefficient. Use `.queue(…)` instead.
|
||||||
"crossterm::style::style",
|
"crossterm::style::style",
|
||||||
|
# Use `thread::Builder::spawn` instead and handle the error.
|
||||||
|
"std::thread::spawn",
|
||||||
|
"std::thread::Scope::spawn",
|
||||||
]
|
]
|
||||||
|
|
|
@ -116,6 +116,8 @@ bin = [
|
||||||
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
|
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
|
||||||
{ name = "generics2", path = "../exercises/14_generics/generics2.rs" },
|
{ name = "generics2", path = "../exercises/14_generics/generics2.rs" },
|
||||||
{ name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" },
|
{ name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" },
|
||||||
|
{ name = "generics3", path = "../exercises/14_generics/generics3.rs" },
|
||||||
|
{ name = "generics3_sol", path = "../solutions/14_generics/generics3.rs" },
|
||||||
{ name = "traits1", path = "../exercises/15_traits/traits1.rs" },
|
{ name = "traits1", path = "../exercises/15_traits/traits1.rs" },
|
||||||
{ name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" },
|
{ name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" },
|
||||||
{ name = "traits2", path = "../exercises/15_traits/traits2.rs" },
|
{ name = "traits2", path = "../exercises/15_traits/traits2.rs" },
|
||||||
|
@ -217,3 +219,5 @@ empty_loop = "forbid"
|
||||||
infinite_loop = "deny"
|
infinite_loop = "deny"
|
||||||
# You shouldn't leak memory while still learning Rust
|
# You shouldn't leak memory while still learning Rust
|
||||||
mem_forget = "deny"
|
mem_forget = "deny"
|
||||||
|
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
|
||||||
|
disallowed_methods = "allow"
|
||||||
|
|
54
exercises/14_generics/generics3.rs
Normal file
54
exercises/14_generics/generics3.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// generics3.rs
|
||||||
|
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
|
||||||
|
|
||||||
|
// This function should take an array of `Option` elements and returns array of not None elements
|
||||||
|
// TODO fix this function signature
|
||||||
|
fn into_dispose_nulls(list: Vec<Option<&str>>) -> Vec<&str> {
|
||||||
|
list.into_iter().flatten().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// You can optionally experiment here.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_str_on_list() {
|
||||||
|
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
|
||||||
|
let only_values = into_dispose_nulls(names_list);
|
||||||
|
assert_eq!(only_values.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_numbers_on_list() {
|
||||||
|
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
|
||||||
|
let only_values = into_dispose_nulls(numbers_list);
|
||||||
|
assert_eq!(only_values.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_custom_type_on_list() {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct Rectangle {
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
}
|
||||||
|
impl Rectangle {
|
||||||
|
fn new(width: i32, height: i32) -> Self {
|
||||||
|
Self { width, height }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let custom_list = vec![
|
||||||
|
Some(Rectangle::new(1, 2)),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(Rectangle::new(3, 4)),
|
||||||
|
];
|
||||||
|
let only_values = into_dispose_nulls(custom_list);
|
||||||
|
assert_eq!(only_values.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -749,6 +749,17 @@ hint = """
|
||||||
Related section in The Book:
|
Related section in The Book:
|
||||||
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions"""
|
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions"""
|
||||||
|
|
||||||
|
[[exercises]]
|
||||||
|
name = "generics3"
|
||||||
|
dir = "14_generics"
|
||||||
|
hint = """
|
||||||
|
Vectors in Rust use generics to create dynamically-sized arrays of any type.
|
||||||
|
The `into_dispose_nulls` function takes a vector as an argument, but only accepts vectors that store the &str type.
|
||||||
|
To allow the function to accept vectors that store any type, you can leverage your knowledge about generics.
|
||||||
|
If you're unsure how to proceed, please refer to the Rust Book at:
|
||||||
|
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-function-definitions.
|
||||||
|
"""
|
||||||
|
|
||||||
# TRAITS
|
# TRAITS
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
|
|
53
solutions/14_generics/generics3.rs
Normal file
53
solutions/14_generics/generics3.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// generics3.rs
|
||||||
|
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
|
||||||
|
|
||||||
|
// Here we added generic type `T` to function signature
|
||||||
|
// Now this function can be used with vector of any
|
||||||
|
fn into_dispose_nulls<T>(list: Vec<Option<T>>) -> Vec<T> {
|
||||||
|
list.into_iter().flatten().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// You can optionally experiment here.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_str_on_list() {
|
||||||
|
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
|
||||||
|
let only_values = into_dispose_nulls(names_list);
|
||||||
|
assert_eq!(only_values.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_numbers_on_list() {
|
||||||
|
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
|
||||||
|
let only_values = into_dispose_nulls(numbers_list);
|
||||||
|
assert_eq!(only_values.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_custom_type_on_list() {
|
||||||
|
struct Rectangle {
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
}
|
||||||
|
impl Rectangle {
|
||||||
|
fn new(width: i32, height: i32) -> Self {
|
||||||
|
Self { width, height }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let custom_list = vec![
|
||||||
|
Some(Rectangle::new(1, 2)),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(Rectangle::new(3, 4)),
|
||||||
|
];
|
||||||
|
let only_values = into_dispose_nulls(custom_list);
|
||||||
|
assert_eq!(only_values.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,10 +24,10 @@ const STATE_FILE_NAME: &str = ".rustlings-state.txt";
|
||||||
pub enum ExercisesProgress {
|
pub enum ExercisesProgress {
|
||||||
// All exercises are done.
|
// All exercises are done.
|
||||||
AllDone,
|
AllDone,
|
||||||
// The current exercise failed and is still pending.
|
|
||||||
CurrentPending,
|
|
||||||
// A new exercise is now pending.
|
// A new exercise is now pending.
|
||||||
NewPending,
|
NewPending,
|
||||||
|
// The current exercise is still pending.
|
||||||
|
CurrentPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum StateFileStatus {
|
pub enum StateFileStatus {
|
||||||
|
@ -388,13 +388,20 @@ impl AppState {
|
||||||
let handles = self
|
let handles = self
|
||||||
.exercises
|
.exercises
|
||||||
.iter()
|
.iter()
|
||||||
.map(|exercise| s.spawn(|| exercise.run_exercise(None, &self.cmd_runner)))
|
.map(|exercise| {
|
||||||
|
thread::Builder::new()
|
||||||
|
.spawn_scoped(s, || exercise.run_exercise(None, &self.cmd_runner))
|
||||||
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (exercise_ind, handle) in handles.into_iter().enumerate() {
|
for (exercise_ind, spawn_res) in handles.into_iter().enumerate() {
|
||||||
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
|
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
|
|
||||||
|
let Ok(handle) = spawn_res else {
|
||||||
|
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
|
||||||
|
};
|
||||||
|
|
||||||
let Ok(success) = handle.join().unwrap() else {
|
let Ok(success) = handle.join().unwrap() else {
|
||||||
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
|
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,10 +41,10 @@ fn check_cargo_toml(
|
||||||
|
|
||||||
if old_bins != new_bins {
|
if old_bins != new_bins {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
|
bail!("The file `dev/Cargo.toml` is outdated. Run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
|
bail!("The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -185,12 +185,14 @@ fn check_exercises_unsolved(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((
|
Some(
|
||||||
exercise_info.name.as_str(),
|
thread::Builder::new()
|
||||||
thread::spawn(|| exercise_info.run_exercise(None, cmd_runner)),
|
.spawn(|| exercise_info.run_exercise(None, cmd_runner))
|
||||||
))
|
.map(|handle| (exercise_info.name.as_str(), handle)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.context("Failed to spawn a thread to check if an exercise is already solved")?;
|
||||||
|
|
||||||
let n_handles = handles.len();
|
let n_handles = handles.len();
|
||||||
write!(stdout, "Progress: 0/{n_handles}")?;
|
write!(stdout, "Progress: 0/{n_handles}")?;
|
||||||
|
@ -226,7 +228,9 @@ fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner)
|
||||||
Ordering::Equal => (),
|
Ordering::Equal => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
let handle = thread::spawn(move || check_exercises_unsolved(info_file, cmd_runner));
|
let handle = thread::Builder::new()
|
||||||
|
.spawn(move || check_exercises_unsolved(info_file, cmd_runner))
|
||||||
|
.context("Failed to spawn a thread to check if any exercise is already solved")?;
|
||||||
|
|
||||||
let info_file_paths = check_info_file_exercises(info_file)?;
|
let info_file_paths = check_info_file_exercises(info_file)?;
|
||||||
check_unexpected_files("exercises", &info_file_paths)?;
|
check_unexpected_files("exercises", &info_file_paths)?;
|
||||||
|
@ -253,7 +257,7 @@ fn check_solutions(
|
||||||
.exercises
|
.exercises
|
||||||
.iter()
|
.iter()
|
||||||
.map(|exercise_info| {
|
.map(|exercise_info| {
|
||||||
thread::spawn(move || {
|
thread::Builder::new().spawn(move || {
|
||||||
let sol_path = exercise_info.sol_path();
|
let sol_path = exercise_info.sol_path();
|
||||||
if !Path::new(&sol_path).exists() {
|
if !Path::new(&sol_path).exists() {
|
||||||
if require_solutions {
|
if require_solutions {
|
||||||
|
@ -274,7 +278,8 @@ fn check_solutions(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.context("Failed to spawn a thread to check a solution")?;
|
||||||
|
|
||||||
let mut sol_paths = hash_set_with_capacity(info_file.exercises.len());
|
let mut sol_paths = hash_set_with_capacity(info_file.exercises.len());
|
||||||
let mut fmt_cmd = Command::new("rustfmt");
|
let mut fmt_cmd = Command::new("rustfmt");
|
||||||
|
@ -322,7 +327,11 @@ fn check_solutions(
|
||||||
}
|
}
|
||||||
stdout.write_all(b"\n")?;
|
stdout.write_all(b"\n")?;
|
||||||
|
|
||||||
let handle = thread::spawn(move || check_unexpected_files("solutions", &sol_paths));
|
let handle = thread::Builder::new()
|
||||||
|
.spawn(move || check_unexpected_files("solutions", &sol_paths))
|
||||||
|
.context(
|
||||||
|
"Failed to spawn a thread to check for unexpected files in the solutions directory",
|
||||||
|
)?;
|
||||||
|
|
||||||
if !fmt_cmd
|
if !fmt_cmd
|
||||||
.status()
|
.status()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{Context, Error, Result};
|
use anyhow::{Context, Error, Result};
|
||||||
use std::{
|
use std::{
|
||||||
fs::{create_dir, OpenOptions},
|
fs::{self, create_dir},
|
||||||
io::{self, Write},
|
io,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::info_file::ExerciseInfo;
|
use crate::info_file::ExerciseInfo;
|
||||||
|
@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo;
|
||||||
/// Contains all embedded files.
|
/// 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)]
|
|
||||||
pub enum WriteStrategy {
|
|
||||||
IfNotExists,
|
|
||||||
Overwrite,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WriteStrategy {
|
|
||||||
fn write(self, path: &str, content: &[u8]) -> Result<()> {
|
|
||||||
let file = match self {
|
|
||||||
Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path),
|
|
||||||
Self::Overwrite => OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(path),
|
|
||||||
};
|
|
||||||
|
|
||||||
file.with_context(|| format!("Failed to open the file `{path}` in write mode"))?
|
|
||||||
.write_all(content)
|
|
||||||
.with_context(|| format!("Failed to write the file {path}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files related to one exercise.
|
// Files related to one exercise.
|
||||||
struct ExerciseFiles {
|
struct ExerciseFiles {
|
||||||
// The content of the exercise file.
|
// The content of the exercise file.
|
||||||
|
@ -42,6 +19,16 @@ struct ExerciseFiles {
|
||||||
dir_ind: usize,
|
dir_ind: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_dir_if_not_exists(path: &str) -> Result<()> {
|
||||||
|
if let Err(e) = create_dir(path) {
|
||||||
|
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||||
|
return Err(Error::from(e).context(format!("Failed to create the directory {path}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// A directory in the `exercises/` directory.
|
// A directory in the `exercises/` directory.
|
||||||
pub struct ExerciseDir {
|
pub struct ExerciseDir {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
|
@ -55,21 +42,13 @@ impl ExerciseDir {
|
||||||
let mut dir_path = String::with_capacity(20 + self.name.len());
|
let mut dir_path = String::with_capacity(20 + self.name.len());
|
||||||
dir_path.push_str("exercises/");
|
dir_path.push_str("exercises/");
|
||||||
dir_path.push_str(self.name);
|
dir_path.push_str(self.name);
|
||||||
|
create_dir_if_not_exists(&dir_path)?;
|
||||||
if let Err(e) = create_dir(&dir_path) {
|
|
||||||
if e.kind() == io::ErrorKind::AlreadyExists {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(
|
|
||||||
Error::from(e).context(format!("Failed to create the directory {dir_path}"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
fs::write(&readme_path, self.readme)
|
||||||
|
.with_context(|| format!("Failed to write the file {readme_path}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,17 +65,31 @@ impl EmbeddedFiles {
|
||||||
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
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`")?;
|
||||||
|
|
||||||
WriteStrategy::IfNotExists.write(
|
fs::write(
|
||||||
"exercises/README.md",
|
"exercises/README.md",
|
||||||
include_bytes!("../exercises/README.md"),
|
include_bytes!("../exercises/README.md"),
|
||||||
)?;
|
)
|
||||||
|
.context("Failed to write the file exercises/README.md")?;
|
||||||
|
|
||||||
for dir in self.exercise_dirs {
|
for dir in self.exercise_dirs {
|
||||||
dir.init_on_disk()?;
|
dir.init_on_disk()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut exercise_path = String::with_capacity(64);
|
||||||
|
let prefix = "exercises/";
|
||||||
|
exercise_path.push_str(prefix);
|
||||||
|
|
||||||
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
|
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
|
||||||
WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?;
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
|
exercise_path.truncate(prefix.len());
|
||||||
|
exercise_path.push_str(dir.name);
|
||||||
|
exercise_path.push('/');
|
||||||
|
exercise_path.push_str(&exercise_info.name);
|
||||||
|
exercise_path.push_str(".rs");
|
||||||
|
|
||||||
|
fs::write(&exercise_path, exercise_files.exercise)
|
||||||
|
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -107,7 +100,8 @@ impl EmbeddedFiles {
|
||||||
let dir = &self.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)
|
fs::write(path, exercise_files.exercise)
|
||||||
|
.with_context(|| format!("Failed to write the exercise file {path}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the solution file to disk and return its path.
|
/// Write the solution file to disk and return its path.
|
||||||
|
@ -116,19 +110,25 @@ impl EmbeddedFiles {
|
||||||
exercise_ind: usize,
|
exercise_ind: usize,
|
||||||
exercise_name: &str,
|
exercise_name: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
create_dir_if_not_exists("solutions")?;
|
||||||
|
|
||||||
let exercise_files = &self.exercise_files[exercise_ind];
|
let exercise_files = &self.exercise_files[exercise_ind];
|
||||||
let dir = &self.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
|
||||||
let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||||
solution_path.push_str("solutions/");
|
dir_path.push_str("solutions/");
|
||||||
solution_path.push_str(dir.name);
|
dir_path.push_str(dir.name);
|
||||||
|
create_dir_if_not_exists(&dir_path)?;
|
||||||
|
|
||||||
|
let mut solution_path = dir_path;
|
||||||
solution_path.push('/');
|
solution_path.push('/');
|
||||||
solution_path.push_str(exercise_name);
|
solution_path.push_str(exercise_name);
|
||||||
solution_path.push_str(".rs");
|
solution_path.push_str(".rs");
|
||||||
|
|
||||||
WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?;
|
fs::write(&solution_path, exercise_files.solution)
|
||||||
|
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
|
||||||
|
|
||||||
Ok(solution_path)
|
Ok(solution_path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ mod scroll_state;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
|
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
|
||||||
let mut list_state = ListState::new(app_state, stdout)?;
|
let mut list_state = ListState::build(app_state, stdout)?;
|
||||||
let mut is_searching = false;
|
let mut is_searching = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
|
@ -48,7 +48,7 @@ pub struct ListState<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ListState<'a> {
|
impl<'a> ListState<'a> {
|
||||||
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> {
|
pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
|
||||||
stdout.queue(Clear(ClearType::All))?;
|
stdout.queue(Clear(ClearType::All))?;
|
||||||
|
|
||||||
let name_col_title_len = 4;
|
let name_col_title_len = 4;
|
||||||
|
@ -64,7 +64,7 @@ impl<'a> ListState<'a> {
|
||||||
let n_rows_with_filter = app_state.exercises().len();
|
let n_rows_with_filter = app_state.exercises().len();
|
||||||
let selected = app_state.current_exercise_ind();
|
let selected = app_state.current_exercise_ind();
|
||||||
|
|
||||||
let (width, height) = terminal::size()?;
|
let (width, height) = terminal::size().context("Failed to get the terminal size")?;
|
||||||
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
|
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
|
||||||
|
|
||||||
let mut slf = Self {
|
let mut slf = Self {
|
||||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -8,7 +8,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use term::{clear_terminal, press_enter_prompt};
|
use term::{clear_terminal, press_enter_prompt};
|
||||||
|
|
||||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
|
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile};
|
||||||
|
|
||||||
mod app_state;
|
mod app_state;
|
||||||
mod cargo_toml;
|
mod cargo_toml;
|
||||||
|
@ -130,15 +130,7 @@ fn main() -> Result<()> {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
watch::watch(&mut app_state, notify_exercise_names)?;
|
||||||
match watch::watch(&mut app_state, notify_exercise_names)? {
|
|
||||||
WatchExit::Shutdown => break,
|
|
||||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
|
||||||
// the watch mode instead of trying to pause the watch threads and correct the
|
|
||||||
// watch state.
|
|
||||||
WatchExit::List => list::list(&mut app_state)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(Subcommands::Run { name }) => {
|
Some(Subcommands::Run { name }) => {
|
||||||
if let Some(name) = name {
|
if let Some(name) = name {
|
||||||
|
|
|
@ -45,7 +45,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::CurrentPending | ExercisesProgress::NewPending => {
|
ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => {
|
||||||
stdout.write_all(b"Next exercise: ")?;
|
stdout.write_all(b"Next exercise: ")?;
|
||||||
app_state
|
app_state
|
||||||
.current_exercise()
|
.current_exercise()
|
||||||
|
|
75
src/watch.rs
75
src/watch.rs
|
@ -1,4 +1,4 @@
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Context, Error, Result};
|
||||||
use notify_debouncer_mini::{
|
use notify_debouncer_mini::{
|
||||||
new_debouncer,
|
new_debouncer,
|
||||||
notify::{self, RecursiveMode},
|
notify::{self, RecursiveMode},
|
||||||
|
@ -11,7 +11,10 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app_state::{AppState, ExercisesProgress};
|
use crate::{
|
||||||
|
app_state::{AppState, ExercisesProgress},
|
||||||
|
list,
|
||||||
|
};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
notify_event::NotifyEventHandler,
|
notify_event::NotifyEventHandler,
|
||||||
|
@ -26,22 +29,21 @@ mod terminal_event;
|
||||||
enum WatchEvent {
|
enum WatchEvent {
|
||||||
Input(InputEvent),
|
Input(InputEvent),
|
||||||
FileChange { exercise_ind: usize },
|
FileChange { exercise_ind: usize },
|
||||||
TerminalResize,
|
TerminalResize { width: u16 },
|
||||||
NotifyErr(notify::Error),
|
NotifyErr(notify::Error),
|
||||||
TerminalEventErr(io::Error),
|
TerminalEventErr(io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by the watch mode to indicate what to do afterwards.
|
/// Returned by the watch mode to indicate what to do afterwards.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub enum WatchExit {
|
enum WatchExit {
|
||||||
/// Exit the program.
|
/// Exit the program.
|
||||||
Shutdown,
|
Shutdown,
|
||||||
/// Enter the list mode and restart the watch mode afterwards.
|
/// Enter the list mode and restart the watch mode afterwards.
|
||||||
List,
|
List,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `notify_exercise_names` as None activates the manual run mode.
|
fn run_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]]>,
|
||||||
) -> Result<WatchExit> {
|
) -> Result<WatchExit> {
|
||||||
|
@ -70,37 +72,36 @@ pub fn watch(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut watch_state = WatchState::new(app_state, manual_run);
|
let mut watch_state = WatchState::build(app_state, manual_run)?;
|
||||||
|
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
watch_state.run_current_exercise(&mut stdout)?;
|
watch_state.run_current_exercise(&mut stdout)?;
|
||||||
|
|
||||||
thread::spawn(move || terminal_event_handler(tx, manual_run));
|
thread::Builder::new()
|
||||||
|
.spawn(move || terminal_event_handler(tx, manual_run))
|
||||||
|
.context("Failed to spawn a thread to handle terminal events")?;
|
||||||
|
|
||||||
while let Ok(event) = rx.recv() {
|
while let Ok(event) = rx.recv() {
|
||||||
match event {
|
match event {
|
||||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
|
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
|
||||||
ExercisesProgress::AllDone => break,
|
ExercisesProgress::AllDone => break,
|
||||||
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
|
|
||||||
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||||
|
ExercisesProgress::CurrentPending => (),
|
||||||
},
|
},
|
||||||
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
|
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
|
||||||
WatchEvent::Input(InputEvent::List) => {
|
WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
|
||||||
return Ok(WatchExit::List);
|
|
||||||
}
|
|
||||||
WatchEvent::Input(InputEvent::Quit) => {
|
WatchEvent::Input(InputEvent::Quit) => {
|
||||||
stdout.write_all(QUIT_MSG)?;
|
stdout.write_all(QUIT_MSG)?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
|
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
|
||||||
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render(&mut stdout)?,
|
|
||||||
WatchEvent::FileChange { exercise_ind } => {
|
WatchEvent::FileChange { exercise_ind } => {
|
||||||
watch_state.handle_file_change(exercise_ind, &mut stdout)?;
|
watch_state.handle_file_change(exercise_ind, &mut stdout)?;
|
||||||
}
|
}
|
||||||
WatchEvent::TerminalResize => watch_state.render(&mut stdout)?,
|
WatchEvent::TerminalResize { width } => {
|
||||||
WatchEvent::NotifyErr(e) => {
|
watch_state.update_term_width(width, &mut stdout)?;
|
||||||
return Err(Error::from(e).context(NOTIFY_ERR));
|
|
||||||
}
|
}
|
||||||
|
WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)),
|
||||||
WatchEvent::TerminalEventErr(e) => {
|
WatchEvent::TerminalEventErr(e) => {
|
||||||
return Err(Error::from(e).context("Terminal event listener failed"));
|
return Err(Error::from(e).context("Terminal event listener failed"));
|
||||||
}
|
}
|
||||||
|
@ -110,6 +111,48 @@ pub fn watch(
|
||||||
Ok(WatchExit::Shutdown)
|
Ok(WatchExit::Shutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn watch_list_loop(
|
||||||
|
app_state: &mut AppState,
|
||||||
|
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||||
|
) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
match run_watch(app_state, notify_exercise_names)? {
|
||||||
|
WatchExit::Shutdown => break Ok(()),
|
||||||
|
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||||
|
// the watch mode instead of trying to pause the watch threads and correct the
|
||||||
|
// watch state.
|
||||||
|
WatchExit::List => list::list(app_state)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `notify_exercise_names` as None activates the manual run mode.
|
||||||
|
pub fn watch(
|
||||||
|
app_state: &mut AppState,
|
||||||
|
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||||
|
) -> Result<()> {
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let stdin_fd = rustix::stdio::stdin();
|
||||||
|
let mut termios = rustix::termios::tcgetattr(stdin_fd)?;
|
||||||
|
let original_local_modes = termios.local_modes;
|
||||||
|
// Disable stdin line buffering and hide input.
|
||||||
|
termios.local_modes -=
|
||||||
|
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
|
||||||
|
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||||
|
|
||||||
|
let res = watch_list_loop(app_state, notify_exercise_names);
|
||||||
|
|
||||||
|
termios.local_modes = original_local_modes;
|
||||||
|
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
watch_list_loop(app_state, notify_exercise_names)
|
||||||
|
}
|
||||||
|
|
||||||
const QUIT_MSG: &[u8] = b"
|
const QUIT_MSG: &[u8] = b"
|
||||||
We hope you're enjoying learning Rust!
|
We hope you're enjoying learning Rust!
|
||||||
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
|
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
style::{
|
style::{
|
||||||
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||||
|
@ -27,17 +27,23 @@ pub struct WatchState<'a> {
|
||||||
show_hint: bool,
|
show_hint: bool,
|
||||||
done_status: DoneStatus,
|
done_status: DoneStatus,
|
||||||
manual_run: bool,
|
manual_run: bool,
|
||||||
|
term_width: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> WatchState<'a> {
|
impl<'a> WatchState<'a> {
|
||||||
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
|
pub fn build(app_state: &'a mut AppState, manual_run: bool) -> Result<Self> {
|
||||||
Self {
|
let term_width = terminal::size()
|
||||||
|
.context("Failed to get the terminal size")?
|
||||||
|
.0;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
app_state,
|
app_state,
|
||||||
output: Vec::with_capacity(OUTPUT_CAPACITY),
|
output: Vec::with_capacity(OUTPUT_CAPACITY),
|
||||||
show_hint: false,
|
show_hint: false,
|
||||||
done_status: DoneStatus::Pending,
|
done_status: DoneStatus::Pending,
|
||||||
manual_run,
|
manual_run,
|
||||||
}
|
term_width,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
||||||
|
@ -175,12 +181,11 @@ impl<'a> WatchState<'a> {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_width = terminal::size()?.0;
|
|
||||||
progress_bar(
|
progress_bar(
|
||||||
stdout,
|
stdout,
|
||||||
self.app_state.n_done(),
|
self.app_state.n_done(),
|
||||||
self.app_state.exercises().len() as u16,
|
self.app_state.exercises().len() as u16,
|
||||||
line_width,
|
self.term_width,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
stdout.write_all(b"\nCurrent exercise: ")?;
|
stdout.write_all(b"\nCurrent exercise: ")?;
|
||||||
|
@ -195,7 +200,20 @@ impl<'a> WatchState<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
self.show_hint = true;
|
if !self.show_hint {
|
||||||
self.render(stdout)
|
self.show_hint = true;
|
||||||
|
self.render(stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
if self.term_width != width {
|
||||||
|
self.term_width = width;
|
||||||
|
self.render(stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
use super::WatchEvent;
|
use super::WatchEvent;
|
||||||
|
@ -9,13 +9,9 @@ pub enum InputEvent {
|
||||||
Hint,
|
Hint,
|
||||||
List,
|
List,
|
||||||
Quit,
|
Quit,
|
||||||
Unrecognized,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
||||||
// Only send `Unrecognized` on ENTER if the last input wasn't valid.
|
|
||||||
let mut last_input_valid = false;
|
|
||||||
|
|
||||||
let last_input_event = loop {
|
let last_input_event = loop {
|
||||||
let terminal_event = match event::read() {
|
let terminal_event = match event::read() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
@ -34,47 +30,21 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
||||||
KeyEventKind::Press => (),
|
KeyEventKind::Press => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
if key.modifiers != KeyModifiers::NONE {
|
|
||||||
last_input_valid = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let input_event = match key.code {
|
let input_event = match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Char('n') => InputEvent::Next,
|
||||||
if last_input_valid {
|
KeyCode::Char('h') => InputEvent::Hint,
|
||||||
continue;
|
KeyCode::Char('l') => break InputEvent::List,
|
||||||
}
|
KeyCode::Char('q') => break InputEvent::Quit,
|
||||||
|
KeyCode::Char('r') if manual_run => InputEvent::Run,
|
||||||
InputEvent::Unrecognized
|
_ => continue,
|
||||||
}
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
let input_event = match c {
|
|
||||||
'n' => InputEvent::Next,
|
|
||||||
'h' => InputEvent::Hint,
|
|
||||||
'l' => break InputEvent::List,
|
|
||||||
'q' => break InputEvent::Quit,
|
|
||||||
'r' if manual_run => InputEvent::Run,
|
|
||||||
_ => {
|
|
||||||
last_input_valid = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
last_input_valid = true;
|
|
||||||
input_event
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
last_input_valid = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Resize(_, _) => {
|
Event::Resize(width, _) => {
|
||||||
if tx.send(WatchEvent::TerminalResize).is_err() {
|
if tx.send(WatchEvent::TerminalResize { width }).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue