1 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
15 #define VERSION "tig-0.6.git"
33 #include <sys/types.h>
43 #define __NORETURN __attribute__((__noreturn__))
48 static void __NORETURN die(const char *err, ...);
49 static void report(const char *msg, ...);
50 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
51 static void set_nonblocking_input(bool loading);
52 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
54 #define ABS(x) ((x) >= 0 ? (x) : -(x))
55 #define MIN(x, y) ((x) < (y) ? (x) : (y))
57 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
58 #define STRING_SIZE(x) (sizeof(x) - 1)
60 #define SIZEOF_STR 1024 /* Default string size. */
61 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
62 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
66 #define REVGRAPH_INIT 'I'
67 #define REVGRAPH_MERGE 'M'
68 #define REVGRAPH_BRANCH '+'
69 #define REVGRAPH_COMMIT '*'
70 #define REVGRAPH_LINE '|'
72 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
74 /* This color name can be used to refer to the default term colors. */
75 #define COLOR_DEFAULT (-1)
77 #define ICONV_NONE ((iconv_t) -1)
79 /* The format and size of the date column in the main view. */
80 #define DATE_FORMAT "%Y-%m-%d %H:%M"
81 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
83 #define AUTHOR_COLS 20
85 /* The default interval between line numbers. */
86 #define NUMBER_INTERVAL 1
90 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
92 #define TIG_LS_REMOTE \
93 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
95 #define TIG_DIFF_CMD \
96 "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
99 "git log --cc --stat -n100 %s 2>/dev/null"
101 #define TIG_MAIN_CMD \
102 "git log --topo-order --pretty=raw %s 2>/dev/null"
104 #define TIG_TREE_CMD \
107 #define TIG_BLOB_CMD \
108 "git cat-file blob %s"
110 /* XXX: Needs to be defined to the empty string. */
111 #define TIG_HELP_CMD ""
112 #define TIG_PAGER_CMD ""
113 #define TIG_STATUS_CMD ""
115 /* Some ascii-shorthands fitted into the ncurses namespace. */
117 #define KEY_RETURN '\r'
122 char *name; /* Ref name; tag or head names are shortened. */
123 char id[SIZEOF_REV]; /* Commit SHA1 ID */
124 unsigned int tag:1; /* Is it a tag? */
125 unsigned int remote:1; /* Is it a remote ref? */
126 unsigned int next:1; /* For ref lists: are there more refs? */
129 static struct ref **get_refs(char *id);
138 set_from_int_map(struct int_map *map, size_t map_size,
139 int *value, const char *name, int namelen)
144 for (i = 0; i < map_size; i++)
145 if (namelen == map[i].namelen &&
146 !strncasecmp(name, map[i].name, namelen)) {
147 *value = map[i].value;
160 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
162 if (srclen > dstlen - 1)
165 strncpy(dst, src, srclen);
169 /* Shorthands for safely copying into a fixed buffer. */
171 #define string_copy(dst, src) \
172 string_ncopy_do(dst, sizeof(dst), src, sizeof(dst))
174 #define string_ncopy(dst, src, srclen) \
175 string_ncopy_do(dst, sizeof(dst), src, srclen)
177 #define string_copy_rev(dst, src) \
178 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
181 chomp_string(char *name)
185 while (isspace(*name))
188 namelen = strlen(name) - 1;
189 while (namelen > 0 && isspace(name[namelen]))
196 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
199 size_t pos = bufpos ? *bufpos : 0;
202 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
208 return pos >= bufsize ? FALSE : TRUE;
211 #define string_format(buf, fmt, args...) \
212 string_nformat(buf, sizeof(buf), NULL, fmt, args)
214 #define string_format_from(buf, from, fmt, args...) \
215 string_nformat(buf, sizeof(buf), from, fmt, args)
218 string_enum_compare(const char *str1, const char *str2, int len)
222 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
224 /* Diff-Header == DIFF_HEADER */
225 for (i = 0; i < len; i++) {
226 if (toupper(str1[i]) == toupper(str2[i]))
229 if (string_enum_sep(str1[i]) &&
230 string_enum_sep(str2[i]))
233 return str1[i] - str2[i];
241 * NOTE: The following is a slightly modified copy of the git project's shell
242 * quoting routines found in the quote.c file.
244 * Help to copy the thing properly quoted for the shell safety. any single
245 * quote is replaced with '\'', any exclamation point is replaced with '\!',
246 * and the whole thing is enclosed in a
249 * original sq_quote result
250 * name ==> name ==> 'name'
251 * a b ==> a b ==> 'a b'
252 * a'b ==> a'\''b ==> 'a'\''b'
253 * a!b ==> a'\!'b ==> 'a'\!'b'
257 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
261 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
264 while ((c = *src++)) {
265 if (c == '\'' || c == '!') {
276 if (bufsize < SIZEOF_STR)
288 /* XXX: Keep the view request first and in sync with views[]. */ \
289 REQ_GROUP("View switching") \
290 REQ_(VIEW_MAIN, "Show main view"), \
291 REQ_(VIEW_DIFF, "Show diff view"), \
292 REQ_(VIEW_LOG, "Show log view"), \
293 REQ_(VIEW_TREE, "Show tree view"), \
294 REQ_(VIEW_BLOB, "Show blob view"), \
295 REQ_(VIEW_HELP, "Show help page"), \
296 REQ_(VIEW_PAGER, "Show pager view"), \
297 REQ_(VIEW_STATUS, "Show status view"), \
299 REQ_GROUP("View manipulation") \
300 REQ_(ENTER, "Enter current line and scroll"), \
301 REQ_(NEXT, "Move to next"), \
302 REQ_(PREVIOUS, "Move to previous"), \
303 REQ_(VIEW_NEXT, "Move focus to next view"), \
304 REQ_(VIEW_CLOSE, "Close the current view"), \
305 REQ_(QUIT, "Close all views and quit"), \
307 REQ_GROUP("Cursor navigation") \
308 REQ_(MOVE_UP, "Move cursor one line up"), \
309 REQ_(MOVE_DOWN, "Move cursor one line down"), \
310 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
311 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
312 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
313 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
315 REQ_GROUP("Scrolling") \
316 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
317 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
318 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
319 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
321 REQ_GROUP("Searching") \
322 REQ_(SEARCH, "Search the view"), \
323 REQ_(SEARCH_BACK, "Search backwards in the view"), \
324 REQ_(FIND_NEXT, "Find next search match"), \
325 REQ_(FIND_PREV, "Find previous search match"), \
328 REQ_(NONE, "Do nothing"), \
329 REQ_(PROMPT, "Bring up the prompt"), \
330 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
331 REQ_(SCREEN_RESIZE, "Resize the screen"), \
332 REQ_(SHOW_VERSION, "Show version information"), \
333 REQ_(STOP_LOADING, "Stop all loading views"), \
334 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
335 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization")
338 /* User action requests. */
340 #define REQ_GROUP(help)
341 #define REQ_(req, help) REQ_##req
343 /* Offset all requests to avoid conflicts with ncurses getch values. */
344 REQ_OFFSET = KEY_MAX + 1,
352 struct request_info {
353 enum request request;
359 static struct request_info req_info[] = {
360 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
361 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
368 get_request(const char *name)
370 int namelen = strlen(name);
373 for (i = 0; i < ARRAY_SIZE(req_info); i++)
374 if (req_info[i].namelen == namelen &&
375 !string_enum_compare(req_info[i].name, name, namelen))
376 return req_info[i].request;
386 static const char usage[] =
387 VERSION " (" __DATE__ ")\n"
389 "Usage: tig [options]\n"
390 " or: tig [options] [--] [git log options]\n"
391 " or: tig [options] log [git log options]\n"
392 " or: tig [options] diff [git diff options]\n"
393 " or: tig [options] show [git show options]\n"
394 " or: tig [options] < [git command output]\n"
397 " -l Start up in log view\n"
398 " -d Start up in diff view\n"
399 " -S Start up in status view\n"
400 " -n[I], --line-number[=I] Show line numbers with given interval\n"
401 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
402 " -- Mark end of tig options\n"
403 " -v, --version Show version and exit\n"
404 " -h, --help Show help message and exit\n";
406 /* Option and state variables. */
407 static bool opt_line_number = FALSE;
408 static bool opt_rev_graph = FALSE;
409 static int opt_num_interval = NUMBER_INTERVAL;
410 static int opt_tab_size = TABSIZE;
411 static enum request opt_request = REQ_VIEW_MAIN;
412 static char opt_cmd[SIZEOF_STR] = "";
413 static char opt_path[SIZEOF_STR] = "";
414 static FILE *opt_pipe = NULL;
415 static char opt_encoding[20] = "UTF-8";
416 static bool opt_utf8 = TRUE;
417 static char opt_codeset[20] = "UTF-8";
418 static iconv_t opt_iconv = ICONV_NONE;
419 static char opt_search[SIZEOF_STR] = "";
427 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
437 int namelen = strlen(name);
441 if (strncmp(opt, name, namelen))
444 if (opt[namelen] == '=')
445 value = opt + namelen + 1;
448 if (!short_name || opt[1] != short_name)
453 va_start(args, type);
454 if (type == OPT_INT) {
455 number = va_arg(args, int *);
457 *number = atoi(value);
464 /* Returns the index of log or diff command or -1 to exit. */
466 parse_options(int argc, char *argv[])
470 for (i = 1; i < argc; i++) {
473 if (!strcmp(opt, "log") ||
474 !strcmp(opt, "diff") ||
475 !strcmp(opt, "show")) {
476 opt_request = opt[0] == 'l'
477 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
481 if (opt[0] && opt[0] != '-')
484 if (!strcmp(opt, "-l")) {
485 opt_request = REQ_VIEW_LOG;
489 if (!strcmp(opt, "-d")) {
490 opt_request = REQ_VIEW_DIFF;
494 if (!strcmp(opt, "-S")) {
495 opt_request = REQ_VIEW_STATUS;
499 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
500 opt_line_number = TRUE;
504 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
505 opt_tab_size = MIN(opt_tab_size, TABSIZE);
509 if (check_option(opt, 'v', "version", OPT_NONE)) {
510 printf("tig version %s\n", VERSION);
514 if (check_option(opt, 'h', "help", OPT_NONE)) {
519 if (!strcmp(opt, "--")) {
524 die("unknown option '%s'\n\n%s", opt, usage);
527 if (!isatty(STDIN_FILENO)) {
528 opt_request = REQ_VIEW_PAGER;
531 } else if (i < argc) {
534 if (opt_request == REQ_VIEW_MAIN)
535 /* XXX: This is vulnerable to the user overriding
536 * options required for the main view parser. */
537 string_copy(opt_cmd, "git log --pretty=raw");
539 string_copy(opt_cmd, "git");
540 buf_size = strlen(opt_cmd);
542 while (buf_size < sizeof(opt_cmd) && i < argc) {
543 opt_cmd[buf_size++] = ' ';
544 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
547 if (buf_size >= sizeof(opt_cmd))
548 die("command too long");
550 opt_cmd[buf_size] = 0;
553 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
561 * Line-oriented content detection.
565 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
568 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
569 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
570 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
586 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
587 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
590 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
594 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
595 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
596 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
597 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
598 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
599 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
600 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
601 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
602 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
603 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
604 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
605 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
606 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
607 LINE(STAT_SECTION, "", COLOR_DEFAULT, COLOR_BLUE, A_BOLD), \
608 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
609 LINE(STAT_STAGED, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
610 LINE(STAT_UNSTAGED,"", COLOR_YELLOW, COLOR_DEFAULT, 0), \
611 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
614 #define LINE(type, line, fg, bg, attr) \
621 const char *name; /* Option name. */
622 int namelen; /* Size of option name. */
623 const char *line; /* The start of line to match. */
624 int linelen; /* Size of string to match. */
625 int fg, bg, attr; /* Color and text attributes for the lines. */
628 static struct line_info line_info[] = {
629 #define LINE(type, line, fg, bg, attr) \
630 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
635 static enum line_type
636 get_line_type(char *line)
638 int linelen = strlen(line);
641 for (type = 0; type < ARRAY_SIZE(line_info); type++)
642 /* Case insensitive search matches Signed-off-by lines better. */
643 if (linelen >= line_info[type].linelen &&
644 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
651 get_line_attr(enum line_type type)
653 assert(type < ARRAY_SIZE(line_info));
654 return COLOR_PAIR(type) | line_info[type].attr;
657 static struct line_info *
658 get_line_info(char *name, int namelen)
662 for (type = 0; type < ARRAY_SIZE(line_info); type++)
663 if (namelen == line_info[type].namelen &&
664 !string_enum_compare(line_info[type].name, name, namelen))
665 return &line_info[type];
673 int default_bg = COLOR_BLACK;
674 int default_fg = COLOR_WHITE;
679 if (use_default_colors() != ERR) {
684 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
685 struct line_info *info = &line_info[type];
686 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
687 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
689 init_pair(type, fg, bg);
697 unsigned int selected:1;
699 void *data; /* User data */
709 enum request request;
710 struct keybinding *next;
713 static struct keybinding default_keybindings[] = {
715 { 'm', REQ_VIEW_MAIN },
716 { 'd', REQ_VIEW_DIFF },
717 { 'l', REQ_VIEW_LOG },
718 { 't', REQ_VIEW_TREE },
719 { 'f', REQ_VIEW_BLOB },
720 { 'p', REQ_VIEW_PAGER },
721 { 'h', REQ_VIEW_HELP },
722 { 'S', REQ_VIEW_STATUS },
724 /* View manipulation */
725 { 'q', REQ_VIEW_CLOSE },
726 { KEY_TAB, REQ_VIEW_NEXT },
727 { KEY_RETURN, REQ_ENTER },
728 { KEY_UP, REQ_PREVIOUS },
729 { KEY_DOWN, REQ_NEXT },
731 /* Cursor navigation */
732 { 'k', REQ_MOVE_UP },
733 { 'j', REQ_MOVE_DOWN },
734 { KEY_HOME, REQ_MOVE_FIRST_LINE },
735 { KEY_END, REQ_MOVE_LAST_LINE },
736 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
737 { ' ', REQ_MOVE_PAGE_DOWN },
738 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
739 { 'b', REQ_MOVE_PAGE_UP },
740 { '-', REQ_MOVE_PAGE_UP },
743 { KEY_IC, REQ_SCROLL_LINE_UP },
744 { KEY_DC, REQ_SCROLL_LINE_DOWN },
745 { 'w', REQ_SCROLL_PAGE_UP },
746 { 's', REQ_SCROLL_PAGE_DOWN },
750 { '?', REQ_SEARCH_BACK },
751 { 'n', REQ_FIND_NEXT },
752 { 'N', REQ_FIND_PREV },
756 { 'z', REQ_STOP_LOADING },
757 { 'v', REQ_SHOW_VERSION },
758 { 'r', REQ_SCREEN_REDRAW },
759 { '.', REQ_TOGGLE_LINENO },
760 { 'g', REQ_TOGGLE_REV_GRAPH },
763 /* Using the ncurses SIGWINCH handler. */
764 { KEY_RESIZE, REQ_SCREEN_RESIZE },
767 #define KEYMAP_INFO \
779 #define KEYMAP_(name) KEYMAP_##name
784 static struct int_map keymap_table[] = {
785 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
790 #define set_keymap(map, name) \
791 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
793 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
796 add_keybinding(enum keymap keymap, enum request request, int key)
798 struct keybinding *keybinding;
800 keybinding = calloc(1, sizeof(*keybinding));
802 die("Failed to allocate keybinding");
804 keybinding->alias = key;
805 keybinding->request = request;
806 keybinding->next = keybindings[keymap];
807 keybindings[keymap] = keybinding;
810 /* Looks for a key binding first in the given map, then in the generic map, and
811 * lastly in the default keybindings. */
813 get_keybinding(enum keymap keymap, int key)
815 struct keybinding *kbd;
818 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
819 if (kbd->alias == key)
822 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
823 if (kbd->alias == key)
826 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
827 if (default_keybindings[i].alias == key)
828 return default_keybindings[i].request;
830 return (enum request) key;
839 static struct key key_table[] = {
840 { "Enter", KEY_RETURN },
842 { "Backspace", KEY_BACKSPACE },
844 { "Escape", KEY_ESC },
845 { "Left", KEY_LEFT },
846 { "Right", KEY_RIGHT },
848 { "Down", KEY_DOWN },
849 { "Insert", KEY_IC },
850 { "Delete", KEY_DC },
852 { "Home", KEY_HOME },
854 { "PageUp", KEY_PPAGE },
855 { "PageDown", KEY_NPAGE },
865 { "F10", KEY_F(10) },
866 { "F11", KEY_F(11) },
867 { "F12", KEY_F(12) },
871 get_key_value(const char *name)
875 for (i = 0; i < ARRAY_SIZE(key_table); i++)
876 if (!strcasecmp(key_table[i].name, name))
877 return key_table[i].value;
879 if (strlen(name) == 1 && isprint(*name))
886 get_key(enum request request)
888 static char buf[BUFSIZ];
889 static char key_char[] = "'X'";
896 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
897 struct keybinding *keybinding = &default_keybindings[i];
901 if (keybinding->request != request)
904 for (key = 0; key < ARRAY_SIZE(key_table); key++)
905 if (key_table[key].value == keybinding->alias)
906 seq = key_table[key].name;
909 keybinding->alias < 127 &&
910 isprint(keybinding->alias)) {
911 key_char[1] = (char) keybinding->alias;
918 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
919 return "Too many keybindings!";
928 * User config file handling.
931 static struct int_map color_map[] = {
932 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
944 #define set_color(color, name) \
945 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
947 static struct int_map attr_map[] = {
948 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
958 #define set_attribute(attr, name) \
959 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
961 static int config_lineno;
962 static bool config_errors;
963 static char *config_msg;
965 /* Wants: object fgcolor bgcolor [attr] */
967 option_color_command(int argc, char *argv[])
969 struct line_info *info;
971 if (argc != 3 && argc != 4) {
972 config_msg = "Wrong number of arguments given to color command";
976 info = get_line_info(argv[0], strlen(argv[0]));
978 config_msg = "Unknown color name";
982 if (set_color(&info->fg, argv[1]) == ERR ||
983 set_color(&info->bg, argv[2]) == ERR) {
984 config_msg = "Unknown color";
988 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
989 config_msg = "Unknown attribute";
996 /* Wants: name = value */
998 option_set_command(int argc, char *argv[])
1001 config_msg = "Wrong number of arguments given to set command";
1005 if (strcmp(argv[1], "=")) {
1006 config_msg = "No value assigned";
1010 if (!strcmp(argv[0], "show-rev-graph")) {
1011 opt_rev_graph = (!strcmp(argv[2], "1") ||
1012 !strcmp(argv[2], "true") ||
1013 !strcmp(argv[2], "yes"));
1017 if (!strcmp(argv[0], "line-number-interval")) {
1018 opt_num_interval = atoi(argv[2]);
1022 if (!strcmp(argv[0], "tab-size")) {
1023 opt_tab_size = atoi(argv[2]);
1027 if (!strcmp(argv[0], "commit-encoding")) {
1028 char *arg = argv[2];
1029 int delimiter = *arg;
1032 switch (delimiter) {
1035 for (arg++, i = 0; arg[i]; i++)
1036 if (arg[i] == delimiter) {
1041 string_copy(opt_encoding, arg);
1046 config_msg = "Unknown variable name";
1050 /* Wants: mode request key */
1052 option_bind_command(int argc, char *argv[])
1054 enum request request;
1059 config_msg = "Wrong number of arguments given to bind command";
1063 if (set_keymap(&keymap, argv[0]) == ERR) {
1064 config_msg = "Unknown key map";
1068 key = get_key_value(argv[1]);
1070 config_msg = "Unknown key";
1074 request = get_request(argv[2]);
1075 if (request == REQ_UNKNOWN) {
1076 config_msg = "Unknown request name";
1080 add_keybinding(keymap, request, key);
1086 set_option(char *opt, char *value)
1093 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1094 argv[argc++] = value;
1101 while (isspace(*value))
1105 if (!strcmp(opt, "color"))
1106 return option_color_command(argc, argv);
1108 if (!strcmp(opt, "set"))
1109 return option_set_command(argc, argv);
1111 if (!strcmp(opt, "bind"))
1112 return option_bind_command(argc, argv);
1114 config_msg = "Unknown option command";
1119 read_option(char *opt, int optlen, char *value, int valuelen)
1124 config_msg = "Internal error";
1126 /* Check for comment markers, since read_properties() will
1127 * only ensure opt and value are split at first " \t". */
1128 optlen = strcspn(opt, "#");
1132 if (opt[optlen] != 0) {
1133 config_msg = "No option value";
1137 /* Look for comment endings in the value. */
1138 int len = strcspn(value, "#");
1140 if (len < valuelen) {
1142 value[valuelen] = 0;
1145 status = set_option(opt, value);
1148 if (status == ERR) {
1149 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1150 config_lineno, optlen, opt, config_msg);
1151 config_errors = TRUE;
1154 /* Always keep going if errors are encountered. */
1161 char *home = getenv("HOME");
1162 char buf[SIZEOF_STR];
1166 config_errors = FALSE;
1168 if (!home || !string_format(buf, "%s/.tigrc", home))
1171 /* It's ok that the file doesn't exist. */
1172 file = fopen(buf, "r");
1176 if (read_properties(file, " \t", read_option) == ERR ||
1177 config_errors == TRUE)
1178 fprintf(stderr, "Errors while loading %s.\n", buf);
1191 /* The display array of active views and the index of the current view. */
1192 static struct view *display[2];
1193 static unsigned int current_view;
1195 /* Reading from the prompt? */
1196 static bool input_mode = FALSE;
1198 #define foreach_displayed_view(view, i) \
1199 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1201 #define displayed_views() (display[1] != NULL ? 2 : 1)
1203 /* Current head and commit ID */
1204 static char ref_blob[SIZEOF_REF] = "";
1205 static char ref_commit[SIZEOF_REF] = "HEAD";
1206 static char ref_head[SIZEOF_REF] = "HEAD";
1209 const char *name; /* View name */
1210 const char *cmd_fmt; /* Default command line format */
1211 const char *cmd_env; /* Command line set via environment */
1212 const char *id; /* Points to either of ref_{head,commit,blob} */
1214 struct view_ops *ops; /* View operations */
1216 enum keymap keymap; /* What keymap does this view have */
1218 char cmd[SIZEOF_STR]; /* Command buffer */
1219 char ref[SIZEOF_REF]; /* Hovered commit reference */
1220 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1222 int height, width; /* The width and height of the main window */
1223 WINDOW *win; /* The main window */
1224 WINDOW *title; /* The title window living below the main window */
1227 unsigned long offset; /* Offset of the window top */
1228 unsigned long lineno; /* Current line number */
1231 char grep[SIZEOF_STR]; /* Search string */
1232 regex_t *regex; /* Pre-compiled regex */
1234 /* If non-NULL, points to the view that opened this view. If this view
1235 * is closed tig will switch back to the parent view. */
1236 struct view *parent;
1239 unsigned long lines; /* Total number of lines */
1240 struct line *line; /* Line index */
1241 unsigned long line_size;/* Total number of allocated lines */
1242 unsigned int digits; /* Number of digits in the lines member. */
1250 /* What type of content being displayed. Used in the title bar. */
1252 /* Open and reads in all view content. */
1253 bool (*open)(struct view *view);
1254 /* Read one line; updates view->line. */
1255 bool (*read)(struct view *view, char *data);
1256 /* Draw one line; @lineno must be < view->height. */
1257 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1258 /* Depending on view, change display based on current line. */
1259 bool (*enter)(struct view *view, struct line *line);
1260 /* Search for regex in a line. */
1261 bool (*grep)(struct view *view, struct line *line);
1263 void (*select)(struct view *view, struct line *line);
1266 static struct view_ops pager_ops;
1267 static struct view_ops main_ops;
1268 static struct view_ops tree_ops;
1269 static struct view_ops blob_ops;
1270 static struct view_ops help_ops;
1271 static struct view_ops status_ops;
1273 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1274 { name, cmd, #env, ref, ops, map}
1276 #define VIEW_(id, name, ops, ref) \
1277 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1280 static struct view views[] = {
1281 VIEW_(MAIN, "main", &main_ops, ref_head),
1282 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1283 VIEW_(LOG, "log", &pager_ops, ref_head),
1284 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1285 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1286 VIEW_(HELP, "help", &help_ops, ""),
1287 VIEW_(PAGER, "pager", &pager_ops, ""),
1288 VIEW_(STATUS, "status", &status_ops, ""),
1291 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1293 #define foreach_view(view, i) \
1294 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1296 #define view_is_displayed(view) \
1297 (view == display[0] || view == display[1])
1300 draw_view_line(struct view *view, unsigned int lineno)
1303 bool selected = (view->offset + lineno == view->lineno);
1306 assert(view_is_displayed(view));
1308 if (view->offset + lineno >= view->lines)
1311 line = &view->line[view->offset + lineno];
1314 line->selected = TRUE;
1315 view->ops->select(view, line);
1316 } else if (line->selected) {
1317 line->selected = FALSE;
1318 wmove(view->win, lineno, 0);
1319 wclrtoeol(view->win);
1322 scrollok(view->win, FALSE);
1323 draw_ok = view->ops->draw(view, line, lineno, selected);
1324 scrollok(view->win, TRUE);
1330 redraw_view_from(struct view *view, int lineno)
1332 assert(0 <= lineno && lineno < view->height);
1334 for (; lineno < view->height; lineno++) {
1335 if (!draw_view_line(view, lineno))
1339 redrawwin(view->win);
1341 wnoutrefresh(view->win);
1343 wrefresh(view->win);
1347 redraw_view(struct view *view)
1350 redraw_view_from(view, 0);
1355 update_view_title(struct view *view)
1357 char buf[SIZEOF_STR];
1358 char state[SIZEOF_STR];
1359 size_t bufpos = 0, statelen = 0;
1361 assert(view_is_displayed(view));
1363 if (view->lines || view->pipe) {
1364 unsigned int view_lines = view->offset + view->height;
1365 unsigned int lines = view->lines
1366 ? MIN(view_lines, view->lines) * 100 / view->lines
1369 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1376 time_t secs = time(NULL) - view->start_time;
1378 /* Three git seconds are a long time ... */
1380 string_format_from(state, &statelen, " %lds", secs);
1384 string_format_from(buf, &bufpos, "[%s]", view->name);
1385 if (*view->ref && bufpos < view->width) {
1386 size_t refsize = strlen(view->ref);
1387 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1389 if (minsize < view->width)
1390 refsize = view->width - minsize + 7;
1391 string_format_from(buf, &bufpos, " %.*s", refsize, view->ref);
1394 if (statelen && bufpos < view->width) {
1395 string_format_from(buf, &bufpos, " %s", state);
1398 if (view == display[current_view])
1399 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1401 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1403 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1404 wclrtoeol(view->title);
1405 wmove(view->title, 0, view->width - 1);
1408 wnoutrefresh(view->title);
1410 wrefresh(view->title);
1414 resize_display(void)
1417 struct view *base = display[0];
1418 struct view *view = display[1] ? display[1] : display[0];
1420 /* Setup window dimensions */
1422 getmaxyx(stdscr, base->height, base->width);
1424 /* Make room for the status window. */
1428 /* Horizontal split. */
1429 view->width = base->width;
1430 view->height = SCALE_SPLIT_VIEW(base->height);
1431 base->height -= view->height;
1433 /* Make room for the title bar. */
1437 /* Make room for the title bar. */
1442 foreach_displayed_view (view, i) {
1444 view->win = newwin(view->height, 0, offset, 0);
1446 die("Failed to create %s view", view->name);
1448 scrollok(view->win, TRUE);
1450 view->title = newwin(1, 0, offset + view->height, 0);
1452 die("Failed to create title window");
1455 wresize(view->win, view->height, view->width);
1456 mvwin(view->win, offset, 0);
1457 mvwin(view->title, offset + view->height, 0);
1460 offset += view->height + 1;
1465 redraw_display(void)
1470 foreach_displayed_view (view, i) {
1472 update_view_title(view);
1477 update_display_cursor(struct view *view)
1479 /* Move the cursor to the right-most column of the cursor line.
1481 * XXX: This could turn out to be a bit expensive, but it ensures that
1482 * the cursor does not jump around. */
1484 wmove(view->win, view->lineno - view->offset, view->width - 1);
1485 wrefresh(view->win);
1493 /* Scrolling backend */
1495 do_scroll_view(struct view *view, int lines)
1497 bool redraw_current_line = FALSE;
1499 /* The rendering expects the new offset. */
1500 view->offset += lines;
1502 assert(0 <= view->offset && view->offset < view->lines);
1505 /* Move current line into the view. */
1506 if (view->lineno < view->offset) {
1507 view->lineno = view->offset;
1508 redraw_current_line = TRUE;
1509 } else if (view->lineno >= view->offset + view->height) {
1510 view->lineno = view->offset + view->height - 1;
1511 redraw_current_line = TRUE;
1514 assert(view->offset <= view->lineno && view->lineno < view->lines);
1516 /* Redraw the whole screen if scrolling is pointless. */
1517 if (view->height < ABS(lines)) {
1521 int line = lines > 0 ? view->height - lines : 0;
1522 int end = line + ABS(lines);
1524 wscrl(view->win, lines);
1526 for (; line < end; line++) {
1527 if (!draw_view_line(view, line))
1531 if (redraw_current_line)
1532 draw_view_line(view, view->lineno - view->offset);
1535 redrawwin(view->win);
1536 wrefresh(view->win);
1540 /* Scroll frontend */
1542 scroll_view(struct view *view, enum request request)
1546 assert(view_is_displayed(view));
1549 case REQ_SCROLL_PAGE_DOWN:
1550 lines = view->height;
1551 case REQ_SCROLL_LINE_DOWN:
1552 if (view->offset + lines > view->lines)
1553 lines = view->lines - view->offset;
1555 if (lines == 0 || view->offset + view->height >= view->lines) {
1556 report("Cannot scroll beyond the last line");
1561 case REQ_SCROLL_PAGE_UP:
1562 lines = view->height;
1563 case REQ_SCROLL_LINE_UP:
1564 if (lines > view->offset)
1565 lines = view->offset;
1568 report("Cannot scroll beyond the first line");
1576 die("request %d not handled in switch", request);
1579 do_scroll_view(view, lines);
1584 move_view(struct view *view, enum request request)
1586 int scroll_steps = 0;
1590 case REQ_MOVE_FIRST_LINE:
1591 steps = -view->lineno;
1594 case REQ_MOVE_LAST_LINE:
1595 steps = view->lines - view->lineno - 1;
1598 case REQ_MOVE_PAGE_UP:
1599 steps = view->height > view->lineno
1600 ? -view->lineno : -view->height;
1603 case REQ_MOVE_PAGE_DOWN:
1604 steps = view->lineno + view->height >= view->lines
1605 ? view->lines - view->lineno - 1 : view->height;
1617 die("request %d not handled in switch", request);
1620 if (steps <= 0 && view->lineno == 0) {
1621 report("Cannot move beyond the first line");
1624 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1625 report("Cannot move beyond the last line");
1629 /* Move the current line */
1630 view->lineno += steps;
1631 assert(0 <= view->lineno && view->lineno < view->lines);
1633 /* Check whether the view needs to be scrolled */
1634 if (view->lineno < view->offset ||
1635 view->lineno >= view->offset + view->height) {
1636 scroll_steps = steps;
1637 if (steps < 0 && -steps > view->offset) {
1638 scroll_steps = -view->offset;
1640 } else if (steps > 0) {
1641 if (view->lineno == view->lines - 1 &&
1642 view->lines > view->height) {
1643 scroll_steps = view->lines - view->offset - 1;
1644 if (scroll_steps >= view->height)
1645 scroll_steps -= view->height - 1;
1650 if (!view_is_displayed(view)) {
1651 view->offset += scroll_steps;
1652 assert(0 <= view->offset && view->offset < view->lines);
1653 view->ops->select(view, &view->line[view->lineno]);
1657 /* Repaint the old "current" line if we be scrolling */
1658 if (ABS(steps) < view->height)
1659 draw_view_line(view, view->lineno - steps - view->offset);
1662 do_scroll_view(view, scroll_steps);
1666 /* Draw the current line */
1667 draw_view_line(view, view->lineno - view->offset);
1669 redrawwin(view->win);
1670 wrefresh(view->win);
1679 static void search_view(struct view *view, enum request request);
1682 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1684 assert(view_is_displayed(view));
1686 if (!view->ops->grep(view, line))
1689 if (lineno - view->offset >= view->height) {
1690 view->offset = lineno;
1691 view->lineno = lineno;
1695 unsigned long old_lineno = view->lineno - view->offset;
1697 view->lineno = lineno;
1698 draw_view_line(view, old_lineno);
1700 draw_view_line(view, view->lineno - view->offset);
1701 redrawwin(view->win);
1702 wrefresh(view->win);
1705 report("Line %ld matches '%s'", lineno + 1, view->grep);
1710 find_next(struct view *view, enum request request)
1712 unsigned long lineno = view->lineno;
1717 report("No previous search");
1719 search_view(view, request);
1729 case REQ_SEARCH_BACK:
1738 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1739 lineno += direction;
1741 /* Note, lineno is unsigned long so will wrap around in which case it
1742 * will become bigger than view->lines. */
1743 for (; lineno < view->lines; lineno += direction) {
1744 struct line *line = &view->line[lineno];
1746 if (find_next_line(view, lineno, line))
1750 report("No match found for '%s'", view->grep);
1754 search_view(struct view *view, enum request request)
1759 regfree(view->regex);
1762 view->regex = calloc(1, sizeof(*view->regex));
1767 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1768 if (regex_err != 0) {
1769 char buf[SIZEOF_STR] = "unknown error";
1771 regerror(regex_err, view->regex, buf, sizeof(buf));
1772 report("Search failed: %s", buf);
1776 string_copy(view->grep, opt_search);
1778 find_next(view, request);
1782 * Incremental updating
1786 end_update(struct view *view)
1790 set_nonblocking_input(FALSE);
1791 if (view->pipe == stdin)
1799 begin_update(struct view *view)
1801 const char *id = view->id;
1807 string_copy(view->cmd, opt_cmd);
1809 /* When running random commands, initially show the
1810 * command in the title. However, it maybe later be
1811 * overwritten if a commit line is selected. */
1812 string_copy(view->ref, view->cmd);
1814 } else if (view == VIEW(REQ_VIEW_TREE)) {
1815 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1816 char path[SIZEOF_STR];
1818 if (strcmp(view->vid, view->id))
1819 opt_path[0] = path[0] = 0;
1820 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1823 if (!string_format(view->cmd, format, id, path))
1827 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1829 if (!string_format(view->cmd, format, id, id, id, id, id))
1832 /* Put the current ref_* value to the view title ref
1833 * member. This is needed by the blob view. Most other
1834 * views sets it automatically after loading because the
1835 * first line is a commit line. */
1836 string_copy(view->ref, id);
1839 /* Special case for the pager view. */
1841 view->pipe = opt_pipe;
1844 view->pipe = popen(view->cmd, "r");
1850 set_nonblocking_input(TRUE);
1855 string_copy_rev(view->vid, id);
1860 for (i = 0; i < view->lines; i++)
1861 if (view->line[i].data)
1862 free(view->line[i].data);
1868 view->start_time = time(NULL);
1873 static struct line *
1874 realloc_lines(struct view *view, size_t line_size)
1876 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1882 view->line_size = line_size;
1887 update_view(struct view *view)
1889 char in_buffer[BUFSIZ];
1890 char out_buffer[BUFSIZ * 2];
1892 /* The number of lines to read. If too low it will cause too much
1893 * redrawing (and possible flickering), if too high responsiveness
1895 unsigned long lines = view->height;
1896 int redraw_from = -1;
1901 /* Only redraw if lines are visible. */
1902 if (view->offset + view->height >= view->lines)
1903 redraw_from = view->lines - view->offset;
1905 /* FIXME: This is probably not perfect for backgrounded views. */
1906 if (!realloc_lines(view, view->lines + lines))
1909 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1910 size_t linelen = strlen(line);
1913 line[linelen - 1] = 0;
1915 if (opt_iconv != ICONV_NONE) {
1917 size_t inlen = linelen;
1919 char *outbuf = out_buffer;
1920 size_t outlen = sizeof(out_buffer);
1924 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
1925 if (ret != (size_t) -1) {
1927 linelen = strlen(out_buffer);
1931 if (!view->ops->read(view, line))
1941 lines = view->lines;
1942 for (digits = 0; lines; digits++)
1945 /* Keep the displayed view in sync with line number scaling. */
1946 if (digits != view->digits) {
1947 view->digits = digits;
1952 if (!view_is_displayed(view))
1955 if (view == VIEW(REQ_VIEW_TREE)) {
1956 /* Clear the view and redraw everything since the tree sorting
1957 * might have rearranged things. */
1960 } else if (redraw_from >= 0) {
1961 /* If this is an incremental update, redraw the previous line
1962 * since for commits some members could have changed when
1963 * loading the main view. */
1964 if (redraw_from > 0)
1967 /* Since revision graph visualization requires knowledge
1968 * about the parent commit, it causes a further one-off
1969 * needed to be redrawn for incremental updates. */
1970 if (redraw_from > 0 && opt_rev_graph)
1973 /* Incrementally draw avoids flickering. */
1974 redraw_view_from(view, redraw_from);
1977 /* Update the title _after_ the redraw so that if the redraw picks up a
1978 * commit reference in view->ref it'll be available here. */
1979 update_view_title(view);
1982 if (ferror(view->pipe)) {
1983 report("Failed to read: %s", strerror(errno));
1986 } else if (feof(view->pipe)) {
1994 report("Allocation failure");
1997 view->ops->read(view, NULL);
2002 static struct line *
2003 add_line_data(struct view *view, void *data, enum line_type type)
2005 struct line *line = &view->line[view->lines++];
2007 memset(line, 0, sizeof(*line));
2014 static struct line *
2015 add_line_text(struct view *view, char *data, enum line_type type)
2018 data = strdup(data);
2020 return data ? add_line_data(view, data, type) : NULL;
2029 OPEN_DEFAULT = 0, /* Use default view switching. */
2030 OPEN_SPLIT = 1, /* Split current view. */
2031 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2032 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2036 open_view(struct view *prev, enum request request, enum open_flags flags)
2038 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2039 bool split = !!(flags & OPEN_SPLIT);
2040 bool reload = !!(flags & OPEN_RELOAD);
2041 struct view *view = VIEW(request);
2042 int nviews = displayed_views();
2043 struct view *base_view = display[0];
2045 if (view == prev && nviews == 1 && !reload) {
2046 report("Already in %s view", view->name);
2050 if (view->ops->open) {
2051 if (!view->ops->open(view)) {
2052 report("Failed to load %s view", view->name);
2056 } else if ((reload || strcmp(view->vid, view->id)) &&
2057 !begin_update(view)) {
2058 report("Failed to load %s view", view->name);
2067 /* Maximize the current view. */
2068 memset(display, 0, sizeof(display));
2070 display[current_view] = view;
2073 /* Resize the view when switching between split- and full-screen,
2074 * or when switching between two different full-screen views. */
2075 if (nviews != displayed_views() ||
2076 (nviews == 1 && base_view != display[0]))
2079 if (split && prev->lineno - prev->offset >= prev->height) {
2080 /* Take the title line into account. */
2081 int lines = prev->lineno - prev->offset - prev->height + 1;
2083 /* Scroll the view that was split if the current line is
2084 * outside the new limited view. */
2085 do_scroll_view(prev, lines);
2088 if (prev && view != prev) {
2089 if (split && !backgrounded) {
2090 /* "Blur" the previous view. */
2091 update_view_title(prev);
2094 view->parent = prev;
2097 if (view->pipe && view->lines == 0) {
2098 /* Clear the old view and let the incremental updating refill
2107 /* If the view is backgrounded the above calls to report()
2108 * won't redraw the view title. */
2110 update_view_title(view);
2115 * User request switch noodle
2119 view_driver(struct view *view, enum request request)
2126 case REQ_MOVE_PAGE_UP:
2127 case REQ_MOVE_PAGE_DOWN:
2128 case REQ_MOVE_FIRST_LINE:
2129 case REQ_MOVE_LAST_LINE:
2130 move_view(view, request);
2133 case REQ_SCROLL_LINE_DOWN:
2134 case REQ_SCROLL_LINE_UP:
2135 case REQ_SCROLL_PAGE_DOWN:
2136 case REQ_SCROLL_PAGE_UP:
2137 scroll_view(view, request);
2142 report("No file chosen, press %s to open tree view",
2143 get_key(REQ_VIEW_TREE));
2146 open_view(view, request, OPEN_DEFAULT);
2149 case REQ_VIEW_PAGER:
2150 if (!VIEW(REQ_VIEW_PAGER)->lines) {
2151 report("No pager content, press %s to run command from prompt",
2152 get_key(REQ_PROMPT));
2155 open_view(view, request, OPEN_DEFAULT);
2163 case REQ_VIEW_STATUS:
2164 open_view(view, request, OPEN_DEFAULT);
2169 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2171 if ((view == VIEW(REQ_VIEW_DIFF) &&
2172 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2173 (view == VIEW(REQ_VIEW_BLOB) &&
2174 view->parent == VIEW(REQ_VIEW_TREE))) {
2175 view = view->parent;
2176 move_view(view, request);
2177 if (view_is_displayed(view))
2178 update_view_title(view);
2180 move_view(view, request);
2187 report("Nothing to enter");
2190 return view->ops->enter(view, &view->line[view->lineno]);
2194 int nviews = displayed_views();
2195 int next_view = (current_view + 1) % nviews;
2197 if (next_view == current_view) {
2198 report("Only one view is displayed");
2202 current_view = next_view;
2203 /* Blur out the title of the previous view. */
2204 update_view_title(view);
2208 case REQ_TOGGLE_LINENO:
2209 opt_line_number = !opt_line_number;
2213 case REQ_TOGGLE_REV_GRAPH:
2214 opt_rev_graph = !opt_rev_graph;
2219 /* Always reload^Wrerun commands from the prompt. */
2220 open_view(view, opt_request, OPEN_RELOAD);
2224 case REQ_SEARCH_BACK:
2225 search_view(view, request);
2230 find_next(view, request);
2233 case REQ_STOP_LOADING:
2234 for (i = 0; i < ARRAY_SIZE(views); i++) {
2237 report("Stopped loading the %s view", view->name),
2242 case REQ_SHOW_VERSION:
2243 report("%s (built %s)", VERSION, __DATE__);
2246 case REQ_SCREEN_RESIZE:
2249 case REQ_SCREEN_REDRAW:
2257 case REQ_VIEW_CLOSE:
2258 /* XXX: Mark closed views by letting view->parent point to the
2259 * view itself. Parents to closed view should never be
2262 view->parent->parent != view->parent) {
2263 memset(display, 0, sizeof(display));
2265 display[current_view] = view->parent;
2266 view->parent = view;
2276 /* An unknown key will show most commonly used commands. */
2277 report("Unknown key, press 'h' for help");
2290 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2292 char *text = line->data;
2293 enum line_type type = line->type;
2294 int textlen = strlen(text);
2297 wmove(view->win, lineno, 0);
2301 wchgat(view->win, -1, 0, type, NULL);
2304 attr = get_line_attr(type);
2305 wattrset(view->win, attr);
2307 if (opt_line_number || opt_tab_size < TABSIZE) {
2308 static char spaces[] = " ";
2309 int col_offset = 0, col = 0;
2311 if (opt_line_number) {
2312 unsigned long real_lineno = view->offset + lineno + 1;
2314 if (real_lineno == 1 ||
2315 (real_lineno % opt_num_interval) == 0) {
2316 wprintw(view->win, "%.*d", view->digits, real_lineno);
2319 waddnstr(view->win, spaces,
2320 MIN(view->digits, STRING_SIZE(spaces)));
2322 waddstr(view->win, ": ");
2323 col_offset = view->digits + 2;
2326 while (text && col_offset + col < view->width) {
2327 int cols_max = view->width - col_offset - col;
2331 if (*text == '\t') {
2333 assert(sizeof(spaces) > TABSIZE);
2335 cols = opt_tab_size - (col % opt_tab_size);
2338 text = strchr(text, '\t');
2339 cols = line ? text - pos : strlen(pos);
2342 waddnstr(view->win, pos, MIN(cols, cols_max));
2347 int col = 0, pos = 0;
2349 for (; pos < textlen && col < view->width; pos++, col++)
2350 if (text[pos] == '\t')
2351 col += TABSIZE - (col % TABSIZE) - 1;
2353 waddnstr(view->win, text, pos);
2360 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2362 char refbuf[SIZEOF_STR];
2366 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2369 pipe = popen(refbuf, "r");
2373 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2374 ref = chomp_string(ref);
2380 /* This is the only fatal call, since it can "corrupt" the buffer. */
2381 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2388 add_pager_refs(struct view *view, struct line *line)
2390 char buf[SIZEOF_STR];
2391 char *commit_id = line->data + STRING_SIZE("commit ");
2393 size_t bufpos = 0, refpos = 0;
2394 const char *sep = "Refs: ";
2395 bool is_tag = FALSE;
2397 assert(line->type == LINE_COMMIT);
2399 refs = get_refs(commit_id);
2401 if (view == VIEW(REQ_VIEW_DIFF))
2402 goto try_add_describe_ref;
2407 struct ref *ref = refs[refpos];
2408 char *fmt = ref->tag ? "%s[%s]" :
2409 ref->remote ? "%s<%s>" : "%s%s";
2411 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2416 } while (refs[refpos++]->next);
2418 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2419 try_add_describe_ref:
2420 /* Add <tag>-g<commit_id> "fake" reference. */
2421 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2428 if (!realloc_lines(view, view->line_size + 1))
2431 add_line_text(view, buf, LINE_PP_REFS);
2435 pager_read(struct view *view, char *data)
2442 line = add_line_text(view, data, get_line_type(data));
2446 if (line->type == LINE_COMMIT &&
2447 (view == VIEW(REQ_VIEW_DIFF) ||
2448 view == VIEW(REQ_VIEW_LOG)))
2449 add_pager_refs(view, line);
2455 pager_enter(struct view *view, struct line *line)
2459 if (line->type == LINE_COMMIT &&
2460 (view == VIEW(REQ_VIEW_LOG) ||
2461 view == VIEW(REQ_VIEW_PAGER))) {
2462 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2466 /* Always scroll the view even if it was split. That way
2467 * you can use Enter to scroll through the log view and
2468 * split open each commit diff. */
2469 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2471 /* FIXME: A minor workaround. Scrolling the view will call report("")
2472 * but if we are scrolling a non-current view this won't properly
2473 * update the view title. */
2475 update_view_title(view);
2481 pager_grep(struct view *view, struct line *line)
2484 char *text = line->data;
2489 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2496 pager_select(struct view *view, struct line *line)
2498 if (line->type == LINE_COMMIT) {
2499 char *text = line->data + STRING_SIZE("commit ");
2501 if (view != VIEW(REQ_VIEW_PAGER))
2502 string_copy_rev(view->ref, text);
2503 string_copy_rev(ref_commit, text);
2507 static struct view_ops pager_ops = {
2523 help_open(struct view *view)
2526 int lines = ARRAY_SIZE(req_info) + 2;
2529 if (view->lines > 0)
2532 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2533 if (!req_info[i].request)
2536 view->line = calloc(lines, sizeof(*view->line));
2540 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2542 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2545 if (!req_info[i].request) {
2546 add_line_text(view, "", LINE_DEFAULT);
2547 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2551 key = get_key(req_info[i].request);
2552 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2555 add_line_text(view, buf, LINE_DEFAULT);
2561 static struct view_ops help_ops = {
2576 /* Parse output from git-ls-tree(1):
2578 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2579 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2580 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2581 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2584 #define SIZEOF_TREE_ATTR \
2585 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2587 #define TREE_UP_FORMAT "040000 tree %s\t.."
2590 tree_compare_entry(enum line_type type1, char *name1,
2591 enum line_type type2, char *name2)
2593 if (type1 != type2) {
2594 if (type1 == LINE_TREE_DIR)
2599 return strcmp(name1, name2);
2603 tree_read(struct view *view, char *text)
2605 size_t textlen = text ? strlen(text) : 0;
2606 char buf[SIZEOF_STR];
2608 enum line_type type;
2609 bool first_read = view->lines == 0;
2611 if (textlen <= SIZEOF_TREE_ATTR)
2614 type = text[STRING_SIZE("100644 ")] == 't'
2615 ? LINE_TREE_DIR : LINE_TREE_FILE;
2618 /* Add path info line */
2619 if (!string_format(buf, "Directory path /%s", opt_path) ||
2620 !realloc_lines(view, view->line_size + 1) ||
2621 !add_line_text(view, buf, LINE_DEFAULT))
2624 /* Insert "link" to parent directory. */
2626 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2627 !realloc_lines(view, view->line_size + 1) ||
2628 !add_line_text(view, buf, LINE_TREE_DIR))
2633 /* Strip the path part ... */
2635 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2636 size_t striplen = strlen(opt_path);
2637 char *path = text + SIZEOF_TREE_ATTR;
2639 if (pathlen > striplen)
2640 memmove(path, path + striplen,
2641 pathlen - striplen + 1);
2644 /* Skip "Directory ..." and ".." line. */
2645 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2646 struct line *line = &view->line[pos];
2647 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2648 char *path2 = text + SIZEOF_TREE_ATTR;
2649 int cmp = tree_compare_entry(line->type, path1, type, path2);
2654 text = strdup(text);
2658 if (view->lines > pos)
2659 memmove(&view->line[pos + 1], &view->line[pos],
2660 (view->lines - pos) * sizeof(*line));
2662 line = &view->line[pos];
2669 if (!add_line_text(view, text, type))
2672 /* Move the current line to the first tree entry. */
2680 tree_enter(struct view *view, struct line *line)
2682 enum open_flags flags;
2683 enum request request;
2685 switch (line->type) {
2687 /* Depending on whether it is a subdir or parent (updir?) link
2688 * mangle the path buffer. */
2689 if (line == &view->line[1] && *opt_path) {
2690 size_t path_len = strlen(opt_path);
2691 char *dirsep = opt_path + path_len - 1;
2693 while (dirsep > opt_path && dirsep[-1] != '/')
2699 size_t pathlen = strlen(opt_path);
2700 size_t origlen = pathlen;
2701 char *data = line->data;
2702 char *basename = data + SIZEOF_TREE_ATTR;
2704 if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
2705 opt_path[origlen] = 0;
2710 /* Trees and subtrees share the same ID, so they are not not
2711 * unique like blobs. */
2712 flags = OPEN_RELOAD;
2713 request = REQ_VIEW_TREE;
2716 case LINE_TREE_FILE:
2717 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2718 request = REQ_VIEW_BLOB;
2725 open_view(view, request, flags);
2731 tree_select(struct view *view, struct line *line)
2733 char *text = line->data + STRING_SIZE("100644 blob ");
2735 if (line->type == LINE_TREE_FILE) {
2736 string_copy_rev(ref_blob, text);
2738 } else if (line->type != LINE_TREE_DIR) {
2742 string_copy_rev(view->ref, text);
2745 static struct view_ops tree_ops = {
2756 blob_read(struct view *view, char *line)
2758 return add_line_text(view, line, LINE_DEFAULT);
2761 static struct view_ops blob_ops = {
2780 char rev[SIZEOF_REV];
2784 char rev[SIZEOF_REV];
2786 char name[SIZEOF_STR];
2789 /* Get fields from the diff line:
2790 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
2793 status_get_diff(struct status *file, char *buf, size_t bufsize)
2795 char *old_mode = buf + 1;
2796 char *new_mode = buf + 8;
2797 char *old_rev = buf + 15;
2798 char *new_rev = buf + 56;
2799 char *status = buf + 97;
2801 if (bufsize != 99 ||
2802 old_mode[-1] != ':' ||
2803 new_mode[-1] != ' ' ||
2804 old_rev[-1] != ' ' ||
2805 new_rev[-1] != ' ' ||
2809 file->status = *status;
2811 string_copy_rev(file->old.rev, old_rev);
2812 string_copy_rev(file->new.rev, new_rev);
2814 file->old.mode = strtoul(old_mode, NULL, 8);
2815 file->new.mode = strtoul(new_mode, NULL, 8);
2823 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
2825 struct status *file = NULL;
2826 char buf[SIZEOF_STR * 4];
2830 pipe = popen(cmd, "r");
2834 add_line_data(view, NULL, type);
2836 while (!feof(pipe) && !ferror(pipe)) {
2840 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
2843 bufsize += readsize;
2845 /* Process while we have NUL chars. */
2846 while ((sep = memchr(buf, 0, bufsize))) {
2847 size_t sepsize = sep - buf + 1;
2850 if (!realloc_lines(view, view->line_size + 1))
2853 file = calloc(1, sizeof(*file));
2857 add_line_data(view, file, type);
2860 /* Parse diff info part. */
2864 } else if (!file->status) {
2865 if (!status_get_diff(file, buf, sepsize))
2869 memmove(buf, sep + 1, bufsize);
2871 sep = memchr(buf, 0, bufsize);
2874 sepsize = sep - buf + 1;
2877 /* git-ls-files just delivers a NUL separated
2878 * list of file names similar to the second half
2879 * of the git-diff-* output. */
2880 string_ncopy(file->name, buf, sepsize);
2882 memmove(buf, sep + 1, bufsize);
2893 if (!view->line[view->lines - 1].data)
2894 add_line_data(view, NULL, LINE_STAT_NONE);
2900 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --cached HEAD"
2901 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
2902 #define STATUS_LIST_OTHER_CMD \
2903 "_git_exclude=$(git rev-parse --git-dir)/info/exclude;" \
2904 "test -f \"$_git_exclude\" && exclude=\"--exclude-from=$_git_exclude\";" \
2905 "git ls-files -z --others --exclude-per-directory=.gitignore \"$exclude\"" \
2907 /* First parse staged info using git-diff-index(1), then parse unstaged
2908 * info using git-diff-files(1), and finally untracked files using
2909 * git-ls-files(1). */
2911 status_open(struct view *view)
2915 for (i = 0; i < view->lines; i++)
2916 free(view->line[i].data);
2918 view->lines = view->line_size = 0;
2921 if (!realloc_lines(view, view->line_size + 6))
2924 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
2925 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
2926 !status_run(view, STATUS_LIST_OTHER_CMD, FALSE, LINE_STAT_UNTRACKED))
2933 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2935 struct status *status = line->data;
2937 wmove(view->win, lineno, 0);
2940 wattrset(view->win, get_line_attr(LINE_CURSOR));
2941 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
2943 } else if (!status && line->type != LINE_STAT_NONE) {
2944 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
2945 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
2948 wattrset(view->win, get_line_attr(line->type));
2954 switch (line->type) {
2955 case LINE_STAT_STAGED:
2956 text = "Changes to be committed:";
2959 case LINE_STAT_UNSTAGED:
2960 text = "Changed but not updated:";
2963 case LINE_STAT_UNTRACKED:
2964 text = "Untracked files:";
2967 case LINE_STAT_NONE:
2968 text = " (no files)";
2975 waddstr(view->win, text);
2979 waddch(view->win, status->status);
2981 wattrset(view->win, A_NORMAL);
2982 wmove(view->win, lineno, 4);
2983 waddstr(view->win, status->name);
2989 status_enter(struct view *view, struct line *line)
2991 struct status *status = line->data;
2993 char buf[SIZEOF_STR];
3001 switch (line->type) {
3002 case LINE_STAT_STAGED:
3003 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3008 cmd = "git update-index -z --index-info";
3011 case LINE_STAT_UNSTAGED:
3012 case LINE_STAT_UNTRACKED:
3013 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3015 cmd = "git update-index -z --add --remove --stdin";
3022 pipe = popen(cmd, "w");
3026 while (!ferror(pipe) && written < bufsize) {
3027 written += fwrite(buf + written, 1, bufsize - written, pipe);
3032 if (written != bufsize)
3035 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3040 status_select(struct view *view, struct line *line)
3044 switch (line->type) {
3045 case LINE_STAT_STAGED:
3046 text = "Press Enter to unstage file for commit";
3049 case LINE_STAT_UNSTAGED:
3050 text = "Press Enter to stage file for commit ";
3053 case LINE_STAT_UNTRACKED:
3054 text = "Press Enter to stage file for addition";
3057 case LINE_STAT_NONE:
3064 string_ncopy(view->ref, text, strlen(text));
3068 status_grep(struct view *view, struct line *line)
3070 struct status *status = line->data;
3071 enum { S_STATUS, S_NAME, S_END } state;
3078 for (state = S_STATUS; state < S_END; state++) {
3082 case S_NAME: text = status->name; break;
3084 buf[0] = status->status;
3092 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3099 static struct view_ops status_ops = {
3115 char id[SIZEOF_REV]; /* SHA1 ID. */
3116 char title[128]; /* First line of the commit message. */
3117 char author[75]; /* Author of the commit. */
3118 struct tm time; /* Date from the author ident. */
3119 struct ref **refs; /* Repository references. */
3120 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3121 size_t graph_size; /* The width of the graph array. */
3124 /* Size of rev graph with no "padding" columns */
3125 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3128 struct rev_graph *prev, *next, *parents;
3129 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3131 struct commit *commit;
3135 /* Parents of the commit being visualized. */
3136 static struct rev_graph graph_parents[4];
3138 /* The current stack of revisions on the graph. */
3139 static struct rev_graph graph_stacks[4] = {
3140 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3141 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3142 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3143 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3147 graph_parent_is_merge(struct rev_graph *graph)
3149 return graph->parents->size > 1;
3153 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3155 struct commit *commit = graph->commit;
3157 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3158 commit->graph[commit->graph_size++] = symbol;
3162 done_rev_graph(struct rev_graph *graph)
3164 if (graph_parent_is_merge(graph) &&
3165 graph->pos < graph->size - 1 &&
3166 graph->next->size == graph->size + graph->parents->size - 1) {
3167 size_t i = graph->pos + graph->parents->size - 1;
3169 graph->commit->graph_size = i * 2;
3170 while (i < graph->next->size - 1) {
3171 append_to_rev_graph(graph, ' ');
3172 append_to_rev_graph(graph, '\\');
3177 graph->size = graph->pos = 0;
3178 graph->commit = NULL;
3179 memset(graph->parents, 0, sizeof(*graph->parents));
3183 push_rev_graph(struct rev_graph *graph, char *parent)
3187 /* "Collapse" duplicate parents lines.
3189 * FIXME: This needs to also update update the drawn graph but
3190 * for now it just serves as a method for pruning graph lines. */
3191 for (i = 0; i < graph->size; i++)
3192 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3195 if (graph->size < SIZEOF_REVITEMS) {
3196 string_ncopy(graph->rev[graph->size++], parent, SIZEOF_REV);
3201 get_rev_graph_symbol(struct rev_graph *graph)
3205 if (graph->parents->size == 0)
3206 symbol = REVGRAPH_INIT;
3207 else if (graph_parent_is_merge(graph))
3208 symbol = REVGRAPH_MERGE;
3209 else if (graph->pos >= graph->size)
3210 symbol = REVGRAPH_BRANCH;
3212 symbol = REVGRAPH_COMMIT;
3218 draw_rev_graph(struct rev_graph *graph)
3221 chtype separator, line;
3223 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3224 static struct rev_filler fillers[] = {
3225 { ' ', REVGRAPH_LINE },
3230 chtype symbol = get_rev_graph_symbol(graph);
3231 struct rev_filler *filler;
3234 filler = &fillers[DEFAULT];
3236 for (i = 0; i < graph->pos; i++) {
3237 append_to_rev_graph(graph, filler->line);
3238 if (graph_parent_is_merge(graph->prev) &&
3239 graph->prev->pos == i)
3240 filler = &fillers[RSHARP];
3242 append_to_rev_graph(graph, filler->separator);
3245 /* Place the symbol for this revision. */
3246 append_to_rev_graph(graph, symbol);
3248 if (graph->prev->size > graph->size)
3249 filler = &fillers[RDIAG];
3251 filler = &fillers[DEFAULT];
3255 for (; i < graph->size; i++) {
3256 append_to_rev_graph(graph, filler->separator);
3257 append_to_rev_graph(graph, filler->line);
3258 if (graph_parent_is_merge(graph->prev) &&
3259 i < graph->prev->pos + graph->parents->size)
3260 filler = &fillers[RSHARP];
3261 if (graph->prev->size > graph->size)
3262 filler = &fillers[LDIAG];
3265 if (graph->prev->size > graph->size) {
3266 append_to_rev_graph(graph, filler->separator);
3267 if (filler->line != ' ')
3268 append_to_rev_graph(graph, filler->line);
3272 /* Prepare the next rev graph */
3274 prepare_rev_graph(struct rev_graph *graph)
3278 /* First, traverse all lines of revisions up to the active one. */
3279 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
3280 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
3283 push_rev_graph(graph->next, graph->rev[graph->pos]);
3286 /* Interleave the new revision parent(s). */
3287 for (i = 0; i < graph->parents->size; i++)
3288 push_rev_graph(graph->next, graph->parents->rev[i]);
3290 /* Lastly, put any remaining revisions. */
3291 for (i = graph->pos + 1; i < graph->size; i++)
3292 push_rev_graph(graph->next, graph->rev[i]);
3296 update_rev_graph(struct rev_graph *graph)
3298 /* If this is the finalizing update ... */
3300 prepare_rev_graph(graph);
3302 /* Graph visualization needs a one rev look-ahead,
3303 * so the first update doesn't visualize anything. */
3304 if (!graph->prev->commit)
3307 draw_rev_graph(graph->prev);
3308 done_rev_graph(graph->prev->prev);
3317 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3319 char buf[DATE_COLS + 1];
3320 struct commit *commit = line->data;
3321 enum line_type type;
3327 if (!*commit->author)
3330 wmove(view->win, lineno, col);
3334 wattrset(view->win, get_line_attr(type));
3335 wchgat(view->win, -1, 0, type, NULL);
3338 type = LINE_MAIN_COMMIT;
3339 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3342 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
3343 waddnstr(view->win, buf, timelen);
3344 waddstr(view->win, " ");
3347 wmove(view->win, lineno, col);
3348 if (type != LINE_CURSOR)
3349 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3352 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
3354 authorlen = strlen(commit->author);
3355 if (authorlen > AUTHOR_COLS - 2) {
3356 authorlen = AUTHOR_COLS - 2;
3362 waddnstr(view->win, commit->author, authorlen);
3363 if (type != LINE_CURSOR)
3364 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
3365 waddch(view->win, '~');
3367 waddstr(view->win, commit->author);
3371 if (type != LINE_CURSOR)
3372 wattrset(view->win, A_NORMAL);
3374 if (opt_rev_graph && commit->graph_size) {
3377 wmove(view->win, lineno, col);
3378 /* Using waddch() instead of waddnstr() ensures that
3379 * they'll be rendered correctly for the cursor line. */
3380 for (i = 0; i < commit->graph_size; i++)
3381 waddch(view->win, commit->graph[i]);
3383 waddch(view->win, ' ');
3384 col += commit->graph_size + 1;
3387 wmove(view->win, lineno, col);
3393 if (type == LINE_CURSOR)
3395 else if (commit->refs[i]->tag)
3396 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
3397 else if (commit->refs[i]->remote)
3398 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
3400 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
3401 waddstr(view->win, "[");
3402 waddstr(view->win, commit->refs[i]->name);
3403 waddstr(view->win, "]");
3404 if (type != LINE_CURSOR)
3405 wattrset(view->win, A_NORMAL);
3406 waddstr(view->win, " ");
3407 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
3408 } while (commit->refs[i++]->next);
3411 if (type != LINE_CURSOR)
3412 wattrset(view->win, get_line_attr(type));
3415 int titlelen = strlen(commit->title);
3417 if (col + titlelen > view->width)
3418 titlelen = view->width - col;
3420 waddnstr(view->win, commit->title, titlelen);
3426 /* Reads git log --pretty=raw output and parses it into the commit struct. */
3428 main_read(struct view *view, char *line)
3430 static struct rev_graph *graph = graph_stacks;
3431 enum line_type type;
3432 struct commit *commit;
3435 update_rev_graph(graph);
3439 type = get_line_type(line);
3440 if (type == LINE_COMMIT) {
3441 commit = calloc(1, sizeof(struct commit));
3445 string_copy_rev(commit->id, line + STRING_SIZE("commit "));
3446 commit->refs = get_refs(commit->id);
3447 graph->commit = commit;
3448 add_line_data(view, commit, LINE_MAIN_COMMIT);
3454 commit = view->line[view->lines - 1].data;
3458 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
3463 /* Parse author lines where the name may be empty:
3464 * author <email@address.tld> 1138474660 +0100
3466 char *ident = line + STRING_SIZE("author ");
3467 char *nameend = strchr(ident, '<');
3468 char *emailend = strchr(ident, '>');
3470 if (!nameend || !emailend)
3473 update_rev_graph(graph);
3474 graph = graph->next;
3476 *nameend = *emailend = 0;
3477 ident = chomp_string(ident);
3479 ident = chomp_string(nameend + 1);
3484 string_copy(commit->author, ident);
3486 /* Parse epoch and timezone */
3487 if (emailend[1] == ' ') {
3488 char *secs = emailend + 2;
3489 char *zone = strchr(secs, ' ');
3490 time_t time = (time_t) atol(secs);
3492 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
3496 tz = ('0' - zone[1]) * 60 * 60 * 10;
3497 tz += ('0' - zone[2]) * 60 * 60;
3498 tz += ('0' - zone[3]) * 60;
3499 tz += ('0' - zone[4]) * 60;
3507 gmtime_r(&time, &commit->time);
3512 /* Fill in the commit title if it has not already been set. */
3513 if (commit->title[0])
3516 /* Require titles to start with a non-space character at the
3517 * offset used by git log. */
3518 if (strncmp(line, " ", 4))
3521 /* Well, if the title starts with a whitespace character,
3522 * try to be forgiving. Otherwise we end up with no title. */
3523 while (isspace(*line))
3527 /* FIXME: More graceful handling of titles; append "..." to
3528 * shortened titles, etc. */
3530 string_copy(commit->title, line);
3537 main_enter(struct view *view, struct line *line)
3539 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3541 open_view(view, REQ_VIEW_DIFF, flags);
3546 main_grep(struct view *view, struct line *line)
3548 struct commit *commit = line->data;
3549 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
3550 char buf[DATE_COLS + 1];
3553 for (state = S_TITLE; state < S_END; state++) {
3557 case S_TITLE: text = commit->title; break;
3558 case S_AUTHOR: text = commit->author; break;
3560 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
3569 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3577 main_select(struct view *view, struct line *line)
3579 struct commit *commit = line->data;
3581 string_copy_rev(view->ref, commit->id);
3582 string_copy_rev(ref_commit, view->ref);
3585 static struct view_ops main_ops = {
3597 * Unicode / UTF-8 handling
3599 * NOTE: Much of the following code for dealing with unicode is derived from
3600 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
3601 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
3604 /* I've (over)annotated a lot of code snippets because I am not entirely
3605 * confident that the approach taken by this small UTF-8 interface is correct.
3609 unicode_width(unsigned long c)
3612 (c <= 0x115f /* Hangul Jamo */
3615 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
3617 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
3618 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
3619 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
3620 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
3621 || (c >= 0xffe0 && c <= 0xffe6)
3622 || (c >= 0x20000 && c <= 0x2fffd)
3623 || (c >= 0x30000 && c <= 0x3fffd)))
3629 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
3630 * Illegal bytes are set one. */
3631 static const unsigned char utf8_bytes[256] = {
3632 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,
3633 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,
3634 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,
3635 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,
3636 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,
3637 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,
3638 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,
3639 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,
3642 /* Decode UTF-8 multi-byte representation into a unicode character. */
3643 static inline unsigned long
3644 utf8_to_unicode(const char *string, size_t length)
3646 unsigned long unicode;
3650 unicode = string[0];
3653 unicode = (string[0] & 0x1f) << 6;
3654 unicode += (string[1] & 0x3f);
3657 unicode = (string[0] & 0x0f) << 12;
3658 unicode += ((string[1] & 0x3f) << 6);
3659 unicode += (string[2] & 0x3f);
3662 unicode = (string[0] & 0x0f) << 18;
3663 unicode += ((string[1] & 0x3f) << 12);
3664 unicode += ((string[2] & 0x3f) << 6);
3665 unicode += (string[3] & 0x3f);
3668 unicode = (string[0] & 0x0f) << 24;
3669 unicode += ((string[1] & 0x3f) << 18);
3670 unicode += ((string[2] & 0x3f) << 12);
3671 unicode += ((string[3] & 0x3f) << 6);
3672 unicode += (string[4] & 0x3f);
3675 unicode = (string[0] & 0x01) << 30;
3676 unicode += ((string[1] & 0x3f) << 24);
3677 unicode += ((string[2] & 0x3f) << 18);
3678 unicode += ((string[3] & 0x3f) << 12);
3679 unicode += ((string[4] & 0x3f) << 6);
3680 unicode += (string[5] & 0x3f);
3683 die("Invalid unicode length");
3686 /* Invalid characters could return the special 0xfffd value but NUL
3687 * should be just as good. */
3688 return unicode > 0xffff ? 0 : unicode;
3691 /* Calculates how much of string can be shown within the given maximum width
3692 * and sets trimmed parameter to non-zero value if all of string could not be
3695 * Additionally, adds to coloffset how many many columns to move to align with
3696 * the expected position. Takes into account how multi-byte and double-width
3697 * characters will effect the cursor position.
3699 * Returns the number of bytes to output from string to satisfy max_width. */
3701 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3703 const char *start = string;
3704 const char *end = strchr(string, '\0');
3710 while (string < end) {
3711 int c = *(unsigned char *) string;
3712 unsigned char bytes = utf8_bytes[c];
3714 unsigned long unicode;
3716 if (string + bytes > end)
3719 /* Change representation to figure out whether
3720 * it is a single- or double-width character. */
3722 unicode = utf8_to_unicode(string, bytes);
3723 /* FIXME: Graceful handling of invalid unicode character. */
3727 ucwidth = unicode_width(unicode);
3729 if (width > max_width) {
3734 /* The column offset collects the differences between the
3735 * number of bytes encoding a character and the number of
3736 * columns will be used for rendering said character.
3738 * So if some character A is encoded in 2 bytes, but will be
3739 * represented on the screen using only 1 byte this will and up
3740 * adding 1 to the multi-byte column offset.
3742 * Assumes that no double-width character can be encoding in
3743 * less than two bytes. */
3744 if (bytes > ucwidth)
3745 mbwidth += bytes - ucwidth;
3750 *coloffset += mbwidth;
3752 return string - start;
3760 /* Whether or not the curses interface has been initialized. */
3761 static bool cursed = FALSE;
3763 /* The status window is used for polling keystrokes. */
3764 static WINDOW *status_win;
3766 static bool status_empty = TRUE;
3768 /* Update status and title window. */
3770 report(const char *msg, ...)
3772 struct view *view = display[current_view];
3777 if (!status_empty || *msg) {
3780 va_start(args, msg);
3782 wmove(status_win, 0, 0);
3784 vwprintw(status_win, msg, args);
3785 status_empty = FALSE;
3787 status_empty = TRUE;
3789 wclrtoeol(status_win);
3790 wrefresh(status_win);
3795 update_view_title(view);
3796 update_display_cursor(view);
3799 /* Controls when nodelay should be in effect when polling user input. */
3801 set_nonblocking_input(bool loading)
3803 static unsigned int loading_views;
3805 if ((loading == FALSE && loading_views-- == 1) ||
3806 (loading == TRUE && loading_views++ == 0))
3807 nodelay(status_win, loading);
3815 /* Initialize the curses library */
3816 if (isatty(STDIN_FILENO)) {
3817 cursed = !!initscr();
3819 /* Leave stdin and stdout alone when acting as a pager. */
3820 FILE *io = fopen("/dev/tty", "r+");
3823 die("Failed to open /dev/tty");
3824 cursed = !!newterm(NULL, io, io);
3828 die("Failed to initialize curses");
3830 nonl(); /* Tell curses not to do NL->CR/NL on output */
3831 cbreak(); /* Take input chars one at a time, no wait for \n */
3832 noecho(); /* Don't echo input */
3833 leaveok(stdscr, TRUE);
3838 getmaxyx(stdscr, y, x);
3839 status_win = newwin(1, 0, y - 1, 0);
3841 die("Failed to create status window");
3843 /* Enable keyboard mapping */
3844 keypad(status_win, TRUE);
3845 wbkgdset(status_win, get_line_attr(LINE_STATUS));
3849 read_prompt(const char *prompt)
3851 enum { READING, STOP, CANCEL } status = READING;
3852 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
3855 while (status == READING) {
3861 foreach_view (view, i)
3866 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
3867 wclrtoeol(status_win);
3869 /* Refresh, accept single keystroke of input */
3870 key = wgetch(status_win);
3875 status = pos ? STOP : CANCEL;
3893 if (pos >= sizeof(buf)) {
3894 report("Input string too long");
3899 buf[pos++] = (char) key;
3903 /* Clear the status window */
3904 status_empty = FALSE;
3907 if (status == CANCEL)
3916 * Repository references
3919 static struct ref *refs;
3920 static size_t refs_size;
3922 /* Id <-> ref store */
3923 static struct ref ***id_refs;
3924 static size_t id_refs_size;
3926 static struct ref **
3929 struct ref ***tmp_id_refs;
3930 struct ref **ref_list = NULL;
3931 size_t ref_list_size = 0;
3934 for (i = 0; i < id_refs_size; i++)
3935 if (!strcmp(id, id_refs[i][0]->id))
3938 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
3942 id_refs = tmp_id_refs;
3944 for (i = 0; i < refs_size; i++) {
3947 if (strcmp(id, refs[i].id))
3950 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
3958 if (ref_list_size > 0)
3959 ref_list[ref_list_size - 1]->next = 1;
3960 ref_list[ref_list_size] = &refs[i];
3962 /* XXX: The properties of the commit chains ensures that we can
3963 * safely modify the shared ref. The repo references will
3964 * always be similar for the same id. */
3965 ref_list[ref_list_size]->next = 0;
3970 id_refs[id_refs_size++] = ref_list;
3976 read_ref(char *id, int idlen, char *name, int namelen)
3980 bool remote = FALSE;
3982 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
3983 /* Commits referenced by tags has "^{}" appended. */
3984 if (name[namelen - 1] != '}')
3987 while (namelen > 0 && name[namelen] != '^')
3991 namelen -= STRING_SIZE("refs/tags/");
3992 name += STRING_SIZE("refs/tags/");
3994 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
3996 namelen -= STRING_SIZE("refs/remotes/");
3997 name += STRING_SIZE("refs/remotes/");
3999 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4000 namelen -= STRING_SIZE("refs/heads/");
4001 name += STRING_SIZE("refs/heads/");
4003 } else if (!strcmp(name, "HEAD")) {
4007 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4011 ref = &refs[refs_size++];
4012 ref->name = malloc(namelen + 1);
4016 strncpy(ref->name, name, namelen);
4017 ref->name[namelen] = 0;
4019 ref->remote = remote;
4020 string_copy_rev(ref->id, id);
4028 const char *cmd_env = getenv("TIG_LS_REMOTE");
4029 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4031 return read_properties(popen(cmd, "r"), "\t", read_ref);
4035 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
4037 if (!strcmp(name, "i18n.commitencoding"))
4038 string_copy(opt_encoding, value);
4044 load_repo_config(void)
4046 return read_properties(popen("git repo-config --list", "r"),
4047 "=", read_repo_config_option);
4051 read_properties(FILE *pipe, const char *separators,
4052 int (*read_property)(char *, int, char *, int))
4054 char buffer[BUFSIZ];
4061 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4066 name = chomp_string(name);
4067 namelen = strcspn(name, separators);
4069 if (name[namelen]) {
4071 value = chomp_string(name + namelen + 1);
4072 valuelen = strlen(value);
4079 state = read_property(name, namelen, value, valuelen);
4082 if (state != ERR && ferror(pipe))
4095 static void __NORETURN
4098 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4104 static void __NORETURN
4105 die(const char *err, ...)
4111 va_start(args, err);
4112 fputs("tig: ", stderr);
4113 vfprintf(stderr, err, args);
4114 fputs("\n", stderr);
4121 main(int argc, char *argv[])
4124 enum request request;
4127 signal(SIGINT, quit);
4129 if (setlocale(LC_ALL, "")) {
4130 string_copy(opt_codeset, nl_langinfo(CODESET));
4133 if (load_options() == ERR)
4134 die("Failed to load user config.");
4136 /* Load the repo config file so options can be overwritten from
4137 * the command line. */
4138 if (load_repo_config() == ERR)
4139 die("Failed to load repo config.");
4141 if (!parse_options(argc, argv))
4144 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4145 opt_iconv = iconv_open(opt_codeset, opt_encoding);
4146 if (opt_iconv == ICONV_NONE)
4147 die("Failed to initialize character set conversion");
4150 if (load_refs() == ERR)
4151 die("Failed to load refs.");
4153 /* Require a git repository unless when running in pager mode. */
4154 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
4155 die("Not a git repository");
4157 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4158 view->cmd_env = getenv(view->cmd_env);
4160 request = opt_request;
4164 while (view_driver(display[current_view], request)) {
4168 foreach_view (view, i)
4171 /* Refresh, accept single keystroke of input */
4172 key = wgetch(status_win);
4174 /* wgetch() with nodelay() enabled returns ERR when there's no
4181 request = get_keybinding(display[current_view]->keymap, key);
4183 /* Some low-level request handling. This keeps access to
4184 * status_win restricted. */
4188 char *cmd = read_prompt(":");
4190 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4191 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4192 opt_request = REQ_VIEW_DIFF;
4194 opt_request = REQ_VIEW_PAGER;
4203 case REQ_SEARCH_BACK:
4205 const char *prompt = request == REQ_SEARCH
4207 char *search = read_prompt(prompt);
4210 string_copy(opt_search, search);
4215 case REQ_SCREEN_RESIZE:
4219 getmaxyx(stdscr, height, width);
4221 /* Resize the status view and let the view driver take
4222 * care of resizing the displayed views. */
4223 wresize(status_win, 1, width);
4224 mvwin(status_win, height - 1, 0);
4225 wrefresh(status_win);