summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormo8it <mo8it@proton.me>2024-04-11 02:51:02 +0200
committermo8it <mo8it@proton.me>2024-04-11 02:51:02 +0200
commitfa1f239a702eb2c0b7e0115e986481156961bbc8 (patch)
tree08ac7c8638546f80ac650474dfe4126103a15e54 /src
parent4bb6bda9f6416e30233342e73fc9a8486faa3f98 (diff)
Remove "I AM NOT DONE" and the verify mode and add AppState
Diffstat (limited to 'src')
-rw-r--r--src/app_state.rs185
-rw-r--r--src/exercise.rs206
-rw-r--r--src/list.rs21
-rw-r--r--src/list/state.rs71
-rw-r--r--src/main.rs67
-rw-r--r--src/run.rs23
-rw-r--r--src/state_file.rs68
-rw-r--r--src/verify.rs85
-rw-r--r--src/watch.rs10
-rw-r--r--src/watch/state.rs96
10 files changed, 301 insertions, 531 deletions
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<bool>,
+}
+
+impl StateFile {
+ fn read(exercises: &[Exercise]) -> Option<Self> {
+ 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<Exercise>) -> 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<usize> {
+ 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<ExercisesProgress> {
+ 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<ContextLine>),
-}
-
// 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<State> {
- 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<bool> {
- 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<String>,
},
/// 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<bool>,
-}
-
-const BAD_INDEX_ERR: &str = "The next exercise index is higher than the number of exercises";
-
-impl StateFile {
- fn read(exercises: &[Exercise]) -> Option<Self> {
- 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<VerifyState> {
- 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<WatchExit> {
+pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
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<Vec<u8>>,
stderr: Option<Vec<u8>>,
message: Option<String>,
@@ -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<bool> {
- let output = self.exercise.run()?;
+ pub fn run_current_exercise(&mut self) -> Result<bool> {
+ 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<bool> {
- 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()?;