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::style::*;
12
13pub use anstyle_hyperlink::Hyperlink;
14
15pub struct Shell {
18 output: ShellOut,
21 verbosity: Verbosity,
23 needs_clear: bool,
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 }
66 }
67
68 pub fn from_write(out: Box<dyn Write + Send + Sync>) -> Shell {
70 Shell {
71 output: ShellOut::Write(AutoStream::never(out)), verbosity: Verbosity::Verbose,
73 needs_clear: false,
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 transient_status<T>(&mut self, status: T) -> CargoResult<()>
172 where
173 T: fmt::Display,
174 {
175 self.print(&status, None, &TRANSIENT, 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 let report = &[annotate_snippets::Group::with_title(
231 annotate_snippets::Level::NOTE.secondary_title(message.to_string()),
232 )];
233 self.print_report(report, false)
234 }
235
236 pub fn set_verbosity(&mut self, verbosity: Verbosity) {
238 self.verbosity = verbosity;
239 }
240
241 pub fn verbosity(&self) -> Verbosity {
243 self.verbosity
244 }
245
246 pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> {
248 if let ShellOut::Stream {
249 stdout,
250 stderr,
251 color_choice,
252 ..
253 } = &mut self.output
254 {
255 let cfg = color
256 .map(|c| c.parse())
257 .transpose()?
258 .unwrap_or(ColorChoice::CargoAuto);
259 *color_choice = cfg;
260 let stdout_choice = cfg.to_anstream_color_choice();
261 let stderr_choice = cfg.to_anstream_color_choice();
262 *stdout = AutoStream::new(std::io::stdout(), stdout_choice);
263 *stderr = AutoStream::new(std::io::stderr(), stderr_choice);
264 }
265 Ok(())
266 }
267
268 pub fn set_unicode(&mut self, yes: bool) -> CargoResult<()> {
269 if let ShellOut::Stream {
270 stdout_unicode,
271 stderr_unicode,
272 ..
273 } = &mut self.output
274 {
275 *stdout_unicode = yes;
276 *stderr_unicode = yes;
277 }
278 Ok(())
279 }
280
281 pub fn set_hyperlinks(&mut self, yes: bool) -> CargoResult<()> {
282 if let ShellOut::Stream { hyperlinks, .. } = &mut self.output {
283 *hyperlinks = yes;
284 }
285 Ok(())
286 }
287
288 pub fn out_unicode(&self) -> bool {
289 match &self.output {
290 ShellOut::Write(_) => true,
291 ShellOut::Stream { stdout_unicode, .. } => *stdout_unicode,
292 }
293 }
294
295 pub fn err_unicode(&self) -> bool {
296 match &self.output {
297 ShellOut::Write(_) => true,
298 ShellOut::Stream { stderr_unicode, .. } => *stderr_unicode,
299 }
300 }
301
302 pub fn color_choice(&self) -> ColorChoice {
307 match self.output {
308 ShellOut::Stream { color_choice, .. } => color_choice,
309 ShellOut::Write(_) => ColorChoice::Never,
310 }
311 }
312
313 pub fn err_supports_color(&self) -> bool {
315 match &self.output {
316 ShellOut::Write(_) => false,
317 ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
318 }
319 }
320
321 pub fn out_supports_color(&self) -> bool {
322 match &self.output {
323 ShellOut::Write(_) => false,
324 ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
325 }
326 }
327
328 pub fn out_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
329 let supports_hyperlinks = match &self.output {
330 ShellOut::Write(_) => false,
331 ShellOut::Stream {
332 stdout, hyperlinks, ..
333 } => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
334 };
335 if supports_hyperlinks {
336 Hyperlink::with_url(url)
337 } else {
338 Hyperlink::default()
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::with_url(url)
351 } else {
352 Hyperlink::default()
353 }
354 }
355
356 pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<String> {
357 let url = anstyle_hyperlink::path_to_url(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<String> {
362 let url = anstyle_hyperlink::path_to_url(path);
363 url.map(|u| self.err_hyperlink(u)).unwrap_or_default()
364 }
365
366 fn unstable_flags_rustc_unicode(&self) -> bool {
367 match &self.output {
368 ShellOut::Write(_) => false,
369 ShellOut::Stream {
370 unstable_flags_rustc_unicode,
371 ..
372 } => *unstable_flags_rustc_unicode,
373 }
374 }
375
376 pub(crate) fn set_unstable_flags_rustc_unicode(&mut self, yes: bool) -> CargoResult<()> {
377 if let ShellOut::Stream {
378 unstable_flags_rustc_unicode,
379 ..
380 } = &mut self.output
381 {
382 *unstable_flags_rustc_unicode = yes;
383 }
384 Ok(())
385 }
386
387 pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> {
389 if self.needs_clear {
390 self.err_erase_line();
391 }
392 self.err().write_all(message)?;
393 Ok(())
394 }
395
396 pub fn print_ansi_stdout(&mut self, message: &[u8]) -> CargoResult<()> {
398 if self.needs_clear {
399 self.err_erase_line();
400 }
401 self.out().write_all(message)?;
402 Ok(())
403 }
404
405 pub fn print_json<T: serde::ser::Serialize>(&mut self, obj: &T) -> CargoResult<()> {
406 let encoded = serde_json::to_string(obj)?;
408 drop(writeln!(self.out(), "{}", encoded));
410 Ok(())
411 }
412
413 pub fn print_report(&mut self, report: Report<'_>, force: bool) -> CargoResult<()> {
415 if !force && matches!(self.verbosity, Verbosity::Quiet) {
416 return Ok(());
417 }
418
419 if self.needs_clear {
420 self.err_erase_line();
421 }
422 let term_width = self
423 .err_width()
424 .diagnostic_terminal_width()
425 .unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH);
426 let decor_style = if self.err_unicode() && self.unstable_flags_rustc_unicode() {
427 DecorStyle::Unicode
428 } else {
429 DecorStyle::Ascii
430 };
431 let rendered = Renderer::styled()
432 .term_width(term_width)
433 .decor_style(decor_style)
434 .render(report);
435 self.err().write_all(rendered.as_bytes())?;
436 self.err().write_all(b"\n")?;
437 Ok(())
438 }
439}
440
441impl Default for Shell {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447enum ShellOut {
449 Write(AutoStream<Box<dyn Write + Send + Sync>>),
451 Stream {
453 stdout: AutoStream<std::io::Stdout>,
454 stderr: AutoStream<std::io::Stderr>,
455 stderr_tty: bool,
456 color_choice: ColorChoice,
457 hyperlinks: bool,
458 stdout_unicode: bool,
459 stderr_unicode: bool,
460 stderr_term_integration: bool,
461 unstable_flags_rustc_unicode: bool,
462 },
463}
464
465impl ShellOut {
466 fn message_stderr(
470 &mut self,
471 status: &dyn fmt::Display,
472 message: Option<&dyn fmt::Display>,
473 style: &Style,
474 justified: bool,
475 ) -> CargoResult<()> {
476 let mut buffer = Vec::new();
477 if justified {
478 write!(&mut buffer, "{style}{status:>12}{style:#}")?;
479 } else {
480 write!(&mut buffer, "{style}{status}{style:#}:")?;
481 }
482 match message {
483 Some(message) => writeln!(buffer, " {message}")?,
484 None => write!(buffer, " ")?,
485 }
486 self.stderr().write_all(&buffer)?;
487 Ok(())
488 }
489
490 fn stdout(&mut self) -> &mut dyn Write {
492 match self {
493 ShellOut::Stream { stdout, .. } => stdout,
494 ShellOut::Write(w) => w,
495 }
496 }
497
498 fn stderr(&mut self) -> &mut dyn Write {
500 match self {
501 ShellOut::Stream { stderr, .. } => stderr,
502 ShellOut::Write(w) => w,
503 }
504 }
505}
506
507pub enum TtyWidth {
508 NoTty,
509 Known(usize),
510 Guess(usize),
511}
512
513impl TtyWidth {
514 pub fn diagnostic_terminal_width(&self) -> Option<usize> {
517 #[expect(
518 clippy::disallowed_methods,
519 reason = "testing only, no reason for config support"
520 )]
521 if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") {
522 return Some(width.parse().unwrap());
523 }
524 match *self {
525 TtyWidth::NoTty | TtyWidth::Guess(_) => None,
526 TtyWidth::Known(width) => Some(width),
527 }
528 }
529
530 pub fn progress_max_width(&self) -> Option<usize> {
532 match *self {
533 TtyWidth::NoTty => None,
534 TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width),
535 }
536 }
537}
538
539#[derive(Debug, Clone, Copy, PartialEq)]
541pub enum Verbosity {
542 Verbose,
543 Normal,
544 Quiet,
545}
546
547#[derive(Debug, PartialEq, Clone, Copy)]
549pub enum ColorChoice {
550 Always,
552 Never,
554 CargoAuto,
556}
557
558impl ColorChoice {
559 fn to_anstream_color_choice(self) -> anstream::ColorChoice {
561 match self {
562 ColorChoice::Always => anstream::ColorChoice::Always,
563 ColorChoice::Never => anstream::ColorChoice::Never,
564 ColorChoice::CargoAuto => anstream::ColorChoice::Auto,
565 }
566 }
567}
568
569impl std::str::FromStr for ColorChoice {
570 type Err = anyhow::Error;
571 fn from_str(color: &str) -> Result<Self, Self::Err> {
572 let cfg = match color {
573 "always" => ColorChoice::Always,
574 "never" => ColorChoice::Never,
575
576 "auto" => ColorChoice::CargoAuto,
577
578 arg => anyhow::bail!(
579 "argument for --color must be auto, always, or \
580 never, but found `{}`",
581 arg
582 ),
583 };
584 Ok(cfg)
585 }
586}
587
588fn supports_color(choice: anstream::ColorChoice) -> bool {
589 match choice {
590 anstream::ColorChoice::Always
591 | anstream::ColorChoice::AlwaysAnsi
592 | anstream::ColorChoice::Auto => true,
593 anstream::ColorChoice::Never => false,
594 }
595}
596
597fn supports_unicode(stream: &dyn IsTerminal) -> bool {
598 !stream.is_terminal() || supports_unicode::supports_unicode()
599}
600
601fn supports_hyperlinks() -> bool {
602 #[expect(
603 clippy::disallowed_methods,
604 reason = "reading the state of the system, not config"
605 )]
606 if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
607 return false;
609 }
610
611 supports_hyperlinks::supports_hyperlinks()
612}
613
614fn supports_term_integration(stream: &dyn IsTerminal) -> bool {
616 anstyle_progress::supports_term_progress(stream.is_terminal())
617}
618
619#[cfg(unix)]
620mod imp {
621 use super::{Shell, TtyWidth};
622 use std::mem;
623
624 pub fn stderr_width() -> TtyWidth {
625 unsafe {
626 let mut winsize: libc::winsize = mem::zeroed();
627 if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
630 return TtyWidth::NoTty;
631 }
632 if winsize.ws_col > 0 {
633 TtyWidth::Known(winsize.ws_col as usize)
634 } else {
635 TtyWidth::NoTty
636 }
637 }
638 }
639
640 pub fn err_erase_line(shell: &mut Shell) {
641 let _ = shell.output.stderr().write_all(b"\x1B[K");
645 }
646}
647
648#[cfg(windows)]
649mod imp {
650 use std::{cmp, mem, ptr};
651
652 use windows_sys::Win32::Foundation::CloseHandle;
653 use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
654 use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
655 use windows_sys::Win32::Storage::FileSystem::{
656 CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
657 };
658 use windows_sys::Win32::System::Console::{
659 CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle, STD_ERROR_HANDLE,
660 };
661 use windows_sys::core::PCSTR;
662
663 pub(super) use super::{TtyWidth, default_err_erase_line as err_erase_line};
664
665 pub fn stderr_width() -> TtyWidth {
666 unsafe {
667 let stdout = GetStdHandle(STD_ERROR_HANDLE);
668 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
669 if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
670 return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
671 }
672
673 let h = CreateFileA(
677 "CONOUT$\0".as_ptr() as PCSTR,
678 GENERIC_READ | GENERIC_WRITE,
679 FILE_SHARE_READ | FILE_SHARE_WRITE,
680 ptr::null_mut(),
681 OPEN_EXISTING,
682 0,
683 std::ptr::null_mut(),
684 );
685 if h == INVALID_HANDLE_VALUE {
686 return TtyWidth::NoTty;
687 }
688
689 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
690 let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
691 CloseHandle(h);
692 if rc != 0 {
693 let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
694 return TtyWidth::Guess(cmp::min(60, width));
703 }
704
705 TtyWidth::NoTty
706 }
707 }
708}
709
710#[cfg(windows)]
711fn default_err_erase_line(shell: &mut Shell) {
712 match imp::stderr_width() {
713 TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
714 let blank = " ".repeat(max_width);
715 drop(write!(shell.output.stderr(), "{}\r", blank));
716 }
717 _ => (),
718 }
719}