Compare commits

...

17 commits

Author SHA1 Message Date
Kacper Poneta 9ad31aa30d
Merge 59e8f70e55 into 2b7caf6fcb 2024-09-09 21:18:11 -05:00
mo8it 2b7caf6fcb Too polite :P 2024-09-06 16:36:36 +02:00
mo8it 938500fd2f Fix dev check in official repo 2024-09-06 16:35:12 +02:00
mo8it 2d26358602 Use the thread builder and handle the spawn error 2024-09-06 15:40:25 +02:00
mo8it 9faa5d3aa4 Avoid asking for terminal size on each rendering 2024-09-05 17:45:27 +02:00
mo8it bcc2a136c8 Add error message when unable to get terminal size 2024-09-05 17:37:34 +02:00
mo8it dcad002057 Only render when needed 2024-09-05 17:32:59 +02:00
mo8it 51b8d2ab25 Remove unused import 2024-09-05 17:23:56 +02:00
mo8it aa3eda70e5 Simplify handling terminal events for unbuffered stdin 2024-09-05 17:12:26 +02:00
mo8it 2d0860fe1b Hide input and disable its line buffering 2024-09-05 02:11:19 +02:00
mo8it 17877366b7 Update deps 2024-09-05 01:55:31 +02:00
mo8it 5eb3dee59c Create solution even if the solution's directory is missing 2024-09-05 00:21:24 +02:00
mo8it 59e8f70e55 Format code 2024-07-12 18:31:23 +02:00
mo8it 4c8365fe88 Update dev/Cargo.toml 2024-07-12 18:25:01 +02:00
Kacper Poneta 52af0674c1 changed the task to make it more appropriate 2024-07-12 18:14:40 +02:00
Kacper Poneta 938b90e5f2 very small solution update 2024-07-11 22:55:48 +02:00
Kacper Poneta 55cc8584bd added exercise 2024-07-11 22:53:38 +02:00
17 changed files with 316 additions and 148 deletions

21
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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",
] ]

View file

@ -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"

View 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);
}
}

View file

@ -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]]

View 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);
}
}

View file

@ -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));
}; };

View file

@ -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()

View file

@ -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)
} }

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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()

View file

@ -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.

View file

@ -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(())
} }
} }

View file

@ -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;
} }
} }