1use std::io::IsTerminal;
2
3use crate::config::load_config;
4use crate::models::{ColorMode, SymbolMode, Verbosity};
5
6#[derive(Clone, Copy)]
11pub struct Output {
12 pub colors: bool,
14 pub symbols: SymbolMode,
16 pub verbosity: Verbosity,
18 pub json: bool,
20}
21
22impl Output {
23 #[must_use]
28 pub fn new(verbose: bool, quiet: bool, json: bool) -> Self {
29 let config = load_config().unwrap_or_default();
30
31 let colors = match config.style.colors {
33 ColorMode::Always => std::env::var("NO_COLOR").is_err(),
34 ColorMode::Never => false,
35 ColorMode::Auto => {
36 std::env::var("NO_COLOR").is_err() && std::io::stdout().is_terminal()
37 }
38 };
39
40 let verbosity = if quiet {
43 Verbosity::Quiet
44 } else if verbose {
45 Verbosity::Verbose
46 } else {
47 config.style.verbosity
48 };
49
50 Self {
51 colors,
52 symbols: config.style.symbols,
53 verbosity,
54 json,
55 }
56 }
57
58 #[must_use]
63 pub fn use_color(&self) -> bool {
64 self.colors
65 }
66
67 #[must_use]
69 pub fn success_symbol(&self) -> &'static str {
70 match self.symbols {
71 SymbolMode::Unicode => "\u{2713}", SymbolMode::Ascii => "[OK]",
73 }
74 }
75
76 #[must_use]
78 pub fn error_symbol(&self) -> &'static str {
79 match self.symbols {
80 SymbolMode::Unicode => "\u{2717}", SymbolMode::Ascii => "[ERR]",
82 }
83 }
84
85 #[must_use]
87 pub fn info_symbol(&self) -> &'static str {
88 match self.symbols {
89 SymbolMode::Unicode => "\u{2192}", SymbolMode::Ascii => "->",
91 }
92 }
93
94 #[must_use]
96 pub fn warning_symbol(&self) -> &'static str {
97 match self.symbols {
98 SymbolMode::Unicode => "\u{26a0}", SymbolMode::Ascii => "[WARN]",
100 }
101 }
102
103 pub fn success(&self, message: &str) {
108 if matches!(self.verbosity, Verbosity::Quiet) {
109 return;
110 }
111
112 if self.json {
113 eprintln!(
114 r#"{{"status": "success", "message": "{}"}}"#,
115 escape_json_string(message)
116 );
117 } else if self.colors {
118 eprintln!("\x1b[32m{}\x1b[0m {}", self.success_symbol(), message);
119 } else {
120 eprintln!("{} {}", self.success_symbol(), message);
121 }
122 }
123
124 pub fn error(&self, error_type: &str, message: &str, cause: Option<&str>, help: Option<&str>) {
128 if self.json {
129 let mut obj = format!(
130 r#"{{"status": "error", "type": "{}", "message": "{}""#,
131 escape_json_string(error_type),
132 escape_json_string(message)
133 );
134 if let Some(c) = cause {
135 use std::fmt::Write;
136 let _ = write!(obj, r#", "cause": "{}""#, escape_json_string(c));
137 }
138 if let Some(h) = help {
139 use std::fmt::Write;
140 let _ = write!(obj, r#", "help": "{}""#, escape_json_string(h));
141 }
142 obj.push('}');
143 eprintln!("{obj}");
144 } else {
145 let symbol = self.error_symbol();
146 if self.colors {
147 eprintln!("\x1b[31m{symbol} {error_type}\x1b[0m: {message}");
148 } else {
149 eprintln!("{symbol} {error_type}: {message}");
150 }
151
152 if let Some(c) = cause {
153 eprintln!(" cause: {c}");
154 }
155 if let Some(h) = help {
156 eprintln!(" help: {h}");
157 }
158 }
159 }
160
161 pub fn warning(&self, message: &str) {
166 if matches!(self.verbosity, Verbosity::Quiet) {
167 return;
168 }
169
170 if self.json {
171 eprintln!(
172 r#"{{"level": "warning", "message": "{}"}}"#,
173 escape_json_string(message)
174 );
175 } else if self.colors {
176 eprintln!("\x1b[33m{}\x1b[0m {}", self.warning_symbol(), message);
177 } else {
178 eprintln!("{} {}", self.warning_symbol(), message);
179 }
180 }
181
182 pub fn debug(&self, message: &str) {
187 if !matches!(self.verbosity, Verbosity::Verbose) {
188 return;
189 }
190
191 if self.json {
192 eprintln!(
193 r#"{{"level": "debug", "message": "{}"}}"#,
194 escape_json_string(message)
195 );
196 } else if self.colors {
197 eprintln!("\x1b[90m[DEBUG] {message}\x1b[0m");
198 } else {
199 eprintln!("[DEBUG] {message}");
200 }
201 }
202
203 pub fn info(&self, message: &str) {
208 if matches!(self.verbosity, Verbosity::Quiet) {
209 return;
210 }
211
212 if self.json {
213 eprintln!(
214 r#"{{"level": "info", "message": "{}"}}"#,
215 escape_json_string(message)
216 );
217 } else {
218 eprintln!("{} {}", self.info_symbol(), message);
219 }
220 }
221 #[must_use]
225 pub fn style_command(&self, text: &str) -> String {
226 if self.colors {
227 format!("\x1b[36m{text}\x1b[0m")
228 } else {
229 format!("`{text}`")
230 }
231 }
232
233 #[must_use]
235 pub fn style_success(&self, text: &str) -> String {
236 if self.colors {
237 format!("\x1b[32m{text}\x1b[0m")
238 } else {
239 text.to_string()
240 }
241 }
242
243 #[must_use]
245 pub fn style_error(&self, text: &str) -> String {
246 if self.colors {
247 format!("\x1b[31m{text}\x1b[0m")
248 } else {
249 text.to_string()
250 }
251 }
252
253 #[must_use]
255 pub fn is_verbose(&self) -> bool {
256 matches!(self.verbosity, Verbosity::Verbose)
257 }
258
259 #[must_use]
261 pub fn is_quiet(&self) -> bool {
262 matches!(self.verbosity, Verbosity::Quiet)
263 }
264}
265
266impl Default for Output {
267 fn default() -> Self {
268 Self::new(false, false, false)
269 }
270}
271
272pub fn print_success(message: &str) {
274 Output::default().success(message);
275}
276
277pub fn print_error(error_type: &str, message: &str, cause: Option<&str>, help: Option<&str>) {
279 Output::default().error(error_type, message, cause, help);
280}
281
282fn escape_json_string(s: &str) -> String {
284 s.replace('\\', "\\\\")
285 .replace('"', "\\\"")
286 .replace('\n', "\\n")
287 .replace('\r', "\\r")
288 .replace('\t', "\\t")
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_output_default() {
297 let output = Output {
300 colors: false,
301 symbols: SymbolMode::Unicode,
302 verbosity: Verbosity::Normal,
303 json: false,
304 };
305 assert!(!output.json);
306 assert_eq!(output.verbosity, Verbosity::Normal);
307 }
308
309 #[test]
310 fn test_output_symbols_unicode() {
311 let output = Output {
312 colors: false,
313 symbols: SymbolMode::Unicode,
314 verbosity: Verbosity::Normal,
315 json: false,
316 };
317
318 assert_eq!(output.success_symbol(), "\u{2713}");
319 assert_eq!(output.error_symbol(), "\u{2717}");
320 assert_eq!(output.info_symbol(), "\u{2192}");
321 }
322
323 #[test]
324 fn test_output_symbols_ascii() {
325 let output = Output {
326 colors: false,
327 symbols: SymbolMode::Ascii,
328 verbosity: Verbosity::Normal,
329 json: false,
330 };
331
332 assert_eq!(output.success_symbol(), "[OK]");
333 assert_eq!(output.error_symbol(), "[ERR]");
334 assert_eq!(output.info_symbol(), "->");
335 }
336
337 #[test]
338 fn test_escape_json_string() {
339 assert_eq!(escape_json_string("hello"), "hello");
340 assert_eq!(escape_json_string("hello\"world"), "hello\\\"world");
341 assert_eq!(escape_json_string("line1\nline2"), "line1\\nline2");
342 assert_eq!(escape_json_string("back\\slash"), "back\\\\slash");
343 }
344
345 #[test]
346 fn test_verbosity_quiet() {
347 let output = Output::new(false, true, false);
348 assert_eq!(output.verbosity, Verbosity::Quiet);
349 }
350
351 #[test]
352 fn test_verbosity_verbose() {
353 let output = Output::new(true, false, false);
354 assert_eq!(output.verbosity, Verbosity::Verbose);
355 }
356
357 #[test]
358 fn test_quiet_overrides_verbose() {
359 let output = Output::new(true, true, false);
361 assert_eq!(output.verbosity, Verbosity::Quiet);
362 }
363}