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,
303 struct request_info {
304 enum request request;
308 static struct request_info req_info[] = {
309 #define REQ_GROUP(help) { 0, (help) },
310 #define REQ_(req, help) { REQ_##req, (help) }
320 static const char usage[] =
321 VERSION " (" __DATE__ ")\n"
323 "Usage: tig [options]\n"
324 " or: tig [options] [--] [git log options]\n"
325 " or: tig [options] log [git log options]\n"
326 " or: tig [options] diff [git diff options]\n"
327 " or: tig [options] show [git show options]\n"
328 " or: tig [options] < [git command output]\n"
331 " -l Start up in log view\n"
332 " -d Start up in diff view\n"
333 " -n[I], --line-number[=I] Show line numbers with given interval\n"
334 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
335 " -- Mark end of tig options\n"
336 " -v, --version Show version and exit\n"
337 " -h, --help Show help message and exit\n";
339 /* Option and state variables. */
340 static bool opt_line_number = FALSE;
341 static bool opt_rev_graph = TRUE;
342 static int opt_num_interval = NUMBER_INTERVAL;
343 static int opt_tab_size = TABSIZE;
344 static enum request opt_request = REQ_VIEW_MAIN;
345 static char opt_cmd[SIZEOF_CMD] = "";
346 static char opt_encoding[20] = "";
347 static bool opt_utf8 = TRUE;
348 static FILE *opt_pipe = NULL;
356 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
366 int namelen = strlen(name);
370 if (strncmp(opt, name, namelen))
373 if (opt[namelen] == '=')
374 value = opt + namelen + 1;
377 if (!short_name || opt[1] != short_name)
382 va_start(args, type);
383 if (type == OPT_INT) {
384 number = va_arg(args, int *);
386 *number = atoi(value);
393 /* Returns the index of log or diff command or -1 to exit. */
395 parse_options(int argc, char *argv[])
399 for (i = 1; i < argc; i++) {
402 if (!strcmp(opt, "-l")) {
403 opt_request = REQ_VIEW_LOG;
407 if (!strcmp(opt, "-d")) {
408 opt_request = REQ_VIEW_DIFF;
412 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
413 opt_line_number = TRUE;
417 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
418 opt_tab_size = MIN(opt_tab_size, TABSIZE);
422 if (check_option(opt, 'v', "version", OPT_NONE)) {
423 printf("tig version %s\n", VERSION);
427 if (check_option(opt, 'h', "help", OPT_NONE)) {
432 if (!strcmp(opt, "--")) {
437 if (!strcmp(opt, "log") ||
438 !strcmp(opt, "diff") ||
439 !strcmp(opt, "show")) {
440 opt_request = opt[0] == 'l'
441 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
445 if (opt[0] && opt[0] != '-')
448 die("unknown option '%s'\n\n%s", opt, usage);
451 if (!isatty(STDIN_FILENO)) {
452 opt_request = REQ_VIEW_PAGER;
455 } else if (i < argc) {
458 if (opt_request == REQ_VIEW_MAIN)
459 /* XXX: This is vulnerable to the user overriding
460 * options required for the main view parser. */
461 string_copy(opt_cmd, "git log --stat --pretty=raw");
463 string_copy(opt_cmd, "git");
464 buf_size = strlen(opt_cmd);
466 while (buf_size < sizeof(opt_cmd) && i < argc) {
467 opt_cmd[buf_size++] = ' ';
468 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
471 if (buf_size >= sizeof(opt_cmd))
472 die("command too long");
474 opt_cmd[buf_size] = 0;
478 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
486 * Line-oriented content detection.
490 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
491 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
492 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
493 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
494 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
495 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
496 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
497 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
498 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
499 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
500 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
501 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
502 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
503 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
504 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
505 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
506 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
507 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
508 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
509 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
510 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
511 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
512 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
513 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
514 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
515 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
516 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
517 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
518 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
519 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
520 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
521 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
522 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
523 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
524 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
525 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
526 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
527 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
530 #define LINE(type, line, fg, bg, attr) \
537 const char *name; /* Option name. */
538 int namelen; /* Size of option name. */
539 const char *line; /* The start of line to match. */
540 int linelen; /* Size of string to match. */
541 int fg, bg, attr; /* Color and text attributes for the lines. */
544 static struct line_info line_info[] = {
545 #define LINE(type, line, fg, bg, attr) \
546 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
551 static enum line_type
552 get_line_type(char *line)
554 int linelen = strlen(line);
557 for (type = 0; type < ARRAY_SIZE(line_info); type++)
558 /* Case insensitive search matches Signed-off-by lines better. */
559 if (linelen >= line_info[type].linelen &&
560 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
567 get_line_attr(enum line_type type)
569 assert(type < ARRAY_SIZE(line_info));
570 return COLOR_PAIR(type) | line_info[type].attr;
573 static struct line_info *
574 get_line_info(char *name, int namelen)
578 for (type = 0; type < ARRAY_SIZE(line_info); type++)
579 if (namelen == line_info[type].namelen &&
580 !string_enum_compare(line_info[type].name, name, namelen))
581 return &line_info[type];
589 int default_bg = COLOR_BLACK;
590 int default_fg = COLOR_WHITE;
595 if (use_default_colors() != ERR) {
600 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
601 struct line_info *info = &line_info[type];
602 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
603 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
605 init_pair(type, fg, bg);
611 void *data; /* User data */
624 static struct keymap keymap[] = {
626 { 'm', REQ_VIEW_MAIN },
627 { 'd', REQ_VIEW_DIFF },
628 { 'l', REQ_VIEW_LOG },
629 { 'p', REQ_VIEW_PAGER },
630 { 'h', REQ_VIEW_HELP },
631 { '?', REQ_VIEW_HELP },
633 /* View manipulation */
634 { 'q', REQ_VIEW_CLOSE },
635 { KEY_TAB, REQ_VIEW_NEXT },
636 { KEY_RETURN, REQ_ENTER },
637 { KEY_UP, REQ_PREVIOUS },
638 { KEY_DOWN, REQ_NEXT },
640 /* Cursor navigation */
641 { 'k', REQ_MOVE_UP },
642 { 'j', REQ_MOVE_DOWN },
643 { KEY_HOME, REQ_MOVE_FIRST_LINE },
644 { KEY_END, REQ_MOVE_LAST_LINE },
645 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
646 { ' ', REQ_MOVE_PAGE_DOWN },
647 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
648 { 'b', REQ_MOVE_PAGE_UP },
649 { '-', REQ_MOVE_PAGE_UP },
652 { KEY_IC, REQ_SCROLL_LINE_UP },
653 { KEY_DC, REQ_SCROLL_LINE_DOWN },
654 { 'w', REQ_SCROLL_PAGE_UP },
655 { 's', REQ_SCROLL_PAGE_DOWN },
659 { 'z', REQ_STOP_LOADING },
660 { 'v', REQ_SHOW_VERSION },
661 { 'r', REQ_SCREEN_REDRAW },
662 { 'n', REQ_TOGGLE_LINENO },
663 { 'g', REQ_TOGGLE_REV_GRAPH},
666 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
667 { ERR, REQ_SCREEN_UPDATE },
669 /* Use the ncurses SIGWINCH handler. */
670 { KEY_RESIZE, REQ_SCREEN_RESIZE },
678 for (i = 0; i < ARRAY_SIZE(keymap); i++)
679 if (keymap[i].alias == key)
680 return keymap[i].request;
682 return (enum request) key;
690 static struct key key_table[] = {
691 { "Enter", KEY_RETURN },
693 { "Backspace", KEY_BACKSPACE },
695 { "Escape", KEY_ESC },
696 { "Left", KEY_LEFT },
697 { "Right", KEY_RIGHT },
699 { "Down", KEY_DOWN },
700 { "Insert", KEY_IC },
701 { "Delete", KEY_DC },
702 { "Home", KEY_HOME },
704 { "PageUp", KEY_PPAGE },
705 { "PageDown", KEY_NPAGE },
715 { "F10", KEY_F(10) },
716 { "F11", KEY_F(11) },
717 { "F12", KEY_F(12) },
721 get_key(enum request request)
723 static char buf[BUFSIZ];
724 static char key_char[] = "'X'";
731 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
735 if (keymap[i].request != request)
738 for (key = 0; key < ARRAY_SIZE(key_table); key++)
739 if (key_table[key].value == keymap[i].alias)
740 seq = key_table[key].name;
743 keymap[i].alias < 127 &&
744 isprint(keymap[i].alias)) {
745 key_char[1] = (char) keymap[i].alias;
752 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
753 return "Too many keybindings!";
762 * User config file handling.
765 static struct int_map color_map[] = {
766 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
778 #define set_color(color, name) \
779 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
781 static struct int_map attr_map[] = {
782 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
792 #define set_attribute(attr, name) \
793 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
795 static int config_lineno;
796 static bool config_errors;
797 static char *config_msg;
799 /* Wants: object fgcolor bgcolor [attr] */
801 option_color_command(int argc, char *argv[])
803 struct line_info *info;
805 if (argc != 3 && argc != 4) {
806 config_msg = "Wrong number of arguments given to color command";
810 info = get_line_info(argv[0], strlen(argv[0]));
812 config_msg = "Unknown color name";
816 if (set_color(&info->fg, argv[1]) == ERR) {
817 config_msg = "Unknown color";
821 if (set_color(&info->bg, argv[2]) == ERR) {
822 config_msg = "Unknown color";
826 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
827 config_msg = "Unknown attribute";
834 /* Wants: name = value */
836 option_set_command(int argc, char *argv[])
839 config_msg = "Wrong number of arguments given to set command";
843 if (strcmp(argv[1], "=")) {
844 config_msg = "No value assigned";
848 if (!strcmp(argv[0], "show-rev-graph")) {
849 opt_rev_graph = (!strcmp(argv[2], "1") ||
850 !strcmp(argv[2], "true") ||
851 !strcmp(argv[2], "yes"));
855 if (!strcmp(argv[0], "line-number-interval")) {
856 opt_num_interval = atoi(argv[2]);
860 if (!strcmp(argv[0], "tab-size")) {
861 opt_tab_size = atoi(argv[2]);
865 if (!strcmp(argv[0], "encoding")) {
866 string_copy(opt_encoding, argv[2]);
874 set_option(char *opt, char *value)
881 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
882 argv[argc++] = value;
889 while (isspace(*value))
893 if (!strcmp(opt, "color"))
894 return option_color_command(argc, argv);
896 if (!strcmp(opt, "set"))
897 return option_set_command(argc, argv);
903 read_option(char *opt, int optlen, char *value, int valuelen)
906 config_msg = "Internal error";
908 optlen = strcspn(opt, "#;");
910 /* The whole line is a commend or empty. */
913 } else if (opt[optlen] != 0) {
914 /* Part of the option name is a comment, so the value part
915 * should be ignored. */
917 opt[optlen] = value[valuelen] = 0;
919 /* Else look for comment endings in the value. */
920 valuelen = strcspn(value, "#;");
924 if (set_option(opt, value) == ERR) {
925 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
926 config_lineno, optlen, opt, config_msg);
927 config_errors = TRUE;
930 /* Always keep going if errors are encountered. */
937 char *home = getenv("HOME");
942 config_errors = FALSE;
944 if (!home || !string_format(buf, "%s/.tigrc", home))
947 /* It's ok that the file doesn't exist. */
948 file = fopen(buf, "r");
952 if (read_properties(file, " \t", read_option) == ERR ||
953 config_errors == TRUE)
954 fprintf(stderr, "Errors while loading %s.\n", buf);
967 /* The display array of active views and the index of the current view. */
968 static struct view *display[2];
969 static unsigned int current_view;
971 #define foreach_view(view, i) \
972 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
974 #define displayed_views() (display[1] != NULL ? 2 : 1)
976 /* Current head and commit ID */
977 static char ref_commit[SIZEOF_REF] = "HEAD";
978 static char ref_head[SIZEOF_REF] = "HEAD";
981 const char *name; /* View name */
982 const char *cmd_fmt; /* Default command line format */
983 const char *cmd_env; /* Command line set via environment */
984 const char *id; /* Points to either of ref_{head,commit} */
986 struct view_ops *ops; /* View operations */
988 char cmd[SIZEOF_CMD]; /* Command buffer */
989 char ref[SIZEOF_REF]; /* Hovered commit reference */
990 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
992 int height, width; /* The width and height of the main window */
993 WINDOW *win; /* The main window */
994 WINDOW *title; /* The title window living below the main window */
997 unsigned long offset; /* Offset of the window top */
998 unsigned long lineno; /* Current line number */
1000 /* If non-NULL, points to the view that opened this view. If this view
1001 * is closed tig will switch back to the parent view. */
1002 struct view *parent;
1005 unsigned long lines; /* Total number of lines */
1006 struct line *line; /* Line index */
1007 unsigned long line_size;/* Total number of allocated lines */
1008 unsigned int digits; /* Number of digits in the lines member. */
1016 /* What type of content being displayed. Used in the title bar. */
1018 /* Draw one line; @lineno must be < view->height. */
1019 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1020 /* Read one line; updates view->line. */
1021 bool (*read)(struct view *view, char *data);
1022 /* Depending on view, change display based on current line. */
1023 bool (*enter)(struct view *view, struct line *line);
1026 static struct view_ops pager_ops;
1027 static struct view_ops main_ops;
1029 #define VIEW_STR(name, cmd, env, ref, ops) \
1030 { name, cmd, #env, ref, ops }
1032 #define VIEW_(id, name, ops, ref) \
1033 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
1036 static struct view views[] = {
1037 VIEW_(MAIN, "main", &main_ops, ref_head),
1038 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1039 VIEW_(LOG, "log", &pager_ops, ref_head),
1040 VIEW_(HELP, "help", &pager_ops, "static"),
1041 VIEW_(PAGER, "pager", &pager_ops, "static"),
1044 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1048 draw_view_line(struct view *view, unsigned int lineno)
1050 if (view->offset + lineno >= view->lines)
1053 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
1057 redraw_view_from(struct view *view, int lineno)
1059 assert(0 <= lineno && lineno < view->height);
1061 for (; lineno < view->height; lineno++) {
1062 if (!draw_view_line(view, lineno))
1066 redrawwin(view->win);
1067 wrefresh(view->win);
1071 redraw_view(struct view *view)
1074 redraw_view_from(view, 0);
1079 update_view_title(struct view *view)
1081 if (view == display[current_view])
1082 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1084 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1086 werase(view->title);
1087 wmove(view->title, 0, 0);
1090 wprintw(view->title, "[%s] %s", view->name, view->ref);
1092 wprintw(view->title, "[%s]", view->name);
1094 if (view->lines || view->pipe) {
1095 unsigned int view_lines = view->offset + view->height;
1096 unsigned int lines = view->lines
1097 ? MIN(view_lines, view->lines) * 100 / view->lines
1100 wprintw(view->title, " - %s %d of %d (%d%%)",
1108 time_t secs = time(NULL) - view->start_time;
1110 /* Three git seconds are a long time ... */
1112 wprintw(view->title, " %lds", secs);
1115 wmove(view->title, 0, view->width - 1);
1116 wrefresh(view->title);
1120 resize_display(void)
1123 struct view *base = display[0];
1124 struct view *view = display[1] ? display[1] : display[0];
1126 /* Setup window dimensions */
1128 getmaxyx(stdscr, base->height, base->width);
1130 /* Make room for the status window. */
1134 /* Horizontal split. */
1135 view->width = base->width;
1136 view->height = SCALE_SPLIT_VIEW(base->height);
1137 base->height -= view->height;
1139 /* Make room for the title bar. */
1143 /* Make room for the title bar. */
1148 foreach_view (view, i) {
1150 view->win = newwin(view->height, 0, offset, 0);
1152 die("Failed to create %s view", view->name);
1154 scrollok(view->win, TRUE);
1156 view->title = newwin(1, 0, offset + view->height, 0);
1158 die("Failed to create title window");
1161 wresize(view->win, view->height, view->width);
1162 mvwin(view->win, offset, 0);
1163 mvwin(view->title, offset + view->height, 0);
1166 offset += view->height + 1;
1171 redraw_display(void)
1176 foreach_view (view, i) {
1178 update_view_title(view);
1183 update_display_cursor(void)
1185 struct view *view = display[current_view];
1187 /* Move the cursor to the right-most column of the cursor line.
1189 * XXX: This could turn out to be a bit expensive, but it ensures that
1190 * the cursor does not jump around. */
1192 wmove(view->win, view->lineno - view->offset, view->width - 1);
1193 wrefresh(view->win);
1201 /* Scrolling backend */
1203 do_scroll_view(struct view *view, int lines, bool redraw)
1205 /* The rendering expects the new offset. */
1206 view->offset += lines;
1208 assert(0 <= view->offset && view->offset < view->lines);
1211 /* Redraw the whole screen if scrolling is pointless. */
1212 if (view->height < ABS(lines)) {
1216 int line = lines > 0 ? view->height - lines : 0;
1217 int end = line + ABS(lines);
1219 wscrl(view->win, lines);
1221 for (; line < end; line++) {
1222 if (!draw_view_line(view, line))
1227 /* Move current line into the view. */
1228 if (view->lineno < view->offset) {
1229 view->lineno = view->offset;
1230 draw_view_line(view, 0);
1232 } else if (view->lineno >= view->offset + view->height) {
1233 if (view->lineno == view->offset + view->height) {
1234 /* Clear the hidden line so it doesn't show if the view
1235 * is scrolled up. */
1236 wmove(view->win, view->height, 0);
1237 wclrtoeol(view->win);
1239 view->lineno = view->offset + view->height - 1;
1240 draw_view_line(view, view->lineno - view->offset);
1243 assert(view->offset <= view->lineno && view->lineno < view->lines);
1248 redrawwin(view->win);
1249 wrefresh(view->win);
1253 /* Scroll frontend */
1255 scroll_view(struct view *view, enum request request)
1260 case REQ_SCROLL_PAGE_DOWN:
1261 lines = view->height;
1262 case REQ_SCROLL_LINE_DOWN:
1263 if (view->offset + lines > view->lines)
1264 lines = view->lines - view->offset;
1266 if (lines == 0 || view->offset + view->height >= view->lines) {
1267 report("Cannot scroll beyond the last line");
1272 case REQ_SCROLL_PAGE_UP:
1273 lines = view->height;
1274 case REQ_SCROLL_LINE_UP:
1275 if (lines > view->offset)
1276 lines = view->offset;
1279 report("Cannot scroll beyond the first line");
1287 die("request %d not handled in switch", request);
1290 do_scroll_view(view, lines, TRUE);
1295 move_view(struct view *view, enum request request, bool redraw)
1300 case REQ_MOVE_FIRST_LINE:
1301 steps = -view->lineno;
1304 case REQ_MOVE_LAST_LINE:
1305 steps = view->lines - view->lineno - 1;
1308 case REQ_MOVE_PAGE_UP:
1309 steps = view->height > view->lineno
1310 ? -view->lineno : -view->height;
1313 case REQ_MOVE_PAGE_DOWN:
1314 steps = view->lineno + view->height >= view->lines
1315 ? view->lines - view->lineno - 1 : view->height;
1327 die("request %d not handled in switch", request);
1330 if (steps <= 0 && view->lineno == 0) {
1331 report("Cannot move beyond the first line");
1334 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1335 report("Cannot move beyond the last line");
1339 /* Move the current line */
1340 view->lineno += steps;
1341 assert(0 <= view->lineno && view->lineno < view->lines);
1343 /* Repaint the old "current" line if we be scrolling */
1344 if (ABS(steps) < view->height) {
1345 int prev_lineno = view->lineno - steps - view->offset;
1347 wmove(view->win, prev_lineno, 0);
1348 wclrtoeol(view->win);
1349 draw_view_line(view, prev_lineno);
1352 /* Check whether the view needs to be scrolled */
1353 if (view->lineno < view->offset ||
1354 view->lineno >= view->offset + view->height) {
1355 if (steps < 0 && -steps > view->offset) {
1356 steps = -view->offset;
1358 } else if (steps > 0) {
1359 if (view->lineno == view->lines - 1 &&
1360 view->lines > view->height) {
1361 steps = view->lines - view->offset - 1;
1362 if (steps >= view->height)
1363 steps -= view->height - 1;
1367 do_scroll_view(view, steps, redraw);
1371 /* Draw the current line */
1372 draw_view_line(view, view->lineno - view->offset);
1377 redrawwin(view->win);
1378 wrefresh(view->win);
1384 * Incremental updating
1388 end_update(struct view *view)
1392 set_nonblocking_input(FALSE);
1393 if (view->pipe == stdin)
1401 begin_update(struct view *view)
1403 const char *id = view->id;
1409 string_copy(view->cmd, opt_cmd);
1411 /* When running random commands, the view ref could have become
1412 * invalid so clear it. */
1415 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1417 if (!string_format(view->cmd, format, id, id, id, id, id))
1421 /* Special case for the pager view. */
1423 view->pipe = opt_pipe;
1426 view->pipe = popen(view->cmd, "r");
1432 set_nonblocking_input(TRUE);
1437 string_copy(view->vid, id);
1442 for (i = 0; i < view->lines; i++)
1443 if (view->line[i].data)
1444 free(view->line[i].data);
1450 view->start_time = time(NULL);
1455 static struct line *
1456 realloc_lines(struct view *view, size_t line_size)
1458 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1464 view->line_size = line_size;
1469 update_view(struct view *view)
1471 char buffer[BUFSIZ];
1473 /* The number of lines to read. If too low it will cause too much
1474 * redrawing (and possible flickering), if too high responsiveness
1476 unsigned long lines = view->height;
1477 int redraw_from = -1;
1482 /* Only redraw if lines are visible. */
1483 if (view->offset + view->height >= view->lines)
1484 redraw_from = view->lines - view->offset;
1486 if (!realloc_lines(view, view->lines + lines))
1489 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1490 int linelen = strlen(line);
1493 line[linelen - 1] = 0;
1495 if (!view->ops->read(view, line))
1505 lines = view->lines;
1506 for (digits = 0; lines; digits++)
1509 /* Keep the displayed view in sync with line number scaling. */
1510 if (digits != view->digits) {
1511 view->digits = digits;
1516 if (redraw_from >= 0) {
1517 /* If this is an incremental update, redraw the previous line
1518 * since for commits some members could have changed when
1519 * loading the main view. */
1520 if (redraw_from > 0)
1523 /* Incrementally draw avoids flickering. */
1524 redraw_view_from(view, redraw_from);
1527 /* Update the title _after_ the redraw so that if the redraw picks up a
1528 * commit reference in view->ref it'll be available here. */
1529 update_view_title(view);
1531 if (ferror(view->pipe)) {
1532 report("Failed to read: %s", strerror(errno));
1535 } else if (feof(view->pipe)) {
1543 report("Allocation failure");
1555 static void open_help_view(struct view *view)
1558 int lines = ARRAY_SIZE(req_info) + 2;
1561 if (view->lines > 0)
1564 for (i = 0; i < ARRAY_SIZE(req_info); i++)
1565 if (!req_info[i].request)
1568 view->line = calloc(lines, sizeof(*view->line));
1570 report("Allocation failure");
1574 view->ops->read(view, "Quick reference for tig keybindings:");
1576 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
1579 if (!req_info[i].request) {
1580 view->ops->read(view, "");
1581 view->ops->read(view, req_info[i].help);
1585 key = get_key(req_info[i].request);
1586 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
1589 view->ops->read(view, buf);
1594 OPEN_DEFAULT = 0, /* Use default view switching. */
1595 OPEN_SPLIT = 1, /* Split current view. */
1596 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1597 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1601 open_view(struct view *prev, enum request request, enum open_flags flags)
1603 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1604 bool split = !!(flags & OPEN_SPLIT);
1605 bool reload = !!(flags & OPEN_RELOAD);
1606 struct view *view = VIEW(request);
1607 int nviews = displayed_views();
1608 struct view *base_view = display[0];
1610 if (view == prev && nviews == 1 && !reload) {
1611 report("Already in %s view", view->name);
1615 if (view == VIEW(REQ_VIEW_HELP)) {
1616 open_help_view(view);
1618 } else if ((reload || strcmp(view->vid, view->id)) &&
1619 !begin_update(view)) {
1620 report("Failed to load %s view", view->name);
1629 /* Maximize the current view. */
1630 memset(display, 0, sizeof(display));
1632 display[current_view] = view;
1635 /* Resize the view when switching between split- and full-screen,
1636 * or when switching between two different full-screen views. */
1637 if (nviews != displayed_views() ||
1638 (nviews == 1 && base_view != display[0]))
1641 if (split && prev->lineno - prev->offset >= prev->height) {
1642 /* Take the title line into account. */
1643 int lines = prev->lineno - prev->offset - prev->height + 1;
1645 /* Scroll the view that was split if the current line is
1646 * outside the new limited view. */
1647 do_scroll_view(prev, lines, TRUE);
1650 if (prev && view != prev) {
1651 if (split && !backgrounded) {
1652 /* "Blur" the previous view. */
1653 update_view_title(prev);
1656 view->parent = prev;
1659 if (view->pipe && view->lines == 0) {
1660 /* Clear the old view and let the incremental updating refill
1669 /* If the view is backgrounded the above calls to report()
1670 * won't redraw the view title. */
1672 update_view_title(view);
1677 * User request switch noodle
1681 view_driver(struct view *view, enum request request)
1688 case REQ_MOVE_PAGE_UP:
1689 case REQ_MOVE_PAGE_DOWN:
1690 case REQ_MOVE_FIRST_LINE:
1691 case REQ_MOVE_LAST_LINE:
1692 move_view(view, request, TRUE);
1695 case REQ_SCROLL_LINE_DOWN:
1696 case REQ_SCROLL_LINE_UP:
1697 case REQ_SCROLL_PAGE_DOWN:
1698 case REQ_SCROLL_PAGE_UP:
1699 scroll_view(view, request);
1706 case REQ_VIEW_PAGER:
1707 open_view(view, request, OPEN_DEFAULT);
1712 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1714 if (view == VIEW(REQ_VIEW_DIFF) &&
1715 view->parent == VIEW(REQ_VIEW_MAIN)) {
1716 bool redraw = display[1] == view;
1718 view = view->parent;
1719 move_view(view, request, redraw);
1721 update_view_title(view);
1723 move_view(view, request, TRUE);
1730 report("Nothing to enter");
1733 return view->ops->enter(view, &view->line[view->lineno]);
1737 int nviews = displayed_views();
1738 int next_view = (current_view + 1) % nviews;
1740 if (next_view == current_view) {
1741 report("Only one view is displayed");
1745 current_view = next_view;
1746 /* Blur out the title of the previous view. */
1747 update_view_title(view);
1751 case REQ_TOGGLE_LINENO:
1752 opt_line_number = !opt_line_number;
1756 case REQ_TOGGLE_REV_GRAPH:
1757 opt_rev_graph = !opt_rev_graph;
1762 /* Always reload^Wrerun commands from the prompt. */
1763 open_view(view, opt_request, OPEN_RELOAD);
1766 case REQ_STOP_LOADING:
1767 for (i = 0; i < ARRAY_SIZE(views); i++) {
1770 report("Stopped loading the %s view", view->name),
1775 case REQ_SHOW_VERSION:
1776 report("%s (built %s)", VERSION, __DATE__);
1779 case REQ_SCREEN_RESIZE:
1782 case REQ_SCREEN_REDRAW:
1786 case REQ_SCREEN_UPDATE:
1790 case REQ_VIEW_CLOSE:
1791 /* XXX: Mark closed views by letting view->parent point to the
1792 * view itself. Parents to closed view should never be
1795 view->parent->parent != view->parent) {
1796 memset(display, 0, sizeof(display));
1798 display[current_view] = view->parent;
1799 view->parent = view;
1809 /* An unknown key will show most commonly used commands. */
1810 report("Unknown key, press 'h' for help");
1823 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1825 char *text = line->data;
1826 enum line_type type = line->type;
1827 int textlen = strlen(text);
1830 wmove(view->win, lineno, 0);
1832 if (view->offset + lineno == view->lineno) {
1833 if (type == LINE_COMMIT) {
1834 string_copy(view->ref, text + 7);
1835 string_copy(ref_commit, view->ref);
1839 wchgat(view->win, -1, 0, type, NULL);
1842 attr = get_line_attr(type);
1843 wattrset(view->win, attr);
1845 if (opt_line_number || opt_tab_size < TABSIZE) {
1846 static char spaces[] = " ";
1847 int col_offset = 0, col = 0;
1849 if (opt_line_number) {
1850 unsigned long real_lineno = view->offset + lineno + 1;
1852 if (real_lineno == 1 ||
1853 (real_lineno % opt_num_interval) == 0) {
1854 wprintw(view->win, "%.*d", view->digits, real_lineno);
1857 waddnstr(view->win, spaces,
1858 MIN(view->digits, STRING_SIZE(spaces)));
1860 waddstr(view->win, ": ");
1861 col_offset = view->digits + 2;
1864 while (text && col_offset + col < view->width) {
1865 int cols_max = view->width - col_offset - col;
1869 if (*text == '\t') {
1871 assert(sizeof(spaces) > TABSIZE);
1873 cols = opt_tab_size - (col % opt_tab_size);
1876 text = strchr(text, '\t');
1877 cols = line ? text - pos : strlen(pos);
1880 waddnstr(view->win, pos, MIN(cols, cols_max));
1885 int col = 0, pos = 0;
1887 for (; pos < textlen && col < view->width; pos++, col++)
1888 if (text[pos] == '\t')
1889 col += TABSIZE - (col % TABSIZE) - 1;
1891 waddnstr(view->win, text, pos);
1898 add_pager_refs(struct view *view, struct line *line)
1901 char *data = line->data;
1903 int bufpos = 0, refpos = 0;
1904 const char *sep = "Refs: ";
1906 assert(line->type == LINE_COMMIT);
1908 refs = get_refs(data + STRING_SIZE("commit "));
1913 struct ref *ref = refs[refpos];
1914 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1916 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1919 } while (refs[refpos++]->next);
1921 if (!realloc_lines(view, view->line_size + 1))
1924 line = &view->line[view->lines];
1925 line->data = strdup(buf);
1929 line->type = LINE_PP_REFS;
1934 pager_read(struct view *view, char *data)
1936 struct line *line = &view->line[view->lines];
1938 line->data = strdup(data);
1942 line->type = get_line_type(line->data);
1945 if (line->type == LINE_COMMIT &&
1946 (view == VIEW(REQ_VIEW_DIFF) ||
1947 view == VIEW(REQ_VIEW_LOG)))
1948 add_pager_refs(view, line);
1954 pager_enter(struct view *view, struct line *line)
1958 if (line->type == LINE_COMMIT &&
1959 (view == VIEW(REQ_VIEW_LOG) ||
1960 view == VIEW(REQ_VIEW_PAGER))) {
1961 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1965 /* Always scroll the view even if it was split. That way
1966 * you can use Enter to scroll through the log view and
1967 * split open each commit diff. */
1968 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1970 /* FIXME: A minor workaround. Scrolling the view will call report("")
1971 * but if we are scrolling a non-current view this won't properly
1972 * update the view title. */
1974 update_view_title(view);
1979 static struct view_ops pager_ops = {
1992 char id[41]; /* SHA1 ID. */
1993 char title[75]; /* First line of the commit message. */
1994 char author[75]; /* Author of the commit. */
1995 struct tm time; /* Date from the author ident. */
1996 struct ref **refs; /* Repository references. */
1997 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
1998 size_t graph_size; /* The width of the graph array. */
2002 main_draw(struct view *view, struct line *line, unsigned int lineno)
2004 char buf[DATE_COLS + 1];
2005 struct commit *commit = line->data;
2006 enum line_type type;
2012 if (!*commit->author)
2015 wmove(view->win, lineno, col);
2017 if (view->offset + lineno == view->lineno) {
2018 string_copy(view->ref, commit->id);
2019 string_copy(ref_commit, view->ref);
2021 wattrset(view->win, get_line_attr(type));
2022 wchgat(view->win, -1, 0, type, NULL);
2025 type = LINE_MAIN_COMMIT;
2026 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
2029 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
2030 waddnstr(view->win, buf, timelen);
2031 waddstr(view->win, " ");
2034 wmove(view->win, lineno, col);
2035 if (type != LINE_CURSOR)
2036 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
2039 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
2041 authorlen = strlen(commit->author);
2042 if (authorlen > AUTHOR_COLS - 2) {
2043 authorlen = AUTHOR_COLS - 2;
2049 waddnstr(view->win, commit->author, authorlen);
2050 if (type != LINE_CURSOR)
2051 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
2052 waddch(view->win, '~');
2054 waddstr(view->win, commit->author);
2058 if (type != LINE_CURSOR)
2059 wattrset(view->win, A_NORMAL);
2061 if (opt_rev_graph && commit->graph_size) {
2064 wmove(view->win, lineno, col);
2065 /* Using waddch() instead of waddnstr() ensures that
2066 * they'll be rendered correctly for the cursor line. */
2067 for (i = 0; i < commit->graph_size; i++)
2068 waddch(view->win, commit->graph[i]);
2070 col += commit->graph_size + 1;
2073 wmove(view->win, lineno, col);
2079 if (type == LINE_CURSOR)
2081 else if (commit->refs[i]->tag)
2082 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
2084 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
2085 waddstr(view->win, "[");
2086 waddstr(view->win, commit->refs[i]->name);
2087 waddstr(view->win, "]");
2088 if (type != LINE_CURSOR)
2089 wattrset(view->win, A_NORMAL);
2090 waddstr(view->win, " ");
2091 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
2092 } while (commit->refs[i++]->next);
2095 if (type != LINE_CURSOR)
2096 wattrset(view->win, get_line_attr(type));
2099 int titlelen = strlen(commit->title);
2101 if (col + titlelen > view->width)
2102 titlelen = view->width - col;
2104 waddnstr(view->win, commit->title, titlelen);
2110 /* Reads git log --pretty=raw output and parses it into the commit struct. */
2112 main_read(struct view *view, char *line)
2114 enum line_type type = get_line_type(line);
2115 struct commit *commit = view->lines
2116 ? view->line[view->lines - 1].data : NULL;
2120 commit = calloc(1, sizeof(struct commit));
2124 line += STRING_SIZE("commit ");
2126 view->line[view->lines++].data = commit;
2127 string_copy(commit->id, line);
2128 commit->refs = get_refs(commit->id);
2129 commit->graph[commit->graph_size++] = ACS_LTEE;
2134 char *ident = line + STRING_SIZE("author ");
2135 char *end = strchr(ident, '<');
2141 for (; end > ident && isspace(end[-1]); end--) ;
2145 string_copy(commit->author, ident);
2147 /* Parse epoch and timezone */
2149 char *secs = strchr(end + 1, '>');
2153 if (!secs || secs[1] != ' ')
2157 time = (time_t) atol(secs);
2158 zone = strchr(secs, ' ');
2159 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
2163 tz = ('0' - zone[1]) * 60 * 60 * 10;
2164 tz += ('0' - zone[2]) * 60 * 60;
2165 tz += ('0' - zone[3]) * 60;
2166 tz += ('0' - zone[4]) * 60;
2173 gmtime_r(&time, &commit->time);
2181 /* Fill in the commit title if it has not already been set. */
2182 if (commit->title[0])
2185 /* Require titles to start with a non-space character at the
2186 * offset used by git log. */
2187 /* FIXME: More gracefull handling of titles; append "..." to
2188 * shortened titles, etc. */
2189 if (strncmp(line, " ", 4) ||
2193 string_copy(commit->title, line + 4);
2200 main_enter(struct view *view, struct line *line)
2202 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2204 open_view(view, REQ_VIEW_DIFF, flags);
2208 static struct view_ops main_ops = {
2217 * Unicode / UTF-8 handling
2219 * NOTE: Much of the following code for dealing with unicode is derived from
2220 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2221 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2224 /* I've (over)annotated a lot of code snippets because I am not entirely
2225 * confident that the approach taken by this small UTF-8 interface is correct.
2229 unicode_width(unsigned long c)
2232 (c <= 0x115f /* Hangul Jamo */
2235 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2237 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2238 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2239 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2240 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2241 || (c >= 0xffe0 && c <= 0xffe6)
2242 || (c >= 0x20000 && c <= 0x2fffd)
2243 || (c >= 0x30000 && c <= 0x3fffd)))
2249 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2250 * Illegal bytes are set one. */
2251 static const unsigned char utf8_bytes[256] = {
2252 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,
2253 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,
2254 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,
2255 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,
2256 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,
2257 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,
2258 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,
2259 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,
2262 /* Decode UTF-8 multi-byte representation into a unicode character. */
2263 static inline unsigned long
2264 utf8_to_unicode(const char *string, size_t length)
2266 unsigned long unicode;
2270 unicode = string[0];
2273 unicode = (string[0] & 0x1f) << 6;
2274 unicode += (string[1] & 0x3f);
2277 unicode = (string[0] & 0x0f) << 12;
2278 unicode += ((string[1] & 0x3f) << 6);
2279 unicode += (string[2] & 0x3f);
2282 unicode = (string[0] & 0x0f) << 18;
2283 unicode += ((string[1] & 0x3f) << 12);
2284 unicode += ((string[2] & 0x3f) << 6);
2285 unicode += (string[3] & 0x3f);
2288 unicode = (string[0] & 0x0f) << 24;
2289 unicode += ((string[1] & 0x3f) << 18);
2290 unicode += ((string[2] & 0x3f) << 12);
2291 unicode += ((string[3] & 0x3f) << 6);
2292 unicode += (string[4] & 0x3f);
2295 unicode = (string[0] & 0x01) << 30;
2296 unicode += ((string[1] & 0x3f) << 24);
2297 unicode += ((string[2] & 0x3f) << 18);
2298 unicode += ((string[3] & 0x3f) << 12);
2299 unicode += ((string[4] & 0x3f) << 6);
2300 unicode += (string[5] & 0x3f);
2303 die("Invalid unicode length");
2306 /* Invalid characters could return the special 0xfffd value but NUL
2307 * should be just as good. */
2308 return unicode > 0xffff ? 0 : unicode;
2311 /* Calculates how much of string can be shown within the given maximum width
2312 * and sets trimmed parameter to non-zero value if all of string could not be
2315 * Additionally, adds to coloffset how many many columns to move to align with
2316 * the expected position. Takes into account how multi-byte and double-width
2317 * characters will effect the cursor position.
2319 * Returns the number of bytes to output from string to satisfy max_width. */
2321 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2323 const char *start = string;
2324 const char *end = strchr(string, '\0');
2330 while (string < end) {
2331 int c = *(unsigned char *) string;
2332 unsigned char bytes = utf8_bytes[c];
2334 unsigned long unicode;
2336 if (string + bytes > end)
2339 /* Change representation to figure out whether
2340 * it is a single- or double-width character. */
2342 unicode = utf8_to_unicode(string, bytes);
2343 /* FIXME: Graceful handling of invalid unicode character. */
2347 ucwidth = unicode_width(unicode);
2349 if (width > max_width) {
2354 /* The column offset collects the differences between the
2355 * number of bytes encoding a character and the number of
2356 * columns will be used for rendering said character.
2358 * So if some character A is encoded in 2 bytes, but will be
2359 * represented on the screen using only 1 byte this will and up
2360 * adding 1 to the multi-byte column offset.
2362 * Assumes that no double-width character can be encoding in
2363 * less than two bytes. */
2364 if (bytes > ucwidth)
2365 mbwidth += bytes - ucwidth;
2370 *coloffset += mbwidth;
2372 return string - start;
2380 /* Whether or not the curses interface has been initialized. */
2381 static bool cursed = FALSE;
2383 /* The status window is used for polling keystrokes. */
2384 static WINDOW *status_win;
2386 /* Update status and title window. */
2388 report(const char *msg, ...)
2390 static bool empty = TRUE;
2391 struct view *view = display[current_view];
2393 if (!empty || *msg) {
2396 va_start(args, msg);
2399 wmove(status_win, 0, 0);
2401 vwprintw(status_win, msg, args);
2406 wrefresh(status_win);
2411 update_view_title(view);
2412 update_display_cursor();
2415 /* Controls when nodelay should be in effect when polling user input. */
2417 set_nonblocking_input(bool loading)
2419 static unsigned int loading_views;
2421 if ((loading == FALSE && loading_views-- == 1) ||
2422 (loading == TRUE && loading_views++ == 0))
2423 nodelay(status_win, loading);
2431 /* Initialize the curses library */
2432 if (isatty(STDIN_FILENO)) {
2433 cursed = !!initscr();
2435 /* Leave stdin and stdout alone when acting as a pager. */
2436 FILE *io = fopen("/dev/tty", "r+");
2438 cursed = !!newterm(NULL, io, io);
2442 die("Failed to initialize curses");
2444 nonl(); /* Tell curses not to do NL->CR/NL on output */
2445 cbreak(); /* Take input chars one at a time, no wait for \n */
2446 noecho(); /* Don't echo input */
2447 leaveok(stdscr, TRUE);
2452 getmaxyx(stdscr, y, x);
2453 status_win = newwin(1, 0, y - 1, 0);
2455 die("Failed to create status window");
2457 /* Enable keyboard mapping */
2458 keypad(status_win, TRUE);
2459 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2464 * Repository references
2467 static struct ref *refs;
2468 static size_t refs_size;
2470 /* Id <-> ref store */
2471 static struct ref ***id_refs;
2472 static size_t id_refs_size;
2474 static struct ref **
2477 struct ref ***tmp_id_refs;
2478 struct ref **ref_list = NULL;
2479 size_t ref_list_size = 0;
2482 for (i = 0; i < id_refs_size; i++)
2483 if (!strcmp(id, id_refs[i][0]->id))
2486 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2490 id_refs = tmp_id_refs;
2492 for (i = 0; i < refs_size; i++) {
2495 if (strcmp(id, refs[i].id))
2498 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2506 if (ref_list_size > 0)
2507 ref_list[ref_list_size - 1]->next = 1;
2508 ref_list[ref_list_size] = &refs[i];
2510 /* XXX: The properties of the commit chains ensures that we can
2511 * safely modify the shared ref. The repo references will
2512 * always be similar for the same id. */
2513 ref_list[ref_list_size]->next = 0;
2518 id_refs[id_refs_size++] = ref_list;
2524 read_ref(char *id, int idlen, char *name, int namelen)
2529 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2530 /* Commits referenced by tags has "^{}" appended. */
2531 if (name[namelen - 1] != '}')
2534 while (namelen > 0 && name[namelen] != '^')
2538 namelen -= STRING_SIZE("refs/tags/");
2539 name += STRING_SIZE("refs/tags/");
2541 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2542 namelen -= STRING_SIZE("refs/heads/");
2543 name += STRING_SIZE("refs/heads/");
2545 } else if (!strcmp(name, "HEAD")) {
2549 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2553 ref = &refs[refs_size++];
2554 ref->name = malloc(namelen + 1);
2558 strncpy(ref->name, name, namelen);
2559 ref->name[namelen] = 0;
2561 string_copy(ref->id, id);
2569 const char *cmd_env = getenv("TIG_LS_REMOTE");
2570 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2572 return read_properties(popen(cmd, "r"), "\t", read_ref);
2576 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2578 if (!strcmp(name, "i18n.commitencoding"))
2579 string_copy(opt_encoding, value);
2585 load_repo_config(void)
2587 return read_properties(popen("git repo-config --list", "r"),
2588 "=", read_repo_config_option);
2592 read_properties(FILE *pipe, const char *separators,
2593 int (*read_property)(char *, int, char *, int))
2595 char buffer[BUFSIZ];
2602 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2607 name = chomp_string(name);
2608 namelen = strcspn(name, separators);
2610 if (name[namelen]) {
2612 value = chomp_string(name + namelen + 1);
2613 valuelen = strlen(value);
2620 state = read_property(name, namelen, value, valuelen);
2623 if (state != ERR && ferror(pipe))
2636 static void __NORETURN
2639 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2645 static void __NORETURN
2646 die(const char *err, ...)
2652 va_start(args, err);
2653 fputs("tig: ", stderr);
2654 vfprintf(stderr, err, args);
2655 fputs("\n", stderr);
2662 main(int argc, char *argv[])
2665 enum request request;
2668 signal(SIGINT, quit);
2670 if (load_options() == ERR)
2671 die("Failed to load user config.");
2673 /* Load the repo config file so options can be overwritten from
2674 * the command line. */
2675 if (load_repo_config() == ERR)
2676 die("Failed to load repo config.");
2678 if (!parse_options(argc, argv))
2681 if (load_refs() == ERR)
2682 die("Failed to load refs.");
2684 /* Require a git repository unless when running in pager mode. */
2685 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2686 die("Not a git repository");
2688 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2689 view->cmd_env = getenv(view->cmd_env);
2691 request = opt_request;
2695 while (view_driver(display[current_view], request)) {
2699 foreach_view (view, i)
2702 /* Refresh, accept single keystroke of input */
2703 key = wgetch(status_win);
2704 request = get_request(key);
2706 /* Some low-level request handling. This keeps access to
2707 * status_win restricted. */
2711 /* Temporarily switch to line-oriented and echoed
2716 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2717 memcpy(opt_cmd, "git ", 4);
2718 opt_request = REQ_VIEW_PAGER;
2720 report("Prompt interrupted by loading view, "
2721 "press 'z' to stop loading views");
2722 request = REQ_SCREEN_UPDATE;
2729 case REQ_SCREEN_RESIZE:
2733 getmaxyx(stdscr, height, width);
2735 /* Resize the status view and let the view driver take
2736 * care of resizing the displayed views. */
2737 wresize(status_win, 1, width);
2738 mvwin(status_win, height - 1, 0);
2739 wrefresh(status_win);