1use std::fmt;
2use std::io::prelude::*;
3
4use termcolor::Color::{Cyan, Green, Red, Yellow};
5use termcolor::{self, Color, ColorSpec, StandardStream, WriteColor};
6
7use crate::error::{Error, Result};
8
9pub enum TtyWidth {
10 NoTty,
11 Known(usize),
12 Guess(usize),
13}
14
15impl TtyWidth {
16 pub fn diagnostic_terminal_width(&self) -> Option<usize> {
19 match *self {
20 TtyWidth::NoTty | TtyWidth::Guess(_) => None,
21 TtyWidth::Known(width) => Some(width),
22 }
23 }
24
25 pub fn progress_max_width(&self) -> Option<usize> {
27 match *self {
28 TtyWidth::NoTty => None,
29 TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Verbosity {
37 Verbose,
38 Normal,
39 Quiet,
40}
41
42pub struct Shell {
45 output: ShellOut,
48 verbosity: Verbosity,
50 needs_clear: bool,
53}
54
55impl fmt::Debug for Shell {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self.output {
58 ShellOut::Write(_) => f
59 .debug_struct("Shell")
60 .field("verbosity", &self.verbosity)
61 .finish(),
62 ShellOut::Stream { color_choice, .. } => f
63 .debug_struct("Shell")
64 .field("verbosity", &self.verbosity)
65 .field("color_choice", &color_choice)
66 .finish(),
67 }
68 }
69}
70
71enum ShellOut {
73 Write(Box<dyn Write>),
75 Stream {
77 stdout: StandardStream,
78 stderr: StandardStream,
79 stderr_tty: bool,
80 color_choice: ColorChoice,
81 },
82}
83
84#[derive(Debug, PartialEq, Eq, Clone, Copy)]
86pub enum ColorChoice {
87 Always,
89 Never,
91 CargoAuto,
93}
94
95impl Shell {
96 pub fn new() -> Shell {
99 let auto = ColorChoice::CargoAuto.to_termcolor_color_choice();
100 Shell {
101 output: ShellOut::Stream {
102 stdout: StandardStream::stdout(auto),
103 stderr: StandardStream::stderr(auto),
104 color_choice: ColorChoice::CargoAuto,
105 stderr_tty: atty::is(atty::Stream::Stderr),
106 },
107 verbosity: Verbosity::Verbose,
108 needs_clear: false,
109 }
110 }
111
112 pub fn from_write(out: Box<dyn Write>) -> Shell {
114 Shell {
115 output: ShellOut::Write(out),
116 verbosity: Verbosity::Verbose,
117 needs_clear: false,
118 }
119 }
120
121 fn print(
124 &mut self,
125 status: &dyn fmt::Display,
126 message: Option<&dyn fmt::Display>,
127 color: Color,
128 justified: bool,
129 ) -> Result<()> {
130 match self.verbosity {
131 Verbosity::Quiet => Ok(()),
132 _ => {
133 if self.needs_clear {
134 self.err_erase_line();
135 }
136 self.output
137 .message_stderr(status, message, color, justified)
138 }
139 }
140 }
141
142 pub fn set_needs_clear(&mut self, needs_clear: bool) {
144 self.needs_clear = needs_clear;
145 }
146
147 pub fn is_cleared(&self) -> bool {
149 !self.needs_clear
150 }
151
152 pub fn err_width(&self) -> TtyWidth {
154 match self.output {
155 ShellOut::Stream {
156 stderr_tty: true, ..
157 } => imp::stderr_width(),
158 _ => TtyWidth::NoTty,
159 }
160 }
161
162 pub fn is_err_tty(&self) -> bool {
164 match self.output {
165 ShellOut::Stream { stderr_tty, .. } => stderr_tty,
166 _ => false,
167 }
168 }
169
170 pub fn out(&mut self) -> &mut dyn Write {
172 if self.needs_clear {
173 self.err_erase_line();
174 }
175 self.output.stdout()
176 }
177
178 pub fn err(&mut self) -> &mut dyn Write {
180 if self.needs_clear {
181 self.err_erase_line();
182 }
183 self.output.stderr()
184 }
185
186 pub fn err_erase_line(&mut self) {
188 if let ShellOut::Stream {
189 stderr_tty: true, ..
190 } = self.output
191 {
192 imp::err_erase_line(self);
193 self.needs_clear = false;
194 }
195 }
196
197 pub fn status<T>(&mut self, status: T) -> Result<()>
199 where
200 T: fmt::Display,
201 {
202 self.print(&status, None, Green, false)
203 }
204
205 pub fn status_message<T, U>(&mut self, status: T, message: U) -> Result<()>
207 where
208 T: fmt::Display,
209 U: fmt::Display,
210 {
211 self.print(&status, Some(&message), Green, false)
212 }
213
214 pub fn status_header<T>(&mut self, status: T) -> Result<()>
215 where
216 T: fmt::Display,
217 {
218 self.print(&status, None, Cyan, false)
219 }
220
221 pub fn status_with_color<T, U>(&mut self, status: T, message: U, color: Color) -> Result<()>
223 where
224 T: fmt::Display,
225 U: fmt::Display,
226 {
227 self.print(&status, Some(&message), color, false)
228 }
229
230 pub fn verbose<F>(&mut self, mut callback: F) -> Result<()>
232 where
233 F: FnMut(&mut Shell) -> Result<()>,
234 {
235 match self.verbosity {
236 Verbosity::Verbose => callback(self),
237 _ => Ok(()),
238 }
239 }
240
241 pub fn concise<F>(&mut self, mut callback: F) -> Result<()>
243 where
244 F: FnMut(&mut Shell) -> Result<()>,
245 {
246 match self.verbosity {
247 Verbosity::Verbose => Ok(()),
248 _ => callback(self),
249 }
250 }
251
252 pub fn error<T: fmt::Display>(&mut self, message: T) -> Result<()> {
254 if self.needs_clear {
255 self.err_erase_line();
256 }
257 self.output
258 .message_stderr(&"error", Some(&message), Red, false)
259 }
260
261 pub fn warn<T: fmt::Display>(&mut self, message: T) -> Result<()> {
263 match self.verbosity {
264 Verbosity::Quiet => Ok(()),
265 _ => self.print(&"warning", Some(&message), Yellow, false),
266 }
267 }
268
269 pub fn note<T: fmt::Display>(&mut self, message: T) -> Result<()> {
271 self.print(&"note", Some(&message), Cyan, false)
272 }
273
274 pub fn set_verbosity(&mut self, verbosity: Verbosity) {
276 self.verbosity = verbosity;
277 }
278
279 pub fn verbosity(&self) -> Verbosity {
281 self.verbosity
282 }
283
284 pub fn set_color_choice(&mut self, color: Option<&str>) -> Result<()> {
286 if let ShellOut::Stream {
287 ref mut stdout,
288 ref mut stderr,
289 ref mut color_choice,
290 ..
291 } = self.output
292 {
293 let cfg = match color {
294 Some("always") => ColorChoice::Always,
295 Some("never") => ColorChoice::Never,
296
297 Some("auto") | None => ColorChoice::CargoAuto,
298
299 Some(arg) => return Err(Error::FailedToChooseShellStringColor(arg.to_owned())),
300 };
301 *color_choice = cfg;
302 let choice = cfg.to_termcolor_color_choice();
303 *stdout = StandardStream::stdout(choice);
304 *stderr = StandardStream::stderr(choice);
305 }
306 Ok(())
307 }
308
309 pub fn color_choice(&self) -> ColorChoice {
314 match self.output {
315 ShellOut::Stream { color_choice, .. } => color_choice,
316 ShellOut::Write(_) => ColorChoice::Never,
317 }
318 }
319
320 pub fn err_supports_color(&self) -> bool {
322 match &self.output {
323 ShellOut::Write(_) => false,
324 ShellOut::Stream { stderr, .. } => stderr.supports_color(),
325 }
326 }
327
328 pub fn print_ansi(&mut self, message: &[u8]) -> Result<()> {
330 if self.needs_clear {
331 self.err_erase_line();
332 }
333 #[cfg(windows)]
334 {
335 if let ShellOut::Stream { stderr, .. } = &mut self.output {
336 fwdansi::write_ansi(stderr, message)?;
337 return Ok(());
338 }
339 }
340 self.err().write_all(message)?;
341 Ok(())
342 }
343}
344
345impl Default for Shell {
346 fn default() -> Self {
347 Self::new()
348 }
349}
350
351impl ShellOut {
352 fn message_stderr(
356 &mut self,
357 status: &dyn fmt::Display,
358 message: Option<&dyn fmt::Display>,
359 color: Color,
360 justified: bool,
361 ) -> Result<()> {
362 match *self {
363 ShellOut::Stream { ref mut stderr, .. } => {
364 stderr.reset()?;
365 stderr.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
366 if justified {
367 write!(stderr, "{:>12}", status)?;
368 } else {
369 write!(stderr, "{}", status)?;
370 stderr.set_color(ColorSpec::new().set_bold(true))?;
371 if message.is_some() {
372 write!(stderr, ":")?;
373 }
374 }
375 stderr.reset()?;
376 match message {
377 Some(message) => writeln!(stderr, " {}", message)?,
378 None => writeln!(stderr, " ")?,
379 }
380 }
381 ShellOut::Write(ref mut w) => {
382 if justified {
383 write!(w, "{:>12}", status)?;
384 } else {
385 write!(w, "{}:", status)?;
386 }
387 match message {
388 Some(message) => writeln!(w, " {}", message)?,
389 None => write!(w, " ")?,
390 }
391 }
392 }
393 Ok(())
394 }
395
396 fn stdout(&mut self) -> &mut dyn Write {
398 match *self {
399 ShellOut::Stream { ref mut stdout, .. } => stdout,
400 ShellOut::Write(ref mut w) => w,
401 }
402 }
403
404 fn stderr(&mut self) -> &mut dyn Write {
406 match *self {
407 ShellOut::Stream { ref mut stderr, .. } => stderr,
408 ShellOut::Write(ref mut w) => w,
409 }
410 }
411}
412
413impl ColorChoice {
414 fn to_termcolor_color_choice(self) -> termcolor::ColorChoice {
416 match self {
417 ColorChoice::Always => termcolor::ColorChoice::Always,
418 ColorChoice::Never => termcolor::ColorChoice::Never,
419 ColorChoice::CargoAuto => {
420 if atty::is(atty::Stream::Stderr) {
421 termcolor::ColorChoice::Auto
422 } else {
423 termcolor::ColorChoice::Never
424 }
425 }
426 }
427 }
428}
429
430#[cfg(unix)]
431mod imp {
432 use super::{Shell, TtyWidth};
433 use std::mem;
434
435 pub fn stderr_width() -> TtyWidth {
436 unsafe {
437 let mut winsize: libc::winsize = mem::zeroed();
438 if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
441 return TtyWidth::NoTty;
442 }
443 if winsize.ws_col > 0 {
444 TtyWidth::Known(winsize.ws_col as usize)
445 } else {
446 TtyWidth::NoTty
447 }
448 }
449 }
450
451 pub fn err_erase_line(shell: &mut Shell) {
452 let _ = shell.output.stderr().write_all(b"\x1B[K");
456 }
457}
458
459#[cfg(windows)]
460mod imp {
461 use std::{cmp, mem, ptr};
462 use winapi::um::fileapi::*;
463 use winapi::um::handleapi::*;
464 use winapi::um::processenv::*;
465 use winapi::um::winbase::*;
466 use winapi::um::wincon::*;
467 use winapi::um::winnt::*;
468
469 pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth};
470
471 pub fn stderr_width() -> TtyWidth {
472 unsafe {
473 let stdout = GetStdHandle(STD_ERROR_HANDLE);
474 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
475 if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
476 return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
477 }
478
479 let h = CreateFileA(
483 "CONOUT$\0".as_ptr() as *const CHAR,
484 GENERIC_READ | GENERIC_WRITE,
485 FILE_SHARE_READ | FILE_SHARE_WRITE,
486 ptr::null_mut(),
487 OPEN_EXISTING,
488 0,
489 ptr::null_mut(),
490 );
491 if h == INVALID_HANDLE_VALUE {
492 return TtyWidth::NoTty;
493 }
494
495 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
496 let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
497 CloseHandle(h);
498 if rc != 0 {
499 let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
500 return TtyWidth::Guess(cmp::min(60, width));
509 }
510
511 TtyWidth::NoTty
512 }
513 }
514}
515
516#[cfg(windows)]
517fn default_err_erase_line(shell: &mut Shell) {
518 match imp::stderr_width() {
519 TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
520 let blank = " ".repeat(max_width);
521 drop(write!(shell.output.stderr(), "{}\r", blank));
522 }
523 _ => (),
524 }
525}