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.5.git"
33 #include <sys/types.h>
43 #define __NORETURN __attribute__((__noreturn__))
48 static void __NORETURN die(const char *err, ...);
49 static void report(const char *msg, ...);
50 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
51 static void set_nonblocking_input(bool loading);
52 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
54 #define ABS(x) ((x) >= 0 ? (x) : -(x))
55 #define MIN(x, y) ((x) < (y) ? (x) : (y))
57 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
58 #define STRING_SIZE(x) (sizeof(x) - 1)
60 #define SIZEOF_STR 1024 /* Default string size. */
61 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
62 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
66 #define REVGRAPH_INIT 'I'
67 #define REVGRAPH_MERGE 'M'
68 #define REVGRAPH_BRANCH '+'
69 #define REVGRAPH_COMMIT '*'
70 #define REVGRAPH_LINE '|'
72 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
74 /* This color name can be used to refer to the default term colors. */
75 #define COLOR_DEFAULT (-1)
77 #define ICONV_NONE ((iconv_t) -1)
79 /* The format and size of the date column in the main view. */
80 #define DATE_FORMAT "%Y-%m-%d %H:%M"
81 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
83 #define AUTHOR_COLS 20
85 /* The default interval between line numbers. */
86 #define NUMBER_INTERVAL 1
90 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
92 #define TIG_LS_REMOTE \
93 "git ls-remote . 2>/dev/null"
95 #define TIG_DIFF_CMD \
96 "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
99 "git log --cc --stat -n100 %s 2>/dev/null"
101 #define TIG_MAIN_CMD \
102 "git log --topo-order --pretty=raw %s 2>/dev/null"
104 #define TIG_TREE_CMD \
107 #define TIG_BLOB_CMD \
108 "git cat-file blob %s"
110 /* XXX: Needs to be defined to the empty string. */
111 #define TIG_HELP_CMD ""
112 #define TIG_PAGER_CMD ""
114 /* Some ascii-shorthands fitted into the ncurses namespace. */
116 #define KEY_RETURN '\r'
121 char *name; /* Ref name; tag or head names are shortened. */
122 char id[SIZEOF_REV]; /* Commit SHA1 ID */
123 unsigned int tag:1; /* Is it a tag? */
124 unsigned int next:1; /* For ref lists: are there more refs? */
127 static struct ref **get_refs(char *id);
136 set_from_int_map(struct int_map *map, size_t map_size,
137 int *value, const char *name, int namelen)
142 for (i = 0; i < map_size; i++)
143 if (namelen == map[i].namelen &&
144 !strncasecmp(name, map[i].name, namelen)) {
145 *value = map[i].value;
158 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
160 if (srclen > dstlen - 1)
163 strncpy(dst, src, srclen);
167 /* Shorthands for safely copying into a fixed buffer. */
169 #define string_copy(dst, src) \
170 string_ncopy_do(dst, sizeof(dst), src, sizeof(dst))
172 #define string_ncopy(dst, src, srclen) \
173 string_ncopy_do(dst, sizeof(dst), src, srclen)
176 chomp_string(char *name)
180 while (isspace(*name))
183 namelen = strlen(name) - 1;
184 while (namelen > 0 && isspace(name[namelen]))
191 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
194 size_t pos = bufpos ? *bufpos : 0;
197 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
203 return pos >= bufsize ? FALSE : TRUE;
206 #define string_format(buf, fmt, args...) \
207 string_nformat(buf, sizeof(buf), NULL, fmt, args)
209 #define string_format_from(buf, from, fmt, args...) \
210 string_nformat(buf, sizeof(buf), from, fmt, args)
213 string_enum_compare(const char *str1, const char *str2, int len)
217 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
219 /* Diff-Header == DIFF_HEADER */
220 for (i = 0; i < len; i++) {
221 if (toupper(str1[i]) == toupper(str2[i]))
224 if (string_enum_sep(str1[i]) &&
225 string_enum_sep(str2[i]))
228 return str1[i] - str2[i];
236 * NOTE: The following is a slightly modified copy of the git project's shell
237 * quoting routines found in the quote.c file.
239 * Help to copy the thing properly quoted for the shell safety. any single
240 * quote is replaced with '\'', any exclamation point is replaced with '\!',
241 * and the whole thing is enclosed in a
244 * original sq_quote result
245 * name ==> name ==> 'name'
246 * a b ==> a b ==> 'a b'
247 * a'b ==> a'\''b ==> 'a'\''b'
248 * a!b ==> a'\!'b ==> 'a'\!'b'
252 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
256 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
259 while ((c = *src++)) {
260 if (c == '\'' || c == '!') {
280 /* XXX: Keep the view request first and in sync with views[]. */ \
281 REQ_GROUP("View switching") \
282 REQ_(VIEW_MAIN, "Show main view"), \
283 REQ_(VIEW_DIFF, "Show diff view"), \
284 REQ_(VIEW_LOG, "Show log view"), \
285 REQ_(VIEW_TREE, "Show tree view"), \
286 REQ_(VIEW_BLOB, "Show blob view"), \
287 REQ_(VIEW_HELP, "Show help page"), \
288 REQ_(VIEW_PAGER, "Show pager view"), \
290 REQ_GROUP("View manipulation") \
291 REQ_(ENTER, "Enter current line and scroll"), \
292 REQ_(NEXT, "Move to next"), \
293 REQ_(PREVIOUS, "Move to previous"), \
294 REQ_(VIEW_NEXT, "Move focus to next view"), \
295 REQ_(VIEW_CLOSE, "Close the current view"), \
296 REQ_(QUIT, "Close all views and quit"), \
298 REQ_GROUP("Cursor navigation") \
299 REQ_(MOVE_UP, "Move cursor one line up"), \
300 REQ_(MOVE_DOWN, "Move cursor one line down"), \
301 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
302 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
303 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
304 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
306 REQ_GROUP("Scrolling") \
307 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
308 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
309 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
310 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
312 REQ_GROUP("Searching") \
313 REQ_(SEARCH, "Search the view"), \
314 REQ_(SEARCH_BACK, "Search backwards in the view"), \
315 REQ_(FIND_NEXT, "Find next search match"), \
316 REQ_(FIND_PREV, "Find previous search match"), \
319 REQ_(NONE, "Do nothing"), \
320 REQ_(PROMPT, "Bring up the prompt"), \
321 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
322 REQ_(SCREEN_RESIZE, "Resize the screen"), \
323 REQ_(SHOW_VERSION, "Show version information"), \
324 REQ_(STOP_LOADING, "Stop all loading views"), \
325 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
326 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization")
329 /* User action requests. */
331 #define REQ_GROUP(help)
332 #define REQ_(req, help) REQ_##req
334 /* Offset all requests to avoid conflicts with ncurses getch values. */
335 REQ_OFFSET = KEY_MAX + 1,
343 struct request_info {
344 enum request request;
350 static struct request_info req_info[] = {
351 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
352 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
359 get_request(const char *name)
361 int namelen = strlen(name);
364 for (i = 0; i < ARRAY_SIZE(req_info); i++)
365 if (req_info[i].namelen == namelen &&
366 !string_enum_compare(req_info[i].name, name, namelen))
367 return req_info[i].request;
377 static const char usage[] =
378 VERSION " (" __DATE__ ")\n"
380 "Usage: tig [options]\n"
381 " or: tig [options] [--] [git log options]\n"
382 " or: tig [options] log [git log options]\n"
383 " or: tig [options] diff [git diff options]\n"
384 " or: tig [options] show [git show options]\n"
385 " or: tig [options] < [git command output]\n"
388 " -l Start up in log view\n"
389 " -d Start up in diff view\n"
390 " -n[I], --line-number[=I] Show line numbers with given interval\n"
391 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
392 " -- Mark end of tig options\n"
393 " -v, --version Show version and exit\n"
394 " -h, --help Show help message and exit\n";
396 /* Option and state variables. */
397 static bool opt_line_number = FALSE;
398 static bool opt_rev_graph = TRUE;
399 static int opt_num_interval = NUMBER_INTERVAL;
400 static int opt_tab_size = TABSIZE;
401 static enum request opt_request = REQ_VIEW_MAIN;
402 static char opt_cmd[SIZEOF_STR] = "";
403 static char opt_path[SIZEOF_STR] = "";
404 static FILE *opt_pipe = NULL;
405 static char opt_encoding[20] = "UTF-8";
406 static bool opt_utf8 = TRUE;
407 static char opt_codeset[20] = "UTF-8";
408 static iconv_t opt_iconv = ICONV_NONE;
409 static char opt_search[SIZEOF_STR] = "";
417 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
427 int namelen = strlen(name);
431 if (strncmp(opt, name, namelen))
434 if (opt[namelen] == '=')
435 value = opt + namelen + 1;
438 if (!short_name || opt[1] != short_name)
443 va_start(args, type);
444 if (type == OPT_INT) {
445 number = va_arg(args, int *);
447 *number = atoi(value);
454 /* Returns the index of log or diff command or -1 to exit. */
456 parse_options(int argc, char *argv[])
460 for (i = 1; i < argc; i++) {
463 if (!strcmp(opt, "log") ||
464 !strcmp(opt, "diff") ||
465 !strcmp(opt, "show")) {
466 opt_request = opt[0] == 'l'
467 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
471 if (opt[0] && opt[0] != '-')
474 if (!strcmp(opt, "-l")) {
475 opt_request = REQ_VIEW_LOG;
479 if (!strcmp(opt, "-d")) {
480 opt_request = REQ_VIEW_DIFF;
484 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
485 opt_line_number = TRUE;
489 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
490 opt_tab_size = MIN(opt_tab_size, TABSIZE);
494 if (check_option(opt, 'v', "version", OPT_NONE)) {
495 printf("tig version %s\n", VERSION);
499 if (check_option(opt, 'h', "help", OPT_NONE)) {
504 if (!strcmp(opt, "--")) {
509 die("unknown option '%s'\n\n%s", opt, usage);
512 if (!isatty(STDIN_FILENO)) {
513 opt_request = REQ_VIEW_PAGER;
516 } else if (i < argc) {
519 if (opt_request == REQ_VIEW_MAIN)
520 /* XXX: This is vulnerable to the user overriding
521 * options required for the main view parser. */
522 string_copy(opt_cmd, "git log --stat --pretty=raw");
524 string_copy(opt_cmd, "git");
525 buf_size = strlen(opt_cmd);
527 while (buf_size < sizeof(opt_cmd) && i < argc) {
528 opt_cmd[buf_size++] = ' ';
529 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
532 if (buf_size >= sizeof(opt_cmd))
533 die("command too long");
535 opt_cmd[buf_size] = 0;
539 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
547 * Line-oriented content detection.
551 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
552 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
553 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
554 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
555 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
556 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
557 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
558 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
559 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
560 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
561 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
562 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
563 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
565 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
566 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
568 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
570 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
572 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
573 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
574 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
575 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
576 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
577 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
579 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
580 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
581 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
582 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
583 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
584 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
585 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
586 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
587 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
588 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
589 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
590 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
591 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL)
594 #define LINE(type, line, fg, bg, attr) \
601 const char *name; /* Option name. */
602 int namelen; /* Size of option name. */
603 const char *line; /* The start of line to match. */
604 int linelen; /* Size of string to match. */
605 int fg, bg, attr; /* Color and text attributes for the lines. */
608 static struct line_info line_info[] = {
609 #define LINE(type, line, fg, bg, attr) \
610 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
615 static enum line_type
616 get_line_type(char *line)
618 int linelen = strlen(line);
621 for (type = 0; type < ARRAY_SIZE(line_info); type++)
622 /* Case insensitive search matches Signed-off-by lines better. */
623 if (linelen >= line_info[type].linelen &&
624 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
631 get_line_attr(enum line_type type)
633 assert(type < ARRAY_SIZE(line_info));
634 return COLOR_PAIR(type) | line_info[type].attr;
637 static struct line_info *
638 get_line_info(char *name, int namelen)
642 for (type = 0; type < ARRAY_SIZE(line_info); type++)
643 if (namelen == line_info[type].namelen &&
644 !string_enum_compare(line_info[type].name, name, namelen))
645 return &line_info[type];
653 int default_bg = COLOR_BLACK;
654 int default_fg = COLOR_WHITE;
659 if (use_default_colors() != ERR) {
664 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
665 struct line_info *info = &line_info[type];
666 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
667 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
669 init_pair(type, fg, bg);
677 unsigned int selected:1;
679 void *data; /* User data */
689 enum request request;
690 struct keybinding *next;
693 static struct keybinding default_keybindings[] = {
695 { 'm', REQ_VIEW_MAIN },
696 { 'd', REQ_VIEW_DIFF },
697 { 'l', REQ_VIEW_LOG },
698 { 't', REQ_VIEW_TREE },
699 { 'f', REQ_VIEW_BLOB },
700 { 'p', REQ_VIEW_PAGER },
701 { 'h', REQ_VIEW_HELP },
703 /* View manipulation */
704 { 'q', REQ_VIEW_CLOSE },
705 { KEY_TAB, REQ_VIEW_NEXT },
706 { KEY_RETURN, REQ_ENTER },
707 { KEY_UP, REQ_PREVIOUS },
708 { KEY_DOWN, REQ_NEXT },
710 /* Cursor navigation */
711 { 'k', REQ_MOVE_UP },
712 { 'j', REQ_MOVE_DOWN },
713 { KEY_HOME, REQ_MOVE_FIRST_LINE },
714 { KEY_END, REQ_MOVE_LAST_LINE },
715 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
716 { ' ', REQ_MOVE_PAGE_DOWN },
717 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
718 { 'b', REQ_MOVE_PAGE_UP },
719 { '-', REQ_MOVE_PAGE_UP },
722 { KEY_IC, REQ_SCROLL_LINE_UP },
723 { KEY_DC, REQ_SCROLL_LINE_DOWN },
724 { 'w', REQ_SCROLL_PAGE_UP },
725 { 's', REQ_SCROLL_PAGE_DOWN },
729 { '?', REQ_SEARCH_BACK },
730 { 'n', REQ_FIND_NEXT },
731 { 'N', REQ_FIND_PREV },
735 { 'z', REQ_STOP_LOADING },
736 { 'v', REQ_SHOW_VERSION },
737 { 'r', REQ_SCREEN_REDRAW },
738 { '.', REQ_TOGGLE_LINENO },
739 { 'g', REQ_TOGGLE_REV_GRAPH },
742 /* Using the ncurses SIGWINCH handler. */
743 { KEY_RESIZE, REQ_SCREEN_RESIZE },
746 #define KEYMAP_INFO \
757 #define KEYMAP_(name) KEYMAP_##name
762 static struct int_map keymap_table[] = {
763 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
768 #define set_keymap(map, name) \
769 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
771 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
774 add_keybinding(enum keymap keymap, enum request request, int key)
776 struct keybinding *keybinding;
778 keybinding = calloc(1, sizeof(*keybinding));
780 die("Failed to allocate keybinding");
782 keybinding->alias = key;
783 keybinding->request = request;
784 keybinding->next = keybindings[keymap];
785 keybindings[keymap] = keybinding;
788 /* Looks for a key binding first in the given map, then in the generic map, and
789 * lastly in the default keybindings. */
791 get_keybinding(enum keymap keymap, int key)
793 struct keybinding *kbd;
796 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
797 if (kbd->alias == key)
800 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
801 if (kbd->alias == key)
804 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
805 if (default_keybindings[i].alias == key)
806 return default_keybindings[i].request;
808 return (enum request) key;
817 static struct key key_table[] = {
818 { "Enter", KEY_RETURN },
820 { "Backspace", KEY_BACKSPACE },
822 { "Escape", KEY_ESC },
823 { "Left", KEY_LEFT },
824 { "Right", KEY_RIGHT },
826 { "Down", KEY_DOWN },
827 { "Insert", KEY_IC },
828 { "Delete", KEY_DC },
830 { "Home", KEY_HOME },
832 { "PageUp", KEY_PPAGE },
833 { "PageDown", KEY_NPAGE },
843 { "F10", KEY_F(10) },
844 { "F11", KEY_F(11) },
845 { "F12", KEY_F(12) },
849 get_key_value(const char *name)
853 for (i = 0; i < ARRAY_SIZE(key_table); i++)
854 if (!strcasecmp(key_table[i].name, name))
855 return key_table[i].value;
857 if (strlen(name) == 1 && isprint(*name))
864 get_key(enum request request)
866 static char buf[BUFSIZ];
867 static char key_char[] = "'X'";
874 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
875 struct keybinding *keybinding = &default_keybindings[i];
879 if (keybinding->request != request)
882 for (key = 0; key < ARRAY_SIZE(key_table); key++)
883 if (key_table[key].value == keybinding->alias)
884 seq = key_table[key].name;
887 keybinding->alias < 127 &&
888 isprint(keybinding->alias)) {
889 key_char[1] = (char) keybinding->alias;
896 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
897 return "Too many keybindings!";
906 * User config file handling.
909 static struct int_map color_map[] = {
910 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
922 #define set_color(color, name) \
923 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
925 static struct int_map attr_map[] = {
926 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
936 #define set_attribute(attr, name) \
937 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
939 static int config_lineno;
940 static bool config_errors;
941 static char *config_msg;
943 /* Wants: object fgcolor bgcolor [attr] */
945 option_color_command(int argc, char *argv[])
947 struct line_info *info;
949 if (argc != 3 && argc != 4) {
950 config_msg = "Wrong number of arguments given to color command";
954 info = get_line_info(argv[0], strlen(argv[0]));
956 config_msg = "Unknown color name";
960 if (set_color(&info->fg, argv[1]) == ERR ||
961 set_color(&info->bg, argv[2]) == ERR) {
962 config_msg = "Unknown color";
966 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
967 config_msg = "Unknown attribute";
974 /* Wants: name = value */
976 option_set_command(int argc, char *argv[])
979 config_msg = "Wrong number of arguments given to set command";
983 if (strcmp(argv[1], "=")) {
984 config_msg = "No value assigned";
988 if (!strcmp(argv[0], "show-rev-graph")) {
989 opt_rev_graph = (!strcmp(argv[2], "1") ||
990 !strcmp(argv[2], "true") ||
991 !strcmp(argv[2], "yes"));
995 if (!strcmp(argv[0], "line-number-interval")) {
996 opt_num_interval = atoi(argv[2]);
1000 if (!strcmp(argv[0], "tab-size")) {
1001 opt_tab_size = atoi(argv[2]);
1005 if (!strcmp(argv[0], "commit-encoding")) {
1006 char *arg = argv[2];
1007 int delimiter = *arg;
1010 switch (delimiter) {
1013 for (arg++, i = 0; arg[i]; i++)
1014 if (arg[i] == delimiter) {
1019 string_copy(opt_encoding, arg);
1024 config_msg = "Unknown variable name";
1028 /* Wants: mode request key */
1030 option_bind_command(int argc, char *argv[])
1032 enum request request;
1037 config_msg = "Wrong number of arguments given to bind command";
1041 if (set_keymap(&keymap, argv[0]) == ERR) {
1042 config_msg = "Unknown key map";
1046 key = get_key_value(argv[1]);
1048 config_msg = "Unknown key";
1052 request = get_request(argv[2]);
1053 if (request == REQ_UNKNOWN) {
1054 config_msg = "Unknown request name";
1058 add_keybinding(keymap, request, key);
1064 set_option(char *opt, char *value)
1071 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1072 argv[argc++] = value;
1079 while (isspace(*value))
1083 if (!strcmp(opt, "color"))
1084 return option_color_command(argc, argv);
1086 if (!strcmp(opt, "set"))
1087 return option_set_command(argc, argv);
1089 if (!strcmp(opt, "bind"))
1090 return option_bind_command(argc, argv);
1092 config_msg = "Unknown option command";
1097 read_option(char *opt, int optlen, char *value, int valuelen)
1102 config_msg = "Internal error";
1104 /* Check for comment markers, since read_properties() will
1105 * only ensure opt and value are split at first " \t". */
1106 optlen = strcspn(opt, "#");
1110 if (opt[optlen] != 0) {
1111 config_msg = "No option value";
1115 /* Look for comment endings in the value. */
1116 int len = strcspn(value, "#");
1118 if (len < valuelen) {
1120 value[valuelen] = 0;
1123 status = set_option(opt, value);
1126 if (status == ERR) {
1127 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1128 config_lineno, optlen, opt, config_msg);
1129 config_errors = TRUE;
1132 /* Always keep going if errors are encountered. */
1139 char *home = getenv("HOME");
1140 char buf[SIZEOF_STR];
1144 config_errors = FALSE;
1146 if (!home || !string_format(buf, "%s/.tigrc", home))
1149 /* It's ok that the file doesn't exist. */
1150 file = fopen(buf, "r");
1154 if (read_properties(file, " \t", read_option) == ERR ||
1155 config_errors == TRUE)
1156 fprintf(stderr, "Errors while loading %s.\n", buf);
1169 /* The display array of active views and the index of the current view. */
1170 static struct view *display[2];
1171 static unsigned int current_view;
1173 /* Reading from the prompt? */
1174 static bool input_mode = FALSE;
1176 #define foreach_displayed_view(view, i) \
1177 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1179 #define displayed_views() (display[1] != NULL ? 2 : 1)
1181 /* Current head and commit ID */
1182 static char ref_blob[SIZEOF_REF] = "";
1183 static char ref_commit[SIZEOF_REF] = "HEAD";
1184 static char ref_head[SIZEOF_REF] = "HEAD";
1187 const char *name; /* View name */
1188 const char *cmd_fmt; /* Default command line format */
1189 const char *cmd_env; /* Command line set via environment */
1190 const char *id; /* Points to either of ref_{head,commit,blob} */
1192 struct view_ops *ops; /* View operations */
1194 enum keymap keymap; /* What keymap does this view have */
1196 char cmd[SIZEOF_STR]; /* Command buffer */
1197 char ref[SIZEOF_REF]; /* Hovered commit reference */
1198 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1200 int height, width; /* The width and height of the main window */
1201 WINDOW *win; /* The main window */
1202 WINDOW *title; /* The title window living below the main window */
1205 unsigned long offset; /* Offset of the window top */
1206 unsigned long lineno; /* Current line number */
1209 char grep[SIZEOF_STR]; /* Search string */
1210 regex_t *regex; /* Pre-compiled regex */
1212 /* If non-NULL, points to the view that opened this view. If this view
1213 * is closed tig will switch back to the parent view. */
1214 struct view *parent;
1217 unsigned long lines; /* Total number of lines */
1218 struct line *line; /* Line index */
1219 unsigned long line_size;/* Total number of allocated lines */
1220 unsigned int digits; /* Number of digits in the lines member. */
1228 /* What type of content being displayed. Used in the title bar. */
1230 /* Draw one line; @lineno must be < view->height. */
1231 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1232 /* Read one line; updates view->line. */
1233 bool (*read)(struct view *view, char *data);
1234 /* Depending on view, change display based on current line. */
1235 bool (*enter)(struct view *view, struct line *line);
1236 /* Search for regex in a line. */
1237 bool (*grep)(struct view *view, struct line *line);
1239 void (*select)(struct view *view, struct line *line);
1242 static struct view_ops pager_ops;
1243 static struct view_ops main_ops;
1244 static struct view_ops tree_ops;
1245 static struct view_ops blob_ops;
1247 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1248 { name, cmd, #env, ref, ops, map}
1250 #define VIEW_(id, name, ops, ref) \
1251 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1254 static struct view views[] = {
1255 VIEW_(MAIN, "main", &main_ops, ref_head),
1256 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1257 VIEW_(LOG, "log", &pager_ops, ref_head),
1258 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1259 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1260 VIEW_(HELP, "help", &pager_ops, "static"),
1261 VIEW_(PAGER, "pager", &pager_ops, "static"),
1264 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1266 #define foreach_view(view, i) \
1267 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1269 #define view_is_displayed(view) \
1270 (view == display[0] || view == display[1])
1273 draw_view_line(struct view *view, unsigned int lineno)
1276 bool selected = (view->offset + lineno == view->lineno);
1279 assert(view_is_displayed(view));
1281 if (view->offset + lineno >= view->lines)
1284 line = &view->line[view->offset + lineno];
1287 line->selected = TRUE;
1288 view->ops->select(view, line);
1289 } else if (line->selected) {
1290 line->selected = FALSE;
1291 wmove(view->win, lineno, 0);
1292 wclrtoeol(view->win);
1295 scrollok(view->win, FALSE);
1296 draw_ok = view->ops->draw(view, line, lineno, selected);
1297 scrollok(view->win, TRUE);
1303 redraw_view_from(struct view *view, int lineno)
1305 assert(0 <= lineno && lineno < view->height);
1307 for (; lineno < view->height; lineno++) {
1308 if (!draw_view_line(view, lineno))
1312 redrawwin(view->win);
1314 wnoutrefresh(view->win);
1316 wrefresh(view->win);
1320 redraw_view(struct view *view)
1323 redraw_view_from(view, 0);
1328 update_view_title(struct view *view)
1330 char buf[SIZEOF_STR];
1331 char state[SIZEOF_STR];
1332 size_t bufpos = 0, statelen = 0;
1334 assert(view_is_displayed(view));
1336 if (view->lines || view->pipe) {
1337 unsigned int view_lines = view->offset + view->height;
1338 unsigned int lines = view->lines
1339 ? MIN(view_lines, view->lines) * 100 / view->lines
1342 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1349 time_t secs = time(NULL) - view->start_time;
1351 /* Three git seconds are a long time ... */
1353 string_format_from(state, &statelen, " %lds", secs);
1357 string_format_from(buf, &bufpos, "[%s]", view->name);
1358 if (*view->ref && bufpos < view->width) {
1359 size_t refsize = strlen(view->ref);
1360 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1362 if (minsize < view->width)
1363 refsize = view->width - minsize + 7;
1364 string_format_from(buf, &bufpos, " %.*s", refsize, view->ref);
1367 if (statelen && bufpos < view->width) {
1368 string_format_from(buf, &bufpos, " %s", state);
1371 if (view == display[current_view])
1372 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1374 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1376 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1377 wclrtoeol(view->title);
1378 wmove(view->title, 0, view->width - 1);
1381 wnoutrefresh(view->title);
1383 wrefresh(view->title);
1387 resize_display(void)
1390 struct view *base = display[0];
1391 struct view *view = display[1] ? display[1] : display[0];
1393 /* Setup window dimensions */
1395 getmaxyx(stdscr, base->height, base->width);
1397 /* Make room for the status window. */
1401 /* Horizontal split. */
1402 view->width = base->width;
1403 view->height = SCALE_SPLIT_VIEW(base->height);
1404 base->height -= view->height;
1406 /* Make room for the title bar. */
1410 /* Make room for the title bar. */
1415 foreach_displayed_view (view, i) {
1417 view->win = newwin(view->height, 0, offset, 0);
1419 die("Failed to create %s view", view->name);
1421 scrollok(view->win, TRUE);
1423 view->title = newwin(1, 0, offset + view->height, 0);
1425 die("Failed to create title window");
1428 wresize(view->win, view->height, view->width);
1429 mvwin(view->win, offset, 0);
1430 mvwin(view->title, offset + view->height, 0);
1433 offset += view->height + 1;
1438 redraw_display(void)
1443 foreach_displayed_view (view, i) {
1445 update_view_title(view);
1450 update_display_cursor(struct view *view)
1452 /* Move the cursor to the right-most column of the cursor line.
1454 * XXX: This could turn out to be a bit expensive, but it ensures that
1455 * the cursor does not jump around. */
1457 wmove(view->win, view->lineno - view->offset, view->width - 1);
1458 wrefresh(view->win);
1466 /* Scrolling backend */
1468 do_scroll_view(struct view *view, int lines)
1470 bool redraw_current_line = FALSE;
1472 /* The rendering expects the new offset. */
1473 view->offset += lines;
1475 assert(0 <= view->offset && view->offset < view->lines);
1478 /* Move current line into the view. */
1479 if (view->lineno < view->offset) {
1480 view->lineno = view->offset;
1481 redraw_current_line = TRUE;
1482 } else if (view->lineno >= view->offset + view->height) {
1483 view->lineno = view->offset + view->height - 1;
1484 redraw_current_line = TRUE;
1487 assert(view->offset <= view->lineno && view->lineno < view->lines);
1489 /* Redraw the whole screen if scrolling is pointless. */
1490 if (view->height < ABS(lines)) {
1494 int line = lines > 0 ? view->height - lines : 0;
1495 int end = line + ABS(lines);
1497 wscrl(view->win, lines);
1499 for (; line < end; line++) {
1500 if (!draw_view_line(view, line))
1504 if (redraw_current_line)
1505 draw_view_line(view, view->lineno - view->offset);
1508 redrawwin(view->win);
1509 wrefresh(view->win);
1513 /* Scroll frontend */
1515 scroll_view(struct view *view, enum request request)
1519 assert(view_is_displayed(view));
1522 case REQ_SCROLL_PAGE_DOWN:
1523 lines = view->height;
1524 case REQ_SCROLL_LINE_DOWN:
1525 if (view->offset + lines > view->lines)
1526 lines = view->lines - view->offset;
1528 if (lines == 0 || view->offset + view->height >= view->lines) {
1529 report("Cannot scroll beyond the last line");
1534 case REQ_SCROLL_PAGE_UP:
1535 lines = view->height;
1536 case REQ_SCROLL_LINE_UP:
1537 if (lines > view->offset)
1538 lines = view->offset;
1541 report("Cannot scroll beyond the first line");
1549 die("request %d not handled in switch", request);
1552 do_scroll_view(view, lines);
1557 move_view(struct view *view, enum request request)
1559 int scroll_steps = 0;
1563 case REQ_MOVE_FIRST_LINE:
1564 steps = -view->lineno;
1567 case REQ_MOVE_LAST_LINE:
1568 steps = view->lines - view->lineno - 1;
1571 case REQ_MOVE_PAGE_UP:
1572 steps = view->height > view->lineno
1573 ? -view->lineno : -view->height;
1576 case REQ_MOVE_PAGE_DOWN:
1577 steps = view->lineno + view->height >= view->lines
1578 ? view->lines - view->lineno - 1 : view->height;
1590 die("request %d not handled in switch", request);
1593 if (steps <= 0 && view->lineno == 0) {
1594 report("Cannot move beyond the first line");
1597 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1598 report("Cannot move beyond the last line");
1602 /* Move the current line */
1603 view->lineno += steps;
1604 assert(0 <= view->lineno && view->lineno < view->lines);
1606 /* Check whether the view needs to be scrolled */
1607 if (view->lineno < view->offset ||
1608 view->lineno >= view->offset + view->height) {
1609 scroll_steps = steps;
1610 if (steps < 0 && -steps > view->offset) {
1611 scroll_steps = -view->offset;
1613 } else if (steps > 0) {
1614 if (view->lineno == view->lines - 1 &&
1615 view->lines > view->height) {
1616 scroll_steps = view->lines - view->offset - 1;
1617 if (scroll_steps >= view->height)
1618 scroll_steps -= view->height - 1;
1623 if (!view_is_displayed(view)) {
1624 view->offset += steps;
1625 view->ops->select(view, &view->line[view->lineno]);
1629 /* Repaint the old "current" line if we be scrolling */
1630 if (ABS(steps) < view->height)
1631 draw_view_line(view, view->lineno - steps - view->offset);
1634 do_scroll_view(view, scroll_steps);
1638 /* Draw the current line */
1639 draw_view_line(view, view->lineno - view->offset);
1641 redrawwin(view->win);
1642 wrefresh(view->win);
1651 static void search_view(struct view *view, enum request request);
1654 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1656 assert(view_is_displayed(view));
1658 if (!view->ops->grep(view, line))
1661 if (lineno - view->offset >= view->height) {
1662 view->offset = lineno;
1663 view->lineno = lineno;
1667 unsigned long old_lineno = view->lineno - view->offset;
1669 view->lineno = lineno;
1670 draw_view_line(view, old_lineno);
1672 draw_view_line(view, view->lineno - view->offset);
1673 redrawwin(view->win);
1674 wrefresh(view->win);
1677 report("Line %ld matches '%s'", lineno + 1, view->grep);
1682 find_next(struct view *view, enum request request)
1684 unsigned long lineno = view->lineno;
1689 report("No previous search");
1691 search_view(view, request);
1701 case REQ_SEARCH_BACK:
1710 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1711 lineno += direction;
1713 /* Note, lineno is unsigned long so will wrap around in which case it
1714 * will become bigger than view->lines. */
1715 for (; lineno < view->lines; lineno += direction) {
1716 struct line *line = &view->line[lineno];
1718 if (find_next_line(view, lineno, line))
1722 report("No match found for '%s'", view->grep);
1726 search_view(struct view *view, enum request request)
1731 regfree(view->regex);
1734 view->regex = calloc(1, sizeof(*view->regex));
1739 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1740 if (regex_err != 0) {
1741 char buf[SIZEOF_STR] = "unknown error";
1743 regerror(regex_err, view->regex, buf, sizeof(buf));
1744 report("Search failed: %s", buf);
1748 string_copy(view->grep, opt_search);
1750 find_next(view, request);
1754 * Incremental updating
1758 end_update(struct view *view)
1762 set_nonblocking_input(FALSE);
1763 if (view->pipe == stdin)
1771 begin_update(struct view *view)
1773 const char *id = view->id;
1779 string_copy(view->cmd, opt_cmd);
1781 /* When running random commands, the view ref could have become
1782 * invalid so clear it. */
1785 } else if (view == VIEW(REQ_VIEW_TREE)) {
1786 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1788 if (strcmp(view->vid, view->id))
1791 if (!string_format(view->cmd, format, id, opt_path))
1795 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1797 if (!string_format(view->cmd, format, id, id, id, id, id))
1801 /* Special case for the pager view. */
1803 view->pipe = opt_pipe;
1806 view->pipe = popen(view->cmd, "r");
1812 set_nonblocking_input(TRUE);
1817 string_copy(view->vid, id);
1822 for (i = 0; i < view->lines; i++)
1823 if (view->line[i].data)
1824 free(view->line[i].data);
1830 view->start_time = time(NULL);
1835 static struct line *
1836 realloc_lines(struct view *view, size_t line_size)
1838 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1844 view->line_size = line_size;
1849 update_view(struct view *view)
1851 char in_buffer[BUFSIZ];
1852 char out_buffer[BUFSIZ * 2];
1854 /* The number of lines to read. If too low it will cause too much
1855 * redrawing (and possible flickering), if too high responsiveness
1857 unsigned long lines = view->height;
1858 int redraw_from = -1;
1863 /* Only redraw if lines are visible. */
1864 if (view->offset + view->height >= view->lines)
1865 redraw_from = view->lines - view->offset;
1867 /* FIXME: This is probably not perfect for backgrounded views. */
1868 if (!realloc_lines(view, view->lines + lines))
1871 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1872 size_t linelen = strlen(line);
1875 line[linelen - 1] = 0;
1877 if (opt_iconv != ICONV_NONE) {
1879 size_t inlen = linelen;
1881 char *outbuf = out_buffer;
1882 size_t outlen = sizeof(out_buffer);
1886 ret = iconv(opt_iconv, (const char **) &inbuf, &inlen, &outbuf, &outlen);
1887 if (ret != (size_t) -1) {
1889 linelen = strlen(out_buffer);
1893 if (!view->ops->read(view, line))
1903 lines = view->lines;
1904 for (digits = 0; lines; digits++)
1907 /* Keep the displayed view in sync with line number scaling. */
1908 if (digits != view->digits) {
1909 view->digits = digits;
1914 if (!view_is_displayed(view))
1917 if (view == VIEW(REQ_VIEW_TREE)) {
1918 /* Clear the view and redraw everything since the tree sorting
1919 * might have rearranged things. */
1922 } else if (redraw_from >= 0) {
1923 /* If this is an incremental update, redraw the previous line
1924 * since for commits some members could have changed when
1925 * loading the main view. */
1926 if (redraw_from > 0)
1929 /* Incrementally draw avoids flickering. */
1930 redraw_view_from(view, redraw_from);
1933 /* Update the title _after_ the redraw so that if the redraw picks up a
1934 * commit reference in view->ref it'll be available here. */
1935 update_view_title(view);
1938 if (ferror(view->pipe)) {
1939 report("Failed to read: %s", strerror(errno));
1942 } else if (feof(view->pipe)) {
1950 report("Allocation failure");
1953 view->ops->read(view, NULL);
1963 static void open_help_view(struct view *view)
1966 int lines = ARRAY_SIZE(req_info) + 2;
1969 if (view->lines > 0)
1972 for (i = 0; i < ARRAY_SIZE(req_info); i++)
1973 if (!req_info[i].request)
1976 view->line = calloc(lines, sizeof(*view->line));
1978 report("Allocation failure");
1982 view->ops->read(view, "Quick reference for tig keybindings:");
1984 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
1987 if (!req_info[i].request) {
1988 view->ops->read(view, "");
1989 view->ops->read(view, req_info[i].help);
1993 key = get_key(req_info[i].request);
1994 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
1997 view->ops->read(view, buf);
2002 OPEN_DEFAULT = 0, /* Use default view switching. */
2003 OPEN_SPLIT = 1, /* Split current view. */
2004 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2005 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2009 open_view(struct view *prev, enum request request, enum open_flags flags)
2011 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2012 bool split = !!(flags & OPEN_SPLIT);
2013 bool reload = !!(flags & OPEN_RELOAD);
2014 struct view *view = VIEW(request);
2015 int nviews = displayed_views();
2016 struct view *base_view = display[0];
2018 if (view == prev && nviews == 1 && !reload) {
2019 report("Already in %s view", view->name);
2023 if (view == VIEW(REQ_VIEW_HELP)) {
2024 open_help_view(view);
2026 } else if ((reload || strcmp(view->vid, view->id)) &&
2027 !begin_update(view)) {
2028 report("Failed to load %s view", view->name);
2037 /* Maximize the current view. */
2038 memset(display, 0, sizeof(display));
2040 display[current_view] = view;
2043 /* Resize the view when switching between split- and full-screen,
2044 * or when switching between two different full-screen views. */
2045 if (nviews != displayed_views() ||
2046 (nviews == 1 && base_view != display[0]))
2049 if (split && prev->lineno - prev->offset >= prev->height) {
2050 /* Take the title line into account. */
2051 int lines = prev->lineno - prev->offset - prev->height + 1;
2053 /* Scroll the view that was split if the current line is
2054 * outside the new limited view. */
2055 do_scroll_view(prev, lines);
2058 if (prev && view != prev) {
2059 if (split && !backgrounded) {
2060 /* "Blur" the previous view. */
2061 update_view_title(prev);
2064 view->parent = prev;
2067 if (view->pipe && view->lines == 0) {
2068 /* Clear the old view and let the incremental updating refill
2077 /* If the view is backgrounded the above calls to report()
2078 * won't redraw the view title. */
2080 update_view_title(view);
2085 * User request switch noodle
2089 view_driver(struct view *view, enum request request)
2096 case REQ_MOVE_PAGE_UP:
2097 case REQ_MOVE_PAGE_DOWN:
2098 case REQ_MOVE_FIRST_LINE:
2099 case REQ_MOVE_LAST_LINE:
2100 move_view(view, request);
2103 case REQ_SCROLL_LINE_DOWN:
2104 case REQ_SCROLL_LINE_UP:
2105 case REQ_SCROLL_PAGE_DOWN:
2106 case REQ_SCROLL_PAGE_UP:
2107 scroll_view(view, request);
2112 report("No file chosen, press 't' to open tree view");
2121 case REQ_VIEW_PAGER:
2122 open_view(view, request, OPEN_DEFAULT);
2127 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2129 if ((view == VIEW(REQ_VIEW_DIFF) &&
2130 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2131 (view == VIEW(REQ_VIEW_BLOB) &&
2132 view->parent == VIEW(REQ_VIEW_TREE))) {
2133 view = view->parent;
2134 move_view(view, request);
2135 if (view_is_displayed(view))
2136 update_view_title(view);
2138 move_view(view, request);
2145 report("Nothing to enter");
2148 return view->ops->enter(view, &view->line[view->lineno]);
2152 int nviews = displayed_views();
2153 int next_view = (current_view + 1) % nviews;
2155 if (next_view == current_view) {
2156 report("Only one view is displayed");
2160 current_view = next_view;
2161 /* Blur out the title of the previous view. */
2162 update_view_title(view);
2166 case REQ_TOGGLE_LINENO:
2167 opt_line_number = !opt_line_number;
2171 case REQ_TOGGLE_REV_GRAPH:
2172 opt_rev_graph = !opt_rev_graph;
2177 /* Always reload^Wrerun commands from the prompt. */
2178 open_view(view, opt_request, OPEN_RELOAD);
2182 case REQ_SEARCH_BACK:
2183 search_view(view, request);
2188 find_next(view, request);
2191 case REQ_STOP_LOADING:
2192 for (i = 0; i < ARRAY_SIZE(views); i++) {
2195 report("Stopped loading the %s view", view->name),
2200 case REQ_SHOW_VERSION:
2201 report("%s (built %s)", VERSION, __DATE__);
2204 case REQ_SCREEN_RESIZE:
2207 case REQ_SCREEN_REDRAW:
2215 case REQ_VIEW_CLOSE:
2216 /* XXX: Mark closed views by letting view->parent point to the
2217 * view itself. Parents to closed view should never be
2220 view->parent->parent != view->parent) {
2221 memset(display, 0, sizeof(display));
2223 display[current_view] = view->parent;
2224 view->parent = view;
2234 /* An unknown key will show most commonly used commands. */
2235 report("Unknown key, press 'h' for help");
2248 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2250 char *text = line->data;
2251 enum line_type type = line->type;
2252 int textlen = strlen(text);
2255 wmove(view->win, lineno, 0);
2259 wchgat(view->win, -1, 0, type, NULL);
2262 attr = get_line_attr(type);
2263 wattrset(view->win, attr);
2265 if (opt_line_number || opt_tab_size < TABSIZE) {
2266 static char spaces[] = " ";
2267 int col_offset = 0, col = 0;
2269 if (opt_line_number) {
2270 unsigned long real_lineno = view->offset + lineno + 1;
2272 if (real_lineno == 1 ||
2273 (real_lineno % opt_num_interval) == 0) {
2274 wprintw(view->win, "%.*d", view->digits, real_lineno);
2277 waddnstr(view->win, spaces,
2278 MIN(view->digits, STRING_SIZE(spaces)));
2280 waddstr(view->win, ": ");
2281 col_offset = view->digits + 2;
2284 while (text && col_offset + col < view->width) {
2285 int cols_max = view->width - col_offset - col;
2289 if (*text == '\t') {
2291 assert(sizeof(spaces) > TABSIZE);
2293 cols = opt_tab_size - (col % opt_tab_size);
2296 text = strchr(text, '\t');
2297 cols = line ? text - pos : strlen(pos);
2300 waddnstr(view->win, pos, MIN(cols, cols_max));
2305 int col = 0, pos = 0;
2307 for (; pos < textlen && col < view->width; pos++, col++)
2308 if (text[pos] == '\t')
2309 col += TABSIZE - (col % TABSIZE) - 1;
2311 waddnstr(view->win, text, pos);
2318 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2320 char refbuf[SIZEOF_STR];
2324 if (!string_format(refbuf, "git describe %s", commit_id))
2327 pipe = popen(refbuf, "r");
2331 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2332 ref = chomp_string(ref);
2338 /* This is the only fatal call, since it can "corrupt" the buffer. */
2339 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2346 add_pager_refs(struct view *view, struct line *line)
2348 char buf[SIZEOF_STR];
2349 char *commit_id = line->data + STRING_SIZE("commit ");
2351 size_t bufpos = 0, refpos = 0;
2352 const char *sep = "Refs: ";
2353 bool is_tag = FALSE;
2355 assert(line->type == LINE_COMMIT);
2357 refs = get_refs(commit_id);
2359 if (view == VIEW(REQ_VIEW_DIFF))
2360 goto try_add_describe_ref;
2365 struct ref *ref = refs[refpos];
2366 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
2368 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2373 } while (refs[refpos++]->next);
2375 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2376 try_add_describe_ref:
2377 /* Add <tag>-g<commit_id> "fake" reference. */
2378 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2385 if (!realloc_lines(view, view->line_size + 1))
2388 line = &view->line[view->lines];
2389 line->data = strdup(buf);
2393 line->type = LINE_PP_REFS;
2398 pager_read(struct view *view, char *data)
2400 struct line *line = &view->line[view->lines];
2405 line->data = strdup(data);
2409 line->type = get_line_type(line->data);
2412 if (line->type == LINE_COMMIT &&
2413 (view == VIEW(REQ_VIEW_DIFF) ||
2414 view == VIEW(REQ_VIEW_LOG)))
2415 add_pager_refs(view, line);
2421 pager_enter(struct view *view, struct line *line)
2425 if (line->type == LINE_COMMIT &&
2426 (view == VIEW(REQ_VIEW_LOG) ||
2427 view == VIEW(REQ_VIEW_PAGER))) {
2428 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2432 /* Always scroll the view even if it was split. That way
2433 * you can use Enter to scroll through the log view and
2434 * split open each commit diff. */
2435 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2437 /* FIXME: A minor workaround. Scrolling the view will call report("")
2438 * but if we are scrolling a non-current view this won't properly
2439 * update the view title. */
2441 update_view_title(view);
2447 pager_grep(struct view *view, struct line *line)
2450 char *text = line->data;
2455 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2462 pager_select(struct view *view, struct line *line)
2464 if (line->type == LINE_COMMIT) {
2465 char *text = line->data;
2467 string_copy(view->ref, text + STRING_SIZE("commit "));
2468 string_copy(ref_commit, view->ref);
2472 static struct view_ops pager_ops = {
2486 /* Parse output from git-ls-tree(1):
2488 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2489 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2490 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2491 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2494 #define SIZEOF_TREE_ATTR \
2495 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2497 #define TREE_UP_FORMAT "040000 tree %s\t.."
2500 tree_compare_entry(enum line_type type1, char *name1,
2501 enum line_type type2, char *name2)
2503 if (type1 != type2) {
2504 if (type1 == LINE_TREE_DIR)
2509 return strcmp(name1, name2);
2513 tree_read(struct view *view, char *text)
2515 size_t textlen = text ? strlen(text) : 0;
2516 char buf[SIZEOF_STR];
2518 enum line_type type;
2519 bool first_read = view->lines == 0;
2521 if (textlen <= SIZEOF_TREE_ATTR)
2524 type = text[STRING_SIZE("100644 ")] == 't'
2525 ? LINE_TREE_DIR : LINE_TREE_FILE;
2528 /* Add path info line */
2529 if (string_format(buf, "Directory path /%s", opt_path) &&
2530 realloc_lines(view, view->line_size + 1) &&
2531 pager_read(view, buf))
2532 view->line[view->lines - 1].type = LINE_DEFAULT;
2536 /* Insert "link" to parent directory. */
2538 string_format(buf, TREE_UP_FORMAT, view->ref) &&
2539 realloc_lines(view, view->line_size + 1) &&
2540 pager_read(view, buf))
2541 view->line[view->lines - 1].type = LINE_TREE_DIR;
2546 /* Strip the path part ... */
2548 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2549 size_t striplen = strlen(opt_path);
2550 char *path = text + SIZEOF_TREE_ATTR;
2552 if (pathlen > striplen)
2553 memmove(path, path + striplen,
2554 pathlen - striplen + 1);
2557 /* Skip "Directory ..." and ".." line. */
2558 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2559 struct line *line = &view->line[pos];
2560 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2561 char *path2 = text + SIZEOF_TREE_ATTR;
2562 int cmp = tree_compare_entry(line->type, path1, type, path2);
2567 text = strdup(text);
2571 if (view->lines > pos)
2572 memmove(&view->line[pos + 1], &view->line[pos],
2573 (view->lines - pos) * sizeof(*line));
2575 line = &view->line[pos];
2582 if (!pager_read(view, text))
2585 /* Move the current line to the first tree entry. */
2589 view->line[view->lines - 1].type = type;
2594 tree_enter(struct view *view, struct line *line)
2596 enum open_flags flags;
2597 enum request request;
2599 switch (line->type) {
2601 /* Depending on whether it is a subdir or parent (updir?) link
2602 * mangle the path buffer. */
2603 if (line == &view->line[1] && *opt_path) {
2604 size_t path_len = strlen(opt_path);
2605 char *dirsep = opt_path + path_len - 1;
2607 while (dirsep > opt_path && dirsep[-1] != '/')
2613 size_t pathlen = strlen(opt_path);
2614 size_t origlen = pathlen;
2615 char *data = line->data;
2616 char *basename = data + SIZEOF_TREE_ATTR;
2618 if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
2619 opt_path[origlen] = 0;
2624 /* Trees and subtrees share the same ID, so they are not not
2625 * unique like blobs. */
2626 flags = OPEN_RELOAD;
2627 request = REQ_VIEW_TREE;
2630 case LINE_TREE_FILE:
2631 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2632 request = REQ_VIEW_BLOB;
2639 open_view(view, request, flags);
2645 tree_select(struct view *view, struct line *line)
2647 char *text = line->data;
2649 text += STRING_SIZE("100644 blob ");
2651 if (line->type == LINE_TREE_FILE) {
2652 string_ncopy(ref_blob, text, 40);
2653 /* Also update the blob view's ref, since all there must always
2655 string_copy(VIEW(REQ_VIEW_BLOB)->ref, ref_blob);
2657 } else if (line->type != LINE_TREE_DIR) {
2661 string_ncopy(view->ref, text, 40);
2664 static struct view_ops tree_ops = {
2674 blob_read(struct view *view, char *line)
2676 bool state = pager_read(view, line);
2679 view->line[view->lines - 1].type = LINE_DEFAULT;
2684 static struct view_ops blob_ops = {
2699 char id[SIZEOF_REV]; /* SHA1 ID. */
2700 char title[128]; /* First line of the commit message. */
2701 char author[75]; /* Author of the commit. */
2702 struct tm time; /* Date from the author ident. */
2703 struct ref **refs; /* Repository references. */
2704 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
2705 size_t graph_size; /* The width of the graph array. */
2708 /* Size of rev graph with no "padding" columns */
2709 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
2712 struct rev_graph *prev, *next, *parents;
2713 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
2715 struct commit *commit;
2719 /* Parents of the commit being visualized. */
2720 static struct rev_graph graph_parents[4];
2722 /* The current stack of revisions on the graph. */
2723 static struct rev_graph graph_stacks[4] = {
2724 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
2725 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
2726 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
2727 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
2731 graph_parent_is_merge(struct rev_graph *graph)
2733 return graph->parents->size > 1;
2737 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
2739 struct commit *commit = graph->commit;
2741 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
2742 commit->graph[commit->graph_size++] = symbol;
2746 done_rev_graph(struct rev_graph *graph)
2748 if (graph_parent_is_merge(graph) &&
2749 graph->pos < graph->size - 1 &&
2750 graph->next->size == graph->size + graph->parents->size - 1) {
2751 size_t i = graph->pos + graph->parents->size - 1;
2753 graph->commit->graph_size = i * 2;
2754 while (i < graph->next->size - 1) {
2755 append_to_rev_graph(graph, ' ');
2756 append_to_rev_graph(graph, '\\');
2761 graph->size = graph->pos = 0;
2762 graph->commit = NULL;
2763 memset(graph->parents, 0, sizeof(*graph->parents));
2767 push_rev_graph(struct rev_graph *graph, char *parent)
2771 /* "Collapse" duplicate parents lines.
2773 * FIXME: This needs to also update update the drawn graph but
2774 * for now it just serves as a method for pruning graph lines. */
2775 for (i = 0; i < graph->size; i++)
2776 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
2779 if (graph->size < SIZEOF_REVITEMS) {
2780 string_ncopy(graph->rev[graph->size++], parent, SIZEOF_REV);
2785 get_rev_graph_symbol(struct rev_graph *graph)
2789 if (graph->parents->size == 0)
2790 symbol = REVGRAPH_INIT;
2791 else if (graph_parent_is_merge(graph))
2792 symbol = REVGRAPH_MERGE;
2793 else if (graph->pos >= graph->size)
2794 symbol = REVGRAPH_BRANCH;
2796 symbol = REVGRAPH_COMMIT;
2802 draw_rev_graph(struct rev_graph *graph)
2805 chtype separator, line;
2807 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
2808 static struct rev_filler fillers[] = {
2809 { ' ', REVGRAPH_LINE },
2814 chtype symbol = get_rev_graph_symbol(graph);
2815 struct rev_filler *filler;
2818 filler = &fillers[DEFAULT];
2820 for (i = 0; i < graph->pos; i++) {
2821 append_to_rev_graph(graph, filler->line);
2822 if (graph_parent_is_merge(graph->prev) &&
2823 graph->prev->pos == i)
2824 filler = &fillers[RSHARP];
2826 append_to_rev_graph(graph, filler->separator);
2829 /* Place the symbol for this revision. */
2830 append_to_rev_graph(graph, symbol);
2832 if (graph->prev->size > graph->size)
2833 filler = &fillers[RDIAG];
2835 filler = &fillers[DEFAULT];
2839 for (; i < graph->size; i++) {
2840 append_to_rev_graph(graph, filler->separator);
2841 append_to_rev_graph(graph, filler->line);
2842 if (graph_parent_is_merge(graph->prev) &&
2843 i < graph->prev->pos + graph->parents->size)
2844 filler = &fillers[RSHARP];
2845 if (graph->prev->size > graph->size)
2846 filler = &fillers[LDIAG];
2849 if (graph->prev->size > graph->size) {
2850 append_to_rev_graph(graph, filler->separator);
2851 if (filler->line != ' ')
2852 append_to_rev_graph(graph, filler->line);
2856 /* Prepare the next rev graph */
2858 prepare_rev_graph(struct rev_graph *graph)
2862 /* First, traverse all lines of revisions up to the active one. */
2863 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
2864 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
2867 push_rev_graph(graph->next, graph->rev[graph->pos]);
2870 /* Interleave the new revision parent(s). */
2871 for (i = 0; i < graph->parents->size; i++)
2872 push_rev_graph(graph->next, graph->parents->rev[i]);
2874 /* Lastly, put any remaining revisions. */
2875 for (i = graph->pos + 1; i < graph->size; i++)
2876 push_rev_graph(graph->next, graph->rev[i]);
2880 update_rev_graph(struct rev_graph *graph)
2882 /* If this is the finalizing update ... */
2884 prepare_rev_graph(graph);
2886 /* Graph visualization needs a one rev look-ahead,
2887 * so the first update doesn't visualize anything. */
2888 if (!graph->prev->commit)
2891 draw_rev_graph(graph->prev);
2892 done_rev_graph(graph->prev->prev);
2901 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2903 char buf[DATE_COLS + 1];
2904 struct commit *commit = line->data;
2905 enum line_type type;
2911 if (!*commit->author)
2914 wmove(view->win, lineno, col);
2918 wattrset(view->win, get_line_attr(type));
2919 wchgat(view->win, -1, 0, type, NULL);
2922 type = LINE_MAIN_COMMIT;
2923 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
2926 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
2927 waddnstr(view->win, buf, timelen);
2928 waddstr(view->win, " ");
2931 wmove(view->win, lineno, col);
2932 if (type != LINE_CURSOR)
2933 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
2936 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
2938 authorlen = strlen(commit->author);
2939 if (authorlen > AUTHOR_COLS - 2) {
2940 authorlen = AUTHOR_COLS - 2;
2946 waddnstr(view->win, commit->author, authorlen);
2947 if (type != LINE_CURSOR)
2948 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
2949 waddch(view->win, '~');
2951 waddstr(view->win, commit->author);
2955 if (type != LINE_CURSOR)
2956 wattrset(view->win, A_NORMAL);
2958 if (opt_rev_graph && commit->graph_size) {
2961 wmove(view->win, lineno, col);
2962 /* Using waddch() instead of waddnstr() ensures that
2963 * they'll be rendered correctly for the cursor line. */
2964 for (i = 0; i < commit->graph_size; i++)
2965 waddch(view->win, commit->graph[i]);
2967 waddch(view->win, ' ');
2968 col += commit->graph_size + 1;
2971 wmove(view->win, lineno, col);
2977 if (type == LINE_CURSOR)
2979 else if (commit->refs[i]->tag)
2980 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
2982 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
2983 waddstr(view->win, "[");
2984 waddstr(view->win, commit->refs[i]->name);
2985 waddstr(view->win, "]");
2986 if (type != LINE_CURSOR)
2987 wattrset(view->win, A_NORMAL);
2988 waddstr(view->win, " ");
2989 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
2990 } while (commit->refs[i++]->next);
2993 if (type != LINE_CURSOR)
2994 wattrset(view->win, get_line_attr(type));
2997 int titlelen = strlen(commit->title);
2999 if (col + titlelen > view->width)
3000 titlelen = view->width - col;
3002 waddnstr(view->win, commit->title, titlelen);
3008 /* Reads git log --pretty=raw output and parses it into the commit struct. */
3010 main_read(struct view *view, char *line)
3012 static struct rev_graph *graph = graph_stacks;
3013 enum line_type type;
3014 struct commit *commit = view->lines
3015 ? view->line[view->lines - 1].data : NULL;
3018 update_rev_graph(graph);
3022 type = get_line_type(line);
3026 commit = calloc(1, sizeof(struct commit));
3030 line += STRING_SIZE("commit ");
3032 view->line[view->lines++].data = commit;
3033 string_copy(commit->id, line);
3034 commit->refs = get_refs(commit->id);
3035 graph->commit = commit;
3040 line += STRING_SIZE("parent ");
3041 push_rev_graph(graph->parents, line);
3047 char *ident = line + STRING_SIZE("author ");
3048 char *end = strchr(ident, '<');
3053 update_rev_graph(graph);
3054 graph = graph->next;
3057 char *email = end + 1;
3059 for (; end > ident && isspace(end[-1]); end--) ;
3061 if (end == ident && *email) {
3063 end = strchr(ident, '>');
3064 for (; end > ident && isspace(end[-1]); end--) ;
3069 /* End is NULL or ident meaning there's no author. */
3073 string_copy(commit->author, ident);
3075 /* Parse epoch and timezone */
3077 char *secs = strchr(end + 1, '>');
3081 if (!secs || secs[1] != ' ')
3085 time = (time_t) atol(secs);
3086 zone = strchr(secs, ' ');
3087 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
3091 tz = ('0' - zone[1]) * 60 * 60 * 10;
3092 tz += ('0' - zone[2]) * 60 * 60;
3093 tz += ('0' - zone[3]) * 60;
3094 tz += ('0' - zone[4]) * 60;
3101 gmtime_r(&time, &commit->time);
3109 /* Fill in the commit title if it has not already been set. */
3110 if (commit->title[0])
3113 /* Require titles to start with a non-space character at the
3114 * offset used by git log. */
3115 if (strncmp(line, " ", 4))
3118 /* Well, if the title starts with a whitespace character,
3119 * try to be forgiving. Otherwise we end up with no title. */
3120 while (isspace(*line))
3124 /* FIXME: More graceful handling of titles; append "..." to
3125 * shortened titles, etc. */
3127 string_copy(commit->title, line);
3134 main_enter(struct view *view, struct line *line)
3136 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3138 open_view(view, REQ_VIEW_DIFF, flags);
3143 main_grep(struct view *view, struct line *line)
3145 struct commit *commit = line->data;
3146 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
3147 char buf[DATE_COLS + 1];
3150 for (state = S_TITLE; state < S_END; state++) {
3154 case S_TITLE: text = commit->title; break;
3155 case S_AUTHOR: text = commit->author; break;
3157 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
3166 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3174 main_select(struct view *view, struct line *line)
3176 struct commit *commit = line->data;
3178 string_copy(view->ref, commit->id);
3179 string_copy(ref_commit, view->ref);
3182 static struct view_ops main_ops = {
3193 * Unicode / UTF-8 handling
3195 * NOTE: Much of the following code for dealing with unicode is derived from
3196 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
3197 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
3200 /* I've (over)annotated a lot of code snippets because I am not entirely
3201 * confident that the approach taken by this small UTF-8 interface is correct.
3205 unicode_width(unsigned long c)
3208 (c <= 0x115f /* Hangul Jamo */
3211 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
3213 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
3214 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
3215 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
3216 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
3217 || (c >= 0xffe0 && c <= 0xffe6)
3218 || (c >= 0x20000 && c <= 0x2fffd)
3219 || (c >= 0x30000 && c <= 0x3fffd)))
3225 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
3226 * Illegal bytes are set one. */
3227 static const unsigned char utf8_bytes[256] = {
3228 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,
3229 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,
3230 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,
3231 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,
3232 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,
3233 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,
3234 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,
3235 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,
3238 /* Decode UTF-8 multi-byte representation into a unicode character. */
3239 static inline unsigned long
3240 utf8_to_unicode(const char *string, size_t length)
3242 unsigned long unicode;
3246 unicode = string[0];
3249 unicode = (string[0] & 0x1f) << 6;
3250 unicode += (string[1] & 0x3f);
3253 unicode = (string[0] & 0x0f) << 12;
3254 unicode += ((string[1] & 0x3f) << 6);
3255 unicode += (string[2] & 0x3f);
3258 unicode = (string[0] & 0x0f) << 18;
3259 unicode += ((string[1] & 0x3f) << 12);
3260 unicode += ((string[2] & 0x3f) << 6);
3261 unicode += (string[3] & 0x3f);
3264 unicode = (string[0] & 0x0f) << 24;
3265 unicode += ((string[1] & 0x3f) << 18);
3266 unicode += ((string[2] & 0x3f) << 12);
3267 unicode += ((string[3] & 0x3f) << 6);
3268 unicode += (string[4] & 0x3f);
3271 unicode = (string[0] & 0x01) << 30;
3272 unicode += ((string[1] & 0x3f) << 24);
3273 unicode += ((string[2] & 0x3f) << 18);
3274 unicode += ((string[3] & 0x3f) << 12);
3275 unicode += ((string[4] & 0x3f) << 6);
3276 unicode += (string[5] & 0x3f);
3279 die("Invalid unicode length");
3282 /* Invalid characters could return the special 0xfffd value but NUL
3283 * should be just as good. */
3284 return unicode > 0xffff ? 0 : unicode;
3287 /* Calculates how much of string can be shown within the given maximum width
3288 * and sets trimmed parameter to non-zero value if all of string could not be
3291 * Additionally, adds to coloffset how many many columns to move to align with
3292 * the expected position. Takes into account how multi-byte and double-width
3293 * characters will effect the cursor position.
3295 * Returns the number of bytes to output from string to satisfy max_width. */
3297 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3299 const char *start = string;
3300 const char *end = strchr(string, '\0');
3306 while (string < end) {
3307 int c = *(unsigned char *) string;
3308 unsigned char bytes = utf8_bytes[c];
3310 unsigned long unicode;
3312 if (string + bytes > end)
3315 /* Change representation to figure out whether
3316 * it is a single- or double-width character. */
3318 unicode = utf8_to_unicode(string, bytes);
3319 /* FIXME: Graceful handling of invalid unicode character. */
3323 ucwidth = unicode_width(unicode);
3325 if (width > max_width) {
3330 /* The column offset collects the differences between the
3331 * number of bytes encoding a character and the number of
3332 * columns will be used for rendering said character.
3334 * So if some character A is encoded in 2 bytes, but will be
3335 * represented on the screen using only 1 byte this will and up
3336 * adding 1 to the multi-byte column offset.
3338 * Assumes that no double-width character can be encoding in
3339 * less than two bytes. */
3340 if (bytes > ucwidth)
3341 mbwidth += bytes - ucwidth;
3346 *coloffset += mbwidth;
3348 return string - start;
3356 /* Whether or not the curses interface has been initialized. */
3357 static bool cursed = FALSE;
3359 /* The status window is used for polling keystrokes. */
3360 static WINDOW *status_win;
3362 static bool status_empty = TRUE;
3364 /* Update status and title window. */
3366 report(const char *msg, ...)
3368 struct view *view = display[current_view];
3373 if (!status_empty || *msg) {
3376 va_start(args, msg);
3378 wmove(status_win, 0, 0);
3380 vwprintw(status_win, msg, args);
3381 status_empty = FALSE;
3383 status_empty = TRUE;
3385 wclrtoeol(status_win);
3386 wrefresh(status_win);
3391 update_view_title(view);
3392 update_display_cursor(view);
3395 /* Controls when nodelay should be in effect when polling user input. */
3397 set_nonblocking_input(bool loading)
3399 static unsigned int loading_views;
3401 if ((loading == FALSE && loading_views-- == 1) ||
3402 (loading == TRUE && loading_views++ == 0))
3403 nodelay(status_win, loading);
3411 /* Initialize the curses library */
3412 if (isatty(STDIN_FILENO)) {
3413 cursed = !!initscr();
3415 /* Leave stdin and stdout alone when acting as a pager. */
3416 FILE *io = fopen("/dev/tty", "r+");
3419 die("Failed to open /dev/tty");
3420 cursed = !!newterm(NULL, io, io);
3424 die("Failed to initialize curses");
3426 nonl(); /* Tell curses not to do NL->CR/NL on output */
3427 cbreak(); /* Take input chars one at a time, no wait for \n */
3428 noecho(); /* Don't echo input */
3429 leaveok(stdscr, TRUE);
3434 getmaxyx(stdscr, y, x);
3435 status_win = newwin(1, 0, y - 1, 0);
3437 die("Failed to create status window");
3439 /* Enable keyboard mapping */
3440 keypad(status_win, TRUE);
3441 wbkgdset(status_win, get_line_attr(LINE_STATUS));
3445 read_prompt(const char *prompt)
3447 enum { READING, STOP, CANCEL } status = READING;
3448 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
3451 while (status == READING) {
3457 foreach_view (view, i)
3462 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
3463 wclrtoeol(status_win);
3465 /* Refresh, accept single keystroke of input */
3466 key = wgetch(status_win);
3471 status = pos ? STOP : CANCEL;
3489 if (pos >= sizeof(buf)) {
3490 report("Input string too long");
3495 buf[pos++] = (char) key;
3499 /* Clear the status window */
3500 status_empty = FALSE;
3503 if (status == CANCEL)
3512 * Repository references
3515 static struct ref *refs;
3516 static size_t refs_size;
3518 /* Id <-> ref store */
3519 static struct ref ***id_refs;
3520 static size_t id_refs_size;
3522 static struct ref **
3525 struct ref ***tmp_id_refs;
3526 struct ref **ref_list = NULL;
3527 size_t ref_list_size = 0;
3530 for (i = 0; i < id_refs_size; i++)
3531 if (!strcmp(id, id_refs[i][0]->id))
3534 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
3538 id_refs = tmp_id_refs;
3540 for (i = 0; i < refs_size; i++) {
3543 if (strcmp(id, refs[i].id))
3546 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
3554 if (ref_list_size > 0)
3555 ref_list[ref_list_size - 1]->next = 1;
3556 ref_list[ref_list_size] = &refs[i];
3558 /* XXX: The properties of the commit chains ensures that we can
3559 * safely modify the shared ref. The repo references will
3560 * always be similar for the same id. */
3561 ref_list[ref_list_size]->next = 0;
3566 id_refs[id_refs_size++] = ref_list;
3572 read_ref(char *id, int idlen, char *name, int namelen)
3577 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
3578 /* Commits referenced by tags has "^{}" appended. */
3579 if (name[namelen - 1] != '}')
3582 while (namelen > 0 && name[namelen] != '^')
3586 namelen -= STRING_SIZE("refs/tags/");
3587 name += STRING_SIZE("refs/tags/");
3589 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
3590 namelen -= STRING_SIZE("refs/heads/");
3591 name += STRING_SIZE("refs/heads/");
3593 } else if (!strcmp(name, "HEAD")) {
3597 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
3601 ref = &refs[refs_size++];
3602 ref->name = malloc(namelen + 1);
3606 strncpy(ref->name, name, namelen);
3607 ref->name[namelen] = 0;
3609 string_copy(ref->id, id);
3617 const char *cmd_env = getenv("TIG_LS_REMOTE");
3618 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
3620 return read_properties(popen(cmd, "r"), "\t", read_ref);
3624 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
3626 if (!strcmp(name, "i18n.commitencoding"))
3627 string_copy(opt_encoding, value);
3633 load_repo_config(void)
3635 return read_properties(popen("git repo-config --list", "r"),
3636 "=", read_repo_config_option);
3640 read_properties(FILE *pipe, const char *separators,
3641 int (*read_property)(char *, int, char *, int))
3643 char buffer[BUFSIZ];
3650 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
3655 name = chomp_string(name);
3656 namelen = strcspn(name, separators);
3658 if (name[namelen]) {
3660 value = chomp_string(name + namelen + 1);
3661 valuelen = strlen(value);
3668 state = read_property(name, namelen, value, valuelen);
3671 if (state != ERR && ferror(pipe))
3684 static void __NORETURN
3687 /* XXX: Restore tty modes and let the OS cleanup the rest! */
3693 static void __NORETURN
3694 die(const char *err, ...)
3700 va_start(args, err);
3701 fputs("tig: ", stderr);
3702 vfprintf(stderr, err, args);
3703 fputs("\n", stderr);
3710 main(int argc, char *argv[])
3713 enum request request;
3716 signal(SIGINT, quit);
3718 if (setlocale(LC_ALL, "")) {
3719 string_copy(opt_codeset, nl_langinfo(CODESET));
3722 if (load_options() == ERR)
3723 die("Failed to load user config.");
3725 /* Load the repo config file so options can be overwritten from
3726 * the command line. */
3727 if (load_repo_config() == ERR)
3728 die("Failed to load repo config.");
3730 if (!parse_options(argc, argv))
3733 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
3734 opt_iconv = iconv_open(opt_codeset, opt_encoding);
3735 if (opt_iconv == ICONV_NONE)
3736 die("Failed to initialize character set conversion");
3739 if (load_refs() == ERR)
3740 die("Failed to load refs.");
3742 /* Require a git repository unless when running in pager mode. */
3743 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
3744 die("Not a git repository");
3746 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
3747 view->cmd_env = getenv(view->cmd_env);
3749 request = opt_request;
3753 while (view_driver(display[current_view], request)) {
3757 foreach_view (view, i)
3760 /* Refresh, accept single keystroke of input */
3761 key = wgetch(status_win);
3763 /* wgetch() with nodelay() enabled returns ERR when there's no
3770 request = get_keybinding(display[current_view]->keymap, key);
3772 /* Some low-level request handling. This keeps access to
3773 * status_win restricted. */
3777 char *cmd = read_prompt(":");
3779 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
3780 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
3781 opt_request = REQ_VIEW_DIFF;
3783 opt_request = REQ_VIEW_PAGER;
3792 case REQ_SEARCH_BACK:
3794 const char *prompt = request == REQ_SEARCH
3796 char *search = read_prompt(prompt);
3799 string_copy(opt_search, search);
3804 case REQ_SCREEN_RESIZE:
3808 getmaxyx(stdscr, height, width);
3810 /* Resize the status view and let the view driver take
3811 * care of resizing the displayed views. */
3812 wresize(status_win, 1, width);
3813 mvwin(status_win, height - 1, 0);
3814 wrefresh(status_win);