diff --git a/Makefile b/Makefile index 20cbb7e45..9c1e15d7d 100644 --- a/Makefile +++ b/Makefile @@ -360,6 +360,7 @@ TIG_OBJS = \ src/grep.o \ src/ui.o \ src/apps.o \ + src/ansi.o \ $(GRAPH_OBJS) \ $(COMPAT_OBJS) diff --git a/include/tig/ansi.h b/include/tig/ansi.h new file mode 100644 index 000000000..36bcc6f87 --- /dev/null +++ b/include/tig/ansi.h @@ -0,0 +1,35 @@ +/* Copyright (c) 2006-2015 Jonas Fonseca + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#ifndef TIG_ANSI_H +#define TIG_ANSI_H + +#include "tig/line.h" +#include "tig/tig.h" +#include "tig/view.h" + +struct ansi_status { + short fg; + short bg; + unsigned int attr; +}; + +void split_ansi(const char *string, int *ansi_num, char **ansi_ptrs); +void draw_ansi(struct view *view, int *ansi_num, char **ansi_ptrs, int max_width, size_t skip); +void draw_ansi_line(struct view *view, char *ansi_end_ptr, int *after_ansi_len, size_t *skip, int *cur_width, int *widths_of_display); +void wattrset_by_ansi_status(struct view *view, struct ansi_status* cur_ansi_status); +short convert_ansi_into_256_color(char **save_ptr); + +#endif + +/* vim: set ts=8 sw=8 noexpandtab: */ diff --git a/include/tig/line.h b/include/tig/line.h index ad3118d61..fd5bad174 100644 --- a/include/tig/line.h +++ b/include/tig/line.h @@ -17,6 +17,10 @@ #include "tig/tig.h" struct ref; +#if defined(NCURSES_VERSION_PATCH) && NCURSES_VERSION_PATCH >= 20180127 +extern short color_pairs_map[257][257]; +#endif + /* * Line-oriented content detection. */ diff --git a/src/ansi.c b/src/ansi.c new file mode 100644 index 000000000..1aa17d8cb --- /dev/null +++ b/src/ansi.c @@ -0,0 +1,233 @@ +/* Copyright (c) 2006-2015 Jonas Fonseca + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "tig/ansi.h" +#include "tig/draw.h" +#include "tig/line.h" +#include "tig/tig.h" +#include "tig/view.h" +#include "compat/utf8proc.h" + +void +split_ansi(const char *string, int *ansi_num, char **ansi_ptrs) { + char *head_of_ansi = "\033["; + int current_ansi_idx = 0; + char *next_ansi_ptr = strstr(string + current_ansi_idx, head_of_ansi); + + if (next_ansi_ptr == NULL) + return; + while (next_ansi_ptr != NULL) { + if (strcmp(string, next_ansi_ptr) == 0) { + next_ansi_ptr = strstr(string + current_ansi_idx + strlen(head_of_ansi), head_of_ansi); + continue; + } + int current_ansi_length = strlen(string + current_ansi_idx) - strlen(next_ansi_ptr); + strncpy(ansi_ptrs[*ansi_num], string + current_ansi_idx, current_ansi_length); + ansi_ptrs[*ansi_num][current_ansi_length] = '\0'; + *ansi_num += 1; + current_ansi_idx += current_ansi_length / sizeof(char); + next_ansi_ptr = strstr(string + current_ansi_idx + strlen(head_of_ansi), head_of_ansi); + } + + strcpy(ansi_ptrs[*ansi_num], string + current_ansi_idx); + *ansi_num += 1; +} + +void +draw_ansi(struct view *view, int *ansi_num, char **ansi_ptrs, int max_width, size_t skip) { + static struct ansi_status cur_ansi_status; + cur_ansi_status.fg = 256; + cur_ansi_status.bg = 256; + cur_ansi_status.attr = A_NORMAL; + int cur_width = 0; + + for (int i = 0; i < *ansi_num; i++) { + if (cur_width >= view->width) + break; + + int len = strlen(ansi_ptrs[i]); + char text[len + 1]; + strcpy(text, ansi_ptrs[i]); + + if (i == 0 && text[0] != '\033') { + waddnstr(view->win, text, len); + continue; + } + + // ncurses can't handle J and K of ANSI code behavior. + if ((text[3] == 'J') || (text[3] == 'K')) + continue; + + char *ansi_end_ptr = strchr(text, 'm'); + int after_ansi_len = strlen(ansi_end_ptr); + int ansi_code_len = len - after_ansi_len - 2; + ansi_end_ptr += 1; + + int widths_of_display = utf8_width_of(ansi_end_ptr, after_ansi_len, after_ansi_len); + if (skip > widths_of_display) { + skip -= widths_of_display; + continue; + } + + if (view->curline->selected) { + draw_ansi_line(view, ansi_end_ptr, &after_ansi_len, &skip, &cur_width, &widths_of_display); + continue; + } + + // ncurses can't handle multiple attribute such as BOLD & UNDERLINE. + // If input-ansi has "\033[1;4" we'll give priority to the latter one. + char *saveptr; + char *ansi_code = malloc(sizeof(char) * (ansi_code_len + 1)); + strncpy(ansi_code, text + 2, ansi_code_len); + ansi_code[ansi_code_len] = '\0'; + char *ansi_code_part = strtok_r(ansi_code, ";", &saveptr); + while (ansi_code_part != NULL) { + if (strcmp(ansi_code_part, "0") == 0) { + cur_ansi_status.fg = 256; + cur_ansi_status.bg = 256; + cur_ansi_status.attr = A_NORMAL; + } + if (strcmp(ansi_code_part, "1") == 0) + cur_ansi_status.attr = A_BOLD; + if (strcmp(ansi_code_part, "2") == 0) + cur_ansi_status.attr = A_DIM; + if (strcmp(ansi_code_part, "3") == 0) + cur_ansi_status.attr = A_ITALIC; + if (strcmp(ansi_code_part, "4") == 0) + cur_ansi_status.attr = A_UNDERLINE; + if (strcmp(ansi_code_part, "5") == 0) + cur_ansi_status.attr = A_BLINK; + if (strcmp(ansi_code_part, "6") == 0) + cur_ansi_status.attr = A_BLINK; // This is supposed to be faster than normal blink, but ncurses doesn't have any way to achieve. + if (strcmp(ansi_code_part, "7") == 0) + cur_ansi_status.attr = A_REVERSE; + if (strcmp(ansi_code_part, "8") == 0) + cur_ansi_status.attr = A_INVIS; + if (strcmp(ansi_code_part, "9") == 0) + // This is supposed to be strikethrough, but ncurses doesn't have any way to achieve. + if (strcmp(ansi_code_part, "30") == 0) + cur_ansi_status.fg = COLOR_BLACK; + if (strcmp(ansi_code_part, "31") == 0) + cur_ansi_status.fg = COLOR_RED; + if (strcmp(ansi_code_part, "32") == 0) + cur_ansi_status.fg = COLOR_GREEN; + if (strcmp(ansi_code_part, "33") == 0) + cur_ansi_status.fg = COLOR_YELLOW; + if (strcmp(ansi_code_part, "34") == 0) + cur_ansi_status.fg = COLOR_BLUE; + if (strcmp(ansi_code_part, "35") == 0) + cur_ansi_status.fg = COLOR_MAGENTA; + if (strcmp(ansi_code_part, "36") == 0) + cur_ansi_status.fg = COLOR_CYAN; + if (strcmp(ansi_code_part, "37") == 0) + cur_ansi_status.fg = COLOR_WHITE; + if (strcmp(ansi_code_part, "38") == 0) { + short c256 = convert_ansi_into_256_color(&saveptr); + if (c256 != -1) + cur_ansi_status.fg = c256; + } + if (strcmp(ansi_code_part, "40") == 0) + cur_ansi_status.bg = COLOR_BLACK; + if (strcmp(ansi_code_part, "41") == 0) + cur_ansi_status.bg = COLOR_RED; + if (strcmp(ansi_code_part, "42") == 0) + cur_ansi_status.bg = COLOR_GREEN; + if (strcmp(ansi_code_part, "43") == 0) + cur_ansi_status.bg = COLOR_YELLOW; + if (strcmp(ansi_code_part, "44") == 0) + cur_ansi_status.bg = COLOR_BLUE; + if (strcmp(ansi_code_part, "45") == 0) + cur_ansi_status.bg = COLOR_MAGENTA; + if (strcmp(ansi_code_part, "46") == 0) + cur_ansi_status.bg = COLOR_CYAN; + if (strcmp(ansi_code_part, "47") == 0) + cur_ansi_status.bg = COLOR_WHITE; + if (strcmp(ansi_code_part, "48") == 0) { + short c256 = convert_ansi_into_256_color(&saveptr); + if (c256 != -1) + cur_ansi_status.bg = c256; + } + + ansi_code_part = strtok_r(NULL, ";", &saveptr); + } + wattrset_by_ansi_status(view, &cur_ansi_status); + draw_ansi_line(view, ansi_end_ptr, &after_ansi_len, &skip, &cur_width, &widths_of_display); + + free(ansi_code); + ansi_code = NULL; + } +} + +void +draw_ansi_line(struct view *view, char *ansi_end_ptr, int *after_ansi_len, size_t *skip, int *cur_width, int *widths_of_display) { + while (*skip > 0) { + utf8proc_int32_t unicode; + int bytes_to_skip = utf8proc_iterate((const utf8proc_uint8_t *) ansi_end_ptr, *after_ansi_len, &unicode); + ansi_end_ptr += bytes_to_skip; + *after_ansi_len -= bytes_to_skip; + *skip -= 1; + *widths_of_display -= 1; + } + + if (*cur_width + *widths_of_display > view->width) { + int left_widths = view->width - *cur_width; + while (left_widths > 0) { + utf8proc_int32_t unicode; + int bytes_to_display = utf8proc_iterate((const utf8proc_uint8_t *) ansi_end_ptr, *after_ansi_len, &unicode); + waddnstr(view->win, ansi_end_ptr, bytes_to_display); + ansi_end_ptr += bytes_to_display; + *after_ansi_len -= bytes_to_display; + left_widths -= 1; + } + } else { + waddnstr(view->win, ansi_end_ptr, *after_ansi_len); + } + + *cur_width += *widths_of_display; +} + +void +wattrset_by_ansi_status(struct view *view, struct ansi_status* cur_ansi_status) { + // Because init_extended_pair can't accept more than 32768 pairs, + // we skip the colors with color codes odd numbered and greater than 15 currently. + if (cur_ansi_status->fg < 256 && cur_ansi_status->fg > 15 && cur_ansi_status->fg % 2 == 1) + cur_ansi_status->fg -= 1; + if (cur_ansi_status->bg < 256 && cur_ansi_status->bg > 15 && cur_ansi_status->bg % 2 == 1) + cur_ansi_status->bg -= 1; + short id = color_pairs_map[cur_ansi_status->fg][cur_ansi_status->bg]; + wattr_set(view->win, cur_ansi_status->attr, id, NULL); +} + +short +convert_ansi_into_256_color(char **save_ptr) { + char *color_method_mark = strtok_r(NULL, ";", save_ptr); + short c256 = -1; + if (strcmp(color_method_mark, "5") == 0) { + char *color_code = strtok_r(NULL, ";", save_ptr); + c256 = atoi(color_code); + } + + // WONTFIX: You can't init_color with numerous RGB code in ncurses. + // Therefore, \e[(3 or 4)8;2;r;g;bm syntax is disabled currently. + // The below code is left for when it is someday implemented. + // if (strcmp(color_method_mark, "2") == 0) { + // char *r = strtok(NULL, ";"); + // char *g = strtok(NULL, ";"); + // char *b = strtok(NULL, ";"); + // } + // Return a color pair ID that matches this rgb combination. + + return c256; +} + +/* vim: set ts=8 sw=8 noexpandtab: */ diff --git a/src/apps.c b/src/apps.c index a8afc0ade..9a5063c15 100644 --- a/src/apps.c +++ b/src/apps.c @@ -107,8 +107,14 @@ struct app_external && app_diff_highlight_path_search(dhlt_path, sizeof(dhlt_path), query) && *dhlt_path) { if (suffixcmp(dhlt_path, strlen(dhlt_path), "/diff-highlight.perl")) { - dhlt_app.argv[0] = dhlt_path; - dhlt_app.argv[1] = NULL; + if (strcmp(strrchr(dhlt_path, '/'), "/delta") == 0) { + dhlt_app.argv[0] = dhlt_path; + dhlt_app.argv[1] = "--true-color=never"; + dhlt_app.argv[2] = NULL; + } else { + dhlt_app.argv[0] = dhlt_path; + dhlt_app.argv[1] = NULL; + } } else if (path_search(perl_path, sizeof(perl_path), "perl", getenv("PATH"), X_OK)) { /* if the package manager failed to "make install" within the contrib dir, rescue via */ /* perl -MDiffHighlight -I/path/containing /path/containing/diff-highlight.perl */ diff --git a/src/draw.c b/src/draw.c index b764294b6..b6f8eeb2d 100644 --- a/src/draw.c +++ b/src/draw.c @@ -17,6 +17,10 @@ #include "tig/options.h" #include "compat/hashtab.h" +#if defined(NCURSES_VERSION_PATCH) && NCURSES_VERSION_PATCH >= 20180127 +#include "tig/ansi.h" +#endif + static const enum line_type palette_colors[] = { LINE_PALETTE_0, LINE_PALETTE_1, @@ -91,6 +95,56 @@ draw_chars(struct view *view, enum line_type type, const char *string, int lengt return VIEW_MAX_LEN(view) <= 0; } +static bool +draw_chars_with_ansi(struct view *view, enum line_type type, const char *string, int length, + int max_width, bool use_tilde) +{ + int len = 0; + int col = 0; + int trimmed = false; + size_t skip = view->pos.col > view->col ? view->pos.col - view->col : 0; + + if (max_width <= 0) + return VIEW_MAX_LEN(view) <= 0; + + if (opt_iconv_out != ICONV_NONE) { + string = encoding_iconv(opt_iconv_out, string, len); + if (!string) + return VIEW_MAX_LEN(view) <= 0; + } + + set_view_attr(view, type); + + int ansi_num = 0; + int len_with_ansi = strlen(string); + int max_num = (len_with_ansi / 4) + 1; + int max_len = (len_with_ansi - 4) + 1; + char **ansi_ptrs = (char **)malloc(sizeof(char *) * max_num); + char *ansi_ptrs_for_free = (char *)malloc(sizeof(char) * max_num * max_len); + for (int i = 0; i < max_num; i++) + ansi_ptrs[i] = ansi_ptrs_for_free + i * max_len; + split_ansi(string, &ansi_num, ansi_ptrs); + + if (ansi_num > 0) + draw_ansi(view, &ansi_num, ansi_ptrs, max_width, skip); + else { + len = utf8_length(&string, length, skip, &col, max_width, &trimmed, use_tilde, opt_tab_size); + waddnstr(view->win, string, len); + } + + free(ansi_ptrs_for_free); + free(ansi_ptrs); + + if (trimmed && use_tilde) { + set_view_attr(view, LINE_DELIMITER); + waddstr(view->win, opt_truncation_delimiter ? opt_truncation_delimiter : "~"); + col++; + } + + view->col += col; + return VIEW_MAX_LEN(view) <= 0; +} + static bool draw_space(struct view *view, enum line_type type, int max, int spaces) { @@ -121,8 +175,19 @@ draw_text_expanded(struct view *view, enum line_type type, const char *string, i size_t pos = string_expand(text, sizeof(text), string, length, opt_tab_size); size_t col = view->col; +#if defined(NCURSES_VERSION_PATCH) && NCURSES_VERSION_PATCH >= 20180127 + if (strstr(string, "\033[") != NULL) { + if (draw_chars_with_ansi(view, type, text, -1, max_width, use_tilde)) + return true; + } else { + if (draw_chars(view, type, text, -1, max_width, use_tilde)) + return true; + } +#else if (draw_chars(view, type, text, -1, max_width, use_tilde)) return true; +#endif + string += pos; length -= pos; max_width -= view->col - col; diff --git a/src/line.c b/src/line.c index a5f3936df..3cd8c3b27 100644 --- a/src/line.c +++ b/src/line.c @@ -23,6 +23,10 @@ static size_t line_rules; static struct line_info **color_pair; static size_t color_pairs; +#if defined(NCURSES_VERSION_PATCH) && NCURSES_VERSION_PATCH >= 20180127 +short color_pairs_map[257][257]; +#endif + DEFINE_ALLOCATOR(realloc_line_rule, struct line_rule, 8) DEFINE_ALLOCATOR(realloc_color_pair, struct line_info *, 8) @@ -240,6 +244,30 @@ init_colors(void) init_line_info_color_pair(info, type, default_bg, default_fg); } } + +#if defined(NCURSES_VERSION_PATCH) && NCURSES_VERSION_PATCH >= 20180127 + // Because init_extended_pair can't accept more than 32768 pairs, + // we skip the colors with color codes odd numbered and greater than 15 currently. + short cnt = COLOR_ID(LINE_NONE) + 1; + for (short bg = 0; bg < 256; bg++) { + for (short fg = 0; fg < 256; fg++) { + if ((fg > 15 && fg % 2 == 1) || (bg > 15 && bg % 2 == 1)) + continue; + init_extended_pair(++cnt, fg, bg); + color_pairs_map[fg][bg] = cnt; + } + } + for (short bg = 0; bg < 256; bg++) { + init_extended_pair(++cnt, COLOR_DEFAULT, bg); + color_pairs_map[256][bg] = cnt; + } + for (short fg = 0; fg < 256; fg++) { + init_extended_pair(++cnt, fg, COLOR_DEFAULT); + color_pairs_map[fg][256] = cnt; + } + init_extended_pair(++cnt, COLOR_DEFAULT, COLOR_DEFAULT); + color_pairs_map[256][256] = cnt; +#endif } /* vim: set ts=8 sw=8 noexpandtab: */ diff --git a/src/string.c b/src/string.c index 6146d8aee..14995c9d2 100644 --- a/src/string.c +++ b/src/string.c @@ -103,7 +103,7 @@ string_expand(char *dst, size_t dstlen, const char *src, int srclen, int tabsize expanded = dstlen - size - 1; memcpy(dst + size, " ", expanded); size += expanded; - } else if (isspace(c) || iscntrl(c)) { + } else if ((isspace(c) || iscntrl(c)) && !(c == '\033' && src[pos+1] == '[')) { dst[size++] = ' '; } else { dst[size++] = src[pos]; diff --git a/test/diff/diff-highlight-with-delta-test b/test/diff/diff-highlight-with-delta-test new file mode 100755 index 000000000..658956690 --- /dev/null +++ b/test/diff/diff-highlight-with-delta-test @@ -0,0 +1,52 @@ +#!/bin/sh + +. libtest.sh +. libgit.sh + +test_require delta + +export PATH="$(dirname -- "$delta_path"):$PATH" + +tigrc < +AuthorDate: Sat Mar 1 15:59:02 2014 -0500 +Commit: Jonas Fonseca +CommitDate: Sat Mar 1 15:59:02 2014 -0500 + + Add type parameter for js.Dynamic +--- + common/src/main/scala/org/scalajs/benchmark/Benchmark.scala | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + + +common/src/main/scala/org/scalajs/benchmark/Benchmark.scala +──────────────────────────────────────────────────────────────────────────────── + +───────────────────────┐ +15: object Benchmark { │ +───────────────────────┘ + val benchmarks = js.Array[Benchmark]() + val benchmarkApps = js.Array[BenchmarkApp]() + + val global = js.Dynamic.global.asInstanceOf[js.Dictionary] + val global = js.Dynamic.global.asInstanceOf[js.Dictionary[js.Any]] + global("runScalaJSBenchmarks") = runBenchmarks _ + global("initScalaJSBenchmarkApps") = initBenchmarkApps _ + + + +[diff] a1dcf1aaa11470978db1d5d8bcf9e16201eb70ff - line 1 of 26 100% +EOF diff --git a/test/tools/libtest.sh b/test/tools/libtest.sh index ceb85a337..391177fd8 100644 --- a/test/tools/libtest.sh +++ b/test/tools/libtest.sh @@ -440,7 +440,12 @@ show_test_results() fi # Replace CR used by Git progress messages - tr '\r' '\n' < .test-result + kernel="$(uname -s 2>/dev/null || printf 'unknown\n')" + case "$kernel" in + Darwin) LC_ALL=C tr '\r' '\n' < .test-result ;; + *) tr '\r' '\n' < .test-result ;; + esac + elif [ -n "$verbose" ]; then count="$(grep -c '^ *\[OK\]' < .test-result || true)" printf 'Passed %d assertions\n' "$count" @@ -548,6 +553,12 @@ test_require() test_skip "The test requires diff-highlight, usually found in share/git-core-contrib" fi ;; + delta) + delta_path="/usr/local/bin/delta" + if [ ! -e "$delta_path" ]; then + test_skip "The test requires git-delta, https://dandavison.github.io/delta/installation.html" + fi + ;; readline) if ! has_readline; then test_skip "The test requires a tig compiled with readline"