on master help sign in
Color palettes (#393)
79f0969Jakub Bartodziej
book/src/themes.md+18
@@ -99,3 +99,21 @@ Possible keys:
99These 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.
99These 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.
100
100
101For 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.
101For 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.
102+
103+## Color palettes
104+
105+You can define a palette of named colors, and refer to them from the
106+configuration values in your theme. To do this, add a table called
107+`palette` to your theme file:
108+
109+```toml
110+ui.background = "white"
111+ui.text = "black"
112+
113+[palette]
114+white = "#ffffff"
115+black = "#000000"
116+```
117+
118+Remember that the `[palette]` table includes all keys after its header,
119+so you should define the palette after normal theme options.
helix-view/src/theme.rs+42−10
@@ -102,12 +102,13 @@ impl<'de> Deserialize<'de> for Theme {
102 {
102 {
103 let mut styles = HashMap::new();
103 let mut styles = HashMap::new();
104
104
105− if let Ok(colors) = HashMap::<String, Value>::deserialize(deserializer) {
105+ if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
106+ let palette = parse_palette(colors.remove("palette"));
106 // scopes.reserve(colors.len());
107 // scopes.reserve(colors.len());
107 styles.reserve(colors.len());
108 styles.reserve(colors.len());
108 for (name, style_value) in colors {
109 for (name, style_value) in colors {
109 let mut style = Style::default();
110 let mut style = Style::default();
110− parse_style(&mut style, style_value);
111+ parse_style(&mut style, style_value, &palette);
111 // scopes.push(name);
112 // scopes.push(name);
112 styles.insert(name, style);
113 styles.insert(name, style);
113 }
114 }
@@ -118,18 +119,31 @@ impl<'de> Deserialize<'de> for Theme {
118 }
119 }
119}
120}
120
121
122+fn parse_palette(value: Option<Value>) -> HashMap<String, Color> {
123+ match value {
124+ Some(Value::Table(entries)) => entries,
125+ _ => return HashMap::default(),
126+ }
127+ .into_iter()
128+ .filter_map(|(name, value)| {
129+ let color = parse_color(value, &HashMap::default())?;
130+ Some((name, color))
131+ })
132+ .collect()
133+}
134+
121−fn parse_style(style: &mut Style, value: Value) {
135+fn parse_style(style: &mut Style, value: Value, palette: &HashMap<String, Color>) {
122 //TODO: alert user of parsing failures
136 //TODO: alert user of parsing failures
123 if let Value::Table(entries) = value {
137 if let Value::Table(entries) = value {
124 for (name, value) in entries {
138 for (name, value) in entries {
125 match name.as_str() {
139 match name.as_str() {
126 "fg" => {
140 "fg" => {
127− if let Some(color) = parse_color(value) {
141+ if let Some(color) = parse_color(value, palette) {
128 *style = style.fg(color);
142 *style = style.fg(color);
129 }
143 }
130 }
144 }
131 "bg" => {
145 "bg" => {
132− if let Some(color) = parse_color(value) {
146+ if let Some(color) = parse_color(value, palette) {
133 *style = style.bg(color);
147 *style = style.bg(color);
134 }
148 }
135 }
149 }
@@ -143,7 +157,7 @@ fn parse_style(style: &mut Style, value: Value) {
143 _ => (),
157 _ => (),
144 }
158 }
145 }
159 }
146− } else if let Some(color) = parse_color(value) {
160+ } else if let Some(color) = parse_color(value, palette) {
147 *style = style.fg(color);
161 *style = style.fg(color);
148 }
162 }
149}
163}
@@ -164,9 +178,11 @@ fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> {
164 }
178 }
165}
179}
166
180
167−fn parse_color(value: Value) -> Option<Color> {
181+fn parse_color(value: Value, palette: &HashMap<String, Color>) -> Option<Color> {
168 if let Value::String(s) = value {
182 if let Value::String(s) = value {
183+ if let Some(color) = palette.get(&s) {
184+ Some(*color)
169− if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
185+ } else if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
170 Some(Color::Rgb(red, green, blue))
186 Some(Color::Rgb(red, green, blue))
171 } else {
187 } else {
172 warn!("malformed hexcode in theme: {}", s);
188 warn!("malformed hexcode in theme: {}", s);
@@ -226,7 +242,23 @@ fn test_parse_style_string() {
226 let fg = Value::String("#ffffff".to_string());
242 let fg = Value::String("#ffffff".to_string());
227
243
228 let mut style = Style::default();
244 let mut style = Style::default();
229− parse_style(&mut style, fg);
245+ parse_style(&mut style, fg, &HashMap::default());
246+
247+ assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
248+}
249+
250+#[test]
251+fn test_palette() {
252+ let fg = Value::String("my_color".to_string());
253+
254+ let mut style = Style::default();
255+ parse_style(
256+ &mut style,
257+ fg,
258+ &vec![("my_color".to_string(), Color::Rgb(255, 255, 255))]
259+ .into_iter()
260+ .collect(),
261+ );
230
262
231 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
263 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
232}
264}
@@ -244,7 +276,7 @@ fn test_parse_style_table() {
244 let mut style = Style::default();
276 let mut style = Style::default();
245 if let Value::Table(entries) = table {
277 if let Value::Table(entries) = table {
246 for (_name, value) in entries {
278 for (_name, value) in entries {
247− parse_style(&mut style, value);
279+ parse_style(&mut style, value, &HashMap::default());
248 }
280 }
249 }
281 }
250
282
runtime/themes/gruvbox.tomlnew file+83
@@ -0,0 +1,83 @@
1+# Author : Jakub Bartodziej <kubabartodziej@gmail.com>
2+# The theme uses the gruvbox dark palette with standard contrast: github.com/morhetz/gruvbox
3+
4+"attribute" = "fg2"
5+"keyword" = { fg = "red1" }
6+"keyword.directive" = "red0"
7+"namespace" = "yellow0"
8+"punctuation" = "fg4"
9+"punctuation.delimiter" = "fg4"
10+"operator" = "orange0"
11+"special" = "purple0"
12+"property" = "fg2"
13+"variable" = "fg2"
14+"variable.builtin" = "fg3"
15+"variable.parameter" = "fg2"
16+"type" = "orange1"
17+"type.builtin" = "orange0"
18+"constructor" = "fg2"
19+"function" = "green0"
20+"function.macro" = "aqua1"
21+"function.builtin" = "yellow1"
22+"comment" = "gray1"
23+"constant" = { fg = "fg2", modifiers = ["bold"] }
24+"constant.builtin" = { fg = "fg1", modifiers = ["bold"] }
25+"string" = "green1"
26+"number" = "purple1"
27+"escape" = { fg = "fg2", modifiers = ["bold"] }
28+"label" = "aqua1"
29+"module" = "yellow1"
30+
31+"warning" = "orange0"
32+"error" = "red0"
33+"info" = "purple0"
34+"hint" = "blue0"
35+
36+"ui.background" = { bg = "bg0" }
37+"ui.linenr" = { fg = "fg3" }
38+"ui.linenr.selected" = { fg = "fg1", modifiers = ["bold"] }
39+"ui.statusline" = { fg = "fg2", bg = "bg2" }
40+"ui.statusline.inactive" = { fg = "fg3", bg = "bg4" }
41+"ui.popup" = { bg = "bg1" }
42+"ui.window" = { bg = "bg1" }
43+"ui.help" = { bg = "bg1", fg = "fg1" }
44+"ui.text" = { fg = "fg1" }
45+"ui.text.focus" = { fg = "fg1", modifiers = ["bold"] }
46+"ui.selection" = { bg = "bg3" }
47+"ui.cursor.primary" = { bg = "bg3" }
48+"ui.cursor.match" = { bg = "bg4" }
49+"ui.menu" = { fg = "fg1" }
50+"ui.menu.selected" = { fg = "fg3", bg = "bg3" }
51+
52+"diagnostic" = { modifiers = ["underlined"] }
53+
54+[palette]
55+bg0 = "#282828" # main background
56+bg1 = "#3c3836"
57+bg2 = "#504945"
58+bg3 = "#665c54"
59+bg4 = "#7c6f64"
60+
61+fg0 = "#fbf1c7"
62+fg1 = "#ebdbb2" # main foreground
63+fg2 = "#d5c4a1"
64+fg3 = "#bdae93"
65+fg4 = "#a89984" # gray0
66+
67+gray0 = "#a89984"
68+gray1 = "#928374"
69+
70+red0 = "#cc241d" # neutral
71+red1 = "#fb4934" # bright
72+green0 = "#98971a"
73+green1 = "#b8bb26"
74+yellow0 = "#d79921"
75+yellow1 = "#fabd2f"
76+blue0 = "#458588"
77+blue1 = "#83a598"
78+purple0 = "#b16286"
79+purple1 = "#d3869b"
80+aqua0 = "#689d6a"
81+aqua1 = "#8ec07c"
82+orange0 = "#d65d0e"
83+orange1 = "#fe8019"