cargo_compete/
shell.rs

1use indicatif::ProgressDrawTarget;
2use snowchains_core::{color_spec, web::StatusCodeColor};
3use std::{
4    fmt,
5    io::{self, BufRead, Write},
6};
7use strum::{EnumString, EnumVariantNames};
8use termcolor::{BufferedStandardStream, Color, NoColor, WriteColor};
9
10pub struct Shell {
11    input: ShellIn,
12    output: ShellOut,
13    needs_clear: bool,
14}
15
16impl Shell {
17    pub fn new() -> Self {
18        Self {
19            input: ShellIn::stdin(),
20            output: ShellOut::stream(),
21            needs_clear: false,
22        }
23    }
24
25    pub fn from_read_write(rdr: Box<dyn BufRead>, wtr: Box<dyn Write>) -> Self {
26        Self {
27            input: ShellIn::Reader(rdr),
28            output: ShellOut::Write(NoColor::new(wtr)),
29            needs_clear: false,
30        }
31    }
32
33    pub(crate) fn progress_draw_target(&self) -> ProgressDrawTarget {
34        if self.output.stderr_tty() {
35            ProgressDrawTarget::stderr()
36        } else {
37            ProgressDrawTarget::hidden()
38        }
39    }
40
41    pub(crate) fn out(&mut self) -> &mut dyn Write {
42        self.output.stdout()
43    }
44
45    pub fn err(&mut self) -> &mut dyn WriteColor {
46        self.output.stderr()
47    }
48
49    pub(crate) fn set_color_choice(&mut self, color: ColorChoice) {
50        self.output.set_color_choice(color);
51    }
52
53    pub(crate) fn warn(&mut self, message: impl fmt::Display) -> io::Result<()> {
54        if self.needs_clear {
55            self.err_erase_line();
56        }
57
58        let stderr = self.err();
59
60        stderr.set_color(color_spec!(Bold, Fg(Color::Yellow)))?;
61        write!(stderr, "warning:")?;
62        stderr.reset()?;
63
64        writeln!(stderr, " {message}")?;
65
66        stderr.flush()
67    }
68
69    pub(crate) fn status(
70        &mut self,
71        status: impl fmt::Display,
72        message: impl fmt::Display,
73    ) -> io::Result<()> {
74        self.status_with_color(status, message, Color::Green)
75    }
76
77    pub(crate) fn status_with_color(
78        &mut self,
79        status: impl fmt::Display,
80        message: impl fmt::Display,
81        color: Color,
82    ) -> io::Result<()> {
83        if self.needs_clear {
84            self.err_erase_line();
85        }
86        self.output.message_stderr(status, message, color)
87    }
88
89    fn err_erase_line(&mut self) {
90        if let ShellOut::Stream {
91            stderr,
92            stderr_tty: true,
93            ..
94        } = &mut self.output
95        {
96            err_erase_line(stderr);
97            let _ = stderr.flush();
98            self.needs_clear = false;
99        }
100
101        #[cfg(unix)]
102        fn err_erase_line(stderr: &mut impl Write) {
103            let _ = stderr.write_all(b"\x1B[K");
104        }
105
106        #[cfg(windows)]
107        fn err_erase_line(stderr: &mut impl Write) {
108            if let Some((width, _)) = term_size::dimensions_stderr() {
109                let _ = write!(stderr, "{}\r", " ".repeat(width));
110            }
111        }
112    }
113
114    pub(crate) fn read_reply(&mut self, prompt: &str) -> io::Result<String> {
115        if self.needs_clear {
116            self.err_erase_line();
117        }
118
119        let stderr = self.err();
120
121        write!(stderr, "{prompt}")?;
122        stderr.flush()?;
123        self.input.read_reply()
124    }
125
126    pub(crate) fn read_password(&mut self, prompt: &str) -> io::Result<String> {
127        if self.needs_clear {
128            self.err_erase_line();
129        }
130
131        let stderr = self.err();
132
133        write!(stderr, "{prompt}")?;
134        stderr.flush()?;
135        self.input.read_password()
136    }
137}
138
139impl Default for Shell {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145impl snowchains_core::web::Shell for Shell {
146    fn progress_draw_target(&self) -> ProgressDrawTarget {
147        self.progress_draw_target()
148    }
149
150    fn print_ansi(&mut self, message: &[u8]) -> io::Result<()> {
151        fwdansi::write_ansi(self.err(), message)
152    }
153
154    fn warn<T: fmt::Display>(&mut self, message: T) -> io::Result<()> {
155        self.warn(message)
156    }
157
158    fn on_request(&mut self, req: &reqwest::blocking::Request) -> io::Result<()> {
159        if let ShellOut::Stream {
160            stderr,
161            stderr_tty: true,
162            ..
163        } = &mut self.output
164        {
165            stderr.set_color(color_spec!(Bold, Fg(Color::Cyan)))?;
166            write!(stderr, "{:>12}", req.method())?;
167            stderr.reset()?;
168            write!(stderr, " {} ...\r", req.url())?;
169            stderr.flush()?;
170
171            self.needs_clear = true;
172        }
173        Ok(())
174    }
175
176    fn on_response(
177        &mut self,
178        _: &reqwest::blocking::Response,
179        _: StatusCodeColor,
180    ) -> io::Result<()> {
181        if self.needs_clear {
182            self.err_erase_line();
183        }
184        Ok(())
185    }
186}
187
188enum ShellIn {
189    Tty,
190    PipedStdin,
191    Reader(Box<dyn BufRead>),
192}
193
194impl ShellIn {
195    fn stdin() -> Self {
196        if atty::is(atty::Stream::Stdin) {
197            Self::Tty
198        } else {
199            Self::PipedStdin
200        }
201    }
202}
203
204impl ShellIn {
205    fn read_reply(&mut self) -> io::Result<String> {
206        match self {
207            Self::Tty | Self::PipedStdin => rprompt::read_reply(),
208            Self::Reader(r) => rpassword::read_password_with_reader(Some(r)),
209        }
210    }
211
212    fn read_password(&mut self) -> io::Result<String> {
213        match self {
214            Self::Tty => rpassword::read_password_from_tty(None),
215            Self::PipedStdin => rprompt::read_reply(),
216            Self::Reader(r) => rpassword::read_password_with_reader(Some(r)),
217        }
218    }
219}
220
221enum ShellOut {
222    Write(NoColor<Box<dyn Write>>),
223    Stream {
224        stdout: BufferedStandardStream,
225        stderr: BufferedStandardStream,
226        stderr_tty: bool,
227    },
228}
229
230impl ShellOut {
231    fn stream() -> Self {
232        Self::Stream {
233            stdout: BufferedStandardStream::stdout(if atty::is(atty::Stream::Stdout) {
234                termcolor::ColorChoice::Auto
235            } else {
236                termcolor::ColorChoice::Never
237            }),
238            stderr: BufferedStandardStream::stderr(if atty::is(atty::Stream::Stderr) {
239                termcolor::ColorChoice::Auto
240            } else {
241                termcolor::ColorChoice::Never
242            }),
243            stderr_tty: atty::is(atty::Stream::Stderr),
244        }
245    }
246
247    fn stdout(&mut self) -> &mut dyn Write {
248        match self {
249            Self::Write(wtr) => wtr,
250            Self::Stream { stdout, .. } => stdout,
251        }
252    }
253
254    fn stderr(&mut self) -> &mut dyn WriteColor {
255        match self {
256            Self::Write(wtr) => wtr,
257            Self::Stream { stderr, .. } => stderr,
258        }
259    }
260
261    fn stderr_tty(&self) -> bool {
262        match *self {
263            Self::Write(_) => false,
264            Self::Stream { stderr_tty, .. } => stderr_tty,
265        }
266    }
267
268    fn set_color_choice(&mut self, color: ColorChoice) {
269        if let Self::Stream { stdout, stderr, .. } = self {
270            let _ = stdout.flush();
271            let _ = stderr.flush();
272
273            *stdout = BufferedStandardStream::stdout(
274                color.to_termcolor_color_choice(atty::Stream::Stdout),
275            );
276
277            *stderr = BufferedStandardStream::stderr(
278                color.to_termcolor_color_choice(atty::Stream::Stderr),
279            );
280        }
281    }
282
283    fn message_stderr(
284        &mut self,
285        status: impl fmt::Display,
286        message: impl fmt::Display,
287        color: Color,
288    ) -> io::Result<()> {
289        let stderr = self.stderr();
290
291        stderr.set_color(color_spec!(Bold, Fg(color)))?;
292        write!(stderr, "{status:>12}")?;
293        stderr.reset()?;
294
295        writeln!(stderr, " {message}")?;
296        stderr.flush()
297    }
298}
299
300#[derive(EnumString, EnumVariantNames, strum::Display, Clone, Copy, Debug)]
301#[strum(serialize_all = "kebab-case")]
302pub enum ColorChoice {
303    Auto,
304    Always,
305    Never,
306}
307
308impl ColorChoice {
309    fn to_termcolor_color_choice(self, stream: atty::Stream) -> termcolor::ColorChoice {
310        match (self, atty::is(stream)) {
311            (Self::Auto, true) => termcolor::ColorChoice::Auto,
312            (Self::Always, _) => termcolor::ColorChoice::Always,
313            _ => termcolor::ColorChoice::Never,
314        }
315    }
316}