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,
        ));
    }
}