1use colored::Colorize;
5
6pub 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 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 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 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 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 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 pub fn code_block(&self, code: &str, language: &str) -> String {
67 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 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 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 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 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 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 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 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 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 pub fn list_item(&self, item: &str) -> String {
158 format!(" • {}", item)
159 }
160
161 pub fn numbered_item(&self, number: usize, item: &str) -> String {
163 format!(" {}. {}", number, item)
164 }
165
166 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 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 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
194pub 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}