ricecoder_cli/
output.rs

1// Output formatting and styling
2// Adapted from automation/src/utils/colors.rs
3
4use colored::Colorize;
5
6/// Output styling configuration
7pub struct OutputStyle {
8    pub use_colors: bool,
9}
10
11impl Default for OutputStyle {
12    fn default() -> Self {
13        Self {
14            use_colors: atty::is(atty::Stream::Stdout),
15        }
16    }
17}
18
19impl OutputStyle {
20    /// Format success message
21    pub fn success(&self, msg: &str) -> String {
22        if self.use_colors {
23            format!("{} {}", "✓".green().bold(), msg)
24        } else {
25            format!("✓ {}", msg)
26        }
27    }
28
29    /// Format error message
30    pub fn error(&self, msg: &str) -> String {
31        if self.use_colors {
32            format!("{} {}", "✗".red().bold(), msg)
33        } else {
34            format!("✗ {}", msg)
35        }
36    }
37
38    /// Format warning message
39    pub fn warning(&self, msg: &str) -> String {
40        if self.use_colors {
41            format!("{} {}", "⚠".yellow(), msg)
42        } else {
43            format!("⚠ {}", msg)
44        }
45    }
46
47    /// Format info message
48    pub fn info(&self, msg: &str) -> String {
49        if self.use_colors {
50            format!("{} {}", "ℹ".blue(), msg)
51        } else {
52            format!("ℹ {}", msg)
53        }
54    }
55
56    /// Format code block
57    pub fn code(&self, code: &str) -> String {
58        if self.use_colors {
59            code.cyan().to_string()
60        } else {
61            code.to_string()
62        }
63    }
64
65    /// Format code block with language-specific syntax highlighting
66    pub fn code_block(&self, code: &str, language: &str) -> String {
67        // For now, just apply basic syntax highlighting
68        // In a full implementation, this would use syntect for proper highlighting
69        if self.use_colors {
70            match language {
71                "rust" | "rs" => code.cyan().to_string(),
72                "python" | "py" => code.yellow().to_string(),
73                "javascript" | "js" | "typescript" | "ts" => code.yellow().to_string(),
74                "json" => code.cyan().to_string(),
75                "yaml" | "yml" => code.cyan().to_string(),
76                _ => code.to_string(),
77            }
78        } else {
79            code.to_string()
80        }
81    }
82
83    /// Format prompt
84    pub fn prompt(&self, prompt: &str) -> String {
85        if self.use_colors {
86            format!("{} ", prompt.magenta().bold())
87        } else {
88            format!("{} ", prompt)
89        }
90    }
91
92    /// Format header
93    pub fn header(&self, title: &str) -> String {
94        if self.use_colors {
95            title.bold().to_string()
96        } else {
97            title.to_string()
98        }
99    }
100
101    /// Format error with suggestions
102    pub fn error_with_suggestion(&self, error: &str, suggestion: &str) -> String {
103        let error_msg = self.error(error);
104        let suggestion_msg = self.info(&format!("Suggestion: {}", suggestion));
105        format!("{}\n{}", error_msg, suggestion_msg)
106    }
107
108    /// Format error with context
109    pub fn error_with_context(&self, error: &str, context: &str) -> String {
110        let error_msg = self.error(error);
111        let context_msg = self.info(&format!("Context: {}", context));
112        format!("{}\n{}", error_msg, context_msg)
113    }
114
115    /// Format verbose error with details
116    pub fn error_verbose(&self, error: &str, details: &str) -> String {
117        let error_msg = self.error(error);
118        let details_msg = format!("\n{}", details);
119        format!("{}{}", error_msg, details_msg)
120    }
121
122    /// Format error with multiple suggestions
123    pub fn error_with_suggestions(&self, error: &str, suggestions: &[&str]) -> String {
124        let mut output = self.error(error);
125        if !suggestions.is_empty() {
126            output.push_str("\n\n💡 Suggestions:");
127            for (i, suggestion) in suggestions.iter().enumerate() {
128                output.push_str(&format!("\n  {}. {}", i + 1, suggestion));
129            }
130        }
131        output
132    }
133
134    /// Format error with documentation link
135    pub fn error_with_docs(&self, error: &str, doc_url: &str) -> String {
136        format!(
137            "{}\n\n📖 Learn more: {}",
138            self.error(error),
139            doc_url
140        )
141    }
142
143    /// Format a section header
144    pub fn section(&self, title: &str) -> String {
145        if self.use_colors {
146            format!(
147                "\n{}\n{}",
148                title.bold().underline(),
149                "─".repeat(title.len())
150            )
151        } else {
152            format!("\n{}\n{}", title, "─".repeat(title.len()))
153        }
154    }
155
156    /// Format a list item
157    pub fn list_item(&self, item: &str) -> String {
158        format!("  • {}", item)
159    }
160
161    /// Format a numbered list item
162    pub fn numbered_item(&self, number: usize, item: &str) -> String {
163        format!("  {}. {}", number, item)
164    }
165
166    /// Format a key-value pair
167    pub fn key_value(&self, key: &str, value: &str) -> String {
168        if self.use_colors {
169            format!("  {}: {}", key.bold(), value)
170        } else {
171            format!("  {}: {}", key, value)
172        }
173    }
174
175    /// Format a tip/hint
176    pub fn tip(&self, tip: &str) -> String {
177        if self.use_colors {
178            format!("{} {}", "💡".yellow(), tip)
179        } else {
180            format!("💡 {}", tip)
181        }
182    }
183
184    /// Format a link
185    pub fn link(&self, text: &str, url: &str) -> String {
186        if self.use_colors {
187            format!("{} ({})", text.cyan(), url.cyan())
188        } else {
189            format!("{} ({})", text, url)
190        }
191    }
192}
193
194/// Print formatted output
195pub fn print_success(msg: &str) {
196    let style = OutputStyle::default();
197    println!("{}", style.success(msg));
198}
199
200pub fn print_error(msg: &str) {
201    let style = OutputStyle::default();
202    eprintln!("{}", style.error(msg));
203}
204
205pub fn print_warning(msg: &str) {
206    let style = OutputStyle::default();
207    println!("{}", style.warning(msg));
208}
209
210pub fn print_info(msg: &str) {
211    let style = OutputStyle::default();
212    println!("{}", style.info(msg));
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_output_style_without_colors() {
221        let style = OutputStyle { use_colors: false };
222        assert_eq!(style.success("test"), "✓ test");
223        assert_eq!(style.error("test"), "✗ test");
224        assert_eq!(style.warning("test"), "⚠ test");
225        assert_eq!(style.info("test"), "ℹ test");
226    }
227
228    #[test]
229    fn test_output_formatting_idempotence() {
230        let style = OutputStyle { use_colors: false };
231        let msg = "test message";
232        let formatted1 = style.success(msg);
233        let formatted2 = style.success(msg);
234        assert_eq!(formatted1, formatted2);
235    }
236
237    #[test]
238    fn test_error_with_suggestion() {
239        let style = OutputStyle { use_colors: false };
240        let result = style.error_with_suggestion("File not found", "Check the file path");
241        assert!(result.contains("✗ File not found"));
242        assert!(result.contains("Suggestion: Check the file path"));
243    }
244
245    #[test]
246    fn test_error_with_context() {
247        let style = OutputStyle { use_colors: false };
248        let result = style.error_with_context("Invalid config", "in ~/.ricecoder/config.toml");
249        assert!(result.contains("✗ Invalid config"));
250        assert!(result.contains("Context: in ~/.ricecoder/config.toml"));
251    }
252
253    #[test]
254    fn test_section_formatting() {
255        let style = OutputStyle { use_colors: false };
256        let result = style.section("Configuration");
257        assert!(result.contains("Configuration"));
258        assert!(result.contains("─"));
259    }
260
261    #[test]
262    fn test_list_item_formatting() {
263        let style = OutputStyle { use_colors: false };
264        let result = style.list_item("First item");
265        assert!(result.contains("•"));
266        assert!(result.contains("First item"));
267    }
268
269    #[test]
270    fn test_key_value_formatting() {
271        let style = OutputStyle { use_colors: false };
272        let result = style.key_value("key", "value");
273        assert!(result.contains("key"));
274        assert!(result.contains("value"));
275    }
276
277    #[test]
278    fn test_error_with_suggestions() {
279        let style = OutputStyle { use_colors: false };
280        let suggestions = vec!["Try this", "Or that"];
281        let result = style.error_with_suggestions("Something failed", &suggestions);
282        assert!(result.contains("✗ Something failed"));
283        assert!(result.contains("Suggestions:"));
284        assert!(result.contains("1. Try this"));
285        assert!(result.contains("2. Or that"));
286    }
287
288    #[test]
289    fn test_error_with_docs() {
290        let style = OutputStyle { use_colors: false };
291        let result = style.error_with_docs("File not found", "https://docs.example.com");
292        assert!(result.contains("✗ File not found"));
293        assert!(result.contains("https://docs.example.com"));
294    }
295
296    #[test]
297    fn test_numbered_item_formatting() {
298        let style = OutputStyle { use_colors: false };
299        let result = style.numbered_item(1, "First item");
300        assert!(result.contains("1. First item"));
301    }
302
303    #[test]
304    fn test_tip_formatting() {
305        let style = OutputStyle { use_colors: false };
306        let result = style.tip("This is a helpful tip");
307        assert!(result.contains("💡"));
308        assert!(result.contains("This is a helpful tip"));
309    }
310
311    #[test]
312    fn test_link_formatting() {
313        let style = OutputStyle { use_colors: false };
314        let result = style.link("Documentation", "https://docs.example.com");
315        assert!(result.contains("Documentation"));
316        assert!(result.contains("https://docs.example.com"));
317    }
318}