1use std::collections::{BTreeMap, HashMap, HashSet};
18use std::ffi::OsStr;
19use std::fmt;
20use std::fs;
21use std::iter::FromIterator;
22use std::path::{Path, PathBuf};
23
24use serde_json::{self, Value};
25use syntect::dumps::{dump_to_file, from_dump_file};
26use syntect::highlighting::StyleModifier as SynStyleModifier;
27use syntect::highlighting::{Color, Highlighter, Theme, ThemeSet};
28use syntect::LoadingError;
29
30pub use syntect::highlighting::ThemeSettings;
31
32pub const N_RESERVED_STYLES: usize = 8;
33const SYNTAX_PRIORITY_DEFAULT: u16 = 200;
34const SYNTAX_PRIORITY_LOWEST: u16 = 0;
35pub const DEFAULT_THEME: &str = "InspiredGitHub";
36
37#[derive(Clone, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
38pub struct Style {
44 #[serde(skip_serializing)]
47 pub priority: u16,
48 pub fg_color: Option<u32>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub bg_color: Option<u32>,
53 #[serde(skip_serializing_if = "Option::is_none")]
56 pub weight: Option<u16>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub underline: Option<bool>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub italic: Option<bool>,
61}
62
63impl Style {
64 pub fn from_syntect_style_mod(style: &SynStyleModifier) -> Self {
66 let font_style = style.font_style.map(|s| s.bits()).unwrap_or_default();
67 let weight = if (font_style & 1) != 0 { Some(700) } else { None };
68 let underline = if (font_style & 2) != 0 { Some(true) } else { None };
69 let italic = if (font_style & 4) != 0 { Some(true) } else { None };
70
71 Self::new(
72 SYNTAX_PRIORITY_DEFAULT,
73 style.foreground.map(Self::rgba_from_syntect_color),
74 style.background.map(Self::rgba_from_syntect_color),
75 weight,
76 underline,
77 italic,
78 )
79 }
80
81 pub fn new<O32, O16, OB>(
82 priority: u16,
83 fg_color: O32,
84 bg_color: O32,
85 weight: O16,
86 underline: OB,
87 italic: OB,
88 ) -> Self
89 where
90 O32: Into<Option<u32>>,
91 O16: Into<Option<u16>>,
92 OB: Into<Option<bool>>,
93 {
94 assert!(priority <= 1000);
95 Style {
96 priority,
97 fg_color: fg_color.into(),
98 bg_color: bg_color.into(),
99 weight: weight.into(),
100 underline: underline.into(),
101 italic: italic.into(),
102 }
103 }
104
105 pub fn default_for_theme(theme: &Theme) -> Self {
107 let fg = theme.settings.foreground.unwrap_or(Color::BLACK);
108 Style::new(
109 SYNTAX_PRIORITY_LOWEST,
110 Some(Self::rgba_from_syntect_color(fg)),
111 None,
112 None,
113 None,
114 None,
115 )
116 }
117
118 pub fn merge(&self, other: &Style) -> Style {
124 let (p1, p2) = if self.priority > other.priority { (self, other) } else { (other, self) };
125
126 Style::new(
127 p1.priority,
128 p1.fg_color.or(p2.fg_color),
129 p1.bg_color.or(p2.bg_color),
130 p1.weight.or(p2.weight),
131 p1.underline.or(p2.underline),
132 p1.italic.or(p2.italic),
133 )
134 }
135
136 pub fn to_json(&self, id: usize) -> Value {
140 let mut as_val = serde_json::to_value(self).expect("failed to encode style");
141 as_val["id"] = id.into();
142 as_val
143 }
144
145 fn rgba_from_syntect_color(color: Color) -> u32 {
146 let Color { r, g, b, a } = color;
147 ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
148 }
149}
150
151pub struct ThemeStyleMap {
153 themes: ThemeSet,
154 theme_name: String,
155 theme: Theme,
156
157 default_themes: Vec<String>,
159 default_style: Style,
160 map: HashMap<Style, usize>,
161
162 path_map: BTreeMap<String, PathBuf>,
164
165 styles: Vec<Style>,
168 themes_dir: Option<PathBuf>,
169 cache_dir: Option<PathBuf>,
170 caching_enabled: bool,
171}
172
173impl ThemeStyleMap {
174 pub fn new(themes_dir: Option<PathBuf>) -> ThemeStyleMap {
175 let themes = ThemeSet::load_defaults();
176 let theme_name = DEFAULT_THEME.to_owned();
177 let theme = themes.themes.get(&theme_name).expect("missing theme").to_owned();
178 let default_themes = themes.themes.keys().cloned().collect();
179 let default_style = Style::default_for_theme(&theme);
180 let cache_dir = None;
181 let caching_enabled = true;
182
183 ThemeStyleMap {
184 themes,
185 theme_name,
186 theme,
187 default_themes,
188 default_style,
189 map: HashMap::new(),
190 path_map: BTreeMap::new(),
191 styles: Vec::new(),
192 themes_dir,
193 cache_dir,
194 caching_enabled,
195 }
196 }
197
198 pub fn get_default_style(&self) -> &Style {
199 &self.default_style
200 }
201
202 pub fn get_highlighter(&self) -> Highlighter {
203 Highlighter::new(&self.theme)
204 }
205
206 pub fn get_theme_name(&self) -> &str {
207 &self.theme_name
208 }
209
210 pub fn get_theme_settings(&self) -> &ThemeSettings {
211 &self.theme.settings
212 }
213
214 pub fn get_theme_names(&self) -> Vec<String> {
215 self.path_map.keys().chain(self.default_themes.iter()).cloned().collect()
216 }
217
218 pub fn contains_theme(&self, k: &str) -> bool {
219 self.themes.themes.contains_key(k)
220 }
221
222 pub fn set_theme(&mut self, theme_name: &str) -> Result<(), &'static str> {
223 match self.load_theme(theme_name) {
224 Ok(()) => {
225 if let Some(new_theme) = self.themes.themes.get(theme_name) {
226 self.theme = new_theme.to_owned();
227 self.theme_name = theme_name.to_owned();
228 self.default_style = Style::default_for_theme(&self.theme);
229 self.map = HashMap::new();
230 self.styles = Vec::new();
231 Ok(())
232 } else {
233 Err("unknown theme")
234 }
235 }
236 Err(e) => {
237 error!("Encountered error {:?} while trying to load {:?}", e, theme_name);
238 Err("could not load theme")
239 }
240 }
241 }
242
243 pub fn merge_with_default(&self, style: &Style) -> Style {
244 self.default_style.merge(style)
245 }
246
247 pub fn lookup(&self, style: &Style) -> Option<usize> {
248 self.map.get(style).cloned()
249 }
250
251 pub fn add(&mut self, style: &Style) -> usize {
252 let result = self.styles.len() + N_RESERVED_STYLES;
253 self.map.insert(style.clone(), result);
254 self.styles.push(style.clone());
255 result
256 }
257
258 pub(crate) fn remove_theme(&mut self, path: &Path) -> Option<String> {
260 validate_theme_file(path).ok()?;
261
262 let theme_name = path.file_stem().and_then(OsStr::to_str)?;
263 self.themes.themes.remove(theme_name);
264 self.path_map.remove(theme_name);
265
266 let dump_p = self.get_dump_path(theme_name)?;
267 if dump_p.exists() {
268 let _ = fs::remove_file(dump_p);
269 }
270
271 Some(theme_name.to_string())
272 }
273
274 pub(crate) fn load_theme_dir(&mut self) {
276 if let Some(themes_dir) = self.themes_dir.clone() {
277 match ThemeSet::discover_theme_paths(themes_dir) {
278 Ok(themes) => {
279 self.caching_enabled = self.caching_enabled && self.init_cache_dir();
280 for theme_p in &themes {
283 match self.load_theme_info_from_path(theme_p) {
284 Ok(_) => (),
285 Err(e) => {
286 error!("Encountered error {:?} loading theme at {:?}", e, theme_p)
287 }
288 }
289 }
290 }
291 Err(e) => error!("Error loading themes dir: {:?}", e),
292 }
293 }
294 }
295
296 fn try_load_from_dump(&self, theme_p: &Path) -> Option<(String, Theme)> {
301 if !self.caching_enabled {
302 return None;
303 }
304
305 let theme_name = theme_p.file_stem().and_then(OsStr::to_str)?;
306
307 let dump_p = self.get_dump_path(theme_name)?;
308
309 if !&dump_p.exists() {
310 return None;
311 }
312
313 let mod_t = fs::metadata(&dump_p).and_then(|md| md.modified()).ok()?;
316 let mod_t_orig = fs::metadata(theme_p).and_then(|md| md.modified()).ok()?;
317
318 if mod_t >= mod_t_orig {
319 from_dump_file(&dump_p).ok().map(|t| (theme_name.to_owned(), t))
320 } else {
321 let _ = fs::remove_file(&dump_p);
323 None
324 }
325 }
326
327 pub(crate) fn load_theme_info_from_path(
329 &mut self,
330 theme_p: &Path,
331 ) -> Result<String, LoadingError> {
332 validate_theme_file(theme_p)?;
333 let theme_name =
334 theme_p.file_stem().and_then(OsStr::to_str).ok_or(LoadingError::BadPath)?;
335
336 self.path_map.insert(theme_name.to_string(), theme_p.to_path_buf());
337
338 Ok(theme_name.to_owned())
339 }
340
341 fn load_theme(&mut self, theme_name: &str) -> Result<(), LoadingError> {
345 if self.contains_theme(theme_name) && self.get_theme_name() != theme_name {
348 return Ok(());
349 }
350 let theme_p = &self.path_map.get(theme_name).cloned();
353 if let Some(theme_p) = theme_p {
354 match self.try_load_from_dump(theme_p) {
355 Some((dump_theme_name, dump_theme_data)) => {
356 self.insert_to_map(dump_theme_name, dump_theme_data);
357 }
358 None => {
359 let theme = ThemeSet::get_theme(theme_p)?;
360 if self.caching_enabled {
361 if let Some(dump_p) = self.get_dump_path(theme_name) {
362 let _ = dump_to_file(&theme, dump_p);
363 }
364 }
365 self.insert_to_map(theme_name.to_owned(), theme);
366 }
367 }
368 Ok(())
369 } else {
370 Err(LoadingError::BadPath)
371 }
372 }
373
374 fn insert_to_map(&mut self, k: String, v: Theme) {
375 self.themes.themes.insert(k, v);
376 }
377
378 fn get_dump_path(&self, theme_name: &str) -> Option<PathBuf> {
380 self.cache_dir.as_ref().map(|p| p.join(theme_name).with_extension("tmdump"))
381 }
382
383 pub(crate) fn sync_dir(&mut self, dir: Option<&Path>) {
386 if let Some(themes_dir) = dir {
387 if let Ok(paths) = ThemeSet::discover_theme_paths(themes_dir) {
388 let current_state: HashSet<PathBuf> = HashSet::from_iter(paths.into_iter());
389 let maintained_state: HashSet<PathBuf> =
390 HashSet::from_iter(self.path_map.values().cloned());
391
392 let to_insert = current_state.difference(&maintained_state);
393 for path in to_insert {
394 let _ = self.load_theme_info_from_path(path);
395 }
396 let to_remove = maintained_state.difference(¤t_state);
397 for path in to_remove {
398 self.remove_theme(path);
399 }
400 }
401 }
402 }
403 fn init_cache_dir(&mut self) -> bool {
407 self.cache_dir = self.themes_dir.clone().map(|p| p.join("cache"));
408
409 if let Some(ref p) = self.cache_dir {
410 if p.exists() {
411 return true;
412 }
413 fs::DirBuilder::new().create(&p).is_ok()
414 } else {
415 false
416 }
417 }
418}
419
420fn validate_theme_file(path: &Path) -> Result<bool, LoadingError> {
422 path.extension().map(|e| e != "tmTheme").ok_or(LoadingError::BadPath)
423}
424
425impl fmt::Debug for Style {
426 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
427 fn fmt_color(f: &mut fmt::Formatter, c: Option<u32>) -> fmt::Result {
428 if let Some(c) = c {
429 write!(f, "#{:X}", c)
430 } else {
431 write!(f, "None")
432 }
433 }
434
435 write!(f, "Style( P{}, fg: ", self.priority)?;
436 fmt_color(f, self.fg_color)?;
437 write!(f, " bg: ")?;
438 fmt_color(f, self.bg_color)?;
439
440 if let Some(w) = self.weight {
441 write!(f, " weight {}", w)?;
442 }
443 if let Some(i) = self.italic {
444 write!(f, " ital: {}", i)?;
445 }
446 if let Some(u) = self.underline {
447 write!(f, " uline: {}", u)?;
448 }
449 write!(f, " )")
450 }
451}