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