Skip to main content

cargo_ndk/
shell.rs

1#![allow(dead_code)]
2
3use std::fmt;
4use std::io::prelude::*;
5
6use is_terminal::IsTerminal;
7use termcolor::Color::{Cyan, Green, Red, Yellow};
8use termcolor::{self, Color, ColorSpec, StandardStream, WriteColor};
9
10pub enum TtyWidth {
11    NoTty,
12    Known(usize),
13    Guess(usize),
14}
15
16impl TtyWidth {
17    /// Returns the width used by progress bars for the tty.
18    pub fn progress_max_width(&self) -> Option<usize> {
19        match *self {
20            TtyWidth::NoTty => None,
21            TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
22        }
23    }
24}
25
26/// The requested verbosity of output.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum Verbosity {
29    VeryVerbose,
30    Verbose,
31    Normal,
32    Quiet,
33}
34
35impl From<u8> for Verbosity {
36    fn from(value: u8) -> Self {
37        match value {
38            0 => Verbosity::Quiet,
39            1 => Verbosity::Normal,
40            2 => Verbosity::Verbose,
41            _ => Verbosity::VeryVerbose,
42        }
43    }
44}
45
46/// An abstraction around console output that remembers preferences for output
47/// verbosity and color.
48pub struct Shell {
49    /// Wrapper around stdout/stderr. This helps with supporting sending
50    /// output to a memory buffer which is useful for tests.
51    output: ShellOut,
52    /// How verbose messages should be.
53    verbosity: Verbosity,
54    /// Flag that indicates the current line needs to be cleared before
55    /// printing. Used when a progress bar is currently displayed.
56    needs_clear: bool,
57}
58
59impl fmt::Debug for Shell {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self.output {
62            ShellOut::Write(_) => f
63                .debug_struct("Shell")
64                .field("verbosity", &self.verbosity)
65                .finish(),
66            ShellOut::Stream { color_choice, .. } => f
67                .debug_struct("Shell")
68                .field("verbosity", &self.verbosity)
69                .field("color_choice", &color_choice)
70                .finish(),
71        }
72    }
73}
74
75/// A `Write`able object, either with or without color support
76enum ShellOut {
77    /// A plain write object without color support
78    Write(Box<dyn Write>),
79    /// Color-enabled stdio, with information on whether color should be used
80    Stream {
81        stdout: StandardStream,
82        stderr: StandardStream,
83        stderr_tty: bool,
84        color_choice: ColorChoice,
85    },
86}
87
88/// Whether messages should use color output
89#[derive(Debug, PartialEq, Clone, Copy)]
90pub enum ColorChoice {
91    /// Force color output
92    Always,
93    /// Force disable color output
94    Never,
95    /// Intelligently guess whether to use color output
96    CargoAuto,
97}
98
99impl Shell {
100    /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose
101    /// output.
102    pub fn new() -> Shell {
103        let auto_clr = ColorChoice::CargoAuto;
104        Shell {
105            output: ShellOut::Stream {
106                stdout: StandardStream::stdout(auto_clr.to_termcolor_color_choice(Stream::Stdout)),
107                stderr: StandardStream::stderr(auto_clr.to_termcolor_color_choice(Stream::Stderr)),
108                color_choice: ColorChoice::CargoAuto,
109                stderr_tty: std::io::stderr().is_terminal(),
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    ) -> anyhow::Result<()> {
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    pub fn reset_err(&mut self) -> anyhow::Result<()> {
191        match self.output {
192            ShellOut::Stream { ref mut stderr, .. } => {
193                stderr.reset()?;
194            }
195            ShellOut::Write(_) => {
196                // No-op for non-color streams
197            }
198        }
199        Ok(())
200    }
201
202    /// Erase from cursor to end of line.
203    pub fn err_erase_line(&mut self) {
204        if self.err_supports_color() {
205            imp::err_erase_line(self);
206            self.needs_clear = false;
207        }
208    }
209
210    /// Shortcut to right-align and color green a status message.
211    pub fn status<T, U>(&mut self, status: T, message: U) -> anyhow::Result<()>
212    where
213        T: fmt::Display,
214        U: fmt::Display,
215    {
216        self.print(&status, Some(&message), Green, true)
217    }
218
219    pub fn status_header<T>(&mut self, status: T) -> anyhow::Result<()>
220    where
221        T: fmt::Display,
222    {
223        self.print(&status, None, Cyan, true)
224    }
225
226    /// Shortcut to right-align a status message.
227    pub fn status_with_color<T, U>(
228        &mut self,
229        status: T,
230        message: U,
231        color: Color,
232    ) -> anyhow::Result<()>
233    where
234        T: fmt::Display,
235        U: fmt::Display,
236    {
237        self.print(&status, Some(&message), color, true)
238    }
239
240    /// Runs the callback only if we are in very verbose mode.
241    pub fn very_verbose<F>(&mut self, mut callback: F) -> anyhow::Result<()>
242    where
243        F: FnMut(&mut Shell) -> anyhow::Result<()>,
244    {
245        match self.verbosity {
246            Verbosity::VeryVerbose => callback(self),
247            _ => Ok(()),
248        }
249    }
250
251    /// Runs the callback only if we are in verbose mode.
252    pub fn verbose<F>(&mut self, mut callback: F) -> anyhow::Result<()>
253    where
254        F: FnMut(&mut Shell) -> anyhow::Result<()>,
255    {
256        match self.verbosity {
257            Verbosity::Verbose | Verbosity::VeryVerbose => callback(self),
258            _ => Ok(()),
259        }
260    }
261
262    /// Runs the callback if we are not in verbose mode.
263    pub fn concise<F>(&mut self, mut callback: F) -> anyhow::Result<()>
264    where
265        F: FnMut(&mut Shell) -> anyhow::Result<()>,
266    {
267        match self.verbosity {
268            Verbosity::Verbose | Verbosity::VeryVerbose => Ok(()),
269            _ => callback(self),
270        }
271    }
272
273    /// Prints a red 'error' message.
274    pub fn error<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
275        if self.needs_clear {
276            self.err_erase_line();
277        }
278        self.output
279            .message_stderr(&"error", Some(&message), Red, false)
280    }
281
282    /// Prints an amber 'warning' message.
283    pub fn warn<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
284        match self.verbosity {
285            Verbosity::Quiet => Ok(()),
286            _ => self.print(&"warning", Some(&message), Yellow, false),
287        }
288    }
289
290    /// Prints a cyan 'note' message.
291    pub fn note<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
292        self.print(&"note", Some(&message), Cyan, false)
293    }
294
295    /// Updates the verbosity of the shell.
296    pub fn set_verbosity(&mut self, verbosity: Verbosity) {
297        self.verbosity = verbosity;
298    }
299
300    /// Gets the verbosity of the shell.
301    pub fn verbosity(&self) -> Verbosity {
302        self.verbosity
303    }
304
305    /// Updates the color choice (always, never, or auto) from a string..
306    pub fn set_color_choice(&mut self, color: Option<&str>) -> anyhow::Result<()> {
307        if let ShellOut::Stream {
308            ref mut stdout,
309            ref mut stderr,
310            ref mut color_choice,
311            ..
312        } = self.output
313        {
314            let cfg = match color {
315                Some("always") => ColorChoice::Always,
316                Some("never") => ColorChoice::Never,
317
318                Some("auto") | None => ColorChoice::CargoAuto,
319
320                Some(arg) => anyhow::bail!(
321                    "argument for --color must be auto, always, or \
322                     never, but found `{}`",
323                    arg
324                ),
325            };
326            *color_choice = cfg;
327            *stdout = StandardStream::stdout(cfg.to_termcolor_color_choice(Stream::Stdout));
328            *stderr = StandardStream::stderr(cfg.to_termcolor_color_choice(Stream::Stderr));
329        }
330        Ok(())
331    }
332
333    /// Gets the current color choice.
334    ///
335    /// If we are not using a color stream, this will always return `Never`, even if the color
336    /// choice has been set to something else.
337    pub fn color_choice(&self) -> ColorChoice {
338        match self.output {
339            ShellOut::Stream { color_choice, .. } => color_choice,
340            ShellOut::Write(_) => ColorChoice::Never,
341        }
342    }
343
344    /// Whether the shell supports color.
345    pub fn err_supports_color(&self) -> bool {
346        match &self.output {
347            ShellOut::Write(_) => false,
348            ShellOut::Stream { stderr, .. } => stderr.supports_color(),
349        }
350    }
351
352    pub fn out_supports_color(&self) -> bool {
353        match &self.output {
354            ShellOut::Write(_) => false,
355            ShellOut::Stream { stdout, .. } => stdout.supports_color(),
356        }
357    }
358
359    /// Write a styled fragment
360    ///
361    /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output.
362    pub fn write_stdout(
363        &mut self,
364        fragment: impl fmt::Display,
365        color: &ColorSpec,
366    ) -> anyhow::Result<()> {
367        self.output.write_stdout(fragment, color)
368    }
369
370    /// Write a styled fragment
371    ///
372    /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output.
373    pub fn write_stderr(
374        &mut self,
375        fragment: impl fmt::Display,
376        color: &ColorSpec,
377    ) -> anyhow::Result<()> {
378        self.output.write_stderr(fragment, color)
379    }
380
381    /// Prints a message to stderr and translates ANSI escape code into console colors.
382    pub fn print_ansi_stderr(&mut self, message: &[u8]) -> anyhow::Result<()> {
383        if self.needs_clear {
384            self.err_erase_line();
385        }
386        #[cfg(windows)]
387        {
388            if let ShellOut::Stream { stderr, .. } = &mut self.output {
389                ::fwdansi::write_ansi(stderr, message)?;
390                return Ok(());
391            }
392        }
393        self.err().write_all(message)?;
394        Ok(())
395    }
396
397    /// Prints a message to stdout and translates ANSI escape code into console colors.
398    pub fn print_ansi_stdout(&mut self, message: &[u8]) -> anyhow::Result<()> {
399        if self.needs_clear {
400            self.err_erase_line();
401        }
402        #[cfg(windows)]
403        {
404            if let ShellOut::Stream { stdout, .. } = &mut self.output {
405                ::fwdansi::write_ansi(stdout, message)?;
406                return Ok(());
407            }
408        }
409        self.out().write_all(message)?;
410        Ok(())
411    }
412}
413
414impl Default for Shell {
415    fn default() -> Self {
416        Self::new()
417    }
418}
419
420impl ShellOut {
421    /// Prints out a message with a status. The status comes first, and is bold plus the given
422    /// color. The status can be justified, in which case the max width that will right align is
423    /// 12 chars.
424    fn message_stderr(
425        &mut self,
426        status: &dyn fmt::Display,
427        message: Option<&dyn fmt::Display>,
428        color: Color,
429        justified: bool,
430    ) -> anyhow::Result<()> {
431        match *self {
432            ShellOut::Stream { ref mut stderr, .. } => {
433                stderr.reset()?;
434                stderr.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
435                if justified {
436                    write!(stderr, "{status:>12}")?;
437                } else {
438                    write!(stderr, "{status}")?;
439                    stderr.set_color(ColorSpec::new().set_bold(true))?;
440                    write!(stderr, ":")?;
441                }
442                stderr.reset()?;
443                match message {
444                    Some(message) => {
445                        write!(stderr, " {message}")?;
446                        stderr.set_color(
447                            ColorSpec::new()
448                                .set_intense(true)
449                                .set_fg(Some(Color::Black)),
450                        )?;
451                        writeln!(stderr)?;
452                    }
453                    None => {
454                        stderr.set_color(
455                            ColorSpec::new()
456                                .set_intense(true)
457                                .set_fg(Some(Color::Black)),
458                        )?;
459                        write!(stderr, " ")?;
460                    }
461                }
462            }
463            ShellOut::Write(ref mut w) => {
464                if justified {
465                    write!(w, "{status:>12}")?;
466                } else {
467                    write!(w, "{status}:")?;
468                }
469                match message {
470                    Some(message) => writeln!(w, " {message}")?,
471                    None => write!(w, " ")?,
472                }
473                writeln!(w)?;
474            }
475        }
476        Ok(())
477    }
478
479    /// Write a styled fragment
480    fn write_stdout(
481        &mut self,
482        fragment: impl fmt::Display,
483        color: &ColorSpec,
484    ) -> anyhow::Result<()> {
485        match *self {
486            ShellOut::Stream { ref mut stdout, .. } => {
487                stdout.reset()?;
488                stdout.set_color(color)?;
489                write!(stdout, "{fragment}")?;
490                stdout.reset()?;
491            }
492            ShellOut::Write(ref mut w) => {
493                write!(w, "{fragment}")?;
494            }
495        }
496        Ok(())
497    }
498
499    /// Write a styled fragment
500    fn write_stderr(
501        &mut self,
502        fragment: impl fmt::Display,
503        color: &ColorSpec,
504    ) -> anyhow::Result<()> {
505        match *self {
506            ShellOut::Stream { ref mut stderr, .. } => {
507                stderr.reset()?;
508                stderr.set_color(color)?;
509                write!(stderr, "{fragment}")?;
510                stderr.reset()?;
511            }
512            ShellOut::Write(ref mut w) => {
513                write!(w, "{fragment}")?;
514            }
515        }
516        Ok(())
517    }
518
519    /// Gets stdout as a `io::Write`.
520    fn stdout(&mut self) -> &mut dyn Write {
521        match *self {
522            ShellOut::Stream { ref mut stdout, .. } => stdout,
523            ShellOut::Write(ref mut w) => w,
524        }
525    }
526
527    /// Gets stderr as a `io::Write`.
528    fn stderr(&mut self) -> &mut dyn Write {
529        match *self {
530            ShellOut::Stream { ref mut stderr, .. } => stderr,
531            ShellOut::Write(ref mut w) => w,
532        }
533    }
534}
535
536impl ColorChoice {
537    /// Converts our color choice to termcolor's version.
538    fn to_termcolor_color_choice(self, stream: Stream) -> termcolor::ColorChoice {
539        match self {
540            ColorChoice::Always => termcolor::ColorChoice::Always,
541            ColorChoice::Never => termcolor::ColorChoice::Never,
542            ColorChoice::CargoAuto => {
543                if stream.is_terminal() {
544                    termcolor::ColorChoice::Auto
545                } else {
546                    termcolor::ColorChoice::Never
547                }
548            }
549        }
550    }
551}
552
553enum Stream {
554    Stdout,
555    Stderr,
556}
557
558impl Stream {
559    fn is_terminal(&self) -> bool {
560        match self {
561            Self::Stdout => std::io::stdout().is_terminal(),
562            Self::Stderr => std::io::stderr().is_terminal(),
563        }
564    }
565}
566
567#[cfg(unix)]
568mod imp {
569    use super::{Shell, TtyWidth};
570    use std::mem;
571
572    pub fn stderr_width() -> TtyWidth {
573        unsafe {
574            let mut winsize: libc::winsize = mem::zeroed();
575            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
576            // as c_uint but ioctl wants c_ulong.
577            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
578                return TtyWidth::NoTty;
579            }
580            if winsize.ws_col > 0 {
581                TtyWidth::Known(winsize.ws_col as usize)
582            } else {
583                TtyWidth::NoTty
584            }
585        }
586    }
587
588    pub fn err_erase_line(shell: &mut Shell) {
589        // This is the "EL - Erase in Line" sequence. It clears from the cursor
590        // to the end of line.
591        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
592        let _ = shell.output.stderr().write_all(b"\x1B[K");
593    }
594}
595
596#[cfg(windows)]
597mod imp {
598    use std::{cmp, mem, ptr};
599
600    use windows_sys::Win32::Foundation::CloseHandle;
601    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
602    use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
603    use windows_sys::Win32::Storage::FileSystem::{
604        CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
605    };
606    use windows_sys::Win32::System::Console::{
607        CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle, STD_ERROR_HANDLE,
608    };
609    use windows_sys::core::PCSTR;
610
611    pub(super) use super::{TtyWidth, default_err_erase_line as err_erase_line};
612
613    pub fn stderr_width() -> TtyWidth {
614        unsafe {
615            let stdout = GetStdHandle(STD_ERROR_HANDLE);
616            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
617            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
618                return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
619            }
620
621            // On mintty/msys/cygwin based terminals, the above fails with
622            // INVALID_HANDLE_VALUE. Use an alternate method which works
623            // in that case as well.
624            let h = CreateFileA(
625                c"CONOUT$".as_ptr() as PCSTR,
626                GENERIC_READ | GENERIC_WRITE,
627                FILE_SHARE_READ | FILE_SHARE_WRITE,
628                ptr::null_mut(),
629                OPEN_EXISTING,
630                0,
631                std::ptr::null_mut(),
632            );
633            if h == INVALID_HANDLE_VALUE {
634                return TtyWidth::NoTty;
635            }
636
637            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
638            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
639            CloseHandle(h);
640            if rc != 0 {
641                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
642                // Unfortunately cygwin/mintty does not set the size of the
643                // backing console to match the actual window size. This
644                // always reports a size of 80 or 120 (not sure what
645                // determines that). Use a conservative max of 60 which should
646                // work in most circumstances. ConEmu does some magic to
647                // resize the console correctly, but there's no reasonable way
648                // to detect which kind of terminal we are running in, or if
649                // GetConsoleScreenBufferInfo returns accurate information.
650                return TtyWidth::Guess(cmp::min(60, width));
651            }
652
653            TtyWidth::NoTty
654        }
655    }
656}
657
658#[cfg(windows)]
659fn default_err_erase_line(shell: &mut Shell) {
660    match imp::stderr_width() {
661        TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
662            let blank = " ".repeat(max_width);
663            drop(write!(shell.output.stderr(), "{blank}\r"));
664        }
665        _ => (),
666    }
667}