Skip to main content

vtcode_tui/core_tui/
theme_parser.rs

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