Skip to main content

batuta/
ansi_colors.rs

1//! ANSI Color Support
2//!
3//! Zero-dependency replacement for the `colored` crate.
4//! Provides ANSI escape code based text styling via trait extension.
5//!
6//! Created as part of DEP-REDUCE to eliminate external dependencies.
7
8use std::fmt;
9
10/// ANSI escape codes for terminal colors and styles
11pub mod codes {
12    // Reset
13    pub const RESET: &str = "\x1b[0m";
14
15    // Styles
16    pub const BOLD: &str = "\x1b[1m";
17    pub const DIMMED: &str = "\x1b[2m";
18
19    // Standard colors (foreground)
20    pub const RED: &str = "\x1b[31m";
21    pub const GREEN: &str = "\x1b[32m";
22    pub const YELLOW: &str = "\x1b[33m";
23    pub const BLUE: &str = "\x1b[34m";
24    pub const MAGENTA: &str = "\x1b[35m";
25    pub const CYAN: &str = "\x1b[36m";
26    pub const WHITE: &str = "\x1b[37m";
27
28    // Bright colors (foreground)
29    pub const BRIGHT_RED: &str = "\x1b[91m";
30    pub const BRIGHT_GREEN: &str = "\x1b[92m";
31    pub const BRIGHT_YELLOW: &str = "\x1b[93m";
32    pub const BRIGHT_BLUE: &str = "\x1b[94m";
33    pub const BRIGHT_MAGENTA: &str = "\x1b[95m";
34    pub const BRIGHT_CYAN: &str = "\x1b[96m";
35    pub const BRIGHT_WHITE: &str = "\x1b[97m";
36
37    // Background colors
38    pub const ON_RED: &str = "\x1b[41m";
39}
40
41/// A styled string that wraps content with ANSI codes
42#[derive(Clone)]
43pub struct StyledString {
44    content: String,
45    styles: Vec<&'static str>,
46}
47
48impl StyledString {
49    fn new(content: impl Into<String>) -> Self {
50        Self { content: content.into(), styles: Vec::new() }
51    }
52
53    fn with_style(mut self, style: &'static str) -> Self {
54        self.styles.push(style);
55        self
56    }
57
58    // Standard colors
59    pub fn red(self) -> Self {
60        self.with_style(codes::RED)
61    }
62    pub fn green(self) -> Self {
63        self.with_style(codes::GREEN)
64    }
65    pub fn yellow(self) -> Self {
66        self.with_style(codes::YELLOW)
67    }
68    pub fn blue(self) -> Self {
69        self.with_style(codes::BLUE)
70    }
71    pub fn magenta(self) -> Self {
72        self.with_style(codes::MAGENTA)
73    }
74    pub fn cyan(self) -> Self {
75        self.with_style(codes::CYAN)
76    }
77    pub fn white(self) -> Self {
78        self.with_style(codes::WHITE)
79    }
80
81    // Bright colors
82    pub fn bright_red(self) -> Self {
83        self.with_style(codes::BRIGHT_RED)
84    }
85    pub fn bright_green(self) -> Self {
86        self.with_style(codes::BRIGHT_GREEN)
87    }
88    pub fn bright_yellow(self) -> Self {
89        self.with_style(codes::BRIGHT_YELLOW)
90    }
91    pub fn bright_blue(self) -> Self {
92        self.with_style(codes::BRIGHT_BLUE)
93    }
94    pub fn bright_magenta(self) -> Self {
95        self.with_style(codes::BRIGHT_MAGENTA)
96    }
97    pub fn bright_cyan(self) -> Self {
98        self.with_style(codes::BRIGHT_CYAN)
99    }
100    pub fn bright_white(self) -> Self {
101        self.with_style(codes::BRIGHT_WHITE)
102    }
103
104    // Background colors
105    pub fn on_red(self) -> Self {
106        self.with_style(codes::ON_RED)
107    }
108
109    // Styles
110    pub fn bold(self) -> Self {
111        self.with_style(codes::BOLD)
112    }
113    pub fn dimmed(self) -> Self {
114        self.with_style(codes::DIMMED)
115    }
116}
117
118impl fmt::Display for StyledString {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        // Apply all styles
121        for style in &self.styles {
122            write!(f, "{}", style)?;
123        }
124        // Write content
125        write!(f, "{}", self.content)?;
126        // Reset
127        write!(f, "{}", codes::RESET)
128    }
129}
130
131/// Extension trait to add color methods to strings
132/// Uses `AsRef<str>` to work with both `&str`, `String`, and `&String` without moving
133pub trait Colorize {
134    fn to_styled(&self) -> StyledString;
135
136    // Standard colors
137    fn red(&self) -> StyledString {
138        self.to_styled().red()
139    }
140    fn green(&self) -> StyledString {
141        self.to_styled().green()
142    }
143    fn yellow(&self) -> StyledString {
144        self.to_styled().yellow()
145    }
146    fn blue(&self) -> StyledString {
147        self.to_styled().blue()
148    }
149    fn magenta(&self) -> StyledString {
150        self.to_styled().magenta()
151    }
152    fn cyan(&self) -> StyledString {
153        self.to_styled().cyan()
154    }
155    fn white(&self) -> StyledString {
156        self.to_styled().white()
157    }
158
159    // Bright colors
160    fn bright_red(&self) -> StyledString {
161        self.to_styled().bright_red()
162    }
163    fn bright_green(&self) -> StyledString {
164        self.to_styled().bright_green()
165    }
166    fn bright_yellow(&self) -> StyledString {
167        self.to_styled().bright_yellow()
168    }
169    fn bright_blue(&self) -> StyledString {
170        self.to_styled().bright_blue()
171    }
172    fn bright_magenta(&self) -> StyledString {
173        self.to_styled().bright_magenta()
174    }
175    fn bright_cyan(&self) -> StyledString {
176        self.to_styled().bright_cyan()
177    }
178    fn bright_white(&self) -> StyledString {
179        self.to_styled().bright_white()
180    }
181
182    // Background colors
183    fn on_red(&self) -> StyledString {
184        self.to_styled().on_red()
185    }
186
187    // Styles
188    fn bold(&self) -> StyledString {
189        self.to_styled().bold()
190    }
191    fn dimmed(&self) -> StyledString {
192        self.to_styled().dimmed()
193    }
194}
195
196impl Colorize for str {
197    fn to_styled(&self) -> StyledString {
198        StyledString::new(self)
199    }
200}
201
202impl Colorize for String {
203    fn to_styled(&self) -> StyledString {
204        StyledString::new(self.as_str())
205    }
206}
207
208impl Colorize for StyledString {
209    fn to_styled(&self) -> StyledString {
210        self.clone()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_basic_color() {
220        let s = "hello".red();
221        assert!(s.to_string().contains("\x1b[31m"));
222        assert!(s.to_string().contains("hello"));
223        assert!(s.to_string().contains("\x1b[0m"));
224    }
225
226    #[test]
227    fn test_chained_styles() {
228        let s = "hello".bright_cyan().bold();
229        let output = s.to_string();
230        assert!(output.contains("\x1b[96m")); // bright cyan
231        assert!(output.contains("\x1b[1m")); // bold
232        assert!(output.contains("hello"));
233        assert!(output.contains("\x1b[0m")); // reset
234    }
235
236    #[test]
237    fn test_string_owned() {
238        let s = String::from("world").green().dimmed();
239        assert!(s.to_string().contains("\x1b[32m"));
240        assert!(s.to_string().contains("\x1b[2m"));
241    }
242
243    #[test]
244    fn test_borrowed_string() {
245        let owned = String::from("borrowed");
246        let s = owned.yellow(); // Should not move owned
247        assert!(s.to_string().contains("\x1b[33m"));
248        // owned is still usable
249        assert_eq!(owned, "borrowed");
250    }
251
252    #[test]
253    fn test_all_standard_colors() {
254        assert!("x".red().to_string().contains(codes::RED));
255        assert!("x".green().to_string().contains(codes::GREEN));
256        assert!("x".yellow().to_string().contains(codes::YELLOW));
257        assert!("x".blue().to_string().contains(codes::BLUE));
258        assert!("x".magenta().to_string().contains(codes::MAGENTA));
259        assert!("x".cyan().to_string().contains(codes::CYAN));
260        assert!("x".white().to_string().contains(codes::WHITE));
261    }
262
263    #[test]
264    fn test_all_bright_colors() {
265        assert!("x".bright_red().to_string().contains(codes::BRIGHT_RED));
266        assert!("x".bright_green().to_string().contains(codes::BRIGHT_GREEN));
267        assert!("x".bright_yellow().to_string().contains(codes::BRIGHT_YELLOW));
268        assert!("x".bright_blue().to_string().contains(codes::BRIGHT_BLUE));
269        assert!("x".bright_magenta().to_string().contains(codes::BRIGHT_MAGENTA));
270        assert!("x".bright_cyan().to_string().contains(codes::BRIGHT_CYAN));
271        assert!("x".bright_white().to_string().contains(codes::BRIGHT_WHITE));
272    }
273
274    #[test]
275    fn test_background_color() {
276        let s = "test".on_red();
277        assert!(s.to_string().contains(codes::ON_RED));
278    }
279
280    #[test]
281    fn test_bold_style() {
282        let s = "bold text".bold();
283        assert!(s.to_string().contains(codes::BOLD));
284    }
285
286    #[test]
287    fn test_dimmed_style() {
288        let s = "dimmed text".dimmed();
289        assert!(s.to_string().contains(codes::DIMMED));
290    }
291
292    #[test]
293    fn test_styled_string_clone() {
294        let s1 = "text".red().bold();
295        let s2 = s1.clone();
296        assert_eq!(s1.to_string(), s2.to_string());
297    }
298
299    #[test]
300    fn test_colorize_for_styled_string() {
301        let s = "text".red();
302        let s2 = s.to_styled().bold();
303        assert!(s2.to_string().contains(codes::RED));
304        assert!(s2.to_string().contains(codes::BOLD));
305    }
306
307    #[test]
308    fn test_multiple_styles_order() {
309        let s = "text".red().bold().dimmed();
310        let output = s.to_string();
311        // All styles should be present
312        assert!(output.contains(codes::RED));
313        assert!(output.contains(codes::BOLD));
314        assert!(output.contains(codes::DIMMED));
315        // Content should be there
316        assert!(output.contains("text"));
317        // Reset at the end
318        assert!(output.ends_with(codes::RESET));
319    }
320
321    #[test]
322    fn test_empty_string() {
323        let s = "".red();
324        let output = s.to_string();
325        assert!(output.contains(codes::RED));
326        assert!(output.contains(codes::RESET));
327    }
328
329    #[test]
330    fn test_styled_string_display() {
331        let s = StyledString::new("hello").with_style(codes::GREEN);
332        let output = format!("{}", s);
333        assert!(output.contains(codes::GREEN));
334        assert!(output.contains("hello"));
335        assert!(output.contains(codes::RESET));
336    }
337
338    #[test]
339    fn test_styled_string_new() {
340        let s = StyledString::new("test content");
341        assert!(s.to_string().contains("test content"));
342    }
343
344    #[test]
345    fn test_styled_string_from_string() {
346        let s = StyledString::new(String::from("owned string"));
347        assert!(s.to_string().contains("owned string"));
348    }
349
350    #[test]
351    fn test_codes_module() {
352        // Verify escape codes are correct
353        assert_eq!(codes::RESET, "\x1b[0m");
354        assert_eq!(codes::BOLD, "\x1b[1m");
355        assert_eq!(codes::DIMMED, "\x1b[2m");
356        assert_eq!(codes::RED, "\x1b[31m");
357        assert_eq!(codes::GREEN, "\x1b[32m");
358        assert_eq!(codes::ON_RED, "\x1b[41m");
359    }
360
361    #[test]
362    fn test_styled_string_direct_color_methods() {
363        // Test color methods directly on StyledString (not through trait)
364        let s = StyledString::new("test");
365        assert!(s.red().to_string().contains(codes::RED));
366
367        let s = StyledString::new("test");
368        assert!(s.green().to_string().contains(codes::GREEN));
369
370        let s = StyledString::new("test");
371        assert!(s.yellow().to_string().contains(codes::YELLOW));
372
373        let s = StyledString::new("test");
374        assert!(s.blue().to_string().contains(codes::BLUE));
375    }
376
377    #[test]
378    fn test_styled_string_direct_bright_methods() {
379        let s = StyledString::new("test");
380        assert!(s.bright_red().to_string().contains(codes::BRIGHT_RED));
381
382        let s = StyledString::new("test");
383        assert!(s.bright_green().to_string().contains(codes::BRIGHT_GREEN));
384    }
385
386    #[test]
387    fn test_styled_string_magenta() {
388        let s = StyledString::new("test").magenta();
389        assert!(s.to_string().contains(codes::MAGENTA));
390    }
391
392    #[test]
393    fn test_styled_string_cyan() {
394        let s = StyledString::new("test").cyan();
395        assert!(s.to_string().contains(codes::CYAN));
396    }
397
398    #[test]
399    fn test_styled_string_white() {
400        let s = StyledString::new("test").white();
401        assert!(s.to_string().contains(codes::WHITE));
402    }
403
404    #[test]
405    fn test_styled_string_bright_yellow() {
406        let s = StyledString::new("test").bright_yellow();
407        assert!(s.to_string().contains(codes::BRIGHT_YELLOW));
408    }
409
410    #[test]
411    fn test_styled_string_bright_blue() {
412        let s = StyledString::new("test").bright_blue();
413        assert!(s.to_string().contains(codes::BRIGHT_BLUE));
414    }
415
416    #[test]
417    fn test_styled_string_bright_magenta() {
418        let s = StyledString::new("test").bright_magenta();
419        assert!(s.to_string().contains(codes::BRIGHT_MAGENTA));
420    }
421
422    #[test]
423    fn test_styled_string_bright_cyan() {
424        let s = StyledString::new("test").bright_cyan();
425        assert!(s.to_string().contains(codes::BRIGHT_CYAN));
426    }
427
428    #[test]
429    fn test_styled_string_bright_white() {
430        let s = StyledString::new("test").bright_white();
431        assert!(s.to_string().contains(codes::BRIGHT_WHITE));
432    }
433
434    #[test]
435    fn test_styled_string_on_red() {
436        let s = StyledString::new("test").on_red();
437        assert!(s.to_string().contains(codes::ON_RED));
438    }
439
440    #[test]
441    fn test_styled_string_bold() {
442        let s = StyledString::new("test").bold();
443        assert!(s.to_string().contains(codes::BOLD));
444    }
445
446    #[test]
447    fn test_styled_string_dimmed() {
448        let s = StyledString::new("test").dimmed();
449        assert!(s.to_string().contains(codes::DIMMED));
450    }
451}