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::{IsTerminal, prelude::*},
14 ops::DerefMut,
15 sync::{
16 Mutex, OnceLock, PoisonError,
17 atomic::{AtomicBool, Ordering},
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_err_tty() -> bool {
45 Shell::get().is_err_tty()
46}
47
48pub fn is_out_tty() -> bool {
53 Shell::get().is_out_tty()
54}
55
56pub fn is_json() -> bool {
58 Shell::get().is_json()
59}
60
61pub fn is_markdown() -> bool {
63 Shell::get().is_markdown()
64}
65
66static GLOBAL_SHELL: OnceLock<Mutex<Shell>> = OnceLock::new();
68
69#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
70pub enum OutputMode {
72 #[default]
74 Normal,
75 Quiet,
77}
78
79impl OutputMode {
80 pub fn is_normal(self) -> bool {
82 self == Self::Normal
83 }
84
85 pub fn is_quiet(self) -> bool {
87 self == Self::Quiet
88 }
89}
90
91#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
93pub enum OutputFormat {
94 #[default]
96 Text,
97 Json,
99 Markdown,
101}
102
103impl OutputFormat {
104 pub fn is_text(self) -> bool {
106 self == Self::Text
107 }
108
109 pub fn is_json(self) -> bool {
111 self == Self::Json
112 }
113
114 pub fn is_markdown(self) -> bool {
116 self == Self::Markdown
117 }
118}
119
120pub type Verbosity = u8;
122
123pub struct Shell {
126 output: ShellOut,
129
130 output_format: OutputFormat,
132
133 output_mode: OutputMode,
135
136 verbosity: Verbosity,
138
139 needs_clear: AtomicBool,
142}
143
144impl fmt::Debug for Shell {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 let mut s = f.debug_struct("Shell");
147 s.field("output_format", &self.output_format);
148 s.field("output_mode", &self.output_mode);
149 s.field("verbosity", &self.verbosity);
150 if let ShellOut::Stream { color_choice, .. } = self.output {
151 s.field("color_choice", &color_choice);
152 }
153 s.finish()
154 }
155}
156
157enum ShellOut {
159 Stream {
161 stdout: AutoStream<std::io::Stdout>,
162 stderr: AutoStream<std::io::Stderr>,
163 stdout_tty: bool,
164 stderr_tty: bool,
165 color_choice: ColorChoice,
166 },
167 Empty(std::io::Empty),
169 Captured { stdout: Vec<u8>, stderr: Vec<u8> },
171}
172
173#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, ValueEnum)]
175pub enum ColorChoice {
176 #[default]
178 Auto,
179 Always,
181 Never,
183}
184
185impl Default for Shell {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191impl Shell {
192 pub fn new() -> Self {
195 Self::new_with(
196 OutputFormat::Text,
197 OutputMode::Normal,
198 ColorChoice::Auto,
199 Verbosity::default(),
200 )
201 }
202
203 pub fn new_with(
205 format: OutputFormat,
206 mode: OutputMode,
207 color: ColorChoice,
208 verbosity: Verbosity,
209 ) -> Self {
210 Self {
211 output: ShellOut::Stream {
212 stdout: AutoStream::new(std::io::stdout(), color.to_anstream_color_choice()),
213 stderr: AutoStream::new(std::io::stderr(), color.to_anstream_color_choice()),
214 color_choice: color,
215 stdout_tty: std::io::stdout().is_terminal(),
216 stderr_tty: std::io::stderr().is_terminal(),
217 },
218 output_format: format,
219 output_mode: mode,
220 verbosity,
221 needs_clear: AtomicBool::new(false),
222 }
223 }
224
225 pub const fn empty() -> Self {
227 Self {
228 output: ShellOut::Empty(std::io::empty()),
229 output_format: OutputFormat::Text,
230 output_mode: OutputMode::Quiet,
231 verbosity: 0,
232 needs_clear: AtomicBool::new(false),
233 }
234 }
235
236 pub const fn captured() -> Self {
242 Self {
243 output: ShellOut::Captured { stdout: Vec::new(), stderr: Vec::new() },
244 output_format: OutputFormat::Text,
245 output_mode: OutputMode::Normal,
246 verbosity: 0,
247 needs_clear: AtomicBool::new(false),
248 }
249 }
250
251 pub fn captured_stdout(&self) -> Option<&[u8]> {
253 match &self.output {
254 ShellOut::Captured { stdout, .. } => Some(stdout),
255 _ => None,
256 }
257 }
258
259 pub fn captured_stderr(&self) -> Option<&[u8]> {
261 match &self.output {
262 ShellOut::Captured { stderr, .. } => Some(stderr),
263 _ => None,
264 }
265 }
266
267 pub fn get() -> impl DerefMut<Target = Self> + 'static {
271 GLOBAL_SHELL.get_or_init(Default::default).lock().unwrap_or_else(PoisonError::into_inner)
272 }
273
274 #[track_caller]
280 pub fn set(self) {
281 GLOBAL_SHELL
282 .set(Mutex::new(self))
283 .unwrap_or_else(|_| panic!("attempted to set global shell twice"))
284 }
285
286 pub fn set_needs_clear(&self, needs_clear: bool) -> bool {
288 self.needs_clear.swap(needs_clear, Ordering::Relaxed)
289 }
290
291 pub fn is_json(&self) -> bool {
293 self.output_format.is_json()
294 }
295
296 pub fn is_markdown(&self) -> bool {
298 self.output_format.is_markdown()
299 }
300
301 pub fn is_quiet(&self) -> bool {
303 self.output_mode.is_quiet()
304 }
305
306 pub fn needs_clear(&self) -> bool {
308 self.needs_clear.load(Ordering::Relaxed)
309 }
310
311 pub const fn output_format(&self) -> OutputFormat {
313 self.output_format
314 }
315
316 pub const fn output_mode(&self) -> OutputMode {
318 self.output_mode
319 }
320
321 pub const fn verbosity(&self) -> Verbosity {
323 self.verbosity
324 }
325
326 pub const fn set_verbosity(&mut self, verbosity: Verbosity) {
328 self.verbosity = verbosity;
329 }
330
331 pub const fn set_output_mode(&mut self, output_mode: OutputMode) {
333 self.output_mode = output_mode;
334 }
335
336 pub const fn color_choice(&self) -> ColorChoice {
341 match self.output {
342 ShellOut::Stream { color_choice, .. } => color_choice,
343 ShellOut::Empty(_) | ShellOut::Captured { .. } => ColorChoice::Never,
344 }
345 }
346
347 pub const fn is_err_tty(&self) -> bool {
349 match self.output {
350 ShellOut::Stream { stderr_tty, .. } => stderr_tty,
351 ShellOut::Empty(_) | ShellOut::Captured { .. } => false,
352 }
353 }
354
355 pub const fn is_out_tty(&self) -> bool {
357 match self.output {
358 ShellOut::Stream { stdout_tty, .. } => stdout_tty,
359 ShellOut::Empty(_) | ShellOut::Captured { .. } => false,
360 }
361 }
362
363 pub fn err_supports_color(&self) -> bool {
365 match &self.output {
366 ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
367 ShellOut::Empty(_) | ShellOut::Captured { .. } => false,
368 }
369 }
370
371 pub fn out_supports_color(&self) -> bool {
373 match &self.output {
374 ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
375 ShellOut::Empty(_) | ShellOut::Captured { .. } => false,
376 }
377 }
378
379 pub fn out(&mut self) -> &mut dyn Write {
381 self.maybe_err_erase_line();
382 self.output.stdout()
383 }
384
385 pub fn err(&mut self) -> &mut dyn Write {
387 self.maybe_err_erase_line();
388 self.output.stderr()
389 }
390
391 pub fn maybe_err_erase_line(&mut self) {
393 if self.err_supports_color() && self.set_needs_clear(false) {
394 let _ = self.output.stderr().write_all(b"\x1B[K");
398 }
399 }
400
401 pub fn error(&mut self, message: impl fmt::Display) -> Result<()> {
406 self.maybe_err_erase_line();
407 self.output.message_stderr(&"Error", &ERROR, Some(&message), false)
408 }
409
410 pub fn warn(&mut self, message: impl fmt::Display) -> Result<()> {
415 match self.output_mode {
416 OutputMode::Quiet => Ok(()),
417 _ => self.print(&"Warning", &WARN, Some(&message), false),
418 }
419 }
420
421 pub fn write_stdout(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> {
425 self.output.write_stdout(fragment, color)
426 }
427
428 pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> {
436 match self.output_mode {
437 OutputMode::Quiet => Ok(()),
438 _ => self.write_stdout(fragment, &Style::new()),
439 }
440 }
441
442 pub fn write_stderr(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> {
446 self.output.write_stderr(fragment, color)
447 }
448
449 pub fn print_err(&mut self, fragment: impl fmt::Display) -> Result<()> {
453 match self.output_mode {
454 OutputMode::Quiet => Ok(()),
455 _ => self.write_stderr(fragment, &Style::new()),
456 }
457 }
458
459 fn print(
462 &mut self,
463 status: &dyn fmt::Display,
464 style: &Style,
465 message: Option<&dyn fmt::Display>,
466 justified: bool,
467 ) -> Result<()> {
468 match self.output_mode {
469 OutputMode::Quiet => Ok(()),
470 _ => {
471 self.maybe_err_erase_line();
472 self.output.message_stderr(status, style, message, justified)
473 }
474 }
475 }
476}
477
478impl ShellOut {
479 fn message_stderr(
483 &mut self,
484 status: &dyn fmt::Display,
485 style: &Style,
486 message: Option<&dyn fmt::Display>,
487 justified: bool,
488 ) -> Result<()> {
489 let buffer = Self::format_message(status, message, style, justified)?;
490 self.stderr().write_all(&buffer)?;
491 Ok(())
492 }
493
494 fn write_stdout(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> {
496 let mut buffer = Vec::new();
497 write!(buffer, "{style}{fragment}{style:#}")?;
498 self.stdout().write_all(&buffer)?;
499 Ok(())
500 }
501
502 fn write_stderr(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> {
504 let mut buffer = Vec::new();
505 write!(buffer, "{style}{fragment}{style:#}")?;
506 self.stderr().write_all(&buffer)?;
507 Ok(())
508 }
509
510 fn stdout(&mut self) -> &mut dyn Write {
512 match self {
513 Self::Stream { stdout, .. } => stdout,
514 Self::Empty(e) => e,
515 Self::Captured { stdout, .. } => stdout,
516 }
517 }
518
519 fn stderr(&mut self) -> &mut dyn Write {
521 match self {
522 Self::Stream { stderr, .. } => stderr,
523 Self::Empty(e) => e,
524 Self::Captured { stderr, .. } => stderr,
525 }
526 }
527
528 fn format_message(
530 status: &dyn fmt::Display,
531 message: Option<&dyn fmt::Display>,
532 style: &Style,
533 justified: bool,
534 ) -> Result<Vec<u8>> {
535 let bold = anstyle::Style::new().bold();
536
537 let mut buffer = Vec::new();
538 if justified {
539 write!(buffer, "{style}{status:>12}{style:#}")?;
540 } else {
541 write!(buffer, "{style}{status}{style:#}{bold}:{bold:#}")?;
542 }
543 match message {
544 Some(message) => {
545 writeln!(buffer, " {message}")?;
546 }
547 None => write!(buffer, " ")?,
548 }
549
550 Ok(buffer)
551 }
552}
553
554impl ColorChoice {
555 const fn to_anstream_color_choice(self) -> anstream::ColorChoice {
557 match self {
558 Self::Always => anstream::ColorChoice::Always,
559 Self::Never => anstream::ColorChoice::Never,
560 Self::Auto => anstream::ColorChoice::Auto,
561 }
562 }
563}
564
565const fn supports_color(choice: anstream::ColorChoice) -> bool {
566 match choice {
567 anstream::ColorChoice::Always
568 | anstream::ColorChoice::AlwaysAnsi
569 | anstream::ColorChoice::Auto => true,
570 anstream::ColorChoice::Never => false,
571 }
572}