ares/cli/
output.rs

1//! Colored output helpers for CLI
2//!
3//! Provides consistent, colored terminal output for the A.R.E.S CLI.
4
5use owo_colors::OwoColorize;
6use std::io::{self, Write};
7
8/// Output style configuration
9pub struct Output {
10    /// Whether to use colored output
11    pub colored: bool,
12}
13
14impl Default for Output {
15    fn default() -> Self {
16        Self::new()
17    }
18}
19
20impl Output {
21    /// Create a new output helper with colors enabled
22    pub fn new() -> Self {
23        Self { colored: true }
24    }
25
26    /// Create a new output helper with colors disabled
27    pub fn no_color() -> Self {
28        Self { colored: false }
29    }
30
31    /// Print the A.R.E.S banner
32    pub fn banner(&self) {
33        if self.colored {
34            println!(
35                r#"
36   {}
37   {}
38   {}
39   {}
40   {}
41"#,
42                "    _    ____  _____ ____  ".bright_cyan().bold(),
43                "   / \\  |  _ \\| ____/ ___| ".bright_cyan().bold(),
44                "  / _ \\ | |_) |  _| \\___ \\ ".cyan().bold(),
45                " / ___ \\|  _ <| |___ ___) |".blue().bold(),
46                "/_/   \\_\\_| \\_\\_____|____/ ".blue().bold(),
47            );
48            println!(
49                "   {} {}\n",
50                "Agentic Retrieval Enhanced Server".bright_white().bold(),
51                format!("v{}", env!("CARGO_PKG_VERSION")).dimmed()
52            );
53        } else {
54            println!(
55                r#"
56    _    ____  _____ ____
57   / \  |  _ \| ____/ ___|
58  / _ \ | |_) |  _| \___ \
59 / ___ \|  _ <| |___ ___) |
60/_/   \_\_| \_\_____|____/
61
62   Agentic Retrieval Enhanced Server v{}
63"#,
64                env!("CARGO_PKG_VERSION")
65            );
66        }
67    }
68
69    /// Print a success message with a checkmark
70    pub fn success(&self, message: &str) {
71        if self.colored {
72            println!("  {} {}", "✓".green().bold(), message.green());
73        } else {
74            println!("  [OK] {}", message);
75        }
76    }
77
78    /// Print an info message
79    pub fn info(&self, message: &str) {
80        if self.colored {
81            println!("  {} {}", "•".blue(), message);
82        } else {
83            println!("  [INFO] {}", message);
84        }
85    }
86
87    /// Print a warning message
88    pub fn warning(&self, message: &str) {
89        if self.colored {
90            println!("  {} {}", "⚠".yellow().bold(), message.yellow());
91        } else {
92            println!("  [WARN] {}", message);
93        }
94    }
95
96    /// Print an error message
97    pub fn error(&self, message: &str) {
98        if self.colored {
99            eprintln!("  {} {}", "✗".red().bold(), message.red());
100        } else {
101            eprintln!("  [ERROR] {}", message);
102        }
103    }
104
105    /// Print a step message (for multi-step operations)
106    pub fn step(&self, step_num: u32, total: u32, message: &str) {
107        if self.colored {
108            println!(
109                "  {} {}",
110                format!("[{}/{}]", step_num, total).dimmed(),
111                message.bright_white()
112            );
113        } else {
114            println!("  [{}/{}] {}", step_num, total, message);
115        }
116    }
117
118    /// Print a file creation message
119    pub fn created(&self, file_type: &str, path: &str) {
120        if self.colored {
121            println!(
122                "  {} {} {}",
123                "✓".green().bold(),
124                file_type.dimmed(),
125                path.bright_white()
126            );
127        } else {
128            println!("  [CREATED] {} {}", file_type, path);
129        }
130    }
131
132    /// Print a file skipped message
133    pub fn skipped(&self, path: &str, reason: &str) {
134        if self.colored {
135            println!(
136                "  {} {} {}",
137                "○".yellow(),
138                path.dimmed(),
139                format!("({})", reason).yellow()
140            );
141        } else {
142            println!("  [SKIPPED] {} ({})", path, reason);
143        }
144    }
145
146    /// Print a directory creation message
147    pub fn created_dir(&self, path: &str) {
148        if self.colored {
149            println!(
150                "  {} {} {}",
151                "✓".green().bold(),
152                "directory".dimmed(),
153                path.bright_white()
154            );
155        } else {
156            println!("  [CREATED] directory {}", path);
157        }
158    }
159
160    /// Print a header for a section
161    pub fn header(&self, title: &str) {
162        if self.colored {
163            println!("\n  {}", title.bright_white().bold().underline());
164        } else {
165            println!("\n  === {} ===", title);
166        }
167    }
168
169    /// Print a subheader
170    pub fn subheader(&self, title: &str) {
171        if self.colored {
172            println!("\n  {}", title.cyan().bold());
173        } else {
174            println!("\n  --- {} ---", title);
175        }
176    }
177
178    /// Print a key-value pair
179    pub fn kv(&self, key: &str, value: &str) {
180        if self.colored {
181            println!("    {}: {}", key.dimmed(), value.bright_white());
182        } else {
183            println!("    {}: {}", key, value);
184        }
185    }
186
187    /// Print a list item
188    pub fn list_item(&self, item: &str) {
189        if self.colored {
190            println!("    {} {}", "•".blue(), item);
191        } else {
192            println!("    - {}", item);
193        }
194    }
195
196    /// Print a hint/tip message
197    pub fn hint(&self, message: &str) {
198        if self.colored {
199            println!("\n  {} {}", "💡".dimmed(), message.dimmed().italic());
200        } else {
201            println!("\n  [TIP] {}", message);
202        }
203    }
204
205    /// Print a command suggestion
206    pub fn command(&self, cmd: &str) {
207        if self.colored {
208            println!("     {}", format!("$ {}", cmd).bright_cyan());
209        } else {
210            println!("     $ {}", cmd);
211        }
212    }
213
214    /// Print completion message with next steps
215    pub fn complete(&self, message: &str) {
216        if self.colored {
217            println!("\n  {} {}", "🚀".green(), message.bright_green().bold());
218        } else {
219            println!("\n  [DONE] {}", message);
220        }
221    }
222
223    /// Prompt for confirmation (returns true if user confirms)
224    pub fn confirm(&self, message: &str) -> bool {
225        if self.colored {
226            print!(
227                "  {} {} [y/N]: ",
228                "?".bright_yellow().bold(),
229                message.bright_white()
230            );
231        } else {
232            print!("  [?] {} [y/N]: ", message);
233        }
234
235        io::stdout().flush().ok();
236
237        let mut input = String::new();
238        if io::stdin().read_line(&mut input).is_ok() {
239            let input = input.trim().to_lowercase();
240            input == "y" || input == "yes"
241        } else {
242            false
243        }
244    }
245
246    /// Print a table header row
247    pub fn table_header(&self, columns: &[&str]) {
248        if self.colored {
249            let header: String = columns
250                .iter()
251                .map(|c| format!("{:<15}", c))
252                .collect::<Vec<_>>()
253                .join(" ");
254            println!("    {}", header.bright_white().bold());
255            println!("    {}", "─".repeat(columns.len() * 16).dimmed());
256        } else {
257            let header: String = columns
258                .iter()
259                .map(|c| format!("{:<15}", c))
260                .collect::<Vec<_>>()
261                .join(" ");
262            println!("    {}", header);
263            println!("    {}", "-".repeat(columns.len() * 16));
264        }
265    }
266
267    /// Print a table row
268    pub fn table_row(&self, values: &[&str]) {
269        let row: String = values
270            .iter()
271            .map(|v| format!("{:<15}", v))
272            .collect::<Vec<_>>()
273            .join(" ");
274        println!("    {}", row);
275    }
276
277    /// Print newline
278    pub fn newline(&self) {
279        println!();
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_output_new() {
289        let output = Output::new();
290        assert!(output.colored);
291    }
292
293    #[test]
294    fn test_output_no_color() {
295        let output = Output::no_color();
296        assert!(!output.colored);
297    }
298
299    #[test]
300    fn test_output_default() {
301        let output = Output::default();
302        assert!(output.colored);
303    }
304
305    #[test]
306    fn test_confirm_parsing() {
307        // Note: We can't easily test interactive confirm in unit tests,
308        // but we can verify the Output struct is created correctly
309        let output = Output::new();
310        assert!(output.colored);
311
312        let output_no_color = Output::no_color();
313        assert!(!output_no_color.colored);
314    }
315
316    #[test]
317    fn test_table_row_formatting() {
318        // Verify table row doesn't panic with various inputs
319        let output = Output::no_color();
320
321        // These should not panic
322        output.table_row(&["a", "b", "c"]);
323        output.table_row(&["long_value_here", "another", "third"]);
324        output.table_row(&[]);
325    }
326
327    #[test]
328    fn test_table_header_formatting() {
329        // Verify table header doesn't panic with various inputs
330        let output = Output::no_color();
331
332        // These should not panic
333        output.table_header(&["Name", "Model", "Tools"]);
334        output.table_header(&["Single"]);
335        output.table_header(&[]);
336    }
337
338    #[test]
339    fn test_output_methods_no_panic() {
340        // Smoke test - ensure none of the output methods panic
341        let output = Output::no_color();
342
343        output.success("test success");
344        output.info("test info");
345        output.warning("test warning");
346        output.error("test error");
347        output.step(1, 3, "step message");
348        output.created("file", "path/to/file");
349        output.skipped("path", "reason");
350        output.created_dir("some/dir");
351        output.header("Test Header");
352        output.subheader("Test Subheader");
353        output.kv("key", "value");
354        output.list_item("item");
355        output.hint("hint message");
356        output.command("some command");
357        output.complete("complete message");
358        output.newline();
359    }
360
361    #[test]
362    fn test_output_methods_colored_no_panic() {
363        // Smoke test for colored output
364        let output = Output::new();
365
366        output.success("test success");
367        output.info("test info");
368        output.warning("test warning");
369        output.error("test error");
370        output.step(1, 3, "step message");
371        output.created("file", "path/to/file");
372        output.skipped("path", "reason");
373        output.created_dir("some/dir");
374        output.header("Test Header");
375        output.subheader("Test Subheader");
376        output.kv("key", "value");
377        output.list_item("item");
378        output.hint("hint message");
379        output.command("some command");
380        output.complete("complete message");
381        output.newline();
382        output.banner();
383    }
384}