Remove "I AM NOT DONE" and the verify mode and add AppState

This commit is contained in:
mo8it
2024-04-11 02:51:02 +02:00
parent 4bb6bda9f6
commit fa1f239a70
113 changed files with 306 additions and 769 deletions
+185
View File
@@ -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)
}
}
+1 -205
View File
@@ -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"));
}
}
+9 -12
View File
@@ -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();
}
_ => (),
}
+40 -31
View File
@@ -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)
}
}
+21 -46
View File
@@ -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(())
+16 -7
View File
@@ -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(())
}
-68
View File
@@ -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()
}
}
-85
View File
@@ -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)
}
+5 -5
View File
@@ -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));
+24 -72
View File
@@ -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()?;