#
cargo/core/
shell.rs

    1use std::fmt;
    2use std::io::IsTerminal;
    3use std::io::prelude::*;
    4
    5use annotate_snippets::{Renderer, Report};
    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                stderr_term_integration: supports_term_integration(&std::io::stderr()),
    60            },
    61            verbosity: Verbosity::Verbose,
    62            needs_clear: false,
    63            hostname: None,
    64        }
    65    }
    66
    67    /// Creates a shell from a plain writable object, with no color, and max verbosity.
    68    pub fn from_write(out: Box<dyn Write>) -> Shell {
    69        Shell {
    70            output: ShellOut::Write(AutoStream::never(out)), // strip all formatting on write
    71            verbosity: Verbosity::Verbose,
    72            needs_clear: false,
    73            hostname: None,
    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        self.print(&"note", Some(&message), &NOTE, false)
    231    }
    232
    233    /// Updates the verbosity of the shell.
    234    pub fn set_verbosity(&mut self, verbosity: Verbosity) {
    235        self.verbosity = verbosity;
    236    }
    237
    238    /// Gets the verbosity of the shell.
    239    pub fn verbosity(&self) -> Verbosity {
    240        self.verbosity
    241    }
    242
    243    /// Updates the color choice (always, never, or auto) from a string..
    244    pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> {
    245        if let ShellOut::Stream {
    246            stdout,
    247            stderr,
    248            color_choice,
    249            ..
    250        } = &mut self.output
    251        {
    252            let cfg = color
    253                .map(|c| c.parse())
    254                .transpose()?
    255                .unwrap_or(ColorChoice::CargoAuto);
    256            *color_choice = cfg;
    257            let stdout_choice = cfg.to_anstream_color_choice();
    258            let stderr_choice = cfg.to_anstream_color_choice();
    259            *stdout = AutoStream::new(std::io::stdout(), stdout_choice);
    260            *stderr = AutoStream::new(std::io::stderr(), stderr_choice);
    261        }
    262        Ok(())
    263    }
    264
    265    pub fn set_unicode(&mut self, yes: bool) -> CargoResult<()> {
    266        if let ShellOut::Stream {
    267            stdout_unicode,
    268            stderr_unicode,
    269            ..
    270        } = &mut self.output
    271        {
    272            *stdout_unicode = yes;
    273            *stderr_unicode = yes;
    274        }
    275        Ok(())
    276    }
    277
    278    pub fn set_hyperlinks(&mut self, yes: bool) -> CargoResult<()> {
    279        if let ShellOut::Stream { hyperlinks, .. } = &mut self.output {
    280            *hyperlinks = yes;
    281        }
    282        Ok(())
    283    }
    284
    285    pub fn out_unicode(&self) -> bool {
    286        match &self.output {
    287            ShellOut::Write(_) => true,
    288            ShellOut::Stream { stdout_unicode, .. } => *stdout_unicode,
    289        }
    290    }
    291
    292    pub fn err_unicode(&self) -> bool {
    293        match &self.output {
    294            ShellOut::Write(_) => true,
    295            ShellOut::Stream { stderr_unicode, .. } => *stderr_unicode,
    296        }
    297    }
    298
    299    /// Gets the current color choice.
    300    ///
    301    /// If we are not using a color stream, this will always return `Never`, even if the color
    302    /// choice has been set to something else.
    303    pub fn color_choice(&self) -> ColorChoice {
    304        match self.output {
    305            ShellOut::Stream { color_choice, .. } => color_choice,
    306            ShellOut::Write(_) => ColorChoice::Never,
    307        }
    308    }
    309
    310    /// Whether the shell supports color.
    311    pub fn err_supports_color(&self) -> bool {
    312        match &self.output {
    313            ShellOut::Write(_) => false,
    314            ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
    315        }
    316    }
    317
    318    pub fn out_supports_color(&self) -> bool {
    319        match &self.output {
    320            ShellOut::Write(_) => false,
    321            ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
    322        }
    323    }
    324
    325    pub fn out_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
    326        let supports_hyperlinks = match &self.output {
    327            ShellOut::Write(_) => false,
    328            ShellOut::Stream {
    329                stdout, hyperlinks, ..
    330            } => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
    331        };
    332        Hyperlink {
    333            url: supports_hyperlinks.then_some(url),
    334        }
    335    }
    336
    337    pub fn err_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
    338        let supports_hyperlinks = match &self.output {
    339            ShellOut::Write(_) => false,
    340            ShellOut::Stream {
    341                stderr, hyperlinks, ..
    342            } => stderr.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
    343        };
    344        if supports_hyperlinks {
    345            Hyperlink { url: Some(url) }
    346        } else {
    347            Hyperlink { url: None }
    348        }
    349    }
    350
    351    pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
    352        let url = self.file_hyperlink(path);
    353        url.map(|u| self.out_hyperlink(u)).unwrap_or_default()
    354    }
    355
    356    pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
    357        let url = self.file_hyperlink(path);
    358        url.map(|u| self.err_hyperlink(u)).unwrap_or_default()
    359    }
    360
    361    fn file_hyperlink(&mut self, path: &std::path::Path) -> Option<url::Url> {
    362        let mut url = url::Url::from_file_path(path).ok()?;
    363        // Do a best-effort of setting the host in the URL to avoid issues with opening a link
    364        // scoped to the computer you've SSHed into
    365        let hostname = if cfg!(windows) {
    366            // Not supported correctly on windows
    367            None
    368        } else {
    369            if let Some(hostname) = self.hostname.as_deref() {
    370                Some(hostname)
    371            } else {
    372                self.hostname = hostname().ok().and_then(|h| h.into_string().ok());
    373                self.hostname.as_deref()
    374            }
    375        };
    376        let _ = url.set_host(hostname);
    377        Some(url)
    378    }
    379
    380    /// Prints a message to stderr and translates ANSI escape code into console colors.
    381    pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> {
    382        if self.needs_clear {
    383            self.err_erase_line();
    384        }
    385        self.err().write_all(message)?;
    386        Ok(())
    387    }
    388
    389    /// Prints a message to stdout and translates ANSI escape code into console colors.
    390    pub fn print_ansi_stdout(&mut self, message: &[u8]) -> CargoResult<()> {
    391        if self.needs_clear {
    392            self.err_erase_line();
    393        }
    394        self.out().write_all(message)?;
    395        Ok(())
    396    }
    397
    398    pub fn print_json<T: serde::ser::Serialize>(&mut self, obj: &T) -> CargoResult<()> {
    399        // Path may fail to serialize to JSON ...
    400        let encoded = serde_json::to_string(obj)?;
    401        // ... but don't fail due to a closed pipe.
    402        drop(writeln!(self.out(), "{}", encoded));
    403        Ok(())
    404    }
    405
    406    /// Prints the passed in [`Report`] to stderr
    407    pub fn print_report(&mut self, report: Report<'_>, force: bool) -> CargoResult<()> {
    408        if !force && matches!(self.verbosity, Verbosity::Quiet) {
    409            return Ok(());
    410        }
    411
    412        if self.needs_clear {
    413            self.err_erase_line();
    414        }
    415        let term_width = self
    416            .err_width()
    417            .diagnostic_terminal_width()
    418            .unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH);
    419        let rendered = Renderer::styled().term_width(term_width).render(report);
    420        self.err().write_all(rendered.as_bytes())?;
    421        self.err().write_all(b"\n")?;
    422        Ok(())
    423    }
    424}
    425
    426impl Default for Shell {
    427    fn default() -> Self {
    428        Self::new()
    429    }
    430}
    431
    432/// A `Write`able object, either with or without color support
    433enum ShellOut {
    434    /// A plain write object without color support
    435    Write(AutoStream<Box<dyn Write>>),
    436    /// Color-enabled stdio, with information on whether color should be used
    437    Stream {
    438        stdout: AutoStream<std::io::Stdout>,
    439        stderr: AutoStream<std::io::Stderr>,
    440        stderr_tty: bool,
    441        color_choice: ColorChoice,
    442        hyperlinks: bool,
    443        stdout_unicode: bool,
    444        stderr_unicode: bool,
    445        stderr_term_integration: bool,
    446    },
    447}
    448
    449impl ShellOut {
    450    /// Prints out a message with a status. The status comes first, and is bold plus the given
    451    /// color. The status can be justified, in which case the max width that will right align is
    452    /// 12 chars.
    453    fn message_stderr(
    454        &mut self,
    455        status: &dyn fmt::Display,
    456        message: Option<&dyn fmt::Display>,
    457        style: &Style,
    458        justified: bool,
    459    ) -> CargoResult<()> {
    460        let mut buffer = Vec::new();
    461        if justified {
    462            write!(&mut buffer, "{style}{status:>12}{style:#}")?;
    463        } else {
    464            write!(&mut buffer, "{style}{status}{style:#}:")?;
    465        }
    466        match message {
    467            Some(message) => writeln!(buffer, " {message}")?,
    468            None => write!(buffer, " ")?,
    469        }
    470        self.stderr().write_all(&buffer)?;
    471        Ok(())
    472    }
    473
    474    /// Gets stdout as a `io::Write`.
    475    fn stdout(&mut self) -> &mut dyn Write {
    476        match self {
    477            ShellOut::Stream { stdout, .. } => stdout,
    478            ShellOut::Write(w) => w,
    479        }
    480    }
    481
    482    /// Gets stderr as a `io::Write`.
    483    fn stderr(&mut self) -> &mut dyn Write {
    484        match self {
    485            ShellOut::Stream { stderr, .. } => stderr,
    486            ShellOut::Write(w) => w,
    487        }
    488    }
    489}
    490
    491pub enum TtyWidth {
    492    NoTty,
    493    Known(usize),
    494    Guess(usize),
    495}
    496
    497impl TtyWidth {
    498    /// Returns the width of the terminal to use for diagnostics (which is
    499    /// relayed to rustc via `--diagnostic-width`).
    500    pub fn diagnostic_terminal_width(&self) -> Option<usize> {
    501        // ALLOWED: For testing cargo itself only.
    502        #[allow(clippy::disallowed_methods)]
    503        if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") {
    504            return Some(width.parse().unwrap());
    505        }
    506        match *self {
    507            TtyWidth::NoTty | TtyWidth::Guess(_) => None,
    508            TtyWidth::Known(width) => Some(width),
    509        }
    510    }
    511
    512    /// Returns the width used by progress bars for the tty.
    513    pub fn progress_max_width(&self) -> Option<usize> {
    514        match *self {
    515            TtyWidth::NoTty => None,
    516            TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
    517        }
    518    }
    519}
    520
    521/// The requested verbosity of output.
    522#[derive(Debug, Clone, Copy, PartialEq)]
    523pub enum Verbosity {
    524    Verbose,
    525    Normal,
    526    Quiet,
    527}
    528
    529/// Whether messages should use color output
    530#[derive(Debug, PartialEq, Clone, Copy)]
    531pub enum ColorChoice {
    532    /// Force color output
    533    Always,
    534    /// Force disable color output
    535    Never,
    536    /// Intelligently guess whether to use color output
    537    CargoAuto,
    538}
    539
    540impl ColorChoice {
    541    /// Converts our color choice to anstream's version.
    542    fn to_anstream_color_choice(self) -> anstream::ColorChoice {
    543        match self {
    544            ColorChoice::Always => anstream::ColorChoice::Always,
    545            ColorChoice::Never => anstream::ColorChoice::Never,
    546            ColorChoice::CargoAuto => anstream::ColorChoice::Auto,
    547        }
    548    }
    549}
    550
    551impl std::str::FromStr for ColorChoice {
    552    type Err = anyhow::Error;
    553    fn from_str(color: &str) -> Result<Self, Self::Err> {
    554        let cfg = match color {
    555            "always" => ColorChoice::Always,
    556            "never" => ColorChoice::Never,
    557
    558            "auto" => ColorChoice::CargoAuto,
    559
    560            arg => anyhow::bail!(
    561                "argument for --color must be auto, always, or \
    562                     never, but found `{}`",
    563                arg
    564            ),
    565        };
    566        Ok(cfg)
    567    }
    568}
    569
    570fn supports_color(choice: anstream::ColorChoice) -> bool {
    571    match choice {
    572        anstream::ColorChoice::Always
    573        | anstream::ColorChoice::AlwaysAnsi
    574        | anstream::ColorChoice::Auto => true,
    575        anstream::ColorChoice::Never => false,
    576    }
    577}
    578
    579fn supports_unicode(stream: &dyn IsTerminal) -> bool {
    580    !stream.is_terminal() || supports_unicode::supports_unicode()
    581}
    582
    583fn supports_hyperlinks() -> bool {
    584    #[allow(clippy::disallowed_methods)] // We are reading the state of the system, not config
    585    if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
    586        // Override `supports_hyperlinks` as we have an unknown incompatibility with iTerm2
    587        return false;
    588    }
    589
    590    supports_hyperlinks::supports_hyperlinks()
    591}
    592
    593/// Determines whether the terminal supports ANSI OSC 9;4.
    594#[allow(clippy::disallowed_methods)] // Read environment variables to detect terminal
    595fn supports_term_integration(stream: &dyn IsTerminal) -> bool {
    596    let windows_terminal = std::env::var("WT_SESSION").is_ok();
    597    let conemu = std::env::var("ConEmuANSI").ok() == Some("ON".into());
    598    let wezterm = std::env::var("TERM_PROGRAM").ok() == Some("WezTerm".into());
    599
    600    (windows_terminal || conemu || wezterm) && stream.is_terminal()
    601}
    602
    603pub struct Hyperlink<D: fmt::Display> {
    604    url: Option<D>,
    605}
    606
    607impl<D: fmt::Display> Default for Hyperlink<D> {
    608    fn default() -> Self {
    609        Self { url: None }
    610    }
    611}
    612
    613impl<D: fmt::Display> fmt::Display for Hyperlink<D> {
    614    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    615        let Some(url) = self.url.as_ref() else {
    616            return Ok(());
    617        };
    618        if f.alternate() {
    619            write!(f, "\x1B]8;;\x1B\\")
    620        } else {
    621            write!(f, "\x1B]8;;{url}\x1B\\")
    622        }
    623    }
    624}
    625
    626#[cfg(unix)]
    627mod imp {
    628    use super::{Shell, TtyWidth};
    629    use std::mem;
    630
    631    pub fn stderr_width() -> TtyWidth {
    632        unsafe {
    633            let mut winsize: libc::winsize = mem::zeroed();
    634            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
    635            // as c_uint but ioctl wants c_ulong.
    636            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
    637                return TtyWidth::NoTty;
    638            }
    639            if winsize.ws_col > 0 {
    640                TtyWidth::Known(winsize.ws_col as usize)
    641            } else {
    642                TtyWidth::NoTty
    643            }
    644        }
    645    }
    646
    647    pub fn err_erase_line(shell: &mut Shell) {
    648        // This is the "EL - Erase in Line" sequence. It clears from the cursor
    649        // to the end of line.
    650        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
    651        let _ = shell.output.stderr().write_all(b"\x1B[K");
    652    }
    653}
    654
    655#[cfg(windows)]
    656mod imp {
    657    use std::{cmp, mem, ptr};
    658
    659    use windows_sys::Win32::Foundation::CloseHandle;
    660    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
    661    use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
    662    use windows_sys::Win32::Storage::FileSystem::{
    663        CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
    664    };
    665    use windows_sys::Win32::System::Console::{
    666        CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle, STD_ERROR_HANDLE,
    667    };
    668    use windows_sys::core::PCSTR;
    669
    670    pub(super) use super::{TtyWidth, default_err_erase_line as err_erase_line};
    671
    672    pub fn stderr_width() -> TtyWidth {
    673        unsafe {
    674            let stdout = GetStdHandle(STD_ERROR_HANDLE);
    675            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
    676            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
    677                return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
    678            }
    679
    680            // On mintty/msys/cygwin based terminals, the above fails with
    681            // INVALID_HANDLE_VALUE. Use an alternate method which works
    682            // in that case as well.
    683            let h = CreateFileA(
    684                "CONOUT$\0".as_ptr() as PCSTR,
    685                GENERIC_READ | GENERIC_WRITE,
    686                FILE_SHARE_READ | FILE_SHARE_WRITE,
    687                ptr::null_mut(),
    688                OPEN_EXISTING,
    689                0,
    690                std::ptr::null_mut(),
    691            );
    692            if h == INVALID_HANDLE_VALUE {
    693                return TtyWidth::NoTty;
    694            }
    695
    696            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
    697            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
    698            CloseHandle(h);
    699            if rc != 0 {
    700                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
    701                // Unfortunately cygwin/mintty does not set the size of the
    702                // backing console to match the actual window size. This
    703                // always reports a size of 80 or 120 (not sure what
    704                // determines that). Use a conservative max of 60 which should
    705                // work in most circumstances. ConEmu does some magic to
    706                // resize the console correctly, but there's no reasonable way
    707                // to detect which kind of terminal we are running in, or if
    708                // GetConsoleScreenBufferInfo returns accurate information.
    709                return TtyWidth::Guess(cmp::min(60, width));
    710            }
    711
    712            TtyWidth::NoTty
    713        }
    714    }
    715}
    716
    717#[cfg(windows)]
    718fn default_err_erase_line(shell: &mut Shell) {
    719    match imp::stderr_width() {
    720        TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
    721            let blank = " ".repeat(max_width);
    722            drop(write!(shell.output.stderr(), "{}\r", blank));
    723        }
    724        _ => (),
    725    }
    726}