summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock12
-rw-r--r--Cargo.toml1
-rw-r--r--src/app_state.rs128
-rw-r--r--src/app_state/state_file.rs110
-rw-r--r--src/init.rs2
6 files changed, 86 insertions, 169 deletions
diff --git a/.gitignore b/.gitignore
index c9172e0..80f9092 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@ target/
/dev/Cargo.lock
# State file
-.rustlings-state.json
+.rustlings-state.txt
# oranda
public/
diff --git a/Cargo.lock b/Cargo.lock
index dbf1923..6bc68f0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -690,7 +690,6 @@ dependencies = [
"ratatui",
"rustlings-macros",
"serde",
- "serde_json",
"toml_edit",
"which",
]
@@ -750,17 +749,6 @@ dependencies = [
]
[[package]]
-name = "serde_json"
-version = "1.0.115"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
-dependencies = [
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
name = "serde_spanned"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 14ae9a1..07865ab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,7 +41,6 @@ hashbrown = "0.14.3"
notify-debouncer-mini = "0.4.1"
ratatui = "0.26.1"
rustlings-macros = { path = "rustlings-macros" }
-serde_json = "1.0.115"
serde.workspace = true
toml_edit.workspace = true
which = "6.0.1"
diff --git a/src/app_state.rs b/src/app_state.rs
index 98c6384..9a378de 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -4,15 +4,14 @@ use crossterm::{
terminal::{Clear, ClearType},
ExecutableCommand,
};
-use std::io::{StdoutLock, Write};
-
-mod state_file;
+use std::{
+ fs::{self, File},
+ io::{Read, StdoutLock, Write},
+};
use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE};
-use self::state_file::{write, StateFileDeser};
-
-const STATE_FILE_NAME: &str = ".rustlings-state.json";
+const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
#[must_use]
@@ -27,11 +26,51 @@ pub struct AppState {
n_done: u16,
welcome_message: String,
final_message: String,
+ file_buf: Vec<u8>,
}
impl AppState {
+ fn update_from_file(&mut self) {
+ 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()
+ {
+ let mut lines = self.file_buf.split(|c| *c == b'\n');
+ let Some(current_exercise_name) = lines.next() else {
+ return;
+ };
+
+ if lines.next().is_none() {
+ return;
+ }
+
+ 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;
+ }
+ }
+ }
+ }
+
pub fn new(info_file: InfoFile) -> Self {
- let mut exercises = info_file
+ let exercises = info_file
.exercises
.into_iter()
.map(|mut exercise_info| {
@@ -55,42 +94,18 @@ impl AppState {
})
.collect::<Vec<_>>();
- let (current_exercise_ind, n_done) = StateFileDeser::read().map_or((0, 0), |state_file| {
- let mut state_file_exercises =
- hashbrown::HashMap::with_capacity(state_file.exercises.len());
-
- for (ind, exercise_state) in state_file.exercises.into_iter().enumerate() {
- state_file_exercises.insert(
- exercise_state.name,
- (ind == state_file.current_exercise_ind, exercise_state.done),
- );
- }
-
- let mut current_exercise_ind = 0;
- let mut n_done = 0;
- for (ind, exercise) in exercises.iter_mut().enumerate() {
- if let Some((current, done)) = state_file_exercises.get(exercise.name) {
- if *done {
- exercise.done = true;
- n_done += 1;
- }
-
- if *current {
- current_exercise_ind = ind;
- }
- }
- }
-
- (current_exercise_ind, n_done)
- });
-
- Self {
- current_exercise_ind,
+ let mut slf = Self {
+ current_exercise_ind: 0,
exercises,
- n_done,
+ n_done: 0,
welcome_message: info_file.welcome_message.unwrap_or_default(),
final_message: info_file.final_message.unwrap_or_default(),
- }
+ file_buf: Vec::with_capacity(2048),
+ };
+
+ slf.update_from_file();
+
+ slf
}
#[inline]
@@ -120,7 +135,7 @@ impl AppState {
self.current_exercise_ind = ind;
- write(self)
+ self.write()
}
pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
@@ -132,7 +147,7 @@ impl AppState {
.position(|exercise| exercise.name == name)
.with_context(|| format!("No exercise found for '{name}'!"))?;
- write(self)
+ self.write()
}
pub fn set_pending(&mut self, ind: usize) -> Result<()> {
@@ -141,7 +156,7 @@ impl AppState {
if exercise.done {
exercise.done = false;
self.n_done -= 1;
- write(self)?;
+ self.write()?;
}
Ok(())
@@ -193,7 +208,7 @@ impl AppState {
self.exercises[exercise_ind].done = false;
self.n_done -= 1;
- write(self)?;
+ self.write()?;
return Ok(ExercisesProgress::Pending);
}
@@ -213,6 +228,31 @@ impl AppState {
Ok(ExercisesProgress::Pending)
}
+
+ // Write the state file.
+ // The file's format is very simple:
+ // - The first line is the name of the current exercise.
+ // - The second line is an empty line.
+ // - All remaining lines are the names of done exercises.
+ fn write(&mut self) -> Result<()> {
+ self.file_buf.clear();
+
+ self.file_buf
+ .extend_from_slice(self.current_exercise().name.as_bytes());
+ self.file_buf.extend_from_slice(b"\n\n");
+
+ for exercise in &self.exercises {
+ if exercise.done {
+ self.file_buf.extend_from_slice(exercise.name.as_bytes());
+ self.file_buf.extend_from_slice(b"\n");
+ }
+ }
+
+ fs::write(STATE_FILE_NAME, &self.file_buf)
+ .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
+
+ Ok(())
+ }
}
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs
deleted file mode 100644
index 4e4a0e1..0000000
--- a/src/app_state/state_file.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-use anyhow::{Context, Result};
-use serde::{Deserialize, Serialize};
-use std::fs;
-
-use crate::exercise::Exercise;
-
-use super::{AppState, STATE_FILE_NAME};
-
-#[derive(Deserialize)]
-pub struct ExerciseStateDeser {
- pub name: String,
- pub done: bool,
-}
-
-#[derive(Serialize)]
-struct ExerciseStateSer<'a> {
- name: &'a str,
- done: bool,
-}
-
-struct ExercisesStateSerializer<'a>(&'a [Exercise]);
-
-impl<'a> Serialize for ExercisesStateSerializer<'a> {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- let iter = self.0.iter().map(|exercise| ExerciseStateSer {
- name: exercise.name,
- done: exercise.done,
- });
-
- serializer.collect_seq(iter)
- }
-}
-
-#[derive(Deserialize)]
-pub struct StateFileDeser {
- pub current_exercise_ind: usize,
- pub exercises: Vec<ExerciseStateDeser>,
-}
-
-#[derive(Serialize)]
-struct StateFileSer<'a> {
- current_exercise_ind: usize,
- exercises: ExercisesStateSerializer<'a>,
-}
-
-impl StateFileDeser {
- pub fn read() -> Option<Self> {
- let file_content = fs::read(STATE_FILE_NAME).ok()?;
- serde_json::de::from_slice(&file_content).ok()
- }
-}
-
-pub fn write(app_state: &AppState) -> Result<()> {
- let content = StateFileSer {
- current_exercise_ind: app_state.current_exercise_ind,
- exercises: ExercisesStateSerializer(&app_state.exercises),
- };
-
- let mut buf = Vec::with_capacity(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}`"))?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use crate::info_file::Mode;
-
- use super::*;
-
- #[test]
- fn ser_deser_sync() {
- let current_exercise_ind = 1;
- let exercises = [
- Exercise {
- name: "1",
- path: "exercises/1.rs",
- mode: Mode::Run,
- hint: String::new(),
- done: true,
- },
- Exercise {
- name: "2",
- path: "exercises/2.rs",
- mode: Mode::Test,
- hint: String::new(),
- done: false,
- },
- ];
-
- let ser = StateFileSer {
- current_exercise_ind,
- exercises: ExercisesStateSerializer(&exercises),
- };
- let deser: StateFileDeser =
- serde_json::de::from_slice(&serde_json::ser::to_vec(&ser).unwrap()).unwrap();
-
- assert_eq!(deser.current_exercise_ind, current_exercise_ind);
- assert!(deser
- .exercises
- .iter()
- .zip(exercises)
- .all(|(deser, ser)| deser.name == ser.name && deser.done == ser.done));
- }
-}
diff --git a/src/init.rs b/src/init.rs
index 2badf37..4ee503a 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -89,7 +89,7 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> {
}
const GITIGNORE: &[u8] = b"/target
-/.rustlings-state.json
+/.rustlings-state.txt
";
const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;