pretty_console/
lib.rs

1use std::io::Write;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum Color {
5    Named(u8),
6    RGB(u8, u8, u8),
7}
8
9impl Color {
10    pub const BLACK: Color = Color::Named(0);
11    pub const RED: Color = Color::Named(1);
12    pub const GREEN: Color = Color::Named(2);
13    pub const YELLOW: Color = Color::Named(3);
14    pub const BLUE: Color = Color::Named(4);
15    pub const MAGENTA: Color = Color::Named(5);
16    pub const CYAN: Color = Color::Named(6);
17    pub const WHITE: Color = Color::Named(7);
18    pub const BRIGHT_BLACK: Color = Color::Named(8);
19    pub const BRIGHT_RED: Color = Color::Named(9);
20    pub const BRIGHT_GREEN: Color = Color::Named(10);
21    pub const BRIGHT_YELLOW: Color = Color::Named(11);
22    pub const BRIGHT_BLUE: Color = Color::Named(12);
23    pub const BRIGHT_MAGENTA: Color = Color::Named(13);
24    pub const BRIGHT_CYAN: Color = Color::Named(14);
25    pub const BRIGHT_WHITE: Color = Color::Named(15);
26
27    fn to_fg_code(&self) -> String {
28        match self {
29            Color::Named(n) => format!("38;5;{}", n),
30            Color::RGB(r, g, b) => format!("38;2;{};{};{}", r, g, b),
31        }
32    }
33
34    fn to_bg_code(&self) -> String {
35        match self {
36            Color::Named(n) => format!("48;5;{}", n),
37            Color::RGB(r, g, b) => format!("48;2;{};{};{}", r, g, b),
38        }
39    }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Attribute {
44    Bold,
45    Dim,
46    Italic,
47    Underline,
48    Blink,
49    Reverse,
50    Hidden,
51    Strikethrough,
52}
53
54impl Attribute {
55    fn to_code(&self) -> &'static str {
56        match self {
57            Attribute::Bold => "1",
58            Attribute::Dim => "2",
59            Attribute::Italic => "3",
60            Attribute::Underline => "4",
61            Attribute::Blink => "5",
62            Attribute::Reverse => "7",
63            Attribute::Hidden => "8",
64            Attribute::Strikethrough => "9",
65        }
66    }
67}
68
69#[derive(Clone, Default)]
70pub struct Style {
71    foreground: Option<Color>,
72    background: Option<Color>,
73    attributes: Vec<Attribute>,
74}
75
76impl Style {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    pub fn fg(mut self, color: Color) -> Self {
82        self.foreground = Some(color);
83        self
84    }
85
86    pub fn bg(mut self, color: Color) -> Self {
87        self.background = Some(color);
88        self
89    }
90
91    pub fn attr(mut self, attribute: Attribute) -> Self {
92        self.attributes.push(attribute);
93        self
94    }
95
96    pub fn bold(self) -> Self {
97        self.attr(Attribute::Bold)
98    }
99
100    pub fn dim(self) -> Self {
101        self.attr(Attribute::Dim)
102    }
103
104    pub fn italic(self) -> Self {
105        self.attr(Attribute::Italic)
106    }
107
108    pub fn underline(self) -> Self {
109        self.attr(Attribute::Underline)
110    }
111
112    pub fn blink(self) -> Self {
113        self.attr(Attribute::Blink)
114    }
115
116    pub fn reverse(self) -> Self {
117        self.attr(Attribute::Reverse)
118    }
119
120    pub fn hidden(self) -> Self {
121        self.attr(Attribute::Hidden)
122    }
123
124    pub fn strikethrough(self) -> Self {
125        self.attr(Attribute::Strikethrough)
126    }
127
128    #[cfg(not(feature = "no-color"))]
129    fn to_ansi_start(&self) -> String {
130        let mut codes: Vec<String> = Vec::new();
131
132        for attr in &self.attributes {
133            codes.push(attr.to_code().into());
134        }
135
136        if let Some(fg) = &self.foreground {
137            codes.push(fg.to_fg_code());
138        }
139
140        if let Some(bg) = &self.background {
141            codes.push(bg.to_bg_code());
142        }
143
144        if codes.is_empty() {
145            String::new()
146        } else {
147            format!("\x1b[{}m", codes.join(";"))
148        }
149    }
150
151    #[cfg(feature = "no-color")]
152    fn to_ansi_start(&self) -> String {
153        String::new()
154    }
155}
156
157#[derive(Clone)]
158pub struct Console {
159    text: String,
160    style: Style,
161}
162
163impl Console {
164    pub fn new<T: Into<String>>(text: T) -> Self {
165        Console {
166            text: text.into(),
167            style: Style::default(),
168        }
169    }
170
171    pub fn new_with_style<T: Into<String>>(text: T, style: Style) -> Self {
172        Console {
173            text: text.into(),
174            style,
175        }
176    }
177
178    pub fn with_text<T: Into<String>>(&self, text: T) -> Self {
179        Console {
180            text: text.into(),
181            style: self.style.clone(),
182        }
183    }
184
185    // Color methods
186    pub fn fg(self, color: Color) -> Self {
187        Console {
188            style: self.style.fg(color),
189            ..self
190        }
191    }
192
193    pub fn bg(self, color: Color) -> Self {
194        Console {
195            style: self.style.bg(color),
196            ..self
197        }
198    }
199
200    pub fn fg_rgb(self, r: u8, g: u8, b: u8) -> Self {
201        self.fg(Color::RGB(r, g, b))
202    }
203
204    pub fn bg_rgb(self, r: u8, g: u8, b: u8) -> Self {
205        self.bg(Color::RGB(r, g, b))
206    }
207
208    // Named color convenience methods
209    pub fn black(self) -> Self {
210        self.fg(Color::BLACK)
211    }
212
213    pub fn red(self) -> Self {
214        self.fg(Color::RED)
215    }
216
217    pub fn green(self) -> Self {
218        self.fg(Color::GREEN)
219    }
220
221    pub fn yellow(self) -> Self {
222        self.fg(Color::YELLOW)
223    }
224
225    pub fn blue(self) -> Self {
226        self.fg(Color::BLUE)
227    }
228    pub fn magenta(self) -> Self {
229        self.fg(Color::MAGENTA)
230    }
231
232    pub fn cyan(self) -> Self {
233        self.fg(Color::CYAN)
234    }
235
236    pub fn white(self) -> Self {
237        self.fg(Color::WHITE)
238    }
239
240    pub fn bright_black(self) -> Self {
241        self.fg(Color::BRIGHT_BLACK)
242    }
243
244    pub fn bright_red(self) -> Self {
245        self.fg(Color::BRIGHT_RED)
246    }
247
248    pub fn bright_green(self) -> Self {
249        self.fg(Color::BRIGHT_GREEN)
250    }
251
252    pub fn bright_yellow(self) -> Self {
253        self.fg(Color::BRIGHT_YELLOW)
254    }
255
256    pub fn bright_blue(self) -> Self {
257        self.fg(Color::BRIGHT_BLUE)
258    }
259
260    pub fn bright_magenta(self) -> Self {
261        self.fg(Color::BRIGHT_MAGENTA)
262    }
263
264    pub fn bright_cyan(self) -> Self {
265        self.fg(Color::BRIGHT_CYAN)
266    }
267
268    pub fn bright_white(self) -> Self {
269        self.fg(Color::BRIGHT_WHITE)
270    }
271
272    // Background color convenience methods
273    pub fn on_black(self) -> Self {
274        self.bg(Color::BLACK)
275    }
276
277    pub fn on_red(self) -> Self {
278        self.bg(Color::RED)
279    }
280
281    pub fn on_green(self) -> Self {
282        self.bg(Color::GREEN)
283    }
284
285    pub fn on_yellow(self) -> Self {
286        self.bg(Color::YELLOW)
287    }
288
289    pub fn on_blue(self) -> Self {
290        self.bg(Color::BLUE)
291    }
292
293    pub fn on_magenta(self) -> Self {
294        self.bg(Color::MAGENTA)
295    }
296
297    pub fn on_cyan(self) -> Self {
298        self.bg(Color::CYAN)
299    }
300
301    pub fn on_white(self) -> Self {
302        self.bg(Color::WHITE)
303    }
304
305    pub fn on_bright_black(self) -> Self {
306        self.bg(Color::BRIGHT_BLACK)
307    }
308
309    pub fn on_bright_red(self) -> Self {
310        self.bg(Color::BRIGHT_RED)
311    }
312
313    pub fn on_bright_green(self) -> Self {
314        self.bg(Color::BRIGHT_GREEN)
315    }
316
317    pub fn on_bright_yellow(self) -> Self {
318        self.bg(Color::BRIGHT_YELLOW)
319    }
320
321    pub fn on_bright_blue(self) -> Self {
322        self.bg(Color::BRIGHT_BLUE)
323    }
324
325    pub fn on_bright_magenta(self) -> Self {
326        self.bg(Color::BRIGHT_MAGENTA)
327    }
328
329    pub fn on_bright_cyan(self) -> Self {
330        self.bg(Color::BRIGHT_CYAN)
331    }
332
333    pub fn on_bright_white(self) -> Self {
334        self.bg(Color::BRIGHT_WHITE)
335    }
336
337    // Attribute methods
338    pub fn attr(self, attribute: Attribute) -> Self {
339        Console {
340            style: self.style.attr(attribute),
341            ..self
342        }
343    }
344
345    pub fn bold(self) -> Self {
346        Console {
347            style: self.style.bold(),
348            ..self
349        }
350    }
351
352    pub fn dim(self) -> Self {
353        Console {
354            style: self.style.dim(),
355            ..self
356        }
357    }
358
359    pub fn italic(self) -> Self {
360        Console {
361            style: self.style.italic(),
362            ..self
363        }
364    }
365
366    pub fn underline(self) -> Self {
367        Console {
368            style: self.style.underline(),
369            ..self
370        }
371    }
372
373    pub fn blink(self) -> Self {
374        Console {
375            style: self.style.blink(),
376            ..self
377        }
378    }
379
380    pub fn reverse(self) -> Self {
381        Console {
382            style: self.style.reverse(),
383            ..self
384        }
385    }
386
387    pub fn hidden(self) -> Self {
388        Console {
389            style: self.style.hidden(),
390            ..self
391        }
392    }
393
394    pub fn strikethrough(self) -> Self {
395        Console {
396            style: self.style.strikethrough(),
397            ..self
398        }
399    }
400
401    // Output methods
402    pub fn print(&self) {
403        let mut stdout = std::io::stdout();
404        self.write_to(&mut stdout).unwrap();
405    }
406
407    pub fn println(&self) {
408        let mut stdout = std::io::stdout();
409        self.write_to(&mut stdout).unwrap();
410        writeln!(stdout).unwrap();
411    }
412
413    pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
414        let ansi_code = self.style.to_ansi_start();
415        if !ansi_code.is_empty() {
416            write!(writer, "{}", ansi_code)?;
417        }
418        write!(writer, "{}", self.text)?;
419        if !ansi_code.is_empty() {
420            write!(writer, "\x1b[0m")?;
421        }
422        Ok(())
423    }
424
425    pub fn to_string(&self) -> String {
426        format!("{}", self)
427    }
428}
429
430impl std::fmt::Display for Console {
431    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
432        let ansi_code = self.style.to_ansi_start();
433        if !ansi_code.is_empty() {
434            write!(f, "{}", ansi_code)?;
435        }
436        write!(f, "{}", self.text)?;
437        if !ansi_code.is_empty() {
438            write!(f, "\x1b[0m")?;
439        }
440        Ok(())
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn test_color_constants() {
450        assert_eq!(Color::BLACK, Color::Named(0));
451        assert_eq!(Color::RED, Color::Named(1));
452        assert_eq!(Color::GREEN, Color::Named(2));
453        assert_eq!(Color::YELLOW, Color::Named(3));
454        assert_eq!(Color::BLUE, Color::Named(4));
455        assert_eq!(Color::MAGENTA, Color::Named(5));
456        assert_eq!(Color::CYAN, Color::Named(6));
457        assert_eq!(Color::WHITE, Color::Named(7));
458        assert_eq!(Color::BRIGHT_BLACK, Color::Named(8));
459        assert_eq!(Color::BRIGHT_RED, Color::Named(9));
460        assert_eq!(Color::BRIGHT_GREEN, Color::Named(10));
461        assert_eq!(Color::BRIGHT_YELLOW, Color::Named(11));
462        assert_eq!(Color::BRIGHT_BLUE, Color::Named(12));
463        assert_eq!(Color::BRIGHT_MAGENTA, Color::Named(13));
464        assert_eq!(Color::BRIGHT_CYAN, Color::Named(14));
465        assert_eq!(Color::BRIGHT_WHITE, Color::Named(15));
466    }
467
468    #[test]
469    fn test_color_codes() {
470        // Test named colors
471        let color = Color::RED;
472        assert_eq!(color.to_fg_code(), "38;5;1");
473        assert_eq!(color.to_bg_code(), "48;5;1");
474
475        // Test RGB colors
476        let rgb = Color::RGB(255, 128, 0);
477        assert_eq!(rgb.to_fg_code(), "38;2;255;128;0");
478        assert_eq!(rgb.to_bg_code(), "48;2;255;128;0");
479
480        // Test edge cases
481        let black = Color::BLACK;
482        assert_eq!(black.to_fg_code(), "38;5;0");
483
484        let white = Color::BRIGHT_WHITE;
485        assert_eq!(white.to_fg_code(), "38;5;15");
486    }
487
488    #[test]
489    fn test_attribute_codes() {
490        assert_eq!(Attribute::Bold.to_code(), "1");
491        assert_eq!(Attribute::Dim.to_code(), "2");
492        assert_eq!(Attribute::Italic.to_code(), "3");
493        assert_eq!(Attribute::Underline.to_code(), "4");
494        assert_eq!(Attribute::Blink.to_code(), "5");
495        assert_eq!(Attribute::Reverse.to_code(), "7");
496        assert_eq!(Attribute::Hidden.to_code(), "8");
497        assert_eq!(Attribute::Strikethrough.to_code(), "9");
498    }
499
500    #[test]
501    fn test_style_builder() {
502        let style = Style::new()
503            .fg(Color::RED)
504            .bg(Color::BLUE)
505            .bold()
506            .underline();
507
508        #[cfg(not(feature = "no-color"))]
509        {
510            let ansi = style.to_ansi_start();
511            assert!(ansi.contains("1")); // bold
512            assert!(ansi.contains("4")); // underline
513            assert!(ansi.contains("38;5;1")); // red foreground
514            assert!(ansi.contains("48;5;4")); // blue background
515        }
516
517        #[cfg(feature = "no-color")]
518        assert_eq!(style.to_ansi_start(), "");
519    }
520
521    #[test]
522    fn test_style_empty() {
523        let style = Style::new();
524        #[cfg(not(feature = "no-color"))]
525        assert_eq!(style.to_ansi_start(), "");
526        #[cfg(feature = "no-color")]
527        assert_eq!(style.to_ansi_start(), "");
528    }
529
530    #[test]
531    fn test_style_only_foreground() {
532        let style = Style::new().fg(Color::GREEN);
533
534        #[cfg(not(feature = "no-color"))]
535        assert_eq!(style.to_ansi_start(), "\x1b[38;5;2m");
536        #[cfg(feature = "no-color")]
537        assert_eq!(style.to_ansi_start(), "");
538    }
539
540    #[test]
541    fn test_style_only_background() {
542        let style = Style::new().bg(Color::YELLOW);
543
544        #[cfg(not(feature = "no-color"))]
545        assert_eq!(style.to_ansi_start(), "\x1b[48;5;3m");
546        #[cfg(feature = "no-color")]
547        assert_eq!(style.to_ansi_start(), "");
548    }
549
550    #[test]
551    fn test_style_only_attributes() {
552        let style = Style::new().bold().italic();
553
554        #[cfg(not(feature = "no-color"))]
555        {
556            let ansi = style.to_ansi_start();
557            assert!(ansi.contains("1"));
558            assert!(ansi.contains("3"));
559        }
560        #[cfg(feature = "no-color")]
561        assert_eq!(style.to_ansi_start(), "");
562    }
563
564    #[test]
565    fn test_style_rgb_colors() {
566        let style = Style::new()
567            .fg(Color::RGB(255, 0, 0))
568            .bg(Color::RGB(0, 255, 0));
569
570        #[cfg(not(feature = "no-color"))]
571        {
572            let ansi = style.to_ansi_start();
573            assert!(ansi.contains("38;2;255;0;0"));
574            assert!(ansi.contains("48;2;0;255;0"));
575        }
576        #[cfg(feature = "no-color")]
577        assert_eq!(style.to_ansi_start(), "");
578    }
579
580    #[test]
581    fn test_console_creation() {
582        let console = Console::new("test");
583        assert_eq!(console.text, "test");
584
585        let console_from_str = Console::new("test");
586        assert_eq!(console_from_str.text, "test");
587    }
588
589    #[test]
590    fn test_console_with_text() {
591        let base = Console::new("").red().bold();
592        let derived = base.with_text("new text");
593
594        assert_eq!(derived.text, "new text");
595        // Should maintain the same style
596        assert_eq!(derived.style.foreground, base.style.foreground);
597        assert_eq!(derived.style.attributes, base.style.attributes);
598    }
599
600    #[test]
601    fn test_console_color_methods() {
602        let console = Console::new("test").red();
603        assert_eq!(console.style.foreground, Some(Color::RED));
604
605        let console = Console::new("test").on_blue();
606        assert_eq!(console.style.background, Some(Color::BLUE));
607
608        let console = Console::new("test").fg_rgb(255, 0, 0);
609        assert_eq!(console.style.foreground, Some(Color::RGB(255, 0, 0)));
610
611        let console = Console::new("test").bg_rgb(0, 255, 0);
612        assert_eq!(console.style.background, Some(Color::RGB(0, 255, 0)));
613    }
614
615    #[test]
616    fn test_console_attribute_methods() {
617        let console = Console::new("test").bold();
618        assert!(console.style.attributes.contains(&Attribute::Bold));
619
620        let console = Console::new("test").italic().underline();
621        assert!(console.style.attributes.contains(&Attribute::Italic));
622        assert!(console.style.attributes.contains(&Attribute::Underline));
623
624        let console = Console::new("test")
625            .bold()
626            .dim()
627            .italic()
628            .underline()
629            .blink()
630            .reverse()
631            .hidden()
632            .strikethrough();
633
634        let attrs = &console.style.attributes;
635        assert!(attrs.contains(&Attribute::Bold));
636        assert!(attrs.contains(&Attribute::Dim));
637        assert!(attrs.contains(&Attribute::Italic));
638        assert!(attrs.contains(&Attribute::Underline));
639        assert!(attrs.contains(&Attribute::Blink));
640        assert!(attrs.contains(&Attribute::Reverse));
641        assert!(attrs.contains(&Attribute::Hidden));
642        assert!(attrs.contains(&Attribute::Strikethrough));
643    }
644
645    #[test]
646    fn test_console_display_trait() {
647        let console = Console::new("hello").red().bold();
648        let output = format!("{}", console);
649
650        #[cfg(not(feature = "no-color"))]
651        {
652            assert!(output.starts_with("\x1b["));
653            assert!(output.contains("hello"));
654            assert!(output.ends_with("\x1b[0m"));
655        }
656        #[cfg(feature = "no-color")]
657        assert_eq!(output, "hello");
658    }
659
660    #[test]
661    fn test_console_to_string() {
662        let console = Console::new("test").blue().underline();
663        let string = console.to_string();
664
665        #[cfg(not(feature = "no-color"))]
666        {
667            assert!(string.contains("test"));
668            assert!(string.contains("38;5;4")); // blue
669            assert!(string.contains("4")); // underline
670        }
671        #[cfg(feature = "no-color")]
672        assert_eq!(string, "test");
673    }
674
675    #[test]
676    fn test_console_complex_styling() {
677        let console = Console::new("complex")
678            .fg_rgb(128, 64, 255)
679            .on_bright_white()
680            .bold()
681            .italic()
682            .underline();
683
684        let output = console.to_string();
685
686        #[cfg(not(feature = "no-color"))]
687        {
688            assert!(output.contains("complex"));
689            assert!(output.contains("38;2;128;64;255"));
690            assert!(output.contains("48;5;15"));
691            assert!(output.contains("1"));
692            assert!(output.contains("3"));
693            assert!(output.contains("4"));
694        }
695        #[cfg(feature = "no-color")]
696        assert_eq!(output, "complex");
697    }
698
699    #[test]
700    fn test_console_method_chaining() {
701        // Test that methods can be chained and each returns Self
702        let console = Console::new("test").red().on_white().bold().underline();
703
704        assert_eq!(console.style.foreground, Some(Color::RED));
705        assert_eq!(console.style.background, Some(Color::WHITE));
706        assert!(console.style.attributes.contains(&Attribute::Bold));
707        assert!(console.style.attributes.contains(&Attribute::Underline));
708    }
709
710    #[test]
711    fn test_console_clone() {
712        let original = Console::new("original").red().bold();
713        let cloned = original.clone();
714
715        assert_eq!(original.text, cloned.text);
716        assert_eq!(original.style.foreground, cloned.style.foreground);
717        assert_eq!(original.style.background, cloned.style.background);
718        assert_eq!(original.style.attributes, cloned.style.attributes);
719    }
720
721    #[test]
722    fn test_color_and_attribute_copy() {
723        // These should be Copy types, so we can use them multiple times
724        let color = Color::RED;
725        let attr = Attribute::Bold;
726
727        let style1 = Style::new().fg(color).attr(attr);
728        let style2 = Style::new().fg(color).attr(attr);
729
730        assert_eq!(style1.foreground, style2.foreground);
731        assert_eq!(style1.attributes, style2.attributes);
732    }
733
734    #[test]
735    fn test_all_named_colors() {
736        // Test that all named color methods work correctly
737        let colors = [
738            (Console::new("").black(), Color::BLACK),
739            (Console::new("").red(), Color::RED),
740            (Console::new("").green(), Color::GREEN),
741            (Console::new("").yellow(), Color::YELLOW),
742            (Console::new("").blue(), Color::BLUE),
743            (Console::new("").magenta(), Color::MAGENTA),
744            (Console::new("").cyan(), Color::CYAN),
745            (Console::new("").white(), Color::WHITE),
746            (Console::new("").bright_black(), Color::BRIGHT_BLACK),
747            (Console::new("").bright_red(), Color::BRIGHT_RED),
748            (Console::new("").bright_green(), Color::BRIGHT_GREEN),
749            (Console::new("").bright_yellow(), Color::BRIGHT_YELLOW),
750            (Console::new("").bright_blue(), Color::BRIGHT_BLUE),
751            (Console::new("").bright_magenta(), Color::BRIGHT_MAGENTA),
752            (Console::new("").bright_cyan(), Color::BRIGHT_CYAN),
753            (Console::new("").bright_white(), Color::BRIGHT_WHITE),
754        ];
755
756        for (console, expected_color) in colors {
757            assert_eq!(console.style.foreground, Some(expected_color));
758        }
759    }
760
761    #[test]
762    fn test_all_background_colors() {
763        // Test that all background color methods work correctly
764        let backgrounds = [
765            (Console::new("").on_black(), Color::BLACK),
766            (Console::new("").on_red(), Color::RED),
767            (Console::new("").on_green(), Color::GREEN),
768            (Console::new("").on_yellow(), Color::YELLOW),
769            (Console::new("").on_blue(), Color::BLUE),
770            (Console::new("").on_magenta(), Color::MAGENTA),
771            (Console::new("").on_cyan(), Color::CYAN),
772            (Console::new("").on_white(), Color::WHITE),
773            (Console::new("").on_bright_black(), Color::BRIGHT_BLACK),
774            (Console::new("").on_bright_red(), Color::BRIGHT_RED),
775            (Console::new("").on_bright_green(), Color::BRIGHT_GREEN),
776            (Console::new("").on_bright_yellow(), Color::BRIGHT_YELLOW),
777            (Console::new("").on_bright_blue(), Color::BRIGHT_BLUE),
778            (Console::new("").on_bright_magenta(), Color::BRIGHT_MAGENTA),
779            (Console::new("").on_bright_cyan(), Color::BRIGHT_CYAN),
780            (Console::new("").on_bright_white(), Color::BRIGHT_WHITE),
781        ];
782
783        for (console, expected_color) in backgrounds {
784            assert_eq!(console.style.background, Some(expected_color));
785        }
786    }
787
788    #[test]
789    fn test_console_write_to() {
790        let console = Console::new("write test").green();
791        let mut buffer = Vec::new();
792
793        console.write_to(&mut buffer).unwrap();
794        let output = String::from_utf8(buffer).unwrap();
795
796        #[cfg(not(feature = "no-color"))]
797        {
798            assert!(output.contains("write test"));
799            assert!(output.contains("38;5;2")); // green
800            assert!(output.ends_with("\x1b[0m"));
801        }
802        #[cfg(feature = "no-color")]
803        assert_eq!(output, "write test");
804    }
805
806    #[test]
807    fn test_console_empty_text() {
808        let console = Console::new("").red().bold();
809        let output = console.to_string();
810
811        #[cfg(not(feature = "no-color"))]
812        {
813            // Even with empty text, ANSI codes should be generated and reset
814            assert!(output.starts_with("\x1b["));
815            assert!(output.ends_with("\x1b[0m"));
816        }
817        #[cfg(feature = "no-color")]
818        assert_eq!(output, "");
819    }
820
821    #[test]
822    fn test_multiple_identical_attributes() {
823        // Adding the same attribute multiple times should work
824        // (though it might not make sense semantically)
825        let console = Console::new("test").bold().bold().bold();
826
827        // Should have multiple bold attributes in the vector
828        let bold_count = console
829            .style
830            .attributes
831            .iter()
832            .filter(|&&attr| attr == Attribute::Bold)
833            .count();
834        assert_eq!(bold_count, 3);
835    }
836
837    #[test]
838    fn test_ansi_code_ordering() {
839        // Test that ANSI codes are generated in consistent order:
840        // attributes first, then foreground, then background
841        let style = Style::new()
842            .bg(Color::BLUE)
843            .fg(Color::RED)
844            .underline()
845            .bold();
846
847        #[cfg(not(feature = "no-color"))]
848        {
849            let ansi = style.to_ansi_start();
850            // The order should be: 1 (bold), 4 (underline), 38;5;1 (red), 48;5;4 (blue)
851            let expected_parts = ["1", "4", "38;5;1", "48;5;4"];
852            let ansi_without_prefix = ansi.trim_start_matches("\x1b[").trim_end_matches('m');
853            let parts: Vec<&str> = ansi_without_prefix.split(';').collect();
854
855            // We can't easily test the exact order because it depends on Vec iteration order,
856            // but we can test that all expected parts are present
857            for expected in expected_parts {
858                assert!(
859                    ansi.contains(expected),
860                    "ANSI code should contain {}",
861                    expected
862                );
863            }
864        }
865    }
866
867    #[test]
868    fn test_console_builder() {
869        let console = Console::new("test").red().on_white().bold().underline();
870
871        let output = console.to_string();
872
873        #[cfg(not(feature = "no-color"))]
874        {
875            assert!(output.contains("test"));
876            assert!(output.contains("\x1b["));
877            assert!(output.contains("0m"));
878        }
879
880        #[cfg(feature = "no-color")]
881        assert_eq!(output, "test");
882    }
883    #[test]
884    fn test_basic_colors() {
885        let console = Console::new("Hello, world!").red().bold();
886        insta::assert_yaml_snapshot!(console.to_string());
887    }
888
889    #[test]
890    fn test_rgb_colors() {
891        let console = Console::new("RGB Text")
892            .fg_rgb(255, 0, 128)
893            .bg_rgb(0, 255, 0)
894            .underline();
895        insta::assert_yaml_snapshot!(console.to_string());
896    }
897
898    #[test]
899    fn test_complex_styling() {
900        let console = Console::new("Complex Style")
901            .bright_red()
902            .on_bright_white()
903            .bold()
904            .italic()
905            .blink();
906        insta::assert_yaml_snapshot!(console.to_string());
907    }
908    #[test]
909    fn test_style_combinations() {
910        let styles = vec![
911            Console::new("Error style").red().bold(),
912            Console::new("Warning style").yellow().italic(),
913            Console::new("Success style").green().bold(),
914        ];
915
916        let outputs: Vec<String> = styles.iter().map(|c| c.to_string()).collect();
917        insta::assert_yaml_snapshot!(outputs);
918    }
919
920    #[test]
921    fn test_reusable_style() {
922        let error_style = Console::new("").red().bold();
923        let messages = vec![
924            error_style.with_text("Error: File not found").to_string(),
925            error_style
926                .with_text("Error: Permission denied")
927                .to_string(),
928        ];
929
930        insta::assert_yaml_snapshot!(messages);
931    }
932}