crossbundle_tools/types/common/
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, Eq)]
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, Eq, 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
345impl Default for Shell {
346    fn default() -> Self {
347        Self::new()
348    }
349}
350
351impl ShellOut {
352    /// Prints out a message with a status. The status comes first, and is bold plus the
353    /// given color. The status can be justified, in which case the max width that
354    /// will right align is 12 chars.
355    fn message_stderr(
356        &mut self,
357        status: &dyn fmt::Display,
358        message: Option<&dyn fmt::Display>,
359        color: Color,
360        justified: bool,
361    ) -> Result<()> {
362        match *self {
363            ShellOut::Stream { ref mut stderr, .. } => {
364                stderr.reset()?;
365                stderr.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
366                if justified {
367                    write!(stderr, "{:>12}", status)?;
368                } else {
369                    write!(stderr, "{}", status)?;
370                    stderr.set_color(ColorSpec::new().set_bold(true))?;
371                    if message.is_some() {
372                        write!(stderr, ":")?;
373                    }
374                }
375                stderr.reset()?;
376                match message {
377                    Some(message) => writeln!(stderr, " {}", message)?,
378                    None => writeln!(stderr, " ")?,
379                }
380            }
381            ShellOut::Write(ref mut w) => {
382                if justified {
383                    write!(w, "{:>12}", status)?;
384                } else {
385                    write!(w, "{}:", status)?;
386                }
387                match message {
388                    Some(message) => writeln!(w, " {}", message)?,
389                    None => write!(w, " ")?,
390                }
391            }
392        }
393        Ok(())
394    }
395
396    /// Gets stdout as a `io::Write`.
397    fn stdout(&mut self) -> &mut dyn Write {
398        match *self {
399            ShellOut::Stream { ref mut stdout, .. } => stdout,
400            ShellOut::Write(ref mut w) => w,
401        }
402    }
403
404    /// Gets stderr as a `io::Write`.
405    fn stderr(&mut self) -> &mut dyn Write {
406        match *self {
407            ShellOut::Stream { ref mut stderr, .. } => stderr,
408            ShellOut::Write(ref mut w) => w,
409        }
410    }
411}
412
413impl ColorChoice {
414    /// Converts our color choice to termcolor's version.
415    fn to_termcolor_color_choice(self) -> termcolor::ColorChoice {
416        match self {
417            ColorChoice::Always => termcolor::ColorChoice::Always,
418            ColorChoice::Never => termcolor::ColorChoice::Never,
419            ColorChoice::CargoAuto => {
420                if atty::is(atty::Stream::Stderr) {
421                    termcolor::ColorChoice::Auto
422                } else {
423                    termcolor::ColorChoice::Never
424                }
425            }
426        }
427    }
428}
429
430#[cfg(unix)]
431mod imp {
432    use super::{Shell, TtyWidth};
433    use std::mem;
434
435    pub fn stderr_width() -> TtyWidth {
436        unsafe {
437            let mut winsize: libc::winsize = mem::zeroed();
438            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
439            // as c_uint but ioctl wants c_ulong.
440            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
441                return TtyWidth::NoTty;
442            }
443            if winsize.ws_col > 0 {
444                TtyWidth::Known(winsize.ws_col as usize)
445            } else {
446                TtyWidth::NoTty
447            }
448        }
449    }
450
451    pub fn err_erase_line(shell: &mut Shell) {
452        // This is the "EL - Erase in Line" sequence. It clears from the cursor
453        // to the end of line.
454        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
455        let _ = shell.output.stderr().write_all(b"\x1B[K");
456    }
457}
458
459#[cfg(windows)]
460mod imp {
461    use std::{cmp, mem, ptr};
462    use winapi::um::fileapi::*;
463    use winapi::um::handleapi::*;
464    use winapi::um::processenv::*;
465    use winapi::um::winbase::*;
466    use winapi::um::wincon::*;
467    use winapi::um::winnt::*;
468
469    pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth};
470
471    pub fn stderr_width() -> TtyWidth {
472        unsafe {
473            let stdout = GetStdHandle(STD_ERROR_HANDLE);
474            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
475            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
476                return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
477            }
478
479            // On mintty/msys/cygwin based terminals, the above fails with
480            // INVALID_HANDLE_VALUE. Use an alternate method which works
481            // in that case as well.
482            let h = CreateFileA(
483                "CONOUT$\0".as_ptr() as *const CHAR,
484                GENERIC_READ | GENERIC_WRITE,
485                FILE_SHARE_READ | FILE_SHARE_WRITE,
486                ptr::null_mut(),
487                OPEN_EXISTING,
488                0,
489                ptr::null_mut(),
490            );
491            if h == INVALID_HANDLE_VALUE {
492                return TtyWidth::NoTty;
493            }
494
495            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
496            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
497            CloseHandle(h);
498            if rc != 0 {
499                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
500                // Unfortunately cygwin/mintty does not set the size of the
501                // backing console to match the actual window size. This
502                // always reports a size of 80 or 120 (not sure what
503                // determines that). Use a conservative max of 60 which should
504                // work in most circumstances. ConEmu does some magic to
505                // resize the console correctly, but there's no reasonable way
506                // to detect which kind of terminal we are running in, or if
507                // GetConsoleScreenBufferInfo returns accurate information.
508                return TtyWidth::Guess(cmp::min(60, width));
509            }
510
511            TtyWidth::NoTty
512        }
513    }
514}
515
516#[cfg(windows)]
517fn default_err_erase_line(shell: &mut Shell) {
518    match imp::stderr_width() {
519        TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
520            let blank = " ".repeat(max_width);
521            drop(write!(shell.output.stderr(), "{}\r", blank));
522        }
523        _ => (),
524    }
525}