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}