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_json() -> bool {
50 Shell::get().is_json()
51}
52
53pub fn is_markdown() -> bool {
55 Shell::get().is_markdown()
56}
57
58static GLOBAL_SHELL: OnceLock<Mutex<Shell>> = OnceLock::new();
60
61#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
62pub enum OutputMode {
64 #[default]
66 Normal,
67 Quiet,
69}
70
71impl OutputMode {
72 pub fn is_normal(self) -> bool {
74 self == Self::Normal
75 }
76
77 pub fn is_quiet(self) -> bool {
79 self == Self::Quiet
80 }
81}
82
83#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
85pub enum OutputFormat {
86 #[default]
88 Text,
89 Json,
91 Markdown,
93}
94
95impl OutputFormat {
96 pub fn is_text(self) -> bool {
98 self == Self::Text
99 }
100
101 pub fn is_json(self) -> bool {
103 self == Self::Json
104 }
105
106 pub fn is_markdown(self) -> bool {
108 self == Self::Markdown
109 }
110}
111
112pub type Verbosity = u8;
114
115pub struct Shell {
118 output: ShellOut,
121
122 output_format: OutputFormat,
124
125 output_mode: OutputMode,
127
128 verbosity: Verbosity,
130
131 needs_clear: AtomicBool,
134}
135
136impl fmt::Debug for Shell {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 let mut s = f.debug_struct("Shell");
139 s.field("output_format", &self.output_format);
140 s.field("output_mode", &self.output_mode);
141 s.field("verbosity", &self.verbosity);
142 if let ShellOut::Stream { color_choice, .. } = self.output {
143 s.field("color_choice", &color_choice);
144 }
145 s.finish()
146 }
147}
148
149enum ShellOut {
151 Stream {
153 stdout: AutoStream<std::io::Stdout>,
154 stderr: AutoStream<std::io::Stderr>,
155 stderr_tty: bool,
156 color_choice: ColorChoice,
157 },
158 Empty(std::io::Empty),
160 Captured { stdout: Vec<u8>, stderr: Vec<u8> },
162}
163
164#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, ValueEnum)]
166pub enum ColorChoice {
167 #[default]
169 Auto,
170 Always,
172 Never,
174}
175
176impl Default for Shell {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182impl Shell {
183 pub fn new() -> Self {
186 Self::new_with(
187 OutputFormat::Text,
188 OutputMode::Normal,
189 ColorChoice::Auto,
190 Verbosity::default(),
191 )
192 }
193
194 pub fn new_with(
196 format: OutputFormat,
197 mode: OutputMode,
198 color: ColorChoice,
199 verbosity: Verbosity,
200 ) -> Self {
201 Self {
202 output: ShellOut::Stream {
203 stdout: AutoStream::new(std::io::stdout(), color.to_anstream_color_choice()),
204 stderr: AutoStream::new(std::io::stderr(), color.to_anstream_color_choice()),
205 color_choice: color,
206 stderr_tty: std::io::stderr().is_terminal(),
207 },
208 output_format: format,
209 output_mode: mode,
210 verbosity,
211 needs_clear: AtomicBool::new(false),
212 }
213 }
214
215 pub const fn empty() -> Self {
217 Self {
218 output: ShellOut::Empty(std::io::empty()),
219 output_format: OutputFormat::Text,
220 output_mode: OutputMode::Quiet,
221 verbosity: 0,
222 needs_clear: AtomicBool::new(false),
223 }
224 }
225
226 pub const fn captured() -> Self {
232 Self {
233 output: ShellOut::Captured { stdout: Vec::new(), stderr: Vec::new() },
234 output_format: OutputFormat::Text,
235 output_mode: OutputMode::Normal,
236 verbosity: 0,
237 needs_clear: AtomicBool::new(false),
238 }
239 }
240
241 pub fn captured_stdout(&self) -> Option<&[u8]> {
243 match &self.output {
244 ShellOut::Captured { stdout, .. } => Some(stdout),
245 _ => None,
246 }
247 }
248
249 pub fn captured_stderr(&self) -> Option<&[u8]> {
251 match &self.output {
252 ShellOut::Captured { stderr, .. } => Some(stderr),
253 _ => None,
254 }
255 }
256
257 pub fn get() -> impl DerefMut<Target = Self> + 'static {
261 GLOBAL_SHELL.get_or_init(Default::default).lock().unwrap_or_else(PoisonError::into_inner)
262 }
263
264 #[track_caller]
270 pub fn set(self) {
271 GLOBAL_SHELL
272 .set(Mutex::new(self))
273 .unwrap_or_else(|_| panic!("attempted to set global shell twice"))
274 }
275
276 pub fn set_needs_clear(&self, needs_clear: bool) -> bool {
278 self.needs_clear.swap(needs_clear, Ordering::Relaxed)
279 }
280
281 pub fn is_json(&self) -> bool {
283 self.output_format.is_json()
284 }
285
286 pub fn is_markdown(&self) -> bool {
288 self.output_format.is_markdown()
289 }
290
291 pub fn is_quiet(&self) -> bool {
293 self.output_mode.is_quiet()
294 }
295
296 pub fn needs_clear(&self) -> bool {
298 self.needs_clear.load(Ordering::Relaxed)
299 }
300
301 pub fn is_cleared(&self) -> bool {
303 !self.needs_clear()
304 }
305
306 pub const fn output_format(&self) -> OutputFormat {
308 self.output_format
309 }
310
311 pub const fn output_mode(&self) -> OutputMode {
313 self.output_mode
314 }
315
316 pub const fn verbosity(&self) -> Verbosity {
318 self.verbosity
319 }
320
321 pub const fn set_verbosity(&mut self, verbosity: Verbosity) {
323 self.verbosity = verbosity;
324 }
325
326 pub const fn set_output_mode(&mut self, output_mode: OutputMode) {
328 self.output_mode = output_mode;
329 }
330
331 pub const fn color_choice(&self) -> ColorChoice {
336 match self.output {
337 ShellOut::Stream { color_choice, .. } => color_choice,
338 ShellOut::Empty(_) | ShellOut::Captured { .. } => ColorChoice::Never,
339 }
340 }
341
342 pub const fn is_err_tty(&self) -> bool {
344 match self.output {
345 ShellOut::Stream { stderr_tty, .. } => stderr_tty,
346 ShellOut::Empty(_) | ShellOut::Captured { .. } => false,
347 }
348 }
349
350 pub fn err_supports_color(&self) -> bool {
352 match &self.output {
353 ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
354 ShellOut::Empty(_) | ShellOut::Captured { .. } => false,
355 }
356 }
357
358 pub fn out_supports_color(&self) -> bool {
360 match &self.output {
361 ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
362 ShellOut::Empty(_) | ShellOut::Captured { .. } => false,
363 }
364 }
365
366 pub fn out(&mut self) -> &mut dyn Write {
368 self.maybe_err_erase_line();
369 self.output.stdout()
370 }
371
372 pub fn err(&mut self) -> &mut dyn Write {
374 self.maybe_err_erase_line();
375 self.output.stderr()
376 }
377
378 pub fn maybe_err_erase_line(&mut self) {
380 if self.err_supports_color() && self.set_needs_clear(false) {
381 let _ = self.output.stderr().write_all(b"\x1B[K");
385 }
386 }
387
388 pub fn error(&mut self, message: impl fmt::Display) -> Result<()> {
393 self.maybe_err_erase_line();
394 self.output.message_stderr(&"Error", &ERROR, Some(&message), false)
395 }
396
397 pub fn warn(&mut self, message: impl fmt::Display) -> Result<()> {
402 match self.output_mode {
403 OutputMode::Quiet => Ok(()),
404 _ => self.print(&"Warning", &WARN, Some(&message), false),
405 }
406 }
407
408 pub fn write_stdout(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> {
412 self.output.write_stdout(fragment, color)
413 }
414
415 pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> {
423 match self.output_mode {
424 OutputMode::Quiet => Ok(()),
425 _ => self.write_stdout(fragment, &Style::new()),
426 }
427 }
428
429 pub fn write_stderr(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> {
433 self.output.write_stderr(fragment, color)
434 }
435
436 pub fn print_err(&mut self, fragment: impl fmt::Display) -> Result<()> {
440 match self.output_mode {
441 OutputMode::Quiet => Ok(()),
442 _ => self.write_stderr(fragment, &Style::new()),
443 }
444 }
445
446 fn print(
449 &mut self,
450 status: &dyn fmt::Display,
451 style: &Style,
452 message: Option<&dyn fmt::Display>,
453 justified: bool,
454 ) -> Result<()> {
455 match self.output_mode {
456 OutputMode::Quiet => Ok(()),
457 _ => {
458 self.maybe_err_erase_line();
459 self.output.message_stderr(status, style, message, justified)
460 }
461 }
462 }
463}
464
465impl ShellOut {
466 fn message_stderr(
470 &mut self,
471 status: &dyn fmt::Display,
472 style: &Style,
473 message: Option<&dyn fmt::Display>,
474 justified: bool,
475 ) -> Result<()> {
476 let buffer = Self::format_message(status, message, style, justified)?;
477 self.stderr().write_all(&buffer)?;
478 Ok(())
479 }
480
481 fn write_stdout(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> {
483 let mut buffer = Vec::new();
484 write!(buffer, "{style}{fragment}{style:#}")?;
485 self.stdout().write_all(&buffer)?;
486 Ok(())
487 }
488
489 fn write_stderr(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> {
491 let mut buffer = Vec::new();
492 write!(buffer, "{style}{fragment}{style:#}")?;
493 self.stderr().write_all(&buffer)?;
494 Ok(())
495 }
496
497 fn stdout(&mut self) -> &mut dyn Write {
499 match self {
500 Self::Stream { stdout, .. } => stdout,
501 Self::Empty(e) => e,
502 Self::Captured { stdout, .. } => stdout,
503 }
504 }
505
506 fn stderr(&mut self) -> &mut dyn Write {
508 match self {
509 Self::Stream { stderr, .. } => stderr,
510 Self::Empty(e) => e,
511 Self::Captured { stderr, .. } => stderr,
512 }
513 }
514
515 fn format_message(
517 status: &dyn fmt::Display,
518 message: Option<&dyn fmt::Display>,
519 style: &Style,
520 justified: bool,
521 ) -> Result<Vec<u8>> {
522 let bold = anstyle::Style::new().bold();
523
524 let mut buffer = Vec::new();
525 if justified {
526 write!(buffer, "{style}{status:>12}{style:#}")?;
527 } else {
528 write!(buffer, "{style}{status}{style:#}{bold}:{bold:#}")?;
529 }
530 match message {
531 Some(message) => {
532 writeln!(buffer, " {message}")?;
533 }
534 None => write!(buffer, " ")?,
535 }
536
537 Ok(buffer)
538 }
539}
540
541impl ColorChoice {
542 const fn to_anstream_color_choice(self) -> anstream::ColorChoice {
544 match self {
545 Self::Always => anstream::ColorChoice::Always,
546 Self::Never => anstream::ColorChoice::Never,
547 Self::Auto => anstream::ColorChoice::Auto,
548 }
549 }
550}
551
552const fn supports_color(choice: anstream::ColorChoice) -> bool {
553 match choice {
554 anstream::ColorChoice::Always
555 | anstream::ColorChoice::AlwaysAnsi
556 | anstream::ColorChoice::Auto => true,
557 anstream::ColorChoice::Never => false,
558 }
559}