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")) {
950 int delimiter = *arg;
956 for (arg++, i = 0; arg[i]; i++)
957 if (arg[i] == delimiter) {
962 string_copy(opt_encoding, arg);
967 config_msg = "Unknown variable name";
971 /* Wants: mode request key */
973 option_bind_command(int argc, char *argv[])
975 enum request request;
980 config_msg = "Wrong number of arguments given to bind command";
984 if (set_keymap(&keymap, argv[0]) == ERR) {
985 config_msg = "Unknown key map";
989 key = get_key_value(argv[1]);
991 config_msg = "Unknown key";
995 request = get_request(argv[2]);
996 if (request == REQ_UNKNOWN) {
997 config_msg = "Unknown request name";
1001 add_keybinding(keymap, request, key);
1007 set_option(char *opt, char *value)
1014 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1015 argv[argc++] = value;
1022 while (isspace(*value))
1026 if (!strcmp(opt, "color"))
1027 return option_color_command(argc, argv);
1029 if (!strcmp(opt, "set"))
1030 return option_set_command(argc, argv);
1032 if (!strcmp(opt, "bind"))
1033 return option_bind_command(argc, argv);
1035 config_msg = "Unknown option command";
1040 read_option(char *opt, int optlen, char *value, int valuelen)
1045 config_msg = "Internal error";
1047 /* Check for comment markers, since read_properties() will
1048 * only ensure opt and value are split at first " \t". */
1049 optlen = strcspn(opt, "#;");
1053 if (opt[optlen] != 0) {
1054 config_msg = "No option value";
1058 /* Look for comment endings in the value. */
1059 int len = strcspn(value, "#;");
1061 if (len < valuelen) {
1063 value[valuelen] = 0;
1066 status = set_option(opt, value);
1069 if (status == ERR) {
1070 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1071 config_lineno, optlen, opt, config_msg);
1072 config_errors = TRUE;
1075 /* Always keep going if errors are encountered. */
1082 char *home = getenv("HOME");
1087 config_errors = FALSE;
1089 if (!home || !string_format(buf, "%s/.tigrc", home))
1092 /* It's ok that the file doesn't exist. */
1093 file = fopen(buf, "r");
1097 if (read_properties(file, " \t", read_option) == ERR ||
1098 config_errors == TRUE)
1099 fprintf(stderr, "Errors while loading %s.\n", buf);
1112 /* The display array of active views and the index of the current view. */
1113 static struct view *display[2];
1114 static unsigned int current_view;
1116 #define foreach_view(view, i) \
1117 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1119 #define displayed_views() (display[1] != NULL ? 2 : 1)
1121 /* Current head and commit ID */
1122 static char ref_commit[SIZEOF_REF] = "HEAD";
1123 static char ref_head[SIZEOF_REF] = "HEAD";
1126 const char *name; /* View name */
1127 const char *cmd_fmt; /* Default command line format */
1128 const char *cmd_env; /* Command line set via environment */
1129 const char *id; /* Points to either of ref_{head,commit} */
1131 struct view_ops *ops; /* View operations */
1133 enum keymap keymap; /* What keymap does this view have */
1135 char cmd[SIZEOF_CMD]; /* Command buffer */
1136 char ref[SIZEOF_REF]; /* Hovered commit reference */
1137 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1139 int height, width; /* The width and height of the main window */
1140 WINDOW *win; /* The main window */
1141 WINDOW *title; /* The title window living below the main window */
1144 unsigned long offset; /* Offset of the window top */
1145 unsigned long lineno; /* Current line number */
1147 /* If non-NULL, points to the view that opened this view. If this view
1148 * is closed tig will switch back to the parent view. */
1149 struct view *parent;
1152 unsigned long lines; /* Total number of lines */
1153 struct line *line; /* Line index */
1154 unsigned long line_size;/* Total number of allocated lines */
1155 unsigned int digits; /* Number of digits in the lines member. */
1163 /* What type of content being displayed. Used in the title bar. */
1165 /* Draw one line; @lineno must be < view->height. */
1166 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1167 /* Read one line; updates view->line. */
1168 bool (*read)(struct view *view, char *data);
1169 /* Depending on view, change display based on current line. */
1170 bool (*enter)(struct view *view, struct line *line);
1173 static struct view_ops pager_ops;
1174 static struct view_ops main_ops;
1176 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1177 { name, cmd, #env, ref, ops, map}
1179 #define VIEW_(id, name, ops, ref) \
1180 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1183 static struct view views[] = {
1184 VIEW_(MAIN, "main", &main_ops, ref_head),
1185 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1186 VIEW_(LOG, "log", &pager_ops, ref_head),
1187 VIEW_(HELP, "help", &pager_ops, "static"),
1188 VIEW_(PAGER, "pager", &pager_ops, "static"),
1191 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1195 draw_view_line(struct view *view, unsigned int lineno)
1197 if (view->offset + lineno >= view->lines)
1200 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
1204 redraw_view_from(struct view *view, int lineno)
1206 assert(0 <= lineno && lineno < view->height);
1208 for (; lineno < view->height; lineno++) {
1209 if (!draw_view_line(view, lineno))
1213 redrawwin(view->win);
1214 wrefresh(view->win);
1218 redraw_view(struct view *view)
1221 redraw_view_from(view, 0);
1226 update_view_title(struct view *view)
1228 if (view == display[current_view])
1229 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1231 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1233 werase(view->title);
1234 wmove(view->title, 0, 0);
1237 wprintw(view->title, "[%s] %s", view->name, view->ref);
1239 wprintw(view->title, "[%s]", view->name);
1241 if (view->lines || view->pipe) {
1242 unsigned int view_lines = view->offset + view->height;
1243 unsigned int lines = view->lines
1244 ? MIN(view_lines, view->lines) * 100 / view->lines
1247 wprintw(view->title, " - %s %d of %d (%d%%)",
1255 time_t secs = time(NULL) - view->start_time;
1257 /* Three git seconds are a long time ... */
1259 wprintw(view->title, " %lds", secs);
1262 wmove(view->title, 0, view->width - 1);
1263 wrefresh(view->title);
1267 resize_display(void)
1270 struct view *base = display[0];
1271 struct view *view = display[1] ? display[1] : display[0];
1273 /* Setup window dimensions */
1275 getmaxyx(stdscr, base->height, base->width);
1277 /* Make room for the status window. */
1281 /* Horizontal split. */
1282 view->width = base->width;
1283 view->height = SCALE_SPLIT_VIEW(base->height);
1284 base->height -= view->height;
1286 /* Make room for the title bar. */
1290 /* Make room for the title bar. */
1295 foreach_view (view, i) {
1297 view->win = newwin(view->height, 0, offset, 0);
1299 die("Failed to create %s view", view->name);
1301 scrollok(view->win, TRUE);
1303 view->title = newwin(1, 0, offset + view->height, 0);
1305 die("Failed to create title window");
1308 wresize(view->win, view->height, view->width);
1309 mvwin(view->win, offset, 0);
1310 mvwin(view->title, offset + view->height, 0);
1313 offset += view->height + 1;
1318 redraw_display(void)
1323 foreach_view (view, i) {
1325 update_view_title(view);
1330 update_display_cursor(void)
1332 struct view *view = display[current_view];
1334 /* Move the cursor to the right-most column of the cursor line.
1336 * XXX: This could turn out to be a bit expensive, but it ensures that
1337 * the cursor does not jump around. */
1339 wmove(view->win, view->lineno - view->offset, view->width - 1);
1340 wrefresh(view->win);
1348 /* Scrolling backend */
1350 do_scroll_view(struct view *view, int lines, bool redraw)
1352 /* The rendering expects the new offset. */
1353 view->offset += lines;
1355 assert(0 <= view->offset && view->offset < view->lines);
1358 /* Redraw the whole screen if scrolling is pointless. */
1359 if (view->height < ABS(lines)) {
1363 int line = lines > 0 ? view->height - lines : 0;
1364 int end = line + ABS(lines);
1366 wscrl(view->win, lines);
1368 for (; line < end; line++) {
1369 if (!draw_view_line(view, line))
1374 /* Move current line into the view. */
1375 if (view->lineno < view->offset) {
1376 view->lineno = view->offset;
1377 draw_view_line(view, 0);
1379 } else if (view->lineno >= view->offset + view->height) {
1380 if (view->lineno == view->offset + view->height) {
1381 /* Clear the hidden line so it doesn't show if the view
1382 * is scrolled up. */
1383 wmove(view->win, view->height, 0);
1384 wclrtoeol(view->win);
1386 view->lineno = view->offset + view->height - 1;
1387 draw_view_line(view, view->lineno - view->offset);
1390 assert(view->offset <= view->lineno && view->lineno < view->lines);
1395 redrawwin(view->win);
1396 wrefresh(view->win);
1400 /* Scroll frontend */
1402 scroll_view(struct view *view, enum request request)
1407 case REQ_SCROLL_PAGE_DOWN:
1408 lines = view->height;
1409 case REQ_SCROLL_LINE_DOWN:
1410 if (view->offset + lines > view->lines)
1411 lines = view->lines - view->offset;
1413 if (lines == 0 || view->offset + view->height >= view->lines) {
1414 report("Cannot scroll beyond the last line");
1419 case REQ_SCROLL_PAGE_UP:
1420 lines = view->height;
1421 case REQ_SCROLL_LINE_UP:
1422 if (lines > view->offset)
1423 lines = view->offset;
1426 report("Cannot scroll beyond the first line");
1434 die("request %d not handled in switch", request);
1437 do_scroll_view(view, lines, TRUE);
1442 move_view(struct view *view, enum request request, bool redraw)
1447 case REQ_MOVE_FIRST_LINE:
1448 steps = -view->lineno;
1451 case REQ_MOVE_LAST_LINE:
1452 steps = view->lines - view->lineno - 1;
1455 case REQ_MOVE_PAGE_UP:
1456 steps = view->height > view->lineno
1457 ? -view->lineno : -view->height;
1460 case REQ_MOVE_PAGE_DOWN:
1461 steps = view->lineno + view->height >= view->lines
1462 ? view->lines - view->lineno - 1 : view->height;
1474 die("request %d not handled in switch", request);
1477 if (steps <= 0 && view->lineno == 0) {
1478 report("Cannot move beyond the first line");
1481 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1482 report("Cannot move beyond the last line");
1486 /* Move the current line */
1487 view->lineno += steps;
1488 assert(0 <= view->lineno && view->lineno < view->lines);
1490 /* Repaint the old "current" line if we be scrolling */
1491 if (ABS(steps) < view->height) {
1492 int prev_lineno = view->lineno - steps - view->offset;
1494 wmove(view->win, prev_lineno, 0);
1495 wclrtoeol(view->win);
1496 draw_view_line(view, prev_lineno);
1499 /* Check whether the view needs to be scrolled */
1500 if (view->lineno < view->offset ||
1501 view->lineno >= view->offset + view->height) {
1502 if (steps < 0 && -steps > view->offset) {
1503 steps = -view->offset;
1505 } else if (steps > 0) {
1506 if (view->lineno == view->lines - 1 &&
1507 view->lines > view->height) {
1508 steps = view->lines - view->offset - 1;
1509 if (steps >= view->height)
1510 steps -= view->height - 1;
1514 do_scroll_view(view, steps, redraw);
1518 /* Draw the current line */
1519 draw_view_line(view, view->lineno - view->offset);
1524 redrawwin(view->win);
1525 wrefresh(view->win);
1531 * Incremental updating
1535 end_update(struct view *view)
1539 set_nonblocking_input(FALSE);
1540 if (view->pipe == stdin)
1548 begin_update(struct view *view)
1550 const char *id = view->id;
1556 string_copy(view->cmd, opt_cmd);
1558 /* When running random commands, the view ref could have become
1559 * invalid so clear it. */
1562 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1564 if (!string_format(view->cmd, format, id, id, id, id, id))
1568 /* Special case for the pager view. */
1570 view->pipe = opt_pipe;
1573 view->pipe = popen(view->cmd, "r");
1579 set_nonblocking_input(TRUE);
1584 string_copy(view->vid, id);
1589 for (i = 0; i < view->lines; i++)
1590 if (view->line[i].data)
1591 free(view->line[i].data);
1597 view->start_time = time(NULL);
1602 static struct line *
1603 realloc_lines(struct view *view, size_t line_size)
1605 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1611 view->line_size = line_size;
1616 update_view(struct view *view)
1618 char buffer[BUFSIZ];
1620 /* The number of lines to read. If too low it will cause too much
1621 * redrawing (and possible flickering), if too high responsiveness
1623 unsigned long lines = view->height;
1624 int redraw_from = -1;
1629 /* Only redraw if lines are visible. */
1630 if (view->offset + view->height >= view->lines)
1631 redraw_from = view->lines - view->offset;
1633 if (!realloc_lines(view, view->lines + lines))
1636 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1637 int linelen = strlen(line);
1640 line[linelen - 1] = 0;
1642 if (!view->ops->read(view, line))
1652 lines = view->lines;
1653 for (digits = 0; lines; digits++)
1656 /* Keep the displayed view in sync with line number scaling. */
1657 if (digits != view->digits) {
1658 view->digits = digits;
1663 if (redraw_from >= 0) {
1664 /* If this is an incremental update, redraw the previous line
1665 * since for commits some members could have changed when
1666 * loading the main view. */
1667 if (redraw_from > 0)
1670 /* Incrementally draw avoids flickering. */
1671 redraw_view_from(view, redraw_from);
1674 /* Update the title _after_ the redraw so that if the redraw picks up a
1675 * commit reference in view->ref it'll be available here. */
1676 update_view_title(view);
1678 if (ferror(view->pipe)) {
1679 report("Failed to read: %s", strerror(errno));
1682 } else if (feof(view->pipe)) {
1690 report("Allocation failure");
1702 static void open_help_view(struct view *view)
1705 int lines = ARRAY_SIZE(req_info) + 2;
1708 if (view->lines > 0)
1711 for (i = 0; i < ARRAY_SIZE(req_info); i++)
1712 if (!req_info[i].request)
1715 view->line = calloc(lines, sizeof(*view->line));
1717 report("Allocation failure");
1721 view->ops->read(view, "Quick reference for tig keybindings:");
1723 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
1726 if (!req_info[i].request) {
1727 view->ops->read(view, "");
1728 view->ops->read(view, req_info[i].help);
1732 key = get_key(req_info[i].request);
1733 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
1736 view->ops->read(view, buf);
1741 OPEN_DEFAULT = 0, /* Use default view switching. */
1742 OPEN_SPLIT = 1, /* Split current view. */
1743 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1744 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1748 open_view(struct view *prev, enum request request, enum open_flags flags)
1750 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1751 bool split = !!(flags & OPEN_SPLIT);
1752 bool reload = !!(flags & OPEN_RELOAD);
1753 struct view *view = VIEW(request);
1754 int nviews = displayed_views();
1755 struct view *base_view = display[0];
1757 if (view == prev && nviews == 1 && !reload) {
1758 report("Already in %s view", view->name);
1762 if (view == VIEW(REQ_VIEW_HELP)) {
1763 open_help_view(view);
1765 } else if ((reload || strcmp(view->vid, view->id)) &&
1766 !begin_update(view)) {
1767 report("Failed to load %s view", view->name);
1776 /* Maximize the current view. */
1777 memset(display, 0, sizeof(display));
1779 display[current_view] = view;
1782 /* Resize the view when switching between split- and full-screen,
1783 * or when switching between two different full-screen views. */
1784 if (nviews != displayed_views() ||
1785 (nviews == 1 && base_view != display[0]))
1788 if (split && prev->lineno - prev->offset >= prev->height) {
1789 /* Take the title line into account. */
1790 int lines = prev->lineno - prev->offset - prev->height + 1;
1792 /* Scroll the view that was split if the current line is
1793 * outside the new limited view. */
1794 do_scroll_view(prev, lines, TRUE);
1797 if (prev && view != prev) {
1798 if (split && !backgrounded) {
1799 /* "Blur" the previous view. */
1800 update_view_title(prev);
1803 view->parent = prev;
1806 if (view->pipe && view->lines == 0) {
1807 /* Clear the old view and let the incremental updating refill
1816 /* If the view is backgrounded the above calls to report()
1817 * won't redraw the view title. */
1819 update_view_title(view);
1824 * User request switch noodle
1828 view_driver(struct view *view, enum request request)
1835 case REQ_MOVE_PAGE_UP:
1836 case REQ_MOVE_PAGE_DOWN:
1837 case REQ_MOVE_FIRST_LINE:
1838 case REQ_MOVE_LAST_LINE:
1839 move_view(view, request, TRUE);
1842 case REQ_SCROLL_LINE_DOWN:
1843 case REQ_SCROLL_LINE_UP:
1844 case REQ_SCROLL_PAGE_DOWN:
1845 case REQ_SCROLL_PAGE_UP:
1846 scroll_view(view, request);
1853 case REQ_VIEW_PAGER:
1854 open_view(view, request, OPEN_DEFAULT);
1859 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1861 if (view == VIEW(REQ_VIEW_DIFF) &&
1862 view->parent == VIEW(REQ_VIEW_MAIN)) {
1863 bool redraw = display[1] == view;
1865 view = view->parent;
1866 move_view(view, request, redraw);
1868 update_view_title(view);
1870 move_view(view, request, TRUE);
1877 report("Nothing to enter");
1880 return view->ops->enter(view, &view->line[view->lineno]);
1884 int nviews = displayed_views();
1885 int next_view = (current_view + 1) % nviews;
1887 if (next_view == current_view) {
1888 report("Only one view is displayed");
1892 current_view = next_view;
1893 /* Blur out the title of the previous view. */
1894 update_view_title(view);
1898 case REQ_TOGGLE_LINENO:
1899 opt_line_number = !opt_line_number;
1903 case REQ_TOGGLE_REV_GRAPH:
1904 opt_rev_graph = !opt_rev_graph;
1909 /* Always reload^Wrerun commands from the prompt. */
1910 open_view(view, opt_request, OPEN_RELOAD);
1913 case REQ_STOP_LOADING:
1914 for (i = 0; i < ARRAY_SIZE(views); i++) {
1917 report("Stopped loading the %s view", view->name),
1922 case REQ_SHOW_VERSION:
1923 report("%s (built %s)", VERSION, __DATE__);
1926 case REQ_SCREEN_RESIZE:
1929 case REQ_SCREEN_REDRAW:
1933 case REQ_SCREEN_UPDATE:
1937 case REQ_VIEW_CLOSE:
1938 /* XXX: Mark closed views by letting view->parent point to the
1939 * view itself. Parents to closed view should never be
1942 view->parent->parent != view->parent) {
1943 memset(display, 0, sizeof(display));
1945 display[current_view] = view->parent;
1946 view->parent = view;
1956 /* An unknown key will show most commonly used commands. */
1957 report("Unknown key, press 'h' for help");
1970 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1972 char *text = line->data;
1973 enum line_type type = line->type;
1974 int textlen = strlen(text);
1977 wmove(view->win, lineno, 0);
1979 if (view->offset + lineno == view->lineno) {
1980 if (type == LINE_COMMIT) {
1981 string_copy(view->ref, text + 7);
1982 string_copy(ref_commit, view->ref);
1986 wchgat(view->win, -1, 0, type, NULL);
1989 attr = get_line_attr(type);
1990 wattrset(view->win, attr);
1992 if (opt_line_number || opt_tab_size < TABSIZE) {
1993 static char spaces[] = " ";
1994 int col_offset = 0, col = 0;
1996 if (opt_line_number) {
1997 unsigned long real_lineno = view->offset + lineno + 1;
1999 if (real_lineno == 1 ||
2000 (real_lineno % opt_num_interval) == 0) {
2001 wprintw(view->win, "%.*d", view->digits, real_lineno);
2004 waddnstr(view->win, spaces,
2005 MIN(view->digits, STRING_SIZE(spaces)));
2007 waddstr(view->win, ": ");
2008 col_offset = view->digits + 2;
2011 while (text && col_offset + col < view->width) {
2012 int cols_max = view->width - col_offset - col;
2016 if (*text == '\t') {
2018 assert(sizeof(spaces) > TABSIZE);
2020 cols = opt_tab_size - (col % opt_tab_size);
2023 text = strchr(text, '\t');
2024 cols = line ? text - pos : strlen(pos);
2027 waddnstr(view->win, pos, MIN(cols, cols_max));
2032 int col = 0, pos = 0;
2034 for (; pos < textlen && col < view->width; pos++, col++)
2035 if (text[pos] == '\t')
2036 col += TABSIZE - (col % TABSIZE) - 1;
2038 waddnstr(view->win, text, pos);
2045 add_pager_refs(struct view *view, struct line *line)
2048 char *data = line->data;
2050 int bufpos = 0, refpos = 0;
2051 const char *sep = "Refs: ";
2053 assert(line->type == LINE_COMMIT);
2055 refs = get_refs(data + STRING_SIZE("commit "));
2060 struct ref *ref = refs[refpos];
2061 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
2063 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2066 } while (refs[refpos++]->next);
2068 if (!realloc_lines(view, view->line_size + 1))
2071 line = &view->line[view->lines];
2072 line->data = strdup(buf);
2076 line->type = LINE_PP_REFS;
2081 pager_read(struct view *view, char *data)
2083 struct line *line = &view->line[view->lines];
2085 line->data = strdup(data);
2089 line->type = get_line_type(line->data);
2092 if (line->type == LINE_COMMIT &&
2093 (view == VIEW(REQ_VIEW_DIFF) ||
2094 view == VIEW(REQ_VIEW_LOG)))
2095 add_pager_refs(view, line);
2101 pager_enter(struct view *view, struct line *line)
2105 if (line->type == LINE_COMMIT &&
2106 (view == VIEW(REQ_VIEW_LOG) ||
2107 view == VIEW(REQ_VIEW_PAGER))) {
2108 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2112 /* Always scroll the view even if it was split. That way
2113 * you can use Enter to scroll through the log view and
2114 * split open each commit diff. */
2115 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2117 /* FIXME: A minor workaround. Scrolling the view will call report("")
2118 * but if we are scrolling a non-current view this won't properly
2119 * update the view title. */
2121 update_view_title(view);
2126 static struct view_ops pager_ops = {
2139 char id[41]; /* SHA1 ID. */
2140 char title[75]; /* First line of the commit message. */
2141 char author[75]; /* Author of the commit. */
2142 struct tm time; /* Date from the author ident. */
2143 struct ref **refs; /* Repository references. */
2144 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
2145 size_t graph_size; /* The width of the graph array. */
2149 main_draw(struct view *view, struct line *line, unsigned int lineno)
2151 char buf[DATE_COLS + 1];
2152 struct commit *commit = line->data;
2153 enum line_type type;
2159 if (!*commit->author)
2162 wmove(view->win, lineno, col);
2164 if (view->offset + lineno == view->lineno) {
2165 string_copy(view->ref, commit->id);
2166 string_copy(ref_commit, view->ref);
2168 wattrset(view->win, get_line_attr(type));
2169 wchgat(view->win, -1, 0, type, NULL);
2172 type = LINE_MAIN_COMMIT;
2173 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
2176 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
2177 waddnstr(view->win, buf, timelen);
2178 waddstr(view->win, " ");
2181 wmove(view->win, lineno, col);
2182 if (type != LINE_CURSOR)
2183 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
2186 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
2188 authorlen = strlen(commit->author);
2189 if (authorlen > AUTHOR_COLS - 2) {
2190 authorlen = AUTHOR_COLS - 2;
2196 waddnstr(view->win, commit->author, authorlen);
2197 if (type != LINE_CURSOR)
2198 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
2199 waddch(view->win, '~');
2201 waddstr(view->win, commit->author);
2205 if (type != LINE_CURSOR)
2206 wattrset(view->win, A_NORMAL);
2208 if (opt_rev_graph && commit->graph_size) {
2211 wmove(view->win, lineno, col);
2212 /* Using waddch() instead of waddnstr() ensures that
2213 * they'll be rendered correctly for the cursor line. */
2214 for (i = 0; i < commit->graph_size; i++)
2215 waddch(view->win, commit->graph[i]);
2217 col += commit->graph_size + 1;
2220 wmove(view->win, lineno, col);
2226 if (type == LINE_CURSOR)
2228 else if (commit->refs[i]->tag)
2229 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
2231 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
2232 waddstr(view->win, "[");
2233 waddstr(view->win, commit->refs[i]->name);
2234 waddstr(view->win, "]");
2235 if (type != LINE_CURSOR)
2236 wattrset(view->win, A_NORMAL);
2237 waddstr(view->win, " ");
2238 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
2239 } while (commit->refs[i++]->next);
2242 if (type != LINE_CURSOR)
2243 wattrset(view->win, get_line_attr(type));
2246 int titlelen = strlen(commit->title);
2248 if (col + titlelen > view->width)
2249 titlelen = view->width - col;
2251 waddnstr(view->win, commit->title, titlelen);
2257 /* Reads git log --pretty=raw output and parses it into the commit struct. */
2259 main_read(struct view *view, char *line)
2261 enum line_type type = get_line_type(line);
2262 struct commit *commit = view->lines
2263 ? view->line[view->lines - 1].data : NULL;
2267 commit = calloc(1, sizeof(struct commit));
2271 line += STRING_SIZE("commit ");
2273 view->line[view->lines++].data = commit;
2274 string_copy(commit->id, line);
2275 commit->refs = get_refs(commit->id);
2276 commit->graph[commit->graph_size++] = ACS_LTEE;
2281 char *ident = line + STRING_SIZE("author ");
2282 char *end = strchr(ident, '<');
2288 for (; end > ident && isspace(end[-1]); end--) ;
2292 string_copy(commit->author, ident);
2294 /* Parse epoch and timezone */
2296 char *secs = strchr(end + 1, '>');
2300 if (!secs || secs[1] != ' ')
2304 time = (time_t) atol(secs);
2305 zone = strchr(secs, ' ');
2306 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
2310 tz = ('0' - zone[1]) * 60 * 60 * 10;
2311 tz += ('0' - zone[2]) * 60 * 60;
2312 tz += ('0' - zone[3]) * 60;
2313 tz += ('0' - zone[4]) * 60;
2320 gmtime_r(&time, &commit->time);
2328 /* Fill in the commit title if it has not already been set. */
2329 if (commit->title[0])
2332 /* Require titles to start with a non-space character at the
2333 * offset used by git log. */
2334 /* FIXME: More gracefull handling of titles; append "..." to
2335 * shortened titles, etc. */
2336 if (strncmp(line, " ", 4) ||
2340 string_copy(commit->title, line + 4);
2347 main_enter(struct view *view, struct line *line)
2349 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2351 open_view(view, REQ_VIEW_DIFF, flags);
2355 static struct view_ops main_ops = {
2364 * Unicode / UTF-8 handling
2366 * NOTE: Much of the following code for dealing with unicode is derived from
2367 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2368 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2371 /* I've (over)annotated a lot of code snippets because I am not entirely
2372 * confident that the approach taken by this small UTF-8 interface is correct.
2376 unicode_width(unsigned long c)
2379 (c <= 0x115f /* Hangul Jamo */
2382 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2384 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2385 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2386 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2387 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2388 || (c >= 0xffe0 && c <= 0xffe6)
2389 || (c >= 0x20000 && c <= 0x2fffd)
2390 || (c >= 0x30000 && c <= 0x3fffd)))
2396 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2397 * Illegal bytes are set one. */
2398 static const unsigned char utf8_bytes[256] = {
2399 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,
2400 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,
2401 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,
2402 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,
2403 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,
2404 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,
2405 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,
2406 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,
2409 /* Decode UTF-8 multi-byte representation into a unicode character. */
2410 static inline unsigned long
2411 utf8_to_unicode(const char *string, size_t length)
2413 unsigned long unicode;
2417 unicode = string[0];
2420 unicode = (string[0] & 0x1f) << 6;
2421 unicode += (string[1] & 0x3f);
2424 unicode = (string[0] & 0x0f) << 12;
2425 unicode += ((string[1] & 0x3f) << 6);
2426 unicode += (string[2] & 0x3f);
2429 unicode = (string[0] & 0x0f) << 18;
2430 unicode += ((string[1] & 0x3f) << 12);
2431 unicode += ((string[2] & 0x3f) << 6);
2432 unicode += (string[3] & 0x3f);
2435 unicode = (string[0] & 0x0f) << 24;
2436 unicode += ((string[1] & 0x3f) << 18);
2437 unicode += ((string[2] & 0x3f) << 12);
2438 unicode += ((string[3] & 0x3f) << 6);
2439 unicode += (string[4] & 0x3f);
2442 unicode = (string[0] & 0x01) << 30;
2443 unicode += ((string[1] & 0x3f) << 24);
2444 unicode += ((string[2] & 0x3f) << 18);
2445 unicode += ((string[3] & 0x3f) << 12);
2446 unicode += ((string[4] & 0x3f) << 6);
2447 unicode += (string[5] & 0x3f);
2450 die("Invalid unicode length");
2453 /* Invalid characters could return the special 0xfffd value but NUL
2454 * should be just as good. */
2455 return unicode > 0xffff ? 0 : unicode;
2458 /* Calculates how much of string can be shown within the given maximum width
2459 * and sets trimmed parameter to non-zero value if all of string could not be
2462 * Additionally, adds to coloffset how many many columns to move to align with
2463 * the expected position. Takes into account how multi-byte and double-width
2464 * characters will effect the cursor position.
2466 * Returns the number of bytes to output from string to satisfy max_width. */
2468 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2470 const char *start = string;
2471 const char *end = strchr(string, '\0');
2477 while (string < end) {
2478 int c = *(unsigned char *) string;
2479 unsigned char bytes = utf8_bytes[c];
2481 unsigned long unicode;
2483 if (string + bytes > end)
2486 /* Change representation to figure out whether
2487 * it is a single- or double-width character. */
2489 unicode = utf8_to_unicode(string, bytes);
2490 /* FIXME: Graceful handling of invalid unicode character. */
2494 ucwidth = unicode_width(unicode);
2496 if (width > max_width) {
2501 /* The column offset collects the differences between the
2502 * number of bytes encoding a character and the number of
2503 * columns will be used for rendering said character.
2505 * So if some character A is encoded in 2 bytes, but will be
2506 * represented on the screen using only 1 byte this will and up
2507 * adding 1 to the multi-byte column offset.
2509 * Assumes that no double-width character can be encoding in
2510 * less than two bytes. */
2511 if (bytes > ucwidth)
2512 mbwidth += bytes - ucwidth;
2517 *coloffset += mbwidth;
2519 return string - start;
2527 /* Whether or not the curses interface has been initialized. */
2528 static bool cursed = FALSE;
2530 /* The status window is used for polling keystrokes. */
2531 static WINDOW *status_win;
2533 /* Update status and title window. */
2535 report(const char *msg, ...)
2537 static bool empty = TRUE;
2538 struct view *view = display[current_view];
2540 if (!empty || *msg) {
2543 va_start(args, msg);
2546 wmove(status_win, 0, 0);
2548 vwprintw(status_win, msg, args);
2553 wrefresh(status_win);
2558 update_view_title(view);
2559 update_display_cursor();
2562 /* Controls when nodelay should be in effect when polling user input. */
2564 set_nonblocking_input(bool loading)
2566 static unsigned int loading_views;
2568 if ((loading == FALSE && loading_views-- == 1) ||
2569 (loading == TRUE && loading_views++ == 0))
2570 nodelay(status_win, loading);
2578 /* Initialize the curses library */
2579 if (isatty(STDIN_FILENO)) {
2580 cursed = !!initscr();
2582 /* Leave stdin and stdout alone when acting as a pager. */
2583 FILE *io = fopen("/dev/tty", "r+");
2585 cursed = !!newterm(NULL, io, io);
2589 die("Failed to initialize curses");
2591 nonl(); /* Tell curses not to do NL->CR/NL on output */
2592 cbreak(); /* Take input chars one at a time, no wait for \n */
2593 noecho(); /* Don't echo input */
2594 leaveok(stdscr, TRUE);
2599 getmaxyx(stdscr, y, x);
2600 status_win = newwin(1, 0, y - 1, 0);
2602 die("Failed to create status window");
2604 /* Enable keyboard mapping */
2605 keypad(status_win, TRUE);
2606 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2612 enum { READING, STOP, CANCEL } status = READING;
2613 char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
2616 while (status == READING) {
2620 foreach_view (view, i)
2623 report(":%.*s", pos, buf);
2624 /* Refresh, accept single keystroke of input */
2625 key = wgetch(status_win);
2630 status = pos ? STOP : CANCEL;
2648 if (pos >= sizeof(buf)) {
2649 report("Input string too long");
2654 buf[pos++] = (char) key;
2658 if (status == CANCEL) {
2659 /* Clear the status window */
2665 if (!string_format(opt_cmd, "git %s", buf))
2667 opt_request = REQ_VIEW_PAGER;
2673 * Repository references
2676 static struct ref *refs;
2677 static size_t refs_size;
2679 /* Id <-> ref store */
2680 static struct ref ***id_refs;
2681 static size_t id_refs_size;
2683 static struct ref **
2686 struct ref ***tmp_id_refs;
2687 struct ref **ref_list = NULL;
2688 size_t ref_list_size = 0;
2691 for (i = 0; i < id_refs_size; i++)
2692 if (!strcmp(id, id_refs[i][0]->id))
2695 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2699 id_refs = tmp_id_refs;
2701 for (i = 0; i < refs_size; i++) {
2704 if (strcmp(id, refs[i].id))
2707 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2715 if (ref_list_size > 0)
2716 ref_list[ref_list_size - 1]->next = 1;
2717 ref_list[ref_list_size] = &refs[i];
2719 /* XXX: The properties of the commit chains ensures that we can
2720 * safely modify the shared ref. The repo references will
2721 * always be similar for the same id. */
2722 ref_list[ref_list_size]->next = 0;
2727 id_refs[id_refs_size++] = ref_list;
2733 read_ref(char *id, int idlen, char *name, int namelen)
2738 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2739 /* Commits referenced by tags has "^{}" appended. */
2740 if (name[namelen - 1] != '}')
2743 while (namelen > 0 && name[namelen] != '^')
2747 namelen -= STRING_SIZE("refs/tags/");
2748 name += STRING_SIZE("refs/tags/");
2750 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2751 namelen -= STRING_SIZE("refs/heads/");
2752 name += STRING_SIZE("refs/heads/");
2754 } else if (!strcmp(name, "HEAD")) {
2758 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2762 ref = &refs[refs_size++];
2763 ref->name = malloc(namelen + 1);
2767 strncpy(ref->name, name, namelen);
2768 ref->name[namelen] = 0;
2770 string_copy(ref->id, id);
2778 const char *cmd_env = getenv("TIG_LS_REMOTE");
2779 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2781 return read_properties(popen(cmd, "r"), "\t", read_ref);
2785 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2787 if (!strcmp(name, "i18n.commitencoding"))
2788 string_copy(opt_encoding, value);
2794 load_repo_config(void)
2796 return read_properties(popen("git repo-config --list", "r"),
2797 "=", read_repo_config_option);
2801 read_properties(FILE *pipe, const char *separators,
2802 int (*read_property)(char *, int, char *, int))
2804 char buffer[BUFSIZ];
2811 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2816 name = chomp_string(name);
2817 namelen = strcspn(name, separators);
2819 if (name[namelen]) {
2821 value = chomp_string(name + namelen + 1);
2822 valuelen = strlen(value);
2829 state = read_property(name, namelen, value, valuelen);
2832 if (state != ERR && ferror(pipe))
2845 static void __NORETURN
2848 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2854 static void __NORETURN
2855 die(const char *err, ...)
2861 va_start(args, err);
2862 fputs("tig: ", stderr);
2863 vfprintf(stderr, err, args);
2864 fputs("\n", stderr);
2871 main(int argc, char *argv[])
2874 enum request request;
2877 signal(SIGINT, quit);
2879 if (load_options() == ERR)
2880 die("Failed to load user config.");
2882 /* Load the repo config file so options can be overwritten from
2883 * the command line. */
2884 if (load_repo_config() == ERR)
2885 die("Failed to load repo config.");
2887 if (!parse_options(argc, argv))
2890 if (load_refs() == ERR)
2891 die("Failed to load refs.");
2893 /* Require a git repository unless when running in pager mode. */
2894 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2895 die("Not a git repository");
2897 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2898 view->cmd_env = getenv(view->cmd_env);
2900 request = opt_request;
2904 while (view_driver(display[current_view], request)) {
2908 foreach_view (view, i)
2911 /* Refresh, accept single keystroke of input */
2912 key = wgetch(status_win);
2914 request = get_keybinding(display[current_view]->keymap, key);
2916 /* Some low-level request handling. This keeps access to
2917 * status_win restricted. */
2920 if (read_prompt() == ERR)
2921 request = REQ_SCREEN_UPDATE;
2924 case REQ_SCREEN_RESIZE:
2928 getmaxyx(stdscr, height, width);
2930 /* Resize the status view and let the view driver take
2931 * care of resizing the displayed views. */
2932 wresize(status_win, 1, width);
2933 mvwin(status_win, height - 1, 0);
2934 wrefresh(status_win);