| use std::fmt; |
| use std::io::prelude::*; |
| use std::io::IsTerminal; |
| |
| use anstream::AutoStream; |
| use anstyle::Style; |
| |
| use crate::util::errors::CargoResult; |
| use crate::util::hostname; |
| use crate::util::style::*; |
| |
| pub enum TtyWidth { |
| NoTty, |
| Known(usize), |
| Guess(usize), |
| } |
| |
| impl TtyWidth { |
| /// Returns the width of the terminal to use for diagnostics (which is |
| /// relayed to rustc via `--diagnostic-width`). |
| pub fn diagnostic_terminal_width(&self) -> Option<usize> { |
| // ALLOWED: For testing cargo itself only. |
| #[allow(clippy::disallowed_methods)] |
| if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") { |
| return Some(width.parse().unwrap()); |
| } |
| match *self { |
| TtyWidth::NoTty | TtyWidth::Guess(_) => None, |
| TtyWidth::Known(width) => Some(width), |
| } |
| } |
| |
| /// Returns the width used by progress bars for the tty. |
| pub fn progress_max_width(&self) -> Option<usize> { |
| match *self { |
| TtyWidth::NoTty => None, |
| TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width), |
| } |
| } |
| } |
| |
| /// The requested verbosity of output. |
| #[derive(Debug, Clone, Copy, PartialEq)] |
| pub enum Verbosity { |
| Verbose, |
| Normal, |
| Quiet, |
| } |
| |
| /// An abstraction around console output that remembers preferences for output |
| /// verbosity and color. |
| pub struct Shell { |
| /// Wrapper around stdout/stderr. This helps with supporting sending |
| /// output to a memory buffer which is useful for tests. |
| output: ShellOut, |
| /// How verbose messages should be. |
| verbosity: Verbosity, |
| /// Flag that indicates the current line needs to be cleared before |
| /// printing. Used when a progress bar is currently displayed. |
| needs_clear: bool, |
| hostname: Option<String>, |
| } |
| |
| impl fmt::Debug for Shell { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self.output { |
| ShellOut::Write(_) => f |
| .debug_struct("Shell") |
| .field("verbosity", &self.verbosity) |
| .finish(), |
| ShellOut::Stream { color_choice, .. } => f |
| .debug_struct("Shell") |
| .field("verbosity", &self.verbosity) |
| .field("color_choice", &color_choice) |
| .finish(), |
| } |
| } |
| } |
| |
| /// A `Write`able object, either with or without color support |
| enum ShellOut { |
| /// A plain write object without color support |
| Write(AutoStream<Box<dyn Write>>), |
| /// Color-enabled stdio, with information on whether color should be used |
| Stream { |
| stdout: AutoStream<std::io::Stdout>, |
| stderr: AutoStream<std::io::Stderr>, |
| stderr_tty: bool, |
| color_choice: ColorChoice, |
| hyperlinks: bool, |
| }, |
| } |
| |
| /// Whether messages should use color output |
| #[derive(Debug, PartialEq, Clone, Copy)] |
| pub enum ColorChoice { |
| /// Force color output |
| Always, |
| /// Force disable color output |
| Never, |
| /// Intelligently guess whether to use color output |
| CargoAuto, |
| } |
| |
| impl Shell { |
| /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose |
| /// output. |
| pub fn new() -> Shell { |
| let auto_clr = ColorChoice::CargoAuto; |
| let stdout_choice = auto_clr.to_anstream_color_choice(); |
| let stderr_choice = auto_clr.to_anstream_color_choice(); |
| Shell { |
| output: ShellOut::Stream { |
| stdout: AutoStream::new(std::io::stdout(), stdout_choice), |
| stderr: AutoStream::new(std::io::stderr(), stderr_choice), |
| color_choice: auto_clr, |
| hyperlinks: supports_hyperlinks(), |
| stderr_tty: std::io::stderr().is_terminal(), |
| }, |
| verbosity: Verbosity::Verbose, |
| needs_clear: false, |
| hostname: None, |
| } |
| } |
| |
| /// Creates a shell from a plain writable object, with no color, and max verbosity. |
| pub fn from_write(out: Box<dyn Write>) -> Shell { |
| Shell { |
| output: ShellOut::Write(AutoStream::never(out)), // strip all formatting on write |
| verbosity: Verbosity::Verbose, |
| needs_clear: false, |
| hostname: None, |
| } |
| } |
| |
| /// Prints a message, where the status will have `color` color, and can be justified. The |
| /// messages follows without color. |
| fn print( |
| &mut self, |
| status: &dyn fmt::Display, |
| message: Option<&dyn fmt::Display>, |
| color: &Style, |
| justified: bool, |
| ) -> CargoResult<()> { |
| match self.verbosity { |
| Verbosity::Quiet => Ok(()), |
| _ => { |
| if self.needs_clear { |
| self.err_erase_line(); |
| } |
| self.output |
| .message_stderr(status, message, color, justified) |
| } |
| } |
| } |
| |
| /// Sets whether the next print should clear the current line. |
| pub fn set_needs_clear(&mut self, needs_clear: bool) { |
| self.needs_clear = needs_clear; |
| } |
| |
| /// Returns `true` if the `needs_clear` flag is unset. |
| pub fn is_cleared(&self) -> bool { |
| !self.needs_clear |
| } |
| |
| /// Returns the width of the terminal in spaces, if any. |
| pub fn err_width(&self) -> TtyWidth { |
| match self.output { |
| ShellOut::Stream { |
| stderr_tty: true, .. |
| } => imp::stderr_width(), |
| _ => TtyWidth::NoTty, |
| } |
| } |
| |
| /// Returns `true` if stderr is a tty. |
| pub fn is_err_tty(&self) -> bool { |
| match self.output { |
| ShellOut::Stream { stderr_tty, .. } => stderr_tty, |
| _ => false, |
| } |
| } |
| |
| /// Gets a reference to the underlying stdout writer. |
| pub fn out(&mut self) -> &mut dyn Write { |
| if self.needs_clear { |
| self.err_erase_line(); |
| } |
| self.output.stdout() |
| } |
| |
| /// Gets a reference to the underlying stderr writer. |
| pub fn err(&mut self) -> &mut dyn Write { |
| if self.needs_clear { |
| self.err_erase_line(); |
| } |
| self.output.stderr() |
| } |
| |
| /// Erase from cursor to end of line. |
| pub fn err_erase_line(&mut self) { |
| if self.err_supports_color() { |
| imp::err_erase_line(self); |
| self.needs_clear = false; |
| } |
| } |
| |
| /// Shortcut to right-align and color green a status message. |
| pub fn status<T, U>(&mut self, status: T, message: U) -> CargoResult<()> |
| where |
| T: fmt::Display, |
| U: fmt::Display, |
| { |
| self.print(&status, Some(&message), &HEADER, true) |
| } |
| |
| pub fn status_header<T>(&mut self, status: T) -> CargoResult<()> |
| where |
| T: fmt::Display, |
| { |
| self.print(&status, None, &NOTE, true) |
| } |
| |
| /// Shortcut to right-align a status message. |
| pub fn status_with_color<T, U>( |
| &mut self, |
| status: T, |
| message: U, |
| color: &Style, |
| ) -> CargoResult<()> |
| where |
| T: fmt::Display, |
| U: fmt::Display, |
| { |
| self.print(&status, Some(&message), color, true) |
| } |
| |
| /// Runs the callback only if we are in verbose mode. |
| pub fn verbose<F>(&mut self, mut callback: F) -> CargoResult<()> |
| where |
| F: FnMut(&mut Shell) -> CargoResult<()>, |
| { |
| match self.verbosity { |
| Verbosity::Verbose => callback(self), |
| _ => Ok(()), |
| } |
| } |
| |
| /// Runs the callback if we are not in verbose mode. |
| pub fn concise<F>(&mut self, mut callback: F) -> CargoResult<()> |
| where |
| F: FnMut(&mut Shell) -> CargoResult<()>, |
| { |
| match self.verbosity { |
| Verbosity::Verbose => Ok(()), |
| _ => callback(self), |
| } |
| } |
| |
| /// Prints a red 'error' message. |
| pub fn error<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> { |
| if self.needs_clear { |
| self.err_erase_line(); |
| } |
| self.output |
| .message_stderr(&"error", Some(&message), &ERROR, false) |
| } |
| |
| /// Prints an amber 'warning' message. |
| pub fn warn<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> { |
| match self.verbosity { |
| Verbosity::Quiet => Ok(()), |
| _ => self.print(&"warning", Some(&message), &WARN, false), |
| } |
| } |
| |
| /// Prints a cyan 'note' message. |
| pub fn note<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> { |
| self.print(&"note", Some(&message), &NOTE, false) |
| } |
| |
| /// Updates the verbosity of the shell. |
| pub fn set_verbosity(&mut self, verbosity: Verbosity) { |
| self.verbosity = verbosity; |
| } |
| |
| /// Gets the verbosity of the shell. |
| pub fn verbosity(&self) -> Verbosity { |
| self.verbosity |
| } |
| |
| /// Updates the color choice (always, never, or auto) from a string.. |
| pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> { |
| if let ShellOut::Stream { |
| ref mut stdout, |
| ref mut stderr, |
| ref mut color_choice, |
| .. |
| } = self.output |
| { |
| let cfg = match color { |
| Some("always") => ColorChoice::Always, |
| Some("never") => ColorChoice::Never, |
| |
| Some("auto") | None => ColorChoice::CargoAuto, |
| |
| Some(arg) => anyhow::bail!( |
| "argument for --color must be auto, always, or \ |
| never, but found `{}`", |
| arg |
| ), |
| }; |
| *color_choice = cfg; |
| let stdout_choice = cfg.to_anstream_color_choice(); |
| let stderr_choice = cfg.to_anstream_color_choice(); |
| *stdout = AutoStream::new(std::io::stdout(), stdout_choice); |
| *stderr = AutoStream::new(std::io::stderr(), stderr_choice); |
| } |
| Ok(()) |
| } |
| |
| pub fn set_hyperlinks(&mut self, yes: bool) -> CargoResult<()> { |
| if let ShellOut::Stream { |
| ref mut hyperlinks, .. |
| } = self.output |
| { |
| *hyperlinks = yes; |
| } |
| Ok(()) |
| } |
| |
| /// Gets the current color choice. |
| /// |
| /// If we are not using a color stream, this will always return `Never`, even if the color |
| /// choice has been set to something else. |
| pub fn color_choice(&self) -> ColorChoice { |
| match self.output { |
| ShellOut::Stream { color_choice, .. } => color_choice, |
| ShellOut::Write(_) => ColorChoice::Never, |
| } |
| } |
| |
| /// Whether the shell supports color. |
| pub fn err_supports_color(&self) -> bool { |
| match &self.output { |
| ShellOut::Write(_) => false, |
| ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()), |
| } |
| } |
| |
| pub fn out_supports_color(&self) -> bool { |
| match &self.output { |
| ShellOut::Write(_) => false, |
| ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()), |
| } |
| } |
| |
| pub fn out_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> { |
| let supports_hyperlinks = match &self.output { |
| ShellOut::Write(_) => false, |
| ShellOut::Stream { |
| stdout, hyperlinks, .. |
| } => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks, |
| }; |
| Hyperlink { |
| url: supports_hyperlinks.then_some(url), |
| } |
| } |
| |
| pub fn err_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> { |
| let supports_hyperlinks = match &self.output { |
| ShellOut::Write(_) => false, |
| ShellOut::Stream { |
| stderr, hyperlinks, .. |
| } => stderr.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks, |
| }; |
| if supports_hyperlinks { |
| Hyperlink { url: Some(url) } |
| } else { |
| Hyperlink { url: None } |
| } |
| } |
| |
| pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> { |
| let url = self.file_hyperlink(path); |
| url.map(|u| self.out_hyperlink(u)).unwrap_or_default() |
| } |
| |
| pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> { |
| let url = self.file_hyperlink(path); |
| url.map(|u| self.err_hyperlink(u)).unwrap_or_default() |
| } |
| |
| fn file_hyperlink(&mut self, path: &std::path::Path) -> Option<url::Url> { |
| let mut url = url::Url::from_file_path(path).ok()?; |
| // Do a best-effort of setting the host in the URL to avoid issues with opening a link |
| // scoped to the computer you've SSHed into |
| let hostname = if cfg!(windows) { |
| // Not supported correctly on windows |
| None |
| } else { |
| if let Some(hostname) = self.hostname.as_deref() { |
| Some(hostname) |
| } else { |
| self.hostname = hostname().ok().and_then(|h| h.into_string().ok()); |
| self.hostname.as_deref() |
| } |
| }; |
| let _ = url.set_host(hostname); |
| Some(url) |
| } |
| |
| /// Prints a message to stderr and translates ANSI escape code into console colors. |
| pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> { |
| if self.needs_clear { |
| self.err_erase_line(); |
| } |
| self.err().write_all(message)?; |
| Ok(()) |
| } |
| |
| /// Prints a message to stdout and translates ANSI escape code into console colors. |
| pub fn print_ansi_stdout(&mut self, message: &[u8]) -> CargoResult<()> { |
| if self.needs_clear { |
| self.err_erase_line(); |
| } |
| self.out().write_all(message)?; |
| Ok(()) |
| } |
| |
| pub fn print_json<T: serde::ser::Serialize>(&mut self, obj: &T) -> CargoResult<()> { |
| // Path may fail to serialize to JSON ... |
| let encoded = serde_json::to_string(&obj)?; |
| // ... but don't fail due to a closed pipe. |
| drop(writeln!(self.out(), "{}", encoded)); |
| Ok(()) |
| } |
| } |
| |
| impl Default for Shell { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl ShellOut { |
| /// Prints out a message with a status. The status comes first, and is bold plus the given |
| /// color. The status can be justified, in which case the max width that will right align is |
| /// 12 chars. |
| fn message_stderr( |
| &mut self, |
| status: &dyn fmt::Display, |
| message: Option<&dyn fmt::Display>, |
| style: &Style, |
| justified: bool, |
| ) -> CargoResult<()> { |
| let style = style.render(); |
| let bold = (anstyle::Style::new() | anstyle::Effects::BOLD).render(); |
| let reset = anstyle::Reset.render(); |
| |
| let mut buffer = Vec::new(); |
| if justified { |
| write!(&mut buffer, "{style}{status:>12}{reset}")?; |
| } else { |
| write!(&mut buffer, "{style}{status}{reset}{bold}:{reset}")?; |
| } |
| match message { |
| Some(message) => writeln!(buffer, " {message}")?, |
| None => write!(buffer, " ")?, |
| } |
| self.stderr().write_all(&buffer)?; |
| Ok(()) |
| } |
| |
| /// Gets stdout as a `io::Write`. |
| fn stdout(&mut self) -> &mut dyn Write { |
| match *self { |
| ShellOut::Stream { ref mut stdout, .. } => stdout, |
| ShellOut::Write(ref mut w) => w, |
| } |
| } |
| |
| /// Gets stderr as a `io::Write`. |
| fn stderr(&mut self) -> &mut dyn Write { |
| match *self { |
| ShellOut::Stream { ref mut stderr, .. } => stderr, |
| ShellOut::Write(ref mut w) => w, |
| } |
| } |
| } |
| |
| impl ColorChoice { |
| /// Converts our color choice to anstream's version. |
| fn to_anstream_color_choice(self) -> anstream::ColorChoice { |
| match self { |
| ColorChoice::Always => anstream::ColorChoice::Always, |
| ColorChoice::Never => anstream::ColorChoice::Never, |
| ColorChoice::CargoAuto => anstream::ColorChoice::Auto, |
| } |
| } |
| } |
| |
| fn supports_color(choice: anstream::ColorChoice) -> bool { |
| match choice { |
| anstream::ColorChoice::Always |
| | anstream::ColorChoice::AlwaysAnsi |
| | anstream::ColorChoice::Auto => true, |
| anstream::ColorChoice::Never => false, |
| } |
| } |
| |
| fn supports_hyperlinks() -> bool { |
| #[allow(clippy::disallowed_methods)] // We are reading the state of the system, not config |
| if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) { |
| // Override `supports_hyperlinks` as we have an unknown incompatibility with iTerm2 |
| return false; |
| } |
| |
| supports_hyperlinks::supports_hyperlinks() |
| } |
| |
| pub struct Hyperlink<D: fmt::Display> { |
| url: Option<D>, |
| } |
| |
| impl<D: fmt::Display> Default for Hyperlink<D> { |
| fn default() -> Self { |
| Self { url: None } |
| } |
| } |
| |
| impl<D: fmt::Display> Hyperlink<D> { |
| pub fn open(&self) -> impl fmt::Display { |
| if let Some(url) = self.url.as_ref() { |
| format!("\x1B]8;;{url}\x1B\\") |
| } else { |
| String::new() |
| } |
| } |
| |
| pub fn close(&self) -> impl fmt::Display { |
| if self.url.is_some() { |
| "\x1B]8;;\x1B\\" |
| } else { |
| "" |
| } |
| } |
| } |
| |
| #[cfg(unix)] |
| mod imp { |
| use super::{Shell, TtyWidth}; |
| use std::mem; |
| |
| pub fn stderr_width() -> TtyWidth { |
| unsafe { |
| let mut winsize: libc::winsize = mem::zeroed(); |
| // The .into() here is needed for FreeBSD which defines TIOCGWINSZ |
| // as c_uint but ioctl wants c_ulong. |
| if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 { |
| return TtyWidth::NoTty; |
| } |
| if winsize.ws_col > 0 { |
| TtyWidth::Known(winsize.ws_col as usize) |
| } else { |
| TtyWidth::NoTty |
| } |
| } |
| } |
| |
| pub fn err_erase_line(shell: &mut Shell) { |
| // This is the "EL - Erase in Line" sequence. It clears from the cursor |
| // to the end of line. |
| // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences |
| let _ = shell.output.stderr().write_all(b"\x1B[K"); |
| } |
| } |
| |
| #[cfg(windows)] |
| mod imp { |
| use std::{cmp, mem, ptr}; |
| |
| use windows_sys::core::PCSTR; |
| use windows_sys::Win32::Foundation::CloseHandle; |
| use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; |
| use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE}; |
| use windows_sys::Win32::Storage::FileSystem::{ |
| CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, |
| }; |
| use windows_sys::Win32::System::Console::{ |
| GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, STD_ERROR_HANDLE, |
| }; |
| |
| pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth}; |
| |
| pub fn stderr_width() -> TtyWidth { |
| unsafe { |
| let stdout = GetStdHandle(STD_ERROR_HANDLE); |
| let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed(); |
| if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 { |
| return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize); |
| } |
| |
| // On mintty/msys/cygwin based terminals, the above fails with |
| // INVALID_HANDLE_VALUE. Use an alternate method which works |
| // in that case as well. |
| let h = CreateFileA( |
| "CONOUT$\0".as_ptr() as PCSTR, |
| GENERIC_READ | GENERIC_WRITE, |
| FILE_SHARE_READ | FILE_SHARE_WRITE, |
| ptr::null_mut(), |
| OPEN_EXISTING, |
| 0, |
| 0, |
| ); |
| if h == INVALID_HANDLE_VALUE { |
| return TtyWidth::NoTty; |
| } |
| |
| let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed(); |
| let rc = GetConsoleScreenBufferInfo(h, &mut csbi); |
| CloseHandle(h); |
| if rc != 0 { |
| let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize; |
| // Unfortunately cygwin/mintty does not set the size of the |
| // backing console to match the actual window size. This |
| // always reports a size of 80 or 120 (not sure what |
| // determines that). Use a conservative max of 60 which should |
| // work in most circumstances. ConEmu does some magic to |
| // resize the console correctly, but there's no reasonable way |
| // to detect which kind of terminal we are running in, or if |
| // GetConsoleScreenBufferInfo returns accurate information. |
| return TtyWidth::Guess(cmp::min(60, width)); |
| } |
| |
| TtyWidth::NoTty |
| } |
| } |
| } |
| |
| #[cfg(windows)] |
| fn default_err_erase_line(shell: &mut Shell) { |
| match imp::stderr_width() { |
| TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => { |
| let blank = " ".repeat(max_width); |
| drop(write!(shell.output.stderr(), "{}\r", blank)); |
| } |
| _ => (), |
| } |
| } |