1use std::fmt;
19use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
20
21use anstyle::{AnsiColor, Style};
22use clap::builder::Styles;
23
24pub const EXIT_OK: i32 = 0;
26pub const EXIT_FAILURE: i32 = 1;
29pub const EXIT_USAGE: i32 = 2;
32
33pub const ERROR_STYLE: Style = AnsiColor::Red.on_default().bold();
35pub const HELP_STYLE: Style = AnsiColor::Cyan.on_default().bold();
37pub const PHASE_STYLE: Style = AnsiColor::Green.on_default().bold();
39pub const WARN_STYLE: Style = AnsiColor::Yellow.on_default().bold();
41
42pub const CLAP_STYLES: Styles = Styles::styled()
45 .header(AnsiColor::Green.on_default().bold())
46 .usage(AnsiColor::Green.on_default().bold())
47 .literal(AnsiColor::Cyan.on_default().bold())
48 .placeholder(AnsiColor::Cyan.on_default());
49
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
52pub enum ColorMode {
53 #[default]
55 Auto,
56 Always,
58 Never,
60}
61
62static QUIET: AtomicBool = AtomicBool::new(false);
63static VERBOSITY: AtomicU8 = AtomicU8::new(0);
64
65pub fn init(color: ColorMode, quiet: bool, verbosity: u8) {
72 QUIET.store(quiet, Ordering::Relaxed);
73 VERBOSITY.store(verbosity, Ordering::Relaxed);
74 let choice = match color {
75 ColorMode::Auto => anstream::ColorChoice::Auto,
76 ColorMode::Always => anstream::ColorChoice::Always,
77 ColorMode::Never => anstream::ColorChoice::Never,
78 };
79 choice.write_global();
80}
81
82pub fn is_quiet() -> bool {
84 QUIET.load(Ordering::Relaxed)
85}
86
87pub fn verbosity() -> u8 {
89 VERBOSITY.load(Ordering::Relaxed)
90}
91
92pub fn info(msg: impl fmt::Display) {
94 if !is_quiet() {
95 anstream::println!("{msg}");
96 }
97}
98
99pub fn phase(verb: &str, rest: impl fmt::Display) {
107 if !is_quiet() {
108 anstream::eprintln!("{PHASE_STYLE}{verb:>12}{PHASE_STYLE:#} {rest}");
109 }
110}
111
112#[derive(Debug)]
123pub struct CliError {
124 pub message: String,
126 pub hint: Option<String>,
128 pub exit_code: i32,
130}
131
132impl CliError {
133 pub fn new(message: impl Into<String>) -> Self {
135 Self { message: message.into(), hint: None, exit_code: EXIT_FAILURE }
136 }
137
138 pub fn with_hint(message: impl Into<String>, hint: impl Into<String>) -> Self {
140 Self { message: message.into(), hint: Some(hint.into()), exit_code: EXIT_FAILURE }
141 }
142
143 pub fn exit_code(mut self, code: i32) -> Self {
145 self.exit_code = code;
146 self
147 }
148}
149
150impl fmt::Display for CliError {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 write!(f, "{}", self.message)
153 }
154}
155
156impl std::error::Error for CliError {}
157
158pub fn render_error(e: &(dyn std::error::Error + 'static)) -> i32 {
164 if let Some(cli_err) = e.downcast_ref::<CliError>() {
165 anstream::eprintln!("{ERROR_STYLE}error:{ERROR_STYLE:#} {}", cli_err.message);
166 if let Some(hint) = &cli_err.hint {
167 anstream::eprintln!("{HELP_STYLE}help:{HELP_STYLE:#} {hint}");
168 }
169 cli_err.exit_code
170 } else {
171 anstream::eprintln!("{ERROR_STYLE}error:{ERROR_STYLE:#} {e}");
172 EXIT_FAILURE
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn cli_error_defaults_to_failure_exit_code() {
182 let e = CliError::new("boom");
183 assert_eq!(e.exit_code, EXIT_FAILURE);
184 assert!(e.hint.is_none());
185 assert_eq!(e.to_string(), "boom");
186 }
187
188 #[test]
189 fn cli_error_carries_hint_and_custom_code() {
190 let e = CliError::with_hint("boom", "try --fix").exit_code(EXIT_USAGE);
191 assert_eq!(e.hint.as_deref(), Some("try --fix"));
192 assert_eq!(e.exit_code, EXIT_USAGE);
193 }
194
195 #[test]
196 fn render_error_uses_cli_error_exit_code() {
197 let boxed: Box<dyn std::error::Error> =
198 Box::new(CliError::new("x").exit_code(EXIT_USAGE));
199 assert_eq!(render_error(boxed.as_ref()), EXIT_USAGE);
200 }
201
202 #[test]
203 fn render_error_defaults_other_errors_to_failure() {
204 let boxed: Box<dyn std::error::Error> = "plain".into();
205 assert_eq!(render_error(boxed.as_ref()), EXIT_FAILURE);
206 }
207}