Skip to main content

vtcode_design/
color.rs

1//! Unified color conversion between `anstyle` and `ratatui`.
2//!
3//! This module provides the single correct mapping from `anstyle::Color` to
4//! `ratatui::style::Color`. Previous implementations in `vtcode-commons` and
5//! `vtcode-tui` had bugs mapping `Magenta` and bright variants to incorrect
6//! ratatui colors.
7
8use anstyle::{AnsiColor, Color as AnstyleColor, RgbColor};
9
10/// Convert an `anstyle::Color` to a `ratatui::style::Color`.
11///
12/// This is the canonical, correct mapping. It properly handles:
13/// - All 16 standard ANSI colors (including bright variants as `Light*`)
14/// - 256-color palette via `Indexed`
15/// - True color via `Rgb`
16///
17/// # Bug fixes over prior implementations
18///
19/// Prior implementations in `vtcode-commons::anstyle_utils` and
20/// `vtcode-tui::core_tui::style` incorrectly mapped:
21/// - `Magenta` to `DarkGray` (now correctly `Magenta`)
22/// - `BrightMagenta` to `DarkGray` (now correctly `LightMagenta`)
23/// - `BrightRed/Green/Yellow/Blue/Cyan` to non-bright variants
24/// - `Ansi256` colors to `Reset` instead of `Indexed`
25pub fn anstyle_to_ratatui_color(color: AnstyleColor) -> ratatui::style::Color {
26    match color {
27        AnstyleColor::Ansi(ansi) => ansi_to_ratatui(ansi),
28        AnstyleColor::Ansi256(c) => ratatui::style::Color::Indexed(c.0),
29        AnstyleColor::Rgb(RgbColor(r, g, b)) => ratatui::style::Color::Rgb(r, g, b),
30    }
31}
32
33/// Map a standard ANSI color (0-15) to its ratatui equivalent.
34fn ansi_to_ratatui(color: AnsiColor) -> ratatui::style::Color {
35    match color {
36        AnsiColor::Black => ratatui::style::Color::Black,
37        AnsiColor::Red => ratatui::style::Color::Red,
38        AnsiColor::Green => ratatui::style::Color::Green,
39        AnsiColor::Yellow => ratatui::style::Color::Yellow,
40        AnsiColor::Blue => ratatui::style::Color::Blue,
41        AnsiColor::Magenta => ratatui::style::Color::Magenta,
42        AnsiColor::Cyan => ratatui::style::Color::Cyan,
43        AnsiColor::White => ratatui::style::Color::White,
44        AnsiColor::BrightBlack => ratatui::style::Color::DarkGray,
45        AnsiColor::BrightRed => ratatui::style::Color::LightRed,
46        AnsiColor::BrightGreen => ratatui::style::Color::LightGreen,
47        AnsiColor::BrightYellow => ratatui::style::Color::LightYellow,
48        AnsiColor::BrightBlue => ratatui::style::Color::LightBlue,
49        AnsiColor::BrightMagenta => ratatui::style::Color::LightMagenta,
50        AnsiColor::BrightCyan => ratatui::style::Color::LightCyan,
51        AnsiColor::BrightWhite => ratatui::style::Color::White,
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn standard_ansi_colors() {
61        assert_eq!(
62            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::Black)),
63            ratatui::style::Color::Black
64        );
65        assert_eq!(
66            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::Red)),
67            ratatui::style::Color::Red
68        );
69        assert_eq!(
70            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::Green)),
71            ratatui::style::Color::Green
72        );
73        assert_eq!(
74            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::Yellow)),
75            ratatui::style::Color::Yellow
76        );
77        assert_eq!(
78            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::Blue)),
79            ratatui::style::Color::Blue
80        );
81        assert_eq!(
82            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::Cyan)),
83            ratatui::style::Color::Cyan
84        );
85        assert_eq!(
86            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::White)),
87            ratatui::style::Color::White
88        );
89    }
90
91    #[test]
92    fn magenta_maps_to_magenta_not_dark_gray() {
93        // Regression test: previous implementations incorrectly mapped
94        // Magenta to DarkGray.
95        assert_eq!(
96            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::Magenta)),
97            ratatui::style::Color::Magenta
98        );
99    }
100
101    #[test]
102    fn bright_ansi_colors() {
103        assert_eq!(
104            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightBlack)),
105            ratatui::style::Color::DarkGray
106        );
107        assert_eq!(
108            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightRed)),
109            ratatui::style::Color::LightRed
110        );
111        assert_eq!(
112            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightGreen)),
113            ratatui::style::Color::LightGreen
114        );
115        assert_eq!(
116            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightYellow)),
117            ratatui::style::Color::LightYellow
118        );
119        assert_eq!(
120            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightBlue)),
121            ratatui::style::Color::LightBlue
122        );
123        assert_eq!(
124            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightMagenta)),
125            ratatui::style::Color::LightMagenta
126        );
127        assert_eq!(
128            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightCyan)),
129            ratatui::style::Color::LightCyan
130        );
131        assert_eq!(
132            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightWhite)),
133            ratatui::style::Color::White
134        );
135    }
136
137    #[test]
138    fn bright_magenta_maps_to_light_magenta() {
139        // Regression test: previous implementations incorrectly mapped
140        // BrightMagenta to DarkGray.
141        assert_eq!(
142            anstyle_to_ratatui_color(AnstyleColor::Ansi(AnsiColor::BrightMagenta)),
143            ratatui::style::Color::LightMagenta
144        );
145    }
146
147    #[test]
148    fn ansi256_color() {
149        assert_eq!(
150            anstyle_to_ratatui_color(AnstyleColor::Ansi256(anstyle::Ansi256Color(42))),
151            ratatui::style::Color::Indexed(42)
152        );
153        assert_eq!(
154            anstyle_to_ratatui_color(AnstyleColor::Ansi256(anstyle::Ansi256Color(0))),
155            ratatui::style::Color::Indexed(0)
156        );
157        assert_eq!(
158            anstyle_to_ratatui_color(AnstyleColor::Ansi256(anstyle::Ansi256Color(255))),
159            ratatui::style::Color::Indexed(255)
160        );
161    }
162
163    #[test]
164    fn rgb_color() {
165        assert_eq!(
166            anstyle_to_ratatui_color(AnstyleColor::Rgb(RgbColor(255, 128, 0))),
167            ratatui::style::Color::Rgb(255, 128, 0)
168        );
169        assert_eq!(
170            anstyle_to_ratatui_color(AnstyleColor::Rgb(RgbColor(0, 0, 0))),
171            ratatui::style::Color::Rgb(0, 0, 0)
172        );
173    }
174
175    #[test]
176    fn all_16_ansi_colors_covered() {
177        // Ensure every ANSI color maps to something other than Reset/Black
178        // for non-Black colors.
179        let colors = [
180            (AnsiColor::Black, ratatui::style::Color::Black),
181            (AnsiColor::Red, ratatui::style::Color::Red),
182            (AnsiColor::Green, ratatui::style::Color::Green),
183            (AnsiColor::Yellow, ratatui::style::Color::Yellow),
184            (AnsiColor::Blue, ratatui::style::Color::Blue),
185            (AnsiColor::Magenta, ratatui::style::Color::Magenta),
186            (AnsiColor::Cyan, ratatui::style::Color::Cyan),
187            (AnsiColor::White, ratatui::style::Color::White),
188            (AnsiColor::BrightBlack, ratatui::style::Color::DarkGray),
189            (AnsiColor::BrightRed, ratatui::style::Color::LightRed),
190            (AnsiColor::BrightGreen, ratatui::style::Color::LightGreen),
191            (AnsiColor::BrightYellow, ratatui::style::Color::LightYellow),
192            (AnsiColor::BrightBlue, ratatui::style::Color::LightBlue),
193            (
194                AnsiColor::BrightMagenta,
195                ratatui::style::Color::LightMagenta,
196            ),
197            (AnsiColor::BrightCyan, ratatui::style::Color::LightCyan),
198            (AnsiColor::BrightWhite, ratatui::style::Color::White),
199        ];
200        for (input, expected) in colors {
201            assert_eq!(
202                anstyle_to_ratatui_color(AnstyleColor::Ansi(input)),
203                expected,
204                "mismatch for {input:?}"
205            );
206        }
207    }
208}