diff --git a/book/src/themes.md b/book/src/themes.md index 2ece24912..d6ed78ba4 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -99,3 +99,21 @@ Possible keys: These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences. For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently. + +## Color palettes + +You can define a palette of named colors, and refer to them from the +configuration values in your theme. To do this, add a table called +`palette` to your theme file: + +```toml +ui.background = "white" +ui.text = "black" + +[palette] +white = "#ffffff" +black = "#000000" +``` + +Remember that the `[palette]` table includes all keys after its header, +so you should define the palette after normal theme options. diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index ece4fe9ae..756e34f6e 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -102,12 +102,13 @@ impl<'de> Deserialize<'de> for Theme { { let mut styles = HashMap::new(); - if let Ok(colors) = HashMap::::deserialize(deserializer) { + if let Ok(mut colors) = HashMap::::deserialize(deserializer) { + let palette = parse_palette(colors.remove("palette")); // scopes.reserve(colors.len()); styles.reserve(colors.len()); for (name, style_value) in colors { let mut style = Style::default(); - parse_style(&mut style, style_value); + parse_style(&mut style, style_value, &palette); // scopes.push(name); styles.insert(name, style); } @@ -118,18 +119,31 @@ impl<'de> Deserialize<'de> for Theme { } } -fn parse_style(style: &mut Style, value: Value) { +fn parse_palette(value: Option) -> HashMap { + 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) { //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) { + if let Some(color) = parse_color(value, palette) { *style = style.fg(color); } } "bg" => { - if let Some(color) = parse_color(value) { + if let Some(color) = parse_color(value, palette) { *style = style.bg(color); } } @@ -143,7 +157,7 @@ fn parse_style(style: &mut Style, value: Value) { _ => (), } } - } else if let Some(color) = parse_color(value) { + } else if let Some(color) = parse_color(value, palette) { *style = style.fg(color); } } @@ -164,9 +178,11 @@ fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> { } } -fn parse_color(value: Value) -> Option { +fn parse_color(value: Value, palette: &HashMap) -> Option { if let Value::String(s) = value { - if let Some((red, green, blue)) = hex_string_to_rgb(&s) { + 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); @@ -226,7 +242,23 @@ fn test_parse_style_string() { let fg = Value::String("#ffffff".to_string()); let mut style = Style::default(); - parse_style(&mut style, fg); + parse_style(&mut style, fg, &HashMap::default()); + + assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255))); +} + +#[test] +fn test_palette() { + 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(), + ); assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255))); } @@ -244,7 +276,7 @@ fn test_parse_style_table() { let mut style = Style::default(); if let Value::Table(entries) = table { for (_name, value) in entries { - parse_style(&mut style, value); + parse_style(&mut style, value, &HashMap::default()); } } diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml new file mode 100644 index 000000000..b606568d9 --- /dev/null +++ b/runtime/themes/gruvbox.toml @@ -0,0 +1,83 @@ +# Author : Jakub Bartodziej +# The theme uses the gruvbox dark palette with standard contrast: github.com/morhetz/gruvbox + +"attribute" = "fg2" +"keyword" = { fg = "red1" } +"keyword.directive" = "red0" +"namespace" = "yellow0" +"punctuation" = "fg4" +"punctuation.delimiter" = "fg4" +"operator" = "orange0" +"special" = "purple0" +"property" = "fg2" +"variable" = "fg2" +"variable.builtin" = "fg3" +"variable.parameter" = "fg2" +"type" = "orange1" +"type.builtin" = "orange0" +"constructor" = "fg2" +"function" = "green0" +"function.macro" = "aqua1" +"function.builtin" = "yellow1" +"comment" = "gray1" +"constant" = { fg = "fg2", modifiers = ["bold"] } +"constant.builtin" = { fg = "fg1", modifiers = ["bold"] } +"string" = "green1" +"number" = "purple1" +"escape" = { fg = "fg2", modifiers = ["bold"] } +"label" = "aqua1" +"module" = "yellow1" + +"warning" = "orange0" +"error" = "red0" +"info" = "purple0" +"hint" = "blue0" + +"ui.background" = { bg = "bg0" } +"ui.linenr" = { fg = "fg3" } +"ui.linenr.selected" = { fg = "fg1", modifiers = ["bold"] } +"ui.statusline" = { fg = "fg2", bg = "bg2" } +"ui.statusline.inactive" = { fg = "fg3", bg = "bg4" } +"ui.popup" = { bg = "bg1" } +"ui.window" = { bg = "bg1" } +"ui.help" = { bg = "bg1", fg = "fg1" } +"ui.text" = { fg = "fg1" } +"ui.text.focus" = { fg = "fg1", modifiers = ["bold"] } +"ui.selection" = { bg = "bg3" } +"ui.cursor.primary" = { bg = "bg3" } +"ui.cursor.match" = { bg = "bg4" } +"ui.menu" = { fg = "fg1" } +"ui.menu.selected" = { fg = "fg3", bg = "bg3" } + +"diagnostic" = { modifiers = ["underlined"] } + +[palette] +bg0 = "#282828" # main background +bg1 = "#3c3836" +bg2 = "#504945" +bg3 = "#665c54" +bg4 = "#7c6f64" + +fg0 = "#fbf1c7" +fg1 = "#ebdbb2" # main foreground +fg2 = "#d5c4a1" +fg3 = "#bdae93" +fg4 = "#a89984" # gray0 + +gray0 = "#a89984" +gray1 = "#928374" + +red0 = "#cc241d" # neutral +red1 = "#fb4934" # bright +green0 = "#98971a" +green1 = "#b8bb26" +yellow0 = "#d79921" +yellow1 = "#fabd2f" +blue0 = "#458588" +blue1 = "#83a598" +purple0 = "#b16286" +purple1 = "#d3869b" +aqua0 = "#689d6a" +aqua1 = "#8ec07c" +orange0 = "#d65d0e" +orange1 = "#fe8019"