mirror of
https://github.com/rust-lang/rustlings.git
synced 2026-08-14 14:26:56 +00:00
New check progress visualization
This commit is contained in:
parent
a93a10ac68
commit
b970e17bdf
@ -1,5 +1,5 @@
|
|||||||
use anyhow::{Context, Error, Result, bail};
|
use anyhow::{Context, Error, Result, bail};
|
||||||
use crossterm::{QueueableCommand, cursor, terminal};
|
use crossterm::{QueueableCommand, cursor};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fs::{File, OpenOptions},
|
fs::{File, OpenOptions},
|
||||||
@ -43,8 +43,6 @@ pub enum StateFileStatus {
|
|||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum CheckProgress {
|
pub enum CheckProgress {
|
||||||
None,
|
|
||||||
Checking,
|
|
||||||
Done,
|
Done,
|
||||||
Pending,
|
Pending,
|
||||||
}
|
}
|
||||||
@ -398,22 +396,18 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
||||||
let term_width = terminal::size()
|
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, self.exercises.len())?;
|
||||||
.context("Failed to get the terminal size")?
|
|
||||||
.0;
|
|
||||||
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
|
|
||||||
|
|
||||||
let next_exercise_ind = AtomicUsize::new(0);
|
let next_exercise_ind = &AtomicUsize::new(0);
|
||||||
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
|
let mut progresses = vec![None; self.exercises.len()];
|
||||||
|
|
||||||
thread::scope(|s| {
|
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()
|
let n_threads = thread::available_parallelism()
|
||||||
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
|
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
|
||||||
|
|
||||||
for _ in 0..n_threads {
|
for _ in 0..n_threads {
|
||||||
let exercise_progress_sender = exercise_progress_sender.clone();
|
let progress_sender = progress_sender.clone();
|
||||||
let next_exercise_ind = &next_exercise_ind;
|
|
||||||
let slf = &self;
|
let slf = &self;
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.spawn_scoped(s, move || {
|
.spawn_scoped(s, move || {
|
||||||
@ -424,25 +418,16 @@ impl AppState {
|
|||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
if exercise_progress_sender
|
if let Ok(success) = exercise.run_exercise(None, &slf.cmd_runner) {
|
||||||
.send((exercise_ind, CheckProgress::Checking))
|
let progress = if success {
|
||||||
.is_err()
|
CheckProgress::Done
|
||||||
{
|
} else {
|
||||||
break;
|
CheckProgress::Pending
|
||||||
}
|
};
|
||||||
|
|
||||||
let success = exercise.run_exercise(None, &slf.cmd_runner);
|
if progress_sender.send((exercise_ind, progress)).is_err() {
|
||||||
let progress = match success {
|
break;
|
||||||
Ok(true) => CheckProgress::Done,
|
}
|
||||||
Ok(false) => CheckProgress::Pending,
|
|
||||||
Err(_) => CheckProgress::None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if exercise_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 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() {
|
// TODO: Timeout
|
||||||
progresses[exercise_ind] = progress;
|
while let Ok((exercise_ind, progress)) = progress_receiver.recv() {
|
||||||
progress_visualizer.update(&progresses)?;
|
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>(())
|
Ok::<_, Error>(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut first_pending_exercise_ind = None;
|
let mut first_pending_exercise_ind = None;
|
||||||
for exercise_ind in 0..progresses.len() {
|
for (exercise_ind, progress) in progresses.into_iter().enumerate() {
|
||||||
match progresses[exercise_ind] {
|
match progress {
|
||||||
CheckProgress::Done => {
|
Some(CheckProgress::Done) => {
|
||||||
self.set_status(exercise_ind, true)?;
|
self.set_status(exercise_ind, true)?;
|
||||||
}
|
}
|
||||||
CheckProgress::Pending => {
|
Some(CheckProgress::Pending) => {
|
||||||
self.set_status(exercise_ind, false)?;
|
self.set_status(exercise_ind, false)?;
|
||||||
if first_pending_exercise_ind.is_none() {
|
if first_pending_exercise_ind.is_none() {
|
||||||
first_pending_exercise_ind = Some(exercise_ind);
|
first_pending_exercise_ind = Some(exercise_ind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CheckProgress::None | CheckProgress::Checking => {
|
None => {
|
||||||
// If we got an error while checking all exercises in parallel,
|
// If we got an error while checking all exercises in parallel,
|
||||||
// it could be because we exceeded the limit of open file descriptors.
|
// it could be because we exceeded the limit of open file descriptors.
|
||||||
// Therefore, try running exercises with errors sequentially.
|
// Therefore, try running exercises with errors sequentially.
|
||||||
progresses[exercise_ind] = CheckProgress::Checking;
|
|
||||||
progress_visualizer.update(&progresses)?;
|
|
||||||
|
|
||||||
let exercise = &self.exercises[exercise_ind];
|
let exercise = &self.exercises[exercise_ind];
|
||||||
let success = exercise.run_exercise(None, &self.cmd_runner)?;
|
let success = exercise.run_exercise(None, &self.cmd_runner)?;
|
||||||
if success {
|
if success {
|
||||||
progresses[exercise_ind] = CheckProgress::Done;
|
progress_visualizer.done(exercise.name)?;
|
||||||
} else {
|
} else {
|
||||||
progresses[exercise_ind] = CheckProgress::Pending;
|
progress_visualizer.pending(exercise.name)?;
|
||||||
if first_pending_exercise_ind.is_none() {
|
if first_pending_exercise_ind.is_none() {
|
||||||
first_pending_exercise_ind = Some(exercise_ind);
|
first_pending_exercise_ind = Some(exercise_ind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.set_status(exercise_ind, success)?;
|
self.set_status(exercise_ind, success)?;
|
||||||
progress_visualizer.update(&progresses)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
107
src/term.rs
107
src/term.rs
@ -9,8 +9,6 @@ use std::{
|
|||||||
io::{self, BufRead, StdoutLock, Write},
|
io::{self, BufRead, StdoutLock, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app_state::CheckProgress;
|
|
||||||
|
|
||||||
pub struct MaxLenWriter<'a, 'lock> {
|
pub struct MaxLenWriter<'a, 'lock> {
|
||||||
pub stdout: &'a mut StdoutLock<'lock>,
|
pub stdout: &'a mut StdoutLock<'lock>,
|
||||||
len: usize,
|
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<Self> {
|
|
||||||
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> {
|
pub struct ProgressCounter<'a, 'lock> {
|
||||||
stdout: &'a mut StdoutLock<'lock>,
|
stdout: &'a mut StdoutLock<'lock>,
|
||||||
total: usize,
|
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<Self> {
|
||||||
|
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>(
|
pub fn progress_bar<'a>(
|
||||||
writer: &mut impl CountedWrite<'a>,
|
writer: &mut impl CountedWrite<'a>,
|
||||||
progress: u32,
|
progress: u32,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user