vtcode_tui/ui/
file_colorizer.rs1use anstyle::Style as AnsiStyle;
7use std::collections::HashMap;
8use std::env;
9use std::path::Path;
10
11#[derive(Debug, Clone)]
13pub struct FileColorizer {
14 ls_colors_map: HashMap<String, AnsiStyle>,
16 has_ls_colors: bool,
18}
19
20impl FileColorizer {
21 pub fn new() -> Self {
23 let ls_colors = env::var("LS_COLORS").unwrap_or_default();
24 let ls_colors_map = if ls_colors.is_empty() {
25 HashMap::new()
26 } else {
27 Self::parse_ls_colors(&ls_colors)
28 };
29
30 Self {
31 has_ls_colors: !ls_colors_map.is_empty(),
32 ls_colors_map,
33 }
34 }
35
36 fn parse_ls_colors(ls_colors: &str) -> HashMap<String, AnsiStyle> {
41 let mut map = HashMap::new();
42
43 for pair in ls_colors.split(':') {
44 if pair.is_empty() {
45 continue;
46 }
47
48 if let Some((key, value)) = pair.split_once('=') {
49 let parser = crate::utils::CachedStyleParser::default();
50 if let Ok(style) = parser.parse_ls_colors(value) {
51 map.insert(key.to_owned(), style);
52 }
53 }
54 }
55
56 map
57 }
58
59 pub fn style_for_path(&self, path: &Path) -> Option<AnsiStyle> {
61 if !self.has_ls_colors {
62 return None;
63 }
64
65 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
67 let ext_key = format!("*.{}", ext);
68 if let Some(style) = self.get_style(&ext_key) {
69 return Some(style);
70 }
71 }
72
73 let file_type_key = self.determine_file_type_key(path);
75 if let Some(style) = self.get_style(&file_type_key) {
76 return Some(style);
77 }
78
79 self.get_style("fi") }
82
83 fn get_style(&self, key: &str) -> Option<AnsiStyle> {
85 if let Some(style) = self.ls_colors_map.get(key) {
86 return Some(*style);
87 }
88
89 if key.starts_with("*.")
91 && let Some(style) = self.ls_colors_map.get("fi")
92 {
93 return Some(*style);
95 }
96
97 None
98 }
99
100 pub fn determine_file_type_key(&self, path: &Path) -> String {
104 let path_str = path.to_string_lossy();
106 if path_str.ends_with('/') || path_str.ends_with('\\') {
107 return "di".to_string(); }
109
110 if let Some(name) = path.file_name().and_then(|n| n.to_str())
112 && (name.ends_with(".sh")
113 || name.ends_with(".py")
114 || name.ends_with(".rb")
115 || name.ends_with(".pl")
116 || name.ends_with(".php"))
117 {
118 return "ex".to_string(); }
120
121 match path.file_name().and_then(|n| n.to_str()) {
123 Some(name) if name.starts_with('.') => "so".to_string(), _ => "fi".to_string(), }
126 }
127}
128
129impl Default for FileColorizer {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_new_without_ls_colors() {
141 let ls_colors_val = std::env::var("LS_COLORS").unwrap_or_default();
144 let colorizer = FileColorizer::new();
145
146 if ls_colors_val.is_empty() {
148 assert!(!colorizer.has_ls_colors);
149 assert!(colorizer.ls_colors_map.is_empty());
150 }
151 }
152
153 #[test]
154 fn test_parse_ls_colors() {
155 let ls_colors = "di=01;34:ln=01;36:ex=01;32:*rs=00;35";
156 let map = FileColorizer::parse_ls_colors(ls_colors);
157
158 assert_eq!(map.len(), 4);
159 assert!(map.contains_key("di"));
160 assert!(map.contains_key("ln"));
161 assert!(map.contains_key("ex"));
162 assert!(map.contains_key("*rs"));
163 }
164
165 #[test]
166 fn test_style_for_path_no_ls_colors() {
167 let colorizer = FileColorizer {
169 ls_colors_map: HashMap::new(),
170 has_ls_colors: false,
171 };
172
173 let path = Path::new("/tmp/test.rs");
174 let style = colorizer.style_for_path(path);
175
176 assert!(style.is_none());
177 }
178
179 #[test]
180 fn test_determine_file_type_key_directory() {
181 let colorizer = FileColorizer {
183 ls_colors_map: {
184 let mut map = HashMap::new();
185 map.insert("di".to_string(), anstyle::Style::new().bold());
186 map
187 },
188 has_ls_colors: true,
189 };
190
191 assert_eq!(
194 colorizer.determine_file_type_key(Path::new("/tmp/dir/")),
195 "di"
196 );
197 }
198
199 #[test]
200 fn test_determine_file_type_key_extension() {
201 let colorizer = FileColorizer {
202 ls_colors_map: {
203 let mut map = HashMap::new();
204 map.insert("*.rs".to_string(), anstyle::Style::new().bold());
205 map.insert("fi".to_string(), anstyle::Style::new().underline());
206 map
207 },
208 has_ls_colors: true,
209 };
210
211 let rs_path = Path::new("/tmp/test.rs");
212 let rs_style = colorizer.style_for_path(rs_path);
213 assert!(rs_style.is_some());
214
215 let txt_path = Path::new("/tmp/test.txt");
216 let txt_style = colorizer.style_for_path(txt_path);
217 assert!(txt_style.is_some()); }
219
220 #[test]
221 fn test_determine_file_type_key_executables() {
222 let colorizer = FileColorizer {
223 ls_colors_map: {
224 let mut map = HashMap::new();
225 map.insert("ex".to_string(), anstyle::Style::new().bold());
226 map
227 },
228 has_ls_colors: true,
229 };
230
231 assert_eq!(
232 colorizer.determine_file_type_key(Path::new("/tmp/script.sh")),
233 "ex"
234 );
235 assert_eq!(
236 colorizer.determine_file_type_key(Path::new("/tmp/main.py")),
237 "ex"
238 );
239 assert_eq!(
240 colorizer.determine_file_type_key(Path::new("/tmp/main.rb")),
241 "ex"
242 );
243 }
244}