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, 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);
1371 line[linelen - 1] = 0;
1373 if (!view->ops->read(view, line))
1383 lines = view->lines;
1384 for (digits = 0; lines; digits++)
1387 /* Keep the displayed view in sync with line number scaling. */
1388 if (digits != view->digits) {
1389 view->digits = digits;
1394 if (redraw_from >= 0) {
1395 /* If this is an incremental update, redraw the previous line
1396 * since for commits some members could have changed when
1397 * loading the main view. */
1398 if (redraw_from > 0)
1401 /* Incrementally draw avoids flickering. */
1402 redraw_view_from(view, redraw_from);
1405 /* Update the title _after_ the redraw so that if the redraw picks up a
1406 * commit reference in view->ref it'll be available here. */
1407 update_view_title(view);
1409 if (ferror(view->pipe)) {
1410 report("Failed to read: %s", strerror(errno));
1413 } else if (feof(view->pipe)) {
1421 report("Allocation failure");
1429 OPEN_DEFAULT = 0, /* Use default view switching. */
1430 OPEN_SPLIT = 1, /* Split current view. */
1431 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1432 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1436 open_view(struct view *prev, enum request request, enum open_flags flags)
1438 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1439 bool split = !!(flags & OPEN_SPLIT);
1440 bool reload = !!(flags & OPEN_RELOAD);
1441 struct view *view = VIEW(request);
1442 int nviews = displayed_views();
1443 struct view *base_view = display[0];
1445 if (view == prev && nviews == 1 && !reload) {
1446 report("Already in %s view", view->name);
1450 if ((reload || strcmp(view->vid, view->id)) &&
1451 !begin_update(view)) {
1452 report("Failed to load %s view", view->name);
1461 /* Maximize the current view. */
1462 memset(display, 0, sizeof(display));
1464 display[current_view] = view;
1467 /* Resize the view when switching between split- and full-screen,
1468 * or when switching between two different full-screen views. */
1469 if (nviews != displayed_views() ||
1470 (nviews == 1 && base_view != display[0]))
1473 if (split && prev->lineno - prev->offset >= prev->height) {
1474 /* Take the title line into account. */
1475 int lines = prev->lineno - prev->offset - prev->height + 1;
1477 /* Scroll the view that was split if the current line is
1478 * outside the new limited view. */
1479 do_scroll_view(prev, lines, TRUE);
1482 if (prev && view != prev) {
1483 if (split && !backgrounded) {
1484 /* "Blur" the previous view. */
1485 update_view_title(prev);
1488 view->parent = prev;
1491 if (view == VIEW(REQ_VIEW_HELP))
1494 if (view->pipe && view->lines == 0) {
1495 /* Clear the old view and let the incremental updating refill
1504 /* If the view is backgrounded the above calls to report()
1505 * won't redraw the view title. */
1507 update_view_title(view);
1512 * User request switch noodle
1516 view_driver(struct view *view, enum request request)
1523 case REQ_MOVE_PAGE_UP:
1524 case REQ_MOVE_PAGE_DOWN:
1525 case REQ_MOVE_FIRST_LINE:
1526 case REQ_MOVE_LAST_LINE:
1527 move_view(view, request, TRUE);
1530 case REQ_SCROLL_LINE_DOWN:
1531 case REQ_SCROLL_LINE_UP:
1532 case REQ_SCROLL_PAGE_DOWN:
1533 case REQ_SCROLL_PAGE_UP:
1534 scroll_view(view, request);
1541 case REQ_VIEW_PAGER:
1542 open_view(view, request, OPEN_DEFAULT);
1547 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1549 if (view == VIEW(REQ_VIEW_DIFF) &&
1550 view->parent == VIEW(REQ_VIEW_MAIN)) {
1551 bool redraw = display[1] == view;
1553 view = view->parent;
1554 move_view(view, request, redraw);
1556 update_view_title(view);
1558 move_view(view, request, TRUE);
1565 report("Nothing to enter");
1568 return view->ops->enter(view, &view->line[view->lineno]);
1572 int nviews = displayed_views();
1573 int next_view = (current_view + 1) % nviews;
1575 if (next_view == current_view) {
1576 report("Only one view is displayed");
1580 current_view = next_view;
1581 /* Blur out the title of the previous view. */
1582 update_view_title(view);
1586 case REQ_TOGGLE_LINENO:
1587 opt_line_number = !opt_line_number;
1592 /* Always reload^Wrerun commands from the prompt. */
1593 open_view(view, opt_request, OPEN_RELOAD);
1596 case REQ_STOP_LOADING:
1597 for (i = 0; i < ARRAY_SIZE(views); i++) {
1600 report("Stopped loading the %s view", view->name),
1605 case REQ_SHOW_VERSION:
1606 report("%s (built %s)", VERSION, __DATE__);
1609 case REQ_SCREEN_RESIZE:
1612 case REQ_SCREEN_REDRAW:
1616 case REQ_SCREEN_UPDATE:
1620 case REQ_VIEW_CLOSE:
1621 /* XXX: Mark closed views by letting view->parent point to the
1622 * view itself. Parents to closed view should never be
1625 view->parent->parent != view->parent) {
1626 memset(display, 0, sizeof(display));
1628 display[current_view] = view->parent;
1629 view->parent = view;
1639 /* An unknown key will show most commonly used commands. */
1640 report("Unknown key, press 'h' for help");
1653 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1655 char *text = line->data;
1656 enum line_type type = line->type;
1657 int textlen = strlen(text);
1660 wmove(view->win, lineno, 0);
1662 if (view->offset + lineno == view->lineno) {
1663 if (type == LINE_COMMIT) {
1664 string_copy(view->ref, text + 7);
1665 string_copy(ref_commit, view->ref);
1669 wchgat(view->win, -1, 0, type, NULL);
1672 attr = get_line_attr(type);
1673 wattrset(view->win, attr);
1675 if (opt_line_number || opt_tab_size < TABSIZE) {
1676 static char spaces[] = " ";
1677 int col_offset = 0, col = 0;
1679 if (opt_line_number) {
1680 unsigned long real_lineno = view->offset + lineno + 1;
1682 if (real_lineno == 1 ||
1683 (real_lineno % opt_num_interval) == 0) {
1684 wprintw(view->win, "%.*d", view->digits, real_lineno);
1687 waddnstr(view->win, spaces,
1688 MIN(view->digits, STRING_SIZE(spaces)));
1690 waddstr(view->win, ": ");
1691 col_offset = view->digits + 2;
1694 while (text && col_offset + col < view->width) {
1695 int cols_max = view->width - col_offset - col;
1699 if (*text == '\t') {
1701 assert(sizeof(spaces) > TABSIZE);
1703 cols = opt_tab_size - (col % opt_tab_size);
1706 text = strchr(text, '\t');
1707 cols = line ? text - pos : strlen(pos);
1710 waddnstr(view->win, pos, MIN(cols, cols_max));
1715 int col = 0, pos = 0;
1717 for (; pos < textlen && col < view->width; pos++, col++)
1718 if (text[pos] == '\t')
1719 col += TABSIZE - (col % TABSIZE) - 1;
1721 waddnstr(view->win, text, pos);
1728 add_pager_refs(struct view *view, struct line *line)
1731 char *data = line->data;
1733 int bufpos = 0, refpos = 0;
1734 const char *sep = "Refs: ";
1736 assert(line->type == LINE_COMMIT);
1738 refs = get_refs(data + STRING_SIZE("commit "));
1743 struct ref *ref = refs[refpos];
1744 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1746 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1749 } while (refs[refpos++]->next);
1751 if (!realloc_lines(view, view->line_size + 1))
1754 line = &view->line[view->lines];
1755 line->data = strdup(buf);
1759 line->type = LINE_PP_REFS;
1764 pager_read(struct view *view, char *data)
1766 struct line *line = &view->line[view->lines];
1768 line->data = strdup(data);
1772 line->type = get_line_type(line->data);
1775 if (line->type == LINE_COMMIT &&
1776 (view == VIEW(REQ_VIEW_DIFF) ||
1777 view == VIEW(REQ_VIEW_LOG)))
1778 add_pager_refs(view, line);
1784 pager_enter(struct view *view, struct line *line)
1788 if (line->type == LINE_COMMIT &&
1789 (view == VIEW(REQ_VIEW_LOG) ||
1790 view == VIEW(REQ_VIEW_PAGER))) {
1791 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1795 /* Always scroll the view even if it was split. That way
1796 * you can use Enter to scroll through the log view and
1797 * split open each commit diff. */
1798 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1800 /* FIXME: A minor workaround. Scrolling the view will call report("")
1801 * but if we are scrolling a non-current view this won't properly
1802 * update the view title. */
1804 update_view_title(view);
1809 static struct view_ops pager_ops = {
1822 char id[41]; /* SHA1 ID. */
1823 char title[75]; /* The first line of the commit message. */
1824 char author[75]; /* The author of the commit. */
1825 struct tm time; /* Date from the author ident. */
1826 struct ref **refs; /* Repository references; tags & branch heads. */
1830 main_draw(struct view *view, struct line *line, unsigned int lineno)
1832 char buf[DATE_COLS + 1];
1833 struct commit *commit = line->data;
1834 enum line_type type;
1840 if (!*commit->author)
1843 wmove(view->win, lineno, col);
1845 if (view->offset + lineno == view->lineno) {
1846 string_copy(view->ref, commit->id);
1847 string_copy(ref_commit, view->ref);
1849 wattrset(view->win, get_line_attr(type));
1850 wchgat(view->win, -1, 0, type, NULL);
1853 type = LINE_MAIN_COMMIT;
1854 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1857 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1858 waddnstr(view->win, buf, timelen);
1859 waddstr(view->win, " ");
1862 wmove(view->win, lineno, col);
1863 if (type != LINE_CURSOR)
1864 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1867 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1869 authorlen = strlen(commit->author);
1870 if (authorlen > AUTHOR_COLS - 2) {
1871 authorlen = AUTHOR_COLS - 2;
1877 waddnstr(view->win, commit->author, authorlen);
1878 if (type != LINE_CURSOR)
1879 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1880 waddch(view->win, '~');
1882 waddstr(view->win, commit->author);
1886 if (type != LINE_CURSOR)
1887 wattrset(view->win, A_NORMAL);
1889 mvwaddch(view->win, lineno, col, ACS_LTEE);
1890 wmove(view->win, lineno, col + 2);
1897 if (type == LINE_CURSOR)
1899 else if (commit->refs[i]->tag)
1900 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1902 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1903 waddstr(view->win, "[");
1904 waddstr(view->win, commit->refs[i]->name);
1905 waddstr(view->win, "]");
1906 if (type != LINE_CURSOR)
1907 wattrset(view->win, A_NORMAL);
1908 waddstr(view->win, " ");
1909 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1910 } while (commit->refs[i++]->next);
1913 if (type != LINE_CURSOR)
1914 wattrset(view->win, get_line_attr(type));
1917 int titlelen = strlen(commit->title);
1919 if (col + titlelen > view->width)
1920 titlelen = view->width - col;
1922 waddnstr(view->win, commit->title, titlelen);
1928 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1930 main_read(struct view *view, char *line)
1932 enum line_type type = get_line_type(line);
1933 struct commit *commit = view->lines
1934 ? view->line[view->lines - 1].data : NULL;
1938 commit = calloc(1, sizeof(struct commit));
1942 line += STRING_SIZE("commit ");
1944 view->line[view->lines++].data = commit;
1945 string_copy(commit->id, line);
1946 commit->refs = get_refs(commit->id);
1951 char *ident = line + STRING_SIZE("author ");
1952 char *end = strchr(ident, '<');
1958 for (; end > ident && isspace(end[-1]); end--) ;
1962 string_copy(commit->author, ident);
1964 /* Parse epoch and timezone */
1966 char *secs = strchr(end + 1, '>');
1970 if (!secs || secs[1] != ' ')
1974 time = (time_t) atol(secs);
1975 zone = strchr(secs, ' ');
1976 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1980 tz = ('0' - zone[1]) * 60 * 60 * 10;
1981 tz += ('0' - zone[2]) * 60 * 60;
1982 tz += ('0' - zone[3]) * 60;
1983 tz += ('0' - zone[4]) * 60;
1990 gmtime_r(&time, &commit->time);
1998 /* Fill in the commit title if it has not already been set. */
1999 if (commit->title[0])
2002 /* Require titles to start with a non-space character at the
2003 * offset used by git log. */
2004 /* FIXME: More gracefull handling of titles; append "..." to
2005 * shortened titles, etc. */
2006 if (strncmp(line, " ", 4) ||
2010 string_copy(commit->title, line + 4);
2017 main_enter(struct view *view, struct line *line)
2019 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2021 open_view(view, REQ_VIEW_DIFF, flags);
2025 static struct view_ops main_ops = {
2042 static struct keymap keymap[] = {
2043 /* View switching */
2044 { 'm', REQ_VIEW_MAIN },
2045 { 'd', REQ_VIEW_DIFF },
2046 { 'l', REQ_VIEW_LOG },
2047 { 'p', REQ_VIEW_PAGER },
2048 { 'h', REQ_VIEW_HELP },
2049 { '?', REQ_VIEW_HELP },
2051 /* View manipulation */
2052 { 'q', REQ_VIEW_CLOSE },
2053 { KEY_TAB, REQ_VIEW_NEXT },
2054 { KEY_RETURN, REQ_ENTER },
2055 { KEY_UP, REQ_PREVIOUS },
2056 { KEY_DOWN, REQ_NEXT },
2058 /* Cursor navigation */
2059 { 'k', REQ_MOVE_UP },
2060 { 'j', REQ_MOVE_DOWN },
2061 { KEY_HOME, REQ_MOVE_FIRST_LINE },
2062 { KEY_END, REQ_MOVE_LAST_LINE },
2063 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
2064 { ' ', REQ_MOVE_PAGE_DOWN },
2065 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
2066 { 'b', REQ_MOVE_PAGE_UP },
2067 { '-', REQ_MOVE_PAGE_UP },
2070 { KEY_IC, REQ_SCROLL_LINE_UP },
2071 { KEY_DC, REQ_SCROLL_LINE_DOWN },
2072 { 'w', REQ_SCROLL_PAGE_UP },
2073 { 's', REQ_SCROLL_PAGE_DOWN },
2077 { 'z', REQ_STOP_LOADING },
2078 { 'v', REQ_SHOW_VERSION },
2079 { 'r', REQ_SCREEN_REDRAW },
2080 { 'n', REQ_TOGGLE_LINENO },
2081 { ':', REQ_PROMPT },
2083 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2084 { ERR, REQ_SCREEN_UPDATE },
2086 /* Use the ncurses SIGWINCH handler. */
2087 { KEY_RESIZE, REQ_SCREEN_RESIZE },
2091 get_request(int key)
2095 for (i = 0; i < ARRAY_SIZE(keymap); i++)
2096 if (keymap[i].alias == key)
2097 return keymap[i].request;
2099 return (enum request) key;
2107 static struct key key_table[] = {
2108 { "Enter", KEY_RETURN },
2110 { "Backspace", KEY_BACKSPACE },
2112 { "Escape", KEY_ESC },
2113 { "Left", KEY_LEFT },
2114 { "Right", KEY_RIGHT },
2116 { "Down", KEY_DOWN },
2117 { "Insert", KEY_IC },
2118 { "Delete", KEY_DC },
2119 { "Home", KEY_HOME },
2121 { "PageUp", KEY_PPAGE },
2122 { "PageDown", KEY_NPAGE },
2132 { "F10", KEY_F(10) },
2133 { "F11", KEY_F(11) },
2134 { "F12", KEY_F(12) },
2138 get_key(enum request request)
2140 static char buf[BUFSIZ];
2141 static char key_char[] = "'X'";
2148 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2152 if (keymap[i].request != request)
2155 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2156 if (key_table[key].value == keymap[i].alias)
2157 seq = key_table[key].name;
2160 keymap[i].alias < 127 &&
2161 isprint(keymap[i].alias)) {
2162 key_char[1] = (char) keymap[i].alias;
2169 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
2170 return "Too many keybindings!";
2177 static void load_help_page(void)
2180 struct view *view = VIEW(REQ_VIEW_HELP);
2181 int lines = ARRAY_SIZE(req_info) + 2;
2184 if (view->lines > 0)
2187 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2188 if (!req_info[i].request)
2191 view->line = calloc(lines, sizeof(*view->line));
2193 report("Allocation failure");
2197 pager_read(view, "Quick reference for tig keybindings:");
2199 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2202 if (!req_info[i].request) {
2203 pager_read(view, "");
2204 pager_read(view, req_info[i].help);
2208 key = get_key(req_info[i].request);
2209 if (string_format(buf, "%-25s %s", key, req_info[i].help))
2212 pager_read(view, buf);
2218 * Unicode / UTF-8 handling
2220 * NOTE: Much of the following code for dealing with unicode is derived from
2221 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2222 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2225 /* I've (over)annotated a lot of code snippets because I am not entirely
2226 * confident that the approach taken by this small UTF-8 interface is correct.
2230 unicode_width(unsigned long c)
2233 (c <= 0x115f /* Hangul Jamo */
2236 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2238 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2239 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2240 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2241 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2242 || (c >= 0xffe0 && c <= 0xffe6)
2243 || (c >= 0x20000 && c <= 0x2fffd)
2244 || (c >= 0x30000 && c <= 0x3fffd)))
2250 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2251 * Illegal bytes are set one. */
2252 static const unsigned char utf8_bytes[256] = {
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 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,
2259 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,
2260 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,
2263 /* Decode UTF-8 multi-byte representation into a unicode character. */
2264 static inline unsigned long
2265 utf8_to_unicode(const char *string, size_t length)
2267 unsigned long unicode;
2271 unicode = string[0];
2274 unicode = (string[0] & 0x1f) << 6;
2275 unicode += (string[1] & 0x3f);
2278 unicode = (string[0] & 0x0f) << 12;
2279 unicode += ((string[1] & 0x3f) << 6);
2280 unicode += (string[2] & 0x3f);
2283 unicode = (string[0] & 0x0f) << 18;
2284 unicode += ((string[1] & 0x3f) << 12);
2285 unicode += ((string[2] & 0x3f) << 6);
2286 unicode += (string[3] & 0x3f);
2289 unicode = (string[0] & 0x0f) << 24;
2290 unicode += ((string[1] & 0x3f) << 18);
2291 unicode += ((string[2] & 0x3f) << 12);
2292 unicode += ((string[3] & 0x3f) << 6);
2293 unicode += (string[4] & 0x3f);
2296 unicode = (string[0] & 0x01) << 30;
2297 unicode += ((string[1] & 0x3f) << 24);
2298 unicode += ((string[2] & 0x3f) << 18);
2299 unicode += ((string[3] & 0x3f) << 12);
2300 unicode += ((string[4] & 0x3f) << 6);
2301 unicode += (string[5] & 0x3f);
2304 die("Invalid unicode length");
2307 /* Invalid characters could return the special 0xfffd value but NUL
2308 * should be just as good. */
2309 return unicode > 0xffff ? 0 : unicode;
2312 /* Calculates how much of string can be shown within the given maximum width
2313 * and sets trimmed parameter to non-zero value if all of string could not be
2316 * Additionally, adds to coloffset how many many columns to move to align with
2317 * the expected position. Takes into account how multi-byte and double-width
2318 * characters will effect the cursor position.
2320 * Returns the number of bytes to output from string to satisfy max_width. */
2322 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2324 const char *start = string;
2325 const char *end = strchr(string, '\0');
2331 while (string < end) {
2332 int c = *(unsigned char *) string;
2333 unsigned char bytes = utf8_bytes[c];
2335 unsigned long unicode;
2337 if (string + bytes > end)
2340 /* Change representation to figure out whether
2341 * it is a single- or double-width character. */
2343 unicode = utf8_to_unicode(string, bytes);
2344 /* FIXME: Graceful handling of invalid unicode character. */
2348 ucwidth = unicode_width(unicode);
2350 if (width > max_width) {
2355 /* The column offset collects the differences between the
2356 * number of bytes encoding a character and the number of
2357 * columns will be used for rendering said character.
2359 * So if some character A is encoded in 2 bytes, but will be
2360 * represented on the screen using only 1 byte this will and up
2361 * adding 1 to the multi-byte column offset.
2363 * Assumes that no double-width character can be encoding in
2364 * less than two bytes. */
2365 if (bytes > ucwidth)
2366 mbwidth += bytes - ucwidth;
2371 *coloffset += mbwidth;
2373 return string - start;
2381 /* Whether or not the curses interface has been initialized. */
2382 static bool cursed = FALSE;
2384 /* The status window is used for polling keystrokes. */
2385 static WINDOW *status_win;
2387 /* Update status and title window. */
2389 report(const char *msg, ...)
2391 static bool empty = TRUE;
2392 struct view *view = display[current_view];
2394 if (!empty || *msg) {
2397 va_start(args, msg);
2400 wmove(status_win, 0, 0);
2402 vwprintw(status_win, msg, args);
2407 wrefresh(status_win);
2412 update_view_title(view);
2413 update_display_cursor();
2416 /* Controls when nodelay should be in effect when polling user input. */
2418 set_nonblocking_input(bool loading)
2420 static unsigned int loading_views;
2422 if ((loading == FALSE && loading_views-- == 1) ||
2423 (loading == TRUE && loading_views++ == 0))
2424 nodelay(status_win, loading);
2432 /* Initialize the curses library */
2433 if (isatty(STDIN_FILENO)) {
2434 cursed = !!initscr();
2436 /* Leave stdin and stdout alone when acting as a pager. */
2437 FILE *io = fopen("/dev/tty", "r+");
2439 cursed = !!newterm(NULL, io, io);
2443 die("Failed to initialize curses");
2445 nonl(); /* Tell curses not to do NL->CR/NL on output */
2446 cbreak(); /* Take input chars one at a time, no wait for \n */
2447 noecho(); /* Don't echo input */
2448 leaveok(stdscr, TRUE);
2453 getmaxyx(stdscr, y, x);
2454 status_win = newwin(1, 0, y - 1, 0);
2456 die("Failed to create status window");
2458 /* Enable keyboard mapping */
2459 keypad(status_win, TRUE);
2460 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2465 * Repository references
2468 static struct ref *refs;
2469 static size_t refs_size;
2471 /* Id <-> ref store */
2472 static struct ref ***id_refs;
2473 static size_t id_refs_size;
2475 static struct ref **
2478 struct ref ***tmp_id_refs;
2479 struct ref **ref_list = NULL;
2480 size_t ref_list_size = 0;
2483 for (i = 0; i < id_refs_size; i++)
2484 if (!strcmp(id, id_refs[i][0]->id))
2487 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2491 id_refs = tmp_id_refs;
2493 for (i = 0; i < refs_size; i++) {
2496 if (strcmp(id, refs[i].id))
2499 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2507 if (ref_list_size > 0)
2508 ref_list[ref_list_size - 1]->next = 1;
2509 ref_list[ref_list_size] = &refs[i];
2511 /* XXX: The properties of the commit chains ensures that we can
2512 * safely modify the shared ref. The repo references will
2513 * always be similar for the same id. */
2514 ref_list[ref_list_size]->next = 0;
2519 id_refs[id_refs_size++] = ref_list;
2525 read_ref(char *id, int idlen, char *name, int namelen)
2529 bool tag_commit = FALSE;
2531 /* Commits referenced by tags has "^{}" appended. */
2532 if (name[namelen - 1] == '}') {
2533 while (namelen > 0 && name[namelen] != '^')
2540 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2543 name += STRING_SIZE("refs/tags/");
2546 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2547 name += STRING_SIZE("refs/heads/");
2549 } else if (!strcmp(name, "HEAD")) {
2553 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2557 ref = &refs[refs_size++];
2558 ref->name = strdup(name);
2563 string_copy(ref->id, id);
2571 const char *cmd_env = getenv("TIG_LS_REMOTE");
2572 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2574 return read_properties(popen(cmd, "r"), "\t", read_ref);
2578 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2580 if (!strcmp(name, "i18n.commitencoding")) {
2581 string_copy(opt_encoding, value);
2588 load_repo_config(void)
2590 return read_properties(popen("git repo-config --list", "r"),
2591 "=", read_repo_config_option);
2595 read_properties(FILE *pipe, const char *separators,
2596 int (*read_property)(char *, int, char *, int))
2598 char buffer[BUFSIZ];
2605 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2610 name = chomp_string(name);
2611 namelen = strcspn(name, separators);
2613 if (name[namelen]) {
2615 value = chomp_string(name + namelen + 1);
2616 valuelen = strlen(value);
2623 state = read_property(name, namelen, value, valuelen);
2626 if (state != ERR && ferror(pipe))
2640 #define __NORETURN __attribute__((__noreturn__))
2645 static void __NORETURN
2648 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2654 static void __NORETURN
2655 die(const char *err, ...)
2661 va_start(args, err);
2662 fputs("tig: ", stderr);
2663 vfprintf(stderr, err, args);
2664 fputs("\n", stderr);
2671 main(int argc, char *argv[])
2674 enum request request;
2677 signal(SIGINT, quit);
2679 if (load_options() == ERR)
2680 die("Failed to load user config.");
2682 /* Load the repo config file so options can be overwritten from
2683 * the command line. */
2684 if (load_repo_config() == ERR)
2685 die("Failed to load repo config.");
2687 if (!parse_options(argc, argv))
2690 if (load_refs() == ERR)
2691 die("Failed to load refs.");
2693 /* Require a git repository unless when running in pager mode. */
2694 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2695 die("Not a git repository");
2697 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2698 view->cmd_env = getenv(view->cmd_env);
2700 request = opt_request;
2704 while (view_driver(display[current_view], request)) {
2708 foreach_view (view, i)
2711 /* Refresh, accept single keystroke of input */
2712 key = wgetch(status_win);
2713 request = get_request(key);
2715 /* Some low-level request handling. This keeps access to
2716 * status_win restricted. */
2720 /* Temporarily switch to line-oriented and echoed
2725 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2726 memcpy(opt_cmd, "git ", 4);
2727 opt_request = REQ_VIEW_PAGER;
2729 report("Prompt interrupted by loading view, "
2730 "press 'z' to stop loading views");
2731 request = REQ_SCREEN_UPDATE;
2738 case REQ_SCREEN_RESIZE:
2742 getmaxyx(stdscr, height, width);
2744 /* Resize the status view and let the view driver take
2745 * care of resizing the displayed views. */
2746 wresize(status_win, 1, width);
2747 mvwin(status_win, height - 1, 0);
2748 wrefresh(status_win);
2766 * Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2768 * This program is free software; you can redistribute it and/or modify
2769 * it under the terms of the GNU General Public License as published by
2770 * the Free Software Foundation; either version 2 of the License, or
2771 * (at your option) any later version.
2775 * - link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
2776 * - link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
2778 * Other git repository browsers: