1use std::{collections::BTreeMap, fmt::Write};
4
5use rgb::RGB8;
6
7#[derive(Debug, Clone, Default)]
9pub struct Theme {
10 pub name: String,
12 pub default: Style,
14 pub styles: BTreeMap<String, Style>,
16 pub rainbow: Vec<Color>,
18 pub line_highlight: Style,
20}
21
22#[derive(Debug, Clone, Default)]
24pub struct Style {
25 pub fg: Option<Color>,
27 pub bg: Option<Color>,
29 pub modifiers: Vec<Modifier>,
31 pub underline: Option<Underline>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Color {
38 Rgb(RGB8),
40 Indexed(u8),
42 Default,
44}
45
46#[derive(Debug, Clone, Default)]
48pub struct Underline {
49 pub color: Option<Color>,
51 pub style: Option<UnderlineStyle>,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum UnderlineStyle {
58 Line,
60 Curl,
62 Dashed,
64 Dotted,
66 DoubleLine,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Modifier {
73 Bold,
75 Dim,
77 Italic,
79 SlowBlink,
81 RapidBlink,
83 Reversed,
85 Hidden,
87 CrossedOut,
89}
90
91impl Theme {
92 pub fn style(&self, capture: &str) -> Style {
114 let mut result = Style {
115 fg: self.default.fg,
116 bg: self.default.bg,
117 underline: self.default.underline.clone(),
118 modifiers: Vec::new(),
119 };
120 let mut modifiers = &self.default.modifiers;
121 let normalized = capture.trim_start_matches('.');
122 let mut end = 0;
123
124 for part in capture.split('.') {
125 if end > 0 {
126 end += 1;
127 }
128 end += part.len();
129
130 if let Some(style) = self.styles.get(&normalized[..end]) {
131 if style.fg.is_some() {
132 result.fg = style.fg;
133 }
134
135 if style.bg.is_some() {
136 result.bg = style.bg;
137 }
138
139 modifiers = &style.modifiers;
140
141 if style.underline.is_some() {
142 result.underline.clone_from(&style.underline);
143 }
144 }
145 }
146
147 result.modifiers = modifiers.clone();
148 result
149 }
150
151 #[cfg(feature = "theme-toml")]
158 pub fn from_toml(source: &str) -> Result<Self, crate::Error> {
159 let value: toml::Value =
160 toml::from_str(source).map_err(|error| crate::Error::Theme(error.to_string()))?;
161 let mut theme = Self {
162 name: value
163 .get("name")
164 .and_then(toml::Value::as_str)
165 .unwrap_or_default()
166 .to_owned(),
167 ..Self::default()
168 };
169
170 if let Some(default) = value.get("default") {
171 theme.default = parse_style(default)?;
172 }
173
174 if let Some(styles) = value.get("styles") {
175 let styles = styles
176 .as_table()
177 .ok_or_else(|| crate::Error::Theme("styles must be a table".into()))?;
178
179 for (name, style) in styles {
180 theme.styles.insert(name.clone(), parse_style(style)?);
181 }
182 }
183
184 if let Some(colors) = value.get("rainbow") {
185 let colors = colors
186 .as_array()
187 .ok_or_else(|| crate::Error::Theme("rainbow must be an array".into()))?;
188
189 for color in colors {
190 theme.rainbow.push(parse_color(color)?);
191 }
192 }
193
194 if let Some(style) = value.get("line_highlight") {
195 theme.line_highlight = parse_style(style)?;
196 }
197
198 Ok(theme)
199 }
200}
201
202impl Color {
203 pub fn to_css(self) -> String {
205 let mut css = String::new();
206 self.write_css(&mut css);
207 css
208 }
209
210 fn write_css(self, css: &mut String) {
211 let color = match self {
212 Self::Default => {
213 css.push_str("inherit");
214 return;
215 }
216 Self::Rgb(color) => color,
217 Self::Indexed(index) if index < 16 => {
218 const COLORS: [(u8, u8, u8); 16] = [
219 (0, 0, 0),
220 (128, 0, 0),
221 (0, 128, 0),
222 (128, 128, 0),
223 (0, 0, 128),
224 (128, 0, 128),
225 (0, 128, 128),
226 (192, 192, 192),
227 (128, 128, 128),
228 (255, 0, 0),
229 (0, 255, 0),
230 (255, 255, 0),
231 (0, 0, 255),
232 (255, 0, 255),
233 (0, 255, 255),
234 (255, 255, 255),
235 ];
236 COLORS[index as usize].into()
237 }
238
239 Self::Indexed(index) if index >= 232 => {
240 let shade = 8 + (index - 232) * 10;
241 RGB8::new(shade, shade, shade)
242 }
243
244 Self::Indexed(index) => {
245 let index = index - 16;
246 let level = |value: u8| if value == 0 { 0 } else { 55 + value * 40 };
247 RGB8::new(level(index / 36), level(index / 6 % 6), level(index % 6))
248 }
249 };
250
251 write!(css, "#{:02x}{:02x}{:02x}", color.r, color.g, color.b).unwrap();
252 }
253}
254
255impl Style {
256 pub fn to_css(&self) -> String {
261 let mut css = String::new();
262 self.write_css(&mut css);
263 css
264 }
265
266 pub(crate) fn write_css(&self, css: &mut String) {
268 let (foreground, background) = if self.modifiers.contains(&Modifier::Reversed) {
269 (self.bg, self.fg)
270 } else {
271 (self.fg, self.bg)
272 };
273
274 if let Some(color) = foreground {
275 css.push_str("color:");
276 color.write_css(css);
277 css.push(';');
278 }
279
280 if let Some(color) = background {
281 css.push_str("background-color:");
282 color.write_css(css);
283 css.push(';');
284 }
285
286 for modifier in &self.modifiers {
287 css.push_str(match modifier {
288 Modifier::Bold => "font-weight:bold;",
289 Modifier::Dim => "opacity:0.5;",
290 Modifier::Italic => "font-style:italic;",
291 Modifier::Hidden => "visibility:hidden;",
292 Modifier::CrossedOut => "text-decoration-line:line-through;",
293 Modifier::SlowBlink | Modifier::RapidBlink => "text-decoration-line:blink;",
294 Modifier::Reversed => "",
295 });
296 }
297
298 if let Some(underline) = &self.underline {
299 let crossed = self.modifiers.contains(&Modifier::CrossedOut);
300 css.push_str(if crossed {
301 "text-decoration-line:underline line-through;"
302 } else {
303 "text-decoration-line:underline;"
304 });
305 let style = match underline.style.unwrap_or(UnderlineStyle::Line) {
306 UnderlineStyle::Line => "solid",
307 UnderlineStyle::Curl => "wavy",
308 UnderlineStyle::Dashed => "dashed",
309 UnderlineStyle::Dotted => "dotted",
310 UnderlineStyle::DoubleLine => "double",
311 };
312
313 css.push_str("text-decoration-style:");
314 css.push_str(style);
315 css.push(';');
316
317 if let Some(color) = underline.color {
318 css.push_str("text-decoration-color:");
319 color.write_css(css);
320 css.push(';');
321 }
322 }
323 }
324}
325
326impl std::str::FromStr for Color {
327 type Err = crate::Error;
328
329 fn from_str(color: &str) -> Result<Self, Self::Err> {
335 match color {
336 "default" => Ok(Color::Default),
337 color
338 if color.len() == 4
339 && color.starts_with('#')
340 && color[1..].bytes().all(|byte| byte.is_ascii_hexdigit()) =>
341 {
342 let value = u16::from_str_radix(&color[1..], 16).unwrap();
343
344 Ok(Color::Rgb(RGB8::new(
345 ((value >> 8) as u8) * 17,
346 ((value >> 4 & 15) as u8) * 17,
347 ((value & 15) as u8) * 17,
348 )))
349 }
350 color
351 if color.len() == 7
352 && color.starts_with('#')
353 && color[1..].bytes().all(|byte| byte.is_ascii_hexdigit()) =>
354 {
355 let value = u32::from_str_radix(&color[1..], 16).unwrap();
356
357 Ok(Color::Rgb(RGB8::new(
358 (value >> 16) as u8,
359 (value >> 8) as u8,
360 value as u8,
361 )))
362 }
363 _ => Err(crate::Error::Theme(format!("invalid color: {color:?}"))),
364 }
365 }
366}
367
368#[cfg(feature = "theme-toml")]
369fn parse_color(value: &toml::Value) -> Result<Color, crate::Error> {
370 if let Some(index) = value
371 .as_integer()
372 .and_then(|index| u8::try_from(index).ok())
373 {
374 return Ok(Color::Indexed(index));
375 }
376
377 value
378 .as_str()
379 .and_then(|color| color.parse().ok())
380 .ok_or_else(|| crate::Error::Theme(format!("invalid color: {value}")))
381}
382
383#[cfg(feature = "theme-toml")]
384fn parse_style(value: &toml::Value) -> Result<Style, crate::Error> {
385 let value = value
386 .as_table()
387 .ok_or_else(|| crate::Error::Theme("style must be a table".into()))?;
388
389 let mut style = Style::default();
390
391 if let Some(color) = value.get("fg") {
392 style.fg = Some(parse_color(color)?);
393 }
394
395 if let Some(color) = value.get("bg") {
396 style.bg = Some(parse_color(color)?);
397 }
398
399 if let Some(modifiers) = value.get("modifiers") {
400 let modifiers = modifiers
401 .as_array()
402 .ok_or_else(|| crate::Error::Theme("modifiers must be an array".into()))?;
403
404 for modifier in modifiers {
405 style.modifiers.push(match modifier.as_str() {
406 Some("bold") => Modifier::Bold,
407 Some("dim") => Modifier::Dim,
408 Some("italic") => Modifier::Italic,
409 Some("slow_blink") => Modifier::SlowBlink,
410 Some("rapid_blink") => Modifier::RapidBlink,
411 Some("reversed") => Modifier::Reversed,
412 Some("hidden") => Modifier::Hidden,
413 Some("crossed_out") => Modifier::CrossedOut,
414 _ => return Err(crate::Error::Theme(format!("invalid modifier: {modifier}"))),
415 });
416 }
417 }
418
419 if let Some(value) = value.get("underline") {
420 let value = value
421 .as_table()
422 .ok_or_else(|| crate::Error::Theme("underline must be a table".into()))?;
423 let color = value.get("color").map(parse_color).transpose()?;
424 let underline = match value
425 .get("style")
426 .and_then(toml::Value::as_str)
427 .unwrap_or("line")
428 {
429 "line" => UnderlineStyle::Line,
430 "curl" => UnderlineStyle::Curl,
431 "dashed" => UnderlineStyle::Dashed,
432 "dotted" => UnderlineStyle::Dotted,
433 "double_line" => UnderlineStyle::DoubleLine,
434 other => {
435 return Err(crate::Error::Theme(format!(
436 "invalid underline style: {other}"
437 )));
438 }
439 };
440 style.underline = Some(Underline {
441 color,
442 style: Some(underline),
443 });
444 }
445
446 Ok(style)
447}