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 --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[])
499 char *subcommand = NULL;
502 for (i = 1; i < argc; i++) {
505 if (!strcmp(opt, "log") ||
506 !strcmp(opt, "diff")) {
508 opt_request = opt[0] == 'l'
509 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
510 warn("`tig %s' has been deprecated", opt);
514 if (!strcmp(opt, "show")) {
516 opt_request = REQ_VIEW_DIFF;
520 if (!strcmp(opt, "status")) {
522 opt_request = REQ_VIEW_STATUS;
526 if (opt[0] && opt[0] != '-')
529 if (!strcmp(opt, "--")) {
534 if (check_option(opt, 'v', "version", OPT_NONE)) {
535 printf("tig version %s\n", TIG_VERSION);
539 if (check_option(opt, 'h', "help", OPT_NONE)) {
544 if (!strcmp(opt, "-S")) {
545 warn("`%s' has been deprecated; use `tig status' instead", opt);
546 opt_request = REQ_VIEW_STATUS;
550 if (!strcmp(opt, "-l")) {
551 opt_request = REQ_VIEW_LOG;
552 } else if (!strcmp(opt, "-d")) {
553 opt_request = REQ_VIEW_DIFF;
554 } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
555 opt_line_number = TRUE;
556 } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
557 opt_tab_size = MIN(opt_tab_size, TABSIZE);
559 if (altargc >= ARRAY_SIZE(altargv))
560 die("maximum number of arguments exceeded");
561 altargv[altargc++] = opt;
565 warn("`%s' has been deprecated", opt);
568 /* Check that no 'alt' arguments occured before a subcommand. */
569 if (subcommand && i < argc && altargc > 0)
570 die("unknown arguments before `%s'", argv[i]);
572 if (!isatty(STDIN_FILENO)) {
573 opt_request = REQ_VIEW_PAGER;
576 } else if (opt_request == REQ_VIEW_STATUS) {
578 warn("ignoring arguments after `%s'", argv[i]);
580 } else if (i < argc || altargc > 0) {
584 if (opt_request == REQ_VIEW_MAIN)
585 /* XXX: This is vulnerable to the user overriding
586 * options required for the main view parser. */
587 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary");
589 string_copy(opt_cmd, "git");
590 buf_size = strlen(opt_cmd);
592 while (buf_size < sizeof(opt_cmd) && alti < altargc) {
593 opt_cmd[buf_size++] = ' ';
594 buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
597 while (buf_size < sizeof(opt_cmd) && i < argc) {
598 opt_cmd[buf_size++] = ' ';
599 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
602 if (buf_size >= sizeof(opt_cmd))
603 die("command too long");
605 opt_cmd[buf_size] = 0;
608 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
616 * Line-oriented content detection.
620 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
621 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
622 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
623 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
624 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
625 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
626 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
627 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
628 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
629 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
630 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
631 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
632 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
633 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
634 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
635 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
636 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
637 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
638 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
639 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
640 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
641 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
642 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
643 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
644 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
645 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
646 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
647 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
648 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
649 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
650 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
651 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
652 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
653 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
654 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
655 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
656 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
657 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
658 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
659 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
660 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
661 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
662 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
663 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
664 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
665 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
666 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
667 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
668 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
671 #define LINE(type, line, fg, bg, attr) \
678 const char *name; /* Option name. */
679 int namelen; /* Size of option name. */
680 const char *line; /* The start of line to match. */
681 int linelen; /* Size of string to match. */
682 int fg, bg, attr; /* Color and text attributes for the lines. */
685 static struct line_info line_info[] = {
686 #define LINE(type, line, fg, bg, attr) \
687 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
692 static enum line_type
693 get_line_type(char *line)
695 int linelen = strlen(line);
698 for (type = 0; type < ARRAY_SIZE(line_info); type++)
699 /* Case insensitive search matches Signed-off-by lines better. */
700 if (linelen >= line_info[type].linelen &&
701 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
708 get_line_attr(enum line_type type)
710 assert(type < ARRAY_SIZE(line_info));
711 return COLOR_PAIR(type) | line_info[type].attr;
714 static struct line_info *
715 get_line_info(char *name, int namelen)
719 for (type = 0; type < ARRAY_SIZE(line_info); type++)
720 if (namelen == line_info[type].namelen &&
721 !string_enum_compare(line_info[type].name, name, namelen))
722 return &line_info[type];
730 int default_bg = line_info[LINE_DEFAULT].bg;
731 int default_fg = line_info[LINE_DEFAULT].fg;
736 if (assume_default_colors(default_fg, default_bg) == ERR) {
737 default_bg = COLOR_BLACK;
738 default_fg = COLOR_WHITE;
741 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
742 struct line_info *info = &line_info[type];
743 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
744 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
746 init_pair(type, fg, bg);
754 unsigned int selected:1;
756 void *data; /* User data */
766 enum request request;
767 struct keybinding *next;
770 static struct keybinding default_keybindings[] = {
772 { 'm', REQ_VIEW_MAIN },
773 { 'd', REQ_VIEW_DIFF },
774 { 'l', REQ_VIEW_LOG },
775 { 't', REQ_VIEW_TREE },
776 { 'f', REQ_VIEW_BLOB },
777 { 'p', REQ_VIEW_PAGER },
778 { 'h', REQ_VIEW_HELP },
779 { 'S', REQ_VIEW_STATUS },
780 { 'c', REQ_VIEW_STAGE },
782 /* View manipulation */
783 { 'q', REQ_VIEW_CLOSE },
784 { KEY_TAB, REQ_VIEW_NEXT },
785 { KEY_RETURN, REQ_ENTER },
786 { KEY_UP, REQ_PREVIOUS },
787 { KEY_DOWN, REQ_NEXT },
788 { 'R', REQ_REFRESH },
790 /* Cursor navigation */
791 { 'k', REQ_MOVE_UP },
792 { 'j', REQ_MOVE_DOWN },
793 { KEY_HOME, REQ_MOVE_FIRST_LINE },
794 { KEY_END, REQ_MOVE_LAST_LINE },
795 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
796 { ' ', REQ_MOVE_PAGE_DOWN },
797 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
798 { 'b', REQ_MOVE_PAGE_UP },
799 { '-', REQ_MOVE_PAGE_UP },
802 { KEY_IC, REQ_SCROLL_LINE_UP },
803 { KEY_DC, REQ_SCROLL_LINE_DOWN },
804 { 'w', REQ_SCROLL_PAGE_UP },
805 { 's', REQ_SCROLL_PAGE_DOWN },
809 { '?', REQ_SEARCH_BACK },
810 { 'n', REQ_FIND_NEXT },
811 { 'N', REQ_FIND_PREV },
815 { 'z', REQ_STOP_LOADING },
816 { 'v', REQ_SHOW_VERSION },
817 { 'r', REQ_SCREEN_REDRAW },
818 { '.', REQ_TOGGLE_LINENO },
819 { 'D', REQ_TOGGLE_DATE },
820 { 'A', REQ_TOGGLE_AUTHOR },
821 { 'g', REQ_TOGGLE_REV_GRAPH },
822 { 'F', REQ_TOGGLE_REFS },
824 { 'u', REQ_STATUS_UPDATE },
825 { 'M', REQ_STATUS_MERGE },
826 { ',', REQ_TREE_PARENT },
829 /* Using the ncurses SIGWINCH handler. */
830 { KEY_RESIZE, REQ_SCREEN_RESIZE },
833 #define KEYMAP_INFO \
846 #define KEYMAP_(name) KEYMAP_##name
851 static struct int_map keymap_table[] = {
852 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
857 #define set_keymap(map, name) \
858 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
860 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
863 add_keybinding(enum keymap keymap, enum request request, int key)
865 struct keybinding *keybinding;
867 keybinding = calloc(1, sizeof(*keybinding));
869 die("Failed to allocate keybinding");
871 keybinding->alias = key;
872 keybinding->request = request;
873 keybinding->next = keybindings[keymap];
874 keybindings[keymap] = keybinding;
877 /* Looks for a key binding first in the given map, then in the generic map, and
878 * lastly in the default keybindings. */
880 get_keybinding(enum keymap keymap, int key)
882 struct keybinding *kbd;
885 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
886 if (kbd->alias == key)
889 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
890 if (kbd->alias == key)
893 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
894 if (default_keybindings[i].alias == key)
895 return default_keybindings[i].request;
897 return (enum request) key;
906 static struct key key_table[] = {
907 { "Enter", KEY_RETURN },
909 { "Backspace", KEY_BACKSPACE },
911 { "Escape", KEY_ESC },
912 { "Left", KEY_LEFT },
913 { "Right", KEY_RIGHT },
915 { "Down", KEY_DOWN },
916 { "Insert", KEY_IC },
917 { "Delete", KEY_DC },
919 { "Home", KEY_HOME },
921 { "PageUp", KEY_PPAGE },
922 { "PageDown", KEY_NPAGE },
932 { "F10", KEY_F(10) },
933 { "F11", KEY_F(11) },
934 { "F12", KEY_F(12) },
938 get_key_value(const char *name)
942 for (i = 0; i < ARRAY_SIZE(key_table); i++)
943 if (!strcasecmp(key_table[i].name, name))
944 return key_table[i].value;
946 if (strlen(name) == 1 && isprint(*name))
953 get_key_name(int key_value)
955 static char key_char[] = "'X'";
959 for (key = 0; key < ARRAY_SIZE(key_table); key++)
960 if (key_table[key].value == key_value)
961 seq = key_table[key].name;
965 isprint(key_value)) {
966 key_char[1] = (char) key_value;
970 return seq ? seq : "'?'";
974 get_key(enum request request)
976 static char buf[BUFSIZ];
983 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
984 struct keybinding *keybinding = &default_keybindings[i];
986 if (keybinding->request != request)
989 if (!string_format_from(buf, &pos, "%s%s", sep,
990 get_key_name(keybinding->alias)))
991 return "Too many keybindings!";
1001 char cmd[SIZEOF_STR];
1004 static struct run_request *run_request;
1005 static size_t run_requests;
1008 add_run_request(enum keymap keymap, int key, int argc, char **argv)
1010 struct run_request *tmp;
1011 struct run_request req = { keymap, key };
1014 for (bufpos = 0; argc > 0; argc--, argv++)
1015 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
1018 req.cmd[bufpos - 1] = 0;
1020 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1025 run_request[run_requests++] = req;
1027 return REQ_NONE + run_requests;
1030 static struct run_request *
1031 get_run_request(enum request request)
1033 if (request <= REQ_NONE)
1035 return &run_request[request - REQ_NONE - 1];
1039 add_builtin_run_requests(void)
1046 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1047 { KEYMAP_GENERIC, 'G', { "git gc" } },
1051 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1054 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1055 if (req != REQ_NONE)
1056 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1061 * User config file handling.
1064 static struct int_map color_map[] = {
1065 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1077 #define set_color(color, name) \
1078 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1080 static struct int_map attr_map[] = {
1081 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1088 ATTR_MAP(UNDERLINE),
1091 #define set_attribute(attr, name) \
1092 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1094 static int config_lineno;
1095 static bool config_errors;
1096 static char *config_msg;
1098 /* Wants: object fgcolor bgcolor [attr] */
1100 option_color_command(int argc, char *argv[])
1102 struct line_info *info;
1104 if (argc != 3 && argc != 4) {
1105 config_msg = "Wrong number of arguments given to color command";
1109 info = get_line_info(argv[0], strlen(argv[0]));
1111 config_msg = "Unknown color name";
1115 if (set_color(&info->fg, argv[1]) == ERR ||
1116 set_color(&info->bg, argv[2]) == ERR) {
1117 config_msg = "Unknown color";
1121 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1122 config_msg = "Unknown attribute";
1129 static bool parse_bool(const char *s)
1131 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1132 !strcmp(s, "yes")) ? TRUE : FALSE;
1135 /* Wants: name = value */
1137 option_set_command(int argc, char *argv[])
1140 config_msg = "Wrong number of arguments given to set command";
1144 if (strcmp(argv[1], "=")) {
1145 config_msg = "No value assigned";
1149 if (!strcmp(argv[0], "show-author")) {
1150 opt_author = parse_bool(argv[2]);
1154 if (!strcmp(argv[0], "show-date")) {
1155 opt_date = parse_bool(argv[2]);
1159 if (!strcmp(argv[0], "show-rev-graph")) {
1160 opt_rev_graph = parse_bool(argv[2]);
1164 if (!strcmp(argv[0], "show-refs")) {
1165 opt_show_refs = parse_bool(argv[2]);
1169 if (!strcmp(argv[0], "show-line-numbers")) {
1170 opt_line_number = parse_bool(argv[2]);
1174 if (!strcmp(argv[0], "line-number-interval")) {
1175 opt_num_interval = atoi(argv[2]);
1179 if (!strcmp(argv[0], "tab-size")) {
1180 opt_tab_size = atoi(argv[2]);
1184 if (!strcmp(argv[0], "commit-encoding")) {
1185 char *arg = argv[2];
1186 int delimiter = *arg;
1189 switch (delimiter) {
1192 for (arg++, i = 0; arg[i]; i++)
1193 if (arg[i] == delimiter) {
1198 string_ncopy(opt_encoding, arg, strlen(arg));
1203 config_msg = "Unknown variable name";
1207 /* Wants: mode request key */
1209 option_bind_command(int argc, char *argv[])
1211 enum request request;
1216 config_msg = "Wrong number of arguments given to bind command";
1220 if (set_keymap(&keymap, argv[0]) == ERR) {
1221 config_msg = "Unknown key map";
1225 key = get_key_value(argv[1]);
1227 config_msg = "Unknown key";
1231 request = get_request(argv[2]);
1232 if (request == REQ_NONE) {
1233 const char *obsolete[] = { "cherry-pick" };
1234 size_t namelen = strlen(argv[2]);
1237 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1238 if (namelen == strlen(obsolete[i]) &&
1239 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1240 config_msg = "Obsolete request name";
1245 if (request == REQ_NONE && *argv[2]++ == '!')
1246 request = add_run_request(keymap, key, argc - 2, argv + 2);
1247 if (request == REQ_NONE) {
1248 config_msg = "Unknown request name";
1252 add_keybinding(keymap, request, key);
1258 set_option(char *opt, char *value)
1265 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1266 argv[argc++] = value;
1269 /* Nothing more to tokenize or last available token. */
1270 if (!*value || argc >= ARRAY_SIZE(argv))
1274 while (isspace(*value))
1278 if (!strcmp(opt, "color"))
1279 return option_color_command(argc, argv);
1281 if (!strcmp(opt, "set"))
1282 return option_set_command(argc, argv);
1284 if (!strcmp(opt, "bind"))
1285 return option_bind_command(argc, argv);
1287 config_msg = "Unknown option command";
1292 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1297 config_msg = "Internal error";
1299 /* Check for comment markers, since read_properties() will
1300 * only ensure opt and value are split at first " \t". */
1301 optlen = strcspn(opt, "#");
1305 if (opt[optlen] != 0) {
1306 config_msg = "No option value";
1310 /* Look for comment endings in the value. */
1311 size_t len = strcspn(value, "#");
1313 if (len < valuelen) {
1315 value[valuelen] = 0;
1318 status = set_option(opt, value);
1321 if (status == ERR) {
1322 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1323 config_lineno, (int) optlen, opt, config_msg);
1324 config_errors = TRUE;
1327 /* Always keep going if errors are encountered. */
1332 load_option_file(const char *path)
1336 /* It's ok that the file doesn't exist. */
1337 file = fopen(path, "r");
1342 config_errors = FALSE;
1344 if (read_properties(file, " \t", read_option) == ERR ||
1345 config_errors == TRUE)
1346 fprintf(stderr, "Errors while loading %s.\n", path);
1352 char *home = getenv("HOME");
1353 char *tigrc_user = getenv("TIGRC_USER");
1354 char *tigrc_system = getenv("TIGRC_SYSTEM");
1355 char buf[SIZEOF_STR];
1357 add_builtin_run_requests();
1359 if (!tigrc_system) {
1360 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1364 load_option_file(tigrc_system);
1367 if (!home || !string_format(buf, "%s/.tigrc", home))
1371 load_option_file(tigrc_user);
1384 /* The display array of active views and the index of the current view. */
1385 static struct view *display[2];
1386 static unsigned int current_view;
1388 /* Reading from the prompt? */
1389 static bool input_mode = FALSE;
1391 #define foreach_displayed_view(view, i) \
1392 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1394 #define displayed_views() (display[1] != NULL ? 2 : 1)
1396 /* Current head and commit ID */
1397 static char ref_blob[SIZEOF_REF] = "";
1398 static char ref_commit[SIZEOF_REF] = "HEAD";
1399 static char ref_head[SIZEOF_REF] = "HEAD";
1402 const char *name; /* View name */
1403 const char *cmd_fmt; /* Default command line format */
1404 const char *cmd_env; /* Command line set via environment */
1405 const char *id; /* Points to either of ref_{head,commit,blob} */
1407 struct view_ops *ops; /* View operations */
1409 enum keymap keymap; /* What keymap does this view have */
1411 char cmd[SIZEOF_STR]; /* Command buffer */
1412 char ref[SIZEOF_REF]; /* Hovered commit reference */
1413 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1415 int height, width; /* The width and height of the main window */
1416 WINDOW *win; /* The main window */
1417 WINDOW *title; /* The title window living below the main window */
1420 unsigned long offset; /* Offset of the window top */
1421 unsigned long lineno; /* Current line number */
1424 char grep[SIZEOF_STR]; /* Search string */
1425 regex_t *regex; /* Pre-compiled regex */
1427 /* If non-NULL, points to the view that opened this view. If this view
1428 * is closed tig will switch back to the parent view. */
1429 struct view *parent;
1432 size_t lines; /* Total number of lines */
1433 struct line *line; /* Line index */
1434 size_t line_alloc; /* Total number of allocated lines */
1435 size_t line_size; /* Total number of used lines */
1436 unsigned int digits; /* Number of digits in the lines member. */
1444 /* What type of content being displayed. Used in the title bar. */
1446 /* Open and reads in all view content. */
1447 bool (*open)(struct view *view);
1448 /* Read one line; updates view->line. */
1449 bool (*read)(struct view *view, char *data);
1450 /* Draw one line; @lineno must be < view->height. */
1451 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1452 /* Depending on view handle a special requests. */
1453 enum request (*request)(struct view *view, enum request request, struct line *line);
1454 /* Search for regex in a line. */
1455 bool (*grep)(struct view *view, struct line *line);
1457 void (*select)(struct view *view, struct line *line);
1460 static struct view_ops pager_ops;
1461 static struct view_ops main_ops;
1462 static struct view_ops tree_ops;
1463 static struct view_ops blob_ops;
1464 static struct view_ops help_ops;
1465 static struct view_ops status_ops;
1466 static struct view_ops stage_ops;
1468 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1469 { name, cmd, #env, ref, ops, map}
1471 #define VIEW_(id, name, ops, ref) \
1472 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1475 static struct view views[] = {
1476 VIEW_(MAIN, "main", &main_ops, ref_head),
1477 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1478 VIEW_(LOG, "log", &pager_ops, ref_head),
1479 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1480 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1481 VIEW_(HELP, "help", &help_ops, ""),
1482 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1483 VIEW_(STATUS, "status", &status_ops, ""),
1484 VIEW_(STAGE, "stage", &stage_ops, ""),
1487 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1489 #define foreach_view(view, i) \
1490 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1492 #define view_is_displayed(view) \
1493 (view == display[0] || view == display[1])
1496 draw_text(struct view *view, const char *string, int max_len, int col,
1497 bool use_tilde, int tilde_attr)
1500 int trimmed = FALSE;
1506 len = utf8_length(string, max_len, &trimmed, use_tilde);
1508 len = strlen(string);
1509 if (len > max_len) {
1518 waddnstr(view->win, string, len);
1519 if (trimmed && use_tilde) {
1520 if (tilde_attr != -1)
1521 wattrset(view->win, tilde_attr);
1522 waddch(view->win, '~');
1530 draw_view_line(struct view *view, unsigned int lineno)
1533 bool selected = (view->offset + lineno == view->lineno);
1536 assert(view_is_displayed(view));
1538 if (view->offset + lineno >= view->lines)
1541 line = &view->line[view->offset + lineno];
1544 line->selected = TRUE;
1545 view->ops->select(view, line);
1546 } else if (line->selected) {
1547 line->selected = FALSE;
1548 wmove(view->win, lineno, 0);
1549 wclrtoeol(view->win);
1552 scrollok(view->win, FALSE);
1553 draw_ok = view->ops->draw(view, line, lineno, selected);
1554 scrollok(view->win, TRUE);
1560 redraw_view_from(struct view *view, int lineno)
1562 assert(0 <= lineno && lineno < view->height);
1564 for (; lineno < view->height; lineno++) {
1565 if (!draw_view_line(view, lineno))
1569 redrawwin(view->win);
1571 wnoutrefresh(view->win);
1573 wrefresh(view->win);
1577 redraw_view(struct view *view)
1580 redraw_view_from(view, 0);
1585 update_view_title(struct view *view)
1587 char buf[SIZEOF_STR];
1588 char state[SIZEOF_STR];
1589 size_t bufpos = 0, statelen = 0;
1591 assert(view_is_displayed(view));
1593 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1594 unsigned int view_lines = view->offset + view->height;
1595 unsigned int lines = view->lines
1596 ? MIN(view_lines, view->lines) * 100 / view->lines
1599 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1606 time_t secs = time(NULL) - view->start_time;
1608 /* Three git seconds are a long time ... */
1610 string_format_from(state, &statelen, " %lds", secs);
1614 string_format_from(buf, &bufpos, "[%s]", view->name);
1615 if (*view->ref && bufpos < view->width) {
1616 size_t refsize = strlen(view->ref);
1617 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1619 if (minsize < view->width)
1620 refsize = view->width - minsize + 7;
1621 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1624 if (statelen && bufpos < view->width) {
1625 string_format_from(buf, &bufpos, " %s", state);
1628 if (view == display[current_view])
1629 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1631 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1633 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1634 wclrtoeol(view->title);
1635 wmove(view->title, 0, view->width - 1);
1638 wnoutrefresh(view->title);
1640 wrefresh(view->title);
1644 resize_display(void)
1647 struct view *base = display[0];
1648 struct view *view = display[1] ? display[1] : display[0];
1650 /* Setup window dimensions */
1652 getmaxyx(stdscr, base->height, base->width);
1654 /* Make room for the status window. */
1658 /* Horizontal split. */
1659 view->width = base->width;
1660 view->height = SCALE_SPLIT_VIEW(base->height);
1661 base->height -= view->height;
1663 /* Make room for the title bar. */
1667 /* Make room for the title bar. */
1672 foreach_displayed_view (view, i) {
1674 view->win = newwin(view->height, 0, offset, 0);
1676 die("Failed to create %s view", view->name);
1678 scrollok(view->win, TRUE);
1680 view->title = newwin(1, 0, offset + view->height, 0);
1682 die("Failed to create title window");
1685 wresize(view->win, view->height, view->width);
1686 mvwin(view->win, offset, 0);
1687 mvwin(view->title, offset + view->height, 0);
1690 offset += view->height + 1;
1695 redraw_display(void)
1700 foreach_displayed_view (view, i) {
1702 update_view_title(view);
1707 update_display_cursor(struct view *view)
1709 /* Move the cursor to the right-most column of the cursor line.
1711 * XXX: This could turn out to be a bit expensive, but it ensures that
1712 * the cursor does not jump around. */
1714 wmove(view->win, view->lineno - view->offset, view->width - 1);
1715 wrefresh(view->win);
1723 /* Scrolling backend */
1725 do_scroll_view(struct view *view, int lines)
1727 bool redraw_current_line = FALSE;
1729 /* The rendering expects the new offset. */
1730 view->offset += lines;
1732 assert(0 <= view->offset && view->offset < view->lines);
1735 /* Move current line into the view. */
1736 if (view->lineno < view->offset) {
1737 view->lineno = view->offset;
1738 redraw_current_line = TRUE;
1739 } else if (view->lineno >= view->offset + view->height) {
1740 view->lineno = view->offset + view->height - 1;
1741 redraw_current_line = TRUE;
1744 assert(view->offset <= view->lineno && view->lineno < view->lines);
1746 /* Redraw the whole screen if scrolling is pointless. */
1747 if (view->height < ABS(lines)) {
1751 int line = lines > 0 ? view->height - lines : 0;
1752 int end = line + ABS(lines);
1754 wscrl(view->win, lines);
1756 for (; line < end; line++) {
1757 if (!draw_view_line(view, line))
1761 if (redraw_current_line)
1762 draw_view_line(view, view->lineno - view->offset);
1765 redrawwin(view->win);
1766 wrefresh(view->win);
1770 /* Scroll frontend */
1772 scroll_view(struct view *view, enum request request)
1776 assert(view_is_displayed(view));
1779 case REQ_SCROLL_PAGE_DOWN:
1780 lines = view->height;
1781 case REQ_SCROLL_LINE_DOWN:
1782 if (view->offset + lines > view->lines)
1783 lines = view->lines - view->offset;
1785 if (lines == 0 || view->offset + view->height >= view->lines) {
1786 report("Cannot scroll beyond the last line");
1791 case REQ_SCROLL_PAGE_UP:
1792 lines = view->height;
1793 case REQ_SCROLL_LINE_UP:
1794 if (lines > view->offset)
1795 lines = view->offset;
1798 report("Cannot scroll beyond the first line");
1806 die("request %d not handled in switch", request);
1809 do_scroll_view(view, lines);
1814 move_view(struct view *view, enum request request)
1816 int scroll_steps = 0;
1820 case REQ_MOVE_FIRST_LINE:
1821 steps = -view->lineno;
1824 case REQ_MOVE_LAST_LINE:
1825 steps = view->lines - view->lineno - 1;
1828 case REQ_MOVE_PAGE_UP:
1829 steps = view->height > view->lineno
1830 ? -view->lineno : -view->height;
1833 case REQ_MOVE_PAGE_DOWN:
1834 steps = view->lineno + view->height >= view->lines
1835 ? view->lines - view->lineno - 1 : view->height;
1847 die("request %d not handled in switch", request);
1850 if (steps <= 0 && view->lineno == 0) {
1851 report("Cannot move beyond the first line");
1854 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1855 report("Cannot move beyond the last line");
1859 /* Move the current line */
1860 view->lineno += steps;
1861 assert(0 <= view->lineno && view->lineno < view->lines);
1863 /* Check whether the view needs to be scrolled */
1864 if (view->lineno < view->offset ||
1865 view->lineno >= view->offset + view->height) {
1866 scroll_steps = steps;
1867 if (steps < 0 && -steps > view->offset) {
1868 scroll_steps = -view->offset;
1870 } else if (steps > 0) {
1871 if (view->lineno == view->lines - 1 &&
1872 view->lines > view->height) {
1873 scroll_steps = view->lines - view->offset - 1;
1874 if (scroll_steps >= view->height)
1875 scroll_steps -= view->height - 1;
1880 if (!view_is_displayed(view)) {
1881 view->offset += scroll_steps;
1882 assert(0 <= view->offset && view->offset < view->lines);
1883 view->ops->select(view, &view->line[view->lineno]);
1887 /* Repaint the old "current" line if we be scrolling */
1888 if (ABS(steps) < view->height)
1889 draw_view_line(view, view->lineno - steps - view->offset);
1892 do_scroll_view(view, scroll_steps);
1896 /* Draw the current line */
1897 draw_view_line(view, view->lineno - view->offset);
1899 redrawwin(view->win);
1900 wrefresh(view->win);
1909 static void search_view(struct view *view, enum request request);
1912 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1914 assert(view_is_displayed(view));
1916 if (!view->ops->grep(view, line))
1919 if (lineno - view->offset >= view->height) {
1920 view->offset = lineno;
1921 view->lineno = lineno;
1925 unsigned long old_lineno = view->lineno - view->offset;
1927 view->lineno = lineno;
1928 draw_view_line(view, old_lineno);
1930 draw_view_line(view, view->lineno - view->offset);
1931 redrawwin(view->win);
1932 wrefresh(view->win);
1935 report("Line %ld matches '%s'", lineno + 1, view->grep);
1940 find_next(struct view *view, enum request request)
1942 unsigned long lineno = view->lineno;
1947 report("No previous search");
1949 search_view(view, request);
1959 case REQ_SEARCH_BACK:
1968 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1969 lineno += direction;
1971 /* Note, lineno is unsigned long so will wrap around in which case it
1972 * will become bigger than view->lines. */
1973 for (; lineno < view->lines; lineno += direction) {
1974 struct line *line = &view->line[lineno];
1976 if (find_next_line(view, lineno, line))
1980 report("No match found for '%s'", view->grep);
1984 search_view(struct view *view, enum request request)
1989 regfree(view->regex);
1992 view->regex = calloc(1, sizeof(*view->regex));
1997 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1998 if (regex_err != 0) {
1999 char buf[SIZEOF_STR] = "unknown error";
2001 regerror(regex_err, view->regex, buf, sizeof(buf));
2002 report("Search failed: %s", buf);
2006 string_copy(view->grep, opt_search);
2008 find_next(view, request);
2012 * Incremental updating
2016 end_update(struct view *view)
2020 set_nonblocking_input(FALSE);
2021 if (view->pipe == stdin)
2029 begin_update(struct view *view)
2035 string_copy(view->cmd, opt_cmd);
2037 /* When running random commands, initially show the
2038 * command in the title. However, it maybe later be
2039 * overwritten if a commit line is selected. */
2040 if (view == VIEW(REQ_VIEW_PAGER))
2041 string_copy(view->ref, view->cmd);
2045 } else if (view == VIEW(REQ_VIEW_TREE)) {
2046 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2047 char path[SIZEOF_STR];
2049 if (strcmp(view->vid, view->id))
2050 opt_path[0] = path[0] = 0;
2051 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2054 if (!string_format(view->cmd, format, view->id, path))
2058 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2059 const char *id = view->id;
2061 if (!string_format(view->cmd, format, id, id, id, id, id))
2064 /* Put the current ref_* value to the view title ref
2065 * member. This is needed by the blob view. Most other
2066 * views sets it automatically after loading because the
2067 * first line is a commit line. */
2068 string_copy_rev(view->ref, view->id);
2071 /* Special case for the pager view. */
2073 view->pipe = opt_pipe;
2076 view->pipe = popen(view->cmd, "r");
2082 set_nonblocking_input(TRUE);
2087 string_copy_rev(view->vid, view->id);
2092 for (i = 0; i < view->lines; i++)
2093 if (view->line[i].data)
2094 free(view->line[i].data);
2100 view->start_time = time(NULL);
2105 #define ITEM_CHUNK_SIZE 256
2107 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2109 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2110 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2112 if (mem == NULL || num_chunks != num_chunks_new) {
2113 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2114 mem = realloc(mem, *size * item_size);
2120 static struct line *
2121 realloc_lines(struct view *view, size_t line_size)
2123 size_t alloc = view->line_alloc;
2124 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2125 sizeof(*view->line));
2131 view->line_alloc = alloc;
2132 view->line_size = line_size;
2137 update_view(struct view *view)
2139 char in_buffer[BUFSIZ];
2140 char out_buffer[BUFSIZ * 2];
2142 /* The number of lines to read. If too low it will cause too much
2143 * redrawing (and possible flickering), if too high responsiveness
2145 unsigned long lines = view->height;
2146 int redraw_from = -1;
2151 /* Only redraw if lines are visible. */
2152 if (view->offset + view->height >= view->lines)
2153 redraw_from = view->lines - view->offset;
2155 /* FIXME: This is probably not perfect for backgrounded views. */
2156 if (!realloc_lines(view, view->lines + lines))
2159 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2160 size_t linelen = strlen(line);
2163 line[linelen - 1] = 0;
2165 if (opt_iconv != ICONV_NONE) {
2166 ICONV_CONST char *inbuf = line;
2167 size_t inlen = linelen;
2169 char *outbuf = out_buffer;
2170 size_t outlen = sizeof(out_buffer);
2174 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2175 if (ret != (size_t) -1) {
2177 linelen = strlen(out_buffer);
2181 if (!view->ops->read(view, line))
2191 lines = view->lines;
2192 for (digits = 0; lines; digits++)
2195 /* Keep the displayed view in sync with line number scaling. */
2196 if (digits != view->digits) {
2197 view->digits = digits;
2202 if (!view_is_displayed(view))
2205 if (view == VIEW(REQ_VIEW_TREE)) {
2206 /* Clear the view and redraw everything since the tree sorting
2207 * might have rearranged things. */
2210 } else if (redraw_from >= 0) {
2211 /* If this is an incremental update, redraw the previous line
2212 * since for commits some members could have changed when
2213 * loading the main view. */
2214 if (redraw_from > 0)
2217 /* Since revision graph visualization requires knowledge
2218 * about the parent commit, it causes a further one-off
2219 * needed to be redrawn for incremental updates. */
2220 if (redraw_from > 0 && opt_rev_graph)
2223 /* Incrementally draw avoids flickering. */
2224 redraw_view_from(view, redraw_from);
2227 /* Update the title _after_ the redraw so that if the redraw picks up a
2228 * commit reference in view->ref it'll be available here. */
2229 update_view_title(view);
2232 if (ferror(view->pipe)) {
2233 report("Failed to read: %s", strerror(errno));
2236 } else if (feof(view->pipe)) {
2244 report("Allocation failure");
2247 view->ops->read(view, NULL);
2252 static struct line *
2253 add_line_data(struct view *view, void *data, enum line_type type)
2255 struct line *line = &view->line[view->lines++];
2257 memset(line, 0, sizeof(*line));
2264 static struct line *
2265 add_line_text(struct view *view, char *data, enum line_type type)
2268 data = strdup(data);
2270 return data ? add_line_data(view, data, type) : NULL;
2279 OPEN_DEFAULT = 0, /* Use default view switching. */
2280 OPEN_SPLIT = 1, /* Split current view. */
2281 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2282 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2286 open_view(struct view *prev, enum request request, enum open_flags flags)
2288 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2289 bool split = !!(flags & OPEN_SPLIT);
2290 bool reload = !!(flags & OPEN_RELOAD);
2291 struct view *view = VIEW(request);
2292 int nviews = displayed_views();
2293 struct view *base_view = display[0];
2295 if (view == prev && nviews == 1 && !reload) {
2296 report("Already in %s view", view->name);
2300 if (view->ops->open) {
2301 if (!view->ops->open(view)) {
2302 report("Failed to load %s view", view->name);
2306 } else if ((reload || strcmp(view->vid, view->id)) &&
2307 !begin_update(view)) {
2308 report("Failed to load %s view", view->name);
2317 /* Maximize the current view. */
2318 memset(display, 0, sizeof(display));
2320 display[current_view] = view;
2323 /* Resize the view when switching between split- and full-screen,
2324 * or when switching between two different full-screen views. */
2325 if (nviews != displayed_views() ||
2326 (nviews == 1 && base_view != display[0]))
2329 if (split && prev->lineno - prev->offset >= prev->height) {
2330 /* Take the title line into account. */
2331 int lines = prev->lineno - prev->offset - prev->height + 1;
2333 /* Scroll the view that was split if the current line is
2334 * outside the new limited view. */
2335 do_scroll_view(prev, lines);
2338 if (prev && view != prev) {
2339 if (split && !backgrounded) {
2340 /* "Blur" the previous view. */
2341 update_view_title(prev);
2344 view->parent = prev;
2347 if (view->pipe && view->lines == 0) {
2348 /* Clear the old view and let the incremental updating refill
2357 /* If the view is backgrounded the above calls to report()
2358 * won't redraw the view title. */
2360 update_view_title(view);
2364 open_external_viewer(const char *cmd)
2366 def_prog_mode(); /* save current tty modes */
2367 endwin(); /* restore original tty modes */
2369 fprintf(stderr, "Press Enter to continue");
2376 open_mergetool(const char *file)
2378 char cmd[SIZEOF_STR];
2379 char file_sq[SIZEOF_STR];
2381 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2382 string_format(cmd, "git mergetool %s", file_sq)) {
2383 open_external_viewer(cmd);
2388 open_editor(bool from_root, const char *file)
2390 char cmd[SIZEOF_STR];
2391 char file_sq[SIZEOF_STR];
2393 char *prefix = from_root ? opt_cdup : "";
2395 editor = getenv("GIT_EDITOR");
2396 if (!editor && *opt_editor)
2397 editor = opt_editor;
2399 editor = getenv("VISUAL");
2401 editor = getenv("EDITOR");
2405 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2406 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2407 open_external_viewer(cmd);
2412 open_run_request(enum request request)
2414 struct run_request *req = get_run_request(request);
2415 char buf[SIZEOF_STR * 2];
2420 report("Unknown run request");
2428 char *next = strstr(cmd, "%(");
2429 int len = next - cmd;
2436 } else if (!strncmp(next, "%(head)", 7)) {
2439 } else if (!strncmp(next, "%(commit)", 9)) {
2442 } else if (!strncmp(next, "%(blob)", 7)) {
2446 report("Unknown replacement in run request: `%s`", req->cmd);
2450 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2454 next = strchr(next, ')') + 1;
2458 open_external_viewer(buf);
2462 * User request switch noodle
2466 view_driver(struct view *view, enum request request)
2470 if (request == REQ_NONE) {
2475 if (request > REQ_NONE) {
2476 open_run_request(request);
2480 if (view && view->lines) {
2481 request = view->ops->request(view, request, &view->line[view->lineno]);
2482 if (request == REQ_NONE)
2489 case REQ_MOVE_PAGE_UP:
2490 case REQ_MOVE_PAGE_DOWN:
2491 case REQ_MOVE_FIRST_LINE:
2492 case REQ_MOVE_LAST_LINE:
2493 move_view(view, request);
2496 case REQ_SCROLL_LINE_DOWN:
2497 case REQ_SCROLL_LINE_UP:
2498 case REQ_SCROLL_PAGE_DOWN:
2499 case REQ_SCROLL_PAGE_UP:
2500 scroll_view(view, request);
2505 report("No file chosen, press %s to open tree view",
2506 get_key(REQ_VIEW_TREE));
2509 open_view(view, request, OPEN_DEFAULT);
2512 case REQ_VIEW_PAGER:
2513 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2514 report("No pager content, press %s to run command from prompt",
2515 get_key(REQ_PROMPT));
2518 open_view(view, request, OPEN_DEFAULT);
2521 case REQ_VIEW_STAGE:
2522 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2523 report("No stage content, press %s to open the status view and choose file",
2524 get_key(REQ_VIEW_STATUS));
2527 open_view(view, request, OPEN_DEFAULT);
2530 case REQ_VIEW_STATUS:
2531 if (opt_is_inside_work_tree == FALSE) {
2532 report("The status view requires a working tree");
2535 open_view(view, request, OPEN_DEFAULT);
2543 open_view(view, request, OPEN_DEFAULT);
2548 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2550 if ((view == VIEW(REQ_VIEW_DIFF) &&
2551 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2552 (view == VIEW(REQ_VIEW_STAGE) &&
2553 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2554 (view == VIEW(REQ_VIEW_BLOB) &&
2555 view->parent == VIEW(REQ_VIEW_TREE))) {
2558 view = view->parent;
2559 line = view->lineno;
2560 move_view(view, request);
2561 if (view_is_displayed(view))
2562 update_view_title(view);
2563 if (line != view->lineno)
2564 view->ops->request(view, REQ_ENTER,
2565 &view->line[view->lineno]);
2568 move_view(view, request);
2574 int nviews = displayed_views();
2575 int next_view = (current_view + 1) % nviews;
2577 if (next_view == current_view) {
2578 report("Only one view is displayed");
2582 current_view = next_view;
2583 /* Blur out the title of the previous view. */
2584 update_view_title(view);
2589 report("Refreshing is not yet supported for the %s view", view->name);
2592 case REQ_TOGGLE_LINENO:
2593 opt_line_number = !opt_line_number;
2597 case REQ_TOGGLE_DATE:
2598 opt_date = !opt_date;
2602 case REQ_TOGGLE_AUTHOR:
2603 opt_author = !opt_author;
2607 case REQ_TOGGLE_REV_GRAPH:
2608 opt_rev_graph = !opt_rev_graph;
2612 case REQ_TOGGLE_REFS:
2613 opt_show_refs = !opt_show_refs;
2618 /* Always reload^Wrerun commands from the prompt. */
2619 open_view(view, opt_request, OPEN_RELOAD);
2623 case REQ_SEARCH_BACK:
2624 search_view(view, request);
2629 find_next(view, request);
2632 case REQ_STOP_LOADING:
2633 for (i = 0; i < ARRAY_SIZE(views); i++) {
2636 report("Stopped loading the %s view", view->name),
2641 case REQ_SHOW_VERSION:
2642 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2645 case REQ_SCREEN_RESIZE:
2648 case REQ_SCREEN_REDRAW:
2653 report("Nothing to edit");
2658 report("Nothing to enter");
2662 case REQ_VIEW_CLOSE:
2663 /* XXX: Mark closed views by letting view->parent point to the
2664 * view itself. Parents to closed view should never be
2667 view->parent->parent != view->parent) {
2668 memset(display, 0, sizeof(display));
2670 display[current_view] = view->parent;
2671 view->parent = view;
2681 /* An unknown key will show most commonly used commands. */
2682 report("Unknown key, press 'h' for help");
2695 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2697 char *text = line->data;
2698 enum line_type type = line->type;
2701 wmove(view->win, lineno, 0);
2705 wchgat(view->win, -1, 0, type, NULL);
2708 attr = get_line_attr(type);
2709 wattrset(view->win, attr);
2711 if (opt_line_number || opt_tab_size < TABSIZE) {
2712 static char spaces[] = " ";
2713 int col_offset = 0, col = 0;
2715 if (opt_line_number) {
2716 unsigned long real_lineno = view->offset + lineno + 1;
2718 if (real_lineno == 1 ||
2719 (real_lineno % opt_num_interval) == 0) {
2720 wprintw(view->win, "%.*d", view->digits, real_lineno);
2723 waddnstr(view->win, spaces,
2724 MIN(view->digits, STRING_SIZE(spaces)));
2726 waddstr(view->win, ": ");
2727 col_offset = view->digits + 2;
2730 while (text && col_offset + col < view->width) {
2731 int cols_max = view->width - col_offset - col;
2735 if (*text == '\t') {
2737 assert(sizeof(spaces) > TABSIZE);
2739 cols = opt_tab_size - (col % opt_tab_size);
2742 text = strchr(text, '\t');
2743 cols = line ? text - pos : strlen(pos);
2746 waddnstr(view->win, pos, MIN(cols, cols_max));
2751 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
2753 draw_text(view, text, view->width, 0, TRUE, tilde_attr);
2760 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2762 char refbuf[SIZEOF_STR];
2766 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2769 pipe = popen(refbuf, "r");
2773 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2774 ref = chomp_string(ref);
2780 /* This is the only fatal call, since it can "corrupt" the buffer. */
2781 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2788 add_pager_refs(struct view *view, struct line *line)
2790 char buf[SIZEOF_STR];
2791 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2793 size_t bufpos = 0, refpos = 0;
2794 const char *sep = "Refs: ";
2795 bool is_tag = FALSE;
2797 assert(line->type == LINE_COMMIT);
2799 refs = get_refs(commit_id);
2801 if (view == VIEW(REQ_VIEW_DIFF))
2802 goto try_add_describe_ref;
2807 struct ref *ref = refs[refpos];
2808 char *fmt = ref->tag ? "%s[%s]" :
2809 ref->remote ? "%s<%s>" : "%s%s";
2811 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2816 } while (refs[refpos++]->next);
2818 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2819 try_add_describe_ref:
2820 /* Add <tag>-g<commit_id> "fake" reference. */
2821 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2828 if (!realloc_lines(view, view->line_size + 1))
2831 add_line_text(view, buf, LINE_PP_REFS);
2835 pager_read(struct view *view, char *data)
2842 line = add_line_text(view, data, get_line_type(data));
2846 if (line->type == LINE_COMMIT &&
2847 (view == VIEW(REQ_VIEW_DIFF) ||
2848 view == VIEW(REQ_VIEW_LOG)))
2849 add_pager_refs(view, line);
2855 pager_request(struct view *view, enum request request, struct line *line)
2859 if (request != REQ_ENTER)
2862 if (line->type == LINE_COMMIT &&
2863 (view == VIEW(REQ_VIEW_LOG) ||
2864 view == VIEW(REQ_VIEW_PAGER))) {
2865 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2869 /* Always scroll the view even if it was split. That way
2870 * you can use Enter to scroll through the log view and
2871 * split open each commit diff. */
2872 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2874 /* FIXME: A minor workaround. Scrolling the view will call report("")
2875 * but if we are scrolling a non-current view this won't properly
2876 * update the view title. */
2878 update_view_title(view);
2884 pager_grep(struct view *view, struct line *line)
2887 char *text = line->data;
2892 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2899 pager_select(struct view *view, struct line *line)
2901 if (line->type == LINE_COMMIT) {
2902 char *text = (char *)line->data + STRING_SIZE("commit ");
2904 if (view != VIEW(REQ_VIEW_PAGER))
2905 string_copy_rev(view->ref, text);
2906 string_copy_rev(ref_commit, text);
2910 static struct view_ops pager_ops = {
2926 help_open(struct view *view)
2929 int lines = ARRAY_SIZE(req_info) + 2;
2932 if (view->lines > 0)
2935 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2936 if (!req_info[i].request)
2939 lines += run_requests + 1;
2941 view->line = calloc(lines, sizeof(*view->line));
2945 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2947 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2950 if (req_info[i].request == REQ_NONE)
2953 if (!req_info[i].request) {
2954 add_line_text(view, "", LINE_DEFAULT);
2955 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2959 key = get_key(req_info[i].request);
2961 key = "(no key defined)";
2963 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2966 add_line_text(view, buf, LINE_DEFAULT);
2970 add_line_text(view, "", LINE_DEFAULT);
2971 add_line_text(view, "External commands:", LINE_DEFAULT);
2974 for (i = 0; i < run_requests; i++) {
2975 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2981 key = get_key_name(req->key);
2983 key = "(no key defined)";
2985 if (!string_format(buf, " %-10s %-14s `%s`",
2986 keymap_table[req->keymap].name,
2990 add_line_text(view, buf, LINE_DEFAULT);
2996 static struct view_ops help_ops = {
3011 struct tree_stack_entry {
3012 struct tree_stack_entry *prev; /* Entry below this in the stack */
3013 unsigned long lineno; /* Line number to restore */
3014 char *name; /* Position of name in opt_path */
3017 /* The top of the path stack. */
3018 static struct tree_stack_entry *tree_stack = NULL;
3019 unsigned long tree_lineno = 0;
3022 pop_tree_stack_entry(void)
3024 struct tree_stack_entry *entry = tree_stack;
3026 tree_lineno = entry->lineno;
3028 tree_stack = entry->prev;
3033 push_tree_stack_entry(char *name, unsigned long lineno)
3035 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3036 size_t pathlen = strlen(opt_path);
3041 entry->prev = tree_stack;
3042 entry->name = opt_path + pathlen;
3045 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3046 pop_tree_stack_entry();
3050 /* Move the current line to the first tree entry. */
3052 entry->lineno = lineno;
3055 /* Parse output from git-ls-tree(1):
3057 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3058 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3059 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3060 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3063 #define SIZEOF_TREE_ATTR \
3064 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3066 #define TREE_UP_FORMAT "040000 tree %s\t.."
3069 tree_compare_entry(enum line_type type1, char *name1,
3070 enum line_type type2, char *name2)
3072 if (type1 != type2) {
3073 if (type1 == LINE_TREE_DIR)
3078 return strcmp(name1, name2);
3082 tree_read(struct view *view, char *text)
3084 size_t textlen = text ? strlen(text) : 0;
3085 char buf[SIZEOF_STR];
3087 enum line_type type;
3088 bool first_read = view->lines == 0;
3090 if (textlen <= SIZEOF_TREE_ATTR)
3093 type = text[STRING_SIZE("100644 ")] == 't'
3094 ? LINE_TREE_DIR : LINE_TREE_FILE;
3097 /* Add path info line */
3098 if (!string_format(buf, "Directory path /%s", opt_path) ||
3099 !realloc_lines(view, view->line_size + 1) ||
3100 !add_line_text(view, buf, LINE_DEFAULT))
3103 /* Insert "link" to parent directory. */
3105 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3106 !realloc_lines(view, view->line_size + 1) ||
3107 !add_line_text(view, buf, LINE_TREE_DIR))
3112 /* Strip the path part ... */
3114 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3115 size_t striplen = strlen(opt_path);
3116 char *path = text + SIZEOF_TREE_ATTR;
3118 if (pathlen > striplen)
3119 memmove(path, path + striplen,
3120 pathlen - striplen + 1);
3123 /* Skip "Directory ..." and ".." line. */
3124 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3125 struct line *line = &view->line[pos];
3126 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
3127 char *path2 = text + SIZEOF_TREE_ATTR;
3128 int cmp = tree_compare_entry(line->type, path1, type, path2);
3133 text = strdup(text);
3137 if (view->lines > pos)
3138 memmove(&view->line[pos + 1], &view->line[pos],
3139 (view->lines - pos) * sizeof(*line));
3141 line = &view->line[pos];
3148 if (!add_line_text(view, text, type))
3151 if (tree_lineno > view->lineno) {
3152 view->lineno = tree_lineno;
3160 tree_request(struct view *view, enum request request, struct line *line)
3162 enum open_flags flags;
3164 if (request == REQ_TREE_PARENT) {
3167 request = REQ_ENTER;
3168 line = &view->line[1];
3170 /* quit view if at top of tree */
3171 return REQ_VIEW_CLOSE;
3174 if (request != REQ_ENTER)
3177 /* Cleanup the stack if the tree view is at a different tree. */
3178 while (!*opt_path && tree_stack)
3179 pop_tree_stack_entry();
3181 switch (line->type) {
3183 /* Depending on whether it is a subdir or parent (updir?) link
3184 * mangle the path buffer. */
3185 if (line == &view->line[1] && *opt_path) {
3186 pop_tree_stack_entry();
3189 char *data = line->data;
3190 char *basename = data + SIZEOF_TREE_ATTR;
3192 push_tree_stack_entry(basename, view->lineno);
3195 /* Trees and subtrees share the same ID, so they are not not
3196 * unique like blobs. */
3197 flags = OPEN_RELOAD;
3198 request = REQ_VIEW_TREE;
3201 case LINE_TREE_FILE:
3202 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3203 request = REQ_VIEW_BLOB;
3210 open_view(view, request, flags);
3211 if (request == REQ_VIEW_TREE) {
3212 view->lineno = tree_lineno;
3219 tree_select(struct view *view, struct line *line)
3221 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3223 if (line->type == LINE_TREE_FILE) {
3224 string_copy_rev(ref_blob, text);
3226 } else if (line->type != LINE_TREE_DIR) {
3230 string_copy_rev(view->ref, text);
3233 static struct view_ops tree_ops = {
3244 blob_read(struct view *view, char *line)
3246 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3249 static struct view_ops blob_ops = {
3268 char rev[SIZEOF_REV];
3269 char name[SIZEOF_STR];
3273 char rev[SIZEOF_REV];
3274 char name[SIZEOF_STR];
3278 static struct status stage_status;
3279 static enum line_type stage_line_type;
3281 /* Get fields from the diff line:
3282 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3285 status_get_diff(struct status *file, char *buf, size_t bufsize)
3287 char *old_mode = buf + 1;
3288 char *new_mode = buf + 8;
3289 char *old_rev = buf + 15;
3290 char *new_rev = buf + 56;
3291 char *status = buf + 97;
3294 old_mode[-1] != ':' ||
3295 new_mode[-1] != ' ' ||
3296 old_rev[-1] != ' ' ||
3297 new_rev[-1] != ' ' ||
3301 file->status = *status;
3303 string_copy_rev(file->old.rev, old_rev);
3304 string_copy_rev(file->new.rev, new_rev);
3306 file->old.mode = strtoul(old_mode, NULL, 8);
3307 file->new.mode = strtoul(new_mode, NULL, 8);
3309 file->old.name[0] = file->new.name[0] = 0;
3315 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3317 struct status *file = NULL;
3318 struct status *unmerged = NULL;
3319 char buf[SIZEOF_STR * 4];
3323 pipe = popen(cmd, "r");
3327 add_line_data(view, NULL, type);
3329 while (!feof(pipe) && !ferror(pipe)) {
3333 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3336 bufsize += readsize;
3338 /* Process while we have NUL chars. */
3339 while ((sep = memchr(buf, 0, bufsize))) {
3340 size_t sepsize = sep - buf + 1;
3343 if (!realloc_lines(view, view->line_size + 1))
3346 file = calloc(1, sizeof(*file));
3350 add_line_data(view, file, type);
3353 /* Parse diff info part. */
3357 } else if (!file->status) {
3358 if (!status_get_diff(file, buf, sepsize))
3362 memmove(buf, sep + 1, bufsize);
3364 sep = memchr(buf, 0, bufsize);
3367 sepsize = sep - buf + 1;
3369 /* Collapse all 'M'odified entries that
3370 * follow a associated 'U'nmerged entry.
3372 if (file->status == 'U') {
3375 } else if (unmerged) {
3376 int collapse = !strcmp(buf, unmerged->new.name);
3387 /* Grab the old name for rename/copy. */
3388 if (!*file->old.name &&
3389 (file->status == 'R' || file->status == 'C')) {
3390 sepsize = sep - buf + 1;
3391 string_ncopy(file->old.name, buf, sepsize);
3393 memmove(buf, sep + 1, bufsize);
3395 sep = memchr(buf, 0, bufsize);
3398 sepsize = sep - buf + 1;
3401 /* git-ls-files just delivers a NUL separated
3402 * list of file names similar to the second half
3403 * of the git-diff-* output. */
3404 string_ncopy(file->new.name, buf, sepsize);
3405 if (!*file->old.name)
3406 string_copy(file->old.name, file->new.name);
3408 memmove(buf, sep + 1, bufsize);
3419 if (!view->line[view->lines - 1].data)
3420 add_line_data(view, NULL, LINE_STAT_NONE);
3426 /* Don't show unmerged entries in the staged section. */
3427 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3428 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3429 #define STATUS_LIST_OTHER_CMD \
3430 "git ls-files -z --others --exclude-per-directory=.gitignore"
3432 #define STATUS_DIFF_INDEX_SHOW_CMD \
3433 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3435 #define STATUS_DIFF_FILES_SHOW_CMD \
3436 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3438 /* First parse staged info using git-diff-index(1), then parse unstaged
3439 * info using git-diff-files(1), and finally untracked files using
3440 * git-ls-files(1). */
3442 status_open(struct view *view)
3444 struct stat statbuf;
3445 char exclude[SIZEOF_STR];
3446 char cmd[SIZEOF_STR];
3447 unsigned long prev_lineno = view->lineno;
3450 for (i = 0; i < view->lines; i++)
3451 free(view->line[i].data);
3453 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3456 if (!realloc_lines(view, view->line_size + 6))
3459 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3462 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3464 if (stat(exclude, &statbuf) >= 0) {
3465 size_t cmdsize = strlen(cmd);
3467 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3468 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3472 system("git update-index -q --refresh");
3474 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3475 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3476 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3479 /* If all went well restore the previous line number to stay in
3481 if (prev_lineno < view->lines)
3482 view->lineno = prev_lineno;
3484 view->lineno = view->lines - 1;
3490 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3492 struct status *status = line->data;
3493 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
3495 wmove(view->win, lineno, 0);
3498 wattrset(view->win, get_line_attr(LINE_CURSOR));
3499 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3502 } else if (!status && line->type != LINE_STAT_NONE) {
3503 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3504 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3507 wattrset(view->win, get_line_attr(line->type));
3513 switch (line->type) {
3514 case LINE_STAT_STAGED:
3515 text = "Changes to be committed:";
3518 case LINE_STAT_UNSTAGED:
3519 text = "Changed but not updated:";
3522 case LINE_STAT_UNTRACKED:
3523 text = "Untracked files:";
3526 case LINE_STAT_NONE:
3527 text = " (no files)";
3534 draw_text(view, text, view->width, 0, TRUE, tilde_attr);
3538 waddch(view->win, status->status);
3540 wattrset(view->win, A_NORMAL);
3541 wmove(view->win, lineno, 4);
3542 if (view->width < 5)
3545 draw_text(view, status->new.name, view->width - 5, 5, TRUE, tilde_attr);
3550 status_enter(struct view *view, struct line *line)
3552 struct status *status = line->data;
3553 char oldpath[SIZEOF_STR] = "";
3554 char newpath[SIZEOF_STR] = "";
3558 if (line->type == LINE_STAT_NONE ||
3559 (!status && line[1].type == LINE_STAT_NONE)) {
3560 report("No file to diff");
3565 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
3567 /* Diffs for unmerged entries are empty when pasing the
3568 * new path, so leave it empty. */
3569 if (status->status != 'U' &&
3570 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
3575 line->type != LINE_STAT_UNTRACKED &&
3576 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3579 switch (line->type) {
3580 case LINE_STAT_STAGED:
3581 if (!string_format_from(opt_cmd, &cmdsize,
3582 STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
3585 info = "Staged changes to %s";
3587 info = "Staged changes";
3590 case LINE_STAT_UNSTAGED:
3591 if (!string_format_from(opt_cmd, &cmdsize,
3592 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
3595 info = "Unstaged changes to %s";
3597 info = "Unstaged changes";
3600 case LINE_STAT_UNTRACKED:
3606 report("No file to show");
3610 opt_pipe = fopen(status->new.name, "r");
3611 info = "Untracked file %s";
3615 die("line type %d not handled in switch", line->type);
3618 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3619 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
3621 stage_status = *status;
3623 memset(&stage_status, 0, sizeof(stage_status));
3626 stage_line_type = line->type;
3627 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
3635 status_update_file(struct view *view, struct status *status, enum line_type type)
3637 char cmd[SIZEOF_STR];
3638 char buf[SIZEOF_STR];
3645 type != LINE_STAT_UNTRACKED &&
3646 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3650 case LINE_STAT_STAGED:
3651 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3654 status->old.name, 0))
3657 string_add(cmd, cmdsize, "git update-index -z --index-info");
3660 case LINE_STAT_UNSTAGED:
3661 case LINE_STAT_UNTRACKED:
3662 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
3665 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3669 die("line type %d not handled in switch", type);
3672 pipe = popen(cmd, "w");
3676 while (!ferror(pipe) && written < bufsize) {
3677 written += fwrite(buf + written, 1, bufsize - written, pipe);
3682 if (written != bufsize)
3689 status_update(struct view *view)
3691 struct line *line = &view->line[view->lineno];
3693 assert(view->lines);
3696 while (++line < view->line + view->lines && line->data) {
3697 if (!status_update_file(view, line->data, line->type))
3698 report("Failed to update file status");
3701 if (!line[-1].data) {
3702 report("Nothing to update");
3706 } else if (!status_update_file(view, line->data, line->type)) {
3707 report("Failed to update file status");
3712 status_request(struct view *view, enum request request, struct line *line)
3714 struct status *status = line->data;
3717 case REQ_STATUS_UPDATE:
3718 status_update(view);
3721 case REQ_STATUS_MERGE:
3722 if (!status || status->status != 'U') {
3723 report("Merging only possible for files with unmerged status ('U').");
3726 open_mergetool(status->new.name);
3733 open_editor(status->status != '?', status->new.name);
3737 /* After returning the status view has been split to
3738 * show the stage view. No further reloading is
3740 status_enter(view, line);
3744 /* Simply reload the view. */
3751 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3757 status_select(struct view *view, struct line *line)
3759 struct status *status = line->data;
3760 char file[SIZEOF_STR] = "all files";
3764 if (status && !string_format(file, "'%s'", status->new.name))
3767 if (!status && line[1].type == LINE_STAT_NONE)
3770 switch (line->type) {
3771 case LINE_STAT_STAGED:
3772 text = "Press %s to unstage %s for commit";
3775 case LINE_STAT_UNSTAGED:
3776 text = "Press %s to stage %s for commit";
3779 case LINE_STAT_UNTRACKED:
3780 text = "Press %s to stage %s for addition";
3783 case LINE_STAT_NONE:
3784 text = "Nothing to update";
3788 die("line type %d not handled in switch", line->type);
3791 if (status && status->status == 'U') {
3792 text = "Press %s to resolve conflict in %s";
3793 key = get_key(REQ_STATUS_MERGE);
3796 key = get_key(REQ_STATUS_UPDATE);
3799 string_format(view->ref, text, key, file);
3803 status_grep(struct view *view, struct line *line)
3805 struct status *status = line->data;
3806 enum { S_STATUS, S_NAME, S_END } state;
3813 for (state = S_STATUS; state < S_END; state++) {
3817 case S_NAME: text = status->new.name; break;
3819 buf[0] = status->status;
3827 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3834 static struct view_ops status_ops = {
3846 stage_diff_line(FILE *pipe, struct line *line)
3848 char *buf = line->data;
3849 size_t bufsize = strlen(buf);
3852 while (!ferror(pipe) && written < bufsize) {
3853 written += fwrite(buf + written, 1, bufsize - written, pipe);
3858 return written == bufsize;
3861 static struct line *
3862 stage_diff_hdr(struct view *view, struct line *line)
3864 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3865 struct line *diff_hdr;
3867 if (line->type == LINE_DIFF_CHUNK)
3868 diff_hdr = line - 1;
3870 diff_hdr = view->line + 1;
3872 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3873 if (diff_hdr->type == LINE_DIFF_HEADER)
3876 diff_hdr += diff_hdr_dir;
3883 stage_update_chunk(struct view *view, struct line *line)
3885 char cmd[SIZEOF_STR];
3887 struct line *diff_hdr, *diff_chunk, *diff_end;
3890 diff_hdr = stage_diff_hdr(view, line);
3895 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3898 if (!string_format_from(cmd, &cmdsize,
3899 "git apply --cached %s - && "
3900 "git update-index -q --unmerged --refresh 2>/dev/null",
3901 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3904 pipe = popen(cmd, "w");
3908 diff_end = view->line + view->lines;
3909 if (line->type != LINE_DIFF_CHUNK) {
3910 diff_chunk = diff_hdr;
3913 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3914 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3915 diff_chunk->type == LINE_DIFF_HEADER)
3916 diff_end = diff_chunk;
3920 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3921 switch (diff_hdr->type) {
3922 case LINE_DIFF_HEADER:
3923 case LINE_DIFF_INDEX:
3933 if (!stage_diff_line(pipe, diff_hdr++)) {
3940 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3945 if (diff_chunk != diff_end)
3952 stage_update(struct view *view, struct line *line)
3954 if (stage_line_type != LINE_STAT_UNTRACKED &&
3955 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3956 if (!stage_update_chunk(view, line)) {
3957 report("Failed to apply chunk");
3961 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3962 report("Failed to update file");
3966 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3968 view = VIEW(REQ_VIEW_STATUS);
3969 if (view_is_displayed(view))
3970 status_enter(view, &view->line[view->lineno]);
3974 stage_request(struct view *view, enum request request, struct line *line)
3977 case REQ_STATUS_UPDATE:
3978 stage_update(view, line);
3982 if (!stage_status.new.name[0])
3985 open_editor(stage_status.status != '?', stage_status.new.name);
3989 pager_request(view, request, line);
3999 static struct view_ops stage_ops = {
4015 char id[SIZEOF_REV]; /* SHA1 ID. */
4016 char title[128]; /* First line of the commit message. */
4017 char author[75]; /* Author of the commit. */
4018 struct tm time; /* Date from the author ident. */
4019 struct ref **refs; /* Repository references. */
4020 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4021 size_t graph_size; /* The width of the graph array. */
4024 /* Size of rev graph with no "padding" columns */
4025 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4028 struct rev_graph *prev, *next, *parents;
4029 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4031 struct commit *commit;
4033 unsigned int boundary:1;
4036 /* Parents of the commit being visualized. */
4037 static struct rev_graph graph_parents[4];
4039 /* The current stack of revisions on the graph. */
4040 static struct rev_graph graph_stacks[4] = {
4041 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4042 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4043 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4044 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4048 graph_parent_is_merge(struct rev_graph *graph)
4050 return graph->parents->size > 1;
4054 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4056 struct commit *commit = graph->commit;
4058 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4059 commit->graph[commit->graph_size++] = symbol;
4063 done_rev_graph(struct rev_graph *graph)
4065 if (graph_parent_is_merge(graph) &&
4066 graph->pos < graph->size - 1 &&
4067 graph->next->size == graph->size + graph->parents->size - 1) {
4068 size_t i = graph->pos + graph->parents->size - 1;
4070 graph->commit->graph_size = i * 2;
4071 while (i < graph->next->size - 1) {
4072 append_to_rev_graph(graph, ' ');
4073 append_to_rev_graph(graph, '\\');
4078 graph->size = graph->pos = 0;
4079 graph->commit = NULL;
4080 memset(graph->parents, 0, sizeof(*graph->parents));
4084 push_rev_graph(struct rev_graph *graph, char *parent)
4088 /* "Collapse" duplicate parents lines.
4090 * FIXME: This needs to also update update the drawn graph but
4091 * for now it just serves as a method for pruning graph lines. */
4092 for (i = 0; i < graph->size; i++)
4093 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4096 if (graph->size < SIZEOF_REVITEMS) {
4097 string_copy_rev(graph->rev[graph->size++], parent);
4102 get_rev_graph_symbol(struct rev_graph *graph)
4106 if (graph->boundary)
4107 symbol = REVGRAPH_BOUND;
4108 else if (graph->parents->size == 0)
4109 symbol = REVGRAPH_INIT;
4110 else if (graph_parent_is_merge(graph))
4111 symbol = REVGRAPH_MERGE;
4112 else if (graph->pos >= graph->size)
4113 symbol = REVGRAPH_BRANCH;
4115 symbol = REVGRAPH_COMMIT;
4121 draw_rev_graph(struct rev_graph *graph)
4124 chtype separator, line;
4126 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4127 static struct rev_filler fillers[] = {
4128 { ' ', REVGRAPH_LINE },
4133 chtype symbol = get_rev_graph_symbol(graph);
4134 struct rev_filler *filler;
4137 filler = &fillers[DEFAULT];
4139 for (i = 0; i < graph->pos; i++) {
4140 append_to_rev_graph(graph, filler->line);
4141 if (graph_parent_is_merge(graph->prev) &&
4142 graph->prev->pos == i)
4143 filler = &fillers[RSHARP];
4145 append_to_rev_graph(graph, filler->separator);
4148 /* Place the symbol for this revision. */
4149 append_to_rev_graph(graph, symbol);
4151 if (graph->prev->size > graph->size)
4152 filler = &fillers[RDIAG];
4154 filler = &fillers[DEFAULT];
4158 for (; i < graph->size; i++) {
4159 append_to_rev_graph(graph, filler->separator);
4160 append_to_rev_graph(graph, filler->line);
4161 if (graph_parent_is_merge(graph->prev) &&
4162 i < graph->prev->pos + graph->parents->size)
4163 filler = &fillers[RSHARP];
4164 if (graph->prev->size > graph->size)
4165 filler = &fillers[LDIAG];
4168 if (graph->prev->size > graph->size) {
4169 append_to_rev_graph(graph, filler->separator);
4170 if (filler->line != ' ')
4171 append_to_rev_graph(graph, filler->line);
4175 /* Prepare the next rev graph */
4177 prepare_rev_graph(struct rev_graph *graph)
4181 /* First, traverse all lines of revisions up to the active one. */
4182 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4183 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4186 push_rev_graph(graph->next, graph->rev[graph->pos]);
4189 /* Interleave the new revision parent(s). */
4190 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4191 push_rev_graph(graph->next, graph->parents->rev[i]);
4193 /* Lastly, put any remaining revisions. */
4194 for (i = graph->pos + 1; i < graph->size; i++)
4195 push_rev_graph(graph->next, graph->rev[i]);
4199 update_rev_graph(struct rev_graph *graph)
4201 /* If this is the finalizing update ... */
4203 prepare_rev_graph(graph);
4205 /* Graph visualization needs a one rev look-ahead,
4206 * so the first update doesn't visualize anything. */
4207 if (!graph->prev->commit)
4210 draw_rev_graph(graph->prev);
4211 done_rev_graph(graph->prev->prev);
4220 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4222 char buf[DATE_COLS + 1];
4223 struct commit *commit = line->data;
4224 enum line_type type;
4230 if (!*commit->author)
4233 space = view->width;
4234 wmove(view->win, lineno, col);
4238 wattrset(view->win, get_line_attr(type));
4239 wchgat(view->win, -1, 0, type, NULL);
4242 type = LINE_MAIN_COMMIT;
4243 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4244 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4250 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4252 view, buf, view->width - col, col, FALSE, tilde_attr);
4254 view, " ", view->width - col - n, col + n, FALSE,
4258 wmove(view->win, lineno, col);
4259 if (col >= view->width)
4262 if (type != LINE_CURSOR)
4263 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4268 max_len = view->width - col;
4269 if (max_len > AUTHOR_COLS - 1)
4270 max_len = AUTHOR_COLS - 1;
4272 view, commit->author, max_len, col, TRUE, tilde_attr);
4274 if (col >= view->width)
4278 if (opt_rev_graph && commit->graph_size) {
4279 size_t graph_size = view->width - col;
4282 if (type != LINE_CURSOR)
4283 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4284 wmove(view->win, lineno, col);
4285 if (graph_size > commit->graph_size)
4286 graph_size = commit->graph_size;
4287 /* Using waddch() instead of waddnstr() ensures that
4288 * they'll be rendered correctly for the cursor line. */
4289 for (i = 0; i < graph_size; i++)
4290 waddch(view->win, commit->graph[i]);
4292 col += commit->graph_size + 1;
4293 if (col >= view->width)
4295 waddch(view->win, ' ');
4297 if (type != LINE_CURSOR)
4298 wattrset(view->win, A_NORMAL);
4300 wmove(view->win, lineno, col);
4302 if (opt_show_refs && commit->refs) {
4306 if (type == LINE_CURSOR)
4308 else if (commit->refs[i]->ltag)
4309 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4310 else if (commit->refs[i]->tag)
4311 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4312 else if (commit->refs[i]->remote)
4313 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4315 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4318 view, "[", view->width - col, col, TRUE,
4321 view, commit->refs[i]->name, view->width - col,
4322 col, TRUE, tilde_attr);
4324 view, "]", view->width - col, col, TRUE,
4326 if (type != LINE_CURSOR)
4327 wattrset(view->win, A_NORMAL);
4329 view, " ", view->width - col, col, TRUE,
4331 if (col >= view->width)
4333 } while (commit->refs[i++]->next);
4336 if (type != LINE_CURSOR)
4337 wattrset(view->win, get_line_attr(type));
4340 view, commit->title, view->width - col, col, TRUE, tilde_attr);
4345 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4347 main_read(struct view *view, char *line)
4349 static struct rev_graph *graph = graph_stacks;
4350 enum line_type type;
4351 struct commit *commit;
4354 update_rev_graph(graph);
4358 type = get_line_type(line);
4359 if (type == LINE_COMMIT) {
4360 commit = calloc(1, sizeof(struct commit));
4364 line += STRING_SIZE("commit ");
4366 graph->boundary = 1;
4370 string_copy_rev(commit->id, line);
4371 commit->refs = get_refs(commit->id);
4372 graph->commit = commit;
4373 add_line_data(view, commit, LINE_MAIN_COMMIT);
4379 commit = view->line[view->lines - 1].data;
4383 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4388 /* Parse author lines where the name may be empty:
4389 * author <email@address.tld> 1138474660 +0100
4391 char *ident = line + STRING_SIZE("author ");
4392 char *nameend = strchr(ident, '<');
4393 char *emailend = strchr(ident, '>');
4395 if (!nameend || !emailend)
4398 update_rev_graph(graph);
4399 graph = graph->next;
4401 *nameend = *emailend = 0;
4402 ident = chomp_string(ident);
4404 ident = chomp_string(nameend + 1);
4409 string_ncopy(commit->author, ident, strlen(ident));
4411 /* Parse epoch and timezone */
4412 if (emailend[1] == ' ') {
4413 char *secs = emailend + 2;
4414 char *zone = strchr(secs, ' ');
4415 time_t time = (time_t) atol(secs);
4417 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4421 tz = ('0' - zone[1]) * 60 * 60 * 10;
4422 tz += ('0' - zone[2]) * 60 * 60;
4423 tz += ('0' - zone[3]) * 60;
4424 tz += ('0' - zone[4]) * 60;
4432 gmtime_r(&time, &commit->time);
4437 /* Fill in the commit title if it has not already been set. */
4438 if (commit->title[0])
4441 /* Require titles to start with a non-space character at the
4442 * offset used by git log. */
4443 if (strncmp(line, " ", 4))
4446 /* Well, if the title starts with a whitespace character,
4447 * try to be forgiving. Otherwise we end up with no title. */
4448 while (isspace(*line))
4452 /* FIXME: More graceful handling of titles; append "..." to
4453 * shortened titles, etc. */
4455 string_ncopy(commit->title, line, strlen(line));
4462 main_request(struct view *view, enum request request, struct line *line)
4464 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4466 if (request == REQ_ENTER)
4467 open_view(view, REQ_VIEW_DIFF, flags);
4475 main_grep(struct view *view, struct line *line)
4477 struct commit *commit = line->data;
4478 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4479 char buf[DATE_COLS + 1];
4482 for (state = S_TITLE; state < S_END; state++) {
4486 case S_TITLE: text = commit->title; break;
4487 case S_AUTHOR: text = commit->author; break;
4489 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4498 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4506 main_select(struct view *view, struct line *line)
4508 struct commit *commit = line->data;
4510 string_copy_rev(view->ref, commit->id);
4511 string_copy_rev(ref_commit, view->ref);
4514 static struct view_ops main_ops = {
4526 * Unicode / UTF-8 handling
4528 * NOTE: Much of the following code for dealing with unicode is derived from
4529 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4530 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4533 /* I've (over)annotated a lot of code snippets because I am not entirely
4534 * confident that the approach taken by this small UTF-8 interface is correct.
4538 unicode_width(unsigned long c)
4541 (c <= 0x115f /* Hangul Jamo */
4544 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
4546 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4547 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4548 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4549 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4550 || (c >= 0xffe0 && c <= 0xffe6)
4551 || (c >= 0x20000 && c <= 0x2fffd)
4552 || (c >= 0x30000 && c <= 0x3fffd)))
4556 return opt_tab_size;
4561 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4562 * Illegal bytes are set one. */
4563 static const unsigned char utf8_bytes[256] = {
4564 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,
4565 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,
4566 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,
4567 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,
4568 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,
4569 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,
4570 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,
4571 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,
4574 /* Decode UTF-8 multi-byte representation into a unicode character. */
4575 static inline unsigned long
4576 utf8_to_unicode(const char *string, size_t length)
4578 unsigned long unicode;
4582 unicode = string[0];
4585 unicode = (string[0] & 0x1f) << 6;
4586 unicode += (string[1] & 0x3f);
4589 unicode = (string[0] & 0x0f) << 12;
4590 unicode += ((string[1] & 0x3f) << 6);
4591 unicode += (string[2] & 0x3f);
4594 unicode = (string[0] & 0x0f) << 18;
4595 unicode += ((string[1] & 0x3f) << 12);
4596 unicode += ((string[2] & 0x3f) << 6);
4597 unicode += (string[3] & 0x3f);
4600 unicode = (string[0] & 0x0f) << 24;
4601 unicode += ((string[1] & 0x3f) << 18);
4602 unicode += ((string[2] & 0x3f) << 12);
4603 unicode += ((string[3] & 0x3f) << 6);
4604 unicode += (string[4] & 0x3f);
4607 unicode = (string[0] & 0x01) << 30;
4608 unicode += ((string[1] & 0x3f) << 24);
4609 unicode += ((string[2] & 0x3f) << 18);
4610 unicode += ((string[3] & 0x3f) << 12);
4611 unicode += ((string[4] & 0x3f) << 6);
4612 unicode += (string[5] & 0x3f);
4615 die("Invalid unicode length");
4618 /* Invalid characters could return the special 0xfffd value but NUL
4619 * should be just as good. */
4620 return unicode > 0xffff ? 0 : unicode;
4623 /* Calculates how much of string can be shown within the given maximum width
4624 * and sets trimmed parameter to non-zero value if all of string could not be
4625 * shown. If the reserve flag is TRUE, it will reserve at least one
4626 * trailing character, which can be useful when drawing a delimiter.
4628 * Returns the number of bytes to output from string to satisfy max_width. */
4630 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
4632 const char *start = string;
4633 const char *end = strchr(string, '\0');
4634 unsigned char last_bytes = 0;
4639 while (string < end) {
4640 int c = *(unsigned char *) string;
4641 unsigned char bytes = utf8_bytes[c];
4643 unsigned long unicode;
4645 if (string + bytes > end)
4648 /* Change representation to figure out whether
4649 * it is a single- or double-width character. */
4651 unicode = utf8_to_unicode(string, bytes);
4652 /* FIXME: Graceful handling of invalid unicode character. */
4656 ucwidth = unicode_width(unicode);
4658 if (width > max_width) {
4660 if (reserve && width - ucwidth == max_width) {
4661 string -= last_bytes;
4670 return string - start;
4678 /* Whether or not the curses interface has been initialized. */
4679 static bool cursed = FALSE;
4681 /* The status window is used for polling keystrokes. */
4682 static WINDOW *status_win;
4684 static bool status_empty = TRUE;
4686 /* Update status and title window. */
4688 report(const char *msg, ...)
4690 struct view *view = display[current_view];
4696 char buf[SIZEOF_STR];
4699 va_start(args, msg);
4700 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4701 buf[sizeof(buf) - 1] = 0;
4702 buf[sizeof(buf) - 2] = '.';
4703 buf[sizeof(buf) - 3] = '.';
4704 buf[sizeof(buf) - 4] = '.';
4710 if (!status_empty || *msg) {
4713 va_start(args, msg);
4715 wmove(status_win, 0, 0);
4717 vwprintw(status_win, msg, args);
4718 status_empty = FALSE;
4720 status_empty = TRUE;
4722 wclrtoeol(status_win);
4723 wrefresh(status_win);
4728 update_view_title(view);
4729 update_display_cursor(view);
4732 /* Controls when nodelay should be in effect when polling user input. */
4734 set_nonblocking_input(bool loading)
4736 static unsigned int loading_views;
4738 if ((loading == FALSE && loading_views-- == 1) ||
4739 (loading == TRUE && loading_views++ == 0))
4740 nodelay(status_win, loading);
4748 /* Initialize the curses library */
4749 if (isatty(STDIN_FILENO)) {
4750 cursed = !!initscr();
4752 /* Leave stdin and stdout alone when acting as a pager. */
4753 FILE *io = fopen("/dev/tty", "r+");
4756 die("Failed to open /dev/tty");
4757 cursed = !!newterm(NULL, io, io);
4761 die("Failed to initialize curses");
4763 nonl(); /* Tell curses not to do NL->CR/NL on output */
4764 cbreak(); /* Take input chars one at a time, no wait for \n */
4765 noecho(); /* Don't echo input */
4766 leaveok(stdscr, TRUE);
4771 getmaxyx(stdscr, y, x);
4772 status_win = newwin(1, 0, y - 1, 0);
4774 die("Failed to create status window");
4776 /* Enable keyboard mapping */
4777 keypad(status_win, TRUE);
4778 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4782 read_prompt(const char *prompt)
4784 enum { READING, STOP, CANCEL } status = READING;
4785 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4788 while (status == READING) {
4794 foreach_view (view, i)
4799 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4800 wclrtoeol(status_win);
4802 /* Refresh, accept single keystroke of input */
4803 key = wgetch(status_win);
4808 status = pos ? STOP : CANCEL;
4826 if (pos >= sizeof(buf)) {
4827 report("Input string too long");
4832 buf[pos++] = (char) key;
4836 /* Clear the status window */
4837 status_empty = FALSE;
4840 if (status == CANCEL)
4849 * Repository references
4852 static struct ref *refs = NULL;
4853 static size_t refs_alloc = 0;
4854 static size_t refs_size = 0;
4856 /* Id <-> ref store */
4857 static struct ref ***id_refs = NULL;
4858 static size_t id_refs_alloc = 0;
4859 static size_t id_refs_size = 0;
4861 static struct ref **
4864 struct ref ***tmp_id_refs;
4865 struct ref **ref_list = NULL;
4866 size_t ref_list_alloc = 0;
4867 size_t ref_list_size = 0;
4870 for (i = 0; i < id_refs_size; i++)
4871 if (!strcmp(id, id_refs[i][0]->id))
4874 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
4879 id_refs = tmp_id_refs;
4881 for (i = 0; i < refs_size; i++) {
4884 if (strcmp(id, refs[i].id))
4887 tmp = realloc_items(ref_list, &ref_list_alloc,
4888 ref_list_size + 1, sizeof(*ref_list));
4896 if (ref_list_size > 0)
4897 ref_list[ref_list_size - 1]->next = 1;
4898 ref_list[ref_list_size] = &refs[i];
4900 /* XXX: The properties of the commit chains ensures that we can
4901 * safely modify the shared ref. The repo references will
4902 * always be similar for the same id. */
4903 ref_list[ref_list_size]->next = 0;
4908 id_refs[id_refs_size++] = ref_list;
4914 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4919 bool remote = FALSE;
4920 bool check_replace = FALSE;
4922 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4923 if (!strcmp(name + namelen - 3, "^{}")) {
4926 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
4927 check_replace = TRUE;
4933 namelen -= STRING_SIZE("refs/tags/");
4934 name += STRING_SIZE("refs/tags/");
4936 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4938 namelen -= STRING_SIZE("refs/remotes/");
4939 name += STRING_SIZE("refs/remotes/");
4941 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4942 namelen -= STRING_SIZE("refs/heads/");
4943 name += STRING_SIZE("refs/heads/");
4945 } else if (!strcmp(name, "HEAD")) {
4949 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
4950 /* it's an annotated tag, replace the previous sha1 with the
4951 * resolved commit id; relies on the fact git-ls-remote lists
4952 * the commit id of an annotated tag right beofre the commit id
4954 refs[refs_size - 1].ltag = ltag;
4955 string_copy_rev(refs[refs_size - 1].id, id);
4959 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
4963 ref = &refs[refs_size++];
4964 ref->name = malloc(namelen + 1);
4968 strncpy(ref->name, name, namelen);
4969 ref->name[namelen] = 0;
4972 ref->remote = remote;
4973 string_copy_rev(ref->id, id);
4981 const char *cmd_env = getenv("TIG_LS_REMOTE");
4982 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4984 return read_properties(popen(cmd, "r"), "\t", read_ref);
4988 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4990 if (!strcmp(name, "i18n.commitencoding"))
4991 string_ncopy(opt_encoding, value, valuelen);
4993 if (!strcmp(name, "core.editor"))
4994 string_ncopy(opt_editor, value, valuelen);
5000 load_repo_config(void)
5002 return read_properties(popen(GIT_CONFIG " --list", "r"),
5003 "=", read_repo_config_option);
5007 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5009 if (!opt_git_dir[0]) {
5010 string_ncopy(opt_git_dir, name, namelen);
5012 } else if (opt_is_inside_work_tree == -1) {
5013 /* This can be 3 different values depending on the
5014 * version of git being used. If git-rev-parse does not
5015 * understand --is-inside-work-tree it will simply echo
5016 * the option else either "true" or "false" is printed.
5017 * Default to true for the unknown case. */
5018 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5021 string_ncopy(opt_cdup, name, namelen);
5027 /* XXX: The line outputted by "--show-cdup" can be empty so the option
5028 * must be the last one! */
5030 load_repo_info(void)
5032 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
5033 "=", read_repo_info);
5037 read_properties(FILE *pipe, const char *separators,
5038 int (*read_property)(char *, size_t, char *, size_t))
5040 char buffer[BUFSIZ];
5047 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5052 name = chomp_string(name);
5053 namelen = strcspn(name, separators);
5055 if (name[namelen]) {
5057 value = chomp_string(name + namelen + 1);
5058 valuelen = strlen(value);
5065 state = read_property(name, namelen, value, valuelen);
5068 if (state != ERR && ferror(pipe))
5081 static void __NORETURN
5084 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5090 static void __NORETURN
5091 die(const char *err, ...)
5097 va_start(args, err);
5098 fputs("tig: ", stderr);
5099 vfprintf(stderr, err, args);
5100 fputs("\n", stderr);
5107 warn(const char *msg, ...)
5111 va_start(args, msg);
5112 fputs("tig warning: ", stderr);
5113 vfprintf(stderr, msg, args);
5114 fputs("\n", stderr);
5119 main(int argc, char *argv[])
5122 enum request request;
5125 signal(SIGINT, quit);
5127 if (setlocale(LC_ALL, "")) {
5128 char *codeset = nl_langinfo(CODESET);
5130 string_ncopy(opt_codeset, codeset, strlen(codeset));
5133 if (load_repo_info() == ERR)
5134 die("Failed to load repo info.");
5136 if (load_options() == ERR)
5137 die("Failed to load user config.");
5139 /* Load the repo config file so options can be overwritten from
5140 * the command line. */
5141 if (load_repo_config() == ERR)
5142 die("Failed to load repo config.");
5144 if (!parse_options(argc, argv))
5147 /* Require a git repository unless when running in pager mode. */
5148 if (!opt_git_dir[0])
5149 die("Not a git repository");
5151 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5152 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5153 if (opt_iconv == ICONV_NONE)
5154 die("Failed to initialize character set conversion");
5157 if (load_refs() == ERR)
5158 die("Failed to load refs.");
5160 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5161 view->cmd_env = getenv(view->cmd_env);
5163 request = opt_request;
5167 while (view_driver(display[current_view], request)) {
5171 foreach_view (view, i)
5174 /* Refresh, accept single keystroke of input */
5175 key = wgetch(status_win);
5177 /* wgetch() with nodelay() enabled returns ERR when there's no
5184 request = get_keybinding(display[current_view]->keymap, key);
5186 /* Some low-level request handling. This keeps access to
5187 * status_win restricted. */
5191 char *cmd = read_prompt(":");
5193 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5194 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5195 opt_request = REQ_VIEW_DIFF;
5197 opt_request = REQ_VIEW_PAGER;
5206 case REQ_SEARCH_BACK:
5208 const char *prompt = request == REQ_SEARCH
5210 char *search = read_prompt(prompt);
5213 string_ncopy(opt_search, search, strlen(search));
5218 case REQ_SCREEN_RESIZE:
5222 getmaxyx(stdscr, height, width);
5224 /* Resize the status view and let the view driver take
5225 * care of resizing the displayed views. */
5226 wresize(status_win, 1, width);
5227 mvwin(status_win, height - 1, 0);
5228 wrefresh(status_win);