1use std::io::IsTerminal;
9
10use clap::{Args, ValueEnum};
11
12#[derive(Args, Debug, Clone, Default)]
13pub struct OutputArgs {
14 #[arg(short = 'v', long = "verbose", global = true, action = clap::ArgAction::Count)]
16 pub verbose: u8,
17
18 #[arg(long = "color", global = true, value_enum, default_value = "auto")]
20 pub color: ColorMode,
21
22 #[arg(short = 'F', long = "format", global = true, default_value = "tree")]
25 pub format: String,
26}
27
28#[derive(ValueEnum, Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub enum ColorMode {
30 #[default]
31 Auto,
32 Always,
33 Never,
34}
35
36pub struct CliOutput {
37 pub verbosity: Verbosity,
38 pub color: bool,
40 pub format: String,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Verbosity {
45 Quiet,
47 Default,
49 Verbose,
51 Trace,
53}
54
55impl CliOutput {
56 pub fn from_args(args: &OutputArgs) -> Self {
57 let color = match args.color {
58 ColorMode::Always => true,
59 ColorMode::Never => false,
60 ColorMode::Auto => {
61 std::io::stdout().is_terminal() && std::env::var("NO_COLOR").is_err()
62 }
63 };
64 let verbosity = match args.verbose {
65 0 => Verbosity::Default,
66 1 => Verbosity::Verbose,
67 _ => Verbosity::Trace,
68 };
69 Self {
70 verbosity,
71 color,
72 format: args.format.clone(),
73 }
74 }
75
76 pub fn format_info(&self, msg: impl AsRef<str>) -> String {
81 use console::Style;
82 let prefix = if self.color {
83 Style::new().green().bold().apply_to(">").to_string()
84 } else {
85 ">".to_string()
86 };
87 format!("{} {}", prefix, msg.as_ref())
88 }
89
90 pub fn format_warn(&self, msg: impl AsRef<str>) -> String {
92 use console::Style;
93 let prefix = if self.color {
94 Style::new().yellow().bold().apply_to("!").to_string()
95 } else {
96 "!".to_string()
97 };
98 format!("{} {}", prefix, msg.as_ref())
99 }
100
101 pub fn format_error(&self, msg: impl AsRef<str>) -> String {
103 use console::Style;
104 let prefix = if self.color {
105 Style::new().red().bold().apply_to("ERROR").to_string()
106 } else {
107 "ERROR".to_string()
108 };
109 format!("{} {}", prefix, msg.as_ref())
110 }
111
112 pub fn info(&self, msg: impl AsRef<str>) {
116 eprintln!("{}", self.format_info(msg));
117 }
118
119 pub fn warn(&self, msg: impl AsRef<str>) {
121 eprintln!("{}", self.format_warn(msg));
122 }
123
124 pub fn error(&self, msg: impl AsRef<str>) {
126 eprintln!("{}", self.format_error(msg));
127 }
128
129 pub fn is_verbose(&self) -> bool {
131 matches!(self.verbosity, Verbosity::Verbose | Verbosity::Trace)
132 }
133
134 pub fn is_trace(&self) -> bool {
136 matches!(self.verbosity, Verbosity::Trace)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn args_with(verbose: u8, color: ColorMode, format: &str) -> OutputArgs {
145 OutputArgs {
146 verbose,
147 color,
148 format: format.to_string(),
149 }
150 }
151
152 #[test]
155 fn color_always_forces_color() {
156 let out = CliOutput::from_args(&args_with(0, ColorMode::Always, "tree"));
157 assert!(out.color);
158 }
159
160 #[test]
161 fn color_never_forces_no_color() {
162 let out = CliOutput::from_args(&args_with(0, ColorMode::Never, "tree"));
163 assert!(!out.color);
164 }
165
166 #[test]
167 fn color_auto_respects_no_color_env() {
168 std::env::set_var("NO_COLOR", "1");
171 let out = CliOutput::from_args(&args_with(0, ColorMode::Auto, "tree"));
172 std::env::remove_var("NO_COLOR");
173 assert!(!out.color);
174 }
175
176 #[test]
179 fn verbosity_zero_is_default() {
180 let out = CliOutput::from_args(&args_with(0, ColorMode::Never, "tree"));
181 assert_eq!(out.verbosity, Verbosity::Default);
182 assert!(!out.is_verbose());
183 }
184
185 #[test]
186 fn verbosity_one_is_verbose() {
187 let out = CliOutput::from_args(&args_with(1, ColorMode::Never, "tree"));
188 assert_eq!(out.verbosity, Verbosity::Verbose);
189 assert!(out.is_verbose());
190 assert!(!out.is_trace());
191 }
192
193 #[test]
194 fn verbosity_two_is_trace() {
195 let out = CliOutput::from_args(&args_with(2, ColorMode::Never, "tree"));
196 assert_eq!(out.verbosity, Verbosity::Trace);
197 assert!(out.is_verbose());
198 assert!(out.is_trace());
199 }
200
201 fn no_color_out() -> CliOutput {
204 CliOutput::from_args(&args_with(0, ColorMode::Never, "tree"))
205 }
206
207 #[test]
208 fn info_prefix_no_color() {
209 let out = no_color_out();
210 assert_eq!(out.format_info("hello"), "> hello");
211 }
212
213 #[test]
214 fn warn_prefix_no_color() {
215 let out = no_color_out();
216 assert_eq!(out.format_warn("careful"), "! careful");
217 }
218
219 #[test]
220 fn error_prefix_no_color() {
221 let out = no_color_out();
222 assert_eq!(out.format_error("boom"), "ERROR boom");
223 }
224
225 #[test]
226 fn format_field_passed_through() {
227 let out = CliOutput::from_args(&args_with(0, ColorMode::Never, "json"));
228 assert_eq!(out.format, "json");
229 }
230}