Skip to content

Commit aa83fd6

Browse files
committed
Show a progress bar when running check_all
Replace the "Progress: xxx/yyy" with a progress bar when checking all the exercises
1 parent e2f7734 commit aa83fd6

File tree

2 files changed

+138
-30
lines changed

2 files changed

+138
-30
lines changed

src/app_state.rs

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
use anyhow::{bail, Context, Result};
2+
use crossterm::{
3+
queue,
4+
style::{Print, ResetColor, SetForegroundColor},
5+
terminal,
6+
};
27
use std::{
38
env,
49
fs::{File, OpenOptions},
@@ -16,7 +21,7 @@ use crate::{
1621
embedded::EMBEDDED_FILES,
1722
exercise::{Exercise, RunnableExercise},
1823
info_file::ExerciseInfo,
19-
term,
24+
term::{self, progress_bar_with_success},
2025
};
2126

2227
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
@@ -428,10 +433,16 @@ impl AppState {
428433
// No more exercises
429434
break;
430435
};
431-
if tx
432-
.send((exercise_ind, exercise.run_exercise(None, &this.cmd_runner)))
433-
.is_err()
434-
{
436+
437+
// Notify the progress bar that this exercise is pending
438+
if tx.send((exercise_ind, None)).is_err() {
439+
break;
440+
};
441+
442+
let result = exercise.run_exercise(None, &this.cmd_runner);
443+
444+
// Notify the progress bar that this exercise is done
445+
if tx.send((exercise_ind, Some(result))).is_err() {
435446
break;
436447
}
437448
}
@@ -443,28 +454,68 @@ impl AppState {
443454
// there are `tx` clones, i.e. threads)
444455
drop(tx);
445456

457+
// Print the legend
458+
queue!(
459+
stdout,
460+
Print("Color legend: "),
461+
SetForegroundColor(term::PROGRESS_FAILED_COLOR),
462+
Print("Failure"),
463+
ResetColor,
464+
Print(" - "),
465+
SetForegroundColor(term::PROGRESS_SUCCESS_COLOR),
466+
Print("Success"),
467+
ResetColor,
468+
Print(" - "),
469+
SetForegroundColor(term::PROGRESS_PENDING_COLOR),
470+
Print("Checking"),
471+
ResetColor,
472+
Print("\n"),
473+
)
474+
.unwrap();
475+
// We expect at least a few "pending" notifications shortly, so don't
476+
// bother printing the initial state of the progress bar and flushing
477+
// stdout
478+
479+
let line_width = terminal::size().unwrap().0;
446480
let mut results = vec![AllExercisesResult::Pending; n_exercises];
447-
let mut checked_count = 0;
448-
write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?;
449-
stdout.flush()?;
481+
let mut pending = 0;
482+
let mut success = 0;
483+
let mut failed = 0;
484+
450485
while let Ok((exercise_ind, result)) = rx.recv() {
451-
results[exercise_ind] = result.map_or_else(
452-
|_| AllExercisesResult::Error,
453-
|success| {
454-
checked_count += 1;
455-
if success {
456-
AllExercisesResult::Success
457-
} else {
458-
AllExercisesResult::Failed
459-
}
460-
},
461-
);
486+
match result {
487+
None => {
488+
pending += 1;
489+
}
490+
Some(Err(_)) => {
491+
results[exercise_ind] = AllExercisesResult::Error;
492+
}
493+
Some(Ok(true)) => {
494+
results[exercise_ind] = AllExercisesResult::Success;
495+
pending -= 1;
496+
success += 1;
497+
}
498+
Some(Ok(false)) => {
499+
results[exercise_ind] = AllExercisesResult::Failed;
500+
pending -= 1;
501+
failed += 1;
502+
}
503+
}
462504

463-
write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?;
505+
write!(stdout, "\r").unwrap();
506+
progress_bar_with_success(
507+
stdout,
508+
pending,
509+
failed,
510+
success,
511+
n_exercises as u16,
512+
line_width,
513+
)
514+
.unwrap();
464515
stdout.flush()?;
465516
}
466517

467-
Ok::<_, io::Error>((checked_count, results))
518+
Ok::<_, io::Error>((success, results))
468519
})?;
469520

470521
// If we got an error while checking all exercises in parallel,

src/term.rs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ use std::{
99
io::{self, BufRead, StdoutLock, Write},
1010
};
1111

12+
pub const PROGRESS_FAILED_COLOR: Color = Color::Red;
13+
pub const PROGRESS_SUCCESS_COLOR: Color = Color::Green;
14+
pub const PROGRESS_PENDING_COLOR: Color = Color::Blue;
15+
1216
pub struct MaxLenWriter<'a, 'b> {
1317
pub stdout: &'a mut StdoutLock<'b>,
1418
len: usize,
@@ -85,15 +89,26 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> {
8589
}
8690
}
8791

88-
/// Terminal progress bar to be used when not using Ratataui.
92+
/// Simple terminal progress bar
8993
pub fn progress_bar<'a>(
9094
writer: &mut impl CountedWrite<'a>,
9195
progress: u16,
9296
total: u16,
9397
line_width: u16,
98+
) -> io::Result<()> {
99+
progress_bar_with_success(writer, 0, 0, progress, total, line_width)
100+
}
101+
/// Terminal progress bar with three states (pending + failed + success)
102+
pub fn progress_bar_with_success<'a>(
103+
writer: &mut impl CountedWrite<'a>,
104+
pending: u16,
105+
failed: u16,
106+
success: u16,
107+
total: u16,
108+
line_width: u16,
94109
) -> io::Result<()> {
95110
debug_assert!(total < 1000);
96-
debug_assert!(progress <= total);
111+
debug_assert!((pending + failed + success) <= total);
97112

98113
const PREFIX: &[u8] = b"Progress: [";
99114
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
@@ -104,25 +119,67 @@ pub fn progress_bar<'a>(
104119
if line_width < MIN_LINE_WIDTH {
105120
writer.write_ascii(b"Progress: ")?;
106121
// Integers are in ASCII.
107-
return writer.write_ascii(format!("{progress}/{total}").as_bytes());
122+
return writer.write_ascii(format!("{}/{total}", failed + success).as_bytes());
108123
}
109124

110125
let stdout = writer.stdout();
111126
stdout.write_all(PREFIX)?;
112127

113128
let width = line_width - WRAPPER_WIDTH;
114-
let filled = (width * progress) / total;
129+
let mut failed_end = (width * failed) / total;
130+
let mut success_end = (width * (failed + success)) / total;
131+
let mut pending_end = (width * (failed + success + pending)) / total;
132+
133+
// In case the range boundaries overlap, "pending" has priority over both
134+
// "failed" and "success" (don't show the bar as "complete" when we are
135+
// still checking some things).
136+
// "Failed" has priority over "success" (don't show 100% success if we
137+
// have some failures, at the risk of showing 100% failures even with
138+
// a few successes).
139+
//
140+
// "Failed" already has priority over "success" because it's displayed
141+
// first. But "pending" is last so we need to fix "success"/"failed".
142+
if pending > 0 {
143+
pending_end = pending_end.max(1);
144+
if pending_end == success_end {
145+
success_end -= 1;
146+
}
147+
if pending_end == failed_end {
148+
failed_end -= 1;
149+
}
115150

116-
stdout.queue(SetForegroundColor(Color::Green))?;
117-
for _ in 0..filled {
151+
// This will replace the last character of the "pending" range with
152+
// the arrow char ('>'). This ensures that even if the progress bar
153+
// is filled (everything either done or pending), we'll still see
154+
// the '>' as long as we are not fully done.
155+
pending_end -= 1;
156+
}
157+
158+
if failed > 0 {
159+
stdout.queue(SetForegroundColor(PROGRESS_FAILED_COLOR))?;
160+
for _ in 0..failed_end {
161+
stdout.write_all(b"#")?;
162+
}
163+
}
164+
165+
stdout.queue(SetForegroundColor(PROGRESS_SUCCESS_COLOR))?;
166+
for _ in failed_end..success_end {
118167
stdout.write_all(b"#")?;
119168
}
120169

121-
if filled < width {
170+
if pending > 0 {
171+
stdout.queue(SetForegroundColor(PROGRESS_PENDING_COLOR))?;
172+
173+
for _ in success_end..pending_end {
174+
stdout.write_all(b"#")?;
175+
}
176+
}
177+
178+
if pending_end < width {
122179
stdout.write_all(b">")?;
123180
}
124181

125-
let width_minus_filled = width - filled;
182+
let width_minus_filled = width - pending_end;
126183
if width_minus_filled > 1 {
127184
let red_part_width = width_minus_filled - 1;
128185
stdout.queue(SetForegroundColor(Color::Red))?;
@@ -133,7 +190,7 @@ pub fn progress_bar<'a>(
133190

134191
stdout.queue(SetForegroundColor(Color::Reset))?;
135192

136-
write!(stdout, "] {progress:>3}/{total}")
193+
write!(stdout, "] {:>3}/{}", failed + success, total)
137194
}
138195

139196
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {

0 commit comments

Comments
 (0)