1use console::{Color, Style, Term};
4use serde_json;
5use std::io::Write;
6
7pub struct OutputFormatter {
9 colored: bool,
10 json_mode: bool,
11 term: Term,
12}
13
14impl OutputFormatter {
15 pub fn new(colored: bool, json_mode: bool) -> Self {
17 Self {
18 colored,
19 json_mode,
20 term: Term::stdout(),
21 }
22 }
23
24 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 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 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 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 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 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 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 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 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 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 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 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 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
268use std::sync::OnceLock;
270static FORMATTER: OnceLock<OutputFormatter> = OnceLock::new();
271
272pub fn init_formatter(colored: bool, json_mode: bool) {
274 let _ = FORMATTER.set(OutputFormatter::new(colored, json_mode));
275}
276
277pub fn get_formatter() -> &'static OutputFormatter {
279 FORMATTER.get_or_init(|| OutputFormatter::new(true, false))
280}
281
282#[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}