cargo/core/
shell.rs

1use std::fmt;
2use std::io::prelude::*;
3use std::io::IsTerminal;
4
5use annotate_snippets::{Message, Renderer};
6use anstream::AutoStream;
7use anstyle::Style;
8
9use crate::util::errors::CargoResult;
10use crate::util::hostname;
11use crate::util::style::*;
12
13/// An abstraction around console output that remembers preferences for output
14/// verbosity and color.
15pub struct Shell {
16    /// Wrapper around stdout/stderr. This helps with supporting sending
17    /// output to a memory buffer which is useful for tests.
18    output: ShellOut,
19    /// How verbose messages should be.
20    verbosity: Verbosity,
21    /// Flag that indicates the current line needs to be cleared before
22    /// printing. Used when a progress bar is currently displayed.
23    needs_clear: bool,
24    hostname: Option<String>,
25}
26
27impl fmt::Debug for Shell {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self.output {
30            ShellOut::Write(_) => f
31                .debug_struct("Shell")
32                .field("verbosity", &self.verbosity)
33                .finish(),
34            ShellOut::Stream { color_choice, .. } => f
35                .debug_struct("Shell")
36                .field("verbosity", &self.verbosity)
37                .field("color_choice", &color_choice)
38                .finish(),
39        }
40    }
41}
42
43impl Shell {
44    /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose
45    /// output.
46    pub fn new() -> Shell {
47        let auto_clr = ColorChoice::CargoAuto;
48        let stdout_choice = auto_clr.to_anstream_color_choice();
49        let stderr_choice = auto_clr.to_anstream_color_choice();
50        Shell {
51            output: ShellOut::Stream {
52                stdout: AutoStream::new(std::io::stdout(), stdout_choice),
53                stderr: AutoStream::new(std::io::stderr(), stderr_choice),
54                color_choice: auto_clr,
55                hyperlinks: supports_hyperlinks(),
56                stderr_tty: std::io::stderr().is_terminal(),
57                stdout_unicode: supports_unicode(&std::io::stdout()),
58                stderr_unicode: supports_unicode(&std::io::stderr()),
59            },
60            verbosity: Verbosity::Verbose,
61            needs_clear: false,
62            hostname: None,
63        }
64    }
65
66    /// Creates a shell from a plain writable object, with no color, and max verbosity.
67    pub fn from_write(out: Box<dyn Write>) -> Shell {
68        Shell {
69            output: ShellOut::Write(AutoStream::never(out)), // strip all formatting on write
70            verbosity: Verbosity::Verbose,
71            needs_clear: false,
72            hostname: None,
73        }
74    }
75
76    /// Prints a message, where the status will have `color` color, and can be justified. The
77    /// messages follows without color.
78    fn print(
79        &mut self,
80        status: &dyn fmt::Display,
81        message: Option<&dyn fmt::Display>,
82        color: &Style,
83        justified: bool,
84    ) -> CargoResult<()> {
85        match self.verbosity {
86            Verbosity::Quiet => Ok(()),
87            _ => {
88                if self.needs_clear {
89                    self.err_erase_line();
90                }
91                self.output
92                    .message_stderr(status, message, color, justified)
93            }
94        }
95    }
96
97    /// Sets whether the next print should clear the current line.
98    pub fn set_needs_clear(&mut self, needs_clear: bool) {
99        self.needs_clear = needs_clear;
100    }
101
102    /// Returns `true` if the `needs_clear` flag is unset.
103    pub fn is_cleared(&self) -> bool {
104        !self.needs_clear
105    }
106
107    /// Returns the width of the terminal in spaces, if any.
108    pub fn err_width(&self) -> TtyWidth {
109        match self.output {
110            ShellOut::Stream {
111                stderr_tty: true, ..
112            } => imp::stderr_width(),
113            _ => TtyWidth::NoTty,
114        }
115    }
116
117    /// Returns `true` if stderr is a tty.
118    pub fn is_err_tty(&self) -> bool {
119        match self.output {
120            ShellOut::Stream { stderr_tty, .. } => stderr_tty,
121            _ => false,
122        }
123    }
124
125    /// Gets a reference to the underlying stdout writer.
126    pub fn out(&mut self) -> &mut dyn Write {
127        if self.needs_clear {
128            self.err_erase_line();
129        }
130        self.output.stdout()
131    }
132
133    /// Gets a reference to the underlying stderr writer.
134    pub fn err(&mut self) -> &mut dyn Write {
135        if self.needs_clear {
136            self.err_erase_line();
137        }
138        self.output.stderr()
139    }
140
141    /// Erase from cursor to end of line.
142    pub fn err_erase_line(&mut self) {
143        if self.err_supports_color() {
144            imp::err_erase_line(self);
145            self.needs_clear = false;
146        }
147    }
148
149    /// Shortcut to right-align and color green a status message.
150    pub fn status<T, U>(&mut self, status: T, message: U) -> CargoResult<()>
151    where
152        T: fmt::Display,
153        U: fmt::Display,
154    {
155        self.print(&status, Some(&message), &HEADER, true)
156    }
157
158    pub fn status_header<T>(&mut self, status: T) -> CargoResult<()>
159    where
160        T: fmt::Display,
161    {
162        self.print(&status, None, &NOTE, true)
163    }
164
165    /// Shortcut to right-align a status message.
166    pub fn status_with_color<T, U>(
167        &mut self,
168        status: T,
169        message: U,
170        color: &Style,
171    ) -> CargoResult<()>
172    where
173        T: fmt::Display,
174        U: fmt::Display,
175    {
176        self.print(&status, Some(&message), color, true)
177    }
178
179    /// Runs the callback only if we are in verbose mode.
180    pub fn verbose<F>(&mut self, mut callback: F) -> CargoResult<()>
181    where
182        F: FnMut(&mut Shell) -> CargoResult<()>,
183    {
184        match self.verbosity {
185            Verbosity::Verbose => callback(self),
186            _ => Ok(()),
187        }
188    }
189
190    /// Runs the callback if we are not in verbose mode.
191    pub fn concise<F>(&mut self, mut callback: F) -> CargoResult<()>
192    where
193        F: FnMut(&mut Shell) -> CargoResult<()>,
194    {
195        match self.verbosity {
196            Verbosity::Verbose => Ok(()),
197            _ => callback(self),
198        }
199    }
200
201    /// Prints a red 'error' message.
202    pub fn error<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
203        if self.needs_clear {
204            self.err_erase_line();
205        }
206        self.output
207            .message_stderr(&"error", Some(&message), &ERROR, false)
208    }
209
210    /// Prints an amber 'warning' message.
211    pub fn warn<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
212        match self.verbosity {
213            Verbosity::Quiet => Ok(()),
214            _ => self.print(&"warning", Some(&message), &WARN, false),
215        }
216    }
217
218    /// Prints a cyan 'note' message.
219    pub fn note<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
220        self.print(&"note", Some(&message), &NOTE, false)
221    }
222
223    /// Updates the verbosity of the shell.
224    pub fn set_verbosity(&mut self, verbosity: Verbosity) {
225        self.verbosity = verbosity;
226    }
227
228    /// Gets the verbosity of the shell.
229    pub fn verbosity(&self) -> Verbosity {
230        self.verbosity
231    }
232
233    /// Updates the color choice (always, never, or auto) from a string..
234    pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> {
235        if let ShellOut::Stream {
236            stdout,
237            stderr,
238            color_choice,
239            ..
240        } = &mut self.output
241        {
242            let cfg = color
243                .map(|c| c.parse())
244                .transpose()?
245                .unwrap_or(ColorChoice::CargoAuto);
246            *color_choice = cfg;
247            let stdout_choice = cfg.to_anstream_color_choice();
248            let stderr_choice = cfg.to_anstream_color_choice();
249            *stdout = AutoStream::new(std::io::stdout(), stdout_choice);
250            *stderr = AutoStream::new(std::io::stderr(), stderr_choice);
251        }
252        Ok(())
253    }
254
255    pub fn set_unicode(&mut self, yes: bool) -> CargoResult<()> {
256        if let ShellOut::Stream {
257            stdout_unicode,
258            stderr_unicode,
259            ..
260        } = &mut self.output
261        {
262            *stdout_unicode = yes;
263            *stderr_unicode = yes;
264        }
265        Ok(())
266    }
267
268    pub fn set_hyperlinks(&mut self, yes: bool) -> CargoResult<()> {
269        if let ShellOut::Stream { hyperlinks, .. } = &mut self.output {
270            *hyperlinks = yes;
271        }
272        Ok(())
273    }
274
275    pub fn out_unicode(&self) -> bool {
276        match &self.output {
277            ShellOut::Write(_) => true,
278            ShellOut::Stream { stdout_unicode, .. } => *stdout_unicode,
279        }
280    }
281
282    pub fn err_unicode(&self) -> bool {
283        match &self.output {
284            ShellOut::Write(_) => true,
285            ShellOut::Stream { stderr_unicode, .. } => *stderr_unicode,
286        }
287    }
288
289    /// Gets the current color choice.
290    ///
291    /// If we are not using a color stream, this will always return `Never`, even if the color
292    /// choice has been set to something else.
293    pub fn color_choice(&self) -> ColorChoice {
294        match self.output {
295            ShellOut::Stream { color_choice, .. } => color_choice,
296            ShellOut::Write(_) => ColorChoice::Never,
297        }
298    }
299
300    /// Whether the shell supports color.
301    pub fn err_supports_color(&self) -> bool {
302        match &self.output {
303            ShellOut::Write(_) => false,
304            ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
305        }
306    }
307
308    pub fn out_supports_color(&self) -> bool {
309        match &self.output {
310            ShellOut::Write(_) => false,
311            ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
312        }
313    }
314
315    pub fn out_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
316        let supports_hyperlinks = match &self.output {
317            ShellOut::Write(_) => false,
318            ShellOut::Stream {
319                stdout, hyperlinks, ..
320            } => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
321        };
322        Hyperlink {
323            url: supports_hyperlinks.then_some(url),
324        }
325    }
326
327    pub fn err_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
328        let supports_hyperlinks = match &self.output {
329            ShellOut::Write(_) => false,
330            ShellOut::Stream {
331                stderr, hyperlinks, ..
332            } => stderr.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
333        };
334        if supports_hyperlinks {
335            Hyperlink { url: Some(url) }
336        } else {
337            Hyperlink { url: None }
338        }
339    }
340
341    pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
342        let url = self.file_hyperlink(path);
343        url.map(|u| self.out_hyperlink(u)).unwrap_or_default()
344    }
345
346    pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
347        let url = self.file_hyperlink(path);
348        url.map(|u| self.err_hyperlink(u)).unwrap_or_default()
349    }
350
351    fn file_hyperlink(&mut self, path: &std::path::Path) -> Option<url::Url> {
352        let mut url = url::Url::from_file_path(path).ok()?;
353        // Do a best-effort of setting the host in the URL to avoid issues with opening a link
354        // scoped to the computer you've SSHed into
355        let hostname = if cfg!(windows) {
356            // Not supported correctly on windows
357            None
358        } else {
359            if let Some(hostname) = self.hostname.as_deref() {
360                Some(hostname)
361            } else {
362                self.hostname = hostname().ok().and_then(|h| h.into_string().ok());
363                self.hostname.as_deref()
364            }
365        };
366        let _ = url.set_host(hostname);
367        Some(url)
368    }
369
370    /// Prints a message to stderr and translates ANSI escape code into console colors.
371    pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> {
372        if self.needs_clear {
373            self.err_erase_line();
374        }
375        self.err().write_all(message)?;
376        Ok(())
377    }
378
379    /// Prints a message to stdout and translates ANSI escape code into console colors.
380    pub fn print_ansi_stdout(&mut self, message: &[u8]) -> CargoResult<()> {
381        if self.needs_clear {
382            self.err_erase_line();
383        }
384        self.out().write_all(message)?;
385        Ok(())
386    }
387
388    pub fn print_json<T: serde::ser::Serialize>(&mut self, obj: &T) -> CargoResult<()> {
389        // Path may fail to serialize to JSON ...
390        let encoded = serde_json::to_string(&obj)?;
391        // ... but don't fail due to a closed pipe.
392        drop(writeln!(self.out(), "{}", encoded));
393        Ok(())
394    }
395
396    /// Prints the passed in [Message] to stderr
397    pub fn print_message(&mut self, message: Message<'_>) -> std::io::Result<()> {
398        let term_width = self
399            .err_width()
400            .diagnostic_terminal_width()
401            .unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH);
402        writeln!(
403            self.err(),
404            "{}",
405            Renderer::styled().term_width(term_width).render(message)
406        )
407    }
408}
409
410impl Default for Shell {
411    fn default() -> Self {
412        Self::new()
413    }
414}
415
416/// A `Write`able object, either with or without color support
417enum ShellOut {
418    /// A plain write object without color support
419    Write(AutoStream<Box<dyn Write>>),
420    /// Color-enabled stdio, with information on whether color should be used
421    Stream {
422        stdout: AutoStream<std::io::Stdout>,
423        stderr: AutoStream<std::io::Stderr>,
424        stderr_tty: bool,
425        color_choice: ColorChoice,
426        hyperlinks: bool,
427        stdout_unicode: bool,
428        stderr_unicode: bool,
429    },
430}
431
432impl ShellOut {
433    /// Prints out a message with a status. The status comes first, and is bold plus the given
434    /// color. The status can be justified, in which case the max width that will right align is
435    /// 12 chars.
436    fn message_stderr(
437        &mut self,
438        status: &dyn fmt::Display,
439        message: Option<&dyn fmt::Display>,
440        style: &Style,
441        justified: bool,
442    ) -> CargoResult<()> {
443        let bold = anstyle::Style::new() | anstyle::Effects::BOLD;
444
445        let mut buffer = Vec::new();
446        if justified {
447            write!(&mut buffer, "{style}{status:>12}{style:#}")?;
448        } else {
449            write!(&mut buffer, "{style}{status}{style:#}{bold}:{bold:#}")?;
450        }
451        match message {
452            Some(message) => writeln!(buffer, " {message}")?,
453            None => write!(buffer, " ")?,
454        }
455        self.stderr().write_all(&buffer)?;
456        Ok(())
457    }
458
459    /// Gets stdout as a `io::Write`.
460    fn stdout(&mut self) -> &mut dyn Write {
461        match self {
462            ShellOut::Stream { stdout, .. } => stdout,
463            ShellOut::Write(w) => w,
464        }
465    }
466
467    /// Gets stderr as a `io::Write`.
468    fn stderr(&mut self) -> &mut dyn Write {
469        match self {
470            ShellOut::Stream { stderr, .. } => stderr,
471            ShellOut::Write(w) => w,
472        }
473    }
474}
475
476pub enum TtyWidth {
477    NoTty,
478    Known(usize),
479    Guess(usize),
480}
481
482impl TtyWidth {
483    /// Returns the width of the terminal to use for diagnostics (which is
484    /// relayed to rustc via `--diagnostic-width`).
485    pub fn diagnostic_terminal_width(&self) -> Option<usize> {
486        // ALLOWED: For testing cargo itself only.
487        #[allow(clippy::disallowed_methods)]
488        if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") {
489            return Some(width.parse().unwrap());
490        }
491        match *self {
492            TtyWidth::NoTty | TtyWidth::Guess(_) => None,
493            TtyWidth::Known(width) => Some(width),
494        }
495    }
496
497    /// Returns the width used by progress bars for the tty.
498    pub fn progress_max_width(&self) -> Option<usize> {
499        match *self {
500            TtyWidth::NoTty => None,
501            TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
502        }
503    }
504}
505
506/// The requested verbosity of output.
507#[derive(Debug, Clone, Copy, PartialEq)]
508pub enum Verbosity {
509    Verbose,
510    Normal,
511    Quiet,
512}
513
514/// Whether messages should use color output
515#[derive(Debug, PartialEq, Clone, Copy)]
516pub enum ColorChoice {
517    /// Force color output
518    Always,
519    /// Force disable color output
520    Never,
521    /// Intelligently guess whether to use color output
522    CargoAuto,
523}
524
525impl ColorChoice {
526    /// Converts our color choice to anstream's version.
527    fn to_anstream_color_choice(self) -> anstream::ColorChoice {
528        match self {
529            ColorChoice::Always => anstream::ColorChoice::Always,
530            ColorChoice::Never => anstream::ColorChoice::Never,
531            ColorChoice::CargoAuto => anstream::ColorChoice::Auto,
532        }
533    }
534}
535
536impl std::str::FromStr for ColorChoice {
537    type Err = anyhow::Error;
538    fn from_str(color: &str) -> Result<Self, Self::Err> {
539        let cfg = match color {
540            "always" => ColorChoice::Always,
541            "never" => ColorChoice::Never,
542
543            "auto" => ColorChoice::CargoAuto,
544
545            arg => anyhow::bail!(
546                "argument for --color must be auto, always, or \
547                     never, but found `{}`",
548                arg
549            ),
550        };
551        Ok(cfg)
552    }
553}
554
555fn supports_color(choice: anstream::ColorChoice) -> bool {
556    match choice {
557        anstream::ColorChoice::Always
558        | anstream::ColorChoice::AlwaysAnsi
559        | anstream::ColorChoice::Auto => true,
560        anstream::ColorChoice::Never => false,
561    }
562}
563
564fn supports_unicode(stream: &dyn IsTerminal) -> bool {
565    !stream.is_terminal() || supports_unicode::supports_unicode()
566}
567
568fn supports_hyperlinks() -> bool {
569    #[allow(clippy::disallowed_methods)] // We are reading the state of the system, not config
570    if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
571        // Override `supports_hyperlinks` as we have an unknown incompatibility with iTerm2
572        return false;
573    }
574
575    supports_hyperlinks::supports_hyperlinks()
576}
577
578pub struct Hyperlink<D: fmt::Display> {
579    url: Option<D>,
580}
581
582impl<D: fmt::Display> Default for Hyperlink<D> {
583    fn default() -> Self {
584        Self { url: None }
585    }
586}
587
588impl<D: fmt::Display> fmt::Display for Hyperlink<D> {
589    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590        let Some(url) = self.url.as_ref() else {
591            return Ok(());
592        };
593        if f.alternate() {
594            write!(f, "\x1B]8;;\x1B\\")
595        } else {
596            write!(f, "\x1B]8;;{url}\x1B\\")
597        }
598    }
599}
600
601#[cfg(unix)]
602mod imp {
603    use super::{Shell, TtyWidth};
604    use std::mem;
605
606    pub fn stderr_width() -> TtyWidth {
607        unsafe {
608            let mut winsize: libc::winsize = mem::zeroed();
609            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
610            // as c_uint but ioctl wants c_ulong.
611            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
612                return TtyWidth::NoTty;
613            }
614            if winsize.ws_col > 0 {
615                TtyWidth::Known(winsize.ws_col as usize)
616            } else {
617                TtyWidth::NoTty
618            }
619        }
620    }
621
622    pub fn err_erase_line(shell: &mut Shell) {
623        // This is the "EL - Erase in Line" sequence. It clears from the cursor
624        // to the end of line.
625        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
626        let _ = shell.output.stderr().write_all(b"\x1B[K");
627    }
628}
629
630#[cfg(windows)]
631mod imp {
632    use std::{cmp, mem, ptr};
633
634    use windows_sys::core::PCSTR;
635    use windows_sys::Win32::Foundation::CloseHandle;
636    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
637    use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
638    use windows_sys::Win32::Storage::FileSystem::{
639        CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
640    };
641    use windows_sys::Win32::System::Console::{
642        GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, STD_ERROR_HANDLE,
643    };
644
645    pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth};
646
647    pub fn stderr_width() -> TtyWidth {
648        unsafe {
649            let stdout = GetStdHandle(STD_ERROR_HANDLE);
650            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
651            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
652                return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
653            }
654
655            // On mintty/msys/cygwin based terminals, the above fails with
656            // INVALID_HANDLE_VALUE. Use an alternate method which works
657            // in that case as well.
658            let h = CreateFileA(
659                "CONOUT$\0".as_ptr() as PCSTR,
660                GENERIC_READ | GENERIC_WRITE,
661                FILE_SHARE_READ | FILE_SHARE_WRITE,
662                ptr::null_mut(),
663                OPEN_EXISTING,
664                0,
665                std::ptr::null_mut(),
666            );
667            if h == INVALID_HANDLE_VALUE {
668                return TtyWidth::NoTty;
669            }
670
671            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
672            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
673            CloseHandle(h);
674            if rc != 0 {
675                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
676                // Unfortunately cygwin/mintty does not set the size of the
677                // backing console to match the actual window size. This
678                // always reports a size of 80 or 120 (not sure what
679                // determines that). Use a conservative max of 60 which should
680                // work in most circumstances. ConEmu does some magic to
681                // resize the console correctly, but there's no reasonable way
682                // to detect which kind of terminal we are running in, or if
683                // GetConsoleScreenBufferInfo returns accurate information.
684                return TtyWidth::Guess(cmp::min(60, width));
685            }
686
687            TtyWidth::NoTty
688        }
689    }
690}
691
692#[cfg(windows)]
693fn default_err_erase_line(shell: &mut Shell) {
694    match imp::stderr_width() {
695        TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
696            let blank = " ".repeat(max_width);
697            drop(write!(shell.output.stderr(), "{}\r", blank));
698        }
699        _ => (),
700    }
701}