9use once_cell::sync::Lazy;
10use serde::{Deserialize, Deserializer};
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")
19#[derive(Clone, Debug)]
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 {
28 user_dir: user_dir.as_ref().join("themes"),
29 default_dir: default_dir.as_ref().join("themes"),
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());
38 let filename = format!("{}.toml", name);
40 let user_path = self.user_dir.join(&filename);
41 let path = if user_path.exists() {
44 self.default_dir.join(filename)
47 let data = std::fs::read(&path)?;
48 toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
51 pub fn read_names(path: &Path) -> Vec<String> {
52 std::fs::read_dir(path)
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())
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));
73 /// Returns the default theme
74 pub fn default(&self) -> Theme {
79#[derive(Clone, Debug)]
82 styles: HashMap<String, Style>,
85impl<'de> Deserialize<'de> for Theme {
86 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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
97 ThemePalette::try_from(value).unwrap_or_else(|err| {
99 ThemePalette::default()
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) {
110 styles.insert(name, style);
114 let scopes = styles.keys().map(ToString::to_string).collect();
115 Ok(Self { scopes, styles })
120 pub fn get(&self, scope: &str) -> Style {
122 .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
125 pub fn try_get(&self, scope: &str) -> Option<Style> {
126 self.styles.get(scope).copied()
130 pub fn scopes(&self) -> &[String] {
134 pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
135 self.scopes().iter().position(|s| s == scope)
140 palette: HashMap<String, Color>,
143impl Default for ThemePalette {
144 fn default() -> Self {
145 Self::new(HashMap::new())
150 pub fn new(palette: HashMap<String, Color>) -> Self {
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),
161 return Ok(Color::Rgb(red, green, blue));
165 Err(format!("Theme: malformed hexcode: {}", s))
168 fn parse_value_as_str(value: &Value) -> Result<&str, String> {
171 .ok_or(format!("Theme: unrecognized value: {}", value))
174 pub fn parse_color(&self, value: Value) -> Result<Color, String> {
175 let value = Self::parse_value_as_str(&value)?;
181 .or_else(|_| Self::hex_string_to_rgb(value))
184 pub fn parse_modifier(value: &Value) -> Result<Modifier, String> {
187 .and_then(|s| s.parse().ok())
188 .ok_or(format!("Theme: invalid modifier: {}", value))
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)?),
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)?);
216impl TryFrom<Value> for ThemePalette {
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))
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)));
249 use helix_core::hashmap;
250 let fg = Value::String("my_color".to_string());
252 let mut style = Style::default();
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)));
261fn test_parse_style_table() {
262 let table = toml::toml! {
266 modifiers = ["bold"],
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();
281 .fg(Color::Rgb(255, 255, 255))
282 .bg(Color::Rgb(0, 0, 0))
283 .add_modifier(Modifier::BOLD)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list