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 #[expect(
537 clippy::disallowed_methods,
538 reason = "testing only, no reason for config support"
539 )]
540 if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") {
541 return Some(width.parse().unwrap());
542 }
543 match *self {
544 TtyWidth::NoTty | TtyWidth::Guess(_) => None,
545 TtyWidth::Known(width) => Some(width),
546 }
547 }
548
549 pub fn progress_max_width(&self) -> Option<usize> {
551 match *self {
552 TtyWidth::NoTty => None,
553 TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
554 }
555 }
556}
557
558#[derive(Debug, Clone, Copy, PartialEq)]
560pub enum Verbosity {
561 Verbose,
562 Normal,
563 Quiet,
564}
565
566#[derive(Debug, PartialEq, Clone, Copy)]
568pub enum ColorChoice {
569 Always,
571 Never,
573 CargoAuto,
575}
576
577impl ColorChoice {
578 fn to_anstream_color_choice(self) -> anstream::ColorChoice {
580 match self {
581 ColorChoice::Always => anstream::ColorChoice::Always,
582 ColorChoice::Never => anstream::ColorChoice::Never,
583 ColorChoice::CargoAuto => anstream::ColorChoice::Auto,
584 }
585 }
586}
587
588impl std::str::FromStr for ColorChoice {
589 type Err = anyhow::Error;
590 fn from_str(color: &str) -> Result<Self, Self::Err> {
591 let cfg = match color {
592 "always" => ColorChoice::Always,
593 "never" => ColorChoice::Never,
594
595 "auto" => ColorChoice::CargoAuto,
596
597 arg => anyhow::bail!(
598 "argument for --color must be auto, always, or \
599 never, but found `{}`",
600 arg
601 ),
602 };
603 Ok(cfg)
604 }
605}
606
607fn supports_color(choice: anstream::ColorChoice) -> bool {
608 match choice {
609 anstream::ColorChoice::Always
610 | anstream::ColorChoice::AlwaysAnsi
611 | anstream::ColorChoice::Auto => true,
612 anstream::ColorChoice::Never => false,
613 }
614}
615
616fn supports_unicode(stream: &dyn IsTerminal) -> bool {
617 !stream.is_terminal() || supports_unicode::supports_unicode()
618}
619
620fn supports_hyperlinks() -> bool {
621 #[expect(
622 clippy::disallowed_methods,
623 reason = "reading the state of the system, not config"
624 )]
625 if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
626 return false;
628 }
629
630 supports_hyperlinks::supports_hyperlinks()
631}
632
633#[expect(
635 clippy::disallowed_methods,
636 reason = "reading the state of the system, not config"
637)]
638fn supports_term_integration(stream: &dyn IsTerminal) -> bool {
639 let windows_terminal = std::env::var("WT_SESSION").is_ok();
640 let conemu = std::env::var("ConEmuANSI").ok() == Some("ON".into());
641 let wezterm = std::env::var("TERM_PROGRAM").ok() == Some("WezTerm".into());
642 let ghostty = std::env::var("TERM_PROGRAM").ok() == Some("ghostty".into());
643
644 (windows_terminal || conemu || wezterm || ghostty) && stream.is_terminal()
645}
646
647pub struct Hyperlink<D: fmt::Display> {
648 url: Option<D>,
649}
650
651impl<D: fmt::Display> Default for Hyperlink<D> {
652 fn default() -> Self {
653 Self { url: None }
654 }
655}
656
657impl<D: fmt::Display> fmt::Display for Hyperlink<D> {
658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659 let Some(url) = self.url.as_ref() else {
660 return Ok(());
661 };
662 if f.alternate() {
663 write!(f, "\x1B]8;;\x1B\\")
664 } else {
665 write!(f, "\x1B]8;;{url}\x1B\\")
666 }
667 }
668}
669
670#[cfg(unix)]
671mod imp {
672 use super::{Shell, TtyWidth};
673 use std::mem;
674
675 pub fn stderr_width() -> TtyWidth {
676 unsafe {
677 let mut winsize: libc::winsize = mem::zeroed();
678 if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
681 return TtyWidth::NoTty;
682 }
683 if winsize.ws_col > 0 {
684 TtyWidth::Known(winsize.ws_col as usize)
685 } else {
686 TtyWidth::NoTty
687 }
688 }
689 }
690
691 pub fn err_erase_line(shell: &mut Shell) {
692 let _ = shell.output.stderr().write_all(b"\x1B[K");
696 }
697}
698
699#[cfg(windows)]
700mod imp {
701 use std::{cmp, mem, ptr};
702
703 use windows_sys::Win32::Foundation::CloseHandle;
704 use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
705 use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
706 use windows_sys::Win32::Storage::FileSystem::{
707 CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
708 };
709 use windows_sys::Win32::System::Console::{
710 CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle, STD_ERROR_HANDLE,
711 };
712 use windows_sys::core::PCSTR;
713
714 pub(super) use super::{TtyWidth, default_err_erase_line as err_erase_line};
715
716 pub fn stderr_width() -> TtyWidth {
717 unsafe {
718 let stdout = GetStdHandle(STD_ERROR_HANDLE);
719 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
720 if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
721 return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
722 }
723
724 let h = CreateFileA(
728 "CONOUT$\0".as_ptr() as PCSTR,
729 GENERIC_READ | GENERIC_WRITE,
730 FILE_SHARE_READ | FILE_SHARE_WRITE,
731 ptr::null_mut(),
732 OPEN_EXISTING,
733 0,
734 std::ptr::null_mut(),
735 );
736 if h == INVALID_HANDLE_VALUE {
737 return TtyWidth::NoTty;
738 }
739
740 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
741 let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
742 CloseHandle(h);
743 if rc != 0 {
744 let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
745 return TtyWidth::Guess(cmp::min(60, width));
754 }
755
756 TtyWidth::NoTty
757 }
758 }
759}
760
761#[cfg(windows)]
762fn default_err_erase_line(shell: &mut Shell) {
763 match imp::stderr_width() {
764 TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
765 let blank = " ".repeat(max_width);
766 drop(write!(shell.output.stderr(), "{}\r", blank));
767 }
768 _ => (),
769 }
770}