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"
35 static void die(const char *err, ...);
36 static void report(const char *msg, ...);
37 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
38 static void set_nonblocking_input(bool loading);
39 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
40 static void load_help_page(void);
42 #define ABS(x) ((x) >= 0 ? (x) : -(x))
43 #define MIN(x, y) ((x) < (y) ? (x) : (y))
45 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
46 #define STRING_SIZE(x) (sizeof(x) - 1)
48 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
49 #define SIZEOF_CMD 1024 /* Size of command buffer. */
51 /* This color name can be used to refer to the default term colors. */
52 #define COLOR_DEFAULT (-1)
54 /* The format and size of the date column in the main view. */
55 #define DATE_FORMAT "%Y-%m-%d %H:%M"
56 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
58 #define AUTHOR_COLS 20
60 /* The default interval between line numbers. */
61 #define NUMBER_INTERVAL 1
65 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
67 #define TIG_LS_REMOTE \
68 "git ls-remote . 2>/dev/null"
70 #define TIG_DIFF_CMD \
71 "git show --patch-with-stat --find-copies-harder -B -C %s"
74 "git log --cc --stat -n100 %s"
76 #define TIG_MAIN_CMD \
77 "git log --topo-order --stat --pretty=raw %s"
79 /* XXX: Needs to be defined to the empty string. */
80 #define TIG_HELP_CMD ""
81 #define TIG_PAGER_CMD ""
83 /* Some ascii-shorthands fitted into the ncurses namespace. */
85 #define KEY_RETURN '\r'
90 char *name; /* Ref name; tag or head names are shortened. */
91 char id[41]; /* Commit SHA1 ID */
92 unsigned int tag:1; /* Is it a tag? */
93 unsigned int next:1; /* For ref lists: are there more refs? */
96 static struct ref **get_refs(char *id);
105 set_from_int_map(struct int_map *map, size_t map_size,
106 int *value, const char *name, int namelen)
111 for (i = 0; i < map_size; i++)
112 if (namelen == map[i].namelen &&
113 !strncasecmp(name, map[i].name, namelen)) {
114 *value = map[i].value;
127 string_ncopy(char *dst, const char *src, int dstlen)
129 strncpy(dst, src, dstlen - 1);
134 /* Shorthand for safely copying into a fixed buffer. */
135 #define string_copy(dst, src) \
136 string_ncopy(dst, src, sizeof(dst))
139 chomp_string(char *name)
143 while (isspace(*name))
146 namelen = strlen(name) - 1;
147 while (namelen > 0 && isspace(name[namelen]))
154 string_nformat(char *buf, size_t bufsize, int *bufpos, const char *fmt, ...)
157 int pos = bufpos ? *bufpos : 0;
160 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
166 return pos >= bufsize ? FALSE : TRUE;
169 #define string_format(buf, fmt, args...) \
170 string_nformat(buf, sizeof(buf), NULL, fmt, args)
172 #define string_format_from(buf, from, fmt, args...) \
173 string_nformat(buf, sizeof(buf), from, fmt, args)
177 * NOTE: The following is a slightly modified copy of the git project's shell
178 * quoting routines found in the quote.c file.
180 * Help to copy the thing properly quoted for the shell safety. any single
181 * quote is replaced with '\'', any exclamation point is replaced with '\!',
182 * and the whole thing is enclosed in a
185 * original sq_quote result
186 * name ==> name ==> 'name'
187 * a b ==> a b ==> 'a b'
188 * a'b ==> a'\''b ==> 'a'\''b'
189 * a!b ==> a'\!'b ==> 'a'\!'b'
193 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
197 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
200 while ((c = *src++)) {
201 if (c == '\'' || c == '!') {
221 /* XXX: Keep the view request first and in sync with views[]. */ \
222 REQ_GROUP("View switching") \
223 REQ_(VIEW_MAIN, "Show main view"), \
224 REQ_(VIEW_DIFF, "Show diff view"), \
225 REQ_(VIEW_LOG, "Show log view"), \
226 REQ_(VIEW_HELP, "Show help page"), \
227 REQ_(VIEW_PAGER, "Show pager view"), \
229 REQ_GROUP("View manipulation") \
230 REQ_(ENTER, "Enter current line and scroll"), \
231 REQ_(NEXT, "Move to next"), \
232 REQ_(PREVIOUS, "Move to previous"), \
233 REQ_(VIEW_NEXT, "Move focus to next view"), \
234 REQ_(VIEW_CLOSE, "Close the current view"), \
235 REQ_(QUIT, "Close all views and quit"), \
237 REQ_GROUP("Cursor navigation") \
238 REQ_(MOVE_UP, "Move cursor one line up"), \
239 REQ_(MOVE_DOWN, "Move cursor one line down"), \
240 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
241 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
242 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
243 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
245 REQ_GROUP("Scrolling") \
246 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
247 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
248 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
249 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
252 REQ_(PROMPT, "Bring up the prompt"), \
253 REQ_(SCREEN_UPDATE, "Update the screen"), \
254 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
255 REQ_(SCREEN_RESIZE, "Resize the screen"), \
256 REQ_(SHOW_VERSION, "Show version information"), \
257 REQ_(STOP_LOADING, "Stop all loading views"), \
258 REQ_(TOGGLE_LINENO, "Toggle line numbers"),
261 /* User action requests. */
263 #define REQ_GROUP(help)
264 #define REQ_(req, help) REQ_##req
266 /* Offset all requests to avoid conflicts with ncurses getch values. */
267 REQ_OFFSET = KEY_MAX + 1,
274 struct request_info {
275 enum request request;
279 static struct request_info req_info[] = {
280 #define REQ_GROUP(help) { 0, (help) },
281 #define REQ_(req, help) { REQ_##req, (help) }
291 static const char usage[] =
292 VERSION " (" __DATE__ ")\n"
294 "Usage: tig [options]\n"
295 " or: tig [options] [--] [git log options]\n"
296 " or: tig [options] log [git log options]\n"
297 " or: tig [options] diff [git diff options]\n"
298 " or: tig [options] show [git show options]\n"
299 " or: tig [options] < [git command output]\n"
302 " -l Start up in log view\n"
303 " -d Start up in diff view\n"
304 " -n[I], --line-number[=I] Show line numbers with given interval\n"
305 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
306 " -- Mark end of tig options\n"
307 " -v, --version Show version and exit\n"
308 " -h, --help Show help message and exit\n";
310 /* Option and state variables. */
311 static bool opt_line_number = FALSE;
312 static int opt_num_interval = NUMBER_INTERVAL;
313 static int opt_tab_size = TABSIZE;
314 static enum request opt_request = REQ_VIEW_MAIN;
315 static char opt_cmd[SIZEOF_CMD] = "";
316 static char opt_encoding[20] = "";
317 static bool opt_utf8 = TRUE;
318 static FILE *opt_pipe = NULL;
326 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
336 int namelen = strlen(name);
340 if (strncmp(opt, name, namelen))
343 if (opt[namelen] == '=')
344 value = opt + namelen + 1;
347 if (!short_name || opt[1] != short_name)
352 va_start(args, type);
353 if (type == OPT_INT) {
354 number = va_arg(args, int *);
356 *number = atoi(value);
363 /* Returns the index of log or diff command or -1 to exit. */
365 parse_options(int argc, char *argv[])
369 for (i = 1; i < argc; i++) {
372 if (!strcmp(opt, "-l")) {
373 opt_request = REQ_VIEW_LOG;
377 if (!strcmp(opt, "-d")) {
378 opt_request = REQ_VIEW_DIFF;
382 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
383 opt_line_number = TRUE;
387 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
388 opt_tab_size = MIN(opt_tab_size, TABSIZE);
392 if (check_option(opt, 'v', "version", OPT_NONE)) {
393 printf("tig version %s\n", VERSION);
397 if (check_option(opt, 'h', "help", OPT_NONE)) {
402 if (!strcmp(opt, "--")) {
407 if (!strcmp(opt, "log") ||
408 !strcmp(opt, "diff") ||
409 !strcmp(opt, "show")) {
410 opt_request = opt[0] == 'l'
411 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
415 if (opt[0] && opt[0] != '-')
418 die("unknown command '%s'", opt);
421 if (!isatty(STDIN_FILENO)) {
422 opt_request = REQ_VIEW_PAGER;
425 } else if (i < argc) {
428 if (opt_request == REQ_VIEW_MAIN)
429 /* XXX: This is vulnerable to the user overriding
430 * options required for the main view parser. */
431 string_copy(opt_cmd, "git log --stat --pretty=raw");
433 string_copy(opt_cmd, "git");
434 buf_size = strlen(opt_cmd);
436 while (buf_size < sizeof(opt_cmd) && i < argc) {
437 opt_cmd[buf_size++] = ' ';
438 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
441 if (buf_size >= sizeof(opt_cmd))
442 die("command too long");
444 opt_cmd[buf_size] = 0;
448 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
456 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
457 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
458 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
459 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
460 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
461 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
462 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
463 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
464 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
465 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
466 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
467 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
468 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
469 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
470 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
471 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
472 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
473 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
474 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
475 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
476 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
477 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
478 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
479 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
480 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
481 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
482 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
483 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
484 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
485 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
486 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
487 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
488 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
489 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
490 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
491 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
492 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
493 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
497 * Line-oriented content detection.
501 #define LINE(type, line, fg, bg, attr) \
508 const char *name; /* Option name. */
509 int namelen; /* Size of option name. */
510 const char *line; /* The start of line to match. */
511 int linelen; /* Size of string to match. */
512 int fg, bg, attr; /* Color and text attributes for the lines. */
515 static struct line_info line_info[] = {
516 #define LINE(type, line, fg, bg, attr) \
517 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
522 static enum line_type
523 get_line_type(char *line)
525 int linelen = strlen(line);
528 for (type = 0; type < ARRAY_SIZE(line_info); type++)
529 /* Case insensitive search matches Signed-off-by lines better. */
530 if (linelen >= line_info[type].linelen &&
531 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
538 get_line_attr(enum line_type type)
540 assert(type < ARRAY_SIZE(line_info));
541 return COLOR_PAIR(type) | line_info[type].attr;
544 static struct line_info *
545 get_line_info(char *name, int namelen)
550 /* Diff-Header -> DIFF_HEADER */
551 for (i = 0; i < namelen; i++) {
554 else if (name[i] == '.')
558 for (type = 0; type < ARRAY_SIZE(line_info); type++)
559 if (namelen == line_info[type].namelen &&
560 !strncasecmp(line_info[type].name, name, namelen))
561 return &line_info[type];
569 int default_bg = COLOR_BLACK;
570 int default_fg = COLOR_WHITE;
575 if (use_default_colors() != ERR) {
580 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
581 struct line_info *info = &line_info[type];
582 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
583 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
585 init_pair(type, fg, bg);
591 void *data; /* User data */
596 * User config file handling.
599 static struct int_map color_map[] = {
600 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
612 #define set_color(color, name, namelen) \
613 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
615 static struct int_map attr_map[] = {
616 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
626 #define set_attribute(attr, name, namelen) \
627 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
629 static int config_lineno;
630 static bool config_errors;
631 static char *config_msg;
634 set_option(char *opt, int optlen, char *value, int valuelen)
636 /* Reads: "color" object fgcolor bgcolor [attr] */
637 if (!strcmp(opt, "color")) {
638 struct line_info *info;
640 value = chomp_string(value);
641 valuelen = strcspn(value, " \t");
642 info = get_line_info(value, valuelen);
644 config_msg = "Unknown color name";
648 value = chomp_string(value + valuelen);
649 valuelen = strcspn(value, " \t");
650 if (set_color(&info->fg, value, valuelen) == ERR) {
651 config_msg = "Unknown color";
655 value = chomp_string(value + valuelen);
656 valuelen = strcspn(value, " \t");
657 if (set_color(&info->bg, value, valuelen) == ERR) {
658 config_msg = "Unknown color";
662 value = chomp_string(value + valuelen);
664 set_attribute(&info->attr, value, strlen(value)) == ERR) {
665 config_msg = "Unknown attribute";
676 read_option(char *opt, int optlen, char *value, int valuelen)
679 config_msg = "Internal error";
681 optlen = strcspn(opt, "#;");
683 /* The whole line is a commend or empty. */
686 } else if (opt[optlen] != 0) {
687 /* Part of the option name is a comment, so the value part
688 * should be ignored. */
690 opt[optlen] = value[valuelen] = 0;
692 /* Else look for comment endings in the value. */
693 valuelen = strcspn(value, "#;");
697 if (set_option(opt, optlen, value, valuelen) == ERR) {
698 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
699 config_lineno, optlen, opt, config_msg);
700 config_errors = TRUE;
703 /* Always keep going if errors are encountered. */
710 char *home = getenv("HOME");
715 config_errors = FALSE;
717 if (!home || !string_format(buf, "%s/.tigrc", home))
720 /* It's ok that the file doesn't exist. */
721 file = fopen(buf, "r");
725 if (read_properties(file, " \t", read_option) == ERR ||
726 config_errors == TRUE)
727 fprintf(stderr, "Errors while loading %s.\n", buf);
740 /* The display array of active views and the index of the current view. */
741 static struct view *display[2];
742 static unsigned int current_view;
744 #define foreach_view(view, i) \
745 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
747 #define displayed_views() (display[1] != NULL ? 2 : 1)
749 /* Current head and commit ID */
750 static char ref_commit[SIZEOF_REF] = "HEAD";
751 static char ref_head[SIZEOF_REF] = "HEAD";
754 const char *name; /* View name */
755 const char *cmd_fmt; /* Default command line format */
756 const char *cmd_env; /* Command line set via environment */
757 const char *id; /* Points to either of ref_{head,commit} */
759 struct view_ops *ops; /* View operations */
761 char cmd[SIZEOF_CMD]; /* Command buffer */
762 char ref[SIZEOF_REF]; /* Hovered commit reference */
763 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
765 int height, width; /* The width and height of the main window */
766 WINDOW *win; /* The main window */
767 WINDOW *title; /* The title window living below the main window */
770 unsigned long offset; /* Offset of the window top */
771 unsigned long lineno; /* Current line number */
773 /* If non-NULL, points to the view that opened this view. If this view
774 * is closed tig will switch back to the parent view. */
778 unsigned long lines; /* Total number of lines */
779 struct line *line; /* Line index */
780 unsigned long line_size;/* Total number of allocated lines */
781 unsigned int digits; /* Number of digits in the lines member. */
789 /* What type of content being displayed. Used in the title bar. */
791 /* Draw one line; @lineno must be < view->height. */
792 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
793 /* Read one line; updates view->line. */
794 bool (*read)(struct view *view, char *data);
795 /* Depending on view, change display based on current line. */
796 bool (*enter)(struct view *view, struct line *line);
799 static struct view_ops pager_ops;
800 static struct view_ops main_ops;
802 #define VIEW_STR(name, cmd, env, ref, ops) \
803 { name, cmd, #env, ref, ops }
805 #define VIEW_(id, name, ops, ref) \
806 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
809 static struct view views[] = {
810 VIEW_(MAIN, "main", &main_ops, ref_head),
811 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
812 VIEW_(LOG, "log", &pager_ops, ref_head),
813 VIEW_(HELP, "help", &pager_ops, "static"),
814 VIEW_(PAGER, "pager", &pager_ops, "static"),
817 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
821 draw_view_line(struct view *view, unsigned int lineno)
823 if (view->offset + lineno >= view->lines)
826 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
830 redraw_view_from(struct view *view, int lineno)
832 assert(0 <= lineno && lineno < view->height);
834 for (; lineno < view->height; lineno++) {
835 if (!draw_view_line(view, lineno))
839 redrawwin(view->win);
844 redraw_view(struct view *view)
847 redraw_view_from(view, 0);
852 update_view_title(struct view *view)
854 if (view == display[current_view])
855 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
857 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
860 wmove(view->title, 0, 0);
863 wprintw(view->title, "[%s] %s", view->name, view->ref);
865 wprintw(view->title, "[%s]", view->name);
867 if (view->lines || view->pipe) {
868 unsigned int lines = view->lines
869 ? (view->lineno + 1) * 100 / view->lines
872 wprintw(view->title, " - %s %d of %d (%d%%)",
880 time_t secs = time(NULL) - view->start_time;
882 /* Three git seconds are a long time ... */
884 wprintw(view->title, " %lds", secs);
887 wmove(view->title, 0, view->width - 1);
888 wrefresh(view->title);
895 struct view *base = display[0];
896 struct view *view = display[1] ? display[1] : display[0];
898 /* Setup window dimensions */
900 getmaxyx(stdscr, base->height, base->width);
902 /* Make room for the status window. */
906 /* Horizontal split. */
907 view->width = base->width;
908 view->height = SCALE_SPLIT_VIEW(base->height);
909 base->height -= view->height;
911 /* Make room for the title bar. */
915 /* Make room for the title bar. */
920 foreach_view (view, i) {
922 view->win = newwin(view->height, 0, offset, 0);
924 die("Failed to create %s view", view->name);
926 scrollok(view->win, TRUE);
928 view->title = newwin(1, 0, offset + view->height, 0);
930 die("Failed to create title window");
933 wresize(view->win, view->height, view->width);
934 mvwin(view->win, offset, 0);
935 mvwin(view->title, offset + view->height, 0);
938 offset += view->height + 1;
948 foreach_view (view, i) {
950 update_view_title(view);
955 update_display_cursor(void)
957 struct view *view = display[current_view];
959 /* Move the cursor to the right-most column of the cursor line.
961 * XXX: This could turn out to be a bit expensive, but it ensures that
962 * the cursor does not jump around. */
964 wmove(view->win, view->lineno - view->offset, view->width - 1);
973 /* Scrolling backend */
975 do_scroll_view(struct view *view, int lines, bool redraw)
977 /* The rendering expects the new offset. */
978 view->offset += lines;
980 assert(0 <= view->offset && view->offset < view->lines);
983 /* Redraw the whole screen if scrolling is pointless. */
984 if (view->height < ABS(lines)) {
988 int line = lines > 0 ? view->height - lines : 0;
989 int end = line + ABS(lines);
991 wscrl(view->win, lines);
993 for (; line < end; line++) {
994 if (!draw_view_line(view, line))
999 /* Move current line into the view. */
1000 if (view->lineno < view->offset) {
1001 view->lineno = view->offset;
1002 draw_view_line(view, 0);
1004 } else if (view->lineno >= view->offset + view->height) {
1005 if (view->lineno == view->offset + view->height) {
1006 /* Clear the hidden line so it doesn't show if the view
1007 * is scrolled up. */
1008 wmove(view->win, view->height, 0);
1009 wclrtoeol(view->win);
1011 view->lineno = view->offset + view->height - 1;
1012 draw_view_line(view, view->lineno - view->offset);
1015 assert(view->offset <= view->lineno && view->lineno < view->lines);
1020 redrawwin(view->win);
1021 wrefresh(view->win);
1025 /* Scroll frontend */
1027 scroll_view(struct view *view, enum request request)
1032 case REQ_SCROLL_PAGE_DOWN:
1033 lines = view->height;
1034 case REQ_SCROLL_LINE_DOWN:
1035 if (view->offset + lines > view->lines)
1036 lines = view->lines - view->offset;
1038 if (lines == 0 || view->offset + view->height >= view->lines) {
1039 report("Cannot scroll beyond the last line");
1044 case REQ_SCROLL_PAGE_UP:
1045 lines = view->height;
1046 case REQ_SCROLL_LINE_UP:
1047 if (lines > view->offset)
1048 lines = view->offset;
1051 report("Cannot scroll beyond the first line");
1059 die("request %d not handled in switch", request);
1062 do_scroll_view(view, lines, TRUE);
1067 move_view(struct view *view, enum request request, bool redraw)
1072 case REQ_MOVE_FIRST_LINE:
1073 steps = -view->lineno;
1076 case REQ_MOVE_LAST_LINE:
1077 steps = view->lines - view->lineno - 1;
1080 case REQ_MOVE_PAGE_UP:
1081 steps = view->height > view->lineno
1082 ? -view->lineno : -view->height;
1085 case REQ_MOVE_PAGE_DOWN:
1086 steps = view->lineno + view->height >= view->lines
1087 ? view->lines - view->lineno - 1 : view->height;
1099 die("request %d not handled in switch", request);
1102 if (steps <= 0 && view->lineno == 0) {
1103 report("Cannot move beyond the first line");
1106 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1107 report("Cannot move beyond the last line");
1111 /* Move the current line */
1112 view->lineno += steps;
1113 assert(0 <= view->lineno && view->lineno < view->lines);
1115 /* Repaint the old "current" line if we be scrolling */
1116 if (ABS(steps) < view->height) {
1117 int prev_lineno = view->lineno - steps - view->offset;
1119 wmove(view->win, prev_lineno, 0);
1120 wclrtoeol(view->win);
1121 draw_view_line(view, prev_lineno);
1124 /* Check whether the view needs to be scrolled */
1125 if (view->lineno < view->offset ||
1126 view->lineno >= view->offset + view->height) {
1127 if (steps < 0 && -steps > view->offset) {
1128 steps = -view->offset;
1130 } else if (steps > 0) {
1131 if (view->lineno == view->lines - 1 &&
1132 view->lines > view->height) {
1133 steps = view->lines - view->offset - 1;
1134 if (steps >= view->height)
1135 steps -= view->height - 1;
1139 do_scroll_view(view, steps, redraw);
1143 /* Draw the current line */
1144 draw_view_line(view, view->lineno - view->offset);
1149 redrawwin(view->win);
1150 wrefresh(view->win);
1156 * Incremental updating
1160 end_update(struct view *view)
1164 set_nonblocking_input(FALSE);
1165 if (view->pipe == stdin)
1173 begin_update(struct view *view)
1175 const char *id = view->id;
1181 string_copy(view->cmd, opt_cmd);
1183 /* When running random commands, the view ref could have become
1184 * invalid so clear it. */
1187 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1189 if (!string_format(view->cmd, format, id, id, id, id, id))
1193 /* Special case for the pager view. */
1195 view->pipe = opt_pipe;
1198 view->pipe = popen(view->cmd, "r");
1204 set_nonblocking_input(TRUE);
1209 string_copy(view->vid, id);
1214 for (i = 0; i < view->lines; i++)
1215 if (view->line[i].data)
1216 free(view->line[i].data);
1222 view->start_time = time(NULL);
1227 static struct line *
1228 realloc_lines(struct view *view, size_t line_size)
1230 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1236 view->line_size = line_size;
1241 update_view(struct view *view)
1243 char buffer[BUFSIZ];
1245 /* The number of lines to read. If too low it will cause too much
1246 * redrawing (and possible flickering), if too high responsiveness
1248 unsigned long lines = view->height;
1249 int redraw_from = -1;
1254 /* Only redraw if lines are visible. */
1255 if (view->offset + view->height >= view->lines)
1256 redraw_from = view->lines - view->offset;
1258 if (!realloc_lines(view, view->lines + lines))
1261 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1262 int linelen = strlen(line);
1265 line[linelen - 1] = 0;
1267 if (!view->ops->read(view, line))
1277 lines = view->lines;
1278 for (digits = 0; lines; digits++)
1281 /* Keep the displayed view in sync with line number scaling. */
1282 if (digits != view->digits) {
1283 view->digits = digits;
1288 if (redraw_from >= 0) {
1289 /* If this is an incremental update, redraw the previous line
1290 * since for commits some members could have changed when
1291 * loading the main view. */
1292 if (redraw_from > 0)
1295 /* Incrementally draw avoids flickering. */
1296 redraw_view_from(view, redraw_from);
1299 /* Update the title _after_ the redraw so that if the redraw picks up a
1300 * commit reference in view->ref it'll be available here. */
1301 update_view_title(view);
1303 if (ferror(view->pipe)) {
1304 report("Failed to read: %s", strerror(errno));
1307 } else if (feof(view->pipe)) {
1315 report("Allocation failure");
1323 OPEN_DEFAULT = 0, /* Use default view switching. */
1324 OPEN_SPLIT = 1, /* Split current view. */
1325 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1326 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1330 open_view(struct view *prev, enum request request, enum open_flags flags)
1332 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1333 bool split = !!(flags & OPEN_SPLIT);
1334 bool reload = !!(flags & OPEN_RELOAD);
1335 struct view *view = VIEW(request);
1336 int nviews = displayed_views();
1337 struct view *base_view = display[0];
1339 if (view == prev && nviews == 1 && !reload) {
1340 report("Already in %s view", view->name);
1344 if (view == VIEW(REQ_VIEW_HELP)) {
1347 } else if ((reload || strcmp(view->vid, view->id)) &&
1348 !begin_update(view)) {
1349 report("Failed to load %s view", view->name);
1358 /* Maximize the current view. */
1359 memset(display, 0, sizeof(display));
1361 display[current_view] = view;
1364 /* Resize the view when switching between split- and full-screen,
1365 * or when switching between two different full-screen views. */
1366 if (nviews != displayed_views() ||
1367 (nviews == 1 && base_view != display[0]))
1370 if (split && prev->lineno - prev->offset >= prev->height) {
1371 /* Take the title line into account. */
1372 int lines = prev->lineno - prev->offset - prev->height + 1;
1374 /* Scroll the view that was split if the current line is
1375 * outside the new limited view. */
1376 do_scroll_view(prev, lines, TRUE);
1379 if (prev && view != prev) {
1380 if (split && !backgrounded) {
1381 /* "Blur" the previous view. */
1382 update_view_title(prev);
1385 view->parent = prev;
1388 if (view->pipe && view->lines == 0) {
1389 /* Clear the old view and let the incremental updating refill
1398 /* If the view is backgrounded the above calls to report()
1399 * won't redraw the view title. */
1401 update_view_title(view);
1406 * User request switch noodle
1410 view_driver(struct view *view, enum request request)
1417 case REQ_MOVE_PAGE_UP:
1418 case REQ_MOVE_PAGE_DOWN:
1419 case REQ_MOVE_FIRST_LINE:
1420 case REQ_MOVE_LAST_LINE:
1421 move_view(view, request, TRUE);
1424 case REQ_SCROLL_LINE_DOWN:
1425 case REQ_SCROLL_LINE_UP:
1426 case REQ_SCROLL_PAGE_DOWN:
1427 case REQ_SCROLL_PAGE_UP:
1428 scroll_view(view, request);
1435 case REQ_VIEW_PAGER:
1436 open_view(view, request, OPEN_DEFAULT);
1441 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1443 if (view == VIEW(REQ_VIEW_DIFF) &&
1444 view->parent == VIEW(REQ_VIEW_MAIN)) {
1445 bool redraw = display[1] == view;
1447 view = view->parent;
1448 move_view(view, request, redraw);
1450 update_view_title(view);
1452 move_view(view, request, TRUE);
1459 report("Nothing to enter");
1462 return view->ops->enter(view, &view->line[view->lineno]);
1466 int nviews = displayed_views();
1467 int next_view = (current_view + 1) % nviews;
1469 if (next_view == current_view) {
1470 report("Only one view is displayed");
1474 current_view = next_view;
1475 /* Blur out the title of the previous view. */
1476 update_view_title(view);
1480 case REQ_TOGGLE_LINENO:
1481 opt_line_number = !opt_line_number;
1486 /* Always reload^Wrerun commands from the prompt. */
1487 open_view(view, opt_request, OPEN_RELOAD);
1490 case REQ_STOP_LOADING:
1491 for (i = 0; i < ARRAY_SIZE(views); i++) {
1494 report("Stopped loading the %s view", view->name),
1499 case REQ_SHOW_VERSION:
1500 report("%s (built %s)", VERSION, __DATE__);
1503 case REQ_SCREEN_RESIZE:
1506 case REQ_SCREEN_REDRAW:
1510 case REQ_SCREEN_UPDATE:
1514 case REQ_VIEW_CLOSE:
1515 /* XXX: Mark closed views by letting view->parent point to the
1516 * view itself. Parents to closed view should never be
1519 view->parent->parent != view->parent) {
1520 memset(display, 0, sizeof(display));
1522 display[current_view] = view->parent;
1523 view->parent = view;
1533 /* An unknown key will show most commonly used commands. */
1534 report("Unknown key, press 'h' for help");
1547 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1549 char *text = line->data;
1550 enum line_type type = line->type;
1551 int textlen = strlen(text);
1554 wmove(view->win, lineno, 0);
1556 if (view->offset + lineno == view->lineno) {
1557 if (type == LINE_COMMIT) {
1558 string_copy(view->ref, text + 7);
1559 string_copy(ref_commit, view->ref);
1563 wchgat(view->win, -1, 0, type, NULL);
1566 attr = get_line_attr(type);
1567 wattrset(view->win, attr);
1569 if (opt_line_number || opt_tab_size < TABSIZE) {
1570 static char spaces[] = " ";
1571 int col_offset = 0, col = 0;
1573 if (opt_line_number) {
1574 unsigned long real_lineno = view->offset + lineno + 1;
1576 if (real_lineno == 1 ||
1577 (real_lineno % opt_num_interval) == 0) {
1578 wprintw(view->win, "%.*d", view->digits, real_lineno);
1581 waddnstr(view->win, spaces,
1582 MIN(view->digits, STRING_SIZE(spaces)));
1584 waddstr(view->win, ": ");
1585 col_offset = view->digits + 2;
1588 while (text && col_offset + col < view->width) {
1589 int cols_max = view->width - col_offset - col;
1593 if (*text == '\t') {
1595 assert(sizeof(spaces) > TABSIZE);
1597 cols = opt_tab_size - (col % opt_tab_size);
1600 text = strchr(text, '\t');
1601 cols = line ? text - pos : strlen(pos);
1604 waddnstr(view->win, pos, MIN(cols, cols_max));
1609 int col = 0, pos = 0;
1611 for (; pos < textlen && col < view->width; pos++, col++)
1612 if (text[pos] == '\t')
1613 col += TABSIZE - (col % TABSIZE) - 1;
1615 waddnstr(view->win, text, pos);
1622 add_pager_refs(struct view *view, struct line *line)
1625 char *data = line->data;
1627 int bufpos = 0, refpos = 0;
1628 const char *sep = "Refs: ";
1630 assert(line->type == LINE_COMMIT);
1632 refs = get_refs(data + STRING_SIZE("commit "));
1637 struct ref *ref = refs[refpos];
1638 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1640 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1643 } while (refs[refpos++]->next);
1645 if (!realloc_lines(view, view->line_size + 1))
1648 line = &view->line[view->lines];
1649 line->data = strdup(buf);
1653 line->type = LINE_PP_REFS;
1658 pager_read(struct view *view, char *data)
1660 struct line *line = &view->line[view->lines];
1662 line->data = strdup(data);
1666 line->type = get_line_type(line->data);
1669 if (line->type == LINE_COMMIT &&
1670 (view == VIEW(REQ_VIEW_DIFF) ||
1671 view == VIEW(REQ_VIEW_LOG)))
1672 add_pager_refs(view, line);
1678 pager_enter(struct view *view, struct line *line)
1682 if (line->type == LINE_COMMIT &&
1683 (view == VIEW(REQ_VIEW_LOG) ||
1684 view == VIEW(REQ_VIEW_PAGER))) {
1685 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1689 /* Always scroll the view even if it was split. That way
1690 * you can use Enter to scroll through the log view and
1691 * split open each commit diff. */
1692 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1694 /* FIXME: A minor workaround. Scrolling the view will call report("")
1695 * but if we are scrolling a non-current view this won't properly
1696 * update the view title. */
1698 update_view_title(view);
1703 static struct view_ops pager_ops = {
1716 char id[41]; /* SHA1 ID. */
1717 char title[75]; /* The first line of the commit message. */
1718 char author[75]; /* The author of the commit. */
1719 struct tm time; /* Date from the author ident. */
1720 struct ref **refs; /* Repository references; tags & branch heads. */
1724 main_draw(struct view *view, struct line *line, unsigned int lineno)
1726 char buf[DATE_COLS + 1];
1727 struct commit *commit = line->data;
1728 enum line_type type;
1734 if (!*commit->author)
1737 wmove(view->win, lineno, col);
1739 if (view->offset + lineno == view->lineno) {
1740 string_copy(view->ref, commit->id);
1741 string_copy(ref_commit, view->ref);
1743 wattrset(view->win, get_line_attr(type));
1744 wchgat(view->win, -1, 0, type, NULL);
1747 type = LINE_MAIN_COMMIT;
1748 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1751 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1752 waddnstr(view->win, buf, timelen);
1753 waddstr(view->win, " ");
1756 wmove(view->win, lineno, col);
1757 if (type != LINE_CURSOR)
1758 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1761 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1763 authorlen = strlen(commit->author);
1764 if (authorlen > AUTHOR_COLS - 2) {
1765 authorlen = AUTHOR_COLS - 2;
1771 waddnstr(view->win, commit->author, authorlen);
1772 if (type != LINE_CURSOR)
1773 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1774 waddch(view->win, '~');
1776 waddstr(view->win, commit->author);
1780 if (type != LINE_CURSOR)
1781 wattrset(view->win, A_NORMAL);
1783 mvwaddch(view->win, lineno, col, ACS_LTEE);
1784 wmove(view->win, lineno, col + 2);
1791 if (type == LINE_CURSOR)
1793 else if (commit->refs[i]->tag)
1794 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1796 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1797 waddstr(view->win, "[");
1798 waddstr(view->win, commit->refs[i]->name);
1799 waddstr(view->win, "]");
1800 if (type != LINE_CURSOR)
1801 wattrset(view->win, A_NORMAL);
1802 waddstr(view->win, " ");
1803 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1804 } while (commit->refs[i++]->next);
1807 if (type != LINE_CURSOR)
1808 wattrset(view->win, get_line_attr(type));
1811 int titlelen = strlen(commit->title);
1813 if (col + titlelen > view->width)
1814 titlelen = view->width - col;
1816 waddnstr(view->win, commit->title, titlelen);
1822 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1824 main_read(struct view *view, char *line)
1826 enum line_type type = get_line_type(line);
1827 struct commit *commit = view->lines
1828 ? view->line[view->lines - 1].data : NULL;
1832 commit = calloc(1, sizeof(struct commit));
1836 line += STRING_SIZE("commit ");
1838 view->line[view->lines++].data = commit;
1839 string_copy(commit->id, line);
1840 commit->refs = get_refs(commit->id);
1845 char *ident = line + STRING_SIZE("author ");
1846 char *end = strchr(ident, '<');
1852 for (; end > ident && isspace(end[-1]); end--) ;
1856 string_copy(commit->author, ident);
1858 /* Parse epoch and timezone */
1860 char *secs = strchr(end + 1, '>');
1864 if (!secs || secs[1] != ' ')
1868 time = (time_t) atol(secs);
1869 zone = strchr(secs, ' ');
1870 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1874 tz = ('0' - zone[1]) * 60 * 60 * 10;
1875 tz += ('0' - zone[2]) * 60 * 60;
1876 tz += ('0' - zone[3]) * 60;
1877 tz += ('0' - zone[4]) * 60;
1884 gmtime_r(&time, &commit->time);
1892 /* Fill in the commit title if it has not already been set. */
1893 if (commit->title[0])
1896 /* Require titles to start with a non-space character at the
1897 * offset used by git log. */
1898 /* FIXME: More gracefull handling of titles; append "..." to
1899 * shortened titles, etc. */
1900 if (strncmp(line, " ", 4) ||
1904 string_copy(commit->title, line + 4);
1911 main_enter(struct view *view, struct line *line)
1913 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
1915 open_view(view, REQ_VIEW_DIFF, flags);
1919 static struct view_ops main_ops = {
1936 static struct keymap keymap[] = {
1937 /* View switching */
1938 { 'm', REQ_VIEW_MAIN },
1939 { 'd', REQ_VIEW_DIFF },
1940 { 'l', REQ_VIEW_LOG },
1941 { 'p', REQ_VIEW_PAGER },
1942 { 'h', REQ_VIEW_HELP },
1943 { '?', REQ_VIEW_HELP },
1945 /* View manipulation */
1946 { 'q', REQ_VIEW_CLOSE },
1947 { KEY_TAB, REQ_VIEW_NEXT },
1948 { KEY_RETURN, REQ_ENTER },
1949 { KEY_UP, REQ_PREVIOUS },
1950 { KEY_DOWN, REQ_NEXT },
1952 /* Cursor navigation */
1953 { 'k', REQ_MOVE_UP },
1954 { 'j', REQ_MOVE_DOWN },
1955 { KEY_HOME, REQ_MOVE_FIRST_LINE },
1956 { KEY_END, REQ_MOVE_LAST_LINE },
1957 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
1958 { ' ', REQ_MOVE_PAGE_DOWN },
1959 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
1960 { 'b', REQ_MOVE_PAGE_UP },
1961 { '-', REQ_MOVE_PAGE_UP },
1964 { KEY_IC, REQ_SCROLL_LINE_UP },
1965 { KEY_DC, REQ_SCROLL_LINE_DOWN },
1966 { 'w', REQ_SCROLL_PAGE_UP },
1967 { 's', REQ_SCROLL_PAGE_DOWN },
1971 { 'z', REQ_STOP_LOADING },
1972 { 'v', REQ_SHOW_VERSION },
1973 { 'r', REQ_SCREEN_REDRAW },
1974 { 'n', REQ_TOGGLE_LINENO },
1975 { ':', REQ_PROMPT },
1977 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
1978 { ERR, REQ_SCREEN_UPDATE },
1980 /* Use the ncurses SIGWINCH handler. */
1981 { KEY_RESIZE, REQ_SCREEN_RESIZE },
1985 get_request(int key)
1989 for (i = 0; i < ARRAY_SIZE(keymap); i++)
1990 if (keymap[i].alias == key)
1991 return keymap[i].request;
1993 return (enum request) key;
2001 static struct key key_table[] = {
2002 { "Enter", KEY_RETURN },
2004 { "Backspace", KEY_BACKSPACE },
2006 { "Escape", KEY_ESC },
2007 { "Left", KEY_LEFT },
2008 { "Right", KEY_RIGHT },
2010 { "Down", KEY_DOWN },
2011 { "Insert", KEY_IC },
2012 { "Delete", KEY_DC },
2013 { "Home", KEY_HOME },
2015 { "PageUp", KEY_PPAGE },
2016 { "PageDown", KEY_NPAGE },
2026 { "F10", KEY_F(10) },
2027 { "F11", KEY_F(11) },
2028 { "F12", KEY_F(12) },
2032 get_key(enum request request)
2034 static char buf[BUFSIZ];
2035 static char key_char[] = "'X'";
2042 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2046 if (keymap[i].request != request)
2049 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2050 if (key_table[key].value == keymap[i].alias)
2051 seq = key_table[key].name;
2054 keymap[i].alias < 127 &&
2055 isprint(keymap[i].alias)) {
2056 key_char[1] = (char) keymap[i].alias;
2063 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
2064 return "Too many keybindings!";
2071 static void load_help_page(void)
2074 struct view *view = VIEW(REQ_VIEW_HELP);
2075 int lines = ARRAY_SIZE(req_info) + 2;
2078 if (view->lines > 0)
2081 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2082 if (!req_info[i].request)
2085 view->line = calloc(lines, sizeof(*view->line));
2087 report("Allocation failure");
2091 pager_read(view, "Quick reference for tig keybindings:");
2093 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2096 if (!req_info[i].request) {
2097 pager_read(view, "");
2098 pager_read(view, req_info[i].help);
2102 key = get_key(req_info[i].request);
2103 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
2106 pager_read(view, buf);
2112 * Unicode / UTF-8 handling
2114 * NOTE: Much of the following code for dealing with unicode is derived from
2115 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2116 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2119 /* I've (over)annotated a lot of code snippets because I am not entirely
2120 * confident that the approach taken by this small UTF-8 interface is correct.
2124 unicode_width(unsigned long c)
2127 (c <= 0x115f /* Hangul Jamo */
2130 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2132 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2133 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2134 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2135 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2136 || (c >= 0xffe0 && c <= 0xffe6)
2137 || (c >= 0x20000 && c <= 0x2fffd)
2138 || (c >= 0x30000 && c <= 0x3fffd)))
2144 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2145 * Illegal bytes are set one. */
2146 static const unsigned char utf8_bytes[256] = {
2147 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,
2148 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,
2149 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,
2150 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,
2151 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,
2152 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,
2153 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,
2154 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,
2157 /* Decode UTF-8 multi-byte representation into a unicode character. */
2158 static inline unsigned long
2159 utf8_to_unicode(const char *string, size_t length)
2161 unsigned long unicode;
2165 unicode = string[0];
2168 unicode = (string[0] & 0x1f) << 6;
2169 unicode += (string[1] & 0x3f);
2172 unicode = (string[0] & 0x0f) << 12;
2173 unicode += ((string[1] & 0x3f) << 6);
2174 unicode += (string[2] & 0x3f);
2177 unicode = (string[0] & 0x0f) << 18;
2178 unicode += ((string[1] & 0x3f) << 12);
2179 unicode += ((string[2] & 0x3f) << 6);
2180 unicode += (string[3] & 0x3f);
2183 unicode = (string[0] & 0x0f) << 24;
2184 unicode += ((string[1] & 0x3f) << 18);
2185 unicode += ((string[2] & 0x3f) << 12);
2186 unicode += ((string[3] & 0x3f) << 6);
2187 unicode += (string[4] & 0x3f);
2190 unicode = (string[0] & 0x01) << 30;
2191 unicode += ((string[1] & 0x3f) << 24);
2192 unicode += ((string[2] & 0x3f) << 18);
2193 unicode += ((string[3] & 0x3f) << 12);
2194 unicode += ((string[4] & 0x3f) << 6);
2195 unicode += (string[5] & 0x3f);
2198 die("Invalid unicode length");
2201 /* Invalid characters could return the special 0xfffd value but NUL
2202 * should be just as good. */
2203 return unicode > 0xffff ? 0 : unicode;
2206 /* Calculates how much of string can be shown within the given maximum width
2207 * and sets trimmed parameter to non-zero value if all of string could not be
2210 * Additionally, adds to coloffset how many many columns to move to align with
2211 * the expected position. Takes into account how multi-byte and double-width
2212 * characters will effect the cursor position.
2214 * Returns the number of bytes to output from string to satisfy max_width. */
2216 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2218 const char *start = string;
2219 const char *end = strchr(string, '\0');
2225 while (string < end) {
2226 int c = *(unsigned char *) string;
2227 unsigned char bytes = utf8_bytes[c];
2229 unsigned long unicode;
2231 if (string + bytes > end)
2234 /* Change representation to figure out whether
2235 * it is a single- or double-width character. */
2237 unicode = utf8_to_unicode(string, bytes);
2238 /* FIXME: Graceful handling of invalid unicode character. */
2242 ucwidth = unicode_width(unicode);
2244 if (width > max_width) {
2249 /* The column offset collects the differences between the
2250 * number of bytes encoding a character and the number of
2251 * columns will be used for rendering said character.
2253 * So if some character A is encoded in 2 bytes, but will be
2254 * represented on the screen using only 1 byte this will and up
2255 * adding 1 to the multi-byte column offset.
2257 * Assumes that no double-width character can be encoding in
2258 * less than two bytes. */
2259 if (bytes > ucwidth)
2260 mbwidth += bytes - ucwidth;
2265 *coloffset += mbwidth;
2267 return string - start;
2275 /* Whether or not the curses interface has been initialized. */
2276 static bool cursed = FALSE;
2278 /* The status window is used for polling keystrokes. */
2279 static WINDOW *status_win;
2281 /* Update status and title window. */
2283 report(const char *msg, ...)
2285 static bool empty = TRUE;
2286 struct view *view = display[current_view];
2288 if (!empty || *msg) {
2291 va_start(args, msg);
2294 wmove(status_win, 0, 0);
2296 vwprintw(status_win, msg, args);
2301 wrefresh(status_win);
2306 update_view_title(view);
2307 update_display_cursor();
2310 /* Controls when nodelay should be in effect when polling user input. */
2312 set_nonblocking_input(bool loading)
2314 static unsigned int loading_views;
2316 if ((loading == FALSE && loading_views-- == 1) ||
2317 (loading == TRUE && loading_views++ == 0))
2318 nodelay(status_win, loading);
2326 /* Initialize the curses library */
2327 if (isatty(STDIN_FILENO)) {
2328 cursed = !!initscr();
2330 /* Leave stdin and stdout alone when acting as a pager. */
2331 FILE *io = fopen("/dev/tty", "r+");
2333 cursed = !!newterm(NULL, io, io);
2337 die("Failed to initialize curses");
2339 nonl(); /* Tell curses not to do NL->CR/NL on output */
2340 cbreak(); /* Take input chars one at a time, no wait for \n */
2341 noecho(); /* Don't echo input */
2342 leaveok(stdscr, TRUE);
2347 getmaxyx(stdscr, y, x);
2348 status_win = newwin(1, 0, y - 1, 0);
2350 die("Failed to create status window");
2352 /* Enable keyboard mapping */
2353 keypad(status_win, TRUE);
2354 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2359 * Repository references
2362 static struct ref *refs;
2363 static size_t refs_size;
2365 /* Id <-> ref store */
2366 static struct ref ***id_refs;
2367 static size_t id_refs_size;
2369 static struct ref **
2372 struct ref ***tmp_id_refs;
2373 struct ref **ref_list = NULL;
2374 size_t ref_list_size = 0;
2377 for (i = 0; i < id_refs_size; i++)
2378 if (!strcmp(id, id_refs[i][0]->id))
2381 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2385 id_refs = tmp_id_refs;
2387 for (i = 0; i < refs_size; i++) {
2390 if (strcmp(id, refs[i].id))
2393 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2401 if (ref_list_size > 0)
2402 ref_list[ref_list_size - 1]->next = 1;
2403 ref_list[ref_list_size] = &refs[i];
2405 /* XXX: The properties of the commit chains ensures that we can
2406 * safely modify the shared ref. The repo references will
2407 * always be similar for the same id. */
2408 ref_list[ref_list_size]->next = 0;
2413 id_refs[id_refs_size++] = ref_list;
2419 read_ref(char *id, int idlen, char *name, int namelen)
2424 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2425 /* Commits referenced by tags has "^{}" appended. */
2426 if (name[namelen - 1] != '}')
2429 while (namelen > 0 && name[namelen] != '^')
2433 namelen -= STRING_SIZE("refs/tags/");
2434 name += STRING_SIZE("refs/tags/");
2436 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2437 namelen -= STRING_SIZE("refs/heads/");
2438 name += STRING_SIZE("refs/heads/");
2440 } else if (!strcmp(name, "HEAD")) {
2444 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2448 ref = &refs[refs_size++];
2449 ref->name = malloc(namelen + 1);
2453 strncpy(ref->name, name, namelen);
2454 ref->name[namelen] = 0;
2456 string_copy(ref->id, id);
2464 const char *cmd_env = getenv("TIG_LS_REMOTE");
2465 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2467 return read_properties(popen(cmd, "r"), "\t", read_ref);
2471 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2473 if (!strcmp(name, "i18n.commitencoding"))
2474 string_copy(opt_encoding, value);
2480 load_repo_config(void)
2482 return read_properties(popen("git repo-config --list", "r"),
2483 "=", read_repo_config_option);
2487 read_properties(FILE *pipe, const char *separators,
2488 int (*read_property)(char *, int, char *, int))
2490 char buffer[BUFSIZ];
2497 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2502 name = chomp_string(name);
2503 namelen = strcspn(name, separators);
2505 if (name[namelen]) {
2507 value = chomp_string(name + namelen + 1);
2508 valuelen = strlen(value);
2515 state = read_property(name, namelen, value, valuelen);
2518 if (state != ERR && ferror(pipe))
2532 #define __NORETURN __attribute__((__noreturn__))
2537 static void __NORETURN
2540 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2546 static void __NORETURN
2547 die(const char *err, ...)
2553 va_start(args, err);
2554 fputs("tig: ", stderr);
2555 vfprintf(stderr, err, args);
2556 fputs("\n", stderr);
2563 main(int argc, char *argv[])
2566 enum request request;
2569 signal(SIGINT, quit);
2571 if (load_options() == ERR)
2572 die("Failed to load user config.");
2574 /* Load the repo config file so options can be overwritten from
2575 * the command line. */
2576 if (load_repo_config() == ERR)
2577 die("Failed to load repo config.");
2579 if (!parse_options(argc, argv))
2582 if (load_refs() == ERR)
2583 die("Failed to load refs.");
2585 /* Require a git repository unless when running in pager mode. */
2586 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2587 die("Not a git repository");
2589 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2590 view->cmd_env = getenv(view->cmd_env);
2592 request = opt_request;
2596 while (view_driver(display[current_view], request)) {
2600 foreach_view (view, i)
2603 /* Refresh, accept single keystroke of input */
2604 key = wgetch(status_win);
2605 request = get_request(key);
2607 /* Some low-level request handling. This keeps access to
2608 * status_win restricted. */
2612 /* Temporarily switch to line-oriented and echoed
2617 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2618 memcpy(opt_cmd, "git ", 4);
2619 opt_request = REQ_VIEW_PAGER;
2621 report("Prompt interrupted by loading view, "
2622 "press 'z' to stop loading views");
2623 request = REQ_SCREEN_UPDATE;
2630 case REQ_SCREEN_RESIZE:
2634 getmaxyx(stdscr, height, width);
2636 /* Resize the status view and let the view driver take
2637 * care of resizing the displayed views. */
2638 wresize(status_win, 1, width);
2639 mvwin(status_win, height - 1, 0);
2640 wrefresh(status_win);