1#![allow(dead_code)]
2
3use std::fmt;
4use std::io::prelude::*;
5
6use is_terminal::IsTerminal;
7use termcolor::Color::{Cyan, Green, Red, Yellow};
8use termcolor::{self, Color, ColorSpec, StandardStream, WriteColor};
9
10pub enum TtyWidth {
11 NoTty,
12 Known(usize),
13 Guess(usize),
14}
15
16impl TtyWidth {
17 pub fn progress_max_width(&self) -> Option<usize> {
19 match *self {
20 TtyWidth::NoTty => None,
21 TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum Verbosity {
29 VeryVerbose,
30 Verbose,
31 Normal,
32 Quiet,
33}
34
35impl From<u8> for Verbosity {
36 fn from(value: u8) -> Self {
37 match value {
38 0 => Verbosity::Quiet,
39 1 => Verbosity::Normal,
40 2 => Verbosity::Verbose,
41 _ => Verbosity::VeryVerbose,
42 }
43 }
44}
45
46pub struct Shell {
49 output: ShellOut,
52 verbosity: Verbosity,
54 needs_clear: bool,
57}
58
59impl fmt::Debug for Shell {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self.output {
62 ShellOut::Write(_) => f
63 .debug_struct("Shell")
64 .field("verbosity", &self.verbosity)
65 .finish(),
66 ShellOut::Stream { color_choice, .. } => f
67 .debug_struct("Shell")
68 .field("verbosity", &self.verbosity)
69 .field("color_choice", &color_choice)
70 .finish(),
71 }
72 }
73}
74
75enum ShellOut {
77 Write(Box<dyn Write>),
79 Stream {
81 stdout: StandardStream,
82 stderr: StandardStream,
83 stderr_tty: bool,
84 color_choice: ColorChoice,
85 },
86}
87
88#[derive(Debug, PartialEq, Clone, Copy)]
90pub enum ColorChoice {
91 Always,
93 Never,
95 CargoAuto,
97}
98
99impl Shell {
100 pub fn new() -> Shell {
103 let auto_clr = ColorChoice::CargoAuto;
104 Shell {
105 output: ShellOut::Stream {
106 stdout: StandardStream::stdout(auto_clr.to_termcolor_color_choice(Stream::Stdout)),
107 stderr: StandardStream::stderr(auto_clr.to_termcolor_color_choice(Stream::Stderr)),
108 color_choice: ColorChoice::CargoAuto,
109 stderr_tty: std::io::stderr().is_terminal(),
110 },
111 verbosity: Verbosity::Verbose,
112 needs_clear: false,
113 }
114 }
115
116 pub fn from_write(out: Box<dyn Write>) -> Shell {
118 Shell {
119 output: ShellOut::Write(out),
120 verbosity: Verbosity::Verbose,
121 needs_clear: false,
122 }
123 }
124
125 fn print(
128 &mut self,
129 status: &dyn fmt::Display,
130 message: Option<&dyn fmt::Display>,
131 color: Color,
132 justified: bool,
133 ) -> anyhow::Result<()> {
134 match self.verbosity {
135 Verbosity::Quiet => Ok(()),
136 _ => {
137 if self.needs_clear {
138 self.err_erase_line();
139 }
140 self.output
141 .message_stderr(status, message, color, justified)
142 }
143 }
144 }
145
146 pub fn set_needs_clear(&mut self, needs_clear: bool) {
148 self.needs_clear = needs_clear;
149 }
150
151 pub fn is_cleared(&self) -> bool {
153 !self.needs_clear
154 }
155
156 pub fn err_width(&self) -> TtyWidth {
158 match self.output {
159 ShellOut::Stream {
160 stderr_tty: true, ..
161 } => imp::stderr_width(),
162 _ => TtyWidth::NoTty,
163 }
164 }
165
166 pub fn is_err_tty(&self) -> bool {
168 match self.output {
169 ShellOut::Stream { stderr_tty, .. } => stderr_tty,
170 _ => false,
171 }
172 }
173
174 pub fn out(&mut self) -> &mut dyn Write {
176 if self.needs_clear {
177 self.err_erase_line();
178 }
179 self.output.stdout()
180 }
181
182 pub fn err(&mut self) -> &mut dyn Write {
184 if self.needs_clear {
185 self.err_erase_line();
186 }
187 self.output.stderr()
188 }
189
190 pub fn reset_err(&mut self) -> anyhow::Result<()> {
191 match self.output {
192 ShellOut::Stream { ref mut stderr, .. } => {
193 stderr.reset()?;
194 }
195 ShellOut::Write(_) => {
196 }
198 }
199 Ok(())
200 }
201
202 pub fn err_erase_line(&mut self) {
204 if self.err_supports_color() {
205 imp::err_erase_line(self);
206 self.needs_clear = false;
207 }
208 }
209
210 pub fn status<T, U>(&mut self, status: T, message: U) -> anyhow::Result<()>
212 where
213 T: fmt::Display,
214 U: fmt::Display,
215 {
216 self.print(&status, Some(&message), Green, true)
217 }
218
219 pub fn status_header<T>(&mut self, status: T) -> anyhow::Result<()>
220 where
221 T: fmt::Display,
222 {
223 self.print(&status, None, Cyan, true)
224 }
225
226 pub fn status_with_color<T, U>(
228 &mut self,
229 status: T,
230 message: U,
231 color: Color,
232 ) -> anyhow::Result<()>
233 where
234 T: fmt::Display,
235 U: fmt::Display,
236 {
237 self.print(&status, Some(&message), color, true)
238 }
239
240 pub fn very_verbose<F>(&mut self, mut callback: F) -> anyhow::Result<()>
242 where
243 F: FnMut(&mut Shell) -> anyhow::Result<()>,
244 {
245 match self.verbosity {
246 Verbosity::VeryVerbose => callback(self),
247 _ => Ok(()),
248 }
249 }
250
251 pub fn verbose<F>(&mut self, mut callback: F) -> anyhow::Result<()>
253 where
254 F: FnMut(&mut Shell) -> anyhow::Result<()>,
255 {
256 match self.verbosity {
257 Verbosity::Verbose | Verbosity::VeryVerbose => callback(self),
258 _ => Ok(()),
259 }
260 }
261
262 pub fn concise<F>(&mut self, mut callback: F) -> anyhow::Result<()>
264 where
265 F: FnMut(&mut Shell) -> anyhow::Result<()>,
266 {
267 match self.verbosity {
268 Verbosity::Verbose | Verbosity::VeryVerbose => Ok(()),
269 _ => callback(self),
270 }
271 }
272
273 pub fn error<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
275 if self.needs_clear {
276 self.err_erase_line();
277 }
278 self.output
279 .message_stderr(&"error", Some(&message), Red, false)
280 }
281
282 pub fn warn<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
284 match self.verbosity {
285 Verbosity::Quiet => Ok(()),
286 _ => self.print(&"warning", Some(&message), Yellow, false),
287 }
288 }
289
290 pub fn note<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
292 self.print(&"note", Some(&message), Cyan, false)
293 }
294
295 pub fn set_verbosity(&mut self, verbosity: Verbosity) {
297 self.verbosity = verbosity;
298 }
299
300 pub fn verbosity(&self) -> Verbosity {
302 self.verbosity
303 }
304
305 pub fn set_color_choice(&mut self, color: Option<&str>) -> anyhow::Result<()> {
307 if let ShellOut::Stream {
308 ref mut stdout,
309 ref mut stderr,
310 ref mut color_choice,
311 ..
312 } = self.output
313 {
314 let cfg = match color {
315 Some("always") => ColorChoice::Always,
316 Some("never") => ColorChoice::Never,
317
318 Some("auto") | None => ColorChoice::CargoAuto,
319
320 Some(arg) => anyhow::bail!(
321 "argument for --color must be auto, always, or \
322 never, but found `{}`",
323 arg
324 ),
325 };
326 *color_choice = cfg;
327 *stdout = StandardStream::stdout(cfg.to_termcolor_color_choice(Stream::Stdout));
328 *stderr = StandardStream::stderr(cfg.to_termcolor_color_choice(Stream::Stderr));
329 }
330 Ok(())
331 }
332
333 pub fn color_choice(&self) -> ColorChoice {
338 match self.output {
339 ShellOut::Stream { color_choice, .. } => color_choice,
340 ShellOut::Write(_) => ColorChoice::Never,
341 }
342 }
343
344 pub fn err_supports_color(&self) -> bool {
346 match &self.output {
347 ShellOut::Write(_) => false,
348 ShellOut::Stream { stderr, .. } => stderr.supports_color(),
349 }
350 }
351
352 pub fn out_supports_color(&self) -> bool {
353 match &self.output {
354 ShellOut::Write(_) => false,
355 ShellOut::Stream { stdout, .. } => stdout.supports_color(),
356 }
357 }
358
359 pub fn write_stdout(
363 &mut self,
364 fragment: impl fmt::Display,
365 color: &ColorSpec,
366 ) -> anyhow::Result<()> {
367 self.output.write_stdout(fragment, color)
368 }
369
370 pub fn write_stderr(
374 &mut self,
375 fragment: impl fmt::Display,
376 color: &ColorSpec,
377 ) -> anyhow::Result<()> {
378 self.output.write_stderr(fragment, color)
379 }
380
381 pub fn print_ansi_stderr(&mut self, message: &[u8]) -> anyhow::Result<()> {
383 if self.needs_clear {
384 self.err_erase_line();
385 }
386 #[cfg(windows)]
387 {
388 if let ShellOut::Stream { stderr, .. } = &mut self.output {
389 ::fwdansi::write_ansi(stderr, message)?;
390 return Ok(());
391 }
392 }
393 self.err().write_all(message)?;
394 Ok(())
395 }
396
397 pub fn print_ansi_stdout(&mut self, message: &[u8]) -> anyhow::Result<()> {
399 if self.needs_clear {
400 self.err_erase_line();
401 }
402 #[cfg(windows)]
403 {
404 if let ShellOut::Stream { stdout, .. } = &mut self.output {
405 ::fwdansi::write_ansi(stdout, message)?;
406 return Ok(());
407 }
408 }
409 self.out().write_all(message)?;
410 Ok(())
411 }
412}
413
414impl Default for Shell {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420impl ShellOut {
421 fn message_stderr(
425 &mut self,
426 status: &dyn fmt::Display,
427 message: Option<&dyn fmt::Display>,
428 color: Color,
429 justified: bool,
430 ) -> anyhow::Result<()> {
431 match *self {
432 ShellOut::Stream { ref mut stderr, .. } => {
433 stderr.reset()?;
434 stderr.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
435 if justified {
436 write!(stderr, "{status:>12}")?;
437 } else {
438 write!(stderr, "{status}")?;
439 stderr.set_color(ColorSpec::new().set_bold(true))?;
440 write!(stderr, ":")?;
441 }
442 stderr.reset()?;
443 match message {
444 Some(message) => {
445 write!(stderr, " {message}")?;
446 stderr.set_color(
447 ColorSpec::new()
448 .set_intense(true)
449 .set_fg(Some(Color::Black)),
450 )?;
451 writeln!(stderr)?;
452 }
453 None => {
454 stderr.set_color(
455 ColorSpec::new()
456 .set_intense(true)
457 .set_fg(Some(Color::Black)),
458 )?;
459 write!(stderr, " ")?;
460 }
461 }
462 }
463 ShellOut::Write(ref mut w) => {
464 if justified {
465 write!(w, "{status:>12}")?;
466 } else {
467 write!(w, "{status}:")?;
468 }
469 match message {
470 Some(message) => writeln!(w, " {message}")?,
471 None => write!(w, " ")?,
472 }
473 writeln!(w)?;
474 }
475 }
476 Ok(())
477 }
478
479 fn write_stdout(
481 &mut self,
482 fragment: impl fmt::Display,
483 color: &ColorSpec,
484 ) -> anyhow::Result<()> {
485 match *self {
486 ShellOut::Stream { ref mut stdout, .. } => {
487 stdout.reset()?;
488 stdout.set_color(color)?;
489 write!(stdout, "{fragment}")?;
490 stdout.reset()?;
491 }
492 ShellOut::Write(ref mut w) => {
493 write!(w, "{fragment}")?;
494 }
495 }
496 Ok(())
497 }
498
499 fn write_stderr(
501 &mut self,
502 fragment: impl fmt::Display,
503 color: &ColorSpec,
504 ) -> anyhow::Result<()> {
505 match *self {
506 ShellOut::Stream { ref mut stderr, .. } => {
507 stderr.reset()?;
508 stderr.set_color(color)?;
509 write!(stderr, "{fragment}")?;
510 stderr.reset()?;
511 }
512 ShellOut::Write(ref mut w) => {
513 write!(w, "{fragment}")?;
514 }
515 }
516 Ok(())
517 }
518
519 fn stdout(&mut self) -> &mut dyn Write {
521 match *self {
522 ShellOut::Stream { ref mut stdout, .. } => stdout,
523 ShellOut::Write(ref mut w) => w,
524 }
525 }
526
527 fn stderr(&mut self) -> &mut dyn Write {
529 match *self {
530 ShellOut::Stream { ref mut stderr, .. } => stderr,
531 ShellOut::Write(ref mut w) => w,
532 }
533 }
534}
535
536impl ColorChoice {
537 fn to_termcolor_color_choice(self, stream: Stream) -> termcolor::ColorChoice {
539 match self {
540 ColorChoice::Always => termcolor::ColorChoice::Always,
541 ColorChoice::Never => termcolor::ColorChoice::Never,
542 ColorChoice::CargoAuto => {
543 if stream.is_terminal() {
544 termcolor::ColorChoice::Auto
545 } else {
546 termcolor::ColorChoice::Never
547 }
548 }
549 }
550 }
551}
552
553enum Stream {
554 Stdout,
555 Stderr,
556}
557
558impl Stream {
559 fn is_terminal(&self) -> bool {
560 match self {
561 Self::Stdout => std::io::stdout().is_terminal(),
562 Self::Stderr => std::io::stderr().is_terminal(),
563 }
564 }
565}
566
567#[cfg(unix)]
568mod imp {
569 use super::{Shell, TtyWidth};
570 use std::mem;
571
572 pub fn stderr_width() -> TtyWidth {
573 unsafe {
574 let mut winsize: libc::winsize = mem::zeroed();
575 if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
578 return TtyWidth::NoTty;
579 }
580 if winsize.ws_col > 0 {
581 TtyWidth::Known(winsize.ws_col as usize)
582 } else {
583 TtyWidth::NoTty
584 }
585 }
586 }
587
588 pub fn err_erase_line(shell: &mut Shell) {
589 let _ = shell.output.stderr().write_all(b"\x1B[K");
593 }
594}
595
596#[cfg(windows)]
597mod imp {
598 use std::{cmp, mem, ptr};
599
600 use windows_sys::Win32::Foundation::CloseHandle;
601 use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
602 use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
603 use windows_sys::Win32::Storage::FileSystem::{
604 CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
605 };
606 use windows_sys::Win32::System::Console::{
607 CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle, STD_ERROR_HANDLE,
608 };
609 use windows_sys::core::PCSTR;
610
611 pub(super) use super::{TtyWidth, default_err_erase_line as err_erase_line};
612
613 pub fn stderr_width() -> TtyWidth {
614 unsafe {
615 let stdout = GetStdHandle(STD_ERROR_HANDLE);
616 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
617 if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
618 return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
619 }
620
621 let h = CreateFileA(
625 c"CONOUT$".as_ptr() as PCSTR,
626 GENERIC_READ | GENERIC_WRITE,
627 FILE_SHARE_READ | FILE_SHARE_WRITE,
628 ptr::null_mut(),
629 OPEN_EXISTING,
630 0,
631 std::ptr::null_mut(),
632 );
633 if h == INVALID_HANDLE_VALUE {
634 return TtyWidth::NoTty;
635 }
636
637 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
638 let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
639 CloseHandle(h);
640 if rc != 0 {
641 let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
642 return TtyWidth::Guess(cmp::min(60, width));
651 }
652
653 TtyWidth::NoTty
654 }
655 }
656}
657
658#[cfg(windows)]
659fn default_err_erase_line(shell: &mut Shell) {
660 match imp::stderr_width() {
661 TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
662 let blank = " ".repeat(max_width);
663 drop(write!(shell.output.stderr(), "{blank}\r"));
664 }
665 _ => (),
666 }
667}