diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index ed530533..9a7a86c3 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -1,5 +1,8 @@ use bitflags::bitflags; -use std::cmp::{max, min}; +use std::{ + cmp::{max, min}, + str::FromStr, +}; #[derive(Debug, Clone, Copy, PartialEq)] /// UNSTABLE @@ -237,6 +240,25 @@ bitflags! { } } +impl FromStr for Modifier { + type Err = &'static str; + + fn from_str(modifier: &str) -> Result<Self, Self::Err> { + match modifier { + "bold" => Ok(Self::BOLD), + "dim" => Ok(Self::DIM), + "italic" => Ok(Self::ITALIC), + "underlined" => Ok(Self::UNDERLINED), + "slow_blink" => Ok(Self::SLOW_BLINK), + "rapid_blink" => Ok(Self::RAPID_BLINK), + "reversed" => Ok(Self::REVERSED), + "hidden" => Ok(Self::HIDDEN), + "crossed_out" => Ok(Self::CROSSED_OUT), + _ => Err("Invalid modifier"), + } + } +} + /// Style let you control the main characteristics of the displayed elements. /// /// ```rust diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 756e34f6..74b817d0 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + convert::TryFrom, path::{Path, PathBuf}, }; @@ -11,8 +12,6 @@ use toml::Value; pub use crate::graphics::{Color, Modifier, Style}; -/// Color theme for syntax highlighting. - pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") }); @@ -54,22 +53,10 @@ impl Loader { .map(|entries| { entries .filter_map(|entry| { - if let Ok(entry) = entry { - let path = entry.path(); - if let Some(ext) = path.extension() { - if ext != "toml" { - return None; - } - return Some( - entry - .file_name() - .to_string_lossy() - .trim_end_matches(".toml") - .to_owned(), - ); - } - } - None + let entry = entry.ok()?; + let path = entry.path(); + (path.extension()? == "toml") + .then(|| path.file_stem().unwrap().to_string_lossy().into_owned()) }) .collect() }) @@ -103,13 +90,23 @@ impl<'de> Deserialize<'de> for Theme { let mut styles = HashMap::new(); if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) { - let palette = parse_palette(colors.remove("palette")); - // scopes.reserve(colors.len()); + // TODO: alert user of parsing failures in editor + let palette = colors + .remove("palette") + .map(|value| { + ThemePalette::try_from(value).unwrap_or_else(|err| { + warn!("{}", err); + ThemePalette::default() + }) + }) + .unwrap_or_default(); + styles.reserve(colors.len()); for (name, style_value) in colors { let mut style = Style::default(); - parse_style(&mut style, style_value, &palette); - // scopes.push(name); + if let Err(err) = palette.parse_style(&mut style, style_value) { + warn!("{}", err); + } styles.insert(name, style); } } @@ -119,104 +116,6 @@ impl<'de> Deserialize<'de> for Theme { } } -fn parse_palette(value: Option<Value>) -> HashMap<String, Color> { - match value { - Some(Value::Table(entries)) => entries, - _ => return HashMap::default(), - } - .into_iter() - .filter_map(|(name, value)| { - let color = parse_color(value, &HashMap::default())?; - Some((name, color)) - }) - .collect() -} - -fn parse_style(style: &mut Style, value: Value, palette: &HashMap<String, Color>) { - //TODO: alert user of parsing failures - if let Value::Table(entries) = value { - for (name, value) in entries { - match name.as_str() { - "fg" => { - if let Some(color) = parse_color(value, palette) { - *style = style.fg(color); - } - } - "bg" => { - if let Some(color) = parse_color(value, palette) { - *style = style.bg(color); - } - } - "modifiers" => { - if let Value::Array(arr) = value { - for modifier in arr.iter().filter_map(parse_modifier) { - *style = style.add_modifier(modifier); - } - } - } - _ => (), - } - } - } else if let Some(color) = parse_color(value, palette) { - *style = style.fg(color); - } -} - -fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> { - if s.starts_with('#') && s.len() >= 7 { - if let (Ok(red), Ok(green), Ok(blue)) = ( - u8::from_str_radix(&s[1..3], 16), - u8::from_str_radix(&s[3..5], 16), - u8::from_str_radix(&s[5..7], 16), - ) { - Some((red, green, blue)) - } else { - None - } - } else { - None - } -} - -fn parse_color(value: Value, palette: &HashMap<String, Color>) -> Option<Color> { - if let Value::String(s) = value { - if let Some(color) = palette.get(&s) { - Some(*color) - } else if let Some((red, green, blue)) = hex_string_to_rgb(&s) { - Some(Color::Rgb(red, green, blue)) - } else { - warn!("malformed hexcode in theme: {}", s); - None - } - } else { - warn!("unrecognized value in theme: {}", value); - None - } -} - -fn parse_modifier(value: &Value) -> Option<Modifier> { - if let Value::String(s) = value { - match s.as_str() { - "bold" => Some(Modifier::BOLD), - "dim" => Some(Modifier::DIM), - "italic" => Some(Modifier::ITALIC), - "underlined" => Some(Modifier::UNDERLINED), - "slow_blink" => Some(Modifier::SLOW_BLINK), - "rapid_blink" => Some(Modifier::RAPID_BLINK), - "reversed" => Some(Modifier::REVERSED), - "hidden" => Some(Modifier::HIDDEN), - "crossed_out" => Some(Modifier::CROSSED_OUT), - _ => { - warn!("unrecognized modifier in theme: {}", s); - None - } - } - } else { - warn!("unrecognized modifier in theme: {}", value); - None - } -} - impl Theme { pub fn get(&self, scope: &str) -> Style { self.try_get(scope) @@ -237,28 +136,123 @@ impl Theme { } } +struct ThemePalette { + palette: HashMap<String, Color>, +} + +impl Default for ThemePalette { + fn default() -> Self { + Self::new(HashMap::new()) + } +} + +impl ThemePalette { + pub fn new(palette: HashMap<String, Color>) -> Self { + Self { palette } + } + + pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> { + if s.starts_with('#') && s.len() >= 7 { + if let (Ok(red), Ok(green), Ok(blue)) = ( + u8::from_str_radix(&s[1..3], 16), + u8::from_str_radix(&s[3..5], 16), + u8::from_str_radix(&s[5..7], 16), + ) { + return Ok(Color::Rgb(red, green, blue)); + } + } + + Err(format!("Theme: malformed hexcode: {}", s)) + } + + fn parse_value_as_str(value: &Value) -> Result<&str, String> { + value + .as_str() + .ok_or(format!("Theme: unrecognized value: {}", value)) + } + + pub fn parse_color(&self, value: Value) -> Result<Color, String> { + let value = Self::parse_value_as_str(&value)?; + + self.palette + .get(value) + .copied() + .ok_or("") + .or_else(|_| Self::hex_string_to_rgb(value)) + } + + pub fn parse_modifier(value: &Value) -> Result<Modifier, String> { + value + .as_str() + .and_then(|s| s.parse().ok()) + .ok_or(format!("Theme: invalid modifier: {}", value)) + } + + pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> { + if let Value::Table(entries) = value { + for (name, value) in entries { + match name.as_str() { + "fg" => *style = style.fg(self.parse_color(value)?), + "bg" => *style = style.bg(self.parse_color(value)?), + "modifiers" => { + let modifiers = value + .as_array() + .ok_or("Theme: modifiers should be an array")?; + + for modifier in modifiers { + *style = style.add_modifier(Self::parse_modifier(modifier)?); + } + } + _ => return Err(format!("Theme: invalid style attribute: {}", name)), + } + } + } else { + *style = style.fg(self.parse_color(value)?); + } + Ok(()) + } +} + +impl TryFrom<Value> for ThemePalette { + type Error = String; + + fn try_from(value: Value) -> Result<Self, Self::Error> { + let map = match value { + Value::Table(entries) => entries, + _ => return Ok(Self::default()), + }; + + let mut palette = HashMap::with_capacity(map.len()); + for (name, value) in map { + let value = Self::parse_value_as_str(&value)?; + let color = Self::hex_string_to_rgb(value)?; + palette.insert(name, color); + } + + Ok(Self::new(palette)) + } +} + #[test] fn test_parse_style_string() { let fg = Value::String("#ffffff".to_string()); let mut style = Style::default(); - parse_style(&mut style, fg, &HashMap::default()); + let palette = ThemePalette::default(); + palette.parse_style(&mut style, fg).unwrap(); assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255))); } #[test] fn test_palette() { + use helix_core::hashmap; let fg = Value::String("my_color".to_string()); let mut style = Style::default(); - parse_style( - &mut style, - fg, - &vec![("my_color".to_string(), Color::Rgb(255, 255, 255))] - .into_iter() - .collect(), - ); + let palette = + ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) }); + palette.parse_style(&mut style, fg).unwrap(); assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255))); } @@ -274,9 +268,10 @@ fn test_parse_style_table() { }; let mut style = Style::default(); + let palette = ThemePalette::default(); if let Value::Table(entries) = table { for (_name, value) in entries { - parse_style(&mut style, value, &HashMap::default()); + palette.parse_style(&mut style, value).unwrap(); } }