From 695d95df19b74485be62aba0f978044ff1215ea0 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 20 Nov 2014 10:17:05 -0500 Subject: [PATCH 1/5] parse_color: refactor color storage When we parse a color name like "red" into its ANSI color value, we pack the storage into a single int that may take on many values: 1. If it's "-2", no value has been specified. 2. If it's "-1", the value is "normal" (i.e., no color). 3. If it's 0 through 7, the value is a standard ANSI color. 4. If it's larger (up to 255), it is a 256-color extended value. Given these magic numbers, it is often hard to see what is going on in the code. Let's refactor this into a struct with a flag that tells which scheme we are using, along with a numeric value. This is more verbose, but should hopefully be simpler to follow. It will also allow us to easily add support for more schemes, like 24-bit RGB values. The result is also slightly less efficient to store, but that's OK; we only store this intermediate state during the parse, after which we write out the actual ANSI bytes. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- color.c | 138 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 32 deletions(-) diff --git a/color.c b/color.c index 7941e932d..3afda9d9d 100644 --- a/color.c +++ b/color.c @@ -26,23 +26,77 @@ const char *column_colors_ansi[] = { /* Ignore the RESET at the end when giving the size */ const int column_colors_ansi_max = ARRAY_SIZE(column_colors_ansi) - 1; -static int parse_color(const char *name, int len) +/* An individual foreground or background color. */ +struct color { + enum { + COLOR_UNSPECIFIED = 0, + COLOR_NORMAL, + COLOR_ANSI, /* basic 0-7 ANSI colors */ + COLOR_256 + } type; + /* The numeric value for ANSI and 256-color modes */ + unsigned char value; +}; + +/* + * "word" is a buffer of length "len"; does it match the NUL-terminated + * "match" exactly? + */ +static int match_word(const char *word, int len, const char *match) { + return !strncasecmp(word, match, len) && !match[len]; +} + +static int parse_color(struct color *out, const char *name, int len) +{ + /* Positions in array must match ANSI color codes */ static const char * const color_names[] = { - "normal", "black", "red", "green", "yellow", + "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white" }; char *end; int i; + long val; + + /* First try the special word "normal"... */ + if (match_word(name, len, "normal")) { + out->type = COLOR_NORMAL; + return 0; + } + + /* Then pick from our human-readable color names... */ for (i = 0; i < ARRAY_SIZE(color_names); i++) { - const char *str = color_names[i]; - if (!strncasecmp(name, str, len) && !str[len]) - return i - 1; + if (match_word(name, len, color_names[i])) { + out->type = COLOR_ANSI; + out->value = i; + return 0; + } } - i = strtol(name, &end, 10); - if (end - name == len && i >= -1 && i <= 255) - return i; - return -2; + + /* And finally try a literal 256-color-mode number */ + val = strtol(name, &end, 10); + if (end - name == len) { + /* + * Allow "-1" as an alias for "normal", but other negative + * numbers are bogus. + */ + if (val < -1) + ; /* fall through to error */ + else if (val < 0) { + out->type = COLOR_NORMAL; + return 0; + /* Rewrite low numbers as more-portable standard colors. */ + } else if (val < 8) { + out->type = COLOR_ANSI; + out->value = val; + } else if (val < 256) { + out->type = COLOR_256; + out->value = val; + return 0; + } + } + + return -1; } static int parse_attr(const char *name, int len) @@ -65,13 +119,43 @@ int color_parse(const char *value, char *dst) return color_parse_mem(value, strlen(value), dst); } +#define COLOR_FOREGROUND '3' +#define COLOR_BACKGROUND '4' + +/* + * Write the ANSI color codes for "c" to "out"; the string should + * already have the ANSI escape code in it. "out" should have enough + * space in it to fit any color. + */ +static char *color_output(char *out, const struct color *c, char type) +{ + switch (c->type) { + case COLOR_UNSPECIFIED: + case COLOR_NORMAL: + break; + case COLOR_ANSI: + *out++ = type; + *out++ = '0' + c->value; + break; + case COLOR_256: + out += sprintf(out, "%c8;5;%d", type, c->value); + break; + } + return out; +} + +static int color_empty(const struct color *c) +{ + return c->type <= COLOR_NORMAL; +} + int color_parse_mem(const char *value, int value_len, char *dst) { const char *ptr = value; int len = value_len; unsigned int attr = 0; - int fg = -2; - int bg = -2; + struct color fg = { COLOR_UNSPECIFIED }; + struct color bg = { COLOR_UNSPECIFIED }; if (!strncasecmp(value, "reset", len)) { strcpy(dst, GIT_COLOR_RESET); @@ -81,6 +165,7 @@ int color_parse_mem(const char *value, int value_len, char *dst) /* [fg [bg]] [attr]... */ while (len > 0) { const char *word = ptr; + struct color c; int val, wordlen = 0; while (len > 0 && !isspace(word[wordlen])) { @@ -94,14 +179,13 @@ int color_parse_mem(const char *value, int value_len, char *dst) len--; } - val = parse_color(word, wordlen); - if (val >= -1) { - if (fg == -2) { - fg = val; + if (!parse_color(&c, word, wordlen)) { + if (fg.type == COLOR_UNSPECIFIED) { + fg = c; continue; } - if (bg == -2) { - bg = val; + if (bg.type == COLOR_UNSPECIFIED) { + bg = c; continue; } goto bad; @@ -113,7 +197,7 @@ int color_parse_mem(const char *value, int value_len, char *dst) goto bad; } - if (attr || fg >= 0 || bg >= 0) { + if (attr || !color_empty(&fg) || !color_empty(&bg)) { int sep = 0; int i; @@ -129,25 +213,15 @@ int color_parse_mem(const char *value, int value_len, char *dst) *dst++ = ';'; *dst++ = '0' + i; } - if (fg >= 0) { + if (!color_empty(&fg)) { if (sep++) *dst++ = ';'; - if (fg < 8) { - *dst++ = '3'; - *dst++ = '0' + fg; - } else { - dst += sprintf(dst, "38;5;%d", fg); - } + dst = color_output(dst, &fg, COLOR_FOREGROUND); } - if (bg >= 0) { + if (!color_empty(&bg)) { if (sep++) *dst++ = ';'; - if (bg < 8) { - *dst++ = '4'; - *dst++ = '0' + bg; - } else { - dst += sprintf(dst, "48;5;%d", bg); - } + dst = color_output(dst, &bg, COLOR_BACKGROUND); } *dst++ = 'm'; } From 17a4be26060b00a867cbe54ee906fe03813470ec Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 20 Nov 2014 10:25:39 -0500 Subject: [PATCH 2/5] parse_color: support 24-bit RGB values Some terminals (like XTerm) allow full 24-bit RGB color specifications using an extension to the regular ANSI color scheme. Let's allow users to specify hex RGB colors, enabling the all-important feature of hot pink ref decorations: git log --format="%h%C(#ff69b4)%d%C(reset) %s" Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- Documentation/config.txt | 3 ++- color.c | 29 ++++++++++++++++++++++++++++- color.h | 6 +++--- t/t4026-color.sh | 4 ++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Documentation/config.txt b/Documentation/config.txt index 9678ab6aa..35b9731d4 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -828,7 +828,8 @@ doesn't matter. + Colors (foreground and background) may also be given as numbers between 0 and 255; these use ANSI 256-color mode (but note that not all -terminals may support this). +terminals may support this). If your terminal supports it, you may also +specify 24-bit RGB values as hex, like `#ff0ab3`. color.diff:: Whether to use ANSI escape sequences to add color to patches. diff --git a/color.c b/color.c index 3afda9d9d..ae6879292 100644 --- a/color.c +++ b/color.c @@ -32,10 +32,13 @@ struct color { COLOR_UNSPECIFIED = 0, COLOR_NORMAL, COLOR_ANSI, /* basic 0-7 ANSI colors */ - COLOR_256 + COLOR_256, + COLOR_RGB } type; /* The numeric value for ANSI and 256-color modes */ unsigned char value; + /* 24-bit RGB color values */ + unsigned char red, green, blue; }; /* @@ -47,6 +50,16 @@ static int match_word(const char *word, int len, const char *match) return !strncasecmp(word, match, len) && !match[len]; } +static int get_hex_color(const char *in, unsigned char *out) +{ + unsigned int val; + val = (hexval(in[0]) << 4) | hexval(in[1]); + if (val & ~0xff) + return -1; + *out = val; + return 0; +} + static int parse_color(struct color *out, const char *name, int len) { /* Positions in array must match ANSI color codes */ @@ -64,6 +77,16 @@ static int parse_color(struct color *out, const char *name, int len) return 0; } + /* Try a 24-bit RGB value */ + if (len == 7 && name[0] == '#') { + if (!get_hex_color(name + 1, &out->red) && + !get_hex_color(name + 3, &out->green) && + !get_hex_color(name + 5, &out->blue)) { + out->type = COLOR_RGB; + return 0; + } + } + /* Then pick from our human-readable color names... */ for (i = 0; i < ARRAY_SIZE(color_names); i++) { if (match_word(name, len, color_names[i])) { @@ -140,6 +163,10 @@ static char *color_output(char *out, const struct color *c, char type) case COLOR_256: out += sprintf(out, "%c8;5;%d", type, c->value); break; + case COLOR_RGB: + out += sprintf(out, "%c8;2;%d;%d;%d", type, + c->red, c->green, c->blue); + break; } return out; } diff --git a/color.h b/color.h index f5beab1ed..4ec34b448 100644 --- a/color.h +++ b/color.h @@ -9,14 +9,14 @@ struct strbuf; * The maximum length of ANSI color sequence we would generate: * - leading ESC '[' 2 * - attr + ';' 2 * 8 (e.g. "1;") - * - fg color + ';' 9 (e.g. "38;5;2xx;") - * - fg color + ';' 9 (e.g. "48;5;2xx;") + * - fg color + ';' 17 (e.g. "38;2;255;255;255;") + * - bg color + ';' 17 (e.g. "48;2;255;255;255;") * - terminating 'm' NUL 2 * * The above overcounts attr (we only use 5 not 8) and one semicolon * but it is close enough. */ -#define COLOR_MAXLEN 40 +#define COLOR_MAXLEN 56 /* * IMPORTANT: Due to the way these color codes are emulated on Windows, diff --git a/t/t4026-color.sh b/t/t4026-color.sh index 63e423838..65386db91 100755 --- a/t/t4026-color.sh +++ b/t/t4026-color.sh @@ -53,6 +53,10 @@ test_expect_success '256 colors' ' color "254 bold 255" "[1;38;5;254;48;5;255m" ' +test_expect_success '24-bit colors' ' + color "#ff00ff black" "[38;2;255;0;255;40m" +' + test_expect_success '"normal" yields no color at all"' ' color "normal black" "[40m" ' From ff40d185d25ce821bd6ae5a524e58e4eab01dc86 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 20 Nov 2014 10:25:52 -0500 Subject: [PATCH 3/5] parse_color: recognize "no$foo" to clear the $foo attribute You can turn on ANSI text attributes like "reverse" by putting "reverse" in your color spec. However, you cannot ask to turn reverse off. For common cases, this does not matter. You would turn on "reverse" at the start of a colored section, and then clear all attributes with a "reset". However, you may wish to turn on some attributes, then selectively disable others. For example: git log --format="%C(bold ul yellow)%h%C(noul) %s" underlines just the hash, but without the need to re-specify the rest of the attributes. This can also help third-party programs, like contrib/diff-highlight, that want to turn some attribute on/off without disrupting existing coloring. Note that some attribute specifications are probably nonsensical (e.g., "bold nobold"). We do not bother to flag such constructs, and instead let the terminal sort it out. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- Documentation/config.txt | 3 ++- color.c | 8 +++++--- color.h | 4 ++-- t/t4026-color.sh | 11 +++++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Documentation/config.txt b/Documentation/config.txt index 35b9731d4..51947a9e7 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -824,7 +824,8 @@ accepted are `normal`, `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan` and `white`; the attributes are `bold`, `dim`, `ul`, `blink` and `reverse`. The first color given is the foreground; the second is the background. The position of the attribute, if any, -doesn't matter. +doesn't matter. Attributes may be turned off specifically by prefixing +them with `no` (e.g., `noreverse`, `noul`, etc). + Colors (foreground and background) may also be given as numbers between 0 and 255; these use ANSI 256-color mode (but note that not all diff --git a/color.c b/color.c index ae6879292..e2a0a9916 100644 --- a/color.c +++ b/color.c @@ -124,9 +124,11 @@ static int parse_color(struct color *out, const char *name, int len) static int parse_attr(const char *name, int len) { - static const int attr_values[] = { 1, 2, 4, 5, 7 }; + static const int attr_values[] = { 1, 2, 4, 5, 7, + 22, 22, 24, 25, 27 }; static const char * const attr_names[] = { - "bold", "dim", "ul", "blink", "reverse" + "bold", "dim", "ul", "blink", "reverse", + "nobold", "nodim", "noul", "noblink", "noreverse" }; int i; for (i = 0; i < ARRAY_SIZE(attr_names); i++) { @@ -238,7 +240,7 @@ int color_parse_mem(const char *value, int value_len, char *dst) attr &= ~bit; if (sep++) *dst++ = ';'; - *dst++ = '0' + i; + dst += sprintf(dst, "%d", i); } if (!color_empty(&fg)) { if (sep++) diff --git a/color.h b/color.h index 4ec34b448..7fe77fb55 100644 --- a/color.h +++ b/color.h @@ -8,7 +8,7 @@ struct strbuf; /* * The maximum length of ANSI color sequence we would generate: * - leading ESC '[' 2 - * - attr + ';' 2 * 8 (e.g. "1;") + * - attr + ';' 3 * 10 (e.g. "1;") * - fg color + ';' 17 (e.g. "38;2;255;255;255;") * - bg color + ';' 17 (e.g. "48;2;255;255;255;") * - terminating 'm' NUL 2 @@ -16,7 +16,7 @@ struct strbuf; * The above overcounts attr (we only use 5 not 8) and one semicolon * but it is close enough. */ -#define COLOR_MAXLEN 56 +#define COLOR_MAXLEN 70 /* * IMPORTANT: Due to the way these color codes are emulated on Windows, diff --git a/t/t4026-color.sh b/t/t4026-color.sh index 65386db91..267c43bd9 100755 --- a/t/t4026-color.sh +++ b/t/t4026-color.sh @@ -45,10 +45,21 @@ test_expect_success 'fg bg attr...' ' color "blue bold dim ul blink reverse" "[1;2;4;5;7;34m" ' +# note that nobold and nodim are the same code (22) +test_expect_success 'attr negation' ' + color "nobold nodim noul noblink noreverse" "[22;24;25;27m" +' + test_expect_success 'long color specification' ' color "254 255 bold dim ul blink reverse" "[1;2;4;5;7;38;5;254;48;5;255m" ' +test_expect_success 'absurdly long color specification' ' + color \ + "#ffffff #ffffff bold nobold dim nodim ul noul blink noblink reverse noreverse" \ + "[1;2;4;5;7;22;24;25;27;38;2;255;255;255;48;2;255;255;255m" +' + test_expect_success '256 colors' ' color "254 bold 255" "[1;38;5;254;48;5;255m" ' From bca45fbc1f8fe0daa76e840fa2ad4a9c663500a0 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 20 Nov 2014 10:29:18 -0500 Subject: [PATCH 4/5] diff-highlight: allow configurable colors Until now, the highlighting colors were hard-coded in the script (as "reverse" and "noreverse"), and you had to edit the script to change them. This patch teaches diff-highlight to read from color.diff-highlight.* to set them. In addition, it expands the possiblities considerably by adding two features: 1. Old/new lines can be colored independently (so you can use a color scheme that complements existing line coloring). 2. Normal, unhighlighted parts of the lines can be colored, too. Technically this can be done by separately configuring color.diff.old/new and matching it to your diff-highlight colors. But you may want a different look for your highlighted diffs versus your regular diffs. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- contrib/diff-highlight/README | 41 ++++++++++++++++++ contrib/diff-highlight/diff-highlight | 62 +++++++++++++++++++++------ 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/contrib/diff-highlight/README b/contrib/diff-highlight/README index 502e03b30..836b97a73 100644 --- a/contrib/diff-highlight/README +++ b/contrib/diff-highlight/README @@ -58,6 +58,47 @@ following in your git configuration: diff = diff-highlight | less --------------------------------------------- + +Color Config +------------ + +You can configure the highlight colors and attributes using git's +config. The colors for "old" and "new" lines can be specified +independently. There are two "modes" of configuration: + + 1. You can specify a "highlight" color and a matching "reset" color. + This will retain any existing colors in the diff, and apply the + "highlight" and "reset" colors before and after the highlighted + portion. + + 2. You can specify a "normal" color and a "highlight" color. In this + case, existing colors are dropped from that line. The non-highlighted + bits of the line get the "normal" color, and the highlights get the + "highlight" color. + +If no "new" colors are specified, they default to the "old" colors. If +no "old" colors are specified, the default is to reverse the foreground +and background for highlighted portions. + +Examples: + +--------------------------------------------- +# Underline highlighted portions +[color "diff-highlight"] +oldHighlight = ul +oldReset = noul +--------------------------------------------- + +--------------------------------------------- +# Varying background intensities +[color "diff-highlight"] +oldNormal = "black #f8cbcb" +oldHighlight = "black #ffaaaa" +newNormal = "black #cbeecb" +newHighlight = "black #aaffaa" +--------------------------------------------- + + Bugs ---- diff --git a/contrib/diff-highlight/diff-highlight b/contrib/diff-highlight/diff-highlight index c4404d49c..4a5f317b7 100755 --- a/contrib/diff-highlight/diff-highlight +++ b/contrib/diff-highlight/diff-highlight @@ -5,8 +5,18 @@ use strict; # Highlight by reversing foreground and background. You could do # other things like bold or underline if you prefer. -my $HIGHLIGHT = "\x1b[7m"; -my $UNHIGHLIGHT = "\x1b[27m"; +my @OLD_HIGHLIGHT = ( + color_config('color.diff-highlight.oldnormal'), + color_config('color.diff-highlight.oldhighlight', "\x1b[7m"), + color_config('color.diff-highlight.oldreset', "\x1b[27m") +); +my @NEW_HIGHLIGHT = ( + color_config('color.diff-highlight.newnormal', $OLD_HIGHLIGHT[0]), + color_config('color.diff-highlight.newhighlight', $OLD_HIGHLIGHT[1]), + color_config('color.diff-highlight.newreset', $OLD_HIGHLIGHT[2]) +); + +my $RESET = "\x1b[m"; my $COLOR = qr/\x1b\[[0-9;]*m/; my $BORING = qr/$COLOR|\s/; @@ -53,6 +63,17 @@ show_hunk(\@removed, \@added); exit 0; +# Ideally we would feed the default as a human-readable color to +# git-config as the fallback value. But diff-highlight does +# not otherwise depend on git at all, and there are reports +# of it being used in other settings. Let's handle our own +# fallback, which means we will work even if git can't be run. +sub color_config { + my ($key, $default) = @_; + my $s = `git config --get-color $key 2>/dev/null`; + return length($s) ? $s : $default; +} + sub show_hunk { my ($a, $b) = @_; @@ -128,8 +149,8 @@ sub highlight_pair { } if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) { - return highlight_line(\@a, $pa, $sa), - highlight_line(\@b, $pb, $sb); + return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT), + highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT); } else { return join('', @a), @@ -144,15 +165,30 @@ sub split_line { } sub highlight_line { - my ($line, $prefix, $suffix) = @_; - - return join('', - @{$line}[0..($prefix-1)], - $HIGHLIGHT, - @{$line}[$prefix..$suffix], - $UNHIGHLIGHT, - @{$line}[($suffix+1)..$#$line] - ); + my ($line, $prefix, $suffix, $theme) = @_; + + my $start = join('', @{$line}[0..($prefix-1)]); + my $mid = join('', @{$line}[$prefix..$suffix]); + my $end = join('', @{$line}[($suffix+1)..$#$line]); + + # If we have a "normal" color specified, then take over the whole line. + # Otherwise, we try to just manipulate the highlighted bits. + if (defined $theme->[0]) { + s/$COLOR//g for ($start, $mid, $end); + chomp $end; + return join('', + $theme->[0], $start, $RESET, + $theme->[1], $mid, $RESET, + $theme->[0], $end, $RESET, + "\n" + ); + } else { + return join('', + $start, + $theme->[1], $mid, $theme->[2], + $end + ); + } } # Pairs are interesting to highlight only if we are going to end up From 71b59849753589c3faa04176e875071417ceddd5 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 9 Dec 2014 16:01:26 -0500 Subject: [PATCH 5/5] parse_color: drop COLOR_BACKGROUND macro Commit 695d95d (parse_color: refactor color storage, 2014-11-20) introduced two macros, COLOR_FOREGROUND and COLOR_BACKGROUND. The latter conflicts with a system macro defined on Windows, breaking compilation there. The simplest solution is to just get rid of these macros entirely. They are constants that are only used in one place (since the whole point of 695d95d was to avoid repeating ourselves). Their main function is to make the magic character constants more readable, but we can do the same thing with a comment. Reported-by: Johannes Sixt Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- color.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/color.c b/color.c index e2a0a9916..809b359a4 100644 --- a/color.c +++ b/color.c @@ -144,9 +144,6 @@ int color_parse(const char *value, char *dst) return color_parse_mem(value, strlen(value), dst); } -#define COLOR_FOREGROUND '3' -#define COLOR_BACKGROUND '4' - /* * Write the ANSI color codes for "c" to "out"; the string should * already have the ANSI escape code in it. "out" should have enough @@ -245,12 +242,14 @@ int color_parse_mem(const char *value, int value_len, char *dst) if (!color_empty(&fg)) { if (sep++) *dst++ = ';'; - dst = color_output(dst, &fg, COLOR_FOREGROUND); + /* foreground colors are all in the 3x range */ + dst = color_output(dst, &fg, '3'); } if (!color_empty(&bg)) { if (sep++) *dst++ = ';'; - dst = color_output(dst, &bg, COLOR_BACKGROUND); + /* background colors are all in the 4x range */ + dst = color_output(dst, &bg, '4'); } *dst++ = 'm'; }