1use std::collections::HashMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use color_eyre::Help;
7use hex::FromHex;
8use serde_yaml::Value;
9use syntect::easy::HighlightLines;
10use syntect::highlighting::{Color, FontStyle, Style, StyleModifier, ThemeSet, ThemeSettings};
11use syntect::parsing::{SyntaxDefinition, SyntaxSet};
12use syntect::util::LinesWithEndings;
13
14use crate::errors::LostTheWay;
15use crate::utils;
16
17#[derive(Debug, PartialEq, Serialize, Deserialize)]
19struct LanguageYML {
20 #[serde(default)]
21 extensions: Vec<String>,
22 #[serde(default)]
23 aliases: Vec<String>,
24 #[serde(default)]
25 color: Option<String>,
26}
27
28#[derive(Debug, Clone)]
29pub struct Language {
30 name: String,
31 extension: String,
32 pub(crate) color: Color,
33}
34
35impl Default for Language {
36 fn default() -> Self {
37 Self::new(String::from("text"), String::from(".txt"), None).unwrap()
38 }
39}
40
41impl Language {
42 fn new(name: String, extension: String, color: Option<String>) -> color_eyre::Result<Self> {
43 Ok(Self {
44 name,
45 extension,
46 color: Self::get_color(color)?,
47 })
48 }
49
50 fn get_color(color_string: Option<String>) -> color_eyre::Result<Color> {
51 let mut language_color = [0; 3];
52 if let Some(color) = color_string {
53 language_color = <[u8; 3]>::from_hex(color.get(1..).unwrap_or("FFFFFF"))?;
54 }
55 Ok(Color {
56 r: language_color[0],
57 g: language_color[1],
58 b: language_color[2],
59 a: 0xFF,
60 })
61 }
62
63 pub(crate) fn get_extension(language_name: &str, languages: &HashMap<String, Self>) -> String {
65 let default = Self::default();
66 if let Some(l) = languages.get(language_name) {
67 l.extension.clone()
68 } else {
69 eprintln!(
70 "Couldn't find language {} in the list of extensions, defaulting to .txt",
71 language_name
72 );
73 default.extension
74 }
75 }
76}
77
78fn read_languages_from_yml(yml_string: &str) -> color_eyre::Result<HashMap<String, LanguageYML>> {
81 let language_strings: HashMap<String, Value> = serde_yaml::from_str(yml_string)?;
82 let mut languages = HashMap::with_capacity(language_strings.len());
83 for (key, value) in language_strings {
84 languages.insert(key, serde_yaml::from_value(value)?);
85 }
86 Ok(languages)
87}
88
89pub fn get_languages(yml_string: &str) -> color_eyre::Result<HashMap<String, Language>> {
91 let languages = read_languages_from_yml(yml_string)?;
92 let mut name_to_language = HashMap::new();
93 for (name, language_yml) in languages {
94 if let Some(extension) = language_yml.extensions.first() {
95 let mut language = Language::new(name.clone(), extension.clone(), language_yml.color)?;
96 name_to_language.insert(name.to_ascii_lowercase(), language.clone());
97 name_to_language.insert(name, language.clone());
98 for alias in language_yml.aliases {
99 language.name = alias.clone();
100 name_to_language.insert(alias, language.clone());
101 }
102 }
103 }
104 Ok(name_to_language)
105}
106
107pub(crate) struct CodeHighlight {
108 syntax_set: SyntaxSet,
109 theme_set: ThemeSet,
110 theme_name: String,
111 syntect_dir: PathBuf,
112 pub(crate) main_style: Style,
114 pub(crate) accent_style: Style,
116 pub(crate) tag_style: Style,
118 pub(crate) selection_style: Style,
120 pub(crate) skim_theme: String,
122}
123
124fn syntect_theme_to_skim_theme(settings: &ThemeSettings) -> String {
125 let mut theme = String::new();
126 if let Some(c) = settings.foreground {
127 theme.push_str(&format!("fg:#{},", hex::encode(vec![c.r, c.g, c.b,])));
128 }
129 if let Some(c) = settings.selection {
130 theme.push_str(&format!(
131 "current_match_bg:#{},",
132 hex::encode(vec![c.r, c.g, c.b,])
133 ));
134 }
135 if let Some(c) = settings.selection_foreground {
136 theme.push_str(&format!(
137 "current_match:#{},",
138 hex::encode(vec![c.r, c.g, c.b,])
139 ));
140 }
141 if theme[theme.len() - 1..].starts_with(',') {
142 theme.pop();
143 }
144 theme
145}
146
147impl CodeHighlight {
148 pub(crate) fn new(theme: &str, syntect_dir: PathBuf) -> color_eyre::Result<Self> {
151 let mut theme_set = ThemeSet::load_defaults();
152 theme_set
153 .add_from_folder(&syntect_dir)
154 .map_err(|_e| LostTheWay::ThemeError {
155 theme: String::from((syntect_dir).to_str().unwrap()),
156 })
157 .suggestion(format!(
158 "Make sure {:#?} is a valid directory that has .tmTheme files",
159 &syntect_dir
160 ))?;
161 let mut syntax_set = SyntaxSet::load_defaults_newlines().into_builder();
162 syntax_set
163 .add_from_folder(&syntect_dir, true)
164 .map_err(|_e| LostTheWay::ThemeError {
165 theme: String::from((syntect_dir).to_str().unwrap()),
166 })
167 .suggestion(format!(
168 "Make sure {:#?} is a valid directory that has .sublime-syntax files",
169 &syntect_dir
170 ))?;
171 let syntax_set = syntax_set.build();
172 let mut highlighter = Self {
173 skim_theme: syntect_theme_to_skim_theme(&theme_set.themes[theme].settings),
174 syntax_set,
175 theme_name: theme.into(),
176 theme_set,
177 syntect_dir,
178 main_style: Style::default(),
179 accent_style: Style::default(),
180 tag_style: Style::default(),
181 selection_style: Style::default(),
182 };
183 highlighter.set_styles();
184 Ok(highlighter)
185 }
186
187 fn set_styles(&mut self) {
189 self.set_main_style();
190 self.set_accent_style();
191 self.set_tag_style();
192 self.set_selection_style();
193 }
194
195 fn set_main_style(&mut self) {
197 let main_color = self.theme_set.themes[&self.theme_name]
198 .settings
199 .foreground
200 .unwrap_or(Color::WHITE);
201 self.main_style = self.main_style.apply(StyleModifier {
202 foreground: Some(main_color),
203 background: None,
204 font_style: Some(FontStyle::BOLD),
205 });
206 }
207
208 fn set_tag_style(&mut self) {
210 let tag_color = self.theme_set.themes[&self.theme_name]
211 .settings
212 .tags_foreground
213 .unwrap_or_else(|| {
214 self.theme_set.themes[&self.theme_name]
215 .settings
216 .line_highlight
217 .unwrap_or(self.main_style.foreground)
218 });
219 self.tag_style = self.tag_style.apply(StyleModifier {
220 foreground: Some(tag_color),
221 background: self.theme_set.themes[&self.theme_name].settings.background,
222 font_style: Some(FontStyle::ITALIC),
223 });
224 }
225
226 fn set_accent_style(&mut self) {
228 let accent_color = self.theme_set.themes[&self.theme_name]
229 .settings
230 .caret
231 .unwrap_or(self.main_style.foreground);
232 self.accent_style = self.accent_style.apply(StyleModifier {
233 foreground: Some(accent_color),
234 background: None,
235 font_style: None,
236 });
237 }
238
239 fn set_selection_style(&mut self) {
241 self.selection_style = self.selection_style.apply(StyleModifier {
242 foreground: self.theme_set.themes[&self.theme_name]
243 .settings
244 .selection_foreground,
245 background: self.theme_set.themes[&self.theme_name].settings.selection,
246 font_style: None,
247 });
248 }
249
250 pub(crate) fn set_theme(&mut self, theme_name: String) -> color_eyre::Result<()> {
252 if self.theme_set.themes.contains_key(&theme_name) {
253 self.theme_name = theme_name;
254 self.set_styles();
255 Ok(())
256 } else {
257 let error: color_eyre::Result<()> =
258 Err(LostTheWay::ThemeError { theme: theme_name }.into());
259 error.suggestion(
260 "That theme doesn't exist. \
261 Use `the-way themes list` to see all theme possibilities \
262 or use `the-way themes add` to add a new one.",
263 )
264 }
265 }
266
267 pub(crate) fn get_themes(&self) -> Vec<String> {
269 self.theme_set.themes.keys().cloned().collect()
270 }
271
272 pub(crate) fn get_theme_name(&self) -> String {
274 self.theme_name.clone()
275 }
276
277 pub(crate) fn add_theme(&mut self, theme_file: &Path) -> color_eyre::Result<String> {
281 let theme = ThemeSet::get_theme(theme_file)
282 .map_err(|_e| LostTheWay::ThemeError {
283 theme: theme_file.to_str().unwrap().into(),
284 })
285 .suggestion(format!(
286 "Couldn't load a theme from {}, are you sure this is a valid .tmTheme file?",
287 theme_file.display()
288 ))?;
289 let basename = theme_file
290 .file_stem()
291 .and_then(std::ffi::OsStr::to_str)
292 .ok_or(LostTheWay::ThemeError {
293 theme: theme_file.to_str().unwrap().into(),
294 })
295 .suggestion("Something's fishy with the filename, valid Unicode?")?;
296 let new_theme_file = self.syntect_dir.join(format!("{basename}.tmTheme"));
298 fs::copy(theme_file, new_theme_file)?;
299 self.theme_set.themes.insert(basename.to_owned(), theme);
300 Ok(basename.to_owned())
301 }
302
303 pub(crate) fn add_syntax(&mut self, syntax_file: &Path) -> color_eyre::Result<String> {
306 let syntax = SyntaxDefinition::load_from_str(
307 &fs::read_to_string(syntax_file)?,
308 true,
309 None,
310 )
311 .map_err(|e| LostTheWay::SyntaxError {
312 syntax_file: syntax_file.to_str().unwrap().into(),
313 message: e.to_string(),
314 })
315 .suggestion(format!(
316 "Couldn't load a syntax from {}, are you sure this is a valid .sublime-syntax file with a \'name\' key?",
317 syntax_file.display()
318 ))?;
319 let filename = syntax_file
320 .file_name()
321 .and_then(std::ffi::OsStr::to_str)
322 .ok_or(LostTheWay::SyntaxError {
323 syntax_file: syntax_file.to_str().unwrap().into(),
324 message: String::from("Filename is not valid Unicode"),
325 })
326 .suggestion("Something's fishy with the filename, valid Unicode?")?;
327 let new_syntax_file = self.syntect_dir.join(filename);
329 fs::copy(syntax_file, new_syntax_file)?;
330 Ok(syntax.name)
331 }
332
333 pub(crate) fn highlight_block(language_color: Color) -> (Style, String) {
335 (
336 Style::default().apply(StyleModifier {
337 foreground: Some(language_color),
338 background: None,
339 font_style: None,
340 }),
341 format!("{} ", utils::BOX),
342 )
343 }
344
345 pub(crate) fn highlight_code(
347 &self,
348 code: &str,
349 extension: &str,
350 ) -> color_eyre::Result<Vec<(Style, String)>> {
351 let mut colorized = Vec::new();
352 let extension = extension.split('.').nth(1).unwrap_or("txt");
353 let syntax = self.syntax_set.find_syntax_by_extension(extension);
354 let syntax = match syntax {
355 Some(syntax) => syntax,
356 None => self.syntax_set.find_syntax_by_extension("txt").unwrap(),
357 };
358 let mut h = HighlightLines::new(syntax, &self.theme_set.themes[&self.theme_name]);
359 for line in LinesWithEndings::from(code) {
360 colorized.extend(
361 h.highlight_line(line, &self.syntax_set)?
362 .into_iter()
363 .map(|(style, s)| (style, s.to_owned())),
364 );
365 }
366 Ok(colorized)
367 }
368}