substrate_manager/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::SubstrateResult;
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 diagnostics with
17    /// 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 verbose
97    /// output.
98    pub fn new() -> Shell {
99        let auto_clr = ColorChoice::CargoAuto;
100        Shell {
101            output: ShellOut::Stream {
102                stdout: StandardStream::stdout(
103                    auto_clr.to_termcolor_color_choice(atty::Stream::Stdout),
104                ),
105                stderr: StandardStream::stderr(
106                    auto_clr.to_termcolor_color_choice(atty::Stream::Stderr),
107                ),
108                color_choice: ColorChoice::CargoAuto,
109                stderr_tty: atty::is(atty::Stream::Stderr),
110            },
111            verbosity: Verbosity::Verbose,
112            needs_clear: false,
113        }
114    }
115
116    /// Creates a shell from a plain writable object, with no color, and max verbosity.
117    pub fn from_write(out: Box<dyn Write>) -> Shell {
118        Shell {
119            output: ShellOut::Write(out),
120            verbosity: Verbosity::Verbose,
121            needs_clear: false,
122        }
123    }
124
125    /// Prints a message, where the status will have `color` color, and can be justified. The
126    /// messages follows without color.
127    fn print(
128        &mut self,
129        status: &dyn fmt::Display,
130        message: Option<&dyn fmt::Display>,
131        color: Color,
132        justified: bool,
133    ) -> SubstrateResult<()> {
134        match self.verbosity {
135            Verbosity::Quiet => Ok(()),
136            _ => {
137                if self.needs_clear {
138                    self.err_erase_line();
139                }
140                self.output
141                    .message_stderr(status, message, color, justified)
142            }
143        }
144    }
145
146    /// Sets whether the next print should clear the current line.
147    pub fn set_needs_clear(&mut self, needs_clear: bool) {
148        self.needs_clear = needs_clear;
149    }
150
151    /// Returns `true` if the `needs_clear` flag is unset.
152    pub fn is_cleared(&self) -> bool {
153        !self.needs_clear
154    }
155
156    /// Returns the width of the terminal in spaces, if any.
157    pub fn err_width(&self) -> TtyWidth {
158        match self.output {
159            ShellOut::Stream {
160                stderr_tty: true, ..
161            } => imp::stderr_width(),
162            _ => TtyWidth::NoTty,
163        }
164    }
165
166    /// Returns `true` if stderr is a tty.
167    pub fn is_err_tty(&self) -> bool {
168        match self.output {
169            ShellOut::Stream { stderr_tty, .. } => stderr_tty,
170            _ => false,
171        }
172    }
173
174    /// Gets a reference to the underlying stdout writer.
175    pub fn out(&mut self) -> &mut dyn Write {
176        if self.needs_clear {
177            self.err_erase_line();
178        }
179        self.output.stdout()
180    }
181
182    /// Gets a reference to the underlying stderr writer.
183    pub fn err(&mut self) -> &mut dyn Write {
184        if self.needs_clear {
185            self.err_erase_line();
186        }
187        self.output.stderr()
188    }
189
190    /// Erase from cursor to end of line.
191    pub fn err_erase_line(&mut self) {
192        if self.err_supports_color() {
193            imp::err_erase_line(self);
194            self.needs_clear = false;
195        }
196    }
197
198    /// Shortcut to right-align and color green a status message.
199    pub fn status<T, U>(&mut self, status: T, message: U) -> SubstrateResult<()>
200    where
201        T: fmt::Display,
202        U: fmt::Display,
203    {
204        self.print(&status, Some(&message), Green, true)
205    }
206
207    pub fn status_header<T>(&mut self, status: T) -> SubstrateResult<()>
208    where
209        T: fmt::Display,
210    {
211        self.print(&status, None, Cyan, true)
212    }
213
214    /// Shortcut to right-align a status message.
215    pub fn status_with_color<T, U>(
216        &mut self,
217        status: T,
218        message: U,
219        color: Color,
220    ) -> SubstrateResult<()>
221    where
222        T: fmt::Display,
223        U: fmt::Display,
224    {
225        self.print(&status, Some(&message), color, true)
226    }
227
228    /// Runs the callback only if we are in verbose mode.
229    pub fn verbose<F>(&mut self, mut callback: F) -> SubstrateResult<()>
230    where
231        F: FnMut(&mut Shell) -> SubstrateResult<()>,
232    {
233        match self.verbosity {
234            Verbosity::Verbose => callback(self),
235            _ => Ok(()),
236        }
237    }
238
239    /// Runs the callback if we are not in verbose mode.
240    pub fn concise<F>(&mut self, mut callback: F) -> SubstrateResult<()>
241    where
242        F: FnMut(&mut Shell) -> SubstrateResult<()>,
243    {
244        match self.verbosity {
245            Verbosity::Verbose => Ok(()),
246            _ => callback(self),
247        }
248    }
249
250    /// Prints a red 'error' message.
251    pub fn error<T: fmt::Display>(&mut self, message: T) -> SubstrateResult<()> {
252        if self.needs_clear {
253            self.err_erase_line();
254        }
255        self.output
256            .message_stderr(&"error", Some(&message), Red, false)
257    }
258
259    /// Prints an amber 'warning' message.
260    pub fn warn<T: fmt::Display>(&mut self, message: T) -> SubstrateResult<()> {
261        match self.verbosity {
262            Verbosity::Quiet => Ok(()),
263            _ => self.print(&"warning", Some(&message), Yellow, false),
264        }
265    }
266
267    /// Prints a cyan 'note' message.
268    pub fn note<T: fmt::Display>(&mut self, message: T) -> SubstrateResult<()> {
269        self.print(&"note", Some(&message), Cyan, false)
270    }
271
272    /// Updates the verbosity of the shell.
273    pub fn set_verbosity(&mut self, verbosity: Verbosity) {
274        self.verbosity = verbosity;
275    }
276
277    /// Gets the verbosity of the shell.
278    pub fn verbosity(&self) -> Verbosity {
279        self.verbosity
280    }
281
282    /// Updates the color choice (always, never, or auto) from a string..
283    pub fn set_color_choice(&mut self, color: Option<&str>) -> SubstrateResult<()> {
284        if let ShellOut::Stream {
285            ref mut stdout,
286            ref mut stderr,
287            ref mut color_choice,
288            ..
289        } = self.output
290        {
291            let cfg = match color {
292                Some("always") => ColorChoice::Always,
293                Some("never") => ColorChoice::Never,
294
295                Some("auto") | None => ColorChoice::CargoAuto,
296
297                Some(arg) => anyhow::bail!(
298                    "argument for --color must be auto, always, or \
299                     never, but found `{}`",
300                    arg
301                ),
302            };
303            *color_choice = cfg;
304            *stdout = StandardStream::stdout(cfg.to_termcolor_color_choice(atty::Stream::Stdout));
305            *stderr = StandardStream::stderr(cfg.to_termcolor_color_choice(atty::Stream::Stderr));
306        }
307        Ok(())
308    }
309
310    /// Gets the current color choice.
311    ///
312    /// If we are not using a color stream, this will always return `Never`, even if the color
313    /// choice has been set to something else.
314    pub fn color_choice(&self) -> ColorChoice {
315        match self.output {
316            ShellOut::Stream { color_choice, .. } => color_choice,
317            ShellOut::Write(_) => ColorChoice::Never,
318        }
319    }
320
321    /// Whether the shell supports color.
322    pub fn err_supports_color(&self) -> bool {
323        match &self.output {
324            ShellOut::Write(_) => false,
325            ShellOut::Stream { stderr, .. } => stderr.supports_color(),
326        }
327    }
328
329    pub fn out_supports_color(&self) -> bool {
330        match &self.output {
331            ShellOut::Write(_) => false,
332            ShellOut::Stream { stdout, .. } => stdout.supports_color(),
333        }
334    }
335
336    /// Write a styled fragment
337    ///
338    /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output.
339    pub fn write_stdout(
340        &mut self,
341        fragment: impl fmt::Display,
342        color: &ColorSpec,
343    ) -> SubstrateResult<()> {
344        self.output.write_stdout(fragment, color)
345    }
346
347    /// Write a styled fragment
348    ///
349    /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output.
350    pub fn write_stderr(
351        &mut self,
352        fragment: impl fmt::Display,
353        color: &ColorSpec,
354    ) -> SubstrateResult<()> {
355        self.output.write_stderr(fragment, color)
356    }
357
358    /// Prints a message to stderr and translates ANSI escape code into console colors.
359    pub fn print_ansi_stderr(&mut self, message: &[u8]) -> SubstrateResult<()> {
360        if self.needs_clear {
361            self.err_erase_line();
362        }
363        #[cfg(windows)]
364        {
365            if let ShellOut::Stream { stderr, .. } = &mut self.output {
366                ::fwdansi::write_ansi(stderr, message)?;
367                return Ok(());
368            }
369        }
370        self.err().write_all(message)?;
371        Ok(())
372    }
373
374    /// Prints a message to stdout and translates ANSI escape code into console colors.
375    pub fn print_ansi_stdout(&mut self, message: &[u8]) -> SubstrateResult<()> {
376        if self.needs_clear {
377            self.err_erase_line();
378        }
379        #[cfg(windows)]
380        {
381            if let ShellOut::Stream { stdout, .. } = &mut self.output {
382                ::fwdansi::write_ansi(stdout, message)?;
383                return Ok(());
384            }
385        }
386        self.out().write_all(message)?;
387        Ok(())
388    }
389
390    pub fn print_json<T: serde::ser::Serialize>(&mut self, obj: &T) -> SubstrateResult<()> {
391        // Path may fail to serialize to JSON ...
392        let encoded = serde_json::to_string(&obj)?;
393        // ... but don't fail due to a closed pipe.
394        drop(writeln!(self.out(), "{}", encoded));
395        Ok(())
396    }
397}
398
399impl Default for Shell {
400    fn default() -> Self {
401        Self::new()
402    }
403}
404
405impl ShellOut {
406    /// Prints out a message with a status. The status comes first, and is bold plus the given
407    /// color. The status can be justified, in which case the max width that will right align is
408    /// 12 chars.
409    fn message_stderr(
410        &mut self,
411        status: &dyn fmt::Display,
412        message: Option<&dyn fmt::Display>,
413        color: Color,
414        justified: bool,
415    ) -> SubstrateResult<()> {
416        match *self {
417            ShellOut::Stream { ref mut stderr, .. } => {
418                stderr.reset()?;
419                stderr.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
420                if justified {
421                    write!(stderr, "{:>12}", status)?;
422                } else {
423                    write!(stderr, "{}", status)?;
424                    stderr.set_color(ColorSpec::new().set_bold(true))?;
425                    write!(stderr, ":")?;
426                }
427                stderr.reset()?;
428                match message {
429                    Some(message) => writeln!(stderr, " {}", message)?,
430                    None => write!(stderr, " ")?,
431                }
432            }
433            ShellOut::Write(ref mut w) => {
434                if justified {
435                    write!(w, "{:>12}", status)?;
436                } else {
437                    write!(w, "{}:", status)?;
438                }
439                match message {
440                    Some(message) => writeln!(w, " {}", message)?,
441                    None => write!(w, " ")?,
442                }
443            }
444        }
445        Ok(())
446    }
447
448    /// Write a styled fragment
449    fn write_stdout(
450        &mut self,
451        fragment: impl fmt::Display,
452        color: &ColorSpec,
453    ) -> SubstrateResult<()> {
454        match *self {
455            ShellOut::Stream { ref mut stdout, .. } => {
456                stdout.reset()?;
457                stdout.set_color(&color)?;
458                write!(stdout, "{}", fragment)?;
459                stdout.reset()?;
460            }
461            ShellOut::Write(ref mut w) => {
462                write!(w, "{}", fragment)?;
463            }
464        }
465        Ok(())
466    }
467
468    /// Write a styled fragment
469    fn write_stderr(
470        &mut self,
471        fragment: impl fmt::Display,
472        color: &ColorSpec,
473    ) -> SubstrateResult<()> {
474        match *self {
475            ShellOut::Stream { ref mut stderr, .. } => {
476                stderr.reset()?;
477                stderr.set_color(&color)?;
478                write!(stderr, "{}", fragment)?;
479                stderr.reset()?;
480            }
481            ShellOut::Write(ref mut w) => {
482                write!(w, "{}", fragment)?;
483            }
484        }
485        Ok(())
486    }
487
488    /// Gets stdout as a `io::Write`.
489    fn stdout(&mut self) -> &mut dyn Write {
490        match *self {
491            ShellOut::Stream { ref mut stdout, .. } => stdout,
492            ShellOut::Write(ref mut w) => w,
493        }
494    }
495
496    /// Gets stderr as a `io::Write`.
497    fn stderr(&mut self) -> &mut dyn Write {
498        match *self {
499            ShellOut::Stream { ref mut stderr, .. } => stderr,
500            ShellOut::Write(ref mut w) => w,
501        }
502    }
503}
504
505impl ColorChoice {
506    /// Converts our color choice to termcolor's version.
507    fn to_termcolor_color_choice(self, stream: atty::Stream) -> termcolor::ColorChoice {
508        match self {
509            ColorChoice::Always => termcolor::ColorChoice::Always,
510            ColorChoice::Never => termcolor::ColorChoice::Never,
511            ColorChoice::CargoAuto => {
512                if atty::is(stream) {
513                    termcolor::ColorChoice::Auto
514                } else {
515                    termcolor::ColorChoice::Never
516                }
517            }
518        }
519    }
520}
521
522#[cfg(unix)]
523mod imp {
524    use super::{Shell, TtyWidth};
525    use std::mem;
526
527    pub fn stderr_width() -> TtyWidth {
528        unsafe {
529            let mut winsize: libc::winsize = mem::zeroed();
530            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
531            // as c_uint but ioctl wants c_ulong.
532            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
533                return TtyWidth::NoTty;
534            }
535            if winsize.ws_col > 0 {
536                TtyWidth::Known(winsize.ws_col as usize)
537            } else {
538                TtyWidth::NoTty
539            }
540        }
541    }
542
543    pub fn err_erase_line(shell: &mut Shell) {
544        // This is the "EL - Erase in Line" sequence. It clears from the cursor
545        // to the end of line.
546        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
547        let _ = shell.output.stderr().write_all(b"\x1B[K");
548    }
549}
550
551#[cfg(windows)]
552mod imp {
553    use std::{cmp, mem, ptr};
554    use winapi::um::fileapi::*;
555    use winapi::um::handleapi::*;
556    use winapi::um::processenv::*;
557    use winapi::um::winbase::*;
558    use winapi::um::wincon::*;
559    use winapi::um::winnt::*;
560
561    pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth};
562
563    pub fn stderr_width() -> TtyWidth {
564        unsafe {
565            let stdout = GetStdHandle(STD_ERROR_HANDLE);
566            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
567            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
568                return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
569            }
570
571            // On mintty/msys/cygwin based terminals, the above fails with
572            // INVALID_HANDLE_VALUE. Use an alternate method which works
573            // in that case as well.
574            let h = CreateFileA(
575                "CONOUT$\0".as_ptr() as *const CHAR,
576                GENERIC_READ | GENERIC_WRITE,
577                FILE_SHARE_READ | FILE_SHARE_WRITE,
578                ptr::null_mut(),
579                OPEN_EXISTING,
580                0,
581                ptr::null_mut(),
582            );
583            if h == INVALID_HANDLE_VALUE {
584                return TtyWidth::NoTty;
585            }
586
587            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
588            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
589            CloseHandle(h);
590            if rc != 0 {
591                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
592                // Unfortunately cygwin/mintty does not set the size of the
593                // backing console to match the actual window size. This
594                // always reports a size of 80 or 120 (not sure what
595                // determines that). Use a conservative max of 60 which should
596                // work in most circumstances. ConEmu does some magic to
597                // resize the console correctly, but there's no reasonable way
598                // to detect which kind of terminal we are running in, or if
599                // GetConsoleScreenBufferInfo returns accurate information.
600                return TtyWidth::Guess(cmp::min(60, width));
601            }
602
603            TtyWidth::NoTty
604        }
605    }
606}
607
608#[cfg(windows)]
609fn default_err_erase_line(shell: &mut Shell) {
610    match imp::stderr_width() {
611        TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
612            let blank = " ".repeat(max_width);
613            drop(write!(shell.output.stderr(), "{}\r", blank));
614        }
615        _ => (),
616    }
617}