Merge branch 'jk/colors'

"diff-highlight" filter (in contrib/) allows its color output
to be customized via configuration variables.

* jk/colors:
  parse_color: drop COLOR_BACKGROUND macro
  diff-highlight: allow configurable colors
  parse_color: recognize "no$foo" to clear the $foo attribute
  parse_color: support 24-bit RGB values
  parse_color: refactor color storage
This commit is contained in:
Junio C Hamano 2014-12-22 12:27:58 -08:00
commit 3dadfc7e17
6 changed files with 250 additions and 54 deletions

View File

@ -849,11 +849,13 @@ 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
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.

174
color.c
View File

@ -26,30 +26,109 @@ 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,
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;
};
/*
* "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 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 */
static const char * const color_names[] = {
"normal", "black", "red", "green", "yellow",
"black", "red", "green", "yellow",
"blue", "magenta", "cyan", "white"
};
char *end;
int i;
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;
long val;
/* First try the special word "normal"... */
if (match_word(name, len, "normal")) {
out->type = COLOR_NORMAL;
return 0;
}
i = strtol(name, &end, 10);
if (end - name == len && i >= -1 && i <= 255)
return i;
return -2;
/* 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])) {
out->type = COLOR_ANSI;
out->value = i;
return 0;
}
}
/* 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)
{
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++) {
@ -65,13 +144,44 @@ int color_parse(const char *value, char *dst)
return color_parse_mem(value, strlen(value), dst);
}
/*
* 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;
case COLOR_RGB:
out += sprintf(out, "%c8;2;%d;%d;%d", type,
c->red, c->green, c->blue);
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 +191,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 +205,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 +223,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;
@ -127,27 +237,19 @@ 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 (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);
}
/* foreground colors are all in the 3x range */
dst = color_output(dst, &fg, '3');
}
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);
}
/* background colors are all in the 4x range */
dst = color_output(dst, &bg, '4');
}
*dst++ = 'm';
}

View File

@ -8,15 +8,15 @@ 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;")
* - 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
*
* 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 70
/*
* IMPORTANT: Due to the way these color codes are emulated on Windows,

View File

@ -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
----

View File

@ -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/;
@ -57,6 +67,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) = @_;
@ -132,8 +153,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),
@ -148,15 +169,30 @@ sub split_line {
}
sub highlight_line {
my ($line, $prefix, $suffix) = @_;
my ($line, $prefix, $suffix, $theme) = @_;
return join('',
@{$line}[0..($prefix-1)],
$HIGHLIGHT,
@{$line}[$prefix..$suffix],
$UNHIGHLIGHT,
@{$line}[($suffix+1)..$#$line]
);
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

View File

@ -45,14 +45,29 @@ 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"
'
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"
'