cargo_px/shell/
shell_.rs

1use crate::shell::hostname::hostname;
2use crate::shell::shell_::style::{ERROR, HEADER, NOTE, WARN};
3use anstream::AutoStream;
4use anstyle::Style;
5use std::fmt;
6use std::io::{IsTerminal, Write};
7
8pub enum TtyWidth {
9    NoTty,
10    Known(usize),
11    Guess(usize),
12}
13
14/// The requested verbosity of output.
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum Verbosity {
17    Verbose,
18    Normal,
19    Quiet,
20}
21
22/// An abstraction around console output that remembers preferences for output
23/// verbosity and color.
24pub struct Shell {
25    /// Wrapper around stdout/stderr. This helps with supporting sending
26    /// output to a memory buffer which is useful for tests.
27    output: ShellOut,
28    /// How verbose messages should be.
29    verbosity: Verbosity,
30    /// Flag that indicates the current line needs to be cleared before
31    /// printing. Used when a progress bar is currently displayed.
32    needs_clear: bool,
33    hostname: Option<String>,
34}
35
36impl fmt::Debug for Shell {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self.output {
39            ShellOut::Write(_) => f
40                .debug_struct("Shell")
41                .field("verbosity", &self.verbosity)
42                .finish(),
43            ShellOut::Stream { color_choice, .. } => f
44                .debug_struct("Shell")
45                .field("verbosity", &self.verbosity)
46                .field("color_choice", &color_choice)
47                .finish(),
48        }
49    }
50}
51
52/// A `Write`able object, either with or without color support
53enum ShellOut {
54    /// A plain write object without color support
55    Write(AutoStream<Box<dyn Write>>),
56    /// Color-enabled stdio, with information on whether color should be used
57    Stream {
58        stdout: AutoStream<std::io::Stdout>,
59        stderr: AutoStream<std::io::Stderr>,
60        stderr_tty: bool,
61        color_choice: ColorChoice,
62        hyperlinks: bool,
63    },
64}
65
66/// Whether messages should use color output
67#[derive(Debug, PartialEq, Clone, Copy)]
68pub enum ColorChoice {
69    /// Force color output
70    Always,
71    /// Force disable color output
72    Never,
73    /// Intelligently guess whether to use color output
74    CargoAuto,
75}
76
77impl Shell {
78    /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose
79    /// output.
80    pub fn new() -> Shell {
81        let auto_clr = ColorChoice::CargoAuto;
82        let stdout_choice = auto_clr.to_anstream_color_choice();
83        let stderr_choice = auto_clr.to_anstream_color_choice();
84        Shell {
85            output: ShellOut::Stream {
86                stdout: AutoStream::new(std::io::stdout(), stdout_choice),
87                stderr: AutoStream::new(std::io::stderr(), stderr_choice),
88                color_choice: auto_clr,
89                hyperlinks: supports_hyperlinks(),
90                stderr_tty: std::io::stderr().is_terminal(),
91            },
92            verbosity: Verbosity::Verbose,
93            needs_clear: false,
94            hostname: None,
95        }
96    }
97
98    /// Creates a shell from a plain writable object, with no color, and max verbosity.
99    pub fn from_write(out: Box<dyn Write>) -> Shell {
100        Shell {
101            output: ShellOut::Write(AutoStream::never(out)), // strip all formatting on write
102            verbosity: Verbosity::Verbose,
103            needs_clear: false,
104            hostname: None,
105        }
106    }
107
108    /// Prints a message, where the status will have `color` color, and can be justified. The
109    /// messages follows without color.
110    fn print(
111        &mut self,
112        status: &dyn fmt::Display,
113        message: Option<&dyn fmt::Display>,
114        color: &Style,
115        justified: bool,
116    ) -> anyhow::Result<()> {
117        match self.verbosity {
118            Verbosity::Quiet => Ok(()),
119            _ => {
120                if self.needs_clear {
121                    self.err_erase_line();
122                }
123                self.output
124                    .message_stderr(status, message, color, justified)
125            }
126        }
127    }
128
129    /// Sets whether the next print should clear the current line.
130    pub fn set_needs_clear(&mut self, needs_clear: bool) {
131        self.needs_clear = needs_clear;
132    }
133
134    /// Returns `true` if the `needs_clear` flag is unset.
135    pub fn is_cleared(&self) -> bool {
136        !self.needs_clear
137    }
138
139    /// Returns the width of the terminal in spaces, if any.
140    pub fn err_width(&self) -> TtyWidth {
141        match self.output {
142            ShellOut::Stream {
143                stderr_tty: true, ..
144            } => imp::stderr_width(),
145            _ => TtyWidth::NoTty,
146        }
147    }
148
149    /// Returns `true` if stderr is a tty.
150    pub fn is_err_tty(&self) -> bool {
151        match self.output {
152            ShellOut::Stream { stderr_tty, .. } => stderr_tty,
153            _ => false,
154        }
155    }
156
157    /// Gets a reference to the underlying stdout writer.
158    pub fn out(&mut self) -> &mut dyn Write {
159        if self.needs_clear {
160            self.err_erase_line();
161        }
162        self.output.stdout()
163    }
164
165    /// Gets a reference to the underlying stderr writer.
166    pub fn err(&mut self) -> &mut dyn Write {
167        if self.needs_clear {
168            self.err_erase_line();
169        }
170        self.output.stderr()
171    }
172
173    /// Erase from cursor to end of line.
174    pub fn err_erase_line(&mut self) {
175        if self.err_supports_color() {
176            imp::err_erase_line(self);
177            self.needs_clear = false;
178        }
179    }
180
181    /// Shortcut to right-align and color green a status message.
182    pub fn status<T, U>(&mut self, status: T, message: U) -> anyhow::Result<()>
183    where
184        T: fmt::Display,
185        U: fmt::Display,
186    {
187        self.print(&status, Some(&message), &HEADER, true)
188    }
189
190    pub fn status_header<T>(&mut self, status: T) -> anyhow::Result<()>
191    where
192        T: fmt::Display,
193    {
194        self.print(&status, None, &NOTE, true)
195    }
196
197    /// Shortcut to right-align a status message.
198    pub fn status_with_color<T, U>(
199        &mut self,
200        status: T,
201        message: U,
202        color: &Style,
203    ) -> anyhow::Result<()>
204    where
205        T: fmt::Display,
206        U: fmt::Display,
207    {
208        self.print(&status, Some(&message), color, true)
209    }
210
211    /// Runs the callback only if we are in verbose mode.
212    pub fn verbose<F>(&mut self, mut callback: F) -> anyhow::Result<()>
213    where
214        F: FnMut(&mut Shell) -> anyhow::Result<()>,
215    {
216        match self.verbosity {
217            Verbosity::Verbose => callback(self),
218            _ => Ok(()),
219        }
220    }
221
222    /// Runs the callback if we are not in verbose mode.
223    pub fn concise<F>(&mut self, mut callback: F) -> anyhow::Result<()>
224    where
225        F: FnMut(&mut Shell) -> anyhow::Result<()>,
226    {
227        match self.verbosity {
228            Verbosity::Verbose => Ok(()),
229            _ => callback(self),
230        }
231    }
232
233    /// Prints a red 'error' message.
234    pub fn error<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
235        if self.needs_clear {
236            self.err_erase_line();
237        }
238        self.output
239            .message_stderr(&"error", Some(&message), &ERROR, false)
240    }
241
242    /// Prints an amber 'warning' message.
243    pub fn warn<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
244        match self.verbosity {
245            Verbosity::Quiet => Ok(()),
246            _ => self.print(&"warning", Some(&message), &WARN, false),
247        }
248    }
249
250    /// Prints a cyan 'note' message.
251    pub fn note<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
252        self.print(&"note", Some(&message), &NOTE, false)
253    }
254
255    /// Updates the verbosity of the shell.
256    pub fn set_verbosity(&mut self, verbosity: Verbosity) {
257        self.verbosity = verbosity;
258    }
259
260    /// Gets the verbosity of the shell.
261    pub fn verbosity(&self) -> Verbosity {
262        self.verbosity
263    }
264
265    /// Updates the color choice (always, never, or auto) from a string..
266    pub fn set_color_choice(&mut self, color: Option<&str>) -> anyhow::Result<()> {
267        if let ShellOut::Stream {
268            ref mut stdout,
269            ref mut stderr,
270            ref mut color_choice,
271            ..
272        } = self.output
273        {
274            let cfg = match color {
275                Some("always") => ColorChoice::Always,
276                Some("never") => ColorChoice::Never,
277
278                Some("auto") | None => ColorChoice::CargoAuto,
279
280                Some(arg) => anyhow::bail!(
281                    "argument for --color must be auto, always, or \
282                     never, but found `{}`",
283                    arg
284                ),
285            };
286            *color_choice = cfg;
287            let stdout_choice = cfg.to_anstream_color_choice();
288            let stderr_choice = cfg.to_anstream_color_choice();
289            *stdout = AutoStream::new(std::io::stdout(), stdout_choice);
290            *stderr = AutoStream::new(std::io::stderr(), stderr_choice);
291        }
292        Ok(())
293    }
294
295    pub fn set_hyperlinks(&mut self, yes: bool) -> anyhow::Result<()> {
296        if let ShellOut::Stream {
297            ref mut hyperlinks, ..
298        } = self.output
299        {
300            *hyperlinks = yes;
301        }
302        Ok(())
303    }
304
305    /// Gets the current color choice.
306    ///
307    /// If we are not using a color stream, this will always return `Never`, even if the color
308    /// choice has been set to something else.
309    pub fn color_choice(&self) -> ColorChoice {
310        match self.output {
311            ShellOut::Stream { color_choice, .. } => color_choice,
312            ShellOut::Write(_) => ColorChoice::Never,
313        }
314    }
315
316    /// Whether the shell supports color.
317    pub fn err_supports_color(&self) -> bool {
318        match &self.output {
319            ShellOut::Write(_) => false,
320            ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
321        }
322    }
323
324    pub fn out_supports_color(&self) -> bool {
325        match &self.output {
326            ShellOut::Write(_) => false,
327            ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
328        }
329    }
330
331    pub fn out_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
332        let supports_hyperlinks = match &self.output {
333            ShellOut::Write(_) => false,
334            ShellOut::Stream {
335                stdout, hyperlinks, ..
336            } => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
337        };
338        Hyperlink {
339            url: supports_hyperlinks.then_some(url),
340        }
341    }
342
343    pub fn err_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
344        let supports_hyperlinks = match &self.output {
345            ShellOut::Write(_) => false,
346            ShellOut::Stream {
347                stderr, hyperlinks, ..
348            } => stderr.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
349        };
350        if supports_hyperlinks {
351            Hyperlink { url: Some(url) }
352        } else {
353            Hyperlink { url: None }
354        }
355    }
356
357    pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
358        let url = self.file_hyperlink(path);
359        url.map(|u| self.out_hyperlink(u)).unwrap_or_default()
360    }
361
362    pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
363        let url = self.file_hyperlink(path);
364        url.map(|u| self.err_hyperlink(u)).unwrap_or_default()
365    }
366
367    fn file_hyperlink(&mut self, path: &std::path::Path) -> Option<url::Url> {
368        let mut url = url::Url::from_file_path(path).ok()?;
369        // Do a best-effort of setting the host in the URL to avoid issues with opening a link
370        // scoped to the computer you've SSHed into
371        let hostname = if cfg!(windows) {
372            // Not supported correctly on windows
373            None
374        } else 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        let _ = url.set_host(hostname);
381        Some(url)
382    }
383
384    /// Prints a message to stderr and translates ANSI escape code into console colors.
385    pub fn print_ansi_stderr(&mut self, message: &[u8]) -> anyhow::Result<()> {
386        if self.needs_clear {
387            self.err_erase_line();
388        }
389        self.err().write_all(message)?;
390        Ok(())
391    }
392
393    /// Prints a message to stdout and translates ANSI escape code into console colors.
394    pub fn print_ansi_stdout(&mut self, message: &[u8]) -> anyhow::Result<()> {
395        if self.needs_clear {
396            self.err_erase_line();
397        }
398        self.out().write_all(message)?;
399        Ok(())
400    }
401
402    pub fn print_json<T: serde::ser::Serialize>(&mut self, obj: &T) -> anyhow::Result<()> {
403        // Path may fail to serialize to JSON ...
404        let encoded = serde_json::to_string(&obj)?;
405        // ... but don't fail due to a closed pipe.
406        drop(writeln!(self.out(), "{encoded}"));
407        Ok(())
408    }
409}
410
411impl Default for Shell {
412    fn default() -> Self {
413        Self::new()
414    }
415}
416
417impl ShellOut {
418    /// Prints out a message with a status. The status comes first, and is bold plus the given
419    /// color. The status can be justified, in which case the max width that will right align is
420    /// 12 chars.
421    fn message_stderr(
422        &mut self,
423        status: &dyn fmt::Display,
424        message: Option<&dyn fmt::Display>,
425        style: &Style,
426        justified: bool,
427    ) -> anyhow::Result<()> {
428        let style = style.render();
429        let bold = (anstyle::Style::new() | anstyle::Effects::BOLD).render();
430        let reset = anstyle::Reset.render();
431
432        let mut buffer = Vec::new();
433        if justified {
434            write!(&mut buffer, "{style}{status:>12}{reset}")?;
435        } else {
436            write!(&mut buffer, "{style}{status}{reset}{bold}:{reset}")?;
437        }
438        match message {
439            Some(message) => writeln!(buffer, " {message}")?,
440            None => write!(buffer, " ")?,
441        }
442        self.stderr().write_all(&buffer)?;
443        Ok(())
444    }
445
446    /// Gets stdout as a `io::Write`.
447    fn stdout(&mut self) -> &mut dyn Write {
448        match *self {
449            ShellOut::Stream { ref mut stdout, .. } => stdout,
450            ShellOut::Write(ref mut w) => w,
451        }
452    }
453
454    /// Gets stderr as a `io::Write`.
455    fn stderr(&mut self) -> &mut dyn Write {
456        match *self {
457            ShellOut::Stream { ref mut stderr, .. } => stderr,
458            ShellOut::Write(ref mut w) => w,
459        }
460    }
461}
462
463impl ColorChoice {
464    /// Converts our color choice to anstream's version.
465    fn to_anstream_color_choice(self) -> anstream::ColorChoice {
466        match self {
467            ColorChoice::Always => anstream::ColorChoice::Always,
468            ColorChoice::Never => anstream::ColorChoice::Never,
469            ColorChoice::CargoAuto => anstream::ColorChoice::Auto,
470        }
471    }
472}
473
474fn supports_color(choice: anstream::ColorChoice) -> bool {
475    match choice {
476        anstream::ColorChoice::Always
477        | anstream::ColorChoice::AlwaysAnsi
478        | anstream::ColorChoice::Auto => true,
479        anstream::ColorChoice::Never => false,
480    }
481}
482
483fn supports_hyperlinks() -> bool {
484    #[allow(clippy::disallowed_methods)] // We are reading the state of the system, not config
485    if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
486        // Override `supports_hyperlinks` as we have an unknown incompatibility with iTerm2
487        return false;
488    }
489
490    supports_hyperlinks::supports_hyperlinks()
491}
492
493pub struct Hyperlink<D: fmt::Display> {
494    url: Option<D>,
495}
496
497impl<D: fmt::Display> Default for Hyperlink<D> {
498    fn default() -> Self {
499        Self { url: None }
500    }
501}
502
503impl<D: fmt::Display> Hyperlink<D> {
504    pub fn open(&self) -> impl fmt::Display {
505        if let Some(url) = self.url.as_ref() {
506            format!("\x1B]8;;{url}\x1B\\")
507        } else {
508            String::new()
509        }
510    }
511
512    pub fn close(&self) -> impl fmt::Display {
513        if self.url.is_some() {
514            "\x1B]8;;\x1B\\"
515        } else {
516            ""
517        }
518    }
519}
520
521#[cfg(unix)]
522mod imp {
523    use super::{Shell, TtyWidth};
524    use std::mem;
525
526    pub fn stderr_width() -> TtyWidth {
527        unsafe {
528            let mut winsize: libc::winsize = mem::zeroed();
529            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
530            // as c_uint but ioctl wants c_ulong.
531            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
532                return TtyWidth::NoTty;
533            }
534            if winsize.ws_col > 0 {
535                TtyWidth::Known(winsize.ws_col as usize)
536            } else {
537                TtyWidth::NoTty
538            }
539        }
540    }
541
542    pub fn err_erase_line(shell: &mut Shell) {
543        // This is the "EL - Erase in Line" sequence. It clears from the cursor
544        // to the end of line.
545        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
546        let _ = shell.output.stderr().write_all(b"\x1B[K");
547    }
548}
549
550#[cfg(windows)]
551mod imp {
552    use std::{cmp, mem, ptr};
553
554    use windows_sys::core::PCSTR;
555    use windows_sys::Win32::Foundation::CloseHandle;
556    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
557    use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
558    use windows_sys::Win32::Storage::FileSystem::{
559        CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
560    };
561    use windows_sys::Win32::System::Console::{
562        GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, STD_ERROR_HANDLE,
563    };
564
565    pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth};
566
567    pub fn stderr_width() -> TtyWidth {
568        unsafe {
569            let stdout = GetStdHandle(STD_ERROR_HANDLE);
570            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
571            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
572                return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
573            }
574
575            // On mintty/msys/cygwin based terminals, the above fails with
576            // INVALID_HANDLE_VALUE. Use an alternate method which works
577            // in that case as well.
578            let h = CreateFileA(
579                "CONOUT$\0".as_ptr() as PCSTR,
580                GENERIC_READ | GENERIC_WRITE,
581                FILE_SHARE_READ | FILE_SHARE_WRITE,
582                ptr::null_mut(),
583                OPEN_EXISTING,
584                0,
585                0,
586            );
587            if h == INVALID_HANDLE_VALUE {
588                return TtyWidth::NoTty;
589            }
590
591            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
592            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
593            CloseHandle(h);
594            if rc != 0 {
595                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
596                // Unfortunately cygwin/mintty does not set the size of the
597                // backing console to match the actual window size. This
598                // always reports a size of 80 or 120 (not sure what
599                // determines that). Use a conservative max of 60 which should
600                // work in most circumstances. ConEmu does some magic to
601                // resize the console correctly, but there's no reasonable way
602                // to detect which kind of terminal we are running in, or if
603                // GetConsoleScreenBufferInfo returns accurate information.
604                return TtyWidth::Guess(cmp::min(60, width));
605            }
606
607            TtyWidth::NoTty
608        }
609    }
610}
611
612#[cfg(windows)]
613fn default_err_erase_line(shell: &mut Shell) {
614    match imp::stderr_width() {
615        TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
616            let blank = " ".repeat(max_width);
617            drop(write!(shell.output.stderr(), "{}\r", blank));
618        }
619        _ => (),
620    }
621}
622
623mod style {
624    use anstyle::{AnsiColor, Effects, Style};
625
626    pub const HEADER: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
627    pub const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD);
628    pub const WARN: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD);
629    pub const NOTE: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
630}