1use owo_colors::OwoColorize;
6use std::io::{self, Write};
7
8pub struct Output {
10 pub colored: bool,
12}
13
14impl Default for Output {
15 fn default() -> Self {
16 Self::new()
17 }
18}
19
20impl Output {
21 pub fn new() -> Self {
23 Self { colored: true }
24 }
25
26 pub fn no_color() -> Self {
28 Self { colored: false }
29 }
30
31 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 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 pub fn info(&self, message: &str) {
80 if self.colored {
81 println!(" {} {}", "•".blue(), message);
82 } else {
83 println!(" [INFO] {}", message);
84 }
85 }
86
87 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 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 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 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 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 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 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 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 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 pub fn list_item(&self, item: &str) {
189 if self.colored {
190 println!(" {} {}", "•".blue(), item);
191 } else {
192 println!(" - {}", item);
193 }
194 }
195
196 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 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 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 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 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 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 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 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 let output = Output::no_color();
320
321 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 let output = Output::no_color();
331
332 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 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 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}