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]]
|
||||
name = "clap"
|
||||
version = "4.5.16"
|
||||
version = "4.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
|
||||
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
@ -105,9 +105,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.15"
|
||||
version = "4.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
|
||||
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
|
@ -242,9 +242,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
|
@ -490,6 +490,7 @@ dependencies = [
|
|||
"crossterm",
|
||||
"notify-debouncer-mini",
|
||||
"os_pipe",
|
||||
"rustix",
|
||||
"rustlings-macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -549,9 +550,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.127"
|
||||
version = "1.0.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
@ -612,9 +613,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.76"
|
||||
version = "2.0.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
|
||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -48,15 +48,18 @@ include = [
|
|||
[dependencies]
|
||||
ahash = { version = "0.8.11", default-features = false }
|
||||
anyhow = "1.0.86"
|
||||
clap = { version = "4.5.16", features = ["derive"] }
|
||||
clap = { version = "4.5.17", features = ["derive"] }
|
||||
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
|
||||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
||||
os_pipe = "1.2.1"
|
||||
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
|
||||
serde_json = "1.0.127"
|
||||
serde_json = "1.0.128"
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
rustix = { version = "0.38.35", default-features = false, features = ["std", "stdio", "termios"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.12.0"
|
||||
|
||||
|
|
|
@ -10,4 +10,7 @@ disallowed-methods = [
|
|||
"std::collections::HashSet::with_capacity",
|
||||
# Inefficient. Use `.queue(…)` instead.
|
||||
"crossterm::style::style",
|
||||
# Use `thread::Builder::spawn` instead and handle the error.
|
||||
"std::thread::spawn",
|
||||
"std::thread::Scope::spawn",
|
||||
]
|
||||
|
|
|
@ -116,6 +116,8 @@ bin = [
|
|||
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
|
||||
{ name = "generics2", path = "../exercises/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_sol", path = "../solutions/15_traits/traits1.rs" },
|
||||
{ name = "traits2", path = "../exercises/15_traits/traits2.rs" },
|
||||
|
@ -217,3 +219,5 @@ empty_loop = "forbid"
|
|||
infinite_loop = "deny"
|
||||
# You shouldn't leak memory while still learning Rust
|
||||
mem_forget = "deny"
|
||||
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
|
||||
disallowed_methods = "allow"
|
||||
|
|
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:
|
||||
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
|
||||
|
||||
[[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 {
|
||||
// All exercises are done.
|
||||
AllDone,
|
||||
// The current exercise failed and is still pending.
|
||||
CurrentPending,
|
||||
// A new exercise is now pending.
|
||||
NewPending,
|
||||
// The current exercise is still pending.
|
||||
CurrentPending,
|
||||
}
|
||||
|
||||
pub enum StateFileStatus {
|
||||
|
@ -388,13 +388,20 @@ impl AppState {
|
|||
let handles = self
|
||||
.exercises
|
||||
.iter()
|
||||
.map(|exercise| s.spawn(|| exercise.run_exercise(None, &self.cmd_runner)))
|
||||
.map(|exercise| {
|
||||
thread::Builder::new()
|
||||
.spawn_scoped(s, || exercise.run_exercise(None, &self.cmd_runner))
|
||||
})
|
||||
.collect::<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}")?;
|
||||
stdout.flush()?;
|
||||
|
||||
let Ok(handle) = spawn_res else {
|
||||
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
|
||||
};
|
||||
|
||||
let Ok(success) = handle.join().unwrap() else {
|
||||
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
|
||||
};
|
||||
|
|
|
@ -41,10 +41,10 @@ fn check_cargo_toml(
|
|||
|
||||
if old_bins != new_bins {
|
||||
if cfg!(debug_assertions) {
|
||||
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
|
||||
bail!("The file `dev/Cargo.toml` is outdated. Run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
|
||||
}
|
||||
|
||||
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
|
||||
bail!("The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -185,12 +185,14 @@ fn check_exercises_unsolved(
|
|||
return None;
|
||||
}
|
||||
|
||||
Some((
|
||||
exercise_info.name.as_str(),
|
||||
thread::spawn(|| exercise_info.run_exercise(None, cmd_runner)),
|
||||
))
|
||||
Some(
|
||||
thread::Builder::new()
|
||||
.spawn(|| exercise_info.run_exercise(None, cmd_runner))
|
||||
.map(|handle| (exercise_info.name.as_str(), handle)),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Failed to spawn a thread to check if an exercise is already solved")?;
|
||||
|
||||
let n_handles = handles.len();
|
||||
write!(stdout, "Progress: 0/{n_handles}")?;
|
||||
|
@ -226,7 +228,9 @@ fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner)
|
|||
Ordering::Equal => (),
|
||||
}
|
||||
|
||||
let handle = thread::spawn(move || check_exercises_unsolved(info_file, cmd_runner));
|
||||
let handle = thread::Builder::new()
|
||||
.spawn(move || check_exercises_unsolved(info_file, cmd_runner))
|
||||
.context("Failed to spawn a thread to check if any exercise is already solved")?;
|
||||
|
||||
let info_file_paths = check_info_file_exercises(info_file)?;
|
||||
check_unexpected_files("exercises", &info_file_paths)?;
|
||||
|
@ -253,7 +257,7 @@ fn check_solutions(
|
|||
.exercises
|
||||
.iter()
|
||||
.map(|exercise_info| {
|
||||
thread::spawn(move || {
|
||||
thread::Builder::new().spawn(move || {
|
||||
let sol_path = exercise_info.sol_path();
|
||||
if !Path::new(&sol_path).exists() {
|
||||
if require_solutions {
|
||||
|
@ -274,7 +278,8 @@ fn check_solutions(
|
|||
}
|
||||
})
|
||||
})
|
||||
.collect::<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 fmt_cmd = Command::new("rustfmt");
|
||||
|
@ -322,7 +327,11 @@ fn check_solutions(
|
|||
}
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
let handle = thread::spawn(move || check_unexpected_files("solutions", &sol_paths));
|
||||
let handle = thread::Builder::new()
|
||||
.spawn(move || check_unexpected_files("solutions", &sol_paths))
|
||||
.context(
|
||||
"Failed to spawn a thread to check for unexpected files in the solutions directory",
|
||||
)?;
|
||||
|
||||
if !fmt_cmd
|
||||
.status()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{Context, Error, Result};
|
||||
use std::{
|
||||
fs::{create_dir, OpenOptions},
|
||||
io::{self, Write},
|
||||
fs::{self, create_dir},
|
||||
io,
|
||||
};
|
||||
|
||||
use crate::info_file::ExerciseInfo;
|
||||
|
@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo;
|
|||
/// Contains all embedded files.
|
||||
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum WriteStrategy {
|
||||
IfNotExists,
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
impl WriteStrategy {
|
||||
fn write(self, path: &str, content: &[u8]) -> Result<()> {
|
||||
let file = match self {
|
||||
Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path),
|
||||
Self::Overwrite => OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path),
|
||||
};
|
||||
|
||||
file.with_context(|| format!("Failed to open the file `{path}` in write mode"))?
|
||||
.write_all(content)
|
||||
.with_context(|| format!("Failed to write the file {path}"))
|
||||
}
|
||||
}
|
||||
|
||||
// Files related to one exercise.
|
||||
struct ExerciseFiles {
|
||||
// The content of the exercise file.
|
||||
|
@ -42,6 +19,16 @@ struct ExerciseFiles {
|
|||
dir_ind: usize,
|
||||
}
|
||||
|
||||
fn create_dir_if_not_exists(path: &str) -> Result<()> {
|
||||
if let Err(e) = create_dir(path) {
|
||||
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||
return Err(Error::from(e).context(format!("Failed to create the directory {path}")));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// A directory in the `exercises/` directory.
|
||||
pub struct ExerciseDir {
|
||||
pub name: &'static str,
|
||||
|
@ -55,21 +42,13 @@ impl ExerciseDir {
|
|||
let mut dir_path = String::with_capacity(20 + self.name.len());
|
||||
dir_path.push_str("exercises/");
|
||||
dir_path.push_str(self.name);
|
||||
|
||||
if let Err(e) = create_dir(&dir_path) {
|
||||
if e.kind() == io::ErrorKind::AlreadyExists {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(
|
||||
Error::from(e).context(format!("Failed to create the directory {dir_path}"))
|
||||
);
|
||||
}
|
||||
create_dir_if_not_exists(&dir_path)?;
|
||||
|
||||
let mut readme_path = dir_path;
|
||||
readme_path.push_str("/README.md");
|
||||
|
||||
WriteStrategy::Overwrite.write(&readme_path, self.readme)
|
||||
fs::write(&readme_path, self.readme)
|
||||
.with_context(|| format!("Failed to write the file {readme_path}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,17 +65,31 @@ impl EmbeddedFiles {
|
|||
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
||||
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
||||
|
||||
WriteStrategy::IfNotExists.write(
|
||||
fs::write(
|
||||
"exercises/README.md",
|
||||
include_bytes!("../exercises/README.md"),
|
||||
)?;
|
||||
)
|
||||
.context("Failed to write the file exercises/README.md")?;
|
||||
|
||||
for dir in self.exercise_dirs {
|
||||
dir.init_on_disk()?;
|
||||
}
|
||||
|
||||
let mut exercise_path = String::with_capacity(64);
|
||||
let prefix = "exercises/";
|
||||
exercise_path.push_str(prefix);
|
||||
|
||||
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
|
||||
WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?;
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
exercise_path.truncate(prefix.len());
|
||||
exercise_path.push_str(dir.name);
|
||||
exercise_path.push('/');
|
||||
exercise_path.push_str(&exercise_info.name);
|
||||
exercise_path.push_str(".rs");
|
||||
|
||||
fs::write(&exercise_path, exercise_files.exercise)
|
||||
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -107,7 +100,8 @@ impl EmbeddedFiles {
|
|||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
dir.init_on_disk()?;
|
||||
WriteStrategy::Overwrite.write(path, exercise_files.exercise)
|
||||
fs::write(path, exercise_files.exercise)
|
||||
.with_context(|| format!("Failed to write the exercise file {path}"))
|
||||
}
|
||||
|
||||
/// Write the solution file to disk and return its path.
|
||||
|
@ -116,19 +110,25 @@ impl EmbeddedFiles {
|
|||
exercise_ind: usize,
|
||||
exercise_name: &str,
|
||||
) -> Result<String> {
|
||||
create_dir_if_not_exists("solutions")?;
|
||||
|
||||
let exercise_files = &self.exercise_files[exercise_ind];
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
// 14 = 10 + 1 + 3
|
||||
// solutions/ + / + .rs
|
||||
let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||
solution_path.push_str("solutions/");
|
||||
solution_path.push_str(dir.name);
|
||||
let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||
dir_path.push_str("solutions/");
|
||||
dir_path.push_str(dir.name);
|
||||
create_dir_if_not_exists(&dir_path)?;
|
||||
|
||||
let mut solution_path = dir_path;
|
||||
solution_path.push('/');
|
||||
solution_path.push_str(exercise_name);
|
||||
solution_path.push_str(".rs");
|
||||
|
||||
WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?;
|
||||
fs::write(&solution_path, exercise_files.solution)
|
||||
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
|
||||
|
||||
Ok(solution_path)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ mod scroll_state;
|
|||
mod state;
|
||||
|
||||
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
|
||||
let mut list_state = ListState::new(app_state, stdout)?;
|
||||
let mut list_state = ListState::build(app_state, stdout)?;
|
||||
let mut is_searching = false;
|
||||
|
||||
loop {
|
||||
|
|
|
@ -48,7 +48,7 @@ pub struct ListState<'a> {
|
|||
}
|
||||
|
||||
impl<'a> ListState<'a> {
|
||||
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> {
|
||||
pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
|
||||
stdout.queue(Clear(ClearType::All))?;
|
||||
|
||||
let name_col_title_len = 4;
|
||||
|
@ -64,7 +64,7 @@ impl<'a> ListState<'a> {
|
|||
let n_rows_with_filter = app_state.exercises().len();
|
||||
let selected = app_state.current_exercise_ind();
|
||||
|
||||
let (width, height) = terminal::size()?;
|
||||
let (width, height) = terminal::size().context("Failed to get the terminal size")?;
|
||||
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
|
||||
|
||||
let mut slf = Self {
|
||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -8,7 +8,7 @@ use std::{
|
|||
};
|
||||
use term::{clear_terminal, press_enter_prompt};
|
||||
|
||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
|
||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile};
|
||||
|
||||
mod app_state;
|
||||
mod cargo_toml;
|
||||
|
@ -130,15 +130,7 @@ fn main() -> Result<()> {
|
|||
)
|
||||
};
|
||||
|
||||
loop {
|
||||
match watch::watch(&mut app_state, notify_exercise_names)? {
|
||||
WatchExit::Shutdown => break,
|
||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||
// the watch mode instead of trying to pause the watch threads and correct the
|
||||
// watch state.
|
||||
WatchExit::List => list::list(&mut app_state)?,
|
||||
}
|
||||
}
|
||||
watch::watch(&mut app_state, notify_exercise_names)?;
|
||||
}
|
||||
Some(Subcommands::Run { name }) => {
|
||||
if let Some(name) = name {
|
||||
|
|
|
@ -45,7 +45,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
|||
}
|
||||
|
||||
match app_state.done_current_exercise(&mut stdout)? {
|
||||
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => {
|
||||
ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => {
|
||||
stdout.write_all(b"Next exercise: ")?;
|
||||
app_state
|
||||
.current_exercise()
|
||||
|
|
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::{
|
||||
new_debouncer,
|
||||
notify::{self, RecursiveMode},
|
||||
|
@ -11,7 +11,10 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::app_state::{AppState, ExercisesProgress};
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
list,
|
||||
};
|
||||
|
||||
use self::{
|
||||
notify_event::NotifyEventHandler,
|
||||
|
@ -26,22 +29,21 @@ mod terminal_event;
|
|||
enum WatchEvent {
|
||||
Input(InputEvent),
|
||||
FileChange { exercise_ind: usize },
|
||||
TerminalResize,
|
||||
TerminalResize { width: u16 },
|
||||
NotifyErr(notify::Error),
|
||||
TerminalEventErr(io::Error),
|
||||
}
|
||||
|
||||
/// Returned by the watch mode to indicate what to do afterwards.
|
||||
#[must_use]
|
||||
pub enum WatchExit {
|
||||
enum WatchExit {
|
||||
/// Exit the program.
|
||||
Shutdown,
|
||||
/// Enter the list mode and restart the watch mode afterwards.
|
||||
List,
|
||||
}
|
||||
|
||||
/// `notify_exercise_names` as None activates the manual run mode.
|
||||
pub fn watch(
|
||||
fn run_watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<WatchExit> {
|
||||
|
@ -70,37 +72,36 @@ pub fn watch(
|
|||
None
|
||||
};
|
||||
|
||||
let mut watch_state = WatchState::new(app_state, manual_run);
|
||||
let mut watch_state = WatchState::build(app_state, manual_run)?;
|
||||
|
||||
let mut stdout = io::stdout().lock();
|
||||
watch_state.run_current_exercise(&mut stdout)?;
|
||||
|
||||
thread::spawn(move || terminal_event_handler(tx, manual_run));
|
||||
thread::Builder::new()
|
||||
.spawn(move || terminal_event_handler(tx, manual_run))
|
||||
.context("Failed to spawn a thread to handle terminal events")?;
|
||||
|
||||
while let Ok(event) = rx.recv() {
|
||||
match event {
|
||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
|
||||
ExercisesProgress::AllDone => break,
|
||||
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
|
||||
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||
ExercisesProgress::CurrentPending => (),
|
||||
},
|
||||
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::List) => {
|
||||
return Ok(WatchExit::List);
|
||||
}
|
||||
WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
|
||||
WatchEvent::Input(InputEvent::Quit) => {
|
||||
stdout.write_all(QUIT_MSG)?;
|
||||
break;
|
||||
}
|
||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render(&mut stdout)?,
|
||||
WatchEvent::FileChange { exercise_ind } => {
|
||||
watch_state.handle_file_change(exercise_ind, &mut stdout)?;
|
||||
}
|
||||
WatchEvent::TerminalResize => watch_state.render(&mut stdout)?,
|
||||
WatchEvent::NotifyErr(e) => {
|
||||
return Err(Error::from(e).context(NOTIFY_ERR));
|
||||
WatchEvent::TerminalResize { width } => {
|
||||
watch_state.update_term_width(width, &mut stdout)?;
|
||||
}
|
||||
WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)),
|
||||
WatchEvent::TerminalEventErr(e) => {
|
||||
return Err(Error::from(e).context("Terminal event listener failed"));
|
||||
}
|
||||
|
@ -110,6 +111,48 @@ pub fn watch(
|
|||
Ok(WatchExit::Shutdown)
|
||||
}
|
||||
|
||||
fn watch_list_loop(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
match run_watch(app_state, notify_exercise_names)? {
|
||||
WatchExit::Shutdown => break Ok(()),
|
||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||
// the watch mode instead of trying to pause the watch threads and correct the
|
||||
// watch state.
|
||||
WatchExit::List => list::list(app_state)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `notify_exercise_names` as None activates the manual run mode.
|
||||
pub fn watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<()> {
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let stdin_fd = rustix::stdio::stdin();
|
||||
let mut termios = rustix::termios::tcgetattr(stdin_fd)?;
|
||||
let original_local_modes = termios.local_modes;
|
||||
// Disable stdin line buffering and hide input.
|
||||
termios.local_modes -=
|
||||
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
|
||||
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||
|
||||
let res = watch_list_loop(app_state, notify_exercise_names);
|
||||
|
||||
termios.local_modes = original_local_modes;
|
||||
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
watch_list_loop(app_state, notify_exercise_names)
|
||||
}
|
||||
|
||||
const QUIT_MSG: &[u8] = b"
|
||||
We hope you're enjoying learning Rust!
|
||||
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
style::{
|
||||
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||
|
@ -27,17 +27,23 @@ pub struct WatchState<'a> {
|
|||
show_hint: bool,
|
||||
done_status: DoneStatus,
|
||||
manual_run: bool,
|
||||
term_width: u16,
|
||||
}
|
||||
|
||||
impl<'a> WatchState<'a> {
|
||||
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
|
||||
Self {
|
||||
pub fn build(app_state: &'a mut AppState, manual_run: bool) -> Result<Self> {
|
||||
let term_width = terminal::size()
|
||||
.context("Failed to get the terminal size")?
|
||||
.0;
|
||||
|
||||
Ok(Self {
|
||||
app_state,
|
||||
output: Vec::with_capacity(OUTPUT_CAPACITY),
|
||||
show_hint: false,
|
||||
done_status: DoneStatus::Pending,
|
||||
manual_run,
|
||||
}
|
||||
term_width,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
||||
|
@ -175,12 +181,11 @@ impl<'a> WatchState<'a> {
|
|||
)?;
|
||||
}
|
||||
|
||||
let line_width = terminal::size()?.0;
|
||||
progress_bar(
|
||||
stdout,
|
||||
self.app_state.n_done(),
|
||||
self.app_state.exercises().len() as u16,
|
||||
line_width,
|
||||
self.term_width,
|
||||
)?;
|
||||
|
||||
stdout.write_all(b"\nCurrent exercise: ")?;
|
||||
|
@ -195,7 +200,20 @@ impl<'a> WatchState<'a> {
|
|||
}
|
||||
|
||||
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
self.show_hint = true;
|
||||
self.render(stdout)
|
||||
if !self.show_hint {
|
||||
self.show_hint = true;
|
||||
self.render(stdout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 super::WatchEvent;
|
||||
|
@ -9,13 +9,9 @@ pub enum InputEvent {
|
|||
Hint,
|
||||
List,
|
||||
Quit,
|
||||
Unrecognized,
|
||||
}
|
||||
|
||||
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 terminal_event = match event::read() {
|
||||
Ok(v) => v,
|
||||
|
@ -34,47 +30,21 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
|||
KeyEventKind::Press => (),
|
||||
}
|
||||
|
||||
if key.modifiers != KeyModifiers::NONE {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let input_event = match key.code {
|
||||
KeyCode::Enter => {
|
||||
if last_input_valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
InputEvent::Unrecognized
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let input_event = match c {
|
||||
'n' => InputEvent::Next,
|
||||
'h' => InputEvent::Hint,
|
||||
'l' => break InputEvent::List,
|
||||
'q' => break InputEvent::Quit,
|
||||
'r' if manual_run => InputEvent::Run,
|
||||
_ => {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
last_input_valid = true;
|
||||
input_event
|
||||
}
|
||||
_ => {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('n') => InputEvent::Next,
|
||||
KeyCode::Char('h') => InputEvent::Hint,
|
||||
KeyCode::Char('l') => break InputEvent::List,
|
||||
KeyCode::Char('q') => break InputEvent::Quit,
|
||||
KeyCode::Char('r') if manual_run => InputEvent::Run,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Event::Resize(_, _) => {
|
||||
if tx.send(WatchEvent::TerminalResize).is_err() {
|
||||
Event::Resize(width, _) => {
|
||||
if tx.send(WatchEvent::TerminalResize { width }).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue