X-Git-Url: https://git.distorted.org.uk/~mdw/tig/blobdiff_plain/9a66631561319b86f54f20346d953070238cc346..965537fa357e94c1fe6fda5cf26a93c1b06e9d3a:/tig.c diff --git a/tig.c b/tig.c index 055ad11..94af086 100644 --- a/tig.c +++ b/tig.c @@ -64,7 +64,7 @@ static void die(const char *err, ...); static void report(const char *msg, ...); -static int read_properties(const char *cmd, int separator, int (*read)(char *, int, char *, int)); +static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int)); static void set_nonblocking_input(bool loading); static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed); @@ -149,6 +149,29 @@ struct ref { static struct ref **get_refs(char *id); +struct int_map { + const char *name; + int namelen; + int value; +}; + +static int +set_from_int_map(struct int_map *map, size_t map_size, + int *value, const char *name, int namelen) +{ + + int i; + + for (i = 0; i < map_size; i++) + if (namelen == map[i].namelen && + !strncasecmp(name, map[i].name, namelen)) { + *value = map[i].value; + return OK; + } + + return ERR; +} + /* * String helpers @@ -166,6 +189,21 @@ string_ncopy(char *dst, const char *src, int dstlen) #define string_copy(dst, src) \ string_ncopy(dst, src, sizeof(dst)) +static char * +chomp_string(char *name) +{ + int namelen; + + while (isspace(*name)) + name++; + + namelen = strlen(name) - 1; + while (namelen > 0 && isspace(name[namelen])) + name[namelen--] = 0; + + return name; +} + /* Shell quoting * @@ -227,7 +265,7 @@ VERSION " (" __DATE__ ")\n" " -l Start up in log view\n" " -d Start up in diff view\n" " -n[I], --line-number[=I] Show line numbers with given interval\n" -" -t[N], --tab-size[=N] Set number of spaces for tab expansion\n" +" -b[N], --tab-size[=N] Set number of spaces for tab expansion\n" " -- Mark end of tig options\n" " -v, --version Show version and exit\n" " -h, --help Show help message and exit\n"; @@ -521,42 +559,177 @@ parse_options(int argc, char *argv[]) "" -/* - * Line-oriented content detection. - */ +/** + * FILES + * ----- + * '~/.tig':: + * User configuration file. See "<>" + * section for examples. + * + * '.git/config':: + * Repository config file. Read on startup with the help of + * git-repo-config(1). + **/ +/** + * [[config-options]] + * User Configuration file + * ----------------------- + * You can permanently set an option by putting it in the `~/.tig` file. + * The file consists of a series of 'commands'. Each + * line of the file may contain only one command. + * + * The hash mark ('#'), or semi-colon (';') is used as a 'comment' character. + * All text after the comment character to the end of the line is ignored. + * You can use comments to annotate your initialization file. + * + * Some sample options: + * + * ========================================================================== + * # Diff colors + * color diff-header yellow default + * color diff-index blue default + * color diff-chunk magenta default + * # UI colors + * color title-blur white blue + * color title-focus white blue bold + * ========================================================================== + * + * [[color-options]] + * Color options + * ~~~~~~~~~~~~~ + * Color options control highlighting and the user interface styles. + * If your terminal supports color, these commands can be used to assign + * foreground/backgound combinations to certain areas. Optionally, an + * attribute can be given as the last parameter. The syntax is: + * + * [verse] + * .......................................................................... + * *color* 'area' 'fgcolor' 'bgcolor' '[attributes]' + * .......................................................................... + * + * Valid colors include: *white*, *black*, *green*, *magenta*, *blue*, *cyan*, + * *yellow*, *red*, *default*. Use *default* to refer to the default terminal + * colors. + **/ + +static struct int_map color_map[] = { +#define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name } + COLOR_MAP(DEFAULT), + COLOR_MAP(BLACK), + COLOR_MAP(BLUE), + COLOR_MAP(CYAN), + COLOR_MAP(GREEN), + COLOR_MAP(MAGENTA), + COLOR_MAP(RED), + COLOR_MAP(WHITE), + COLOR_MAP(YELLOW), +}; +/** + * Valid attributes include: *normal*, *blink*, *bold*, *dim*, *reverse*, *standout*, + * and *underline*. Note, not all attributes may be supported by the terminal. + **/ + +static struct int_map attr_map[] = { +#define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name } + ATTR_MAP(NORMAL), + ATTR_MAP(BLINK), + ATTR_MAP(BOLD), + ATTR_MAP(DIM), + ATTR_MAP(REVERSE), + ATTR_MAP(STANDOUT), + ATTR_MAP(UNDERLINE), +}; + +/** + * Valid area names are described below. Note, all names are case-insensitive, + * and you may use '-', '_', and '.' interchangeably. So "Diff-Header", + * "DIFF_HEADER", and "diff.header" are the same. + * + * -- + **/ #define LINE_INFO \ -/* Line type String to match Foreground Background Attributes - * --------- --------------- ---------- ---------- ---------- */ \ -/* Diff markup */ \ -LINE(DIFF, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +/** + * Diff markup:: + * + * Options concerning diff start, chunks and lines added and deleted. + * + * *diff-header*, *diff-chunk*, *diff-add*, *diff-del* + **/ \ +LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \ -LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_COPY, "copy ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_RENAME, "rename ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_SIM, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_DISSIM, "dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -/* Pretty print commit header */ \ +/** + * Enhanced git diff markup:: + * + * Extra diff information emitted by the git diff machinery, such as mode + * changes, rename detection, and similarity. + * + * *diff-oldmode*, *diff-newmode*, *diff-copy-from*, *diff-copy-to*, + * *diff-rename-from*, *diff-rename-to*, *diff-similarity* *diff-dissimilarity* + * *diff-tree*, *diff-index* + **/ \ +LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +/** + * Pretty print commit headers:: + * + * Commit diffs and the revision logs are usually formatted using pretty + * printed headers , unless `--pretty=raw` was given. This includes lines, + * such as merge info, commit ID, and author and comitter date. + * + * *pp-author*, *pp-commit*, *pp-merge*, *pp-date*, *pp-adate*, *pp-cdate* + **/ \ LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -/* Raw commit header */ \ +/** + * Raw commit header:: + * + * Usually shown when `--pretty=raw` is given, however 'commit' is pretty + * much omnipresent. + * + * *commit*, *parent*, *tree*, *author*, *committer* + **/ \ LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ -/* Misc */ \ -LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +/** + * Commit message:: + * + * For now only `Signed-off-by lines` are colorized. + * + * *signoff* + **/ \ LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -/* UI colors */ \ +/** + * UI colors:: + * + * Colors for text not matching any of the above: *default* + * + * Status window colors: *status* + * + * Title window colors: *title-blur*, *title-focus* + * + * Cursor line colors: *cursor* + * + * Main view specific: *main-date*, *main-author*, *main-commit*, *main-delim*, + * *main-tag*, *main-ref* + **/ \ LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \ LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \ LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \ @@ -567,7 +740,15 @@ LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \ LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \ -LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), +LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \ +/** + * -- + **/ + + +/* + * Line-oriented content detection. + */ enum line_type { #define LINE(type, line, fg, bg, attr) \ @@ -577,6 +758,8 @@ enum line_type { }; struct line_info { + const char *name; /* Option name. */ + int namelen; /* Size of option name. */ const char *line; /* The start of line to match. */ int linelen; /* Size of string to match. */ int fg, bg, attr; /* Color and text attributes for the lines. */ @@ -584,7 +767,7 @@ struct line_info { static struct line_info line_info[] = { #define LINE(type, line, fg, bg, attr) \ - { (line), STRING_SIZE(line), (fg), (bg), (attr) } + { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) } LINE_INFO #undef LINE }; @@ -611,6 +794,28 @@ get_line_attr(enum line_type type) return COLOR_PAIR(type) | line_info[type].attr; } +static struct line_info * +get_line_info(char *name, int namelen) +{ + enum line_type type; + int i; + + /* Diff-Header -> DIFF_HEADER */ + for (i = 0; i < namelen; i++) { + if (name[i] == '-') + name[i] = '_'; + else if (name[i] == '.') + name[i] = '_'; + } + + for (type = 0; type < ARRAY_SIZE(line_info); type++) + if (namelen == line_info[type].namelen && + !strncasecmp(line_info[type].name, name, namelen)) + return &line_info[type]; + + return NULL; +} + static void init_colors(void) { @@ -640,6 +845,121 @@ struct line { }; +/* + * User config file handling. + */ + +#define set_color(color, name, namelen) \ + set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen) + +#define set_attribute(attr, name, namelen) \ + set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen) + +static int config_lineno; +static bool config_errors; +static char *config_msg; + +static int +set_option(char *opt, int optlen, char *value, int valuelen) +{ + /* Reads: "color" object fgcolor bgcolor [attr] */ + if (!strcmp(opt, "color")) { + struct line_info *info; + + value = chomp_string(value); + valuelen = strcspn(value, " \t"); + info = get_line_info(value, valuelen); + if (!info) { + config_msg = "Unknown color name"; + return ERR; + } + + value = chomp_string(value + valuelen); + valuelen = strcspn(value, " \t"); + if (set_color(&info->fg, value, valuelen) == ERR) { + config_msg = "Unknown color"; + return ERR; + } + + value = chomp_string(value + valuelen); + valuelen = strcspn(value, " \t"); + if (set_color(&info->bg, value, valuelen) == ERR) { + config_msg = "Unknown color"; + return ERR; + } + + value = chomp_string(value + valuelen); + if (*value && + set_attribute(&info->attr, value, strlen(value)) == ERR) { + config_msg = "Unknown attribute"; + return ERR; + } + + return OK; + } + + return ERR; +} + +static int +read_option(char *opt, int optlen, char *value, int valuelen) +{ + config_lineno++; + config_msg = "Internal error"; + + optlen = strcspn(opt, "#;"); + if (optlen == 0) { + /* The whole line is a commend or empty. */ + return OK; + + } else if (opt[optlen] != 0) { + /* Part of the option name is a comment, so the value part + * should be ignored. */ + valuelen = 0; + opt[optlen] = value[valuelen] = 0; + } else { + /* Else look for comment endings in the value. */ + valuelen = strcspn(value, "#;"); + value[valuelen] = 0; + } + + if (set_option(opt, optlen, value, valuelen) == ERR) { + fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n", + config_lineno, optlen, opt, config_msg); + config_errors = TRUE; + } + + /* Always keep going if errors are encountered. */ + return OK; +} + +static int +load_options(void) +{ + char *home = getenv("HOME"); + char buf[1024]; + FILE *file; + + config_lineno = 0; + config_errors = FALSE; + + if (!home || + snprintf(buf, sizeof(buf), "%s/.tig", home) >= sizeof(buf)) + return ERR; + + /* It's ok that the file doesn't exist. */ + file = fopen(buf, "r"); + if (!file) + return OK; + + if (read_properties(file, " \t", read_option) == ERR || + config_errors == TRUE) + fprintf(stderr, "Errors while loading %s.\n", buf); + + return OK; +} + + /** * The viewer * ---------- @@ -2389,11 +2709,11 @@ load_refs(void) const char *cmd_env = getenv("TIG_LS_REMOTE"); const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE; - return read_properties(cmd, '\t', read_ref); + return read_properties(popen(cmd, "r"), "\t", read_ref); } static int -read_config_option(char *name, int namelen, char *value, int valuelen) +read_repo_config_option(char *name, int namelen, char *value, int valuelen) { if (!strcmp(name, "i18n.commitencoding")) { string_copy(opt_encoding, value); @@ -2403,17 +2723,16 @@ read_config_option(char *name, int namelen, char *value, int valuelen) } static int -load_config(void) +load_repo_config(void) { - return read_properties("git repo-config --list", '=', - read_config_option); + return read_properties(popen("git repo-config --list", "r"), + "=", read_repo_config_option); } static int -read_properties(const char *cmd, int separator, +read_properties(FILE *pipe, const char *separators, int (*read_property)(char *, int, char *, int)) { - FILE *pipe = popen(cmd, "r"); char buffer[BUFSIZ]; char *name; int state = OK; @@ -2422,27 +2741,24 @@ read_properties(const char *cmd, int separator, return ERR; while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) { - char *value = strchr(name, separator); - int namelen; - int valuelen; + char *value; + size_t namelen; + size_t valuelen; + + name = chomp_string(name); + namelen = strcspn(name, separators); - if (value) { - namelen = value - name; - *value++ = 0; + if (name[namelen]) { + name[namelen] = 0; + value = chomp_string(name + namelen + 1); valuelen = strlen(value); - if (valuelen > 0) { - valuelen--; - value[valuelen] = 0; - } } else { - namelen = strlen(name); value = ""; valuelen = 0; } - if (namelen) - state = read_property(name, namelen, value, valuelen); + state = read_property(name, namelen, value, valuelen); } if (state != ERR && ferror(pipe)) @@ -2498,9 +2814,12 @@ main(int argc, char *argv[]) signal(SIGINT, quit); - /* Load the repo config file first so options can be overwritten from + if (load_options() == ERR) + die("Failed to load user config."); + + /* Load the repo config file so options can be overwritten from * the command line. */ - if (load_config() == ERR) + if (load_repo_config() == ERR) die("Failed to load repo config."); if (!parse_options(argc, argv)) @@ -2696,6 +3015,8 @@ main(int argc, char *argv[]) * * - Locale support. * + * - Make '?' show a one page keybinding cheat sheet. + * * COPYRIGHT * --------- * Copyright (c) 2006 Jonas Fonseca