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. */
50 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
52 /* This color name can be used to refer to the default term colors. */
53 #define COLOR_DEFAULT (-1)
55 /* The format and size of the date column in the main view. */
56 #define DATE_FORMAT "%Y-%m-%d %H:%M"
57 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
59 #define AUTHOR_COLS 20
61 /* The default interval between line numbers. */
62 #define NUMBER_INTERVAL 1
66 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
68 #define TIG_LS_REMOTE \
69 "git ls-remote . 2>/dev/null"
71 #define TIG_DIFF_CMD \
72 "git show --patch-with-stat --find-copies-harder -B -C %s"
75 "git log --cc --stat -n100 %s"
77 #define TIG_MAIN_CMD \
78 "git log --topo-order --stat --pretty=raw %s"
80 /* XXX: Needs to be defined to the empty string. */
81 #define TIG_HELP_CMD ""
82 #define TIG_PAGER_CMD ""
84 /* Some ascii-shorthands fitted into the ncurses namespace. */
86 #define KEY_RETURN '\r'
91 char *name; /* Ref name; tag or head names are shortened. */
92 char id[41]; /* Commit SHA1 ID */
93 unsigned int tag:1; /* Is it a tag? */
94 unsigned int next:1; /* For ref lists: are there more refs? */
97 static struct ref **get_refs(char *id);
106 set_from_int_map(struct int_map *map, size_t map_size,
107 int *value, const char *name, int namelen)
112 for (i = 0; i < map_size; i++)
113 if (namelen == map[i].namelen &&
114 !strncasecmp(name, map[i].name, namelen)) {
115 *value = map[i].value;
128 string_ncopy(char *dst, const char *src, int dstlen)
130 strncpy(dst, src, dstlen - 1);
135 /* Shorthand for safely copying into a fixed buffer. */
136 #define string_copy(dst, src) \
137 string_ncopy(dst, src, sizeof(dst))
140 chomp_string(char *name)
144 while (isspace(*name))
147 namelen = strlen(name) - 1;
148 while (namelen > 0 && isspace(name[namelen]))
155 string_nformat(char *buf, size_t bufsize, int *bufpos, const char *fmt, ...)
158 int pos = bufpos ? *bufpos : 0;
161 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
167 return pos >= bufsize ? FALSE : TRUE;
170 #define string_format(buf, fmt, args...) \
171 string_nformat(buf, sizeof(buf), NULL, fmt, args)
173 #define string_format_from(buf, from, fmt, args...) \
174 string_nformat(buf, sizeof(buf), from, fmt, args)
178 * NOTE: The following is a slightly modified copy of the git project's shell
179 * quoting routines found in the quote.c file.
181 * Help to copy the thing properly quoted for the shell safety. any single
182 * quote is replaced with '\'', any exclamation point is replaced with '\!',
183 * and the whole thing is enclosed in a
186 * original sq_quote result
187 * name ==> name ==> 'name'
188 * a b ==> a b ==> 'a b'
189 * a'b ==> a'\''b ==> 'a'\''b'
190 * a!b ==> a'\!'b ==> 'a'\!'b'
194 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
198 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
201 while ((c = *src++)) {
202 if (c == '\'' || c == '!') {
222 /* XXX: Keep the view request first and in sync with views[]. */ \
223 REQ_GROUP("View switching") \
224 REQ_(VIEW_MAIN, "Show main view"), \
225 REQ_(VIEW_DIFF, "Show diff view"), \
226 REQ_(VIEW_LOG, "Show log view"), \
227 REQ_(VIEW_HELP, "Show help page"), \
228 REQ_(VIEW_PAGER, "Show pager view"), \
230 REQ_GROUP("View manipulation") \
231 REQ_(ENTER, "Enter current line and scroll"), \
232 REQ_(NEXT, "Move to next"), \
233 REQ_(PREVIOUS, "Move to previous"), \
234 REQ_(VIEW_NEXT, "Move focus to next view"), \
235 REQ_(VIEW_CLOSE, "Close the current view"), \
236 REQ_(QUIT, "Close all views and quit"), \
238 REQ_GROUP("Cursor navigation") \
239 REQ_(MOVE_UP, "Move cursor one line up"), \
240 REQ_(MOVE_DOWN, "Move cursor one line down"), \
241 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
242 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
243 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
244 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
246 REQ_GROUP("Scrolling") \
247 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
248 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
249 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
250 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
253 REQ_(PROMPT, "Bring up the prompt"), \
254 REQ_(SCREEN_UPDATE, "Update the screen"), \
255 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
256 REQ_(SCREEN_RESIZE, "Resize the screen"), \
257 REQ_(SHOW_VERSION, "Show version information"), \
258 REQ_(STOP_LOADING, "Stop all loading views"), \
259 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
260 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"),
263 /* User action requests. */
265 #define REQ_GROUP(help)
266 #define REQ_(req, help) REQ_##req
268 /* Offset all requests to avoid conflicts with ncurses getch values. */
269 REQ_OFFSET = KEY_MAX + 1,
276 struct request_info {
277 enum request request;
281 static struct request_info req_info[] = {
282 #define REQ_GROUP(help) { 0, (help) },
283 #define REQ_(req, help) { REQ_##req, (help) }
293 static const char usage[] =
294 VERSION " (" __DATE__ ")\n"
296 "Usage: tig [options]\n"
297 " or: tig [options] [--] [git log options]\n"
298 " or: tig [options] log [git log options]\n"
299 " or: tig [options] diff [git diff options]\n"
300 " or: tig [options] show [git show options]\n"
301 " or: tig [options] < [git command output]\n"
304 " -l Start up in log view\n"
305 " -d Start up in diff view\n"
306 " -n[I], --line-number[=I] Show line numbers with given interval\n"
307 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
308 " -- Mark end of tig options\n"
309 " -v, --version Show version and exit\n"
310 " -h, --help Show help message and exit\n";
312 /* Option and state variables. */
313 static bool opt_line_number = FALSE;
314 static bool opt_rev_graph = TRUE;
315 static int opt_num_interval = NUMBER_INTERVAL;
316 static int opt_tab_size = TABSIZE;
317 static enum request opt_request = REQ_VIEW_MAIN;
318 static char opt_cmd[SIZEOF_CMD] = "";
319 static char opt_encoding[20] = "";
320 static bool opt_utf8 = TRUE;
321 static FILE *opt_pipe = NULL;
329 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
339 int namelen = strlen(name);
343 if (strncmp(opt, name, namelen))
346 if (opt[namelen] == '=')
347 value = opt + namelen + 1;
350 if (!short_name || opt[1] != short_name)
355 va_start(args, type);
356 if (type == OPT_INT) {
357 number = va_arg(args, int *);
359 *number = atoi(value);
366 /* Returns the index of log or diff command or -1 to exit. */
368 parse_options(int argc, char *argv[])
372 for (i = 1; i < argc; i++) {
375 if (!strcmp(opt, "-l")) {
376 opt_request = REQ_VIEW_LOG;
380 if (!strcmp(opt, "-d")) {
381 opt_request = REQ_VIEW_DIFF;
385 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
386 opt_line_number = TRUE;
390 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
391 opt_tab_size = MIN(opt_tab_size, TABSIZE);
395 if (check_option(opt, 'v', "version", OPT_NONE)) {
396 printf("tig version %s\n", VERSION);
400 if (check_option(opt, 'h', "help", OPT_NONE)) {
405 if (!strcmp(opt, "--")) {
410 if (!strcmp(opt, "log") ||
411 !strcmp(opt, "diff") ||
412 !strcmp(opt, "show")) {
413 opt_request = opt[0] == 'l'
414 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
418 if (opt[0] && opt[0] != '-')
421 die("unknown command '%s'", opt);
424 if (!isatty(STDIN_FILENO)) {
425 opt_request = REQ_VIEW_PAGER;
428 } else if (i < argc) {
431 if (opt_request == REQ_VIEW_MAIN)
432 /* XXX: This is vulnerable to the user overriding
433 * options required for the main view parser. */
434 string_copy(opt_cmd, "git log --stat --pretty=raw");
436 string_copy(opt_cmd, "git");
437 buf_size = strlen(opt_cmd);
439 while (buf_size < sizeof(opt_cmd) && i < argc) {
440 opt_cmd[buf_size++] = ' ';
441 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
444 if (buf_size >= sizeof(opt_cmd))
445 die("command too long");
447 opt_cmd[buf_size] = 0;
451 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
459 * Line-oriented content detection.
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), \
503 #define LINE(type, line, fg, bg, attr) \
510 const char *name; /* Option name. */
511 int namelen; /* Size of option name. */
512 const char *line; /* The start of line to match. */
513 int linelen; /* Size of string to match. */
514 int fg, bg, attr; /* Color and text attributes for the lines. */
517 static struct line_info line_info[] = {
518 #define LINE(type, line, fg, bg, attr) \
519 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
524 static enum line_type
525 get_line_type(char *line)
527 int linelen = strlen(line);
530 for (type = 0; type < ARRAY_SIZE(line_info); type++)
531 /* Case insensitive search matches Signed-off-by lines better. */
532 if (linelen >= line_info[type].linelen &&
533 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
540 get_line_attr(enum line_type type)
542 assert(type < ARRAY_SIZE(line_info));
543 return COLOR_PAIR(type) | line_info[type].attr;
546 static struct line_info *
547 get_line_info(char *name, int namelen)
552 /* Diff-Header -> DIFF_HEADER */
553 for (i = 0; i < namelen; i++) {
556 else if (name[i] == '.')
560 for (type = 0; type < ARRAY_SIZE(line_info); type++)
561 if (namelen == line_info[type].namelen &&
562 !strncasecmp(line_info[type].name, name, namelen))
563 return &line_info[type];
571 int default_bg = COLOR_BLACK;
572 int default_fg = COLOR_WHITE;
577 if (use_default_colors() != ERR) {
582 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
583 struct line_info *info = &line_info[type];
584 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
585 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
587 init_pair(type, fg, bg);
593 void *data; /* User data */
598 * User config file handling.
601 static struct int_map color_map[] = {
602 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
614 #define set_color(color, name, namelen) \
615 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
617 static struct int_map attr_map[] = {
618 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
628 #define set_attribute(attr, name, namelen) \
629 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
631 static int config_lineno;
632 static bool config_errors;
633 static char *config_msg;
636 set_option(char *opt, int optlen, char *value, int valuelen)
638 /* Reads: "color" object fgcolor bgcolor [attr] */
639 if (!strcmp(opt, "color")) {
640 struct line_info *info;
642 value = chomp_string(value);
643 valuelen = strcspn(value, " \t");
644 info = get_line_info(value, valuelen);
646 config_msg = "Unknown color name";
650 value = chomp_string(value + valuelen);
651 valuelen = strcspn(value, " \t");
652 if (set_color(&info->fg, value, valuelen) == ERR) {
653 config_msg = "Unknown color";
657 value = chomp_string(value + valuelen);
658 valuelen = strcspn(value, " \t");
659 if (set_color(&info->bg, value, valuelen) == ERR) {
660 config_msg = "Unknown color";
664 value = chomp_string(value + valuelen);
666 set_attribute(&info->attr, value, strlen(value)) == ERR) {
667 config_msg = "Unknown attribute";
678 read_option(char *opt, int optlen, char *value, int valuelen)
681 config_msg = "Internal error";
683 optlen = strcspn(opt, "#;");
685 /* The whole line is a commend or empty. */
688 } else if (opt[optlen] != 0) {
689 /* Part of the option name is a comment, so the value part
690 * should be ignored. */
692 opt[optlen] = value[valuelen] = 0;
694 /* Else look for comment endings in the value. */
695 valuelen = strcspn(value, "#;");
699 if (set_option(opt, optlen, value, valuelen) == ERR) {
700 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
701 config_lineno, optlen, opt, config_msg);
702 config_errors = TRUE;
705 /* Always keep going if errors are encountered. */
712 char *home = getenv("HOME");
717 config_errors = FALSE;
719 if (!home || !string_format(buf, "%s/.tigrc", home))
722 /* It's ok that the file doesn't exist. */
723 file = fopen(buf, "r");
727 if (read_properties(file, " \t", read_option) == ERR ||
728 config_errors == TRUE)
729 fprintf(stderr, "Errors while loading %s.\n", buf);
742 /* The display array of active views and the index of the current view. */
743 static struct view *display[2];
744 static unsigned int current_view;
746 #define foreach_view(view, i) \
747 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
749 #define displayed_views() (display[1] != NULL ? 2 : 1)
751 /* Current head and commit ID */
752 static char ref_commit[SIZEOF_REF] = "HEAD";
753 static char ref_head[SIZEOF_REF] = "HEAD";
756 const char *name; /* View name */
757 const char *cmd_fmt; /* Default command line format */
758 const char *cmd_env; /* Command line set via environment */
759 const char *id; /* Points to either of ref_{head,commit} */
761 struct view_ops *ops; /* View operations */
763 char cmd[SIZEOF_CMD]; /* Command buffer */
764 char ref[SIZEOF_REF]; /* Hovered commit reference */
765 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
767 int height, width; /* The width and height of the main window */
768 WINDOW *win; /* The main window */
769 WINDOW *title; /* The title window living below the main window */
772 unsigned long offset; /* Offset of the window top */
773 unsigned long lineno; /* Current line number */
775 /* If non-NULL, points to the view that opened this view. If this view
776 * is closed tig will switch back to the parent view. */
780 unsigned long lines; /* Total number of lines */
781 struct line *line; /* Line index */
782 unsigned long line_size;/* Total number of allocated lines */
783 unsigned int digits; /* Number of digits in the lines member. */
791 /* What type of content being displayed. Used in the title bar. */
793 /* Draw one line; @lineno must be < view->height. */
794 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
795 /* Read one line; updates view->line. */
796 bool (*read)(struct view *view, char *data);
797 /* Depending on view, change display based on current line. */
798 bool (*enter)(struct view *view, struct line *line);
801 static struct view_ops pager_ops;
802 static struct view_ops main_ops;
804 #define VIEW_STR(name, cmd, env, ref, ops) \
805 { name, cmd, #env, ref, ops }
807 #define VIEW_(id, name, ops, ref) \
808 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
811 static struct view views[] = {
812 VIEW_(MAIN, "main", &main_ops, ref_head),
813 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
814 VIEW_(LOG, "log", &pager_ops, ref_head),
815 VIEW_(HELP, "help", &pager_ops, "static"),
816 VIEW_(PAGER, "pager", &pager_ops, "static"),
819 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
823 draw_view_line(struct view *view, unsigned int lineno)
825 if (view->offset + lineno >= view->lines)
828 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
832 redraw_view_from(struct view *view, int lineno)
834 assert(0 <= lineno && lineno < view->height);
836 for (; lineno < view->height; lineno++) {
837 if (!draw_view_line(view, lineno))
841 redrawwin(view->win);
846 redraw_view(struct view *view)
849 redraw_view_from(view, 0);
854 update_view_title(struct view *view)
856 if (view == display[current_view])
857 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
859 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
862 wmove(view->title, 0, 0);
865 wprintw(view->title, "[%s] %s", view->name, view->ref);
867 wprintw(view->title, "[%s]", view->name);
869 if (view->lines || view->pipe) {
870 unsigned int view_lines = view->offset + view->height;
871 unsigned int lines = view->lines
872 ? MIN(view_lines, view->lines) * 100 / view->lines
875 wprintw(view->title, " - %s %d of %d (%d%%)",
883 time_t secs = time(NULL) - view->start_time;
885 /* Three git seconds are a long time ... */
887 wprintw(view->title, " %lds", secs);
890 wmove(view->title, 0, view->width - 1);
891 wrefresh(view->title);
898 struct view *base = display[0];
899 struct view *view = display[1] ? display[1] : display[0];
901 /* Setup window dimensions */
903 getmaxyx(stdscr, base->height, base->width);
905 /* Make room for the status window. */
909 /* Horizontal split. */
910 view->width = base->width;
911 view->height = SCALE_SPLIT_VIEW(base->height);
912 base->height -= view->height;
914 /* Make room for the title bar. */
918 /* Make room for the title bar. */
923 foreach_view (view, i) {
925 view->win = newwin(view->height, 0, offset, 0);
927 die("Failed to create %s view", view->name);
929 scrollok(view->win, TRUE);
931 view->title = newwin(1, 0, offset + view->height, 0);
933 die("Failed to create title window");
936 wresize(view->win, view->height, view->width);
937 mvwin(view->win, offset, 0);
938 mvwin(view->title, offset + view->height, 0);
941 offset += view->height + 1;
951 foreach_view (view, i) {
953 update_view_title(view);
958 update_display_cursor(void)
960 struct view *view = display[current_view];
962 /* Move the cursor to the right-most column of the cursor line.
964 * XXX: This could turn out to be a bit expensive, but it ensures that
965 * the cursor does not jump around. */
967 wmove(view->win, view->lineno - view->offset, view->width - 1);
976 /* Scrolling backend */
978 do_scroll_view(struct view *view, int lines, bool redraw)
980 /* The rendering expects the new offset. */
981 view->offset += lines;
983 assert(0 <= view->offset && view->offset < view->lines);
986 /* Redraw the whole screen if scrolling is pointless. */
987 if (view->height < ABS(lines)) {
991 int line = lines > 0 ? view->height - lines : 0;
992 int end = line + ABS(lines);
994 wscrl(view->win, lines);
996 for (; line < end; line++) {
997 if (!draw_view_line(view, line))
1002 /* Move current line into the view. */
1003 if (view->lineno < view->offset) {
1004 view->lineno = view->offset;
1005 draw_view_line(view, 0);
1007 } else if (view->lineno >= view->offset + view->height) {
1008 if (view->lineno == view->offset + view->height) {
1009 /* Clear the hidden line so it doesn't show if the view
1010 * is scrolled up. */
1011 wmove(view->win, view->height, 0);
1012 wclrtoeol(view->win);
1014 view->lineno = view->offset + view->height - 1;
1015 draw_view_line(view, view->lineno - view->offset);
1018 assert(view->offset <= view->lineno && view->lineno < view->lines);
1023 redrawwin(view->win);
1024 wrefresh(view->win);
1028 /* Scroll frontend */
1030 scroll_view(struct view *view, enum request request)
1035 case REQ_SCROLL_PAGE_DOWN:
1036 lines = view->height;
1037 case REQ_SCROLL_LINE_DOWN:
1038 if (view->offset + lines > view->lines)
1039 lines = view->lines - view->offset;
1041 if (lines == 0 || view->offset + view->height >= view->lines) {
1042 report("Cannot scroll beyond the last line");
1047 case REQ_SCROLL_PAGE_UP:
1048 lines = view->height;
1049 case REQ_SCROLL_LINE_UP:
1050 if (lines > view->offset)
1051 lines = view->offset;
1054 report("Cannot scroll beyond the first line");
1062 die("request %d not handled in switch", request);
1065 do_scroll_view(view, lines, TRUE);
1070 move_view(struct view *view, enum request request, bool redraw)
1075 case REQ_MOVE_FIRST_LINE:
1076 steps = -view->lineno;
1079 case REQ_MOVE_LAST_LINE:
1080 steps = view->lines - view->lineno - 1;
1083 case REQ_MOVE_PAGE_UP:
1084 steps = view->height > view->lineno
1085 ? -view->lineno : -view->height;
1088 case REQ_MOVE_PAGE_DOWN:
1089 steps = view->lineno + view->height >= view->lines
1090 ? view->lines - view->lineno - 1 : view->height;
1102 die("request %d not handled in switch", request);
1105 if (steps <= 0 && view->lineno == 0) {
1106 report("Cannot move beyond the first line");
1109 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1110 report("Cannot move beyond the last line");
1114 /* Move the current line */
1115 view->lineno += steps;
1116 assert(0 <= view->lineno && view->lineno < view->lines);
1118 /* Repaint the old "current" line if we be scrolling */
1119 if (ABS(steps) < view->height) {
1120 int prev_lineno = view->lineno - steps - view->offset;
1122 wmove(view->win, prev_lineno, 0);
1123 wclrtoeol(view->win);
1124 draw_view_line(view, prev_lineno);
1127 /* Check whether the view needs to be scrolled */
1128 if (view->lineno < view->offset ||
1129 view->lineno >= view->offset + view->height) {
1130 if (steps < 0 && -steps > view->offset) {
1131 steps = -view->offset;
1133 } else if (steps > 0) {
1134 if (view->lineno == view->lines - 1 &&
1135 view->lines > view->height) {
1136 steps = view->lines - view->offset - 1;
1137 if (steps >= view->height)
1138 steps -= view->height - 1;
1142 do_scroll_view(view, steps, redraw);
1146 /* Draw the current line */
1147 draw_view_line(view, view->lineno - view->offset);
1152 redrawwin(view->win);
1153 wrefresh(view->win);
1159 * Incremental updating
1163 end_update(struct view *view)
1167 set_nonblocking_input(FALSE);
1168 if (view->pipe == stdin)
1176 begin_update(struct view *view)
1178 const char *id = view->id;
1184 string_copy(view->cmd, opt_cmd);
1186 /* When running random commands, the view ref could have become
1187 * invalid so clear it. */
1190 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1192 if (!string_format(view->cmd, format, id, id, id, id, id))
1196 /* Special case for the pager view. */
1198 view->pipe = opt_pipe;
1201 view->pipe = popen(view->cmd, "r");
1207 set_nonblocking_input(TRUE);
1212 string_copy(view->vid, id);
1217 for (i = 0; i < view->lines; i++)
1218 if (view->line[i].data)
1219 free(view->line[i].data);
1225 view->start_time = time(NULL);
1230 static struct line *
1231 realloc_lines(struct view *view, size_t line_size)
1233 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1239 view->line_size = line_size;
1244 update_view(struct view *view)
1246 char buffer[BUFSIZ];
1248 /* The number of lines to read. If too low it will cause too much
1249 * redrawing (and possible flickering), if too high responsiveness
1251 unsigned long lines = view->height;
1252 int redraw_from = -1;
1257 /* Only redraw if lines are visible. */
1258 if (view->offset + view->height >= view->lines)
1259 redraw_from = view->lines - view->offset;
1261 if (!realloc_lines(view, view->lines + lines))
1264 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1265 int linelen = strlen(line);
1268 line[linelen - 1] = 0;
1270 if (!view->ops->read(view, line))
1280 lines = view->lines;
1281 for (digits = 0; lines; digits++)
1284 /* Keep the displayed view in sync with line number scaling. */
1285 if (digits != view->digits) {
1286 view->digits = digits;
1291 if (redraw_from >= 0) {
1292 /* If this is an incremental update, redraw the previous line
1293 * since for commits some members could have changed when
1294 * loading the main view. */
1295 if (redraw_from > 0)
1298 /* Incrementally draw avoids flickering. */
1299 redraw_view_from(view, redraw_from);
1302 /* Update the title _after_ the redraw so that if the redraw picks up a
1303 * commit reference in view->ref it'll be available here. */
1304 update_view_title(view);
1306 if (ferror(view->pipe)) {
1307 report("Failed to read: %s", strerror(errno));
1310 } else if (feof(view->pipe)) {
1318 report("Allocation failure");
1326 OPEN_DEFAULT = 0, /* Use default view switching. */
1327 OPEN_SPLIT = 1, /* Split current view. */
1328 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1329 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1333 open_view(struct view *prev, enum request request, enum open_flags flags)
1335 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1336 bool split = !!(flags & OPEN_SPLIT);
1337 bool reload = !!(flags & OPEN_RELOAD);
1338 struct view *view = VIEW(request);
1339 int nviews = displayed_views();
1340 struct view *base_view = display[0];
1342 if (view == prev && nviews == 1 && !reload) {
1343 report("Already in %s view", view->name);
1347 if (view == VIEW(REQ_VIEW_HELP)) {
1350 } else if ((reload || strcmp(view->vid, view->id)) &&
1351 !begin_update(view)) {
1352 report("Failed to load %s view", view->name);
1361 /* Maximize the current view. */
1362 memset(display, 0, sizeof(display));
1364 display[current_view] = view;
1367 /* Resize the view when switching between split- and full-screen,
1368 * or when switching between two different full-screen views. */
1369 if (nviews != displayed_views() ||
1370 (nviews == 1 && base_view != display[0]))
1373 if (split && prev->lineno - prev->offset >= prev->height) {
1374 /* Take the title line into account. */
1375 int lines = prev->lineno - prev->offset - prev->height + 1;
1377 /* Scroll the view that was split if the current line is
1378 * outside the new limited view. */
1379 do_scroll_view(prev, lines, TRUE);
1382 if (prev && view != prev) {
1383 if (split && !backgrounded) {
1384 /* "Blur" the previous view. */
1385 update_view_title(prev);
1388 view->parent = prev;
1391 if (view->pipe && view->lines == 0) {
1392 /* Clear the old view and let the incremental updating refill
1401 /* If the view is backgrounded the above calls to report()
1402 * won't redraw the view title. */
1404 update_view_title(view);
1409 * User request switch noodle
1413 view_driver(struct view *view, enum request request)
1420 case REQ_MOVE_PAGE_UP:
1421 case REQ_MOVE_PAGE_DOWN:
1422 case REQ_MOVE_FIRST_LINE:
1423 case REQ_MOVE_LAST_LINE:
1424 move_view(view, request, TRUE);
1427 case REQ_SCROLL_LINE_DOWN:
1428 case REQ_SCROLL_LINE_UP:
1429 case REQ_SCROLL_PAGE_DOWN:
1430 case REQ_SCROLL_PAGE_UP:
1431 scroll_view(view, request);
1438 case REQ_VIEW_PAGER:
1439 open_view(view, request, OPEN_DEFAULT);
1444 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1446 if (view == VIEW(REQ_VIEW_DIFF) &&
1447 view->parent == VIEW(REQ_VIEW_MAIN)) {
1448 bool redraw = display[1] == view;
1450 view = view->parent;
1451 move_view(view, request, redraw);
1453 update_view_title(view);
1455 move_view(view, request, TRUE);
1462 report("Nothing to enter");
1465 return view->ops->enter(view, &view->line[view->lineno]);
1469 int nviews = displayed_views();
1470 int next_view = (current_view + 1) % nviews;
1472 if (next_view == current_view) {
1473 report("Only one view is displayed");
1477 current_view = next_view;
1478 /* Blur out the title of the previous view. */
1479 update_view_title(view);
1483 case REQ_TOGGLE_LINENO:
1484 opt_line_number = !opt_line_number;
1488 case REQ_TOGGLE_REV_GRAPH:
1489 opt_rev_graph = !opt_rev_graph;
1494 /* Always reload^Wrerun commands from the prompt. */
1495 open_view(view, opt_request, OPEN_RELOAD);
1498 case REQ_STOP_LOADING:
1499 for (i = 0; i < ARRAY_SIZE(views); i++) {
1502 report("Stopped loading the %s view", view->name),
1507 case REQ_SHOW_VERSION:
1508 report("%s (built %s)", VERSION, __DATE__);
1511 case REQ_SCREEN_RESIZE:
1514 case REQ_SCREEN_REDRAW:
1518 case REQ_SCREEN_UPDATE:
1522 case REQ_VIEW_CLOSE:
1523 /* XXX: Mark closed views by letting view->parent point to the
1524 * view itself. Parents to closed view should never be
1527 view->parent->parent != view->parent) {
1528 memset(display, 0, sizeof(display));
1530 display[current_view] = view->parent;
1531 view->parent = view;
1541 /* An unknown key will show most commonly used commands. */
1542 report("Unknown key, press 'h' for help");
1555 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1557 char *text = line->data;
1558 enum line_type type = line->type;
1559 int textlen = strlen(text);
1562 wmove(view->win, lineno, 0);
1564 if (view->offset + lineno == view->lineno) {
1565 if (type == LINE_COMMIT) {
1566 string_copy(view->ref, text + 7);
1567 string_copy(ref_commit, view->ref);
1571 wchgat(view->win, -1, 0, type, NULL);
1574 attr = get_line_attr(type);
1575 wattrset(view->win, attr);
1577 if (opt_line_number || opt_tab_size < TABSIZE) {
1578 static char spaces[] = " ";
1579 int col_offset = 0, col = 0;
1581 if (opt_line_number) {
1582 unsigned long real_lineno = view->offset + lineno + 1;
1584 if (real_lineno == 1 ||
1585 (real_lineno % opt_num_interval) == 0) {
1586 wprintw(view->win, "%.*d", view->digits, real_lineno);
1589 waddnstr(view->win, spaces,
1590 MIN(view->digits, STRING_SIZE(spaces)));
1592 waddstr(view->win, ": ");
1593 col_offset = view->digits + 2;
1596 while (text && col_offset + col < view->width) {
1597 int cols_max = view->width - col_offset - col;
1601 if (*text == '\t') {
1603 assert(sizeof(spaces) > TABSIZE);
1605 cols = opt_tab_size - (col % opt_tab_size);
1608 text = strchr(text, '\t');
1609 cols = line ? text - pos : strlen(pos);
1612 waddnstr(view->win, pos, MIN(cols, cols_max));
1617 int col = 0, pos = 0;
1619 for (; pos < textlen && col < view->width; pos++, col++)
1620 if (text[pos] == '\t')
1621 col += TABSIZE - (col % TABSIZE) - 1;
1623 waddnstr(view->win, text, pos);
1630 add_pager_refs(struct view *view, struct line *line)
1633 char *data = line->data;
1635 int bufpos = 0, refpos = 0;
1636 const char *sep = "Refs: ";
1638 assert(line->type == LINE_COMMIT);
1640 refs = get_refs(data + STRING_SIZE("commit "));
1645 struct ref *ref = refs[refpos];
1646 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1648 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1651 } while (refs[refpos++]->next);
1653 if (!realloc_lines(view, view->line_size + 1))
1656 line = &view->line[view->lines];
1657 line->data = strdup(buf);
1661 line->type = LINE_PP_REFS;
1666 pager_read(struct view *view, char *data)
1668 struct line *line = &view->line[view->lines];
1670 line->data = strdup(data);
1674 line->type = get_line_type(line->data);
1677 if (line->type == LINE_COMMIT &&
1678 (view == VIEW(REQ_VIEW_DIFF) ||
1679 view == VIEW(REQ_VIEW_LOG)))
1680 add_pager_refs(view, line);
1686 pager_enter(struct view *view, struct line *line)
1690 if (line->type == LINE_COMMIT &&
1691 (view == VIEW(REQ_VIEW_LOG) ||
1692 view == VIEW(REQ_VIEW_PAGER))) {
1693 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1697 /* Always scroll the view even if it was split. That way
1698 * you can use Enter to scroll through the log view and
1699 * split open each commit diff. */
1700 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1702 /* FIXME: A minor workaround. Scrolling the view will call report("")
1703 * but if we are scrolling a non-current view this won't properly
1704 * update the view title. */
1706 update_view_title(view);
1711 static struct view_ops pager_ops = {
1724 char id[41]; /* SHA1 ID. */
1725 char title[75]; /* First line of the commit message. */
1726 char author[75]; /* Author of the commit. */
1727 struct tm time; /* Date from the author ident. */
1728 struct ref **refs; /* Repository references. */
1729 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
1730 size_t graph_size; /* The width of the graph array. */
1734 main_draw(struct view *view, struct line *line, unsigned int lineno)
1736 char buf[DATE_COLS + 1];
1737 struct commit *commit = line->data;
1738 enum line_type type;
1744 if (!*commit->author)
1747 wmove(view->win, lineno, col);
1749 if (view->offset + lineno == view->lineno) {
1750 string_copy(view->ref, commit->id);
1751 string_copy(ref_commit, view->ref);
1753 wattrset(view->win, get_line_attr(type));
1754 wchgat(view->win, -1, 0, type, NULL);
1757 type = LINE_MAIN_COMMIT;
1758 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1761 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1762 waddnstr(view->win, buf, timelen);
1763 waddstr(view->win, " ");
1766 wmove(view->win, lineno, col);
1767 if (type != LINE_CURSOR)
1768 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1771 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1773 authorlen = strlen(commit->author);
1774 if (authorlen > AUTHOR_COLS - 2) {
1775 authorlen = AUTHOR_COLS - 2;
1781 waddnstr(view->win, commit->author, authorlen);
1782 if (type != LINE_CURSOR)
1783 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1784 waddch(view->win, '~');
1786 waddstr(view->win, commit->author);
1790 if (type != LINE_CURSOR)
1791 wattrset(view->win, A_NORMAL);
1793 if (opt_rev_graph && commit->graph_size) {
1796 wmove(view->win, lineno, col);
1797 /* Using waddch() instead of waddnstr() ensures that
1798 * they'll be rendered correctly for the cursor line. */
1799 for (i = 0; i < commit->graph_size; i++)
1800 waddch(view->win, commit->graph[i]);
1802 col += commit->graph_size + 1;
1805 wmove(view->win, lineno, col);
1811 if (type == LINE_CURSOR)
1813 else if (commit->refs[i]->tag)
1814 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1816 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1817 waddstr(view->win, "[");
1818 waddstr(view->win, commit->refs[i]->name);
1819 waddstr(view->win, "]");
1820 if (type != LINE_CURSOR)
1821 wattrset(view->win, A_NORMAL);
1822 waddstr(view->win, " ");
1823 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1824 } while (commit->refs[i++]->next);
1827 if (type != LINE_CURSOR)
1828 wattrset(view->win, get_line_attr(type));
1831 int titlelen = strlen(commit->title);
1833 if (col + titlelen > view->width)
1834 titlelen = view->width - col;
1836 waddnstr(view->win, commit->title, titlelen);
1842 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1844 main_read(struct view *view, char *line)
1846 enum line_type type = get_line_type(line);
1847 struct commit *commit = view->lines
1848 ? view->line[view->lines - 1].data : NULL;
1852 commit = calloc(1, sizeof(struct commit));
1856 line += STRING_SIZE("commit ");
1858 view->line[view->lines++].data = commit;
1859 string_copy(commit->id, line);
1860 commit->refs = get_refs(commit->id);
1861 commit->graph[commit->graph_size++] = ACS_LTEE;
1866 char *ident = line + STRING_SIZE("author ");
1867 char *end = strchr(ident, '<');
1873 for (; end > ident && isspace(end[-1]); end--) ;
1877 string_copy(commit->author, ident);
1879 /* Parse epoch and timezone */
1881 char *secs = strchr(end + 1, '>');
1885 if (!secs || secs[1] != ' ')
1889 time = (time_t) atol(secs);
1890 zone = strchr(secs, ' ');
1891 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1895 tz = ('0' - zone[1]) * 60 * 60 * 10;
1896 tz += ('0' - zone[2]) * 60 * 60;
1897 tz += ('0' - zone[3]) * 60;
1898 tz += ('0' - zone[4]) * 60;
1905 gmtime_r(&time, &commit->time);
1913 /* Fill in the commit title if it has not already been set. */
1914 if (commit->title[0])
1917 /* Require titles to start with a non-space character at the
1918 * offset used by git log. */
1919 /* FIXME: More gracefull handling of titles; append "..." to
1920 * shortened titles, etc. */
1921 if (strncmp(line, " ", 4) ||
1925 string_copy(commit->title, line + 4);
1932 main_enter(struct view *view, struct line *line)
1934 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
1936 open_view(view, REQ_VIEW_DIFF, flags);
1940 static struct view_ops main_ops = {
1957 static struct keymap keymap[] = {
1958 /* View switching */
1959 { 'm', REQ_VIEW_MAIN },
1960 { 'd', REQ_VIEW_DIFF },
1961 { 'l', REQ_VIEW_LOG },
1962 { 'p', REQ_VIEW_PAGER },
1963 { 'h', REQ_VIEW_HELP },
1964 { '?', REQ_VIEW_HELP },
1966 /* View manipulation */
1967 { 'q', REQ_VIEW_CLOSE },
1968 { KEY_TAB, REQ_VIEW_NEXT },
1969 { KEY_RETURN, REQ_ENTER },
1970 { KEY_UP, REQ_PREVIOUS },
1971 { KEY_DOWN, REQ_NEXT },
1973 /* Cursor navigation */
1974 { 'k', REQ_MOVE_UP },
1975 { 'j', REQ_MOVE_DOWN },
1976 { KEY_HOME, REQ_MOVE_FIRST_LINE },
1977 { KEY_END, REQ_MOVE_LAST_LINE },
1978 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
1979 { ' ', REQ_MOVE_PAGE_DOWN },
1980 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
1981 { 'b', REQ_MOVE_PAGE_UP },
1982 { '-', REQ_MOVE_PAGE_UP },
1985 { KEY_IC, REQ_SCROLL_LINE_UP },
1986 { KEY_DC, REQ_SCROLL_LINE_DOWN },
1987 { 'w', REQ_SCROLL_PAGE_UP },
1988 { 's', REQ_SCROLL_PAGE_DOWN },
1992 { 'z', REQ_STOP_LOADING },
1993 { 'v', REQ_SHOW_VERSION },
1994 { 'r', REQ_SCREEN_REDRAW },
1995 { 'n', REQ_TOGGLE_LINENO },
1996 { 'g', REQ_TOGGLE_REV_GRAPH},
1997 { ':', REQ_PROMPT },
1999 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2000 { ERR, REQ_SCREEN_UPDATE },
2002 /* Use the ncurses SIGWINCH handler. */
2003 { KEY_RESIZE, REQ_SCREEN_RESIZE },
2007 get_request(int key)
2011 for (i = 0; i < ARRAY_SIZE(keymap); i++)
2012 if (keymap[i].alias == key)
2013 return keymap[i].request;
2015 return (enum request) key;
2023 static struct key key_table[] = {
2024 { "Enter", KEY_RETURN },
2026 { "Backspace", KEY_BACKSPACE },
2028 { "Escape", KEY_ESC },
2029 { "Left", KEY_LEFT },
2030 { "Right", KEY_RIGHT },
2032 { "Down", KEY_DOWN },
2033 { "Insert", KEY_IC },
2034 { "Delete", KEY_DC },
2035 { "Home", KEY_HOME },
2037 { "PageUp", KEY_PPAGE },
2038 { "PageDown", KEY_NPAGE },
2048 { "F10", KEY_F(10) },
2049 { "F11", KEY_F(11) },
2050 { "F12", KEY_F(12) },
2054 get_key(enum request request)
2056 static char buf[BUFSIZ];
2057 static char key_char[] = "'X'";
2064 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2068 if (keymap[i].request != request)
2071 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2072 if (key_table[key].value == keymap[i].alias)
2073 seq = key_table[key].name;
2076 keymap[i].alias < 127 &&
2077 isprint(keymap[i].alias)) {
2078 key_char[1] = (char) keymap[i].alias;
2085 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
2086 return "Too many keybindings!";
2093 static void load_help_page(void)
2096 struct view *view = VIEW(REQ_VIEW_HELP);
2097 int lines = ARRAY_SIZE(req_info) + 2;
2100 if (view->lines > 0)
2103 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2104 if (!req_info[i].request)
2107 view->line = calloc(lines, sizeof(*view->line));
2109 report("Allocation failure");
2113 pager_read(view, "Quick reference for tig keybindings:");
2115 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2118 if (!req_info[i].request) {
2119 pager_read(view, "");
2120 pager_read(view, req_info[i].help);
2124 key = get_key(req_info[i].request);
2125 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
2128 pager_read(view, buf);
2134 * Unicode / UTF-8 handling
2136 * NOTE: Much of the following code for dealing with unicode is derived from
2137 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2138 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2141 /* I've (over)annotated a lot of code snippets because I am not entirely
2142 * confident that the approach taken by this small UTF-8 interface is correct.
2146 unicode_width(unsigned long c)
2149 (c <= 0x115f /* Hangul Jamo */
2152 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2154 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2155 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2156 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2157 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2158 || (c >= 0xffe0 && c <= 0xffe6)
2159 || (c >= 0x20000 && c <= 0x2fffd)
2160 || (c >= 0x30000 && c <= 0x3fffd)))
2166 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2167 * Illegal bytes are set one. */
2168 static const unsigned char utf8_bytes[256] = {
2169 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,
2170 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,
2171 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,
2172 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,
2173 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,
2174 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,
2175 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,
2176 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,
2179 /* Decode UTF-8 multi-byte representation into a unicode character. */
2180 static inline unsigned long
2181 utf8_to_unicode(const char *string, size_t length)
2183 unsigned long unicode;
2187 unicode = string[0];
2190 unicode = (string[0] & 0x1f) << 6;
2191 unicode += (string[1] & 0x3f);
2194 unicode = (string[0] & 0x0f) << 12;
2195 unicode += ((string[1] & 0x3f) << 6);
2196 unicode += (string[2] & 0x3f);
2199 unicode = (string[0] & 0x0f) << 18;
2200 unicode += ((string[1] & 0x3f) << 12);
2201 unicode += ((string[2] & 0x3f) << 6);
2202 unicode += (string[3] & 0x3f);
2205 unicode = (string[0] & 0x0f) << 24;
2206 unicode += ((string[1] & 0x3f) << 18);
2207 unicode += ((string[2] & 0x3f) << 12);
2208 unicode += ((string[3] & 0x3f) << 6);
2209 unicode += (string[4] & 0x3f);
2212 unicode = (string[0] & 0x01) << 30;
2213 unicode += ((string[1] & 0x3f) << 24);
2214 unicode += ((string[2] & 0x3f) << 18);
2215 unicode += ((string[3] & 0x3f) << 12);
2216 unicode += ((string[4] & 0x3f) << 6);
2217 unicode += (string[5] & 0x3f);
2220 die("Invalid unicode length");
2223 /* Invalid characters could return the special 0xfffd value but NUL
2224 * should be just as good. */
2225 return unicode > 0xffff ? 0 : unicode;
2228 /* Calculates how much of string can be shown within the given maximum width
2229 * and sets trimmed parameter to non-zero value if all of string could not be
2232 * Additionally, adds to coloffset how many many columns to move to align with
2233 * the expected position. Takes into account how multi-byte and double-width
2234 * characters will effect the cursor position.
2236 * Returns the number of bytes to output from string to satisfy max_width. */
2238 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2240 const char *start = string;
2241 const char *end = strchr(string, '\0');
2247 while (string < end) {
2248 int c = *(unsigned char *) string;
2249 unsigned char bytes = utf8_bytes[c];
2251 unsigned long unicode;
2253 if (string + bytes > end)
2256 /* Change representation to figure out whether
2257 * it is a single- or double-width character. */
2259 unicode = utf8_to_unicode(string, bytes);
2260 /* FIXME: Graceful handling of invalid unicode character. */
2264 ucwidth = unicode_width(unicode);
2266 if (width > max_width) {
2271 /* The column offset collects the differences between the
2272 * number of bytes encoding a character and the number of
2273 * columns will be used for rendering said character.
2275 * So if some character A is encoded in 2 bytes, but will be
2276 * represented on the screen using only 1 byte this will and up
2277 * adding 1 to the multi-byte column offset.
2279 * Assumes that no double-width character can be encoding in
2280 * less than two bytes. */
2281 if (bytes > ucwidth)
2282 mbwidth += bytes - ucwidth;
2287 *coloffset += mbwidth;
2289 return string - start;
2297 /* Whether or not the curses interface has been initialized. */
2298 static bool cursed = FALSE;
2300 /* The status window is used for polling keystrokes. */
2301 static WINDOW *status_win;
2303 /* Update status and title window. */
2305 report(const char *msg, ...)
2307 static bool empty = TRUE;
2308 struct view *view = display[current_view];
2310 if (!empty || *msg) {
2313 va_start(args, msg);
2316 wmove(status_win, 0, 0);
2318 vwprintw(status_win, msg, args);
2323 wrefresh(status_win);
2328 update_view_title(view);
2329 update_display_cursor();
2332 /* Controls when nodelay should be in effect when polling user input. */
2334 set_nonblocking_input(bool loading)
2336 static unsigned int loading_views;
2338 if ((loading == FALSE && loading_views-- == 1) ||
2339 (loading == TRUE && loading_views++ == 0))
2340 nodelay(status_win, loading);
2348 /* Initialize the curses library */
2349 if (isatty(STDIN_FILENO)) {
2350 cursed = !!initscr();
2352 /* Leave stdin and stdout alone when acting as a pager. */
2353 FILE *io = fopen("/dev/tty", "r+");
2355 cursed = !!newterm(NULL, io, io);
2359 die("Failed to initialize curses");
2361 nonl(); /* Tell curses not to do NL->CR/NL on output */
2362 cbreak(); /* Take input chars one at a time, no wait for \n */
2363 noecho(); /* Don't echo input */
2364 leaveok(stdscr, TRUE);
2369 getmaxyx(stdscr, y, x);
2370 status_win = newwin(1, 0, y - 1, 0);
2372 die("Failed to create status window");
2374 /* Enable keyboard mapping */
2375 keypad(status_win, TRUE);
2376 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2381 * Repository references
2384 static struct ref *refs;
2385 static size_t refs_size;
2387 /* Id <-> ref store */
2388 static struct ref ***id_refs;
2389 static size_t id_refs_size;
2391 static struct ref **
2394 struct ref ***tmp_id_refs;
2395 struct ref **ref_list = NULL;
2396 size_t ref_list_size = 0;
2399 for (i = 0; i < id_refs_size; i++)
2400 if (!strcmp(id, id_refs[i][0]->id))
2403 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2407 id_refs = tmp_id_refs;
2409 for (i = 0; i < refs_size; i++) {
2412 if (strcmp(id, refs[i].id))
2415 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2423 if (ref_list_size > 0)
2424 ref_list[ref_list_size - 1]->next = 1;
2425 ref_list[ref_list_size] = &refs[i];
2427 /* XXX: The properties of the commit chains ensures that we can
2428 * safely modify the shared ref. The repo references will
2429 * always be similar for the same id. */
2430 ref_list[ref_list_size]->next = 0;
2435 id_refs[id_refs_size++] = ref_list;
2441 read_ref(char *id, int idlen, char *name, int namelen)
2446 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2447 /* Commits referenced by tags has "^{}" appended. */
2448 if (name[namelen - 1] != '}')
2451 while (namelen > 0 && name[namelen] != '^')
2455 namelen -= STRING_SIZE("refs/tags/");
2456 name += STRING_SIZE("refs/tags/");
2458 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2459 namelen -= STRING_SIZE("refs/heads/");
2460 name += STRING_SIZE("refs/heads/");
2462 } else if (!strcmp(name, "HEAD")) {
2466 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2470 ref = &refs[refs_size++];
2471 ref->name = malloc(namelen + 1);
2475 strncpy(ref->name, name, namelen);
2476 ref->name[namelen] = 0;
2478 string_copy(ref->id, id);
2486 const char *cmd_env = getenv("TIG_LS_REMOTE");
2487 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2489 return read_properties(popen(cmd, "r"), "\t", read_ref);
2493 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2495 if (!strcmp(name, "i18n.commitencoding"))
2496 string_copy(opt_encoding, value);
2502 load_repo_config(void)
2504 return read_properties(popen("git repo-config --list", "r"),
2505 "=", read_repo_config_option);
2509 read_properties(FILE *pipe, const char *separators,
2510 int (*read_property)(char *, int, char *, int))
2512 char buffer[BUFSIZ];
2519 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2524 name = chomp_string(name);
2525 namelen = strcspn(name, separators);
2527 if (name[namelen]) {
2529 value = chomp_string(name + namelen + 1);
2530 valuelen = strlen(value);
2537 state = read_property(name, namelen, value, valuelen);
2540 if (state != ERR && ferror(pipe))
2554 #define __NORETURN __attribute__((__noreturn__))
2559 static void __NORETURN
2562 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2568 static void __NORETURN
2569 die(const char *err, ...)
2575 va_start(args, err);
2576 fputs("tig: ", stderr);
2577 vfprintf(stderr, err, args);
2578 fputs("\n", stderr);
2585 main(int argc, char *argv[])
2588 enum request request;
2591 signal(SIGINT, quit);
2593 if (load_options() == ERR)
2594 die("Failed to load user config.");
2596 /* Load the repo config file so options can be overwritten from
2597 * the command line. */
2598 if (load_repo_config() == ERR)
2599 die("Failed to load repo config.");
2601 if (!parse_options(argc, argv))
2604 if (load_refs() == ERR)
2605 die("Failed to load refs.");
2607 /* Require a git repository unless when running in pager mode. */
2608 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2609 die("Not a git repository");
2611 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2612 view->cmd_env = getenv(view->cmd_env);
2614 request = opt_request;
2618 while (view_driver(display[current_view], request)) {
2622 foreach_view (view, i)
2625 /* Refresh, accept single keystroke of input */
2626 key = wgetch(status_win);
2627 request = get_request(key);
2629 /* Some low-level request handling. This keeps access to
2630 * status_win restricted. */
2634 /* Temporarily switch to line-oriented and echoed
2639 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2640 memcpy(opt_cmd, "git ", 4);
2641 opt_request = REQ_VIEW_PAGER;
2643 report("Prompt interrupted by loading view, "
2644 "press 'z' to stop loading views");
2645 request = REQ_SCREEN_UPDATE;
2652 case REQ_SCREEN_RESIZE:
2656 getmaxyx(stdscr, height, width);
2658 /* Resize the status view and let the view driver take
2659 * care of resizing the displayed views. */
2660 wresize(status_win, 1, width);
2661 mvwin(status_win, height - 1, 0);
2662 wrefresh(status_win);