Skip to main content

vtcode_ui/tui/core_tui/
theme_parser.rs

1//! Parse theme configuration from multiple syntaxes (Git, LS_COLORS, custom).
2
3use crate::tui::utils::CachedStyleParser;
4use anstyle::Style as AnsiStyle;
5use anyhow::Result;
6
7/// Parses color configuration strings in different syntaxes.
8///
9/// Supports:
10/// - Git color syntax (e.g., "bold red", "red blue")
11/// - LS_COLORS syntax (e.g., "01;34" for bold blue)
12/// - Flexible parsing that tries multiple formats
13pub struct ThemeConfigParser {
14    /// Cached parser for performance
15    cached_parser: CachedStyleParser,
16}
17
18impl ThemeConfigParser {
19    /// Create a new ThemeConfigParser
20    fn new() -> Self {
21        Self { cached_parser: CachedStyleParser::new() }
22    }
23}
24
25impl Default for ThemeConfigParser {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl ThemeConfigParser {
32    /// Parse a string in Git's color configuration syntax.
33    ///
34    /// # Examples
35    ///
36    /// ```text
37    /// "bold red"       → bold red foreground
38    /// "red blue"       → red foreground on blue background
39    /// "#0000ee ul"     → RGB blue with underline
40    /// "green"          → green foreground
41    /// "dim white"      → dimmed white
42    /// ```
43    ///
44    /// # Errors
45    ///
46    /// Returns error if the input doesn't match Git color syntax.
47    fn parse_git_style(&self, input: &str) -> Result<AnsiStyle> {
48        self.cached_parser.parse_git_style(input)
49    }
50
51    /// Parse a string in LS_COLORS syntax (ANSI escape codes).
52    ///
53    /// # Examples
54    ///
55    /// ```text
56    /// "34"       → blue foreground
57    /// "01;34"    → bold blue
58    /// "34;03"    → blue with italic
59    /// "30;47"    → black text on white background
60    /// ```
61    ///
62    /// # Errors
63    ///
64    /// Returns error if the input doesn't match LS_COLORS syntax.
65    fn parse_ls_colors(&self, input: &str) -> Result<AnsiStyle> {
66        self.cached_parser.parse_ls_colors(input)
67    }
68
69    /// Parse a style string, attempting Git syntax first, then LS_COLORS as fallback.
70    ///
71    /// This is a convenience function for flexible input parsing. It tries the more
72    /// human-readable Git syntax first, then falls back to ANSI codes if that fails.
73    ///
74    /// # Errors
75    ///
76    /// Returns error if the input matches neither Git nor LS_COLORS syntax.
77    pub fn parse_flexible(&self, input: &str) -> Result<AnsiStyle> {
78        self.cached_parser.parse_flexible(input)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_parse_git_bold_red() {
88        let parser = ThemeConfigParser::default();
89        let style = parser.parse_git_style("bold red").unwrap();
90        assert!(style.get_effects().contains(anstyle::Effects::BOLD));
91    }
92
93    #[test]
94    fn test_parse_git_hex_color() {
95        let parser = ThemeConfigParser::default();
96        let style = parser.parse_git_style("#0000ee").unwrap();
97        assert!(style.get_fg_color().is_some());
98    }
99
100    #[test]
101    fn test_parse_git_red_on_blue() {
102        let parser = ThemeConfigParser::default();
103        let style = parser.parse_git_style("red blue").unwrap();
104        assert!(style.get_fg_color().is_some());
105        assert!(style.get_bg_color().is_some());
106    }
107
108    #[test]
109    fn test_parse_git_dim_white() {
110        let parser = ThemeConfigParser::default();
111        let style = parser.parse_git_style("dim white").unwrap();
112        assert!(style.get_effects().contains(anstyle::Effects::DIMMED));
113    }
114
115    #[test]
116    fn test_parse_git_underline() {
117        let parser = ThemeConfigParser::default();
118        let style = parser.parse_git_style("ul green").unwrap();
119        assert!(style.get_effects().contains(anstyle::Effects::UNDERLINE));
120    }
121
122    #[test]
123    fn test_parse_ls_colors_blue() {
124        let parser = ThemeConfigParser::default();
125        let style = parser.parse_ls_colors("34").unwrap();
126        assert!(style.get_fg_color().is_some());
127    }
128
129    #[test]
130    fn test_parse_ls_colors_bold_blue() {
131        let parser = ThemeConfigParser::default();
132        let style = parser.parse_ls_colors("01;34").unwrap();
133        assert!(style.get_effects().contains(anstyle::Effects::BOLD));
134        assert!(style.get_fg_color().is_some());
135    }
136
137    #[test]
138    fn test_parse_ls_colors_black_on_white() {
139        let parser = ThemeConfigParser::default();
140        let style = parser.parse_ls_colors("30;47").unwrap();
141        assert!(style.get_fg_color().is_some());
142        assert!(style.get_bg_color().is_some());
143    }
144
145    #[test]
146    fn test_parse_flexible_tries_git_first() {
147        let parser = ThemeConfigParser::default();
148        let style = parser.parse_flexible("bold red").unwrap();
149        assert!(style.get_effects().contains(anstyle::Effects::BOLD));
150    }
151
152    #[test]
153    fn test_parse_flexible_fallback_to_ls() {
154        let parser = ThemeConfigParser::default();
155        let style = parser.parse_flexible("01;34").unwrap();
156        assert!(style.get_effects().contains(anstyle::Effects::BOLD));
157    }
158
159    #[test]
160    fn test_parse_git_fails_on_invalid() {
161        let parser = ThemeConfigParser::default();
162        let result = parser.parse_git_style("unknown-color");
163        result.unwrap_err();
164    }
165
166    #[test]
167    fn test_parse_ls_colors_fails_on_invalid() {
168        let parser = ThemeConfigParser::default();
169        let result = parser.parse_ls_colors("invalid");
170        result.unwrap_err();
171    }
172}