Skip to content

Commit a6b2672

Browse files
authored
Merge pull request tree-sitter#952 from lazytype/master
Support highlighting in truecolor, falling back to the closest xterm …
2 parents 3f315f1 + 5de649b commit a6b2672

File tree

1 file changed

+118
-24
lines changed

1 file changed

+118
-24
lines changed

cli/src/highlight.rs

Lines changed: 118 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use serde::ser::SerializeMap;
77
use serde::{Deserialize, Deserializer, Serialize, Serializer};
88
use serde_json::{json, Value};
99
use std::collections::HashMap;
10+
use std::fmt::Write;
1011
use std::sync::atomic::AtomicUsize;
1112
use std::time::Instant;
1213
use std::{fs, io, path, str, usize};
@@ -202,6 +203,12 @@ fn parse_style(style: &mut Style, json: Value) {
202203
} else {
203204
style.css = None;
204205
}
206+
207+
if let Some(Color::RGB(red, green, blue)) = style.ansi.foreground {
208+
if !terminal_supports_truecolor() {
209+
style.ansi = style.ansi.fg(closest_xterm_color(red, green, blue));
210+
}
211+
}
205212
}
206213

207214
fn parse_color(json: Value) -> Option<Color> {
@@ -220,16 +227,8 @@ fn parse_color(json: Value) -> Option<Color> {
220227
"white" => Some(Color::White),
221228
"yellow" => Some(Color::Yellow),
222229
s => {
223-
if s.starts_with("#") && s.len() >= 7 {
224-
if let (Ok(red), Ok(green), Ok(blue)) = (
225-
u8::from_str_radix(&s[1..3], 16),
226-
u8::from_str_radix(&s[3..5], 16),
227-
u8::from_str_radix(&s[5..7], 16),
228-
) {
229-
Some(Color::RGB(red, green, blue))
230-
} else {
231-
None
232-
}
230+
if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
231+
Some(Color::RGB(red, green, blue))
233232
} else {
234233
None
235234
}
@@ -239,8 +238,23 @@ fn parse_color(json: Value) -> Option<Color> {
239238
}
240239
}
241240

241+
fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> {
242+
if s.starts_with("#") && s.len() >= 7 {
243+
if let (Ok(red), Ok(green), Ok(blue)) = (
244+
u8::from_str_radix(&s[1..3], 16),
245+
u8::from_str_radix(&s[3..5], 16),
246+
u8::from_str_radix(&s[5..7], 16),
247+
) {
248+
Some((red, green, blue))
249+
} else {
250+
None
251+
}
252+
} else {
253+
None
254+
}
255+
}
256+
242257
fn style_to_css(style: ansi_term::Style) -> String {
243-
use std::fmt::Write;
244258
let mut result = "style='".to_string();
245259
if style.is_underline {
246260
write!(&mut result, "text-decoration: underline;").unwrap();
@@ -252,27 +266,68 @@ fn style_to_css(style: ansi_term::Style) -> String {
252266
write!(&mut result, "font-style: italic;").unwrap();
253267
}
254268
if let Some(color) = style.foreground {
255-
write!(&mut result, "color: {};", color_to_css(color)).unwrap();
269+
write_color(&mut result, color);
256270
}
257271
result.push('\'');
258272
result
259273
}
260274

261-
fn color_to_css(color: Color) -> &'static str {
262-
match color {
263-
Color::Black => "black",
264-
Color::Blue => "blue",
265-
Color::Red => "red",
266-
Color::Green => "green",
267-
Color::Yellow => "yellow",
268-
Color::Cyan => "cyan",
269-
Color::Purple => "purple",
270-
Color::White => "white",
271-
Color::Fixed(n) => CSS_STYLES_BY_COLOR_ID[n as usize].as_str(),
272-
_ => panic!("Unsupported color type"),
275+
fn write_color(buffer: &mut String, color: Color) {
276+
if let Color::RGB(r, g, b) = &color {
277+
write!(buffer, "color: #{:x?}{:x?}{:x?}", r, g, b).unwrap()
278+
} else {
279+
write!(
280+
buffer,
281+
"color: {}",
282+
match color {
283+
Color::Black => "black",
284+
Color::Blue => "blue",
285+
Color::Red => "red",
286+
Color::Green => "green",
287+
Color::Yellow => "yellow",
288+
Color::Cyan => "cyan",
289+
Color::Purple => "purple",
290+
Color::White => "white",
291+
Color::Fixed(n) => CSS_STYLES_BY_COLOR_ID[n as usize].as_str(),
292+
_ => panic!("unreachable"),
293+
}
294+
)
295+
.unwrap()
273296
}
274297
}
275298

299+
fn terminal_supports_truecolor() -> bool {
300+
use std::env;
301+
302+
if let Ok(truecolor) = env::var("COLORTERM") {
303+
truecolor == "truecolor" || truecolor == "24bit"
304+
} else {
305+
false
306+
}
307+
}
308+
309+
fn closest_xterm_color(red: u8, green: u8, blue: u8) -> Color {
310+
use std::cmp::{max, min};
311+
312+
let colors = CSS_STYLES_BY_COLOR_ID
313+
.iter()
314+
.enumerate()
315+
.map(|(color_id, hex)| (color_id as u8, hex_string_to_rgb(hex).unwrap()));
316+
317+
// Get the xterm color with the minimum Euclidean distance to the target color
318+
// i.e. distance = √ (r2 - r1)² + (g2 - g1)² + (b2 - b1)²
319+
let distances = colors.map(|(color_id, (r, g, b))| {
320+
let r_delta: u32 = (max(r, red) - min(r, red)).into();
321+
let g_delta: u32 = (max(g, green) - min(g, green)).into();
322+
let b_delta: u32 = (max(b, blue) - min(b, blue)).into();
323+
let distance = r_delta.pow(2) + g_delta.pow(2) + b_delta.pow(2);
324+
// don't need to actually take the square root for the sake of comparison
325+
(color_id, distance)
326+
});
327+
328+
Color::Fixed(distances.min_by(|(_, d1), (_, d2)| d1.cmp(d2)).unwrap().0)
329+
}
330+
276331
pub fn ansi(
277332
loader: &Loader,
278333
theme: &Theme,
@@ -365,3 +420,42 @@ pub fn html(
365420

366421
Ok(())
367422
}
423+
424+
#[cfg(test)]
425+
mod tests {
426+
use super::*;
427+
use std::env;
428+
429+
const JUNGLE_GREEN: &'static str = "#26A69A";
430+
const DARK_CYAN: &'static str = "#00AF87";
431+
432+
#[test]
433+
fn test_parse_style() {
434+
let original_environment_variable = env::var("COLORTERM");
435+
436+
let mut style = Style::default();
437+
assert_eq!(style.ansi.foreground, None);
438+
assert_eq!(style.css, None);
439+
440+
// darkcyan is an ANSI color and is preserved
441+
parse_style(&mut style, Value::String(DARK_CYAN.to_string()));
442+
assert_eq!(style.ansi.foreground, Some(Color::Fixed(36)));
443+
assert_eq!(style.css, Some("style=\'color: #0af87\'".to_string()));
444+
445+
// junglegreen is not an ANSI color and is preserved when the terminal supports it
446+
env::set_var("COLORTERM", "truecolor");
447+
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
448+
assert_eq!(style.ansi.foreground, Some(Color::RGB(38, 166, 154)));
449+
assert_eq!(style.css, Some("style=\'color: #26a69a\'".to_string()));
450+
451+
// junglegreen gets approximated as darkcyan when the terminal does not support it
452+
env::set_var("COLORTERM", "");
453+
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
454+
assert_eq!(style.ansi.foreground, Some(Color::Fixed(36)));
455+
assert_eq!(style.css, Some("style=\'color: #26a69a\'".to_string()));
456+
457+
if let Ok(environment_variable) = original_environment_variable {
458+
env::set_var("COLORTERM", environment_variable);
459+
}
460+
}
461+
}

0 commit comments

Comments
 (0)