@@ -7,6 +7,7 @@ use serde::ser::SerializeMap;
77use serde:: { Deserialize , Deserializer , Serialize , Serializer } ;
88use serde_json:: { json, Value } ;
99use std:: collections:: HashMap ;
10+ use std:: fmt:: Write ;
1011use std:: sync:: atomic:: AtomicUsize ;
1112use std:: time:: Instant ;
1213use 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
207214fn 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+
242257fn 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+
276331pub 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