cargo/core/
shell.rs

1use std::fmt;
2use std::io::prelude::*;
3
4use termcolor::Color::{Cyan, Green, Red, Yellow};
5use termcolor::{self, Color, ColorSpec, StandardStream, WriteColor};
6
7use crate::util::errors::CargoResult;
8
9/// The requested verbosity of output.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum Verbosity {
12    Verbose,
13    Normal,
14    Quiet,
15}
16
17/// An abstraction around a `Write`able object that remembers preferences for output verbosity and
18/// color.
19pub struct Shell {
20    /// the `Write`able object, either with or without color support (represented by different enum
21    /// variants)
22    err: ShellOut,
23    /// How verbose messages should be
24    verbosity: Verbosity,
25    /// Flag that indicates the current line needs to be cleared before
26    /// printing. Used when a progress bar is currently displayed.
27    needs_clear: bool,
28}
29
30impl fmt::Debug for Shell {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self.err {
33            ShellOut::Write(_) => f
34                .debug_struct("Shell")
35                .field("verbosity", &self.verbosity)
36                .finish(),
37            ShellOut::Stream { color_choice, .. } => f
38                .debug_struct("Shell")
39                .field("verbosity", &self.verbosity)
40                .field("color_choice", &color_choice)
41                .finish(),
42        }
43    }
44}
45
46/// A `Write`able object, either with or without color support
47enum ShellOut {
48    /// A plain write object without color support
49    Write(Box<dyn Write>),
50    /// Color-enabled stdio, with information on whether color should be used
51    Stream {
52        stream: StandardStream,
53        tty: bool,
54        color_choice: ColorChoice,
55    },
56}
57
58/// Whether messages should use color output
59#[derive(Debug, PartialEq, Clone, Copy)]
60pub enum ColorChoice {
61    /// Force color output
62    Always,
63    /// Force disable color output
64    Never,
65    /// Intelligently guess whether to use color output
66    CargoAuto,
67}
68
69impl Shell {
70    /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose
71    /// output.
72    pub fn new() -> Shell {
73        Shell {
74            err: ShellOut::Stream {
75                stream: StandardStream::stderr(ColorChoice::CargoAuto.to_termcolor_color_choice()),
76                color_choice: ColorChoice::CargoAuto,
77                tty: atty::is(atty::Stream::Stderr),
78            },
79            verbosity: Verbosity::Verbose,
80            needs_clear: false,
81        }
82    }
83
84    /// Creates a shell from a plain writable object, with no color, and max verbosity.
85    pub fn from_write(out: Box<dyn Write>) -> Shell {
86        Shell {
87            err: ShellOut::Write(out),
88            verbosity: Verbosity::Verbose,
89            needs_clear: false,
90        }
91    }
92
93    /// Prints a message, where the status will have `color` color, and can be justified. The
94    /// messages follows without color.
95    fn print(
96        &mut self,
97        status: &dyn fmt::Display,
98        message: Option<&dyn fmt::Display>,
99        color: Color,
100        justified: bool,
101    ) -> CargoResult<()> {
102        match self.verbosity {
103            Verbosity::Quiet => Ok(()),
104            _ => {
105                if self.needs_clear {
106                    self.err_erase_line();
107                }
108                self.err.print(status, message, color, justified)
109            }
110        }
111    }
112
113    pub fn stdout_println(&mut self, message: impl fmt::Display) {
114        if self.needs_clear {
115            self.err_erase_line();
116        }
117        println!("{}", message);
118    }
119
120    /// Sets whether the next print should clear the current line.
121    pub fn set_needs_clear(&mut self, needs_clear: bool) {
122        self.needs_clear = needs_clear;
123    }
124
125    /// Returns `true` if the `needs_clear` flag is unset.
126    pub fn is_cleared(&self) -> bool {
127        !self.needs_clear
128    }
129
130    /// Returns the width of the terminal in spaces, if any.
131    pub fn err_width(&self) -> Option<usize> {
132        match self.err {
133            ShellOut::Stream { tty: true, .. } => imp::stderr_width(),
134            _ => None,
135        }
136    }
137
138    /// Returns `true` if stderr is a tty.
139    pub fn is_err_tty(&self) -> bool {
140        match self.err {
141            ShellOut::Stream { tty, .. } => tty,
142            _ => false,
143        }
144    }
145
146    /// Gets a reference to the underlying writer.
147    pub fn err(&mut self) -> &mut dyn Write {
148        if self.needs_clear {
149            self.err_erase_line();
150        }
151        self.err.as_write()
152    }
153
154    /// Erase from cursor to end of line.
155    pub fn err_erase_line(&mut self) {
156        if let ShellOut::Stream { tty: true, .. } = self.err {
157            imp::err_erase_line(self);
158            self.needs_clear = false;
159        }
160    }
161
162    /// Shortcut to right-align and color green a status message.
163    pub fn status<T, U>(&mut self, status: T, message: U) -> CargoResult<()>
164    where
165        T: fmt::Display,
166        U: fmt::Display,
167    {
168        self.print(&status, Some(&message), Green, true)
169    }
170
171    pub fn status_header<T>(&mut self, status: T) -> CargoResult<()>
172    where
173        T: fmt::Display,
174    {
175        self.print(&status, None, Cyan, true)
176    }
177
178    /// Shortcut to right-align a status message.
179    pub fn status_with_color<T, U>(
180        &mut self,
181        status: T,
182        message: U,
183        color: Color,
184    ) -> CargoResult<()>
185    where
186        T: fmt::Display,
187        U: fmt::Display,
188    {
189        self.print(&status, Some(&message), color, true)
190    }
191
192    /// Runs the callback only if we are in verbose mode.
193    pub fn verbose<F>(&mut self, mut callback: F) -> CargoResult<()>
194    where
195        F: FnMut(&mut Shell) -> CargoResult<()>,
196    {
197        match self.verbosity {
198            Verbosity::Verbose => callback(self),
199            _ => Ok(()),
200        }
201    }
202
203    /// Runs the callback if we are not in verbose mode.
204    pub fn concise<F>(&mut self, mut callback: F) -> CargoResult<()>
205    where
206        F: FnMut(&mut Shell) -> CargoResult<()>,
207    {
208        match self.verbosity {
209            Verbosity::Verbose => Ok(()),
210            _ => callback(self),
211        }
212    }
213
214    /// Prints a red 'error' message.
215    pub fn error<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
216        if self.needs_clear {
217            self.err_erase_line();
218        }
219        self.err.print(&"error", Some(&message), Red, false)
220    }
221
222    /// Prints an amber 'warning' message.
223    pub fn warn<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
224        match self.verbosity {
225            Verbosity::Quiet => Ok(()),
226            _ => self.print(&"warning", Some(&message), Yellow, false),
227        }
228    }
229
230    /// Prints a cyan 'note' message.
231    pub fn note<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
232        self.print(&"note", Some(&message), Cyan, false)
233    }
234
235    /// Updates the verbosity of the shell.
236    pub fn set_verbosity(&mut self, verbosity: Verbosity) {
237        self.verbosity = verbosity;
238    }
239
240    /// Gets the verbosity of the shell.
241    pub fn verbosity(&self) -> Verbosity {
242        self.verbosity
243    }
244
245    /// Updates the color choice (always, never, or auto) from a string..
246    pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> {
247        if let ShellOut::Stream {
248            ref mut stream,
249            ref mut color_choice,
250            ..
251        } = self.err
252        {
253            let cfg = match color {
254                Some("always") => ColorChoice::Always,
255                Some("never") => ColorChoice::Never,
256
257                Some("auto") | None => ColorChoice::CargoAuto,
258
259                Some(arg) => anyhow::bail!(
260                    "argument for --color must be auto, always, or \
261                     never, but found `{}`",
262                    arg
263                ),
264            };
265            *color_choice = cfg;
266            *stream = StandardStream::stderr(cfg.to_termcolor_color_choice());
267        }
268        Ok(())
269    }
270
271    /// Gets the current color choice.
272    ///
273    /// If we are not using a color stream, this will always return `Never`, even if the color
274    /// choice has been set to something else.
275    pub fn color_choice(&self) -> ColorChoice {
276        match self.err {
277            ShellOut::Stream { color_choice, .. } => color_choice,
278            ShellOut::Write(_) => ColorChoice::Never,
279        }
280    }
281
282    /// Whether the shell supports color.
283    pub fn supports_color(&self) -> bool {
284        match &self.err {
285            ShellOut::Write(_) => false,
286            ShellOut::Stream { stream, .. } => stream.supports_color(),
287        }
288    }
289
290    /// Prints a message and translates ANSI escape code into console colors.
291    pub fn print_ansi(&mut self, message: &[u8]) -> CargoResult<()> {
292        if self.needs_clear {
293            self.err_erase_line();
294        }
295        #[cfg(windows)]
296        {
297            if let ShellOut::Stream { stream, .. } = &mut self.err {
298                ::fwdansi::write_ansi(stream, message)?;
299                return Ok(());
300            }
301        }
302        self.err().write_all(message)?;
303        Ok(())
304    }
305}
306
307impl Default for Shell {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313impl ShellOut {
314    /// Prints out a message with a status. The status comes first, and is bold plus the given
315    /// color. The status can be justified, in which case the max width that will right align is
316    /// 12 chars.
317    fn print(
318        &mut self,
319        status: &dyn fmt::Display,
320        message: Option<&dyn fmt::Display>,
321        color: Color,
322        justified: bool,
323    ) -> CargoResult<()> {
324        match *self {
325            ShellOut::Stream { ref mut stream, .. } => {
326                stream.reset()?;
327                stream.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
328                if justified {
329                    write!(stream, "{:>12}", status)?;
330                } else {
331                    write!(stream, "{}", status)?;
332                    stream.set_color(ColorSpec::new().set_bold(true))?;
333                    write!(stream, ":")?;
334                }
335                stream.reset()?;
336                match message {
337                    Some(message) => writeln!(stream, " {}", message)?,
338                    None => write!(stream, " ")?,
339                }
340            }
341            ShellOut::Write(ref mut w) => {
342                if justified {
343                    write!(w, "{:>12}", status)?;
344                } else {
345                    write!(w, "{}:", status)?;
346                }
347                match message {
348                    Some(message) => writeln!(w, " {}", message)?,
349                    None => write!(w, " ")?,
350                }
351            }
352        }
353        Ok(())
354    }
355
356    /// Gets this object as a `io::Write`.
357    fn as_write(&mut self) -> &mut dyn Write {
358        match *self {
359            ShellOut::Stream { ref mut stream, .. } => stream,
360            ShellOut::Write(ref mut w) => w,
361        }
362    }
363}
364
365impl ColorChoice {
366    /// Converts our color choice to termcolor's version.
367    fn to_termcolor_color_choice(self) -> termcolor::ColorChoice {
368        match self {
369            ColorChoice::Always => termcolor::ColorChoice::Always,
370            ColorChoice::Never => termcolor::ColorChoice::Never,
371            ColorChoice::CargoAuto => {
372                if atty::is(atty::Stream::Stderr) {
373                    termcolor::ColorChoice::Auto
374                } else {
375                    termcolor::ColorChoice::Never
376                }
377            }
378        }
379    }
380}
381
382#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
383mod imp {
384    use super::Shell;
385    use std::mem;
386
387    pub fn stderr_width() -> Option<usize> {
388        unsafe {
389            let mut winsize: libc::winsize = mem::zeroed();
390            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
391            // as c_uint but ioctl wants c_ulong.
392            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
393                return None;
394            }
395            if winsize.ws_col > 0 {
396                Some(winsize.ws_col as usize)
397            } else {
398                None
399            }
400        }
401    }
402
403    pub fn err_erase_line(shell: &mut Shell) {
404        // This is the "EL - Erase in Line" sequence. It clears from the cursor
405        // to the end of line.
406        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
407        let _ = shell.err.as_write().write_all(b"\x1B[K");
408    }
409}
410
411#[cfg(all(
412    unix,
413    not(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))
414))]
415mod imp {
416    pub(super) use super::default_err_erase_line as err_erase_line;
417
418    pub fn stderr_width() -> Option<usize> {
419        None
420    }
421}
422
423#[cfg(windows)]
424mod imp {
425    use std::{cmp, mem, ptr};
426    use winapi::um::fileapi::*;
427    use winapi::um::handleapi::*;
428    use winapi::um::processenv::*;
429    use winapi::um::winbase::*;
430    use winapi::um::wincon::*;
431    use winapi::um::winnt::*;
432
433    pub(super) use super::default_err_erase_line as err_erase_line;
434
435    pub fn stderr_width() -> Option<usize> {
436        unsafe {
437            let stdout = GetStdHandle(STD_ERROR_HANDLE);
438            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
439            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
440                return Some((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
441            }
442
443            // On mintty/msys/cygwin based terminals, the above fails with
444            // INVALID_HANDLE_VALUE. Use an alternate method which works
445            // in that case as well.
446            let h = CreateFileA(
447                "CONOUT$\0".as_ptr() as *const CHAR,
448                GENERIC_READ | GENERIC_WRITE,
449                FILE_SHARE_READ | FILE_SHARE_WRITE,
450                ptr::null_mut(),
451                OPEN_EXISTING,
452                0,
453                ptr::null_mut(),
454            );
455            if h == INVALID_HANDLE_VALUE {
456                return None;
457            }
458
459            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
460            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
461            CloseHandle(h);
462            if rc != 0 {
463                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
464                // Unfortunately cygwin/mintty does not set the size of the
465                // backing console to match the actual window size. This
466                // always reports a size of 80 or 120 (not sure what
467                // determines that). Use a conservative max of 60 which should
468                // work in most circumstances. ConEmu does some magic to
469                // resize the console correctly, but there's no reasonable way
470                // to detect which kind of terminal we are running in, or if
471                // GetConsoleScreenBufferInfo returns accurate information.
472                return Some(cmp::min(60, width));
473            }
474            None
475        }
476    }
477}
478
479#[cfg(any(
480    all(
481        unix,
482        not(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))
483    ),
484    windows,
485))]
486fn default_err_erase_line(shell: &mut Shell) {
487    if let Some(max_width) = imp::stderr_width() {
488        let blank = " ".repeat(max_width);
489        drop(write!(shell.err.as_write(), "{}\r", blank));
490    }
491}