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