| use std::io; |
| use std::io::IsTerminal; |
| use std::io::Write; |
| use std::sync::Mutex; |
| use std::time::Duration; |
|
|
| use super::CheckStatus; |
|
|
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| |
| |
| 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( |
| true, |
| Some("xterm-256color"), |
| true, |
| )); |
| } |
|
|
| #[test] |
| fn progress_is_quiet_for_non_tty() { |
| assert!(!should_show_progress( |
| false, |
| Some("xterm-256color"), |
| false, |
| )); |
| } |
|
|
| #[test] |
| fn progress_is_quiet_for_dumb_terminal() { |
| assert!(!should_show_progress( |
| false, |
| Some("dumb"), |
| true, |
| )); |
| } |
|
|
| #[test] |
| fn progress_is_shown_for_human_tty_output() { |
| assert!(should_show_progress( |
| false, |
| Some("xterm-256color"), |
| true, |
| )); |
| } |
| } |
|
|