use anyhow::{Context, Result}; use crossterm::{ style::{Attribute, ContentStyle, Stylize}, terminal::{size, Clear, ClearType}, ExecutableCommand, }; use std::{ fmt::Write as _, io::{self, StdoutLock, Write}, }; use crate::{ exercise::{Exercise, State}, progress_bar::progress_bar, state_file::StateFile, }; pub struct WatchState<'a> { writer: StdoutLock<'a>, exercises: &'static [Exercise], exercise: &'static Exercise, current_exercise_ind: usize, progress: u16, stdout: Option>, stderr: Option>, message: Option, hint_displayed: bool, } 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]; let writer = io::stdout().lock(); Self { writer, exercises, exercise, current_exercise_ind, progress, stdout: None, stderr: None, message: None, hint_displayed: false, } } #[inline] pub fn into_writer(self) -> StdoutLock<'a> { self.writer } pub fn run_exercise(&mut self) -> Result { let output = self.exercise.run()?; self.stdout = Some(output.stdout); if !output.status.success() { self.stderr = Some(output.stderr); return Ok(false); } 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 { self.exercise = self .exercises .get(exercise_ind) .context("Invalid exercise index")?; self.current_exercise_ind = exercise_ind; self.run_exercise() } pub fn show_prompt(&mut self) -> io::Result<()> { self.writer.write_all(b"\n\n")?; if !self.hint_displayed { self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; } self.writer .write_fmt(format_args!("{}ist/{}uit? ", 'l'.bold(), 'q'.bold()))?; self.writer.flush() } pub fn render(&mut self) -> Result<()> { // Prevent having the first line shifted after clearing because of the prompt. self.writer.write_all(b"\n")?; self.writer.execute(Clear(ClearType::All))?; if let Some(stdout) = &self.stdout { self.writer.write_all(stdout)?; self.writer.write_all(b"\n")?; } if let Some(stderr) = &self.stderr { self.writer.write_all(stderr)?; self.writer.write_all(b"\n")?; } if let Some(message) = &self.message { self.writer.write_all(message.as_bytes())?; } self.writer.write_all(b"\n")?; 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(b"\n\n")?; } let line_width = size()?.0; let progress_bar = progress_bar(self.progress, self.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.show_prompt()?; Ok(()) } pub fn show_hint(&mut self) -> Result<()> { self.hint_displayed = true; self.render() } pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> { self.writer.write_all(b"Invalid command: ")?; self.writer.write_all(cmd.as_bytes())?; if cmd.len() > 1 { self.writer .write_all(b" (confusing input can occur after resizing the terminal)")?; } self.show_prompt() } }