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.
19 * tig - text-mode interface for git
25 * tig [options] [--] [git log options]
26 * tig [options] log [git log options]
27 * tig [options] diff [git diff options]
28 * tig [options] show [git show options]
29 * tig [options] < [git command output]
33 * Browse changes in a git repository. Additionally, tig(1) can also act
34 * as a pager for output of various git commands.
36 * When browsing repositories, tig(1) uses the underlying git commands
37 * to present the user with various views, such as summarized commit log
38 * and showing the commit with the log message, diffstat, and the diff.
40 * Using tig(1) as a pager, it will display input from stdin and try
45 #define VERSION "tig-0.3"
65 static void die(const char *err, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
70 static void load_help_page(void);
72 #define ABS(x) ((x) >= 0 ? (x) : -(x))
73 #define MIN(x, y) ((x) < (y) ? (x) : (y))
75 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
76 #define STRING_SIZE(x) (sizeof(x) - 1)
78 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
79 #define SIZEOF_CMD 1024 /* Size of command buffer. */
81 /* This color name can be used to refer to the default term colors. */
82 #define COLOR_DEFAULT (-1)
84 /* The format and size of the date column in the main view. */
85 #define DATE_FORMAT "%Y-%m-%d %H:%M"
86 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
88 #define AUTHOR_COLS 20
90 /* The default interval between line numbers. */
91 #define NUMBER_INTERVAL 1
95 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
97 /* Some ascii-shorthands fitted into the ncurses namespace. */
99 #define KEY_RETURN '\r'
104 char *name; /* Ref name; tag or head names are shortened. */
105 char id[41]; /* Commit SHA1 ID */
106 unsigned int tag:1; /* Is it a tag? */
107 unsigned int next:1; /* For ref lists: are there more refs? */
110 static struct ref **get_refs(char *id);
119 set_from_int_map(struct int_map *map, size_t map_size,
120 int *value, const char *name, int namelen)
125 for (i = 0; i < map_size; i++)
126 if (namelen == map[i].namelen &&
127 !strncasecmp(name, map[i].name, namelen)) {
128 *value = map[i].value;
141 string_ncopy(char *dst, const char *src, int dstlen)
143 strncpy(dst, src, dstlen - 1);
148 /* Shorthand for safely copying into a fixed buffer. */
149 #define string_copy(dst, src) \
150 string_ncopy(dst, src, sizeof(dst))
153 chomp_string(char *name)
157 while (isspace(*name))
160 namelen = strlen(name) - 1;
161 while (namelen > 0 && isspace(name[namelen]))
170 * NOTE: The following is a slightly modified copy of the git project's shell
171 * quoting routines found in the quote.c file.
173 * Help to copy the thing properly quoted for the shell safety. any single
174 * quote is replaced with '\'', any exclamation point is replaced with '\!',
175 * and the whole thing is enclosed in a
178 * original sq_quote result
179 * name ==> name ==> 'name'
180 * a b ==> a b ==> 'a b'
181 * a'b ==> a'\''b ==> 'a'\''b'
182 * a!b ==> a'\!'b ==> 'a'\!'b'
186 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
190 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
193 while ((c = *src++)) {
194 if (c == '\'' || c == '!') {
214 /* XXX: Keep the view request first and in sync with views[]. */ \
215 REQ_GROUP("View switching") \
216 REQ_(VIEW_MAIN, "Show main view"), \
217 REQ_(VIEW_DIFF, "Show diff view"), \
218 REQ_(VIEW_LOG, "Show log view"), \
219 REQ_(VIEW_HELP, "Show help page"), \
220 REQ_(VIEW_PAGER, "Show pager view"), \
222 REQ_GROUP("View manipulation") \
223 REQ_(ENTER, "Enter current line and scroll"), \
224 REQ_(NEXT, "Move to next"), \
225 REQ_(PREVIOUS, "Move to previous"), \
226 REQ_(VIEW_NEXT, "Move focus to next view"), \
227 REQ_(VIEW_CLOSE, "Close the current view"), \
228 REQ_(QUIT, "Close all views and quit"), \
230 REQ_GROUP("Cursor navigation") \
231 REQ_(MOVE_UP, "Move cursor one line up"), \
232 REQ_(MOVE_DOWN, "Move cursor one line down"), \
233 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
234 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
235 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
236 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
238 REQ_GROUP("Scrolling") \
239 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
240 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
241 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
242 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
245 REQ_(PROMPT, "Bring up the prompt"), \
246 REQ_(SCREEN_UPDATE, "Update the screen"), \
247 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
248 REQ_(SCREEN_RESIZE, "Resize the screen"), \
249 REQ_(SHOW_VERSION, "Show version information"), \
250 REQ_(STOP_LOADING, "Stop all loading views"), \
251 REQ_(TOGGLE_LINENO, "Toggle line numbers"),
254 /* User action requests. */
256 #define REQ_GROUP(help)
257 #define REQ_(req, help) REQ_##req
259 /* Offset all requests to avoid conflicts with ncurses getch values. */
260 REQ_OFFSET = KEY_MAX + 1,
267 struct request_info {
268 enum request request;
272 static struct request_info req_info[] = {
273 #define REQ_GROUP(help) { 0, (help) },
274 #define REQ_(req, help) { REQ_##req, (help) }
285 static const char usage[] =
286 VERSION " (" __DATE__ ")\n"
288 "Usage: tig [options]\n"
289 " or: tig [options] [--] [git log options]\n"
290 " or: tig [options] log [git log options]\n"
291 " or: tig [options] diff [git diff options]\n"
292 " or: tig [options] show [git show options]\n"
293 " or: tig [options] < [git command output]\n"
296 " -l Start up in log view\n"
297 " -d Start up in diff view\n"
298 " -n[I], --line-number[=I] Show line numbers with given interval\n"
299 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
300 " -- Mark end of tig options\n"
301 " -v, --version Show version and exit\n"
302 " -h, --help Show help message and exit\n";
304 /* Option and state variables. */
305 static bool opt_line_number = FALSE;
306 static int opt_num_interval = NUMBER_INTERVAL;
307 static int opt_tab_size = TABSIZE;
308 static enum request opt_request = REQ_VIEW_MAIN;
309 static char opt_cmd[SIZEOF_CMD] = "";
310 static char opt_encoding[20] = "";
311 static bool opt_utf8 = TRUE;
312 static FILE *opt_pipe = NULL;
314 /* Returns the index of log or diff command or -1 to exit. */
316 parse_options(int argc, char *argv[])
320 for (i = 1; i < argc; i++) {
325 * Start up in log view using the internal log command.
327 if (!strcmp(opt, "-l")) {
328 opt_request = REQ_VIEW_LOG;
334 * Start up in diff view using the internal diff command.
336 if (!strcmp(opt, "-d")) {
337 opt_request = REQ_VIEW_DIFF;
342 * -n[INTERVAL], --line-number[=INTERVAL]::
343 * Prefix line numbers in log and diff view.
344 * Optionally, with interval different than each line.
346 if (!strncmp(opt, "-n", 2) ||
347 !strncmp(opt, "--line-number", 13)) {
353 } else if (opt[STRING_SIZE("--line-number")] == '=') {
354 num = opt + STRING_SIZE("--line-number=");
358 opt_num_interval = atoi(num);
360 opt_line_number = TRUE;
365 * -b[NSPACES], --tab-size[=NSPACES]::
366 * Set the number of spaces tabs should be expanded to.
368 if (!strncmp(opt, "-b", 2) ||
369 !strncmp(opt, "--tab-size", 10)) {
375 } else if (opt[STRING_SIZE("--tab-size")] == '=') {
376 num = opt + STRING_SIZE("--tab-size=");
380 opt_tab_size = MIN(atoi(num), TABSIZE);
386 * Show version and exit.
388 if (!strcmp(opt, "-v") ||
389 !strcmp(opt, "--version")) {
390 printf("tig version %s\n", VERSION);
396 * Show help message and exit.
398 if (!strcmp(opt, "-h") ||
399 !strcmp(opt, "--help")) {
406 * End of tig(1) options. Useful when specifying command
407 * options for the main view. Example:
409 * $ tig -- --since=1.month
411 if (!strcmp(opt, "--")) {
417 * log [git log options]::
418 * Open log view using the given git log options.
420 * diff [git diff options]::
421 * Open diff view using the given git diff options.
423 * show [git show options]::
424 * Open diff view using the given git show options.
426 if (!strcmp(opt, "log") ||
427 !strcmp(opt, "diff") ||
428 !strcmp(opt, "show")) {
429 opt_request = opt[0] == 'l'
430 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
435 * [git log options]::
436 * tig(1) will stop the option parsing when the first
437 * command line parameter not starting with "-" is
438 * encountered. All options including this one will be
439 * passed to git log when loading the main view.
440 * This makes it possible to say:
442 * $ tig tag-1.0..HEAD
444 if (opt[0] && opt[0] != '-')
447 die("unknown command '%s'", opt);
450 if (!isatty(STDIN_FILENO)) {
451 opt_request = REQ_VIEW_PAGER;
454 } else if (i < argc) {
457 if (opt_request == REQ_VIEW_MAIN)
458 /* XXX: This is vulnerable to the user overriding
459 * options required for the main view parser. */
460 string_copy(opt_cmd, "git log --stat --pretty=raw");
462 string_copy(opt_cmd, "git");
463 buf_size = strlen(opt_cmd);
465 while (buf_size < sizeof(opt_cmd) && i < argc) {
466 opt_cmd[buf_size++] = ' ';
467 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
470 if (buf_size >= sizeof(opt_cmd))
471 die("command too long");
473 opt_cmd[buf_size] = 0;
477 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
485 * ENVIRONMENT VARIABLES
486 * ---------------------
488 * Set command for retrieving all repository references. The command
489 * should output data in the same format as git-ls-remote(1).
492 #define TIG_LS_REMOTE \
493 "git ls-remote . 2>/dev/null"
497 * The command used for the diff view. By default, git show is used
501 * The command used for the log view. If you prefer to have both
502 * author and committer shown in the log view be sure to pass
503 * `--pretty=fuller` to git log.
506 * The command used for the main view. Note, you must always specify
507 * the option: `--pretty=raw` since the main view parser expects to
511 #define TIG_DIFF_CMD \
512 "git show --patch-with-stat --find-copies-harder -B -C %s"
514 #define TIG_LOG_CMD \
515 "git log --cc --stat -n100 %s"
517 #define TIG_MAIN_CMD \
518 "git log --topo-order --stat --pretty=raw %s"
520 /* ... silently ignore that the following are also exported. */
522 #define TIG_HELP_CMD \
525 #define TIG_PAGER_CMD \
533 * User configuration file. See tigrc(5) for examples.
536 * Repository config file. Read on startup with the help of
537 * git-repo-config(1).
540 static struct int_map color_map[] = {
541 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
553 static struct int_map attr_map[] = {
554 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
565 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
568 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
569 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
570 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
586 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
587 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
590 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
593 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
594 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
595 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
596 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
597 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
598 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
599 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
600 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
601 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
602 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
606 * Line-oriented content detection.
610 #define LINE(type, line, fg, bg, attr) \
617 const char *name; /* Option name. */
618 int namelen; /* Size of option name. */
619 const char *line; /* The start of line to match. */
620 int linelen; /* Size of string to match. */
621 int fg, bg, attr; /* Color and text attributes for the lines. */
624 static struct line_info line_info[] = {
625 #define LINE(type, line, fg, bg, attr) \
626 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
631 static enum line_type
632 get_line_type(char *line)
634 int linelen = strlen(line);
637 for (type = 0; type < ARRAY_SIZE(line_info); type++)
638 /* Case insensitive search matches Signed-off-by lines better. */
639 if (linelen >= line_info[type].linelen &&
640 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
647 get_line_attr(enum line_type type)
649 assert(type < ARRAY_SIZE(line_info));
650 return COLOR_PAIR(type) | line_info[type].attr;
653 static struct line_info *
654 get_line_info(char *name, int namelen)
659 /* Diff-Header -> DIFF_HEADER */
660 for (i = 0; i < namelen; i++) {
663 else if (name[i] == '.')
667 for (type = 0; type < ARRAY_SIZE(line_info); type++)
668 if (namelen == line_info[type].namelen &&
669 !strncasecmp(line_info[type].name, name, namelen))
670 return &line_info[type];
678 int default_bg = COLOR_BLACK;
679 int default_fg = COLOR_WHITE;
684 if (use_default_colors() != ERR) {
689 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
690 struct line_info *info = &line_info[type];
691 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
692 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
694 init_pair(type, fg, bg);
700 void *data; /* User data */
705 * User config file handling.
708 #define set_color(color, name, namelen) \
709 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
711 #define set_attribute(attr, name, namelen) \
712 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
714 static int config_lineno;
715 static bool config_errors;
716 static char *config_msg;
719 set_option(char *opt, int optlen, char *value, int valuelen)
721 /* Reads: "color" object fgcolor bgcolor [attr] */
722 if (!strcmp(opt, "color")) {
723 struct line_info *info;
725 value = chomp_string(value);
726 valuelen = strcspn(value, " \t");
727 info = get_line_info(value, valuelen);
729 config_msg = "Unknown color name";
733 value = chomp_string(value + valuelen);
734 valuelen = strcspn(value, " \t");
735 if (set_color(&info->fg, value, valuelen) == ERR) {
736 config_msg = "Unknown color";
740 value = chomp_string(value + valuelen);
741 valuelen = strcspn(value, " \t");
742 if (set_color(&info->bg, value, valuelen) == ERR) {
743 config_msg = "Unknown color";
747 value = chomp_string(value + valuelen);
749 set_attribute(&info->attr, value, strlen(value)) == ERR) {
750 config_msg = "Unknown attribute";
761 read_option(char *opt, int optlen, char *value, int valuelen)
764 config_msg = "Internal error";
766 optlen = strcspn(opt, "#;");
768 /* The whole line is a commend or empty. */
771 } else if (opt[optlen] != 0) {
772 /* Part of the option name is a comment, so the value part
773 * should be ignored. */
775 opt[optlen] = value[valuelen] = 0;
777 /* Else look for comment endings in the value. */
778 valuelen = strcspn(value, "#;");
782 if (set_option(opt, optlen, value, valuelen) == ERR) {
783 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
784 config_lineno, optlen, opt, config_msg);
785 config_errors = TRUE;
788 /* Always keep going if errors are encountered. */
795 char *home = getenv("HOME");
800 config_errors = FALSE;
803 snprintf(buf, sizeof(buf), "%s/.tigrc", home) >= sizeof(buf))
806 /* It's ok that the file doesn't exist. */
807 file = fopen(buf, "r");
811 if (read_properties(file, " \t", read_option) == ERR ||
812 config_errors == TRUE)
813 fprintf(stderr, "Errors while loading %s.\n", buf);
826 /* The display array of active views and the index of the current view. */
827 static struct view *display[2];
828 static unsigned int current_view;
830 #define foreach_view(view, i) \
831 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
833 #define displayed_views() (display[1] != NULL ? 2 : 1)
835 /* Current head and commit ID */
836 static char ref_commit[SIZEOF_REF] = "HEAD";
837 static char ref_head[SIZEOF_REF] = "HEAD";
840 const char *name; /* View name */
841 const char *cmd_fmt; /* Default command line format */
842 const char *cmd_env; /* Command line set via environment */
843 const char *id; /* Points to either of ref_{head,commit} */
845 struct view_ops *ops; /* View operations */
847 char cmd[SIZEOF_CMD]; /* Command buffer */
848 char ref[SIZEOF_REF]; /* Hovered commit reference */
849 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
851 int height, width; /* The width and height of the main window */
852 WINDOW *win; /* The main window */
853 WINDOW *title; /* The title window living below the main window */
856 unsigned long offset; /* Offset of the window top */
857 unsigned long lineno; /* Current line number */
859 /* If non-NULL, points to the view that opened this view. If this view
860 * is closed tig will switch back to the parent view. */
864 unsigned long lines; /* Total number of lines */
865 struct line *line; /* Line index */
866 unsigned long line_size;/* Total number of allocated lines */
867 unsigned int digits; /* Number of digits in the lines member. */
875 /* What type of content being displayed. Used in the title bar. */
877 /* Draw one line; @lineno must be < view->height. */
878 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
879 /* Read one line; updates view->line. */
880 bool (*read)(struct view *view, struct line *prev, char *data);
881 /* Depending on view, change display based on current line. */
882 bool (*enter)(struct view *view, struct line *line);
885 static struct view_ops pager_ops;
886 static struct view_ops main_ops;
888 #define VIEW_STR(name, cmd, env, ref, ops) \
889 { name, cmd, #env, ref, ops }
891 #define VIEW_(id, name, ops, ref) \
892 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
895 static struct view views[] = {
896 VIEW_(MAIN, "main", &main_ops, ref_head),
897 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
898 VIEW_(LOG, "log", &pager_ops, ref_head),
899 VIEW_(HELP, "help", &pager_ops, "static"),
900 VIEW_(PAGER, "pager", &pager_ops, "static"),
903 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
907 draw_view_line(struct view *view, unsigned int lineno)
909 if (view->offset + lineno >= view->lines)
912 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
916 redraw_view_from(struct view *view, int lineno)
918 assert(0 <= lineno && lineno < view->height);
920 for (; lineno < view->height; lineno++) {
921 if (!draw_view_line(view, lineno))
925 redrawwin(view->win);
930 redraw_view(struct view *view)
933 redraw_view_from(view, 0);
938 update_view_title(struct view *view)
940 if (view == display[current_view])
941 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
943 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
946 wmove(view->title, 0, 0);
949 wprintw(view->title, "[%s] %s", view->name, view->ref);
951 wprintw(view->title, "[%s]", view->name);
953 if (view->lines || view->pipe) {
954 unsigned int lines = view->lines
955 ? (view->lineno + 1) * 100 / view->lines
958 wprintw(view->title, " - %s %d of %d (%d%%)",
966 time_t secs = time(NULL) - view->start_time;
968 /* Three git seconds are a long time ... */
970 wprintw(view->title, " %lds", secs);
973 wmove(view->title, 0, view->width - 1);
974 wrefresh(view->title);
981 struct view *base = display[0];
982 struct view *view = display[1] ? display[1] : display[0];
984 /* Setup window dimensions */
986 getmaxyx(stdscr, base->height, base->width);
988 /* Make room for the status window. */
992 /* Horizontal split. */
993 view->width = base->width;
994 view->height = SCALE_SPLIT_VIEW(base->height);
995 base->height -= view->height;
997 /* Make room for the title bar. */
1001 /* Make room for the title bar. */
1006 foreach_view (view, i) {
1008 view->win = newwin(view->height, 0, offset, 0);
1010 die("Failed to create %s view", view->name);
1012 scrollok(view->win, TRUE);
1014 view->title = newwin(1, 0, offset + view->height, 0);
1016 die("Failed to create title window");
1019 wresize(view->win, view->height, view->width);
1020 mvwin(view->win, offset, 0);
1021 mvwin(view->title, offset + view->height, 0);
1024 offset += view->height + 1;
1029 redraw_display(void)
1034 foreach_view (view, i) {
1036 update_view_title(view);
1041 update_display_cursor(void)
1043 struct view *view = display[current_view];
1045 /* Move the cursor to the right-most column of the cursor line.
1047 * XXX: This could turn out to be a bit expensive, but it ensures that
1048 * the cursor does not jump around. */
1050 wmove(view->win, view->lineno - view->offset, view->width - 1);
1051 wrefresh(view->win);
1059 /* Scrolling backend */
1061 do_scroll_view(struct view *view, int lines, bool redraw)
1063 /* The rendering expects the new offset. */
1064 view->offset += lines;
1066 assert(0 <= view->offset && view->offset < view->lines);
1069 /* Redraw the whole screen if scrolling is pointless. */
1070 if (view->height < ABS(lines)) {
1074 int line = lines > 0 ? view->height - lines : 0;
1075 int end = line + ABS(lines);
1077 wscrl(view->win, lines);
1079 for (; line < end; line++) {
1080 if (!draw_view_line(view, line))
1085 /* Move current line into the view. */
1086 if (view->lineno < view->offset) {
1087 view->lineno = view->offset;
1088 draw_view_line(view, 0);
1090 } else if (view->lineno >= view->offset + view->height) {
1091 if (view->lineno == view->offset + view->height) {
1092 /* Clear the hidden line so it doesn't show if the view
1093 * is scrolled up. */
1094 wmove(view->win, view->height, 0);
1095 wclrtoeol(view->win);
1097 view->lineno = view->offset + view->height - 1;
1098 draw_view_line(view, view->lineno - view->offset);
1101 assert(view->offset <= view->lineno && view->lineno < view->lines);
1106 redrawwin(view->win);
1107 wrefresh(view->win);
1111 /* Scroll frontend */
1113 scroll_view(struct view *view, enum request request)
1118 case REQ_SCROLL_PAGE_DOWN:
1119 lines = view->height;
1120 case REQ_SCROLL_LINE_DOWN:
1121 if (view->offset + lines > view->lines)
1122 lines = view->lines - view->offset;
1124 if (lines == 0 || view->offset + view->height >= view->lines) {
1125 report("Cannot scroll beyond the last line");
1130 case REQ_SCROLL_PAGE_UP:
1131 lines = view->height;
1132 case REQ_SCROLL_LINE_UP:
1133 if (lines > view->offset)
1134 lines = view->offset;
1137 report("Cannot scroll beyond the first line");
1145 die("request %d not handled in switch", request);
1148 do_scroll_view(view, lines, TRUE);
1153 move_view(struct view *view, enum request request, bool redraw)
1158 case REQ_MOVE_FIRST_LINE:
1159 steps = -view->lineno;
1162 case REQ_MOVE_LAST_LINE:
1163 steps = view->lines - view->lineno - 1;
1166 case REQ_MOVE_PAGE_UP:
1167 steps = view->height > view->lineno
1168 ? -view->lineno : -view->height;
1171 case REQ_MOVE_PAGE_DOWN:
1172 steps = view->lineno + view->height >= view->lines
1173 ? view->lines - view->lineno - 1 : view->height;
1185 die("request %d not handled in switch", request);
1188 if (steps <= 0 && view->lineno == 0) {
1189 report("Cannot move beyond the first line");
1192 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1193 report("Cannot move beyond the last line");
1197 /* Move the current line */
1198 view->lineno += steps;
1199 assert(0 <= view->lineno && view->lineno < view->lines);
1201 /* Repaint the old "current" line if we be scrolling */
1202 if (ABS(steps) < view->height) {
1203 int prev_lineno = view->lineno - steps - view->offset;
1205 wmove(view->win, prev_lineno, 0);
1206 wclrtoeol(view->win);
1207 draw_view_line(view, prev_lineno);
1210 /* Check whether the view needs to be scrolled */
1211 if (view->lineno < view->offset ||
1212 view->lineno >= view->offset + view->height) {
1213 if (steps < 0 && -steps > view->offset) {
1214 steps = -view->offset;
1216 } else if (steps > 0) {
1217 if (view->lineno == view->lines - 1 &&
1218 view->lines > view->height) {
1219 steps = view->lines - view->offset - 1;
1220 if (steps >= view->height)
1221 steps -= view->height - 1;
1225 do_scroll_view(view, steps, redraw);
1229 /* Draw the current line */
1230 draw_view_line(view, view->lineno - view->offset);
1235 redrawwin(view->win);
1236 wrefresh(view->win);
1242 * Incremental updating
1246 end_update(struct view *view)
1250 set_nonblocking_input(FALSE);
1251 if (view->pipe == stdin)
1259 begin_update(struct view *view)
1261 const char *id = view->id;
1267 string_copy(view->cmd, opt_cmd);
1269 /* When running random commands, the view ref could have become
1270 * invalid so clear it. */
1273 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1275 if (snprintf(view->cmd, sizeof(view->cmd), format,
1276 id, id, id, id, id) >= sizeof(view->cmd))
1280 /* Special case for the pager view. */
1282 view->pipe = opt_pipe;
1285 view->pipe = popen(view->cmd, "r");
1291 set_nonblocking_input(TRUE);
1296 string_copy(view->vid, id);
1301 for (i = 0; i < view->lines; i++)
1302 if (view->line[i].data)
1303 free(view->line[i].data);
1309 view->start_time = time(NULL);
1314 static struct line *
1315 realloc_lines(struct view *view, size_t line_size)
1317 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1323 view->line_size = line_size;
1328 update_view(struct view *view)
1330 char buffer[BUFSIZ];
1332 /* The number of lines to read. If too low it will cause too much
1333 * redrawing (and possible flickering), if too high responsiveness
1335 unsigned long lines = view->height;
1336 int redraw_from = -1;
1341 /* Only redraw if lines are visible. */
1342 if (view->offset + view->height >= view->lines)
1343 redraw_from = view->lines - view->offset;
1345 if (!realloc_lines(view, view->lines + lines))
1348 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1349 int linelen = strlen(line);
1351 struct line *prev = view->lines
1352 ? &view->line[view->lines - 1]
1356 line[linelen - 1] = 0;
1358 if (!view->ops->read(view, prev, line))
1368 lines = view->lines;
1369 for (digits = 0; lines; digits++)
1372 /* Keep the displayed view in sync with line number scaling. */
1373 if (digits != view->digits) {
1374 view->digits = digits;
1379 if (redraw_from >= 0) {
1380 /* If this is an incremental update, redraw the previous line
1381 * since for commits some members could have changed when
1382 * loading the main view. */
1383 if (redraw_from > 0)
1386 /* Incrementally draw avoids flickering. */
1387 redraw_view_from(view, redraw_from);
1390 /* Update the title _after_ the redraw so that if the redraw picks up a
1391 * commit reference in view->ref it'll be available here. */
1392 update_view_title(view);
1394 if (ferror(view->pipe)) {
1395 report("Failed to read: %s", strerror(errno));
1398 } else if (feof(view->pipe)) {
1406 report("Allocation failure");
1414 OPEN_DEFAULT = 0, /* Use default view switching. */
1415 OPEN_SPLIT = 1, /* Split current view. */
1416 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1417 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1421 open_view(struct view *prev, enum request request, enum open_flags flags)
1423 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1424 bool split = !!(flags & OPEN_SPLIT);
1425 bool reload = !!(flags & OPEN_RELOAD);
1426 struct view *view = VIEW(request);
1427 int nviews = displayed_views();
1428 struct view *base_view = display[0];
1430 if (view == prev && nviews == 1 && !reload) {
1431 report("Already in %s view", view->name);
1435 if ((reload || strcmp(view->vid, view->id)) &&
1436 !begin_update(view)) {
1437 report("Failed to load %s view", view->name);
1446 /* Maximize the current view. */
1447 memset(display, 0, sizeof(display));
1449 display[current_view] = view;
1452 /* Resize the view when switching between split- and full-screen,
1453 * or when switching between two different full-screen views. */
1454 if (nviews != displayed_views() ||
1455 (nviews == 1 && base_view != display[0]))
1458 if (split && prev->lineno - prev->offset >= prev->height) {
1459 /* Take the title line into account. */
1460 int lines = prev->lineno - prev->offset - prev->height + 1;
1462 /* Scroll the view that was split if the current line is
1463 * outside the new limited view. */
1464 do_scroll_view(prev, lines, TRUE);
1467 if (prev && view != prev) {
1468 if (split && !backgrounded) {
1469 /* "Blur" the previous view. */
1470 update_view_title(prev);
1473 view->parent = prev;
1476 if (view == VIEW(REQ_VIEW_HELP))
1479 if (view->pipe && view->lines == 0) {
1480 /* Clear the old view and let the incremental updating refill
1489 /* If the view is backgrounded the above calls to report()
1490 * won't redraw the view title. */
1492 update_view_title(view);
1497 * User request switch noodle
1501 view_driver(struct view *view, enum request request)
1508 case REQ_MOVE_PAGE_UP:
1509 case REQ_MOVE_PAGE_DOWN:
1510 case REQ_MOVE_FIRST_LINE:
1511 case REQ_MOVE_LAST_LINE:
1512 move_view(view, request, TRUE);
1515 case REQ_SCROLL_LINE_DOWN:
1516 case REQ_SCROLL_LINE_UP:
1517 case REQ_SCROLL_PAGE_DOWN:
1518 case REQ_SCROLL_PAGE_UP:
1519 scroll_view(view, request);
1526 case REQ_VIEW_PAGER:
1527 open_view(view, request, OPEN_DEFAULT);
1532 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1534 if (view == VIEW(REQ_VIEW_DIFF) &&
1535 view->parent == VIEW(REQ_VIEW_MAIN)) {
1536 bool redraw = display[1] == view;
1538 view = view->parent;
1539 move_view(view, request, redraw);
1541 update_view_title(view);
1543 move_view(view, request, TRUE);
1550 report("Nothing to enter");
1553 return view->ops->enter(view, &view->line[view->lineno]);
1557 int nviews = displayed_views();
1558 int next_view = (current_view + 1) % nviews;
1560 if (next_view == current_view) {
1561 report("Only one view is displayed");
1565 current_view = next_view;
1566 /* Blur out the title of the previous view. */
1567 update_view_title(view);
1571 case REQ_TOGGLE_LINENO:
1572 opt_line_number = !opt_line_number;
1577 /* Always reload^Wrerun commands from the prompt. */
1578 open_view(view, opt_request, OPEN_RELOAD);
1581 case REQ_STOP_LOADING:
1582 for (i = 0; i < ARRAY_SIZE(views); i++) {
1585 report("Stopped loading the %s view", view->name),
1590 case REQ_SHOW_VERSION:
1591 report("%s (built %s)", VERSION, __DATE__);
1594 case REQ_SCREEN_RESIZE:
1597 case REQ_SCREEN_REDRAW:
1601 case REQ_SCREEN_UPDATE:
1605 case REQ_VIEW_CLOSE:
1606 /* XXX: Mark closed views by letting view->parent point to the
1607 * view itself. Parents to closed view should never be
1610 view->parent->parent != view->parent) {
1611 memset(display, 0, sizeof(display));
1613 display[current_view] = view->parent;
1614 view->parent = view;
1624 /* An unknown key will show most commonly used commands. */
1625 report("Unknown key, press 'h' for help");
1638 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1640 char *text = line->data;
1641 enum line_type type = line->type;
1642 int textlen = strlen(text);
1645 wmove(view->win, lineno, 0);
1647 if (view->offset + lineno == view->lineno) {
1648 if (type == LINE_COMMIT) {
1649 string_copy(view->ref, text + 7);
1650 string_copy(ref_commit, view->ref);
1654 wchgat(view->win, -1, 0, type, NULL);
1657 attr = get_line_attr(type);
1658 wattrset(view->win, attr);
1660 if (opt_line_number || opt_tab_size < TABSIZE) {
1661 static char spaces[] = " ";
1662 int col_offset = 0, col = 0;
1664 if (opt_line_number) {
1665 unsigned long real_lineno = view->offset + lineno + 1;
1667 if (real_lineno == 1 ||
1668 (real_lineno % opt_num_interval) == 0) {
1669 wprintw(view->win, "%.*d", view->digits, real_lineno);
1672 waddnstr(view->win, spaces,
1673 MIN(view->digits, STRING_SIZE(spaces)));
1675 waddstr(view->win, ": ");
1676 col_offset = view->digits + 2;
1679 while (text && col_offset + col < view->width) {
1680 int cols_max = view->width - col_offset - col;
1684 if (*text == '\t') {
1686 assert(sizeof(spaces) > TABSIZE);
1688 cols = opt_tab_size - (col % opt_tab_size);
1691 text = strchr(text, '\t');
1692 cols = line ? text - pos : strlen(pos);
1695 waddnstr(view->win, pos, MIN(cols, cols_max));
1700 int col = 0, pos = 0;
1702 for (; pos < textlen && col < view->width; pos++, col++)
1703 if (text[pos] == '\t')
1704 col += TABSIZE - (col % TABSIZE) - 1;
1706 waddnstr(view->win, text, pos);
1713 add_pager_refs(struct view *view, struct line *line)
1716 char *data = line->data;
1718 int bufpos = 0, refpos = 0;
1719 const char *sep = "Refs: ";
1721 assert(line->type == LINE_COMMIT);
1723 refs = get_refs(data + STRING_SIZE("commit "));
1728 char *begin = "", *end = "";
1730 if (refs[refpos]->tag) {
1735 bufpos += snprintf(buf + bufpos, sizeof(buf) - bufpos,
1736 "%s%s%s%s", sep, begin, refs[refpos]->name,
1738 if (bufpos >= sizeof(buf))
1741 } while (refs[refpos++]->next);
1744 bufpos >= sizeof(buf) ||
1745 !realloc_lines(view, view->line_size + 1))
1748 line = &view->line[view->lines];
1749 line->data = strdup(buf);
1753 line->type = LINE_PP_REFS;
1758 pager_read(struct view *view, struct line *prev, char *data)
1760 struct line *line = &view->line[view->lines];
1762 line->data = strdup(data);
1766 line->type = get_line_type(line->data);
1769 if (line->type == LINE_COMMIT &&
1770 (view == VIEW(REQ_VIEW_DIFF) ||
1771 view == VIEW(REQ_VIEW_LOG)))
1772 add_pager_refs(view, line);
1778 pager_enter(struct view *view, struct line *line)
1782 if (line->type == LINE_COMMIT &&
1783 (view == VIEW(REQ_VIEW_LOG) ||
1784 view == VIEW(REQ_VIEW_PAGER))) {
1785 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1789 /* Always scroll the view even if it was split. That way
1790 * you can use Enter to scroll through the log view and
1791 * split open each commit diff. */
1792 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1794 /* FIXME: A minor workaround. Scrolling the view will call report("")
1795 * but if we are scrolling a non-current view this won't properly
1796 * update the view title. */
1798 update_view_title(view);
1803 static struct view_ops pager_ops = {
1816 char id[41]; /* SHA1 ID. */
1817 char title[75]; /* The first line of the commit message. */
1818 char author[75]; /* The author of the commit. */
1819 struct tm time; /* Date from the author ident. */
1820 struct ref **refs; /* Repository references; tags & branch heads. */
1824 main_draw(struct view *view, struct line *line, unsigned int lineno)
1826 char buf[DATE_COLS + 1];
1827 struct commit *commit = line->data;
1828 enum line_type type;
1834 if (!*commit->author)
1837 wmove(view->win, lineno, col);
1839 if (view->offset + lineno == view->lineno) {
1840 string_copy(view->ref, commit->id);
1841 string_copy(ref_commit, view->ref);
1843 wattrset(view->win, get_line_attr(type));
1844 wchgat(view->win, -1, 0, type, NULL);
1847 type = LINE_MAIN_COMMIT;
1848 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1851 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1852 waddnstr(view->win, buf, timelen);
1853 waddstr(view->win, " ");
1856 wmove(view->win, lineno, col);
1857 if (type != LINE_CURSOR)
1858 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1861 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1863 authorlen = strlen(commit->author);
1864 if (authorlen > AUTHOR_COLS - 2) {
1865 authorlen = AUTHOR_COLS - 2;
1871 waddnstr(view->win, commit->author, authorlen);
1872 if (type != LINE_CURSOR)
1873 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1874 waddch(view->win, '~');
1876 waddstr(view->win, commit->author);
1880 if (type != LINE_CURSOR)
1881 wattrset(view->win, A_NORMAL);
1883 mvwaddch(view->win, lineno, col, ACS_LTEE);
1884 wmove(view->win, lineno, col + 2);
1891 if (type == LINE_CURSOR)
1893 else if (commit->refs[i]->tag)
1894 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1896 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1897 waddstr(view->win, "[");
1898 waddstr(view->win, commit->refs[i]->name);
1899 waddstr(view->win, "]");
1900 if (type != LINE_CURSOR)
1901 wattrset(view->win, A_NORMAL);
1902 waddstr(view->win, " ");
1903 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1904 } while (commit->refs[i++]->next);
1907 if (type != LINE_CURSOR)
1908 wattrset(view->win, get_line_attr(type));
1911 int titlelen = strlen(commit->title);
1913 if (col + titlelen > view->width)
1914 titlelen = view->width - col;
1916 waddnstr(view->win, commit->title, titlelen);
1922 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1924 main_read(struct view *view, struct line *prev, char *line)
1926 enum line_type type = get_line_type(line);
1927 struct commit *commit;
1931 commit = calloc(1, sizeof(struct commit));
1935 line += STRING_SIZE("commit ");
1937 view->line[view->lines++].data = commit;
1938 string_copy(commit->id, line);
1939 commit->refs = get_refs(commit->id);
1944 char *ident = line + STRING_SIZE("author ");
1945 char *end = strchr(ident, '<');
1950 commit = prev->data;
1953 for (; end > ident && isspace(end[-1]); end--) ;
1957 string_copy(commit->author, ident);
1959 /* Parse epoch and timezone */
1961 char *secs = strchr(end + 1, '>');
1965 if (!secs || secs[1] != ' ')
1969 time = (time_t) atol(secs);
1970 zone = strchr(secs, ' ');
1971 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1975 tz = ('0' - zone[1]) * 60 * 60 * 10;
1976 tz += ('0' - zone[2]) * 60 * 60;
1977 tz += ('0' - zone[3]) * 60;
1978 tz += ('0' - zone[4]) * 60;
1985 gmtime_r(&time, &commit->time);
1993 commit = prev->data;
1995 /* Fill in the commit title if it has not already been set. */
1996 if (commit->title[0])
1999 /* Require titles to start with a non-space character at the
2000 * offset used by git log. */
2001 /* FIXME: More gracefull handling of titles; append "..." to
2002 * shortened titles, etc. */
2003 if (strncmp(line, " ", 4) ||
2007 string_copy(commit->title, line + 4);
2014 main_enter(struct view *view, struct line *line)
2016 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2018 open_view(view, REQ_VIEW_DIFF, flags);
2022 static struct view_ops main_ops = {
2039 static struct keymap keymap[] = {
2040 /* View switching */
2041 { 'm', REQ_VIEW_MAIN },
2042 { 'd', REQ_VIEW_DIFF },
2043 { 'l', REQ_VIEW_LOG },
2044 { 'p', REQ_VIEW_PAGER },
2045 { 'h', REQ_VIEW_HELP },
2046 { '?', REQ_VIEW_HELP },
2048 /* View manipulation */
2049 { 'q', REQ_VIEW_CLOSE },
2050 { KEY_TAB, REQ_VIEW_NEXT },
2051 { KEY_RETURN, REQ_ENTER },
2052 { KEY_UP, REQ_PREVIOUS },
2053 { KEY_DOWN, REQ_NEXT },
2055 /* Cursor navigation */
2056 { 'k', REQ_MOVE_UP },
2057 { 'j', REQ_MOVE_DOWN },
2058 { KEY_HOME, REQ_MOVE_FIRST_LINE },
2059 { KEY_END, REQ_MOVE_LAST_LINE },
2060 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
2061 { ' ', REQ_MOVE_PAGE_DOWN },
2062 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
2063 { 'b', REQ_MOVE_PAGE_UP },
2064 { '-', REQ_MOVE_PAGE_UP },
2067 { KEY_IC, REQ_SCROLL_LINE_UP },
2068 { KEY_DC, REQ_SCROLL_LINE_DOWN },
2069 { 'w', REQ_SCROLL_PAGE_UP },
2070 { 's', REQ_SCROLL_PAGE_DOWN },
2074 { 'z', REQ_STOP_LOADING },
2075 { 'v', REQ_SHOW_VERSION },
2076 { 'r', REQ_SCREEN_REDRAW },
2077 { 'n', REQ_TOGGLE_LINENO },
2078 { ':', REQ_PROMPT },
2080 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2081 { ERR, REQ_SCREEN_UPDATE },
2083 /* Use the ncurses SIGWINCH handler. */
2084 { KEY_RESIZE, REQ_SCREEN_RESIZE },
2088 get_request(int key)
2092 for (i = 0; i < ARRAY_SIZE(keymap); i++)
2093 if (keymap[i].alias == key)
2094 return keymap[i].request;
2096 return (enum request) key;
2104 static struct key key_table[] = {
2105 { "Enter", KEY_RETURN },
2107 { "Backspace", KEY_BACKSPACE },
2109 { "Escape", KEY_ESC },
2110 { "Left", KEY_LEFT },
2111 { "Right", KEY_RIGHT },
2113 { "Down", KEY_DOWN },
2114 { "Insert", KEY_IC },
2115 { "Delete", KEY_DC },
2116 { "Home", KEY_HOME },
2118 { "PageUp", KEY_PPAGE },
2119 { "PageDown", KEY_NPAGE },
2129 { "F10", KEY_F(10) },
2130 { "F11", KEY_F(11) },
2131 { "F12", KEY_F(12) },
2135 get_key(enum request request)
2137 static char buf[BUFSIZ];
2138 static char key_char[] = "'X'";
2145 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2149 if (keymap[i].request != request)
2152 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2153 if (key_table[key].value == keymap[i].alias)
2154 seq = key_table[key].name;
2157 keymap[i].alias < 127 &&
2158 isprint(keymap[i].alias)) {
2159 key_char[1] = (char) keymap[i].alias;
2166 pos += snprintf(buf + pos, sizeof(buf) - pos, "%s%s", sep, seq);
2167 if (pos >= sizeof(buf))
2168 return "Too many keybindings!";
2175 static void load_help_page(void)
2178 struct view *view = VIEW(REQ_VIEW_HELP);
2179 int lines = ARRAY_SIZE(req_info) + 2;
2182 if (view->lines > 0)
2185 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2186 if (!req_info[i].request)
2189 view->line = calloc(lines, sizeof(*view->line));
2191 report("Allocation failure");
2195 pager_read(view, NULL, "Quick reference for tig keybindings:");
2197 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2200 if (!req_info[i].request) {
2201 pager_read(view, NULL, "");
2202 pager_read(view, NULL, req_info[i].help);
2206 key = get_key(req_info[i].request);
2207 if (snprintf(buf, sizeof(buf), "%-25s %s", key, req_info[i].help)
2211 pager_read(view, NULL, buf);
2217 * Unicode / UTF-8 handling
2219 * NOTE: Much of the following code for dealing with unicode is derived from
2220 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2221 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2224 /* I've (over)annotated a lot of code snippets because I am not entirely
2225 * confident that the approach taken by this small UTF-8 interface is correct.
2229 unicode_width(unsigned long c)
2232 (c <= 0x115f /* Hangul Jamo */
2235 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2237 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2238 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2239 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2240 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2241 || (c >= 0xffe0 && c <= 0xffe6)
2242 || (c >= 0x20000 && c <= 0x2fffd)
2243 || (c >= 0x30000 && c <= 0x3fffd)))
2249 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2250 * Illegal bytes are set one. */
2251 static const unsigned char utf8_bytes[256] = {
2252 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2253 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2254 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2255 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2256 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2257 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2258 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,
2259 3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4, 5,5,5,5,6,6,1,1,
2262 /* Decode UTF-8 multi-byte representation into a unicode character. */
2263 static inline unsigned long
2264 utf8_to_unicode(const char *string, size_t length)
2266 unsigned long unicode;
2270 unicode = string[0];
2273 unicode = (string[0] & 0x1f) << 6;
2274 unicode += (string[1] & 0x3f);
2277 unicode = (string[0] & 0x0f) << 12;
2278 unicode += ((string[1] & 0x3f) << 6);
2279 unicode += (string[2] & 0x3f);
2282 unicode = (string[0] & 0x0f) << 18;
2283 unicode += ((string[1] & 0x3f) << 12);
2284 unicode += ((string[2] & 0x3f) << 6);
2285 unicode += (string[3] & 0x3f);
2288 unicode = (string[0] & 0x0f) << 24;
2289 unicode += ((string[1] & 0x3f) << 18);
2290 unicode += ((string[2] & 0x3f) << 12);
2291 unicode += ((string[3] & 0x3f) << 6);
2292 unicode += (string[4] & 0x3f);
2295 unicode = (string[0] & 0x01) << 30;
2296 unicode += ((string[1] & 0x3f) << 24);
2297 unicode += ((string[2] & 0x3f) << 18);
2298 unicode += ((string[3] & 0x3f) << 12);
2299 unicode += ((string[4] & 0x3f) << 6);
2300 unicode += (string[5] & 0x3f);
2303 die("Invalid unicode length");
2306 /* Invalid characters could return the special 0xfffd value but NUL
2307 * should be just as good. */
2308 return unicode > 0xffff ? 0 : unicode;
2311 /* Calculates how much of string can be shown within the given maximum width
2312 * and sets trimmed parameter to non-zero value if all of string could not be
2315 * Additionally, adds to coloffset how many many columns to move to align with
2316 * the expected position. Takes into account how multi-byte and double-width
2317 * characters will effect the cursor position.
2319 * Returns the number of bytes to output from string to satisfy max_width. */
2321 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2323 const char *start = string;
2324 const char *end = strchr(string, '\0');
2330 while (string < end) {
2331 int c = *(unsigned char *) string;
2332 unsigned char bytes = utf8_bytes[c];
2334 unsigned long unicode;
2336 if (string + bytes > end)
2339 /* Change representation to figure out whether
2340 * it is a single- or double-width character. */
2342 unicode = utf8_to_unicode(string, bytes);
2343 /* FIXME: Graceful handling of invalid unicode character. */
2347 ucwidth = unicode_width(unicode);
2349 if (width > max_width) {
2354 /* The column offset collects the differences between the
2355 * number of bytes encoding a character and the number of
2356 * columns will be used for rendering said character.
2358 * So if some character A is encoded in 2 bytes, but will be
2359 * represented on the screen using only 1 byte this will and up
2360 * adding 1 to the multi-byte column offset.
2362 * Assumes that no double-width character can be encoding in
2363 * less than two bytes. */
2364 if (bytes > ucwidth)
2365 mbwidth += bytes - ucwidth;
2370 *coloffset += mbwidth;
2372 return string - start;
2380 /* Whether or not the curses interface has been initialized. */
2381 static bool cursed = FALSE;
2383 /* The status window is used for polling keystrokes. */
2384 static WINDOW *status_win;
2386 /* Update status and title window. */
2388 report(const char *msg, ...)
2390 static bool empty = TRUE;
2391 struct view *view = display[current_view];
2393 if (!empty || *msg) {
2396 va_start(args, msg);
2399 wmove(status_win, 0, 0);
2401 vwprintw(status_win, msg, args);
2406 wrefresh(status_win);
2411 update_view_title(view);
2412 update_display_cursor();
2415 /* Controls when nodelay should be in effect when polling user input. */
2417 set_nonblocking_input(bool loading)
2419 static unsigned int loading_views;
2421 if ((loading == FALSE && loading_views-- == 1) ||
2422 (loading == TRUE && loading_views++ == 0))
2423 nodelay(status_win, loading);
2431 /* Initialize the curses library */
2432 if (isatty(STDIN_FILENO)) {
2433 cursed = !!initscr();
2435 /* Leave stdin and stdout alone when acting as a pager. */
2436 FILE *io = fopen("/dev/tty", "r+");
2438 cursed = !!newterm(NULL, io, io);
2442 die("Failed to initialize curses");
2444 nonl(); /* Tell curses not to do NL->CR/NL on output */
2445 cbreak(); /* Take input chars one at a time, no wait for \n */
2446 noecho(); /* Don't echo input */
2447 leaveok(stdscr, TRUE);
2452 getmaxyx(stdscr, y, x);
2453 status_win = newwin(1, 0, y - 1, 0);
2455 die("Failed to create status window");
2457 /* Enable keyboard mapping */
2458 keypad(status_win, TRUE);
2459 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2464 * Repository references
2467 static struct ref *refs;
2468 static size_t refs_size;
2470 /* Id <-> ref store */
2471 static struct ref ***id_refs;
2472 static size_t id_refs_size;
2474 static struct ref **
2477 struct ref ***tmp_id_refs;
2478 struct ref **ref_list = NULL;
2479 size_t ref_list_size = 0;
2482 for (i = 0; i < id_refs_size; i++)
2483 if (!strcmp(id, id_refs[i][0]->id))
2486 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2490 id_refs = tmp_id_refs;
2492 for (i = 0; i < refs_size; i++) {
2495 if (strcmp(id, refs[i].id))
2498 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2506 if (ref_list_size > 0)
2507 ref_list[ref_list_size - 1]->next = 1;
2508 ref_list[ref_list_size] = &refs[i];
2510 /* XXX: The properties of the commit chains ensures that we can
2511 * safely modify the shared ref. The repo references will
2512 * always be similar for the same id. */
2513 ref_list[ref_list_size]->next = 0;
2518 id_refs[id_refs_size++] = ref_list;
2524 read_ref(char *id, int idlen, char *name, int namelen)
2528 bool tag_commit = FALSE;
2530 /* Commits referenced by tags has "^{}" appended. */
2531 if (name[namelen - 1] == '}') {
2532 while (namelen > 0 && name[namelen] != '^')
2539 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2542 name += STRING_SIZE("refs/tags/");
2545 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2546 name += STRING_SIZE("refs/heads/");
2548 } else if (!strcmp(name, "HEAD")) {
2552 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2556 ref = &refs[refs_size++];
2557 ref->name = strdup(name);
2562 string_copy(ref->id, id);
2570 const char *cmd_env = getenv("TIG_LS_REMOTE");
2571 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2573 return read_properties(popen(cmd, "r"), "\t", read_ref);
2577 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2579 if (!strcmp(name, "i18n.commitencoding")) {
2580 string_copy(opt_encoding, value);
2587 load_repo_config(void)
2589 return read_properties(popen("git repo-config --list", "r"),
2590 "=", read_repo_config_option);
2594 read_properties(FILE *pipe, const char *separators,
2595 int (*read_property)(char *, int, char *, int))
2597 char buffer[BUFSIZ];
2604 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2609 name = chomp_string(name);
2610 namelen = strcspn(name, separators);
2612 if (name[namelen]) {
2614 value = chomp_string(name + namelen + 1);
2615 valuelen = strlen(value);
2622 state = read_property(name, namelen, value, valuelen);
2625 if (state != ERR && ferror(pipe))
2639 #define __NORETURN __attribute__((__noreturn__))
2644 static void __NORETURN
2647 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2653 static void __NORETURN
2654 die(const char *err, ...)
2660 va_start(args, err);
2661 fputs("tig: ", stderr);
2662 vfprintf(stderr, err, args);
2663 fputs("\n", stderr);
2670 main(int argc, char *argv[])
2673 enum request request;
2676 signal(SIGINT, quit);
2678 if (load_options() == ERR)
2679 die("Failed to load user config.");
2681 /* Load the repo config file so options can be overwritten from
2682 * the command line. */
2683 if (load_repo_config() == ERR)
2684 die("Failed to load repo config.");
2686 if (!parse_options(argc, argv))
2689 if (load_refs() == ERR)
2690 die("Failed to load refs.");
2692 /* Require a git repository unless when running in pager mode. */
2693 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2694 die("Not a git repository");
2696 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2697 view->cmd_env = getenv(view->cmd_env);
2699 request = opt_request;
2703 while (view_driver(display[current_view], request)) {
2707 foreach_view (view, i)
2710 /* Refresh, accept single keystroke of input */
2711 key = wgetch(status_win);
2712 request = get_request(key);
2714 /* Some low-level request handling. This keeps access to
2715 * status_win restricted. */
2719 /* Temporarily switch to line-oriented and echoed
2724 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2725 memcpy(opt_cmd, "git ", 4);
2726 opt_request = REQ_VIEW_PAGER;
2728 report("Prompt interrupted by loading view, "
2729 "press 'z' to stop loading views");
2730 request = REQ_SCREEN_UPDATE;
2737 case REQ_SCREEN_RESIZE:
2741 getmaxyx(stdscr, height, width);
2743 /* Resize the status view and let the view driver take
2744 * care of resizing the displayed views. */
2745 wresize(status_win, 1, width);
2746 mvwin(status_win, height - 1, 0);
2747 wrefresh(status_win);
2765 * Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2767 * This program is free software; you can redistribute it and/or modify
2768 * it under the terms of the GNU General Public License as published by
2769 * the Free Software Foundation; either version 2 of the License, or
2770 * (at your option) any later version.
2774 * - link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
2775 * - link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
2777 * Other git repository browsers: