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]))
168 string_nformat(char *buf, size_t bufsize, int *bufpos, const char *fmt, ...)
171 int pos = bufpos ? *bufpos : 0;
174 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
180 return pos >= bufsize ? FALSE : TRUE;
183 #define string_format(buf, fmt, args...) \
184 string_nformat(buf, sizeof(buf), NULL, fmt, args)
186 #define string_format_from(buf, from, fmt, args...) \
187 string_nformat(buf, sizeof(buf), from, fmt, args)
191 * NOTE: The following is a slightly modified copy of the git project's shell
192 * quoting routines found in the quote.c file.
194 * Help to copy the thing properly quoted for the shell safety. any single
195 * quote is replaced with '\'', any exclamation point is replaced with '\!',
196 * and the whole thing is enclosed in a
199 * original sq_quote result
200 * name ==> name ==> 'name'
201 * a b ==> a b ==> 'a b'
202 * a'b ==> a'\''b ==> 'a'\''b'
203 * a!b ==> a'\!'b ==> 'a'\!'b'
207 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
211 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
214 while ((c = *src++)) {
215 if (c == '\'' || c == '!') {
235 /* XXX: Keep the view request first and in sync with views[]. */ \
236 REQ_GROUP("View switching") \
237 REQ_(VIEW_MAIN, "Show main view"), \
238 REQ_(VIEW_DIFF, "Show diff view"), \
239 REQ_(VIEW_LOG, "Show log view"), \
240 REQ_(VIEW_HELP, "Show help page"), \
241 REQ_(VIEW_PAGER, "Show pager view"), \
243 REQ_GROUP("View manipulation") \
244 REQ_(ENTER, "Enter current line and scroll"), \
245 REQ_(NEXT, "Move to next"), \
246 REQ_(PREVIOUS, "Move to previous"), \
247 REQ_(VIEW_NEXT, "Move focus to next view"), \
248 REQ_(VIEW_CLOSE, "Close the current view"), \
249 REQ_(QUIT, "Close all views and quit"), \
251 REQ_GROUP("Cursor navigation") \
252 REQ_(MOVE_UP, "Move cursor one line up"), \
253 REQ_(MOVE_DOWN, "Move cursor one line down"), \
254 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
255 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
256 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
257 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
259 REQ_GROUP("Scrolling") \
260 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
261 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
262 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
263 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
266 REQ_(PROMPT, "Bring up the prompt"), \
267 REQ_(SCREEN_UPDATE, "Update the screen"), \
268 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
269 REQ_(SCREEN_RESIZE, "Resize the screen"), \
270 REQ_(SHOW_VERSION, "Show version information"), \
271 REQ_(STOP_LOADING, "Stop all loading views"), \
272 REQ_(TOGGLE_LINENO, "Toggle line numbers"),
275 /* User action requests. */
277 #define REQ_GROUP(help)
278 #define REQ_(req, help) REQ_##req
280 /* Offset all requests to avoid conflicts with ncurses getch values. */
281 REQ_OFFSET = KEY_MAX + 1,
288 struct request_info {
289 enum request request;
293 static struct request_info req_info[] = {
294 #define REQ_GROUP(help) { 0, (help) },
295 #define REQ_(req, help) { REQ_##req, (help) }
306 static const char usage[] =
307 VERSION " (" __DATE__ ")\n"
309 "Usage: tig [options]\n"
310 " or: tig [options] [--] [git log options]\n"
311 " or: tig [options] log [git log options]\n"
312 " or: tig [options] diff [git diff options]\n"
313 " or: tig [options] show [git show options]\n"
314 " or: tig [options] < [git command output]\n"
317 " -l Start up in log view\n"
318 " -d Start up in diff view\n"
319 " -n[I], --line-number[=I] Show line numbers with given interval\n"
320 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
321 " -- Mark end of tig options\n"
322 " -v, --version Show version and exit\n"
323 " -h, --help Show help message and exit\n";
325 /* Option and state variables. */
326 static bool opt_line_number = FALSE;
327 static int opt_num_interval = NUMBER_INTERVAL;
328 static int opt_tab_size = TABSIZE;
329 static enum request opt_request = REQ_VIEW_MAIN;
330 static char opt_cmd[SIZEOF_CMD] = "";
331 static char opt_encoding[20] = "";
332 static bool opt_utf8 = TRUE;
333 static FILE *opt_pipe = NULL;
335 /* Returns the index of log or diff command or -1 to exit. */
337 parse_options(int argc, char *argv[])
341 for (i = 1; i < argc; i++) {
346 * Start up in log view using the internal log command.
348 if (!strcmp(opt, "-l")) {
349 opt_request = REQ_VIEW_LOG;
355 * Start up in diff view using the internal diff command.
357 if (!strcmp(opt, "-d")) {
358 opt_request = REQ_VIEW_DIFF;
363 * -n[INTERVAL], --line-number[=INTERVAL]::
364 * Prefix line numbers in log and diff view.
365 * Optionally, with interval different than each line.
367 if (!strncmp(opt, "-n", 2) ||
368 !strncmp(opt, "--line-number", 13)) {
374 } else if (opt[STRING_SIZE("--line-number")] == '=') {
375 num = opt + STRING_SIZE("--line-number=");
379 opt_num_interval = atoi(num);
381 opt_line_number = TRUE;
386 * -b[NSPACES], --tab-size[=NSPACES]::
387 * Set the number of spaces tabs should be expanded to.
389 if (!strncmp(opt, "-b", 2) ||
390 !strncmp(opt, "--tab-size", 10)) {
396 } else if (opt[STRING_SIZE("--tab-size")] == '=') {
397 num = opt + STRING_SIZE("--tab-size=");
401 opt_tab_size = MIN(atoi(num), TABSIZE);
407 * Show version and exit.
409 if (!strcmp(opt, "-v") ||
410 !strcmp(opt, "--version")) {
411 printf("tig version %s\n", VERSION);
417 * Show help message and exit.
419 if (!strcmp(opt, "-h") ||
420 !strcmp(opt, "--help")) {
427 * End of tig(1) options. Useful when specifying command
428 * options for the main view. Example:
430 * $ tig -- --since=1.month
432 if (!strcmp(opt, "--")) {
438 * log [git log options]::
439 * Open log view using the given git log options.
441 * diff [git diff options]::
442 * Open diff view using the given git diff options.
444 * show [git show options]::
445 * Open diff view using the given git show options.
447 if (!strcmp(opt, "log") ||
448 !strcmp(opt, "diff") ||
449 !strcmp(opt, "show")) {
450 opt_request = opt[0] == 'l'
451 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
456 * [git log options]::
457 * tig(1) will stop the option parsing when the first
458 * command line parameter not starting with "-" is
459 * encountered. All options including this one will be
460 * passed to git log when loading the main view.
461 * This makes it possible to say:
463 * $ tig tag-1.0..HEAD
465 if (opt[0] && opt[0] != '-')
468 die("unknown command '%s'", opt);
471 if (!isatty(STDIN_FILENO)) {
472 opt_request = REQ_VIEW_PAGER;
475 } else if (i < argc) {
478 if (opt_request == REQ_VIEW_MAIN)
479 /* XXX: This is vulnerable to the user overriding
480 * options required for the main view parser. */
481 string_copy(opt_cmd, "git log --stat --pretty=raw");
483 string_copy(opt_cmd, "git");
484 buf_size = strlen(opt_cmd);
486 while (buf_size < sizeof(opt_cmd) && i < argc) {
487 opt_cmd[buf_size++] = ' ';
488 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
491 if (buf_size >= sizeof(opt_cmd))
492 die("command too long");
494 opt_cmd[buf_size] = 0;
498 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
506 * ENVIRONMENT VARIABLES
507 * ---------------------
509 * Set command for retrieving all repository references. The command
510 * should output data in the same format as git-ls-remote(1).
513 #define TIG_LS_REMOTE \
514 "git ls-remote . 2>/dev/null"
518 * The command used for the diff view. By default, git show is used
522 * The command used for the log view. If you prefer to have both
523 * author and committer shown in the log view be sure to pass
524 * `--pretty=fuller` to git log.
527 * The command used for the main view. Note, you must always specify
528 * the option: `--pretty=raw` since the main view parser expects to
532 #define TIG_DIFF_CMD \
533 "git show --patch-with-stat --find-copies-harder -B -C %s"
535 #define TIG_LOG_CMD \
536 "git log --cc --stat -n100 %s"
538 #define TIG_MAIN_CMD \
539 "git log --topo-order --stat --pretty=raw %s"
541 /* ... silently ignore that the following are also exported. */
543 #define TIG_HELP_CMD \
546 #define TIG_PAGER_CMD \
554 * User configuration file. See tigrc(5) for examples.
557 * Repository config file. Read on startup with the help of
558 * git-repo-config(1).
561 static struct int_map color_map[] = {
562 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
574 static struct int_map attr_map[] = {
575 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
586 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
587 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
588 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
589 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
590 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
591 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
594 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
595 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
596 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
597 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
599 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
600 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
601 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
602 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
603 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
604 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
605 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
606 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
607 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
608 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
609 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
610 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
611 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
612 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
613 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
614 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
615 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
616 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
617 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
618 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
619 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
620 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
621 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
622 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
623 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
627 * Line-oriented content detection.
631 #define LINE(type, line, fg, bg, attr) \
638 const char *name; /* Option name. */
639 int namelen; /* Size of option name. */
640 const char *line; /* The start of line to match. */
641 int linelen; /* Size of string to match. */
642 int fg, bg, attr; /* Color and text attributes for the lines. */
645 static struct line_info line_info[] = {
646 #define LINE(type, line, fg, bg, attr) \
647 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
652 static enum line_type
653 get_line_type(char *line)
655 int linelen = strlen(line);
658 for (type = 0; type < ARRAY_SIZE(line_info); type++)
659 /* Case insensitive search matches Signed-off-by lines better. */
660 if (linelen >= line_info[type].linelen &&
661 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
668 get_line_attr(enum line_type type)
670 assert(type < ARRAY_SIZE(line_info));
671 return COLOR_PAIR(type) | line_info[type].attr;
674 static struct line_info *
675 get_line_info(char *name, int namelen)
680 /* Diff-Header -> DIFF_HEADER */
681 for (i = 0; i < namelen; i++) {
684 else if (name[i] == '.')
688 for (type = 0; type < ARRAY_SIZE(line_info); type++)
689 if (namelen == line_info[type].namelen &&
690 !strncasecmp(line_info[type].name, name, namelen))
691 return &line_info[type];
699 int default_bg = COLOR_BLACK;
700 int default_fg = COLOR_WHITE;
705 if (use_default_colors() != ERR) {
710 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
711 struct line_info *info = &line_info[type];
712 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
713 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
715 init_pair(type, fg, bg);
721 void *data; /* User data */
726 * User config file handling.
729 #define set_color(color, name, namelen) \
730 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
732 #define set_attribute(attr, name, namelen) \
733 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
735 static int config_lineno;
736 static bool config_errors;
737 static char *config_msg;
740 set_option(char *opt, int optlen, char *value, int valuelen)
742 /* Reads: "color" object fgcolor bgcolor [attr] */
743 if (!strcmp(opt, "color")) {
744 struct line_info *info;
746 value = chomp_string(value);
747 valuelen = strcspn(value, " \t");
748 info = get_line_info(value, valuelen);
750 config_msg = "Unknown color name";
754 value = chomp_string(value + valuelen);
755 valuelen = strcspn(value, " \t");
756 if (set_color(&info->fg, value, valuelen) == ERR) {
757 config_msg = "Unknown color";
761 value = chomp_string(value + valuelen);
762 valuelen = strcspn(value, " \t");
763 if (set_color(&info->bg, value, valuelen) == ERR) {
764 config_msg = "Unknown color";
768 value = chomp_string(value + valuelen);
770 set_attribute(&info->attr, value, strlen(value)) == ERR) {
771 config_msg = "Unknown attribute";
782 read_option(char *opt, int optlen, char *value, int valuelen)
785 config_msg = "Internal error";
787 optlen = strcspn(opt, "#;");
789 /* The whole line is a commend or empty. */
792 } else if (opt[optlen] != 0) {
793 /* Part of the option name is a comment, so the value part
794 * should be ignored. */
796 opt[optlen] = value[valuelen] = 0;
798 /* Else look for comment endings in the value. */
799 valuelen = strcspn(value, "#;");
803 if (set_option(opt, optlen, value, valuelen) == ERR) {
804 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
805 config_lineno, optlen, opt, config_msg);
806 config_errors = TRUE;
809 /* Always keep going if errors are encountered. */
816 char *home = getenv("HOME");
821 config_errors = FALSE;
823 if (!home || !string_format(buf, "%s/.tigrc", home))
826 /* It's ok that the file doesn't exist. */
827 file = fopen(buf, "r");
831 if (read_properties(file, " \t", read_option) == ERR ||
832 config_errors == TRUE)
833 fprintf(stderr, "Errors while loading %s.\n", buf);
846 /* The display array of active views and the index of the current view. */
847 static struct view *display[2];
848 static unsigned int current_view;
850 #define foreach_view(view, i) \
851 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
853 #define displayed_views() (display[1] != NULL ? 2 : 1)
855 /* Current head and commit ID */
856 static char ref_commit[SIZEOF_REF] = "HEAD";
857 static char ref_head[SIZEOF_REF] = "HEAD";
860 const char *name; /* View name */
861 const char *cmd_fmt; /* Default command line format */
862 const char *cmd_env; /* Command line set via environment */
863 const char *id; /* Points to either of ref_{head,commit} */
865 struct view_ops *ops; /* View operations */
867 char cmd[SIZEOF_CMD]; /* Command buffer */
868 char ref[SIZEOF_REF]; /* Hovered commit reference */
869 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
871 int height, width; /* The width and height of the main window */
872 WINDOW *win; /* The main window */
873 WINDOW *title; /* The title window living below the main window */
876 unsigned long offset; /* Offset of the window top */
877 unsigned long lineno; /* Current line number */
879 /* If non-NULL, points to the view that opened this view. If this view
880 * is closed tig will switch back to the parent view. */
884 unsigned long lines; /* Total number of lines */
885 struct line *line; /* Line index */
886 unsigned long line_size;/* Total number of allocated lines */
887 unsigned int digits; /* Number of digits in the lines member. */
895 /* What type of content being displayed. Used in the title bar. */
897 /* Draw one line; @lineno must be < view->height. */
898 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
899 /* Read one line; updates view->line. */
900 bool (*read)(struct view *view, struct line *prev, char *data);
901 /* Depending on view, change display based on current line. */
902 bool (*enter)(struct view *view, struct line *line);
905 static struct view_ops pager_ops;
906 static struct view_ops main_ops;
908 #define VIEW_STR(name, cmd, env, ref, ops) \
909 { name, cmd, #env, ref, ops }
911 #define VIEW_(id, name, ops, ref) \
912 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
915 static struct view views[] = {
916 VIEW_(MAIN, "main", &main_ops, ref_head),
917 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
918 VIEW_(LOG, "log", &pager_ops, ref_head),
919 VIEW_(HELP, "help", &pager_ops, "static"),
920 VIEW_(PAGER, "pager", &pager_ops, "static"),
923 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
927 draw_view_line(struct view *view, unsigned int lineno)
929 if (view->offset + lineno >= view->lines)
932 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
936 redraw_view_from(struct view *view, int lineno)
938 assert(0 <= lineno && lineno < view->height);
940 for (; lineno < view->height; lineno++) {
941 if (!draw_view_line(view, lineno))
945 redrawwin(view->win);
950 redraw_view(struct view *view)
953 redraw_view_from(view, 0);
958 update_view_title(struct view *view)
960 if (view == display[current_view])
961 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
963 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
966 wmove(view->title, 0, 0);
969 wprintw(view->title, "[%s] %s", view->name, view->ref);
971 wprintw(view->title, "[%s]", view->name);
973 if (view->lines || view->pipe) {
974 unsigned int lines = view->lines
975 ? (view->lineno + 1) * 100 / view->lines
978 wprintw(view->title, " - %s %d of %d (%d%%)",
986 time_t secs = time(NULL) - view->start_time;
988 /* Three git seconds are a long time ... */
990 wprintw(view->title, " %lds", secs);
993 wmove(view->title, 0, view->width - 1);
994 wrefresh(view->title);
1001 struct view *base = display[0];
1002 struct view *view = display[1] ? display[1] : display[0];
1004 /* Setup window dimensions */
1006 getmaxyx(stdscr, base->height, base->width);
1008 /* Make room for the status window. */
1012 /* Horizontal split. */
1013 view->width = base->width;
1014 view->height = SCALE_SPLIT_VIEW(base->height);
1015 base->height -= view->height;
1017 /* Make room for the title bar. */
1021 /* Make room for the title bar. */
1026 foreach_view (view, i) {
1028 view->win = newwin(view->height, 0, offset, 0);
1030 die("Failed to create %s view", view->name);
1032 scrollok(view->win, TRUE);
1034 view->title = newwin(1, 0, offset + view->height, 0);
1036 die("Failed to create title window");
1039 wresize(view->win, view->height, view->width);
1040 mvwin(view->win, offset, 0);
1041 mvwin(view->title, offset + view->height, 0);
1044 offset += view->height + 1;
1049 redraw_display(void)
1054 foreach_view (view, i) {
1056 update_view_title(view);
1061 update_display_cursor(void)
1063 struct view *view = display[current_view];
1065 /* Move the cursor to the right-most column of the cursor line.
1067 * XXX: This could turn out to be a bit expensive, but it ensures that
1068 * the cursor does not jump around. */
1070 wmove(view->win, view->lineno - view->offset, view->width - 1);
1071 wrefresh(view->win);
1079 /* Scrolling backend */
1081 do_scroll_view(struct view *view, int lines, bool redraw)
1083 /* The rendering expects the new offset. */
1084 view->offset += lines;
1086 assert(0 <= view->offset && view->offset < view->lines);
1089 /* Redraw the whole screen if scrolling is pointless. */
1090 if (view->height < ABS(lines)) {
1094 int line = lines > 0 ? view->height - lines : 0;
1095 int end = line + ABS(lines);
1097 wscrl(view->win, lines);
1099 for (; line < end; line++) {
1100 if (!draw_view_line(view, line))
1105 /* Move current line into the view. */
1106 if (view->lineno < view->offset) {
1107 view->lineno = view->offset;
1108 draw_view_line(view, 0);
1110 } else if (view->lineno >= view->offset + view->height) {
1111 if (view->lineno == view->offset + view->height) {
1112 /* Clear the hidden line so it doesn't show if the view
1113 * is scrolled up. */
1114 wmove(view->win, view->height, 0);
1115 wclrtoeol(view->win);
1117 view->lineno = view->offset + view->height - 1;
1118 draw_view_line(view, view->lineno - view->offset);
1121 assert(view->offset <= view->lineno && view->lineno < view->lines);
1126 redrawwin(view->win);
1127 wrefresh(view->win);
1131 /* Scroll frontend */
1133 scroll_view(struct view *view, enum request request)
1138 case REQ_SCROLL_PAGE_DOWN:
1139 lines = view->height;
1140 case REQ_SCROLL_LINE_DOWN:
1141 if (view->offset + lines > view->lines)
1142 lines = view->lines - view->offset;
1144 if (lines == 0 || view->offset + view->height >= view->lines) {
1145 report("Cannot scroll beyond the last line");
1150 case REQ_SCROLL_PAGE_UP:
1151 lines = view->height;
1152 case REQ_SCROLL_LINE_UP:
1153 if (lines > view->offset)
1154 lines = view->offset;
1157 report("Cannot scroll beyond the first line");
1165 die("request %d not handled in switch", request);
1168 do_scroll_view(view, lines, TRUE);
1173 move_view(struct view *view, enum request request, bool redraw)
1178 case REQ_MOVE_FIRST_LINE:
1179 steps = -view->lineno;
1182 case REQ_MOVE_LAST_LINE:
1183 steps = view->lines - view->lineno - 1;
1186 case REQ_MOVE_PAGE_UP:
1187 steps = view->height > view->lineno
1188 ? -view->lineno : -view->height;
1191 case REQ_MOVE_PAGE_DOWN:
1192 steps = view->lineno + view->height >= view->lines
1193 ? view->lines - view->lineno - 1 : view->height;
1205 die("request %d not handled in switch", request);
1208 if (steps <= 0 && view->lineno == 0) {
1209 report("Cannot move beyond the first line");
1212 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1213 report("Cannot move beyond the last line");
1217 /* Move the current line */
1218 view->lineno += steps;
1219 assert(0 <= view->lineno && view->lineno < view->lines);
1221 /* Repaint the old "current" line if we be scrolling */
1222 if (ABS(steps) < view->height) {
1223 int prev_lineno = view->lineno - steps - view->offset;
1225 wmove(view->win, prev_lineno, 0);
1226 wclrtoeol(view->win);
1227 draw_view_line(view, prev_lineno);
1230 /* Check whether the view needs to be scrolled */
1231 if (view->lineno < view->offset ||
1232 view->lineno >= view->offset + view->height) {
1233 if (steps < 0 && -steps > view->offset) {
1234 steps = -view->offset;
1236 } else if (steps > 0) {
1237 if (view->lineno == view->lines - 1 &&
1238 view->lines > view->height) {
1239 steps = view->lines - view->offset - 1;
1240 if (steps >= view->height)
1241 steps -= view->height - 1;
1245 do_scroll_view(view, steps, redraw);
1249 /* Draw the current line */
1250 draw_view_line(view, view->lineno - view->offset);
1255 redrawwin(view->win);
1256 wrefresh(view->win);
1262 * Incremental updating
1266 end_update(struct view *view)
1270 set_nonblocking_input(FALSE);
1271 if (view->pipe == stdin)
1279 begin_update(struct view *view)
1281 const char *id = view->id;
1287 string_copy(view->cmd, opt_cmd);
1289 /* When running random commands, the view ref could have become
1290 * invalid so clear it. */
1293 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1295 if (!string_format(view->cmd, format, id, id, id, id, id))
1299 /* Special case for the pager view. */
1301 view->pipe = opt_pipe;
1304 view->pipe = popen(view->cmd, "r");
1310 set_nonblocking_input(TRUE);
1315 string_copy(view->vid, id);
1320 for (i = 0; i < view->lines; i++)
1321 if (view->line[i].data)
1322 free(view->line[i].data);
1328 view->start_time = time(NULL);
1333 static struct line *
1334 realloc_lines(struct view *view, size_t line_size)
1336 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1342 view->line_size = line_size;
1347 update_view(struct view *view)
1349 char buffer[BUFSIZ];
1351 /* The number of lines to read. If too low it will cause too much
1352 * redrawing (and possible flickering), if too high responsiveness
1354 unsigned long lines = view->height;
1355 int redraw_from = -1;
1360 /* Only redraw if lines are visible. */
1361 if (view->offset + view->height >= view->lines)
1362 redraw_from = view->lines - view->offset;
1364 if (!realloc_lines(view, view->lines + lines))
1367 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1368 int linelen = strlen(line);
1370 struct line *prev = view->lines
1371 ? &view->line[view->lines - 1]
1375 line[linelen - 1] = 0;
1377 if (!view->ops->read(view, prev, line))
1387 lines = view->lines;
1388 for (digits = 0; lines; digits++)
1391 /* Keep the displayed view in sync with line number scaling. */
1392 if (digits != view->digits) {
1393 view->digits = digits;
1398 if (redraw_from >= 0) {
1399 /* If this is an incremental update, redraw the previous line
1400 * since for commits some members could have changed when
1401 * loading the main view. */
1402 if (redraw_from > 0)
1405 /* Incrementally draw avoids flickering. */
1406 redraw_view_from(view, redraw_from);
1409 /* Update the title _after_ the redraw so that if the redraw picks up a
1410 * commit reference in view->ref it'll be available here. */
1411 update_view_title(view);
1413 if (ferror(view->pipe)) {
1414 report("Failed to read: %s", strerror(errno));
1417 } else if (feof(view->pipe)) {
1425 report("Allocation failure");
1433 OPEN_DEFAULT = 0, /* Use default view switching. */
1434 OPEN_SPLIT = 1, /* Split current view. */
1435 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1436 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1440 open_view(struct view *prev, enum request request, enum open_flags flags)
1442 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1443 bool split = !!(flags & OPEN_SPLIT);
1444 bool reload = !!(flags & OPEN_RELOAD);
1445 struct view *view = VIEW(request);
1446 int nviews = displayed_views();
1447 struct view *base_view = display[0];
1449 if (view == prev && nviews == 1 && !reload) {
1450 report("Already in %s view", view->name);
1454 if ((reload || strcmp(view->vid, view->id)) &&
1455 !begin_update(view)) {
1456 report("Failed to load %s view", view->name);
1465 /* Maximize the current view. */
1466 memset(display, 0, sizeof(display));
1468 display[current_view] = view;
1471 /* Resize the view when switching between split- and full-screen,
1472 * or when switching between two different full-screen views. */
1473 if (nviews != displayed_views() ||
1474 (nviews == 1 && base_view != display[0]))
1477 if (split && prev->lineno - prev->offset >= prev->height) {
1478 /* Take the title line into account. */
1479 int lines = prev->lineno - prev->offset - prev->height + 1;
1481 /* Scroll the view that was split if the current line is
1482 * outside the new limited view. */
1483 do_scroll_view(prev, lines, TRUE);
1486 if (prev && view != prev) {
1487 if (split && !backgrounded) {
1488 /* "Blur" the previous view. */
1489 update_view_title(prev);
1492 view->parent = prev;
1495 if (view == VIEW(REQ_VIEW_HELP))
1498 if (view->pipe && view->lines == 0) {
1499 /* Clear the old view and let the incremental updating refill
1508 /* If the view is backgrounded the above calls to report()
1509 * won't redraw the view title. */
1511 update_view_title(view);
1516 * User request switch noodle
1520 view_driver(struct view *view, enum request request)
1527 case REQ_MOVE_PAGE_UP:
1528 case REQ_MOVE_PAGE_DOWN:
1529 case REQ_MOVE_FIRST_LINE:
1530 case REQ_MOVE_LAST_LINE:
1531 move_view(view, request, TRUE);
1534 case REQ_SCROLL_LINE_DOWN:
1535 case REQ_SCROLL_LINE_UP:
1536 case REQ_SCROLL_PAGE_DOWN:
1537 case REQ_SCROLL_PAGE_UP:
1538 scroll_view(view, request);
1545 case REQ_VIEW_PAGER:
1546 open_view(view, request, OPEN_DEFAULT);
1551 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1553 if (view == VIEW(REQ_VIEW_DIFF) &&
1554 view->parent == VIEW(REQ_VIEW_MAIN)) {
1555 bool redraw = display[1] == view;
1557 view = view->parent;
1558 move_view(view, request, redraw);
1560 update_view_title(view);
1562 move_view(view, request, TRUE);
1569 report("Nothing to enter");
1572 return view->ops->enter(view, &view->line[view->lineno]);
1576 int nviews = displayed_views();
1577 int next_view = (current_view + 1) % nviews;
1579 if (next_view == current_view) {
1580 report("Only one view is displayed");
1584 current_view = next_view;
1585 /* Blur out the title of the previous view. */
1586 update_view_title(view);
1590 case REQ_TOGGLE_LINENO:
1591 opt_line_number = !opt_line_number;
1596 /* Always reload^Wrerun commands from the prompt. */
1597 open_view(view, opt_request, OPEN_RELOAD);
1600 case REQ_STOP_LOADING:
1601 for (i = 0; i < ARRAY_SIZE(views); i++) {
1604 report("Stopped loading the %s view", view->name),
1609 case REQ_SHOW_VERSION:
1610 report("%s (built %s)", VERSION, __DATE__);
1613 case REQ_SCREEN_RESIZE:
1616 case REQ_SCREEN_REDRAW:
1620 case REQ_SCREEN_UPDATE:
1624 case REQ_VIEW_CLOSE:
1625 /* XXX: Mark closed views by letting view->parent point to the
1626 * view itself. Parents to closed view should never be
1629 view->parent->parent != view->parent) {
1630 memset(display, 0, sizeof(display));
1632 display[current_view] = view->parent;
1633 view->parent = view;
1643 /* An unknown key will show most commonly used commands. */
1644 report("Unknown key, press 'h' for help");
1657 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1659 char *text = line->data;
1660 enum line_type type = line->type;
1661 int textlen = strlen(text);
1664 wmove(view->win, lineno, 0);
1666 if (view->offset + lineno == view->lineno) {
1667 if (type == LINE_COMMIT) {
1668 string_copy(view->ref, text + 7);
1669 string_copy(ref_commit, view->ref);
1673 wchgat(view->win, -1, 0, type, NULL);
1676 attr = get_line_attr(type);
1677 wattrset(view->win, attr);
1679 if (opt_line_number || opt_tab_size < TABSIZE) {
1680 static char spaces[] = " ";
1681 int col_offset = 0, col = 0;
1683 if (opt_line_number) {
1684 unsigned long real_lineno = view->offset + lineno + 1;
1686 if (real_lineno == 1 ||
1687 (real_lineno % opt_num_interval) == 0) {
1688 wprintw(view->win, "%.*d", view->digits, real_lineno);
1691 waddnstr(view->win, spaces,
1692 MIN(view->digits, STRING_SIZE(spaces)));
1694 waddstr(view->win, ": ");
1695 col_offset = view->digits + 2;
1698 while (text && col_offset + col < view->width) {
1699 int cols_max = view->width - col_offset - col;
1703 if (*text == '\t') {
1705 assert(sizeof(spaces) > TABSIZE);
1707 cols = opt_tab_size - (col % opt_tab_size);
1710 text = strchr(text, '\t');
1711 cols = line ? text - pos : strlen(pos);
1714 waddnstr(view->win, pos, MIN(cols, cols_max));
1719 int col = 0, pos = 0;
1721 for (; pos < textlen && col < view->width; pos++, col++)
1722 if (text[pos] == '\t')
1723 col += TABSIZE - (col % TABSIZE) - 1;
1725 waddnstr(view->win, text, pos);
1732 add_pager_refs(struct view *view, struct line *line)
1735 char *data = line->data;
1737 int bufpos = 0, refpos = 0;
1738 const char *sep = "Refs: ";
1740 assert(line->type == LINE_COMMIT);
1742 refs = get_refs(data + STRING_SIZE("commit "));
1747 struct ref *ref = refs[refpos];
1748 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1750 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1753 } while (refs[refpos++]->next);
1755 if (!realloc_lines(view, view->line_size + 1))
1758 line = &view->line[view->lines];
1759 line->data = strdup(buf);
1763 line->type = LINE_PP_REFS;
1768 pager_read(struct view *view, struct line *prev, char *data)
1770 struct line *line = &view->line[view->lines];
1772 line->data = strdup(data);
1776 line->type = get_line_type(line->data);
1779 if (line->type == LINE_COMMIT &&
1780 (view == VIEW(REQ_VIEW_DIFF) ||
1781 view == VIEW(REQ_VIEW_LOG)))
1782 add_pager_refs(view, line);
1788 pager_enter(struct view *view, struct line *line)
1792 if (line->type == LINE_COMMIT &&
1793 (view == VIEW(REQ_VIEW_LOG) ||
1794 view == VIEW(REQ_VIEW_PAGER))) {
1795 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1799 /* Always scroll the view even if it was split. That way
1800 * you can use Enter to scroll through the log view and
1801 * split open each commit diff. */
1802 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1804 /* FIXME: A minor workaround. Scrolling the view will call report("")
1805 * but if we are scrolling a non-current view this won't properly
1806 * update the view title. */
1808 update_view_title(view);
1813 static struct view_ops pager_ops = {
1826 char id[41]; /* SHA1 ID. */
1827 char title[75]; /* The first line of the commit message. */
1828 char author[75]; /* The author of the commit. */
1829 struct tm time; /* Date from the author ident. */
1830 struct ref **refs; /* Repository references; tags & branch heads. */
1834 main_draw(struct view *view, struct line *line, unsigned int lineno)
1836 char buf[DATE_COLS + 1];
1837 struct commit *commit = line->data;
1838 enum line_type type;
1844 if (!*commit->author)
1847 wmove(view->win, lineno, col);
1849 if (view->offset + lineno == view->lineno) {
1850 string_copy(view->ref, commit->id);
1851 string_copy(ref_commit, view->ref);
1853 wattrset(view->win, get_line_attr(type));
1854 wchgat(view->win, -1, 0, type, NULL);
1857 type = LINE_MAIN_COMMIT;
1858 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1861 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1862 waddnstr(view->win, buf, timelen);
1863 waddstr(view->win, " ");
1866 wmove(view->win, lineno, col);
1867 if (type != LINE_CURSOR)
1868 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1871 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1873 authorlen = strlen(commit->author);
1874 if (authorlen > AUTHOR_COLS - 2) {
1875 authorlen = AUTHOR_COLS - 2;
1881 waddnstr(view->win, commit->author, authorlen);
1882 if (type != LINE_CURSOR)
1883 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1884 waddch(view->win, '~');
1886 waddstr(view->win, commit->author);
1890 if (type != LINE_CURSOR)
1891 wattrset(view->win, A_NORMAL);
1893 mvwaddch(view->win, lineno, col, ACS_LTEE);
1894 wmove(view->win, lineno, col + 2);
1901 if (type == LINE_CURSOR)
1903 else if (commit->refs[i]->tag)
1904 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1906 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1907 waddstr(view->win, "[");
1908 waddstr(view->win, commit->refs[i]->name);
1909 waddstr(view->win, "]");
1910 if (type != LINE_CURSOR)
1911 wattrset(view->win, A_NORMAL);
1912 waddstr(view->win, " ");
1913 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1914 } while (commit->refs[i++]->next);
1917 if (type != LINE_CURSOR)
1918 wattrset(view->win, get_line_attr(type));
1921 int titlelen = strlen(commit->title);
1923 if (col + titlelen > view->width)
1924 titlelen = view->width - col;
1926 waddnstr(view->win, commit->title, titlelen);
1932 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1934 main_read(struct view *view, struct line *prev, char *line)
1936 enum line_type type = get_line_type(line);
1937 struct commit *commit;
1941 commit = calloc(1, sizeof(struct commit));
1945 line += STRING_SIZE("commit ");
1947 view->line[view->lines++].data = commit;
1948 string_copy(commit->id, line);
1949 commit->refs = get_refs(commit->id);
1954 char *ident = line + STRING_SIZE("author ");
1955 char *end = strchr(ident, '<');
1960 commit = prev->data;
1963 for (; end > ident && isspace(end[-1]); end--) ;
1967 string_copy(commit->author, ident);
1969 /* Parse epoch and timezone */
1971 char *secs = strchr(end + 1, '>');
1975 if (!secs || secs[1] != ' ')
1979 time = (time_t) atol(secs);
1980 zone = strchr(secs, ' ');
1981 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1985 tz = ('0' - zone[1]) * 60 * 60 * 10;
1986 tz += ('0' - zone[2]) * 60 * 60;
1987 tz += ('0' - zone[3]) * 60;
1988 tz += ('0' - zone[4]) * 60;
1995 gmtime_r(&time, &commit->time);
2003 commit = prev->data;
2005 /* Fill in the commit title if it has not already been set. */
2006 if (commit->title[0])
2009 /* Require titles to start with a non-space character at the
2010 * offset used by git log. */
2011 /* FIXME: More gracefull handling of titles; append "..." to
2012 * shortened titles, etc. */
2013 if (strncmp(line, " ", 4) ||
2017 string_copy(commit->title, line + 4);
2024 main_enter(struct view *view, struct line *line)
2026 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2028 open_view(view, REQ_VIEW_DIFF, flags);
2032 static struct view_ops main_ops = {
2049 static struct keymap keymap[] = {
2050 /* View switching */
2051 { 'm', REQ_VIEW_MAIN },
2052 { 'd', REQ_VIEW_DIFF },
2053 { 'l', REQ_VIEW_LOG },
2054 { 'p', REQ_VIEW_PAGER },
2055 { 'h', REQ_VIEW_HELP },
2056 { '?', REQ_VIEW_HELP },
2058 /* View manipulation */
2059 { 'q', REQ_VIEW_CLOSE },
2060 { KEY_TAB, REQ_VIEW_NEXT },
2061 { KEY_RETURN, REQ_ENTER },
2062 { KEY_UP, REQ_PREVIOUS },
2063 { KEY_DOWN, REQ_NEXT },
2065 /* Cursor navigation */
2066 { 'k', REQ_MOVE_UP },
2067 { 'j', REQ_MOVE_DOWN },
2068 { KEY_HOME, REQ_MOVE_FIRST_LINE },
2069 { KEY_END, REQ_MOVE_LAST_LINE },
2070 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
2071 { ' ', REQ_MOVE_PAGE_DOWN },
2072 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
2073 { 'b', REQ_MOVE_PAGE_UP },
2074 { '-', REQ_MOVE_PAGE_UP },
2077 { KEY_IC, REQ_SCROLL_LINE_UP },
2078 { KEY_DC, REQ_SCROLL_LINE_DOWN },
2079 { 'w', REQ_SCROLL_PAGE_UP },
2080 { 's', REQ_SCROLL_PAGE_DOWN },
2084 { 'z', REQ_STOP_LOADING },
2085 { 'v', REQ_SHOW_VERSION },
2086 { 'r', REQ_SCREEN_REDRAW },
2087 { 'n', REQ_TOGGLE_LINENO },
2088 { ':', REQ_PROMPT },
2090 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2091 { ERR, REQ_SCREEN_UPDATE },
2093 /* Use the ncurses SIGWINCH handler. */
2094 { KEY_RESIZE, REQ_SCREEN_RESIZE },
2098 get_request(int key)
2102 for (i = 0; i < ARRAY_SIZE(keymap); i++)
2103 if (keymap[i].alias == key)
2104 return keymap[i].request;
2106 return (enum request) key;
2114 static struct key key_table[] = {
2115 { "Enter", KEY_RETURN },
2117 { "Backspace", KEY_BACKSPACE },
2119 { "Escape", KEY_ESC },
2120 { "Left", KEY_LEFT },
2121 { "Right", KEY_RIGHT },
2123 { "Down", KEY_DOWN },
2124 { "Insert", KEY_IC },
2125 { "Delete", KEY_DC },
2126 { "Home", KEY_HOME },
2128 { "PageUp", KEY_PPAGE },
2129 { "PageDown", KEY_NPAGE },
2139 { "F10", KEY_F(10) },
2140 { "F11", KEY_F(11) },
2141 { "F12", KEY_F(12) },
2145 get_key(enum request request)
2147 static char buf[BUFSIZ];
2148 static char key_char[] = "'X'";
2155 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2159 if (keymap[i].request != request)
2162 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2163 if (key_table[key].value == keymap[i].alias)
2164 seq = key_table[key].name;
2167 keymap[i].alias < 127 &&
2168 isprint(keymap[i].alias)) {
2169 key_char[1] = (char) keymap[i].alias;
2176 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
2177 return "Too many keybindings!";
2184 static void load_help_page(void)
2187 struct view *view = VIEW(REQ_VIEW_HELP);
2188 int lines = ARRAY_SIZE(req_info) + 2;
2191 if (view->lines > 0)
2194 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2195 if (!req_info[i].request)
2198 view->line = calloc(lines, sizeof(*view->line));
2200 report("Allocation failure");
2204 pager_read(view, NULL, "Quick reference for tig keybindings:");
2206 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2209 if (!req_info[i].request) {
2210 pager_read(view, NULL, "");
2211 pager_read(view, NULL, req_info[i].help);
2215 key = get_key(req_info[i].request);
2216 if (string_format(buf, "%-25s %s", key, req_info[i].help))
2219 pager_read(view, NULL, buf);
2225 * Unicode / UTF-8 handling
2227 * NOTE: Much of the following code for dealing with unicode is derived from
2228 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2229 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2232 /* I've (over)annotated a lot of code snippets because I am not entirely
2233 * confident that the approach taken by this small UTF-8 interface is correct.
2237 unicode_width(unsigned long c)
2240 (c <= 0x115f /* Hangul Jamo */
2243 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2245 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2246 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2247 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2248 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2249 || (c >= 0xffe0 && c <= 0xffe6)
2250 || (c >= 0x20000 && c <= 0x2fffd)
2251 || (c >= 0x30000 && c <= 0x3fffd)))
2257 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2258 * Illegal bytes are set one. */
2259 static const unsigned char utf8_bytes[256] = {
2260 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,
2261 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,
2262 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,
2263 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,
2264 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,
2265 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,
2266 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,
2267 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,
2270 /* Decode UTF-8 multi-byte representation into a unicode character. */
2271 static inline unsigned long
2272 utf8_to_unicode(const char *string, size_t length)
2274 unsigned long unicode;
2278 unicode = string[0];
2281 unicode = (string[0] & 0x1f) << 6;
2282 unicode += (string[1] & 0x3f);
2285 unicode = (string[0] & 0x0f) << 12;
2286 unicode += ((string[1] & 0x3f) << 6);
2287 unicode += (string[2] & 0x3f);
2290 unicode = (string[0] & 0x0f) << 18;
2291 unicode += ((string[1] & 0x3f) << 12);
2292 unicode += ((string[2] & 0x3f) << 6);
2293 unicode += (string[3] & 0x3f);
2296 unicode = (string[0] & 0x0f) << 24;
2297 unicode += ((string[1] & 0x3f) << 18);
2298 unicode += ((string[2] & 0x3f) << 12);
2299 unicode += ((string[3] & 0x3f) << 6);
2300 unicode += (string[4] & 0x3f);
2303 unicode = (string[0] & 0x01) << 30;
2304 unicode += ((string[1] & 0x3f) << 24);
2305 unicode += ((string[2] & 0x3f) << 18);
2306 unicode += ((string[3] & 0x3f) << 12);
2307 unicode += ((string[4] & 0x3f) << 6);
2308 unicode += (string[5] & 0x3f);
2311 die("Invalid unicode length");
2314 /* Invalid characters could return the special 0xfffd value but NUL
2315 * should be just as good. */
2316 return unicode > 0xffff ? 0 : unicode;
2319 /* Calculates how much of string can be shown within the given maximum width
2320 * and sets trimmed parameter to non-zero value if all of string could not be
2323 * Additionally, adds to coloffset how many many columns to move to align with
2324 * the expected position. Takes into account how multi-byte and double-width
2325 * characters will effect the cursor position.
2327 * Returns the number of bytes to output from string to satisfy max_width. */
2329 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2331 const char *start = string;
2332 const char *end = strchr(string, '\0');
2338 while (string < end) {
2339 int c = *(unsigned char *) string;
2340 unsigned char bytes = utf8_bytes[c];
2342 unsigned long unicode;
2344 if (string + bytes > end)
2347 /* Change representation to figure out whether
2348 * it is a single- or double-width character. */
2350 unicode = utf8_to_unicode(string, bytes);
2351 /* FIXME: Graceful handling of invalid unicode character. */
2355 ucwidth = unicode_width(unicode);
2357 if (width > max_width) {
2362 /* The column offset collects the differences between the
2363 * number of bytes encoding a character and the number of
2364 * columns will be used for rendering said character.
2366 * So if some character A is encoded in 2 bytes, but will be
2367 * represented on the screen using only 1 byte this will and up
2368 * adding 1 to the multi-byte column offset.
2370 * Assumes that no double-width character can be encoding in
2371 * less than two bytes. */
2372 if (bytes > ucwidth)
2373 mbwidth += bytes - ucwidth;
2378 *coloffset += mbwidth;
2380 return string - start;
2388 /* Whether or not the curses interface has been initialized. */
2389 static bool cursed = FALSE;
2391 /* The status window is used for polling keystrokes. */
2392 static WINDOW *status_win;
2394 /* Update status and title window. */
2396 report(const char *msg, ...)
2398 static bool empty = TRUE;
2399 struct view *view = display[current_view];
2401 if (!empty || *msg) {
2404 va_start(args, msg);
2407 wmove(status_win, 0, 0);
2409 vwprintw(status_win, msg, args);
2414 wrefresh(status_win);
2419 update_view_title(view);
2420 update_display_cursor();
2423 /* Controls when nodelay should be in effect when polling user input. */
2425 set_nonblocking_input(bool loading)
2427 static unsigned int loading_views;
2429 if ((loading == FALSE && loading_views-- == 1) ||
2430 (loading == TRUE && loading_views++ == 0))
2431 nodelay(status_win, loading);
2439 /* Initialize the curses library */
2440 if (isatty(STDIN_FILENO)) {
2441 cursed = !!initscr();
2443 /* Leave stdin and stdout alone when acting as a pager. */
2444 FILE *io = fopen("/dev/tty", "r+");
2446 cursed = !!newterm(NULL, io, io);
2450 die("Failed to initialize curses");
2452 nonl(); /* Tell curses not to do NL->CR/NL on output */
2453 cbreak(); /* Take input chars one at a time, no wait for \n */
2454 noecho(); /* Don't echo input */
2455 leaveok(stdscr, TRUE);
2460 getmaxyx(stdscr, y, x);
2461 status_win = newwin(1, 0, y - 1, 0);
2463 die("Failed to create status window");
2465 /* Enable keyboard mapping */
2466 keypad(status_win, TRUE);
2467 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2472 * Repository references
2475 static struct ref *refs;
2476 static size_t refs_size;
2478 /* Id <-> ref store */
2479 static struct ref ***id_refs;
2480 static size_t id_refs_size;
2482 static struct ref **
2485 struct ref ***tmp_id_refs;
2486 struct ref **ref_list = NULL;
2487 size_t ref_list_size = 0;
2490 for (i = 0; i < id_refs_size; i++)
2491 if (!strcmp(id, id_refs[i][0]->id))
2494 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2498 id_refs = tmp_id_refs;
2500 for (i = 0; i < refs_size; i++) {
2503 if (strcmp(id, refs[i].id))
2506 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2514 if (ref_list_size > 0)
2515 ref_list[ref_list_size - 1]->next = 1;
2516 ref_list[ref_list_size] = &refs[i];
2518 /* XXX: The properties of the commit chains ensures that we can
2519 * safely modify the shared ref. The repo references will
2520 * always be similar for the same id. */
2521 ref_list[ref_list_size]->next = 0;
2526 id_refs[id_refs_size++] = ref_list;
2532 read_ref(char *id, int idlen, char *name, int namelen)
2536 bool tag_commit = FALSE;
2538 /* Commits referenced by tags has "^{}" appended. */
2539 if (name[namelen - 1] == '}') {
2540 while (namelen > 0 && name[namelen] != '^')
2547 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2550 name += STRING_SIZE("refs/tags/");
2553 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2554 name += STRING_SIZE("refs/heads/");
2556 } else if (!strcmp(name, "HEAD")) {
2560 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2564 ref = &refs[refs_size++];
2565 ref->name = strdup(name);
2570 string_copy(ref->id, id);
2578 const char *cmd_env = getenv("TIG_LS_REMOTE");
2579 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2581 return read_properties(popen(cmd, "r"), "\t", read_ref);
2585 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2587 if (!strcmp(name, "i18n.commitencoding")) {
2588 string_copy(opt_encoding, value);
2595 load_repo_config(void)
2597 return read_properties(popen("git repo-config --list", "r"),
2598 "=", read_repo_config_option);
2602 read_properties(FILE *pipe, const char *separators,
2603 int (*read_property)(char *, int, char *, int))
2605 char buffer[BUFSIZ];
2612 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2617 name = chomp_string(name);
2618 namelen = strcspn(name, separators);
2620 if (name[namelen]) {
2622 value = chomp_string(name + namelen + 1);
2623 valuelen = strlen(value);
2630 state = read_property(name, namelen, value, valuelen);
2633 if (state != ERR && ferror(pipe))
2647 #define __NORETURN __attribute__((__noreturn__))
2652 static void __NORETURN
2655 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2661 static void __NORETURN
2662 die(const char *err, ...)
2668 va_start(args, err);
2669 fputs("tig: ", stderr);
2670 vfprintf(stderr, err, args);
2671 fputs("\n", stderr);
2678 main(int argc, char *argv[])
2681 enum request request;
2684 signal(SIGINT, quit);
2686 if (load_options() == ERR)
2687 die("Failed to load user config.");
2689 /* Load the repo config file so options can be overwritten from
2690 * the command line. */
2691 if (load_repo_config() == ERR)
2692 die("Failed to load repo config.");
2694 if (!parse_options(argc, argv))
2697 if (load_refs() == ERR)
2698 die("Failed to load refs.");
2700 /* Require a git repository unless when running in pager mode. */
2701 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2702 die("Not a git repository");
2704 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2705 view->cmd_env = getenv(view->cmd_env);
2707 request = opt_request;
2711 while (view_driver(display[current_view], request)) {
2715 foreach_view (view, i)
2718 /* Refresh, accept single keystroke of input */
2719 key = wgetch(status_win);
2720 request = get_request(key);
2722 /* Some low-level request handling. This keeps access to
2723 * status_win restricted. */
2727 /* Temporarily switch to line-oriented and echoed
2732 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2733 memcpy(opt_cmd, "git ", 4);
2734 opt_request = REQ_VIEW_PAGER;
2736 report("Prompt interrupted by loading view, "
2737 "press 'z' to stop loading views");
2738 request = REQ_SCREEN_UPDATE;
2745 case REQ_SCREEN_RESIZE:
2749 getmaxyx(stdscr, height, width);
2751 /* Resize the status view and let the view driver take
2752 * care of resizing the displayed views. */
2753 wresize(status_win, 1, width);
2754 mvwin(status_win, height - 1, 0);
2755 wrefresh(status_win);
2773 * Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2775 * This program is free software; you can redistribute it and/or modify
2776 * it under the terms of the GNU General Public License as published by
2777 * the Free Software Foundation; either version 2 of the License, or
2778 * (at your option) any later version.
2782 * - link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
2783 * - link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
2785 * Other git repository browsers: