File size: 3,679 Bytes
afa0cbf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | use std::io;
use std::io::IsTerminal;
use std::io::Write;
use std::sync::Mutex;
use std::time::Duration;
use super::CheckStatus;
/// Receives check lifecycle events while doctor builds the final report.
///
/// Progress implementations must not write to stdout. The final report owns
/// stdout so JSON and redirected human reports stay clean.
pub(super) trait DoctorProgress: Send + Sync {
fn begin(&self, label: &'static str);
fn heartbeat(&self, label: &'static str, elapsed: Duration);
fn finish(&self, label: &'static str, status: CheckStatus);
fn settle(&self);
}
/// Selects the progress implementation for the current output mode.
///
/// JSON output is always quiet so stdout remains valid JSON. Human output uses a
/// transient stderr line only for interactive terminals, then clears it before
/// the final report is printed.
pub(super) fn doctor_progress(json: bool) -> std::sync::Arc<dyn DoctorProgress> {
if should_show_progress(
json,
std::env::var("TERM").ok().as_deref(),
io::stderr().is_terminal(),
) {
std::sync::Arc::new(StderrProgress::default())
} else {
std::sync::Arc::new(QuietProgress)
}
}
fn should_show_progress(json: bool, term: Option<&str>, stderr_is_tty: bool) -> bool {
!json && stderr_is_tty && term != Some("dumb")
}
struct QuietProgress;
impl DoctorProgress for QuietProgress {
fn begin(&self, _label: &'static str) {}
fn heartbeat(&self, _label: &'static str, _elapsed: Duration) {}
fn finish(&self, _label: &'static str, _status: CheckStatus) {}
fn settle(&self) {}
}
#[derive(Default)]
struct StderrProgress {
state: Mutex<StderrProgressState>,
}
#[derive(Default)]
struct StderrProgressState {
wrote_line: bool,
}
impl StderrProgress {
fn render(&self, message: String) {
let Ok(mut state) = self.state.lock() else {
return;
};
let mut stderr = io::stderr().lock();
let _ = write!(stderr, "\r\x1b[2K{message}");
let _ = stderr.flush();
state.wrote_line = true;
}
}
impl DoctorProgress for StderrProgress {
fn begin(&self, label: &'static str) {
self.render(format!("Checking {label}..."));
}
fn heartbeat(&self, label: &'static str, elapsed: Duration) {
self.render(format!("Still checking {label}... {}s", elapsed.as_secs()));
}
fn finish(&self, _label: &'static str, _status: CheckStatus) {}
fn settle(&self) {
let Ok(mut state) = self.state.lock() else {
return;
};
if !state.wrote_line {
return;
}
let mut stderr = io::stderr().lock();
let _ = write!(stderr, "\r\x1b[2K");
let _ = stderr.flush();
state.wrote_line = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn progress_is_quiet_for_json() {
assert!(!should_show_progress(
/*json*/ true,
Some("xterm-256color"),
/*stderr_is_tty*/ true,
));
}
#[test]
fn progress_is_quiet_for_non_tty() {
assert!(!should_show_progress(
/*json*/ false,
Some("xterm-256color"),
/*stderr_is_tty*/ false,
));
}
#[test]
fn progress_is_quiet_for_dumb_terminal() {
assert!(!should_show_progress(
/*json*/ false,
Some("dumb"),
/*stderr_is_tty*/ true,
));
}
#[test]
fn progress_is_shown_for_human_tty_output() {
assert!(should_show_progress(
/*json*/ false,
Some("xterm-256color"),
/*stderr_is_tty*/ true,
));
}
}
|