Skip to main content

cargo/core/
shell.rs

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