Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Color palettes #393

Merged
merged 6 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions book/src/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
52 changes: 42 additions & 10 deletions helix-view/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,13 @@ impl<'de> Deserialize<'de> for Theme {
{
let mut styles = HashMap::new();

if let Ok(colors) = HashMap::<String, Value>::deserialize(deserializer) {
if let Ok(mut colors) = HashMap::<String, Value>::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);
}
Expand All @@ -118,18 +119,31 @@ impl<'de> Deserialize<'de> for Theme {
}
}

fn parse_style(style: &mut Style, value: Value) {
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) {
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);
}
}
Expand All @@ -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);
}
}
Expand All @@ -164,9 +178,11 @@ fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> {
}
}

fn parse_color(value: Value) -> Option<Color> {
fn parse_color(value: Value, palette: &HashMap<String, Color>) -> Option<Color> {
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);
Expand Down Expand Up @@ -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)));
}
Expand All @@ -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());
}
}

Expand Down
83 changes: 83 additions & 0 deletions runtime/themes/gruvbox.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Author : Jakub Bartodziej <kubabartodziej@gmail.com>
# 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"
pickfire marked this conversation as resolved.
Show resolved Hide resolved

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"