@@ -1,5 +1,6 @@
2 collections::HashMap,
2 collections::HashMap,
3 path::{Path, PathBuf},
4 path::{Path, PathBuf},
@@ -11,8 +12,6 @@ use toml::Value;
12pub use crate::graphics::{Color, Modifier, Style};
13pub use crate::graphics::{Color, Modifier, Style};
14−/// Color theme for syntax highlighting.
16pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
15pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
17 toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
16 toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
@@ -54,22 +53,10 @@ impl Loader {
54 .map(|entries| {
53 .map(|entries| {
56 .filter_map(|entry| {
55 .filter_map(|entry| {
57− if let Ok(entry) = entry {
56+ let entry = entry.ok()?;
58− let path = entry.path();
57+ let path = entry.path();
59− if let Some(ext) = path.extension() {
58+ (path.extension()? == "toml")
66− .to_string_lossy()
59+ .then(|| path.file_stem().unwrap().to_string_lossy().into_owned())
67− .trim_end_matches(".toml")
74 .collect()
61 .collect()
@@ -103,13 +90,23 @@ impl<'de> Deserialize<'de> for Theme {
103 let mut styles = HashMap::new();
90 let mut styles = HashMap::new();
105 if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
92 if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
93+ // TODO: alert user of parsing failures in editor
106− let palette = parse_palette(colors.remove("palette"));
94+ let palette = colors
107− // scopes.reserve(colors.len());
95+ .remove("palette")
97+ ThemePalette::try_from(value).unwrap_or_else(|err| {
99+ ThemePalette::default()
102+ .unwrap_or_default();
108 styles.reserve(colors.len());
104 styles.reserve(colors.len());
109 for (name, style_value) in colors {
105 for (name, style_value) in colors {
110 let mut style = Style::default();
106 let mut style = Style::default();
111− parse_style(&mut style, style_value, &palette);
107+ if let Err(err) = palette.parse_style(&mut style, style_value) {
112− // scopes.push(name);
108+ warn!("{}", err);
113 styles.insert(name, style);
110 styles.insert(name, style);
@@ -119,121 +116,120 @@ impl<'de> Deserialize<'de> for Theme {
122−fn parse_palette(value: Option<Value>) -> HashMap<String, Color> {
119+impl Theme {
123− match value {
120+ pub fn get(&self, scope: &str) -> Style {
124− Some(Value::Table(entries)) => entries,
121+ self.try_get(scope)
125− _ => return HashMap::default(),
122+ .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
128− .filter_map(|(name, value)| {
129− let color = parse_color(value, &HashMap::default())?;
135−fn parse_style(style: &mut Style, value: Value, palette: &HashMap<String, Color>) {
125+ pub fn try_get(&self, scope: &str) -> Option<Style> {
136− //TODO: alert user of parsing failures
126+ self.styles.get(scope).copied()
137− if let Value::Table(entries) = value {
138− for (name, value) in entries {
139− match name.as_str() {
141− if let Some(color) = parse_color(value, palette) {
142− *style = style.fg(color);
146− if let Some(color) = parse_color(value, palette) {
147− *style = style.bg(color);
151− if let Value::Array(arr) = value {
152− for modifier in arr.iter().filter_map(parse_modifier) {
153− *style = style.add_modifier(modifier);
160− } else if let Some(color) = parse_color(value, palette) {
161− *style = style.fg(color);
165−fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> {
129+ #[inline]
166− if s.starts_with('#') && s.len() >= 7 {
130+ pub fn scopes(&self) -> &[String] {
167− if let (Ok(red), Ok(green), Ok(blue)) = (
131+ &self.scopes
168− u8::from_str_radix(&s[1..3], 16),
132+ }
169− u8::from_str_radix(&s[3..5], 16),
133+
170− u8::from_str_radix(&s[5..7], 16),
134+ pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
171− ) {
135+ self.scopes().iter().position(|s| s == scope)
172− Some((red, green, blue))
181−fn parse_color(value: Value, palette: &HashMap<String, Color>) -> Option<Color> {
139+struct ThemePalette {
182− if let Value::String(s) = value {
140+ palette: HashMap<String, Color>,
183− if let Some(color) = palette.get(&s) {
141+}
185− } else if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
143+impl Default for ThemePalette {
186− Some(Color::Rgb(red, green, blue))
144+ fn default() -> Self {
187− } else {
145+ Self::new(HashMap::new())
188− warn!("malformed hexcode in theme: {}", s);
192− warn!("unrecognized value in theme: {}", value);
197−fn parse_modifier(value: &Value) -> Option<Modifier> {
149+impl ThemePalette {
198− if let Value::String(s) = value {
150+ pub fn new(palette: HashMap<String, Color>) -> Self {
199− match s.as_str() {
151+ Self { palette }
200− "bold" => Some(Modifier::BOLD),
152+ }
201− "dim" => Some(Modifier::DIM),
153+
202− "italic" => Some(Modifier::ITALIC),
154+ pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
203− "underlined" => Some(Modifier::UNDERLINED),
155+ if s.starts_with('#') && s.len() >= 7 {
204− "slow_blink" => Some(Modifier::SLOW_BLINK),
156+ if let (Ok(red), Ok(green), Ok(blue)) = (
205− "rapid_blink" => Some(Modifier::RAPID_BLINK),
157+ u8::from_str_radix(&s[1..3], 16),
206− "reversed" => Some(Modifier::REVERSED),
158+ u8::from_str_radix(&s[3..5], 16),
207− "hidden" => Some(Modifier::HIDDEN),
159+ u8::from_str_radix(&s[5..7], 16),
208− "crossed_out" => Some(Modifier::CROSSED_OUT),
160+ ) {
209− _ => {
161+ return Ok(Color::Rgb(red, green, blue));
210− warn!("unrecognized modifier in theme: {}", s);
215− warn!("unrecognized modifier in theme: {}", value);
165+ Err(format!("Theme: malformed hexcode: {}", s))
220−impl Theme {
168+ fn parse_value_as_str(value: &Value) -> Result<&str, String> {
221− pub fn get(&self, scope: &str) -> Style {
169+ value
222− self.try_get(scope)
170+ .as_str()
223− .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
171+ .ok_or(format!("Theme: unrecognized value: {}", value))
226− pub fn try_get(&self, scope: &str) -> Option<Style> {
174+ pub fn parse_color(&self, value: Value) -> Result<Color, String> {
175+ let value = Self::parse_value_as_str(&value)?;
227− self.styles.get(scope).copied()
179+ .copied()
181+ .or_else(|_| Self::hex_string_to_rgb(value))
230− #[inline]
184+ pub fn parse_modifier(value: &Value) -> Result<Modifier, String> {
231− pub fn scopes(&self) -> &[String] {
185+ value
232− &self.scopes
186+ .as_str()
187+ .and_then(|s| s.parse().ok())
188+ .ok_or(format!("Theme: invalid modifier: {}", value))
235− pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
191+ pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
236− self.scopes().iter().position(|s| s == scope)
192+ if let Value::Table(entries) = value {
193+ for (name, value) in entries {
194+ match name.as_str() {
195+ "fg" => *style = style.fg(self.parse_color(value)?),
196+ "bg" => *style = style.bg(self.parse_color(value)?),
198+ let modifiers = value
200+ .ok_or("Theme: modifiers should be an array")?;
202+ for modifier in modifiers {
203+ *style = style.add_modifier(Self::parse_modifier(modifier)?);
206+ _ => return Err(format!("Theme: invalid style attribute: {}", name)),
210+ *style = style.fg(self.parse_color(value)?);
216+impl TryFrom<Value> for ThemePalette {
217+ type Error = String;
219+ fn try_from(value: Value) -> Result<Self, Self::Error> {
220+ let map = match value {
221+ Value::Table(entries) => entries,
222+ _ => return Ok(Self::default()),
225+ let mut palette = HashMap::with_capacity(map.len());
226+ for (name, value) in map {
227+ let value = Self::parse_value_as_str(&value)?;
228+ let color = Self::hex_string_to_rgb(value)?;
229+ palette.insert(name, color);
232+ Ok(Self::new(palette))
@@ -242,23 +238,21 @@ fn test_parse_style_string() {
242 let fg = Value::String("#ffffff".to_string());
238 let fg = Value::String("#ffffff".to_string());
244 let mut style = Style::default();
240 let mut style = Style::default();
241+ let palette = ThemePalette::default();
245− parse_style(&mut style, fg, &HashMap::default());
242+ palette.parse_style(&mut style, fg).unwrap();
247 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
244 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
251fn test_palette() {
248fn test_palette() {
249+ use helix_core::hashmap;
252 let fg = Value::String("my_color".to_string());
250 let fg = Value::String("my_color".to_string());
254 let mut style = Style::default();
252 let mut style = Style::default();
255− parse_style(
253+ let palette =
258− &vec![("my_color".to_string(), Color::Rgb(255, 255, 255))]
254+ ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
259− .into_iter()
255+ palette.parse_style(&mut style, fg).unwrap();
263 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
257 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
@@ -274,9 +268,10 @@ fn test_parse_style_table() {
276 let mut style = Style::default();
270 let mut style = Style::default();
271+ let palette = ThemePalette::default();
277 if let Value::Table(entries) = table {
272 if let Value::Table(entries) = table {
278 for (_name, value) in entries {
273 for (_name, value) in entries {
279− parse_style(&mut style, value, &HashMap::default());
274+ palette.parse_style(&mut style, value).unwrap();