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