1 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
15 #define VERSION "tig-0.3"
36 #define __NORETURN __attribute__((__noreturn__))
41 static void __NORETURN die(const char *err, ...);
42 static void report(const char *msg, ...);
43 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
44 static void set_nonblocking_input(bool loading);
45 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
46 static void load_help_page(void);
48 #define ABS(x) ((x) >= 0 ? (x) : -(x))
49 #define MIN(x, y) ((x) < (y) ? (x) : (y))
51 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
52 #define STRING_SIZE(x) (sizeof(x) - 1)
54 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
55 #define SIZEOF_CMD 1024 /* Size of command buffer. */
56 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
58 /* This color name can be used to refer to the default term colors. */
59 #define COLOR_DEFAULT (-1)
61 /* The format and size of the date column in the main view. */
62 #define DATE_FORMAT "%Y-%m-%d %H:%M"
63 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
65 #define AUTHOR_COLS 20
67 /* The default interval between line numbers. */
68 #define NUMBER_INTERVAL 1
72 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
74 #define TIG_LS_REMOTE \
75 "git ls-remote . 2>/dev/null"
77 #define TIG_DIFF_CMD \
78 "git show --patch-with-stat --find-copies-harder -B -C %s"
81 "git log --cc --stat -n100 %s"
83 #define TIG_MAIN_CMD \
84 "git log --topo-order --stat --pretty=raw %s"
86 /* XXX: Needs to be defined to the empty string. */
87 #define TIG_HELP_CMD ""
88 #define TIG_PAGER_CMD ""
90 /* Some ascii-shorthands fitted into the ncurses namespace. */
92 #define KEY_RETURN '\r'
97 char *name; /* Ref name; tag or head names are shortened. */
98 char id[41]; /* Commit SHA1 ID */
99 unsigned int tag:1; /* Is it a tag? */
100 unsigned int next:1; /* For ref lists: are there more refs? */
103 static struct ref **get_refs(char *id);
112 set_from_int_map(struct int_map *map, size_t map_size,
113 int *value, const char *name, int namelen)
118 for (i = 0; i < map_size; i++)
119 if (namelen == map[i].namelen &&
120 !strncasecmp(name, map[i].name, namelen)) {
121 *value = map[i].value;
134 string_ncopy(char *dst, const char *src, int dstlen)
136 strncpy(dst, src, dstlen - 1);
141 /* Shorthand for safely copying into a fixed buffer. */
142 #define string_copy(dst, src) \
143 string_ncopy(dst, src, sizeof(dst))
146 chomp_string(char *name)
150 while (isspace(*name))
153 namelen = strlen(name) - 1;
154 while (namelen > 0 && isspace(name[namelen]))
161 string_nformat(char *buf, size_t bufsize, int *bufpos, const char *fmt, ...)
164 int pos = bufpos ? *bufpos : 0;
167 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
173 return pos >= bufsize ? FALSE : TRUE;
176 #define string_format(buf, fmt, args...) \
177 string_nformat(buf, sizeof(buf), NULL, fmt, args)
179 #define string_format_from(buf, from, fmt, args...) \
180 string_nformat(buf, sizeof(buf), from, fmt, args)
184 * NOTE: The following is a slightly modified copy of the git project's shell
185 * quoting routines found in the quote.c file.
187 * Help to copy the thing properly quoted for the shell safety. any single
188 * quote is replaced with '\'', any exclamation point is replaced with '\!',
189 * and the whole thing is enclosed in a
192 * original sq_quote result
193 * name ==> name ==> 'name'
194 * a b ==> a b ==> 'a b'
195 * a'b ==> a'\''b ==> 'a'\''b'
196 * a!b ==> a'\!'b ==> 'a'\!'b'
200 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
204 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
207 while ((c = *src++)) {
208 if (c == '\'' || c == '!') {
228 /* XXX: Keep the view request first and in sync with views[]. */ \
229 REQ_GROUP("View switching") \
230 REQ_(VIEW_MAIN, "Show main view"), \
231 REQ_(VIEW_DIFF, "Show diff view"), \
232 REQ_(VIEW_LOG, "Show log view"), \
233 REQ_(VIEW_HELP, "Show help page"), \
234 REQ_(VIEW_PAGER, "Show pager view"), \
236 REQ_GROUP("View manipulation") \
237 REQ_(ENTER, "Enter current line and scroll"), \
238 REQ_(NEXT, "Move to next"), \
239 REQ_(PREVIOUS, "Move to previous"), \
240 REQ_(VIEW_NEXT, "Move focus to next view"), \
241 REQ_(VIEW_CLOSE, "Close the current view"), \
242 REQ_(QUIT, "Close all views and quit"), \
244 REQ_GROUP("Cursor navigation") \
245 REQ_(MOVE_UP, "Move cursor one line up"), \
246 REQ_(MOVE_DOWN, "Move cursor one line down"), \
247 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
248 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
249 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
250 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
252 REQ_GROUP("Scrolling") \
253 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
254 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
255 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
256 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
259 REQ_(PROMPT, "Bring up the prompt"), \
260 REQ_(SCREEN_UPDATE, "Update the screen"), \
261 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
262 REQ_(SCREEN_RESIZE, "Resize the screen"), \
263 REQ_(SHOW_VERSION, "Show version information"), \
264 REQ_(STOP_LOADING, "Stop all loading views"), \
265 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
266 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"),
269 /* User action requests. */
271 #define REQ_GROUP(help)
272 #define REQ_(req, help) REQ_##req
274 /* Offset all requests to avoid conflicts with ncurses getch values. */
275 REQ_OFFSET = KEY_MAX + 1,
282 struct request_info {
283 enum request request;
287 static struct request_info req_info[] = {
288 #define REQ_GROUP(help) { 0, (help) },
289 #define REQ_(req, help) { REQ_##req, (help) }
299 static const char usage[] =
300 VERSION " (" __DATE__ ")\n"
302 "Usage: tig [options]\n"
303 " or: tig [options] [--] [git log options]\n"
304 " or: tig [options] log [git log options]\n"
305 " or: tig [options] diff [git diff options]\n"
306 " or: tig [options] show [git show options]\n"
307 " or: tig [options] < [git command output]\n"
310 " -l Start up in log view\n"
311 " -d Start up in diff view\n"
312 " -n[I], --line-number[=I] Show line numbers with given interval\n"
313 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
314 " -- Mark end of tig options\n"
315 " -v, --version Show version and exit\n"
316 " -h, --help Show help message and exit\n";
318 /* Option and state variables. */
319 static bool opt_line_number = FALSE;
320 static bool opt_rev_graph = TRUE;
321 static int opt_num_interval = NUMBER_INTERVAL;
322 static int opt_tab_size = TABSIZE;
323 static enum request opt_request = REQ_VIEW_MAIN;
324 static char opt_cmd[SIZEOF_CMD] = "";
325 static char opt_encoding[20] = "";
326 static bool opt_utf8 = TRUE;
327 static FILE *opt_pipe = NULL;
335 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
345 int namelen = strlen(name);
349 if (strncmp(opt, name, namelen))
352 if (opt[namelen] == '=')
353 value = opt + namelen + 1;
356 if (!short_name || opt[1] != short_name)
361 va_start(args, type);
362 if (type == OPT_INT) {
363 number = va_arg(args, int *);
365 *number = atoi(value);
372 /* Returns the index of log or diff command or -1 to exit. */
374 parse_options(int argc, char *argv[])
378 for (i = 1; i < argc; i++) {
381 if (!strcmp(opt, "-l")) {
382 opt_request = REQ_VIEW_LOG;
386 if (!strcmp(opt, "-d")) {
387 opt_request = REQ_VIEW_DIFF;
391 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
392 opt_line_number = TRUE;
396 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
397 opt_tab_size = MIN(opt_tab_size, TABSIZE);
401 if (check_option(opt, 'v', "version", OPT_NONE)) {
402 printf("tig version %s\n", VERSION);
406 if (check_option(opt, 'h', "help", OPT_NONE)) {
411 if (!strcmp(opt, "--")) {
416 if (!strcmp(opt, "log") ||
417 !strcmp(opt, "diff") ||
418 !strcmp(opt, "show")) {
419 opt_request = opt[0] == 'l'
420 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
424 if (opt[0] && opt[0] != '-')
427 die("unknown option '%s'\n\n%s", opt, usage);
430 if (!isatty(STDIN_FILENO)) {
431 opt_request = REQ_VIEW_PAGER;
434 } else if (i < argc) {
437 if (opt_request == REQ_VIEW_MAIN)
438 /* XXX: This is vulnerable to the user overriding
439 * options required for the main view parser. */
440 string_copy(opt_cmd, "git log --stat --pretty=raw");
442 string_copy(opt_cmd, "git");
443 buf_size = strlen(opt_cmd);
445 while (buf_size < sizeof(opt_cmd) && i < argc) {
446 opt_cmd[buf_size++] = ' ';
447 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
450 if (buf_size >= sizeof(opt_cmd))
451 die("command too long");
453 opt_cmd[buf_size] = 0;
457 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
465 * Line-oriented content detection.
469 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
470 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
471 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
472 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
473 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
474 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
475 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
476 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
477 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
478 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
479 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
480 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
481 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
482 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
483 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
484 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
485 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
486 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
487 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
488 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
489 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
490 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
491 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
492 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
493 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
494 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
495 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
496 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
497 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
498 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
499 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
500 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
501 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
502 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
503 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
504 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
505 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
506 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
509 #define LINE(type, line, fg, bg, attr) \
516 const char *name; /* Option name. */
517 int namelen; /* Size of option name. */
518 const char *line; /* The start of line to match. */
519 int linelen; /* Size of string to match. */
520 int fg, bg, attr; /* Color and text attributes for the lines. */
523 static struct line_info line_info[] = {
524 #define LINE(type, line, fg, bg, attr) \
525 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
530 static enum line_type
531 get_line_type(char *line)
533 int linelen = strlen(line);
536 for (type = 0; type < ARRAY_SIZE(line_info); type++)
537 /* Case insensitive search matches Signed-off-by lines better. */
538 if (linelen >= line_info[type].linelen &&
539 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
546 get_line_attr(enum line_type type)
548 assert(type < ARRAY_SIZE(line_info));
549 return COLOR_PAIR(type) | line_info[type].attr;
552 static struct line_info *
553 get_line_info(char *name, int namelen)
558 /* Diff-Header -> DIFF_HEADER */
559 for (i = 0; i < namelen; i++) {
562 else if (name[i] == '.')
566 for (type = 0; type < ARRAY_SIZE(line_info); type++)
567 if (namelen == line_info[type].namelen &&
568 !strncasecmp(line_info[type].name, name, namelen))
569 return &line_info[type];
577 int default_bg = COLOR_BLACK;
578 int default_fg = COLOR_WHITE;
583 if (use_default_colors() != ERR) {
588 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
589 struct line_info *info = &line_info[type];
590 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
591 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
593 init_pair(type, fg, bg);
599 void *data; /* User data */
604 * User config file handling.
607 static struct int_map color_map[] = {
608 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
620 #define set_color(color, name, namelen) \
621 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
623 static struct int_map attr_map[] = {
624 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
634 #define set_attribute(attr, name, namelen) \
635 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
637 static int config_lineno;
638 static bool config_errors;
639 static char *config_msg;
643 * object fgcolor bgcolor [attr]
645 * from the value string. */
647 set_option_color(char *value, int valuelen)
649 struct line_info *info;
651 value = chomp_string(value);
652 valuelen = strcspn(value, " \t");
653 info = get_line_info(value, valuelen);
655 config_msg = "Unknown color name";
659 value = chomp_string(value + valuelen);
660 valuelen = strcspn(value, " \t");
661 if (set_color(&info->fg, value, valuelen) == ERR) {
662 config_msg = "Unknown color";
666 value = chomp_string(value + valuelen);
667 valuelen = strcspn(value, " \t");
668 if (set_color(&info->bg, value, valuelen) == ERR) {
669 config_msg = "Unknown color";
673 value = chomp_string(value + valuelen);
675 set_attribute(&info->attr, value, strlen(value)) == ERR) {
676 config_msg = "Unknown attribute";
684 set_option(char *opt, int optlen, char *value, int valuelen)
686 if (optlen == STRING_SIZE("color") &&
687 !strncmp(opt, "color", optlen))
688 return set_option_color(value, valuelen);
694 read_option(char *opt, int optlen, char *value, int valuelen)
697 config_msg = "Internal error";
699 optlen = strcspn(opt, "#;");
701 /* The whole line is a commend or empty. */
704 } else if (opt[optlen] != 0) {
705 /* Part of the option name is a comment, so the value part
706 * should be ignored. */
708 opt[optlen] = value[valuelen] = 0;
710 /* Else look for comment endings in the value. */
711 valuelen = strcspn(value, "#;");
715 if (set_option(opt, optlen, value, valuelen) == ERR) {
716 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
717 config_lineno, optlen, opt, config_msg);
718 config_errors = TRUE;
721 /* Always keep going if errors are encountered. */
728 char *home = getenv("HOME");
733 config_errors = FALSE;
735 if (!home || !string_format(buf, "%s/.tigrc", home))
738 /* It's ok that the file doesn't exist. */
739 file = fopen(buf, "r");
743 if (read_properties(file, " \t", read_option) == ERR ||
744 config_errors == TRUE)
745 fprintf(stderr, "Errors while loading %s.\n", buf);
758 /* The display array of active views and the index of the current view. */
759 static struct view *display[2];
760 static unsigned int current_view;
762 #define foreach_view(view, i) \
763 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
765 #define displayed_views() (display[1] != NULL ? 2 : 1)
767 /* Current head and commit ID */
768 static char ref_commit[SIZEOF_REF] = "HEAD";
769 static char ref_head[SIZEOF_REF] = "HEAD";
772 const char *name; /* View name */
773 const char *cmd_fmt; /* Default command line format */
774 const char *cmd_env; /* Command line set via environment */
775 const char *id; /* Points to either of ref_{head,commit} */
777 struct view_ops *ops; /* View operations */
779 char cmd[SIZEOF_CMD]; /* Command buffer */
780 char ref[SIZEOF_REF]; /* Hovered commit reference */
781 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
783 int height, width; /* The width and height of the main window */
784 WINDOW *win; /* The main window */
785 WINDOW *title; /* The title window living below the main window */
788 unsigned long offset; /* Offset of the window top */
789 unsigned long lineno; /* Current line number */
791 /* If non-NULL, points to the view that opened this view. If this view
792 * is closed tig will switch back to the parent view. */
796 unsigned long lines; /* Total number of lines */
797 struct line *line; /* Line index */
798 unsigned long line_size;/* Total number of allocated lines */
799 unsigned int digits; /* Number of digits in the lines member. */
807 /* What type of content being displayed. Used in the title bar. */
809 /* Draw one line; @lineno must be < view->height. */
810 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
811 /* Read one line; updates view->line. */
812 bool (*read)(struct view *view, char *data);
813 /* Depending on view, change display based on current line. */
814 bool (*enter)(struct view *view, struct line *line);
817 static struct view_ops pager_ops;
818 static struct view_ops main_ops;
820 #define VIEW_STR(name, cmd, env, ref, ops) \
821 { name, cmd, #env, ref, ops }
823 #define VIEW_(id, name, ops, ref) \
824 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
827 static struct view views[] = {
828 VIEW_(MAIN, "main", &main_ops, ref_head),
829 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
830 VIEW_(LOG, "log", &pager_ops, ref_head),
831 VIEW_(HELP, "help", &pager_ops, "static"),
832 VIEW_(PAGER, "pager", &pager_ops, "static"),
835 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
839 draw_view_line(struct view *view, unsigned int lineno)
841 if (view->offset + lineno >= view->lines)
844 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
848 redraw_view_from(struct view *view, int lineno)
850 assert(0 <= lineno && lineno < view->height);
852 for (; lineno < view->height; lineno++) {
853 if (!draw_view_line(view, lineno))
857 redrawwin(view->win);
862 redraw_view(struct view *view)
865 redraw_view_from(view, 0);
870 update_view_title(struct view *view)
872 if (view == display[current_view])
873 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
875 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
878 wmove(view->title, 0, 0);
881 wprintw(view->title, "[%s] %s", view->name, view->ref);
883 wprintw(view->title, "[%s]", view->name);
885 if (view->lines || view->pipe) {
886 unsigned int view_lines = view->offset + view->height;
887 unsigned int lines = view->lines
888 ? MIN(view_lines, view->lines) * 100 / view->lines
891 wprintw(view->title, " - %s %d of %d (%d%%)",
899 time_t secs = time(NULL) - view->start_time;
901 /* Three git seconds are a long time ... */
903 wprintw(view->title, " %lds", secs);
906 wmove(view->title, 0, view->width - 1);
907 wrefresh(view->title);
914 struct view *base = display[0];
915 struct view *view = display[1] ? display[1] : display[0];
917 /* Setup window dimensions */
919 getmaxyx(stdscr, base->height, base->width);
921 /* Make room for the status window. */
925 /* Horizontal split. */
926 view->width = base->width;
927 view->height = SCALE_SPLIT_VIEW(base->height);
928 base->height -= view->height;
930 /* Make room for the title bar. */
934 /* Make room for the title bar. */
939 foreach_view (view, i) {
941 view->win = newwin(view->height, 0, offset, 0);
943 die("Failed to create %s view", view->name);
945 scrollok(view->win, TRUE);
947 view->title = newwin(1, 0, offset + view->height, 0);
949 die("Failed to create title window");
952 wresize(view->win, view->height, view->width);
953 mvwin(view->win, offset, 0);
954 mvwin(view->title, offset + view->height, 0);
957 offset += view->height + 1;
967 foreach_view (view, i) {
969 update_view_title(view);
974 update_display_cursor(void)
976 struct view *view = display[current_view];
978 /* Move the cursor to the right-most column of the cursor line.
980 * XXX: This could turn out to be a bit expensive, but it ensures that
981 * the cursor does not jump around. */
983 wmove(view->win, view->lineno - view->offset, view->width - 1);
992 /* Scrolling backend */
994 do_scroll_view(struct view *view, int lines, bool redraw)
996 /* The rendering expects the new offset. */
997 view->offset += lines;
999 assert(0 <= view->offset && view->offset < view->lines);
1002 /* Redraw the whole screen if scrolling is pointless. */
1003 if (view->height < ABS(lines)) {
1007 int line = lines > 0 ? view->height - lines : 0;
1008 int end = line + ABS(lines);
1010 wscrl(view->win, lines);
1012 for (; line < end; line++) {
1013 if (!draw_view_line(view, line))
1018 /* Move current line into the view. */
1019 if (view->lineno < view->offset) {
1020 view->lineno = view->offset;
1021 draw_view_line(view, 0);
1023 } else if (view->lineno >= view->offset + view->height) {
1024 if (view->lineno == view->offset + view->height) {
1025 /* Clear the hidden line so it doesn't show if the view
1026 * is scrolled up. */
1027 wmove(view->win, view->height, 0);
1028 wclrtoeol(view->win);
1030 view->lineno = view->offset + view->height - 1;
1031 draw_view_line(view, view->lineno - view->offset);
1034 assert(view->offset <= view->lineno && view->lineno < view->lines);
1039 redrawwin(view->win);
1040 wrefresh(view->win);
1044 /* Scroll frontend */
1046 scroll_view(struct view *view, enum request request)
1051 case REQ_SCROLL_PAGE_DOWN:
1052 lines = view->height;
1053 case REQ_SCROLL_LINE_DOWN:
1054 if (view->offset + lines > view->lines)
1055 lines = view->lines - view->offset;
1057 if (lines == 0 || view->offset + view->height >= view->lines) {
1058 report("Cannot scroll beyond the last line");
1063 case REQ_SCROLL_PAGE_UP:
1064 lines = view->height;
1065 case REQ_SCROLL_LINE_UP:
1066 if (lines > view->offset)
1067 lines = view->offset;
1070 report("Cannot scroll beyond the first line");
1078 die("request %d not handled in switch", request);
1081 do_scroll_view(view, lines, TRUE);
1086 move_view(struct view *view, enum request request, bool redraw)
1091 case REQ_MOVE_FIRST_LINE:
1092 steps = -view->lineno;
1095 case REQ_MOVE_LAST_LINE:
1096 steps = view->lines - view->lineno - 1;
1099 case REQ_MOVE_PAGE_UP:
1100 steps = view->height > view->lineno
1101 ? -view->lineno : -view->height;
1104 case REQ_MOVE_PAGE_DOWN:
1105 steps = view->lineno + view->height >= view->lines
1106 ? view->lines - view->lineno - 1 : view->height;
1118 die("request %d not handled in switch", request);
1121 if (steps <= 0 && view->lineno == 0) {
1122 report("Cannot move beyond the first line");
1125 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1126 report("Cannot move beyond the last line");
1130 /* Move the current line */
1131 view->lineno += steps;
1132 assert(0 <= view->lineno && view->lineno < view->lines);
1134 /* Repaint the old "current" line if we be scrolling */
1135 if (ABS(steps) < view->height) {
1136 int prev_lineno = view->lineno - steps - view->offset;
1138 wmove(view->win, prev_lineno, 0);
1139 wclrtoeol(view->win);
1140 draw_view_line(view, prev_lineno);
1143 /* Check whether the view needs to be scrolled */
1144 if (view->lineno < view->offset ||
1145 view->lineno >= view->offset + view->height) {
1146 if (steps < 0 && -steps > view->offset) {
1147 steps = -view->offset;
1149 } else if (steps > 0) {
1150 if (view->lineno == view->lines - 1 &&
1151 view->lines > view->height) {
1152 steps = view->lines - view->offset - 1;
1153 if (steps >= view->height)
1154 steps -= view->height - 1;
1158 do_scroll_view(view, steps, redraw);
1162 /* Draw the current line */
1163 draw_view_line(view, view->lineno - view->offset);
1168 redrawwin(view->win);
1169 wrefresh(view->win);
1175 * Incremental updating
1179 end_update(struct view *view)
1183 set_nonblocking_input(FALSE);
1184 if (view->pipe == stdin)
1192 begin_update(struct view *view)
1194 const char *id = view->id;
1200 string_copy(view->cmd, opt_cmd);
1202 /* When running random commands, the view ref could have become
1203 * invalid so clear it. */
1206 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1208 if (!string_format(view->cmd, format, id, id, id, id, id))
1212 /* Special case for the pager view. */
1214 view->pipe = opt_pipe;
1217 view->pipe = popen(view->cmd, "r");
1223 set_nonblocking_input(TRUE);
1228 string_copy(view->vid, id);
1233 for (i = 0; i < view->lines; i++)
1234 if (view->line[i].data)
1235 free(view->line[i].data);
1241 view->start_time = time(NULL);
1246 static struct line *
1247 realloc_lines(struct view *view, size_t line_size)
1249 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1255 view->line_size = line_size;
1260 update_view(struct view *view)
1262 char buffer[BUFSIZ];
1264 /* The number of lines to read. If too low it will cause too much
1265 * redrawing (and possible flickering), if too high responsiveness
1267 unsigned long lines = view->height;
1268 int redraw_from = -1;
1273 /* Only redraw if lines are visible. */
1274 if (view->offset + view->height >= view->lines)
1275 redraw_from = view->lines - view->offset;
1277 if (!realloc_lines(view, view->lines + lines))
1280 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1281 int linelen = strlen(line);
1284 line[linelen - 1] = 0;
1286 if (!view->ops->read(view, line))
1296 lines = view->lines;
1297 for (digits = 0; lines; digits++)
1300 /* Keep the displayed view in sync with line number scaling. */
1301 if (digits != view->digits) {
1302 view->digits = digits;
1307 if (redraw_from >= 0) {
1308 /* If this is an incremental update, redraw the previous line
1309 * since for commits some members could have changed when
1310 * loading the main view. */
1311 if (redraw_from > 0)
1314 /* Incrementally draw avoids flickering. */
1315 redraw_view_from(view, redraw_from);
1318 /* Update the title _after_ the redraw so that if the redraw picks up a
1319 * commit reference in view->ref it'll be available here. */
1320 update_view_title(view);
1322 if (ferror(view->pipe)) {
1323 report("Failed to read: %s", strerror(errno));
1326 } else if (feof(view->pipe)) {
1334 report("Allocation failure");
1342 OPEN_DEFAULT = 0, /* Use default view switching. */
1343 OPEN_SPLIT = 1, /* Split current view. */
1344 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1345 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1349 open_view(struct view *prev, enum request request, enum open_flags flags)
1351 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1352 bool split = !!(flags & OPEN_SPLIT);
1353 bool reload = !!(flags & OPEN_RELOAD);
1354 struct view *view = VIEW(request);
1355 int nviews = displayed_views();
1356 struct view *base_view = display[0];
1358 if (view == prev && nviews == 1 && !reload) {
1359 report("Already in %s view", view->name);
1363 if (view == VIEW(REQ_VIEW_HELP)) {
1366 } else if ((reload || strcmp(view->vid, view->id)) &&
1367 !begin_update(view)) {
1368 report("Failed to load %s view", view->name);
1377 /* Maximize the current view. */
1378 memset(display, 0, sizeof(display));
1380 display[current_view] = view;
1383 /* Resize the view when switching between split- and full-screen,
1384 * or when switching between two different full-screen views. */
1385 if (nviews != displayed_views() ||
1386 (nviews == 1 && base_view != display[0]))
1389 if (split && prev->lineno - prev->offset >= prev->height) {
1390 /* Take the title line into account. */
1391 int lines = prev->lineno - prev->offset - prev->height + 1;
1393 /* Scroll the view that was split if the current line is
1394 * outside the new limited view. */
1395 do_scroll_view(prev, lines, TRUE);
1398 if (prev && view != prev) {
1399 if (split && !backgrounded) {
1400 /* "Blur" the previous view. */
1401 update_view_title(prev);
1404 view->parent = prev;
1407 if (view->pipe && view->lines == 0) {
1408 /* Clear the old view and let the incremental updating refill
1417 /* If the view is backgrounded the above calls to report()
1418 * won't redraw the view title. */
1420 update_view_title(view);
1425 * User request switch noodle
1429 view_driver(struct view *view, enum request request)
1436 case REQ_MOVE_PAGE_UP:
1437 case REQ_MOVE_PAGE_DOWN:
1438 case REQ_MOVE_FIRST_LINE:
1439 case REQ_MOVE_LAST_LINE:
1440 move_view(view, request, TRUE);
1443 case REQ_SCROLL_LINE_DOWN:
1444 case REQ_SCROLL_LINE_UP:
1445 case REQ_SCROLL_PAGE_DOWN:
1446 case REQ_SCROLL_PAGE_UP:
1447 scroll_view(view, request);
1454 case REQ_VIEW_PAGER:
1455 open_view(view, request, OPEN_DEFAULT);
1460 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1462 if (view == VIEW(REQ_VIEW_DIFF) &&
1463 view->parent == VIEW(REQ_VIEW_MAIN)) {
1464 bool redraw = display[1] == view;
1466 view = view->parent;
1467 move_view(view, request, redraw);
1469 update_view_title(view);
1471 move_view(view, request, TRUE);
1478 report("Nothing to enter");
1481 return view->ops->enter(view, &view->line[view->lineno]);
1485 int nviews = displayed_views();
1486 int next_view = (current_view + 1) % nviews;
1488 if (next_view == current_view) {
1489 report("Only one view is displayed");
1493 current_view = next_view;
1494 /* Blur out the title of the previous view. */
1495 update_view_title(view);
1499 case REQ_TOGGLE_LINENO:
1500 opt_line_number = !opt_line_number;
1504 case REQ_TOGGLE_REV_GRAPH:
1505 opt_rev_graph = !opt_rev_graph;
1510 /* Always reload^Wrerun commands from the prompt. */
1511 open_view(view, opt_request, OPEN_RELOAD);
1514 case REQ_STOP_LOADING:
1515 for (i = 0; i < ARRAY_SIZE(views); i++) {
1518 report("Stopped loading the %s view", view->name),
1523 case REQ_SHOW_VERSION:
1524 report("%s (built %s)", VERSION, __DATE__);
1527 case REQ_SCREEN_RESIZE:
1530 case REQ_SCREEN_REDRAW:
1534 case REQ_SCREEN_UPDATE:
1538 case REQ_VIEW_CLOSE:
1539 /* XXX: Mark closed views by letting view->parent point to the
1540 * view itself. Parents to closed view should never be
1543 view->parent->parent != view->parent) {
1544 memset(display, 0, sizeof(display));
1546 display[current_view] = view->parent;
1547 view->parent = view;
1557 /* An unknown key will show most commonly used commands. */
1558 report("Unknown key, press 'h' for help");
1571 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1573 char *text = line->data;
1574 enum line_type type = line->type;
1575 int textlen = strlen(text);
1578 wmove(view->win, lineno, 0);
1580 if (view->offset + lineno == view->lineno) {
1581 if (type == LINE_COMMIT) {
1582 string_copy(view->ref, text + 7);
1583 string_copy(ref_commit, view->ref);
1587 wchgat(view->win, -1, 0, type, NULL);
1590 attr = get_line_attr(type);
1591 wattrset(view->win, attr);
1593 if (opt_line_number || opt_tab_size < TABSIZE) {
1594 static char spaces[] = " ";
1595 int col_offset = 0, col = 0;
1597 if (opt_line_number) {
1598 unsigned long real_lineno = view->offset + lineno + 1;
1600 if (real_lineno == 1 ||
1601 (real_lineno % opt_num_interval) == 0) {
1602 wprintw(view->win, "%.*d", view->digits, real_lineno);
1605 waddnstr(view->win, spaces,
1606 MIN(view->digits, STRING_SIZE(spaces)));
1608 waddstr(view->win, ": ");
1609 col_offset = view->digits + 2;
1612 while (text && col_offset + col < view->width) {
1613 int cols_max = view->width - col_offset - col;
1617 if (*text == '\t') {
1619 assert(sizeof(spaces) > TABSIZE);
1621 cols = opt_tab_size - (col % opt_tab_size);
1624 text = strchr(text, '\t');
1625 cols = line ? text - pos : strlen(pos);
1628 waddnstr(view->win, pos, MIN(cols, cols_max));
1633 int col = 0, pos = 0;
1635 for (; pos < textlen && col < view->width; pos++, col++)
1636 if (text[pos] == '\t')
1637 col += TABSIZE - (col % TABSIZE) - 1;
1639 waddnstr(view->win, text, pos);
1646 add_pager_refs(struct view *view, struct line *line)
1649 char *data = line->data;
1651 int bufpos = 0, refpos = 0;
1652 const char *sep = "Refs: ";
1654 assert(line->type == LINE_COMMIT);
1656 refs = get_refs(data + STRING_SIZE("commit "));
1661 struct ref *ref = refs[refpos];
1662 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1664 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1667 } while (refs[refpos++]->next);
1669 if (!realloc_lines(view, view->line_size + 1))
1672 line = &view->line[view->lines];
1673 line->data = strdup(buf);
1677 line->type = LINE_PP_REFS;
1682 pager_read(struct view *view, char *data)
1684 struct line *line = &view->line[view->lines];
1686 line->data = strdup(data);
1690 line->type = get_line_type(line->data);
1693 if (line->type == LINE_COMMIT &&
1694 (view == VIEW(REQ_VIEW_DIFF) ||
1695 view == VIEW(REQ_VIEW_LOG)))
1696 add_pager_refs(view, line);
1702 pager_enter(struct view *view, struct line *line)
1706 if (line->type == LINE_COMMIT &&
1707 (view == VIEW(REQ_VIEW_LOG) ||
1708 view == VIEW(REQ_VIEW_PAGER))) {
1709 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1713 /* Always scroll the view even if it was split. That way
1714 * you can use Enter to scroll through the log view and
1715 * split open each commit diff. */
1716 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1718 /* FIXME: A minor workaround. Scrolling the view will call report("")
1719 * but if we are scrolling a non-current view this won't properly
1720 * update the view title. */
1722 update_view_title(view);
1727 static struct view_ops pager_ops = {
1740 char id[41]; /* SHA1 ID. */
1741 char title[75]; /* First line of the commit message. */
1742 char author[75]; /* Author of the commit. */
1743 struct tm time; /* Date from the author ident. */
1744 struct ref **refs; /* Repository references. */
1745 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
1746 size_t graph_size; /* The width of the graph array. */
1750 main_draw(struct view *view, struct line *line, unsigned int lineno)
1752 char buf[DATE_COLS + 1];
1753 struct commit *commit = line->data;
1754 enum line_type type;
1760 if (!*commit->author)
1763 wmove(view->win, lineno, col);
1765 if (view->offset + lineno == view->lineno) {
1766 string_copy(view->ref, commit->id);
1767 string_copy(ref_commit, view->ref);
1769 wattrset(view->win, get_line_attr(type));
1770 wchgat(view->win, -1, 0, type, NULL);
1773 type = LINE_MAIN_COMMIT;
1774 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1777 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1778 waddnstr(view->win, buf, timelen);
1779 waddstr(view->win, " ");
1782 wmove(view->win, lineno, col);
1783 if (type != LINE_CURSOR)
1784 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1787 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1789 authorlen = strlen(commit->author);
1790 if (authorlen > AUTHOR_COLS - 2) {
1791 authorlen = AUTHOR_COLS - 2;
1797 waddnstr(view->win, commit->author, authorlen);
1798 if (type != LINE_CURSOR)
1799 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1800 waddch(view->win, '~');
1802 waddstr(view->win, commit->author);
1806 if (type != LINE_CURSOR)
1807 wattrset(view->win, A_NORMAL);
1809 if (opt_rev_graph && commit->graph_size) {
1812 wmove(view->win, lineno, col);
1813 /* Using waddch() instead of waddnstr() ensures that
1814 * they'll be rendered correctly for the cursor line. */
1815 for (i = 0; i < commit->graph_size; i++)
1816 waddch(view->win, commit->graph[i]);
1818 col += commit->graph_size + 1;
1821 wmove(view->win, lineno, col);
1827 if (type == LINE_CURSOR)
1829 else if (commit->refs[i]->tag)
1830 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1832 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1833 waddstr(view->win, "[");
1834 waddstr(view->win, commit->refs[i]->name);
1835 waddstr(view->win, "]");
1836 if (type != LINE_CURSOR)
1837 wattrset(view->win, A_NORMAL);
1838 waddstr(view->win, " ");
1839 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1840 } while (commit->refs[i++]->next);
1843 if (type != LINE_CURSOR)
1844 wattrset(view->win, get_line_attr(type));
1847 int titlelen = strlen(commit->title);
1849 if (col + titlelen > view->width)
1850 titlelen = view->width - col;
1852 waddnstr(view->win, commit->title, titlelen);
1858 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1860 main_read(struct view *view, char *line)
1862 enum line_type type = get_line_type(line);
1863 struct commit *commit = view->lines
1864 ? view->line[view->lines - 1].data : NULL;
1868 commit = calloc(1, sizeof(struct commit));
1872 line += STRING_SIZE("commit ");
1874 view->line[view->lines++].data = commit;
1875 string_copy(commit->id, line);
1876 commit->refs = get_refs(commit->id);
1877 commit->graph[commit->graph_size++] = ACS_LTEE;
1882 char *ident = line + STRING_SIZE("author ");
1883 char *end = strchr(ident, '<');
1889 for (; end > ident && isspace(end[-1]); end--) ;
1893 string_copy(commit->author, ident);
1895 /* Parse epoch and timezone */
1897 char *secs = strchr(end + 1, '>');
1901 if (!secs || secs[1] != ' ')
1905 time = (time_t) atol(secs);
1906 zone = strchr(secs, ' ');
1907 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1911 tz = ('0' - zone[1]) * 60 * 60 * 10;
1912 tz += ('0' - zone[2]) * 60 * 60;
1913 tz += ('0' - zone[3]) * 60;
1914 tz += ('0' - zone[4]) * 60;
1921 gmtime_r(&time, &commit->time);
1929 /* Fill in the commit title if it has not already been set. */
1930 if (commit->title[0])
1933 /* Require titles to start with a non-space character at the
1934 * offset used by git log. */
1935 /* FIXME: More gracefull handling of titles; append "..." to
1936 * shortened titles, etc. */
1937 if (strncmp(line, " ", 4) ||
1941 string_copy(commit->title, line + 4);
1948 main_enter(struct view *view, struct line *line)
1950 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
1952 open_view(view, REQ_VIEW_DIFF, flags);
1956 static struct view_ops main_ops = {
1973 static struct keymap keymap[] = {
1974 /* View switching */
1975 { 'm', REQ_VIEW_MAIN },
1976 { 'd', REQ_VIEW_DIFF },
1977 { 'l', REQ_VIEW_LOG },
1978 { 'p', REQ_VIEW_PAGER },
1979 { 'h', REQ_VIEW_HELP },
1980 { '?', REQ_VIEW_HELP },
1982 /* View manipulation */
1983 { 'q', REQ_VIEW_CLOSE },
1984 { KEY_TAB, REQ_VIEW_NEXT },
1985 { KEY_RETURN, REQ_ENTER },
1986 { KEY_UP, REQ_PREVIOUS },
1987 { KEY_DOWN, REQ_NEXT },
1989 /* Cursor navigation */
1990 { 'k', REQ_MOVE_UP },
1991 { 'j', REQ_MOVE_DOWN },
1992 { KEY_HOME, REQ_MOVE_FIRST_LINE },
1993 { KEY_END, REQ_MOVE_LAST_LINE },
1994 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
1995 { ' ', REQ_MOVE_PAGE_DOWN },
1996 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
1997 { 'b', REQ_MOVE_PAGE_UP },
1998 { '-', REQ_MOVE_PAGE_UP },
2001 { KEY_IC, REQ_SCROLL_LINE_UP },
2002 { KEY_DC, REQ_SCROLL_LINE_DOWN },
2003 { 'w', REQ_SCROLL_PAGE_UP },
2004 { 's', REQ_SCROLL_PAGE_DOWN },
2008 { 'z', REQ_STOP_LOADING },
2009 { 'v', REQ_SHOW_VERSION },
2010 { 'r', REQ_SCREEN_REDRAW },
2011 { 'n', REQ_TOGGLE_LINENO },
2012 { 'g', REQ_TOGGLE_REV_GRAPH},
2013 { ':', REQ_PROMPT },
2015 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2016 { ERR, REQ_SCREEN_UPDATE },
2018 /* Use the ncurses SIGWINCH handler. */
2019 { KEY_RESIZE, REQ_SCREEN_RESIZE },
2023 get_request(int key)
2027 for (i = 0; i < ARRAY_SIZE(keymap); i++)
2028 if (keymap[i].alias == key)
2029 return keymap[i].request;
2031 return (enum request) key;
2039 static struct key key_table[] = {
2040 { "Enter", KEY_RETURN },
2042 { "Backspace", KEY_BACKSPACE },
2044 { "Escape", KEY_ESC },
2045 { "Left", KEY_LEFT },
2046 { "Right", KEY_RIGHT },
2048 { "Down", KEY_DOWN },
2049 { "Insert", KEY_IC },
2050 { "Delete", KEY_DC },
2051 { "Home", KEY_HOME },
2053 { "PageUp", KEY_PPAGE },
2054 { "PageDown", KEY_NPAGE },
2064 { "F10", KEY_F(10) },
2065 { "F11", KEY_F(11) },
2066 { "F12", KEY_F(12) },
2070 get_key(enum request request)
2072 static char buf[BUFSIZ];
2073 static char key_char[] = "'X'";
2080 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2084 if (keymap[i].request != request)
2087 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2088 if (key_table[key].value == keymap[i].alias)
2089 seq = key_table[key].name;
2092 keymap[i].alias < 127 &&
2093 isprint(keymap[i].alias)) {
2094 key_char[1] = (char) keymap[i].alias;
2101 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
2102 return "Too many keybindings!";
2109 static void load_help_page(void)
2112 struct view *view = VIEW(REQ_VIEW_HELP);
2113 int lines = ARRAY_SIZE(req_info) + 2;
2116 if (view->lines > 0)
2119 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2120 if (!req_info[i].request)
2123 view->line = calloc(lines, sizeof(*view->line));
2125 report("Allocation failure");
2129 pager_read(view, "Quick reference for tig keybindings:");
2131 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2134 if (!req_info[i].request) {
2135 pager_read(view, "");
2136 pager_read(view, req_info[i].help);
2140 key = get_key(req_info[i].request);
2141 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
2144 pager_read(view, buf);
2150 * Unicode / UTF-8 handling
2152 * NOTE: Much of the following code for dealing with unicode is derived from
2153 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2154 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2157 /* I've (over)annotated a lot of code snippets because I am not entirely
2158 * confident that the approach taken by this small UTF-8 interface is correct.
2162 unicode_width(unsigned long c)
2165 (c <= 0x115f /* Hangul Jamo */
2168 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2170 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2171 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2172 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2173 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2174 || (c >= 0xffe0 && c <= 0xffe6)
2175 || (c >= 0x20000 && c <= 0x2fffd)
2176 || (c >= 0x30000 && c <= 0x3fffd)))
2182 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2183 * Illegal bytes are set one. */
2184 static const unsigned char utf8_bytes[256] = {
2185 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,
2186 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,
2187 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,
2188 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,
2189 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,
2190 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,
2191 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,
2192 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,
2195 /* Decode UTF-8 multi-byte representation into a unicode character. */
2196 static inline unsigned long
2197 utf8_to_unicode(const char *string, size_t length)
2199 unsigned long unicode;
2203 unicode = string[0];
2206 unicode = (string[0] & 0x1f) << 6;
2207 unicode += (string[1] & 0x3f);
2210 unicode = (string[0] & 0x0f) << 12;
2211 unicode += ((string[1] & 0x3f) << 6);
2212 unicode += (string[2] & 0x3f);
2215 unicode = (string[0] & 0x0f) << 18;
2216 unicode += ((string[1] & 0x3f) << 12);
2217 unicode += ((string[2] & 0x3f) << 6);
2218 unicode += (string[3] & 0x3f);
2221 unicode = (string[0] & 0x0f) << 24;
2222 unicode += ((string[1] & 0x3f) << 18);
2223 unicode += ((string[2] & 0x3f) << 12);
2224 unicode += ((string[3] & 0x3f) << 6);
2225 unicode += (string[4] & 0x3f);
2228 unicode = (string[0] & 0x01) << 30;
2229 unicode += ((string[1] & 0x3f) << 24);
2230 unicode += ((string[2] & 0x3f) << 18);
2231 unicode += ((string[3] & 0x3f) << 12);
2232 unicode += ((string[4] & 0x3f) << 6);
2233 unicode += (string[5] & 0x3f);
2236 die("Invalid unicode length");
2239 /* Invalid characters could return the special 0xfffd value but NUL
2240 * should be just as good. */
2241 return unicode > 0xffff ? 0 : unicode;
2244 /* Calculates how much of string can be shown within the given maximum width
2245 * and sets trimmed parameter to non-zero value if all of string could not be
2248 * Additionally, adds to coloffset how many many columns to move to align with
2249 * the expected position. Takes into account how multi-byte and double-width
2250 * characters will effect the cursor position.
2252 * Returns the number of bytes to output from string to satisfy max_width. */
2254 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2256 const char *start = string;
2257 const char *end = strchr(string, '\0');
2263 while (string < end) {
2264 int c = *(unsigned char *) string;
2265 unsigned char bytes = utf8_bytes[c];
2267 unsigned long unicode;
2269 if (string + bytes > end)
2272 /* Change representation to figure out whether
2273 * it is a single- or double-width character. */
2275 unicode = utf8_to_unicode(string, bytes);
2276 /* FIXME: Graceful handling of invalid unicode character. */
2280 ucwidth = unicode_width(unicode);
2282 if (width > max_width) {
2287 /* The column offset collects the differences between the
2288 * number of bytes encoding a character and the number of
2289 * columns will be used for rendering said character.
2291 * So if some character A is encoded in 2 bytes, but will be
2292 * represented on the screen using only 1 byte this will and up
2293 * adding 1 to the multi-byte column offset.
2295 * Assumes that no double-width character can be encoding in
2296 * less than two bytes. */
2297 if (bytes > ucwidth)
2298 mbwidth += bytes - ucwidth;
2303 *coloffset += mbwidth;
2305 return string - start;
2313 /* Whether or not the curses interface has been initialized. */
2314 static bool cursed = FALSE;
2316 /* The status window is used for polling keystrokes. */
2317 static WINDOW *status_win;
2319 /* Update status and title window. */
2321 report(const char *msg, ...)
2323 static bool empty = TRUE;
2324 struct view *view = display[current_view];
2326 if (!empty || *msg) {
2329 va_start(args, msg);
2332 wmove(status_win, 0, 0);
2334 vwprintw(status_win, msg, args);
2339 wrefresh(status_win);
2344 update_view_title(view);
2345 update_display_cursor();
2348 /* Controls when nodelay should be in effect when polling user input. */
2350 set_nonblocking_input(bool loading)
2352 static unsigned int loading_views;
2354 if ((loading == FALSE && loading_views-- == 1) ||
2355 (loading == TRUE && loading_views++ == 0))
2356 nodelay(status_win, loading);
2364 /* Initialize the curses library */
2365 if (isatty(STDIN_FILENO)) {
2366 cursed = !!initscr();
2368 /* Leave stdin and stdout alone when acting as a pager. */
2369 FILE *io = fopen("/dev/tty", "r+");
2371 cursed = !!newterm(NULL, io, io);
2375 die("Failed to initialize curses");
2377 nonl(); /* Tell curses not to do NL->CR/NL on output */
2378 cbreak(); /* Take input chars one at a time, no wait for \n */
2379 noecho(); /* Don't echo input */
2380 leaveok(stdscr, TRUE);
2385 getmaxyx(stdscr, y, x);
2386 status_win = newwin(1, 0, y - 1, 0);
2388 die("Failed to create status window");
2390 /* Enable keyboard mapping */
2391 keypad(status_win, TRUE);
2392 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2397 * Repository references
2400 static struct ref *refs;
2401 static size_t refs_size;
2403 /* Id <-> ref store */
2404 static struct ref ***id_refs;
2405 static size_t id_refs_size;
2407 static struct ref **
2410 struct ref ***tmp_id_refs;
2411 struct ref **ref_list = NULL;
2412 size_t ref_list_size = 0;
2415 for (i = 0; i < id_refs_size; i++)
2416 if (!strcmp(id, id_refs[i][0]->id))
2419 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2423 id_refs = tmp_id_refs;
2425 for (i = 0; i < refs_size; i++) {
2428 if (strcmp(id, refs[i].id))
2431 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2439 if (ref_list_size > 0)
2440 ref_list[ref_list_size - 1]->next = 1;
2441 ref_list[ref_list_size] = &refs[i];
2443 /* XXX: The properties of the commit chains ensures that we can
2444 * safely modify the shared ref. The repo references will
2445 * always be similar for the same id. */
2446 ref_list[ref_list_size]->next = 0;
2451 id_refs[id_refs_size++] = ref_list;
2457 read_ref(char *id, int idlen, char *name, int namelen)
2462 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2463 /* Commits referenced by tags has "^{}" appended. */
2464 if (name[namelen - 1] != '}')
2467 while (namelen > 0 && name[namelen] != '^')
2471 namelen -= STRING_SIZE("refs/tags/");
2472 name += STRING_SIZE("refs/tags/");
2474 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2475 namelen -= STRING_SIZE("refs/heads/");
2476 name += STRING_SIZE("refs/heads/");
2478 } else if (!strcmp(name, "HEAD")) {
2482 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2486 ref = &refs[refs_size++];
2487 ref->name = malloc(namelen + 1);
2491 strncpy(ref->name, name, namelen);
2492 ref->name[namelen] = 0;
2494 string_copy(ref->id, id);
2502 const char *cmd_env = getenv("TIG_LS_REMOTE");
2503 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2505 return read_properties(popen(cmd, "r"), "\t", read_ref);
2509 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2511 if (!strcmp(name, "i18n.commitencoding"))
2512 string_copy(opt_encoding, value);
2518 load_repo_config(void)
2520 return read_properties(popen("git repo-config --list", "r"),
2521 "=", read_repo_config_option);
2525 read_properties(FILE *pipe, const char *separators,
2526 int (*read_property)(char *, int, char *, int))
2528 char buffer[BUFSIZ];
2535 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2540 name = chomp_string(name);
2541 namelen = strcspn(name, separators);
2543 if (name[namelen]) {
2545 value = chomp_string(name + namelen + 1);
2546 valuelen = strlen(value);
2553 state = read_property(name, namelen, value, valuelen);
2556 if (state != ERR && ferror(pipe))
2569 static void __NORETURN
2572 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2578 static void __NORETURN
2579 die(const char *err, ...)
2585 va_start(args, err);
2586 fputs("tig: ", stderr);
2587 vfprintf(stderr, err, args);
2588 fputs("\n", stderr);
2595 main(int argc, char *argv[])
2598 enum request request;
2601 signal(SIGINT, quit);
2603 if (load_options() == ERR)
2604 die("Failed to load user config.");
2606 /* Load the repo config file so options can be overwritten from
2607 * the command line. */
2608 if (load_repo_config() == ERR)
2609 die("Failed to load repo config.");
2611 if (!parse_options(argc, argv))
2614 if (load_refs() == ERR)
2615 die("Failed to load refs.");
2617 /* Require a git repository unless when running in pager mode. */
2618 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2619 die("Not a git repository");
2621 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2622 view->cmd_env = getenv(view->cmd_env);
2624 request = opt_request;
2628 while (view_driver(display[current_view], request)) {
2632 foreach_view (view, i)
2635 /* Refresh, accept single keystroke of input */
2636 key = wgetch(status_win);
2637 request = get_request(key);
2639 /* Some low-level request handling. This keeps access to
2640 * status_win restricted. */
2644 /* Temporarily switch to line-oriented and echoed
2649 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2650 memcpy(opt_cmd, "git ", 4);
2651 opt_request = REQ_VIEW_PAGER;
2653 report("Prompt interrupted by loading view, "
2654 "press 'z' to stop loading views");
2655 request = REQ_SCREEN_UPDATE;
2662 case REQ_SCREEN_RESIZE:
2666 getmaxyx(stdscr, height, width);
2668 /* Resize the status view and let the view driver take
2669 * care of resizing the displayed views. */
2670 wresize(status_win, 1, width);
2671 mvwin(status_win, height - 1, 0);
2672 wrefresh(status_win);