diff --git a/src/app_state.rs b/src/app_state.rs index 31cb2cbf..fb4b92e7 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -9,7 +9,7 @@ use std::{ io::{Read, StdoutLock, Write}, }; -use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE}; +use crate::{exercise::Exercise, info_file::ExerciseInfo, FENISH_LINE}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -20,58 +20,69 @@ pub enum ExercisesProgress { Pending, } +pub enum StateFileStatus { + Read, + NotRead, +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, n_done: u16, - welcome_message: String, final_message: String, file_buf: Vec, } impl AppState { - fn update_from_file(&mut self) { + fn update_from_file(&mut self) -> StateFileStatus { self.file_buf.clear(); self.n_done = 0; if File::open(STATE_FILE_NAME) .and_then(|mut file| file.read_to_end(&mut self.file_buf)) - .is_ok() + .is_err() { - let mut lines = self.file_buf.split(|c| *c == b'\n'); - let Some(current_exercise_name) = lines.next() else { - return; - }; + return StateFileStatus::NotRead; + } - if lines.next().is_none() { - return; + // See `Self::write` for more information about the file format. + let mut lines = self.file_buf.split(|c| *c == b'\n'); + let Some(current_exercise_name) = lines.next() else { + return StateFileStatus::NotRead; + }; + + if current_exercise_name.is_empty() || lines.next().is_none() { + return StateFileStatus::NotRead; + } + + let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len()); + + for done_exerise_name in lines { + if done_exerise_name.is_empty() { + break; + } + done_exercises.insert(done_exerise_name); + } + + for (ind, exercise) in self.exercises.iter_mut().enumerate() { + if done_exercises.contains(exercise.name.as_bytes()) { + exercise.done = true; + self.n_done += 1; } - let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len()); - - for done_exerise_name in lines { - if done_exerise_name.is_empty() { - break; - } - done_exercises.insert(done_exerise_name); - } - - for (ind, exercise) in self.exercises.iter_mut().enumerate() { - if done_exercises.contains(exercise.name.as_bytes()) { - exercise.done = true; - self.n_done += 1; - } - - if exercise.name.as_bytes() == current_exercise_name { - self.current_exercise_ind = ind; - } + if exercise.name.as_bytes() == current_exercise_name { + self.current_exercise_ind = ind; } } + + StateFileStatus::Read } - pub fn new(info_file: InfoFile) -> Self { - let exercises = info_file - .exercises + pub fn new( + exercise_infos: Vec, + final_message: String, + ) -> (Self, StateFileStatus) { + let exercises = exercise_infos .into_iter() .map(|mut exercise_info| { // Leaking to be able to borrow in the watch mode `Table`. @@ -98,14 +109,13 @@ impl AppState { current_exercise_ind: 0, exercises, n_done: 0, - welcome_message: info_file.welcome_message.unwrap_or_default(), - final_message: info_file.final_message.unwrap_or_default(), + final_message, file_buf: Vec::with_capacity(2048), }; - slf.update_from_file(); + let state_file_status = slf.update_from_file(); - slf + (slf, state_file_status) } #[inline] @@ -231,7 +241,8 @@ impl AppState { // Write the state file. // The file's format is very simple: - // - The first line is the name of the current exercise. + // - The first line is the name of the current exercise. It must end with `\n` even if there + // are no done exercises. // - The second line is an empty line. // - All remaining lines are the names of done exercises. fn write(&mut self) -> Result<()> { @@ -239,12 +250,12 @@ impl AppState { self.file_buf .extend_from_slice(self.current_exercise().name.as_bytes()); - self.file_buf.extend_from_slice(b"\n\n"); + self.file_buf.push(b'\n'); for exercise in &self.exercises { if exercise.done { - self.file_buf.extend_from_slice(exercise.name.as_bytes()); self.file_buf.push(b'\n'); + self.file_buf.extend_from_slice(exercise.name.as_bytes()); } } diff --git a/src/main.rs b/src/main.rs index a96e3230..aeb94321 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,15 @@ use anyhow::{Context, Result}; +use app_state::StateFileStatus; use clap::{Parser, Subcommand}; -use std::{path::Path, process::exit}; +use crossterm::{ + terminal::{Clear, ClearType}, + ExecutableCommand, +}; +use std::{ + io::{self, BufRead, Write}, + path::Path, + process::exit, +}; mod app_state; mod embedded; @@ -67,7 +76,26 @@ fn main() -> Result<()> { exit(1); } - let mut app_state = AppState::new(info_file); + let (mut app_state, state_file_status) = AppState::new( + info_file.exercises, + info_file.final_message.unwrap_or_default(), + ); + + if let Some(welcome_message) = info_file.welcome_message { + match state_file_status { + StateFileStatus::NotRead => { + let mut stdout = io::stdout().lock(); + stdout.execute(Clear(ClearType::All))?; + + let welcome_message = welcome_message.trim(); + write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; + stdout.flush()?; + + io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + } + StateFileStatus::Read => (), + } + } match args.command { None => {