From 0819bbe21fc86315d3acdcdb2bc14b21f3acb788 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 01:17:53 +0200 Subject: Can't use Ratatui for the watch mode :( --- src/watch.rs | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 src/watch.rs (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs new file mode 100644 index 0000000..92da20d --- /dev/null +++ b/src/watch.rs @@ -0,0 +1,240 @@ +use anyhow::Result; +use crossterm::{ + style::{Attribute, ContentStyle, Stylize}, + terminal::{Clear, ClearType}, + ExecutableCommand, +}; +use notify_debouncer_mini::{ + new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, +}; +use std::{ + fmt::Write as _, + io::{self, BufRead, StdoutLock, Write}, + path::Path, + sync::mpsc::{channel, sync_channel, Receiver}, + thread, + time::Duration, +}; + +use crate::{ + exercise::{self, Exercise}, + state::State, +}; + +enum Event { + Hint, + Clear, + Quit, +} + +struct WatchState<'a> { + writer: StdoutLock<'a>, + rx: Receiver, + exercises: &'a [Exercise], + exercise: &'a Exercise, + current_exercise_ind: usize, + stdout: Option>, + stderr: Option>, + message: Option, + prompt: Vec, +} + +impl<'a> WatchState<'a> { + fn run_exercise(&mut self) -> Result { + let output = self.exercise.run()?; + + if !output.status.success() { + self.stdout = Some(output.stdout); + self.stderr = Some(output.stderr); + return Ok(false); + } + + if let exercise::State::Pending(context) = self.exercise.state()? { + let mut message = format!( + " +You can keep working on this exercise or jump into the next one by removing the {} comment: + +", + "`I AM NOT DONE`".bold(), + ); + + for context_line in context { + let formatted_line = if context_line.important { + context_line.line.bold() + } else { + context_line.line.stylize() + }; + + writeln!( + message, + "{:>2} {} {}", + ContentStyle { + foreground_color: Some(crossterm::style::Color::Blue), + background_color: None, + underline_color: None, + attributes: Attribute::Bold.into() + } + .apply(context_line.number), + "|".blue(), + formatted_line, + )?; + } + + self.stdout = Some(output.stdout); + self.message = Some(message); + return Ok(false); + } + + Ok(true) + } + + fn try_recv_event(&mut self) -> Result<()> { + let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { + return Ok(()); + }; + + if let Some(current_exercise_ind) = events? + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + { + self.current_exercise_ind = current_exercise_ind; + } else { + return Ok(()); + }; + + while self.current_exercise_ind < self.exercises.len() { + self.exercise = &self.exercises[self.current_exercise_ind]; + if !self.run_exercise()? { + break; + } + + self.current_exercise_ind += 1; + } + + Ok(()) + } + + fn prompt(&mut self) -> io::Result<()> { + self.writer.write_all(&self.prompt)?; + self.writer.flush() + } + + fn render(&mut self) -> Result<()> { + self.writer.execute(Clear(ClearType::All))?; + + if let Some(stdout) = &self.stdout { + self.writer.write_all(stdout)?; + } + + if let Some(stderr) = &self.stderr { + self.writer.write_all(stderr)?; + } + + if let Some(message) = &self.message { + self.writer.write_all(message.as_bytes())?; + } + + self.prompt()?; + + Ok(()) + } +} + +pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> { + let (tx, rx) = channel(); + let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; + debouncer + .watcher() + .watch(Path::new("exercises"), RecursiveMode::Recursive)?; + + let current_exercise_ind = state.progress.iter().position(|done| *done).unwrap_or(0); + + let exercise = &exercises[current_exercise_ind]; + + let writer = io::stdout().lock(); + + let mut watch_state = WatchState { + writer, + rx, + exercises, + exercise, + current_exercise_ind, + stdout: None, + stderr: None, + message: None, + prompt: format!( + "\n\n{}int/{}lear/{}uit? ", + "h".bold(), + "c".bold(), + "q".bold() + ) + .into_bytes(), + }; + + watch_state.run_exercise()?; + watch_state.render()?; + + let (tx, rx) = sync_channel(0); + thread::spawn(move || { + let mut stdin = io::stdin().lock(); + let mut stdin_buf = String::with_capacity(8); + + loop { + stdin.read_line(&mut stdin_buf).unwrap(); + + let event = match stdin_buf.trim() { + "h" | "hint" => Some(Event::Hint), + "c" | "clear" => Some(Event::Clear), + "q" | "quit" => Some(Event::Quit), + _ => None, + }; + + stdin_buf.clear(); + + if tx.send(event).is_err() { + break; + }; + } + }); + + loop { + watch_state.try_recv_event()?; + + if let Ok(event) = rx.try_recv() { + match event { + Some(Event::Hint) => { + watch_state + .writer + .write_all(watch_state.exercise.hint.as_bytes())?; + watch_state.prompt()?; + } + Some(Event::Clear) => { + watch_state.render()?; + } + Some(Event::Quit) => break, + None => { + watch_state.writer.write_all(b"Invalid command")?; + watch_state.prompt()?; + } + } + } + } + + watch_state.writer.write_all(b" +We hope you're enjoying learning Rust! +If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. +")?; + + Ok(()) +} -- cgit v1.2.3 From b0a475062445705853b4f861ee9e3135065f0660 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 04:59:22 +0200 Subject: Implement "continue at" --- src/list.rs | 54 +++++++++++++++++++++++++++++++----------------------- src/main.rs | 4 ++-- src/state.rs | 31 +++++++++++++++++++++++++------ src/watch.rs | 2 +- 4 files changed, 59 insertions(+), 32 deletions(-) (limited to 'src/watch.rs') diff --git a/src/list.rs b/src/list.rs index ff031cb..bb5ba1c 100644 --- a/src/list.rs +++ b/src/list.rs @@ -16,25 +16,13 @@ use std::io; use crate::{exercise::Exercise, state::State}; -fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { - let header = Row::new(["Next", "State", "Name", "Path"]); - - let max_name_len = exercises +fn rows<'s, 'e>(state: &'s State, exercises: &'e [Exercise]) -> impl Iterator> + 's +where + 'e: 's, +{ + exercises .iter() - .map(|exercise| exercise.name.len()) - .max() - .unwrap_or(4) as u16; - - let widths = [ - Constraint::Length(4), - Constraint::Length(7), - Constraint::Length(max_name_len), - Constraint::Fill(1), - ]; - - let rows = exercises - .iter() - .zip(&state.progress) + .zip(state.progress()) .enumerate() .map(|(ind, (exercise, done))| { let exercise_state = if *done { @@ -43,7 +31,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { "PENDING".yellow() }; - let next = if ind == state.next_exercise_ind { + let next = if ind == state.next_exercise_ind() { ">>>>".bold().red() } else { Span::default() @@ -56,9 +44,25 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { Span::raw(exercise.path.to_string_lossy()), ]) }) - .collect::>(); +} - Table::new(rows, widths) +fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { + let header = Row::new(["Next", "State", "Name", "Path"]); + + let max_name_len = exercises + .iter() + .map(|exercise| exercise.name.len()) + .max() + .unwrap_or(4) as u16; + + let widths = [ + Constraint::Length(4), + Constraint::Length(7), + Constraint::Length(max_name_len), + Constraint::Fill(1), + ]; + + Table::new(rows(state, exercises), widths) .header(header) .column_spacing(2) .highlight_spacing(HighlightSpacing::Always) @@ -67,7 +71,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { .block(Block::default().borders(Borders::BOTTOM)) } -pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { +pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; @@ -76,7 +80,7 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; terminal.clear()?; - let table = table(state, exercises); + let mut table = table(state, exercises); let last_ind = exercises.len() - 1; let mut selected = 0; @@ -143,6 +147,10 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { selected = last_ind; table_state.select(Some(selected)); } + KeyCode::Char('c') => { + state.set_next_exercise_ind(selected)?; + table = table.rows(rows(state, exercises)); + } _ => (), } } diff --git a/src/main.rs b/src/main.rs index 34d1784..e82fc80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,7 +85,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let state = State::read_or_default(&exercises); + let mut state = State::read_or_default(&exercises); match args.command { None | Some(Subcommands::Watch) => { @@ -94,7 +94,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::List) => { - list::list(&state, &exercises)?; + list::list(&mut state, &exercises)?; } Some(Subcommands::Run { name }) => { let exercise = find_exercise(&name, &exercises)?; diff --git a/src/state.rs b/src/state.rs index f29dc13..5a64487 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use std::fs; @@ -6,8 +6,8 @@ use crate::exercise::Exercise; #[derive(Serialize, Deserialize)] pub struct State { - pub next_exercise_ind: usize, - pub progress: Vec, + next_exercise_ind: usize, + progress: Vec, } impl State { @@ -30,11 +30,30 @@ impl State { }) } - pub fn write(&self) -> Result<()> { + fn write(&self) -> Result<()> { // TODO: Capacity - let mut buf = Vec::with_capacity(1 << 12); + let mut buf = Vec::with_capacity(1024); serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; - dbg!(buf.len()); + Ok(()) } + + #[inline] + pub fn next_exercise_ind(&self) -> usize { + self.next_exercise_ind + } + + pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> { + if ind >= self.progress.len() { + bail!("The next exercise index is higher than the number of exercises"); + } + + self.next_exercise_ind = ind; + self.write() + } + + #[inline] + pub fn progress(&self) -> &[bool] { + &self.progress + } } diff --git a/src/watch.rs b/src/watch.rs index 92da20d..cc9668d 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -158,7 +158,7 @@ pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> { .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let current_exercise_ind = state.progress.iter().position(|done| *done).unwrap_or(0); + let current_exercise_ind = state.next_exercise_ind(); let exercise = &exercises[current_exercise_ind]; -- cgit v1.2.3 From 3bd26c7a24a97f9b4b87c453fbdbb06fe9971920 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 19:01:08 +0200 Subject: State -> StateFile --- src/list.rs | 20 +++++++++---------- src/main.rs | 17 ++++++++-------- src/state.rs | 59 ------------------------------------------------------- src/state_file.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/watch.rs | 6 +++--- 5 files changed, 81 insertions(+), 80 deletions(-) delete mode 100644 src/state.rs create mode 100644 src/state_file.rs (limited to 'src/watch.rs') diff --git a/src/list.rs b/src/list.rs index cff0a3d..c59b8d8 100644 --- a/src/list.rs +++ b/src/list.rs @@ -14,7 +14,7 @@ use ratatui::{ }; use std::io; -use crate::{exercise::Exercise, state::State}; +use crate::{exercise::Exercise, state_file::StateFile}; struct UiState<'a> { pub table: Table<'a>, @@ -25,7 +25,7 @@ struct UiState<'a> { impl<'a> UiState<'a> { pub fn rows<'s, 'i>( - state: &'s State, + state_file: &'s StateFile, exercises: &'a [Exercise], ) -> impl Iterator> + 'i where @@ -34,10 +34,10 @@ impl<'a> UiState<'a> { { exercises .iter() - .zip(state.progress()) + .zip(state_file.progress()) .enumerate() .map(|(ind, (exercise, done))| { - let next = if ind == state.next_exercise_ind() { + let next = if ind == state_file.next_exercise_ind() { ">>>>".bold().red() } else { Span::default() @@ -58,7 +58,7 @@ impl<'a> UiState<'a> { }) } - pub fn new(state: &State, exercises: &'a [Exercise]) -> Self { + pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self { let header = Row::new(["Next", "State", "Name", "Path"]); let max_name_len = exercises @@ -74,7 +74,7 @@ impl<'a> UiState<'a> { Constraint::Fill(1), ]; - let rows = Self::rows(state, exercises); + let rows = Self::rows(state_file, exercises); let table = Table::new(rows, widths) .header(header) @@ -147,7 +147,7 @@ impl<'a> UiState<'a> { } } -pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { +pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; enable_raw_mode()?; @@ -155,7 +155,7 @@ pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; terminal.clear()?; - let mut ui_state = UiState::new(state, exercises); + let mut ui_state = UiState::new(state_file, exercises); 'outer: loop { terminal.draw(|frame| ui_state.draw(frame))?; @@ -183,8 +183,8 @@ pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), KeyCode::Char('c') => { - state.set_next_exercise_ind(ui_state.selected)?; - ui_state.table = ui_state.table.rows(UiState::rows(state, exercises)); + state_file.set_next_exercise_ind(ui_state.selected)?; + ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises)); } _ => (), } diff --git a/src/main.rs b/src/main.rs index e82fc80..3d691b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,6 @@ -use crate::consts::WELCOME; -use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; -use crate::exercise::{Exercise, ExerciseList}; -use crate::run::run; -use crate::verify::verify; use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; -use state::State; +use state_file::StateFile; use std::path::Path; use std::process::exit; use verify::VerifyState; @@ -16,10 +11,16 @@ mod exercise; mod init; mod list; mod run; -mod state; +mod state_file; mod verify; mod watch; +use crate::consts::WELCOME; +use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; +use crate::exercise::{Exercise, ExerciseList}; +use crate::run::run; +use crate::verify::verify; + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -85,7 +86,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let mut state = State::read_or_default(&exercises); + let mut state = StateFile::read_or_default(&exercises); match args.command { None | Some(Subcommands::Watch) => { diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index 5a64487..0000000 --- a/src/state.rs +++ /dev/null @@ -1,59 +0,0 @@ -use anyhow::{bail, Context, Result}; -use serde::{Deserialize, Serialize}; -use std::fs; - -use crate::exercise::Exercise; - -#[derive(Serialize, Deserialize)] -pub struct State { - next_exercise_ind: usize, - progress: Vec, -} - -impl State { - fn read(exercises: &[Exercise]) -> Option { - let file_content = fs::read(".rustlings.json").ok()?; - - let slf: Self = serde_json::de::from_slice(&file_content).ok()?; - - if slf.progress.len() != exercises.len() || slf.next_exercise_ind >= exercises.len() { - return None; - } - - Some(slf) - } - - pub fn read_or_default(exercises: &[Exercise]) -> Self { - Self::read(exercises).unwrap_or_else(|| Self { - next_exercise_ind: 0, - progress: vec![false; exercises.len()], - }) - } - - fn write(&self) -> Result<()> { - // TODO: Capacity - let mut buf = Vec::with_capacity(1024); - serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; - - Ok(()) - } - - #[inline] - pub fn next_exercise_ind(&self) -> usize { - self.next_exercise_ind - } - - pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> { - if ind >= self.progress.len() { - bail!("The next exercise index is higher than the number of exercises"); - } - - self.next_exercise_ind = ind; - self.write() - } - - #[inline] - pub fn progress(&self) -> &[bool] { - &self.progress - } -} diff --git a/src/state_file.rs b/src/state_file.rs new file mode 100644 index 0000000..ca7ed34 --- /dev/null +++ b/src/state_file.rs @@ -0,0 +1,59 @@ +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; + +use crate::exercise::Exercise; + +#[derive(Serialize, Deserialize)] +pub struct StateFile { + next_exercise_ind: usize, + progress: Vec, +} + +impl StateFile { + fn read(exercises: &[Exercise]) -> Option { + let file_content = fs::read(".rustlings.json").ok()?; + + let slf: Self = serde_json::de::from_slice(&file_content).ok()?; + + if slf.progress.len() != exercises.len() || slf.next_exercise_ind >= exercises.len() { + return None; + } + + Some(slf) + } + + pub fn read_or_default(exercises: &[Exercise]) -> Self { + Self::read(exercises).unwrap_or_else(|| Self { + next_exercise_ind: 0, + progress: vec![false; exercises.len()], + }) + } + + fn write(&self) -> Result<()> { + // TODO: Capacity + let mut buf = Vec::with_capacity(1024); + serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; + + Ok(()) + } + + #[inline] + pub fn next_exercise_ind(&self) -> usize { + self.next_exercise_ind + } + + pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> { + if ind >= self.progress.len() { + bail!("The next exercise index is higher than the number of exercises"); + } + + self.next_exercise_ind = ind; + self.write() + } + + #[inline] + pub fn progress(&self) -> &[bool] { + &self.progress + } +} diff --git a/src/watch.rs b/src/watch.rs index cc9668d..1503fdf 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -18,7 +18,7 @@ use std::{ use crate::{ exercise::{self, Exercise}, - state::State, + state_file::StateFile, }; enum Event { @@ -151,14 +151,14 @@ You can keep working on this exercise or jump into the next one by removing the } } -pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> { +pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { let (tx, rx) = channel(); let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; debouncer .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let current_exercise_ind = state.next_exercise_ind(); + let current_exercise_ind = state_file.next_exercise_ind(); let exercise = &exercises[current_exercise_ind]; -- cgit v1.2.3 From 9a4ee47c527251fc3efacacc31bd0e73ef527969 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 19:29:16 +0200 Subject: Separate WatchState --- src/watch.rs | 181 ++++----------------------------------------------- src/watch/state.rs | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 169 deletions(-) create mode 100644 src/watch/state.rs (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 1503fdf..967f98c 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,25 +1,18 @@ use anyhow::Result; -use crossterm::{ - style::{Attribute, ContentStyle, Stylize}, - terminal::{Clear, ClearType}, - ExecutableCommand, -}; -use notify_debouncer_mini::{ - new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, -}; +use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; use std::{ - fmt::Write as _, - io::{self, BufRead, StdoutLock, Write}, + io::{self, BufRead, Write}, path::Path, - sync::mpsc::{channel, sync_channel, Receiver}, + sync::mpsc::{channel, sync_channel}, thread, time::Duration, }; -use crate::{ - exercise::{self, Exercise}, - state_file::StateFile, -}; +mod state; + +use crate::{exercise::Exercise, state_file::StateFile}; + +use self::state::WatchState; enum Event { Hint, @@ -27,130 +20,6 @@ enum Event { Quit, } -struct WatchState<'a> { - writer: StdoutLock<'a>, - rx: Receiver, - exercises: &'a [Exercise], - exercise: &'a Exercise, - current_exercise_ind: usize, - stdout: Option>, - stderr: Option>, - message: Option, - prompt: Vec, -} - -impl<'a> WatchState<'a> { - fn run_exercise(&mut self) -> Result { - let output = self.exercise.run()?; - - if !output.status.success() { - self.stdout = Some(output.stdout); - self.stderr = Some(output.stderr); - return Ok(false); - } - - if let exercise::State::Pending(context) = self.exercise.state()? { - let mut message = format!( - " -You can keep working on this exercise or jump into the next one by removing the {} comment: - -", - "`I AM NOT DONE`".bold(), - ); - - for context_line in context { - let formatted_line = if context_line.important { - context_line.line.bold() - } else { - context_line.line.stylize() - }; - - writeln!( - message, - "{:>2} {} {}", - ContentStyle { - foreground_color: Some(crossterm::style::Color::Blue), - background_color: None, - underline_color: None, - attributes: Attribute::Bold.into() - } - .apply(context_line.number), - "|".blue(), - formatted_line, - )?; - } - - self.stdout = Some(output.stdout); - self.message = Some(message); - return Ok(false); - } - - Ok(true) - } - - fn try_recv_event(&mut self) -> Result<()> { - let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { - return Ok(()); - }; - - if let Some(current_exercise_ind) = events? - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises - .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - { - self.current_exercise_ind = current_exercise_ind; - } else { - return Ok(()); - }; - - while self.current_exercise_ind < self.exercises.len() { - self.exercise = &self.exercises[self.current_exercise_ind]; - if !self.run_exercise()? { - break; - } - - self.current_exercise_ind += 1; - } - - Ok(()) - } - - fn prompt(&mut self) -> io::Result<()> { - self.writer.write_all(&self.prompt)?; - self.writer.flush() - } - - fn render(&mut self) -> Result<()> { - self.writer.execute(Clear(ClearType::All))?; - - if let Some(stdout) = &self.stdout { - self.writer.write_all(stdout)?; - } - - if let Some(stderr) = &self.stderr { - self.writer.write_all(stderr)?; - } - - if let Some(message) = &self.message { - self.writer.write_all(message.as_bytes())?; - } - - self.prompt()?; - - Ok(()) - } -} - pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { let (tx, rx) = channel(); let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; @@ -158,29 +27,7 @@ pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let current_exercise_ind = state_file.next_exercise_ind(); - - let exercise = &exercises[current_exercise_ind]; - - let writer = io::stdout().lock(); - - let mut watch_state = WatchState { - writer, - rx, - exercises, - exercise, - current_exercise_ind, - stdout: None, - stderr: None, - message: None, - prompt: format!( - "\n\n{}int/{}lear/{}uit? ", - "h".bold(), - "c".bold(), - "q".bold() - ) - .into_bytes(), - }; + let mut watch_state = WatchState::new(state_file, exercises, rx); watch_state.run_exercise()?; watch_state.render()?; @@ -214,24 +61,20 @@ pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { if let Ok(event) = rx.try_recv() { match event { Some(Event::Hint) => { - watch_state - .writer - .write_all(watch_state.exercise.hint.as_bytes())?; - watch_state.prompt()?; + watch_state.show_hint()?; } Some(Event::Clear) => { watch_state.render()?; } Some(Event::Quit) => break, None => { - watch_state.writer.write_all(b"Invalid command")?; - watch_state.prompt()?; + watch_state.handle_invalid_cmd()?; } } } } - watch_state.writer.write_all(b" + watch_state.into_writer().write_all(b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; diff --git a/src/watch/state.rs b/src/watch/state.rs new file mode 100644 index 0000000..40f48ef --- /dev/null +++ b/src/watch/state.rs @@ -0,0 +1,186 @@ +use anyhow::Result; +use crossterm::{ + style::{Attribute, ContentStyle, Stylize}, + terminal::{Clear, ClearType}, + ExecutableCommand, +}; +use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; +use std::{ + fmt::Write as _, + io::{self, StdoutLock, Write as _}, + sync::mpsc::Receiver, + time::Duration, +}; + +use crate::{ + exercise::{Exercise, State}, + state_file::StateFile, +}; + +pub struct WatchState<'a> { + writer: StdoutLock<'a>, + rx: Receiver, + exercises: &'a [Exercise], + exercise: &'a Exercise, + current_exercise_ind: usize, + stdout: Option>, + stderr: Option>, + message: Option, + prompt: Vec, +} + +impl<'a> WatchState<'a> { + pub fn new( + state_file: &StateFile, + exercises: &'a [Exercise], + rx: Receiver, + ) -> Self { + let current_exercise_ind = state_file.next_exercise_ind(); + let exercise = &exercises[current_exercise_ind]; + + let writer = io::stdout().lock(); + + let prompt = format!( + "\n\n{}int/{}lear/{}uit? ", + "h".bold(), + "c".bold(), + "q".bold() + ) + .into_bytes(); + + Self { + writer, + rx, + exercises, + exercise, + current_exercise_ind, + stdout: None, + stderr: None, + message: None, + prompt, + } + } + + #[inline] + pub fn into_writer(self) -> StdoutLock<'a> { + self.writer + } + + pub fn run_exercise(&mut self) -> Result { + let output = self.exercise.run()?; + + if !output.status.success() { + self.stdout = Some(output.stdout); + self.stderr = Some(output.stderr); + return Ok(false); + } + + if let State::Pending(context) = self.exercise.state()? { + let mut message = format!( + " +You can keep working on this exercise or jump into the next one by removing the {} comment: + +", + "`I AM NOT DONE`".bold(), + ); + + for context_line in context { + let formatted_line = if context_line.important { + context_line.line.bold() + } else { + context_line.line.stylize() + }; + + writeln!( + message, + "{:>2} {} {}", + ContentStyle { + foreground_color: Some(crossterm::style::Color::Blue), + background_color: None, + underline_color: None, + attributes: Attribute::Bold.into() + } + .apply(context_line.number), + "|".blue(), + formatted_line, + )?; + } + + self.stdout = Some(output.stdout); + self.message = Some(message); + return Ok(false); + } + + Ok(true) + } + + pub fn try_recv_event(&mut self) -> Result<()> { + let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { + return Ok(()); + }; + + if let Some(current_exercise_ind) = events? + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + { + self.current_exercise_ind = current_exercise_ind; + } else { + return Ok(()); + }; + + while self.current_exercise_ind < self.exercises.len() { + self.exercise = &self.exercises[self.current_exercise_ind]; + if !self.run_exercise()? { + break; + } + + self.current_exercise_ind += 1; + } + + Ok(()) + } + + pub fn show_prompt(&mut self) -> io::Result<()> { + self.writer.write_all(&self.prompt)?; + self.writer.flush() + } + + pub fn render(&mut self) -> io::Result<()> { + self.writer.execute(Clear(ClearType::All))?; + + if let Some(stdout) = &self.stdout { + self.writer.write_all(stdout)?; + } + + if let Some(stderr) = &self.stderr { + self.writer.write_all(stderr)?; + } + + if let Some(message) = &self.message { + self.writer.write_all(message.as_bytes())?; + } + + self.show_prompt() + } + + pub fn show_hint(&mut self) -> io::Result<()> { + self.writer.write_all(self.exercise.hint.as_bytes())?; + self.show_prompt() + } + + pub fn handle_invalid_cmd(&mut self) -> io::Result<()> { + self.writer.write_all(b"Invalid command")?; + self.show_prompt() + } +} -- cgit v1.2.3 From f0ce2c1afa21fdaa34aed8f21c1ef4d3c47cebdd Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 21:07:53 +0200 Subject: Improve event handling in the watch mode --- src/main.rs | 5 +- src/watch.rs | 150 +++++++++++++++++++++++++++++++++++++---------------- src/watch/state.rs | 73 +++++++++----------------- 3 files changed, 133 insertions(+), 95 deletions(-) (limited to 'src/watch.rs') diff --git a/src/main.rs b/src/main.rs index 356b77c..6af66bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,7 +85,8 @@ Did you already install Rust? Try running `cargo --version` to diagnose the problem.", )?; - let exercises = InfoFile::parse()?.exercises; + // Leaking is not a problem since the exercises are used until the end of the program. + let exercises = InfoFile::parse()?.exercises.leak(); if matches!(args.command, Some(Subcommands::Init)) { init::init(&exercises).context("Initialization failed")?; @@ -110,7 +111,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini match args.command { None | Some(Subcommands::Watch) => { - watch::watch(&state_file, &exercises)?; + watch::watch(&state_file, exercises)?; } // `Init` is handled above. Some(Subcommands::Init) => (), diff --git a/src/watch.rs b/src/watch.rs index 967f98c..abf4002 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,9 +1,11 @@ -use anyhow::Result; -use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; +use anyhow::{bail, Context, Result}; +use notify_debouncer_mini::{ + new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, +}; use std::{ io::{self, BufRead, Write}, path::Path, - sync::mpsc::{channel, sync_channel}, + sync::mpsc::{channel, Sender}, thread, time::Duration, }; @@ -14,70 +16,130 @@ use crate::{exercise::Exercise, state_file::StateFile}; use self::state::WatchState; -enum Event { +enum InputEvent { Hint, Clear, Quit, + Unrecognized, } -pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { - let (tx, rx) = channel(); - let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; - debouncer - .watcher() - .watch(Path::new("exercises"), RecursiveMode::Recursive)?; +enum WatchEvent { + Input(InputEvent), + FileChange { exercise_ind: usize }, + TerminalResize, +} - let mut watch_state = WatchState::new(state_file, exercises, rx); +struct DebouceEventHandler { + tx: Sender, + exercises: &'static [Exercise], +} - watch_state.run_exercise()?; - watch_state.render()?; +impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { + fn handle_event(&mut self, event: DebounceEventResult) { + let Ok(event) = event else { + // TODO + return; + }; + + let Some(exercise_ind) = event + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } - let (tx, rx) = sync_channel(0); - thread::spawn(move || { - let mut stdin = io::stdin().lock(); - let mut stdin_buf = String::with_capacity(8); + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + else { + return; + }; - loop { - stdin.read_line(&mut stdin_buf).unwrap(); + self.tx.send(WatchEvent::FileChange { exercise_ind }); + } +} - let event = match stdin_buf.trim() { - "h" | "hint" => Some(Event::Hint), - "c" | "clear" => Some(Event::Clear), - "q" | "quit" => Some(Event::Quit), - _ => None, - }; +fn input_handler(tx: Sender) -> Result<()> { + let mut stdin = io::stdin().lock(); + let mut stdin_buf = String::with_capacity(8); + + loop { + stdin + .read_line(&mut stdin_buf) + .context("Failed to read the user's input from stdin")?; - stdin_buf.clear(); + let event = match stdin_buf.trim() { + "h" | "hint" => InputEvent::Hint, + "c" | "clear" => InputEvent::Clear, + "q" | "quit" => InputEvent::Quit, + _ => InputEvent::Unrecognized, + }; - if tx.send(event).is_err() { - break; - }; + stdin_buf.clear(); + + if tx.send(WatchEvent::Input(event)).is_err() { + return Ok(()); } - }); + } +} - loop { - watch_state.try_recv_event()?; +pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> { + let (tx, rx) = channel(); + let mut debouncer = new_debouncer( + Duration::from_secs(1), + DebouceEventHandler { + tx: tx.clone(), + exercises, + }, + )?; + debouncer + .watcher() + .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - if let Ok(event) = rx.try_recv() { - match event { - Some(Event::Hint) => { - watch_state.show_hint()?; - } - Some(Event::Clear) => { - watch_state.render()?; - } - Some(Event::Quit) => break, - None => { - watch_state.handle_invalid_cmd()?; - } + let mut watch_state = WatchState::new(state_file, exercises); + + // TODO: bool + watch_state.run_exercise()?; + watch_state.render()?; + + let input_thread = thread::spawn(move || input_handler(tx)); + + while let Ok(event) = rx.recv() { + match event { + WatchEvent::Input(InputEvent::Hint) => { + watch_state.show_hint()?; + } + WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => { + watch_state.render()?; + } + WatchEvent::Input(InputEvent::Quit) => break, + WatchEvent::Input(InputEvent::Unrecognized) => { + watch_state.handle_invalid_cmd()?; + } + WatchEvent::FileChange { exercise_ind } => { + // TODO: bool + watch_state.run_exercise_with_ind(exercise_ind)?; + watch_state.render()?; } } } + // Drop the receiver for the sender threads to exit. + drop(rx); + watch_state.into_writer().write_all(b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; + match input_thread.join() { + Ok(res) => res?, + Err(_) => bail!("The input thread panicked"), + } + Ok(()) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 40f48ef..f614ae0 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,26 +1,23 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use crossterm::{ style::{Attribute, ContentStyle, Stylize}, - terminal::{Clear, ClearType}, + terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; use std::{ fmt::Write as _, io::{self, StdoutLock, Write as _}, - sync::mpsc::Receiver, - time::Duration, }; use crate::{ exercise::{Exercise, State}, + progress_bar::progress_bar, state_file::StateFile, }; pub struct WatchState<'a> { writer: StdoutLock<'a>, - rx: Receiver, - exercises: &'a [Exercise], + exercises: &'static [Exercise], exercise: &'a Exercise, current_exercise_ind: usize, stdout: Option>, @@ -30,11 +27,7 @@ pub struct WatchState<'a> { } impl<'a> WatchState<'a> { - pub fn new( - state_file: &StateFile, - exercises: &'a [Exercise], - rx: Receiver, - ) -> Self { + pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { let current_exercise_ind = state_file.next_exercise_ind(); let exercise = &exercises[current_exercise_ind]; @@ -50,7 +43,6 @@ impl<'a> WatchState<'a> { Self { writer, - rx, exercises, exercise, current_exercise_ind, @@ -114,41 +106,14 @@ You can keep working on this exercise or jump into the next one by removing the Ok(true) } - pub fn try_recv_event(&mut self) -> Result<()> { - let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { - return Ok(()); - }; - - if let Some(current_exercise_ind) = events? - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises - .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - { - self.current_exercise_ind = current_exercise_ind; - } else { - return Ok(()); - }; - - while self.current_exercise_ind < self.exercises.len() { - self.exercise = &self.exercises[self.current_exercise_ind]; - if !self.run_exercise()? { - break; - } - - self.current_exercise_ind += 1; - } + pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result { + self.exercise = self + .exercises + .get(exercise_ind) + .context("Invalid exercise index")?; + self.current_exercise_ind = exercise_ind; - Ok(()) + self.run_exercise() } pub fn show_prompt(&mut self) -> io::Result<()> { @@ -156,7 +121,7 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.flush() } - pub fn render(&mut self) -> io::Result<()> { + pub fn render(&mut self) -> Result<()> { self.writer.execute(Clear(ClearType::All))?; if let Some(stdout) = &self.stdout { @@ -171,7 +136,17 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.write_all(message.as_bytes())?; } - self.show_prompt() + let line_width = size()?.0; + let progress_bar = progress_bar( + self.current_exercise_ind as u16, + self.exercises.len() as u16, + line_width, + )?; + self.writer.write_all(progress_bar.as_bytes())?; + + self.show_prompt()?; + + Ok(()) } pub fn show_hint(&mut self) -> io::Result<()> { -- cgit v1.2.3 From 4110ae21afd2c026e49d330918e212f4ab0eb5cc Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 21:46:55 +0200 Subject: Handle notify errors --- src/watch.rs | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index abf4002..5a1e38a 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,6 +1,8 @@ use anyhow::{bail, Context, Result}; use notify_debouncer_mini::{ - new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, + new_debouncer, + notify::{self, RecursiveMode}, + DebounceEventResult, DebouncedEventKind, }; use std::{ io::{self, BufRead, Write}, @@ -26,6 +28,7 @@ enum InputEvent { enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, + NotifyErr(notify::Error), TerminalResize, } @@ -36,30 +39,32 @@ struct DebouceEventHandler { impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { fn handle_event(&mut self, event: DebounceEventResult) { - let Ok(event) = event else { - // TODO - return; - }; - - let Some(exercise_ind) = event - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises + let event = match event { + Ok(event) => { + let Some(exercise_ind) = event .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - else { - return; + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + else { + return; + }; + + WatchEvent::FileChange { exercise_ind } + } + Err(e) => WatchEvent::NotifyErr(e), }; - self.tx.send(WatchEvent::FileChange { exercise_ind }); + let _ = self.tx.send(event); } } @@ -125,6 +130,7 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<( watch_state.run_exercise_with_ind(exercise_ind)?; watch_state.render()?; } + WatchEvent::NotifyErr(e) => return Err(e.into()), } } -- cgit v1.2.3 From ff6c15f9c15ae80b48d3acd7091eb6328c931e7a Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 22:04:10 +0200 Subject: Don't try to join the input thread --- src/watch.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 5a1e38a..6324eb3 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Result}; +use anyhow::Result; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, @@ -29,6 +29,7 @@ enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, NotifyErr(notify::Error), + StdinErr(io::Error), TerminalResize, } @@ -64,18 +65,23 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { Err(e) => WatchEvent::NotifyErr(e), }; + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. let _ = self.tx.send(event); } } -fn input_handler(tx: Sender) -> Result<()> { +fn input_handler(tx: Sender) { let mut stdin = io::stdin().lock(); let mut stdin_buf = String::with_capacity(8); loop { - stdin - .read_line(&mut stdin_buf) - .context("Failed to read the user's input from stdin")?; + if let Err(e) = stdin.read_line(&mut stdin_buf) { + // If `send` returns an error, then the receiver is dropped and + // a shutdown has been already initialized. + let _ = tx.send(WatchEvent::StdinErr(e)); + return; + } let event = match stdin_buf.trim() { "h" | "hint" => InputEvent::Hint, @@ -87,7 +93,8 @@ fn input_handler(tx: Sender) -> Result<()> { stdin_buf.clear(); if tx.send(WatchEvent::Input(event)).is_err() { - return Ok(()); + // The receiver was dropped. + return; } } } @@ -111,7 +118,7 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<( watch_state.run_exercise()?; watch_state.render()?; - let input_thread = thread::spawn(move || input_handler(tx)); + thread::spawn(move || input_handler(tx)); while let Ok(event) = rx.recv() { match event { @@ -131,21 +138,14 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<( watch_state.render()?; } WatchEvent::NotifyErr(e) => return Err(e.into()), + WatchEvent::StdinErr(e) => return Err(e.into()), } } - // Drop the receiver for the sender threads to exit. - drop(rx); - watch_state.into_writer().write_all(b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; - match input_thread.join() { - Ok(res) => res?, - Err(_) => bail!("The input thread panicked"), - } - Ok(()) } -- cgit v1.2.3 From d1a965f019d0e8f22d5a57f0a7abd8cd4a8d0d0c Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 02:12:50 +0200 Subject: Make the list mode part of the watch mode --- src/main.rs | 19 +++++++++++-------- src/watch.rs | 27 +++++++++++++++++++++++---- src/watch/state.rs | 5 +++-- 3 files changed, 37 insertions(+), 14 deletions(-) (limited to 'src/watch.rs') diff --git a/src/main.rs b/src/main.rs index 504c02d..fc83e0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,9 +16,11 @@ mod watch; use self::{ consts::WELCOME, exercise::{Exercise, InfoFile}, + list::list, run::run, state_file::StateFile, verify::{verify, VerifyState}, + watch::{watch, WatchExit}, }; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code @@ -52,8 +54,6 @@ enum Subcommands { /// The name of the exercise name: String, }, - /// List the exercises available in Rustlings - List, } fn find_exercise(name: &str, exercises: &'static [Exercise]) -> Result<(usize, &'static Exercise)> { @@ -112,14 +112,17 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let mut state_file = StateFile::read_or_default(exercises); match args.command { - None | Some(Subcommands::Watch) => { - watch::watch(&state_file, exercises)?; - } + None | Some(Subcommands::Watch) => loop { + match watch(&mut state_file, exercises)? { + WatchExit::Shutdown => break, + // It is much easier to exit the watch mode, launch the list mode and then restart + // the watch mode instead of trying to pause the watch threads and correct the + // watch state. + WatchExit::List => list(&mut state_file, exercises)?, + } + }, // `Init` is handled above. Some(Subcommands::Init) => (), - Some(Subcommands::List) => { - list::list(&mut state_file, exercises)?; - } Some(Subcommands::Run { name }) => { let (_, exercise) = find_exercise(&name, exercises)?; run(exercise).unwrap_or_else(|_| exit(1)); diff --git a/src/watch.rs b/src/watch.rs index 6324eb3..004a13f 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -18,9 +18,19 @@ use crate::{exercise::Exercise, state_file::StateFile}; use self::state::WatchState; +/// Returned by the watch mode to indicate what to do afterwards. +pub enum WatchExit { + /// Exit the program. + Shutdown, + /// Enter the list mode and restart the watch mode afterwards. + List, +} + +#[derive(Copy, Clone)] enum InputEvent { Hint, Clear, + List, Quit, Unrecognized, } @@ -86,20 +96,26 @@ fn input_handler(tx: Sender) { let event = match stdin_buf.trim() { "h" | "hint" => InputEvent::Hint, "c" | "clear" => InputEvent::Clear, + "l" | "list" => InputEvent::List, "q" | "quit" => InputEvent::Quit, _ => InputEvent::Unrecognized, }; - stdin_buf.clear(); - if tx.send(WatchEvent::Input(event)).is_err() { // The receiver was dropped. return; } + + match event { + InputEvent::List | InputEvent::Quit => return, + _ => (), + } + + stdin_buf.clear(); } } -pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> { +pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( Duration::from_secs(1), @@ -125,6 +141,9 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<( WatchEvent::Input(InputEvent::Hint) => { watch_state.show_hint()?; } + WatchEvent::Input(InputEvent::List) => { + return Ok(WatchExit::List); + } WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => { watch_state.render()?; } @@ -147,5 +166,5 @@ We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; - Ok(()) + Ok(WatchExit::Shutdown) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 4db9440..393ea02 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -36,10 +36,11 @@ impl<'a> WatchState<'a> { let writer = io::stdout().lock(); let prompt = format!( - "\n\n{}int/{}lear/{}uit? ", + "\n\n{}int/{}lear/{}ist/{}uit? ", "h".bold(), "c".bold(), - "q".bold() + "l".bold(), + "q".bold(), ) .into_bytes(); -- cgit v1.2.3 From f034899c7f8de93ff572722b1cdf44f73c6452b5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 03:54:48 +0200 Subject: Capture terminal resize events --- src/watch.rs | 91 +++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 31 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 004a13f..7b4a02d 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,11 +1,12 @@ -use anyhow::Result; +use anyhow::{Error, Result}; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, DebounceEventResult, DebouncedEventKind, }; use std::{ - io::{self, BufRead, Write}, + io::{self, Write}, path::Path, sync::mpsc::{channel, Sender}, thread, @@ -39,7 +40,7 @@ enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, NotifyErr(notify::Error), - StdinErr(io::Error), + TerminalEventErr(io::Error), TerminalResize, } @@ -81,37 +82,61 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { } } -fn input_handler(tx: Sender) { - let mut stdin = io::stdin().lock(); - let mut stdin_buf = String::with_capacity(8); +fn terminal_event_handler(tx: Sender) { + let mut input = String::with_capacity(8); loop { - if let Err(e) = stdin.read_line(&mut stdin_buf) { - // If `send` returns an error, then the receiver is dropped and - // a shutdown has been already initialized. - let _ = tx.send(WatchEvent::StdinErr(e)); - return; - } - - let event = match stdin_buf.trim() { - "h" | "hint" => InputEvent::Hint, - "c" | "clear" => InputEvent::Clear, - "l" | "list" => InputEvent::List, - "q" | "quit" => InputEvent::Quit, - _ => InputEvent::Unrecognized, + let terminal_event = match event::read() { + Ok(v) => v, + Err(e) => { + // If `send` returns an error, then the receiver is dropped and + // a shutdown has been already initialized. + let _ = tx.send(WatchEvent::TerminalEventErr(e)); + return; + } }; - if tx.send(WatchEvent::Input(event)).is_err() { - // The receiver was dropped. - return; - } + match terminal_event { + Event::Key(key) => { + match key.kind { + KeyEventKind::Release => continue, + KeyEventKind::Press | KeyEventKind::Repeat => (), + } + + match key.code { + KeyCode::Enter => { + let input_event = match input.trim() { + "h" | "hint" => InputEvent::Hint, + "c" | "clear" => InputEvent::Clear, + "l" | "list" => InputEvent::List, + "q" | "quit" => InputEvent::Quit, + _ => InputEvent::Unrecognized, + }; + + if tx.send(WatchEvent::Input(input_event)).is_err() { + return; + } - match event { - InputEvent::List | InputEvent::Quit => return, - _ => (), - } + match input_event { + InputEvent::List | InputEvent::Quit => return, + _ => (), + } - stdin_buf.clear(); + input.clear(); + } + KeyCode::Char(c) => { + input.push(c); + } + _ => (), + } + } + Event::Resize(_, _) => { + if tx.send(WatchEvent::TerminalResize).is_err() { + return; + } + } + Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, + } } } @@ -134,7 +159,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu watch_state.run_exercise()?; watch_state.render()?; - thread::spawn(move || input_handler(tx)); + thread::spawn(move || terminal_event_handler(tx)); while let Ok(event) = rx.recv() { match event { @@ -156,8 +181,12 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu watch_state.run_exercise_with_ind(exercise_ind)?; watch_state.render()?; } - WatchEvent::NotifyErr(e) => return Err(e.into()), - WatchEvent::StdinErr(e) => return Err(e.into()), + WatchEvent::NotifyErr(e) => { + return Err(Error::from(e).context("Exercise file watcher failed")) + } + WatchEvent::TerminalEventErr(e) => { + return Err(Error::from(e).context("Terminal event listener failed")) + } } } -- cgit v1.2.3 From 6255efe8b2de9d8d7f69871584444ab34fae122d Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 04:08:40 +0200 Subject: Show the invalid command to avoid confusion after resizing the terminal --- src/watch.rs | 24 ++++++++++-------------- src/watch/state.rs | 9 +++++++-- 2 files changed, 17 insertions(+), 16 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 7b4a02d..8b21103 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -27,13 +27,12 @@ pub enum WatchExit { List, } -#[derive(Copy, Clone)] enum InputEvent { Hint, Clear, List, Quit, - Unrecognized, + Unrecognized(String), } enum WatchEvent { @@ -85,7 +84,7 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { fn terminal_event_handler(tx: Sender) { let mut input = String::with_capacity(8); - loop { + let last_input_event = loop { let terminal_event = match event::read() { Ok(v) => v, Err(e) => { @@ -108,20 +107,15 @@ fn terminal_event_handler(tx: Sender) { let input_event = match input.trim() { "h" | "hint" => InputEvent::Hint, "c" | "clear" => InputEvent::Clear, - "l" | "list" => InputEvent::List, - "q" | "quit" => InputEvent::Quit, - _ => InputEvent::Unrecognized, + "l" | "list" => break InputEvent::List, + "q" | "quit" => break InputEvent::Quit, + _ => InputEvent::Unrecognized(input.clone()), }; if tx.send(WatchEvent::Input(input_event)).is_err() { return; } - match input_event { - InputEvent::List | InputEvent::Quit => return, - _ => (), - } - input.clear(); } KeyCode::Char(c) => { @@ -137,7 +131,9 @@ fn terminal_event_handler(tx: Sender) { } Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, } - } + }; + + let _ = tx.send(WatchEvent::Input(last_input_event)); } pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result { @@ -173,8 +169,8 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu watch_state.render()?; } WatchEvent::Input(InputEvent::Quit) => break, - WatchEvent::Input(InputEvent::Unrecognized) => { - watch_state.handle_invalid_cmd()?; + WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { + watch_state.handle_invalid_cmd(&cmd)?; } WatchEvent::FileChange { exercise_ind } => { // TODO: bool diff --git a/src/watch/state.rs b/src/watch/state.rs index 08707a4..751285f 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -159,8 +159,13 @@ You can keep working on this exercise or jump into the next one by removing the self.show_prompt() } - pub fn handle_invalid_cmd(&mut self) -> io::Result<()> { - self.writer.write_all(b"Invalid command")?; + pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> { + self.writer.write_all(b"Invalid command: ")?; + self.writer.write_all(cmd.as_bytes())?; + if cmd.len() > 1 { + self.writer + .write_all(b" (confusing input can occur after resizing the terminal)")?; + } self.show_prompt() } } -- cgit v1.2.3 From 62e92476e6dad1fc191fd666eae2fccb263f5ff0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 04:10:05 +0200 Subject: Fix typo --- src/watch.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 8b21103..cf63627 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -43,12 +43,12 @@ enum WatchEvent { TerminalResize, } -struct DebouceEventHandler { +struct DebounceEventHandler { tx: Sender, exercises: &'static [Exercise], } -impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { +impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { fn handle_event(&mut self, event: DebounceEventResult) { let event = match event { Ok(event) => { @@ -140,7 +140,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu let (tx, rx) = channel(); let mut debouncer = new_debouncer( Duration::from_secs(1), - DebouceEventHandler { + DebounceEventHandler { tx: tx.clone(), exercises, }, -- cgit v1.2.3 From 256c4013b759368b97f08aeb38d1b03f2eb42d7a Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 15:56:38 +0200 Subject: Keep hint displayed after resizing the terminal --- src/watch.rs | 4 +--- src/watch/state.rs | 41 +++++++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 19 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index cf63627..6d791f4 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -29,7 +29,6 @@ pub enum WatchExit { enum InputEvent { Hint, - Clear, List, Quit, Unrecognized(String), @@ -106,7 +105,6 @@ fn terminal_event_handler(tx: Sender) { KeyCode::Enter => { let input_event = match input.trim() { "h" | "hint" => InputEvent::Hint, - "c" | "clear" => InputEvent::Clear, "l" | "list" => break InputEvent::List, "q" | "quit" => break InputEvent::Quit, _ => InputEvent::Unrecognized(input.clone()), @@ -165,7 +163,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu WatchEvent::Input(InputEvent::List) => { return Ok(WatchExit::List); } - WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => { + WatchEvent::TerminalResize => { watch_state.render()?; } WatchEvent::Input(InputEvent::Quit) => break, diff --git a/src/watch/state.rs b/src/watch/state.rs index da5ac3d..6f6d2f1 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use std::{ fmt::Write as _, - io::{self, StdoutLock, Write as _}, + io::{self, StdoutLock, Write}, }; use crate::{ @@ -24,7 +24,7 @@ pub struct WatchState<'a> { stdout: Option>, stderr: Option>, message: Option, - prompt: Vec, + hint_displayed: bool, } impl<'a> WatchState<'a> { @@ -35,15 +35,6 @@ impl<'a> WatchState<'a> { let writer = io::stdout().lock(); - let prompt = format!( - "\n\n{}int/{}lear/{}ist/{}uit? ", - "h".bold(), - "c".bold(), - "l".bold(), - "q".bold(), - ) - .into_bytes(); - Self { writer, exercises, @@ -53,7 +44,7 @@ impl<'a> WatchState<'a> { stdout: None, stderr: None, message: None, - prompt, + hint_displayed: false, } } @@ -122,7 +113,15 @@ You can keep working on this exercise or jump into the next one by removing the } pub fn show_prompt(&mut self) -> io::Result<()> { - self.writer.write_all(&self.prompt)?; + self.writer.write_all(b"\n\n")?; + + if !self.hint_displayed { + self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; + } + + self.writer + .write_fmt(format_args!("{}ist/{}uit? ", 'l'.bold(), 'q'.bold()))?; + self.writer.flush() } @@ -134,10 +133,12 @@ You can keep working on this exercise or jump into the next one by removing the if let Some(stdout) = &self.stdout { self.writer.write_all(stdout)?; + self.writer.write_all(b"\n")?; } if let Some(stderr) = &self.stderr { self.writer.write_all(stderr)?; + self.writer.write_all(b"\n")?; } if let Some(message) = &self.message { @@ -145,6 +146,14 @@ You can keep working on this exercise or jump into the next one by removing the } self.writer.write_all(b"\n")?; + + if self.hint_displayed { + self.writer + .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?; + self.writer.write_all(self.exercise.hint.as_bytes())?; + self.writer.write_all(b"\n\n")?; + } + let line_width = size()?.0; let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?; self.writer.write_all(progress_bar.as_bytes())?; @@ -160,9 +169,9 @@ You can keep working on this exercise or jump into the next one by removing the Ok(()) } - pub fn show_hint(&mut self) -> io::Result<()> { - self.writer.write_all(self.exercise.hint.as_bytes())?; - self.show_prompt() + pub fn show_hint(&mut self) -> Result<()> { + self.hint_displayed = true; + self.render() } pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> { -- cgit v1.2.3 From 4bb6bda9f6416e30233342e73fc9a8486faa3f98 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 16:02:12 +0200 Subject: Separate event handlers --- src/watch.rs | 123 +++++--------------------------------------- src/watch/debounce_event.rs | 44 ++++++++++++++++ src/watch/terminal_event.rs | 65 +++++++++++++++++++++++ 3 files changed, 123 insertions(+), 109 deletions(-) create mode 100644 src/watch/debounce_event.rs create mode 100644 src/watch/terminal_event.rs (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 6d791f4..b29169b 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,38 +1,27 @@ use anyhow::{Error, Result}; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, - DebounceEventResult, DebouncedEventKind, }; use std::{ io::{self, Write}, path::Path, - sync::mpsc::{channel, Sender}, + sync::mpsc::channel, thread, time::Duration, }; +mod debounce_event; mod state; +mod terminal_event; use crate::{exercise::Exercise, state_file::StateFile}; -use self::state::WatchState; - -/// Returned by the watch mode to indicate what to do afterwards. -pub enum WatchExit { - /// Exit the program. - Shutdown, - /// Enter the list mode and restart the watch mode afterwards. - List, -} - -enum InputEvent { - Hint, - List, - Quit, - Unrecognized(String), -} +use self::{ + debounce_event::DebounceEventHandler, + state::WatchState, + terminal_event::{terminal_event_handler, InputEvent}, +}; enum WatchEvent { Input(InputEvent), @@ -42,96 +31,12 @@ enum WatchEvent { TerminalResize, } -struct DebounceEventHandler { - tx: Sender, - exercises: &'static [Exercise], -} - -impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { - fn handle_event(&mut self, event: DebounceEventResult) { - let event = match event { - Ok(event) => { - let Some(exercise_ind) = event - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises - .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - else { - return; - }; - - WatchEvent::FileChange { exercise_ind } - } - Err(e) => WatchEvent::NotifyErr(e), - }; - - // An error occurs when the receiver is dropped. - // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.tx.send(event); - } -} - -fn terminal_event_handler(tx: Sender) { - let mut input = String::with_capacity(8); - - let last_input_event = loop { - let terminal_event = match event::read() { - Ok(v) => v, - Err(e) => { - // If `send` returns an error, then the receiver is dropped and - // a shutdown has been already initialized. - let _ = tx.send(WatchEvent::TerminalEventErr(e)); - return; - } - }; - - match terminal_event { - Event::Key(key) => { - match key.kind { - KeyEventKind::Release => continue, - KeyEventKind::Press | KeyEventKind::Repeat => (), - } - - match key.code { - KeyCode::Enter => { - let input_event = match input.trim() { - "h" | "hint" => InputEvent::Hint, - "l" | "list" => break InputEvent::List, - "q" | "quit" => break InputEvent::Quit, - _ => InputEvent::Unrecognized(input.clone()), - }; - - if tx.send(WatchEvent::Input(input_event)).is_err() { - return; - } - - input.clear(); - } - KeyCode::Char(c) => { - input.push(c); - } - _ => (), - } - } - Event::Resize(_, _) => { - if tx.send(WatchEvent::TerminalResize).is_err() { - return; - } - } - Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, - } - }; - - let _ = tx.send(WatchEvent::Input(last_input_event)); +/// Returned by the watch mode to indicate what to do afterwards. +pub enum WatchExit { + /// Exit the program. + Shutdown, + /// Enter the list mode and restart the watch mode afterwards. + List, } pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result { diff --git a/src/watch/debounce_event.rs b/src/watch/debounce_event.rs new file mode 100644 index 0000000..1dc92cb --- /dev/null +++ b/src/watch/debounce_event.rs @@ -0,0 +1,44 @@ +use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; +use std::sync::mpsc::Sender; + +use crate::exercise::Exercise; + +use super::WatchEvent; + +pub struct DebounceEventHandler { + pub tx: Sender, + pub exercises: &'static [Exercise], +} + +impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { + fn handle_event(&mut self, event: DebounceEventResult) { + let event = match event { + Ok(event) => { + let Some(exercise_ind) = event + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + else { + return; + }; + + WatchEvent::FileChange { exercise_ind } + } + Err(e) => WatchEvent::NotifyErr(e), + }; + + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. + let _ = self.tx.send(event); + } +} diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs new file mode 100644 index 0000000..7c85b5b --- /dev/null +++ b/src/watch/terminal_event.rs @@ -0,0 +1,65 @@ +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use std::sync::mpsc::Sender; + +use super::WatchEvent; + +pub enum InputEvent { + Hint, + List, + Quit, + Unrecognized(String), +} + +pub fn terminal_event_handler(tx: Sender) { + let mut input = String::with_capacity(8); + + let last_input_event = loop { + let terminal_event = match event::read() { + Ok(v) => v, + Err(e) => { + // If `send` returns an error, then the receiver is dropped and + // a shutdown has been already initialized. + let _ = tx.send(WatchEvent::TerminalEventErr(e)); + return; + } + }; + + match terminal_event { + Event::Key(key) => { + match key.kind { + KeyEventKind::Release => continue, + KeyEventKind::Press | KeyEventKind::Repeat => (), + } + + match key.code { + KeyCode::Enter => { + let input_event = match input.trim() { + "h" | "hint" => InputEvent::Hint, + "l" | "list" => break InputEvent::List, + "q" | "quit" => break InputEvent::Quit, + _ => InputEvent::Unrecognized(input.clone()), + }; + + if tx.send(WatchEvent::Input(input_event)).is_err() { + return; + } + + input.clear(); + } + KeyCode::Char(c) => { + input.push(c); + } + _ => (), + } + } + Event::Resize(_, _) => { + if tx.send(WatchEvent::TerminalResize).is_err() { + return; + } + } + Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, + } + }; + + let _ = tx.send(WatchEvent::Input(last_input_event)); +} -- cgit v1.2.3 From fa1f239a702eb2c0b7e0115e986481156961bbc8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 02:51:02 +0200 Subject: Remove "I AM NOT DONE" and the verify mode and add AppState --- Cargo.lock | 1 - Cargo.toml | 1 - README.md | 8 +- exercises/00_intro/intro1.rs | 4 +- exercises/00_intro/intro2.rs | 2 - exercises/01_variables/variables1.rs | 2 - exercises/01_variables/variables2.rs | 2 - exercises/01_variables/variables3.rs | 2 - exercises/01_variables/variables4.rs | 2 - exercises/01_variables/variables5.rs | 2 - exercises/01_variables/variables6.rs | 2 - exercises/02_functions/functions1.rs | 2 - exercises/02_functions/functions2.rs | 2 - exercises/02_functions/functions3.rs | 2 - exercises/02_functions/functions4.rs | 2 - exercises/02_functions/functions5.rs | 2 - exercises/03_if/if1.rs | 2 - exercises/03_if/if2.rs | 2 - exercises/03_if/if3.rs | 2 - exercises/04_primitive_types/primitive_types1.rs | 2 - exercises/04_primitive_types/primitive_types2.rs | 2 - exercises/04_primitive_types/primitive_types3.rs | 2 - exercises/04_primitive_types/primitive_types4.rs | 2 - exercises/04_primitive_types/primitive_types5.rs | 2 - exercises/04_primitive_types/primitive_types6.rs | 2 - exercises/05_vecs/vecs1.rs | 2 - exercises/05_vecs/vecs2.rs | 2 - exercises/06_move_semantics/move_semantics1.rs | 2 - exercises/06_move_semantics/move_semantics2.rs | 2 - exercises/06_move_semantics/move_semantics3.rs | 2 - exercises/06_move_semantics/move_semantics4.rs | 2 - exercises/06_move_semantics/move_semantics5.rs | 2 - exercises/06_move_semantics/move_semantics6.rs | 2 - exercises/07_structs/structs1.rs | 2 - exercises/07_structs/structs2.rs | 2 - exercises/07_structs/structs3.rs | 2 - exercises/08_enums/enums1.rs | 2 - exercises/08_enums/enums2.rs | 2 - exercises/08_enums/enums3.rs | 2 - exercises/09_strings/strings1.rs | 2 - exercises/09_strings/strings2.rs | 2 - exercises/09_strings/strings3.rs | 2 - exercises/09_strings/strings4.rs | 2 - exercises/10_modules/modules1.rs | 2 - exercises/10_modules/modules2.rs | 2 - exercises/10_modules/modules3.rs | 2 - exercises/11_hashmaps/hashmaps1.rs | 2 - exercises/11_hashmaps/hashmaps2.rs | 2 - exercises/11_hashmaps/hashmaps3.rs | 2 - exercises/12_options/options1.rs | 2 - exercises/12_options/options2.rs | 2 - exercises/12_options/options3.rs | 2 - exercises/13_error_handling/errors1.rs | 2 - exercises/13_error_handling/errors2.rs | 2 - exercises/13_error_handling/errors3.rs | 2 - exercises/13_error_handling/errors4.rs | 2 - exercises/13_error_handling/errors5.rs | 2 - exercises/13_error_handling/errors6.rs | 2 - exercises/14_generics/generics1.rs | 2 - exercises/14_generics/generics2.rs | 2 - exercises/15_traits/traits1.rs | 2 - exercises/15_traits/traits2.rs | 2 - exercises/15_traits/traits3.rs | 2 - exercises/15_traits/traits4.rs | 2 - exercises/15_traits/traits5.rs | 2 - exercises/16_lifetimes/lifetimes1.rs | 2 - exercises/16_lifetimes/lifetimes2.rs | 2 - exercises/16_lifetimes/lifetimes3.rs | 2 - exercises/17_tests/tests1.rs | 2 - exercises/17_tests/tests2.rs | 2 - exercises/17_tests/tests3.rs | 2 - exercises/17_tests/tests4.rs | 2 - exercises/18_iterators/iterators1.rs | 2 - exercises/18_iterators/iterators2.rs | 2 - exercises/18_iterators/iterators3.rs | 2 - exercises/18_iterators/iterators4.rs | 2 - exercises/18_iterators/iterators5.rs | 2 - exercises/19_smart_pointers/arc1.rs | 2 - exercises/19_smart_pointers/box1.rs | 2 - exercises/19_smart_pointers/cow1.rs | 2 - exercises/19_smart_pointers/rc1.rs | 2 - exercises/20_threads/threads1.rs | 2 - exercises/20_threads/threads2.rs | 2 - exercises/20_threads/threads3.rs | 2 - exercises/21_macros/macros1.rs | 2 - exercises/21_macros/macros2.rs | 2 - exercises/21_macros/macros3.rs | 2 - exercises/21_macros/macros4.rs | 2 - exercises/22_clippy/clippy1.rs | 2 - exercises/22_clippy/clippy2.rs | 2 - exercises/22_clippy/clippy3.rs | 2 - exercises/23_conversions/as_ref_mut.rs | 2 - exercises/23_conversions/from_into.rs | 2 - exercises/23_conversions/from_str.rs | 2 - exercises/23_conversions/try_from_into.rs | 2 - exercises/23_conversions/using_as.rs | 2 - exercises/quiz1.rs | 2 - exercises/quiz2.rs | 2 - exercises/quiz3.rs | 2 - info.toml | 7 +- src/app_state.rs | 185 ++++++++++++++++++ src/exercise.rs | 206 +-------------------- src/list.rs | 21 +-- src/list/state.rs | 71 +++---- src/main.rs | 67 +++---- src/run.rs | 23 ++- src/state_file.rs | 68 ------- src/verify.rs | 85 --------- src/watch.rs | 10 +- src/watch/state.rs | 96 +++------- tests/fixture/state/exercises/pending_exercise.rs | 2 - .../state/exercises/pending_test_exercise.rs | 2 - tests/integration_tests.rs | 28 +-- 113 files changed, 306 insertions(+), 769 deletions(-) create mode 100644 src/app_state.rs delete mode 100644 src/state_file.rs delete mode 100644 src/verify.rs (limited to 'src/watch.rs') diff --git a/Cargo.lock b/Cargo.lock index ee46943..aeb6c61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,7 +699,6 @@ dependencies = [ "serde_json", "toml_edit", "which", - "winnow", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index da09ba1..435dfd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ serde_json = "1.0.115" serde.workspace = true toml_edit.workspace = true which = "6.0.1" -winnow = "0.6.5" [dev-dependencies] assert_cmd = "2.0.14" diff --git a/README.md b/README.md index 6b9c983..fd76fdf 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,7 @@ The task is simple. Most exercises contain an error that keeps them from compili rustlings watch ``` -This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory. If you want to only run it once, you can use: - -```bash -rustlings verify -``` - -This will do the same as watch, but it'll quit after running. +This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory. In case you want to go by your own order, or want to only verify a single exercise, you can run: diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 5dd18b4..aa505a1 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,6 +1,6 @@ // intro1.rs // -// About this `I AM NOT DONE` thing: +// TODO: Update comment // We sometimes encourage you to keep trying things on a given exercise, even // after you already figured it out. If you got everything working and feel // ready for the next exercise, remove the `I AM NOT DONE` comment below. @@ -13,8 +13,6 @@ // Execute `rustlings hint intro1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { println!("Hello and"); println!(r#" welcome to... "#); diff --git a/exercises/00_intro/intro2.rs b/exercises/00_intro/intro2.rs index a28ad3d..84e0d75 100644 --- a/exercises/00_intro/intro2.rs +++ b/exercises/00_intro/intro2.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint intro2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { printline!("Hello there!") } diff --git a/exercises/01_variables/variables1.rs b/exercises/01_variables/variables1.rs index b3e089a..56408f3 100644 --- a/exercises/01_variables/variables1.rs +++ b/exercises/01_variables/variables1.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint variables1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { x = 5; println!("x has the value {}", x); diff --git a/exercises/01_variables/variables2.rs b/exercises/01_variables/variables2.rs index e1c23ed..0f417e0 100644 --- a/exercises/01_variables/variables2.rs +++ b/exercises/01_variables/variables2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let x; if x == 10 { diff --git a/exercises/01_variables/variables3.rs b/exercises/01_variables/variables3.rs index 86bed41..421c6b1 100644 --- a/exercises/01_variables/variables3.rs +++ b/exercises/01_variables/variables3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let x: i32; println!("Number {}", x); diff --git a/exercises/01_variables/variables4.rs b/exercises/01_variables/variables4.rs index 5394f39..68f8f50 100644 --- a/exercises/01_variables/variables4.rs +++ b/exercises/01_variables/variables4.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let x = 3; println!("Number {}", x); diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs index a29b38b..7014c56 100644 --- a/exercises/01_variables/variables5.rs +++ b/exercises/01_variables/variables5.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let number = "T-H-R-E-E"; // don't change this line println!("Spell a Number : {}", number); diff --git a/exercises/01_variables/variables6.rs b/exercises/01_variables/variables6.rs index 853183b..9f47682 100644 --- a/exercises/01_variables/variables6.rs +++ b/exercises/01_variables/variables6.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables6` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - const NUMBER = 3; fn main() { println!("Number {}", NUMBER); diff --git a/exercises/02_functions/functions1.rs b/exercises/02_functions/functions1.rs index 40ed9a0..2365f91 100644 --- a/exercises/02_functions/functions1.rs +++ b/exercises/02_functions/functions1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint functions1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { call_me(); } diff --git a/exercises/02_functions/functions2.rs b/exercises/02_functions/functions2.rs index 5154f34..64dbd66 100644 --- a/exercises/02_functions/functions2.rs +++ b/exercises/02_functions/functions2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint functions2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { call_me(3); } diff --git a/exercises/02_functions/functions3.rs b/exercises/02_functions/functions3.rs index 74f44d6..5037121 100644 --- a/exercises/02_functions/functions3.rs +++ b/exercises/02_functions/functions3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint functions3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { call_me(); } diff --git a/exercises/02_functions/functions4.rs b/exercises/02_functions/functions4.rs index 77c4b2a..6b449ed 100644 --- a/exercises/02_functions/functions4.rs +++ b/exercises/02_functions/functions4.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint functions4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let original_price = 51; println!("Your sale price is {}", sale_price(original_price)); diff --git a/exercises/02_functions/functions5.rs b/exercises/02_functions/functions5.rs index f1b63f4..0c96322 100644 --- a/exercises/02_functions/functions5.rs +++ b/exercises/02_functions/functions5.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint functions5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let answer = square(3); println!("The square of 3 is {}", answer); diff --git a/exercises/03_if/if1.rs b/exercises/03_if/if1.rs index d2afccf..a1df66b 100644 --- a/exercises/03_if/if1.rs +++ b/exercises/03_if/if1.rs @@ -2,8 +2,6 @@ // // Execute `rustlings hint if1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - pub fn bigger(a: i32, b: i32) -> i32 { // Complete this function to return the bigger number! // If both numbers are equal, any of them can be returned. diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index f512f13..7b9c05f 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -5,8 +5,6 @@ // // Execute `rustlings hint if2` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - pub fn foo_if_fizz(fizzish: &str) -> &str { if fizzish == "fizz" { "foo" diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs index 1696274..caba172 100644 --- a/exercises/03_if/if3.rs +++ b/exercises/03_if/if3.rs @@ -2,8 +2,6 @@ // // Execute `rustlings hint if3` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - pub fn animal_habitat(animal: &str) -> &'static str { let identifier = if animal == "crab" { 1 diff --git a/exercises/04_primitive_types/primitive_types1.rs b/exercises/04_primitive_types/primitive_types1.rs index 3663340..f9169c8 100644 --- a/exercises/04_primitive_types/primitive_types1.rs +++ b/exercises/04_primitive_types/primitive_types1.rs @@ -3,8 +3,6 @@ // Fill in the rest of the line that has code missing! No hints, there's no // tricks, just get used to typing these :) -// I AM NOT DONE - fn main() { // Booleans (`bool`) diff --git a/exercises/04_primitive_types/primitive_types2.rs b/exercises/04_primitive_types/primitive_types2.rs index f1616ed..1911b12 100644 --- a/exercises/04_primitive_types/primitive_types2.rs +++ b/exercises/04_primitive_types/primitive_types2.rs @@ -3,8 +3,6 @@ // Fill in the rest of the line that has code missing! No hints, there's no // tricks, just get used to typing these :) -// I AM NOT DONE - fn main() { // Characters (`char`) diff --git a/exercises/04_primitive_types/primitive_types3.rs b/exercises/04_primitive_types/primitive_types3.rs index 8b0de44..70a8cc2 100644 --- a/exercises/04_primitive_types/primitive_types3.rs +++ b/exercises/04_primitive_types/primitive_types3.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint primitive_types3` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - fn main() { let a = ??? diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs index d44d877..8ed0a82 100644 --- a/exercises/04_primitive_types/primitive_types4.rs +++ b/exercises/04_primitive_types/primitive_types4.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint primitive_types4` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn slice_out_of_array() { let a = [1, 2, 3, 4, 5]; diff --git a/exercises/04_primitive_types/primitive_types5.rs b/exercises/04_primitive_types/primitive_types5.rs index f646986..5754a3d 100644 --- a/exercises/04_primitive_types/primitive_types5.rs +++ b/exercises/04_primitive_types/primitive_types5.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint primitive_types5` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - fn main() { let cat = ("Furry McFurson", 3.5); let /* your pattern here */ = cat; diff --git a/exercises/04_primitive_types/primitive_types6.rs b/exercises/04_primitive_types/primitive_types6.rs index 07cc46c..5f82f10 100644 --- a/exercises/04_primitive_types/primitive_types6.rs +++ b/exercises/04_primitive_types/primitive_types6.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint primitive_types6` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn indexing_tuple() { let numbers = (1, 2, 3); diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index 65b7a7f..c64acbb 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -7,8 +7,6 @@ // // Execute `rustlings hint vecs1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - fn array_and_vec() -> ([i32; 4], Vec) { let a = [10, 20, 30, 40]; // a plain array let v = // TODO: declare your vector here with the macro for vectors diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs index e92c970..d64d3d1 100644 --- a/exercises/05_vecs/vecs2.rs +++ b/exercises/05_vecs/vecs2.rs @@ -7,8 +7,6 @@ // // Execute `rustlings hint vecs2` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - fn vec_loop(mut v: Vec) -> Vec { for element in v.iter_mut() { // TODO: Fill this up so that each element in the Vec `v` is diff --git a/exercises/06_move_semantics/move_semantics1.rs b/exercises/06_move_semantics/move_semantics1.rs index e063937..c612ba9 100644 --- a/exercises/06_move_semantics/move_semantics1.rs +++ b/exercises/06_move_semantics/move_semantics1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint move_semantics1` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let vec0 = vec![22, 44, 66]; diff --git a/exercises/06_move_semantics/move_semantics2.rs b/exercises/06_move_semantics/move_semantics2.rs index dc58be5..3457d11 100644 --- a/exercises/06_move_semantics/move_semantics2.rs +++ b/exercises/06_move_semantics/move_semantics2.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint move_semantics2` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let vec0 = vec![22, 44, 66]; diff --git a/exercises/06_move_semantics/move_semantics3.rs b/exercises/06_move_semantics/move_semantics3.rs index 7152c71..9415eb1 100644 --- a/exercises/06_move_semantics/move_semantics3.rs +++ b/exercises/06_move_semantics/move_semantics3.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint move_semantics3` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let vec0 = vec![22, 44, 66]; diff --git a/exercises/06_move_semantics/move_semantics4.rs b/exercises/06_move_semantics/move_semantics4.rs index bfc917f..1509f5d 100644 --- a/exercises/06_move_semantics/move_semantics4.rs +++ b/exercises/06_move_semantics/move_semantics4.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint move_semantics4` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let vec0 = vec![22, 44, 66]; diff --git a/exercises/06_move_semantics/move_semantics5.rs b/exercises/06_move_semantics/move_semantics5.rs index 267bdcc..c84d2fe 100644 --- a/exercises/06_move_semantics/move_semantics5.rs +++ b/exercises/06_move_semantics/move_semantics5.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint move_semantics5` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let mut x = 100; diff --git a/exercises/06_move_semantics/move_semantics6.rs b/exercises/06_move_semantics/move_semantics6.rs index cace4ca..6059e61 100644 --- a/exercises/06_move_semantics/move_semantics6.rs +++ b/exercises/06_move_semantics/move_semantics6.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint move_semantics6` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - fn main() { let data = "Rust is great!".to_string(); diff --git a/exercises/07_structs/structs1.rs b/exercises/07_structs/structs1.rs index 5fa5821..2978121 100644 --- a/exercises/07_structs/structs1.rs +++ b/exercises/07_structs/structs1.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint structs1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct ColorClassicStruct { // TODO: Something goes here } diff --git a/exercises/07_structs/structs2.rs b/exercises/07_structs/structs2.rs index 328567f..a7a2dec 100644 --- a/exercises/07_structs/structs2.rs +++ b/exercises/07_structs/structs2.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint structs2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(Debug)] struct Order { name: String, diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 7cda5af..9835b81 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint structs3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(Debug)] struct Package { sender_country: String, diff --git a/exercises/08_enums/enums1.rs b/exercises/08_enums/enums1.rs index 25525b2..330269c 100644 --- a/exercises/08_enums/enums1.rs +++ b/exercises/08_enums/enums1.rs @@ -2,8 +2,6 @@ // // No hints this time! ;) -// I AM NOT DONE - #[derive(Debug)] enum Message { // TODO: define a few types of messages as used below diff --git a/exercises/08_enums/enums2.rs b/exercises/08_enums/enums2.rs index df93fe0..f0e4e6d 100644 --- a/exercises/08_enums/enums2.rs +++ b/exercises/08_enums/enums2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint enums2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(Debug)] enum Message { // TODO: define the different variants used below diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs index 92d18c4..580a553 100644 --- a/exercises/08_enums/enums3.rs +++ b/exercises/08_enums/enums3.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint enums3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - enum Message { // TODO: implement the message variant types based on their usage below } diff --git a/exercises/09_strings/strings1.rs b/exercises/09_strings/strings1.rs index f50e1fa..a1255a3 100644 --- a/exercises/09_strings/strings1.rs +++ b/exercises/09_strings/strings1.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint strings1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let answer = current_favorite_color(); println!("My current favorite color is {}", answer); diff --git a/exercises/09_strings/strings2.rs b/exercises/09_strings/strings2.rs index 4d95d16..ba76fe6 100644 --- a/exercises/09_strings/strings2.rs +++ b/exercises/09_strings/strings2.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint strings2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let word = String::from("green"); // Try not changing this line :) if is_a_color_word(word) { diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs index 384e7ce..dedc081 100644 --- a/exercises/09_strings/strings3.rs +++ b/exercises/09_strings/strings3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint strings3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn trim_me(input: &str) -> String { // TODO: Remove whitespace from both ends of a string! ??? diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs index e8c54ac..a034aa4 100644 --- a/exercises/09_strings/strings4.rs +++ b/exercises/09_strings/strings4.rs @@ -7,8 +7,6 @@ // // No hints this time! -// I AM NOT DONE - fn string_slice(arg: &str) { println!("{}", arg); } diff --git a/exercises/10_modules/modules1.rs b/exercises/10_modules/modules1.rs index 9eb5a48..c750946 100644 --- a/exercises/10_modules/modules1.rs +++ b/exercises/10_modules/modules1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint modules1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - mod sausage_factory { // Don't let anybody outside of this module see this! fn get_secret_recipe() -> String { diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs index 0415454..4d3106c 100644 --- a/exercises/10_modules/modules2.rs +++ b/exercises/10_modules/modules2.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint modules2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - mod delicious_snacks { // TODO: Fix these use statements use self::fruits::PEAR as ??? diff --git a/exercises/10_modules/modules3.rs b/exercises/10_modules/modules3.rs index f2bb050..c211a76 100644 --- a/exercises/10_modules/modules3.rs +++ b/exercises/10_modules/modules3.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint modules3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - // TODO: Complete this use statement use ??? diff --git a/exercises/11_hashmaps/hashmaps1.rs b/exercises/11_hashmaps/hashmaps1.rs index 80829ea..5a52f61 100644 --- a/exercises/11_hashmaps/hashmaps1.rs +++ b/exercises/11_hashmaps/hashmaps1.rs @@ -11,8 +11,6 @@ // Execute `rustlings hint hashmaps1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::collections::HashMap; fn fruit_basket() -> HashMap { diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs index a592569..2730643 100644 --- a/exercises/11_hashmaps/hashmaps2.rs +++ b/exercises/11_hashmaps/hashmaps2.rs @@ -14,8 +14,6 @@ // Execute `rustlings hint hashmaps2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::collections::HashMap; #[derive(Hash, PartialEq, Eq)] diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs index 8d9236d..775a401 100644 --- a/exercises/11_hashmaps/hashmaps3.rs +++ b/exercises/11_hashmaps/hashmaps3.rs @@ -15,8 +15,6 @@ // Execute `rustlings hint hashmaps3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::collections::HashMap; // A structure to store the goal details of a team. diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs index 3cbfecd..ba4b1cd 100644 --- a/exercises/12_options/options1.rs +++ b/exercises/12_options/options1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint options1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - // This function returns how much icecream there is left in the fridge. // If it's before 10PM, there's 5 scoops left. At 10PM, someone eats it // all, so there'll be no more left :( diff --git a/exercises/12_options/options2.rs b/exercises/12_options/options2.rs index 4d998e7..73f707e 100644 --- a/exercises/12_options/options2.rs +++ b/exercises/12_options/options2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint options2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[cfg(test)] mod tests { #[test] diff --git a/exercises/12_options/options3.rs b/exercises/12_options/options3.rs index 23c15ea..7922ef9 100644 --- a/exercises/12_options/options3.rs +++ b/exercises/12_options/options3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint options3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct Point { x: i32, y: i32, diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index 0ba59a5..9767f2c 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint errors1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub fn generate_nametag_text(name: String) -> Option { if name.is_empty() { // Empty names aren't allowed. diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs index 631fe67..88d1bf4 100644 --- a/exercises/13_error_handling/errors2.rs +++ b/exercises/13_error_handling/errors2.rs @@ -19,8 +19,6 @@ // Execute `rustlings hint errors2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::num::ParseIntError; pub fn total_cost(item_quantity: &str) -> Result { diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs index d42d3b1..56bb31b 100644 --- a/exercises/13_error_handling/errors3.rs +++ b/exercises/13_error_handling/errors3.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint errors3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::num::ParseIntError; fn main() { diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index d6d6fcb..0e5c08b 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint errors4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(PartialEq, Debug)] struct PositiveNonzeroInteger(u64); diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs index 92461a7..0bcb4b8 100644 --- a/exercises/13_error_handling/errors5.rs +++ b/exercises/13_error_handling/errors5.rs @@ -22,8 +22,6 @@ // Execute `rustlings hint errors5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::error; use std::fmt; use std::num::ParseIntError; diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index aaf0948..de73a9a 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint errors6` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::num::ParseIntError; // This is a custom error type that we will be using in `parse_pos_nonzero()`. diff --git a/exercises/14_generics/generics1.rs b/exercises/14_generics/generics1.rs index 35c1d2f..545fd95 100644 --- a/exercises/14_generics/generics1.rs +++ b/exercises/14_generics/generics1.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint generics1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let mut shopping_list: Vec = Vec::new(); shopping_list.push("milk"); diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs index 074cd93..d50ed17 100644 --- a/exercises/14_generics/generics2.rs +++ b/exercises/14_generics/generics2.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint generics2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct Wrapper { value: u32, } diff --git a/exercises/15_traits/traits1.rs b/exercises/15_traits/traits1.rs index 37dfcbf..c51d3b8 100644 --- a/exercises/15_traits/traits1.rs +++ b/exercises/15_traits/traits1.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint traits1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - trait AppendBar { fn append_bar(self) -> Self; } diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs index 3e35f8e..9a2bc07 100644 --- a/exercises/15_traits/traits2.rs +++ b/exercises/15_traits/traits2.rs @@ -8,8 +8,6 @@ // // Execute `rustlings hint traits2` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - trait AppendBar { fn append_bar(self) -> Self; } diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs index 4e2b06b..357f1d7 100644 --- a/exercises/15_traits/traits3.rs +++ b/exercises/15_traits/traits3.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint traits3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub trait Licensed { fn licensing_info(&self) -> String; } diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs index 4bda3e5..7242c48 100644 --- a/exercises/15_traits/traits4.rs +++ b/exercises/15_traits/traits4.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint traits4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub trait Licensed { fn licensing_info(&self) -> String { "some information".to_string() diff --git a/exercises/15_traits/traits5.rs b/exercises/15_traits/traits5.rs index df18380..f258d32 100644 --- a/exercises/15_traits/traits5.rs +++ b/exercises/15_traits/traits5.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint traits5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub trait SomeTrait { fn some_function(&self) -> bool { true diff --git a/exercises/16_lifetimes/lifetimes1.rs b/exercises/16_lifetimes/lifetimes1.rs index 87bde49..4f544b4 100644 --- a/exercises/16_lifetimes/lifetimes1.rs +++ b/exercises/16_lifetimes/lifetimes1.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint lifetimes1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x diff --git a/exercises/16_lifetimes/lifetimes2.rs b/exercises/16_lifetimes/lifetimes2.rs index 4f3d8c1..33b5565 100644 --- a/exercises/16_lifetimes/lifetimes2.rs +++ b/exercises/16_lifetimes/lifetimes2.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint lifetimes2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x diff --git a/exercises/16_lifetimes/lifetimes3.rs b/exercises/16_lifetimes/lifetimes3.rs index 9c59f9c..de6005e 100644 --- a/exercises/16_lifetimes/lifetimes3.rs +++ b/exercises/16_lifetimes/lifetimes3.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint lifetimes3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct Book { author: &str, title: &str, diff --git a/exercises/17_tests/tests1.rs b/exercises/17_tests/tests1.rs index 810277a..bde2108 100644 --- a/exercises/17_tests/tests1.rs +++ b/exercises/17_tests/tests1.rs @@ -10,8 +10,6 @@ // Execute `rustlings hint tests1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[cfg(test)] mod tests { #[test] diff --git a/exercises/17_tests/tests2.rs b/exercises/17_tests/tests2.rs index f8024e9..aea5c0e 100644 --- a/exercises/17_tests/tests2.rs +++ b/exercises/17_tests/tests2.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint tests2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[cfg(test)] mod tests { #[test] diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs index 4013e38..d815e05 100644 --- a/exercises/17_tests/tests3.rs +++ b/exercises/17_tests/tests3.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint tests3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub fn is_even(num: i32) -> bool { num % 2 == 0 } diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs index 935d0db..0972a5b 100644 --- a/exercises/17_tests/tests4.rs +++ b/exercises/17_tests/tests4.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint tests4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct Rectangle { width: i32, height: i32 diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index 31076bb..7ec7da2 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint iterators1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[test] fn main() { let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"]; diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs index dda82a0..4ca7742 100644 --- a/exercises/18_iterators/iterators2.rs +++ b/exercises/18_iterators/iterators2.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint iterators2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - // Step 1. // Complete the `capitalize_first` function. // "hello" -> "Hello" diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index 29fa23a..f7da049 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint iterators3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(Debug, PartialEq, Eq)] pub enum DivisionError { NotDivisible(NotDivisibleError), diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs index 3c0724e..af3958c 100644 --- a/exercises/18_iterators/iterators4.rs +++ b/exercises/18_iterators/iterators4.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint iterators4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub fn factorial(num: u64) -> u64 { // Complete this function to return the factorial of num // Do not use: diff --git a/exercises/18_iterators/iterators5.rs b/exercises/18_iterators/iterators5.rs index a062ee4..ceec536 100644 --- a/exercises/18_iterators/iterators5.rs +++ b/exercises/18_iterators/iterators5.rs @@ -11,8 +11,6 @@ // Execute `rustlings hint iterators5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::collections::HashMap; #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/arc1.rs index 3526ddc..0647eea 100644 --- a/exercises/19_smart_pointers/arc1.rs +++ b/exercises/19_smart_pointers/arc1.rs @@ -21,8 +21,6 @@ // // Execute `rustlings hint arc1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - #![forbid(unused_imports)] // Do not change this, (or the next) line. use std::sync::Arc; use std::thread; diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs index 513e7da..2abc024 100644 --- a/exercises/19_smart_pointers/box1.rs +++ b/exercises/19_smart_pointers/box1.rs @@ -18,8 +18,6 @@ // // Execute `rustlings hint box1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - #[derive(PartialEq, Debug)] pub enum List { Cons(i32, List), diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/cow1.rs index fcd3e0b..b24591b 100644 --- a/exercises/19_smart_pointers/cow1.rs +++ b/exercises/19_smart_pointers/cow1.rs @@ -12,8 +12,6 @@ // // Execute `rustlings hint cow1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - use std::borrow::Cow; fn abs_all<'a, 'b>(input: &'a mut Cow<'b, [i32]>) -> &'a mut Cow<'b, [i32]> { diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/rc1.rs index 1b90346..e96e625 100644 --- a/exercises/19_smart_pointers/rc1.rs +++ b/exercises/19_smart_pointers/rc1.rs @@ -10,8 +10,6 @@ // // Execute `rustlings hint rc1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - use std::rc::Rc; #[derive(Debug)] diff --git a/exercises/20_threads/threads1.rs b/exercises/20_threads/threads1.rs index 80b6def..be1301d 100644 --- a/exercises/20_threads/threads1.rs +++ b/exercises/20_threads/threads1.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint threads1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::thread; use std::time::{Duration, Instant}; diff --git a/exercises/20_threads/threads2.rs b/exercises/20_threads/threads2.rs index 60d6824..13cb840 100644 --- a/exercises/20_threads/threads2.rs +++ b/exercises/20_threads/threads2.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint threads2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::sync::Arc; use std::thread; use std::time::Duration; diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs index acb97b4..35b914a 100644 --- a/exercises/20_threads/threads3.rs +++ b/exercises/20_threads/threads3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint threads3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::sync::mpsc; use std::sync::Arc; use std::thread; diff --git a/exercises/21_macros/macros1.rs b/exercises/21_macros/macros1.rs index 678de6e..65986db 100644 --- a/exercises/21_macros/macros1.rs +++ b/exercises/21_macros/macros1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint macros1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - macro_rules! my_macro { () => { println!("Check out my macro!"); diff --git a/exercises/21_macros/macros2.rs b/exercises/21_macros/macros2.rs index 788fc16..b7c37fd 100644 --- a/exercises/21_macros/macros2.rs +++ b/exercises/21_macros/macros2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint macros2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { my_macro!(); } diff --git a/exercises/21_macros/macros3.rs b/exercises/21_macros/macros3.rs index b795c14..92a1922 100644 --- a/exercises/21_macros/macros3.rs +++ b/exercises/21_macros/macros3.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint macros3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - mod macros { macro_rules! my_macro { () => { diff --git a/exercises/21_macros/macros4.rs b/exercises/21_macros/macros4.rs index 71b45a0..83a6e44 100644 --- a/exercises/21_macros/macros4.rs +++ b/exercises/21_macros/macros4.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint macros4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[rustfmt::skip] macro_rules! my_macro { () => { diff --git a/exercises/22_clippy/clippy1.rs b/exercises/22_clippy/clippy1.rs index e0c6ce7c4..1e0f42e 100644 --- a/exercises/22_clippy/clippy1.rs +++ b/exercises/22_clippy/clippy1.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint clippy1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::f32; fn main() { diff --git a/exercises/22_clippy/clippy2.rs b/exercises/22_clippy/clippy2.rs index 9b87a0b..37ac089 100644 --- a/exercises/22_clippy/clippy2.rs +++ b/exercises/22_clippy/clippy2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint clippy2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let mut res = 42; let option = Some(12); diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs index 5a95f5b..6a6a36b 100644 --- a/exercises/22_clippy/clippy3.rs +++ b/exercises/22_clippy/clippy3.rs @@ -3,8 +3,6 @@ // Here's a couple more easy Clippy fixes, so you can see its utility. // No hints. -// I AM NOT DONE - #[allow(unused_variables, unused_assignments)] fn main() { let my_option: Option<()> = None; diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs index 2ba9e3f..cd2c93b 100644 --- a/exercises/23_conversions/as_ref_mut.rs +++ b/exercises/23_conversions/as_ref_mut.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint as_ref_mut` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - // Obtain the number of bytes (not characters) in the given argument. // TODO: Add the AsRef trait appropriately as a trait bound. fn byte_counter(arg: T) -> usize { diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/from_into.rs index 11787c3..d2a1609 100644 --- a/exercises/23_conversions/from_into.rs +++ b/exercises/23_conversions/from_into.rs @@ -41,8 +41,6 @@ impl Default for Person { // If while parsing the age, something goes wrong, then return the default of // Person Otherwise, then return an instantiated Person object with the results -// I AM NOT DONE - impl From<&str> for Person { fn from(s: &str) -> Person {} } diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs index e209347..ed91ca5 100644 --- a/exercises/23_conversions/from_str.rs +++ b/exercises/23_conversions/from_str.rs @@ -31,8 +31,6 @@ enum ParsePersonError { ParseInt(ParseIntError), } -// I AM NOT DONE - // Steps: // 1. If the length of the provided string is 0, an error should be returned // 2. Split the given string on the commas present in it diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/try_from_into.rs index 32d6ef3..2316655 100644 --- a/exercises/23_conversions/try_from_into.rs +++ b/exercises/23_conversions/try_from_into.rs @@ -27,8 +27,6 @@ enum IntoColorError { IntConversion, } -// I AM NOT DONE - // Your task is to complete this implementation and return an Ok result of inner // type Color. You need to create an implementation for a tuple of three // integers, an array of three integers, and a slice of integers. diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/using_as.rs index 414cef3..9f617ec 100644 --- a/exercises/23_conversions/using_as.rs +++ b/exercises/23_conversions/using_as.rs @@ -10,8 +10,6 @@ // Execute `rustlings hint using_as` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn average(values: &[f64]) -> f64 { let total = values.iter().sum::(); total / values.len() diff --git a/exercises/quiz1.rs b/exercises/quiz1.rs index 4ee5ada..b9e71f5 100644 --- a/exercises/quiz1.rs +++ b/exercises/quiz1.rs @@ -13,8 +13,6 @@ // // No hints this time ;) -// I AM NOT DONE - // Put your function here! // fn calculate_price_of_apples { diff --git a/exercises/quiz2.rs b/exercises/quiz2.rs index 29925ca..8ace3fe 100644 --- a/exercises/quiz2.rs +++ b/exercises/quiz2.rs @@ -20,8 +20,6 @@ // // No hints this time! -// I AM NOT DONE - pub enum Command { Uppercase, Trim, diff --git a/exercises/quiz3.rs b/exercises/quiz3.rs index 3b01d31..24f7082 100644 --- a/exercises/quiz3.rs +++ b/exercises/quiz3.rs @@ -16,8 +16,6 @@ // // Execute `rustlings hint quiz3` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - pub struct ReportCard { pub grade: f32, pub student_name: String, diff --git a/info.toml b/info.toml index 36629b3..c085e89 100644 --- a/info.toml +++ b/info.toml @@ -4,6 +4,7 @@ name = "intro1" path = "exercises/00_intro/intro1.rs" mode = "compile" +# TODO: Fix hint hint = """ Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file to move on to the next exercise.""" @@ -129,11 +130,7 @@ path = "exercises/02_functions/functions3.rs" mode = "compile" hint = """ This time, the function *declaration* is okay, but there's something wrong -with the place where we're calling the function. - -As a reminder, you can freely play around with different solutions in Rustlings! -Watch mode will only jump to the next exercise if you remove the `I AM NOT -DONE` comment.""" +with the place where we're calling the function.""" [[exercises]] name = "functions4" diff --git a/src/app_state.rs b/src/app_state.rs new file mode 100644 index 0000000..4a0912e --- /dev/null +++ b/src/app_state.rs @@ -0,0 +1,185 @@ +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; + +use crate::exercise::Exercise; + +const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct StateFile { + current_exercise_ind: usize, + progress: Vec, +} + +impl StateFile { + fn read(exercises: &[Exercise]) -> Option { + let file_content = fs::read(".rustlings-state.json").ok()?; + + let slf: Self = serde_json::de::from_slice(&file_content).ok()?; + + if slf.progress.len() != exercises.len() || slf.current_exercise_ind >= exercises.len() { + return None; + } + + Some(slf) + } + + fn read_or_default(exercises: &[Exercise]) -> Self { + Self::read(exercises).unwrap_or_else(|| Self { + current_exercise_ind: 0, + progress: vec![false; exercises.len()], + }) + } + + fn write(&self) -> Result<()> { + let mut buf = Vec::with_capacity(1024); + serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; + fs::write(".rustlings-state.json", buf) + .context("Failed to write the state file `.rustlings-state.json`")?; + + Ok(()) + } +} + +pub struct AppState { + state_file: StateFile, + exercises: &'static [Exercise], + n_done: u16, + current_exercise: &'static Exercise, +} + +#[must_use] +pub enum ExercisesProgress { + AllDone, + Pending, +} + +impl AppState { + pub fn new(exercises: Vec) -> Self { + // Leaking for sending the exercises to the debounce event handler. + // Leaking is not a problem since the exercises' slice is used until the end of the program. + let exercises = exercises.leak(); + + let state_file = StateFile::read_or_default(exercises); + let n_done = state_file + .progress + .iter() + .fold(0, |acc, done| acc + u16::from(*done)); + let current_exercise = &exercises[state_file.current_exercise_ind]; + + Self { + state_file, + exercises, + n_done, + current_exercise, + } + } + + #[inline] + pub fn current_exercise_ind(&self) -> usize { + self.state_file.current_exercise_ind + } + + #[inline] + pub fn progress(&self) -> &[bool] { + &self.state_file.progress + } + + #[inline] + pub fn exercises(&self) -> &'static [Exercise] { + self.exercises + } + + #[inline] + pub fn n_done(&self) -> u16 { + self.n_done + } + + #[inline] + pub fn current_exercise(&self) -> &'static Exercise { + self.current_exercise + } + + pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { + if ind >= self.exercises.len() { + bail!(BAD_INDEX_ERR); + } + + self.state_file.current_exercise_ind = ind; + self.current_exercise = &self.exercises[ind]; + + self.state_file.write() + } + + pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { + let (ind, exercise) = self + .exercises + .iter() + .enumerate() + .find(|(_, exercise)| exercise.name == name) + .with_context(|| format!("No exercise found for '{name}'!"))?; + + self.state_file.current_exercise_ind = ind; + self.current_exercise = exercise; + + self.state_file.write() + } + + pub fn set_pending(&mut self, ind: usize) -> Result<()> { + let done = self + .state_file + .progress + .get_mut(ind) + .context(BAD_INDEX_ERR)?; + + if *done { + *done = false; + self.n_done -= 1; + self.state_file.write()?; + } + + Ok(()) + } + + fn next_exercise_ind(&self) -> Option { + let current_ind = self.state_file.current_exercise_ind; + + if current_ind == self.state_file.progress.len() - 1 { + // The last exercise is done. + // Search for exercises not done from the start. + return self.state_file.progress[..current_ind] + .iter() + .position(|done| !done); + } + + // The done exercise isn't the last one. + // Search for a pending exercise after the current one and then from the start. + match self.state_file.progress[current_ind + 1..] + .iter() + .position(|done| !done) + { + Some(ind) => Some(current_ind + 1 + ind), + None => self.state_file.progress[..current_ind] + .iter() + .position(|done| !done), + } + } + + pub fn done_current_exercise(&mut self) -> Result { + let done = &mut self.state_file.progress[self.state_file.current_exercise_ind]; + if !*done { + *done = true; + self.n_done += 1; + } + + let Some(ind) = self.next_exercise_ind() else { + return Ok(ExercisesProgress::AllDone); + }; + + self.set_current_exercise_ind(ind)?; + + Ok(ExercisesProgress::Pending) + } +} diff --git a/src/exercise.rs b/src/exercise.rs index ca47009..de435d1 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,38 +1,14 @@ use anyhow::{Context, Result}; use serde::Deserialize; use std::{ - array, fmt::{self, Debug, Display, Formatter}, - fs::{self, File}, - io::{self, BufRead, BufReader}, - mem, + fs::{self}, path::PathBuf, process::{Command, Output}, }; -use winnow::{ - ascii::{space0, Caseless}, - combinator::opt, - Parser, -}; use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; -// The number of context lines above and below a highlighted line. -const CONTEXT: usize = 2; - -// Check if the line contains the "I AM NOT DONE" comment. -fn contains_not_done_comment(input: &str) -> bool { - ( - space0::<_, ()>, - "//", - opt('/'), - space0, - Caseless("I AM NOT DONE"), - ) - .parse_next(&mut &*input) - .is_ok() -} - // The mode of the exercise. #[derive(Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] @@ -78,13 +54,6 @@ pub struct Exercise { pub hint: String, } -// The state of an Exercise. -#[derive(PartialEq, Eq, Debug)] -pub enum State { - Done, - Pending(Vec), -} - // The context information of a pending exercise. #[derive(PartialEq, Eq, Debug)] pub struct ContextLine { @@ -129,105 +98,6 @@ impl Exercise { } } - pub fn state(&self) -> Result { - let source_file = File::open(&self.path) - .with_context(|| format!("Failed to open the exercise file {}", self.path.display()))?; - let mut source_reader = BufReader::new(source_file); - - // Read the next line into `buf` without the newline at the end. - let mut read_line = |buf: &mut String| -> io::Result<_> { - let n = source_reader.read_line(buf)?; - if buf.ends_with('\n') { - buf.pop(); - if buf.ends_with('\r') { - buf.pop(); - } - } - Ok(n) - }; - - let mut current_line_number: usize = 1; - // Keep the last `CONTEXT` lines while iterating over the file lines. - let mut prev_lines: [_; CONTEXT] = array::from_fn(|_| String::with_capacity(256)); - let mut line = String::with_capacity(256); - - loop { - let n = read_line(&mut line).with_context(|| { - format!("Failed to read the exercise file {}", self.path.display()) - })?; - - // Reached the end of the file and didn't find the comment. - if n == 0 { - return Ok(State::Done); - } - - if contains_not_done_comment(&line) { - let mut context = Vec::with_capacity(2 * CONTEXT + 1); - // Previous lines. - for (ind, prev_line) in prev_lines - .into_iter() - .take(current_line_number - 1) - .enumerate() - .rev() - { - context.push(ContextLine { - line: prev_line, - number: current_line_number - 1 - ind, - important: false, - }); - } - - // Current line. - context.push(ContextLine { - line, - number: current_line_number, - important: true, - }); - - // Next lines. - for ind in 0..CONTEXT { - let mut next_line = String::with_capacity(256); - let Ok(n) = read_line(&mut next_line) else { - // If an error occurs, just ignore the next lines. - break; - }; - - // Reached the end of the file. - if n == 0 { - break; - } - - context.push(ContextLine { - line: next_line, - number: current_line_number + 1 + ind, - important: false, - }); - } - - return Ok(State::Pending(context)); - } - - current_line_number += 1; - // Add the current line as a previous line and shift the older lines by one. - for prev_line in &mut prev_lines { - mem::swap(&mut line, prev_line); - } - // The current line now contains the oldest previous line. - // Recycle it for reading the next line. - line.clear(); - } - } - - // Check that the exercise looks to be solved using self.state() - // This is not the best way to check since - // the user can just remove the "I AM NOT DONE" string from the file - // without actually having solved anything. - // The only other way to truly check this would to compile and run - // the exercise; which would be both costly and counterintuitive - pub fn looks_done(&self) -> Result { - self.state().map(|state| state == State::Done) - } - pub fn reset(&self) -> Result<()> { EMBEDDED_FILES .write_exercise_to_disk(&self.path, WriteStrategy::Overwrite) @@ -240,77 +110,3 @@ impl Display for Exercise { self.path.fmt(f) } } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_pending_state() { - let exercise = Exercise { - name: "pending_exercise".into(), - path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), - mode: Mode::Compile, - hint: String::new(), - }; - - let state = exercise.state(); - let expected = vec![ - ContextLine { - line: "// fake_exercise".to_string(), - number: 1, - important: false, - }, - ContextLine { - line: "".to_string(), - number: 2, - important: false, - }, - ContextLine { - line: "// I AM NOT DONE".to_string(), - number: 3, - important: true, - }, - ContextLine { - line: "".to_string(), - number: 4, - important: false, - }, - ContextLine { - line: "fn main() {".to_string(), - number: 5, - important: false, - }, - ]; - - assert_eq!(state.unwrap(), State::Pending(expected)); - } - - #[test] - fn test_finished_exercise() { - let exercise = Exercise { - name: "finished_exercise".into(), - path: PathBuf::from("tests/fixture/state/exercises/finished_exercise.rs"), - mode: Mode::Compile, - hint: String::new(), - }; - - assert_eq!(exercise.state().unwrap(), State::Done); - } - - #[test] - fn test_not_done() { - assert!(contains_not_done_comment("// I AM NOT DONE")); - assert!(contains_not_done_comment("/// I AM NOT DONE")); - assert!(contains_not_done_comment("// I AM NOT DONE")); - assert!(contains_not_done_comment("/// I AM NOT DONE")); - assert!(contains_not_done_comment("// I AM NOT DONE ")); - assert!(contains_not_done_comment("// I AM NOT DONE!")); - assert!(contains_not_done_comment("// I am not done")); - assert!(contains_not_done_comment("// i am NOT done")); - - assert!(!contains_not_done_comment("I AM NOT DONE")); - assert!(!contains_not_done_comment("// NOT DONE")); - assert!(!contains_not_done_comment("DONE")); - } -} diff --git a/src/list.rs b/src/list.rs index 560b85a..80b78e8 100644 --- a/src/list.rs +++ b/src/list.rs @@ -9,11 +9,11 @@ use std::{fmt::Write, io}; mod state; -use crate::{exercise::Exercise, state_file::StateFile}; +use crate::app_state::AppState; use self::state::{Filter, UiState}; -pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<()> { +pub fn list(app_state: &mut AppState) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; enable_raw_mode()?; @@ -21,7 +21,7 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; terminal.clear()?; - let mut ui_state = UiState::new(state_file, exercises); + let mut ui_state = UiState::new(app_state); 'outer: loop { terminal.draw(|frame| ui_state.draw(frame).unwrap())?; @@ -56,7 +56,7 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul "Enabled filter DONE β”‚ Press d again to disable the filter" }; - ui_state = ui_state.with_updated_rows(state_file); + ui_state = ui_state.with_updated_rows(); ui_state.message.push_str(message); } KeyCode::Char('p') => { @@ -68,23 +68,20 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul "Enabled filter PENDING β”‚ Press p again to disable the filter" }; - ui_state = ui_state.with_updated_rows(state_file); + ui_state = ui_state.with_updated_rows(); ui_state.message.push_str(message); } KeyCode::Char('r') => { - let selected = ui_state.selected(); - let exercise = &exercises[selected]; - exercise.reset()?; - state_file.reset(selected)?; + let exercise = ui_state.reset_selected()?; - ui_state = ui_state.with_updated_rows(state_file); + ui_state = ui_state.with_updated_rows(); ui_state .message .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; } KeyCode::Char('c') => { - state_file.set_next_exercise_ind(ui_state.selected())?; - ui_state = ui_state.with_updated_rows(state_file); + ui_state.selected_to_current_exercise()?; + ui_state = ui_state.with_updated_rows(); } _ => (), } diff --git a/src/list/state.rs b/src/list/state.rs index 209374b..7714268 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -7,7 +7,7 @@ use ratatui::{ Frame, }; -use crate::{exercise::Exercise, progress_bar::progress_bar_ratatui, state_file::StateFile}; +use crate::{app_state::AppState, exercise::Exercise, progress_bar::progress_bar_ratatui}; #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { @@ -16,30 +16,29 @@ pub enum Filter { None, } -pub struct UiState { +pub struct UiState<'a> { pub table: Table<'static>, pub message: String, pub filter: Filter, - exercises: &'static [Exercise], - progress: u16, - selected: usize, + app_state: &'a mut AppState, table_state: TableState, + selected: usize, last_ind: usize, } -impl UiState { - pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { +impl<'a> UiState<'a> { + pub fn with_updated_rows(mut self) -> Self { + let current_exercise_ind = self.app_state.current_exercise_ind(); + let mut rows_counter: usize = 0; - let mut progress: u16 = 0; let rows = self - .exercises + .app_state + .exercises() .iter() - .zip(state_file.progress().iter().copied()) + .zip(self.app_state.progress().iter().copied()) .enumerate() .filter_map(|(ind, (exercise, done))| { let exercise_state = if done { - progress += 1; - if self.filter == Filter::Pending { return None; } @@ -55,7 +54,7 @@ impl UiState { rows_counter += 1; - let next = if ind == state_file.next_exercise_ind() { + let next = if ind == current_exercise_ind { ">>>>".bold().red() } else { Span::default() @@ -74,15 +73,14 @@ impl UiState { self.last_ind = rows_counter.saturating_sub(1); self.select(self.selected.min(self.last_ind)); - self.progress = progress; - self } - pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { + pub fn new(app_state: &'a mut AppState) -> Self { let header = Row::new(["Next", "State", "Name", "Path"]); - let max_name_len = exercises + let max_name_len = app_state + .exercises() .iter() .map(|exercise| exercise.name.len()) .max() @@ -104,7 +102,7 @@ impl UiState { .highlight_symbol("πŸ¦€") .block(Block::default().borders(Borders::BOTTOM)); - let selected = state_file.next_exercise_ind(); + let selected = app_state.current_exercise_ind(); let table_state = TableState::default() .with_offset(selected.saturating_sub(10)) .with_selected(Some(selected)); @@ -113,19 +111,13 @@ impl UiState { table, message: String::with_capacity(128), filter: Filter::None, - exercises, - progress: 0, - selected, + app_state, table_state, + selected, last_ind: 0, }; - slf.with_updated_rows(state_file) - } - - #[inline] - pub fn selected(&self) -> usize { - self.selected + slf.with_updated_rows() } fn select(&mut self, ind: usize) { @@ -134,11 +126,13 @@ impl UiState { } pub fn select_next(&mut self) { - self.select(self.selected.saturating_add(1).min(self.last_ind)); + let next = (self.selected + 1).min(self.last_ind); + self.select(next); } pub fn select_previous(&mut self) { - self.select(self.selected.saturating_sub(1)); + let previous = self.selected.saturating_sub(1); + self.select(previous); } #[inline] @@ -167,8 +161,8 @@ impl UiState { frame.render_widget( Paragraph::new(progress_bar_ratatui( - self.progress, - self.exercises.len() as u16, + self.app_state.n_done(), + self.app_state.exercises().len() as u16, area.width, )?) .block(Block::default().borders(Borders::BOTTOM)), @@ -200,4 +194,19 @@ impl UiState { Ok(()) } + + pub fn reset_selected(&mut self) -> Result<&'static Exercise> { + self.app_state.set_pending(self.selected)?; + // TODO: Take care of filters! + let exercise = &self.app_state.exercises()[self.selected]; + exercise.reset()?; + + Ok(exercise) + } + + #[inline] + pub fn selected_to_current_exercise(&mut self) -> Result<()> { + // TODO: Take care of filters! + self.app_state.set_current_exercise_ind(self.selected) + } } diff --git a/src/main.rs b/src/main.rs index fc83e0f..926605c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use std::{path::Path, process::exit}; +mod app_state; mod consts; mod embedded; mod exercise; @@ -9,17 +10,15 @@ mod init; mod list; mod progress_bar; mod run; -mod state_file; -mod verify; mod watch; use self::{ + app_state::AppState, consts::WELCOME, - exercise::{Exercise, InfoFile}, + exercise::InfoFile, + init::init, list::list, run::run, - state_file::StateFile, - verify::{verify, VerifyState}, watch::{watch, WatchExit}, }; @@ -35,14 +34,12 @@ struct Args { enum Subcommands { /// Initialize Rustlings Init, - /// Verify all exercises according to the recommended order - Verify, /// Same as just running `rustlings` without a subcommand. Watch, - /// Run/Test a single exercise + /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified. Run { /// The name of the exercise - name: String, + name: Option, }, /// Reset a single exercise Reset { @@ -56,26 +53,6 @@ enum Subcommands { }, } -fn find_exercise(name: &str, exercises: &'static [Exercise]) -> Result<(usize, &'static Exercise)> { - if name == "next" { - for (ind, exercise) in exercises.iter().enumerate() { - if !exercise.looks_done()? { - return Ok((ind, exercise)); - } - } - - println!("πŸŽ‰ Congratulations! You have done all the exercises!"); - println!("πŸ”š There are no more exercises to do next!"); - exit(0); - } - - exercises - .iter() - .enumerate() - .find(|(_, exercise)| exercise.name == name) - .with_context(|| format!("No exercise found for '{name}'!")) -} - fn main() -> Result<()> { let args = Args::parse(); @@ -87,11 +64,10 @@ Try running `cargo --version` to diagnose the problem.", let mut info_file = InfoFile::parse()?; info_file.exercises.shrink_to_fit(); - // Leaking is not a problem since the exercises' slice is used until the end of the program. - let exercises = info_file.exercises.leak(); + let exercises = info_file.exercises; if matches!(args.command, Some(Subcommands::Init)) { - init::init(exercises).context("Initialization failed")?; + init(&exercises).context("Initialization failed")?; println!( "\nDone initialization!\n Run `cd rustlings` to go into the generated directory. @@ -109,38 +85,37 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let mut state_file = StateFile::read_or_default(exercises); + let mut app_state = AppState::new(exercises); match args.command { None | Some(Subcommands::Watch) => loop { - match watch(&mut state_file, exercises)? { + match watch(&mut app_state)? { WatchExit::Shutdown => break, // It is much easier to exit the watch mode, launch the list mode and then restart // the watch mode instead of trying to pause the watch threads and correct the // watch state. - WatchExit::List => list(&mut state_file, exercises)?, + WatchExit::List => list(&mut app_state)?, } }, // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::Run { name }) => { - let (_, exercise) = find_exercise(&name, exercises)?; - run(exercise).unwrap_or_else(|_| exit(1)); + if let Some(name) = name { + app_state.set_current_exercise_by_name(&name)?; + } + run(&mut app_state)?; } Some(Subcommands::Reset { name }) => { - let (ind, exercise) = find_exercise(&name, exercises)?; + app_state.set_current_exercise_by_name(&name)?; + app_state.set_pending(app_state.current_exercise_ind())?; + let exercise = app_state.current_exercise(); exercise.reset()?; - state_file.reset(ind)?; println!("The exercise {exercise} has been reset!"); } Some(Subcommands::Hint { name }) => { - let (_, exercise) = find_exercise(&name, exercises)?; - println!("{}", exercise.hint); + app_state.set_current_exercise_by_name(&name)?; + println!("{}", app_state.current_exercise().hint); } - Some(Subcommands::Verify) => match verify(exercises, 0)? { - VerifyState::AllExercisesDone => println!("All exercises done!"), - VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), - }, } Ok(()) diff --git a/src/run.rs b/src/run.rs index 2fd6f40..18da193 100644 --- a/src/run.rs +++ b/src/run.rs @@ -2,13 +2,10 @@ use anyhow::{bail, Result}; use crossterm::style::Stylize; use std::io::{stdout, Write}; -use crate::exercise::Exercise; +use crate::app_state::{AppState, ExercisesProgress}; -// Invoke the rust compiler on the path of the given exercise, -// and run the ensuing binary. -// The verbose argument helps determine whether or not to show -// the output from the test harnesses (if the mode of the exercise is test) -pub fn run(exercise: &Exercise) -> Result<()> { +pub fn run(app_state: &mut AppState) -> Result<()> { + let exercise = app_state.current_exercise(); let output = exercise.run()?; { @@ -22,7 +19,19 @@ pub fn run(exercise: &Exercise) -> Result<()> { bail!("Ran {exercise} with errors"); } - println!("{}", "βœ“ Successfully ran {exercise}".green()); + println!( + "{}{}", + "βœ“ Successfully ran ".green(), + exercise.path.to_string_lossy().green(), + ); + + match app_state.done_current_exercise()? { + ExercisesProgress::AllDone => println!( + "πŸŽ‰ Congratulations! You have done all the exercises! +πŸ”š There are no more exercises to do next!" + ), + ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()), + } Ok(()) } diff --git a/src/state_file.rs b/src/state_file.rs deleted file mode 100644 index 6b80354..0000000 --- a/src/state_file.rs +++ /dev/null @@ -1,68 +0,0 @@ -use anyhow::{bail, Context, Result}; -use serde::{Deserialize, Serialize}; -use std::fs; - -use crate::exercise::Exercise; - -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct StateFile { - next_exercise_ind: usize, - progress: Vec, -} - -const BAD_INDEX_ERR: &str = "The next exercise index is higher than the number of exercises"; - -impl StateFile { - fn read(exercises: &[Exercise]) -> Option { - let file_content = fs::read(".rustlings-state.json").ok()?; - - let slf: Self = serde_json::de::from_slice(&file_content).ok()?; - - if slf.progress.len() != exercises.len() || slf.next_exercise_ind >= exercises.len() { - return None; - } - - Some(slf) - } - - pub fn read_or_default(exercises: &[Exercise]) -> Self { - Self::read(exercises).unwrap_or_else(|| Self { - next_exercise_ind: 0, - progress: vec![false; exercises.len()], - }) - } - - fn write(&self) -> Result<()> { - let mut buf = Vec::with_capacity(1024); - serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; - fs::write(".rustlings-state.json", buf) - .context("Failed to write the state file `.rustlings-state.json`")?; - - Ok(()) - } - - #[inline] - pub fn next_exercise_ind(&self) -> usize { - self.next_exercise_ind - } - - pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> { - if ind >= self.progress.len() { - bail!(BAD_INDEX_ERR); - } - self.next_exercise_ind = ind; - self.write() - } - - #[inline] - pub fn progress(&self) -> &[bool] { - &self.progress - } - - pub fn reset(&mut self, ind: usize) -> Result<()> { - let done = self.progress.get_mut(ind).context(BAD_INDEX_ERR)?; - *done = false; - self.write() - } -} diff --git a/src/verify.rs b/src/verify.rs deleted file mode 100644 index cea6bdf..0000000 --- a/src/verify.rs +++ /dev/null @@ -1,85 +0,0 @@ -use anyhow::Result; -use crossterm::style::{Attribute, ContentStyle, Stylize}; -use std::io::{stdout, Write}; - -use crate::exercise::{Exercise, Mode, State}; - -pub enum VerifyState { - AllExercisesDone, - Failed(&'static Exercise), -} - -// Verify that the provided container of Exercise objects -// can be compiled and run without any failures. -// Any such failures will be reported to the end user. -// If the Exercise being verified is a test, the verbose boolean -// determines whether or not the test harness outputs are displayed. -pub fn verify( - exercises: &'static [Exercise], - mut current_exercise_ind: usize, -) -> Result { - while current_exercise_ind < exercises.len() { - let exercise = &exercises[current_exercise_ind]; - - println!( - "Progress: {current_exercise_ind}/{} ({:.1}%)\n", - exercises.len(), - current_exercise_ind as f32 / exercises.len() as f32 * 100.0, - ); - - let output = exercise.run()?; - - { - let mut stdout = stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; - } - - if !output.status.success() { - return Ok(VerifyState::Failed(exercise)); - } - - println!(); - // TODO: Color - match exercise.mode { - Mode::Compile => println!("Successfully ran {exercise}!"), - Mode::Test => println!("Successfully tested {exercise}!"), - Mode::Clippy => println!("Successfully checked {exercise}!"), - } - - if let State::Pending(context) = exercise.state()? { - println!( - "\nYou can keep working on this exercise, -or jump into the next one by removing the {} comment:\n", - "`I AM NOT DONE`".bold() - ); - - for context_line in context { - let formatted_line = if context_line.important { - format!("{}", context_line.line.bold()) - } else { - context_line.line - }; - - println!( - "{:>2} {} {}", - ContentStyle { - foreground_color: Some(crossterm::style::Color::Blue), - background_color: None, - underline_color: None, - attributes: Attribute::Bold.into() - } - .apply(context_line.number), - "|".blue(), - formatted_line, - ); - } - return Ok(VerifyState::Failed(exercise)); - } - - current_exercise_ind += 1; - } - - Ok(VerifyState::AllExercisesDone) -} diff --git a/src/watch.rs b/src/watch.rs index b29169b..929275f 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -15,7 +15,7 @@ mod debounce_event; mod state; mod terminal_event; -use crate::{exercise::Exercise, state_file::StateFile}; +use crate::app_state::AppState; use self::{ debounce_event::DebounceEventHandler, @@ -39,23 +39,23 @@ pub enum WatchExit { List, } -pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result { +pub fn watch(app_state: &mut AppState) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( Duration::from_secs(1), DebounceEventHandler { tx: tx.clone(), - exercises, + exercises: app_state.exercises(), }, )?; debouncer .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let mut watch_state = WatchState::new(state_file, exercises); + let mut watch_state = WatchState::new(app_state); // TODO: bool - watch_state.run_exercise()?; + watch_state.run_current_exercise()?; watch_state.render()?; thread::spawn(move || terminal_event_handler(tx)); diff --git a/src/watch/state.rs b/src/watch/state.rs index 6f6d2f1..a7647d8 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,26 +1,16 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use crossterm::{ - style::{Attribute, ContentStyle, Stylize}, + style::Stylize, terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use std::{ - fmt::Write as _, - io::{self, StdoutLock, Write}, -}; +use std::io::{self, StdoutLock, Write}; -use crate::{ - exercise::{Exercise, State}, - progress_bar::progress_bar, - state_file::StateFile, -}; +use crate::{app_state::AppState, progress_bar::progress_bar}; pub struct WatchState<'a> { writer: StdoutLock<'a>, - exercises: &'static [Exercise], - exercise: &'static Exercise, - current_exercise_ind: usize, - progress: u16, + app_state: &'a mut AppState, stdout: Option>, stderr: Option>, message: Option, @@ -28,19 +18,12 @@ pub struct WatchState<'a> { } impl<'a> WatchState<'a> { - pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { - let current_exercise_ind = state_file.next_exercise_ind(); - let progress = state_file.progress().iter().filter(|done| **done).count() as u16; - let exercise = &exercises[current_exercise_ind]; - + pub fn new(app_state: &'a mut AppState) -> Self { let writer = io::stdout().lock(); Self { writer, - exercises, - exercise, - current_exercise_ind, - progress, + app_state, stdout: None, stderr: None, message: None, @@ -53,8 +36,8 @@ impl<'a> WatchState<'a> { self.writer } - pub fn run_exercise(&mut self) -> Result { - let output = self.exercise.run()?; + pub fn run_current_exercise(&mut self) -> Result { + let output = self.app_state.current_exercise().run()?; self.stdout = Some(output.stdout); if !output.status.success() { @@ -64,55 +47,15 @@ impl<'a> WatchState<'a> { self.stderr = None; - if let State::Pending(context) = self.exercise.state()? { - let mut message = format!( - " -You can keep working on this exercise or jump into the next one by removing the {} comment: - -", - "`I AM NOT DONE`".bold(), - ); - - for context_line in context { - let formatted_line = if context_line.important { - context_line.line.bold() - } else { - context_line.line.stylize() - }; - - writeln!( - message, - "{:>2} {} {}", - ContentStyle { - foreground_color: Some(crossterm::style::Color::Blue), - background_color: None, - underline_color: None, - attributes: Attribute::Bold.into() - } - .apply(context_line.number), - "|".blue(), - formatted_line, - )?; - } - - self.message = Some(message); - return Ok(false); - } - Ok(true) } pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result { - self.exercise = self - .exercises - .get(exercise_ind) - .context("Invalid exercise index")?; - self.current_exercise_ind = exercise_ind; - - self.run_exercise() + self.app_state.set_current_exercise_ind(exercise_ind)?; + self.run_current_exercise() } - pub fn show_prompt(&mut self) -> io::Result<()> { + fn show_prompt(&mut self) -> io::Result<()> { self.writer.write_all(b"\n\n")?; if !self.hint_displayed { @@ -150,18 +93,27 @@ You can keep working on this exercise or jump into the next one by removing the if self.hint_displayed { self.writer .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?; - self.writer.write_all(self.exercise.hint.as_bytes())?; + self.writer + .write_all(self.app_state.current_exercise().hint.as_bytes())?; self.writer.write_all(b"\n\n")?; } let line_width = size()?.0; - let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?; + let progress_bar = progress_bar( + self.app_state.n_done(), + self.app_state.exercises().len() as u16, + line_width, + )?; self.writer.write_all(progress_bar.as_bytes())?; self.writer.write_all(b"Current exercise: ")?; self.writer.write_fmt(format_args!( "{}", - self.exercise.path.to_string_lossy().bold() + self.app_state + .current_exercise() + .path + .to_string_lossy() + .bold(), ))?; self.show_prompt()?; diff --git a/tests/fixture/state/exercises/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs index f579d0b..016b827 100644 --- a/tests/fixture/state/exercises/pending_exercise.rs +++ b/tests/fixture/state/exercises/pending_exercise.rs @@ -1,7 +1,5 @@ // fake_exercise -// I AM NOT DONE - fn main() { } diff --git a/tests/fixture/state/exercises/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs index 8756f02..2002ef1 100644 --- a/tests/fixture/state/exercises/pending_test_exercise.rs +++ b/tests/fixture/state/exercises/pending_test_exercise.rs @@ -1,4 +1,2 @@ -// I AM NOT DONE - #[test] fn it_works() {} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index f8f4383..51cdefb 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,7 +1,6 @@ use assert_cmd::prelude::*; -use glob::glob; use predicates::boolean::PredicateBooleanExt; -use std::{fs::File, io::Read, process::Command}; +use std::process::Command; #[test] fn fails_when_in_wrong_dir() { @@ -137,31 +136,6 @@ fn get_hint_for_single_test() { .stdout("Hello!\n"); } -#[test] -fn all_exercises_require_confirmation() { - for exercise in glob("exercises/**/*.rs").unwrap() { - let path = exercise.unwrap(); - if path.file_name().unwrap() == "mod.rs" { - continue; - } - let source = { - let mut file = File::open(&path).unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s).unwrap(); - s - }; - source - .matches("// I AM NOT DONE") - .next() - .unwrap_or_else(|| { - panic!( - "There should be an `I AM NOT DONE` annotation in {:?}", - path - ) - }); - } -} - #[test] fn run_compile_exercise_does_not_prompt() { Command::cargo_bin("rustlings") -- cgit v1.2.3 From 2a95a3e96644a0f769019204a518816c9f2e2aee Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 01:24:01 +0200 Subject: Deal with long strings --- info.toml | 32 ++++++++++++++++++++++++++++ src/consts.rs | 59 --------------------------------------------------- src/exercise.rs | 12 +++++++---- src/init.rs | 40 +++++++++++++++++++---------------- src/main.rs | 65 ++++++++++++++++++++++++++++++++++++++++----------------- src/watch.rs | 10 +++++---- 6 files changed, 114 insertions(+), 104 deletions(-) delete mode 100644 src/consts.rs (limited to 'src/watch.rs') diff --git a/info.toml b/info.toml index c085e89..d35b570 100644 --- a/info.toml +++ b/info.toml @@ -1,3 +1,35 @@ +welcome_message = """Is this your first time? Don't worry, Rustlings was made for beginners! We are +going to teach you a lot of things about Rust, but before we can get +started, here's a couple of notes about how Rustlings operates: + +1. The central concept behind Rustlings is that you solve exercises. These + exercises usually have some sort of syntax error in them, which will cause + them to fail compilation or testing. Sometimes there's a logic error instead + of a syntax error. No matter what error, it's your job to find it and fix it! + You'll know when you fixed it because then, the exercise will compile and + Rustlings will be able to move on to the next exercise. +2. If you run Rustlings in watch mode (which we recommend), it'll automatically + start with the first exercise. Don't get confused by an error message popping + up as soon as you run Rustlings! This is part of the exercise that you're + supposed to solve, so open the exercise file in an editor and start your + detective work! +3. If you're stuck on an exercise, there is a helpful hint you can view by typing + 'hint' (in watch mode), or running `rustlings hint exercise_name`. +4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! + (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, + and sometimes, other learners do too so you can help each other out! + +Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the `rustlings` directory!""" + +final_message = """We hope you enjoyed learning about the various aspects of Rust! +If you noticed any issues, please don't hesitate to report them to our repo. +You can also contribute your own exercises to help the greater community! + +Before reporting an issue or contributing, please read our guidelines: +https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md +""" + # INTRO [[exercises]] diff --git a/src/consts.rs b/src/consts.rs deleted file mode 100644 index 40bf150..0000000 --- a/src/consts.rs +++ /dev/null @@ -1,59 +0,0 @@ -pub const WELCOME: &str = r" welcome to... - _ _ _ - _ __ _ _ ___| |_| (_)_ __ __ _ ___ - | '__| | | / __| __| | | '_ \ / _` / __| - | | | |_| \__ \ |_| | | | | | (_| \__ \ - |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; - -pub const DEFAULT_OUT: &str = - "Is this your first time? Don't worry, Rustlings was made for beginners! We are -going to teach you a lot of things about Rust, but before we can get -started, here's a couple of notes about how Rustlings operates: - -1. The central concept behind Rustlings is that you solve exercises. These - exercises usually have some sort of syntax error in them, which will cause - them to fail compilation or testing. Sometimes there's a logic error instead - of a syntax error. No matter what error, it's your job to find it and fix it! - You'll know when you fixed it because then, the exercise will compile and - Rustlings will be able to move on to the next exercise. -2. If you run Rustlings in watch mode (which we recommend), it'll automatically - start with the first exercise. Don't get confused by an error message popping - up as soon as you run Rustlings! This is part of the exercise that you're - supposed to solve, so open the exercise file in an editor and start your - detective work! -3. If you're stuck on an exercise, there is a helpful hint you can view by typing - 'hint' (in watch mode), or running `rustlings hint exercise_name`. -4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! - (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, - and sometimes, other learners do too so you can help each other out! - -Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. -Make sure to have your editor open in the `rustlings` directory!"; - -pub const FENISH_LINE: &str = "+----------------------------------------------------+ -| You made it to the Fe-nish line! | -+-------------------------- ------------------------+ - \\/\x1b[31m - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ - β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ - β–‘β–‘β–’β–’β–’β–’β–‘β–‘β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–‘β–‘β–’β–’β–’β–’ - β–“β–“β–“β–“β–“β–“β–“β–“ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–“β–“β–“β–“β–“β–“ - β–’β–’β–’β–’ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’β–‘β–‘ β–’β–’β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–’β–’β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’ β–’β–’\x1b[0m - -We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, please don't hesitate to report them to our repo. -You can also contribute your own exercises to help the greater community! - -Before reporting an issue or contributing, please read our guidelines: -https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; diff --git a/src/exercise.rs b/src/exercise.rs index f01c6fc..d28f4db 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -24,6 +24,10 @@ pub enum Mode { #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct InfoFile { + // TODO + pub welcome_message: Option, + // TODO + pub final_message: Option, pub exercises: Vec, } @@ -39,10 +43,7 @@ impl InfoFile { .context("Failed to parse `info.toml`")?; if slf.exercises.is_empty() { - panic!( - "There are no exercises yet! -If you are developing third-party exercises, add at least one exercise before testing." - ); + panic!("{NO_EXERCISES_ERR}"); } Ok(slf) @@ -119,3 +120,6 @@ impl Display for Exercise { self.path.fmt(f) } } + +const NO_EXERCISES_ERR: &str = "There are no exercises yet! +If you are developing third-party exercises, add at least one exercise before testing."; diff --git a/src/init.rs b/src/init.rs index bc561ea..4474743 100644 --- a/src/init.rs +++ b/src/init.rs @@ -36,47 +36,33 @@ publish = false } fn create_gitignore() -> io::Result<()> { - let gitignore = b"/target -/.rustlings-state.json"; OpenOptions::new() .create_new(true) .write(true) .open(".gitignore")? - .write_all(gitignore) + .write_all(GITIGNORE) } fn create_vscode_dir() -> Result<()> { create_dir(".vscode").context("Failed to create the directory `.vscode`")?; - let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; OpenOptions::new() .create_new(true) .write(true) .open(".vscode/extensions.json")? - .write_all(vs_code_extensions_json)?; + .write_all(VS_CODE_EXTENSIONS_JSON)?; Ok(()) } pub fn init(exercises: &[Exercise]) -> Result<()> { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { - bail!( - "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist -in the current directory. It looks like Rustlings was already initialized here. -Run `rustlings` for instructions on getting started with the exercises. - -If you didn't already initialize Rustlings, please initialize it in another directory." - ); + bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); } let rustlings_path = Path::new("rustlings"); if let Err(e) = create_dir(rustlings_path) { if e.kind() == ErrorKind::AlreadyExists { - bail!( - "A directory with the name `rustlings` already exists in the current directory. -You probably already initialized Rustlings. -Run `cd rustlings` -Then run `rustlings` again" - ); + bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); } return Err(e.into()); } @@ -96,3 +82,21 @@ Then run `rustlings` again" Ok(()) } + +const GITIGNORE: &[u8] = b"/target +/.rustlings-state.json"; + +const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; + +const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str = + "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist +in the current directory. It looks like Rustlings was already initialized here. +Run `rustlings` for instructions on getting started with the exercises. + +If you didn't already initialize Rustlings, please initialize it in another directory."; + +const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str = + "A directory with the name `rustlings` already exists in the current directory. +You probably already initialized Rustlings. +Run `cd rustlings` +Then run `rustlings` again"; diff --git a/src/main.rs b/src/main.rs index 7bc10ac..fdbb710 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use clap::{Parser, Subcommand}; use std::{path::Path, process::exit}; mod app_state; -mod consts; mod embedded; mod exercise; mod init; @@ -14,7 +13,6 @@ mod watch; use self::{ app_state::AppState, - consts::WELCOME, exercise::InfoFile, init::init, list::list, @@ -54,11 +52,7 @@ enum Subcommands { fn main() -> Result<()> { let args = Args::parse(); - which::which("cargo").context( - "Failed to find `cargo`. -Did you already install Rust? -Try running `cargo --version` to diagnose the problem.", - )?; + which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; let mut info_file = InfoFile::parse()?; info_file.exercises.shrink_to_fit(); @@ -66,20 +60,11 @@ Try running `cargo --version` to diagnose the problem.", if matches!(args.command, Some(Subcommands::Init)) { init(&exercises).context("Initialization failed")?; - println!( - "\nDone initialization!\n -Run `cd rustlings` to go into the generated directory. -Then run `rustlings` for further instructions on getting started." - ); + + println!("{POST_INIT_MSG}"); return Ok(()); } else if !Path::new("exercises").is_dir() { - println!( - " -{WELCOME} - -The `exercises` directory wasn't found in the current directory. -If you are just starting with Rustlings, run the command `rustlings init` to initialize it." - ); + println!("{PRE_INIT_MSG}"); exit(1); } @@ -118,3 +103,45 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Ok(()) } + +const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. +Did you already install Rust? +Try running `cargo --version` to diagnose the problem."; + +const PRE_INIT_MSG: &str = r" + welcome to... + _ _ _ + _ __ _ _ ___| |_| (_)_ __ __ _ ___ + | '__| | | / __| __| | | '_ \ / _` / __| + | | | |_| \__ \ |_| | | | | | (_| \__ \ + |_| \__,_|___/\__|_|_|_| |_|\__, |___/ + |___/ + +The `exercises` directory wasn't found in the current directory. +If you are just starting with Rustlings, run the command `rustlings init` to initialize it."; + +const POST_INIT_MSG: &str = " +Done initialization! + +Run `cd rustlings` to go into the generated directory. +Then run `rustlings` for further instructions on getting started."; + +const FENISH_LINE: &str = "+----------------------------------------------------+ +| You made it to the Fe-nish line! | ++-------------------------- ------------------------+ + \\/\x1b[31m + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ + β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ + β–‘β–‘β–’β–’β–’β–’β–‘β–‘β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–‘β–‘β–’β–’β–’β–’ + β–“β–“β–“β–“β–“β–“β–“β–“ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–“β–“β–“β–“β–“β–“ + β–’β–’β–’β–’ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’β–‘β–‘ β–’β–’β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’ β–’β–’\x1b[0m"; diff --git a/src/watch.rs b/src/watch.rs index 929275f..bfa0f88 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -89,10 +89,12 @@ pub fn watch(app_state: &mut AppState) -> Result { } } - watch_state.into_writer().write_all(b" -We hope you're enjoying learning Rust! -If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. -")?; + watch_state.into_writer().write_all(QUIT_MSG)?; Ok(WatchExit::Shutdown) } + +const QUIT_MSG: &[u8] = b" +We hope you're enjoying learning Rust! +If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. +"; -- cgit v1.2.3 From 6807e63c5f26ee01b60460355ce2c5411c603f16 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 02:45:54 +0200 Subject: Show done message --- src/watch.rs | 4 ---- src/watch/state.rs | 52 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 25 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index bfa0f88..928fc5f 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -54,9 +54,7 @@ pub fn watch(app_state: &mut AppState) -> Result { let mut watch_state = WatchState::new(app_state); - // TODO: bool watch_state.run_current_exercise()?; - watch_state.render()?; thread::spawn(move || terminal_event_handler(tx)); @@ -76,9 +74,7 @@ pub fn watch(app_state: &mut AppState) -> Result { watch_state.handle_invalid_cmd(&cmd)?; } WatchEvent::FileChange { exercise_ind } => { - // TODO: bool watch_state.run_exercise_with_ind(exercise_ind)?; - watch_state.render()?; } WatchEvent::NotifyErr(e) => { return Err(Error::from(e).context("Exercise file watcher failed")) diff --git a/src/watch/state.rs b/src/watch/state.rs index a7647d8..5a5c0ca 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -13,8 +13,8 @@ pub struct WatchState<'a> { app_state: &'a mut AppState, stdout: Option>, stderr: Option>, - message: Option, - hint_displayed: bool, + show_hint: bool, + show_done: bool, } impl<'a> WatchState<'a> { @@ -26,8 +26,8 @@ impl<'a> WatchState<'a> { app_state, stdout: None, stderr: None, - message: None, - hint_displayed: false, + show_hint: false, + show_done: false, } } @@ -36,29 +36,32 @@ impl<'a> WatchState<'a> { self.writer } - pub fn run_current_exercise(&mut self) -> Result { + pub fn run_current_exercise(&mut self) -> Result<()> { + self.show_hint = false; + let output = self.app_state.current_exercise().run()?; self.stdout = Some(output.stdout); - if !output.status.success() { + if output.status.success() { + self.stderr = None; + self.show_done = true; + } else { self.stderr = Some(output.stderr); - return Ok(false); + self.show_done = false; } - self.stderr = None; - - Ok(true) + self.render() } - pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result { + pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> { self.app_state.set_current_exercise_ind(exercise_ind)?; self.run_current_exercise() } fn show_prompt(&mut self) -> io::Result<()> { - self.writer.write_all(b"\n\n")?; + self.writer.write_all(b"\n")?; - if !self.hint_displayed { + if !self.show_hint { self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; } @@ -84,20 +87,26 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; } - if let Some(message) = &self.message { - self.writer.write_all(message.as_bytes())?; - } - self.writer.write_all(b"\n")?; - if self.hint_displayed { + if self.show_hint { self.writer - .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?; + .write_fmt(format_args!("{}\n", "Hint".bold().cyan().underlined()))?; self.writer .write_all(self.app_state.current_exercise().hint.as_bytes())?; self.writer.write_all(b"\n\n")?; } + if self.show_done { + self.writer.write_fmt(format_args!( + "{}\n\n", + "Exercise done βœ“ +When you are done experimenting, enter `n` or `next` to go to the next exercise πŸ¦€" + .bold() + .green(), + ))?; + } + let line_width = size()?.0; let progress_bar = progress_bar( self.app_state.n_done(), @@ -108,7 +117,7 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"Current exercise: ")?; self.writer.write_fmt(format_args!( - "{}", + "{}\n", self.app_state .current_exercise() .path @@ -122,7 +131,7 @@ impl<'a> WatchState<'a> { } pub fn show_hint(&mut self) -> Result<()> { - self.hint_displayed = true; + self.show_hint = true; self.render() } @@ -133,6 +142,7 @@ impl<'a> WatchState<'a> { self.writer .write_all(b" (confusing input can occur after resizing the terminal)")?; } + self.writer.write_all(b"\n")?; self.show_prompt() } } -- cgit v1.2.3 From a534de0312ff47d5e87b3bf60d508bdaafb98fbc Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 15:27:29 +0200 Subject: Implement going to the next exercise --- src/watch.rs | 11 +++++++---- src/watch/state.rs | 23 ++++++++++++++++++++++- src/watch/terminal_event.rs | 2 ++ 3 files changed, 31 insertions(+), 5 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 928fc5f..357b5c7 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -26,9 +26,9 @@ use self::{ enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, + TerminalResize, NotifyErr(notify::Error), TerminalEventErr(io::Error), - TerminalResize, } /// Returned by the watch mode to indicate what to do afterwards. @@ -60,15 +60,15 @@ pub fn watch(app_state: &mut AppState) -> Result { while let Ok(event) = rx.recv() { match event { + WatchEvent::Input(InputEvent::Next) => { + watch_state.next_exercise()?; + } WatchEvent::Input(InputEvent::Hint) => { watch_state.show_hint()?; } WatchEvent::Input(InputEvent::List) => { return Ok(WatchExit::List); } - WatchEvent::TerminalResize => { - watch_state.render()?; - } WatchEvent::Input(InputEvent::Quit) => break, WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { watch_state.handle_invalid_cmd(&cmd)?; @@ -76,6 +76,9 @@ pub fn watch(app_state: &mut AppState) -> Result { WatchEvent::FileChange { exercise_ind } => { watch_state.run_exercise_with_ind(exercise_ind)?; } + WatchEvent::TerminalResize => { + watch_state.render()?; + } WatchEvent::NotifyErr(e) => { return Err(Error::from(e).context("Exercise file watcher failed")) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 5a5c0ca..462633d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -6,7 +6,10 @@ use crossterm::{ }; use std::io::{self, StdoutLock, Write}; -use crate::{app_state::AppState, progress_bar::progress_bar}; +use crate::{ + app_state::{AppState, ExercisesProgress}, + progress_bar::progress_bar, +}; pub struct WatchState<'a> { writer: StdoutLock<'a>, @@ -58,9 +61,27 @@ impl<'a> WatchState<'a> { self.run_current_exercise() } + pub fn next_exercise(&mut self) -> Result<()> { + if !self.show_done { + self.writer + .write_all(b"The current exercise isn't done yet\n")?; + self.show_prompt()?; + return Ok(()); + } + + match self.app_state.done_current_exercise()? { + ExercisesProgress::AllDone => todo!(), + ExercisesProgress::Pending => self.run_current_exercise(), + } + } + fn show_prompt(&mut self) -> io::Result<()> { self.writer.write_all(b"\n")?; + if self.show_done { + self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; + } + if !self.show_hint { self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index faca8a2..7f7ebe0 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -4,6 +4,7 @@ use std::sync::mpsc::Sender; use super::WatchEvent; pub enum InputEvent { + Next, Hint, List, Quit, @@ -38,6 +39,7 @@ pub fn terminal_event_handler(tx: Sender) { match key.code { KeyCode::Enter => { let input_event = match input.trim() { + "n" | "next" => InputEvent::Next, "h" | "hint" => InputEvent::Hint, "l" | "list" => break InputEvent::List, "q" | "quit" => break InputEvent::Quit, -- cgit v1.2.3 From d5a6dee1b329f68d00bee61c6b6c7a0adbf8bab5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 18:57:04 +0200 Subject: Handle the case when all exercises are done --- src/app_state.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- src/run.rs | 24 ++++++++++-------------- src/watch.rs | 17 ++++++++++------- src/watch/state.rs | 34 +++++++++++++++++----------------- 4 files changed, 84 insertions(+), 43 deletions(-) (limited to 'src/watch.rs') diff --git a/src/app_state.rs b/src/app_state.rs index 4a0912e..b1440e8 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,8 +1,16 @@ use anyhow::{bail, Context, Result}; +use crossterm::{ + style::Stylize, + terminal::{Clear, ClearType}, + ExecutableCommand, +}; use serde::{Deserialize, Serialize}; -use std::fs; +use std::{ + fs, + io::{StdoutLock, Write}, +}; -use crate::exercise::Exercise; +use crate::{exercise::Exercise, FENISH_LINE}; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -143,7 +151,7 @@ impl AppState { Ok(()) } - fn next_exercise_ind(&self) -> Option { + fn next_pending_exercise_ind(&self) -> Option { let current_ind = self.state_file.current_exercise_ind; if current_ind == self.state_file.progress.len() - 1 { @@ -167,14 +175,41 @@ impl AppState { } } - pub fn done_current_exercise(&mut self) -> Result { + pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { let done = &mut self.state_file.progress[self.state_file.current_exercise_ind]; if !*done { *done = true; self.n_done += 1; } - let Some(ind) = self.next_exercise_ind() else { + let Some(ind) = self.next_pending_exercise_ind() else { + writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + + for (exercise_ind, exercise) in self.exercises().iter().enumerate() { + writer.write_fmt(format_args!("Running {exercise} ... "))?; + writer.flush()?; + + if !exercise.run()?.status.success() { + self.state_file.current_exercise_ind = exercise_ind; + self.current_exercise = exercise; + + // No check if the exercise is done before setting it to pending + // because no pending exercise was found. + self.state_file.progress[exercise_ind] = false; + self.n_done -= 1; + + self.state_file.write()?; + + return Ok(ExercisesProgress::Pending); + } + + writer.write_fmt(format_args!("{}\n", "ok".green()))?; + } + + writer.execute(Clear(ClearType::All))?; + writer.write_all(FENISH_LINE.as_bytes())?; + // TODO: Show final message. + return Ok(ExercisesProgress::AllDone); }; @@ -183,3 +218,10 @@ impl AppState { Ok(ExercisesProgress::Pending) } } + +const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" +All exercises seem to be done. +Recompiling and running all exercises to make sure that all of them are actually done. +This might take some minutes. + +"; diff --git a/src/run.rs b/src/run.rs index 18da193..ea790e9 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Result}; use crossterm::style::Stylize; -use std::io::{stdout, Write}; +use std::io::{self, Write}; use crate::app_state::{AppState, ExercisesProgress}; @@ -8,28 +8,24 @@ pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let output = exercise.run()?; - { - let mut stdout = stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; - } + let mut stdout = io::stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(b"\n")?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; if !output.status.success() { bail!("Ran {exercise} with errors"); } - println!( + stdout.write_fmt(format_args!( "{}{}", "βœ“ Successfully ran ".green(), exercise.path.to_string_lossy().green(), - ); + ))?; - match app_state.done_current_exercise()? { - ExercisesProgress::AllDone => println!( - "πŸŽ‰ Congratulations! You have done all the exercises! -πŸ”š There are no more exercises to do next!" - ), + match app_state.done_current_exercise(&mut stdout)? { + ExercisesProgress::AllDone => (), ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()), } diff --git a/src/watch.rs b/src/watch.rs index 357b5c7..beb69b3 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -15,7 +15,7 @@ mod debounce_event; mod state; mod terminal_event; -use crate::app_state::AppState; +use crate::app_state::{AppState, ExercisesProgress}; use self::{ debounce_event::DebounceEventHandler, @@ -32,6 +32,7 @@ enum WatchEvent { } /// Returned by the watch mode to indicate what to do afterwards. +#[must_use] pub enum WatchExit { /// Exit the program. Shutdown, @@ -60,16 +61,20 @@ pub fn watch(app_state: &mut AppState) -> Result { while let Ok(event) = rx.recv() { match event { - WatchEvent::Input(InputEvent::Next) => { - watch_state.next_exercise()?; - } + WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? { + ExercisesProgress::AllDone => break, + ExercisesProgress::Pending => watch_state.run_current_exercise()?, + }, WatchEvent::Input(InputEvent::Hint) => { watch_state.show_hint()?; } WatchEvent::Input(InputEvent::List) => { return Ok(WatchExit::List); } - WatchEvent::Input(InputEvent::Quit) => break, + WatchEvent::Input(InputEvent::Quit) => { + watch_state.into_writer().write_all(QUIT_MSG)?; + break; + } WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { watch_state.handle_invalid_cmd(&cmd)?; } @@ -88,8 +93,6 @@ pub fn watch(app_state: &mut AppState) -> Result { } } - watch_state.into_writer().write_all(QUIT_MSG)?; - Ok(WatchExit::Shutdown) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 462633d..70b6ae4 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -4,7 +4,10 @@ use crossterm::{ terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use std::io::{self, StdoutLock, Write}; +use std::{ + io::{self, StdoutLock, Write}, + process::Output, +}; use crate::{ app_state::{AppState, ExercisesProgress}, @@ -49,6 +52,9 @@ impl<'a> WatchState<'a> { self.stderr = None; self.show_done = true; } else { + self.app_state + .set_pending(self.app_state.current_exercise_ind())?; + self.stderr = Some(output.stderr); self.show_done = false; } @@ -61,18 +67,15 @@ impl<'a> WatchState<'a> { self.run_current_exercise() } - pub fn next_exercise(&mut self) -> Result<()> { + pub fn next_exercise(&mut self) -> Result { if !self.show_done { self.writer .write_all(b"The current exercise isn't done yet\n")?; self.show_prompt()?; - return Ok(()); + return Ok(ExercisesProgress::Pending); } - match self.app_state.done_current_exercise()? { - ExercisesProgress::AllDone => todo!(), - ExercisesProgress::Pending => self.run_current_exercise(), - } + self.app_state.done_current_exercise(&mut self.writer) } fn show_prompt(&mut self) -> io::Result<()> { @@ -93,7 +96,7 @@ impl<'a> WatchState<'a> { } pub fn render(&mut self) -> Result<()> { - // Prevent having the first line shifted after clearing because of the prompt. + // Prevent having the first line shifted. self.writer.write_all(b"\n")?; self.writer.execute(Clear(ClearType::All))?; @@ -111,11 +114,11 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; if self.show_hint { - self.writer - .write_fmt(format_args!("{}\n", "Hint".bold().cyan().underlined()))?; - self.writer - .write_all(self.app_state.current_exercise().hint.as_bytes())?; - self.writer.write_all(b"\n\n")?; + self.writer.write_fmt(format_args!( + "{}\n{}\n\n", + "Hint".bold().cyan().underlined(), + self.app_state.current_exercise().hint, + ))?; } if self.show_done { @@ -134,11 +137,8 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise self.app_state.exercises().len() as u16, line_width, )?; - self.writer.write_all(progress_bar.as_bytes())?; - - self.writer.write_all(b"Current exercise: ")?; self.writer.write_fmt(format_args!( - "{}\n", + "{progress_bar}Current exercise: {}\n", self.app_state .current_exercise() .path -- cgit v1.2.3 From 5c0073a9485c4226e58b657cb49628919a28a942 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 01:15:43 +0200 Subject: Tolerate changes in the state file --- Cargo.lock | 1 + Cargo.toml | 1 + exercises/00_intro/intro1.rs | 1 - info.toml | 272 +++++++++++++++++++++---------------------- src/app_state.rs | 207 +++++++++++++++----------------- src/app_state/state_file.rs | 112 ++++++++++++++++++ src/exercise.rs | 72 +++--------- src/info_file.rs | 81 +++++++++++++ src/init.rs | 23 ++-- src/list.rs | 11 +- src/list/state.rs | 35 +++--- src/main.rs | 40 ++++--- src/run.rs | 2 +- src/watch.rs | 15 ++- src/watch/debounce_event.rs | 44 ------- src/watch/notify_event.rs | 42 +++++++ 16 files changed, 552 insertions(+), 407 deletions(-) create mode 100644 src/app_state/state_file.rs create mode 100644 src/info_file.rs delete mode 100644 src/watch/debounce_event.rs create mode 100644 src/watch/notify_event.rs (limited to 'src/watch.rs') diff --git a/Cargo.lock b/Cargo.lock index 6c64661..dbf1923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,7 @@ dependencies = [ "assert_cmd", "clap", "crossterm", + "hashbrown", "notify-debouncer-mini", "predicates", "ratatui", diff --git a/Cargo.toml b/Cargo.toml index 285e7df..14ae9a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ edition.workspace = true anyhow.workspace = true clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" +hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" rustlings-macros = { path = "rustlings-macros" } diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index e4e0444..170d195 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,6 +1,5 @@ // intro1.rs // -// TODO: Update comment // We sometimes encourage you to keep trying things on a given exercise, even // after you already figured it out. If you got everything working and feel // ready for the next exercise, remove the `I AM NOT DONE` comment below. diff --git a/info.toml b/info.toml index b6b6800..fa90ad7 100644 --- a/info.toml +++ b/info.toml @@ -33,10 +33,11 @@ https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md # INTRO +# TODO: Update exercise [[exercises]] name = "intro1" -path = "exercises/00_intro/intro1.rs" -mode = "compile" +dir = "00_intro" +mode = "run" # TODO: Fix hint hint = """ Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file @@ -44,8 +45,8 @@ to move on to the next exercise.""" [[exercises]] name = "intro2" -path = "exercises/00_intro/intro2.rs" -mode = "compile" +dir = "00_intro" +mode = "run" hint = """ The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" @@ -53,16 +54,16 @@ The compiler is informing us that we've got the name of the print macro wrong, a [[exercises]] name = "variables1" -path = "exercises/01_variables/variables1.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ The declaration in the first line in the main function is missing a keyword that is needed in Rust to create a new variable binding.""" [[exercises]] name = "variables2" -path = "exercises/01_variables/variables2.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ The compiler message is saying that Rust cannot infer the type that the variable binding `x` has with what is given here. @@ -80,8 +81,8 @@ What if `x` is the same type as `10`? What if it's a different type?""" [[exercises]] name = "variables3" -path = "exercises/01_variables/variables3.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ Oops! In this exercise, we have a variable binding that we've created on in the first line in the `main` function, and we're trying to use it in the next line, @@ -94,8 +95,8 @@ programming language -- thankfully the Rust compiler has caught this for us!""" [[exercises]] name = "variables4" -path = "exercises/01_variables/variables4.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ In Rust, variable bindings are immutable by default. But here we're trying to reassign a different value to `x`! There's a keyword we can use to make @@ -103,8 +104,8 @@ a variable binding mutable instead.""" [[exercises]] name = "variables5" -path = "exercises/01_variables/variables5.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ In `variables4` we already learned how to make an immutable variable mutable using a special keyword. Unfortunately this doesn't help us much in this @@ -121,8 +122,8 @@ Try to solve this exercise afterwards using this technique.""" [[exercises]] name = "variables6" -path = "exercises/01_variables/variables6.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ We know about variables and mutability, but there is another important type of variable available: constants. @@ -141,8 +142,8 @@ https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants [[exercises]] name = "functions1" -path = "exercises/02_functions/functions1.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ This main function is calling a function that it expects to exist, but the function doesn't exist. It expects this function to have the name `call_me`. @@ -151,24 +152,24 @@ Sounds a lot like `main`, doesn't it?""" [[exercises]] name = "functions2" -path = "exercises/02_functions/functions2.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ Rust requires that all parts of a function's signature have type annotations, but `call_me` is missing the type annotation of `num`.""" [[exercises]] name = "functions3" -path = "exercises/02_functions/functions3.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ This time, the function *declaration* is okay, but there's something wrong with the place where we're calling the function.""" [[exercises]] name = "functions4" -path = "exercises/02_functions/functions4.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ The error message points to the function `sale_price` and says it expects a type after the `->`. This is where the function's return type should be -- take a @@ -179,8 +180,8 @@ for the inputs of the functions here, since the original prices shouldn't be neg [[exercises]] name = "functions5" -path = "exercises/02_functions/functions5.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ This is a really common error that can be fixed by removing one character. It happens because Rust distinguishes between expressions and statements: @@ -198,7 +199,7 @@ They are not the same. There are two solutions: [[exercises]] name = "if1" -path = "exercises/03_if/if1.rs" +dir = "03_if" mode = "test" hint = """ It's possible to do this in one line if you would like! @@ -214,7 +215,7 @@ Remember in Rust that: [[exercises]] name = "if2" -path = "exercises/03_if/if2.rs" +dir = "03_if" mode = "test" hint = """ For that first compiler error, it's important in Rust that each conditional @@ -223,7 +224,7 @@ conditions checking different input values.""" [[exercises]] name = "if3" -path = "exercises/03_if/if3.rs" +dir = "03_if" mode = "test" hint = """ In Rust, every arm of an `if` expression has to return the same type of value. @@ -233,7 +234,6 @@ Make sure the type is consistent across all arms.""" [[exercises]] name = "quiz1" -path = "exercises/quiz1.rs" mode = "test" hint = "No hints this time ;)" @@ -241,20 +241,20 @@ hint = "No hints this time ;)" [[exercises]] name = "primitive_types1" -path = "exercises/04_primitive_types/primitive_types1.rs" -mode = "compile" +dir = "04_primitive_types" +mode = "run" hint = "No hints this time ;)" [[exercises]] name = "primitive_types2" -path = "exercises/04_primitive_types/primitive_types2.rs" -mode = "compile" +dir = "04_primitive_types" +mode = "run" hint = "No hints this time ;)" [[exercises]] name = "primitive_types3" -path = "exercises/04_primitive_types/primitive_types3.rs" -mode = "compile" +dir = "04_primitive_types" +mode = "run" hint = """ There's a shorthand to initialize Arrays with a certain size that does not require you to type in 100 items (but you certainly can if you want!). @@ -269,7 +269,7 @@ for `a.len() >= 100`?""" [[exercises]] name = "primitive_types4" -path = "exercises/04_primitive_types/primitive_types4.rs" +dir = "04_primitive_types" mode = "test" hint = """ Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section @@ -284,8 +284,8 @@ https://doc.rust-lang.org/nomicon/coercions.html""" [[exercises]] name = "primitive_types5" -path = "exercises/04_primitive_types/primitive_types5.rs" -mode = "compile" +dir = "04_primitive_types" +mode = "run" hint = """ Take a look at the 'Data Types -> The Tuple Type' section of the book: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type @@ -297,7 +297,7 @@ of the tuple. You can do it!!""" [[exercises]] name = "primitive_types6" -path = "exercises/04_primitive_types/primitive_types6.rs" +dir = "04_primitive_types" mode = "test" hint = """ While you could use a destructuring `let` for the tuple here, try @@ -310,7 +310,7 @@ Now you have another tool in your toolbox!""" [[exercises]] name = "vecs1" -path = "exercises/05_vecs/vecs1.rs" +dir = "05_vecs" mode = "test" hint = """ In Rust, there are two ways to define a Vector. @@ -325,7 +325,7 @@ of the Rust book to learn more. [[exercises]] name = "vecs2" -path = "exercises/05_vecs/vecs2.rs" +dir = "05_vecs" mode = "test" hint = """ In the first function we are looping over the Vector and getting a reference to @@ -348,7 +348,7 @@ What do you think is the more commonly used pattern under Rust developers? [[exercises]] name = "move_semantics1" -path = "exercises/06_move_semantics/move_semantics1.rs" +dir = "06_move_semantics" mode = "test" hint = """ So you've got the "cannot borrow immutable local variable `vec` as mutable" @@ -362,7 +362,7 @@ happens!""" [[exercises]] name = "move_semantics2" -path = "exercises/06_move_semantics/move_semantics2.rs" +dir = "06_move_semantics" mode = "test" hint = """ When running this exercise for the first time, you'll notice an error about @@ -383,7 +383,7 @@ try them all: [[exercises]] name = "move_semantics3" -path = "exercises/06_move_semantics/move_semantics3.rs" +dir = "06_move_semantics" mode = "test" hint = """ The difference between this one and the previous ones is that the first line @@ -393,7 +393,7 @@ an existing binding to be a mutable binding instead of an immutable one :)""" [[exercises]] name = "move_semantics4" -path = "exercises/06_move_semantics/move_semantics4.rs" +dir = "06_move_semantics" mode = "test" hint = """ Stop reading whenever you feel like you have enough direction :) Or try @@ -407,7 +407,7 @@ So the end goal is to: [[exercises]] name = "move_semantics5" -path = "exercises/06_move_semantics/move_semantics5.rs" +dir = "06_move_semantics" mode = "test" hint = """ Carefully reason about the range in which each mutable reference is in @@ -419,8 +419,8 @@ https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-ref [[exercises]] name = "move_semantics6" -path = "exercises/06_move_semantics/move_semantics6.rs" -mode = "compile" +dir = "06_move_semantics" +mode = "run" hint = """ To find the answer, you can consult the book section "References and Borrowing": https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html @@ -440,7 +440,7 @@ Another hint: it has to do with the `&` character.""" [[exercises]] name = "structs1" -path = "exercises/07_structs/structs1.rs" +dir = "07_structs" mode = "test" hint = """ Rust has more than one type of struct. Three actually, all variants are used to @@ -460,7 +460,7 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html""" [[exercises]] name = "structs2" -path = "exercises/07_structs/structs2.rs" +dir = "07_structs" mode = "test" hint = """ Creating instances of structs is easy, all you need to do is assign some values @@ -472,7 +472,7 @@ https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-ins [[exercises]] name = "structs3" -path = "exercises/07_structs/structs3.rs" +dir = "07_structs" mode = "test" hint = """ For `is_international`: What makes a package international? Seems related to @@ -488,21 +488,21 @@ https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" [[exercises]] name = "enums1" -path = "exercises/08_enums/enums1.rs" -mode = "compile" +dir = "08_enums" +mode = "run" hint = "No hints this time ;)" [[exercises]] name = "enums2" -path = "exercises/08_enums/enums2.rs" -mode = "compile" +dir = "08_enums" +mode = "run" hint = """ You can create enumerations that have different variants with different types such as no data, anonymous structs, a single string, tuples, ...etc""" [[exercises]] name = "enums3" -path = "exercises/08_enums/enums3.rs" +dir = "08_enums" mode = "test" hint = """ As a first step, you can define enums to compile this code without errors. @@ -516,8 +516,8 @@ to get value in the variant.""" [[exercises]] name = "strings1" -path = "exercises/09_strings/strings1.rs" -mode = "compile" +dir = "09_strings" +mode = "run" hint = """ The `current_favorite_color` function is currently returning a string slice with the `'static` lifetime. We know this because the data of the string lives @@ -530,8 +530,8 @@ another way that uses the `From` trait.""" [[exercises]] name = "strings2" -path = "exercises/09_strings/strings2.rs" -mode = "compile" +dir = "09_strings" +mode = "run" hint = """ Yes, it would be really easy to fix this by just changing the value bound to `word` to be a string slice instead of a `String`, wouldn't it?? There is a way @@ -545,7 +545,7 @@ https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercion [[exercises]] name = "strings3" -path = "exercises/09_strings/strings3.rs" +dir = "09_strings" mode = "test" hint = """ There's tons of useful standard library functions for strings. Let's try and use some of them: @@ -556,16 +556,16 @@ the string slice into an owned string, which you can then freely extend.""" [[exercises]] name = "strings4" -path = "exercises/09_strings/strings4.rs" -mode = "compile" +dir = "09_strings" +mode = "run" hint = "No hints this time ;)" # MODULES [[exercises]] name = "modules1" -path = "exercises/10_modules/modules1.rs" -mode = "compile" +dir = "10_modules" +mode = "run" hint = """ Everything is private in Rust by default-- but there's a keyword we can use to make something public! The compiler error should point to the thing that @@ -573,8 +573,8 @@ needs to be public.""" [[exercises]] name = "modules2" -path = "exercises/10_modules/modules2.rs" -mode = "compile" +dir = "10_modules" +mode = "run" hint = """ The delicious_snacks module is trying to present an external interface that is different than its internal structure (the `fruits` and `veggies` modules and @@ -585,8 +585,8 @@ Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-w [[exercises]] name = "modules3" -path = "exercises/10_modules/modules3.rs" -mode = "compile" +dir = "10_modules" +mode = "run" hint = """ `UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a `use` statement for these two to bring them into scope. You can use nested @@ -596,7 +596,7 @@ paths or the glob operator to bring these two in using only one line.""" [[exercises]] name = "hashmaps1" -path = "exercises/11_hashmaps/hashmaps1.rs" +dir = "11_hashmaps" mode = "test" hint = """ Hint 1: Take a look at the return type of the function to figure out @@ -608,7 +608,7 @@ Hint 2: Number of fruits should be at least 5. And you have to put [[exercises]] name = "hashmaps2" -path = "exercises/11_hashmaps/hashmaps2.rs" +dir = "11_hashmaps" mode = "test" hint = """ Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. @@ -617,7 +617,7 @@ Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only- [[exercises]] name = "hashmaps3" -path = "exercises/11_hashmaps/hashmaps3.rs" +dir = "11_hashmaps" mode = "test" hint = """ Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert @@ -635,7 +635,6 @@ Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-v [[exercises]] name = "quiz2" -path = "exercises/quiz2.rs" mode = "test" hint = "No hints this time ;)" @@ -643,7 +642,7 @@ hint = "No hints this time ;)" [[exercises]] name = "options1" -path = "exercises/12_options/options1.rs" +dir = "12_options" mode = "test" hint = """ Options can have a `Some` value, with an inner value, or a `None` value, @@ -655,7 +654,7 @@ it doesn't panic in your face later?""" [[exercises]] name = "options2" -path = "exercises/12_options/options2.rs" +dir = "12_options" mode = "test" hint = """ Check out: @@ -672,8 +671,8 @@ Also see `Option::flatten` [[exercises]] name = "options3" -path = "exercises/12_options/options3.rs" -mode = "compile" +dir = "12_options" +mode = "run" hint = """ The compiler says a partial move happened in the `match` statement. How can this be avoided? The compiler shows the correction needed. @@ -685,7 +684,7 @@ https://doc.rust-lang.org/std/keyword.ref.html""" [[exercises]] name = "errors1" -path = "exercises/13_error_handling/errors1.rs" +dir = "13_error_handling" mode = "test" hint = """ `Ok` and `Err` are the two variants of `Result`, so what the tests are saying @@ -701,7 +700,7 @@ To make this change, you'll need to: [[exercises]] name = "errors2" -path = "exercises/13_error_handling/errors2.rs" +dir = "13_error_handling" mode = "test" hint = """ One way to handle this is using a `match` statement on @@ -717,8 +716,8 @@ and give it a try!""" [[exercises]] name = "errors3" -path = "exercises/13_error_handling/errors3.rs" -mode = "compile" +dir = "13_error_handling" +mode = "run" hint = """ If other functions can return a `Result`, why shouldn't `main`? It's a fairly common convention to return something like `Result<(), ErrorType>` from your @@ -729,7 +728,7 @@ positive results.""" [[exercises]] name = "errors4" -path = "exercises/13_error_handling/errors4.rs" +dir = "13_error_handling" mode = "test" hint = """ `PositiveNonzeroInteger::new` is always creating a new instance and returning @@ -741,8 +740,8 @@ everything is... okay :)""" [[exercises]] name = "errors5" -path = "exercises/13_error_handling/errors5.rs" -mode = "compile" +dir = "13_error_handling" +mode = "run" hint = """ There are two different possible `Result` types produced within `main()`, which are propagated using `?` operators. How do we declare a return type from @@ -765,7 +764,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen [[exercises]] name = "errors6" -path = "exercises/13_error_handling/errors6.rs" +dir = "13_error_handling" mode = "test" hint = """ This exercise uses a completed version of `PositiveNonzeroInteger` from @@ -787,8 +786,8 @@ https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err""" [[exercises]] name = "generics1" -path = "exercises/14_generics/generics1.rs" -mode = "compile" +dir = "14_generics" +mode = "run" hint = """ Vectors in Rust make use of generics to create dynamically sized arrays of any type. @@ -797,7 +796,7 @@ You need to tell the compiler what type we are pushing onto this vector.""" [[exercises]] name = "generics2" -path = "exercises/14_generics/generics2.rs" +dir = "14_generics" mode = "test" hint = """ Currently we are wrapping only values of type `u32`. @@ -811,7 +810,7 @@ If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html [[exercises]] name = "traits1" -path = "exercises/15_traits/traits1.rs" +dir = "15_traits" mode = "test" hint = """ A discussion about Traits in Rust can be found at: @@ -820,7 +819,7 @@ https://doc.rust-lang.org/book/ch10-02-traits.html [[exercises]] name = "traits2" -path = "exercises/15_traits/traits2.rs" +dir = "15_traits" mode = "test" hint = """ Notice how the trait takes ownership of `self`, and returns `Self`. @@ -833,7 +832,7 @@ the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html""" [[exercises]] name = "traits3" -path = "exercises/15_traits/traits3.rs" +dir = "15_traits" mode = "test" hint = """ Traits can have a default implementation for functions. Structs that implement @@ -845,7 +844,7 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#def [[exercises]] name = "traits4" -path = "exercises/15_traits/traits4.rs" +dir = "15_traits" mode = "test" hint = """ Instead of using concrete types as parameters you can use traits. Try replacing @@ -856,8 +855,8 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#tra [[exercises]] name = "traits5" -path = "exercises/15_traits/traits5.rs" -mode = "compile" +dir = "15_traits" +mode = "run" hint = """ To ensure a parameter implements multiple traits use the '+ syntax'. Try replacing the '??' with 'impl <> + <>'. @@ -869,7 +868,6 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#spe [[exercises]] name = "quiz3" -path = "exercises/quiz3.rs" mode = "test" hint = """ To find the best solution to this challenge you're going to need to think back @@ -881,16 +879,16 @@ You may also need this: `use std::fmt::Display;`.""" [[exercises]] name = "lifetimes1" -path = "exercises/16_lifetimes/lifetimes1.rs" -mode = "compile" +dir = "16_lifetimes" +mode = "run" hint = """ Let the compiler guide you. Also take a look at the book if you need help: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" [[exercises]] name = "lifetimes2" -path = "exercises/16_lifetimes/lifetimes2.rs" -mode = "compile" +dir = "16_lifetimes" +mode = "run" hint = """ Remember that the generic lifetime `'a` will get the concrete lifetime that is equal to the smaller of the lifetimes of `x` and `y`. @@ -903,8 +901,8 @@ inner block: [[exercises]] name = "lifetimes3" -path = "exercises/16_lifetimes/lifetimes3.rs" -mode = "compile" +dir = "16_lifetimes" +mode = "run" hint = """ If you use a lifetime annotation in a struct's fields, where else does it need to be added?""" @@ -913,7 +911,7 @@ to be added?""" [[exercises]] name = "tests1" -path = "exercises/17_tests/tests1.rs" +dir = "17_tests" mode = "test" hint = """ You don't even need to write any code to test -- you can just test values and @@ -928,7 +926,7 @@ ones pass, and which ones fail :)""" [[exercises]] name = "tests2" -path = "exercises/17_tests/tests2.rs" +dir = "17_tests" mode = "test" hint = """ Like the previous exercise, you don't need to write any code to get this test @@ -941,7 +939,7 @@ argument comes first and which comes second!""" [[exercises]] name = "tests3" -path = "exercises/17_tests/tests3.rs" +dir = "17_tests" mode = "test" hint = """ You can call a function right where you're passing arguments to `assert!`. So @@ -952,7 +950,7 @@ what you're doing using `!`, like `assert!(!having_fun())`.""" [[exercises]] name = "tests4" -path = "exercises/17_tests/tests4.rs" +dir = "17_tests" mode = "test" hint = """ We expect method `Rectangle::new()` to panic for negative values. @@ -966,7 +964,7 @@ https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-pa [[exercises]] name = "iterators1" -path = "exercises/18_iterators/iterators1.rs" +dir = "18_iterators" mode = "test" hint = """ Step 1: @@ -989,7 +987,7 @@ https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas. [[exercises]] name = "iterators2" -path = "exercises/18_iterators/iterators2.rs" +dir = "18_iterators" mode = "test" hint = """ Step 1: @@ -1015,7 +1013,7 @@ powerful and very general. Rust just needs to know the desired type.""" [[exercises]] name = "iterators3" -path = "exercises/18_iterators/iterators3.rs" +dir = "18_iterators" mode = "test" hint = """ The `divide` function needs to return the correct error when even division is @@ -1034,7 +1032,7 @@ powerful! It can make the solution to this exercise infinitely easier.""" [[exercises]] name = "iterators4" -path = "exercises/18_iterators/iterators4.rs" +dir = "18_iterators" mode = "test" hint = """ In an imperative language, you might write a `for` loop that updates a mutable @@ -1046,7 +1044,7 @@ Hint 2: Check out the `fold` and `rfold` methods!""" [[exercises]] name = "iterators5" -path = "exercises/18_iterators/iterators5.rs" +dir = "18_iterators" mode = "test" hint = """ The documentation for the `std::iter::Iterator` trait contains numerous methods @@ -1065,7 +1063,7 @@ a different method that could make your code more compact than using `fold`.""" [[exercises]] name = "box1" -path = "exercises/19_smart_pointers/box1.rs" +dir = "19_smart_pointers" mode = "test" hint = """ Step 1: @@ -1089,7 +1087,7 @@ definition and try other types! [[exercises]] name = "rc1" -path = "exercises/19_smart_pointers/rc1.rs" +dir = "19_smart_pointers" mode = "test" hint = """ This is a straightforward exercise to use the `Rc` type. Each `Planet` has @@ -1108,8 +1106,8 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html [[exercises]] name = "arc1" -path = "exercises/19_smart_pointers/arc1.rs" -mode = "compile" +dir = "19_smart_pointers" +mode = "run" hint = """ Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order to avoid creating a copy of `numbers`, you'll need to create `child_numbers` @@ -1126,7 +1124,7 @@ https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html [[exercises]] name = "cow1" -path = "exercises/19_smart_pointers/cow1.rs" +dir = "19_smart_pointers" mode = "test" hint = """ If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is @@ -1140,8 +1138,8 @@ on the `Cow` type. [[exercises]] name = "threads1" -path = "exercises/20_threads/threads1.rs" -mode = "compile" +dir = "20_threads" +mode = "run" hint = """ `JoinHandle` is a struct that is returned from a spawned thread: https://doc.rust-lang.org/std/thread/fn.spawn.html @@ -1158,8 +1156,8 @@ https://doc.rust-lang.org/std/thread/struct.JoinHandle.html [[exercises]] name = "threads2" -path = "exercises/20_threads/threads2.rs" -mode = "compile" +dir = "20_threads" +mode = "run" hint = """ `Arc` is an Atomic Reference Counted pointer that allows safe, shared access to **immutable** data. But we want to *change* the number of `jobs_completed` @@ -1180,7 +1178,7 @@ https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-betwee [[exercises]] name = "threads3" -path = "exercises/20_threads/threads3.rs" +dir = "20_threads" mode = "test" hint = """ An alternate way to handle concurrency between threads is to use an `mpsc` @@ -1199,8 +1197,8 @@ See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info. [[exercises]] name = "macros1" -path = "exercises/21_macros/macros1.rs" -mode = "compile" +dir = "21_macros" +mode = "run" hint = """ When you call a macro, you need to add something special compared to a regular function call. If you're stuck, take a look at what's inside @@ -1208,8 +1206,8 @@ regular function call. If you're stuck, take a look at what's inside [[exercises]] name = "macros2" -path = "exercises/21_macros/macros2.rs" -mode = "compile" +dir = "21_macros" +mode = "run" hint = """ Macros don't quite play by the same rules as the rest of Rust, in terms of what's available where. @@ -1219,8 +1217,8 @@ Unlike other things in Rust, the order of "where you define a macro" versus [[exercises]] name = "macros3" -path = "exercises/21_macros/macros3.rs" -mode = "compile" +dir = "21_macros" +mode = "run" hint = """ In order to use a macro outside of its module, you need to do something special to the module to lift the macro out into its parent. @@ -1230,8 +1228,8 @@ exported macros, if you've seen any of those around.""" [[exercises]] name = "macros4" -path = "exercises/21_macros/macros4.rs" -mode = "compile" +dir = "21_macros" +mode = "run" hint = """ You only need to add a single character to make this compile. @@ -1247,7 +1245,7 @@ https://veykril.github.io/tlborm/""" [[exercises]] name = "clippy1" -path = "exercises/22_clippy/clippy1.rs" +dir = "22_clippy" mode = "clippy" hint = """ Rust stores the highest precision version of any long or infinite precision @@ -1263,14 +1261,14 @@ appropriate replacement constant from `std::f32::consts`...""" [[exercises]] name = "clippy2" -path = "exercises/22_clippy/clippy2.rs" +dir = "22_clippy" mode = "clippy" hint = """ `for` loops over `Option` values are more clearly expressed as an `if let`""" [[exercises]] name = "clippy3" -path = "exercises/22_clippy/clippy3.rs" +dir = "22_clippy" mode = "clippy" hint = "No hints this time!" @@ -1278,7 +1276,7 @@ hint = "No hints this time!" [[exercises]] name = "using_as" -path = "exercises/23_conversions/using_as.rs" +dir = "23_conversions" mode = "test" hint = """ Use the `as` operator to cast one of the operands in the last line of the @@ -1286,14 +1284,14 @@ Use the `as` operator to cast one of the operands in the last line of the [[exercises]] name = "from_into" -path = "exercises/23_conversions/from_into.rs" +dir = "23_conversions" mode = "test" hint = """ Follow the steps provided right before the `From` implementation""" [[exercises]] name = "from_str" -path = "exercises/23_conversions/from_str.rs" +dir = "23_conversions" mode = "test" hint = """ The implementation of `FromStr` should return an `Ok` with a `Person` object, @@ -1314,7 +1312,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen [[exercises]] name = "try_from_into" -path = "exercises/23_conversions/try_from_into.rs" +dir = "23_conversions" mode = "test" hint = """ Follow the steps provided right before the `TryFrom` implementation. @@ -1337,7 +1335,7 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer [[exercises]] name = "as_ref_mut" -path = "exercises/23_conversions/as_ref_mut.rs" +dir = "23_conversions" mode = "test" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/src/app_state.rs b/src/app_state.rs index 2ea3db4..1a051b9 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -4,52 +4,16 @@ use crossterm::{ terminal::{Clear, ClearType}, ExecutableCommand, }; -use serde::{Deserialize, Serialize}; -use std::{ - fs, - io::{StdoutLock, Write}, -}; - -use crate::{exercise::Exercise, FENISH_LINE}; - -const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; - -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct StateFile { - current_exercise_ind: usize, - progress: Vec, -} - -impl StateFile { - fn read(exercises: &[Exercise]) -> Option { - let file_content = fs::read(".rustlings-state.json").ok()?; +use std::io::{StdoutLock, Write}; - let slf: Self = serde_json::de::from_slice(&file_content).ok()?; +mod state_file; - if slf.progress.len() != exercises.len() || slf.current_exercise_ind >= exercises.len() { - return None; - } - - Some(slf) - } - - fn read_or_default(exercises: &[Exercise]) -> Self { - Self::read(exercises).unwrap_or_else(|| Self { - current_exercise_ind: 0, - progress: vec![false; exercises.len()], - }) - } +use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE}; - fn write(&self) -> Result<()> { - let mut buf = Vec::with_capacity(1024); - serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; - fs::write(".rustlings-state.json", buf) - .context("Failed to write the state file `.rustlings-state.json`")?; +use self::state_file::{write, StateFileDeser}; - Ok(()) - } -} +const STATE_FILE_NAME: &str = ".rustlings-state.json"; +const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; #[must_use] pub enum ExercisesProgress { @@ -58,52 +22,85 @@ pub enum ExercisesProgress { } pub struct AppState { - state_file: StateFile, - exercises: &'static [Exercise], + current_exercise_ind: usize, + exercises: Vec, n_done: u16, - current_exercise: &'static Exercise, - final_message: &'static str, + welcome_message: String, + final_message: String, } impl AppState { - pub fn new(mut exercises: Vec, mut final_message: String) -> Self { - // Leaking especially for sending the exercises to the debounce event handler. - // Leaking is not a problem because the `AppState` instance lives until - // the end of the program. - exercises.shrink_to_fit(); - let exercises = exercises.leak(); - final_message.shrink_to_fit(); - let final_message = final_message.leak(); - - let state_file = StateFile::read_or_default(exercises); - let n_done = state_file - .progress - .iter() - .fold(0, |acc, done| acc + u16::from(*done)); - let current_exercise = &exercises[state_file.current_exercise_ind]; + pub fn new(info_file: InfoFile) -> Self { + let mut exercises = info_file + .exercises + .into_iter() + .map(|mut exercise_info| { + // Leaking to be able to borrow in the watch mode `Table`. + // Leaking is not a problem because the `AppState` instance lives until + // the end of the program. + let path = Box::leak(exercise_info.path().into_boxed_path()); + + exercise_info.name.shrink_to_fit(); + let name = exercise_info.name.leak(); + + let hint = exercise_info.hint.trim().to_owned(); + + Exercise { + name, + path, + mode: exercise_info.mode, + hint, + done: false, + } + }) + .collect::>(); + + let (current_exercise_ind, n_done) = StateFileDeser::read().map_or((0, 0), |state_file| { + let mut state_file_exercises = + hashbrown::HashMap::with_capacity(state_file.exercises.len()); + + for (ind, exercise_state) in state_file.exercises.into_iter().enumerate() { + state_file_exercises.insert( + exercise_state.name, + (ind == state_file.current_exercise_ind, exercise_state.done), + ); + } + + let mut current_exercise_ind = 0; + let mut n_done = 0; + for (ind, exercise) in exercises.iter_mut().enumerate() { + if let Some((current, done)) = state_file_exercises.get(exercise.name) { + if *done { + exercise.done = true; + n_done += 1; + } + + if *current { + current_exercise_ind = ind; + } + } + } + + (current_exercise_ind, n_done) + }); Self { - state_file, + current_exercise_ind, exercises, n_done, - current_exercise, - final_message, + welcome_message: info_file.welcome_message.unwrap_or_default(), + final_message: info_file.final_message.unwrap_or_default(), } } #[inline] pub fn current_exercise_ind(&self) -> usize { - self.state_file.current_exercise_ind - } - - #[inline] - pub fn progress(&self) -> &[bool] { - &self.state_file.progress + self.current_exercise_ind } #[inline] - pub fn exercises(&self) -> &'static [Exercise] { - self.exercises + pub fn exercises(&self) -> &[Exercise] { + &self.exercises } #[inline] @@ -112,8 +109,8 @@ impl AppState { } #[inline] - pub fn current_exercise(&self) -> &'static Exercise { - self.current_exercise + pub fn current_exercise(&self) -> &Exercise { + &self.exercises[self.current_exercise_ind] } pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { @@ -121,70 +118,61 @@ impl AppState { bail!(BAD_INDEX_ERR); } - self.state_file.current_exercise_ind = ind; - self.current_exercise = &self.exercises[ind]; + self.current_exercise_ind = ind; - self.state_file.write() + write(self) } pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { - let (ind, exercise) = self + // O(N) is fine since this method is used only once until the program exits. + // Building a hashmap would have more overhead. + self.current_exercise_ind = self .exercises .iter() - .enumerate() - .find(|(_, exercise)| exercise.name == name) + .position(|exercise| exercise.name == name) .with_context(|| format!("No exercise found for '{name}'!"))?; - self.state_file.current_exercise_ind = ind; - self.current_exercise = exercise; - - self.state_file.write() + write(self) } pub fn set_pending(&mut self, ind: usize) -> Result<()> { - let done = self - .state_file - .progress - .get_mut(ind) - .context(BAD_INDEX_ERR)?; - - if *done { - *done = false; + let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?; + + if exercise.done { + exercise.done = false; self.n_done -= 1; - self.state_file.write()?; + write(self)?; } Ok(()) } fn next_pending_exercise_ind(&self) -> Option { - let current_ind = self.state_file.current_exercise_ind; - - if current_ind == self.state_file.progress.len() - 1 { + if self.current_exercise_ind == self.exercises.len() - 1 { // The last exercise is done. // Search for exercises not done from the start. - return self.state_file.progress[..current_ind] + return self.exercises[..self.current_exercise_ind] .iter() - .position(|done| !done); + .position(|exercise| !exercise.done); } // The done exercise isn't the last one. // Search for a pending exercise after the current one and then from the start. - match self.state_file.progress[current_ind + 1..] + match self.exercises[self.current_exercise_ind + 1..] .iter() - .position(|done| !done) + .position(|exercise| !exercise.done) { - Some(ind) => Some(current_ind + 1 + ind), - None => self.state_file.progress[..current_ind] + Some(ind) => Some(self.current_exercise_ind + 1 + ind), + None => self.exercises[..self.current_exercise_ind] .iter() - .position(|done| !done), + .position(|exercise| !exercise.done), } } pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { - let done = &mut self.state_file.progress[self.state_file.current_exercise_ind]; - if !*done { - *done = true; + let exercise = &mut self.exercises[self.current_exercise_ind]; + if !exercise.done { + exercise.done = true; self.n_done += 1; } @@ -198,15 +186,14 @@ impl AppState { if !exercise.run()?.status.success() { writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; - self.state_file.current_exercise_ind = exercise_ind; - self.current_exercise = exercise; + self.current_exercise_ind = exercise_ind; // No check if the exercise is done before setting it to pending // because no pending exercise was found. - self.state_file.progress[exercise_ind] = false; + self.exercises[exercise_ind].done = false; self.n_done -= 1; - self.state_file.write()?; + write(self)?; return Ok(ExercisesProgress::Pending); } diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs new file mode 100644 index 0000000..364a1fa --- /dev/null +++ b/src/app_state/state_file.rs @@ -0,0 +1,112 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; + +use crate::exercise::Exercise; + +use super::{AppState, STATE_FILE_NAME}; + +#[derive(Deserialize)] +pub struct ExerciseStateDeser { + pub name: String, + pub done: bool, +} + +#[derive(Serialize)] +struct ExerciseStateSer<'a> { + name: &'a str, + done: bool, +} + +struct ExercisesStateSerializer<'a>(&'a [Exercise]); + +impl<'a> Serialize for ExercisesStateSerializer<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let iter = self.0.iter().map(|exercise| ExerciseStateSer { + name: exercise.name, + done: exercise.done, + }); + + serializer.collect_seq(iter) + } +} + +#[derive(Deserialize)] +pub struct StateFileDeser { + pub current_exercise_ind: usize, + pub exercises: Vec, +} + +#[derive(Serialize)] +struct StateFileSer<'a> { + current_exercise_ind: usize, + exercises: ExercisesStateSerializer<'a>, +} + +impl StateFileDeser { + pub fn read() -> Option { + let file_content = fs::read(STATE_FILE_NAME).ok()?; + serde_json::de::from_slice(&file_content).ok() + } +} + +pub fn write(app_state: &AppState) -> Result<()> { + let content = StateFileSer { + current_exercise_ind: app_state.current_exercise_ind, + exercises: ExercisesStateSerializer(&app_state.exercises), + }; + + let mut buf = Vec::with_capacity(1024); + serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?; + fs::write(STATE_FILE_NAME, buf) + .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use crate::info_file::Mode; + + use super::*; + + #[test] + fn ser_deser_sync() { + let current_exercise_ind = 1; + let exercises = [ + Exercise { + name: "1", + path: Path::new("exercises/1.rs"), + mode: Mode::Run, + hint: String::new(), + done: true, + }, + Exercise { + name: "2", + path: Path::new("exercises/2.rs"), + mode: Mode::Test, + hint: String::new(), + done: false, + }, + ]; + + let ser = StateFileSer { + current_exercise_ind, + exercises: ExercisesStateSerializer(&exercises), + }; + let deser: StateFileDeser = + serde_json::de::from_slice(&serde_json::ser::to_vec(&ser).unwrap()).unwrap(); + + assert_eq!(deser.current_exercise_ind, current_exercise_ind); + assert!(deser + .exercises + .iter() + .zip(exercises) + .all(|(deser, ser)| deser.name == ser.name && deser.done == ser.done)); + } +} diff --git a/src/exercise.rs b/src/exercise.rs index 6aa3b82..c5ece5f 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,66 +1,25 @@ use anyhow::{Context, Result}; -use serde::Deserialize; use std::{ - fmt::{self, Debug, Display, Formatter}, - fs::{self}, - path::PathBuf, + fmt::{self, Display, Formatter}, + path::Path, process::{Command, Output}, }; -use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; - -// The mode of the exercise. -#[derive(Deserialize, Copy, Clone)] -#[serde(rename_all = "lowercase")] -pub enum Mode { - // The exercise should be compiled as a binary - Compile, - // The exercise should be compiled as a test harness - Test, - // The exercise should be linted with clippy - Clippy, -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -pub struct InfoFile { - // TODO - pub welcome_message: Option, - pub final_message: Option, - pub exercises: Vec, -} - -impl InfoFile { - pub fn parse() -> Result { - // Read a local `info.toml` if it exists. - // Mainly to let the tests work for now. - let slf: Self = if let Ok(file_content) = fs::read_to_string("info.toml") { - toml_edit::de::from_str(&file_content) - } else { - toml_edit::de::from_str(include_str!("../info.toml")) - } - .context("Failed to parse `info.toml`")?; - - if slf.exercises.is_empty() { - panic!("{NO_EXERCISES_ERR}"); - } - - Ok(slf) - } -} +use crate::{ + embedded::{WriteStrategy, EMBEDDED_FILES}, + info_file::Mode, +}; -// Deserialized from the `info.toml` file. -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] pub struct Exercise { - // Name of the exercise - pub name: String, - // The path to the file containing the exercise's source code - pub path: PathBuf, + // Exercise's unique name + pub name: &'static str, + // Exercise's path + pub path: &'static Path, // The mode of the exercise pub mode: Mode, // The hint text associated with the exercise pub hint: String, + pub done: bool, } impl Exercise { @@ -79,7 +38,7 @@ impl Exercise { .arg("always") .arg("-q") .arg("--bin") - .arg(&self.name) + .arg(self.name) .args(args) .output() .context("Failed to run Cargo") @@ -87,7 +46,7 @@ impl Exercise { pub fn run(&self) -> Result { match self.mode { - Mode::Compile => self.cargo_cmd("run", &[]), + Mode::Run => self.cargo_cmd("run", &[]), Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]), Mode::Clippy => self.cargo_cmd( "clippy", @@ -98,7 +57,7 @@ impl Exercise { pub fn reset(&self) -> Result<()> { EMBEDDED_FILES - .write_exercise_to_disk(&self.path, WriteStrategy::Overwrite) + .write_exercise_to_disk(self.path, WriteStrategy::Overwrite) .with_context(|| format!("Failed to reset the exercise {self}")) } } @@ -108,6 +67,3 @@ impl Display for Exercise { Display::fmt(&self.path.display(), f) } } - -const NO_EXERCISES_ERR: &str = "There are no exercises yet! -If you are developing third-party exercises, add at least one exercise before testing."; diff --git a/src/info_file.rs b/src/info_file.rs new file mode 100644 index 0000000..dc97b92 --- /dev/null +++ b/src/info_file.rs @@ -0,0 +1,81 @@ +use anyhow::{bail, Context, Error, Result}; +use serde::Deserialize; +use std::{fs, path::PathBuf}; + +// The mode of the exercise. +#[derive(Deserialize, Copy, Clone)] +#[serde(rename_all = "lowercase")] +pub enum Mode { + // The exercise should be compiled as a binary + Run, + // The exercise should be compiled as a test harness + Test, + // The exercise should be linted with clippy + Clippy, +} + +// Deserialized from the `info.toml` file. +#[derive(Deserialize)] +pub struct ExerciseInfo { + // Name of the exercise + pub name: String, + // The exercise's directory inside the `exercises` directory + pub dir: Option, + // The mode of the exercise + pub mode: Mode, + // The hint text associated with the exercise + pub hint: String, +} + +impl ExerciseInfo { + pub fn path(&self) -> PathBuf { + let path = if let Some(dir) = &self.dir { + format!("exercises/{dir}/{}.rs", self.name) + } else { + format!("exercises/{}.rs", self.name) + }; + + PathBuf::from(path) + } +} + +#[derive(Deserialize)] +pub struct InfoFile { + pub welcome_message: Option, + pub final_message: Option, + pub exercises: Vec, +} + +impl InfoFile { + pub fn parse() -> Result { + // Read a local `info.toml` if it exists. + let slf: Self = match fs::read_to_string("info.toml") { + Ok(file_content) => toml_edit::de::from_str(&file_content) + .context("Failed to parse the `info.toml` file")?, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => { + toml_edit::de::from_str(include_str!("../info.toml")) + .context("Failed to parse the embedded `info.toml` file")? + } + _ => return Err(Error::from(e).context("Failed to read the `info.toml` file")), + }, + }; + + if slf.exercises.is_empty() { + bail!("{NO_EXERCISES_ERR}"); + } + + let mut names_set = hashbrown::HashSet::with_capacity(slf.exercises.len()); + for exercise in &slf.exercises { + if !names_set.insert(exercise.name.as_str()) { + bail!("Exercise names must all be unique!") + } + } + drop(names_set); + + Ok(slf) + } +} + +const NO_EXERCISES_ERR: &str = "There are no exercises yet! +If you are developing third-party exercises, add at least one exercise before testing."; diff --git a/src/init.rs b/src/init.rs index 093610a..2badf37 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,17 +6,21 @@ use std::{ path::Path, }; -use crate::{embedded::EMBEDDED_FILES, exercise::Exercise}; +use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; -fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { +fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> { let mut cargo_toml = Vec::with_capacity(1 << 13); cargo_toml.extend_from_slice(b"bin = [\n"); - for exercise in exercises { + for exercise_info in exercise_infos { cargo_toml.extend_from_slice(b" { name = \""); - cargo_toml.extend_from_slice(exercise.name.as_bytes()); - cargo_toml.extend_from_slice(b"\", path = \""); - cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); - cargo_toml.extend_from_slice(b"\" },\n"); + cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); + cargo_toml.extend_from_slice(b"\", path = \"exercises/"); + if let Some(dir) = &exercise_info.dir { + cargo_toml.extend_from_slice(dir.as_bytes()); + cargo_toml.extend_from_slice(b"/"); + } + cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); + cargo_toml.extend_from_slice(b".rs\" },\n"); } cargo_toml.extend_from_slice( @@ -54,7 +58,7 @@ fn create_vscode_dir() -> Result<()> { Ok(()) } -pub fn init(exercises: &[Exercise]) -> Result<()> { +pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); } @@ -74,7 +78,8 @@ pub fn init(exercises: &[Exercise]) -> Result<()> { .init_exercises_dir() .context("Failed to initialize the `rustlings/exercises` directory")?; - create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?; + create_cargo_toml(exercise_infos) + .context("Failed to create the file `rustlings/Cargo.toml`")?; create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?; diff --git a/src/list.rs b/src/list.rs index de120ea..2bb813d 100644 --- a/src/list.rs +++ b/src/list.rs @@ -5,7 +5,7 @@ use crossterm::{ ExecutableCommand, }; use ratatui::{backend::CrosstermBackend, Terminal}; -use std::{fmt::Write, io}; +use std::io; mod state; @@ -72,14 +72,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> { ui_state.message.push_str(message); } KeyCode::Char('r') => { - let Some(exercise) = ui_state.reset_selected()? else { - continue; - }; - - ui_state = ui_state.with_updated_rows(); - ui_state - .message - .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; + ui_state = ui_state.with_reset_selected()?; } KeyCode::Char('c') => { ui_state.selected_to_current_exercise()?; diff --git a/src/list/state.rs b/src/list/state.rs index 0dcfe88..38391a4 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -6,8 +6,9 @@ use ratatui::{ widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, Frame, }; +use std::fmt::Write; -use crate::{app_state::AppState, exercise::Exercise, progress_bar::progress_bar_ratatui}; +use crate::{app_state::AppState, progress_bar::progress_bar_ratatui}; #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { @@ -34,10 +35,9 @@ impl<'a> UiState<'a> { .app_state .exercises() .iter() - .zip(self.app_state.progress().iter().copied()) .enumerate() - .filter_map(|(ind, (exercise, done))| { - let exercise_state = if done { + .filter_map(|(ind, exercise)| { + let exercise_state = if exercise.done { if self.filter == Filter::Pending { return None; } @@ -62,7 +62,7 @@ impl<'a> UiState<'a> { Some(Row::new([ next, exercise_state, - Span::raw(&exercise.name), + Span::raw(exercise.name), Span::raw(exercise.path.to_string_lossy()), ])) }); @@ -212,29 +212,30 @@ impl<'a> UiState<'a> { Ok(()) } - pub fn reset_selected(&mut self) -> Result> { + pub fn with_reset_selected(mut self) -> Result { let Some(selected) = self.table_state.selected() else { - return Ok(None); + return Ok(self); }; let (ind, exercise) = self .app_state .exercises() .iter() - .zip(self.app_state.progress()) .enumerate() - .filter_map(|(ind, (exercise, done))| match self.filter { - Filter::Done => done.then_some((ind, exercise)), - Filter::Pending => (!done).then_some((ind, exercise)), + .filter_map(|(ind, exercise)| match self.filter { + Filter::Done => exercise.done.then_some((ind, exercise)), + Filter::Pending => (!exercise.done).then_some((ind, exercise)), Filter::None => Some((ind, exercise)), }) .nth(selected) .context("Invalid selection index")?; - self.app_state.set_pending(ind)?; exercise.reset()?; + self.message + .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; + self.app_state.set_pending(ind)?; - Ok(Some(exercise)) + Ok(self.with_updated_rows()) } pub fn selected_to_current_exercise(&mut self) -> Result<()> { @@ -244,12 +245,12 @@ impl<'a> UiState<'a> { let ind = self .app_state - .progress() + .exercises() .iter() .enumerate() - .filter_map(|(ind, done)| match self.filter { - Filter::Done => done.then_some(ind), - Filter::Pending => (!done).then_some(ind), + .filter_map(|(ind, exercise)| match self.filter { + Filter::Done => exercise.done.then_some(ind), + Filter::Pending => (!exercise.done).then_some(ind), Filter::None => Some(ind), }) .nth(selected) diff --git a/src/main.rs b/src/main.rs index cdfa21f..a96e323 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::{path::Path, process::exit}; mod app_state; mod embedded; mod exercise; +mod info_file; mod init; mod list; mod progress_bar; @@ -13,7 +14,7 @@ mod watch; use self::{ app_state::AppState, - exercise::InfoFile, + info_file::InfoFile, init::init, list::list, run::run, @@ -54,12 +55,10 @@ fn main() -> Result<()> { which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; - let mut info_file = InfoFile::parse()?; - info_file.exercises.shrink_to_fit(); - let exercises = info_file.exercises; + let info_file = InfoFile::parse()?; if matches!(args.command, Some(Subcommands::Init)) { - init(&exercises).context("Initialization failed")?; + init(&info_file.exercises).context("Initialization failed")?; println!("{POST_INIT_MSG}"); return Ok(()); @@ -68,18 +67,29 @@ fn main() -> Result<()> { exit(1); } - let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default()); + let mut app_state = AppState::new(info_file); match args.command { - None => loop { - match watch(&mut app_state)? { - WatchExit::Shutdown => break, - // It is much easier to exit the watch mode, launch the list mode and then restart - // the watch mode instead of trying to pause the watch threads and correct the - // watch state. - WatchExit::List => list(&mut app_state)?, + None => { + // For the the notify event handler thread. + // Leaking is not a problem because the slice lives until the end of the program. + let exercise_paths = app_state + .exercises() + .iter() + .map(|exercise| exercise.path) + .collect::>() + .leak(); + + loop { + match watch(&mut app_state, exercise_paths)? { + WatchExit::Shutdown => break, + // It is much easier to exit the watch mode, launch the list mode and then restart + // the watch mode instead of trying to pause the watch threads and correct the + // watch state. + WatchExit::List => list(&mut app_state)?, + } } - }, + } // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::Run { name }) => { @@ -90,10 +100,10 @@ fn main() -> Result<()> { } Some(Subcommands::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; - app_state.set_pending(app_state.current_exercise_ind())?; let exercise = app_state.current_exercise(); exercise.reset()?; println!("The exercise {exercise} has been reset!"); + app_state.set_pending(app_state.current_exercise_ind())?; } Some(Subcommands::Hint { name }) => { app_state.set_current_exercise_by_name(&name)?; diff --git a/src/run.rs b/src/run.rs index 4748549..9c504b5 100644 --- a/src/run.rs +++ b/src/run.rs @@ -17,7 +17,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { if !output.status.success() { app_state.set_pending(app_state.current_exercise_ind())?; - bail!("Ran {exercise} with errors"); + bail!("Ran {} with errors", app_state.current_exercise()); } stdout.write_fmt(format_args!( diff --git a/src/watch.rs b/src/watch.rs index beb69b3..58e829f 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -11,14 +11,14 @@ use std::{ time::Duration, }; -mod debounce_event; +mod notify_event; mod state; mod terminal_event; use crate::app_state::{AppState, ExercisesProgress}; use self::{ - debounce_event::DebounceEventHandler, + notify_event::DebounceEventHandler, state::WatchState, terminal_event::{terminal_event_handler, InputEvent}, }; @@ -40,13 +40,16 @@ pub enum WatchExit { List, } -pub fn watch(app_state: &mut AppState) -> Result { +pub fn watch( + app_state: &mut AppState, + exercise_paths: &'static [&'static Path], +) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( Duration::from_secs(1), DebounceEventHandler { tx: tx.clone(), - exercises: app_state.exercises(), + exercise_paths, }, )?; debouncer @@ -85,10 +88,10 @@ pub fn watch(app_state: &mut AppState) -> Result { watch_state.render()?; } WatchEvent::NotifyErr(e) => { - return Err(Error::from(e).context("Exercise file watcher failed")) + return Err(Error::from(e).context("Exercise file watcher failed")); } WatchEvent::TerminalEventErr(e) => { - return Err(Error::from(e).context("Terminal event listener failed")) + return Err(Error::from(e).context("Terminal event listener failed")); } } } diff --git a/src/watch/debounce_event.rs b/src/watch/debounce_event.rs deleted file mode 100644 index 1dc92cb..0000000 --- a/src/watch/debounce_event.rs +++ /dev/null @@ -1,44 +0,0 @@ -use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; -use std::sync::mpsc::Sender; - -use crate::exercise::Exercise; - -use super::WatchEvent; - -pub struct DebounceEventHandler { - pub tx: Sender, - pub exercises: &'static [Exercise], -} - -impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { - fn handle_event(&mut self, event: DebounceEventResult) { - let event = match event { - Ok(event) => { - let Some(exercise_ind) = event - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises - .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - else { - return; - }; - - WatchEvent::FileChange { exercise_ind } - } - Err(e) => WatchEvent::NotifyErr(e), - }; - - // An error occurs when the receiver is dropped. - // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.tx.send(event); - } -} diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs new file mode 100644 index 0000000..0c8d669 --- /dev/null +++ b/src/watch/notify_event.rs @@ -0,0 +1,42 @@ +use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; +use std::{path::Path, sync::mpsc::Sender}; + +use super::WatchEvent; + +pub struct DebounceEventHandler { + pub tx: Sender, + pub exercise_paths: &'static [&'static Path], +} + +impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { + fn handle_event(&mut self, event: DebounceEventResult) { + let event = match event { + Ok(event) => { + let Some(exercise_ind) = event + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercise_paths + .iter() + .position(|path| event.path.ends_with(path)) + }) + .min() + else { + return; + }; + + WatchEvent::FileChange { exercise_ind } + } + Err(e) => WatchEvent::NotifyErr(e), + }; + + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. + let _ = self.tx.send(event); + } +} -- cgit v1.2.3 From bee62c89de09fdd9823cba81e07f0f8528fe8ef9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 02:41:19 +0200 Subject: Add terminal links --- src/app_state.rs | 2 +- src/app_state/state_file.rs | 8 +++----- src/embedded.rs | 7 ++++++- src/exercise.rs | 34 +++++++++++++++++++++++++++++++--- src/info_file.rs | 10 ++++------ src/list/state.rs | 2 +- src/run.rs | 12 +++++++++--- src/watch.rs | 2 +- src/watch/notify_event.rs | 4 ++-- src/watch/state.rs | 6 +----- 10 files changed, 59 insertions(+), 28 deletions(-) (limited to 'src/watch.rs') diff --git a/src/app_state.rs b/src/app_state.rs index 1a051b9..98c6384 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -38,7 +38,7 @@ impl AppState { // Leaking to be able to borrow in the watch mode `Table`. // Leaking is not a problem because the `AppState` instance lives until // the end of the program. - let path = Box::leak(exercise_info.path().into_boxed_path()); + let path = exercise_info.path().leak(); exercise_info.name.shrink_to_fit(); let name = exercise_info.name.leak(); diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs index 364a1fa..4e4a0e1 100644 --- a/src/app_state/state_file.rs +++ b/src/app_state/state_file.rs @@ -59,7 +59,7 @@ pub fn write(app_state: &AppState) -> Result<()> { exercises: ExercisesStateSerializer(&app_state.exercises), }; - let mut buf = Vec::with_capacity(1024); + let mut buf = Vec::with_capacity(4096); serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?; fs::write(STATE_FILE_NAME, buf) .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?; @@ -69,8 +69,6 @@ pub fn write(app_state: &AppState) -> Result<()> { #[cfg(test)] mod tests { - use std::path::Path; - use crate::info_file::Mode; use super::*; @@ -81,14 +79,14 @@ mod tests { let exercises = [ Exercise { name: "1", - path: Path::new("exercises/1.rs"), + path: "exercises/1.rs", mode: Mode::Run, hint: String::new(), done: true, }, Exercise { name: "2", - path: Path::new("exercises/2.rs"), + path: "exercises/2.rs", mode: Mode::Test, hint: String::new(), done: false, diff --git a/src/embedded.rs b/src/embedded.rs index 1e2d677..866b12b 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -91,7 +91,12 @@ impl EmbeddedFiles { Ok(()) } - pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> { + pub fn write_exercise_to_disk

(&self, path: P, strategy: WriteStrategy) -> io::Result<()> + where + P: AsRef, + { + let path = path.as_ref(); + if let Some(file) = self .exercises_dir .files diff --git a/src/exercise.rs b/src/exercise.rs index c5ece5f..2ec8d97 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; +use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - path::Path, + fs, process::{Command, Output}, }; @@ -10,11 +11,32 @@ use crate::{ info_file::Mode, }; +pub struct TerminalFileLink<'a> { + path: &'a str, +} + +impl<'a> Display for TerminalFileLink<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if let Ok(Some(canonical_path)) = fs::canonicalize(self.path) + .as_deref() + .map(|path| path.to_str()) + { + write!( + f, + "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", + canonical_path, self.path, + ) + } else { + write!(f, "{}", self.path,) + } + } +} + pub struct Exercise { // Exercise's unique name pub name: &'static str, // Exercise's path - pub path: &'static Path, + pub path: &'static str, // The mode of the exercise pub mode: Mode, // The hint text associated with the exercise @@ -60,10 +82,16 @@ impl Exercise { .write_exercise_to_disk(self.path, WriteStrategy::Overwrite) .with_context(|| format!("Failed to reset the exercise {self}")) } + + pub fn terminal_link(&self) -> StyledContent> { + style(TerminalFileLink { path: self.path }) + .underlined() + .blue() + } } impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(&self.path.display(), f) + self.path.fmt(f) } } diff --git a/src/info_file.rs b/src/info_file.rs index dc97b92..2a45e02 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; -use std::{fs, path::PathBuf}; +use std::fs; // The mode of the exercise. #[derive(Deserialize, Copy, Clone)] @@ -28,14 +28,12 @@ pub struct ExerciseInfo { } impl ExerciseInfo { - pub fn path(&self) -> PathBuf { - let path = if let Some(dir) = &self.dir { + pub fn path(&self) -> String { + if let Some(dir) = &self.dir { format!("exercises/{dir}/{}.rs", self.name) } else { format!("exercises/{}.rs", self.name) - }; - - PathBuf::from(path) + } } } diff --git a/src/list/state.rs b/src/list/state.rs index 38391a4..2a1fef1 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -63,7 +63,7 @@ impl<'a> UiState<'a> { next, exercise_state, Span::raw(exercise.name), - Span::raw(exercise.path.to_string_lossy()), + Span::raw(exercise.path), ])) }); diff --git a/src/run.rs b/src/run.rs index 9c504b5..863b584 100644 --- a/src/run.rs +++ b/src/run.rs @@ -17,18 +17,24 @@ pub fn run(app_state: &mut AppState) -> Result<()> { if !output.status.success() { app_state.set_pending(app_state.current_exercise_ind())?; - bail!("Ran {} with errors", app_state.current_exercise()); + bail!( + "Ran {} with errors", + app_state.current_exercise().terminal_link(), + ); } stdout.write_fmt(format_args!( "{}{}\n", "βœ“ Successfully ran ".green(), - exercise.path.to_string_lossy().green(), + exercise.path.green(), ))?; match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()), + ExercisesProgress::Pending => println!( + "Next exercise: {}", + app_state.current_exercise().terminal_link(), + ), } Ok(()) diff --git a/src/watch.rs b/src/watch.rs index 58e829f..bab64ae 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -42,7 +42,7 @@ pub enum WatchExit { pub fn watch( app_state: &mut AppState, - exercise_paths: &'static [&'static Path], + exercise_paths: &'static [&'static str], ) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 0c8d669..fb9a8c0 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,11 +1,11 @@ use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; -use std::{path::Path, sync::mpsc::Sender}; +use std::sync::mpsc::Sender; use super::WatchEvent; pub struct DebounceEventHandler { pub tx: Sender, - pub exercise_paths: &'static [&'static Path], + pub exercise_paths: &'static [&'static str], } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { diff --git a/src/watch/state.rs b/src/watch/state.rs index 6a97637..1a79573 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -136,11 +136,7 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise )?; self.writer.write_fmt(format_args!( "{progress_bar}Current exercise: {}\n", - self.app_state - .current_exercise() - .path - .to_string_lossy() - .bold(), + self.app_state.current_exercise().terminal_link(), ))?; self.show_prompt()?; -- cgit v1.2.3 From 1cbabc3d28a29a01caeffba969ed640e00e5f0be Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 17:10:53 +0200 Subject: Add the manual-run option --- src/main.rs | 28 ++++++++++++++++-------- src/watch.rs | 53 ++++++++++++++++++++++++++++++++------------- src/watch/state.rs | 8 ++++++- src/watch/terminal_event.rs | 4 +++- 4 files changed, 67 insertions(+), 26 deletions(-) (limited to 'src/watch.rs') diff --git a/src/main.rs b/src/main.rs index 6796921..28a426b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,10 @@ use self::{ struct Args { #[command(subcommand)] command: Option, + /// Manually run the current exercise using `r` or `run` in the watch mode. + /// Only use this if Rustlings fails to detect exercise file changes. + #[arg(long)] + manual_run: bool, } #[derive(Subcommand)] @@ -101,17 +105,23 @@ fn main() -> Result<()> { match args.command { None => { - // For the the notify event handler thread. - // Leaking is not a problem because the slice lives until the end of the program. - let exercise_paths = app_state - .exercises() - .iter() - .map(|exercise| exercise.path) - .collect::>() - .leak(); + let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run { + None + } else { + // For the the notify event handler thread. + // Leaking is not a problem because the slice lives until the end of the program. + Some( + app_state + .exercises() + .iter() + .map(|exercise| exercise.path) + .collect::>() + .leak(), + ) + }; loop { - match watch(&mut app_state, exercise_paths)? { + match watch(&mut app_state, notify_exercise_paths)? { WatchExit::Shutdown => break, // It is much easier to exit the watch mode, launch the list mode and then restart // the watch mode instead of trying to pause the watch threads and correct the diff --git a/src/watch.rs b/src/watch.rs index bab64ae..d20e552 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -42,25 +42,38 @@ pub enum WatchExit { pub fn watch( app_state: &mut AppState, - exercise_paths: &'static [&'static str], + notify_exercise_paths: Option<&'static [&'static str]>, ) -> Result { let (tx, rx) = channel(); - let mut debouncer = new_debouncer( - Duration::from_secs(1), - DebounceEventHandler { - tx: tx.clone(), - exercise_paths, - }, - )?; - debouncer - .watcher() - .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - - let mut watch_state = WatchState::new(app_state); + + let mut manual_run = false; + // Prevent dropping the guard until the end of the function. + // Otherwise, the file watcher exits. + let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths { + let mut debouncer = new_debouncer( + Duration::from_secs(1), + DebounceEventHandler { + tx: tx.clone(), + exercise_paths, + }, + ) + .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; + debouncer + .watcher() + .watch(Path::new("exercises"), RecursiveMode::Recursive) + .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; + + Some(debouncer) + } else { + manual_run = true; + None + }; + + let mut watch_state = WatchState::new(app_state, manual_run); watch_state.run_current_exercise()?; - thread::spawn(move || terminal_event_handler(tx)); + thread::spawn(move || terminal_event_handler(tx, manual_run)); while let Ok(event) = rx.recv() { match event { @@ -78,6 +91,7 @@ pub fn watch( watch_state.into_writer().write_all(QUIT_MSG)?; break; } + WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { watch_state.handle_invalid_cmd(&cmd)?; } @@ -88,7 +102,8 @@ pub fn watch( watch_state.render()?; } WatchEvent::NotifyErr(e) => { - return Err(Error::from(e).context("Exercise file watcher failed")); + watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?; + return Err(Error::from(e)); } WatchEvent::TerminalEventErr(e) => { return Err(Error::from(e).context("Terminal event listener failed")); @@ -103,3 +118,11 @@ const QUIT_MSG: &[u8] = b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. "; + +const NOTIFY_ERR: &str = " +The automatic detection of exercise file changes failed :( +Please try running `rustlings` again. + +If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher. +You need to manually trigger running the current exercise using `r` or `run` then. +"; diff --git a/src/watch/state.rs b/src/watch/state.rs index 1a79573..c0f6c53 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -18,10 +18,11 @@ pub struct WatchState<'a> { stderr: Option>, show_hint: bool, show_done: bool, + manual_run: bool, } impl<'a> WatchState<'a> { - pub fn new(app_state: &'a mut AppState) -> Self { + pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self { let writer = io::stdout().lock(); Self { @@ -31,6 +32,7 @@ impl<'a> WatchState<'a> { stderr: None, show_hint: false, show_done: false, + manual_run, } } @@ -78,6 +80,10 @@ impl<'a> WatchState<'a> { fn show_prompt(&mut self) -> io::Result<()> { self.writer.write_all(b"\n")?; + if self.manual_run { + self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?; + } + if self.show_done { self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 7f7ebe0..6d790b7 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -4,6 +4,7 @@ use std::sync::mpsc::Sender; use super::WatchEvent; pub enum InputEvent { + Run, Next, Hint, List, @@ -11,7 +12,7 @@ pub enum InputEvent { Unrecognized(String), } -pub fn terminal_event_handler(tx: Sender) { +pub fn terminal_event_handler(tx: Sender, manual_run: bool) { let mut input = String::with_capacity(8); let last_input_event = loop { @@ -43,6 +44,7 @@ pub fn terminal_event_handler(tx: Sender) { "h" | "hint" => InputEvent::Hint, "l" | "list" => break InputEvent::List, "q" | "quit" => break InputEvent::Quit, + "r" | "run" if manual_run => InputEvent::Run, _ => InputEvent::Unrecognized(input.clone()), }; -- cgit v1.2.3 From 2566f9aaf674458f6faff7e8aaf77bb108b7d56c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 11:31:08 +0200 Subject: Place mods under all imports --- src/list.rs | 4 ++-- src/main.rs | 4 ++-- src/watch.rs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) (limited to 'src/watch.rs') diff --git a/src/list.rs b/src/list.rs index 2bb813d..790c02f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -7,12 +7,12 @@ use crossterm::{ use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; -mod state; - use crate::app_state::AppState; use self::state::{Filter, UiState}; +mod state; + pub fn list(app_state: &mut AppState) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; diff --git a/src/main.rs b/src/main.rs index d40f978..c8940af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ use std::{ process::exit, }; +use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; + mod app_state; mod dev; mod embedded; @@ -22,8 +24,6 @@ mod progress_bar; mod run; mod watch; -use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; - const CURRENT_FORMAT_VERSION: u8 = 1; const DEVELOPING_OFFICIAL_RUSTLINGS: bool = { #[allow(unused_assignments, unused_mut)] diff --git a/src/watch.rs b/src/watch.rs index d20e552..5ebe526 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -11,10 +11,6 @@ use std::{ time::Duration, }; -mod notify_event; -mod state; -mod terminal_event; - use crate::app_state::{AppState, ExercisesProgress}; use self::{ @@ -23,6 +19,10 @@ use self::{ terminal_event::{terminal_event_handler, InputEvent}, }; +mod notify_event; +mod state; +mod terminal_event; + enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, -- cgit v1.2.3 From 428998a4cf74246b0a1da4b8013b504d86cdabeb Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 03:28:44 +0200 Subject: Quicker response to file changes --- src/watch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 5ebe526..1e22f59 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -51,7 +51,7 @@ pub fn watch( // Otherwise, the file watcher exits. let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths { let mut debouncer = new_debouncer( - Duration::from_secs(1), + Duration::from_millis(500), DebounceEventHandler { tx: tx.clone(), exercise_paths, -- cgit v1.2.3 From 14fe248b4bfc3c577be7deacc346a959c7c0cc47 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 14:44:12 +0200 Subject: Optimize the notify event handler --- src/main.rs | 8 ++++---- src/watch.rs | 6 +++--- src/watch/notify_event.rs | 21 +++++++++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) (limited to 'src/watch.rs') diff --git a/src/main.rs b/src/main.rs index a928504..7a142fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -144,23 +144,23 @@ fn main() -> Result<()> { match args.command { None => { - let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run { + let notify_exercise_names = if args.manual_run { None } else { // For the the notify event handler thread. // Leaking is not a problem because the slice lives until the end of the program. Some( - app_state + &*app_state .exercises() .iter() - .map(|exercise| exercise.path) + .map(|exercise| exercise.name.as_bytes()) .collect::>() .leak(), ) }; loop { - match watch::watch(&mut app_state, notify_exercise_paths)? { + 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 diff --git a/src/watch.rs b/src/watch.rs index 1e22f59..2f4409a 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -42,19 +42,19 @@ pub enum WatchExit { pub fn watch( app_state: &mut AppState, - notify_exercise_paths: Option<&'static [&'static str]>, + notify_exercise_names: Option<&'static [&'static [u8]]>, ) -> Result { let (tx, rx) = channel(); let mut manual_run = false; // Prevent dropping the guard until the end of the function. // Otherwise, the file watcher exits. - let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths { + let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let mut debouncer = new_debouncer( Duration::from_millis(500), DebounceEventHandler { tx: tx.clone(), - exercise_paths, + exercise_names, }, ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index fb9a8c0..f66a834 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -5,7 +5,7 @@ use super::WatchEvent; pub struct DebounceEventHandler { pub tx: Sender, - pub exercise_paths: &'static [&'static str], + pub exercise_names: &'static [&'static [u8]], } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { @@ -15,15 +15,24 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { let Some(exercise_ind) = event .iter() .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { + if event.kind != DebouncedEventKind::Any { return None; } - self.exercise_paths + let file_name = event.path.file_name()?.to_str()?.as_bytes(); + + if file_name.len() < 4 { + return None; + } + let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3); + + if ext != b".rs" { + return None; + } + + self.exercise_names .iter() - .position(|path| event.path.ends_with(path)) + .position(|exercise_name| *exercise_name == file_name_without_ext) }) .min() else { -- cgit v1.2.3 From c7c8d9968040d7df438fa33b32d9495415f751d2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:22:11 +0200 Subject: Moar responsive :P --- src/watch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 2f4409a..5c3f170 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -51,7 +51,7 @@ pub fn watch( // Otherwise, the file watcher exits. let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let mut debouncer = new_debouncer( - Duration::from_millis(500), + Duration::from_millis(200), DebounceEventHandler { tx: tx.clone(), exercise_names, -- cgit v1.2.3 From ea40804371d073730e7b5f6258d2a825c544c0b1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:38:26 +0200 Subject: Put long version in () --- src/watch.rs | 2 +- src/watch/state.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 5c3f170..453d9a4 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -124,5 +124,5 @@ The automatic detection of exercise file changes failed :( Please try running `rustlings` again. If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher. -You need to manually trigger running the current exercise using `r` or `run` then. +You need to manually trigger running the current exercise using `r` (or `run`) then. "; diff --git a/src/watch/state.rs b/src/watch/state.rs index 82b745a..e5364c3 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -130,7 +130,7 @@ impl<'a> WatchState<'a> { self.writer, "{}\n", "Exercise done βœ“ -When you are done experimenting, enter `n` or `next` to go to the next exercise πŸ¦€" +When you are done experimenting, enter `n` (or `next`) to move on to the next exercise πŸ¦€" .bold() .green(), )?; -- cgit v1.2.3 From 052573904604896398a6cc7281398fa9fdf8f083 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:20:04 +0200 Subject: Fix invisible input on Windows --- src/watch.rs | 4 ++-- src/watch/state.rs | 13 +++++-------- src/watch/terminal_event.rs | 46 ++++++++++++++++++--------------------------- 3 files changed, 25 insertions(+), 38 deletions(-) (limited to 'src/watch.rs') diff --git a/src/watch.rs b/src/watch.rs index 453d9a4..944d77b 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -92,8 +92,8 @@ pub fn watch( break; } WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, - WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { - watch_state.handle_invalid_cmd(&cmd)?; + WatchEvent::Input(InputEvent::Unrecognized(input)) => { + watch_state.handle_invalid_input(input)?; } WatchEvent::FileChange { exercise_ind } => { watch_state.run_exercise_with_ind(exercise_ind)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 2cf7521..f3ffac8 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -166,14 +166,11 @@ When you are done experimenting, enter `n` (or `next`) to move on to the next ex self.render() } - pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> { - self.writer.write_all(b"Invalid command: ")?; - self.writer.write_all(cmd.as_bytes())?; - if cmd.len() > 1 { - self.writer - .write_all(b" (confusing input can occur after resizing the terminal)")?; - } - self.writer.write_all(b"\n")?; + pub fn handle_invalid_input(&mut self, input: char) -> io::Result<()> { + writeln!( + self.writer, + "Invalid input: {input} (confusing input can occur after resizing the terminal)", + )?; self.show_prompt() } } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 6d790b7..846bec1 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -9,12 +9,10 @@ pub enum InputEvent { Hint, List, Quit, - Unrecognized(String), + Unrecognized(char), } pub fn terminal_event_handler(tx: Sender, manual_run: bool) { - let mut input = String::with_capacity(8); - let last_input_event = loop { let terminal_event = match event::read() { Ok(v) => v, @@ -28,36 +26,28 @@ pub fn terminal_event_handler(tx: Sender, manual_run: bool) { match terminal_event { Event::Key(key) => { - if key.modifiers != KeyModifiers::NONE { - continue; - } - match key.kind { - KeyEventKind::Release => continue, - KeyEventKind::Press | KeyEventKind::Repeat => (), + KeyEventKind::Release | KeyEventKind::Repeat => continue, + KeyEventKind::Press => (), } - match key.code { - KeyCode::Enter => { - let input_event = match input.trim() { - "n" | "next" => InputEvent::Next, - "h" | "hint" => InputEvent::Hint, - "l" | "list" => break InputEvent::List, - "q" | "quit" => break InputEvent::Quit, - "r" | "run" if manual_run => InputEvent::Run, - _ => InputEvent::Unrecognized(input.clone()), - }; - - if tx.send(WatchEvent::Input(input_event)).is_err() { - return; - } + if key.modifiers != KeyModifiers::NONE { + continue; + } - input.clear(); - } - KeyCode::Char(c) => { - input.push(c); + if let KeyCode::Char(c) = key.code { + 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, + _ => InputEvent::Unrecognized(c), + }; + + if tx.send(WatchEvent::Input(input_event)).is_err() { + return; } - _ => (), } } Event::Resize(_, _) => { -- cgit v1.2.3 From f9e35a4344cd7d51923f1983cf824fb36be92d50 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:32:25 +0200 Subject: Improve input handling --- src/app_state.rs | 10 +++++++--- src/run.rs | 6 +++++- src/watch.rs | 7 +++---- src/watch/state.rs | 13 +------------ src/watch/terminal_event.rs | 4 ++-- 5 files changed, 18 insertions(+), 22 deletions(-) (limited to 'src/watch.rs') diff --git a/src/app_state.rs b/src/app_state.rs index 492be34..85639e5 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -21,8 +21,12 @@ const BAD_INDEX_ERR: &str = "The current exercise index is higher than the numbe #[must_use] pub enum ExercisesProgress { + // All exercises are done. AllDone, - Pending, + // The current exercise failed and is still pending. + CurrentPending, + // A new exercise is now pending. + NewPending, } pub enum StateFileStatus { @@ -343,7 +347,7 @@ impl AppState { if let Some(ind) = self.next_pending_exercise_ind() { self.set_current_exercise_ind(ind)?; - return Ok(ExercisesProgress::Pending); + return Ok(ExercisesProgress::NewPending); } writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; @@ -366,7 +370,7 @@ impl AppState { self.write()?; - return Ok(ExercisesProgress::Pending); + return Ok(ExercisesProgress::NewPending); } writeln!(writer, "{}", "ok".green())?; diff --git a/src/run.rs b/src/run.rs index 9b5ddd3..ac97414 100644 --- a/src/run.rs +++ b/src/run.rs @@ -41,7 +41,11 @@ pub fn run(app_state: &mut AppState) -> Result<()> { match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::Pending => println!( + ExercisesProgress::CurrentPending => println!( + "Current exercise: {}", + app_state.current_exercise().terminal_link(), + ), + ExercisesProgress::NewPending => println!( "Next exercise: {}", app_state.current_exercise().terminal_link(), ), diff --git a/src/watch.rs b/src/watch.rs index 944d77b..7d4f54b 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -79,7 +79,8 @@ pub fn watch( match event { WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? { ExercisesProgress::AllDone => break, - ExercisesProgress::Pending => watch_state.run_current_exercise()?, + ExercisesProgress::CurrentPending => watch_state.render()?, + ExercisesProgress::NewPending => watch_state.run_current_exercise()?, }, WatchEvent::Input(InputEvent::Hint) => { watch_state.show_hint()?; @@ -92,9 +93,7 @@ pub fn watch( break; } WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, - WatchEvent::Input(InputEvent::Unrecognized(input)) => { - watch_state.handle_invalid_input(input)?; - } + WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?, WatchEvent::FileChange { exercise_ind } => { watch_state.run_exercise_with_ind(exercise_ind)?; } diff --git a/src/watch/state.rs b/src/watch/state.rs index f3ffac8..2e98546 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -78,10 +78,7 @@ impl<'a> WatchState<'a> { pub fn next_exercise(&mut self) -> Result { if matches!(self.done_status, DoneStatus::Pending) { - self.writer - .write_all(b"The current exercise isn't done yet\n")?; - self.show_prompt()?; - return Ok(ExercisesProgress::Pending); + return Ok(ExercisesProgress::CurrentPending); } self.app_state.done_current_exercise(&mut self.writer) @@ -165,12 +162,4 @@ When you are done experimenting, enter `n` (or `next`) to move on to the next ex self.show_hint = true; self.render() } - - pub fn handle_invalid_input(&mut self, input: char) -> io::Result<()> { - writeln!( - self.writer, - "Invalid input: {input} (confusing input can occur after resizing the terminal)", - )?; - self.show_prompt() - } } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 846bec1..29a672a 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -9,7 +9,7 @@ pub enum InputEvent { Hint, List, Quit, - Unrecognized(char), + Unrecognized, } pub fn terminal_event_handler(tx: Sender, manual_run: bool) { @@ -42,7 +42,7 @@ pub fn terminal_event_handler(tx: Sender, manual_run: bool) { 'l' => break InputEvent::List, 'q' => break InputEvent::Quit, 'r' if manual_run => InputEvent::Run, - _ => InputEvent::Unrecognized(c), + _ => InputEvent::Unrecognized, }; if tx.send(WatchEvent::Input(input_event)).is_err() { -- cgit v1.2.3 From d2b5906be226f936481ff3a5cb8fccde5c721524 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:37:32 +0200 Subject: No more word input --- README.md | 4 ++-- exercises/00_intro/intro1.rs | 2 +- rustlings-macros/info.toml | 4 ++-- src/main.rs | 2 +- src/watch.rs | 2 +- src/watch/state.rs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) (limited to 'src/watch.rs') diff --git a/README.md b/README.md index 3ba080f..0180608 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ It will rerun the current exercise automatically every time you change the exerc

If detecting file changes in the exercises/ directory fails… (click to expand) -> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` (or `run`) in the watch mode. +> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode. > > Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL). @@ -106,7 +106,7 @@ It will rerun the current exercise automatically every time you change the exerc ### Exercise List -In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` (or `list`) to open the interactive exercise list. +In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list. The list allows you to… diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 62bf95f..bdbf34b 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,6 +1,6 @@ // We sometimes encourage you to keep trying things on a given exercise, even // after you already figured it out. If you got everything working and feel -// ready for the next exercise, enter `n` (or `next`) in the terminal. +// ready for the next exercise, enter `n` in the terminal. // // The exercise file will be reloaded when you change one of the lines below! // Try adding a new `println!`. diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 4204f27..485665e 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -13,7 +13,7 @@ get started, here are some notes about how Rustlings operates: the exercise file in your editor, fix errors and save the file. Rustlings will automatically detect the file change and rerun the exercise. If all errors are fixed, Rustlings will ask you to move on to the next exercise. -3. If you're stuck on an exercise, enter `h` (or `hint`) to show a hint. +3. If you're stuck on an exercise, enter `h` to show a hint. 4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, other learners do too so you can help each other out! @@ -35,7 +35,7 @@ name = "intro1" dir = "00_intro" test = false # TODO: Fix hint -hint = """Enter `n` (or `next`) followed by ENTER to move on to the next exercise""" +hint = """Enter `n` to move on to the next exercise. You might need to press ENTER after typing `n`.""" [[exercises]] name = "intro2" diff --git a/src/main.rs b/src/main.rs index 15bcc8e..cf6f0d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,7 @@ fn press_enter_prompt() -> io::Result<()> { struct Args { #[command(subcommand)] command: Option, - /// Manually run the current exercise using `r` or `run` in the watch mode. + /// Manually run the current exercise using `r` in the watch mode. /// Only use this if Rustlings fails to detect exercise file changes. #[arg(long)] manual_run: bool, diff --git a/src/watch.rs b/src/watch.rs index 7d4f54b..f72ebf7 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -123,5 +123,5 @@ The automatic detection of exercise file changes failed :( Please try running `rustlings` again. If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher. -You need to manually trigger running the current exercise using `r` (or `run`) then. +You need to manually trigger running the current exercise using `r` then. "; diff --git a/src/watch/state.rs b/src/watch/state.rs index 2e98546..c21d7ca 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -127,7 +127,7 @@ impl<'a> WatchState<'a> { self.writer, "{}\n", "Exercise done βœ“ -When you are done experimenting, enter `n` (or `next`) to move on to the next exercise πŸ¦€" +When you are done experimenting, enter `n` to move on to the next exercise πŸ¦€" .bold() .green(), )?; -- cgit v1.2.3 From 4ae3fcc3caf91d4b22680ed4497c8ee05296eaad Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 17:06:11 +0200 Subject: Don't skip exercises on file changes --- src/app_state.rs | 4 ++++ src/watch.rs | 2 +- src/watch/state.rs | 9 ++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) (limited to 'src/watch.rs') diff --git a/src/app_state.rs b/src/app_state.rs index 85639e5..75014ce 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -198,6 +198,10 @@ impl AppState { } pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> { + if exercise_ind == self.current_exercise_ind { + return Ok(()); + } + if exercise_ind >= self.exercises.len() { bail!(BAD_INDEX_ERR); } diff --git a/src/watch.rs b/src/watch.rs index f72ebf7..2fbc533 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -95,7 +95,7 @@ pub fn watch( WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?, WatchEvent::FileChange { exercise_ind } => { - watch_state.run_exercise_with_ind(exercise_ind)?; + watch_state.handle_file_change(exercise_ind)?; } WatchEvent::TerminalResize => { watch_state.render()?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 74cf182..60b6d5a 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -72,7 +72,14 @@ impl<'a> WatchState<'a> { self.render() } - pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> { + pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> { + // Don't skip exercises on file changes to avoid confusion from missing exercises. + // Skipping exercises must be explicit in the interactive list. + // But going back to an earlier exercise on file change is fine. + if self.app_state.current_exercise_ind() < exercise_ind { + return Ok(()); + } + self.app_state.set_current_exercise_ind(exercise_ind)?; self.run_current_exercise() } -- cgit v1.2.3 From c8481d35c120ff99213e6ed73ba889e51cac10c5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 14 May 2024 01:49:22 +0200 Subject: Done documentation --- src/progress_bar.rs | 3 +++ src/run.rs | 6 +----- src/watch.rs | 5 +++-- src/watch/notify_event.rs | 5 +++-- src/watch/state.rs | 9 +++++---- 5 files changed, 15 insertions(+), 13 deletions(-) (limited to 'src/watch.rs') diff --git a/src/progress_bar.rs b/src/progress_bar.rs index d6962b8..4a54170 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -12,6 +12,7 @@ const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; const PROGRESS_EXCEEDS_MAX_ERR: &str = "The progress of the progress bar is higher than the maximum"; +/// Terminal progress bar to be used when not using Ratataui. pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result { use crossterm::style::Stylize; @@ -54,6 +55,8 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result Result> { use ratatui::style::Stylize; diff --git a/src/run.rs b/src/run.rs index ac97414..36899b9 100644 --- a/src/run.rs +++ b/src/run.rs @@ -41,11 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::CurrentPending => println!( - "Current exercise: {}", - app_state.current_exercise().terminal_link(), - ), - ExercisesProgress::NewPending => println!( + ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!( "Next exercise: {}", app_state.current_exercise().terminal_link(), ), diff --git a/src/watch.rs b/src/watch.rs index 2fbc533..88a1230 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -14,7 +14,7 @@ use std::{ use crate::app_state::{AppState, ExercisesProgress}; use self::{ - notify_event::DebounceEventHandler, + notify_event::NotifyEventHandler, state::WatchState, terminal_event::{terminal_event_handler, InputEvent}, }; @@ -40,6 +40,7 @@ pub enum WatchExit { List, } +/// `notify_exercise_names` as None activates the manual run mode. pub fn watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, @@ -52,7 +53,7 @@ pub fn watch( let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let mut debouncer = new_debouncer( Duration::from_millis(200), - DebounceEventHandler { + NotifyEventHandler { tx: tx.clone(), exercise_names, }, diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index a224377..7471640 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -3,12 +3,13 @@ use std::sync::mpsc::Sender; use super::WatchEvent; -pub struct DebounceEventHandler { +pub struct NotifyEventHandler { pub tx: Sender, + /// Used to report which exercise was modified. pub exercise_names: &'static [&'static [u8]], } -impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { +impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { fn handle_event(&mut self, input_event: DebounceEventResult) { let output_event = match input_event { Ok(input_event) => { diff --git a/src/watch/state.rs b/src/watch/state.rs index abd21fb..14c3f01 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,7 +1,7 @@ use anyhow::Result; use crossterm::{ style::{style, Stylize}, - terminal::size, + terminal, }; use std::io::{self, StdoutLock, Write}; @@ -84,6 +84,7 @@ impl<'a> WatchState<'a> { self.run_current_exercise() } + /// Move on to the next exercise if the current one is done. pub fn next_exercise(&mut self) -> Result { if self.done_status == DoneStatus::Pending { return Ok(ExercisesProgress::CurrentPending); @@ -113,7 +114,7 @@ impl<'a> WatchState<'a> { } pub fn render(&mut self) -> Result<()> { - // Prevent having the first line shifted. + // Prevent having the first line shifted if clearing wasn't successful. self.writer.write_all(b"\n")?; clear_terminal(&mut self.writer)?; @@ -145,11 +146,11 @@ When you are done experimenting, enter `n` to move on to the next exercise πŸ¦€" writeln!( self.writer, "A solution file can be found at {}\n", - style(TerminalFileLink(solution_path)).underlined().green() + style(TerminalFileLink(solution_path)).underlined().green(), )?; } - let line_width = size()?.0; + let line_width = terminal::size()?.0; let progress_bar = progress_bar( self.app_state.n_done(), self.app_state.exercises().len() as u16, -- cgit v1.2.3