1use std::fmt;
2use std::io::IsTerminal;
3use std::io::prelude::*;
4
5use annotate_snippets::{Renderer, Report};
6use anstream::AutoStream;
7use anstyle::Style;
8
9use crate::util::errors::CargoResult;
10use crate::util::hostname;
11use crate::util::style::*;
12
13pub struct Shell {
16 output: ShellOut,
19 verbosity: Verbosity,
21 needs_clear: bool,
24 hostname: Option<String>,
25}
26
27impl fmt::Debug for Shell {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self.output {
30 ShellOut::Write(_) => f
31 .debug_struct("Shell")
32 .field("verbosity", &self.verbosity)
33 .finish(),
34 ShellOut::Stream { color_choice, .. } => f
35 .debug_struct("Shell")
36 .field("verbosity", &self.verbosity)
37 .field("color_choice", &color_choice)
38 .finish(),
39 }
40 }
41}
42
43impl Shell {
44 pub fn new() -> Shell {
47 let auto_clr = ColorChoice::CargoAuto;
48 let stdout_choice = auto_clr.to_anstream_color_choice();
49 let stderr_choice = auto_clr.to_anstream_color_choice();
50 Shell {
51 output: ShellOut::Stream {
52 stdout: AutoStream::new(std::io::stdout(), stdout_choice),
53 stderr: AutoStream::new(std::io::stderr(), stderr_choice),
54 color_choice: auto_clr,
55 hyperlinks: supports_hyperlinks(),
56 stderr_tty: std::io::stderr().is_terminal(),
57 stdout_unicode: supports_unicode(&std::io::stdout()),
58 stderr_unicode: supports_unicode(&std::io::stderr()),
59 stderr_term_integration: supports_term_integration(&std::io::stderr()),
60 },
61 verbosity: Verbosity::Verbose,
62 needs_clear: false,
63 hostname: None,
64 }
65 }
66
67 pub fn from_write(out: Box<dyn Write>) -> Shell {
69 Shell {
70 output: ShellOut::Write(AutoStream::never(out)), verbosity: Verbosity::Verbose,
72 needs_clear: false,
73 hostname: None,
74 }
75 }
76
77 fn print(
80 &mut self,
81 status: &dyn fmt::Display,
82 message: Option<&dyn fmt::Display>,
83 color: &Style,
84 justified: bool,
85 ) -> CargoResult<()> {
86 match self.verbosity {
87 Verbosity::Quiet => Ok(()),
88 _ => {
89 if self.needs_clear {
90 self.err_erase_line();
91 }
92 self.output
93 .message_stderr(status, message, color, justified)
94 }
95 }
96 }
97
98 pub fn set_needs_clear(&mut self, needs_clear: bool) {
100 self.needs_clear = needs_clear;
101 }
102
103 pub fn is_cleared(&self) -> bool {
105 !self.needs_clear
106 }
107
108 pub fn err_width(&self) -> TtyWidth {
110 match self.output {
111 ShellOut::Stream {
112 stderr_tty: true, ..
113 } => imp::stderr_width(),
114 _ => TtyWidth::NoTty,
115 }
116 }
117
118 pub fn is_err_tty(&self) -> bool {
120 match self.output {
121 ShellOut::Stream { stderr_tty, .. } => stderr_tty,
122 _ => false,
123 }
124 }
125
126 pub fn is_err_term_integration_available(&self) -> bool {
127 if let ShellOut::Stream {
128 stderr_term_integration,
129 ..
130 } = self.output
131 {
132 stderr_term_integration
133 } else {
134 false
135 }
136 }
137
138 pub fn out(&mut self) -> &mut dyn Write {
140 if self.needs_clear {
141 self.err_erase_line();
142 }
143 self.output.stdout()
144 }
145
146 pub fn err(&mut self) -> &mut dyn Write {
148 if self.needs_clear {
149 self.err_erase_line();
150 }
151 self.output.stderr()
152 }
153
154 pub fn err_erase_line(&mut self) {
156 if self.err_supports_color() {
157 imp::err_erase_line(self);
158 self.needs_clear = false;
159 }
160 }
161
162 pub fn status<T, U>(&mut self, status: T, message: U) -> CargoResult<()>
164 where
165 T: fmt::Display,
166 U: fmt::Display,
167 {
168 self.print(&status, Some(&message), &HEADER, true)
169 }
170
171 pub fn status_header<T>(&mut self, status: T) -> CargoResult<()>
172 where
173 T: fmt::Display,
174 {
175 self.print(&status, None, &NOTE, true)
176 }
177
178 pub fn status_with_color<T, U>(
180 &mut self,
181 status: T,
182 message: U,
183 color: &Style,
184 ) -> CargoResult<()>
185 where
186 T: fmt::Display,
187 U: fmt::Display,
188 {
189 self.print(&status, Some(&message), color, true)
190 }
191
192 pub fn verbose<F>(&mut self, mut callback: F) -> CargoResult<()>
194 where
195 F: FnMut(&mut Shell) -> CargoResult<()>,
196 {
197 match self.verbosity {
198 Verbosity::Verbose => callback(self),
199 _ => Ok(()),
200 }
201 }
202
203 pub fn concise<F>(&mut self, mut callback: F) -> CargoResult<()>
205 where
206 F: FnMut(&mut Shell) -> CargoResult<()>,
207 {
208 match self.verbosity {
209 Verbosity::Verbose => Ok(()),
210 _ => callback(self),
211 }
212 }
213
214 pub fn error<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
216 if self.needs_clear {
217 self.err_erase_line();
218 }
219 self.output
220 .message_stderr(&"error", Some(&message), &ERROR, false)
221 }
222
223 pub fn warn<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
225 self.print(&"warning", Some(&message), &WARN, false)
226 }
227
228 pub fn note<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
230 self.print(&"note", Some(&message), &NOTE, false)
231 }
232
233 pub fn set_verbosity(&mut self, verbosity: Verbosity) {
235 self.verbosity = verbosity;
236 }
237
238 pub fn verbosity(&self) -> Verbosity {
240 self.verbosity
241 }
242
243 pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> {
245 if let ShellOut::Stream {
246 stdout,
247 stderr,
248 color_choice,
249 ..
250 } = &mut self.output
251 {
252 let cfg = color
253 .map(|c| c.parse())
254 .transpose()?
255 .unwrap_or(ColorChoice::CargoAuto);
256 *color_choice = cfg;
257 let stdout_choice = cfg.to_anstream_color_choice();
258 let stderr_choice = cfg.to_anstream_color_choice();
259 *stdout = AutoStream::new(std::io::stdout(), stdout_choice);
260 *stderr = AutoStream::new(std::io::stderr(), stderr_choice);
261 }
262 Ok(())
263 }
264
265 pub fn set_unicode(&mut self, yes: bool) -> CargoResult<()> {
266 if let ShellOut::Stream {
267 stdout_unicode,
268 stderr_unicode,
269 ..
270 } = &mut self.output
271 {
272 *stdout_unicode = yes;
273 *stderr_unicode = yes;
274 }
275 Ok(())
276 }
277
278 pub fn set_hyperlinks(&mut self, yes: bool) -> CargoResult<()> {
279 if let ShellOut::Stream { hyperlinks, .. } = &mut self.output {
280 *hyperlinks = yes;
281 }
282 Ok(())
283 }
284
285 pub fn out_unicode(&self) -> bool {
286 match &self.output {
287 ShellOut::Write(_) => true,
288 ShellOut::Stream { stdout_unicode, .. } => *stdout_unicode,
289 }
290 }
291
292 pub fn err_unicode(&self) -> bool {
293 match &self.output {
294 ShellOut::Write(_) => true,
295 ShellOut::Stream { stderr_unicode, .. } => *stderr_unicode,
296 }
297 }
298
299 pub fn color_choice(&self) -> ColorChoice {
304 match self.output {
305 ShellOut::Stream { color_choice, .. } => color_choice,
306 ShellOut::Write(_) => ColorChoice::Never,
307 }
308 }
309
310 pub fn err_supports_color(&self) -> bool {
312 match &self.output {
313 ShellOut::Write(_) => false,
314 ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
315 }
316 }
317
318 pub fn out_supports_color(&self) -> bool {
319 match &self.output {
320 ShellOut::Write(_) => false,
321 ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
322 }
323 }
324
325 pub fn out_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
326 let supports_hyperlinks = match &self.output {
327 ShellOut::Write(_) => false,
328 ShellOut::Stream {
329 stdout, hyperlinks, ..
330 } => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
331 };
332 Hyperlink {
333 url: supports_hyperlinks.then_some(url),
334 }
335 }
336
337 pub fn err_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
338 let supports_hyperlinks = match &self.output {
339 ShellOut::Write(_) => false,
340 ShellOut::Stream {
341 stderr, hyperlinks, ..
342 } => stderr.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
343 };
344 if supports_hyperlinks {
345 Hyperlink { url: Some(url) }
346 } else {
347 Hyperlink { url: None }
348 }
349 }
350
351 pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
352 let url = self.file_hyperlink(path);
353 url.map(|u| self.out_hyperlink(u)).unwrap_or_default()
354 }
355
356 pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
357 let url = self.file_hyperlink(path);
358 url.map(|u| self.err_hyperlink(u)).unwrap_or_default()
359 }
360
361 fn file_hyperlink(&mut self, path: &std::path::Path) -> Option<url::Url> {
362 let mut url = url::Url::from_file_path(path).ok()?;
363 let hostname = if cfg!(windows) {
366 None
368 } else {
369 if let Some(hostname) = self.hostname.as_deref() {
370 Some(hostname)
371 } else {
372 self.hostname = hostname().ok().and_then(|h| h.into_string().ok());
373 self.hostname.as_deref()
374 }
375 };
376 let _ = url.set_host(hostname);
377 Some(url)
378 }
379
380 pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> {
382 if self.needs_clear {
383 self.err_erase_line();
384 }
385 self.err().write_all(message)?;
386 Ok(())
387 }
388
389 pub fn print_ansi_stdout(&mut self, message: &[u8]) -> CargoResult<()> {
391 if self.needs_clear {
392 self.err_erase_line();
393 }
394 self.out().write_all(message)?;
395 Ok(())
396 }
397
398 pub fn print_json<T: serde::ser::Serialize>(&mut self, obj: &T) -> CargoResult<()> {
399 let encoded = serde_json::to_string(obj)?;
401 drop(writeln!(self.out(), "{}", encoded));
403 Ok(())
404 }
405
406 pub fn print_report(&mut self, report: Report<'_>, force: bool) -> CargoResult<()> {
408 if !force && matches!(self.verbosity, Verbosity::Quiet) {
409 return Ok(());
410 }
411
412 if self.needs_clear {
413 self.err_erase_line();
414 }
415 let term_width = self
416 .err_width()
417 .diagnostic_terminal_width()
418 .unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH);
419 let rendered = Renderer::styled().term_width(term_width).render(report);
420 self.err().write_all(rendered.as_bytes())?;
421 self.err().write_all("\n".as_bytes())?;
422 Ok(())
423 }
424}
425
426impl Default for Shell {
427 fn default() -> Self {
428 Self::new()
429 }
430}
431
432enum ShellOut {
434 Write(AutoStream<Box<dyn Write>>),
436 Stream {
438 stdout: AutoStream<std::io::Stdout>,
439 stderr: AutoStream<std::io::Stderr>,
440 stderr_tty: bool,
441 color_choice: ColorChoice,
442 hyperlinks: bool,
443 stdout_unicode: bool,
444 stderr_unicode: bool,
445 stderr_term_integration: bool,
446 },
447}
448
449impl ShellOut {
450 fn message_stderr(
454 &mut self,
455 status: &dyn fmt::Display,
456 message: Option<&dyn fmt::Display>,
457 style: &Style,
458 justified: bool,
459 ) -> CargoResult<()> {
460 let bold = anstyle::Style::new() | anstyle::Effects::BOLD;
461
462 let mut buffer = Vec::new();
463 if justified {
464 write!(&mut buffer, "{style}{status:>12}{style:#}")?;
465 } else {
466 write!(&mut buffer, "{style}{status}{style:#}{bold}:{bold:#}")?;
467 }
468 match message {
469 Some(message) => writeln!(buffer, " {message}")?,
470 None => write!(buffer, " ")?,
471 }
472 self.stderr().write_all(&buffer)?;
473 Ok(())
474 }
475
476 fn stdout(&mut self) -> &mut dyn Write {
478 match self {
479 ShellOut::Stream { stdout, .. } => stdout,
480 ShellOut::Write(w) => w,
481 }
482 }
483
484 fn stderr(&mut self) -> &mut dyn Write {
486 match self {
487 ShellOut::Stream { stderr, .. } => stderr,
488 ShellOut::Write(w) => w,
489 }
490 }
491}
492
493pub enum TtyWidth {
494 NoTty,
495 Known(usize),
496 Guess(usize),
497}
498
499impl TtyWidth {
500 pub fn diagnostic_terminal_width(&self) -> Option<usize> {
503 #[allow(clippy::disallowed_methods)]
505 if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") {
506 return Some(width.parse().unwrap());
507 }
508 match *self {
509 TtyWidth::NoTty | TtyWidth::Guess(_) => None,
510 TtyWidth::Known(width) => Some(width),
511 }
512 }
513
514 pub fn progress_max_width(&self) -> Option<usize> {
516 match *self {
517 TtyWidth::NoTty => None,
518 TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
519 }
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq)]
525pub enum Verbosity {
526 Verbose,
527 Normal,
528 Quiet,
529}
530
531#[derive(Debug, PartialEq, Clone, Copy)]
533pub enum ColorChoice {
534 Always,
536 Never,
538 CargoAuto,
540}
541
542impl ColorChoice {
543 fn to_anstream_color_choice(self) -> anstream::ColorChoice {
545 match self {
546 ColorChoice::Always => anstream::ColorChoice::Always,
547 ColorChoice::Never => anstream::ColorChoice::Never,
548 ColorChoice::CargoAuto => anstream::ColorChoice::Auto,
549 }
550 }
551}
552
553impl std::str::FromStr for ColorChoice {
554 type Err = anyhow::Error;
555 fn from_str(color: &str) -> Result<Self, Self::Err> {
556 let cfg = match color {
557 "always" => ColorChoice::Always,
558 "never" => ColorChoice::Never,
559
560 "auto" => ColorChoice::CargoAuto,
561
562 arg => anyhow::bail!(
563 "argument for --color must be auto, always, or \
564 never, but found `{}`",
565 arg
566 ),
567 };
568 Ok(cfg)
569 }
570}
571
572fn supports_color(choice: anstream::ColorChoice) -> bool {
573 match choice {
574 anstream::ColorChoice::Always
575 | anstream::ColorChoice::AlwaysAnsi
576 | anstream::ColorChoice::Auto => true,
577 anstream::ColorChoice::Never => false,
578 }
579}
580
581fn supports_unicode(stream: &dyn IsTerminal) -> bool {
582 !stream.is_terminal() || supports_unicode::supports_unicode()
583}
584
585fn supports_hyperlinks() -> bool {
586 #[allow(clippy::disallowed_methods)] if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
588 return false;
590 }
591
592 supports_hyperlinks::supports_hyperlinks()
593}
594
595#[allow(clippy::disallowed_methods)] fn supports_term_integration(stream: &dyn IsTerminal) -> bool {
598 let windows_terminal = std::env::var("WT_SESSION").is_ok();
599 let conemu = std::env::var("ConEmuANSI").ok() == Some("ON".into());
600 let wezterm = std::env::var("TERM_PROGRAM").ok() == Some("WezTerm".into());
601
602 (windows_terminal || conemu || wezterm) && stream.is_terminal()
603}
604
605pub struct Hyperlink<D: fmt::Display> {
606 url: Option<D>,
607}
608
609impl<D: fmt::Display> Default for Hyperlink<D> {
610 fn default() -> Self {
611 Self { url: None }
612 }
613}
614
615impl<D: fmt::Display> fmt::Display for Hyperlink<D> {
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 let Some(url) = self.url.as_ref() else {
618 return Ok(());
619 };
620 if f.alternate() {
621 write!(f, "\x1B]8;;\x1B\\")
622 } else {
623 write!(f, "\x1B]8;;{url}\x1B\\")
624 }
625 }
626}
627
628#[cfg(unix)]
629mod imp {
630 use super::{Shell, TtyWidth};
631 use std::mem;
632
633 pub fn stderr_width() -> TtyWidth {
634 unsafe {
635 let mut winsize: libc::winsize = mem::zeroed();
636 if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
639 return TtyWidth::NoTty;
640 }
641 if winsize.ws_col > 0 {
642 TtyWidth::Known(winsize.ws_col as usize)
643 } else {
644 TtyWidth::NoTty
645 }
646 }
647 }
648
649 pub fn err_erase_line(shell: &mut Shell) {
650 let _ = shell.output.stderr().write_all(b"\x1B[K");
654 }
655}
656
657#[cfg(windows)]
658mod imp {
659 use std::{cmp, mem, ptr};
660
661 use windows_sys::Win32::Foundation::CloseHandle;
662 use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
663 use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
664 use windows_sys::Win32::Storage::FileSystem::{
665 CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
666 };
667 use windows_sys::Win32::System::Console::{
668 CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle, STD_ERROR_HANDLE,
669 };
670 use windows_sys::core::PCSTR;
671
672 pub(super) use super::{TtyWidth, default_err_erase_line as err_erase_line};
673
674 pub fn stderr_width() -> TtyWidth {
675 unsafe {
676 let stdout = GetStdHandle(STD_ERROR_HANDLE);
677 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
678 if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
679 return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
680 }
681
682 let h = CreateFileA(
686 "CONOUT$\0".as_ptr() as PCSTR,
687 GENERIC_READ | GENERIC_WRITE,
688 FILE_SHARE_READ | FILE_SHARE_WRITE,
689 ptr::null_mut(),
690 OPEN_EXISTING,
691 0,
692 std::ptr::null_mut(),
693 );
694 if h == INVALID_HANDLE_VALUE {
695 return TtyWidth::NoTty;
696 }
697
698 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
699 let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
700 CloseHandle(h);
701 if rc != 0 {
702 let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
703 return TtyWidth::Guess(cmp::min(60, width));
712 }
713
714 TtyWidth::NoTty
715 }
716 }
717}
718
719#[cfg(windows)]
720fn default_err_erase_line(shell: &mut Shell) {
721 match imp::stderr_width() {
722 TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
723 let blank = " ".repeat(max_width);
724 drop(write!(shell.output.stderr(), "{}\r", blank));
725 }
726 _ => (),
727 }
728}