Skip to main content

voirs_cli/
output.rs

1//! Output formatting utilities.
2
3use console::{Color, Style, Term};
4use serde_json;
5use std::io::Write;
6
7/// Output formatter for different display modes
8pub struct OutputFormatter {
9    colored: bool,
10    json_mode: bool,
11    term: Term,
12}
13
14impl OutputFormatter {
15    /// Create a new output formatter
16    pub fn new(colored: bool, json_mode: bool) -> Self {
17        Self {
18            colored,
19            json_mode,
20            term: Term::stdout(),
21        }
22    }
23
24    /// Print a success message
25    pub fn success(&self, msg: &str) {
26        if self.json_mode {
27            self.print_json("success", msg, None);
28        } else if self.colored {
29            let style = Style::new().fg(Color::Green).bold();
30            println!("{} {}", style.apply_to("✓"), msg);
31        } else {
32            println!("✓ {}", msg);
33        }
34    }
35
36    /// Print an error message
37    pub fn error(&self, msg: &str) {
38        if self.json_mode {
39            self.print_json("error", msg, None);
40        } else if self.colored {
41            let style = Style::new().fg(Color::Red).bold();
42            eprintln!("{} {}", style.apply_to("✗"), msg);
43        } else {
44            eprintln!("✗ {}", msg);
45        }
46    }
47
48    /// Print a warning message
49    pub fn warning(&self, msg: &str) {
50        if self.json_mode {
51            self.print_json("warning", msg, None);
52        } else if self.colored {
53            let style = Style::new().fg(Color::Yellow).bold();
54            println!("{} {}", style.apply_to("⚠"), msg);
55        } else {
56            println!("⚠ {}", msg);
57        }
58    }
59
60    /// Print an info message
61    pub fn info(&self, msg: &str) {
62        if self.json_mode {
63            self.print_json("info", msg, None);
64        } else if self.colored {
65            let style = Style::new().fg(Color::Blue);
66            println!("{} {}", style.apply_to("ℹ"), msg);
67        } else {
68            println!("ℹ {}", msg);
69        }
70    }
71
72    /// Print a header
73    pub fn header(&self, title: &str) {
74        if self.json_mode {
75            self.print_json("header", title, None);
76        } else if self.colored {
77            let style = Style::new().fg(Color::Cyan).bold().underlined();
78            println!("{}", style.apply_to(title));
79        } else {
80            println!("{}", title);
81            println!("{}", "=".repeat(title.len()));
82        }
83    }
84
85    /// Print a list item
86    pub fn list_item(&self, item: &str, indent: usize) {
87        let spaces = " ".repeat(indent * 2);
88        if self.json_mode {
89            self.print_json("list_item", item, Some(&format!("indent:{}", indent)));
90        } else if self.colored {
91            let style = Style::new().fg(Color::White);
92            println!("{}• {}", spaces, style.apply_to(item));
93        } else {
94            println!("{}• {}", spaces, item);
95        }
96    }
97
98    /// Print key-value pair
99    pub fn key_value(&self, key: &str, value: &str) {
100        if self.json_mode {
101            let data = serde_json::json!({ key: value });
102            println!("{}", serde_json::to_string(&data).unwrap_or_default());
103        } else if self.colored {
104            let key_style = Style::new().fg(Color::Cyan).bold();
105            let value_style = Style::new().fg(Color::White);
106            println!(
107                "{}: {}",
108                key_style.apply_to(key),
109                value_style.apply_to(value)
110            );
111        } else {
112            println!("{}: {}", key, value);
113        }
114    }
115
116    /// Print a table header
117    pub fn table_header(&self, headers: &[&str]) {
118        if self.json_mode {
119            self.print_json("table_header", &headers.join(","), None);
120        } else if self.colored {
121            let style = Style::new().fg(Color::Cyan).bold();
122            let header_line = headers
123                .iter()
124                .map(|h| format!("{:20}", h))
125                .collect::<Vec<_>>()
126                .join(" ");
127            println!("{}", style.apply_to(&header_line));
128            println!("{}", style.apply_to(&"-".repeat(header_line.len())));
129        } else {
130            let header_line = headers
131                .iter()
132                .map(|h| format!("{:20}", h))
133                .collect::<Vec<_>>()
134                .join(" ");
135            println!("{}", header_line);
136            println!("{}", "-".repeat(header_line.len()));
137        }
138    }
139
140    /// Print a table row
141    pub fn table_row(&self, cells: &[&str]) {
142        if self.json_mode {
143            let data = serde_json::json!(cells);
144            println!("{}", serde_json::to_string(&data).unwrap_or_default());
145        } else {
146            let row = cells
147                .iter()
148                .map(|c| format!("{:20}", c))
149                .collect::<Vec<_>>()
150                .join(" ");
151            println!("{}", row);
152        }
153    }
154
155    /// Print command output
156    pub fn command_output(&self, cmd: &str, output: &str) {
157        if self.json_mode {
158            let data = serde_json::json!({
159                "command": cmd,
160                "output": output
161            });
162            println!("{}", serde_json::to_string(&data).unwrap_or_default());
163        } else if self.colored {
164            let cmd_style = Style::new().fg(Color::Green).bold();
165            println!("{} {}", cmd_style.apply_to("$"), cmd);
166            if !output.is_empty() {
167                println!("{}", output);
168            }
169        } else {
170            println!("$ {}", cmd);
171            if !output.is_empty() {
172                println!("{}", output);
173            }
174        }
175    }
176
177    /// Print structured data as JSON or formatted text
178    pub fn structured_data(&self, data: &serde_json::Value) {
179        if self.json_mode {
180            println!("{}", serde_json::to_string_pretty(data).unwrap_or_default());
181        } else {
182            self.print_value(data, 0);
183        }
184    }
185
186    /// Internal method to print JSON messages
187    fn print_json(&self, level: &str, message: &str, extra: Option<&str>) {
188        let mut data = serde_json::json!({
189            "level": level,
190            "message": message,
191            "timestamp": chrono::Utc::now().to_rfc3339()
192        });
193
194        if let Some(extra_data) = extra {
195            data["extra"] = serde_json::Value::String(extra_data.to_string());
196        }
197
198        println!("{}", serde_json::to_string(&data).unwrap_or_default());
199    }
200
201    /// Recursively print JSON values in a readable format
202    fn print_value(&self, value: &serde_json::Value, indent: usize) {
203        let spaces = "  ".repeat(indent);
204
205        match value {
206            serde_json::Value::Object(map) => {
207                for (key, val) in map {
208                    if self.colored {
209                        let key_style = Style::new().fg(Color::Cyan);
210                        print!("{}{}: ", spaces, key_style.apply_to(key));
211                    } else {
212                        print!("{}{}: ", spaces, key);
213                    }
214
215                    match val {
216                        serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
217                            println!();
218                            self.print_value(val, indent + 1);
219                        }
220                        _ => {
221                            self.print_value(val, 0);
222                        }
223                    }
224                }
225            }
226            serde_json::Value::Array(arr) => {
227                for (i, val) in arr.iter().enumerate() {
228                    print!("{}[{}] ", spaces, i);
229                    self.print_value(val, indent);
230                }
231            }
232            serde_json::Value::String(s) => {
233                if self.colored {
234                    let style = Style::new().fg(Color::Green);
235                    println!("{}", style.apply_to(&format!("\"{}\"", s)));
236                } else {
237                    println!("\"{}\"", s);
238                }
239            }
240            serde_json::Value::Number(n) => {
241                if self.colored {
242                    let style = Style::new().fg(Color::Yellow);
243                    println!("{}", style.apply_to(&n.to_string()));
244                } else {
245                    println!("{}", n);
246                }
247            }
248            serde_json::Value::Bool(b) => {
249                if self.colored {
250                    let style = Style::new().fg(Color::Magenta);
251                    println!("{}", style.apply_to(&b.to_string()));
252                } else {
253                    println!("{}", b);
254                }
255            }
256            serde_json::Value::Null => {
257                if self.colored {
258                    let style = Style::new().fg(Color::Black).italic();
259                    println!("{}", style.apply_to("null"));
260                } else {
261                    println!("null");
262                }
263            }
264        }
265    }
266}
267
268/// Global formatter instance
269use std::sync::OnceLock;
270static FORMATTER: OnceLock<OutputFormatter> = OnceLock::new();
271
272/// Initialize global formatter
273pub fn init_formatter(colored: bool, json_mode: bool) {
274    let _ = FORMATTER.set(OutputFormatter::new(colored, json_mode));
275}
276
277/// Get global formatter
278pub fn get_formatter() -> &'static OutputFormatter {
279    FORMATTER.get_or_init(|| OutputFormatter::new(true, false))
280}
281
282/// Convenience macros for formatted output
283#[macro_export]
284macro_rules! success {
285    ($($arg:tt)*) => {
286        $crate::output::get_formatter().success(&format!($($arg)*))
287    };
288}
289
290#[macro_export]
291macro_rules! error {
292    ($($arg:tt)*) => {
293        $crate::output::get_formatter().error(&format!($($arg)*))
294    };
295}
296
297#[macro_export]
298macro_rules! warning {
299    ($($arg:tt)*) => {
300        $crate::output::get_formatter().warning(&format!($($arg)*))
301    };
302}
303
304#[macro_export]
305macro_rules! info {
306    ($($arg:tt)*) => {
307        $crate::output::get_formatter().info(&format!($($arg)*))
308    };
309}