1use super::style::*;
6use anstream::AutoStream;
7use anstyle::Style;
8use clap::ValueEnum;
9use eyre::Result;
10use serde::{Deserialize, Serialize};
11use std::{
12 fmt,
13 io::{prelude::*, IsTerminal},
14 ops::DerefMut,
15 sync::{
16 atomic::{AtomicBool, Ordering},
17 Mutex, OnceLock, PoisonError,
18 },
19};
20
21pub fn color_choice() -> ColorChoice {
23 Shell::get().color_choice()
24}
25
26pub fn verbosity() -> Verbosity {
28 Shell::get().verbosity()
29}
30
31pub fn set_verbosity(verbosity: Verbosity) {
33 Shell::get().set_verbosity(verbosity);
34}
35
36pub fn is_quiet() -> bool {
38 Shell::get().output_mode().is_quiet()
39}
40
41pub fn is_json() -> bool {
43 Shell::get().is_json()
44}
45
46static GLOBAL_SHELL: OnceLock<Mutex<Shell>> = OnceLock::new();
48
49pub enum TtyWidth {
51 NoTty,
53 Known(usize),
55 Guess(usize),
57}
58
59impl TtyWidth {
60 pub fn get() -> Self {
62 #[cfg(unix)]
64 let opt = terminal_size::terminal_size_of(std::io::stderr());
65 #[cfg(not(unix))]
66 let opt = terminal_size::terminal_size();
67 match opt {
68 Some((w, _)) => Self::Known(w.0 as usize),
69 None => Self::NoTty,
70 }
71 }
72
73 pub fn progress_max_width(&self) -> Option<usize> {
75 match *self {
76 Self::NoTty => None,
77 Self::Known(width) | Self::Guess(width) => Some(width),
78 }
79 }
80}
81
82#[derive(Debug, Default, Clone, Copy, PartialEq)]
83pub enum OutputMode {
85 #[default]
87 Normal,
88 Quiet,
90}
91
92impl OutputMode {
93 #[inline]
95 pub fn is_normal(self) -> bool {
96 self == Self::Normal
97 }
98
99 #[inline]
101 pub fn is_quiet(self) -> bool {
102 self == Self::Quiet
103 }
104}
105
106#[derive(Debug, Default, Clone, Copy, PartialEq)]
108pub enum OutputFormat {
109 #[default]
111 Text,
112 Json,
114}
115
116impl OutputFormat {
117 #[inline]
119 pub fn is_text(self) -> bool {
120 self == Self::Text
121 }
122
123 #[inline]
125 pub fn is_json(self) -> bool {
126 self == Self::Json
127 }
128}
129
130pub type Verbosity = u8;
132
133pub struct Shell {
136 output: ShellOut,
139
140 output_format: OutputFormat,
142
143 output_mode: OutputMode,
145
146 verbosity: Verbosity,
148
149 needs_clear: AtomicBool,
152}
153
154impl fmt::Debug for Shell {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 let mut s = f.debug_struct("Shell");
157 s.field("output_format", &self.output_format);
158 s.field("output_mode", &self.output_mode);
159 s.field("verbosity", &self.verbosity);
160 if let ShellOut::Stream { color_choice, .. } = self.output {
161 s.field("color_choice", &color_choice);
162 }
163 s.finish()
164 }
165}
166
167enum ShellOut {
169 Stream {
171 stdout: AutoStream<std::io::Stdout>,
172 stderr: AutoStream<std::io::Stderr>,
173 stderr_tty: bool,
174 color_choice: ColorChoice,
175 },
176 Empty(std::io::Empty),
178}
179
180#[derive(Debug, Default, PartialEq, Clone, Copy, Serialize, Deserialize, ValueEnum)]
182pub enum ColorChoice {
183 #[default]
185 Auto,
186 Always,
188 Never,
190}
191
192impl Default for Shell {
193 #[inline]
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199impl Shell {
200 #[inline]
203 pub fn new() -> Self {
204 Self::new_with(
205 OutputFormat::Text,
206 OutputMode::Normal,
207 ColorChoice::Auto,
208 Verbosity::default(),
209 )
210 }
211
212 #[inline]
214 pub fn new_with(
215 format: OutputFormat,
216 mode: OutputMode,
217 color: ColorChoice,
218 verbosity: Verbosity,
219 ) -> Self {
220 Self {
221 output: ShellOut::Stream {
222 stdout: AutoStream::new(std::io::stdout(), color.to_anstream_color_choice()),
223 stderr: AutoStream::new(std::io::stderr(), color.to_anstream_color_choice()),
224 color_choice: color,
225 stderr_tty: std::io::stderr().is_terminal(),
226 },
227 output_format: format,
228 output_mode: mode,
229 verbosity,
230 needs_clear: AtomicBool::new(false),
231 }
232 }
233
234 #[inline]
236 pub fn empty() -> Self {
237 Self {
238 output: ShellOut::Empty(std::io::empty()),
239 output_format: OutputFormat::Text,
240 output_mode: OutputMode::Quiet,
241 verbosity: 0,
242 needs_clear: AtomicBool::new(false),
243 }
244 }
245
246 pub fn get() -> impl DerefMut<Target = Self> + 'static {
250 GLOBAL_SHELL.get_or_init(Default::default).lock().unwrap_or_else(PoisonError::into_inner)
251 }
252
253 #[track_caller]
259 pub fn set(self) {
260 GLOBAL_SHELL
261 .set(Mutex::new(self))
262 .unwrap_or_else(|_| panic!("attempted to set global shell twice"))
263 }
264
265 #[inline]
267 pub fn set_needs_clear(&self, needs_clear: bool) -> bool {
268 self.needs_clear.swap(needs_clear, Ordering::Relaxed)
269 }
270
271 pub fn is_json(&self) -> bool {
273 self.output_format.is_json()
274 }
275
276 pub fn is_quiet(&self) -> bool {
278 self.output_mode.is_quiet()
279 }
280
281 #[inline]
283 pub fn needs_clear(&self) -> bool {
284 self.needs_clear.load(Ordering::Relaxed)
285 }
286
287 #[inline]
289 pub fn is_cleared(&self) -> bool {
290 !self.needs_clear()
291 }
292
293 #[inline]
295 pub fn err_width(&self) -> TtyWidth {
296 match self.output {
297 ShellOut::Stream { stderr_tty: true, .. } => TtyWidth::get(),
298 _ => TtyWidth::NoTty,
299 }
300 }
301
302 #[inline]
304 pub fn output_format(&self) -> OutputFormat {
305 self.output_format
306 }
307
308 #[inline]
310 pub fn output_mode(&self) -> OutputMode {
311 self.output_mode
312 }
313
314 #[inline]
316 pub fn verbosity(&self) -> Verbosity {
317 self.verbosity
318 }
319
320 pub fn set_verbosity(&mut self, verbosity: Verbosity) {
322 self.verbosity = verbosity;
323 }
324
325 #[inline]
330 pub fn color_choice(&self) -> ColorChoice {
331 match self.output {
332 ShellOut::Stream { color_choice, .. } => color_choice,
333 ShellOut::Empty(_) => ColorChoice::Never,
334 }
335 }
336
337 #[inline]
339 pub fn is_err_tty(&self) -> bool {
340 match self.output {
341 ShellOut::Stream { stderr_tty, .. } => stderr_tty,
342 ShellOut::Empty(_) => false,
343 }
344 }
345
346 #[inline]
348 pub fn err_supports_color(&self) -> bool {
349 match &self.output {
350 ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
351 ShellOut::Empty(_) => false,
352 }
353 }
354
355 #[inline]
357 pub fn out_supports_color(&self) -> bool {
358 match &self.output {
359 ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
360 ShellOut::Empty(_) => false,
361 }
362 }
363
364 pub fn out(&mut self) -> &mut dyn Write {
366 self.maybe_err_erase_line();
367 self.output.stdout()
368 }
369
370 pub fn err(&mut self) -> &mut dyn Write {
372 self.maybe_err_erase_line();
373 self.output.stderr()
374 }
375
376 pub fn maybe_err_erase_line(&mut self) {
378 if self.err_supports_color() && self.set_needs_clear(false) {
379 let _ = self.output.stderr().write_all(b"\x1B[K");
383 }
384 }
385
386 pub fn error(&mut self, message: impl fmt::Display) -> Result<()> {
391 self.maybe_err_erase_line();
392 self.output.message_stderr(&"Error", &ERROR, Some(&message), false)
393 }
394
395 pub fn warn(&mut self, message: impl fmt::Display) -> Result<()> {
400 match self.output_mode {
401 OutputMode::Quiet => Ok(()),
402 _ => self.print(&"Warning", &WARN, Some(&message), false),
403 }
404 }
405
406 pub fn write_stdout(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> {
410 self.output.write_stdout(fragment, color)
411 }
412
413 pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> {
417 match self.output_mode {
418 OutputMode::Quiet => Ok(()),
419 _ => self.write_stdout(fragment, &Style::new()),
420 }
421 }
422
423 pub fn write_stderr(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> {
427 self.output.write_stderr(fragment, color)
428 }
429
430 pub fn print_err(&mut self, fragment: impl fmt::Display) -> Result<()> {
434 match self.output_mode {
435 OutputMode::Quiet => Ok(()),
436 _ => self.write_stderr(fragment, &Style::new()),
437 }
438 }
439
440 fn print(
443 &mut self,
444 status: &dyn fmt::Display,
445 style: &Style,
446 message: Option<&dyn fmt::Display>,
447 justified: bool,
448 ) -> Result<()> {
449 match self.output_mode {
450 OutputMode::Quiet => Ok(()),
451 _ => {
452 self.maybe_err_erase_line();
453 self.output.message_stderr(status, style, message, justified)
454 }
455 }
456 }
457}
458
459impl ShellOut {
460 fn message_stderr(
464 &mut self,
465 status: &dyn fmt::Display,
466 style: &Style,
467 message: Option<&dyn fmt::Display>,
468 justified: bool,
469 ) -> Result<()> {
470 let buffer = Self::format_message(status, message, style, justified)?;
471 self.stderr().write_all(&buffer)?;
472 Ok(())
473 }
474
475 fn write_stdout(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> {
477 let mut buffer = Vec::new();
478 write!(buffer, "{style}{fragment}{style:#}")?;
479 self.stdout().write_all(&buffer)?;
480 Ok(())
481 }
482
483 fn write_stderr(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> {
485 let mut buffer = Vec::new();
486 write!(buffer, "{style}{fragment}{style:#}")?;
487 self.stderr().write_all(&buffer)?;
488 Ok(())
489 }
490
491 #[inline]
493 fn stdout(&mut self) -> &mut dyn Write {
494 match self {
495 Self::Stream { stdout, .. } => stdout,
496 Self::Empty(e) => e,
497 }
498 }
499
500 #[inline]
502 fn stderr(&mut self) -> &mut dyn Write {
503 match self {
504 Self::Stream { stderr, .. } => stderr,
505 Self::Empty(e) => e,
506 }
507 }
508
509 fn format_message(
511 status: &dyn fmt::Display,
512 message: Option<&dyn fmt::Display>,
513 style: &Style,
514 justified: bool,
515 ) -> Result<Vec<u8>> {
516 let bold = anstyle::Style::new().bold();
517
518 let mut buffer = Vec::new();
519 if justified {
520 write!(buffer, "{style}{status:>12}{style:#}")?;
521 } else {
522 write!(buffer, "{style}{status}{style:#}{bold}:{bold:#}")?;
523 }
524 match message {
525 Some(message) => {
526 writeln!(buffer, " {message}")?;
527 }
528 None => write!(buffer, " ")?,
529 }
530
531 Ok(buffer)
532 }
533}
534
535impl ColorChoice {
536 #[inline]
538 fn to_anstream_color_choice(self) -> anstream::ColorChoice {
539 match self {
540 Self::Always => anstream::ColorChoice::Always,
541 Self::Never => anstream::ColorChoice::Never,
542 Self::Auto => anstream::ColorChoice::Auto,
543 }
544 }
545}
546
547#[inline]
548fn supports_color(choice: anstream::ColorChoice) -> bool {
549 match choice {
550 anstream::ColorChoice::Always |
551 anstream::ColorChoice::AlwaysAnsi |
552 anstream::ColorChoice::Auto => true,
553 anstream::ColorChoice::Never => false,
554 }
555}