summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app_state.rs207
-rw-r--r--src/app_state/state_file.rs112
-rw-r--r--src/exercise.rs72
-rw-r--r--src/info_file.rs81
-rw-r--r--src/init.rs23
-rw-r--r--src/list.rs11
-rw-r--r--src/list/state.rs35
-rw-r--r--src/main.rs40
-rw-r--r--src/run.rs2
-rw-r--r--src/watch.rs15
-rw-r--r--src/watch/notify_event.rs (renamed from src/watch/debounce_event.rs)10
11 files changed, 377 insertions, 231 deletions
diff --git a/src/app_state.rs b/src/app_state.rs
index 2ea3db4..1a051b9 100644
--- a/src/app_state.rs
+++ b/src/app_state.rs
@@ -4,52 +4,16 @@ use crossterm::{
terminal::{Clear, ClearType},
ExecutableCommand,
};
-use serde::{Deserialize, Serialize};
-use std::{
- fs,
- io::{StdoutLock, Write},
-};
-
-use crate::{exercise::Exercise, FENISH_LINE};
-
-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()?;
+use std::io::{StdoutLock, Write};
- let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
+mod state_file;
- 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()],
- })
- }
+use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE};
- 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`")?;
+use self::state_file::{write, StateFileDeser};
- Ok(())
- }
-}
+const STATE_FILE_NAME: &str = ".rustlings-state.json";
+const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
#[must_use]
pub enum ExercisesProgress {
@@ -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();
-
- 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];
+ 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());
+
+ 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
- }
-
- #[inline]
- pub fn progress(&self) -> &[bool] {
- &self.state_file.progress
+ self.current_exercise_ind
}
#[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)?;
-
- if *done {
- *done = false;
+ let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?;
+
+ 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);
}
diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs
new file mode 100644
index 0000000..364a1fa
--- /dev/null
+++ b/src/app_state/state_file.rs
@@ -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));
+ }
+}
diff --git a/src/exercise.rs b/src/exercise.rs
index 6aa3b82..c5ece5f 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -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};
-
-// 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)
- }
-}
+use crate::{
+ embedded::{WriteStrategy, EMBEDDED_FILES},
+ info_file::Mode,
+};
-// 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.";
diff --git a/src/info_file.rs b/src/info_file.rs
new file mode 100644
index 0000000..dc97b92
--- /dev/null
+++ b/src/info_file.rs
@@ -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.";
diff --git a/src/init.rs b/src/init.rs
index 093610a..2badf37 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -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`")?;
diff --git a/src/list.rs b/src/list.rs
index de120ea..2bb813d 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -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()?;
diff --git a/src/list/state.rs b/src/list/state.rs
index 0dcfe88..38391a4 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -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)
diff --git a/src/main.rs b/src/main.rs
index cdfa21f..a96e323 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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)?;
diff --git a/src/run.rs b/src/run.rs
index 4748549..9c504b5 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -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!(
diff --git a/src/watch.rs b/src/watch.rs
index beb69b3..58e829f 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -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"));
}
}
}
diff --git a/src/watch/debounce_event.rs b/src/watch/notify_event.rs
index 1dc92cb..0c8d669 100644
--- a/src/watch/debounce_event.rs
+++ b/src/watch/notify_event.rs
@@ -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 {