Skip to main content

vtcode_design/
style.rs

1//! Unified style bridging between `anstyle` and `ratatui`.
2//!
3//! Converts `anstyle::Style` and `InlineTextStyle` to `ratatui::style::Style`,
4//! using the correct color mapping from [`crate::color`].
5
6use anstyle::{Color as AnstyleColor, Effects, Style as AnstyleStyle};
7use ratatui::style::{Modifier, Style};
8
9use crate::color::anstyle_to_ratatui_color;
10
11/// Convert an `anstyle::Style` directly to a `ratatui::style::Style`.
12///
13/// Uses the correct color mapping from [`crate::color::anstyle_to_ratatui_color`].
14pub fn anstyle_to_ratatui_style(style: AnstyleStyle) -> Style {
15    let mut ratatui_style = Style::default();
16
17    if let Some(fg) = style.get_fg_color() {
18        ratatui_style = ratatui_style.fg(anstyle_to_ratatui_color(fg));
19    }
20
21    if let Some(bg) = style.get_bg_color() {
22        ratatui_style = ratatui_style.bg(anstyle_to_ratatui_color(bg));
23    }
24
25    ratatui_style = ratatui_style.add_modifier(effects_to_modifiers(style.get_effects()));
26    ratatui_style
27}
28
29/// Convert an `InlineTextStyle` to a `ratatui::style::Style`.
30///
31/// If the style has no foreground color, the `fallback` is used.
32///
33/// `InlineTextStyle` is from `vtcode_commons::ui_protocol::InlineTextStyle`.
34/// We accept its fields individually here to avoid a direct dependency on the
35/// type, keeping the coupling loose.
36pub fn inline_text_style_to_ratatui(
37    color: Option<AnstyleColor>,
38    bg_color: Option<AnstyleColor>,
39    effects: Effects,
40    fallback: Option<AnstyleColor>,
41) -> Style {
42    let mut resolved = Style::default();
43
44    if let Some(c) = color.or(fallback) {
45        resolved = resolved.fg(anstyle_to_ratatui_color(c));
46    }
47
48    if let Some(c) = bg_color {
49        resolved = resolved.bg(anstyle_to_ratatui_color(c));
50    }
51
52    resolved = resolved.add_modifier(effects_to_modifiers(effects));
53    resolved
54}
55
56/// Convert `anstyle::Effects` to `ratatui::style::Modifier`.
57pub fn effects_to_modifiers(effects: Effects) -> Modifier {
58    let mut modifier = Modifier::empty();
59
60    if effects.contains(Effects::BOLD) {
61        modifier.insert(Modifier::BOLD);
62    }
63    if effects.contains(Effects::DIMMED) {
64        modifier.insert(Modifier::DIM);
65    }
66    if effects.contains(Effects::ITALIC) {
67        modifier.insert(Modifier::ITALIC);
68    }
69    if effects.contains(Effects::UNDERLINE) {
70        modifier.insert(Modifier::UNDERLINED);
71    }
72    if effects.contains(Effects::BLINK) {
73        modifier.insert(Modifier::SLOW_BLINK);
74    }
75    if effects.contains(Effects::INVERT) {
76        modifier.insert(Modifier::REVERSED);
77    }
78    if effects.contains(Effects::STRIKETHROUGH) {
79        modifier.insert(Modifier::CROSSED_OUT);
80    }
81
82    modifier
83}
84
85/// Create a `ratatui::Style` with a foreground color.
86pub fn fg_style(color: AnstyleColor) -> Style {
87    Style::default().fg(anstyle_to_ratatui_color(color))
88}
89
90/// Create a `ratatui::Style` with a background color.
91pub fn bg_style(color: AnstyleColor) -> Style {
92    Style::default().bg(anstyle_to_ratatui_color(color))
93}
94
95/// Create a `ratatui::Style` with foreground and background colors.
96pub fn fg_bg_style(fg: AnstyleColor, bg: AnstyleColor) -> Style {
97    Style::default()
98        .fg(anstyle_to_ratatui_color(fg))
99        .bg(anstyle_to_ratatui_color(bg))
100}
101
102/// Create a `ratatui::Style` with effects/modifiers.
103pub fn with_effects(effects: Effects) -> Style {
104    Style::default().add_modifier(effects_to_modifiers(effects))
105}
106
107/// Create a `ratatui::Style` with foreground color and effects.
108pub fn colored_with_effects(color: AnstyleColor, effects: Effects) -> Style {
109    Style::default()
110        .fg(anstyle_to_ratatui_color(color))
111        .add_modifier(effects_to_modifiers(effects))
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn anstyle_style_with_fg() {
120        let input =
121            AnstyleStyle::new().fg_color(Some(AnstyleColor::Ansi(anstyle::AnsiColor::Green)));
122        let result = anstyle_to_ratatui_style(input);
123        assert_eq!(result.fg, Some(ratatui::style::Color::Green));
124    }
125
126    #[test]
127    fn anstyle_style_with_magenta_fg() {
128        // Regression test: Magenta should map to Magenta, not DarkGray.
129        let input =
130            AnstyleStyle::new().fg_color(Some(AnstyleColor::Ansi(anstyle::AnsiColor::Magenta)));
131        let result = anstyle_to_ratatui_style(input);
132        assert_eq!(result.fg, Some(ratatui::style::Color::Magenta));
133    }
134
135    #[test]
136    fn anstyle_style_with_bg() {
137        let input = AnstyleStyle::new().bg_color(Some(AnstyleColor::Ansi(anstyle::AnsiColor::Red)));
138        let result = anstyle_to_ratatui_style(input);
139        assert_eq!(result.bg, Some(ratatui::style::Color::Red));
140    }
141
142    #[test]
143    fn anstyle_style_with_effects() {
144        let input = AnstyleStyle::new().bold().italic();
145        let result = anstyle_to_ratatui_style(input);
146        assert!(result.add_modifier.contains(Modifier::BOLD));
147        assert!(result.add_modifier.contains(Modifier::ITALIC));
148    }
149
150    #[test]
151    fn inline_text_style_with_fallback() {
152        let result = inline_text_style_to_ratatui(
153            None,
154            None,
155            Effects::BOLD,
156            Some(AnstyleColor::Ansi(anstyle::AnsiColor::Cyan)),
157        );
158        assert_eq!(result.fg, Some(ratatui::style::Color::Cyan));
159        assert!(result.add_modifier.contains(Modifier::BOLD));
160    }
161
162    #[test]
163    fn inline_text_style_color_overrides_fallback() {
164        let result = inline_text_style_to_ratatui(
165            Some(AnstyleColor::Ansi(anstyle::AnsiColor::Red)),
166            None,
167            Effects::new(),
168            Some(AnstyleColor::Ansi(anstyle::AnsiColor::Cyan)),
169        );
170        assert_eq!(result.fg, Some(ratatui::style::Color::Red));
171    }
172
173    #[test]
174    fn effects_to_modifiers_all() {
175        let effects = Effects::BOLD
176            | Effects::DIMMED
177            | Effects::ITALIC
178            | Effects::UNDERLINE
179            | Effects::BLINK
180            | Effects::INVERT
181            | Effects::STRIKETHROUGH;
182        let modifier = effects_to_modifiers(effects);
183        assert!(modifier.contains(Modifier::BOLD));
184        assert!(modifier.contains(Modifier::DIM));
185        assert!(modifier.contains(Modifier::ITALIC));
186        assert!(modifier.contains(Modifier::UNDERLINED));
187        assert!(modifier.contains(Modifier::SLOW_BLINK));
188        assert!(modifier.contains(Modifier::REVERSED));
189        assert!(modifier.contains(Modifier::CROSSED_OUT));
190    }
191
192    #[test]
193    fn convenience_fg_style() {
194        let s = fg_style(AnstyleColor::Ansi(anstyle::AnsiColor::Blue));
195        assert_eq!(s.fg, Some(ratatui::style::Color::Blue));
196    }
197
198    #[test]
199    fn convenience_bg_style() {
200        let s = bg_style(AnstyleColor::Ansi(anstyle::AnsiColor::Red));
201        assert_eq!(s.bg, Some(ratatui::style::Color::Red));
202    }
203
204    #[test]
205    fn convenience_fg_bg_style() {
206        let s = fg_bg_style(
207            AnstyleColor::Ansi(anstyle::AnsiColor::Green),
208            AnstyleColor::Ansi(anstyle::AnsiColor::Black),
209        );
210        assert_eq!(s.fg, Some(ratatui::style::Color::Green));
211        assert_eq!(s.bg, Some(ratatui::style::Color::Black));
212    }
213
214    #[test]
215    fn convenience_colored_with_effects() {
216        let s = colored_with_effects(
217            AnstyleColor::Ansi(anstyle::AnsiColor::Yellow),
218            Effects::BOLD | Effects::ITALIC,
219        );
220        assert_eq!(s.fg, Some(ratatui::style::Color::Yellow));
221        assert!(s.add_modifier.contains(Modifier::BOLD));
222        assert!(s.add_modifier.contains(Modifier::ITALIC));
223    }
224}