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 SSHed 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        // ALLOWED: For testing cargo itself only.
537        #[allow(clippy::disallowed_methods)]
538        if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") {
539            return Some(width.parse().unwrap());
540        }
541        match *self {
542            TtyWidth::NoTty | TtyWidth::Guess(_) => None,
543            TtyWidth::Known(width) => Some(width),
544        }
545    }
546
547    /// Returns the width used by progress bars for the tty.
548    pub fn progress_max_width(&self) -> Option<usize> {
549        match *self {
550            TtyWidth::NoTty => None,
551            TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
552        }
553    }
554}
555
556/// The requested verbosity of output.
557#[derive(Debug, Clone, Copy, PartialEq)]
558pub enum Verbosity {
559    Verbose,
560    Normal,
561    Quiet,
562}
563
564/// Whether messages should use color output
565#[derive(Debug, PartialEq, Clone, Copy)]
566pub enum ColorChoice {
567    /// Force color output
568    Always,
569    /// Force disable color output
570    Never,
571    /// Intelligently guess whether to use color output
572    CargoAuto,
573}
574
575impl ColorChoice {
576    /// Converts our color choice to anstream's version.
577    fn to_anstream_color_choice(self) -> anstream::ColorChoice {
578        match self {
579            ColorChoice::Always => anstream::ColorChoice::Always,
580            ColorChoice::Never => anstream::ColorChoice::Never,
581            ColorChoice::CargoAuto => anstream::ColorChoice::Auto,
582        }
583    }
584}
585
586impl std::str::FromStr for ColorChoice {
587    type Err = anyhow::Error;
588    fn from_str(color: &str) -> Result<Self, Self::Err> {
589        let cfg = match color {
590            "always" => ColorChoice::Always,
591            "never" => ColorChoice::Never,
592
593            "auto" => ColorChoice::CargoAuto,
594
595            arg => anyhow::bail!(
596                "argument for --color must be auto, always, or \
597                     never, but found `{}`",
598                arg
599            ),
600        };
601        Ok(cfg)
602    }
603}
604
605fn supports_color(choice: anstream::ColorChoice) -> bool {
606    match choice {
607        anstream::ColorChoice::Always
608        | anstream::ColorChoice::AlwaysAnsi
609        | anstream::ColorChoice::Auto => true,
610        anstream::ColorChoice::Never => false,
611    }
612}
613
614fn supports_unicode(stream: &dyn IsTerminal) -> bool {
615    !stream.is_terminal() || supports_unicode::supports_unicode()
616}
617
618fn supports_hyperlinks() -> bool {
619    #[allow(clippy::disallowed_methods)] // We are reading the state of the system, not config
620    if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
621        // Override `supports_hyperlinks` as we have an unknown incompatibility with iTerm2
622        return false;
623    }
624
625    supports_hyperlinks::supports_hyperlinks()
626}
627
628/// Determines whether the terminal supports ANSI OSC 9;4.
629#[allow(clippy::disallowed_methods)] // Read environment variables to detect terminal
630fn supports_term_integration(stream: &dyn IsTerminal) -> bool {
631    let windows_terminal = std::env::var("WT_SESSION").is_ok();
632    let conemu = std::env::var("ConEmuANSI").ok() == Some("ON".into());
633    let wezterm = std::env::var("TERM_PROGRAM").ok() == Some("WezTerm".into());
634    let ghostty = std::env::var("TERM_PROGRAM").ok() == Some("ghostty".into());
635
636    (windows_terminal || conemu || wezterm || ghostty) && stream.is_terminal()
637}
638
639pub struct Hyperlink<D: fmt::Display> {
640    url: Option<D>,
641}
642
643impl<D: fmt::Display> Default for Hyperlink<D> {
644    fn default() -> Self {
645        Self { url: None }
646    }
647}
648
649impl<D: fmt::Display> fmt::Display for Hyperlink<D> {
650    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
651        let Some(url) = self.url.as_ref() else {
652            return Ok(());
653        };
654        if f.alternate() {
655            write!(f, "\x1B]8;;\x1B\\")
656        } else {
657            write!(f, "\x1B]8;;{url}\x1B\\")
658        }
659    }
660}
661
662#[cfg(unix)]
663mod imp {
664    use super::{Shell, TtyWidth};
665    use std::mem;
666
667    pub fn stderr_width() -> TtyWidth {
668        unsafe {
669            let mut winsize: libc::winsize = mem::zeroed();
670            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
671            // as c_uint but ioctl wants c_ulong.
672            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
673                return TtyWidth::NoTty;
674            }
675            if winsize.ws_col > 0 {
676                TtyWidth::Known(winsize.ws_col as usize)
677            } else {
678                TtyWidth::NoTty
679            }
680        }
681    }
682
683    pub fn err_erase_line(shell: &mut Shell) {
684        // This is the "EL - Erase in Line" sequence. It clears from the cursor
685        // to the end of line.
686        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
687        let _ = shell.output.stderr().write_all(b"\x1B[K");
688    }
689}
690
691#[cfg(windows)]
692mod imp {
693    use std::{cmp, mem, ptr};
694
695    use windows_sys::Win32::Foundation::CloseHandle;
696    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
697    use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
698    use windows_sys::Win32::Storage::FileSystem::{
699        CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
700    };
701    use windows_sys::Win32::System::Console::{
702        CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle, STD_ERROR_HANDLE,
703    };
704    use windows_sys::core::PCSTR;
705
706    pub(super) use super::{TtyWidth, default_err_erase_line as err_erase_line};
707
708    pub fn stderr_width() -> TtyWidth {
709        unsafe {
710            let stdout = GetStdHandle(STD_ERROR_HANDLE);
711            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
712            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
713                return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
714            }
715
716            // On mintty/msys/cygwin based terminals, the above fails with
717            // INVALID_HANDLE_VALUE. Use an alternate method which works
718            // in that case as well.
719            let h = CreateFileA(
720                "CONOUT$\0".as_ptr() as PCSTR,
721                GENERIC_READ | GENERIC_WRITE,
722                FILE_SHARE_READ | FILE_SHARE_WRITE,
723                ptr::null_mut(),
724                OPEN_EXISTING,
725                0,
726                std::ptr::null_mut(),
727            );
728            if h == INVALID_HANDLE_VALUE {
729                return TtyWidth::NoTty;
730            }
731
732            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
733            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
734            CloseHandle(h);
735            if rc != 0 {
736                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
737                // Unfortunately cygwin/mintty does not set the size of the
738                // backing console to match the actual window size. This
739                // always reports a size of 80 or 120 (not sure what
740                // determines that). Use a conservative max of 60 which should
741                // work in most circumstances. ConEmu does some magic to
742                // resize the console correctly, but there's no reasonable way
743                // to detect which kind of terminal we are running in, or if
744                // GetConsoleScreenBufferInfo returns accurate information.
745                return TtyWidth::Guess(cmp::min(60, width));
746            }
747
748            TtyWidth::NoTty
749        }
750    }
751}
752
753#[cfg(windows)]
754fn default_err_erase_line(shell: &mut Shell) {
755    match imp::stderr_width() {
756        TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
757            let blank = " ".repeat(max_width);
758            drop(write!(shell.output.stderr(), "{}\r", blank));
759        }
760        _ => (),
761    }
762}