From b970e17bdf8f198c0e6a5bd5a5b89b71dcb1eaad Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Aug 2026 23:30:40 +0200 Subject: [PATCH] New check progress visualization --- src/app_state.rs | 76 ++++++++++++++------------------- src/term.rs | 107 ++++++++++++++--------------------------------- 2 files changed, 63 insertions(+), 120 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index e1d0e0b8..789f3043 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Error, Result, bail}; -use crossterm::{QueueableCommand, cursor, terminal}; +use crossterm::{QueueableCommand, cursor}; use std::{ collections::HashSet, fs::{File, OpenOptions}, @@ -43,8 +43,6 @@ pub enum StateFileStatus { #[derive(Clone, Copy)] pub enum CheckProgress { - None, - Checking, Done, Pending, } @@ -398,22 +396,18 @@ impl AppState { } fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result> { - let term_width = terminal::size() - .context("Failed to get the terminal size")? - .0; - let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?; + let mut progress_visualizer = CheckProgressVisualizer::build(stdout, self.exercises.len())?; - let next_exercise_ind = AtomicUsize::new(0); - let mut progresses = vec![CheckProgress::None; self.exercises.len()]; + let next_exercise_ind = &AtomicUsize::new(0); + let mut progresses = vec![None; self.exercises.len()]; thread::scope(|s| { - let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel(); + let (progress_sender, progress_receiver) = mpsc::channel(); let n_threads = thread::available_parallelism() .map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get()); for _ in 0..n_threads { - let exercise_progress_sender = exercise_progress_sender.clone(); - let next_exercise_ind = &next_exercise_ind; + let progress_sender = progress_sender.clone(); let slf = &self; thread::Builder::new() .spawn_scoped(s, move || { @@ -424,25 +418,16 @@ impl AppState { break; }; - if exercise_progress_sender - .send((exercise_ind, CheckProgress::Checking)) - .is_err() - { - break; - } + if let Ok(success) = exercise.run_exercise(None, &slf.cmd_runner) { + let progress = if success { + CheckProgress::Done + } else { + CheckProgress::Pending + }; - let success = exercise.run_exercise(None, &slf.cmd_runner); - let progress = match success { - Ok(true) => CheckProgress::Done, - Ok(false) => CheckProgress::Pending, - Err(_) => CheckProgress::None, - }; - - if exercise_progress_sender - .send((exercise_ind, progress)) - .is_err() - { - break; + if progress_sender.send((exercise_ind, progress)).is_err() { + break; + } } } }) @@ -450,47 +435,48 @@ impl AppState { } // Drop this sender to detect when the last thread is done. - drop(exercise_progress_sender); + drop(progress_sender); - while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() { - progresses[exercise_ind] = progress; - progress_visualizer.update(&progresses)?; + // TODO: Timeout + while let Ok((exercise_ind, progress)) = progress_receiver.recv() { + let name = self.exercises[exercise_ind].name; + match progress { + CheckProgress::Done => progress_visualizer.done(name)?, + CheckProgress::Pending => progress_visualizer.pending(name)?, + } + progresses[exercise_ind] = Some(progress); } Ok::<_, Error>(()) })?; let mut first_pending_exercise_ind = None; - for exercise_ind in 0..progresses.len() { - match progresses[exercise_ind] { - CheckProgress::Done => { + for (exercise_ind, progress) in progresses.into_iter().enumerate() { + match progress { + Some(CheckProgress::Done) => { self.set_status(exercise_ind, true)?; } - CheckProgress::Pending => { + Some(CheckProgress::Pending) => { self.set_status(exercise_ind, false)?; if first_pending_exercise_ind.is_none() { first_pending_exercise_ind = Some(exercise_ind); } } - CheckProgress::None | CheckProgress::Checking => { + None => { // If we got an error while checking all exercises in parallel, // it could be because we exceeded the limit of open file descriptors. // Therefore, try running exercises with errors sequentially. - progresses[exercise_ind] = CheckProgress::Checking; - progress_visualizer.update(&progresses)?; - let exercise = &self.exercises[exercise_ind]; let success = exercise.run_exercise(None, &self.cmd_runner)?; if success { - progresses[exercise_ind] = CheckProgress::Done; + progress_visualizer.done(exercise.name)?; } else { - progresses[exercise_ind] = CheckProgress::Pending; + progress_visualizer.pending(exercise.name)?; if first_pending_exercise_ind.is_none() { first_pending_exercise_ind = Some(exercise_ind); } } self.set_status(exercise_ind, success)?; - progress_visualizer.update(&progresses)?; } } } diff --git a/src/term.rs b/src/term.rs index 2467b450..455b860e 100644 --- a/src/term.rs +++ b/src/term.rs @@ -9,8 +9,6 @@ use std::{ io::{self, BufRead, StdoutLock, Write}, }; -use crate::app_state::CheckProgress; - pub struct MaxLenWriter<'a, 'lock> { pub stdout: &'a mut StdoutLock<'lock>, len: usize, @@ -81,79 +79,6 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> { } } -pub struct CheckProgressVisualizer<'a, 'lock> { - stdout: &'a mut StdoutLock<'lock>, - n_cols: usize, -} - -impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> { - const CHECKING_COLOR: Color = Color::Blue; - const DONE_COLOR: Color = Color::Green; - const PENDING_COLOR: Color = Color::Red; - - pub fn build(stdout: &'a mut StdoutLock<'lock>, term_width: u16) -> io::Result { - clear_terminal(stdout)?; - stdout.write_all("Checking all exercises…\n".as_bytes())?; - - // Legend - stdout.write_all(b"Color of exercise number: ")?; - stdout.queue(SetForegroundColor(Self::CHECKING_COLOR))?; - stdout.write_all(b"Checking")?; - stdout.queue(ResetColor)?; - stdout.write_all(b" - ")?; - stdout.queue(SetForegroundColor(Self::DONE_COLOR))?; - stdout.write_all(b"Done")?; - stdout.queue(ResetColor)?; - stdout.write_all(b" - ")?; - stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?; - stdout.write_all(b"Pending")?; - stdout.queue(ResetColor)?; - stdout.write_all(b"\n")?; - - // Exercise numbers with up to 3 digits. - // +1 because the last column doesn't end with a whitespace. - let n_cols = usize::from(term_width + 1) / 4; - - Ok(Self { stdout, n_cols }) - } - - pub fn update(&mut self, progresses: &[CheckProgress]) -> io::Result<()> { - self.stdout.queue(MoveTo(0, 2))?; - - let mut exercise_num = 1; - for exercise_progress in progresses { - match exercise_progress { - CheckProgress::None => (), - CheckProgress::Checking => { - self.stdout - .queue(SetForegroundColor(Self::CHECKING_COLOR))?; - } - CheckProgress::Done => { - self.stdout.queue(SetForegroundColor(Self::DONE_COLOR))?; - } - CheckProgress::Pending => { - self.stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?; - } - } - - write!(self.stdout, "{exercise_num:<3}")?; - self.stdout.queue(ResetColor)?; - - if exercise_num != progresses.len() { - if exercise_num % self.n_cols == 0 { - self.stdout.write_all(b"\n")?; - } else { - self.stdout.write_all(b" ")?; - } - - exercise_num += 1; - } - } - - self.stdout.flush() - } -} - pub struct ProgressCounter<'a, 'lock> { stdout: &'a mut StdoutLock<'lock>, total: usize, @@ -185,6 +110,38 @@ impl Drop for ProgressCounter<'_, '_> { } } +pub struct CheckProgressVisualizer<'a, 'lock>(ProgressCounter<'a, 'lock>); + +impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> { + pub fn build(stdout: &'a mut StdoutLock<'lock>, total: usize) -> io::Result { + clear_terminal(stdout)?; + stdout.write_all("Checking all exercises…\n".as_bytes())?; + + Ok(Self(ProgressCounter::new(stdout, total)?)) + } + + fn checked(&mut self, exercise_name: &str) -> io::Result<()> { + self.0.stdout.queue(ResetColor)?; + self.0.stdout.write_all(exercise_name.as_bytes())?; + self.0.stdout.queue(Clear(ClearType::UntilNewLine))?; + + self.0.stdout.write_all(b"\n")?; + self.0.increment() + } + + pub fn done(&mut self, exercise_name: &str) -> io::Result<()> { + self.0.stdout.queue(SetForegroundColor(Color::Green))?; + self.0.stdout.write_all(b"\r DONE ")?; + self.checked(exercise_name) + } + + pub fn pending(&mut self, exercise_name: &str) -> io::Result<()> { + self.0.stdout.queue(SetForegroundColor(Color::Red))?; + self.0.stdout.write_all(b"\rPENDING ")?; + self.checked(exercise_name) + } +} + pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, progress: u32,