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_json() -> bool {
43 Shell::get().is_json()
44}
45
46pub fn is_markdown() -> bool {
48 Shell::get().is_markdown()
49}
50
51static GLOBAL_SHELL: OnceLock<Mutex<Shell>> = OnceLock::new();
53
54pub enum TtyWidth {
56 NoTty,
58 Known(usize),
60 Guess(usize),
62}
63
64impl TtyWidth {
65 pub fn get() -> Self {
67 #[cfg(unix)]
69 let opt = terminal_size::terminal_size_of(std::io::stderr());
70 #[cfg(not(unix))]
71 let opt = terminal_size::terminal_size();
72 match opt {
73 Some((w, _)) => Self::Known(w.0 as usize),
74 None => Self::NoTty,
75 }
76 }
77
78 pub fn progress_max_width(&self) -> Option<usize> {
80 match *self {
81 Self::NoTty => None,
82 Self::Known(width) | Self::Guess(width) => Some(width),
83 }
84 }
85}
86
87#[derive(Debug, Default, Clone, Copy, PartialEq)]
88pub enum OutputMode {
90 #[default]
92 Normal,
93 Quiet,
95}
96
97impl OutputMode {
98 pub fn is_normal(self) -> bool {
100 self == Self::Normal
101 }
102
103 pub fn is_quiet(self) -> bool {
105 self == Self::Quiet
106 }
107}
108
109#[derive(Debug, Default, Clone, Copy, PartialEq)]
111pub enum OutputFormat {
112 #[default]
114 Text,
115 Json,
117 Markdown,
119}
120
121impl OutputFormat {
122 pub fn is_text(self) -> bool {
124 self == Self::Text
125 }
126
127 pub fn is_json(self) -> bool {
129 self == Self::Json
130 }
131
132 pub fn is_markdown(self) -> bool {
134 self == Self::Markdown
135 }
136}
137
138pub type Verbosity = u8;
140
141pub struct Shell {
144 output: ShellOut,
147
148 output_format: OutputFormat,
150
151 output_mode: OutputMode,
153
154 verbosity: Verbosity,
156
157 needs_clear: AtomicBool,
160}
161
162impl fmt::Debug for Shell {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 let mut s = f.debug_struct("Shell");
165 s.field("output_format", &self.output_format);
166 s.field("output_mode", &self.output_mode);
167 s.field("verbosity", &self.verbosity);
168 if let ShellOut::Stream { color_choice, .. } = self.output {
169 s.field("color_choice", &color_choice);
170 }
171 s.finish()
172 }
173}
174
175enum ShellOut {
177 Stream {
179 stdout: AutoStream<std::io::Stdout>,
180 stderr: AutoStream<std::io::Stderr>,
181 stderr_tty: bool,
182 color_choice: ColorChoice,
183 },
184 Empty(std::io::Empty),
186}
187
188#[derive(Debug, Default, PartialEq, Clone, Copy, Serialize, Deserialize, ValueEnum)]
190pub enum ColorChoice {
191 #[default]
193 Auto,
194 Always,
196 Never,
198}
199
200impl Default for Shell {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206impl Shell {
207 pub fn new() -> Self {
210 Self::new_with(
211 OutputFormat::Text,
212 OutputMode::Normal,
213 ColorChoice::Auto,
214 Verbosity::default(),
215 )
216 }
217
218 pub fn new_with(
220 format: OutputFormat,
221 mode: OutputMode,
222 color: ColorChoice,
223 verbosity: Verbosity,
224 ) -> Self {
225 Self {
226 output: ShellOut::Stream {
227 stdout: AutoStream::new(std::io::stdout(), color.to_anstream_color_choice()),
228 stderr: AutoStream::new(std::io::stderr(), color.to_anstream_color_choice()),
229 color_choice: color,
230 stderr_tty: std::io::stderr().is_terminal(),
231 },
232 output_format: format,
233 output_mode: mode,
234 verbosity,
235 needs_clear: AtomicBool::new(false),
236 }
237 }
238
239 pub fn empty() -> Self {
241 Self {
242 output: ShellOut::Empty(std::io::empty()),
243 output_format: OutputFormat::Text,
244 output_mode: OutputMode::Quiet,
245 verbosity: 0,
246 needs_clear: AtomicBool::new(false),
247 }
248 }
249
250 pub fn get() -> impl DerefMut<Target = Self> + 'static {
254 GLOBAL_SHELL.get_or_init(Default::default).lock().unwrap_or_else(PoisonError::into_inner)
255 }
256
257 #[track_caller]
263 pub fn set(self) {
264 GLOBAL_SHELL
265 .set(Mutex::new(self))
266 .unwrap_or_else(|_| panic!("attempted to set global shell twice"))
267 }
268
269 pub fn set_needs_clear(&self, needs_clear: bool) -> bool {
271 self.needs_clear.swap(needs_clear, Ordering::Relaxed)
272 }
273
274 pub fn is_json(&self) -> bool {
276 self.output_format.is_json()
277 }
278
279 pub fn is_markdown(&self) -> bool {
281 self.output_format.is_markdown()
282 }
283
284 pub fn is_quiet(&self) -> bool {
286 self.output_mode.is_quiet()
287 }
288
289 pub fn needs_clear(&self) -> bool {
291 self.needs_clear.load(Ordering::Relaxed)
292 }
293
294 pub fn is_cleared(&self) -> bool {
296 !self.needs_clear()
297 }
298
299 pub fn err_width(&self) -> TtyWidth {
301 match self.output {
302 ShellOut::Stream { stderr_tty: true, .. } => TtyWidth::get(),
303 _ => TtyWidth::NoTty,
304 }
305 }
306
307 pub fn output_format(&self) -> OutputFormat {
309 self.output_format
310 }
311
312 pub fn output_mode(&self) -> OutputMode {
314 self.output_mode
315 }
316
317 pub fn verbosity(&self) -> Verbosity {
319 self.verbosity
320 }
321
322 pub fn set_verbosity(&mut self, verbosity: Verbosity) {
324 self.verbosity = verbosity;
325 }
326
327 pub fn color_choice(&self) -> ColorChoice {
332 match self.output {
333 ShellOut::Stream { color_choice, .. } => color_choice,
334 ShellOut::Empty(_) => ColorChoice::Never,
335 }
336 }
337
338 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 pub fn err_supports_color(&self) -> bool {
348 match &self.output {
349 ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()),
350 ShellOut::Empty(_) => false,
351 }
352 }
353
354 pub fn out_supports_color(&self) -> bool {
356 match &self.output {
357 ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()),
358 ShellOut::Empty(_) => false,
359 }
360 }
361
362 pub fn out(&mut self) -> &mut dyn Write {
364 self.maybe_err_erase_line();
365 self.output.stdout()
366 }
367
368 pub fn err(&mut self) -> &mut dyn Write {
370 self.maybe_err_erase_line();
371 self.output.stderr()
372 }
373
374 pub fn maybe_err_erase_line(&mut self) {
376 if self.err_supports_color() && self.set_needs_clear(false) {
377 let _ = self.output.stderr().write_all(b"\x1B[K");
381 }
382 }
383
384 pub fn error(&mut self, message: impl fmt::Display) -> Result<()> {
389 self.maybe_err_erase_line();
390 self.output.message_stderr(&"Error", &ERROR, Some(&message), false)
391 }
392
393 pub fn warn(&mut self, message: impl fmt::Display) -> Result<()> {
398 match self.output_mode {
399 OutputMode::Quiet => Ok(()),
400 _ => self.print(&"Warning", &WARN, Some(&message), false),
401 }
402 }
403
404 pub fn write_stdout(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> {
408 self.output.write_stdout(fragment, color)
409 }
410
411 pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> {
415 match self.output_mode {
416 OutputMode::Quiet => Ok(()),
417 _ => self.write_stdout(fragment, &Style::new()),
418 }
419 }
420
421 pub fn write_stderr(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> {
425 self.output.write_stderr(fragment, color)
426 }
427
428 pub fn print_err(&mut self, fragment: impl fmt::Display) -> Result<()> {
432 match self.output_mode {
433 OutputMode::Quiet => Ok(()),
434 _ => self.write_stderr(fragment, &Style::new()),
435 }
436 }
437
438 fn print(
441 &mut self,
442 status: &dyn fmt::Display,
443 style: &Style,
444 message: Option<&dyn fmt::Display>,
445 justified: bool,
446 ) -> Result<()> {
447 match self.output_mode {
448 OutputMode::Quiet => Ok(()),
449 _ => {
450 self.maybe_err_erase_line();
451 self.output.message_stderr(status, style, message, justified)
452 }
453 }
454 }
455}
456
457impl ShellOut {
458 fn message_stderr(
462 &mut self,
463 status: &dyn fmt::Display,
464 style: &Style,
465 message: Option<&dyn fmt::Display>,
466 justified: bool,
467 ) -> Result<()> {
468 let buffer = Self::format_message(status, message, style, justified)?;
469 self.stderr().write_all(&buffer)?;
470 Ok(())
471 }
472
473 fn write_stdout(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> {
475 let mut buffer = Vec::new();
476 write!(buffer, "{style}{fragment}{style:#}")?;
477 self.stdout().write_all(&buffer)?;
478 Ok(())
479 }
480
481 fn write_stderr(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> {
483 let mut buffer = Vec::new();
484 write!(buffer, "{style}{fragment}{style:#}")?;
485 self.stderr().write_all(&buffer)?;
486 Ok(())
487 }
488
489 fn stdout(&mut self) -> &mut dyn Write {
491 match self {
492 Self::Stream { stdout, .. } => stdout,
493 Self::Empty(e) => e,
494 }
495 }
496
497 fn stderr(&mut self) -> &mut dyn Write {
499 match self {
500 Self::Stream { stderr, .. } => stderr,
501 Self::Empty(e) => e,
502 }
503 }
504
505 fn format_message(
507 status: &dyn fmt::Display,
508 message: Option<&dyn fmt::Display>,
509 style: &Style,
510 justified: bool,
511 ) -> Result<Vec<u8>> {
512 let bold = anstyle::Style::new().bold();
513
514 let mut buffer = Vec::new();
515 if justified {
516 write!(buffer, "{style}{status:>12}{style:#}")?;
517 } else {
518 write!(buffer, "{style}{status}{style:#}{bold}:{bold:#}")?;
519 }
520 match message {
521 Some(message) => {
522 writeln!(buffer, " {message}")?;
523 }
524 None => write!(buffer, " ")?,
525 }
526
527 Ok(buffer)
528 }
529}
530
531impl ColorChoice {
532 fn to_anstream_color_choice(self) -> anstream::ColorChoice {
534 match self {
535 Self::Always => anstream::ColorChoice::Always,
536 Self::Never => anstream::ColorChoice::Never,
537 Self::Auto => anstream::ColorChoice::Auto,
538 }
539 }
540}
541
542fn supports_color(choice: anstream::ColorChoice) -> bool {
543 match choice {
544 anstream::ColorChoice::Always
545 | anstream::ColorChoice::AlwaysAnsi
546 | anstream::ColorChoice::Auto => true,
547 anstream::ColorChoice::Never => false,
548 }
549}