1use crate::shell::hostname::hostname;
2use crate::shell::shell_::style::{ERROR, HEADER, NOTE, WARN};
3use anstream::AutoStream;
4use anstyle::Style;
5use std::fmt;
6use std::io::{IsTerminal, Write};
7
8pub enum TtyWidth {
9 NoTty,
10 Known(usize),
11 Guess(usize),
12}
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum Verbosity {
17 Verbose,
18 Normal,
19 Quiet,
20}
21
22pub struct Shell {
25 output: ShellOut,
28 verbosity: Verbosity,
30 needs_clear: bool,
33 hostname: Option<String>,
34}
35
36impl fmt::Debug for Shell {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self.output {
39 ShellOut::Write(_) => f
40 .debug_struct("Shell")
41 .field("verbosity", &self.verbosity)
42 .finish(),
43 ShellOut::Stream { color_choice, .. } => f
44 .debug_struct("Shell")
45 .field("verbosity", &self.verbosity)
46 .field("color_choice", &color_choice)
47 .finish(),
48 }
49 }
50}
51
52enum ShellOut {
54 Write(AutoStream<Box<dyn Write>>),
56 Stream {
58 stdout: AutoStream<std::io::Stdout>,
59 stderr: AutoStream<std::io::Stderr>,
60 stderr_tty: bool,
61 color_choice: ColorChoice,
62 hyperlinks: bool,
63 },
64}
65
66#[derive(Debug, PartialEq, Clone, Copy)]
68pub enum ColorChoice {
69 Always,
71 Never,
73 CargoAuto,
75}
76
77impl Shell {
78 pub fn new() -> Shell {
81 let auto_clr = ColorChoice::CargoAuto;
82 let stdout_choice = auto_clr.to_anstream_color_choice();
83 let stderr_choice = auto_clr.to_anstream_color_choice();
84 Shell {
85 output: ShellOut::Stream {
86 stdout: AutoStream::new(std::io::stdout(), stdout_choice),
87 stderr: AutoStream::new(std::io::stderr(), stderr_choice),
88 color_choice: auto_clr,
89 hyperlinks: supports_hyperlinks(),
90 stderr_tty: std::io::stderr().is_terminal(),
91 },
92 verbosity: Verbosity::Verbose,
93 needs_clear: false,
94 hostname: None,
95 }
96 }
97
98 pub fn from_write(out: Box<dyn Write>) -> Shell {
100 Shell {
101 output: ShellOut::Write(AutoStream::never(out)), verbosity: Verbosity::Verbose,
103 needs_clear: false,
104 hostname: None,
105 }
106 }
107
108 fn print(
111 &mut self,
112 status: &dyn fmt::Display,
113 message: Option<&dyn fmt::Display>,
114 color: &Style,
115 justified: bool,
116 ) -> anyhow::Result<()> {
117 match self.verbosity {
118 Verbosity::Quiet => Ok(()),
119 _ => {
120 if self.needs_clear {
121 self.err_erase_line();
122 }
123 self.output
124 .message_stderr(status, message, color, justified)
125 }
126 }
127 }
128
129 pub fn set_needs_clear(&mut self, needs_clear: bool) {
131 self.needs_clear = needs_clear;
132 }
133
134 pub fn is_cleared(&self) -> bool {
136 !self.needs_clear
137 }
138
139 pub fn err_width(&self) -> TtyWidth {
141 match self.output {
142 ShellOut::Stream {
143 stderr_tty: true, ..
144 } => imp::stderr_width(),
145 _ => TtyWidth::NoTty,
146 }
147 }
148
149 pub fn is_err_tty(&self) -> bool {
151 match self.output {
152 ShellOut::Stream { stderr_tty, .. } => stderr_tty,
153 _ => false,
154 }
155 }
156
157 pub fn out(&mut self) -> &mut dyn Write {
159 if self.needs_clear {
160 self.err_erase_line();
161 }
162 self.output.stdout()
163 }
164
165 pub fn err(&mut self) -> &mut dyn Write {
167 if self.needs_clear {
168 self.err_erase_line();
169 }
170 self.output.stderr()
171 }
172
173 pub fn err_erase_line(&mut self) {
175 if self.err_supports_color() {
176 imp::err_erase_line(self);
177 self.needs_clear = false;
178 }
179 }
180
181 pub fn status<T, U>(&mut self, status: T, message: U) -> anyhow::Result<()>
183 where
184 T: fmt::Display,
185 U: fmt::Display,
186 {
187 self.print(&status, Some(&message), &HEADER, true)
188 }
189
190 pub fn status_header<T>(&mut self, status: T) -> anyhow::Result<()>
191 where
192 T: fmt::Display,
193 {
194 self.print(&status, None, &NOTE, true)
195 }
196
197 pub fn status_with_color<T, U>(
199 &mut self,
200 status: T,
201 message: U,
202 color: &Style,
203 ) -> anyhow::Result<()>
204 where
205 T: fmt::Display,
206 U: fmt::Display,
207 {
208 self.print(&status, Some(&message), color, true)
209 }
210
211 pub fn verbose<F>(&mut self, mut callback: F) -> anyhow::Result<()>
213 where
214 F: FnMut(&mut Shell) -> anyhow::Result<()>,
215 {
216 match self.verbosity {
217 Verbosity::Verbose => callback(self),
218 _ => Ok(()),
219 }
220 }
221
222 pub fn concise<F>(&mut self, mut callback: F) -> anyhow::Result<()>
224 where
225 F: FnMut(&mut Shell) -> anyhow::Result<()>,
226 {
227 match self.verbosity {
228 Verbosity::Verbose => Ok(()),
229 _ => callback(self),
230 }
231 }
232
233 pub fn error<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
235 if self.needs_clear {
236 self.err_erase_line();
237 }
238 self.output
239 .message_stderr(&"error", Some(&message), &ERROR, false)
240 }
241
242 pub fn warn<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
244 match self.verbosity {
245 Verbosity::Quiet => Ok(()),
246 _ => self.print(&"warning", Some(&message), &WARN, false),
247 }
248 }
249
250 pub fn note<T: fmt::Display>(&mut self, message: T) -> anyhow::Result<()> {
252 self.print(&"note", Some(&message), &NOTE, false)
253 }
254
255 pub fn set_verbosity(&mut self, verbosity: Verbosity) {
257 self.verbosity = verbosity;
258 }
259
260 pub fn verbosity(&self) -> Verbosity {
262 self.verbosity
263 }
264
265 pub fn set_color_choice(&mut self, color: Option<&str>) -> anyhow::Result<()> {
267 if let ShellOut::Stream {
268 ref mut stdout,
269 ref mut stderr,
270 ref mut color_choice,
271 ..
272 } = self.output
273 {
274 let cfg = match color {
275 Some("always") => ColorChoice::Always,
276 Some("never") => ColorChoice::Never,
277
278 Some("auto") | None => ColorChoice::CargoAuto,
279
280 Some(arg) => anyhow::bail!(
281 "argument for --color must be auto, always, or \
282 never, but found `{}`",
283 arg
284 ),
285 };
286 *color_choice = cfg;
287 let stdout_choice = cfg.to_anstream_color_choice();
288 let stderr_choice = cfg.to_anstream_color_choice();
289 *stdout = AutoStream::new(std::io::stdout(), stdout_choice);
290 *stderr = AutoStream::new(std::io::stderr(), stderr_choice);
291 }
292 Ok(())
293 }
294
295 pub fn set_hyperlinks(&mut self, yes: bool) -> anyhow::Result<()> {
296 if let ShellOut::Stream {
297 ref mut hyperlinks, ..
298 } = self.output
299 {
300 *hyperlinks = yes;
301 }
302 Ok(())
303 }
304
305 pub fn color_choice(&self) -> ColorChoice {
310 match self.output {
311 ShellOut::Stream { color_choice, .. } => color_choice,
312 ShellOut::Write(_) => ColorChoice::Never,
313 }
314 }
315
316 pub fn err_supports_color(&self) -> bool {
318 match &self.output {
319 ShellOut::Write(_) => false,
320 ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
321 }
322 }
323
324 pub fn out_supports_color(&self) -> bool {
325 match &self.output {
326 ShellOut::Write(_) => false,
327 ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
328 }
329 }
330
331 pub fn out_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
332 let supports_hyperlinks = match &self.output {
333 ShellOut::Write(_) => false,
334 ShellOut::Stream {
335 stdout, hyperlinks, ..
336 } => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
337 };
338 Hyperlink {
339 url: supports_hyperlinks.then_some(url),
340 }
341 }
342
343 pub fn err_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
344 let supports_hyperlinks = match &self.output {
345 ShellOut::Write(_) => false,
346 ShellOut::Stream {
347 stderr, hyperlinks, ..
348 } => stderr.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
349 };
350 if supports_hyperlinks {
351 Hyperlink { url: Some(url) }
352 } else {
353 Hyperlink { url: None }
354 }
355 }
356
357 pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
358 let url = self.file_hyperlink(path);
359 url.map(|u| self.out_hyperlink(u)).unwrap_or_default()
360 }
361
362 pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
363 let url = self.file_hyperlink(path);
364 url.map(|u| self.err_hyperlink(u)).unwrap_or_default()
365 }
366
367 fn file_hyperlink(&mut self, path: &std::path::Path) -> Option<url::Url> {
368 let mut url = url::Url::from_file_path(path).ok()?;
369 let hostname = if cfg!(windows) {
372 None
374 } else 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 let _ = url.set_host(hostname);
381 Some(url)
382 }
383
384 pub fn print_ansi_stderr(&mut self, message: &[u8]) -> anyhow::Result<()> {
386 if self.needs_clear {
387 self.err_erase_line();
388 }
389 self.err().write_all(message)?;
390 Ok(())
391 }
392
393 pub fn print_ansi_stdout(&mut self, message: &[u8]) -> anyhow::Result<()> {
395 if self.needs_clear {
396 self.err_erase_line();
397 }
398 self.out().write_all(message)?;
399 Ok(())
400 }
401
402 pub fn print_json<T: serde::ser::Serialize>(&mut self, obj: &T) -> anyhow::Result<()> {
403 let encoded = serde_json::to_string(&obj)?;
405 drop(writeln!(self.out(), "{encoded}"));
407 Ok(())
408 }
409}
410
411impl Default for Shell {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417impl ShellOut {
418 fn message_stderr(
422 &mut self,
423 status: &dyn fmt::Display,
424 message: Option<&dyn fmt::Display>,
425 style: &Style,
426 justified: bool,
427 ) -> anyhow::Result<()> {
428 let style = style.render();
429 let bold = (anstyle::Style::new() | anstyle::Effects::BOLD).render();
430 let reset = anstyle::Reset.render();
431
432 let mut buffer = Vec::new();
433 if justified {
434 write!(&mut buffer, "{style}{status:>12}{reset}")?;
435 } else {
436 write!(&mut buffer, "{style}{status}{reset}{bold}:{reset}")?;
437 }
438 match message {
439 Some(message) => writeln!(buffer, " {message}")?,
440 None => write!(buffer, " ")?,
441 }
442 self.stderr().write_all(&buffer)?;
443 Ok(())
444 }
445
446 fn stdout(&mut self) -> &mut dyn Write {
448 match *self {
449 ShellOut::Stream { ref mut stdout, .. } => stdout,
450 ShellOut::Write(ref mut w) => w,
451 }
452 }
453
454 fn stderr(&mut self) -> &mut dyn Write {
456 match *self {
457 ShellOut::Stream { ref mut stderr, .. } => stderr,
458 ShellOut::Write(ref mut w) => w,
459 }
460 }
461}
462
463impl ColorChoice {
464 fn to_anstream_color_choice(self) -> anstream::ColorChoice {
466 match self {
467 ColorChoice::Always => anstream::ColorChoice::Always,
468 ColorChoice::Never => anstream::ColorChoice::Never,
469 ColorChoice::CargoAuto => anstream::ColorChoice::Auto,
470 }
471 }
472}
473
474fn supports_color(choice: anstream::ColorChoice) -> bool {
475 match choice {
476 anstream::ColorChoice::Always
477 | anstream::ColorChoice::AlwaysAnsi
478 | anstream::ColorChoice::Auto => true,
479 anstream::ColorChoice::Never => false,
480 }
481}
482
483fn supports_hyperlinks() -> bool {
484 #[allow(clippy::disallowed_methods)] if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
486 return false;
488 }
489
490 supports_hyperlinks::supports_hyperlinks()
491}
492
493pub struct Hyperlink<D: fmt::Display> {
494 url: Option<D>,
495}
496
497impl<D: fmt::Display> Default for Hyperlink<D> {
498 fn default() -> Self {
499 Self { url: None }
500 }
501}
502
503impl<D: fmt::Display> Hyperlink<D> {
504 pub fn open(&self) -> impl fmt::Display {
505 if let Some(url) = self.url.as_ref() {
506 format!("\x1B]8;;{url}\x1B\\")
507 } else {
508 String::new()
509 }
510 }
511
512 pub fn close(&self) -> impl fmt::Display {
513 if self.url.is_some() {
514 "\x1B]8;;\x1B\\"
515 } else {
516 ""
517 }
518 }
519}
520
521#[cfg(unix)]
522mod imp {
523 use super::{Shell, TtyWidth};
524 use std::mem;
525
526 pub fn stderr_width() -> TtyWidth {
527 unsafe {
528 let mut winsize: libc::winsize = mem::zeroed();
529 if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
532 return TtyWidth::NoTty;
533 }
534 if winsize.ws_col > 0 {
535 TtyWidth::Known(winsize.ws_col as usize)
536 } else {
537 TtyWidth::NoTty
538 }
539 }
540 }
541
542 pub fn err_erase_line(shell: &mut Shell) {
543 let _ = shell.output.stderr().write_all(b"\x1B[K");
547 }
548}
549
550#[cfg(windows)]
551mod imp {
552 use std::{cmp, mem, ptr};
553
554 use windows_sys::core::PCSTR;
555 use windows_sys::Win32::Foundation::CloseHandle;
556 use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
557 use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
558 use windows_sys::Win32::Storage::FileSystem::{
559 CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
560 };
561 use windows_sys::Win32::System::Console::{
562 GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, STD_ERROR_HANDLE,
563 };
564
565 pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth};
566
567 pub fn stderr_width() -> TtyWidth {
568 unsafe {
569 let stdout = GetStdHandle(STD_ERROR_HANDLE);
570 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
571 if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
572 return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
573 }
574
575 let h = CreateFileA(
579 "CONOUT$\0".as_ptr() as PCSTR,
580 GENERIC_READ | GENERIC_WRITE,
581 FILE_SHARE_READ | FILE_SHARE_WRITE,
582 ptr::null_mut(),
583 OPEN_EXISTING,
584 0,
585 0,
586 );
587 if h == INVALID_HANDLE_VALUE {
588 return TtyWidth::NoTty;
589 }
590
591 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
592 let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
593 CloseHandle(h);
594 if rc != 0 {
595 let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
596 return TtyWidth::Guess(cmp::min(60, width));
605 }
606
607 TtyWidth::NoTty
608 }
609 }
610}
611
612#[cfg(windows)]
613fn default_err_erase_line(shell: &mut Shell) {
614 match imp::stderr_width() {
615 TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => {
616 let blank = " ".repeat(max_width);
617 drop(write!(shell.output.stderr(), "{}\r", blank));
618 }
619 _ => (),
620 }
621}
622
623mod style {
624 use anstyle::{AnsiColor, Effects, Style};
625
626 pub const HEADER: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
627 pub const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD);
628 pub const WARN: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD);
629 pub const NOTE: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
630}