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;
320 /* Returns the index of log or diff command or -1 to exit. */
322 parse_options(int argc, char *argv[])
326 for (i = 1; i < argc; i++) {
329 if (!strcmp(opt, "-l")) {
330 opt_request = REQ_VIEW_LOG;
334 if (!strcmp(opt, "-d")) {
335 opt_request = REQ_VIEW_DIFF;
339 if (!strncmp(opt, "-n", 2) ||
340 !strncmp(opt, "--line-number", 13)) {
346 } else if (opt[STRING_SIZE("--line-number")] == '=') {
347 num = opt + STRING_SIZE("--line-number=");
351 opt_num_interval = atoi(num);
353 opt_line_number = TRUE;
357 if (!strncmp(opt, "-b", 2) ||
358 !strncmp(opt, "--tab-size", 10)) {
364 } else if (opt[STRING_SIZE("--tab-size")] == '=') {
365 num = opt + STRING_SIZE("--tab-size=");
369 opt_tab_size = MIN(atoi(num), TABSIZE);
373 if (!strcmp(opt, "-v") ||
374 !strcmp(opt, "--version")) {
375 printf("tig version %s\n", VERSION);
379 if (!strcmp(opt, "-h") ||
380 !strcmp(opt, "--help")) {
385 if (!strcmp(opt, "--")) {
390 if (!strcmp(opt, "log") ||
391 !strcmp(opt, "diff") ||
392 !strcmp(opt, "show")) {
393 opt_request = opt[0] == 'l'
394 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
398 if (opt[0] && opt[0] != '-')
401 die("unknown command '%s'", opt);
404 if (!isatty(STDIN_FILENO)) {
405 opt_request = REQ_VIEW_PAGER;
408 } else if (i < argc) {
411 if (opt_request == REQ_VIEW_MAIN)
412 /* XXX: This is vulnerable to the user overriding
413 * options required for the main view parser. */
414 string_copy(opt_cmd, "git log --stat --pretty=raw");
416 string_copy(opt_cmd, "git");
417 buf_size = strlen(opt_cmd);
419 while (buf_size < sizeof(opt_cmd) && i < argc) {
420 opt_cmd[buf_size++] = ' ';
421 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
424 if (buf_size >= sizeof(opt_cmd))
425 die("command too long");
427 opt_cmd[buf_size] = 0;
431 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
438 static struct int_map color_map[] = {
439 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
451 static struct int_map attr_map[] = {
452 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
463 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
464 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
465 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
466 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
467 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
468 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
469 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
470 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
471 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
472 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
473 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
474 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
475 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
476 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
477 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
478 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
479 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
480 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
481 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
482 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
483 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
484 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
485 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
486 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
487 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
488 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
489 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
490 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
491 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
492 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
493 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
494 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
495 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
496 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
497 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
498 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
499 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
500 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
504 * Line-oriented content detection.
508 #define LINE(type, line, fg, bg, attr) \
515 const char *name; /* Option name. */
516 int namelen; /* Size of option name. */
517 const char *line; /* The start of line to match. */
518 int linelen; /* Size of string to match. */
519 int fg, bg, attr; /* Color and text attributes for the lines. */
522 static struct line_info line_info[] = {
523 #define LINE(type, line, fg, bg, attr) \
524 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
529 static enum line_type
530 get_line_type(char *line)
532 int linelen = strlen(line);
535 for (type = 0; type < ARRAY_SIZE(line_info); type++)
536 /* Case insensitive search matches Signed-off-by lines better. */
537 if (linelen >= line_info[type].linelen &&
538 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
545 get_line_attr(enum line_type type)
547 assert(type < ARRAY_SIZE(line_info));
548 return COLOR_PAIR(type) | line_info[type].attr;
551 static struct line_info *
552 get_line_info(char *name, int namelen)
557 /* Diff-Header -> DIFF_HEADER */
558 for (i = 0; i < namelen; i++) {
561 else if (name[i] == '.')
565 for (type = 0; type < ARRAY_SIZE(line_info); type++)
566 if (namelen == line_info[type].namelen &&
567 !strncasecmp(line_info[type].name, name, namelen))
568 return &line_info[type];
576 int default_bg = COLOR_BLACK;
577 int default_fg = COLOR_WHITE;
582 if (use_default_colors() != ERR) {
587 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
588 struct line_info *info = &line_info[type];
589 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
590 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
592 init_pair(type, fg, bg);
598 void *data; /* User data */
603 * User config file handling.
606 #define set_color(color, name, namelen) \
607 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
609 #define set_attribute(attr, name, namelen) \
610 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
612 static int config_lineno;
613 static bool config_errors;
614 static char *config_msg;
617 set_option(char *opt, int optlen, char *value, int valuelen)
619 /* Reads: "color" object fgcolor bgcolor [attr] */
620 if (!strcmp(opt, "color")) {
621 struct line_info *info;
623 value = chomp_string(value);
624 valuelen = strcspn(value, " \t");
625 info = get_line_info(value, valuelen);
627 config_msg = "Unknown color name";
631 value = chomp_string(value + valuelen);
632 valuelen = strcspn(value, " \t");
633 if (set_color(&info->fg, value, valuelen) == ERR) {
634 config_msg = "Unknown color";
638 value = chomp_string(value + valuelen);
639 valuelen = strcspn(value, " \t");
640 if (set_color(&info->bg, value, valuelen) == ERR) {
641 config_msg = "Unknown color";
645 value = chomp_string(value + valuelen);
647 set_attribute(&info->attr, value, strlen(value)) == ERR) {
648 config_msg = "Unknown attribute";
659 read_option(char *opt, int optlen, char *value, int valuelen)
662 config_msg = "Internal error";
664 optlen = strcspn(opt, "#;");
666 /* The whole line is a commend or empty. */
669 } else if (opt[optlen] != 0) {
670 /* Part of the option name is a comment, so the value part
671 * should be ignored. */
673 opt[optlen] = value[valuelen] = 0;
675 /* Else look for comment endings in the value. */
676 valuelen = strcspn(value, "#;");
680 if (set_option(opt, optlen, value, valuelen) == ERR) {
681 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
682 config_lineno, optlen, opt, config_msg);
683 config_errors = TRUE;
686 /* Always keep going if errors are encountered. */
693 char *home = getenv("HOME");
698 config_errors = FALSE;
700 if (!home || !string_format(buf, "%s/.tigrc", home))
703 /* It's ok that the file doesn't exist. */
704 file = fopen(buf, "r");
708 if (read_properties(file, " \t", read_option) == ERR ||
709 config_errors == TRUE)
710 fprintf(stderr, "Errors while loading %s.\n", buf);
723 /* The display array of active views and the index of the current view. */
724 static struct view *display[2];
725 static unsigned int current_view;
727 #define foreach_view(view, i) \
728 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
730 #define displayed_views() (display[1] != NULL ? 2 : 1)
732 /* Current head and commit ID */
733 static char ref_commit[SIZEOF_REF] = "HEAD";
734 static char ref_head[SIZEOF_REF] = "HEAD";
737 const char *name; /* View name */
738 const char *cmd_fmt; /* Default command line format */
739 const char *cmd_env; /* Command line set via environment */
740 const char *id; /* Points to either of ref_{head,commit} */
742 struct view_ops *ops; /* View operations */
744 char cmd[SIZEOF_CMD]; /* Command buffer */
745 char ref[SIZEOF_REF]; /* Hovered commit reference */
746 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
748 int height, width; /* The width and height of the main window */
749 WINDOW *win; /* The main window */
750 WINDOW *title; /* The title window living below the main window */
753 unsigned long offset; /* Offset of the window top */
754 unsigned long lineno; /* Current line number */
756 /* If non-NULL, points to the view that opened this view. If this view
757 * is closed tig will switch back to the parent view. */
761 unsigned long lines; /* Total number of lines */
762 struct line *line; /* Line index */
763 unsigned long line_size;/* Total number of allocated lines */
764 unsigned int digits; /* Number of digits in the lines member. */
772 /* What type of content being displayed. Used in the title bar. */
774 /* Draw one line; @lineno must be < view->height. */
775 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
776 /* Read one line; updates view->line. */
777 bool (*read)(struct view *view, char *data);
778 /* Depending on view, change display based on current line. */
779 bool (*enter)(struct view *view, struct line *line);
782 static struct view_ops pager_ops;
783 static struct view_ops main_ops;
785 #define VIEW_STR(name, cmd, env, ref, ops) \
786 { name, cmd, #env, ref, ops }
788 #define VIEW_(id, name, ops, ref) \
789 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
792 static struct view views[] = {
793 VIEW_(MAIN, "main", &main_ops, ref_head),
794 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
795 VIEW_(LOG, "log", &pager_ops, ref_head),
796 VIEW_(HELP, "help", &pager_ops, "static"),
797 VIEW_(PAGER, "pager", &pager_ops, "static"),
800 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
804 draw_view_line(struct view *view, unsigned int lineno)
806 if (view->offset + lineno >= view->lines)
809 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
813 redraw_view_from(struct view *view, int lineno)
815 assert(0 <= lineno && lineno < view->height);
817 for (; lineno < view->height; lineno++) {
818 if (!draw_view_line(view, lineno))
822 redrawwin(view->win);
827 redraw_view(struct view *view)
830 redraw_view_from(view, 0);
835 update_view_title(struct view *view)
837 if (view == display[current_view])
838 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
840 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
843 wmove(view->title, 0, 0);
846 wprintw(view->title, "[%s] %s", view->name, view->ref);
848 wprintw(view->title, "[%s]", view->name);
850 if (view->lines || view->pipe) {
851 unsigned int lines = view->lines
852 ? (view->lineno + 1) * 100 / view->lines
855 wprintw(view->title, " - %s %d of %d (%d%%)",
863 time_t secs = time(NULL) - view->start_time;
865 /* Three git seconds are a long time ... */
867 wprintw(view->title, " %lds", secs);
870 wmove(view->title, 0, view->width - 1);
871 wrefresh(view->title);
878 struct view *base = display[0];
879 struct view *view = display[1] ? display[1] : display[0];
881 /* Setup window dimensions */
883 getmaxyx(stdscr, base->height, base->width);
885 /* Make room for the status window. */
889 /* Horizontal split. */
890 view->width = base->width;
891 view->height = SCALE_SPLIT_VIEW(base->height);
892 base->height -= view->height;
894 /* Make room for the title bar. */
898 /* Make room for the title bar. */
903 foreach_view (view, i) {
905 view->win = newwin(view->height, 0, offset, 0);
907 die("Failed to create %s view", view->name);
909 scrollok(view->win, TRUE);
911 view->title = newwin(1, 0, offset + view->height, 0);
913 die("Failed to create title window");
916 wresize(view->win, view->height, view->width);
917 mvwin(view->win, offset, 0);
918 mvwin(view->title, offset + view->height, 0);
921 offset += view->height + 1;
931 foreach_view (view, i) {
933 update_view_title(view);
938 update_display_cursor(void)
940 struct view *view = display[current_view];
942 /* Move the cursor to the right-most column of the cursor line.
944 * XXX: This could turn out to be a bit expensive, but it ensures that
945 * the cursor does not jump around. */
947 wmove(view->win, view->lineno - view->offset, view->width - 1);
956 /* Scrolling backend */
958 do_scroll_view(struct view *view, int lines, bool redraw)
960 /* The rendering expects the new offset. */
961 view->offset += lines;
963 assert(0 <= view->offset && view->offset < view->lines);
966 /* Redraw the whole screen if scrolling is pointless. */
967 if (view->height < ABS(lines)) {
971 int line = lines > 0 ? view->height - lines : 0;
972 int end = line + ABS(lines);
974 wscrl(view->win, lines);
976 for (; line < end; line++) {
977 if (!draw_view_line(view, line))
982 /* Move current line into the view. */
983 if (view->lineno < view->offset) {
984 view->lineno = view->offset;
985 draw_view_line(view, 0);
987 } else if (view->lineno >= view->offset + view->height) {
988 if (view->lineno == view->offset + view->height) {
989 /* Clear the hidden line so it doesn't show if the view
991 wmove(view->win, view->height, 0);
992 wclrtoeol(view->win);
994 view->lineno = view->offset + view->height - 1;
995 draw_view_line(view, view->lineno - view->offset);
998 assert(view->offset <= view->lineno && view->lineno < view->lines);
1003 redrawwin(view->win);
1004 wrefresh(view->win);
1008 /* Scroll frontend */
1010 scroll_view(struct view *view, enum request request)
1015 case REQ_SCROLL_PAGE_DOWN:
1016 lines = view->height;
1017 case REQ_SCROLL_LINE_DOWN:
1018 if (view->offset + lines > view->lines)
1019 lines = view->lines - view->offset;
1021 if (lines == 0 || view->offset + view->height >= view->lines) {
1022 report("Cannot scroll beyond the last line");
1027 case REQ_SCROLL_PAGE_UP:
1028 lines = view->height;
1029 case REQ_SCROLL_LINE_UP:
1030 if (lines > view->offset)
1031 lines = view->offset;
1034 report("Cannot scroll beyond the first line");
1042 die("request %d not handled in switch", request);
1045 do_scroll_view(view, lines, TRUE);
1050 move_view(struct view *view, enum request request, bool redraw)
1055 case REQ_MOVE_FIRST_LINE:
1056 steps = -view->lineno;
1059 case REQ_MOVE_LAST_LINE:
1060 steps = view->lines - view->lineno - 1;
1063 case REQ_MOVE_PAGE_UP:
1064 steps = view->height > view->lineno
1065 ? -view->lineno : -view->height;
1068 case REQ_MOVE_PAGE_DOWN:
1069 steps = view->lineno + view->height >= view->lines
1070 ? view->lines - view->lineno - 1 : view->height;
1082 die("request %d not handled in switch", request);
1085 if (steps <= 0 && view->lineno == 0) {
1086 report("Cannot move beyond the first line");
1089 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1090 report("Cannot move beyond the last line");
1094 /* Move the current line */
1095 view->lineno += steps;
1096 assert(0 <= view->lineno && view->lineno < view->lines);
1098 /* Repaint the old "current" line if we be scrolling */
1099 if (ABS(steps) < view->height) {
1100 int prev_lineno = view->lineno - steps - view->offset;
1102 wmove(view->win, prev_lineno, 0);
1103 wclrtoeol(view->win);
1104 draw_view_line(view, prev_lineno);
1107 /* Check whether the view needs to be scrolled */
1108 if (view->lineno < view->offset ||
1109 view->lineno >= view->offset + view->height) {
1110 if (steps < 0 && -steps > view->offset) {
1111 steps = -view->offset;
1113 } else if (steps > 0) {
1114 if (view->lineno == view->lines - 1 &&
1115 view->lines > view->height) {
1116 steps = view->lines - view->offset - 1;
1117 if (steps >= view->height)
1118 steps -= view->height - 1;
1122 do_scroll_view(view, steps, redraw);
1126 /* Draw the current line */
1127 draw_view_line(view, view->lineno - view->offset);
1132 redrawwin(view->win);
1133 wrefresh(view->win);
1139 * Incremental updating
1143 end_update(struct view *view)
1147 set_nonblocking_input(FALSE);
1148 if (view->pipe == stdin)
1156 begin_update(struct view *view)
1158 const char *id = view->id;
1164 string_copy(view->cmd, opt_cmd);
1166 /* When running random commands, the view ref could have become
1167 * invalid so clear it. */
1170 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1172 if (!string_format(view->cmd, format, id, id, id, id, id))
1176 /* Special case for the pager view. */
1178 view->pipe = opt_pipe;
1181 view->pipe = popen(view->cmd, "r");
1187 set_nonblocking_input(TRUE);
1192 string_copy(view->vid, id);
1197 for (i = 0; i < view->lines; i++)
1198 if (view->line[i].data)
1199 free(view->line[i].data);
1205 view->start_time = time(NULL);
1210 static struct line *
1211 realloc_lines(struct view *view, size_t line_size)
1213 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1219 view->line_size = line_size;
1224 update_view(struct view *view)
1226 char buffer[BUFSIZ];
1228 /* The number of lines to read. If too low it will cause too much
1229 * redrawing (and possible flickering), if too high responsiveness
1231 unsigned long lines = view->height;
1232 int redraw_from = -1;
1237 /* Only redraw if lines are visible. */
1238 if (view->offset + view->height >= view->lines)
1239 redraw_from = view->lines - view->offset;
1241 if (!realloc_lines(view, view->lines + lines))
1244 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1245 int linelen = strlen(line);
1248 line[linelen - 1] = 0;
1250 if (!view->ops->read(view, line))
1260 lines = view->lines;
1261 for (digits = 0; lines; digits++)
1264 /* Keep the displayed view in sync with line number scaling. */
1265 if (digits != view->digits) {
1266 view->digits = digits;
1271 if (redraw_from >= 0) {
1272 /* If this is an incremental update, redraw the previous line
1273 * since for commits some members could have changed when
1274 * loading the main view. */
1275 if (redraw_from > 0)
1278 /* Incrementally draw avoids flickering. */
1279 redraw_view_from(view, redraw_from);
1282 /* Update the title _after_ the redraw so that if the redraw picks up a
1283 * commit reference in view->ref it'll be available here. */
1284 update_view_title(view);
1286 if (ferror(view->pipe)) {
1287 report("Failed to read: %s", strerror(errno));
1290 } else if (feof(view->pipe)) {
1298 report("Allocation failure");
1306 OPEN_DEFAULT = 0, /* Use default view switching. */
1307 OPEN_SPLIT = 1, /* Split current view. */
1308 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1309 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1313 open_view(struct view *prev, enum request request, enum open_flags flags)
1315 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1316 bool split = !!(flags & OPEN_SPLIT);
1317 bool reload = !!(flags & OPEN_RELOAD);
1318 struct view *view = VIEW(request);
1319 int nviews = displayed_views();
1320 struct view *base_view = display[0];
1322 if (view == prev && nviews == 1 && !reload) {
1323 report("Already in %s view", view->name);
1327 if ((reload || strcmp(view->vid, view->id)) &&
1328 !begin_update(view)) {
1329 report("Failed to load %s view", view->name);
1338 /* Maximize the current view. */
1339 memset(display, 0, sizeof(display));
1341 display[current_view] = view;
1344 /* Resize the view when switching between split- and full-screen,
1345 * or when switching between two different full-screen views. */
1346 if (nviews != displayed_views() ||
1347 (nviews == 1 && base_view != display[0]))
1350 if (split && prev->lineno - prev->offset >= prev->height) {
1351 /* Take the title line into account. */
1352 int lines = prev->lineno - prev->offset - prev->height + 1;
1354 /* Scroll the view that was split if the current line is
1355 * outside the new limited view. */
1356 do_scroll_view(prev, lines, TRUE);
1359 if (prev && view != prev) {
1360 if (split && !backgrounded) {
1361 /* "Blur" the previous view. */
1362 update_view_title(prev);
1365 view->parent = prev;
1368 if (view == VIEW(REQ_VIEW_HELP))
1371 if (view->pipe && view->lines == 0) {
1372 /* Clear the old view and let the incremental updating refill
1381 /* If the view is backgrounded the above calls to report()
1382 * won't redraw the view title. */
1384 update_view_title(view);
1389 * User request switch noodle
1393 view_driver(struct view *view, enum request request)
1400 case REQ_MOVE_PAGE_UP:
1401 case REQ_MOVE_PAGE_DOWN:
1402 case REQ_MOVE_FIRST_LINE:
1403 case REQ_MOVE_LAST_LINE:
1404 move_view(view, request, TRUE);
1407 case REQ_SCROLL_LINE_DOWN:
1408 case REQ_SCROLL_LINE_UP:
1409 case REQ_SCROLL_PAGE_DOWN:
1410 case REQ_SCROLL_PAGE_UP:
1411 scroll_view(view, request);
1418 case REQ_VIEW_PAGER:
1419 open_view(view, request, OPEN_DEFAULT);
1424 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1426 if (view == VIEW(REQ_VIEW_DIFF) &&
1427 view->parent == VIEW(REQ_VIEW_MAIN)) {
1428 bool redraw = display[1] == view;
1430 view = view->parent;
1431 move_view(view, request, redraw);
1433 update_view_title(view);
1435 move_view(view, request, TRUE);
1442 report("Nothing to enter");
1445 return view->ops->enter(view, &view->line[view->lineno]);
1449 int nviews = displayed_views();
1450 int next_view = (current_view + 1) % nviews;
1452 if (next_view == current_view) {
1453 report("Only one view is displayed");
1457 current_view = next_view;
1458 /* Blur out the title of the previous view. */
1459 update_view_title(view);
1463 case REQ_TOGGLE_LINENO:
1464 opt_line_number = !opt_line_number;
1469 /* Always reload^Wrerun commands from the prompt. */
1470 open_view(view, opt_request, OPEN_RELOAD);
1473 case REQ_STOP_LOADING:
1474 for (i = 0; i < ARRAY_SIZE(views); i++) {
1477 report("Stopped loading the %s view", view->name),
1482 case REQ_SHOW_VERSION:
1483 report("%s (built %s)", VERSION, __DATE__);
1486 case REQ_SCREEN_RESIZE:
1489 case REQ_SCREEN_REDRAW:
1493 case REQ_SCREEN_UPDATE:
1497 case REQ_VIEW_CLOSE:
1498 /* XXX: Mark closed views by letting view->parent point to the
1499 * view itself. Parents to closed view should never be
1502 view->parent->parent != view->parent) {
1503 memset(display, 0, sizeof(display));
1505 display[current_view] = view->parent;
1506 view->parent = view;
1516 /* An unknown key will show most commonly used commands. */
1517 report("Unknown key, press 'h' for help");
1530 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1532 char *text = line->data;
1533 enum line_type type = line->type;
1534 int textlen = strlen(text);
1537 wmove(view->win, lineno, 0);
1539 if (view->offset + lineno == view->lineno) {
1540 if (type == LINE_COMMIT) {
1541 string_copy(view->ref, text + 7);
1542 string_copy(ref_commit, view->ref);
1546 wchgat(view->win, -1, 0, type, NULL);
1549 attr = get_line_attr(type);
1550 wattrset(view->win, attr);
1552 if (opt_line_number || opt_tab_size < TABSIZE) {
1553 static char spaces[] = " ";
1554 int col_offset = 0, col = 0;
1556 if (opt_line_number) {
1557 unsigned long real_lineno = view->offset + lineno + 1;
1559 if (real_lineno == 1 ||
1560 (real_lineno % opt_num_interval) == 0) {
1561 wprintw(view->win, "%.*d", view->digits, real_lineno);
1564 waddnstr(view->win, spaces,
1565 MIN(view->digits, STRING_SIZE(spaces)));
1567 waddstr(view->win, ": ");
1568 col_offset = view->digits + 2;
1571 while (text && col_offset + col < view->width) {
1572 int cols_max = view->width - col_offset - col;
1576 if (*text == '\t') {
1578 assert(sizeof(spaces) > TABSIZE);
1580 cols = opt_tab_size - (col % opt_tab_size);
1583 text = strchr(text, '\t');
1584 cols = line ? text - pos : strlen(pos);
1587 waddnstr(view->win, pos, MIN(cols, cols_max));
1592 int col = 0, pos = 0;
1594 for (; pos < textlen && col < view->width; pos++, col++)
1595 if (text[pos] == '\t')
1596 col += TABSIZE - (col % TABSIZE) - 1;
1598 waddnstr(view->win, text, pos);
1605 add_pager_refs(struct view *view, struct line *line)
1608 char *data = line->data;
1610 int bufpos = 0, refpos = 0;
1611 const char *sep = "Refs: ";
1613 assert(line->type == LINE_COMMIT);
1615 refs = get_refs(data + STRING_SIZE("commit "));
1620 struct ref *ref = refs[refpos];
1621 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1623 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1626 } while (refs[refpos++]->next);
1628 if (!realloc_lines(view, view->line_size + 1))
1631 line = &view->line[view->lines];
1632 line->data = strdup(buf);
1636 line->type = LINE_PP_REFS;
1641 pager_read(struct view *view, char *data)
1643 struct line *line = &view->line[view->lines];
1645 line->data = strdup(data);
1649 line->type = get_line_type(line->data);
1652 if (line->type == LINE_COMMIT &&
1653 (view == VIEW(REQ_VIEW_DIFF) ||
1654 view == VIEW(REQ_VIEW_LOG)))
1655 add_pager_refs(view, line);
1661 pager_enter(struct view *view, struct line *line)
1665 if (line->type == LINE_COMMIT &&
1666 (view == VIEW(REQ_VIEW_LOG) ||
1667 view == VIEW(REQ_VIEW_PAGER))) {
1668 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1672 /* Always scroll the view even if it was split. That way
1673 * you can use Enter to scroll through the log view and
1674 * split open each commit diff. */
1675 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1677 /* FIXME: A minor workaround. Scrolling the view will call report("")
1678 * but if we are scrolling a non-current view this won't properly
1679 * update the view title. */
1681 update_view_title(view);
1686 static struct view_ops pager_ops = {
1699 char id[41]; /* SHA1 ID. */
1700 char title[75]; /* The first line of the commit message. */
1701 char author[75]; /* The author of the commit. */
1702 struct tm time; /* Date from the author ident. */
1703 struct ref **refs; /* Repository references; tags & branch heads. */
1707 main_draw(struct view *view, struct line *line, unsigned int lineno)
1709 char buf[DATE_COLS + 1];
1710 struct commit *commit = line->data;
1711 enum line_type type;
1717 if (!*commit->author)
1720 wmove(view->win, lineno, col);
1722 if (view->offset + lineno == view->lineno) {
1723 string_copy(view->ref, commit->id);
1724 string_copy(ref_commit, view->ref);
1726 wattrset(view->win, get_line_attr(type));
1727 wchgat(view->win, -1, 0, type, NULL);
1730 type = LINE_MAIN_COMMIT;
1731 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1734 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1735 waddnstr(view->win, buf, timelen);
1736 waddstr(view->win, " ");
1739 wmove(view->win, lineno, col);
1740 if (type != LINE_CURSOR)
1741 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1744 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1746 authorlen = strlen(commit->author);
1747 if (authorlen > AUTHOR_COLS - 2) {
1748 authorlen = AUTHOR_COLS - 2;
1754 waddnstr(view->win, commit->author, authorlen);
1755 if (type != LINE_CURSOR)
1756 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1757 waddch(view->win, '~');
1759 waddstr(view->win, commit->author);
1763 if (type != LINE_CURSOR)
1764 wattrset(view->win, A_NORMAL);
1766 mvwaddch(view->win, lineno, col, ACS_LTEE);
1767 wmove(view->win, lineno, col + 2);
1774 if (type == LINE_CURSOR)
1776 else if (commit->refs[i]->tag)
1777 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1779 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1780 waddstr(view->win, "[");
1781 waddstr(view->win, commit->refs[i]->name);
1782 waddstr(view->win, "]");
1783 if (type != LINE_CURSOR)
1784 wattrset(view->win, A_NORMAL);
1785 waddstr(view->win, " ");
1786 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1787 } while (commit->refs[i++]->next);
1790 if (type != LINE_CURSOR)
1791 wattrset(view->win, get_line_attr(type));
1794 int titlelen = strlen(commit->title);
1796 if (col + titlelen > view->width)
1797 titlelen = view->width - col;
1799 waddnstr(view->win, commit->title, titlelen);
1805 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1807 main_read(struct view *view, char *line)
1809 enum line_type type = get_line_type(line);
1810 struct commit *commit = view->lines
1811 ? view->line[view->lines - 1].data : NULL;
1815 commit = calloc(1, sizeof(struct commit));
1819 line += STRING_SIZE("commit ");
1821 view->line[view->lines++].data = commit;
1822 string_copy(commit->id, line);
1823 commit->refs = get_refs(commit->id);
1828 char *ident = line + STRING_SIZE("author ");
1829 char *end = strchr(ident, '<');
1835 for (; end > ident && isspace(end[-1]); end--) ;
1839 string_copy(commit->author, ident);
1841 /* Parse epoch and timezone */
1843 char *secs = strchr(end + 1, '>');
1847 if (!secs || secs[1] != ' ')
1851 time = (time_t) atol(secs);
1852 zone = strchr(secs, ' ');
1853 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1857 tz = ('0' - zone[1]) * 60 * 60 * 10;
1858 tz += ('0' - zone[2]) * 60 * 60;
1859 tz += ('0' - zone[3]) * 60;
1860 tz += ('0' - zone[4]) * 60;
1867 gmtime_r(&time, &commit->time);
1875 /* Fill in the commit title if it has not already been set. */
1876 if (commit->title[0])
1879 /* Require titles to start with a non-space character at the
1880 * offset used by git log. */
1881 /* FIXME: More gracefull handling of titles; append "..." to
1882 * shortened titles, etc. */
1883 if (strncmp(line, " ", 4) ||
1887 string_copy(commit->title, line + 4);
1894 main_enter(struct view *view, struct line *line)
1896 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
1898 open_view(view, REQ_VIEW_DIFF, flags);
1902 static struct view_ops main_ops = {
1919 static struct keymap keymap[] = {
1920 /* View switching */
1921 { 'm', REQ_VIEW_MAIN },
1922 { 'd', REQ_VIEW_DIFF },
1923 { 'l', REQ_VIEW_LOG },
1924 { 'p', REQ_VIEW_PAGER },
1925 { 'h', REQ_VIEW_HELP },
1926 { '?', REQ_VIEW_HELP },
1928 /* View manipulation */
1929 { 'q', REQ_VIEW_CLOSE },
1930 { KEY_TAB, REQ_VIEW_NEXT },
1931 { KEY_RETURN, REQ_ENTER },
1932 { KEY_UP, REQ_PREVIOUS },
1933 { KEY_DOWN, REQ_NEXT },
1935 /* Cursor navigation */
1936 { 'k', REQ_MOVE_UP },
1937 { 'j', REQ_MOVE_DOWN },
1938 { KEY_HOME, REQ_MOVE_FIRST_LINE },
1939 { KEY_END, REQ_MOVE_LAST_LINE },
1940 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
1941 { ' ', REQ_MOVE_PAGE_DOWN },
1942 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
1943 { 'b', REQ_MOVE_PAGE_UP },
1944 { '-', REQ_MOVE_PAGE_UP },
1947 { KEY_IC, REQ_SCROLL_LINE_UP },
1948 { KEY_DC, REQ_SCROLL_LINE_DOWN },
1949 { 'w', REQ_SCROLL_PAGE_UP },
1950 { 's', REQ_SCROLL_PAGE_DOWN },
1954 { 'z', REQ_STOP_LOADING },
1955 { 'v', REQ_SHOW_VERSION },
1956 { 'r', REQ_SCREEN_REDRAW },
1957 { 'n', REQ_TOGGLE_LINENO },
1958 { ':', REQ_PROMPT },
1960 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
1961 { ERR, REQ_SCREEN_UPDATE },
1963 /* Use the ncurses SIGWINCH handler. */
1964 { KEY_RESIZE, REQ_SCREEN_RESIZE },
1968 get_request(int key)
1972 for (i = 0; i < ARRAY_SIZE(keymap); i++)
1973 if (keymap[i].alias == key)
1974 return keymap[i].request;
1976 return (enum request) key;
1984 static struct key key_table[] = {
1985 { "Enter", KEY_RETURN },
1987 { "Backspace", KEY_BACKSPACE },
1989 { "Escape", KEY_ESC },
1990 { "Left", KEY_LEFT },
1991 { "Right", KEY_RIGHT },
1993 { "Down", KEY_DOWN },
1994 { "Insert", KEY_IC },
1995 { "Delete", KEY_DC },
1996 { "Home", KEY_HOME },
1998 { "PageUp", KEY_PPAGE },
1999 { "PageDown", KEY_NPAGE },
2009 { "F10", KEY_F(10) },
2010 { "F11", KEY_F(11) },
2011 { "F12", KEY_F(12) },
2015 get_key(enum request request)
2017 static char buf[BUFSIZ];
2018 static char key_char[] = "'X'";
2025 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2029 if (keymap[i].request != request)
2032 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2033 if (key_table[key].value == keymap[i].alias)
2034 seq = key_table[key].name;
2037 keymap[i].alias < 127 &&
2038 isprint(keymap[i].alias)) {
2039 key_char[1] = (char) keymap[i].alias;
2046 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
2047 return "Too many keybindings!";
2054 static void load_help_page(void)
2057 struct view *view = VIEW(REQ_VIEW_HELP);
2058 int lines = ARRAY_SIZE(req_info) + 2;
2061 if (view->lines > 0)
2064 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2065 if (!req_info[i].request)
2068 view->line = calloc(lines, sizeof(*view->line));
2070 report("Allocation failure");
2074 pager_read(view, "Quick reference for tig keybindings:");
2076 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2079 if (!req_info[i].request) {
2080 pager_read(view, "");
2081 pager_read(view, req_info[i].help);
2085 key = get_key(req_info[i].request);
2086 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
2089 pager_read(view, buf);
2095 * Unicode / UTF-8 handling
2097 * NOTE: Much of the following code for dealing with unicode is derived from
2098 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2099 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2102 /* I've (over)annotated a lot of code snippets because I am not entirely
2103 * confident that the approach taken by this small UTF-8 interface is correct.
2107 unicode_width(unsigned long c)
2110 (c <= 0x115f /* Hangul Jamo */
2113 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2115 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2116 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2117 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2118 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2119 || (c >= 0xffe0 && c <= 0xffe6)
2120 || (c >= 0x20000 && c <= 0x2fffd)
2121 || (c >= 0x30000 && c <= 0x3fffd)))
2127 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2128 * Illegal bytes are set one. */
2129 static const unsigned char utf8_bytes[256] = {
2130 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,
2131 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,
2132 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,
2133 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,
2134 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,
2135 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,
2136 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,
2137 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,
2140 /* Decode UTF-8 multi-byte representation into a unicode character. */
2141 static inline unsigned long
2142 utf8_to_unicode(const char *string, size_t length)
2144 unsigned long unicode;
2148 unicode = string[0];
2151 unicode = (string[0] & 0x1f) << 6;
2152 unicode += (string[1] & 0x3f);
2155 unicode = (string[0] & 0x0f) << 12;
2156 unicode += ((string[1] & 0x3f) << 6);
2157 unicode += (string[2] & 0x3f);
2160 unicode = (string[0] & 0x0f) << 18;
2161 unicode += ((string[1] & 0x3f) << 12);
2162 unicode += ((string[2] & 0x3f) << 6);
2163 unicode += (string[3] & 0x3f);
2166 unicode = (string[0] & 0x0f) << 24;
2167 unicode += ((string[1] & 0x3f) << 18);
2168 unicode += ((string[2] & 0x3f) << 12);
2169 unicode += ((string[3] & 0x3f) << 6);
2170 unicode += (string[4] & 0x3f);
2173 unicode = (string[0] & 0x01) << 30;
2174 unicode += ((string[1] & 0x3f) << 24);
2175 unicode += ((string[2] & 0x3f) << 18);
2176 unicode += ((string[3] & 0x3f) << 12);
2177 unicode += ((string[4] & 0x3f) << 6);
2178 unicode += (string[5] & 0x3f);
2181 die("Invalid unicode length");
2184 /* Invalid characters could return the special 0xfffd value but NUL
2185 * should be just as good. */
2186 return unicode > 0xffff ? 0 : unicode;
2189 /* Calculates how much of string can be shown within the given maximum width
2190 * and sets trimmed parameter to non-zero value if all of string could not be
2193 * Additionally, adds to coloffset how many many columns to move to align with
2194 * the expected position. Takes into account how multi-byte and double-width
2195 * characters will effect the cursor position.
2197 * Returns the number of bytes to output from string to satisfy max_width. */
2199 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2201 const char *start = string;
2202 const char *end = strchr(string, '\0');
2208 while (string < end) {
2209 int c = *(unsigned char *) string;
2210 unsigned char bytes = utf8_bytes[c];
2212 unsigned long unicode;
2214 if (string + bytes > end)
2217 /* Change representation to figure out whether
2218 * it is a single- or double-width character. */
2220 unicode = utf8_to_unicode(string, bytes);
2221 /* FIXME: Graceful handling of invalid unicode character. */
2225 ucwidth = unicode_width(unicode);
2227 if (width > max_width) {
2232 /* The column offset collects the differences between the
2233 * number of bytes encoding a character and the number of
2234 * columns will be used for rendering said character.
2236 * So if some character A is encoded in 2 bytes, but will be
2237 * represented on the screen using only 1 byte this will and up
2238 * adding 1 to the multi-byte column offset.
2240 * Assumes that no double-width character can be encoding in
2241 * less than two bytes. */
2242 if (bytes > ucwidth)
2243 mbwidth += bytes - ucwidth;
2248 *coloffset += mbwidth;
2250 return string - start;
2258 /* Whether or not the curses interface has been initialized. */
2259 static bool cursed = FALSE;
2261 /* The status window is used for polling keystrokes. */
2262 static WINDOW *status_win;
2264 /* Update status and title window. */
2266 report(const char *msg, ...)
2268 static bool empty = TRUE;
2269 struct view *view = display[current_view];
2271 if (!empty || *msg) {
2274 va_start(args, msg);
2277 wmove(status_win, 0, 0);
2279 vwprintw(status_win, msg, args);
2284 wrefresh(status_win);
2289 update_view_title(view);
2290 update_display_cursor();
2293 /* Controls when nodelay should be in effect when polling user input. */
2295 set_nonblocking_input(bool loading)
2297 static unsigned int loading_views;
2299 if ((loading == FALSE && loading_views-- == 1) ||
2300 (loading == TRUE && loading_views++ == 0))
2301 nodelay(status_win, loading);
2309 /* Initialize the curses library */
2310 if (isatty(STDIN_FILENO)) {
2311 cursed = !!initscr();
2313 /* Leave stdin and stdout alone when acting as a pager. */
2314 FILE *io = fopen("/dev/tty", "r+");
2316 cursed = !!newterm(NULL, io, io);
2320 die("Failed to initialize curses");
2322 nonl(); /* Tell curses not to do NL->CR/NL on output */
2323 cbreak(); /* Take input chars one at a time, no wait for \n */
2324 noecho(); /* Don't echo input */
2325 leaveok(stdscr, TRUE);
2330 getmaxyx(stdscr, y, x);
2331 status_win = newwin(1, 0, y - 1, 0);
2333 die("Failed to create status window");
2335 /* Enable keyboard mapping */
2336 keypad(status_win, TRUE);
2337 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2342 * Repository references
2345 static struct ref *refs;
2346 static size_t refs_size;
2348 /* Id <-> ref store */
2349 static struct ref ***id_refs;
2350 static size_t id_refs_size;
2352 static struct ref **
2355 struct ref ***tmp_id_refs;
2356 struct ref **ref_list = NULL;
2357 size_t ref_list_size = 0;
2360 for (i = 0; i < id_refs_size; i++)
2361 if (!strcmp(id, id_refs[i][0]->id))
2364 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2368 id_refs = tmp_id_refs;
2370 for (i = 0; i < refs_size; i++) {
2373 if (strcmp(id, refs[i].id))
2376 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2384 if (ref_list_size > 0)
2385 ref_list[ref_list_size - 1]->next = 1;
2386 ref_list[ref_list_size] = &refs[i];
2388 /* XXX: The properties of the commit chains ensures that we can
2389 * safely modify the shared ref. The repo references will
2390 * always be similar for the same id. */
2391 ref_list[ref_list_size]->next = 0;
2396 id_refs[id_refs_size++] = ref_list;
2402 read_ref(char *id, int idlen, char *name, int namelen)
2406 bool tag_commit = FALSE;
2408 /* Commits referenced by tags has "^{}" appended. */
2409 if (name[namelen - 1] == '}') {
2410 while (namelen > 0 && name[namelen] != '^')
2417 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2420 name += STRING_SIZE("refs/tags/");
2423 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2424 name += STRING_SIZE("refs/heads/");
2426 } else if (!strcmp(name, "HEAD")) {
2430 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2434 ref = &refs[refs_size++];
2435 ref->name = strdup(name);
2440 string_copy(ref->id, id);
2448 const char *cmd_env = getenv("TIG_LS_REMOTE");
2449 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2451 return read_properties(popen(cmd, "r"), "\t", read_ref);
2455 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2457 if (!strcmp(name, "i18n.commitencoding")) {
2458 string_copy(opt_encoding, value);
2465 load_repo_config(void)
2467 return read_properties(popen("git repo-config --list", "r"),
2468 "=", read_repo_config_option);
2472 read_properties(FILE *pipe, const char *separators,
2473 int (*read_property)(char *, int, char *, int))
2475 char buffer[BUFSIZ];
2482 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2487 name = chomp_string(name);
2488 namelen = strcspn(name, separators);
2490 if (name[namelen]) {
2492 value = chomp_string(name + namelen + 1);
2493 valuelen = strlen(value);
2500 state = read_property(name, namelen, value, valuelen);
2503 if (state != ERR && ferror(pipe))
2517 #define __NORETURN __attribute__((__noreturn__))
2522 static void __NORETURN
2525 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2531 static void __NORETURN
2532 die(const char *err, ...)
2538 va_start(args, err);
2539 fputs("tig: ", stderr);
2540 vfprintf(stderr, err, args);
2541 fputs("\n", stderr);
2548 main(int argc, char *argv[])
2551 enum request request;
2554 signal(SIGINT, quit);
2556 if (load_options() == ERR)
2557 die("Failed to load user config.");
2559 /* Load the repo config file so options can be overwritten from
2560 * the command line. */
2561 if (load_repo_config() == ERR)
2562 die("Failed to load repo config.");
2564 if (!parse_options(argc, argv))
2567 if (load_refs() == ERR)
2568 die("Failed to load refs.");
2570 /* Require a git repository unless when running in pager mode. */
2571 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2572 die("Not a git repository");
2574 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2575 view->cmd_env = getenv(view->cmd_env);
2577 request = opt_request;
2581 while (view_driver(display[current_view], request)) {
2585 foreach_view (view, i)
2588 /* Refresh, accept single keystroke of input */
2589 key = wgetch(status_win);
2590 request = get_request(key);
2592 /* Some low-level request handling. This keeps access to
2593 * status_win restricted. */
2597 /* Temporarily switch to line-oriented and echoed
2602 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2603 memcpy(opt_cmd, "git ", 4);
2604 opt_request = REQ_VIEW_PAGER;
2606 report("Prompt interrupted by loading view, "
2607 "press 'z' to stop loading views");
2608 request = REQ_SCREEN_UPDATE;
2615 case REQ_SCREEN_RESIZE:
2619 getmaxyx(stdscr, height, width);
2621 /* Resize the status view and let the view driver take
2622 * care of resizing the displayed views. */
2623 wresize(status_win, 1, width);
2624 mvwin(status_win, height - 1, 0);
2625 wrefresh(status_win);