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