mirror of
https://git.aramjonghu.dev/AramJonghu/rustlings.git
synced 2026-09-04 09:13:30 +02:00
Tolerate changes in the state file
This commit is contained in:
+96
-109
@@ -4,53 +4,17 @@ use crossterm::{
|
||||
terminal::{Clear, ClearType},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs,
|
||||
io::{StdoutLock, Write},
|
||||
};
|
||||
use std::io::{StdoutLock, Write};
|
||||
|
||||
use crate::{exercise::Exercise, FENISH_LINE};
|
||||
mod state_file;
|
||||
|
||||
use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE};
|
||||
|
||||
use self::state_file::{write, StateFileDeser};
|
||||
|
||||
const STATE_FILE_NAME: &str = ".rustlings-state.json";
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub enum ExercisesProgress {
|
||||
AllDone,
|
||||
@@ -58,52 +22,85 @@ pub enum ExercisesProgress {
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
state_file: StateFile,
|
||||
exercises: &'static [Exercise],
|
||||
current_exercise_ind: usize,
|
||||
exercises: Vec<Exercise>,
|
||||
n_done: u16,
|
||||
current_exercise: &'static Exercise,
|
||||
final_message: &'static str,
|
||||
welcome_message: String,
|
||||
final_message: String,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(mut exercises: Vec<Exercise>, mut final_message: String) -> Self {
|
||||
// Leaking especially for sending the exercises to the debounce event handler.
|
||||
// Leaking is not a problem because the `AppState` instance lives until
|
||||
// the end of the program.
|
||||
exercises.shrink_to_fit();
|
||||
let exercises = exercises.leak();
|
||||
final_message.shrink_to_fit();
|
||||
let final_message = final_message.leak();
|
||||
pub fn new(info_file: InfoFile) -> Self {
|
||||
let mut exercises = info_file
|
||||
.exercises
|
||||
.into_iter()
|
||||
.map(|mut exercise_info| {
|
||||
// Leaking to be able to borrow in the watch mode `Table`.
|
||||
// Leaking is not a problem because the `AppState` instance lives until
|
||||
// the end of the program.
|
||||
let path = Box::leak(exercise_info.path().into_boxed_path());
|
||||
|
||||
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];
|
||||
exercise_info.name.shrink_to_fit();
|
||||
let name = exercise_info.name.leak();
|
||||
|
||||
let hint = exercise_info.hint.trim().to_owned();
|
||||
|
||||
Exercise {
|
||||
name,
|
||||
path,
|
||||
mode: exercise_info.mode,
|
||||
hint,
|
||||
done: false,
|
||||
}
|
||||
})
|
||||
.collect::<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 {
|
||||
state_file,
|
||||
current_exercise_ind,
|
||||
exercises,
|
||||
n_done,
|
||||
current_exercise,
|
||||
final_message,
|
||||
welcome_message: info_file.welcome_message.unwrap_or_default(),
|
||||
final_message: info_file.final_message.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current_exercise_ind(&self) -> usize {
|
||||
self.state_file.current_exercise_ind
|
||||
self.current_exercise_ind
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn progress(&self) -> &[bool] {
|
||||
&self.state_file.progress
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn exercises(&self) -> &'static [Exercise] {
|
||||
self.exercises
|
||||
pub fn exercises(&self) -> &[Exercise] {
|
||||
&self.exercises
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -112,8 +109,8 @@ impl AppState {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current_exercise(&self) -> &'static Exercise {
|
||||
self.current_exercise
|
||||
pub fn current_exercise(&self) -> &Exercise {
|
||||
&self.exercises[self.current_exercise_ind]
|
||||
}
|
||||
|
||||
pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> {
|
||||
@@ -121,70 +118,61 @@ impl AppState {
|
||||
bail!(BAD_INDEX_ERR);
|
||||
}
|
||||
|
||||
self.state_file.current_exercise_ind = ind;
|
||||
self.current_exercise = &self.exercises[ind];
|
||||
self.current_exercise_ind = ind;
|
||||
|
||||
self.state_file.write()
|
||||
write(self)
|
||||
}
|
||||
|
||||
pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
|
||||
let (ind, exercise) = self
|
||||
// O(N) is fine since this method is used only once until the program exits.
|
||||
// Building a hashmap would have more overhead.
|
||||
self.current_exercise_ind = self
|
||||
.exercises
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, exercise)| exercise.name == name)
|
||||
.position(|exercise| exercise.name == name)
|
||||
.with_context(|| format!("No exercise found for '{name}'!"))?;
|
||||
|
||||
self.state_file.current_exercise_ind = ind;
|
||||
self.current_exercise = exercise;
|
||||
|
||||
self.state_file.write()
|
||||
write(self)
|
||||
}
|
||||
|
||||
pub fn set_pending(&mut self, ind: usize) -> Result<()> {
|
||||
let done = self
|
||||
.state_file
|
||||
.progress
|
||||
.get_mut(ind)
|
||||
.context(BAD_INDEX_ERR)?;
|
||||
let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?;
|
||||
|
||||
if *done {
|
||||
*done = false;
|
||||
if exercise.done {
|
||||
exercise.done = false;
|
||||
self.n_done -= 1;
|
||||
self.state_file.write()?;
|
||||
write(self)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_pending_exercise_ind(&self) -> Option<usize> {
|
||||
let current_ind = self.state_file.current_exercise_ind;
|
||||
|
||||
if current_ind == self.state_file.progress.len() - 1 {
|
||||
if self.current_exercise_ind == self.exercises.len() - 1 {
|
||||
// The last exercise is done.
|
||||
// Search for exercises not done from the start.
|
||||
return self.state_file.progress[..current_ind]
|
||||
return self.exercises[..self.current_exercise_ind]
|
||||
.iter()
|
||||
.position(|done| !done);
|
||||
.position(|exercise| !exercise.done);
|
||||
}
|
||||
|
||||
// The done exercise isn't the last one.
|
||||
// Search for a pending exercise after the current one and then from the start.
|
||||
match self.state_file.progress[current_ind + 1..]
|
||||
match self.exercises[self.current_exercise_ind + 1..]
|
||||
.iter()
|
||||
.position(|done| !done)
|
||||
.position(|exercise| !exercise.done)
|
||||
{
|
||||
Some(ind) => Some(current_ind + 1 + ind),
|
||||
None => self.state_file.progress[..current_ind]
|
||||
Some(ind) => Some(self.current_exercise_ind + 1 + ind),
|
||||
None => self.exercises[..self.current_exercise_ind]
|
||||
.iter()
|
||||
.position(|done| !done),
|
||||
.position(|exercise| !exercise.done),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||
let done = &mut self.state_file.progress[self.state_file.current_exercise_ind];
|
||||
if !*done {
|
||||
*done = true;
|
||||
let exercise = &mut self.exercises[self.current_exercise_ind];
|
||||
if !exercise.done {
|
||||
exercise.done = true;
|
||||
self.n_done += 1;
|
||||
}
|
||||
|
||||
@@ -198,15 +186,14 @@ impl AppState {
|
||||
if !exercise.run()?.status.success() {
|
||||
writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?;
|
||||
|
||||
self.state_file.current_exercise_ind = exercise_ind;
|
||||
self.current_exercise = exercise;
|
||||
self.current_exercise_ind = exercise_ind;
|
||||
|
||||
// No check if the exercise is done before setting it to pending
|
||||
// because no pending exercise was found.
|
||||
self.state_file.progress[exercise_ind] = false;
|
||||
self.exercises[exercise_ind].done = false;
|
||||
self.n_done -= 1;
|
||||
|
||||
self.state_file.write()?;
|
||||
write(self)?;
|
||||
|
||||
return Ok(ExercisesProgress::Pending);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
|
||||
use crate::exercise::Exercise;
|
||||
|
||||
use super::{AppState, STATE_FILE_NAME};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ExerciseStateDeser {
|
||||
pub name: String,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExerciseStateSer<'a> {
|
||||
name: &'a str,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
struct ExercisesStateSerializer<'a>(&'a [Exercise]);
|
||||
|
||||
impl<'a> Serialize for ExercisesStateSerializer<'a> {
|
||||
fn serialize<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(1024);
|
||||
serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?;
|
||||
fs::write(STATE_FILE_NAME, buf)
|
||||
.with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::info_file::Mode;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ser_deser_sync() {
|
||||
let current_exercise_ind = 1;
|
||||
let exercises = [
|
||||
Exercise {
|
||||
name: "1",
|
||||
path: Path::new("exercises/1.rs"),
|
||||
mode: Mode::Run,
|
||||
hint: String::new(),
|
||||
done: true,
|
||||
},
|
||||
Exercise {
|
||||
name: "2",
|
||||
path: Path::new("exercises/2.rs"),
|
||||
mode: Mode::Test,
|
||||
hint: String::new(),
|
||||
done: false,
|
||||
},
|
||||
];
|
||||
|
||||
let ser = StateFileSer {
|
||||
current_exercise_ind,
|
||||
exercises: ExercisesStateSerializer(&exercises),
|
||||
};
|
||||
let deser: StateFileDeser =
|
||||
serde_json::de::from_slice(&serde_json::ser::to_vec(&ser).unwrap()).unwrap();
|
||||
|
||||
assert_eq!(deser.current_exercise_ind, current_exercise_ind);
|
||||
assert!(deser
|
||||
.exercises
|
||||
.iter()
|
||||
.zip(exercises)
|
||||
.all(|(deser, ser)| deser.name == ser.name && deser.done == ser.done));
|
||||
}
|
||||
}
|
||||
+14
-58
@@ -1,66 +1,25 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fmt::{self, Debug, Display, Formatter},
|
||||
fs::{self},
|
||||
path::PathBuf,
|
||||
fmt::{self, Display, Formatter},
|
||||
path::Path,
|
||||
process::{Command, Output},
|
||||
};
|
||||
|
||||
use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
|
||||
use crate::{
|
||||
embedded::{WriteStrategy, EMBEDDED_FILES},
|
||||
info_file::Mode,
|
||||
};
|
||||
|
||||
// The mode of the exercise.
|
||||
#[derive(Deserialize, Copy, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Mode {
|
||||
// The exercise should be compiled as a binary
|
||||
Compile,
|
||||
// The exercise should be compiled as a test harness
|
||||
Test,
|
||||
// The exercise should be linted with clippy
|
||||
Clippy,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct InfoFile {
|
||||
// TODO
|
||||
pub welcome_message: Option<String>,
|
||||
pub final_message: Option<String>,
|
||||
pub exercises: Vec<Exercise>,
|
||||
}
|
||||
|
||||
impl InfoFile {
|
||||
pub fn parse() -> Result<Self> {
|
||||
// Read a local `info.toml` if it exists.
|
||||
// Mainly to let the tests work for now.
|
||||
let slf: Self = if let Ok(file_content) = fs::read_to_string("info.toml") {
|
||||
toml_edit::de::from_str(&file_content)
|
||||
} else {
|
||||
toml_edit::de::from_str(include_str!("../info.toml"))
|
||||
}
|
||||
.context("Failed to parse `info.toml`")?;
|
||||
|
||||
if slf.exercises.is_empty() {
|
||||
panic!("{NO_EXERCISES_ERR}");
|
||||
}
|
||||
|
||||
Ok(slf)
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialized from the `info.toml` file.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Exercise {
|
||||
// Name of the exercise
|
||||
pub name: String,
|
||||
// The path to the file containing the exercise's source code
|
||||
pub path: PathBuf,
|
||||
// Exercise's unique name
|
||||
pub name: &'static str,
|
||||
// Exercise's path
|
||||
pub path: &'static Path,
|
||||
// The mode of the exercise
|
||||
pub mode: Mode,
|
||||
// The hint text associated with the exercise
|
||||
pub hint: String,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
impl Exercise {
|
||||
@@ -79,7 +38,7 @@ impl Exercise {
|
||||
.arg("always")
|
||||
.arg("-q")
|
||||
.arg("--bin")
|
||||
.arg(&self.name)
|
||||
.arg(self.name)
|
||||
.args(args)
|
||||
.output()
|
||||
.context("Failed to run Cargo")
|
||||
@@ -87,7 +46,7 @@ impl Exercise {
|
||||
|
||||
pub fn run(&self) -> Result<Output> {
|
||||
match self.mode {
|
||||
Mode::Compile => self.cargo_cmd("run", &[]),
|
||||
Mode::Run => self.cargo_cmd("run", &[]),
|
||||
Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]),
|
||||
Mode::Clippy => self.cargo_cmd(
|
||||
"clippy",
|
||||
@@ -98,7 +57,7 @@ impl Exercise {
|
||||
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
EMBEDDED_FILES
|
||||
.write_exercise_to_disk(&self.path, WriteStrategy::Overwrite)
|
||||
.write_exercise_to_disk(self.path, WriteStrategy::Overwrite)
|
||||
.with_context(|| format!("Failed to reset the exercise {self}"))
|
||||
}
|
||||
}
|
||||
@@ -108,6 +67,3 @@ impl Display for Exercise {
|
||||
Display::fmt(&self.path.display(), f)
|
||||
}
|
||||
}
|
||||
|
||||
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
|
||||
If you are developing third-party exercises, add at least one exercise before testing.";
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use anyhow::{bail, Context, Error, Result};
|
||||
use serde::Deserialize;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
// The mode of the exercise.
|
||||
#[derive(Deserialize, Copy, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Mode {
|
||||
// The exercise should be compiled as a binary
|
||||
Run,
|
||||
// The exercise should be compiled as a test harness
|
||||
Test,
|
||||
// The exercise should be linted with clippy
|
||||
Clippy,
|
||||
}
|
||||
|
||||
// Deserialized from the `info.toml` file.
|
||||
#[derive(Deserialize)]
|
||||
pub struct ExerciseInfo {
|
||||
// Name of the exercise
|
||||
pub name: String,
|
||||
// The exercise's directory inside the `exercises` directory
|
||||
pub dir: Option<String>,
|
||||
// The mode of the exercise
|
||||
pub mode: Mode,
|
||||
// The hint text associated with the exercise
|
||||
pub hint: String,
|
||||
}
|
||||
|
||||
impl ExerciseInfo {
|
||||
pub fn path(&self) -> PathBuf {
|
||||
let path = if let Some(dir) = &self.dir {
|
||||
format!("exercises/{dir}/{}.rs", self.name)
|
||||
} else {
|
||||
format!("exercises/{}.rs", self.name)
|
||||
};
|
||||
|
||||
PathBuf::from(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct InfoFile {
|
||||
pub welcome_message: Option<String>,
|
||||
pub final_message: Option<String>,
|
||||
pub exercises: Vec<ExerciseInfo>,
|
||||
}
|
||||
|
||||
impl InfoFile {
|
||||
pub fn parse() -> Result<Self> {
|
||||
// Read a local `info.toml` if it exists.
|
||||
let slf: Self = match fs::read_to_string("info.toml") {
|
||||
Ok(file_content) => toml_edit::de::from_str(&file_content)
|
||||
.context("Failed to parse the `info.toml` file")?,
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => {
|
||||
toml_edit::de::from_str(include_str!("../info.toml"))
|
||||
.context("Failed to parse the embedded `info.toml` file")?
|
||||
}
|
||||
_ => return Err(Error::from(e).context("Failed to read the `info.toml` file")),
|
||||
},
|
||||
};
|
||||
|
||||
if slf.exercises.is_empty() {
|
||||
bail!("{NO_EXERCISES_ERR}");
|
||||
}
|
||||
|
||||
let mut names_set = hashbrown::HashSet::with_capacity(slf.exercises.len());
|
||||
for exercise in &slf.exercises {
|
||||
if !names_set.insert(exercise.name.as_str()) {
|
||||
bail!("Exercise names must all be unique!")
|
||||
}
|
||||
}
|
||||
drop(names_set);
|
||||
|
||||
Ok(slf)
|
||||
}
|
||||
}
|
||||
|
||||
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
|
||||
If you are developing third-party exercises, add at least one exercise before testing.";
|
||||
+14
-9
@@ -6,17 +6,21 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{embedded::EMBEDDED_FILES, exercise::Exercise};
|
||||
use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo};
|
||||
|
||||
fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> {
|
||||
fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> {
|
||||
let mut cargo_toml = Vec::with_capacity(1 << 13);
|
||||
cargo_toml.extend_from_slice(b"bin = [\n");
|
||||
for exercise in exercises {
|
||||
for exercise_info in exercise_infos {
|
||||
cargo_toml.extend_from_slice(b" { name = \"");
|
||||
cargo_toml.extend_from_slice(exercise.name.as_bytes());
|
||||
cargo_toml.extend_from_slice(b"\", path = \"");
|
||||
cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes());
|
||||
cargo_toml.extend_from_slice(b"\" },\n");
|
||||
cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
|
||||
cargo_toml.extend_from_slice(b"\", path = \"exercises/");
|
||||
if let Some(dir) = &exercise_info.dir {
|
||||
cargo_toml.extend_from_slice(dir.as_bytes());
|
||||
cargo_toml.extend_from_slice(b"/");
|
||||
}
|
||||
cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
|
||||
cargo_toml.extend_from_slice(b".rs\" },\n");
|
||||
}
|
||||
|
||||
cargo_toml.extend_from_slice(
|
||||
@@ -54,7 +58,7 @@ fn create_vscode_dir() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init(exercises: &[Exercise]) -> Result<()> {
|
||||
pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
||||
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
|
||||
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
|
||||
}
|
||||
@@ -74,7 +78,8 @@ pub fn init(exercises: &[Exercise]) -> Result<()> {
|
||||
.init_exercises_dir()
|
||||
.context("Failed to initialize the `rustlings/exercises` directory")?;
|
||||
|
||||
create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?;
|
||||
create_cargo_toml(exercise_infos)
|
||||
.context("Failed to create the file `rustlings/Cargo.toml`")?;
|
||||
|
||||
create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?;
|
||||
|
||||
|
||||
+2
-9
@@ -5,7 +5,7 @@ use crossterm::{
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::{fmt::Write, io};
|
||||
use std::io;
|
||||
|
||||
mod state;
|
||||
|
||||
@@ -72,14 +72,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
|
||||
ui_state.message.push_str(message);
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
let Some(exercise) = ui_state.reset_selected()? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
ui_state = ui_state.with_updated_rows();
|
||||
ui_state
|
||||
.message
|
||||
.write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
|
||||
ui_state = ui_state.with_reset_selected()?;
|
||||
}
|
||||
KeyCode::Char('c') => {
|
||||
ui_state.selected_to_current_exercise()?;
|
||||
|
||||
+18
-17
@@ -6,8 +6,9 @@ use ratatui::{
|
||||
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
|
||||
Frame,
|
||||
};
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{app_state::AppState, exercise::Exercise, progress_bar::progress_bar_ratatui};
|
||||
use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Filter {
|
||||
@@ -34,10 +35,9 @@ impl<'a> UiState<'a> {
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.zip(self.app_state.progress().iter().copied())
|
||||
.enumerate()
|
||||
.filter_map(|(ind, (exercise, done))| {
|
||||
let exercise_state = if done {
|
||||
.filter_map(|(ind, exercise)| {
|
||||
let exercise_state = if exercise.done {
|
||||
if self.filter == Filter::Pending {
|
||||
return None;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ impl<'a> UiState<'a> {
|
||||
Some(Row::new([
|
||||
next,
|
||||
exercise_state,
|
||||
Span::raw(&exercise.name),
|
||||
Span::raw(exercise.name),
|
||||
Span::raw(exercise.path.to_string_lossy()),
|
||||
]))
|
||||
});
|
||||
@@ -212,29 +212,30 @@ impl<'a> UiState<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset_selected(&mut self) -> Result<Option<&'static Exercise>> {
|
||||
pub fn with_reset_selected(mut self) -> Result<Self> {
|
||||
let Some(selected) = self.table_state.selected() else {
|
||||
return Ok(None);
|
||||
return Ok(self);
|
||||
};
|
||||
|
||||
let (ind, exercise) = self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.zip(self.app_state.progress())
|
||||
.enumerate()
|
||||
.filter_map(|(ind, (exercise, done))| match self.filter {
|
||||
Filter::Done => done.then_some((ind, exercise)),
|
||||
Filter::Pending => (!done).then_some((ind, exercise)),
|
||||
.filter_map(|(ind, exercise)| match self.filter {
|
||||
Filter::Done => exercise.done.then_some((ind, exercise)),
|
||||
Filter::Pending => (!exercise.done).then_some((ind, exercise)),
|
||||
Filter::None => Some((ind, exercise)),
|
||||
})
|
||||
.nth(selected)
|
||||
.context("Invalid selection index")?;
|
||||
|
||||
self.app_state.set_pending(ind)?;
|
||||
exercise.reset()?;
|
||||
self.message
|
||||
.write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
|
||||
self.app_state.set_pending(ind)?;
|
||||
|
||||
Ok(Some(exercise))
|
||||
Ok(self.with_updated_rows())
|
||||
}
|
||||
|
||||
pub fn selected_to_current_exercise(&mut self) -> Result<()> {
|
||||
@@ -244,12 +245,12 @@ impl<'a> UiState<'a> {
|
||||
|
||||
let ind = self
|
||||
.app_state
|
||||
.progress()
|
||||
.exercises()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ind, done)| match self.filter {
|
||||
Filter::Done => done.then_some(ind),
|
||||
Filter::Pending => (!done).then_some(ind),
|
||||
.filter_map(|(ind, exercise)| match self.filter {
|
||||
Filter::Done => exercise.done.then_some(ind),
|
||||
Filter::Pending => (!exercise.done).then_some(ind),
|
||||
Filter::None => Some(ind),
|
||||
})
|
||||
.nth(selected)
|
||||
|
||||
+25
-15
@@ -5,6 +5,7 @@ use std::{path::Path, process::exit};
|
||||
mod app_state;
|
||||
mod embedded;
|
||||
mod exercise;
|
||||
mod info_file;
|
||||
mod init;
|
||||
mod list;
|
||||
mod progress_bar;
|
||||
@@ -13,7 +14,7 @@ mod watch;
|
||||
|
||||
use self::{
|
||||
app_state::AppState,
|
||||
exercise::InfoFile,
|
||||
info_file::InfoFile,
|
||||
init::init,
|
||||
list::list,
|
||||
run::run,
|
||||
@@ -54,12 +55,10 @@ fn main() -> Result<()> {
|
||||
|
||||
which::which("cargo").context(CARGO_NOT_FOUND_ERR)?;
|
||||
|
||||
let mut info_file = InfoFile::parse()?;
|
||||
info_file.exercises.shrink_to_fit();
|
||||
let exercises = info_file.exercises;
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
if matches!(args.command, Some(Subcommands::Init)) {
|
||||
init(&exercises).context("Initialization failed")?;
|
||||
init(&info_file.exercises).context("Initialization failed")?;
|
||||
|
||||
println!("{POST_INIT_MSG}");
|
||||
return Ok(());
|
||||
@@ -68,18 +67,29 @@ fn main() -> Result<()> {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default());
|
||||
let mut app_state = AppState::new(info_file);
|
||||
|
||||
match args.command {
|
||||
None => loop {
|
||||
match watch(&mut app_state)? {
|
||||
WatchExit::Shutdown => break,
|
||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||
// the watch mode instead of trying to pause the watch threads and correct the
|
||||
// watch state.
|
||||
WatchExit::List => list(&mut app_state)?,
|
||||
None => {
|
||||
// For the the notify event handler thread.
|
||||
// Leaking is not a problem because the slice lives until the end of the program.
|
||||
let exercise_paths = app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.map(|exercise| exercise.path)
|
||||
.collect::<Vec<_>>()
|
||||
.leak();
|
||||
|
||||
loop {
|
||||
match watch(&mut app_state, exercise_paths)? {
|
||||
WatchExit::Shutdown => break,
|
||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||
// the watch mode instead of trying to pause the watch threads and correct the
|
||||
// watch state.
|
||||
WatchExit::List => list(&mut app_state)?,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
// `Init` is handled above.
|
||||
Some(Subcommands::Init) => (),
|
||||
Some(Subcommands::Run { name }) => {
|
||||
@@ -90,10 +100,10 @@ fn main() -> Result<()> {
|
||||
}
|
||||
Some(Subcommands::Reset { name }) => {
|
||||
app_state.set_current_exercise_by_name(&name)?;
|
||||
app_state.set_pending(app_state.current_exercise_ind())?;
|
||||
let exercise = app_state.current_exercise();
|
||||
exercise.reset()?;
|
||||
println!("The exercise {exercise} has been reset!");
|
||||
app_state.set_pending(app_state.current_exercise_ind())?;
|
||||
}
|
||||
Some(Subcommands::Hint { name }) => {
|
||||
app_state.set_current_exercise_by_name(&name)?;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||
if !output.status.success() {
|
||||
app_state.set_pending(app_state.current_exercise_ind())?;
|
||||
|
||||
bail!("Ran {exercise} with errors");
|
||||
bail!("Ran {} with errors", app_state.current_exercise());
|
||||
}
|
||||
|
||||
stdout.write_fmt(format_args!(
|
||||
|
||||
+9
-6
@@ -11,14 +11,14 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
mod debounce_event;
|
||||
mod notify_event;
|
||||
mod state;
|
||||
mod terminal_event;
|
||||
|
||||
use crate::app_state::{AppState, ExercisesProgress};
|
||||
|
||||
use self::{
|
||||
debounce_event::DebounceEventHandler,
|
||||
notify_event::DebounceEventHandler,
|
||||
state::WatchState,
|
||||
terminal_event::{terminal_event_handler, InputEvent},
|
||||
};
|
||||
@@ -40,13 +40,16 @@ pub enum WatchExit {
|
||||
List,
|
||||
}
|
||||
|
||||
pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
|
||||
pub fn watch(
|
||||
app_state: &mut AppState,
|
||||
exercise_paths: &'static [&'static Path],
|
||||
) -> Result<WatchExit> {
|
||||
let (tx, rx) = channel();
|
||||
let mut debouncer = new_debouncer(
|
||||
Duration::from_secs(1),
|
||||
DebounceEventHandler {
|
||||
tx: tx.clone(),
|
||||
exercises: app_state.exercises(),
|
||||
exercise_paths,
|
||||
},
|
||||
)?;
|
||||
debouncer
|
||||
@@ -85,10 +88,10 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
|
||||
watch_state.render()?;
|
||||
}
|
||||
WatchEvent::NotifyErr(e) => {
|
||||
return Err(Error::from(e).context("Exercise file watcher failed"))
|
||||
return Err(Error::from(e).context("Exercise file watcher failed"));
|
||||
}
|
||||
WatchEvent::TerminalEventErr(e) => {
|
||||
return Err(Error::from(e).context("Terminal event listener failed"))
|
||||
return Err(Error::from(e).context("Terminal event listener failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use crate::exercise::Exercise;
|
||||
use std::{path::Path, sync::mpsc::Sender};
|
||||
|
||||
use super::WatchEvent;
|
||||
|
||||
pub struct DebounceEventHandler {
|
||||
pub tx: Sender<WatchEvent>,
|
||||
pub exercises: &'static [Exercise],
|
||||
pub exercise_paths: &'static [&'static Path],
|
||||
}
|
||||
|
||||
impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
|
||||
@@ -23,9 +21,9 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.exercises
|
||||
self.exercise_paths
|
||||
.iter()
|
||||
.position(|exercise| event.path.ends_with(&exercise.path))
|
||||
.position(|path| event.path.ends_with(path))
|
||||
})
|
||||
.min()
|
||||
else {
|
||||
Reference in New Issue
Block a user