1 /* Copyright (c) 2006-2007 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 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
51 #define __NORETURN __attribute__((__noreturn__))
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
87 #define ICONV_NONE ((iconv_t) -1)
89 #define ICONV_CONST /* nothing */
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
96 #define AUTHOR_COLS 20
98 /* The default interval between line numbers. */
99 #define NUMBER_INTERVAL 1
103 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
106 #define GIT_CONFIG "git config"
109 #define TIG_LS_REMOTE \
110 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
112 #define TIG_DIFF_CMD \
113 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
115 #define TIG_LOG_CMD \
116 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
118 #define TIG_MAIN_CMD \
119 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
121 #define TIG_TREE_CMD \
124 #define TIG_BLOB_CMD \
125 "git cat-file blob %s"
127 /* XXX: Needs to be defined to the empty string. */
128 #define TIG_HELP_CMD ""
129 #define TIG_PAGER_CMD ""
130 #define TIG_STATUS_CMD ""
131 #define TIG_STAGE_CMD ""
133 /* Some ascii-shorthands fitted into the ncurses namespace. */
135 #define KEY_RETURN '\r'
140 char *name; /* Ref name; tag or head names are shortened. */
141 char id[SIZEOF_REV]; /* Commit SHA1 ID */
142 unsigned int tag:1; /* Is it a tag? */
143 unsigned int ltag:1; /* If so, is the tag local? */
144 unsigned int remote:1; /* Is it a remote ref? */
145 unsigned int next:1; /* For ref lists: are there more refs? */
148 static struct ref **get_refs(char *id);
157 set_from_int_map(struct int_map *map, size_t map_size,
158 int *value, const char *name, int namelen)
163 for (i = 0; i < map_size; i++)
164 if (namelen == map[i].namelen &&
165 !strncasecmp(name, map[i].name, namelen)) {
166 *value = map[i].value;
179 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
181 if (srclen > dstlen - 1)
184 strncpy(dst, src, srclen);
188 /* Shorthands for safely copying into a fixed buffer. */
190 #define string_copy(dst, src) \
191 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
193 #define string_ncopy(dst, src, srclen) \
194 string_ncopy_do(dst, sizeof(dst), src, srclen)
196 #define string_copy_rev(dst, src) \
197 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
199 #define string_add(dst, from, src) \
200 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
203 chomp_string(char *name)
207 while (isspace(*name))
210 namelen = strlen(name) - 1;
211 while (namelen > 0 && isspace(name[namelen]))
218 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
221 size_t pos = bufpos ? *bufpos : 0;
224 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
230 return pos >= bufsize ? FALSE : TRUE;
233 #define string_format(buf, fmt, args...) \
234 string_nformat(buf, sizeof(buf), NULL, fmt, args)
236 #define string_format_from(buf, from, fmt, args...) \
237 string_nformat(buf, sizeof(buf), from, fmt, args)
240 string_enum_compare(const char *str1, const char *str2, int len)
244 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
246 /* Diff-Header == DIFF_HEADER */
247 for (i = 0; i < len; i++) {
248 if (toupper(str1[i]) == toupper(str2[i]))
251 if (string_enum_sep(str1[i]) &&
252 string_enum_sep(str2[i]))
255 return str1[i] - str2[i];
263 * NOTE: The following is a slightly modified copy of the git project's shell
264 * quoting routines found in the quote.c file.
266 * Help to copy the thing properly quoted for the shell safety. any single
267 * quote is replaced with '\'', any exclamation point is replaced with '\!',
268 * and the whole thing is enclosed in a
271 * original sq_quote result
272 * name ==> name ==> 'name'
273 * a b ==> a b ==> 'a b'
274 * a'b ==> a'\''b ==> 'a'\''b'
275 * a!b ==> a'\!'b ==> 'a'\!'b'
279 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
283 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
286 while ((c = *src++)) {
287 if (c == '\'' || c == '!') {
298 if (bufsize < SIZEOF_STR)
310 /* XXX: Keep the view request first and in sync with views[]. */ \
311 REQ_GROUP("View switching") \
312 REQ_(VIEW_MAIN, "Show main view"), \
313 REQ_(VIEW_DIFF, "Show diff view"), \
314 REQ_(VIEW_LOG, "Show log view"), \
315 REQ_(VIEW_TREE, "Show tree view"), \
316 REQ_(VIEW_BLOB, "Show blob view"), \
317 REQ_(VIEW_HELP, "Show help page"), \
318 REQ_(VIEW_PAGER, "Show pager view"), \
319 REQ_(VIEW_STATUS, "Show status view"), \
320 REQ_(VIEW_STAGE, "Show stage view"), \
322 REQ_GROUP("View manipulation") \
323 REQ_(ENTER, "Enter current line and scroll"), \
324 REQ_(NEXT, "Move to next"), \
325 REQ_(PREVIOUS, "Move to previous"), \
326 REQ_(VIEW_NEXT, "Move focus to next view"), \
327 REQ_(REFRESH, "Reload and refresh"), \
328 REQ_(VIEW_CLOSE, "Close the current view"), \
329 REQ_(QUIT, "Close all views and quit"), \
331 REQ_GROUP("Cursor navigation") \
332 REQ_(MOVE_UP, "Move cursor one line up"), \
333 REQ_(MOVE_DOWN, "Move cursor one line down"), \
334 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
335 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
336 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
337 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
339 REQ_GROUP("Scrolling") \
340 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
341 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
342 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
343 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
345 REQ_GROUP("Searching") \
346 REQ_(SEARCH, "Search the view"), \
347 REQ_(SEARCH_BACK, "Search backwards in the view"), \
348 REQ_(FIND_NEXT, "Find next search match"), \
349 REQ_(FIND_PREV, "Find previous search match"), \
352 REQ_(PROMPT, "Bring up the prompt"), \
353 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
354 REQ_(SCREEN_RESIZE, "Resize the screen"), \
355 REQ_(SHOW_VERSION, "Show version information"), \
356 REQ_(STOP_LOADING, "Stop all loading views"), \
357 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
358 REQ_(TOGGLE_DATE, "Toggle date display"), \
359 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
360 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
361 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
362 REQ_(STATUS_UPDATE, "Update file status"), \
363 REQ_(STATUS_MERGE, "Merge file using external tool"), \
364 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
365 REQ_(EDIT, "Open in editor"), \
366 REQ_(NONE, "Do nothing")
369 /* User action requests. */
371 #define REQ_GROUP(help)
372 #define REQ_(req, help) REQ_##req
374 /* Offset all requests to avoid conflicts with ncurses getch values. */
375 REQ_OFFSET = KEY_MAX + 1,
382 struct request_info {
383 enum request request;
389 static struct request_info req_info[] = {
390 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
391 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
398 get_request(const char *name)
400 int namelen = strlen(name);
403 for (i = 0; i < ARRAY_SIZE(req_info); i++)
404 if (req_info[i].namelen == namelen &&
405 !string_enum_compare(req_info[i].name, name, namelen))
406 return req_info[i].request;
416 static const char usage[] =
417 "tig " TIG_VERSION " (" __DATE__ ")\n"
419 "Usage: tig [options] [revs] [--] [paths]\n"
420 " or: tig show [options] [revs] [--] [paths]\n"
422 " or: tig < [git command output]\n"
425 " -v, --version Show version and exit\n"
426 " -h, --help Show help message and exit\n";
428 /* Option and state variables. */
429 static bool opt_date = TRUE;
430 static bool opt_author = TRUE;
431 static bool opt_line_number = FALSE;
432 static bool opt_rev_graph = FALSE;
433 static bool opt_show_refs = TRUE;
434 static int opt_num_interval = NUMBER_INTERVAL;
435 static int opt_tab_size = TABSIZE;
436 static enum request opt_request = REQ_VIEW_MAIN;
437 static char opt_cmd[SIZEOF_STR] = "";
438 static char opt_path[SIZEOF_STR] = "";
439 static FILE *opt_pipe = NULL;
440 static char opt_encoding[20] = "UTF-8";
441 static bool opt_utf8 = TRUE;
442 static char opt_codeset[20] = "UTF-8";
443 static iconv_t opt_iconv = ICONV_NONE;
444 static char opt_search[SIZEOF_STR] = "";
445 static char opt_cdup[SIZEOF_STR] = "";
446 static char opt_git_dir[SIZEOF_STR] = "";
447 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
448 static char opt_editor[SIZEOF_STR] = "";
456 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
466 int namelen = strlen(name);
470 if (strncmp(opt, name, namelen))
473 if (opt[namelen] == '=')
474 value = opt + namelen + 1;
477 if (!short_name || opt[1] != short_name)
482 va_start(args, type);
483 if (type == OPT_INT) {
484 number = va_arg(args, int *);
486 *number = atoi(value);
493 /* Returns the index of log or diff command or -1 to exit. */
495 parse_options(int argc, char *argv[])
505 subcommand = argv[1];
506 if (!strcmp(subcommand, "status")) {
507 opt_request = REQ_VIEW_STATUS;
509 warn("ignoring arguments after `%s'", subcommand);
512 } else if (!strcmp(subcommand, "show")) {
513 opt_request = REQ_VIEW_DIFF;
515 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
516 opt_request = subcommand[0] == 'l'
517 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
518 warn("`tig %s' has been deprecated", subcommand);
524 for (i = 1 + !!subcommand; i < argc; i++) {
527 if (opt[0] && opt[0] != '-')
530 if (!strcmp(opt, "--")) {
535 if (check_option(opt, 'v', "version", OPT_NONE)) {
536 printf("tig version %s\n", TIG_VERSION);
540 if (check_option(opt, 'h', "help", OPT_NONE)) {
545 if (!strcmp(opt, "-S")) {
546 warn("`%s' has been deprecated; use `tig status' instead", opt);
547 opt_request = REQ_VIEW_STATUS;
551 if (!strcmp(opt, "-l")) {
552 opt_request = REQ_VIEW_LOG;
553 } else if (!strcmp(opt, "-d")) {
554 opt_request = REQ_VIEW_DIFF;
555 } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
556 opt_line_number = TRUE;
557 } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
558 opt_tab_size = MIN(opt_tab_size, TABSIZE);
560 if (altargc >= ARRAY_SIZE(altargv))
561 die("maximum number of arguments exceeded");
562 altargv[altargc++] = opt;
566 warn("`%s' has been deprecated", opt);
569 if (!isatty(STDIN_FILENO)) {
570 opt_request = REQ_VIEW_PAGER;
573 } else if (i < argc || altargc > 0) {
577 if (opt_request == REQ_VIEW_MAIN)
578 /* XXX: This is vulnerable to the user overriding
579 * options required for the main view parser. */
580 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
582 string_format(opt_cmd, "git %s", subcommand);
583 buf_size = strlen(opt_cmd);
585 while (buf_size < sizeof(opt_cmd) && alti < altargc) {
586 opt_cmd[buf_size++] = ' ';
587 buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
590 while (buf_size < sizeof(opt_cmd) && i < argc) {
591 opt_cmd[buf_size++] = ' ';
592 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
595 if (buf_size >= sizeof(opt_cmd))
596 die("command too long");
598 opt_cmd[buf_size] = 0;
606 * Line-oriented content detection.
610 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
611 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
612 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
613 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
614 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
615 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
616 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
617 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
618 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
619 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
620 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
621 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
622 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
623 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
624 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
625 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
626 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
627 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
628 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
629 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
630 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
631 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
632 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
633 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
634 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
635 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
636 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
637 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
638 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
639 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
640 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
641 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
642 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
643 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
644 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
645 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
646 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
647 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
648 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
649 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
650 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
651 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
652 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
653 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
654 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
655 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
656 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
657 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
658 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
661 #define LINE(type, line, fg, bg, attr) \
668 const char *name; /* Option name. */
669 int namelen; /* Size of option name. */
670 const char *line; /* The start of line to match. */
671 int linelen; /* Size of string to match. */
672 int fg, bg, attr; /* Color and text attributes for the lines. */
675 static struct line_info line_info[] = {
676 #define LINE(type, line, fg, bg, attr) \
677 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
682 static enum line_type
683 get_line_type(char *line)
685 int linelen = strlen(line);
688 for (type = 0; type < ARRAY_SIZE(line_info); type++)
689 /* Case insensitive search matches Signed-off-by lines better. */
690 if (linelen >= line_info[type].linelen &&
691 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
698 get_line_attr(enum line_type type)
700 assert(type < ARRAY_SIZE(line_info));
701 return COLOR_PAIR(type) | line_info[type].attr;
704 static struct line_info *
705 get_line_info(char *name, int namelen)
709 for (type = 0; type < ARRAY_SIZE(line_info); type++)
710 if (namelen == line_info[type].namelen &&
711 !string_enum_compare(line_info[type].name, name, namelen))
712 return &line_info[type];
720 int default_bg = line_info[LINE_DEFAULT].bg;
721 int default_fg = line_info[LINE_DEFAULT].fg;
726 if (assume_default_colors(default_fg, default_bg) == ERR) {
727 default_bg = COLOR_BLACK;
728 default_fg = COLOR_WHITE;
731 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
732 struct line_info *info = &line_info[type];
733 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
734 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
736 init_pair(type, fg, bg);
744 unsigned int selected:1;
746 void *data; /* User data */
756 enum request request;
757 struct keybinding *next;
760 static struct keybinding default_keybindings[] = {
762 { 'm', REQ_VIEW_MAIN },
763 { 'd', REQ_VIEW_DIFF },
764 { 'l', REQ_VIEW_LOG },
765 { 't', REQ_VIEW_TREE },
766 { 'f', REQ_VIEW_BLOB },
767 { 'p', REQ_VIEW_PAGER },
768 { 'h', REQ_VIEW_HELP },
769 { 'S', REQ_VIEW_STATUS },
770 { 'c', REQ_VIEW_STAGE },
772 /* View manipulation */
773 { 'q', REQ_VIEW_CLOSE },
774 { KEY_TAB, REQ_VIEW_NEXT },
775 { KEY_RETURN, REQ_ENTER },
776 { KEY_UP, REQ_PREVIOUS },
777 { KEY_DOWN, REQ_NEXT },
778 { 'R', REQ_REFRESH },
780 /* Cursor navigation */
781 { 'k', REQ_MOVE_UP },
782 { 'j', REQ_MOVE_DOWN },
783 { KEY_HOME, REQ_MOVE_FIRST_LINE },
784 { KEY_END, REQ_MOVE_LAST_LINE },
785 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
786 { ' ', REQ_MOVE_PAGE_DOWN },
787 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
788 { 'b', REQ_MOVE_PAGE_UP },
789 { '-', REQ_MOVE_PAGE_UP },
792 { KEY_IC, REQ_SCROLL_LINE_UP },
793 { KEY_DC, REQ_SCROLL_LINE_DOWN },
794 { 'w', REQ_SCROLL_PAGE_UP },
795 { 's', REQ_SCROLL_PAGE_DOWN },
799 { '?', REQ_SEARCH_BACK },
800 { 'n', REQ_FIND_NEXT },
801 { 'N', REQ_FIND_PREV },
805 { 'z', REQ_STOP_LOADING },
806 { 'v', REQ_SHOW_VERSION },
807 { 'r', REQ_SCREEN_REDRAW },
808 { '.', REQ_TOGGLE_LINENO },
809 { 'D', REQ_TOGGLE_DATE },
810 { 'A', REQ_TOGGLE_AUTHOR },
811 { 'g', REQ_TOGGLE_REV_GRAPH },
812 { 'F', REQ_TOGGLE_REFS },
814 { 'u', REQ_STATUS_UPDATE },
815 { 'M', REQ_STATUS_MERGE },
816 { ',', REQ_TREE_PARENT },
819 /* Using the ncurses SIGWINCH handler. */
820 { KEY_RESIZE, REQ_SCREEN_RESIZE },
823 #define KEYMAP_INFO \
836 #define KEYMAP_(name) KEYMAP_##name
841 static struct int_map keymap_table[] = {
842 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
847 #define set_keymap(map, name) \
848 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
850 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
853 add_keybinding(enum keymap keymap, enum request request, int key)
855 struct keybinding *keybinding;
857 keybinding = calloc(1, sizeof(*keybinding));
859 die("Failed to allocate keybinding");
861 keybinding->alias = key;
862 keybinding->request = request;
863 keybinding->next = keybindings[keymap];
864 keybindings[keymap] = keybinding;
867 /* Looks for a key binding first in the given map, then in the generic map, and
868 * lastly in the default keybindings. */
870 get_keybinding(enum keymap keymap, int key)
872 struct keybinding *kbd;
875 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
876 if (kbd->alias == key)
879 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
880 if (kbd->alias == key)
883 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
884 if (default_keybindings[i].alias == key)
885 return default_keybindings[i].request;
887 return (enum request) key;
896 static struct key key_table[] = {
897 { "Enter", KEY_RETURN },
899 { "Backspace", KEY_BACKSPACE },
901 { "Escape", KEY_ESC },
902 { "Left", KEY_LEFT },
903 { "Right", KEY_RIGHT },
905 { "Down", KEY_DOWN },
906 { "Insert", KEY_IC },
907 { "Delete", KEY_DC },
909 { "Home", KEY_HOME },
911 { "PageUp", KEY_PPAGE },
912 { "PageDown", KEY_NPAGE },
922 { "F10", KEY_F(10) },
923 { "F11", KEY_F(11) },
924 { "F12", KEY_F(12) },
928 get_key_value(const char *name)
932 for (i = 0; i < ARRAY_SIZE(key_table); i++)
933 if (!strcasecmp(key_table[i].name, name))
934 return key_table[i].value;
936 if (strlen(name) == 1 && isprint(*name))
943 get_key_name(int key_value)
945 static char key_char[] = "'X'";
949 for (key = 0; key < ARRAY_SIZE(key_table); key++)
950 if (key_table[key].value == key_value)
951 seq = key_table[key].name;
955 isprint(key_value)) {
956 key_char[1] = (char) key_value;
960 return seq ? seq : "'?'";
964 get_key(enum request request)
966 static char buf[BUFSIZ];
973 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
974 struct keybinding *keybinding = &default_keybindings[i];
976 if (keybinding->request != request)
979 if (!string_format_from(buf, &pos, "%s%s", sep,
980 get_key_name(keybinding->alias)))
981 return "Too many keybindings!";
991 char cmd[SIZEOF_STR];
994 static struct run_request *run_request;
995 static size_t run_requests;
998 add_run_request(enum keymap keymap, int key, int argc, char **argv)
1000 struct run_request *tmp;
1001 struct run_request req = { keymap, key };
1004 for (bufpos = 0; argc > 0; argc--, argv++)
1005 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
1008 req.cmd[bufpos - 1] = 0;
1010 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1015 run_request[run_requests++] = req;
1017 return REQ_NONE + run_requests;
1020 static struct run_request *
1021 get_run_request(enum request request)
1023 if (request <= REQ_NONE)
1025 return &run_request[request - REQ_NONE - 1];
1029 add_builtin_run_requests(void)
1036 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1037 { KEYMAP_GENERIC, 'G', { "git gc" } },
1041 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1044 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1045 if (req != REQ_NONE)
1046 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1051 * User config file handling.
1054 static struct int_map color_map[] = {
1055 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1067 #define set_color(color, name) \
1068 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1070 static struct int_map attr_map[] = {
1071 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1078 ATTR_MAP(UNDERLINE),
1081 #define set_attribute(attr, name) \
1082 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1084 static int config_lineno;
1085 static bool config_errors;
1086 static char *config_msg;
1088 /* Wants: object fgcolor bgcolor [attr] */
1090 option_color_command(int argc, char *argv[])
1092 struct line_info *info;
1094 if (argc != 3 && argc != 4) {
1095 config_msg = "Wrong number of arguments given to color command";
1099 info = get_line_info(argv[0], strlen(argv[0]));
1101 config_msg = "Unknown color name";
1105 if (set_color(&info->fg, argv[1]) == ERR ||
1106 set_color(&info->bg, argv[2]) == ERR) {
1107 config_msg = "Unknown color";
1111 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1112 config_msg = "Unknown attribute";
1119 static bool parse_bool(const char *s)
1121 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1122 !strcmp(s, "yes")) ? TRUE : FALSE;
1125 /* Wants: name = value */
1127 option_set_command(int argc, char *argv[])
1130 config_msg = "Wrong number of arguments given to set command";
1134 if (strcmp(argv[1], "=")) {
1135 config_msg = "No value assigned";
1139 if (!strcmp(argv[0], "show-author")) {
1140 opt_author = parse_bool(argv[2]);
1144 if (!strcmp(argv[0], "show-date")) {
1145 opt_date = parse_bool(argv[2]);
1149 if (!strcmp(argv[0], "show-rev-graph")) {
1150 opt_rev_graph = parse_bool(argv[2]);
1154 if (!strcmp(argv[0], "show-refs")) {
1155 opt_show_refs = parse_bool(argv[2]);
1159 if (!strcmp(argv[0], "show-line-numbers")) {
1160 opt_line_number = parse_bool(argv[2]);
1164 if (!strcmp(argv[0], "line-number-interval")) {
1165 opt_num_interval = atoi(argv[2]);
1169 if (!strcmp(argv[0], "tab-size")) {
1170 opt_tab_size = atoi(argv[2]);
1174 if (!strcmp(argv[0], "commit-encoding")) {
1175 char *arg = argv[2];
1176 int delimiter = *arg;
1179 switch (delimiter) {
1182 for (arg++, i = 0; arg[i]; i++)
1183 if (arg[i] == delimiter) {
1188 string_ncopy(opt_encoding, arg, strlen(arg));
1193 config_msg = "Unknown variable name";
1197 /* Wants: mode request key */
1199 option_bind_command(int argc, char *argv[])
1201 enum request request;
1206 config_msg = "Wrong number of arguments given to bind command";
1210 if (set_keymap(&keymap, argv[0]) == ERR) {
1211 config_msg = "Unknown key map";
1215 key = get_key_value(argv[1]);
1217 config_msg = "Unknown key";
1221 request = get_request(argv[2]);
1222 if (request == REQ_NONE) {
1223 const char *obsolete[] = { "cherry-pick" };
1224 size_t namelen = strlen(argv[2]);
1227 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1228 if (namelen == strlen(obsolete[i]) &&
1229 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1230 config_msg = "Obsolete request name";
1235 if (request == REQ_NONE && *argv[2]++ == '!')
1236 request = add_run_request(keymap, key, argc - 2, argv + 2);
1237 if (request == REQ_NONE) {
1238 config_msg = "Unknown request name";
1242 add_keybinding(keymap, request, key);
1248 set_option(char *opt, char *value)
1255 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1256 argv[argc++] = value;
1259 /* Nothing more to tokenize or last available token. */
1260 if (!*value || argc >= ARRAY_SIZE(argv))
1264 while (isspace(*value))
1268 if (!strcmp(opt, "color"))
1269 return option_color_command(argc, argv);
1271 if (!strcmp(opt, "set"))
1272 return option_set_command(argc, argv);
1274 if (!strcmp(opt, "bind"))
1275 return option_bind_command(argc, argv);
1277 config_msg = "Unknown option command";
1282 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1287 config_msg = "Internal error";
1289 /* Check for comment markers, since read_properties() will
1290 * only ensure opt and value are split at first " \t". */
1291 optlen = strcspn(opt, "#");
1295 if (opt[optlen] != 0) {
1296 config_msg = "No option value";
1300 /* Look for comment endings in the value. */
1301 size_t len = strcspn(value, "#");
1303 if (len < valuelen) {
1305 value[valuelen] = 0;
1308 status = set_option(opt, value);
1311 if (status == ERR) {
1312 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1313 config_lineno, (int) optlen, opt, config_msg);
1314 config_errors = TRUE;
1317 /* Always keep going if errors are encountered. */
1322 load_option_file(const char *path)
1326 /* It's ok that the file doesn't exist. */
1327 file = fopen(path, "r");
1332 config_errors = FALSE;
1334 if (read_properties(file, " \t", read_option) == ERR ||
1335 config_errors == TRUE)
1336 fprintf(stderr, "Errors while loading %s.\n", path);
1342 char *home = getenv("HOME");
1343 char *tigrc_user = getenv("TIGRC_USER");
1344 char *tigrc_system = getenv("TIGRC_SYSTEM");
1345 char buf[SIZEOF_STR];
1347 add_builtin_run_requests();
1349 if (!tigrc_system) {
1350 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1354 load_option_file(tigrc_system);
1357 if (!home || !string_format(buf, "%s/.tigrc", home))
1361 load_option_file(tigrc_user);
1374 /* The display array of active views and the index of the current view. */
1375 static struct view *display[2];
1376 static unsigned int current_view;
1378 /* Reading from the prompt? */
1379 static bool input_mode = FALSE;
1381 #define foreach_displayed_view(view, i) \
1382 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1384 #define displayed_views() (display[1] != NULL ? 2 : 1)
1386 /* Current head and commit ID */
1387 static char ref_blob[SIZEOF_REF] = "";
1388 static char ref_commit[SIZEOF_REF] = "HEAD";
1389 static char ref_head[SIZEOF_REF] = "HEAD";
1392 const char *name; /* View name */
1393 const char *cmd_fmt; /* Default command line format */
1394 const char *cmd_env; /* Command line set via environment */
1395 const char *id; /* Points to either of ref_{head,commit,blob} */
1397 struct view_ops *ops; /* View operations */
1399 enum keymap keymap; /* What keymap does this view have */
1401 char cmd[SIZEOF_STR]; /* Command buffer */
1402 char ref[SIZEOF_REF]; /* Hovered commit reference */
1403 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1405 int height, width; /* The width and height of the main window */
1406 WINDOW *win; /* The main window */
1407 WINDOW *title; /* The title window living below the main window */
1410 unsigned long offset; /* Offset of the window top */
1411 unsigned long lineno; /* Current line number */
1414 char grep[SIZEOF_STR]; /* Search string */
1415 regex_t *regex; /* Pre-compiled regex */
1417 /* If non-NULL, points to the view that opened this view. If this view
1418 * is closed tig will switch back to the parent view. */
1419 struct view *parent;
1422 size_t lines; /* Total number of lines */
1423 struct line *line; /* Line index */
1424 size_t line_alloc; /* Total number of allocated lines */
1425 size_t line_size; /* Total number of used lines */
1426 unsigned int digits; /* Number of digits in the lines member. */
1434 /* What type of content being displayed. Used in the title bar. */
1436 /* Open and reads in all view content. */
1437 bool (*open)(struct view *view);
1438 /* Read one line; updates view->line. */
1439 bool (*read)(struct view *view, char *data);
1440 /* Draw one line; @lineno must be < view->height. */
1441 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1442 /* Depending on view handle a special requests. */
1443 enum request (*request)(struct view *view, enum request request, struct line *line);
1444 /* Search for regex in a line. */
1445 bool (*grep)(struct view *view, struct line *line);
1447 void (*select)(struct view *view, struct line *line);
1450 static struct view_ops pager_ops;
1451 static struct view_ops main_ops;
1452 static struct view_ops tree_ops;
1453 static struct view_ops blob_ops;
1454 static struct view_ops help_ops;
1455 static struct view_ops status_ops;
1456 static struct view_ops stage_ops;
1458 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1459 { name, cmd, #env, ref, ops, map}
1461 #define VIEW_(id, name, ops, ref) \
1462 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1465 static struct view views[] = {
1466 VIEW_(MAIN, "main", &main_ops, ref_head),
1467 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1468 VIEW_(LOG, "log", &pager_ops, ref_head),
1469 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1470 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1471 VIEW_(HELP, "help", &help_ops, ""),
1472 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1473 VIEW_(STATUS, "status", &status_ops, ""),
1474 VIEW_(STAGE, "stage", &stage_ops, ""),
1477 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1479 #define foreach_view(view, i) \
1480 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1482 #define view_is_displayed(view) \
1483 (view == display[0] || view == display[1])
1486 draw_text(struct view *view, const char *string, int max_len,
1487 bool use_tilde, int tilde_attr)
1490 int trimmed = FALSE;
1496 len = utf8_length(string, max_len, &trimmed, use_tilde);
1498 len = strlen(string);
1499 if (len > max_len) {
1508 waddnstr(view->win, string, len);
1509 if (trimmed && use_tilde) {
1510 if (tilde_attr != -1)
1511 wattrset(view->win, tilde_attr);
1512 waddch(view->win, '~');
1520 draw_view_line(struct view *view, unsigned int lineno)
1523 bool selected = (view->offset + lineno == view->lineno);
1526 assert(view_is_displayed(view));
1528 if (view->offset + lineno >= view->lines)
1531 line = &view->line[view->offset + lineno];
1534 line->selected = TRUE;
1535 view->ops->select(view, line);
1536 } else if (line->selected) {
1537 line->selected = FALSE;
1538 wmove(view->win, lineno, 0);
1539 wclrtoeol(view->win);
1542 scrollok(view->win, FALSE);
1543 draw_ok = view->ops->draw(view, line, lineno, selected);
1544 scrollok(view->win, TRUE);
1550 redraw_view_from(struct view *view, int lineno)
1552 assert(0 <= lineno && lineno < view->height);
1554 for (; lineno < view->height; lineno++) {
1555 if (!draw_view_line(view, lineno))
1559 redrawwin(view->win);
1561 wnoutrefresh(view->win);
1563 wrefresh(view->win);
1567 redraw_view(struct view *view)
1570 redraw_view_from(view, 0);
1575 update_view_title(struct view *view)
1577 char buf[SIZEOF_STR];
1578 char state[SIZEOF_STR];
1579 size_t bufpos = 0, statelen = 0;
1581 assert(view_is_displayed(view));
1583 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1584 unsigned int view_lines = view->offset + view->height;
1585 unsigned int lines = view->lines
1586 ? MIN(view_lines, view->lines) * 100 / view->lines
1589 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1596 time_t secs = time(NULL) - view->start_time;
1598 /* Three git seconds are a long time ... */
1600 string_format_from(state, &statelen, " %lds", secs);
1604 string_format_from(buf, &bufpos, "[%s]", view->name);
1605 if (*view->ref && bufpos < view->width) {
1606 size_t refsize = strlen(view->ref);
1607 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1609 if (minsize < view->width)
1610 refsize = view->width - minsize + 7;
1611 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1614 if (statelen && bufpos < view->width) {
1615 string_format_from(buf, &bufpos, " %s", state);
1618 if (view == display[current_view])
1619 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1621 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1623 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1624 wclrtoeol(view->title);
1625 wmove(view->title, 0, view->width - 1);
1628 wnoutrefresh(view->title);
1630 wrefresh(view->title);
1634 resize_display(void)
1637 struct view *base = display[0];
1638 struct view *view = display[1] ? display[1] : display[0];
1640 /* Setup window dimensions */
1642 getmaxyx(stdscr, base->height, base->width);
1644 /* Make room for the status window. */
1648 /* Horizontal split. */
1649 view->width = base->width;
1650 view->height = SCALE_SPLIT_VIEW(base->height);
1651 base->height -= view->height;
1653 /* Make room for the title bar. */
1657 /* Make room for the title bar. */
1662 foreach_displayed_view (view, i) {
1664 view->win = newwin(view->height, 0, offset, 0);
1666 die("Failed to create %s view", view->name);
1668 scrollok(view->win, TRUE);
1670 view->title = newwin(1, 0, offset + view->height, 0);
1672 die("Failed to create title window");
1675 wresize(view->win, view->height, view->width);
1676 mvwin(view->win, offset, 0);
1677 mvwin(view->title, offset + view->height, 0);
1680 offset += view->height + 1;
1685 redraw_display(void)
1690 foreach_displayed_view (view, i) {
1692 update_view_title(view);
1697 update_display_cursor(struct view *view)
1699 /* Move the cursor to the right-most column of the cursor line.
1701 * XXX: This could turn out to be a bit expensive, but it ensures that
1702 * the cursor does not jump around. */
1704 wmove(view->win, view->lineno - view->offset, view->width - 1);
1705 wrefresh(view->win);
1713 /* Scrolling backend */
1715 do_scroll_view(struct view *view, int lines)
1717 bool redraw_current_line = FALSE;
1719 /* The rendering expects the new offset. */
1720 view->offset += lines;
1722 assert(0 <= view->offset && view->offset < view->lines);
1725 /* Move current line into the view. */
1726 if (view->lineno < view->offset) {
1727 view->lineno = view->offset;
1728 redraw_current_line = TRUE;
1729 } else if (view->lineno >= view->offset + view->height) {
1730 view->lineno = view->offset + view->height - 1;
1731 redraw_current_line = TRUE;
1734 assert(view->offset <= view->lineno && view->lineno < view->lines);
1736 /* Redraw the whole screen if scrolling is pointless. */
1737 if (view->height < ABS(lines)) {
1741 int line = lines > 0 ? view->height - lines : 0;
1742 int end = line + ABS(lines);
1744 wscrl(view->win, lines);
1746 for (; line < end; line++) {
1747 if (!draw_view_line(view, line))
1751 if (redraw_current_line)
1752 draw_view_line(view, view->lineno - view->offset);
1755 redrawwin(view->win);
1756 wrefresh(view->win);
1760 /* Scroll frontend */
1762 scroll_view(struct view *view, enum request request)
1766 assert(view_is_displayed(view));
1769 case REQ_SCROLL_PAGE_DOWN:
1770 lines = view->height;
1771 case REQ_SCROLL_LINE_DOWN:
1772 if (view->offset + lines > view->lines)
1773 lines = view->lines - view->offset;
1775 if (lines == 0 || view->offset + view->height >= view->lines) {
1776 report("Cannot scroll beyond the last line");
1781 case REQ_SCROLL_PAGE_UP:
1782 lines = view->height;
1783 case REQ_SCROLL_LINE_UP:
1784 if (lines > view->offset)
1785 lines = view->offset;
1788 report("Cannot scroll beyond the first line");
1796 die("request %d not handled in switch", request);
1799 do_scroll_view(view, lines);
1804 move_view(struct view *view, enum request request)
1806 int scroll_steps = 0;
1810 case REQ_MOVE_FIRST_LINE:
1811 steps = -view->lineno;
1814 case REQ_MOVE_LAST_LINE:
1815 steps = view->lines - view->lineno - 1;
1818 case REQ_MOVE_PAGE_UP:
1819 steps = view->height > view->lineno
1820 ? -view->lineno : -view->height;
1823 case REQ_MOVE_PAGE_DOWN:
1824 steps = view->lineno + view->height >= view->lines
1825 ? view->lines - view->lineno - 1 : view->height;
1837 die("request %d not handled in switch", request);
1840 if (steps <= 0 && view->lineno == 0) {
1841 report("Cannot move beyond the first line");
1844 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1845 report("Cannot move beyond the last line");
1849 /* Move the current line */
1850 view->lineno += steps;
1851 assert(0 <= view->lineno && view->lineno < view->lines);
1853 /* Check whether the view needs to be scrolled */
1854 if (view->lineno < view->offset ||
1855 view->lineno >= view->offset + view->height) {
1856 scroll_steps = steps;
1857 if (steps < 0 && -steps > view->offset) {
1858 scroll_steps = -view->offset;
1860 } else if (steps > 0) {
1861 if (view->lineno == view->lines - 1 &&
1862 view->lines > view->height) {
1863 scroll_steps = view->lines - view->offset - 1;
1864 if (scroll_steps >= view->height)
1865 scroll_steps -= view->height - 1;
1870 if (!view_is_displayed(view)) {
1871 view->offset += scroll_steps;
1872 assert(0 <= view->offset && view->offset < view->lines);
1873 view->ops->select(view, &view->line[view->lineno]);
1877 /* Repaint the old "current" line if we be scrolling */
1878 if (ABS(steps) < view->height)
1879 draw_view_line(view, view->lineno - steps - view->offset);
1882 do_scroll_view(view, scroll_steps);
1886 /* Draw the current line */
1887 draw_view_line(view, view->lineno - view->offset);
1889 redrawwin(view->win);
1890 wrefresh(view->win);
1899 static void search_view(struct view *view, enum request request);
1902 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1904 assert(view_is_displayed(view));
1906 if (!view->ops->grep(view, line))
1909 if (lineno - view->offset >= view->height) {
1910 view->offset = lineno;
1911 view->lineno = lineno;
1915 unsigned long old_lineno = view->lineno - view->offset;
1917 view->lineno = lineno;
1918 draw_view_line(view, old_lineno);
1920 draw_view_line(view, view->lineno - view->offset);
1921 redrawwin(view->win);
1922 wrefresh(view->win);
1925 report("Line %ld matches '%s'", lineno + 1, view->grep);
1930 find_next(struct view *view, enum request request)
1932 unsigned long lineno = view->lineno;
1937 report("No previous search");
1939 search_view(view, request);
1949 case REQ_SEARCH_BACK:
1958 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1959 lineno += direction;
1961 /* Note, lineno is unsigned long so will wrap around in which case it
1962 * will become bigger than view->lines. */
1963 for (; lineno < view->lines; lineno += direction) {
1964 struct line *line = &view->line[lineno];
1966 if (find_next_line(view, lineno, line))
1970 report("No match found for '%s'", view->grep);
1974 search_view(struct view *view, enum request request)
1979 regfree(view->regex);
1982 view->regex = calloc(1, sizeof(*view->regex));
1987 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1988 if (regex_err != 0) {
1989 char buf[SIZEOF_STR] = "unknown error";
1991 regerror(regex_err, view->regex, buf, sizeof(buf));
1992 report("Search failed: %s", buf);
1996 string_copy(view->grep, opt_search);
1998 find_next(view, request);
2002 * Incremental updating
2006 end_update(struct view *view)
2010 set_nonblocking_input(FALSE);
2011 if (view->pipe == stdin)
2019 begin_update(struct view *view)
2025 string_copy(view->cmd, opt_cmd);
2027 /* When running random commands, initially show the
2028 * command in the title. However, it maybe later be
2029 * overwritten if a commit line is selected. */
2030 if (view == VIEW(REQ_VIEW_PAGER))
2031 string_copy(view->ref, view->cmd);
2035 } else if (view == VIEW(REQ_VIEW_TREE)) {
2036 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2037 char path[SIZEOF_STR];
2039 if (strcmp(view->vid, view->id))
2040 opt_path[0] = path[0] = 0;
2041 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2044 if (!string_format(view->cmd, format, view->id, path))
2048 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2049 const char *id = view->id;
2051 if (!string_format(view->cmd, format, id, id, id, id, id))
2054 /* Put the current ref_* value to the view title ref
2055 * member. This is needed by the blob view. Most other
2056 * views sets it automatically after loading because the
2057 * first line is a commit line. */
2058 string_copy_rev(view->ref, view->id);
2061 /* Special case for the pager view. */
2063 view->pipe = opt_pipe;
2066 view->pipe = popen(view->cmd, "r");
2072 set_nonblocking_input(TRUE);
2077 string_copy_rev(view->vid, view->id);
2082 for (i = 0; i < view->lines; i++)
2083 if (view->line[i].data)
2084 free(view->line[i].data);
2090 view->start_time = time(NULL);
2095 #define ITEM_CHUNK_SIZE 256
2097 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2099 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2100 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2102 if (mem == NULL || num_chunks != num_chunks_new) {
2103 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2104 mem = realloc(mem, *size * item_size);
2110 static struct line *
2111 realloc_lines(struct view *view, size_t line_size)
2113 size_t alloc = view->line_alloc;
2114 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2115 sizeof(*view->line));
2121 view->line_alloc = alloc;
2122 view->line_size = line_size;
2127 update_view(struct view *view)
2129 char in_buffer[BUFSIZ];
2130 char out_buffer[BUFSIZ * 2];
2132 /* The number of lines to read. If too low it will cause too much
2133 * redrawing (and possible flickering), if too high responsiveness
2135 unsigned long lines = view->height;
2136 int redraw_from = -1;
2141 /* Only redraw if lines are visible. */
2142 if (view->offset + view->height >= view->lines)
2143 redraw_from = view->lines - view->offset;
2145 /* FIXME: This is probably not perfect for backgrounded views. */
2146 if (!realloc_lines(view, view->lines + lines))
2149 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2150 size_t linelen = strlen(line);
2153 line[linelen - 1] = 0;
2155 if (opt_iconv != ICONV_NONE) {
2156 ICONV_CONST char *inbuf = line;
2157 size_t inlen = linelen;
2159 char *outbuf = out_buffer;
2160 size_t outlen = sizeof(out_buffer);
2164 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2165 if (ret != (size_t) -1) {
2167 linelen = strlen(out_buffer);
2171 if (!view->ops->read(view, line))
2181 lines = view->lines;
2182 for (digits = 0; lines; digits++)
2185 /* Keep the displayed view in sync with line number scaling. */
2186 if (digits != view->digits) {
2187 view->digits = digits;
2192 if (!view_is_displayed(view))
2195 if (view == VIEW(REQ_VIEW_TREE)) {
2196 /* Clear the view and redraw everything since the tree sorting
2197 * might have rearranged things. */
2200 } else if (redraw_from >= 0) {
2201 /* If this is an incremental update, redraw the previous line
2202 * since for commits some members could have changed when
2203 * loading the main view. */
2204 if (redraw_from > 0)
2207 /* Since revision graph visualization requires knowledge
2208 * about the parent commit, it causes a further one-off
2209 * needed to be redrawn for incremental updates. */
2210 if (redraw_from > 0 && opt_rev_graph)
2213 /* Incrementally draw avoids flickering. */
2214 redraw_view_from(view, redraw_from);
2217 /* Update the title _after_ the redraw so that if the redraw picks up a
2218 * commit reference in view->ref it'll be available here. */
2219 update_view_title(view);
2222 if (ferror(view->pipe)) {
2223 report("Failed to read: %s", strerror(errno));
2226 } else if (feof(view->pipe)) {
2234 report("Allocation failure");
2237 if (view->ops->read(view, NULL))
2242 static struct line *
2243 add_line_data(struct view *view, void *data, enum line_type type)
2245 struct line *line = &view->line[view->lines++];
2247 memset(line, 0, sizeof(*line));
2254 static struct line *
2255 add_line_text(struct view *view, char *data, enum line_type type)
2258 data = strdup(data);
2260 return data ? add_line_data(view, data, type) : NULL;
2269 OPEN_DEFAULT = 0, /* Use default view switching. */
2270 OPEN_SPLIT = 1, /* Split current view. */
2271 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2272 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2276 open_view(struct view *prev, enum request request, enum open_flags flags)
2278 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2279 bool split = !!(flags & OPEN_SPLIT);
2280 bool reload = !!(flags & OPEN_RELOAD);
2281 struct view *view = VIEW(request);
2282 int nviews = displayed_views();
2283 struct view *base_view = display[0];
2285 if (view == prev && nviews == 1 && !reload) {
2286 report("Already in %s view", view->name);
2290 if (view->ops->open) {
2291 if (!view->ops->open(view)) {
2292 report("Failed to load %s view", view->name);
2296 } else if ((reload || strcmp(view->vid, view->id)) &&
2297 !begin_update(view)) {
2298 report("Failed to load %s view", view->name);
2307 /* Maximize the current view. */
2308 memset(display, 0, sizeof(display));
2310 display[current_view] = view;
2313 /* Resize the view when switching between split- and full-screen,
2314 * or when switching between two different full-screen views. */
2315 if (nviews != displayed_views() ||
2316 (nviews == 1 && base_view != display[0]))
2319 if (split && prev->lineno - prev->offset >= prev->height) {
2320 /* Take the title line into account. */
2321 int lines = prev->lineno - prev->offset - prev->height + 1;
2323 /* Scroll the view that was split if the current line is
2324 * outside the new limited view. */
2325 do_scroll_view(prev, lines);
2328 if (prev && view != prev) {
2329 if (split && !backgrounded) {
2330 /* "Blur" the previous view. */
2331 update_view_title(prev);
2334 view->parent = prev;
2337 if (view->pipe && view->lines == 0) {
2338 /* Clear the old view and let the incremental updating refill
2347 /* If the view is backgrounded the above calls to report()
2348 * won't redraw the view title. */
2350 update_view_title(view);
2354 open_external_viewer(const char *cmd)
2356 def_prog_mode(); /* save current tty modes */
2357 endwin(); /* restore original tty modes */
2359 fprintf(stderr, "Press Enter to continue");
2366 open_mergetool(const char *file)
2368 char cmd[SIZEOF_STR];
2369 char file_sq[SIZEOF_STR];
2371 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2372 string_format(cmd, "git mergetool %s", file_sq)) {
2373 open_external_viewer(cmd);
2378 open_editor(bool from_root, const char *file)
2380 char cmd[SIZEOF_STR];
2381 char file_sq[SIZEOF_STR];
2383 char *prefix = from_root ? opt_cdup : "";
2385 editor = getenv("GIT_EDITOR");
2386 if (!editor && *opt_editor)
2387 editor = opt_editor;
2389 editor = getenv("VISUAL");
2391 editor = getenv("EDITOR");
2395 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2396 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2397 open_external_viewer(cmd);
2402 open_run_request(enum request request)
2404 struct run_request *req = get_run_request(request);
2405 char buf[SIZEOF_STR * 2];
2410 report("Unknown run request");
2418 char *next = strstr(cmd, "%(");
2419 int len = next - cmd;
2426 } else if (!strncmp(next, "%(head)", 7)) {
2429 } else if (!strncmp(next, "%(commit)", 9)) {
2432 } else if (!strncmp(next, "%(blob)", 7)) {
2436 report("Unknown replacement in run request: `%s`", req->cmd);
2440 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2444 next = strchr(next, ')') + 1;
2448 open_external_viewer(buf);
2452 * User request switch noodle
2456 view_driver(struct view *view, enum request request)
2460 if (request == REQ_NONE) {
2465 if (request > REQ_NONE) {
2466 open_run_request(request);
2470 if (view && view->lines) {
2471 request = view->ops->request(view, request, &view->line[view->lineno]);
2472 if (request == REQ_NONE)
2479 case REQ_MOVE_PAGE_UP:
2480 case REQ_MOVE_PAGE_DOWN:
2481 case REQ_MOVE_FIRST_LINE:
2482 case REQ_MOVE_LAST_LINE:
2483 move_view(view, request);
2486 case REQ_SCROLL_LINE_DOWN:
2487 case REQ_SCROLL_LINE_UP:
2488 case REQ_SCROLL_PAGE_DOWN:
2489 case REQ_SCROLL_PAGE_UP:
2490 scroll_view(view, request);
2495 report("No file chosen, press %s to open tree view",
2496 get_key(REQ_VIEW_TREE));
2499 open_view(view, request, OPEN_DEFAULT);
2502 case REQ_VIEW_PAGER:
2503 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2504 report("No pager content, press %s to run command from prompt",
2505 get_key(REQ_PROMPT));
2508 open_view(view, request, OPEN_DEFAULT);
2511 case REQ_VIEW_STAGE:
2512 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2513 report("No stage content, press %s to open the status view and choose file",
2514 get_key(REQ_VIEW_STATUS));
2517 open_view(view, request, OPEN_DEFAULT);
2520 case REQ_VIEW_STATUS:
2521 if (opt_is_inside_work_tree == FALSE) {
2522 report("The status view requires a working tree");
2525 open_view(view, request, OPEN_DEFAULT);
2533 open_view(view, request, OPEN_DEFAULT);
2538 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2540 if ((view == VIEW(REQ_VIEW_DIFF) &&
2541 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2542 (view == VIEW(REQ_VIEW_STAGE) &&
2543 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2544 (view == VIEW(REQ_VIEW_BLOB) &&
2545 view->parent == VIEW(REQ_VIEW_TREE))) {
2548 view = view->parent;
2549 line = view->lineno;
2550 move_view(view, request);
2551 if (view_is_displayed(view))
2552 update_view_title(view);
2553 if (line != view->lineno)
2554 view->ops->request(view, REQ_ENTER,
2555 &view->line[view->lineno]);
2558 move_view(view, request);
2564 int nviews = displayed_views();
2565 int next_view = (current_view + 1) % nviews;
2567 if (next_view == current_view) {
2568 report("Only one view is displayed");
2572 current_view = next_view;
2573 /* Blur out the title of the previous view. */
2574 update_view_title(view);
2579 report("Refreshing is not yet supported for the %s view", view->name);
2582 case REQ_TOGGLE_LINENO:
2583 opt_line_number = !opt_line_number;
2587 case REQ_TOGGLE_DATE:
2588 opt_date = !opt_date;
2592 case REQ_TOGGLE_AUTHOR:
2593 opt_author = !opt_author;
2597 case REQ_TOGGLE_REV_GRAPH:
2598 opt_rev_graph = !opt_rev_graph;
2602 case REQ_TOGGLE_REFS:
2603 opt_show_refs = !opt_show_refs;
2608 /* Always reload^Wrerun commands from the prompt. */
2609 open_view(view, opt_request, OPEN_RELOAD);
2613 case REQ_SEARCH_BACK:
2614 search_view(view, request);
2619 find_next(view, request);
2622 case REQ_STOP_LOADING:
2623 for (i = 0; i < ARRAY_SIZE(views); i++) {
2626 report("Stopped loading the %s view", view->name),
2631 case REQ_SHOW_VERSION:
2632 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2635 case REQ_SCREEN_RESIZE:
2638 case REQ_SCREEN_REDRAW:
2643 report("Nothing to edit");
2648 report("Nothing to enter");
2652 case REQ_VIEW_CLOSE:
2653 /* XXX: Mark closed views by letting view->parent point to the
2654 * view itself. Parents to closed view should never be
2657 view->parent->parent != view->parent) {
2658 memset(display, 0, sizeof(display));
2660 display[current_view] = view->parent;
2661 view->parent = view;
2671 /* An unknown key will show most commonly used commands. */
2672 report("Unknown key, press 'h' for help");
2685 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2687 char *text = line->data;
2688 enum line_type type = line->type;
2691 wmove(view->win, lineno, 0);
2695 wchgat(view->win, -1, 0, type, NULL);
2698 attr = get_line_attr(type);
2699 wattrset(view->win, attr);
2701 if (opt_line_number || opt_tab_size < TABSIZE) {
2702 static char spaces[] = " ";
2703 int col_offset = 0, col = 0;
2705 if (opt_line_number) {
2706 unsigned long real_lineno = view->offset + lineno + 1;
2708 if (real_lineno == 1 ||
2709 (real_lineno % opt_num_interval) == 0) {
2710 wprintw(view->win, "%.*d", view->digits, real_lineno);
2713 waddnstr(view->win, spaces,
2714 MIN(view->digits, STRING_SIZE(spaces)));
2716 waddstr(view->win, ": ");
2717 col_offset = view->digits + 2;
2720 while (text && col_offset + col < view->width) {
2721 int cols_max = view->width - col_offset - col;
2725 if (*text == '\t') {
2727 assert(sizeof(spaces) > TABSIZE);
2729 cols = opt_tab_size - (col % opt_tab_size);
2732 text = strchr(text, '\t');
2733 cols = line ? text - pos : strlen(pos);
2736 waddnstr(view->win, pos, MIN(cols, cols_max));
2741 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
2743 draw_text(view, text, view->width, TRUE, tilde_attr);
2750 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2752 char refbuf[SIZEOF_STR];
2756 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2759 pipe = popen(refbuf, "r");
2763 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2764 ref = chomp_string(ref);
2770 /* This is the only fatal call, since it can "corrupt" the buffer. */
2771 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2778 add_pager_refs(struct view *view, struct line *line)
2780 char buf[SIZEOF_STR];
2781 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2783 size_t bufpos = 0, refpos = 0;
2784 const char *sep = "Refs: ";
2785 bool is_tag = FALSE;
2787 assert(line->type == LINE_COMMIT);
2789 refs = get_refs(commit_id);
2791 if (view == VIEW(REQ_VIEW_DIFF))
2792 goto try_add_describe_ref;
2797 struct ref *ref = refs[refpos];
2798 char *fmt = ref->tag ? "%s[%s]" :
2799 ref->remote ? "%s<%s>" : "%s%s";
2801 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2806 } while (refs[refpos++]->next);
2808 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2809 try_add_describe_ref:
2810 /* Add <tag>-g<commit_id> "fake" reference. */
2811 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2818 if (!realloc_lines(view, view->line_size + 1))
2821 add_line_text(view, buf, LINE_PP_REFS);
2825 pager_read(struct view *view, char *data)
2832 line = add_line_text(view, data, get_line_type(data));
2836 if (line->type == LINE_COMMIT &&
2837 (view == VIEW(REQ_VIEW_DIFF) ||
2838 view == VIEW(REQ_VIEW_LOG)))
2839 add_pager_refs(view, line);
2845 pager_request(struct view *view, enum request request, struct line *line)
2849 if (request != REQ_ENTER)
2852 if (line->type == LINE_COMMIT &&
2853 (view == VIEW(REQ_VIEW_LOG) ||
2854 view == VIEW(REQ_VIEW_PAGER))) {
2855 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2859 /* Always scroll the view even if it was split. That way
2860 * you can use Enter to scroll through the log view and
2861 * split open each commit diff. */
2862 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2864 /* FIXME: A minor workaround. Scrolling the view will call report("")
2865 * but if we are scrolling a non-current view this won't properly
2866 * update the view title. */
2868 update_view_title(view);
2874 pager_grep(struct view *view, struct line *line)
2877 char *text = line->data;
2882 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2889 pager_select(struct view *view, struct line *line)
2891 if (line->type == LINE_COMMIT) {
2892 char *text = (char *)line->data + STRING_SIZE("commit ");
2894 if (view != VIEW(REQ_VIEW_PAGER))
2895 string_copy_rev(view->ref, text);
2896 string_copy_rev(ref_commit, text);
2900 static struct view_ops pager_ops = {
2916 help_open(struct view *view)
2919 int lines = ARRAY_SIZE(req_info) + 2;
2922 if (view->lines > 0)
2925 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2926 if (!req_info[i].request)
2929 lines += run_requests + 1;
2931 view->line = calloc(lines, sizeof(*view->line));
2935 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2937 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2940 if (req_info[i].request == REQ_NONE)
2943 if (!req_info[i].request) {
2944 add_line_text(view, "", LINE_DEFAULT);
2945 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2949 key = get_key(req_info[i].request);
2951 key = "(no key defined)";
2953 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2956 add_line_text(view, buf, LINE_DEFAULT);
2960 add_line_text(view, "", LINE_DEFAULT);
2961 add_line_text(view, "External commands:", LINE_DEFAULT);
2964 for (i = 0; i < run_requests; i++) {
2965 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2971 key = get_key_name(req->key);
2973 key = "(no key defined)";
2975 if (!string_format(buf, " %-10s %-14s `%s`",
2976 keymap_table[req->keymap].name,
2980 add_line_text(view, buf, LINE_DEFAULT);
2986 static struct view_ops help_ops = {
3001 struct tree_stack_entry {
3002 struct tree_stack_entry *prev; /* Entry below this in the stack */
3003 unsigned long lineno; /* Line number to restore */
3004 char *name; /* Position of name in opt_path */
3007 /* The top of the path stack. */
3008 static struct tree_stack_entry *tree_stack = NULL;
3009 unsigned long tree_lineno = 0;
3012 pop_tree_stack_entry(void)
3014 struct tree_stack_entry *entry = tree_stack;
3016 tree_lineno = entry->lineno;
3018 tree_stack = entry->prev;
3023 push_tree_stack_entry(char *name, unsigned long lineno)
3025 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3026 size_t pathlen = strlen(opt_path);
3031 entry->prev = tree_stack;
3032 entry->name = opt_path + pathlen;
3035 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3036 pop_tree_stack_entry();
3040 /* Move the current line to the first tree entry. */
3042 entry->lineno = lineno;
3045 /* Parse output from git-ls-tree(1):
3047 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3048 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3049 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3050 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3053 #define SIZEOF_TREE_ATTR \
3054 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3056 #define TREE_UP_FORMAT "040000 tree %s\t.."
3059 tree_compare_entry(enum line_type type1, char *name1,
3060 enum line_type type2, char *name2)
3062 if (type1 != type2) {
3063 if (type1 == LINE_TREE_DIR)
3068 return strcmp(name1, name2);
3072 tree_read(struct view *view, char *text)
3074 size_t textlen = text ? strlen(text) : 0;
3075 char buf[SIZEOF_STR];
3077 enum line_type type;
3078 bool first_read = view->lines == 0;
3082 if (textlen <= SIZEOF_TREE_ATTR)
3085 type = text[STRING_SIZE("100644 ")] == 't'
3086 ? LINE_TREE_DIR : LINE_TREE_FILE;
3089 /* Add path info line */
3090 if (!string_format(buf, "Directory path /%s", opt_path) ||
3091 !realloc_lines(view, view->line_size + 1) ||
3092 !add_line_text(view, buf, LINE_DEFAULT))
3095 /* Insert "link" to parent directory. */
3097 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3098 !realloc_lines(view, view->line_size + 1) ||
3099 !add_line_text(view, buf, LINE_TREE_DIR))
3104 /* Strip the path part ... */
3106 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3107 size_t striplen = strlen(opt_path);
3108 char *path = text + SIZEOF_TREE_ATTR;
3110 if (pathlen > striplen)
3111 memmove(path, path + striplen,
3112 pathlen - striplen + 1);
3115 /* Skip "Directory ..." and ".." line. */
3116 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3117 struct line *line = &view->line[pos];
3118 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
3119 char *path2 = text + SIZEOF_TREE_ATTR;
3120 int cmp = tree_compare_entry(line->type, path1, type, path2);
3125 text = strdup(text);
3129 if (view->lines > pos)
3130 memmove(&view->line[pos + 1], &view->line[pos],
3131 (view->lines - pos) * sizeof(*line));
3133 line = &view->line[pos];
3140 if (!add_line_text(view, text, type))
3143 if (tree_lineno > view->lineno) {
3144 view->lineno = tree_lineno;
3152 tree_request(struct view *view, enum request request, struct line *line)
3154 enum open_flags flags;
3156 if (request == REQ_TREE_PARENT) {
3159 request = REQ_ENTER;
3160 line = &view->line[1];
3162 /* quit view if at top of tree */
3163 return REQ_VIEW_CLOSE;
3166 if (request != REQ_ENTER)
3169 /* Cleanup the stack if the tree view is at a different tree. */
3170 while (!*opt_path && tree_stack)
3171 pop_tree_stack_entry();
3173 switch (line->type) {
3175 /* Depending on whether it is a subdir or parent (updir?) link
3176 * mangle the path buffer. */
3177 if (line == &view->line[1] && *opt_path) {
3178 pop_tree_stack_entry();
3181 char *data = line->data;
3182 char *basename = data + SIZEOF_TREE_ATTR;
3184 push_tree_stack_entry(basename, view->lineno);
3187 /* Trees and subtrees share the same ID, so they are not not
3188 * unique like blobs. */
3189 flags = OPEN_RELOAD;
3190 request = REQ_VIEW_TREE;
3193 case LINE_TREE_FILE:
3194 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3195 request = REQ_VIEW_BLOB;
3202 open_view(view, request, flags);
3203 if (request == REQ_VIEW_TREE) {
3204 view->lineno = tree_lineno;
3211 tree_select(struct view *view, struct line *line)
3213 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3215 if (line->type == LINE_TREE_FILE) {
3216 string_copy_rev(ref_blob, text);
3218 } else if (line->type != LINE_TREE_DIR) {
3222 string_copy_rev(view->ref, text);
3225 static struct view_ops tree_ops = {
3236 blob_read(struct view *view, char *line)
3238 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3241 static struct view_ops blob_ops = {
3260 char rev[SIZEOF_REV];
3261 char name[SIZEOF_STR];
3265 char rev[SIZEOF_REV];
3266 char name[SIZEOF_STR];
3270 static struct status stage_status;
3271 static enum line_type stage_line_type;
3273 /* Get fields from the diff line:
3274 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3277 status_get_diff(struct status *file, char *buf, size_t bufsize)
3279 char *old_mode = buf + 1;
3280 char *new_mode = buf + 8;
3281 char *old_rev = buf + 15;
3282 char *new_rev = buf + 56;
3283 char *status = buf + 97;
3286 old_mode[-1] != ':' ||
3287 new_mode[-1] != ' ' ||
3288 old_rev[-1] != ' ' ||
3289 new_rev[-1] != ' ' ||
3293 file->status = *status;
3295 string_copy_rev(file->old.rev, old_rev);
3296 string_copy_rev(file->new.rev, new_rev);
3298 file->old.mode = strtoul(old_mode, NULL, 8);
3299 file->new.mode = strtoul(new_mode, NULL, 8);
3301 file->old.name[0] = file->new.name[0] = 0;
3307 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3309 struct status *file = NULL;
3310 struct status *unmerged = NULL;
3311 char buf[SIZEOF_STR * 4];
3315 pipe = popen(cmd, "r");
3319 add_line_data(view, NULL, type);
3321 while (!feof(pipe) && !ferror(pipe)) {
3325 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3328 bufsize += readsize;
3330 /* Process while we have NUL chars. */
3331 while ((sep = memchr(buf, 0, bufsize))) {
3332 size_t sepsize = sep - buf + 1;
3335 if (!realloc_lines(view, view->line_size + 1))
3338 file = calloc(1, sizeof(*file));
3342 add_line_data(view, file, type);
3345 /* Parse diff info part. */
3349 } else if (!file->status) {
3350 if (!status_get_diff(file, buf, sepsize))
3354 memmove(buf, sep + 1, bufsize);
3356 sep = memchr(buf, 0, bufsize);
3359 sepsize = sep - buf + 1;
3361 /* Collapse all 'M'odified entries that
3362 * follow a associated 'U'nmerged entry.
3364 if (file->status == 'U') {
3367 } else if (unmerged) {
3368 int collapse = !strcmp(buf, unmerged->new.name);
3379 /* Grab the old name for rename/copy. */
3380 if (!*file->old.name &&
3381 (file->status == 'R' || file->status == 'C')) {
3382 sepsize = sep - buf + 1;
3383 string_ncopy(file->old.name, buf, sepsize);
3385 memmove(buf, sep + 1, bufsize);
3387 sep = memchr(buf, 0, bufsize);
3390 sepsize = sep - buf + 1;
3393 /* git-ls-files just delivers a NUL separated
3394 * list of file names similar to the second half
3395 * of the git-diff-* output. */
3396 string_ncopy(file->new.name, buf, sepsize);
3397 if (!*file->old.name)
3398 string_copy(file->old.name, file->new.name);
3400 memmove(buf, sep + 1, bufsize);
3411 if (!view->line[view->lines - 1].data)
3412 add_line_data(view, NULL, LINE_STAT_NONE);
3418 /* Don't show unmerged entries in the staged section. */
3419 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3420 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3421 #define STATUS_LIST_OTHER_CMD \
3422 "git ls-files -z --others --exclude-per-directory=.gitignore"
3424 #define STATUS_DIFF_INDEX_SHOW_CMD \
3425 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3427 #define STATUS_DIFF_FILES_SHOW_CMD \
3428 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3430 /* First parse staged info using git-diff-index(1), then parse unstaged
3431 * info using git-diff-files(1), and finally untracked files using
3432 * git-ls-files(1). */
3434 status_open(struct view *view)
3436 struct stat statbuf;
3437 char exclude[SIZEOF_STR];
3438 char cmd[SIZEOF_STR];
3439 unsigned long prev_lineno = view->lineno;
3442 for (i = 0; i < view->lines; i++)
3443 free(view->line[i].data);
3445 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3448 if (!realloc_lines(view, view->line_size + 6))
3451 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3454 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3456 if (stat(exclude, &statbuf) >= 0) {
3457 size_t cmdsize = strlen(cmd);
3459 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3460 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3464 system("git update-index -q --refresh");
3466 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3467 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3468 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3471 /* If all went well restore the previous line number to stay in
3473 if (prev_lineno < view->lines)
3474 view->lineno = prev_lineno;
3476 view->lineno = view->lines - 1;
3482 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3484 struct status *status = line->data;
3485 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
3487 wmove(view->win, lineno, 0);
3490 wattrset(view->win, get_line_attr(LINE_CURSOR));
3491 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3494 } else if (!status && line->type != LINE_STAT_NONE) {
3495 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3496 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3499 wattrset(view->win, get_line_attr(line->type));
3505 switch (line->type) {
3506 case LINE_STAT_STAGED:
3507 text = "Changes to be committed:";
3510 case LINE_STAT_UNSTAGED:
3511 text = "Changed but not updated:";
3514 case LINE_STAT_UNTRACKED:
3515 text = "Untracked files:";
3518 case LINE_STAT_NONE:
3519 text = " (no files)";
3526 draw_text(view, text, view->width, TRUE, tilde_attr);
3530 waddch(view->win, status->status);
3532 wattrset(view->win, A_NORMAL);
3533 wmove(view->win, lineno, 4);
3534 if (view->width < 5)
3537 draw_text(view, status->new.name, view->width - 5, TRUE, tilde_attr);
3542 status_enter(struct view *view, struct line *line)
3544 struct status *status = line->data;
3545 char oldpath[SIZEOF_STR] = "";
3546 char newpath[SIZEOF_STR] = "";
3550 if (line->type == LINE_STAT_NONE ||
3551 (!status && line[1].type == LINE_STAT_NONE)) {
3552 report("No file to diff");
3557 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
3559 /* Diffs for unmerged entries are empty when pasing the
3560 * new path, so leave it empty. */
3561 if (status->status != 'U' &&
3562 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
3567 line->type != LINE_STAT_UNTRACKED &&
3568 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3571 switch (line->type) {
3572 case LINE_STAT_STAGED:
3573 if (!string_format_from(opt_cmd, &cmdsize,
3574 STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
3577 info = "Staged changes to %s";
3579 info = "Staged changes";
3582 case LINE_STAT_UNSTAGED:
3583 if (!string_format_from(opt_cmd, &cmdsize,
3584 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
3587 info = "Unstaged changes to %s";
3589 info = "Unstaged changes";
3592 case LINE_STAT_UNTRACKED:
3598 report("No file to show");
3602 opt_pipe = fopen(status->new.name, "r");
3603 info = "Untracked file %s";
3607 die("line type %d not handled in switch", line->type);
3610 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3611 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
3613 stage_status = *status;
3615 memset(&stage_status, 0, sizeof(stage_status));
3618 stage_line_type = line->type;
3619 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
3627 status_update_file(struct view *view, struct status *status, enum line_type type)
3629 char cmd[SIZEOF_STR];
3630 char buf[SIZEOF_STR];
3637 type != LINE_STAT_UNTRACKED &&
3638 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3642 case LINE_STAT_STAGED:
3643 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3646 status->old.name, 0))
3649 string_add(cmd, cmdsize, "git update-index -z --index-info");
3652 case LINE_STAT_UNSTAGED:
3653 case LINE_STAT_UNTRACKED:
3654 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
3657 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3661 die("line type %d not handled in switch", type);
3664 pipe = popen(cmd, "w");
3668 while (!ferror(pipe) && written < bufsize) {
3669 written += fwrite(buf + written, 1, bufsize - written, pipe);
3674 if (written != bufsize)
3681 status_update(struct view *view)
3683 struct line *line = &view->line[view->lineno];
3685 assert(view->lines);
3688 while (++line < view->line + view->lines && line->data) {
3689 if (!status_update_file(view, line->data, line->type))
3690 report("Failed to update file status");
3693 if (!line[-1].data) {
3694 report("Nothing to update");
3698 } else if (!status_update_file(view, line->data, line->type)) {
3699 report("Failed to update file status");
3704 status_request(struct view *view, enum request request, struct line *line)
3706 struct status *status = line->data;
3709 case REQ_STATUS_UPDATE:
3710 status_update(view);
3713 case REQ_STATUS_MERGE:
3714 if (!status || status->status != 'U') {
3715 report("Merging only possible for files with unmerged status ('U').");
3718 open_mergetool(status->new.name);
3725 open_editor(status->status != '?', status->new.name);
3729 /* After returning the status view has been split to
3730 * show the stage view. No further reloading is
3732 status_enter(view, line);
3736 /* Simply reload the view. */
3743 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3749 status_select(struct view *view, struct line *line)
3751 struct status *status = line->data;
3752 char file[SIZEOF_STR] = "all files";
3756 if (status && !string_format(file, "'%s'", status->new.name))
3759 if (!status && line[1].type == LINE_STAT_NONE)
3762 switch (line->type) {
3763 case LINE_STAT_STAGED:
3764 text = "Press %s to unstage %s for commit";
3767 case LINE_STAT_UNSTAGED:
3768 text = "Press %s to stage %s for commit";
3771 case LINE_STAT_UNTRACKED:
3772 text = "Press %s to stage %s for addition";
3775 case LINE_STAT_NONE:
3776 text = "Nothing to update";
3780 die("line type %d not handled in switch", line->type);
3783 if (status && status->status == 'U') {
3784 text = "Press %s to resolve conflict in %s";
3785 key = get_key(REQ_STATUS_MERGE);
3788 key = get_key(REQ_STATUS_UPDATE);
3791 string_format(view->ref, text, key, file);
3795 status_grep(struct view *view, struct line *line)
3797 struct status *status = line->data;
3798 enum { S_STATUS, S_NAME, S_END } state;
3805 for (state = S_STATUS; state < S_END; state++) {
3809 case S_NAME: text = status->new.name; break;
3811 buf[0] = status->status;
3819 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3826 static struct view_ops status_ops = {
3838 stage_diff_line(FILE *pipe, struct line *line)
3840 char *buf = line->data;
3841 size_t bufsize = strlen(buf);
3844 while (!ferror(pipe) && written < bufsize) {
3845 written += fwrite(buf + written, 1, bufsize - written, pipe);
3850 return written == bufsize;
3853 static struct line *
3854 stage_diff_hdr(struct view *view, struct line *line)
3856 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3857 struct line *diff_hdr;
3859 if (line->type == LINE_DIFF_CHUNK)
3860 diff_hdr = line - 1;
3862 diff_hdr = view->line + 1;
3864 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3865 if (diff_hdr->type == LINE_DIFF_HEADER)
3868 diff_hdr += diff_hdr_dir;
3875 stage_update_chunk(struct view *view, struct line *line)
3877 char cmd[SIZEOF_STR];
3879 struct line *diff_hdr, *diff_chunk, *diff_end;
3882 diff_hdr = stage_diff_hdr(view, line);
3887 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3890 if (!string_format_from(cmd, &cmdsize,
3891 "git apply --cached %s - && "
3892 "git update-index -q --unmerged --refresh 2>/dev/null",
3893 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3896 pipe = popen(cmd, "w");
3900 diff_end = view->line + view->lines;
3901 if (line->type != LINE_DIFF_CHUNK) {
3902 diff_chunk = diff_hdr;
3905 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3906 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3907 diff_chunk->type == LINE_DIFF_HEADER)
3908 diff_end = diff_chunk;
3912 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3913 switch (diff_hdr->type) {
3914 case LINE_DIFF_HEADER:
3915 case LINE_DIFF_INDEX:
3925 if (!stage_diff_line(pipe, diff_hdr++)) {
3932 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3937 if (diff_chunk != diff_end)
3944 stage_update(struct view *view, struct line *line)
3946 if (stage_line_type != LINE_STAT_UNTRACKED &&
3947 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3948 if (!stage_update_chunk(view, line)) {
3949 report("Failed to apply chunk");
3953 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3954 report("Failed to update file");
3958 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3960 view = VIEW(REQ_VIEW_STATUS);
3961 if (view_is_displayed(view))
3962 status_enter(view, &view->line[view->lineno]);
3966 stage_request(struct view *view, enum request request, struct line *line)
3969 case REQ_STATUS_UPDATE:
3970 stage_update(view, line);
3974 if (!stage_status.new.name[0])
3977 open_editor(stage_status.status != '?', stage_status.new.name);
3981 pager_request(view, request, line);
3991 static struct view_ops stage_ops = {
4007 char id[SIZEOF_REV]; /* SHA1 ID. */
4008 char title[128]; /* First line of the commit message. */
4009 char author[75]; /* Author of the commit. */
4010 struct tm time; /* Date from the author ident. */
4011 struct ref **refs; /* Repository references. */
4012 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4013 size_t graph_size; /* The width of the graph array. */
4014 bool has_parents; /* Rewritten --parents seen. */
4017 /* Size of rev graph with no "padding" columns */
4018 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4021 struct rev_graph *prev, *next, *parents;
4022 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4024 struct commit *commit;
4026 unsigned int boundary:1;
4029 /* Parents of the commit being visualized. */
4030 static struct rev_graph graph_parents[4];
4032 /* The current stack of revisions on the graph. */
4033 static struct rev_graph graph_stacks[4] = {
4034 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4035 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4036 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4037 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4041 graph_parent_is_merge(struct rev_graph *graph)
4043 return graph->parents->size > 1;
4047 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4049 struct commit *commit = graph->commit;
4051 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4052 commit->graph[commit->graph_size++] = symbol;
4056 done_rev_graph(struct rev_graph *graph)
4058 if (graph_parent_is_merge(graph) &&
4059 graph->pos < graph->size - 1 &&
4060 graph->next->size == graph->size + graph->parents->size - 1) {
4061 size_t i = graph->pos + graph->parents->size - 1;
4063 graph->commit->graph_size = i * 2;
4064 while (i < graph->next->size - 1) {
4065 append_to_rev_graph(graph, ' ');
4066 append_to_rev_graph(graph, '\\');
4071 graph->size = graph->pos = 0;
4072 graph->commit = NULL;
4073 memset(graph->parents, 0, sizeof(*graph->parents));
4077 push_rev_graph(struct rev_graph *graph, char *parent)
4081 /* "Collapse" duplicate parents lines.
4083 * FIXME: This needs to also update update the drawn graph but
4084 * for now it just serves as a method for pruning graph lines. */
4085 for (i = 0; i < graph->size; i++)
4086 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4089 if (graph->size < SIZEOF_REVITEMS) {
4090 string_copy_rev(graph->rev[graph->size++], parent);
4095 get_rev_graph_symbol(struct rev_graph *graph)
4099 if (graph->boundary)
4100 symbol = REVGRAPH_BOUND;
4101 else if (graph->parents->size == 0)
4102 symbol = REVGRAPH_INIT;
4103 else if (graph_parent_is_merge(graph))
4104 symbol = REVGRAPH_MERGE;
4105 else if (graph->pos >= graph->size)
4106 symbol = REVGRAPH_BRANCH;
4108 symbol = REVGRAPH_COMMIT;
4114 draw_rev_graph(struct rev_graph *graph)
4117 chtype separator, line;
4119 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4120 static struct rev_filler fillers[] = {
4121 { ' ', REVGRAPH_LINE },
4126 chtype symbol = get_rev_graph_symbol(graph);
4127 struct rev_filler *filler;
4130 filler = &fillers[DEFAULT];
4132 for (i = 0; i < graph->pos; i++) {
4133 append_to_rev_graph(graph, filler->line);
4134 if (graph_parent_is_merge(graph->prev) &&
4135 graph->prev->pos == i)
4136 filler = &fillers[RSHARP];
4138 append_to_rev_graph(graph, filler->separator);
4141 /* Place the symbol for this revision. */
4142 append_to_rev_graph(graph, symbol);
4144 if (graph->prev->size > graph->size)
4145 filler = &fillers[RDIAG];
4147 filler = &fillers[DEFAULT];
4151 for (; i < graph->size; i++) {
4152 append_to_rev_graph(graph, filler->separator);
4153 append_to_rev_graph(graph, filler->line);
4154 if (graph_parent_is_merge(graph->prev) &&
4155 i < graph->prev->pos + graph->parents->size)
4156 filler = &fillers[RSHARP];
4157 if (graph->prev->size > graph->size)
4158 filler = &fillers[LDIAG];
4161 if (graph->prev->size > graph->size) {
4162 append_to_rev_graph(graph, filler->separator);
4163 if (filler->line != ' ')
4164 append_to_rev_graph(graph, filler->line);
4168 /* Prepare the next rev graph */
4170 prepare_rev_graph(struct rev_graph *graph)
4174 /* First, traverse all lines of revisions up to the active one. */
4175 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4176 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4179 push_rev_graph(graph->next, graph->rev[graph->pos]);
4182 /* Interleave the new revision parent(s). */
4183 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4184 push_rev_graph(graph->next, graph->parents->rev[i]);
4186 /* Lastly, put any remaining revisions. */
4187 for (i = graph->pos + 1; i < graph->size; i++)
4188 push_rev_graph(graph->next, graph->rev[i]);
4192 update_rev_graph(struct rev_graph *graph)
4194 /* If this is the finalizing update ... */
4196 prepare_rev_graph(graph);
4198 /* Graph visualization needs a one rev look-ahead,
4199 * so the first update doesn't visualize anything. */
4200 if (!graph->prev->commit)
4203 draw_rev_graph(graph->prev);
4204 done_rev_graph(graph->prev->prev);
4213 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4215 char buf[DATE_COLS + 1];
4216 struct commit *commit = line->data;
4217 enum line_type type;
4223 if (!*commit->author)
4226 space = view->width;
4227 wmove(view->win, lineno, col);
4231 wattrset(view->win, get_line_attr(type));
4232 wchgat(view->win, -1, 0, type, NULL);
4235 type = LINE_MAIN_COMMIT;
4236 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4237 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4243 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4244 n = draw_text(view, buf, view->width - col, FALSE, tilde_attr);
4245 draw_text(view, " ", view->width - col - n, FALSE, tilde_attr);
4248 wmove(view->win, lineno, col);
4249 if (col >= view->width)
4252 if (type != LINE_CURSOR)
4253 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4258 max_len = view->width - col;
4259 if (max_len > AUTHOR_COLS - 1)
4260 max_len = AUTHOR_COLS - 1;
4261 draw_text(view, commit->author, max_len, TRUE, tilde_attr);
4263 if (col >= view->width)
4267 if (opt_rev_graph && commit->graph_size) {
4268 size_t graph_size = view->width - col;
4271 if (type != LINE_CURSOR)
4272 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4273 wmove(view->win, lineno, col);
4274 if (graph_size > commit->graph_size)
4275 graph_size = commit->graph_size;
4276 /* Using waddch() instead of waddnstr() ensures that
4277 * they'll be rendered correctly for the cursor line. */
4278 for (i = 0; i < graph_size; i++)
4279 waddch(view->win, commit->graph[i]);
4281 col += commit->graph_size + 1;
4282 if (col >= view->width)
4284 waddch(view->win, ' ');
4286 if (type != LINE_CURSOR)
4287 wattrset(view->win, A_NORMAL);
4289 wmove(view->win, lineno, col);
4291 if (opt_show_refs && commit->refs) {
4295 if (type == LINE_CURSOR)
4297 else if (commit->refs[i]->ltag)
4298 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4299 else if (commit->refs[i]->tag)
4300 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4301 else if (commit->refs[i]->remote)
4302 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4304 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4306 col += draw_text(view, "[", view->width - col, TRUE, tilde_attr);
4307 col += draw_text(view, commit->refs[i]->name, view->width - col,
4309 col += draw_text(view, "]", view->width - col, TRUE, tilde_attr);
4310 if (type != LINE_CURSOR)
4311 wattrset(view->win, A_NORMAL);
4312 col += draw_text(view, " ", view->width - col, TRUE, tilde_attr);
4313 if (col >= view->width)
4315 } while (commit->refs[i++]->next);
4318 if (type != LINE_CURSOR)
4319 wattrset(view->win, get_line_attr(type));
4321 draw_text(view, commit->title, view->width - col, TRUE, tilde_attr);
4325 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4327 main_read(struct view *view, char *line)
4329 static struct rev_graph *graph = graph_stacks;
4330 enum line_type type;
4331 struct commit *commit;
4334 update_rev_graph(graph);
4338 type = get_line_type(line);
4339 if (type == LINE_COMMIT) {
4340 commit = calloc(1, sizeof(struct commit));
4344 line += STRING_SIZE("commit ");
4346 graph->boundary = 1;
4350 string_copy_rev(commit->id, line);
4351 commit->refs = get_refs(commit->id);
4352 graph->commit = commit;
4353 add_line_data(view, commit, LINE_MAIN_COMMIT);
4355 while ((line = strchr(line, ' '))) {
4357 push_rev_graph(graph->parents, line);
4358 commit->has_parents = TRUE;
4365 commit = view->line[view->lines - 1].data;
4369 if (commit->has_parents)
4371 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4376 /* Parse author lines where the name may be empty:
4377 * author <email@address.tld> 1138474660 +0100
4379 char *ident = line + STRING_SIZE("author ");
4380 char *nameend = strchr(ident, '<');
4381 char *emailend = strchr(ident, '>');
4383 if (!nameend || !emailend)
4386 update_rev_graph(graph);
4387 graph = graph->next;
4389 *nameend = *emailend = 0;
4390 ident = chomp_string(ident);
4392 ident = chomp_string(nameend + 1);
4397 string_ncopy(commit->author, ident, strlen(ident));
4399 /* Parse epoch and timezone */
4400 if (emailend[1] == ' ') {
4401 char *secs = emailend + 2;
4402 char *zone = strchr(secs, ' ');
4403 time_t time = (time_t) atol(secs);
4405 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4409 tz = ('0' - zone[1]) * 60 * 60 * 10;
4410 tz += ('0' - zone[2]) * 60 * 60;
4411 tz += ('0' - zone[3]) * 60;
4412 tz += ('0' - zone[4]) * 60;
4420 gmtime_r(&time, &commit->time);
4425 /* Fill in the commit title if it has not already been set. */
4426 if (commit->title[0])
4429 /* Require titles to start with a non-space character at the
4430 * offset used by git log. */
4431 if (strncmp(line, " ", 4))
4434 /* Well, if the title starts with a whitespace character,
4435 * try to be forgiving. Otherwise we end up with no title. */
4436 while (isspace(*line))
4440 /* FIXME: More graceful handling of titles; append "..." to
4441 * shortened titles, etc. */
4443 string_ncopy(commit->title, line, strlen(line));
4450 main_request(struct view *view, enum request request, struct line *line)
4452 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4454 if (request == REQ_ENTER)
4455 open_view(view, REQ_VIEW_DIFF, flags);
4463 main_grep(struct view *view, struct line *line)
4465 struct commit *commit = line->data;
4466 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4467 char buf[DATE_COLS + 1];
4470 for (state = S_TITLE; state < S_END; state++) {
4474 case S_TITLE: text = commit->title; break;
4475 case S_AUTHOR: text = commit->author; break;
4477 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4486 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4494 main_select(struct view *view, struct line *line)
4496 struct commit *commit = line->data;
4498 string_copy_rev(view->ref, commit->id);
4499 string_copy_rev(ref_commit, view->ref);
4502 static struct view_ops main_ops = {
4514 * Unicode / UTF-8 handling
4516 * NOTE: Much of the following code for dealing with unicode is derived from
4517 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4518 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4521 /* I've (over)annotated a lot of code snippets because I am not entirely
4522 * confident that the approach taken by this small UTF-8 interface is correct.
4526 unicode_width(unsigned long c)
4529 (c <= 0x115f /* Hangul Jamo */
4532 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
4534 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4535 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4536 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4537 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4538 || (c >= 0xffe0 && c <= 0xffe6)
4539 || (c >= 0x20000 && c <= 0x2fffd)
4540 || (c >= 0x30000 && c <= 0x3fffd)))
4544 return opt_tab_size;
4549 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4550 * Illegal bytes are set one. */
4551 static const unsigned char utf8_bytes[256] = {
4552 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,
4553 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,
4554 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,
4555 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,
4556 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,
4557 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,
4558 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,
4559 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,
4562 /* Decode UTF-8 multi-byte representation into a unicode character. */
4563 static inline unsigned long
4564 utf8_to_unicode(const char *string, size_t length)
4566 unsigned long unicode;
4570 unicode = string[0];
4573 unicode = (string[0] & 0x1f) << 6;
4574 unicode += (string[1] & 0x3f);
4577 unicode = (string[0] & 0x0f) << 12;
4578 unicode += ((string[1] & 0x3f) << 6);
4579 unicode += (string[2] & 0x3f);
4582 unicode = (string[0] & 0x0f) << 18;
4583 unicode += ((string[1] & 0x3f) << 12);
4584 unicode += ((string[2] & 0x3f) << 6);
4585 unicode += (string[3] & 0x3f);
4588 unicode = (string[0] & 0x0f) << 24;
4589 unicode += ((string[1] & 0x3f) << 18);
4590 unicode += ((string[2] & 0x3f) << 12);
4591 unicode += ((string[3] & 0x3f) << 6);
4592 unicode += (string[4] & 0x3f);
4595 unicode = (string[0] & 0x01) << 30;
4596 unicode += ((string[1] & 0x3f) << 24);
4597 unicode += ((string[2] & 0x3f) << 18);
4598 unicode += ((string[3] & 0x3f) << 12);
4599 unicode += ((string[4] & 0x3f) << 6);
4600 unicode += (string[5] & 0x3f);
4603 die("Invalid unicode length");
4606 /* Invalid characters could return the special 0xfffd value but NUL
4607 * should be just as good. */
4608 return unicode > 0xffff ? 0 : unicode;
4611 /* Calculates how much of string can be shown within the given maximum width
4612 * and sets trimmed parameter to non-zero value if all of string could not be
4613 * shown. If the reserve flag is TRUE, it will reserve at least one
4614 * trailing character, which can be useful when drawing a delimiter.
4616 * Returns the number of bytes to output from string to satisfy max_width. */
4618 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
4620 const char *start = string;
4621 const char *end = strchr(string, '\0');
4622 unsigned char last_bytes = 0;
4627 while (string < end) {
4628 int c = *(unsigned char *) string;
4629 unsigned char bytes = utf8_bytes[c];
4631 unsigned long unicode;
4633 if (string + bytes > end)
4636 /* Change representation to figure out whether
4637 * it is a single- or double-width character. */
4639 unicode = utf8_to_unicode(string, bytes);
4640 /* FIXME: Graceful handling of invalid unicode character. */
4644 ucwidth = unicode_width(unicode);
4646 if (width > max_width) {
4648 if (reserve && width - ucwidth == max_width) {
4649 string -= last_bytes;
4658 return string - start;
4666 /* Whether or not the curses interface has been initialized. */
4667 static bool cursed = FALSE;
4669 /* The status window is used for polling keystrokes. */
4670 static WINDOW *status_win;
4672 static bool status_empty = TRUE;
4674 /* Update status and title window. */
4676 report(const char *msg, ...)
4678 struct view *view = display[current_view];
4684 char buf[SIZEOF_STR];
4687 va_start(args, msg);
4688 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4689 buf[sizeof(buf) - 1] = 0;
4690 buf[sizeof(buf) - 2] = '.';
4691 buf[sizeof(buf) - 3] = '.';
4692 buf[sizeof(buf) - 4] = '.';
4698 if (!status_empty || *msg) {
4701 va_start(args, msg);
4703 wmove(status_win, 0, 0);
4705 vwprintw(status_win, msg, args);
4706 status_empty = FALSE;
4708 status_empty = TRUE;
4710 wclrtoeol(status_win);
4711 wrefresh(status_win);
4716 update_view_title(view);
4717 update_display_cursor(view);
4720 /* Controls when nodelay should be in effect when polling user input. */
4722 set_nonblocking_input(bool loading)
4724 static unsigned int loading_views;
4726 if ((loading == FALSE && loading_views-- == 1) ||
4727 (loading == TRUE && loading_views++ == 0))
4728 nodelay(status_win, loading);
4736 /* Initialize the curses library */
4737 if (isatty(STDIN_FILENO)) {
4738 cursed = !!initscr();
4740 /* Leave stdin and stdout alone when acting as a pager. */
4741 FILE *io = fopen("/dev/tty", "r+");
4744 die("Failed to open /dev/tty");
4745 cursed = !!newterm(NULL, io, io);
4749 die("Failed to initialize curses");
4751 nonl(); /* Tell curses not to do NL->CR/NL on output */
4752 cbreak(); /* Take input chars one at a time, no wait for \n */
4753 noecho(); /* Don't echo input */
4754 leaveok(stdscr, TRUE);
4759 getmaxyx(stdscr, y, x);
4760 status_win = newwin(1, 0, y - 1, 0);
4762 die("Failed to create status window");
4764 /* Enable keyboard mapping */
4765 keypad(status_win, TRUE);
4766 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4770 read_prompt(const char *prompt)
4772 enum { READING, STOP, CANCEL } status = READING;
4773 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4776 while (status == READING) {
4782 foreach_view (view, i)
4787 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4788 wclrtoeol(status_win);
4790 /* Refresh, accept single keystroke of input */
4791 key = wgetch(status_win);
4796 status = pos ? STOP : CANCEL;
4814 if (pos >= sizeof(buf)) {
4815 report("Input string too long");
4820 buf[pos++] = (char) key;
4824 /* Clear the status window */
4825 status_empty = FALSE;
4828 if (status == CANCEL)
4837 * Repository references
4840 static struct ref *refs = NULL;
4841 static size_t refs_alloc = 0;
4842 static size_t refs_size = 0;
4844 /* Id <-> ref store */
4845 static struct ref ***id_refs = NULL;
4846 static size_t id_refs_alloc = 0;
4847 static size_t id_refs_size = 0;
4849 static struct ref **
4852 struct ref ***tmp_id_refs;
4853 struct ref **ref_list = NULL;
4854 size_t ref_list_alloc = 0;
4855 size_t ref_list_size = 0;
4858 for (i = 0; i < id_refs_size; i++)
4859 if (!strcmp(id, id_refs[i][0]->id))
4862 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
4867 id_refs = tmp_id_refs;
4869 for (i = 0; i < refs_size; i++) {
4872 if (strcmp(id, refs[i].id))
4875 tmp = realloc_items(ref_list, &ref_list_alloc,
4876 ref_list_size + 1, sizeof(*ref_list));
4884 if (ref_list_size > 0)
4885 ref_list[ref_list_size - 1]->next = 1;
4886 ref_list[ref_list_size] = &refs[i];
4888 /* XXX: The properties of the commit chains ensures that we can
4889 * safely modify the shared ref. The repo references will
4890 * always be similar for the same id. */
4891 ref_list[ref_list_size]->next = 0;
4896 id_refs[id_refs_size++] = ref_list;
4902 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4907 bool remote = FALSE;
4908 bool check_replace = FALSE;
4910 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4911 if (!strcmp(name + namelen - 3, "^{}")) {
4914 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
4915 check_replace = TRUE;
4921 namelen -= STRING_SIZE("refs/tags/");
4922 name += STRING_SIZE("refs/tags/");
4924 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4926 namelen -= STRING_SIZE("refs/remotes/");
4927 name += STRING_SIZE("refs/remotes/");
4929 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4930 namelen -= STRING_SIZE("refs/heads/");
4931 name += STRING_SIZE("refs/heads/");
4933 } else if (!strcmp(name, "HEAD")) {
4937 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
4938 /* it's an annotated tag, replace the previous sha1 with the
4939 * resolved commit id; relies on the fact git-ls-remote lists
4940 * the commit id of an annotated tag right beofre the commit id
4942 refs[refs_size - 1].ltag = ltag;
4943 string_copy_rev(refs[refs_size - 1].id, id);
4947 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
4951 ref = &refs[refs_size++];
4952 ref->name = malloc(namelen + 1);
4956 strncpy(ref->name, name, namelen);
4957 ref->name[namelen] = 0;
4960 ref->remote = remote;
4961 string_copy_rev(ref->id, id);
4969 const char *cmd_env = getenv("TIG_LS_REMOTE");
4970 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4972 return read_properties(popen(cmd, "r"), "\t", read_ref);
4976 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4978 if (!strcmp(name, "i18n.commitencoding"))
4979 string_ncopy(opt_encoding, value, valuelen);
4981 if (!strcmp(name, "core.editor"))
4982 string_ncopy(opt_editor, value, valuelen);
4988 load_repo_config(void)
4990 return read_properties(popen(GIT_CONFIG " --list", "r"),
4991 "=", read_repo_config_option);
4995 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4997 if (!opt_git_dir[0]) {
4998 string_ncopy(opt_git_dir, name, namelen);
5000 } else if (opt_is_inside_work_tree == -1) {
5001 /* This can be 3 different values depending on the
5002 * version of git being used. If git-rev-parse does not
5003 * understand --is-inside-work-tree it will simply echo
5004 * the option else either "true" or "false" is printed.
5005 * Default to true for the unknown case. */
5006 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5009 string_ncopy(opt_cdup, name, namelen);
5015 /* XXX: The line outputted by "--show-cdup" can be empty so the option
5016 * must be the last one! */
5018 load_repo_info(void)
5020 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
5021 "=", read_repo_info);
5025 read_properties(FILE *pipe, const char *separators,
5026 int (*read_property)(char *, size_t, char *, size_t))
5028 char buffer[BUFSIZ];
5035 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5040 name = chomp_string(name);
5041 namelen = strcspn(name, separators);
5043 if (name[namelen]) {
5045 value = chomp_string(name + namelen + 1);
5046 valuelen = strlen(value);
5053 state = read_property(name, namelen, value, valuelen);
5056 if (state != ERR && ferror(pipe))
5069 static void __NORETURN
5072 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5078 static void __NORETURN
5079 die(const char *err, ...)
5085 va_start(args, err);
5086 fputs("tig: ", stderr);
5087 vfprintf(stderr, err, args);
5088 fputs("\n", stderr);
5095 warn(const char *msg, ...)
5099 va_start(args, msg);
5100 fputs("tig warning: ", stderr);
5101 vfprintf(stderr, msg, args);
5102 fputs("\n", stderr);
5107 main(int argc, char *argv[])
5110 enum request request;
5113 signal(SIGINT, quit);
5115 if (setlocale(LC_ALL, "")) {
5116 char *codeset = nl_langinfo(CODESET);
5118 string_ncopy(opt_codeset, codeset, strlen(codeset));
5121 if (load_repo_info() == ERR)
5122 die("Failed to load repo info.");
5124 if (load_options() == ERR)
5125 die("Failed to load user config.");
5127 /* Load the repo config file so options can be overwritten from
5128 * the command line. */
5129 if (load_repo_config() == ERR)
5130 die("Failed to load repo config.");
5132 if (!parse_options(argc, argv))
5135 /* Require a git repository unless when running in pager mode. */
5136 if (!opt_git_dir[0])
5137 die("Not a git repository");
5139 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5142 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5143 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5144 if (opt_iconv == ICONV_NONE)
5145 die("Failed to initialize character set conversion");
5148 if (load_refs() == ERR)
5149 die("Failed to load refs.");
5151 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5152 view->cmd_env = getenv(view->cmd_env);
5154 request = opt_request;
5158 while (view_driver(display[current_view], request)) {
5162 foreach_view (view, i)
5165 /* Refresh, accept single keystroke of input */
5166 key = wgetch(status_win);
5168 /* wgetch() with nodelay() enabled returns ERR when there's no
5175 request = get_keybinding(display[current_view]->keymap, key);
5177 /* Some low-level request handling. This keeps access to
5178 * status_win restricted. */
5182 char *cmd = read_prompt(":");
5184 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5185 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5186 opt_request = REQ_VIEW_DIFF;
5188 opt_request = REQ_VIEW_PAGER;
5197 case REQ_SEARCH_BACK:
5199 const char *prompt = request == REQ_SEARCH
5201 char *search = read_prompt(prompt);
5204 string_ncopy(opt_search, search, strlen(search));
5209 case REQ_SCREEN_RESIZE:
5213 getmaxyx(stdscr, height, width);
5215 /* Resize the status view and let the view driver take
5216 * care of resizing the displayed views. */
5217 wresize(status_win, 1, width);
5218 mvwin(status_win, height - 1, 0);
5219 wrefresh(status_win);