// This file was forked from Cargo on 2019.05.29: // https://github.com/rust-lang/cargo/blob/edd874/src/cargo/core/shell.rs // Cargo is MIT licenced: // https://github.com/rust-lang/cargo/blob/edd874/LICENSE-MIT #![allow(dead_code)] #![allow(irrefutable_let_patterns)] use std::fmt; use std::io::prelude::*; use atty; use deno::ErrBox; use termcolor::Color::{Cyan, Green, Red, Yellow}; use termcolor::{self, Color, ColorSpec, StandardStream, WriteColor}; /// The requested verbosity of output. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Verbosity { Verbose, Normal, Quiet, } /// An abstraction around a `Write`able object that remembers preferences for output verbosity and /// color. pub struct Shell { /// the `Write`able object, either with or without color support (represented by different enum /// variants) err: 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, } impl fmt::Debug for Shell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.err { /* 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 // TODO(ry) Disabling this type of output because it makes Shell // not thread safe and thus not includable in ThreadSafeState. // But I think we will want this in the future. //Write(Box), /// Color-enabled stdio, with information on whether color should be used Stream { stream: StandardStream, tty: bool, color_choice: ColorChoice, }, } /// 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 { Shell { err: ShellOut::Stream { stream: StandardStream::stderr( ColorChoice::CargoAuto.to_termcolor_color_choice(), ), color_choice: ColorChoice::CargoAuto, tty: atty::is(atty::Stream::Stderr), }, verbosity: Verbosity::Verbose, needs_clear: false, } } /* /// Creates a shell from a plain writable object, with no color, and max verbosity. pub fn from_write(out: Box) -> Shell { Shell { err: ShellOut::Write(out), verbosity: Verbosity::Verbose, needs_clear: false, } } */ /// 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: Color, justified: bool, ) -> Result<(), ErrBox> { match self.verbosity { Verbosity::Quiet => Ok(()), _ => { if self.needs_clear { self.err_erase_line(); } self.err.print(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) -> Option { match self.err { ShellOut::Stream { tty: true, .. } => imp::stderr_width(), _ => None, } } /// Returns `true` if stderr is a tty. pub fn is_err_tty(&self) -> bool { match self.err { ShellOut::Stream { tty, .. } => tty, // _ => false, } } /// Gets a reference to the underlying writer. pub fn err(&mut self) -> &mut dyn Write { if self.needs_clear { self.err_erase_line(); } self.err.as_write() } /// Erase from cursor to end of line. pub fn err_erase_line(&mut self) { if let ShellOut::Stream { tty: true, .. } = self.err { imp::err_erase_line(self); self.needs_clear = false; } } /// Shortcut to right-align and color green a status message. pub fn status(&mut self, status: T, message: U) -> Result<(), ErrBox> where T: fmt::Display, U: fmt::Display, { self.print(&status, Some(&message), Green, false) } pub fn status_header(&mut self, status: T) -> Result<(), ErrBox> where T: fmt::Display, { self.print(&status, None, Cyan, true) } /// Shortcut to right-align a status message. pub fn status_with_color( &mut self, status: T, message: U, color: Color, ) -> Result<(), ErrBox> 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(&mut self, mut callback: F) -> Result<(), ErrBox> where F: FnMut(&mut Shell) -> Result<(), ErrBox>, { match self.verbosity { Verbosity::Verbose => callback(self), _ => Ok(()), } } /// Runs the callback if we are not in verbose mode. pub fn concise(&mut self, mut callback: F) -> Result<(), ErrBox> where F: FnMut(&mut Shell) -> Result<(), ErrBox>, { match self.verbosity { Verbosity::Verbose => Ok(()), _ => callback(self), } } /// Prints a red 'error' message. pub fn error(&mut self, message: T) -> Result<(), ErrBox> { self.print(&"error:", Some(&message), Red, false) } /// Prints an amber 'warning' message. pub fn warn(&mut self, message: T) -> Result<(), ErrBox> { match self.verbosity { Verbosity::Quiet => Ok(()), _ => self.print(&"warning:", Some(&message), Yellow, 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>, ) -> Result<(), ErrBox> { if let ShellOut::Stream { ref mut stream, ref mut color_choice, .. } = self.err { let cfg = match color { Some("always") => ColorChoice::Always, Some("never") => ColorChoice::Never, Some("auto") | None => ColorChoice::CargoAuto, Some(arg) => panic!( "argument for --color must be auto, always, or \ never, but found `{}`", arg ), }; *color_choice = cfg; *stream = StandardStream::stderr(cfg.to_termcolor_color_choice()); } 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.err { ShellOut::Stream { color_choice, .. } => color_choice, // ShellOut::Write(_) => ColorChoice::Never, } } /// Whether the shell supports color. pub fn supports_color(&self) -> bool { match &self.err { // ShellOut::Write(_) => false, ShellOut::Stream { stream, .. } => stream.supports_color(), } } /// Prints a message and translates ANSI escape code into console colors. pub fn print_ansi(&mut self, message: &[u8]) -> Result<(), ErrBox> { if self.needs_clear { self.err_erase_line(); } #[cfg(windows)] { if let ShellOut::Stream { stream, .. } = &mut self.err { ::fwdansi::write_ansi(stream, message)?; return Ok(()); } } self.err().write_all(message)?; 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 print( &mut self, status: &dyn fmt::Display, message: Option<&dyn fmt::Display>, color: Color, justified: bool, ) -> Result<(), ErrBox> { match *self { ShellOut::Stream { ref mut stream, .. } => { stream.reset()?; stream .set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?; if justified { write!(stream, "{:>12}", status)?; } else { write!(stream, "{}", status)?; } stream.reset()?; match message { Some(message) => writeln!(stream, " {}", message)?, None => write!(stream, " ")?, } } /* ShellOut::Write(ref mut w) => { if justified { write!(w, "{:>12}", status)?; } else { write!(w, "{}", status)?; } match message { Some(message) => writeln!(w, " {}", message)?, None => write!(w, " ")?, } } */ } Ok(()) } /// Gets this object as a `io::Write`. fn as_write(&mut self) -> &mut dyn Write { match *self { ShellOut::Stream { ref mut stream, .. } => stream, // ShellOut::Write(ref mut w) => w, } } } impl ColorChoice { /// Converts our color choice to termcolor's version. fn to_termcolor_color_choice(self) -> termcolor::ColorChoice { match self { ColorChoice::Always => termcolor::ColorChoice::Always, ColorChoice::Never => termcolor::ColorChoice::Never, ColorChoice::CargoAuto => { if atty::is(atty::Stream::Stderr) { termcolor::ColorChoice::Auto } else { termcolor::ColorChoice::Never } } } } } #[cfg(any(target_os = "linux", target_os = "macos"))] mod imp { use std::mem; use libc; use super::Shell; pub fn stderr_width() -> Option { unsafe { let mut winsize: libc::winsize = mem::zeroed(); if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 { return None; } if winsize.ws_col > 0 { Some(winsize.ws_col as usize) } else { None } } } 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.err.as_write().write_all(b"\x1B[K"); } } #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))] mod imp { pub(super) use super::default_err_erase_line as err_erase_line; pub fn stderr_width() -> Option { None } } #[cfg(windows)] mod imp { use std::{cmp, mem, ptr}; use winapi::um::fileapi::*; use winapi::um::handleapi::*; use winapi::um::processenv::*; use winapi::um::winbase::*; use winapi::um::wincon::*; use winapi::um::winnt::*; pub(super) use super::default_err_erase_line as err_erase_line; pub fn stderr_width() -> Option { unsafe { let stdout = GetStdHandle(STD_ERROR_HANDLE); let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed(); if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 { return Some((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 *const CHAR, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, ptr::null_mut(), OPEN_EXISTING, 0, ptr::null_mut(), ); if h == INVALID_HANDLE_VALUE { return None; } 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 Some(cmp::min(60, width)); } None } } } #[cfg( any( all(unix, not(any(target_os = "linux", target_os = "macos"))), windows ) )] fn default_err_erase_line(shell: &mut Shell) { if let Some(max_width) = imp::stderr_width() { let blank = " ".repeat(max_width); drop(write!(shell.err.as_write(), "{}\r", blank)); } }