Support highlighting in truecolor, falling back to the closest xterm color if the terminal does not support it
Fixes #758
This commit is contained in:
parent
c98dc566d5
commit
5de649b7aa
1 changed files with 118 additions and 24 deletions
|
|
@ -7,6 +7,7 @@ use serde::ser::SerializeMap;
|
|||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::time::Instant;
|
||||
use std::{fs, io, path, str, usize};
|
||||
|
|
@ -202,6 +203,12 @@ fn parse_style(style: &mut Style, json: Value) {
|
|||
} else {
|
||||
style.css = None;
|
||||
}
|
||||
|
||||
if let Some(Color::RGB(red, green, blue)) = style.ansi.foreground {
|
||||
if !terminal_supports_truecolor() {
|
||||
style.ansi = style.ansi.fg(closest_xterm_color(red, green, blue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_color(json: Value) -> Option<Color> {
|
||||
|
|
@ -220,16 +227,8 @@ fn parse_color(json: Value) -> Option<Color> {
|
|||
"white" => Some(Color::White),
|
||||
"yellow" => Some(Color::Yellow),
|
||||
s => {
|
||||
if s.starts_with("#") && s.len() >= 7 {
|
||||
if let (Ok(red), Ok(green), Ok(blue)) = (
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
) {
|
||||
Some(Color::RGB(red, green, blue))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
|
||||
Some(Color::RGB(red, green, blue))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -239,8 +238,23 @@ fn parse_color(json: Value) -> Option<Color> {
|
|||
}
|
||||
}
|
||||
|
||||
fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> {
|
||||
if s.starts_with("#") && s.len() >= 7 {
|
||||
if let (Ok(red), Ok(green), Ok(blue)) = (
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
) {
|
||||
Some((red, green, blue))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn style_to_css(style: ansi_term::Style) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut result = "style='".to_string();
|
||||
if style.is_underline {
|
||||
write!(&mut result, "text-decoration: underline;").unwrap();
|
||||
|
|
@ -252,27 +266,68 @@ fn style_to_css(style: ansi_term::Style) -> String {
|
|||
write!(&mut result, "font-style: italic;").unwrap();
|
||||
}
|
||||
if let Some(color) = style.foreground {
|
||||
write!(&mut result, "color: {};", color_to_css(color)).unwrap();
|
||||
write_color(&mut result, color);
|
||||
}
|
||||
result.push('\'');
|
||||
result
|
||||
}
|
||||
|
||||
fn color_to_css(color: Color) -> &'static str {
|
||||
match color {
|
||||
Color::Black => "black",
|
||||
Color::Blue => "blue",
|
||||
Color::Red => "red",
|
||||
Color::Green => "green",
|
||||
Color::Yellow => "yellow",
|
||||
Color::Cyan => "cyan",
|
||||
Color::Purple => "purple",
|
||||
Color::White => "white",
|
||||
Color::Fixed(n) => CSS_STYLES_BY_COLOR_ID[n as usize].as_str(),
|
||||
_ => panic!("Unsupported color type"),
|
||||
fn write_color(buffer: &mut String, color: Color) {
|
||||
if let Color::RGB(r, g, b) = &color {
|
||||
write!(buffer, "color: #{:x?}{:x?}{:x?}", r, g, b).unwrap()
|
||||
} else {
|
||||
write!(
|
||||
buffer,
|
||||
"color: {}",
|
||||
match color {
|
||||
Color::Black => "black",
|
||||
Color::Blue => "blue",
|
||||
Color::Red => "red",
|
||||
Color::Green => "green",
|
||||
Color::Yellow => "yellow",
|
||||
Color::Cyan => "cyan",
|
||||
Color::Purple => "purple",
|
||||
Color::White => "white",
|
||||
Color::Fixed(n) => CSS_STYLES_BY_COLOR_ID[n as usize].as_str(),
|
||||
_ => panic!("unreachable"),
|
||||
}
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_supports_truecolor() -> bool {
|
||||
use std::env;
|
||||
|
||||
if let Ok(truecolor) = env::var("COLORTERM") {
|
||||
truecolor == "truecolor" || truecolor == "24bit"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn closest_xterm_color(red: u8, green: u8, blue: u8) -> Color {
|
||||
use std::cmp::{max, min};
|
||||
|
||||
let colors = CSS_STYLES_BY_COLOR_ID
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(color_id, hex)| (color_id as u8, hex_string_to_rgb(hex).unwrap()));
|
||||
|
||||
// Get the xterm color with the minimum Euclidean distance to the target color
|
||||
// i.e. distance = √ (r2 - r1)² + (g2 - g1)² + (b2 - b1)²
|
||||
let distances = colors.map(|(color_id, (r, g, b))| {
|
||||
let r_delta: u32 = (max(r, red) - min(r, red)).into();
|
||||
let g_delta: u32 = (max(g, green) - min(g, green)).into();
|
||||
let b_delta: u32 = (max(b, blue) - min(b, blue)).into();
|
||||
let distance = r_delta.pow(2) + g_delta.pow(2) + b_delta.pow(2);
|
||||
// don't need to actually take the square root for the sake of comparison
|
||||
(color_id, distance)
|
||||
});
|
||||
|
||||
Color::Fixed(distances.min_by(|(_, d1), (_, d2)| d1.cmp(d2)).unwrap().0)
|
||||
}
|
||||
|
||||
pub fn ansi(
|
||||
loader: &Loader,
|
||||
theme: &Theme,
|
||||
|
|
@ -365,3 +420,42 @@ pub fn html(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env;
|
||||
|
||||
const JUNGLE_GREEN: &'static str = "#26A69A";
|
||||
const DARK_CYAN: &'static str = "#00AF87";
|
||||
|
||||
#[test]
|
||||
fn test_parse_style() {
|
||||
let original_environment_variable = env::var("COLORTERM");
|
||||
|
||||
let mut style = Style::default();
|
||||
assert_eq!(style.ansi.foreground, None);
|
||||
assert_eq!(style.css, None);
|
||||
|
||||
// darkcyan is an ANSI color and is preserved
|
||||
parse_style(&mut style, Value::String(DARK_CYAN.to_string()));
|
||||
assert_eq!(style.ansi.foreground, Some(Color::Fixed(36)));
|
||||
assert_eq!(style.css, Some("style=\'color: #0af87\'".to_string()));
|
||||
|
||||
// junglegreen is not an ANSI color and is preserved when the terminal supports it
|
||||
env::set_var("COLORTERM", "truecolor");
|
||||
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
|
||||
assert_eq!(style.ansi.foreground, Some(Color::RGB(38, 166, 154)));
|
||||
assert_eq!(style.css, Some("style=\'color: #26a69a\'".to_string()));
|
||||
|
||||
// junglegreen gets approximated as darkcyan when the terminal does not support it
|
||||
env::set_var("COLORTERM", "");
|
||||
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
|
||||
assert_eq!(style.ansi.foreground, Some(Color::Fixed(36)));
|
||||
assert_eq!(style.css, Some("style=\'color: #26a69a\'".to_string()));
|
||||
|
||||
if let Ok(environment_variable) = original_environment_variable {
|
||||
env::set_var("COLORTERM", environment_variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue