1 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
15 #define VERSION "tig-0.3"
36 #define __NORETURN __attribute__((__noreturn__))
41 static void __NORETURN die(const char *err, ...);
42 static void report(const char *msg, ...);
43 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
44 static void set_nonblocking_input(bool loading);
45 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
47 #define ABS(x) ((x) >= 0 ? (x) : -(x))
48 #define MIN(x, y) ((x) < (y) ? (x) : (y))
50 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
51 #define STRING_SIZE(x) (sizeof(x) - 1)
53 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
54 #define SIZEOF_CMD 1024 /* Size of command buffer. */
55 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
57 /* This color name can be used to refer to the default term colors. */
58 #define COLOR_DEFAULT (-1)
60 /* The format and size of the date column in the main view. */
61 #define DATE_FORMAT "%Y-%m-%d %H:%M"
62 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
64 #define AUTHOR_COLS 20
66 /* The default interval between line numbers. */
67 #define NUMBER_INTERVAL 1
71 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
73 #define TIG_LS_REMOTE \
74 "git ls-remote . 2>/dev/null"
76 #define TIG_DIFF_CMD \
77 "git show --patch-with-stat --find-copies-harder -B -C %s"
80 "git log --cc --stat -n100 %s"
82 #define TIG_MAIN_CMD \
83 "git log --topo-order --stat --pretty=raw %s"
85 /* XXX: Needs to be defined to the empty string. */
86 #define TIG_HELP_CMD ""
87 #define TIG_PAGER_CMD ""
89 /* Some ascii-shorthands fitted into the ncurses namespace. */
91 #define KEY_RETURN '\r'
96 char *name; /* Ref name; tag or head names are shortened. */
97 char id[41]; /* Commit SHA1 ID */
98 unsigned int tag:1; /* Is it a tag? */
99 unsigned int next:1; /* For ref lists: are there more refs? */
102 static struct ref **get_refs(char *id);
111 set_from_int_map(struct int_map *map, size_t map_size,
112 int *value, const char *name, int namelen)
117 for (i = 0; i < map_size; i++)
118 if (namelen == map[i].namelen &&
119 !strncasecmp(name, map[i].name, namelen)) {
120 *value = map[i].value;
133 string_ncopy(char *dst, const char *src, int dstlen)
135 strncpy(dst, src, dstlen - 1);
140 /* Shorthand for safely copying into a fixed buffer. */
141 #define string_copy(dst, src) \
142 string_ncopy(dst, src, sizeof(dst))
145 chomp_string(char *name)
149 while (isspace(*name))
152 namelen = strlen(name) - 1;
153 while (namelen > 0 && isspace(name[namelen]))
160 string_nformat(char *buf, size_t bufsize, int *bufpos, const char *fmt, ...)
163 int pos = bufpos ? *bufpos : 0;
166 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
172 return pos >= bufsize ? FALSE : TRUE;
175 #define string_format(buf, fmt, args...) \
176 string_nformat(buf, sizeof(buf), NULL, fmt, args)
178 #define string_format_from(buf, from, fmt, args...) \
179 string_nformat(buf, sizeof(buf), from, fmt, args)
182 string_enum_compare(const char *str1, const char *str2, int len)
186 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
188 /* Diff-Header == DIFF_HEADER */
189 for (i = 0; i < len; i++) {
190 if (toupper(str1[i]) == toupper(str2[i]))
193 if (string_enum_sep(str1[i]) &&
194 string_enum_sep(str2[i]))
197 return str1[i] - str2[i];
205 * NOTE: The following is a slightly modified copy of the git project's shell
206 * quoting routines found in the quote.c file.
208 * Help to copy the thing properly quoted for the shell safety. any single
209 * quote is replaced with '\'', any exclamation point is replaced with '\!',
210 * and the whole thing is enclosed in a
213 * original sq_quote result
214 * name ==> name ==> 'name'
215 * a b ==> a b ==> 'a b'
216 * a'b ==> a'\''b ==> 'a'\''b'
217 * a!b ==> a'\!'b ==> 'a'\!'b'
221 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
225 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
228 while ((c = *src++)) {
229 if (c == '\'' || c == '!') {
249 /* XXX: Keep the view request first and in sync with views[]. */ \
250 REQ_GROUP("View switching") \
251 REQ_(VIEW_MAIN, "Show main view"), \
252 REQ_(VIEW_DIFF, "Show diff view"), \
253 REQ_(VIEW_LOG, "Show log view"), \
254 REQ_(VIEW_HELP, "Show help page"), \
255 REQ_(VIEW_PAGER, "Show pager view"), \
257 REQ_GROUP("View manipulation") \
258 REQ_(ENTER, "Enter current line and scroll"), \
259 REQ_(NEXT, "Move to next"), \
260 REQ_(PREVIOUS, "Move to previous"), \
261 REQ_(VIEW_NEXT, "Move focus to next view"), \
262 REQ_(VIEW_CLOSE, "Close the current view"), \
263 REQ_(QUIT, "Close all views and quit"), \
265 REQ_GROUP("Cursor navigation") \
266 REQ_(MOVE_UP, "Move cursor one line up"), \
267 REQ_(MOVE_DOWN, "Move cursor one line down"), \
268 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
269 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
270 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
271 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
273 REQ_GROUP("Scrolling") \
274 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
275 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
276 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
277 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
280 REQ_(PROMPT, "Bring up the prompt"), \
281 REQ_(SCREEN_UPDATE, "Update the screen"), \
282 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
283 REQ_(SCREEN_RESIZE, "Resize the screen"), \
284 REQ_(SHOW_VERSION, "Show version information"), \
285 REQ_(STOP_LOADING, "Stop all loading views"), \
286 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
287 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization")
290 /* User action requests. */
292 #define REQ_GROUP(help)
293 #define REQ_(req, help) REQ_##req
295 /* Offset all requests to avoid conflicts with ncurses getch values. */
296 REQ_OFFSET = KEY_MAX + 1,
304 struct request_info {
305 enum request request;
311 static struct request_info req_info[] = {
312 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
313 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
320 get_request(const char *name)
322 int namelen = strlen(name);
325 for (i = 0; i < ARRAY_SIZE(req_info); i++)
326 if (req_info[i].namelen == namelen &&
327 !string_enum_compare(req_info[i].name, name, namelen))
328 return req_info[i].request;
338 static const char usage[] =
339 VERSION " (" __DATE__ ")\n"
341 "Usage: tig [options]\n"
342 " or: tig [options] [--] [git log options]\n"
343 " or: tig [options] log [git log options]\n"
344 " or: tig [options] diff [git diff options]\n"
345 " or: tig [options] show [git show options]\n"
346 " or: tig [options] < [git command output]\n"
349 " -l Start up in log view\n"
350 " -d Start up in diff view\n"
351 " -n[I], --line-number[=I] Show line numbers with given interval\n"
352 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
353 " -- Mark end of tig options\n"
354 " -v, --version Show version and exit\n"
355 " -h, --help Show help message and exit\n";
357 /* Option and state variables. */
358 static bool opt_line_number = FALSE;
359 static bool opt_rev_graph = TRUE;
360 static int opt_num_interval = NUMBER_INTERVAL;
361 static int opt_tab_size = TABSIZE;
362 static enum request opt_request = REQ_VIEW_MAIN;
363 static char opt_cmd[SIZEOF_CMD] = "";
364 static char opt_encoding[20] = "";
365 static bool opt_utf8 = TRUE;
366 static FILE *opt_pipe = NULL;
374 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
384 int namelen = strlen(name);
388 if (strncmp(opt, name, namelen))
391 if (opt[namelen] == '=')
392 value = opt + namelen + 1;
395 if (!short_name || opt[1] != short_name)
400 va_start(args, type);
401 if (type == OPT_INT) {
402 number = va_arg(args, int *);
404 *number = atoi(value);
411 /* Returns the index of log or diff command or -1 to exit. */
413 parse_options(int argc, char *argv[])
417 for (i = 1; i < argc; i++) {
420 if (!strcmp(opt, "-l")) {
421 opt_request = REQ_VIEW_LOG;
425 if (!strcmp(opt, "-d")) {
426 opt_request = REQ_VIEW_DIFF;
430 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
431 opt_line_number = TRUE;
435 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
436 opt_tab_size = MIN(opt_tab_size, TABSIZE);
440 if (check_option(opt, 'v', "version", OPT_NONE)) {
441 printf("tig version %s\n", VERSION);
445 if (check_option(opt, 'h', "help", OPT_NONE)) {
450 if (!strcmp(opt, "--")) {
455 if (!strcmp(opt, "log") ||
456 !strcmp(opt, "diff") ||
457 !strcmp(opt, "show")) {
458 opt_request = opt[0] == 'l'
459 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
463 if (opt[0] && opt[0] != '-')
466 die("unknown option '%s'\n\n%s", opt, usage);
469 if (!isatty(STDIN_FILENO)) {
470 opt_request = REQ_VIEW_PAGER;
473 } else if (i < argc) {
476 if (opt_request == REQ_VIEW_MAIN)
477 /* XXX: This is vulnerable to the user overriding
478 * options required for the main view parser. */
479 string_copy(opt_cmd, "git log --stat --pretty=raw");
481 string_copy(opt_cmd, "git");
482 buf_size = strlen(opt_cmd);
484 while (buf_size < sizeof(opt_cmd) && i < argc) {
485 opt_cmd[buf_size++] = ' ';
486 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
489 if (buf_size >= sizeof(opt_cmd))
490 die("command too long");
492 opt_cmd[buf_size] = 0;
496 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
504 * Line-oriented content detection.
508 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
509 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
510 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
511 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
512 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
513 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
514 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
515 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
516 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
517 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
518 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
519 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
520 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
521 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
522 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
523 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
524 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
525 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
526 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
527 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
528 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
529 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
530 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
531 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
532 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
533 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
534 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
535 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
536 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
537 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
538 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
539 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
540 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
541 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
542 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
543 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
544 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
545 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
548 #define LINE(type, line, fg, bg, attr) \
555 const char *name; /* Option name. */
556 int namelen; /* Size of option name. */
557 const char *line; /* The start of line to match. */
558 int linelen; /* Size of string to match. */
559 int fg, bg, attr; /* Color and text attributes for the lines. */
562 static struct line_info line_info[] = {
563 #define LINE(type, line, fg, bg, attr) \
564 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
569 static enum line_type
570 get_line_type(char *line)
572 int linelen = strlen(line);
575 for (type = 0; type < ARRAY_SIZE(line_info); type++)
576 /* Case insensitive search matches Signed-off-by lines better. */
577 if (linelen >= line_info[type].linelen &&
578 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
585 get_line_attr(enum line_type type)
587 assert(type < ARRAY_SIZE(line_info));
588 return COLOR_PAIR(type) | line_info[type].attr;
591 static struct line_info *
592 get_line_info(char *name, int namelen)
596 for (type = 0; type < ARRAY_SIZE(line_info); type++)
597 if (namelen == line_info[type].namelen &&
598 !string_enum_compare(line_info[type].name, name, namelen))
599 return &line_info[type];
607 int default_bg = COLOR_BLACK;
608 int default_fg = COLOR_WHITE;
613 if (use_default_colors() != ERR) {
618 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
619 struct line_info *info = &line_info[type];
620 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
621 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
623 init_pair(type, fg, bg);
629 void *data; /* User data */
639 enum request request;
640 struct keybinding *next;
643 static struct keybinding default_keybindings[] = {
645 { 'm', REQ_VIEW_MAIN },
646 { 'd', REQ_VIEW_DIFF },
647 { 'l', REQ_VIEW_LOG },
648 { 'p', REQ_VIEW_PAGER },
649 { 'h', REQ_VIEW_HELP },
650 { '?', REQ_VIEW_HELP },
652 /* View manipulation */
653 { 'q', REQ_VIEW_CLOSE },
654 { KEY_TAB, REQ_VIEW_NEXT },
655 { KEY_RETURN, REQ_ENTER },
656 { KEY_UP, REQ_PREVIOUS },
657 { KEY_DOWN, REQ_NEXT },
659 /* Cursor navigation */
660 { 'k', REQ_MOVE_UP },
661 { 'j', REQ_MOVE_DOWN },
662 { KEY_HOME, REQ_MOVE_FIRST_LINE },
663 { KEY_END, REQ_MOVE_LAST_LINE },
664 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
665 { ' ', REQ_MOVE_PAGE_DOWN },
666 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
667 { 'b', REQ_MOVE_PAGE_UP },
668 { '-', REQ_MOVE_PAGE_UP },
671 { KEY_IC, REQ_SCROLL_LINE_UP },
672 { KEY_DC, REQ_SCROLL_LINE_DOWN },
673 { 'w', REQ_SCROLL_PAGE_UP },
674 { 's', REQ_SCROLL_PAGE_DOWN },
678 { 'z', REQ_STOP_LOADING },
679 { 'v', REQ_SHOW_VERSION },
680 { 'r', REQ_SCREEN_REDRAW },
681 { 'n', REQ_TOGGLE_LINENO },
682 { 'g', REQ_TOGGLE_REV_GRAPH},
685 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
686 { ERR, REQ_SCREEN_UPDATE },
688 /* Use the ncurses SIGWINCH handler. */
689 { KEY_RESIZE, REQ_SCREEN_RESIZE },
692 #define KEYMAP_INFO \
701 #define KEYMAP_(name) KEYMAP_##name
706 static struct int_map keymap_table[] = {
707 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
712 #define set_keymap(map, name) \
713 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
715 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
718 add_keybinding(enum keymap keymap, enum request request, int key)
720 struct keybinding *keybinding;
722 keybinding = calloc(1, sizeof(*keybinding));
724 die("Failed to allocate keybinding");
726 keybinding->alias = key;
727 keybinding->request = request;
728 keybinding->next = keybindings[keymap];
729 keybindings[keymap] = keybinding;
732 /* Looks for a key binding first in the given map, then in the generic map, and
733 * lastly in the default keybindings. */
735 get_keybinding(enum keymap keymap, int key)
737 struct keybinding *kbd;
740 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
741 if (kbd->alias == key)
744 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
745 if (kbd->alias == key)
748 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
749 if (default_keybindings[i].alias == key)
750 return default_keybindings[i].request;
752 return (enum request) key;
761 static struct key key_table[] = {
762 { "Enter", KEY_RETURN },
764 { "Backspace", KEY_BACKSPACE },
766 { "Escape", KEY_ESC },
767 { "Left", KEY_LEFT },
768 { "Right", KEY_RIGHT },
770 { "Down", KEY_DOWN },
771 { "Insert", KEY_IC },
772 { "Delete", KEY_DC },
773 { "Home", KEY_HOME },
775 { "PageUp", KEY_PPAGE },
776 { "PageDown", KEY_NPAGE },
786 { "F10", KEY_F(10) },
787 { "F11", KEY_F(11) },
788 { "F12", KEY_F(12) },
792 get_key_value(const char *name)
796 for (i = 0; i < ARRAY_SIZE(key_table); i++)
797 if (!strcasecmp(key_table[i].name, name))
798 return key_table[i].value;
800 if (strlen(name) == 1 && isprint(*name))
807 get_key(enum request request)
809 static char buf[BUFSIZ];
810 static char key_char[] = "'X'";
817 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
818 struct keybinding *keybinding = &default_keybindings[i];
822 if (keybinding->request != request)
825 for (key = 0; key < ARRAY_SIZE(key_table); key++)
826 if (key_table[key].value == keybinding->alias)
827 seq = key_table[key].name;
830 keybinding->alias < 127 &&
831 isprint(keybinding->alias)) {
832 key_char[1] = (char) keybinding->alias;
839 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
840 return "Too many keybindings!";
849 * User config file handling.
852 static struct int_map color_map[] = {
853 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
865 #define set_color(color, name) \
866 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
868 static struct int_map attr_map[] = {
869 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
879 #define set_attribute(attr, name) \
880 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
882 static int config_lineno;
883 static bool config_errors;
884 static char *config_msg;
886 /* Wants: object fgcolor bgcolor [attr] */
888 option_color_command(int argc, char *argv[])
890 struct line_info *info;
892 if (argc != 3 && argc != 4) {
893 config_msg = "Wrong number of arguments given to color command";
897 info = get_line_info(argv[0], strlen(argv[0]));
899 config_msg = "Unknown color name";
903 if (set_color(&info->fg, argv[1]) == ERR ||
904 set_color(&info->bg, argv[2]) == ERR) {
905 config_msg = "Unknown color";
909 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
910 config_msg = "Unknown attribute";
917 /* Wants: name = value */
919 option_set_command(int argc, char *argv[])
922 config_msg = "Wrong number of arguments given to set command";
926 if (strcmp(argv[1], "=")) {
927 config_msg = "No value assigned";
931 if (!strcmp(argv[0], "show-rev-graph")) {
932 opt_rev_graph = (!strcmp(argv[2], "1") ||
933 !strcmp(argv[2], "true") ||
934 !strcmp(argv[2], "yes"));
938 if (!strcmp(argv[0], "line-number-interval")) {
939 opt_num_interval = atoi(argv[2]);
943 if (!strcmp(argv[0], "tab-size")) {
944 opt_tab_size = atoi(argv[2]);
948 if (!strcmp(argv[0], "commit-encoding")) {
949 string_copy(opt_encoding, argv[2]);
953 config_msg = "Unknown variable name";
957 /* Wants: mode request key */
959 option_bind_command(int argc, char *argv[])
961 enum request request;
966 config_msg = "Wrong number of arguments given to bind command";
970 if (set_keymap(&keymap, argv[0]) == ERR) {
971 config_msg = "Unknown key map";
975 key = get_key_value(argv[1]);
977 config_msg = "Unknown key";
981 request = get_request(argv[2]);
982 if (request == REQ_UNKNOWN) {
983 config_msg = "Unknown request name";
987 add_keybinding(keymap, request, key);
993 set_option(char *opt, char *value)
1000 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1001 argv[argc++] = value;
1008 while (isspace(*value))
1012 if (!strcmp(opt, "color"))
1013 return option_color_command(argc, argv);
1015 if (!strcmp(opt, "set"))
1016 return option_set_command(argc, argv);
1018 if (!strcmp(opt, "bind"))
1019 return option_bind_command(argc, argv);
1021 config_msg = "Unknown option command";
1026 read_option(char *opt, int optlen, char *value, int valuelen)
1031 config_msg = "Internal error";
1033 /* Check for comment markers, since read_properties() will
1034 * only ensure opt and value are split at first " \t". */
1035 optlen = strcspn(opt, "#;");
1039 if (opt[optlen] != 0) {
1040 config_msg = "No option value";
1044 /* Look for comment endings in the value. */
1045 int len = strcspn(value, "#;");
1047 if (len < valuelen) {
1049 value[valuelen] = 0;
1052 status = set_option(opt, value);
1055 if (status == ERR) {
1056 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1057 config_lineno, optlen, opt, config_msg);
1058 config_errors = TRUE;
1061 /* Always keep going if errors are encountered. */
1068 char *home = getenv("HOME");
1073 config_errors = FALSE;
1075 if (!home || !string_format(buf, "%s/.tigrc", home))
1078 /* It's ok that the file doesn't exist. */
1079 file = fopen(buf, "r");
1083 if (read_properties(file, " \t", read_option) == ERR ||
1084 config_errors == TRUE)
1085 fprintf(stderr, "Errors while loading %s.\n", buf);
1098 /* The display array of active views and the index of the current view. */
1099 static struct view *display[2];
1100 static unsigned int current_view;
1102 #define foreach_view(view, i) \
1103 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1105 #define displayed_views() (display[1] != NULL ? 2 : 1)
1107 /* Current head and commit ID */
1108 static char ref_commit[SIZEOF_REF] = "HEAD";
1109 static char ref_head[SIZEOF_REF] = "HEAD";
1112 const char *name; /* View name */
1113 const char *cmd_fmt; /* Default command line format */
1114 const char *cmd_env; /* Command line set via environment */
1115 const char *id; /* Points to either of ref_{head,commit} */
1117 struct view_ops *ops; /* View operations */
1119 enum keymap keymap; /* What keymap does this view have */
1121 char cmd[SIZEOF_CMD]; /* Command buffer */
1122 char ref[SIZEOF_REF]; /* Hovered commit reference */
1123 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1125 int height, width; /* The width and height of the main window */
1126 WINDOW *win; /* The main window */
1127 WINDOW *title; /* The title window living below the main window */
1130 unsigned long offset; /* Offset of the window top */
1131 unsigned long lineno; /* Current line number */
1133 /* If non-NULL, points to the view that opened this view. If this view
1134 * is closed tig will switch back to the parent view. */
1135 struct view *parent;
1138 unsigned long lines; /* Total number of lines */
1139 struct line *line; /* Line index */
1140 unsigned long line_size;/* Total number of allocated lines */
1141 unsigned int digits; /* Number of digits in the lines member. */
1149 /* What type of content being displayed. Used in the title bar. */
1151 /* Draw one line; @lineno must be < view->height. */
1152 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1153 /* Read one line; updates view->line. */
1154 bool (*read)(struct view *view, char *data);
1155 /* Depending on view, change display based on current line. */
1156 bool (*enter)(struct view *view, struct line *line);
1159 static struct view_ops pager_ops;
1160 static struct view_ops main_ops;
1162 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1163 { name, cmd, #env, ref, ops, map}
1165 #define VIEW_(id, name, ops, ref) \
1166 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1169 static struct view views[] = {
1170 VIEW_(MAIN, "main", &main_ops, ref_head),
1171 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1172 VIEW_(LOG, "log", &pager_ops, ref_head),
1173 VIEW_(HELP, "help", &pager_ops, "static"),
1174 VIEW_(PAGER, "pager", &pager_ops, "static"),
1177 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1181 draw_view_line(struct view *view, unsigned int lineno)
1183 if (view->offset + lineno >= view->lines)
1186 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
1190 redraw_view_from(struct view *view, int lineno)
1192 assert(0 <= lineno && lineno < view->height);
1194 for (; lineno < view->height; lineno++) {
1195 if (!draw_view_line(view, lineno))
1199 redrawwin(view->win);
1200 wrefresh(view->win);
1204 redraw_view(struct view *view)
1207 redraw_view_from(view, 0);
1212 update_view_title(struct view *view)
1214 if (view == display[current_view])
1215 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1217 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1219 werase(view->title);
1220 wmove(view->title, 0, 0);
1223 wprintw(view->title, "[%s] %s", view->name, view->ref);
1225 wprintw(view->title, "[%s]", view->name);
1227 if (view->lines || view->pipe) {
1228 unsigned int view_lines = view->offset + view->height;
1229 unsigned int lines = view->lines
1230 ? MIN(view_lines, view->lines) * 100 / view->lines
1233 wprintw(view->title, " - %s %d of %d (%d%%)",
1241 time_t secs = time(NULL) - view->start_time;
1243 /* Three git seconds are a long time ... */
1245 wprintw(view->title, " %lds", secs);
1248 wmove(view->title, 0, view->width - 1);
1249 wrefresh(view->title);
1253 resize_display(void)
1256 struct view *base = display[0];
1257 struct view *view = display[1] ? display[1] : display[0];
1259 /* Setup window dimensions */
1261 getmaxyx(stdscr, base->height, base->width);
1263 /* Make room for the status window. */
1267 /* Horizontal split. */
1268 view->width = base->width;
1269 view->height = SCALE_SPLIT_VIEW(base->height);
1270 base->height -= view->height;
1272 /* Make room for the title bar. */
1276 /* Make room for the title bar. */
1281 foreach_view (view, i) {
1283 view->win = newwin(view->height, 0, offset, 0);
1285 die("Failed to create %s view", view->name);
1287 scrollok(view->win, TRUE);
1289 view->title = newwin(1, 0, offset + view->height, 0);
1291 die("Failed to create title window");
1294 wresize(view->win, view->height, view->width);
1295 mvwin(view->win, offset, 0);
1296 mvwin(view->title, offset + view->height, 0);
1299 offset += view->height + 1;
1304 redraw_display(void)
1309 foreach_view (view, i) {
1311 update_view_title(view);
1316 update_display_cursor(void)
1318 struct view *view = display[current_view];
1320 /* Move the cursor to the right-most column of the cursor line.
1322 * XXX: This could turn out to be a bit expensive, but it ensures that
1323 * the cursor does not jump around. */
1325 wmove(view->win, view->lineno - view->offset, view->width - 1);
1326 wrefresh(view->win);
1334 /* Scrolling backend */
1336 do_scroll_view(struct view *view, int lines, bool redraw)
1338 /* The rendering expects the new offset. */
1339 view->offset += lines;
1341 assert(0 <= view->offset && view->offset < view->lines);
1344 /* Redraw the whole screen if scrolling is pointless. */
1345 if (view->height < ABS(lines)) {
1349 int line = lines > 0 ? view->height - lines : 0;
1350 int end = line + ABS(lines);
1352 wscrl(view->win, lines);
1354 for (; line < end; line++) {
1355 if (!draw_view_line(view, line))
1360 /* Move current line into the view. */
1361 if (view->lineno < view->offset) {
1362 view->lineno = view->offset;
1363 draw_view_line(view, 0);
1365 } else if (view->lineno >= view->offset + view->height) {
1366 if (view->lineno == view->offset + view->height) {
1367 /* Clear the hidden line so it doesn't show if the view
1368 * is scrolled up. */
1369 wmove(view->win, view->height, 0);
1370 wclrtoeol(view->win);
1372 view->lineno = view->offset + view->height - 1;
1373 draw_view_line(view, view->lineno - view->offset);
1376 assert(view->offset <= view->lineno && view->lineno < view->lines);
1381 redrawwin(view->win);
1382 wrefresh(view->win);
1386 /* Scroll frontend */
1388 scroll_view(struct view *view, enum request request)
1393 case REQ_SCROLL_PAGE_DOWN:
1394 lines = view->height;
1395 case REQ_SCROLL_LINE_DOWN:
1396 if (view->offset + lines > view->lines)
1397 lines = view->lines - view->offset;
1399 if (lines == 0 || view->offset + view->height >= view->lines) {
1400 report("Cannot scroll beyond the last line");
1405 case REQ_SCROLL_PAGE_UP:
1406 lines = view->height;
1407 case REQ_SCROLL_LINE_UP:
1408 if (lines > view->offset)
1409 lines = view->offset;
1412 report("Cannot scroll beyond the first line");
1420 die("request %d not handled in switch", request);
1423 do_scroll_view(view, lines, TRUE);
1428 move_view(struct view *view, enum request request, bool redraw)
1433 case REQ_MOVE_FIRST_LINE:
1434 steps = -view->lineno;
1437 case REQ_MOVE_LAST_LINE:
1438 steps = view->lines - view->lineno - 1;
1441 case REQ_MOVE_PAGE_UP:
1442 steps = view->height > view->lineno
1443 ? -view->lineno : -view->height;
1446 case REQ_MOVE_PAGE_DOWN:
1447 steps = view->lineno + view->height >= view->lines
1448 ? view->lines - view->lineno - 1 : view->height;
1460 die("request %d not handled in switch", request);
1463 if (steps <= 0 && view->lineno == 0) {
1464 report("Cannot move beyond the first line");
1467 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1468 report("Cannot move beyond the last line");
1472 /* Move the current line */
1473 view->lineno += steps;
1474 assert(0 <= view->lineno && view->lineno < view->lines);
1476 /* Repaint the old "current" line if we be scrolling */
1477 if (ABS(steps) < view->height) {
1478 int prev_lineno = view->lineno - steps - view->offset;
1480 wmove(view->win, prev_lineno, 0);
1481 wclrtoeol(view->win);
1482 draw_view_line(view, prev_lineno);
1485 /* Check whether the view needs to be scrolled */
1486 if (view->lineno < view->offset ||
1487 view->lineno >= view->offset + view->height) {
1488 if (steps < 0 && -steps > view->offset) {
1489 steps = -view->offset;
1491 } else if (steps > 0) {
1492 if (view->lineno == view->lines - 1 &&
1493 view->lines > view->height) {
1494 steps = view->lines - view->offset - 1;
1495 if (steps >= view->height)
1496 steps -= view->height - 1;
1500 do_scroll_view(view, steps, redraw);
1504 /* Draw the current line */
1505 draw_view_line(view, view->lineno - view->offset);
1510 redrawwin(view->win);
1511 wrefresh(view->win);
1517 * Incremental updating
1521 end_update(struct view *view)
1525 set_nonblocking_input(FALSE);
1526 if (view->pipe == stdin)
1534 begin_update(struct view *view)
1536 const char *id = view->id;
1542 string_copy(view->cmd, opt_cmd);
1544 /* When running random commands, the view ref could have become
1545 * invalid so clear it. */
1548 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1550 if (!string_format(view->cmd, format, id, id, id, id, id))
1554 /* Special case for the pager view. */
1556 view->pipe = opt_pipe;
1559 view->pipe = popen(view->cmd, "r");
1565 set_nonblocking_input(TRUE);
1570 string_copy(view->vid, id);
1575 for (i = 0; i < view->lines; i++)
1576 if (view->line[i].data)
1577 free(view->line[i].data);
1583 view->start_time = time(NULL);
1588 static struct line *
1589 realloc_lines(struct view *view, size_t line_size)
1591 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1597 view->line_size = line_size;
1602 update_view(struct view *view)
1604 char buffer[BUFSIZ];
1606 /* The number of lines to read. If too low it will cause too much
1607 * redrawing (and possible flickering), if too high responsiveness
1609 unsigned long lines = view->height;
1610 int redraw_from = -1;
1615 /* Only redraw if lines are visible. */
1616 if (view->offset + view->height >= view->lines)
1617 redraw_from = view->lines - view->offset;
1619 if (!realloc_lines(view, view->lines + lines))
1622 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1623 int linelen = strlen(line);
1626 line[linelen - 1] = 0;
1628 if (!view->ops->read(view, line))
1638 lines = view->lines;
1639 for (digits = 0; lines; digits++)
1642 /* Keep the displayed view in sync with line number scaling. */
1643 if (digits != view->digits) {
1644 view->digits = digits;
1649 if (redraw_from >= 0) {
1650 /* If this is an incremental update, redraw the previous line
1651 * since for commits some members could have changed when
1652 * loading the main view. */
1653 if (redraw_from > 0)
1656 /* Incrementally draw avoids flickering. */
1657 redraw_view_from(view, redraw_from);
1660 /* Update the title _after_ the redraw so that if the redraw picks up a
1661 * commit reference in view->ref it'll be available here. */
1662 update_view_title(view);
1664 if (ferror(view->pipe)) {
1665 report("Failed to read: %s", strerror(errno));
1668 } else if (feof(view->pipe)) {
1676 report("Allocation failure");
1688 static void open_help_view(struct view *view)
1691 int lines = ARRAY_SIZE(req_info) + 2;
1694 if (view->lines > 0)
1697 for (i = 0; i < ARRAY_SIZE(req_info); i++)
1698 if (!req_info[i].request)
1701 view->line = calloc(lines, sizeof(*view->line));
1703 report("Allocation failure");
1707 view->ops->read(view, "Quick reference for tig keybindings:");
1709 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
1712 if (!req_info[i].request) {
1713 view->ops->read(view, "");
1714 view->ops->read(view, req_info[i].help);
1718 key = get_key(req_info[i].request);
1719 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
1722 view->ops->read(view, buf);
1727 OPEN_DEFAULT = 0, /* Use default view switching. */
1728 OPEN_SPLIT = 1, /* Split current view. */
1729 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1730 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1734 open_view(struct view *prev, enum request request, enum open_flags flags)
1736 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1737 bool split = !!(flags & OPEN_SPLIT);
1738 bool reload = !!(flags & OPEN_RELOAD);
1739 struct view *view = VIEW(request);
1740 int nviews = displayed_views();
1741 struct view *base_view = display[0];
1743 if (view == prev && nviews == 1 && !reload) {
1744 report("Already in %s view", view->name);
1748 if (view == VIEW(REQ_VIEW_HELP)) {
1749 open_help_view(view);
1751 } else if ((reload || strcmp(view->vid, view->id)) &&
1752 !begin_update(view)) {
1753 report("Failed to load %s view", view->name);
1762 /* Maximize the current view. */
1763 memset(display, 0, sizeof(display));
1765 display[current_view] = view;
1768 /* Resize the view when switching between split- and full-screen,
1769 * or when switching between two different full-screen views. */
1770 if (nviews != displayed_views() ||
1771 (nviews == 1 && base_view != display[0]))
1774 if (split && prev->lineno - prev->offset >= prev->height) {
1775 /* Take the title line into account. */
1776 int lines = prev->lineno - prev->offset - prev->height + 1;
1778 /* Scroll the view that was split if the current line is
1779 * outside the new limited view. */
1780 do_scroll_view(prev, lines, TRUE);
1783 if (prev && view != prev) {
1784 if (split && !backgrounded) {
1785 /* "Blur" the previous view. */
1786 update_view_title(prev);
1789 view->parent = prev;
1792 if (view->pipe && view->lines == 0) {
1793 /* Clear the old view and let the incremental updating refill
1802 /* If the view is backgrounded the above calls to report()
1803 * won't redraw the view title. */
1805 update_view_title(view);
1810 * User request switch noodle
1814 view_driver(struct view *view, enum request request)
1821 case REQ_MOVE_PAGE_UP:
1822 case REQ_MOVE_PAGE_DOWN:
1823 case REQ_MOVE_FIRST_LINE:
1824 case REQ_MOVE_LAST_LINE:
1825 move_view(view, request, TRUE);
1828 case REQ_SCROLL_LINE_DOWN:
1829 case REQ_SCROLL_LINE_UP:
1830 case REQ_SCROLL_PAGE_DOWN:
1831 case REQ_SCROLL_PAGE_UP:
1832 scroll_view(view, request);
1839 case REQ_VIEW_PAGER:
1840 open_view(view, request, OPEN_DEFAULT);
1845 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1847 if (view == VIEW(REQ_VIEW_DIFF) &&
1848 view->parent == VIEW(REQ_VIEW_MAIN)) {
1849 bool redraw = display[1] == view;
1851 view = view->parent;
1852 move_view(view, request, redraw);
1854 update_view_title(view);
1856 move_view(view, request, TRUE);
1863 report("Nothing to enter");
1866 return view->ops->enter(view, &view->line[view->lineno]);
1870 int nviews = displayed_views();
1871 int next_view = (current_view + 1) % nviews;
1873 if (next_view == current_view) {
1874 report("Only one view is displayed");
1878 current_view = next_view;
1879 /* Blur out the title of the previous view. */
1880 update_view_title(view);
1884 case REQ_TOGGLE_LINENO:
1885 opt_line_number = !opt_line_number;
1889 case REQ_TOGGLE_REV_GRAPH:
1890 opt_rev_graph = !opt_rev_graph;
1895 /* Always reload^Wrerun commands from the prompt. */
1896 open_view(view, opt_request, OPEN_RELOAD);
1899 case REQ_STOP_LOADING:
1900 for (i = 0; i < ARRAY_SIZE(views); i++) {
1903 report("Stopped loading the %s view", view->name),
1908 case REQ_SHOW_VERSION:
1909 report("%s (built %s)", VERSION, __DATE__);
1912 case REQ_SCREEN_RESIZE:
1915 case REQ_SCREEN_REDRAW:
1919 case REQ_SCREEN_UPDATE:
1923 case REQ_VIEW_CLOSE:
1924 /* XXX: Mark closed views by letting view->parent point to the
1925 * view itself. Parents to closed view should never be
1928 view->parent->parent != view->parent) {
1929 memset(display, 0, sizeof(display));
1931 display[current_view] = view->parent;
1932 view->parent = view;
1942 /* An unknown key will show most commonly used commands. */
1943 report("Unknown key, press 'h' for help");
1956 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1958 char *text = line->data;
1959 enum line_type type = line->type;
1960 int textlen = strlen(text);
1963 wmove(view->win, lineno, 0);
1965 if (view->offset + lineno == view->lineno) {
1966 if (type == LINE_COMMIT) {
1967 string_copy(view->ref, text + 7);
1968 string_copy(ref_commit, view->ref);
1972 wchgat(view->win, -1, 0, type, NULL);
1975 attr = get_line_attr(type);
1976 wattrset(view->win, attr);
1978 if (opt_line_number || opt_tab_size < TABSIZE) {
1979 static char spaces[] = " ";
1980 int col_offset = 0, col = 0;
1982 if (opt_line_number) {
1983 unsigned long real_lineno = view->offset + lineno + 1;
1985 if (real_lineno == 1 ||
1986 (real_lineno % opt_num_interval) == 0) {
1987 wprintw(view->win, "%.*d", view->digits, real_lineno);
1990 waddnstr(view->win, spaces,
1991 MIN(view->digits, STRING_SIZE(spaces)));
1993 waddstr(view->win, ": ");
1994 col_offset = view->digits + 2;
1997 while (text && col_offset + col < view->width) {
1998 int cols_max = view->width - col_offset - col;
2002 if (*text == '\t') {
2004 assert(sizeof(spaces) > TABSIZE);
2006 cols = opt_tab_size - (col % opt_tab_size);
2009 text = strchr(text, '\t');
2010 cols = line ? text - pos : strlen(pos);
2013 waddnstr(view->win, pos, MIN(cols, cols_max));
2018 int col = 0, pos = 0;
2020 for (; pos < textlen && col < view->width; pos++, col++)
2021 if (text[pos] == '\t')
2022 col += TABSIZE - (col % TABSIZE) - 1;
2024 waddnstr(view->win, text, pos);
2031 add_pager_refs(struct view *view, struct line *line)
2034 char *data = line->data;
2036 int bufpos = 0, refpos = 0;
2037 const char *sep = "Refs: ";
2039 assert(line->type == LINE_COMMIT);
2041 refs = get_refs(data + STRING_SIZE("commit "));
2046 struct ref *ref = refs[refpos];
2047 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
2049 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2052 } while (refs[refpos++]->next);
2054 if (!realloc_lines(view, view->line_size + 1))
2057 line = &view->line[view->lines];
2058 line->data = strdup(buf);
2062 line->type = LINE_PP_REFS;
2067 pager_read(struct view *view, char *data)
2069 struct line *line = &view->line[view->lines];
2071 line->data = strdup(data);
2075 line->type = get_line_type(line->data);
2078 if (line->type == LINE_COMMIT &&
2079 (view == VIEW(REQ_VIEW_DIFF) ||
2080 view == VIEW(REQ_VIEW_LOG)))
2081 add_pager_refs(view, line);
2087 pager_enter(struct view *view, struct line *line)
2091 if (line->type == LINE_COMMIT &&
2092 (view == VIEW(REQ_VIEW_LOG) ||
2093 view == VIEW(REQ_VIEW_PAGER))) {
2094 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2098 /* Always scroll the view even if it was split. That way
2099 * you can use Enter to scroll through the log view and
2100 * split open each commit diff. */
2101 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2103 /* FIXME: A minor workaround. Scrolling the view will call report("")
2104 * but if we are scrolling a non-current view this won't properly
2105 * update the view title. */
2107 update_view_title(view);
2112 static struct view_ops pager_ops = {
2125 char id[41]; /* SHA1 ID. */
2126 char title[75]; /* First line of the commit message. */
2127 char author[75]; /* Author of the commit. */
2128 struct tm time; /* Date from the author ident. */
2129 struct ref **refs; /* Repository references. */
2130 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
2131 size_t graph_size; /* The width of the graph array. */
2135 main_draw(struct view *view, struct line *line, unsigned int lineno)
2137 char buf[DATE_COLS + 1];
2138 struct commit *commit = line->data;
2139 enum line_type type;
2145 if (!*commit->author)
2148 wmove(view->win, lineno, col);
2150 if (view->offset + lineno == view->lineno) {
2151 string_copy(view->ref, commit->id);
2152 string_copy(ref_commit, view->ref);
2154 wattrset(view->win, get_line_attr(type));
2155 wchgat(view->win, -1, 0, type, NULL);
2158 type = LINE_MAIN_COMMIT;
2159 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
2162 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
2163 waddnstr(view->win, buf, timelen);
2164 waddstr(view->win, " ");
2167 wmove(view->win, lineno, col);
2168 if (type != LINE_CURSOR)
2169 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
2172 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
2174 authorlen = strlen(commit->author);
2175 if (authorlen > AUTHOR_COLS - 2) {
2176 authorlen = AUTHOR_COLS - 2;
2182 waddnstr(view->win, commit->author, authorlen);
2183 if (type != LINE_CURSOR)
2184 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
2185 waddch(view->win, '~');
2187 waddstr(view->win, commit->author);
2191 if (type != LINE_CURSOR)
2192 wattrset(view->win, A_NORMAL);
2194 if (opt_rev_graph && commit->graph_size) {
2197 wmove(view->win, lineno, col);
2198 /* Using waddch() instead of waddnstr() ensures that
2199 * they'll be rendered correctly for the cursor line. */
2200 for (i = 0; i < commit->graph_size; i++)
2201 waddch(view->win, commit->graph[i]);
2203 col += commit->graph_size + 1;
2206 wmove(view->win, lineno, col);
2212 if (type == LINE_CURSOR)
2214 else if (commit->refs[i]->tag)
2215 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
2217 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
2218 waddstr(view->win, "[");
2219 waddstr(view->win, commit->refs[i]->name);
2220 waddstr(view->win, "]");
2221 if (type != LINE_CURSOR)
2222 wattrset(view->win, A_NORMAL);
2223 waddstr(view->win, " ");
2224 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
2225 } while (commit->refs[i++]->next);
2228 if (type != LINE_CURSOR)
2229 wattrset(view->win, get_line_attr(type));
2232 int titlelen = strlen(commit->title);
2234 if (col + titlelen > view->width)
2235 titlelen = view->width - col;
2237 waddnstr(view->win, commit->title, titlelen);
2243 /* Reads git log --pretty=raw output and parses it into the commit struct. */
2245 main_read(struct view *view, char *line)
2247 enum line_type type = get_line_type(line);
2248 struct commit *commit = view->lines
2249 ? view->line[view->lines - 1].data : NULL;
2253 commit = calloc(1, sizeof(struct commit));
2257 line += STRING_SIZE("commit ");
2259 view->line[view->lines++].data = commit;
2260 string_copy(commit->id, line);
2261 commit->refs = get_refs(commit->id);
2262 commit->graph[commit->graph_size++] = ACS_LTEE;
2267 char *ident = line + STRING_SIZE("author ");
2268 char *end = strchr(ident, '<');
2274 for (; end > ident && isspace(end[-1]); end--) ;
2278 string_copy(commit->author, ident);
2280 /* Parse epoch and timezone */
2282 char *secs = strchr(end + 1, '>');
2286 if (!secs || secs[1] != ' ')
2290 time = (time_t) atol(secs);
2291 zone = strchr(secs, ' ');
2292 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
2296 tz = ('0' - zone[1]) * 60 * 60 * 10;
2297 tz += ('0' - zone[2]) * 60 * 60;
2298 tz += ('0' - zone[3]) * 60;
2299 tz += ('0' - zone[4]) * 60;
2306 gmtime_r(&time, &commit->time);
2314 /* Fill in the commit title if it has not already been set. */
2315 if (commit->title[0])
2318 /* Require titles to start with a non-space character at the
2319 * offset used by git log. */
2320 /* FIXME: More gracefull handling of titles; append "..." to
2321 * shortened titles, etc. */
2322 if (strncmp(line, " ", 4) ||
2326 string_copy(commit->title, line + 4);
2333 main_enter(struct view *view, struct line *line)
2335 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2337 open_view(view, REQ_VIEW_DIFF, flags);
2341 static struct view_ops main_ops = {
2350 * Unicode / UTF-8 handling
2352 * NOTE: Much of the following code for dealing with unicode is derived from
2353 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2354 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2357 /* I've (over)annotated a lot of code snippets because I am not entirely
2358 * confident that the approach taken by this small UTF-8 interface is correct.
2362 unicode_width(unsigned long c)
2365 (c <= 0x115f /* Hangul Jamo */
2368 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2370 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2371 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2372 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2373 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2374 || (c >= 0xffe0 && c <= 0xffe6)
2375 || (c >= 0x20000 && c <= 0x2fffd)
2376 || (c >= 0x30000 && c <= 0x3fffd)))
2382 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2383 * Illegal bytes are set one. */
2384 static const unsigned char utf8_bytes[256] = {
2385 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2386 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2387 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2388 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2389 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2390 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2391 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,
2392 3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4, 5,5,5,5,6,6,1,1,
2395 /* Decode UTF-8 multi-byte representation into a unicode character. */
2396 static inline unsigned long
2397 utf8_to_unicode(const char *string, size_t length)
2399 unsigned long unicode;
2403 unicode = string[0];
2406 unicode = (string[0] & 0x1f) << 6;
2407 unicode += (string[1] & 0x3f);
2410 unicode = (string[0] & 0x0f) << 12;
2411 unicode += ((string[1] & 0x3f) << 6);
2412 unicode += (string[2] & 0x3f);
2415 unicode = (string[0] & 0x0f) << 18;
2416 unicode += ((string[1] & 0x3f) << 12);
2417 unicode += ((string[2] & 0x3f) << 6);
2418 unicode += (string[3] & 0x3f);
2421 unicode = (string[0] & 0x0f) << 24;
2422 unicode += ((string[1] & 0x3f) << 18);
2423 unicode += ((string[2] & 0x3f) << 12);
2424 unicode += ((string[3] & 0x3f) << 6);
2425 unicode += (string[4] & 0x3f);
2428 unicode = (string[0] & 0x01) << 30;
2429 unicode += ((string[1] & 0x3f) << 24);
2430 unicode += ((string[2] & 0x3f) << 18);
2431 unicode += ((string[3] & 0x3f) << 12);
2432 unicode += ((string[4] & 0x3f) << 6);
2433 unicode += (string[5] & 0x3f);
2436 die("Invalid unicode length");
2439 /* Invalid characters could return the special 0xfffd value but NUL
2440 * should be just as good. */
2441 return unicode > 0xffff ? 0 : unicode;
2444 /* Calculates how much of string can be shown within the given maximum width
2445 * and sets trimmed parameter to non-zero value if all of string could not be
2448 * Additionally, adds to coloffset how many many columns to move to align with
2449 * the expected position. Takes into account how multi-byte and double-width
2450 * characters will effect the cursor position.
2452 * Returns the number of bytes to output from string to satisfy max_width. */
2454 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2456 const char *start = string;
2457 const char *end = strchr(string, '\0');
2463 while (string < end) {
2464 int c = *(unsigned char *) string;
2465 unsigned char bytes = utf8_bytes[c];
2467 unsigned long unicode;
2469 if (string + bytes > end)
2472 /* Change representation to figure out whether
2473 * it is a single- or double-width character. */
2475 unicode = utf8_to_unicode(string, bytes);
2476 /* FIXME: Graceful handling of invalid unicode character. */
2480 ucwidth = unicode_width(unicode);
2482 if (width > max_width) {
2487 /* The column offset collects the differences between the
2488 * number of bytes encoding a character and the number of
2489 * columns will be used for rendering said character.
2491 * So if some character A is encoded in 2 bytes, but will be
2492 * represented on the screen using only 1 byte this will and up
2493 * adding 1 to the multi-byte column offset.
2495 * Assumes that no double-width character can be encoding in
2496 * less than two bytes. */
2497 if (bytes > ucwidth)
2498 mbwidth += bytes - ucwidth;
2503 *coloffset += mbwidth;
2505 return string - start;
2513 /* Whether or not the curses interface has been initialized. */
2514 static bool cursed = FALSE;
2516 /* The status window is used for polling keystrokes. */
2517 static WINDOW *status_win;
2519 /* Update status and title window. */
2521 report(const char *msg, ...)
2523 static bool empty = TRUE;
2524 struct view *view = display[current_view];
2526 if (!empty || *msg) {
2529 va_start(args, msg);
2532 wmove(status_win, 0, 0);
2534 vwprintw(status_win, msg, args);
2539 wrefresh(status_win);
2544 update_view_title(view);
2545 update_display_cursor();
2548 /* Controls when nodelay should be in effect when polling user input. */
2550 set_nonblocking_input(bool loading)
2552 static unsigned int loading_views;
2554 if ((loading == FALSE && loading_views-- == 1) ||
2555 (loading == TRUE && loading_views++ == 0))
2556 nodelay(status_win, loading);
2564 /* Initialize the curses library */
2565 if (isatty(STDIN_FILENO)) {
2566 cursed = !!initscr();
2568 /* Leave stdin and stdout alone when acting as a pager. */
2569 FILE *io = fopen("/dev/tty", "r+");
2571 cursed = !!newterm(NULL, io, io);
2575 die("Failed to initialize curses");
2577 nonl(); /* Tell curses not to do NL->CR/NL on output */
2578 cbreak(); /* Take input chars one at a time, no wait for \n */
2579 noecho(); /* Don't echo input */
2580 leaveok(stdscr, TRUE);
2585 getmaxyx(stdscr, y, x);
2586 status_win = newwin(1, 0, y - 1, 0);
2588 die("Failed to create status window");
2590 /* Enable keyboard mapping */
2591 keypad(status_win, TRUE);
2592 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2597 * Repository references
2600 static struct ref *refs;
2601 static size_t refs_size;
2603 /* Id <-> ref store */
2604 static struct ref ***id_refs;
2605 static size_t id_refs_size;
2607 static struct ref **
2610 struct ref ***tmp_id_refs;
2611 struct ref **ref_list = NULL;
2612 size_t ref_list_size = 0;
2615 for (i = 0; i < id_refs_size; i++)
2616 if (!strcmp(id, id_refs[i][0]->id))
2619 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2623 id_refs = tmp_id_refs;
2625 for (i = 0; i < refs_size; i++) {
2628 if (strcmp(id, refs[i].id))
2631 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2639 if (ref_list_size > 0)
2640 ref_list[ref_list_size - 1]->next = 1;
2641 ref_list[ref_list_size] = &refs[i];
2643 /* XXX: The properties of the commit chains ensures that we can
2644 * safely modify the shared ref. The repo references will
2645 * always be similar for the same id. */
2646 ref_list[ref_list_size]->next = 0;
2651 id_refs[id_refs_size++] = ref_list;
2657 read_ref(char *id, int idlen, char *name, int namelen)
2662 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2663 /* Commits referenced by tags has "^{}" appended. */
2664 if (name[namelen - 1] != '}')
2667 while (namelen > 0 && name[namelen] != '^')
2671 namelen -= STRING_SIZE("refs/tags/");
2672 name += STRING_SIZE("refs/tags/");
2674 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2675 namelen -= STRING_SIZE("refs/heads/");
2676 name += STRING_SIZE("refs/heads/");
2678 } else if (!strcmp(name, "HEAD")) {
2682 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2686 ref = &refs[refs_size++];
2687 ref->name = malloc(namelen + 1);
2691 strncpy(ref->name, name, namelen);
2692 ref->name[namelen] = 0;
2694 string_copy(ref->id, id);
2702 const char *cmd_env = getenv("TIG_LS_REMOTE");
2703 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2705 return read_properties(popen(cmd, "r"), "\t", read_ref);
2709 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2711 if (!strcmp(name, "i18n.commitencoding"))
2712 string_copy(opt_encoding, value);
2718 load_repo_config(void)
2720 return read_properties(popen("git repo-config --list", "r"),
2721 "=", read_repo_config_option);
2725 read_properties(FILE *pipe, const char *separators,
2726 int (*read_property)(char *, int, char *, int))
2728 char buffer[BUFSIZ];
2735 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2740 name = chomp_string(name);
2741 namelen = strcspn(name, separators);
2743 if (name[namelen]) {
2745 value = chomp_string(name + namelen + 1);
2746 valuelen = strlen(value);
2753 state = read_property(name, namelen, value, valuelen);
2756 if (state != ERR && ferror(pipe))
2769 static void __NORETURN
2772 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2778 static void __NORETURN
2779 die(const char *err, ...)
2785 va_start(args, err);
2786 fputs("tig: ", stderr);
2787 vfprintf(stderr, err, args);
2788 fputs("\n", stderr);
2795 main(int argc, char *argv[])
2798 enum request request;
2801 signal(SIGINT, quit);
2803 if (load_options() == ERR)
2804 die("Failed to load user config.");
2806 /* Load the repo config file so options can be overwritten from
2807 * the command line. */
2808 if (load_repo_config() == ERR)
2809 die("Failed to load repo config.");
2811 if (!parse_options(argc, argv))
2814 if (load_refs() == ERR)
2815 die("Failed to load refs.");
2817 /* Require a git repository unless when running in pager mode. */
2818 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2819 die("Not a git repository");
2821 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2822 view->cmd_env = getenv(view->cmd_env);
2824 request = opt_request;
2828 while (view_driver(display[current_view], request)) {
2832 foreach_view (view, i)
2835 /* Refresh, accept single keystroke of input */
2836 key = wgetch(status_win);
2838 request = get_keybinding(display[current_view]->keymap, key);
2840 /* Some low-level request handling. This keeps access to
2841 * status_win restricted. */
2845 /* Temporarily switch to line-oriented and echoed
2850 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2851 memcpy(opt_cmd, "git ", 4);
2852 opt_request = REQ_VIEW_PAGER;
2854 report("Prompt interrupted by loading view, "
2855 "press 'z' to stop loading views");
2856 request = REQ_SCREEN_UPDATE;
2863 case REQ_SCREEN_RESIZE:
2867 getmaxyx(stdscr, height, width);
2869 /* Resize the status view and let the view driver take
2870 * care of resizing the displayed views. */
2871 wresize(status_win, 1, width);
2872 mvwin(status_win, height - 1, 0);
2873 wrefresh(status_win);