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/state.rs | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/watch/state.rs (limited to 'src/watch/state.rs') 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/state.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 787bec9875ec3e76d5870808cc7299da1d26dea6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 21:16:27 +0200 Subject: Use exercises as leaked --- src/list.rs | 2 +- src/list/state.rs | 10 +++++----- src/main.rs | 16 ++++++++-------- src/verify.rs | 9 ++++++--- src/watch/state.rs | 2 +- 5 files changed, 21 insertions(+), 18 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/list.rs b/src/list.rs index db83ea4..c92b369 100644 --- a/src/list.rs +++ b/src/list.rs @@ -13,7 +13,7 @@ use crate::{exercise::Exercise, state_file::StateFile}; use self::state::{Filter, UiState}; -pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { +pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; enable_raw_mode()?; diff --git a/src/list/state.rs b/src/list/state.rs index 7bfc163..b67c624 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -16,18 +16,18 @@ pub enum Filter { None, } -pub struct UiState<'a> { - pub table: Table<'a>, +pub struct UiState { + pub table: Table<'static>, pub message: String, pub filter: Filter, - exercises: &'a [Exercise], + exercises: &'static [Exercise], progress: u16, selected: usize, table_state: TableState, last_ind: usize, } -impl<'a> UiState<'a> { +impl UiState { pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { let mut rows_counter: usize = 0; let mut progress: u16 = 0; @@ -79,7 +79,7 @@ impl<'a> UiState<'a> { self } - pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self { + pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { let header = Row::new(["Next", "State", "Name", "Path"]); let max_name_len = exercises diff --git a/src/main.rs b/src/main.rs index 6af66bd..62bfd98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,7 @@ enum Subcommands { List, } -fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<(usize, &'a Exercise)> { +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()? { @@ -89,7 +89,7 @@ Try running `cargo --version` to diagnose the problem.", let exercises = InfoFile::parse()?.exercises.leak(); if matches!(args.command, Some(Subcommands::Init)) { - init::init(&exercises).context("Initialization failed")?; + init::init(exercises).context("Initialization failed")?; println!( "\nDone initialization!\n Run `cd rustlings` to go into the generated directory. @@ -107,7 +107,7 @@ 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 state_file = StateFile::read_or_default(exercises); match args.command { None | Some(Subcommands::Watch) => { @@ -116,23 +116,23 @@ 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(&mut state_file, &exercises)?; + list::list(&mut state_file, exercises)?; } Some(Subcommands::Run { name }) => { - let (_, exercise) = find_exercise(&name, &exercises)?; + let (_, exercise) = find_exercise(&name, exercises)?; run(exercise).unwrap_or_else(|_| exit(1)); } Some(Subcommands::Reset { name }) => { - let (ind, exercise) = find_exercise(&name, &exercises)?; + let (ind, exercise) = find_exercise(&name, exercises)?; exercise.reset()?; state_file.reset(ind)?; println!("The exercise {exercise} has been reset!"); } Some(Subcommands::Hint { name }) => { - let (_, exercise) = find_exercise(&name, &exercises)?; + let (_, exercise) = find_exercise(&name, exercises)?; println!("{}", exercise.hint); } - Some(Subcommands::Verify) => match verify(&exercises, 0)? { + Some(Subcommands::Verify) => match verify(exercises, 0)? { VerifyState::AllExercisesDone => println!("All exercises done!"), VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), }, diff --git a/src/verify.rs b/src/verify.rs index c4368cc..cea6bdf 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -4,9 +4,9 @@ use std::io::{stdout, Write}; use crate::exercise::{Exercise, Mode, State}; -pub enum VerifyState<'a> { +pub enum VerifyState { AllExercisesDone, - Failed(&'a Exercise), + Failed(&'static Exercise), } // Verify that the provided container of Exercise objects @@ -14,7 +14,10 @@ pub enum VerifyState<'a> { // 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: &[Exercise], mut current_exercise_ind: usize) -> Result> { +pub fn verify( + exercises: &'static [Exercise], + mut current_exercise_ind: usize, +) -> Result { while current_exercise_ind < exercises.len() { let exercise = &exercises[current_exercise_ind]; diff --git a/src/watch/state.rs b/src/watch/state.rs index f614ae0..d8fed5b 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -18,7 +18,7 @@ use crate::{ pub struct WatchState<'a> { writer: StdoutLock<'a>, exercises: &'static [Exercise], - exercise: &'a Exercise, + exercise: &'static Exercise, current_exercise_ind: usize, stdout: Option>, stderr: Option>, -- cgit v1.2.3 From af85f2036cd545013225da04e67257fe4f6a4179 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 22:06:55 +0200 Subject: Print a newline before the progress bar --- src/watch/state.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'src/watch/state.rs') diff --git a/src/watch/state.rs b/src/watch/state.rs index d8fed5b..8fae7e8 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -136,6 +136,7 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.write_all(message.as_bytes())?; } + self.writer.write_all(b"\n")?; let line_width = size()?.0; let progress_bar = progress_bar( self.current_exercise_ind as u16, -- cgit v1.2.3 From c8d217ad50a7117fe35735b4083f2aa1e2b47d97 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 22:20:12 +0200 Subject: Fix showing stdout and stderr --- src/watch/state.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/watch/state.rs b/src/watch/state.rs index 8fae7e8..24978bb 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -60,13 +60,15 @@ impl<'a> WatchState<'a> { pub fn run_exercise(&mut self) -> Result { let output = self.exercise.run()?; + self.stdout = Some(output.stdout); if !output.status.success() { - self.stdout = Some(output.stdout); self.stderr = Some(output.stderr); return Ok(false); } + self.stderr = None; + if let State::Pending(context) = self.exercise.state()? { let mut message = format!( " @@ -98,7 +100,6 @@ You can keep working on this exercise or jump into the next one by removing the )?; } - self.stdout = Some(output.stdout); self.message = Some(message); return Ok(false); } -- cgit v1.2.3 From 533a009257adba0714292d326f57671f77cffbd3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 00:51:41 +0200 Subject: Show the progress in the progress bar, not the current exercise index --- src/watch/state.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/watch/state.rs b/src/watch/state.rs index 24978bb..4db9440 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -20,6 +20,7 @@ pub struct WatchState<'a> { exercises: &'static [Exercise], exercise: &'static Exercise, current_exercise_ind: usize, + progress: u16, stdout: Option>, stderr: Option>, message: Option, @@ -29,6 +30,7 @@ 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]; let writer = io::stdout().lock(); @@ -46,6 +48,7 @@ impl<'a> WatchState<'a> { exercises, exercise, current_exercise_ind, + progress, stdout: None, stderr: None, message: None, @@ -139,11 +142,7 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.write_all(b"\n")?; let line_width = size()?.0; - let progress_bar = progress_bar( - self.current_exercise_ind as u16, - self.exercises.len() as u16, - line_width, - )?; + let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?; self.writer.write_all(progress_bar.as_bytes())?; self.show_prompt()?; -- 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/state.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 a46d66134b26095e553f284c02de9a895e15f180 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 03:56:41 +0200 Subject: Fix shift of first output line --- src/watch/state.rs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/watch/state.rs') diff --git a/src/watch/state.rs b/src/watch/state.rs index 393ea02..08707a4 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -127,6 +127,9 @@ You can keep working on this exercise or jump into the next one by removing the } pub fn render(&mut self) -> Result<()> { + // Prevent having the first line shifted after clearing because of the prompt. + self.writer.write_all(b"\n")?; + self.writer.execute(Clear(ClearType::All))?; if let Some(stdout) = &self.stdout { -- 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/state.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 a59acf88354c8dfba301e59173653bc9a5f4bfb2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 14:29:31 +0200 Subject: Show the current exercise path --- src/progress_bar.rs | 2 +- src/watch/state.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src/watch/state.rs') diff --git a/src/progress_bar.rs b/src/progress_bar.rs index 97c8ad9..d6962b8 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -49,7 +49,7 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result3}/{total} exercises").unwrap(); + writeln!(line, "] {progress:>3}/{total} exercises").unwrap(); Ok(line) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 751285f..da5ac3d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -149,6 +149,12 @@ You can keep working on this exercise or jump into the next one by removing the let progress_bar = progress_bar(self.progress, self.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.show_prompt()?; Ok(()) -- 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/state.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 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/state.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 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/state.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/state.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/state.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 44824718b2155268c79d1ce216abc770df94d05d Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 18:58:01 +0200 Subject: Remove unused import --- src/watch/state.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/watch/state.rs b/src/watch/state.rs index 70b6ae4..6a97637 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -4,10 +4,7 @@ use crossterm::{ terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use std::{ - io::{self, StdoutLock, Write}, - process::Output, -}; +use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, -- 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/state.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/state.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 8a085a0a85c759029cd57c28364867bde817e738 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 02:52:30 +0200 Subject: Dump solution and show its path --- src/app_state.rs | 45 +++++++++++++++++++++++++++++++++++---------- src/embedded.rs | 10 ++++------ src/exercise.rs | 28 ++-------------------------- src/main.rs | 1 + src/run.rs | 14 ++++++++++++-- src/terminal_link.rs | 23 +++++++++++++++++++++++ src/watch/state.rs | 35 +++++++++++++++++++++++++++-------- 7 files changed, 104 insertions(+), 52 deletions(-) create mode 100644 src/terminal_link.rs (limited to 'src/watch/state.rs') diff --git a/src/app_state.rs b/src/app_state.rs index 6f393bc..33d3de2 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -257,21 +257,46 @@ impl AppState { } } - pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { - let exercise = &mut self.exercises[self.current_exercise_ind]; - if !exercise.done { - exercise.done = true; - self.n_done += 1; + pub fn current_solution_path(&self) -> Result> { + if DEBUG_PROFILE { + return Ok(None); } - if self.official_exercises && !DEBUG_PROFILE { + let current_exercise = self.current_exercise(); + + if self.official_exercises { + let dir_name = current_exercise + .dir + .context("Official exercises must be nested in the `exercises` directory")?; + let solution_path = format!("solutions/{dir_name}/{}.rs", current_exercise.name); + EMBEDDED_FILES.write_solution_to_disk( self.current_exercise_ind, - exercise - .dir - .context("Official exercises must be nested in the `exercises` directory")?, - exercise.name, + dir_name, + &solution_path, )?; + + Ok(Some(solution_path)) + } else { + let solution_path = if let Some(dir) = current_exercise.dir { + format!("solutions/{dir}/{}.rs", current_exercise.name) + } else { + format!("solutions/{}.rs", current_exercise.name) + }; + + if Path::new(&solution_path).exists() { + return Ok(Some(solution_path)); + } + + Ok(None) + } + } + + pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { + let exercise = &mut self.exercises[self.current_exercise_ind]; + if !exercise.done { + exercise.done = true; + self.n_done += 1; } let Some(ind) = self.next_pending_exercise_ind() else { diff --git a/src/embedded.rs b/src/embedded.rs index 756b414..d7952a1 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -113,14 +113,12 @@ impl EmbeddedFiles { &self, exercise_ind: usize, dir_name: &str, - exercise_name: &str, + path: &str, ) -> Result<()> { let dir_path = format!("solutions/{dir_name}"); - create_dir_all(&dir_path).context("Failed to create the directory {dir_path}")?; + create_dir_all(&dir_path) + .with_context(|| format!("Failed to create the directory {dir_path}"))?; - WriteStrategy::Overwrite.write( - &format!("{dir_path}/{exercise_name}.rs"), - self.exercise_files[exercise_ind].solution, - ) + WriteStrategy::Overwrite.write(path, self.exercise_files[exercise_ind].solution) } } diff --git a/src/exercise.rs b/src/exercise.rs index 4493a57..45ac208 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -2,33 +2,11 @@ use anyhow::{Context, Result}; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - fs, path::Path, process::{Command, Output}, }; -use crate::{info_file::Mode, DEBUG_PROFILE}; - -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,) - } - } -} +use crate::{info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; pub struct Exercise { pub dir: Option<&'static str>, @@ -85,9 +63,7 @@ impl Exercise { } pub fn terminal_link(&self) -> StyledContent> { - style(TerminalFileLink { path: self.path }) - .underlined() - .blue() + style(TerminalFileLink(self.path)).underlined().blue() } } diff --git a/src/main.rs b/src/main.rs index 9ff218a..790fff6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod init; mod list; mod progress_bar; mod run; +mod terminal_link; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; diff --git a/src/run.rs b/src/run.rs index 863b584..a2b6972 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,8 +1,11 @@ use anyhow::{bail, Result}; -use crossterm::style::Stylize; +use crossterm::style::{style, Stylize}; use std::io::{self, Write}; -use crate::app_state::{AppState, ExercisesProgress}; +use crate::{ + app_state::{AppState, ExercisesProgress}, + terminal_link::TerminalFileLink, +}; pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); @@ -29,6 +32,13 @@ pub fn run(app_state: &mut AppState) -> Result<()> { exercise.path.green(), ))?; + if let Some(solution_path) = app_state.current_solution_path()? { + println!( + "\nA solution file can be found at {}\n", + style(TerminalFileLink(&solution_path)).underlined().green(), + ); + } + match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), ExercisesProgress::Pending => println!( diff --git a/src/terminal_link.rs b/src/terminal_link.rs new file mode 100644 index 0000000..c9e6bce --- /dev/null +++ b/src/terminal_link.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{self, Display, Formatter}, + fs, +}; + +pub struct TerminalFileLink<'a>(pub &'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.0) + .as_deref() + .map(|path| path.to_str()) + { + write!( + f, + "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", + canonical_path, self.0, + ) + } else { + write!(f, "{}", self.0) + } + } +} diff --git a/src/watch/state.rs b/src/watch/state.rs index c0f6c53..5f4abf3 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,6 +1,6 @@ use anyhow::Result; use crossterm::{ - style::Stylize, + style::{style, Stylize}, terminal::{size, Clear, ClearType}, ExecutableCommand, }; @@ -9,15 +9,22 @@ use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, progress_bar::progress_bar, + terminal_link::TerminalFileLink, }; +enum DoneStatus { + DoneWithSolution(String), + DoneWithoutSolution, + Pending, +} + pub struct WatchState<'a> { writer: StdoutLock<'a>, app_state: &'a mut AppState, stdout: Option>, stderr: Option>, show_hint: bool, - show_done: bool, + done_status: DoneStatus, manual_run: bool, } @@ -31,7 +38,7 @@ impl<'a> WatchState<'a> { stdout: None, stderr: None, show_hint: false, - show_done: false, + done_status: DoneStatus::Pending, manual_run, } } @@ -49,13 +56,18 @@ impl<'a> WatchState<'a> { if output.status.success() { self.stderr = None; - self.show_done = true; + self.done_status = + if let Some(solution_path) = self.app_state.current_solution_path()? { + DoneStatus::DoneWithSolution(solution_path) + } else { + DoneStatus::DoneWithoutSolution + }; } else { self.app_state .set_pending(self.app_state.current_exercise_ind())?; self.stderr = Some(output.stderr); - self.show_done = false; + self.done_status = DoneStatus::Pending; } self.render() @@ -67,7 +79,7 @@ impl<'a> WatchState<'a> { } pub fn next_exercise(&mut self) -> Result { - if !self.show_done { + if matches!(self.done_status, DoneStatus::Pending) { self.writer .write_all(b"The current exercise isn't done yet\n")?; self.show_prompt()?; @@ -84,7 +96,7 @@ impl<'a> WatchState<'a> { self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?; } - if self.show_done { + if !matches!(self.done_status, DoneStatus::Pending) { self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; } @@ -124,7 +136,7 @@ impl<'a> WatchState<'a> { ))?; } - if self.show_done { + if !matches!(self.done_status, DoneStatus::Pending) { self.writer.write_fmt(format_args!( "{}\n\n", "Exercise done ✓ @@ -134,6 +146,13 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise ))?; } + if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { + self.writer.write_fmt(format_args!( + "A solution file can be found at {}\n\n", + style(TerminalFileLink(solution_path)).underlined().green() + ))?; + } + let line_width = size()?.0; let progress_bar = progress_bar( self.app_state.n_done(), -- cgit v1.2.3 From 67fa01774223b08833c21baeb13bdec9e4a298a0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 01:56:01 +0200 Subject: Use os_pipe --- Cargo.lock | 11 ++++++ Cargo.toml | 1 + src/app_state.rs | 13 ++++++- src/exercise.rs | 110 ++++++++++++++++++++++++++++++++++++++++++++++------- src/main.rs | 6 ++- src/run.rs | 11 +++--- src/watch/state.rs | 26 +++---------- 7 files changed, 135 insertions(+), 43 deletions(-) (limited to 'src/watch/state.rs') diff --git a/Cargo.lock b/Cargo.lock index 5f0b597..823fec3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,6 +519,16 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -677,6 +687,7 @@ dependencies = [ "crossterm", "hashbrown", "notify-debouncer-mini", + "os_pipe", "predicates", "ratatui", "rustlings-macros", diff --git a/Cargo.toml b/Cargo.toml index f4615b1..31e7456 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" +os_pipe = "1.1.5" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "6.0.0-beta.0" } serde.workspace = true diff --git a/src/app_state.rs b/src/app_state.rs index 33d3de2..4160f6e 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,7 +11,12 @@ use std::{ process::{Command, Stdio}, }; -use crate::{embedded::EMBEDDED_FILES, exercise::Exercise, info_file::ExerciseInfo, DEBUG_PROFILE}; +use crate::{ + embedded::EMBEDDED_FILES, + exercise::{Exercise, OUTPUT_CAPACITY}, + info_file::ExerciseInfo, + DEBUG_PROFILE, +}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -302,11 +307,13 @@ impl AppState { let Some(ind) = self.next_pending_exercise_ind() else { writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); for (exercise_ind, exercise) in self.exercises().iter().enumerate() { writer.write_fmt(format_args!("Running {exercise} ... "))?; writer.flush()?; - if !exercise.run()?.status.success() { + let success = exercise.run(&mut output)?; + if !success { writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; self.current_exercise_ind = exercise_ind; @@ -322,6 +329,8 @@ impl AppState { } writer.write_fmt(format_args!("{}\n", "ok".green()))?; + + output.clear(); } writer.execute(Clear(ClearType::All))?; diff --git a/src/exercise.rs b/src/exercise.rs index 45ac208..17cc8d7 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -2,11 +2,42 @@ use anyhow::{Context, Result}; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - path::Path, - process::{Command, Output}, + io::{Read, Write}, + process::Command, }; -use crate::{info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; +use crate::{in_official_repo, info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; + +// TODO +pub const OUTPUT_CAPACITY: usize = 1 << 12; + +fn run_command(mut cmd: Command, cmd_description: &str, output: &mut Vec) -> Result { + let (mut reader, writer) = os_pipe::pipe().with_context(|| { + format!("Failed to create a pipe to run the command `{cmd_description}``") + })?; + + let mut handle = cmd + .stdout(writer.try_clone().with_context(|| { + format!("Failed to clone the pipe writer for the command `{cmd_description}`") + })?) + .stderr(writer) + .spawn() + .with_context(|| format!("Failed to run the command `{cmd_description}`"))?; + + // Prevent pipe deadlock. + drop(cmd); + + reader + .read_to_end(output) + .with_context(|| format!("Failed to read the output of the command `{cmd_description}`"))?; + + output.push(b'\n'); + + handle + .wait() + .with_context(|| format!("Failed to wait on the command `{cmd_description}` to exit")) + .map(|status| status.success()) +} pub struct Exercise { pub dir: Option<&'static str>, @@ -22,13 +53,30 @@ pub struct Exercise { } impl Exercise { - fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { + fn run_bin(&self, output: &mut Vec) -> Result { + writeln!(output, "{}", "Output".bold().magenta().underlined())?; + + let bin_path = format!("target/debug/{}", self.name); + run_command(Command::new(&bin_path), &bin_path, output) + } + + fn cargo_cmd( + &self, + command: &str, + args: &[&str], + cmd_description: &str, + output: &mut Vec, + dev: bool, + ) -> Result { let mut cmd = Command::new("cargo"); cmd.arg(command); // A hack to make `cargo run` work when developing Rustlings. - if DEBUG_PROFILE && Path::new("tests").exists() { - cmd.arg("--manifest-path").arg("dev/Cargo.toml"); + if dev { + cmd.arg("--manifest-path") + .arg("dev/Cargo.toml") + .arg("--target-dir") + .arg("target"); } cmd.arg("--color") @@ -36,15 +84,43 @@ impl Exercise { .arg("-q") .arg("--bin") .arg(self.name) - .args(args) - .output() - .context("Failed to run Cargo") + .args(args); + + run_command(cmd, cmd_description, output) + } + + fn cargo_cmd_with_bin_output( + &self, + command: &str, + args: &[&str], + cmd_description: &str, + output: &mut Vec, + dev: bool, + ) -> Result { + // Discard the output of `cargo build` because it will be shown again by the Cargo command. + output.clear(); + + let cargo_cmd_success = self.cargo_cmd(command, args, cmd_description, output, dev)?; + + let run_success = self.run_bin(output)?; + + Ok(cargo_cmd_success && run_success) } - pub fn run(&self) -> Result { + pub fn run(&self, output: &mut Vec) -> Result { + output.clear(); + + // Developing the official Rustlings. + let dev = DEBUG_PROFILE && in_official_repo(); + + let build_success = self.cargo_cmd("build", &[], "cargo build …", output, dev)?; + if !build_success { + return Ok(false); + } + match self.mode { - Mode::Run => self.cargo_cmd("run", &[]), - Mode::Test => self.cargo_cmd( + Mode::Run => self.run_bin(output), + Mode::Test => self.cargo_cmd_with_bin_output( "test", &[ "--", @@ -54,10 +130,16 @@ impl Exercise { "--format", "pretty", ], + "cargo test …", + output, + dev, ), - Mode::Clippy => self.cargo_cmd( + Mode::Clippy => self.cargo_cmd_with_bin_output( "clippy", - &["--", "-D", "warnings", "-D", "clippy::float_cmp"], + &["--", "-D", "warnings"], + "cargo clippy …", + output, + dev, ), } } diff --git a/src/main.rs b/src/main.rs index 790fff6..a928504 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,10 +75,14 @@ enum Subcommands { Dev(DevCommands), } +fn in_official_repo() -> bool { + Path::new("dev/rustlings-repo.txt").exists() +} + fn main() -> Result<()> { let args = Args::parse(); - if !DEBUG_PROFILE && Path::new("dev/rustlings-repo.txt").exists() { + if !DEBUG_PROFILE && in_official_repo() { bail!("{OLD_METHOD_ERR}"); } diff --git a/src/run.rs b/src/run.rs index a2b6972..1db8dcb 100644 --- a/src/run.rs +++ b/src/run.rs @@ -4,20 +4,19 @@ use std::io::{self, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, + exercise::OUTPUT_CAPACITY, terminal_link::TerminalFileLink, }; pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); - let output = exercise.run()?; + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + let success = exercise.run(&mut output)?; let mut stdout = io::stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(b"\n")?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; + stdout.write_all(&output)?; - if !output.status.success() { + if !success { app_state.set_pending(app_state.current_exercise_ind())?; bail!( diff --git a/src/watch/state.rs b/src/watch/state.rs index 5f4abf3..df492dc 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -8,6 +8,7 @@ use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, + exercise::OUTPUT_CAPACITY, progress_bar::progress_bar, terminal_link::TerminalFileLink, }; @@ -21,8 +22,7 @@ enum DoneStatus { pub struct WatchState<'a> { writer: StdoutLock<'a>, app_state: &'a mut AppState, - stdout: Option>, - stderr: Option>, + output: Vec, show_hint: bool, done_status: DoneStatus, manual_run: bool, @@ -35,8 +35,7 @@ impl<'a> WatchState<'a> { Self { writer, app_state, - stdout: None, - stderr: None, + output: Vec::with_capacity(OUTPUT_CAPACITY), show_hint: false, done_status: DoneStatus::Pending, manual_run, @@ -51,11 +50,8 @@ impl<'a> WatchState<'a> { 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() { - self.stderr = None; + let success = self.app_state.current_exercise().run(&mut self.output)?; + if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { DoneStatus::DoneWithSolution(solution_path) @@ -66,7 +62,6 @@ impl<'a> WatchState<'a> { self.app_state .set_pending(self.app_state.current_exercise_ind())?; - self.stderr = Some(output.stderr); self.done_status = DoneStatus::Pending; } @@ -116,16 +111,7 @@ impl<'a> WatchState<'a> { self.writer.execute(Clear(ClearType::All))?; - 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")?; - } - + self.writer.write_all(&self.output)?; self.writer.write_all(b"\n")?; if self.show_hint { -- cgit v1.2.3 From f92d45fa685e308c009cdf09d341bda41fcf9c52 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 02:03:26 +0200 Subject: Use write macros instead of write_fmt --- src/app_state.rs | 6 +++--- src/list/state.rs | 3 +-- src/run.rs | 7 ++++--- src/watch/state.rs | 37 ++++++++++++++++++++----------------- 4 files changed, 28 insertions(+), 25 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/app_state.rs b/src/app_state.rs index 4160f6e..11ac8ee 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -309,12 +309,12 @@ impl AppState { let mut output = Vec::with_capacity(OUTPUT_CAPACITY); for (exercise_ind, exercise) in self.exercises().iter().enumerate() { - writer.write_fmt(format_args!("Running {exercise} ... "))?; + write!(writer, "Running {exercise} ... ")?; writer.flush()?; let success = exercise.run(&mut output)?; if !success { - writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; + writeln!(writer, "{}\n", "FAILED".red())?; self.current_exercise_ind = exercise_ind; @@ -328,7 +328,7 @@ impl AppState { return Ok(ExercisesProgress::Pending); } - writer.write_fmt(format_args!("{}\n", "ok".green()))?; + writeln!(writer, "{}", "ok".green())?; output.clear(); } diff --git a/src/list/state.rs b/src/list/state.rs index 0253bb9..19a77fe 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -231,8 +231,7 @@ impl<'a> UiState<'a> { .context("Invalid selection index")?; let exercise_path = self.app_state.reset_exercise_by_ind(ind)?; - self.message - .write_fmt(format_args!("The exercise {exercise_path} has been reset"))?; + write!(self.message, "The exercise {exercise_path} has been reset")?; Ok(self.with_updated_rows()) } diff --git a/src/run.rs b/src/run.rs index 1db8dcb..cbc9ad7 100644 --- a/src/run.rs +++ b/src/run.rs @@ -25,11 +25,12 @@ pub fn run(app_state: &mut AppState) -> Result<()> { ); } - stdout.write_fmt(format_args!( - "{}{}\n", + writeln!( + stdout, + "{}{}", "✓ Successfully ran ".green(), exercise.path.green(), - ))?; + )?; if let Some(solution_path) = app_state.current_solution_path()? { println!( diff --git a/src/watch/state.rs b/src/watch/state.rs index df492dc..40c01bf 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -88,19 +88,18 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; if self.manual_run { - self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?; + write!(self.writer, "{}un/", 'r'.bold())?; } if !matches!(self.done_status, DoneStatus::Pending) { - self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; + write!(self.writer, "{}ext/", 'n'.bold())?; } if !self.show_hint { - self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; + write!(self.writer, "{}int/", 'h'.bold())?; } - self.writer - .write_fmt(format_args!("{}ist/{}uit? ", 'l'.bold(), 'q'.bold()))?; + write!(self.writer, "{}ist/{}uit? ", 'l'.bold(), 'q'.bold())?; self.writer.flush() } @@ -115,28 +114,31 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; if self.show_hint { - self.writer.write_fmt(format_args!( - "{}\n{}\n\n", + writeln!( + self.writer, + "{}\n{}\n", "Hint".bold().cyan().underlined(), self.app_state.current_exercise().hint, - ))?; + )?; } if !matches!(self.done_status, DoneStatus::Pending) { - self.writer.write_fmt(format_args!( - "{}\n\n", + writeln!( + self.writer, + "{}\n", "Exercise done ✓ When you are done experimenting, enter `n` or `next` to go to the next exercise 🦀" .bold() .green(), - ))?; + )?; } if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { - self.writer.write_fmt(format_args!( - "A solution file can be found at {}\n\n", + writeln!( + self.writer, + "A solution file can be found at {}\n", style(TerminalFileLink(solution_path)).underlined().green() - ))?; + )?; } let line_width = size()?.0; @@ -145,10 +147,11 @@ 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_fmt(format_args!( - "{progress_bar}Current exercise: {}\n", + writeln!( + self.writer, + "{progress_bar}Current exercise: {}", self.app_state.current_exercise().terminal_link(), - ))?; + )?; self.show_prompt()?; -- cgit v1.2.3 From c82c3673245ca11d455b067c97fadda4a8406cb9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 04:14:59 +0200 Subject: Respect the target-dir config and show tests' output --- Cargo.lock | 69 +++++-------------------- Cargo.toml | 2 +- src/app_state.rs | 12 ++++- src/cmd.rs | 70 ++++++++++++++++++++++++++ src/exercise.rs | 145 ++++++++++++++++++++--------------------------------- src/main.rs | 29 +++++++++-- src/run.rs | 2 +- src/watch/state.rs | 5 +- 8 files changed, 176 insertions(+), 158 deletions(-) create mode 100644 src/cmd.rs (limited to 'src/watch/state.rs') diff --git a/Cargo.lock b/Cargo.lock index 5767267..f9b48bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,16 +271,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "filetime" version = "0.2.23" @@ -333,15 +323,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -419,12 +400,6 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" -[[package]] -name = "linux-raw-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" - [[package]] name = "lock_api" version = "0.4.11" @@ -664,19 +639,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.5.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - [[package]] name = "rustlings" version = "6.0.0-beta.3" @@ -692,8 +654,8 @@ dependencies = [ "ratatui", "rustlings-macros", "serde", + "serde_json", "toml_edit", - "which", ] [[package]] @@ -752,6 +714,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -935,18 +908,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "which" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" -dependencies = [ - "either", - "home", - "rustix", - "winsafe", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1126,12 +1087,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 1bc26c9..a77c84f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,9 +56,9 @@ notify-debouncer-mini = "0.4.1" os_pipe = "1.1.5" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.3" } +serde_json = "1.0.116" serde.workspace = true toml_edit.workspace = true -which = "6.0.1" [dev-dependencies] assert_cmd = "2.0.14" diff --git a/src/app_state.rs b/src/app_state.rs index 476b5a9..b980bdb 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -7,7 +7,7 @@ use crossterm::{ use std::{ fs::{self, File}, io::{Read, StdoutLock, Write}, - path::Path, + path::{Path, PathBuf}, process::{Command, Stdio}, }; @@ -39,6 +39,7 @@ pub struct AppState { final_message: String, file_buf: Vec, official_exercises: bool, + target_dir: PathBuf, } impl AppState { @@ -90,6 +91,7 @@ impl AppState { pub fn new( exercise_infos: Vec, final_message: String, + target_dir: PathBuf, ) -> (Self, StateFileStatus) { let exercises = exercise_infos .into_iter() @@ -127,6 +129,7 @@ impl AppState { final_message, file_buf: Vec::with_capacity(2048), official_exercises: !Path::new("info.toml").exists(), + target_dir, }; let state_file_status = slf.update_from_file(); @@ -154,6 +157,11 @@ impl AppState { &self.exercises[self.current_exercise_ind] } + #[inline] + pub fn target_dir(&self) -> &Path { + &self.target_dir + } + pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { if ind >= self.exercises.len() { bail!(BAD_INDEX_ERR); @@ -313,7 +321,7 @@ impl AppState { write!(writer, "Running {exercise} ... ")?; writer.flush()?; - let success = exercise.run(&mut output)?; + let success = exercise.run(&mut output, &self.target_dir)?; if !success { writeln!(writer, "{}\n", "FAILED".red())?; diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 0000000..28f21c5 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,70 @@ +use anyhow::{Context, Result}; +use std::{io::Read, path::Path, process::Command}; + +pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Result { + let (mut reader, writer) = os_pipe::pipe() + .with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; + + let writer_clone = writer.try_clone().with_context(|| { + format!("Failed to clone the pipe writer for the command `{description}`") + })?; + + let mut handle = cmd + .stdout(writer_clone) + .stderr(writer) + .spawn() + .with_context(|| format!("Failed to run the command `{description}`"))?; + + // Prevent pipe deadlock. + drop(cmd); + + reader + .read_to_end(output) + .with_context(|| format!("Failed to read the output of the command `{description}`"))?; + + output.push(b'\n'); + + handle + .wait() + .with_context(|| format!("Failed to wait on the command `{description}` to exit")) + .map(|status| status.success()) +} + +pub struct CargoCmd<'a> { + pub subcommand: &'a str, + pub args: &'a [&'a str], + pub exercise_name: &'a str, + pub description: &'a str, + pub hide_warnings: bool, + pub target_dir: &'a Path, + pub output: &'a mut Vec, + pub dev: bool, +} + +impl<'a> CargoCmd<'a> { + pub fn run(&mut self) -> Result { + let mut cmd = Command::new("cargo"); + cmd.arg(self.subcommand); + + // A hack to make `cargo run` work when developing Rustlings. + if self.dev { + cmd.arg("--manifest-path") + .arg("dev/Cargo.toml") + .arg("--target-dir") + .arg(self.target_dir); + } + + cmd.arg("--color") + .arg("always") + .arg("-q") + .arg("--bin") + .arg(self.exercise_name) + .args(self.args); + + if self.hide_warnings { + cmd.env("RUSTFLAGS", "-A warnings"); + } + + run_cmd(cmd, self.description, self.output) + } +} diff --git a/src/exercise.rs b/src/exercise.rs index 50f360e..23dae6f 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,57 +1,21 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - io::{Read, Write}, - process::{Command, Stdio}, + io::Write, + path::{Path, PathBuf}, + process::Command, }; -use crate::{in_official_repo, terminal_link::TerminalFileLink, DEBUG_PROFILE}; +use crate::{ + cmd::{run_cmd, CargoCmd}, + in_official_repo, + terminal_link::TerminalFileLink, + DEBUG_PROFILE, +}; pub const OUTPUT_CAPACITY: usize = 1 << 14; -fn run_command( - mut cmd: Command, - cmd_description: &str, - output: &mut Vec, - stderr: bool, -) -> Result { - let (mut reader, writer) = os_pipe::pipe().with_context(|| { - format!("Failed to create a pipe to run the command `{cmd_description}``") - })?; - - let (stdout, stderr) = if stderr { - ( - Stdio::from(writer.try_clone().with_context(|| { - format!("Failed to clone the pipe writer for the command `{cmd_description}`") - })?), - Stdio::from(writer), - ) - } else { - (Stdio::from(writer), Stdio::null()) - }; - - let mut handle = cmd - .stdout(stdout) - .stderr(stderr) - .spawn() - .with_context(|| format!("Failed to run the command `{cmd_description}`"))?; - - // Prevent pipe deadlock. - drop(cmd); - - reader - .read_to_end(output) - .with_context(|| format!("Failed to read the output of the command `{cmd_description}`"))?; - - output.push(b'\n'); - - handle - .wait() - .with_context(|| format!("Failed to wait on the command `{cmd_description}` to exit")) - .map(|status| status.success()) -} - pub struct Exercise { pub dir: Option<&'static str>, // Exercise's unique name @@ -66,11 +30,16 @@ pub struct Exercise { } impl Exercise { - fn run_bin(&self, output: &mut Vec) -> Result { + fn run_bin(&self, output: &mut Vec, target_dir: &Path) -> Result { writeln!(output, "{}", "Output".underlined())?; - let bin_path = format!("target/debug/{}", self.name); - let success = run_command(Command::new(&bin_path), &bin_path, output, true)?; + let mut bin_path = + PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len()); + bin_path.push(target_dir); + bin_path.push("debug"); + bin_path.push(self.name); + + let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; if !success { writeln!( @@ -85,43 +54,23 @@ impl Exercise { Ok(success) } - fn cargo_cmd( - &self, - command: &str, - args: &[&str], - cmd_description: &str, - output: &mut Vec, - dev: bool, - stderr: bool, - ) -> Result { - let mut cmd = Command::new("cargo"); - cmd.arg(command); - - // A hack to make `cargo run` work when developing Rustlings. - if dev { - cmd.arg("--manifest-path") - .arg("dev/Cargo.toml") - .arg("--target-dir") - .arg("target"); - } - - cmd.arg("--color") - .arg("always") - .arg("-q") - .arg("--bin") - .arg(self.name) - .args(args); - - run_command(cmd, cmd_description, output, stderr) - } - - pub fn run(&self, output: &mut Vec) -> Result { + pub fn run(&self, output: &mut Vec, target_dir: &Path) -> Result { output.clear(); // Developing the official Rustlings. let dev = DEBUG_PROFILE && in_official_repo(); - let build_success = self.cargo_cmd("build", &[], "cargo build …", output, dev, true)?; + let build_success = CargoCmd { + subcommand: "build", + args: &[], + exercise_name: self.name, + description: "cargo build …", + hide_warnings: false, + target_dir, + output, + dev, + } + .run()?; if !build_success { return Ok(false); } @@ -134,19 +83,28 @@ impl Exercise { } else { &["--profile", "test"] }; - let clippy_success = - self.cargo_cmd("clippy", clippy_args, "cargo clippy …", output, dev, true)?; + let clippy_success = CargoCmd { + subcommand: "clippy", + args: clippy_args, + exercise_name: self.name, + description: "cargo clippy …", + hide_warnings: false, + target_dir, + output, + dev, + } + .run()?; if !clippy_success { return Ok(false); } if !self.test { - return self.run_bin(output); + return self.run_bin(output, target_dir); } - let test_success = self.cargo_cmd( - "test", - &[ + let test_success = CargoCmd { + subcommand: "test", + args: &[ "--", "--color", "always", @@ -154,14 +112,17 @@ impl Exercise { "--format", "pretty", ], - "cargo test …", + exercise_name: self.name, + description: "cargo test …", + // Hide warnings because they are shown by Clippy. + hide_warnings: true, + target_dir, output, dev, - // Hide warnings because they are shown by Clippy. - false, - )?; + } + .run()?; - let run_success = self.run_bin(output)?; + let run_success = self.run_bin(output, target_dir)?; Ok(test_success && run_success) } diff --git a/src/main.rs b/src/main.rs index 7a142fd..b03aa52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,16 +5,18 @@ use crossterm::{ terminal::{Clear, ClearType}, ExecutableCommand, }; +use serde::Deserialize; use std::{ io::{self, BufRead, Write}, - path::Path, - process::exit, + path::{Path, PathBuf}, + process::{exit, Command, Stdio}, }; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; mod app_state; mod cargo_toml; +mod cmd; mod dev; mod embedded; mod exercise; @@ -75,6 +77,11 @@ enum Subcommands { Dev(DevCommands), } +#[derive(Deserialize)] +struct CargoMetadata { + target_directory: PathBuf, +} + fn in_official_repo() -> bool { Path::new("dev/rustlings-repo.txt").exists() } @@ -86,7 +93,20 @@ fn main() -> Result<()> { bail!("{OLD_METHOD_ERR}"); } - which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; + let metadata_output = Command::new("cargo") + .arg("metadata") + .arg("-q") + .arg("--format-version") + .arg("1") + .arg("--no-deps") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_METADATA_ERR)? + .stdout; + let target_dir = serde_json::de::from_slice::(&metadata_output) + .context("Failed to read the field `target_directory` from the `cargo metadata` output")? + .target_directory; match args.command { Some(Subcommands::Init) => { @@ -122,6 +142,7 @@ fn main() -> Result<()> { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), + target_dir, ); if let Some(welcome_message) = info_file.welcome_message { @@ -198,7 +219,7 @@ The new method doesn't include cloning the Rustlings' repository. Please follow the instructions in the README: https://github.com/rust-lang/rustlings#getting-started"; -const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. +const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …` Did you already install Rust? Try running `cargo --version` to diagnose the problem."; diff --git a/src/run.rs b/src/run.rs index cbc9ad7..9b5ddd3 100644 --- a/src/run.rs +++ b/src/run.rs @@ -11,7 +11,7 @@ use crate::{ pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let success = exercise.run(&mut output)?; + let success = exercise.run(&mut output, app_state.target_dir())?; let mut stdout = io::stdout().lock(); stdout.write_all(&output)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 40c01bf..82b745a 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -50,7 +50,10 @@ impl<'a> WatchState<'a> { pub fn run_current_exercise(&mut self) -> Result<()> { self.show_hint = false; - let success = self.app_state.current_exercise().run(&mut self.output)?; + let success = self + .app_state + .current_exercise() + .run(&mut self.output, self.app_state.target_dir())?; if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { -- 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/state.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 52c0f5b39efd7c71e63a3a680a1d91f3efc8eda5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 30 Apr 2024 01:41:08 +0200 Subject: Fix clearing the terminal --- src/app_state.rs | 9 +++------ src/main.rs | 14 +++++++------- src/watch/state.rs | 6 +++--- 3 files changed, 13 insertions(+), 16 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/app_state.rs b/src/app_state.rs index 6af1043..907c128 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,9 +1,5 @@ use anyhow::{bail, Context, Result}; -use crossterm::{ - style::Stylize, - terminal::{Clear, ClearType}, - ExecutableCommand, -}; +use crossterm::style::Stylize; use serde::Deserialize; use std::{ fs::{self, File}, @@ -13,6 +9,7 @@ use std::{ }; use crate::{ + clear_terminal, embedded::EMBEDDED_FILES, exercise::{Exercise, OUTPUT_CAPACITY}, info_file::ExerciseInfo, @@ -387,7 +384,7 @@ impl AppState { writeln!(writer, "{}", "ok".green())?; } - writer.execute(Clear(ClearType::All))?; + clear_terminal(writer)?; writer.write_all(FENISH_LINE.as_bytes())?; let final_message = self.final_message.trim(); diff --git a/src/main.rs b/src/main.rs index c51f63c..3e37ce2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,8 @@ use anyhow::{bail, Context, Result}; use app_state::StateFileStatus; use clap::{Parser, Subcommand}; -use crossterm::{ - terminal::{Clear, ClearType}, - ExecutableCommand, -}; use std::{ - io::{self, BufRead, Write}, + io::{self, BufRead, StdoutLock, Write}, path::Path, process::exit, }; @@ -45,6 +41,10 @@ fn in_official_repo() -> bool { Path::new("dev/rustlings-repo.txt").exists() } +fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { + stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") +} + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -129,7 +129,7 @@ fn main() -> Result<()> { match state_file_status { StateFileStatus::NotRead => { let mut stdout = io::stdout().lock(); - stdout.execute(Clear(ClearType::All))?; + clear_terminal(&mut stdout)?; let welcome_message = welcome_message.trim(); write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; @@ -137,7 +137,7 @@ fn main() -> Result<()> { io::stdin().lock().read_until(b'\n', &mut Vec::new())?; - stdout.execute(Clear(ClearType::All))?; + clear_terminal(&mut stdout)?; } StateFileStatus::Read => (), } diff --git a/src/watch/state.rs b/src/watch/state.rs index e5364c3..2cf7521 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,13 +1,13 @@ use anyhow::Result; use crossterm::{ style::{style, Stylize}, - terminal::{size, Clear, ClearType}, - ExecutableCommand, + terminal::size, }; use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, + clear_terminal, exercise::OUTPUT_CAPACITY, progress_bar::progress_bar, terminal_link::TerminalFileLink, @@ -111,7 +111,7 @@ impl<'a> WatchState<'a> { // Prevent having the first line shifted. self.writer.write_all(b"\n")?; - self.writer.execute(Clear(ClearType::All))?; + clear_terminal(&mut self.writer)?; self.writer.write_all(&self.output)?; self.writer.write_all(b"\n")?; -- 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/state.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/state.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/state.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 8b2d9ed50398c4c5c999ab9ab67757770449ed56 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:45:12 +0200 Subject: Use PartialEq instead of matches! --- src/watch/state.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/watch/state.rs b/src/watch/state.rs index c21d7ca..74cf182 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -13,6 +13,7 @@ use crate::{ terminal_link::TerminalFileLink, }; +#[derive(PartialEq, Eq)] enum DoneStatus { DoneWithSolution(String), DoneWithoutSolution, @@ -77,7 +78,7 @@ impl<'a> WatchState<'a> { } pub fn next_exercise(&mut self) -> Result { - if matches!(self.done_status, DoneStatus::Pending) { + if self.done_status == DoneStatus::Pending { return Ok(ExercisesProgress::CurrentPending); } @@ -91,7 +92,7 @@ impl<'a> WatchState<'a> { write!(self.writer, "{}un/", 'r'.bold())?; } - if !matches!(self.done_status, DoneStatus::Pending) { + if self.done_status != DoneStatus::Pending { write!(self.writer, "{}ext/", 'n'.bold())?; } @@ -122,7 +123,7 @@ impl<'a> WatchState<'a> { )?; } - if !matches!(self.done_status, DoneStatus::Pending) { + if self.done_status != DoneStatus::Pending { writeln!( self.writer, "{}\n", -- 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/state.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 96a44f3dcf2dd9e2562b757d7840084b45b90b61 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 14 May 2024 01:23:58 +0200 Subject: Make it more clear that only one char is expected --- src/watch/state.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/watch/state.rs b/src/watch/state.rs index 60b6d5a..abd21fb 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -96,18 +96,18 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; if self.manual_run { - write!(self.writer, "{}un/", 'r'.bold())?; + write!(self.writer, "{}:run / ", 'r'.bold())?; } if self.done_status != DoneStatus::Pending { - write!(self.writer, "{}ext/", 'n'.bold())?; + write!(self.writer, "{}:next / ", 'n'.bold())?; } if !self.show_hint { - write!(self.writer, "{}int/", 'h'.bold())?; + write!(self.writer, "{}:hint / ", 'h'.bold())?; } - write!(self.writer, "{}ist/{}uit? ", 'l'.bold(), 'q'.bold())?; + write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?; self.writer.flush() } -- 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/state.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 From 611f9d8722593430d82187aebee9db5cc6952da1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Jun 2024 21:48:15 +0200 Subject: Check that all solutions run successfully --- src/app_state.rs | 41 ++++++++++-------- src/cmd.rs | 4 +- src/dev/check.rs | 59 +++++++++++++++++-------- src/exercise.rs | 124 +++++++++++++++++++++++++++++++++++------------------ src/info_file.rs | 19 +++++++- src/run.rs | 4 +- src/watch/state.rs | 4 +- 7 files changed, 169 insertions(+), 86 deletions(-) (limited to 'src/watch/state.rs') diff --git a/src/app_state.rs b/src/app_state.rs index c7c090f..e9a5b10 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ clear_terminal, embedded::EMBEDDED_FILES, - exercise::{Exercise, OUTPUT_CAPACITY}, + exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY}, info_file::ExerciseInfo, DEBUG_PROFILE, }; @@ -40,6 +40,25 @@ struct CargoMetadata { target_directory: PathBuf, } +pub fn parse_target_dir() -> Result { + // Get the target directory from Cargo. + let metadata_output = Command::new("cargo") + .arg("metadata") + .arg("-q") + .arg("--format-version") + .arg("1") + .arg("--no-deps") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_METADATA_ERR)? + .stdout; + + serde_json::de::from_slice::(&metadata_output) + .context("Failed to read the field `target_directory` from the `cargo metadata` output") + .map(|metadata| metadata.target_directory) +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -104,23 +123,7 @@ impl AppState { exercise_infos: Vec, final_message: String, ) -> Result<(Self, StateFileStatus)> { - // Get the target directory from Cargo. - let metadata_output = Command::new("cargo") - .arg("metadata") - .arg("-q") - .arg("--format-version") - .arg("1") - .arg("--no-deps") - .stdin(Stdio::null()) - .stderr(Stdio::inherit()) - .output() - .context(CARGO_METADATA_ERR)? - .stdout; - let target_dir = serde_json::de::from_slice::(&metadata_output) - .context( - "Failed to read the field `target_directory` from the `cargo metadata` output", - )? - .target_directory; + let target_dir = parse_target_dir()?; let exercises = exercise_infos .into_iter() @@ -381,7 +384,7 @@ impl AppState { write!(writer, "Running {exercise} ... ")?; writer.flush()?; - let success = exercise.run(&mut output, &self.target_dir)?; + let success = exercise.run_exercise(&mut output, &self.target_dir)?; if !success { writeln!(writer, "{}\n", "FAILED".red())?; diff --git a/src/cmd.rs b/src/cmd.rs index b914ed8..6092f53 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -35,7 +35,7 @@ pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Res pub struct CargoCmd<'a> { pub subcommand: &'a str, pub args: &'a [&'a str], - pub exercise_name: &'a str, + pub bin_name: &'a str, pub description: &'a str, /// RUSTFLAGS="-A warnings" pub hide_warnings: bool, @@ -65,7 +65,7 @@ impl<'a> CargoCmd<'a> { .arg("always") .arg("-q") .arg("--bin") - .arg(self.exercise_name) + .arg(self.bin_name) .args(self.args); if self.hide_warnings { diff --git a/src/dev/check.rs b/src/dev/check.rs index 59352e9..6a3597c 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -2,12 +2,14 @@ use anyhow::{anyhow, bail, Context, Error, Result}; use std::{ cmp::Ordering, fs::{self, read_dir, OpenOptions}, - io::Read, + io::{self, Read, Write}, path::{Path, PathBuf}, }; use crate::{ + app_state::parse_target_dir, cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, info_file::{ExerciseInfo, InfoFile}, CURRENT_FORMAT_VERSION, DEBUG_PROFILE, }; @@ -17,6 +19,29 @@ fn forbidden_char(input: &str) -> Option { input.chars().find(|c| !c.is_alphanumeric() && *c != '_') } +// Check that the Cargo.toml file is up-to-date. +fn check_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + exercise_path_prefix: &[u8], +) -> Result<()> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; + let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY); + append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); + + if old_bins != new_bins { + if DEBUG_PROFILE { + bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); + } + + bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it"); + } + + Ok(()) +} + // Check the info of all exercises and return their paths in a set. fn check_info_file_exercises(info_file: &InfoFile) -> Result> { let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); @@ -136,24 +161,20 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { Ok(()) } -// Check that the Cargo.toml file is up-to-date. -fn check_cargo_toml( - exercise_infos: &[ExerciseInfo], - current_cargo_toml: &str, - exercise_path_prefix: &[u8], -) -> Result<()> { - let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; +fn check_solutions(info_file: &InfoFile) -> Result<()> { + let target_dir = parse_target_dir()?; + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; - let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY); - append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); - - if old_bins != new_bins { - if DEBUG_PROFILE { - bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); + for exercise_info in &info_file.exercises { + let success = exercise_info.run_solution(&mut output, &target_dir)?; + if !success { + io::stderr().write_all(&output)?; + + bail!( + "Failed to run the solution of the exercise {}", + exercise_info.name, + ); } - - bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it"); } Ok(()) @@ -161,7 +182,6 @@ fn check_cargo_toml( pub fn check() -> Result<()> { let info_file = InfoFile::parse()?; - check_exercises(&info_file)?; // A hack to make `cargo run -- dev check` work when developing Rustlings. if DEBUG_PROFILE { @@ -176,6 +196,9 @@ pub fn check() -> Result<()> { check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?; } + check_exercises(&info_file)?; + check_solutions(&info_file)?; + println!("\nEverything looks fine!"); Ok(()) diff --git a/src/exercise.rs b/src/exercise.rs index 4bc37cd..b6adc14 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -17,6 +17,35 @@ use crate::{ /// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; +// Run an exercise binary and append its output to the `output` buffer. +// Compilation must be done before calling this method. +fn run_bin(bin_name: &str, output: &mut Vec, target_dir: &Path) -> Result { + writeln!(output, "{}", "Output".underlined())?; + + // 7 = "/debug/".len() + let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len()); + bin_path.push(target_dir); + bin_path.push("debug"); + bin_path.push(bin_name); + + let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; + + if !success { + // This output is important to show the user that something went wrong. + // Otherwise, calling something like `exit(1)` in an exercise without further output + // leaves the user confused about why the exercise isn't done yet. + writeln!( + output, + "{}", + "The exercise didn't run successfully (nonzero exit code)" + .bold() + .red(), + )?; + } + + Ok(success) +} + /// See `info_file::ExerciseInfo` pub struct Exercise { pub dir: Option<&'static str>, @@ -30,39 +59,25 @@ pub struct Exercise { } impl Exercise { - // Run the exercise's binary and append its output to the `output` buffer. - // Compilation should be done before calling this method. - fn run_bin(&self, output: &mut Vec, target_dir: &Path) -> Result { - writeln!(output, "{}", "Output".underlined())?; - - // 7 = "/debug/".len() - let mut bin_path = - PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len()); - bin_path.push(target_dir); - bin_path.push("debug"); - bin_path.push(self.name); - - let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; - - if !success { - // This output is important to show the user that something went wrong. - // Otherwise, calling something like `exit(1)` in an exercise without further output - // leaves the user confused about why the exercise isn't done yet. - writeln!( - output, - "{}", - "The exercise didn't run successfully (nonzero exit code)" - .bold() - .red(), - )?; - } + pub fn terminal_link(&self) -> StyledContent> { + style(TerminalFileLink(self.path)).underlined().blue() + } +} - Ok(success) +impl Display for Exercise { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.path.fmt(f) } +} - /// Compile, check and run the exercise. - /// The output is written to the `output` buffer after clearing it. - pub fn run(&self, output: &mut Vec, target_dir: &Path) -> Result { +pub trait RunnableExercise { + fn name(&self) -> &str; + fn strict_clippy(&self) -> bool; + fn test(&self) -> bool; + + // Compile, check and run the exercise or its solution (depending on `bin_name´). + // The output is written to the `output` buffer after clearing it. + fn run(&self, bin_name: &str, output: &mut Vec, target_dir: &Path) -> Result { output.clear(); // Developing the official Rustlings. @@ -71,7 +86,7 @@ impl Exercise { let build_success = CargoCmd { subcommand: "build", args: &[], - exercise_name: self.name, + bin_name, description: "cargo build …", hide_warnings: false, target_dir, @@ -87,7 +102,7 @@ impl Exercise { output.clear(); // `--profile test` is required to also check code with `[cfg(test)]`. - let clippy_args: &[&str] = if self.strict_clippy { + let clippy_args: &[&str] = if self.strict_clippy() { &["--profile", "test", "--", "-D", "warnings"] } else { &["--profile", "test"] @@ -95,7 +110,7 @@ impl Exercise { let clippy_success = CargoCmd { subcommand: "clippy", args: clippy_args, - exercise_name: self.name, + bin_name, description: "cargo clippy …", hide_warnings: false, target_dir, @@ -107,14 +122,14 @@ impl Exercise { return Ok(false); } - if !self.test { - return self.run_bin(output, target_dir); + if !self.test() { + return run_bin(bin_name, output, target_dir); } let test_success = CargoCmd { subcommand: "test", args: &["--", "--color", "always", "--show-output"], - exercise_name: self.name, + bin_name, description: "cargo test …", // Hide warnings because they are shown by Clippy. hide_warnings: true, @@ -124,18 +139,43 @@ impl Exercise { } .run()?; - let run_success = self.run_bin(output, target_dir)?; + let run_success = run_bin(bin_name, output, target_dir)?; Ok(test_success && run_success) } - pub fn terminal_link(&self) -> StyledContent> { - style(TerminalFileLink(self.path)).underlined().blue() + /// Compile, check and run the exercise. + /// The output is written to the `output` buffer after clearing it. + #[inline] + fn run_exercise(&self, output: &mut Vec, target_dir: &Path) -> Result { + self.run(self.name(), output, target_dir) + } + + /// Compile, check and run the exercise's solution. + /// The output is written to the `output` buffer after clearing it. + fn run_solution(&self, output: &mut Vec, target_dir: &Path) -> Result { + let name = self.name(); + let mut bin_name = String::with_capacity(name.len()); + bin_name.push_str(name); + bin_name.push_str("_sol"); + + self.run(&bin_name, output, target_dir) } } -impl Display for Exercise { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.path.fmt(f) +impl RunnableExercise for Exercise { + #[inline] + fn name(&self) -> &str { + self.name + } + + #[inline] + fn strict_clippy(&self) -> bool { + self.strict_clippy + } + + #[inline] + fn test(&self) -> bool { + self.test } } diff --git a/src/info_file.rs b/src/info_file.rs index 5ea487f..f226f73 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; use std::{fs, io::ErrorKind}; -use crate::embedded::EMBEDDED_FILES; +use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; /// Deserialized from the `info.toml` file. #[derive(Deserialize)] @@ -75,6 +75,23 @@ impl ExerciseInfo { } } +impl RunnableExercise for ExerciseInfo { + #[inline] + fn name(&self) -> &str { + &self.name + } + + #[inline] + fn strict_clippy(&self) -> bool { + self.strict_clippy + } + + #[inline] + fn test(&self) -> bool { + self.test + } +} + /// The deserialized `info.toml` file. #[derive(Deserialize)] pub struct InfoFile { diff --git a/src/run.rs b/src/run.rs index 36899b9..899d0a9 100644 --- a/src/run.rs +++ b/src/run.rs @@ -4,14 +4,14 @@ use std::io::{self, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, - exercise::OUTPUT_CAPACITY, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, terminal_link::TerminalFileLink, }; pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let success = exercise.run(&mut output, app_state.target_dir())?; + let success = exercise.run_exercise(&mut output, app_state.target_dir())?; let mut stdout = io::stdout().lock(); stdout.write_all(&output)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 14c3f01..dd43c56 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -8,7 +8,7 @@ use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, clear_terminal, - exercise::OUTPUT_CAPACITY, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, progress_bar::progress_bar, terminal_link::TerminalFileLink, }; @@ -54,7 +54,7 @@ impl<'a> WatchState<'a> { let success = self .app_state .current_exercise() - .run(&mut self.output, self.app_state.target_dir())?; + .run_exercise(&mut self.output, self.app_state.target_dir())?; if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { -- cgit v1.2.3 From 67ce9b9e56b1d9e8b8b50b9b48c7eebd80b9ec8f Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 01:50:05 +0200 Subject: Underline "next" --- src/watch/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/watch/state.rs') diff --git a/src/watch/state.rs b/src/watch/state.rs index dd43c56..78af30a 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -101,7 +101,7 @@ impl<'a> WatchState<'a> { } if self.done_status != DoneStatus::Pending { - write!(self.writer, "{}:next / ", 'n'.bold())?; + write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?; } if !self.show_hint { -- cgit v1.2.3