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