on 2158366 help sign in
helix/helix-view/src/theme.rs
285 lines 8.3 kB history raw
1use std::{
2 collections::HashMap,
3 convert::TryFrom,
4 path::{Path, PathBuf},
5};
7use anyhow::Context;
8use log::warn;
9use once_cell::sync::Lazy;
10use serde::{Deserialize, Deserializer};
11use toml::Value;
13pub use crate::graphics::{Color, Modifier, Style};
15pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
16 toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
17});
19#[derive(Clone, Debug)]
20pub struct Loader {
21 user_dir: PathBuf,
22 default_dir: PathBuf,
24impl Loader {
25 /// Creates a new loader that can load themes from two directories.
26 pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
27 Self {
28 user_dir: user_dir.as_ref().join("themes"),
29 default_dir: default_dir.as_ref().join("themes"),
30 }
31 }
33 /// Loads a theme first looking in the `user_dir` then in `default_dir`
34 pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> {
35 if name == "default" {
36 return Ok(self.default());
37 }
38 let filename = format!("{}.toml", name);
40 let user_path = self.user_dir.join(&filename);
41 let path = if user_path.exists() {
42 user_path
43 } else {
44 self.default_dir.join(filename)
45 };
47 let data = std::fs::read(&path)?;
48 toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
49 }
51 pub fn read_names(path: &Path) -> Vec<String> {
52 std::fs::read_dir(path)
53 .map(|entries| {
54 entries
55 .filter_map(|entry| {
56 let entry = entry.ok()?;
57 let path = entry.path();
58 (path.extension()? == "toml")
59 .then(|| path.file_stem().unwrap().to_string_lossy().into_owned())
60 })
61 .collect()
62 })
63 .unwrap_or_default()
64 }
66 /// Lists all theme names available in default and user directory
67 pub fn names(&self) -> Vec<String> {
68 let mut names = Self::read_names(&self.user_dir);
69 names.extend(Self::read_names(&self.default_dir));
70 names
71 }
73 /// Returns the default theme
74 pub fn default(&self) -> Theme {
75 DEFAULT_THEME.clone()
76 }
79#[derive(Clone, Debug)]
80pub struct Theme {
81 scopes: Vec<String>,
82 styles: HashMap<String, Style>,
85impl<'de> Deserialize<'de> for Theme {
86 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
87 where
88 D: Deserializer<'de>,
89 {
90 let mut styles = HashMap::new();
92 if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
93 // TODO: alert user of parsing failures in editor
94 let palette = colors
95 .remove("palette")
96 .map(|value| {
97 ThemePalette::try_from(value).unwrap_or_else(|err| {
98 warn!("{}", err);
99 ThemePalette::default()
100 })
101 })
102 .unwrap_or_default();
104 styles.reserve(colors.len());
105 for (name, style_value) in colors {
106 let mut style = Style::default();
107 if let Err(err) = palette.parse_style(&mut style, style_value) {
108 warn!("{}", err);
109 }
110 styles.insert(name, style);
111 }
112 }
114 let scopes = styles.keys().map(ToString::to_string).collect();
115 Ok(Self { scopes, styles })
116 }
119impl Theme {
120 pub fn get(&self, scope: &str) -> Style {
121 self.try_get(scope)
122 .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
123 }
125 pub fn try_get(&self, scope: &str) -> Option<Style> {
126 self.styles.get(scope).copied()
127 }
129 #[inline]
130 pub fn scopes(&self) -> &[String] {
131 &self.scopes
132 }
134 pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
135 self.scopes().iter().position(|s| s == scope)
136 }
139struct ThemePalette {
140 palette: HashMap<String, Color>,
143impl Default for ThemePalette {
144 fn default() -> Self {
145 Self::new(HashMap::new())
146 }
149impl ThemePalette {
150 pub fn new(palette: HashMap<String, Color>) -> Self {
151 Self { palette }
152 }
154 pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
155 if s.starts_with('#') && s.len() >= 7 {
156 if let (Ok(red), Ok(green), Ok(blue)) = (
157 u8::from_str_radix(&s[1..3], 16),
158 u8::from_str_radix(&s[3..5], 16),
159 u8::from_str_radix(&s[5..7], 16),
160 ) {
161 return Ok(Color::Rgb(red, green, blue));
162 }
163 }
165 Err(format!("Theme: malformed hexcode: {}", s))
166 }
168 fn parse_value_as_str(value: &Value) -> Result<&str, String> {
169 value
170 .as_str()
171 .ok_or(format!("Theme: unrecognized value: {}", value))
172 }
174 pub fn parse_color(&self, value: Value) -> Result<Color, String> {
175 let value = Self::parse_value_as_str(&value)?;
177 self.palette
178 .get(value)
179 .copied()
180 .ok_or("")
181 .or_else(|_| Self::hex_string_to_rgb(value))
182 }
184 pub fn parse_modifier(value: &Value) -> Result<Modifier, String> {
185 value
186 .as_str()
187 .and_then(|s| s.parse().ok())
188 .ok_or(format!("Theme: invalid modifier: {}", value))
189 }
191 pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
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)?),
197 "modifiers" => {
198 let modifiers = value
199 .as_array()
200 .ok_or("Theme: modifiers should be an array")?;
202 for modifier in modifiers {
203 *style = style.add_modifier(Self::parse_modifier(modifier)?);
204 }
205 }
206 _ => return Err(format!("Theme: invalid style attribute: {}", name)),
207 }
208 }
209 } else {
210 *style = style.fg(self.parse_color(value)?);
211 }
212 Ok(())
213 }
216impl 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()),
223 };
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);
230 }
232 Ok(Self::new(palette))
233 }
236#[test]
237fn test_parse_style_string() {
238 let fg = Value::String("#ffffff".to_string());
240 let mut style = Style::default();
241 let palette = ThemePalette::default();
242 palette.parse_style(&mut style, fg).unwrap();
244 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
247#[test]
248fn test_palette() {
249 use helix_core::hashmap;
250 let fg = Value::String("my_color".to_string());
252 let mut style = Style::default();
253 let palette =
254 ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
255 palette.parse_style(&mut style, fg).unwrap();
257 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
260#[test]
261fn test_parse_style_table() {
262 let table = toml::toml! {
263 "keyword" = {
264 fg = "#ffffff",
265 bg = "#000000",
266 modifiers = ["bold"],
267 }
268 };
270 let mut style = Style::default();
271 let palette = ThemePalette::default();
272 if let Value::Table(entries) = table {
273 for (_name, value) in entries {
274 palette.parse_style(&mut style, value).unwrap();
275 }
276 }
278 assert_eq!(
279 style,
280 Style::default()
281 .fg(Color::Rgb(255, 255, 255))
282 .bg(Color::Rgb(0, 0, 0))
283 .add_modifier(Modifier::BOLD)
284 );