1 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2 *
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.
7 *
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.
12 */
14 #ifndef VERSION
15 #define VERSION "tig-0.5.git"
16 #endif
18 #ifndef DEBUG
19 #define NDEBUG
20 #endif
22 #include <assert.h>
23 #include <errno.h>
24 #include <ctype.h>
25 #include <signal.h>
26 #include <stdarg.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <unistd.h>
31 #include <time.h>
33 #include <sys/types.h>
34 #include <regex.h>
36 #include <locale.h>
37 #include <langinfo.h>
38 #include <iconv.h>
40 #include <curses.h>
42 #if __GNUC__ >= 3
43 #define __NORETURN __attribute__((__noreturn__))
44 #else
45 #define __NORETURN
46 #endif
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 */
64 /* Revision graph */
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
88 #define TABSIZE 8
90 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
92 #define TIG_LS_REMOTE \
93 "git ls-remote . 2>/dev/null"
95 #define TIG_DIFF_CMD \
96 "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
98 #define TIG_LOG_CMD \
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 \
105 "git ls-tree %s %s"
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 ""
114 /* Some ascii-shorthands fitted into the ncurses namespace. */
115 #define KEY_TAB '\t'
116 #define KEY_RETURN '\r'
117 #define KEY_ESC 27
120 struct ref {
121 char *name; /* Ref name; tag or head names are shortened. */
122 char id[SIZEOF_REV]; /* Commit SHA1 ID */
123 unsigned int tag:1; /* Is it a tag? */
124 unsigned int next:1; /* For ref lists: are there more refs? */
125 };
127 static struct ref **get_refs(char *id);
129 struct int_map {
130 const char *name;
131 int namelen;
132 int value;
133 };
135 static int
136 set_from_int_map(struct int_map *map, size_t map_size,
137 int *value, const char *name, int namelen)
138 {
140 int i;
142 for (i = 0; i < map_size; i++)
143 if (namelen == map[i].namelen &&
144 !strncasecmp(name, map[i].name, namelen)) {
145 *value = map[i].value;
146 return OK;
147 }
149 return ERR;
150 }
153 /*
154 * String helpers
155 */
157 static inline void
158 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
159 {
160 if (srclen > dstlen - 1)
161 srclen = dstlen - 1;
163 strncpy(dst, src, srclen);
164 dst[srclen] = 0;
165 }
167 /* Shorthands for safely copying into a fixed buffer. */
169 #define string_copy(dst, src) \
170 string_ncopy_do(dst, sizeof(dst), src, sizeof(dst))
172 #define string_ncopy(dst, src, srclen) \
173 string_ncopy_do(dst, sizeof(dst), src, srclen)
175 static char *
176 chomp_string(char *name)
177 {
178 int namelen;
180 while (isspace(*name))
181 name++;
183 namelen = strlen(name) - 1;
184 while (namelen > 0 && isspace(name[namelen]))
185 name[namelen--] = 0;
187 return name;
188 }
190 static bool
191 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
192 {
193 va_list args;
194 size_t pos = bufpos ? *bufpos : 0;
196 va_start(args, fmt);
197 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
198 va_end(args);
200 if (bufpos)
201 *bufpos = pos;
203 return pos >= bufsize ? FALSE : TRUE;
204 }
206 #define string_format(buf, fmt, args...) \
207 string_nformat(buf, sizeof(buf), NULL, fmt, args)
209 #define string_format_from(buf, from, fmt, args...) \
210 string_nformat(buf, sizeof(buf), from, fmt, args)
212 static int
213 string_enum_compare(const char *str1, const char *str2, int len)
214 {
215 size_t i;
217 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
219 /* Diff-Header == DIFF_HEADER */
220 for (i = 0; i < len; i++) {
221 if (toupper(str1[i]) == toupper(str2[i]))
222 continue;
224 if (string_enum_sep(str1[i]) &&
225 string_enum_sep(str2[i]))
226 continue;
228 return str1[i] - str2[i];
229 }
231 return 0;
232 }
234 /* Shell quoting
235 *
236 * NOTE: The following is a slightly modified copy of the git project's shell
237 * quoting routines found in the quote.c file.
238 *
239 * Help to copy the thing properly quoted for the shell safety. any single
240 * quote is replaced with '\'', any exclamation point is replaced with '\!',
241 * and the whole thing is enclosed in a
242 *
243 * E.g.
244 * original sq_quote result
245 * name ==> name ==> 'name'
246 * a b ==> a b ==> 'a b'
247 * a'b ==> a'\''b ==> 'a'\''b'
248 * a!b ==> a'\!'b ==> 'a'\!'b'
249 */
251 static size_t
252 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
253 {
254 char c;
256 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
258 BUFPUT('\'');
259 while ((c = *src++)) {
260 if (c == '\'' || c == '!') {
261 BUFPUT('\'');
262 BUFPUT('\\');
263 BUFPUT(c);
264 BUFPUT('\'');
265 } else {
266 BUFPUT(c);
267 }
268 }
269 BUFPUT('\'');
271 return bufsize;
272 }
275 /*
276 * User requests
277 */
279 #define REQ_INFO \
280 /* XXX: Keep the view request first and in sync with views[]. */ \
281 REQ_GROUP("View switching") \
282 REQ_(VIEW_MAIN, "Show main view"), \
283 REQ_(VIEW_DIFF, "Show diff view"), \
284 REQ_(VIEW_LOG, "Show log view"), \
285 REQ_(VIEW_TREE, "Show tree view"), \
286 REQ_(VIEW_BLOB, "Show blob view"), \
287 REQ_(VIEW_HELP, "Show help page"), \
288 REQ_(VIEW_PAGER, "Show pager view"), \
289 \
290 REQ_GROUP("View manipulation") \
291 REQ_(ENTER, "Enter current line and scroll"), \
292 REQ_(NEXT, "Move to next"), \
293 REQ_(PREVIOUS, "Move to previous"), \
294 REQ_(VIEW_NEXT, "Move focus to next view"), \
295 REQ_(VIEW_CLOSE, "Close the current view"), \
296 REQ_(QUIT, "Close all views and quit"), \
297 \
298 REQ_GROUP("Cursor navigation") \
299 REQ_(MOVE_UP, "Move cursor one line up"), \
300 REQ_(MOVE_DOWN, "Move cursor one line down"), \
301 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
302 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
303 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
304 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
305 \
306 REQ_GROUP("Scrolling") \
307 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
308 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
309 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
310 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
311 \
312 REQ_GROUP("Searching") \
313 REQ_(SEARCH, "Search the view"), \
314 REQ_(SEARCH_BACK, "Search backwards in the view"), \
315 REQ_(FIND_NEXT, "Find next search match"), \
316 REQ_(FIND_PREV, "Find previous search match"), \
317 \
318 REQ_GROUP("Misc") \
319 REQ_(NONE, "Do nothing"), \
320 REQ_(PROMPT, "Bring up the prompt"), \
321 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
322 REQ_(SCREEN_RESIZE, "Resize the screen"), \
323 REQ_(SHOW_VERSION, "Show version information"), \
324 REQ_(STOP_LOADING, "Stop all loading views"), \
325 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
326 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization")
329 /* User action requests. */
330 enum request {
331 #define REQ_GROUP(help)
332 #define REQ_(req, help) REQ_##req
334 /* Offset all requests to avoid conflicts with ncurses getch values. */
335 REQ_OFFSET = KEY_MAX + 1,
336 REQ_INFO,
337 REQ_UNKNOWN,
339 #undef REQ_GROUP
340 #undef REQ_
341 };
343 struct request_info {
344 enum request request;
345 char *name;
346 int namelen;
347 char *help;
348 };
350 static struct request_info req_info[] = {
351 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
352 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
353 REQ_INFO
354 #undef REQ_GROUP
355 #undef REQ_
356 };
358 static enum request
359 get_request(const char *name)
360 {
361 int namelen = strlen(name);
362 int i;
364 for (i = 0; i < ARRAY_SIZE(req_info); i++)
365 if (req_info[i].namelen == namelen &&
366 !string_enum_compare(req_info[i].name, name, namelen))
367 return req_info[i].request;
369 return REQ_UNKNOWN;
370 }
373 /*
374 * Options
375 */
377 static const char usage[] =
378 VERSION " (" __DATE__ ")\n"
379 "\n"
380 "Usage: tig [options]\n"
381 " or: tig [options] [--] [git log options]\n"
382 " or: tig [options] log [git log options]\n"
383 " or: tig [options] diff [git diff options]\n"
384 " or: tig [options] show [git show options]\n"
385 " or: tig [options] < [git command output]\n"
386 "\n"
387 "Options:\n"
388 " -l Start up in log view\n"
389 " -d Start up in diff view\n"
390 " -n[I], --line-number[=I] Show line numbers with given interval\n"
391 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
392 " -- Mark end of tig options\n"
393 " -v, --version Show version and exit\n"
394 " -h, --help Show help message and exit\n";
396 /* Option and state variables. */
397 static bool opt_line_number = FALSE;
398 static bool opt_rev_graph = TRUE;
399 static int opt_num_interval = NUMBER_INTERVAL;
400 static int opt_tab_size = TABSIZE;
401 static enum request opt_request = REQ_VIEW_MAIN;
402 static char opt_cmd[SIZEOF_STR] = "";
403 static char opt_path[SIZEOF_STR] = "";
404 static FILE *opt_pipe = NULL;
405 static char opt_encoding[20] = "UTF-8";
406 static bool opt_utf8 = TRUE;
407 static char opt_codeset[20] = "UTF-8";
408 static iconv_t opt_iconv = ICONV_NONE;
409 static char opt_search[SIZEOF_STR] = "";
411 enum option_type {
412 OPT_NONE,
413 OPT_INT,
414 };
416 static bool
417 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
418 {
419 va_list args;
420 char *value = "";
421 int *number;
423 if (opt[0] != '-')
424 return FALSE;
426 if (opt[1] == '-') {
427 int namelen = strlen(name);
429 opt += 2;
431 if (strncmp(opt, name, namelen))
432 return FALSE;
434 if (opt[namelen] == '=')
435 value = opt + namelen + 1;
437 } else {
438 if (!short_name || opt[1] != short_name)
439 return FALSE;
440 value = opt + 2;
441 }
443 va_start(args, type);
444 if (type == OPT_INT) {
445 number = va_arg(args, int *);
446 if (isdigit(*value))
447 *number = atoi(value);
448 }
449 va_end(args);
451 return TRUE;
452 }
454 /* Returns the index of log or diff command or -1 to exit. */
455 static bool
456 parse_options(int argc, char *argv[])
457 {
458 int i;
460 for (i = 1; i < argc; i++) {
461 char *opt = argv[i];
463 if (!strcmp(opt, "log") ||
464 !strcmp(opt, "diff") ||
465 !strcmp(opt, "show")) {
466 opt_request = opt[0] == 'l'
467 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
468 break;
469 }
471 if (opt[0] && opt[0] != '-')
472 break;
474 if (!strcmp(opt, "-l")) {
475 opt_request = REQ_VIEW_LOG;
476 continue;
477 }
479 if (!strcmp(opt, "-d")) {
480 opt_request = REQ_VIEW_DIFF;
481 continue;
482 }
484 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
485 opt_line_number = TRUE;
486 continue;
487 }
489 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
490 opt_tab_size = MIN(opt_tab_size, TABSIZE);
491 continue;
492 }
494 if (check_option(opt, 'v', "version", OPT_NONE)) {
495 printf("tig version %s\n", VERSION);
496 return FALSE;
497 }
499 if (check_option(opt, 'h', "help", OPT_NONE)) {
500 printf(usage);
501 return FALSE;
502 }
504 if (!strcmp(opt, "--")) {
505 i++;
506 break;
507 }
509 die("unknown option '%s'\n\n%s", opt, usage);
510 }
512 if (!isatty(STDIN_FILENO)) {
513 opt_request = REQ_VIEW_PAGER;
514 opt_pipe = stdin;
516 } else if (i < argc) {
517 size_t buf_size;
519 if (opt_request == REQ_VIEW_MAIN)
520 /* XXX: This is vulnerable to the user overriding
521 * options required for the main view parser. */
522 string_copy(opt_cmd, "git log --stat --pretty=raw");
523 else
524 string_copy(opt_cmd, "git");
525 buf_size = strlen(opt_cmd);
527 while (buf_size < sizeof(opt_cmd) && i < argc) {
528 opt_cmd[buf_size++] = ' ';
529 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
530 }
532 if (buf_size >= sizeof(opt_cmd))
533 die("command too long");
535 opt_cmd[buf_size] = 0;
537 }
539 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
540 opt_utf8 = FALSE;
542 return TRUE;
543 }
546 /*
547 * Line-oriented content detection.
548 */
550 #define LINE_INFO \
551 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
552 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
553 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
554 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
555 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
556 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
557 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
558 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
559 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
560 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
561 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
562 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
563 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
565 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
566 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
568 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
570 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
572 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
573 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
574 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
575 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
576 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
577 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
579 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
580 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
581 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
582 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
583 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
584 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
585 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
586 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
587 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
588 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
589 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
590 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
591 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL)
593 enum line_type {
594 #define LINE(type, line, fg, bg, attr) \
595 LINE_##type
596 LINE_INFO
597 #undef LINE
598 };
600 struct line_info {
601 const char *name; /* Option name. */
602 int namelen; /* Size of option name. */
603 const char *line; /* The start of line to match. */
604 int linelen; /* Size of string to match. */
605 int fg, bg, attr; /* Color and text attributes for the lines. */
606 };
608 static struct line_info line_info[] = {
609 #define LINE(type, line, fg, bg, attr) \
610 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
611 LINE_INFO
612 #undef LINE
613 };
615 static enum line_type
616 get_line_type(char *line)
617 {
618 int linelen = strlen(line);
619 enum line_type type;
621 for (type = 0; type < ARRAY_SIZE(line_info); type++)
622 /* Case insensitive search matches Signed-off-by lines better. */
623 if (linelen >= line_info[type].linelen &&
624 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
625 return type;
627 return LINE_DEFAULT;
628 }
630 static inline int
631 get_line_attr(enum line_type type)
632 {
633 assert(type < ARRAY_SIZE(line_info));
634 return COLOR_PAIR(type) | line_info[type].attr;
635 }
637 static struct line_info *
638 get_line_info(char *name, int namelen)
639 {
640 enum line_type type;
642 for (type = 0; type < ARRAY_SIZE(line_info); type++)
643 if (namelen == line_info[type].namelen &&
644 !string_enum_compare(line_info[type].name, name, namelen))
645 return &line_info[type];
647 return NULL;
648 }
650 static void
651 init_colors(void)
652 {
653 int default_bg = COLOR_BLACK;
654 int default_fg = COLOR_WHITE;
655 enum line_type type;
657 start_color();
659 if (use_default_colors() != ERR) {
660 default_bg = -1;
661 default_fg = -1;
662 }
664 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
665 struct line_info *info = &line_info[type];
666 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
667 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
669 init_pair(type, fg, bg);
670 }
671 }
673 struct line {
674 enum line_type type;
676 /* State flags */
677 unsigned int selected:1;
679 void *data; /* User data */
680 };
683 /*
684 * Keys
685 */
687 struct keybinding {
688 int alias;
689 enum request request;
690 struct keybinding *next;
691 };
693 static struct keybinding default_keybindings[] = {
694 /* View switching */
695 { 'm', REQ_VIEW_MAIN },
696 { 'd', REQ_VIEW_DIFF },
697 { 'l', REQ_VIEW_LOG },
698 { 't', REQ_VIEW_TREE },
699 { 'f', REQ_VIEW_BLOB },
700 { 'p', REQ_VIEW_PAGER },
701 { 'h', REQ_VIEW_HELP },
703 /* View manipulation */
704 { 'q', REQ_VIEW_CLOSE },
705 { KEY_TAB, REQ_VIEW_NEXT },
706 { KEY_RETURN, REQ_ENTER },
707 { KEY_UP, REQ_PREVIOUS },
708 { KEY_DOWN, REQ_NEXT },
710 /* Cursor navigation */
711 { 'k', REQ_MOVE_UP },
712 { 'j', REQ_MOVE_DOWN },
713 { KEY_HOME, REQ_MOVE_FIRST_LINE },
714 { KEY_END, REQ_MOVE_LAST_LINE },
715 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
716 { ' ', REQ_MOVE_PAGE_DOWN },
717 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
718 { 'b', REQ_MOVE_PAGE_UP },
719 { '-', REQ_MOVE_PAGE_UP },
721 /* Scrolling */
722 { KEY_IC, REQ_SCROLL_LINE_UP },
723 { KEY_DC, REQ_SCROLL_LINE_DOWN },
724 { 'w', REQ_SCROLL_PAGE_UP },
725 { 's', REQ_SCROLL_PAGE_DOWN },
727 /* Searching */
728 { '/', REQ_SEARCH },
729 { '?', REQ_SEARCH_BACK },
730 { 'n', REQ_FIND_NEXT },
731 { 'N', REQ_FIND_PREV },
733 /* Misc */
734 { 'Q', REQ_QUIT },
735 { 'z', REQ_STOP_LOADING },
736 { 'v', REQ_SHOW_VERSION },
737 { 'r', REQ_SCREEN_REDRAW },
738 { '.', REQ_TOGGLE_LINENO },
739 { 'g', REQ_TOGGLE_REV_GRAPH },
740 { ':', REQ_PROMPT },
742 /* Using the ncurses SIGWINCH handler. */
743 { KEY_RESIZE, REQ_SCREEN_RESIZE },
744 };
746 #define KEYMAP_INFO \
747 KEYMAP_(GENERIC), \
748 KEYMAP_(MAIN), \
749 KEYMAP_(DIFF), \
750 KEYMAP_(LOG), \
751 KEYMAP_(TREE), \
752 KEYMAP_(BLOB), \
753 KEYMAP_(PAGER), \
754 KEYMAP_(HELP) \
756 enum keymap {
757 #define KEYMAP_(name) KEYMAP_##name
758 KEYMAP_INFO
759 #undef KEYMAP_
760 };
762 static struct int_map keymap_table[] = {
763 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
764 KEYMAP_INFO
765 #undef KEYMAP_
766 };
768 #define set_keymap(map, name) \
769 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
771 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
773 static void
774 add_keybinding(enum keymap keymap, enum request request, int key)
775 {
776 struct keybinding *keybinding;
778 keybinding = calloc(1, sizeof(*keybinding));
779 if (!keybinding)
780 die("Failed to allocate keybinding");
782 keybinding->alias = key;
783 keybinding->request = request;
784 keybinding->next = keybindings[keymap];
785 keybindings[keymap] = keybinding;
786 }
788 /* Looks for a key binding first in the given map, then in the generic map, and
789 * lastly in the default keybindings. */
790 static enum request
791 get_keybinding(enum keymap keymap, int key)
792 {
793 struct keybinding *kbd;
794 int i;
796 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
797 if (kbd->alias == key)
798 return kbd->request;
800 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
801 if (kbd->alias == key)
802 return kbd->request;
804 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
805 if (default_keybindings[i].alias == key)
806 return default_keybindings[i].request;
808 return (enum request) key;
809 }
812 struct key {
813 char *name;
814 int value;
815 };
817 static struct key key_table[] = {
818 { "Enter", KEY_RETURN },
819 { "Space", ' ' },
820 { "Backspace", KEY_BACKSPACE },
821 { "Tab", KEY_TAB },
822 { "Escape", KEY_ESC },
823 { "Left", KEY_LEFT },
824 { "Right", KEY_RIGHT },
825 { "Up", KEY_UP },
826 { "Down", KEY_DOWN },
827 { "Insert", KEY_IC },
828 { "Delete", KEY_DC },
829 { "Hash", '#' },
830 { "Home", KEY_HOME },
831 { "End", KEY_END },
832 { "PageUp", KEY_PPAGE },
833 { "PageDown", KEY_NPAGE },
834 { "F1", KEY_F(1) },
835 { "F2", KEY_F(2) },
836 { "F3", KEY_F(3) },
837 { "F4", KEY_F(4) },
838 { "F5", KEY_F(5) },
839 { "F6", KEY_F(6) },
840 { "F7", KEY_F(7) },
841 { "F8", KEY_F(8) },
842 { "F9", KEY_F(9) },
843 { "F10", KEY_F(10) },
844 { "F11", KEY_F(11) },
845 { "F12", KEY_F(12) },
846 };
848 static int
849 get_key_value(const char *name)
850 {
851 int i;
853 for (i = 0; i < ARRAY_SIZE(key_table); i++)
854 if (!strcasecmp(key_table[i].name, name))
855 return key_table[i].value;
857 if (strlen(name) == 1 && isprint(*name))
858 return (int) *name;
860 return ERR;
861 }
863 static char *
864 get_key(enum request request)
865 {
866 static char buf[BUFSIZ];
867 static char key_char[] = "'X'";
868 size_t pos = 0;
869 char *sep = " ";
870 int i;
872 buf[pos] = 0;
874 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
875 struct keybinding *keybinding = &default_keybindings[i];
876 char *seq = NULL;
877 int key;
879 if (keybinding->request != request)
880 continue;
882 for (key = 0; key < ARRAY_SIZE(key_table); key++)
883 if (key_table[key].value == keybinding->alias)
884 seq = key_table[key].name;
886 if (seq == NULL &&
887 keybinding->alias < 127 &&
888 isprint(keybinding->alias)) {
889 key_char[1] = (char) keybinding->alias;
890 seq = key_char;
891 }
893 if (!seq)
894 seq = "'?'";
896 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
897 return "Too many keybindings!";
898 sep = ", ";
899 }
901 return buf;
902 }
905 /*
906 * User config file handling.
907 */
909 static struct int_map color_map[] = {
910 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
911 COLOR_MAP(DEFAULT),
912 COLOR_MAP(BLACK),
913 COLOR_MAP(BLUE),
914 COLOR_MAP(CYAN),
915 COLOR_MAP(GREEN),
916 COLOR_MAP(MAGENTA),
917 COLOR_MAP(RED),
918 COLOR_MAP(WHITE),
919 COLOR_MAP(YELLOW),
920 };
922 #define set_color(color, name) \
923 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
925 static struct int_map attr_map[] = {
926 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
927 ATTR_MAP(NORMAL),
928 ATTR_MAP(BLINK),
929 ATTR_MAP(BOLD),
930 ATTR_MAP(DIM),
931 ATTR_MAP(REVERSE),
932 ATTR_MAP(STANDOUT),
933 ATTR_MAP(UNDERLINE),
934 };
936 #define set_attribute(attr, name) \
937 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
939 static int config_lineno;
940 static bool config_errors;
941 static char *config_msg;
943 /* Wants: object fgcolor bgcolor [attr] */
944 static int
945 option_color_command(int argc, char *argv[])
946 {
947 struct line_info *info;
949 if (argc != 3 && argc != 4) {
950 config_msg = "Wrong number of arguments given to color command";
951 return ERR;
952 }
954 info = get_line_info(argv[0], strlen(argv[0]));
955 if (!info) {
956 config_msg = "Unknown color name";
957 return ERR;
958 }
960 if (set_color(&info->fg, argv[1]) == ERR ||
961 set_color(&info->bg, argv[2]) == ERR) {
962 config_msg = "Unknown color";
963 return ERR;
964 }
966 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
967 config_msg = "Unknown attribute";
968 return ERR;
969 }
971 return OK;
972 }
974 /* Wants: name = value */
975 static int
976 option_set_command(int argc, char *argv[])
977 {
978 if (argc != 3) {
979 config_msg = "Wrong number of arguments given to set command";
980 return ERR;
981 }
983 if (strcmp(argv[1], "=")) {
984 config_msg = "No value assigned";
985 return ERR;
986 }
988 if (!strcmp(argv[0], "show-rev-graph")) {
989 opt_rev_graph = (!strcmp(argv[2], "1") ||
990 !strcmp(argv[2], "true") ||
991 !strcmp(argv[2], "yes"));
992 return OK;
993 }
995 if (!strcmp(argv[0], "line-number-interval")) {
996 opt_num_interval = atoi(argv[2]);
997 return OK;
998 }
1000 if (!strcmp(argv[0], "tab-size")) {
1001 opt_tab_size = atoi(argv[2]);
1002 return OK;
1003 }
1005 if (!strcmp(argv[0], "commit-encoding")) {
1006 char *arg = argv[2];
1007 int delimiter = *arg;
1008 int i;
1010 switch (delimiter) {
1011 case '"':
1012 case '\'':
1013 for (arg++, i = 0; arg[i]; i++)
1014 if (arg[i] == delimiter) {
1015 arg[i] = 0;
1016 break;
1017 }
1018 default:
1019 string_copy(opt_encoding, arg);
1020 return OK;
1021 }
1022 }
1024 config_msg = "Unknown variable name";
1025 return ERR;
1026 }
1028 /* Wants: mode request key */
1029 static int
1030 option_bind_command(int argc, char *argv[])
1031 {
1032 enum request request;
1033 int keymap;
1034 int key;
1036 if (argc != 3) {
1037 config_msg = "Wrong number of arguments given to bind command";
1038 return ERR;
1039 }
1041 if (set_keymap(&keymap, argv[0]) == ERR) {
1042 config_msg = "Unknown key map";
1043 return ERR;
1044 }
1046 key = get_key_value(argv[1]);
1047 if (key == ERR) {
1048 config_msg = "Unknown key";
1049 return ERR;
1050 }
1052 request = get_request(argv[2]);
1053 if (request == REQ_UNKNOWN) {
1054 config_msg = "Unknown request name";
1055 return ERR;
1056 }
1058 add_keybinding(keymap, request, key);
1060 return OK;
1061 }
1063 static int
1064 set_option(char *opt, char *value)
1065 {
1066 char *argv[16];
1067 int valuelen;
1068 int argc = 0;
1070 /* Tokenize */
1071 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1072 argv[argc++] = value;
1074 value += valuelen;
1075 if (!*value)
1076 break;
1078 *value++ = 0;
1079 while (isspace(*value))
1080 value++;
1081 }
1083 if (!strcmp(opt, "color"))
1084 return option_color_command(argc, argv);
1086 if (!strcmp(opt, "set"))
1087 return option_set_command(argc, argv);
1089 if (!strcmp(opt, "bind"))
1090 return option_bind_command(argc, argv);
1092 config_msg = "Unknown option command";
1093 return ERR;
1094 }
1096 static int
1097 read_option(char *opt, int optlen, char *value, int valuelen)
1098 {
1099 int status = OK;
1101 config_lineno++;
1102 config_msg = "Internal error";
1104 /* Check for comment markers, since read_properties() will
1105 * only ensure opt and value are split at first " \t". */
1106 optlen = strcspn(opt, "#");
1107 if (optlen == 0)
1108 return OK;
1110 if (opt[optlen] != 0) {
1111 config_msg = "No option value";
1112 status = ERR;
1114 } else {
1115 /* Look for comment endings in the value. */
1116 int len = strcspn(value, "#");
1118 if (len < valuelen) {
1119 valuelen = len;
1120 value[valuelen] = 0;
1121 }
1123 status = set_option(opt, value);
1124 }
1126 if (status == ERR) {
1127 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1128 config_lineno, optlen, opt, config_msg);
1129 config_errors = TRUE;
1130 }
1132 /* Always keep going if errors are encountered. */
1133 return OK;
1134 }
1136 static int
1137 load_options(void)
1138 {
1139 char *home = getenv("HOME");
1140 char buf[SIZEOF_STR];
1141 FILE *file;
1143 config_lineno = 0;
1144 config_errors = FALSE;
1146 if (!home || !string_format(buf, "%s/.tigrc", home))
1147 return ERR;
1149 /* It's ok that the file doesn't exist. */
1150 file = fopen(buf, "r");
1151 if (!file)
1152 return OK;
1154 if (read_properties(file, " \t", read_option) == ERR ||
1155 config_errors == TRUE)
1156 fprintf(stderr, "Errors while loading %s.\n", buf);
1158 return OK;
1159 }
1162 /*
1163 * The viewer
1164 */
1166 struct view;
1167 struct view_ops;
1169 /* The display array of active views and the index of the current view. */
1170 static struct view *display[2];
1171 static unsigned int current_view;
1173 /* Reading from the prompt? */
1174 static bool input_mode = FALSE;
1176 #define foreach_displayed_view(view, i) \
1177 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1179 #define displayed_views() (display[1] != NULL ? 2 : 1)
1181 /* Current head and commit ID */
1182 static char ref_blob[SIZEOF_REF] = "";
1183 static char ref_commit[SIZEOF_REF] = "HEAD";
1184 static char ref_head[SIZEOF_REF] = "HEAD";
1186 struct view {
1187 const char *name; /* View name */
1188 const char *cmd_fmt; /* Default command line format */
1189 const char *cmd_env; /* Command line set via environment */
1190 const char *id; /* Points to either of ref_{head,commit,blob} */
1192 struct view_ops *ops; /* View operations */
1194 enum keymap keymap; /* What keymap does this view have */
1196 char cmd[SIZEOF_STR]; /* Command buffer */
1197 char ref[SIZEOF_REF]; /* Hovered commit reference */
1198 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1200 int height, width; /* The width and height of the main window */
1201 WINDOW *win; /* The main window */
1202 WINDOW *title; /* The title window living below the main window */
1204 /* Navigation */
1205 unsigned long offset; /* Offset of the window top */
1206 unsigned long lineno; /* Current line number */
1208 /* Searching */
1209 char grep[SIZEOF_STR]; /* Search string */
1210 regex_t *regex; /* Pre-compiled regex */
1212 /* If non-NULL, points to the view that opened this view. If this view
1213 * is closed tig will switch back to the parent view. */
1214 struct view *parent;
1216 /* Buffering */
1217 unsigned long lines; /* Total number of lines */
1218 struct line *line; /* Line index */
1219 unsigned long line_size;/* Total number of allocated lines */
1220 unsigned int digits; /* Number of digits in the lines member. */
1222 /* Loading */
1223 FILE *pipe;
1224 time_t start_time;
1225 };
1227 struct view_ops {
1228 /* What type of content being displayed. Used in the title bar. */
1229 const char *type;
1230 /* Draw one line; @lineno must be < view->height. */
1231 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1232 /* Read one line; updates view->line. */
1233 bool (*read)(struct view *view, char *data);
1234 /* Depending on view, change display based on current line. */
1235 bool (*enter)(struct view *view, struct line *line);
1236 /* Search for regex in a line. */
1237 bool (*grep)(struct view *view, struct line *line);
1238 /* Select line */
1239 void (*select)(struct view *view, struct line *line);
1240 };
1242 static struct view_ops pager_ops;
1243 static struct view_ops main_ops;
1244 static struct view_ops tree_ops;
1245 static struct view_ops blob_ops;
1247 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1248 { name, cmd, #env, ref, ops, map}
1250 #define VIEW_(id, name, ops, ref) \
1251 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1254 static struct view views[] = {
1255 VIEW_(MAIN, "main", &main_ops, ref_head),
1256 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1257 VIEW_(LOG, "log", &pager_ops, ref_head),
1258 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1259 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1260 VIEW_(HELP, "help", &pager_ops, "static"),
1261 VIEW_(PAGER, "pager", &pager_ops, "static"),
1262 };
1264 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1266 #define foreach_view(view, i) \
1267 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1269 #define view_is_displayed(view) \
1270 (view == display[0] || view == display[1])
1272 static bool
1273 draw_view_line(struct view *view, unsigned int lineno)
1274 {
1275 struct line *line;
1276 bool selected = (view->offset + lineno == view->lineno);
1277 bool draw_ok;
1279 assert(view_is_displayed(view));
1281 if (view->offset + lineno >= view->lines)
1282 return FALSE;
1284 line = &view->line[view->offset + lineno];
1286 if (selected) {
1287 line->selected = TRUE;
1288 view->ops->select(view, line);
1289 } else if (line->selected) {
1290 line->selected = FALSE;
1291 wmove(view->win, lineno, 0);
1292 wclrtoeol(view->win);
1293 }
1295 scrollok(view->win, FALSE);
1296 draw_ok = view->ops->draw(view, line, lineno, selected);
1297 scrollok(view->win, TRUE);
1299 return draw_ok;
1300 }
1302 static void
1303 redraw_view_from(struct view *view, int lineno)
1304 {
1305 assert(0 <= lineno && lineno < view->height);
1307 for (; lineno < view->height; lineno++) {
1308 if (!draw_view_line(view, lineno))
1309 break;
1310 }
1312 redrawwin(view->win);
1313 if (input_mode)
1314 wnoutrefresh(view->win);
1315 else
1316 wrefresh(view->win);
1317 }
1319 static void
1320 redraw_view(struct view *view)
1321 {
1322 wclear(view->win);
1323 redraw_view_from(view, 0);
1324 }
1327 static void
1328 update_view_title(struct view *view)
1329 {
1330 char buf[SIZEOF_STR];
1331 char state[SIZEOF_STR];
1332 size_t bufpos = 0, statelen = 0;
1334 assert(view_is_displayed(view));
1336 if (view->lines || view->pipe) {
1337 unsigned int view_lines = view->offset + view->height;
1338 unsigned int lines = view->lines
1339 ? MIN(view_lines, view->lines) * 100 / view->lines
1340 : 0;
1342 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1343 view->ops->type,
1344 view->lineno + 1,
1345 view->lines,
1346 lines);
1348 if (view->pipe) {
1349 time_t secs = time(NULL) - view->start_time;
1351 /* Three git seconds are a long time ... */
1352 if (secs > 2)
1353 string_format_from(state, &statelen, " %lds", secs);
1354 }
1355 }
1357 string_format_from(buf, &bufpos, "[%s]", view->name);
1358 if (*view->ref && bufpos < view->width) {
1359 size_t refsize = strlen(view->ref);
1360 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1362 if (minsize < view->width)
1363 refsize = view->width - minsize + 7;
1364 string_format_from(buf, &bufpos, " %.*s", refsize, view->ref);
1365 }
1367 if (statelen && bufpos < view->width) {
1368 string_format_from(buf, &bufpos, " %s", state);
1369 }
1371 if (view == display[current_view])
1372 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1373 else
1374 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1376 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1377 wclrtoeol(view->title);
1378 wmove(view->title, 0, view->width - 1);
1380 if (input_mode)
1381 wnoutrefresh(view->title);
1382 else
1383 wrefresh(view->title);
1384 }
1386 static void
1387 resize_display(void)
1388 {
1389 int offset, i;
1390 struct view *base = display[0];
1391 struct view *view = display[1] ? display[1] : display[0];
1393 /* Setup window dimensions */
1395 getmaxyx(stdscr, base->height, base->width);
1397 /* Make room for the status window. */
1398 base->height -= 1;
1400 if (view != base) {
1401 /* Horizontal split. */
1402 view->width = base->width;
1403 view->height = SCALE_SPLIT_VIEW(base->height);
1404 base->height -= view->height;
1406 /* Make room for the title bar. */
1407 view->height -= 1;
1408 }
1410 /* Make room for the title bar. */
1411 base->height -= 1;
1413 offset = 0;
1415 foreach_displayed_view (view, i) {
1416 if (!view->win) {
1417 view->win = newwin(view->height, 0, offset, 0);
1418 if (!view->win)
1419 die("Failed to create %s view", view->name);
1421 scrollok(view->win, TRUE);
1423 view->title = newwin(1, 0, offset + view->height, 0);
1424 if (!view->title)
1425 die("Failed to create title window");
1427 } else {
1428 wresize(view->win, view->height, view->width);
1429 mvwin(view->win, offset, 0);
1430 mvwin(view->title, offset + view->height, 0);
1431 }
1433 offset += view->height + 1;
1434 }
1435 }
1437 static void
1438 redraw_display(void)
1439 {
1440 struct view *view;
1441 int i;
1443 foreach_displayed_view (view, i) {
1444 redraw_view(view);
1445 update_view_title(view);
1446 }
1447 }
1449 static void
1450 update_display_cursor(struct view *view)
1451 {
1452 /* Move the cursor to the right-most column of the cursor line.
1453 *
1454 * XXX: This could turn out to be a bit expensive, but it ensures that
1455 * the cursor does not jump around. */
1456 if (view->lines) {
1457 wmove(view->win, view->lineno - view->offset, view->width - 1);
1458 wrefresh(view->win);
1459 }
1460 }
1462 /*
1463 * Navigation
1464 */
1466 /* Scrolling backend */
1467 static void
1468 do_scroll_view(struct view *view, int lines)
1469 {
1470 bool redraw_current_line = FALSE;
1472 /* The rendering expects the new offset. */
1473 view->offset += lines;
1475 assert(0 <= view->offset && view->offset < view->lines);
1476 assert(lines);
1478 /* Move current line into the view. */
1479 if (view->lineno < view->offset) {
1480 view->lineno = view->offset;
1481 redraw_current_line = TRUE;
1482 } else if (view->lineno >= view->offset + view->height) {
1483 view->lineno = view->offset + view->height - 1;
1484 redraw_current_line = TRUE;
1485 }
1487 assert(view->offset <= view->lineno && view->lineno < view->lines);
1489 /* Redraw the whole screen if scrolling is pointless. */
1490 if (view->height < ABS(lines)) {
1491 redraw_view(view);
1493 } else {
1494 int line = lines > 0 ? view->height - lines : 0;
1495 int end = line + ABS(lines);
1497 wscrl(view->win, lines);
1499 for (; line < end; line++) {
1500 if (!draw_view_line(view, line))
1501 break;
1502 }
1504 if (redraw_current_line)
1505 draw_view_line(view, view->lineno - view->offset);
1506 }
1508 redrawwin(view->win);
1509 wrefresh(view->win);
1510 report("");
1511 }
1513 /* Scroll frontend */
1514 static void
1515 scroll_view(struct view *view, enum request request)
1516 {
1517 int lines = 1;
1519 assert(view_is_displayed(view));
1521 switch (request) {
1522 case REQ_SCROLL_PAGE_DOWN:
1523 lines = view->height;
1524 case REQ_SCROLL_LINE_DOWN:
1525 if (view->offset + lines > view->lines)
1526 lines = view->lines - view->offset;
1528 if (lines == 0 || view->offset + view->height >= view->lines) {
1529 report("Cannot scroll beyond the last line");
1530 return;
1531 }
1532 break;
1534 case REQ_SCROLL_PAGE_UP:
1535 lines = view->height;
1536 case REQ_SCROLL_LINE_UP:
1537 if (lines > view->offset)
1538 lines = view->offset;
1540 if (lines == 0) {
1541 report("Cannot scroll beyond the first line");
1542 return;
1543 }
1545 lines = -lines;
1546 break;
1548 default:
1549 die("request %d not handled in switch", request);
1550 }
1552 do_scroll_view(view, lines);
1553 }
1555 /* Cursor moving */
1556 static void
1557 move_view(struct view *view, enum request request)
1558 {
1559 int scroll_steps = 0;
1560 int steps;
1562 switch (request) {
1563 case REQ_MOVE_FIRST_LINE:
1564 steps = -view->lineno;
1565 break;
1567 case REQ_MOVE_LAST_LINE:
1568 steps = view->lines - view->lineno - 1;
1569 break;
1571 case REQ_MOVE_PAGE_UP:
1572 steps = view->height > view->lineno
1573 ? -view->lineno : -view->height;
1574 break;
1576 case REQ_MOVE_PAGE_DOWN:
1577 steps = view->lineno + view->height >= view->lines
1578 ? view->lines - view->lineno - 1 : view->height;
1579 break;
1581 case REQ_MOVE_UP:
1582 steps = -1;
1583 break;
1585 case REQ_MOVE_DOWN:
1586 steps = 1;
1587 break;
1589 default:
1590 die("request %d not handled in switch", request);
1591 }
1593 if (steps <= 0 && view->lineno == 0) {
1594 report("Cannot move beyond the first line");
1595 return;
1597 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1598 report("Cannot move beyond the last line");
1599 return;
1600 }
1602 /* Move the current line */
1603 view->lineno += steps;
1604 assert(0 <= view->lineno && view->lineno < view->lines);
1606 /* Check whether the view needs to be scrolled */
1607 if (view->lineno < view->offset ||
1608 view->lineno >= view->offset + view->height) {
1609 scroll_steps = steps;
1610 if (steps < 0 && -steps > view->offset) {
1611 scroll_steps = -view->offset;
1613 } else if (steps > 0) {
1614 if (view->lineno == view->lines - 1 &&
1615 view->lines > view->height) {
1616 scroll_steps = view->lines - view->offset - 1;
1617 if (scroll_steps >= view->height)
1618 scroll_steps -= view->height - 1;
1619 }
1620 }
1621 }
1623 if (!view_is_displayed(view)) {
1624 view->offset += steps;
1625 view->ops->select(view, &view->line[view->lineno]);
1626 return;
1627 }
1629 /* Repaint the old "current" line if we be scrolling */
1630 if (ABS(steps) < view->height)
1631 draw_view_line(view, view->lineno - steps - view->offset);
1633 if (scroll_steps) {
1634 do_scroll_view(view, scroll_steps);
1635 return;
1636 }
1638 /* Draw the current line */
1639 draw_view_line(view, view->lineno - view->offset);
1641 redrawwin(view->win);
1642 wrefresh(view->win);
1643 report("");
1644 }
1647 /*
1648 * Searching
1649 */
1651 static void search_view(struct view *view, enum request request);
1653 static bool
1654 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1655 {
1656 assert(view_is_displayed(view));
1658 if (!view->ops->grep(view, line))
1659 return FALSE;
1661 if (lineno - view->offset >= view->height) {
1662 view->offset = lineno;
1663 view->lineno = lineno;
1664 redraw_view(view);
1666 } else {
1667 unsigned long old_lineno = view->lineno - view->offset;
1669 view->lineno = lineno;
1670 draw_view_line(view, old_lineno);
1672 draw_view_line(view, view->lineno - view->offset);
1673 redrawwin(view->win);
1674 wrefresh(view->win);
1675 }
1677 report("Line %ld matches '%s'", lineno + 1, view->grep);
1678 return TRUE;
1679 }
1681 static void
1682 find_next(struct view *view, enum request request)
1683 {
1684 unsigned long lineno = view->lineno;
1685 int direction;
1687 if (!*view->grep) {
1688 if (!*opt_search)
1689 report("No previous search");
1690 else
1691 search_view(view, request);
1692 return;
1693 }
1695 switch (request) {
1696 case REQ_SEARCH:
1697 case REQ_FIND_NEXT:
1698 direction = 1;
1699 break;
1701 case REQ_SEARCH_BACK:
1702 case REQ_FIND_PREV:
1703 direction = -1;
1704 break;
1706 default:
1707 return;
1708 }
1710 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1711 lineno += direction;
1713 /* Note, lineno is unsigned long so will wrap around in which case it
1714 * will become bigger than view->lines. */
1715 for (; lineno < view->lines; lineno += direction) {
1716 struct line *line = &view->line[lineno];
1718 if (find_next_line(view, lineno, line))
1719 return;
1720 }
1722 report("No match found for '%s'", view->grep);
1723 }
1725 static void
1726 search_view(struct view *view, enum request request)
1727 {
1728 int regex_err;
1730 if (view->regex) {
1731 regfree(view->regex);
1732 *view->grep = 0;
1733 } else {
1734 view->regex = calloc(1, sizeof(*view->regex));
1735 if (!view->regex)
1736 return;
1737 }
1739 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1740 if (regex_err != 0) {
1741 char buf[SIZEOF_STR] = "unknown error";
1743 regerror(regex_err, view->regex, buf, sizeof(buf));
1744 report("Search failed: %s", buf);
1745 return;
1746 }
1748 string_copy(view->grep, opt_search);
1750 find_next(view, request);
1751 }
1753 /*
1754 * Incremental updating
1755 */
1757 static void
1758 end_update(struct view *view)
1759 {
1760 if (!view->pipe)
1761 return;
1762 set_nonblocking_input(FALSE);
1763 if (view->pipe == stdin)
1764 fclose(view->pipe);
1765 else
1766 pclose(view->pipe);
1767 view->pipe = NULL;
1768 }
1770 static bool
1771 begin_update(struct view *view)
1772 {
1773 const char *id = view->id;
1775 if (view->pipe)
1776 end_update(view);
1778 if (opt_cmd[0]) {
1779 string_copy(view->cmd, opt_cmd);
1780 opt_cmd[0] = 0;
1781 /* When running random commands, the view ref could have become
1782 * invalid so clear it. */
1783 view->ref[0] = 0;
1785 } else if (view == VIEW(REQ_VIEW_TREE)) {
1786 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1788 if (strcmp(view->vid, view->id))
1789 opt_path[0] = 0;
1791 if (!string_format(view->cmd, format, id, opt_path))
1792 return FALSE;
1794 } else {
1795 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1797 if (!string_format(view->cmd, format, id, id, id, id, id))
1798 return FALSE;
1799 }
1801 /* Special case for the pager view. */
1802 if (opt_pipe) {
1803 view->pipe = opt_pipe;
1804 opt_pipe = NULL;
1805 } else {
1806 view->pipe = popen(view->cmd, "r");
1807 }
1809 if (!view->pipe)
1810 return FALSE;
1812 set_nonblocking_input(TRUE);
1814 view->offset = 0;
1815 view->lines = 0;
1816 view->lineno = 0;
1817 string_copy(view->vid, id);
1819 if (view->line) {
1820 int i;
1822 for (i = 0; i < view->lines; i++)
1823 if (view->line[i].data)
1824 free(view->line[i].data);
1826 free(view->line);
1827 view->line = NULL;
1828 }
1830 view->start_time = time(NULL);
1832 return TRUE;
1833 }
1835 static struct line *
1836 realloc_lines(struct view *view, size_t line_size)
1837 {
1838 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1840 if (!tmp)
1841 return NULL;
1843 view->line = tmp;
1844 view->line_size = line_size;
1845 return view->line;
1846 }
1848 static bool
1849 update_view(struct view *view)
1850 {
1851 char in_buffer[BUFSIZ];
1852 char out_buffer[BUFSIZ * 2];
1853 char *line;
1854 /* The number of lines to read. If too low it will cause too much
1855 * redrawing (and possible flickering), if too high responsiveness
1856 * will suffer. */
1857 unsigned long lines = view->height;
1858 int redraw_from = -1;
1860 if (!view->pipe)
1861 return TRUE;
1863 /* Only redraw if lines are visible. */
1864 if (view->offset + view->height >= view->lines)
1865 redraw_from = view->lines - view->offset;
1867 /* FIXME: This is probably not perfect for backgrounded views. */
1868 if (!realloc_lines(view, view->lines + lines))
1869 goto alloc_error;
1871 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1872 size_t linelen = strlen(line);
1874 if (linelen)
1875 line[linelen - 1] = 0;
1877 if (opt_iconv != ICONV_NONE) {
1878 char *inbuf = line;
1879 size_t inlen = linelen;
1881 char *outbuf = out_buffer;
1882 size_t outlen = sizeof(out_buffer);
1884 size_t ret;
1886 ret = iconv(opt_iconv, (const char **) &inbuf, &inlen, &outbuf, &outlen);
1887 if (ret != (size_t) -1) {
1888 line = out_buffer;
1889 linelen = strlen(out_buffer);
1890 }
1891 }
1893 if (!view->ops->read(view, line))
1894 goto alloc_error;
1896 if (lines-- == 1)
1897 break;
1898 }
1900 {
1901 int digits;
1903 lines = view->lines;
1904 for (digits = 0; lines; digits++)
1905 lines /= 10;
1907 /* Keep the displayed view in sync with line number scaling. */
1908 if (digits != view->digits) {
1909 view->digits = digits;
1910 redraw_from = 0;
1911 }
1912 }
1914 if (!view_is_displayed(view))
1915 goto check_pipe;
1917 if (view == VIEW(REQ_VIEW_TREE)) {
1918 /* Clear the view and redraw everything since the tree sorting
1919 * might have rearranged things. */
1920 redraw_view(view);
1922 } else if (redraw_from >= 0) {
1923 /* If this is an incremental update, redraw the previous line
1924 * since for commits some members could have changed when
1925 * loading the main view. */
1926 if (redraw_from > 0)
1927 redraw_from--;
1929 /* Incrementally draw avoids flickering. */
1930 redraw_view_from(view, redraw_from);
1931 }
1933 /* Update the title _after_ the redraw so that if the redraw picks up a
1934 * commit reference in view->ref it'll be available here. */
1935 update_view_title(view);
1937 check_pipe:
1938 if (ferror(view->pipe)) {
1939 report("Failed to read: %s", strerror(errno));
1940 goto end;
1942 } else if (feof(view->pipe)) {
1943 report("");
1944 goto end;
1945 }
1947 return TRUE;
1949 alloc_error:
1950 report("Allocation failure");
1952 end:
1953 view->ops->read(view, NULL);
1954 end_update(view);
1955 return FALSE;
1956 }
1959 /*
1960 * View opening
1961 */
1963 static void open_help_view(struct view *view)
1964 {
1965 char buf[BUFSIZ];
1966 int lines = ARRAY_SIZE(req_info) + 2;
1967 int i;
1969 if (view->lines > 0)
1970 return;
1972 for (i = 0; i < ARRAY_SIZE(req_info); i++)
1973 if (!req_info[i].request)
1974 lines++;
1976 view->line = calloc(lines, sizeof(*view->line));
1977 if (!view->line) {
1978 report("Allocation failure");
1979 return;
1980 }
1982 view->ops->read(view, "Quick reference for tig keybindings:");
1984 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
1985 char *key;
1987 if (!req_info[i].request) {
1988 view->ops->read(view, "");
1989 view->ops->read(view, req_info[i].help);
1990 continue;
1991 }
1993 key = get_key(req_info[i].request);
1994 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
1995 continue;
1997 view->ops->read(view, buf);
1998 }
1999 }
2001 enum open_flags {
2002 OPEN_DEFAULT = 0, /* Use default view switching. */
2003 OPEN_SPLIT = 1, /* Split current view. */
2004 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2005 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2006 };
2008 static void
2009 open_view(struct view *prev, enum request request, enum open_flags flags)
2010 {
2011 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2012 bool split = !!(flags & OPEN_SPLIT);
2013 bool reload = !!(flags & OPEN_RELOAD);
2014 struct view *view = VIEW(request);
2015 int nviews = displayed_views();
2016 struct view *base_view = display[0];
2018 if (view == prev && nviews == 1 && !reload) {
2019 report("Already in %s view", view->name);
2020 return;
2021 }
2023 if (view == VIEW(REQ_VIEW_HELP)) {
2024 open_help_view(view);
2026 } else if ((reload || strcmp(view->vid, view->id)) &&
2027 !begin_update(view)) {
2028 report("Failed to load %s view", view->name);
2029 return;
2030 }
2032 if (split) {
2033 display[1] = view;
2034 if (!backgrounded)
2035 current_view = 1;
2036 } else {
2037 /* Maximize the current view. */
2038 memset(display, 0, sizeof(display));
2039 current_view = 0;
2040 display[current_view] = view;
2041 }
2043 /* Resize the view when switching between split- and full-screen,
2044 * or when switching between two different full-screen views. */
2045 if (nviews != displayed_views() ||
2046 (nviews == 1 && base_view != display[0]))
2047 resize_display();
2049 if (split && prev->lineno - prev->offset >= prev->height) {
2050 /* Take the title line into account. */
2051 int lines = prev->lineno - prev->offset - prev->height + 1;
2053 /* Scroll the view that was split if the current line is
2054 * outside the new limited view. */
2055 do_scroll_view(prev, lines);
2056 }
2058 if (prev && view != prev) {
2059 if (split && !backgrounded) {
2060 /* "Blur" the previous view. */
2061 update_view_title(prev);
2062 }
2064 view->parent = prev;
2065 }
2067 if (view->pipe && view->lines == 0) {
2068 /* Clear the old view and let the incremental updating refill
2069 * the screen. */
2070 wclear(view->win);
2071 report("");
2072 } else {
2073 redraw_view(view);
2074 report("");
2075 }
2077 /* If the view is backgrounded the above calls to report()
2078 * won't redraw the view title. */
2079 if (backgrounded)
2080 update_view_title(view);
2081 }
2084 /*
2085 * User request switch noodle
2086 */
2088 static int
2089 view_driver(struct view *view, enum request request)
2090 {
2091 int i;
2093 switch (request) {
2094 case REQ_MOVE_UP:
2095 case REQ_MOVE_DOWN:
2096 case REQ_MOVE_PAGE_UP:
2097 case REQ_MOVE_PAGE_DOWN:
2098 case REQ_MOVE_FIRST_LINE:
2099 case REQ_MOVE_LAST_LINE:
2100 move_view(view, request);
2101 break;
2103 case REQ_SCROLL_LINE_DOWN:
2104 case REQ_SCROLL_LINE_UP:
2105 case REQ_SCROLL_PAGE_DOWN:
2106 case REQ_SCROLL_PAGE_UP:
2107 scroll_view(view, request);
2108 break;
2110 case REQ_VIEW_BLOB:
2111 if (!ref_blob[0]) {
2112 report("No file chosen, press 't' to open tree view");
2113 break;
2114 }
2115 /* Fall-through */
2116 case REQ_VIEW_MAIN:
2117 case REQ_VIEW_DIFF:
2118 case REQ_VIEW_LOG:
2119 case REQ_VIEW_TREE:
2120 case REQ_VIEW_HELP:
2121 case REQ_VIEW_PAGER:
2122 open_view(view, request, OPEN_DEFAULT);
2123 break;
2125 case REQ_NEXT:
2126 case REQ_PREVIOUS:
2127 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2129 if ((view == VIEW(REQ_VIEW_DIFF) &&
2130 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2131 (view == VIEW(REQ_VIEW_BLOB) &&
2132 view->parent == VIEW(REQ_VIEW_TREE))) {
2133 view = view->parent;
2134 move_view(view, request);
2135 if (view_is_displayed(view))
2136 update_view_title(view);
2137 } else {
2138 move_view(view, request);
2139 break;
2140 }
2141 /* Fall-through */
2143 case REQ_ENTER:
2144 if (!view->lines) {
2145 report("Nothing to enter");
2146 break;
2147 }
2148 return view->ops->enter(view, &view->line[view->lineno]);
2150 case REQ_VIEW_NEXT:
2151 {
2152 int nviews = displayed_views();
2153 int next_view = (current_view + 1) % nviews;
2155 if (next_view == current_view) {
2156 report("Only one view is displayed");
2157 break;
2158 }
2160 current_view = next_view;
2161 /* Blur out the title of the previous view. */
2162 update_view_title(view);
2163 report("");
2164 break;
2165 }
2166 case REQ_TOGGLE_LINENO:
2167 opt_line_number = !opt_line_number;
2168 redraw_display();
2169 break;
2171 case REQ_TOGGLE_REV_GRAPH:
2172 opt_rev_graph = !opt_rev_graph;
2173 redraw_display();
2174 break;
2176 case REQ_PROMPT:
2177 /* Always reload^Wrerun commands from the prompt. */
2178 open_view(view, opt_request, OPEN_RELOAD);
2179 break;
2181 case REQ_SEARCH:
2182 case REQ_SEARCH_BACK:
2183 search_view(view, request);
2184 break;
2186 case REQ_FIND_NEXT:
2187 case REQ_FIND_PREV:
2188 find_next(view, request);
2189 break;
2191 case REQ_STOP_LOADING:
2192 for (i = 0; i < ARRAY_SIZE(views); i++) {
2193 view = &views[i];
2194 if (view->pipe)
2195 report("Stopped loading the %s view", view->name),
2196 end_update(view);
2197 }
2198 break;
2200 case REQ_SHOW_VERSION:
2201 report("%s (built %s)", VERSION, __DATE__);
2202 return TRUE;
2204 case REQ_SCREEN_RESIZE:
2205 resize_display();
2206 /* Fall-through */
2207 case REQ_SCREEN_REDRAW:
2208 redraw_display();
2209 break;
2211 case REQ_NONE:
2212 doupdate();
2213 return TRUE;
2215 case REQ_VIEW_CLOSE:
2216 /* XXX: Mark closed views by letting view->parent point to the
2217 * view itself. Parents to closed view should never be
2218 * followed. */
2219 if (view->parent &&
2220 view->parent->parent != view->parent) {
2221 memset(display, 0, sizeof(display));
2222 current_view = 0;
2223 display[current_view] = view->parent;
2224 view->parent = view;
2225 resize_display();
2226 redraw_display();
2227 break;
2228 }
2229 /* Fall-through */
2230 case REQ_QUIT:
2231 return FALSE;
2233 default:
2234 /* An unknown key will show most commonly used commands. */
2235 report("Unknown key, press 'h' for help");
2236 return TRUE;
2237 }
2239 return TRUE;
2240 }
2243 /*
2244 * Pager backend
2245 */
2247 static bool
2248 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2249 {
2250 char *text = line->data;
2251 enum line_type type = line->type;
2252 int textlen = strlen(text);
2253 int attr;
2255 wmove(view->win, lineno, 0);
2257 if (selected) {
2258 type = LINE_CURSOR;
2259 wchgat(view->win, -1, 0, type, NULL);
2260 }
2262 attr = get_line_attr(type);
2263 wattrset(view->win, attr);
2265 if (opt_line_number || opt_tab_size < TABSIZE) {
2266 static char spaces[] = " ";
2267 int col_offset = 0, col = 0;
2269 if (opt_line_number) {
2270 unsigned long real_lineno = view->offset + lineno + 1;
2272 if (real_lineno == 1 ||
2273 (real_lineno % opt_num_interval) == 0) {
2274 wprintw(view->win, "%.*d", view->digits, real_lineno);
2276 } else {
2277 waddnstr(view->win, spaces,
2278 MIN(view->digits, STRING_SIZE(spaces)));
2279 }
2280 waddstr(view->win, ": ");
2281 col_offset = view->digits + 2;
2282 }
2284 while (text && col_offset + col < view->width) {
2285 int cols_max = view->width - col_offset - col;
2286 char *pos = text;
2287 int cols;
2289 if (*text == '\t') {
2290 text++;
2291 assert(sizeof(spaces) > TABSIZE);
2292 pos = spaces;
2293 cols = opt_tab_size - (col % opt_tab_size);
2295 } else {
2296 text = strchr(text, '\t');
2297 cols = line ? text - pos : strlen(pos);
2298 }
2300 waddnstr(view->win, pos, MIN(cols, cols_max));
2301 col += cols;
2302 }
2304 } else {
2305 int col = 0, pos = 0;
2307 for (; pos < textlen && col < view->width; pos++, col++)
2308 if (text[pos] == '\t')
2309 col += TABSIZE - (col % TABSIZE) - 1;
2311 waddnstr(view->win, text, pos);
2312 }
2314 return TRUE;
2315 }
2317 static bool
2318 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2319 {
2320 char refbuf[SIZEOF_STR];
2321 char *ref = NULL;
2322 FILE *pipe;
2324 if (!string_format(refbuf, "git describe %s", commit_id))
2325 return TRUE;
2327 pipe = popen(refbuf, "r");
2328 if (!pipe)
2329 return TRUE;
2331 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2332 ref = chomp_string(ref);
2333 pclose(pipe);
2335 if (!ref || !*ref)
2336 return TRUE;
2338 /* This is the only fatal call, since it can "corrupt" the buffer. */
2339 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2340 return FALSE;
2342 return TRUE;
2343 }
2345 static void
2346 add_pager_refs(struct view *view, struct line *line)
2347 {
2348 char buf[SIZEOF_STR];
2349 char *commit_id = line->data + STRING_SIZE("commit ");
2350 struct ref **refs;
2351 size_t bufpos = 0, refpos = 0;
2352 const char *sep = "Refs: ";
2353 bool is_tag = FALSE;
2355 assert(line->type == LINE_COMMIT);
2357 refs = get_refs(commit_id);
2358 if (!refs) {
2359 if (view == VIEW(REQ_VIEW_DIFF))
2360 goto try_add_describe_ref;
2361 return;
2362 }
2364 do {
2365 struct ref *ref = refs[refpos];
2366 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
2368 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2369 return;
2370 sep = ", ";
2371 if (ref->tag)
2372 is_tag = TRUE;
2373 } while (refs[refpos++]->next);
2375 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2376 try_add_describe_ref:
2377 /* Add <tag>-g<commit_id> "fake" reference. */
2378 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2379 return;
2380 }
2382 if (bufpos == 0)
2383 return;
2385 if (!realloc_lines(view, view->line_size + 1))
2386 return;
2388 line = &view->line[view->lines];
2389 line->data = strdup(buf);
2390 if (!line->data)
2391 return;
2393 line->type = LINE_PP_REFS;
2394 view->lines++;
2395 }
2397 static bool
2398 pager_read(struct view *view, char *data)
2399 {
2400 struct line *line = &view->line[view->lines];
2402 if (!data)
2403 return TRUE;
2405 line->data = strdup(data);
2406 if (!line->data)
2407 return FALSE;
2409 line->type = get_line_type(line->data);
2410 view->lines++;
2412 if (line->type == LINE_COMMIT &&
2413 (view == VIEW(REQ_VIEW_DIFF) ||
2414 view == VIEW(REQ_VIEW_LOG)))
2415 add_pager_refs(view, line);
2417 return TRUE;
2418 }
2420 static bool
2421 pager_enter(struct view *view, struct line *line)
2422 {
2423 int split = 0;
2425 if (line->type == LINE_COMMIT &&
2426 (view == VIEW(REQ_VIEW_LOG) ||
2427 view == VIEW(REQ_VIEW_PAGER))) {
2428 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2429 split = 1;
2430 }
2432 /* Always scroll the view even if it was split. That way
2433 * you can use Enter to scroll through the log view and
2434 * split open each commit diff. */
2435 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2437 /* FIXME: A minor workaround. Scrolling the view will call report("")
2438 * but if we are scrolling a non-current view this won't properly
2439 * update the view title. */
2440 if (split)
2441 update_view_title(view);
2443 return TRUE;
2444 }
2446 static bool
2447 pager_grep(struct view *view, struct line *line)
2448 {
2449 regmatch_t pmatch;
2450 char *text = line->data;
2452 if (!*text)
2453 return FALSE;
2455 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2456 return FALSE;
2458 return TRUE;
2459 }
2461 static void
2462 pager_select(struct view *view, struct line *line)
2463 {
2464 if (line->type == LINE_COMMIT) {
2465 char *text = line->data;
2467 string_copy(view->ref, text + STRING_SIZE("commit "));
2468 string_copy(ref_commit, view->ref);
2469 }
2470 }
2472 static struct view_ops pager_ops = {
2473 "line",
2474 pager_draw,
2475 pager_read,
2476 pager_enter,
2477 pager_grep,
2478 pager_select,
2479 };
2482 /*
2483 * Tree backend
2484 */
2486 /* Parse output from git-ls-tree(1):
2487 *
2488 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2489 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2490 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2491 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2492 */
2494 #define SIZEOF_TREE_ATTR \
2495 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2497 #define TREE_UP_FORMAT "040000 tree %s\t.."
2499 static int
2500 tree_compare_entry(enum line_type type1, char *name1,
2501 enum line_type type2, char *name2)
2502 {
2503 if (type1 != type2) {
2504 if (type1 == LINE_TREE_DIR)
2505 return -1;
2506 return 1;
2507 }
2509 return strcmp(name1, name2);
2510 }
2512 static bool
2513 tree_read(struct view *view, char *text)
2514 {
2515 size_t textlen = text ? strlen(text) : 0;
2516 char buf[SIZEOF_STR];
2517 unsigned long pos;
2518 enum line_type type;
2519 bool first_read = view->lines == 0;
2521 if (textlen <= SIZEOF_TREE_ATTR)
2522 return FALSE;
2524 type = text[STRING_SIZE("100644 ")] == 't'
2525 ? LINE_TREE_DIR : LINE_TREE_FILE;
2527 if (first_read) {
2528 /* Add path info line */
2529 if (string_format(buf, "Directory path /%s", opt_path) &&
2530 realloc_lines(view, view->line_size + 1) &&
2531 pager_read(view, buf))
2532 view->line[view->lines - 1].type = LINE_DEFAULT;
2533 else
2534 return FALSE;
2536 /* Insert "link" to parent directory. */
2537 if (*opt_path &&
2538 string_format(buf, TREE_UP_FORMAT, view->ref) &&
2539 realloc_lines(view, view->line_size + 1) &&
2540 pager_read(view, buf))
2541 view->line[view->lines - 1].type = LINE_TREE_DIR;
2542 else if (*opt_path)
2543 return FALSE;
2544 }
2546 /* Strip the path part ... */
2547 if (*opt_path) {
2548 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2549 size_t striplen = strlen(opt_path);
2550 char *path = text + SIZEOF_TREE_ATTR;
2552 if (pathlen > striplen)
2553 memmove(path, path + striplen,
2554 pathlen - striplen + 1);
2555 }
2557 /* Skip "Directory ..." and ".." line. */
2558 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2559 struct line *line = &view->line[pos];
2560 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2561 char *path2 = text + SIZEOF_TREE_ATTR;
2562 int cmp = tree_compare_entry(line->type, path1, type, path2);
2564 if (cmp <= 0)
2565 continue;
2567 text = strdup(text);
2568 if (!text)
2569 return FALSE;
2571 if (view->lines > pos)
2572 memmove(&view->line[pos + 1], &view->line[pos],
2573 (view->lines - pos) * sizeof(*line));
2575 line = &view->line[pos];
2576 line->data = text;
2577 line->type = type;
2578 view->lines++;
2579 return TRUE;
2580 }
2582 if (!pager_read(view, text))
2583 return FALSE;
2585 /* Move the current line to the first tree entry. */
2586 if (first_read)
2587 view->lineno++;
2589 view->line[view->lines - 1].type = type;
2590 return TRUE;
2591 }
2593 static bool
2594 tree_enter(struct view *view, struct line *line)
2595 {
2596 enum open_flags flags;
2597 enum request request;
2599 switch (line->type) {
2600 case LINE_TREE_DIR:
2601 /* Depending on whether it is a subdir or parent (updir?) link
2602 * mangle the path buffer. */
2603 if (line == &view->line[1] && *opt_path) {
2604 size_t path_len = strlen(opt_path);
2605 char *dirsep = opt_path + path_len - 1;
2607 while (dirsep > opt_path && dirsep[-1] != '/')
2608 dirsep--;
2610 dirsep[0] = 0;
2612 } else {
2613 size_t pathlen = strlen(opt_path);
2614 size_t origlen = pathlen;
2615 char *data = line->data;
2616 char *basename = data + SIZEOF_TREE_ATTR;
2618 if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
2619 opt_path[origlen] = 0;
2620 return TRUE;
2621 }
2622 }
2624 /* Trees and subtrees share the same ID, so they are not not
2625 * unique like blobs. */
2626 flags = OPEN_RELOAD;
2627 request = REQ_VIEW_TREE;
2628 break;
2630 case LINE_TREE_FILE:
2631 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2632 request = REQ_VIEW_BLOB;
2633 break;
2635 default:
2636 return TRUE;
2637 }
2639 open_view(view, request, flags);
2641 return TRUE;
2642 }
2644 static void
2645 tree_select(struct view *view, struct line *line)
2646 {
2647 char *text = line->data;
2649 text += STRING_SIZE("100644 blob ");
2651 if (line->type == LINE_TREE_FILE) {
2652 string_ncopy(ref_blob, text, 40);
2653 /* Also update the blob view's ref, since all there must always
2654 * be in sync. */
2655 string_copy(VIEW(REQ_VIEW_BLOB)->ref, ref_blob);
2657 } else if (line->type != LINE_TREE_DIR) {
2658 return;
2659 }
2661 string_ncopy(view->ref, text, 40);
2662 }
2664 static struct view_ops tree_ops = {
2665 "file",
2666 pager_draw,
2667 tree_read,
2668 tree_enter,
2669 pager_grep,
2670 tree_select,
2671 };
2673 static bool
2674 blob_read(struct view *view, char *line)
2675 {
2676 bool state = pager_read(view, line);
2678 if (state == TRUE)
2679 view->line[view->lines - 1].type = LINE_DEFAULT;
2681 return state;
2682 }
2684 static struct view_ops blob_ops = {
2685 "line",
2686 pager_draw,
2687 blob_read,
2688 pager_enter,
2689 pager_grep,
2690 pager_select,
2691 };
2694 /*
2695 * Revision graph
2696 */
2698 struct commit {
2699 char id[SIZEOF_REV]; /* SHA1 ID. */
2700 char title[128]; /* First line of the commit message. */
2701 char author[75]; /* Author of the commit. */
2702 struct tm time; /* Date from the author ident. */
2703 struct ref **refs; /* Repository references. */
2704 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
2705 size_t graph_size; /* The width of the graph array. */
2706 };
2708 /* Size of rev graph with no "padding" columns */
2709 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
2711 struct rev_graph {
2712 struct rev_graph *prev, *next, *parents;
2713 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
2714 size_t size;
2715 struct commit *commit;
2716 size_t pos;
2717 };
2719 /* Parents of the commit being visualized. */
2720 static struct rev_graph graph_parents[4];
2722 /* The current stack of revisions on the graph. */
2723 static struct rev_graph graph_stacks[4] = {
2724 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
2725 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
2726 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
2727 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
2728 };
2730 static inline bool
2731 graph_parent_is_merge(struct rev_graph *graph)
2732 {
2733 return graph->parents->size > 1;
2734 }
2736 static inline void
2737 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
2738 {
2739 struct commit *commit = graph->commit;
2741 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
2742 commit->graph[commit->graph_size++] = symbol;
2743 }
2745 static void
2746 done_rev_graph(struct rev_graph *graph)
2747 {
2748 if (graph_parent_is_merge(graph) &&
2749 graph->pos < graph->size - 1 &&
2750 graph->next->size == graph->size + graph->parents->size - 1) {
2751 size_t i = graph->pos + graph->parents->size - 1;
2753 graph->commit->graph_size = i * 2;
2754 while (i < graph->next->size - 1) {
2755 append_to_rev_graph(graph, ' ');
2756 append_to_rev_graph(graph, '\\');
2757 i++;
2758 }
2759 }
2761 graph->size = graph->pos = 0;
2762 graph->commit = NULL;
2763 memset(graph->parents, 0, sizeof(*graph->parents));
2764 }
2766 static void
2767 push_rev_graph(struct rev_graph *graph, char *parent)
2768 {
2769 int i;
2771 /* "Collapse" duplicate parents lines.
2772 *
2773 * FIXME: This needs to also update update the drawn graph but
2774 * for now it just serves as a method for pruning graph lines. */
2775 for (i = 0; i < graph->size; i++)
2776 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
2777 return;
2779 if (graph->size < SIZEOF_REVITEMS) {
2780 string_ncopy(graph->rev[graph->size++], parent, SIZEOF_REV);
2781 }
2782 }
2784 static chtype
2785 get_rev_graph_symbol(struct rev_graph *graph)
2786 {
2787 chtype symbol;
2789 if (graph->parents->size == 0)
2790 symbol = REVGRAPH_INIT;
2791 else if (graph_parent_is_merge(graph))
2792 symbol = REVGRAPH_MERGE;
2793 else if (graph->pos >= graph->size)
2794 symbol = REVGRAPH_BRANCH;
2795 else
2796 symbol = REVGRAPH_COMMIT;
2798 return symbol;
2799 }
2801 static void
2802 draw_rev_graph(struct rev_graph *graph)
2803 {
2804 struct rev_filler {
2805 chtype separator, line;
2806 };
2807 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
2808 static struct rev_filler fillers[] = {
2809 { ' ', REVGRAPH_LINE },
2810 { '`', '.' },
2811 { '\'', ' ' },
2812 { '/', ' ' },
2813 };
2814 chtype symbol = get_rev_graph_symbol(graph);
2815 struct rev_filler *filler;
2816 size_t i;
2818 filler = &fillers[DEFAULT];
2820 for (i = 0; i < graph->pos; i++) {
2821 append_to_rev_graph(graph, filler->line);
2822 if (graph_parent_is_merge(graph->prev) &&
2823 graph->prev->pos == i)
2824 filler = &fillers[RSHARP];
2826 append_to_rev_graph(graph, filler->separator);
2827 }
2829 /* Place the symbol for this revision. */
2830 append_to_rev_graph(graph, symbol);
2832 if (graph->prev->size > graph->size)
2833 filler = &fillers[RDIAG];
2834 else
2835 filler = &fillers[DEFAULT];
2837 i++;
2839 for (; i < graph->size; i++) {
2840 append_to_rev_graph(graph, filler->separator);
2841 append_to_rev_graph(graph, filler->line);
2842 if (graph_parent_is_merge(graph->prev) &&
2843 i < graph->prev->pos + graph->parents->size)
2844 filler = &fillers[RSHARP];
2845 if (graph->prev->size > graph->size)
2846 filler = &fillers[LDIAG];
2847 }
2849 if (graph->prev->size > graph->size) {
2850 append_to_rev_graph(graph, filler->separator);
2851 if (filler->line != ' ')
2852 append_to_rev_graph(graph, filler->line);
2853 }
2854 }
2856 /* Prepare the next rev graph */
2857 static void
2858 prepare_rev_graph(struct rev_graph *graph)
2859 {
2860 size_t i;
2862 /* First, traverse all lines of revisions up to the active one. */
2863 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
2864 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
2865 break;
2867 push_rev_graph(graph->next, graph->rev[graph->pos]);
2868 }
2870 /* Interleave the new revision parent(s). */
2871 for (i = 0; i < graph->parents->size; i++)
2872 push_rev_graph(graph->next, graph->parents->rev[i]);
2874 /* Lastly, put any remaining revisions. */
2875 for (i = graph->pos + 1; i < graph->size; i++)
2876 push_rev_graph(graph->next, graph->rev[i]);
2877 }
2879 static void
2880 update_rev_graph(struct rev_graph *graph)
2881 {
2882 /* If this is the finalizing update ... */
2883 if (graph->commit)
2884 prepare_rev_graph(graph);
2886 /* Graph visualization needs a one rev look-ahead,
2887 * so the first update doesn't visualize anything. */
2888 if (!graph->prev->commit)
2889 return;
2891 draw_rev_graph(graph->prev);
2892 done_rev_graph(graph->prev->prev);
2893 }
2896 /*
2897 * Main view backend
2898 */
2900 static bool
2901 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2902 {
2903 char buf[DATE_COLS + 1];
2904 struct commit *commit = line->data;
2905 enum line_type type;
2906 int col = 0;
2907 size_t timelen;
2908 size_t authorlen;
2909 int trimmed = 1;
2911 if (!*commit->author)
2912 return FALSE;
2914 wmove(view->win, lineno, col);
2916 if (selected) {
2917 type = LINE_CURSOR;
2918 wattrset(view->win, get_line_attr(type));
2919 wchgat(view->win, -1, 0, type, NULL);
2921 } else {
2922 type = LINE_MAIN_COMMIT;
2923 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
2924 }
2926 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
2927 waddnstr(view->win, buf, timelen);
2928 waddstr(view->win, " ");
2930 col += DATE_COLS;
2931 wmove(view->win, lineno, col);
2932 if (type != LINE_CURSOR)
2933 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
2935 if (opt_utf8) {
2936 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
2937 } else {
2938 authorlen = strlen(commit->author);
2939 if (authorlen > AUTHOR_COLS - 2) {
2940 authorlen = AUTHOR_COLS - 2;
2941 trimmed = 1;
2942 }
2943 }
2945 if (trimmed) {
2946 waddnstr(view->win, commit->author, authorlen);
2947 if (type != LINE_CURSOR)
2948 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
2949 waddch(view->win, '~');
2950 } else {
2951 waddstr(view->win, commit->author);
2952 }
2954 col += AUTHOR_COLS;
2955 if (type != LINE_CURSOR)
2956 wattrset(view->win, A_NORMAL);
2958 if (opt_rev_graph && commit->graph_size) {
2959 size_t i;
2961 wmove(view->win, lineno, col);
2962 /* Using waddch() instead of waddnstr() ensures that
2963 * they'll be rendered correctly for the cursor line. */
2964 for (i = 0; i < commit->graph_size; i++)
2965 waddch(view->win, commit->graph[i]);
2967 waddch(view->win, ' ');
2968 col += commit->graph_size + 1;
2969 }
2971 wmove(view->win, lineno, col);
2973 if (commit->refs) {
2974 size_t i = 0;
2976 do {
2977 if (type == LINE_CURSOR)
2978 ;
2979 else if (commit->refs[i]->tag)
2980 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
2981 else
2982 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
2983 waddstr(view->win, "[");
2984 waddstr(view->win, commit->refs[i]->name);
2985 waddstr(view->win, "]");
2986 if (type != LINE_CURSOR)
2987 wattrset(view->win, A_NORMAL);
2988 waddstr(view->win, " ");
2989 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
2990 } while (commit->refs[i++]->next);
2991 }
2993 if (type != LINE_CURSOR)
2994 wattrset(view->win, get_line_attr(type));
2996 {
2997 int titlelen = strlen(commit->title);
2999 if (col + titlelen > view->width)
3000 titlelen = view->width - col;
3002 waddnstr(view->win, commit->title, titlelen);
3003 }
3005 return TRUE;
3006 }
3008 /* Reads git log --pretty=raw output and parses it into the commit struct. */
3009 static bool
3010 main_read(struct view *view, char *line)
3011 {
3012 static struct rev_graph *graph = graph_stacks;
3013 enum line_type type;
3014 struct commit *commit = view->lines
3015 ? view->line[view->lines - 1].data : NULL;
3017 if (!line) {
3018 update_rev_graph(graph);
3019 return TRUE;
3020 }
3022 type = get_line_type(line);
3024 switch (type) {
3025 case LINE_COMMIT:
3026 commit = calloc(1, sizeof(struct commit));
3027 if (!commit)
3028 return FALSE;
3030 line += STRING_SIZE("commit ");
3032 view->line[view->lines++].data = commit;
3033 string_copy(commit->id, line);
3034 commit->refs = get_refs(commit->id);
3035 graph->commit = commit;
3036 break;
3038 case LINE_PARENT:
3039 if (commit) {
3040 line += STRING_SIZE("parent ");
3041 push_rev_graph(graph->parents, line);
3042 }
3043 break;
3045 case LINE_AUTHOR:
3046 {
3047 char *ident = line + STRING_SIZE("author ");
3048 char *end = strchr(ident, '<');
3050 if (!commit)
3051 break;
3053 update_rev_graph(graph);
3054 graph = graph->next;
3056 if (end) {
3057 char *email = end + 1;
3059 for (; end > ident && isspace(end[-1]); end--) ;
3061 if (end == ident && *email) {
3062 ident = email;
3063 end = strchr(ident, '>');
3064 for (; end > ident && isspace(end[-1]); end--) ;
3065 }
3066 *end = 0;
3067 }
3069 /* End is NULL or ident meaning there's no author. */
3070 if (end <= ident)
3071 ident = "Unknown";
3073 string_copy(commit->author, ident);
3075 /* Parse epoch and timezone */
3076 if (end) {
3077 char *secs = strchr(end + 1, '>');
3078 char *zone;
3079 time_t time;
3081 if (!secs || secs[1] != ' ')
3082 break;
3084 secs += 2;
3085 time = (time_t) atol(secs);
3086 zone = strchr(secs, ' ');
3087 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
3088 long tz;
3090 zone++;
3091 tz = ('0' - zone[1]) * 60 * 60 * 10;
3092 tz += ('0' - zone[2]) * 60 * 60;
3093 tz += ('0' - zone[3]) * 60;
3094 tz += ('0' - zone[4]) * 60;
3096 if (zone[0] == '-')
3097 tz = -tz;
3099 time -= tz;
3100 }
3101 gmtime_r(&time, &commit->time);
3102 }
3103 break;
3104 }
3105 default:
3106 if (!commit)
3107 break;
3109 /* Fill in the commit title if it has not already been set. */
3110 if (commit->title[0])
3111 break;
3113 /* Require titles to start with a non-space character at the
3114 * offset used by git log. */
3115 if (strncmp(line, " ", 4))
3116 break;
3117 line += 4;
3118 /* Well, if the title starts with a whitespace character,
3119 * try to be forgiving. Otherwise we end up with no title. */
3120 while (isspace(*line))
3121 line++;
3122 if (*line == '\0')
3123 break;
3124 /* FIXME: More graceful handling of titles; append "..." to
3125 * shortened titles, etc. */
3127 string_copy(commit->title, line);
3128 }
3130 return TRUE;
3131 }
3133 static bool
3134 main_enter(struct view *view, struct line *line)
3135 {
3136 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3138 open_view(view, REQ_VIEW_DIFF, flags);
3139 return TRUE;
3140 }
3142 static bool
3143 main_grep(struct view *view, struct line *line)
3144 {
3145 struct commit *commit = line->data;
3146 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
3147 char buf[DATE_COLS + 1];
3148 regmatch_t pmatch;
3150 for (state = S_TITLE; state < S_END; state++) {
3151 char *text;
3153 switch (state) {
3154 case S_TITLE: text = commit->title; break;
3155 case S_AUTHOR: text = commit->author; break;
3156 case S_DATE:
3157 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
3158 continue;
3159 text = buf;
3160 break;
3162 default:
3163 return FALSE;
3164 }
3166 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3167 return TRUE;
3168 }
3170 return FALSE;
3171 }
3173 static void
3174 main_select(struct view *view, struct line *line)
3175 {
3176 struct commit *commit = line->data;
3178 string_copy(view->ref, commit->id);
3179 string_copy(ref_commit, view->ref);
3180 }
3182 static struct view_ops main_ops = {
3183 "commit",
3184 main_draw,
3185 main_read,
3186 main_enter,
3187 main_grep,
3188 main_select,
3189 };
3192 /*
3193 * Unicode / UTF-8 handling
3194 *
3195 * NOTE: Much of the following code for dealing with unicode is derived from
3196 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
3197 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
3198 */
3200 /* I've (over)annotated a lot of code snippets because I am not entirely
3201 * confident that the approach taken by this small UTF-8 interface is correct.
3202 * --jonas */
3204 static inline int
3205 unicode_width(unsigned long c)
3206 {
3207 if (c >= 0x1100 &&
3208 (c <= 0x115f /* Hangul Jamo */
3209 || c == 0x2329
3210 || c == 0x232a
3211 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
3212 /* CJK ... Yi */
3213 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
3214 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
3215 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
3216 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
3217 || (c >= 0xffe0 && c <= 0xffe6)
3218 || (c >= 0x20000 && c <= 0x2fffd)
3219 || (c >= 0x30000 && c <= 0x3fffd)))
3220 return 2;
3222 return 1;
3223 }
3225 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
3226 * Illegal bytes are set one. */
3227 static const unsigned char utf8_bytes[256] = {
3228 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,
3229 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,
3230 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,
3231 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,
3232 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,
3233 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,
3234 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,
3235 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,
3236 };
3238 /* Decode UTF-8 multi-byte representation into a unicode character. */
3239 static inline unsigned long
3240 utf8_to_unicode(const char *string, size_t length)
3241 {
3242 unsigned long unicode;
3244 switch (length) {
3245 case 1:
3246 unicode = string[0];
3247 break;
3248 case 2:
3249 unicode = (string[0] & 0x1f) << 6;
3250 unicode += (string[1] & 0x3f);
3251 break;
3252 case 3:
3253 unicode = (string[0] & 0x0f) << 12;
3254 unicode += ((string[1] & 0x3f) << 6);
3255 unicode += (string[2] & 0x3f);
3256 break;
3257 case 4:
3258 unicode = (string[0] & 0x0f) << 18;
3259 unicode += ((string[1] & 0x3f) << 12);
3260 unicode += ((string[2] & 0x3f) << 6);
3261 unicode += (string[3] & 0x3f);
3262 break;
3263 case 5:
3264 unicode = (string[0] & 0x0f) << 24;
3265 unicode += ((string[1] & 0x3f) << 18);
3266 unicode += ((string[2] & 0x3f) << 12);
3267 unicode += ((string[3] & 0x3f) << 6);
3268 unicode += (string[4] & 0x3f);
3269 break;
3270 case 6:
3271 unicode = (string[0] & 0x01) << 30;
3272 unicode += ((string[1] & 0x3f) << 24);
3273 unicode += ((string[2] & 0x3f) << 18);
3274 unicode += ((string[3] & 0x3f) << 12);
3275 unicode += ((string[4] & 0x3f) << 6);
3276 unicode += (string[5] & 0x3f);
3277 break;
3278 default:
3279 die("Invalid unicode length");
3280 }
3282 /* Invalid characters could return the special 0xfffd value but NUL
3283 * should be just as good. */
3284 return unicode > 0xffff ? 0 : unicode;
3285 }
3287 /* Calculates how much of string can be shown within the given maximum width
3288 * and sets trimmed parameter to non-zero value if all of string could not be
3289 * shown.
3290 *
3291 * Additionally, adds to coloffset how many many columns to move to align with
3292 * the expected position. Takes into account how multi-byte and double-width
3293 * characters will effect the cursor position.
3294 *
3295 * Returns the number of bytes to output from string to satisfy max_width. */
3296 static size_t
3297 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3298 {
3299 const char *start = string;
3300 const char *end = strchr(string, '\0');
3301 size_t mbwidth = 0;
3302 size_t width = 0;
3304 *trimmed = 0;
3306 while (string < end) {
3307 int c = *(unsigned char *) string;
3308 unsigned char bytes = utf8_bytes[c];
3309 size_t ucwidth;
3310 unsigned long unicode;
3312 if (string + bytes > end)
3313 break;
3315 /* Change representation to figure out whether
3316 * it is a single- or double-width character. */
3318 unicode = utf8_to_unicode(string, bytes);
3319 /* FIXME: Graceful handling of invalid unicode character. */
3320 if (!unicode)
3321 break;
3323 ucwidth = unicode_width(unicode);
3324 width += ucwidth;
3325 if (width > max_width) {
3326 *trimmed = 1;
3327 break;
3328 }
3330 /* The column offset collects the differences between the
3331 * number of bytes encoding a character and the number of
3332 * columns will be used for rendering said character.
3333 *
3334 * So if some character A is encoded in 2 bytes, but will be
3335 * represented on the screen using only 1 byte this will and up
3336 * adding 1 to the multi-byte column offset.
3337 *
3338 * Assumes that no double-width character can be encoding in
3339 * less than two bytes. */
3340 if (bytes > ucwidth)
3341 mbwidth += bytes - ucwidth;
3343 string += bytes;
3344 }
3346 *coloffset += mbwidth;
3348 return string - start;
3349 }
3352 /*
3353 * Status management
3354 */
3356 /* Whether or not the curses interface has been initialized. */
3357 static bool cursed = FALSE;
3359 /* The status window is used for polling keystrokes. */
3360 static WINDOW *status_win;
3362 static bool status_empty = TRUE;
3364 /* Update status and title window. */
3365 static void
3366 report(const char *msg, ...)
3367 {
3368 struct view *view = display[current_view];
3370 if (input_mode)
3371 return;
3373 if (!status_empty || *msg) {
3374 va_list args;
3376 va_start(args, msg);
3378 wmove(status_win, 0, 0);
3379 if (*msg) {
3380 vwprintw(status_win, msg, args);
3381 status_empty = FALSE;
3382 } else {
3383 status_empty = TRUE;
3384 }
3385 wclrtoeol(status_win);
3386 wrefresh(status_win);
3388 va_end(args);
3389 }
3391 update_view_title(view);
3392 update_display_cursor(view);
3393 }
3395 /* Controls when nodelay should be in effect when polling user input. */
3396 static void
3397 set_nonblocking_input(bool loading)
3398 {
3399 static unsigned int loading_views;
3401 if ((loading == FALSE && loading_views-- == 1) ||
3402 (loading == TRUE && loading_views++ == 0))
3403 nodelay(status_win, loading);
3404 }
3406 static void
3407 init_display(void)
3408 {
3409 int x, y;
3411 /* Initialize the curses library */
3412 if (isatty(STDIN_FILENO)) {
3413 cursed = !!initscr();
3414 } else {
3415 /* Leave stdin and stdout alone when acting as a pager. */
3416 FILE *io = fopen("/dev/tty", "r+");
3418 if (!io)
3419 die("Failed to open /dev/tty");
3420 cursed = !!newterm(NULL, io, io);
3421 }
3423 if (!cursed)
3424 die("Failed to initialize curses");
3426 nonl(); /* Tell curses not to do NL->CR/NL on output */
3427 cbreak(); /* Take input chars one at a time, no wait for \n */
3428 noecho(); /* Don't echo input */
3429 leaveok(stdscr, TRUE);
3431 if (has_colors())
3432 init_colors();
3434 getmaxyx(stdscr, y, x);
3435 status_win = newwin(1, 0, y - 1, 0);
3436 if (!status_win)
3437 die("Failed to create status window");
3439 /* Enable keyboard mapping */
3440 keypad(status_win, TRUE);
3441 wbkgdset(status_win, get_line_attr(LINE_STATUS));
3442 }
3444 static char *
3445 read_prompt(const char *prompt)
3446 {
3447 enum { READING, STOP, CANCEL } status = READING;
3448 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
3449 int pos = 0;
3451 while (status == READING) {
3452 struct view *view;
3453 int i, key;
3455 input_mode = TRUE;
3457 foreach_view (view, i)
3458 update_view(view);
3460 input_mode = FALSE;
3462 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
3463 wclrtoeol(status_win);
3465 /* Refresh, accept single keystroke of input */
3466 key = wgetch(status_win);
3467 switch (key) {
3468 case KEY_RETURN:
3469 case KEY_ENTER:
3470 case '\n':
3471 status = pos ? STOP : CANCEL;
3472 break;
3474 case KEY_BACKSPACE:
3475 if (pos > 0)
3476 pos--;
3477 else
3478 status = CANCEL;
3479 break;
3481 case KEY_ESC:
3482 status = CANCEL;
3483 break;
3485 case ERR:
3486 break;
3488 default:
3489 if (pos >= sizeof(buf)) {
3490 report("Input string too long");
3491 return NULL;
3492 }
3494 if (isprint(key))
3495 buf[pos++] = (char) key;
3496 }
3497 }
3499 /* Clear the status window */
3500 status_empty = FALSE;
3501 report("");
3503 if (status == CANCEL)
3504 return NULL;
3506 buf[pos++] = 0;
3508 return buf;
3509 }
3511 /*
3512 * Repository references
3513 */
3515 static struct ref *refs;
3516 static size_t refs_size;
3518 /* Id <-> ref store */
3519 static struct ref ***id_refs;
3520 static size_t id_refs_size;
3522 static struct ref **
3523 get_refs(char *id)
3524 {
3525 struct ref ***tmp_id_refs;
3526 struct ref **ref_list = NULL;
3527 size_t ref_list_size = 0;
3528 size_t i;
3530 for (i = 0; i < id_refs_size; i++)
3531 if (!strcmp(id, id_refs[i][0]->id))
3532 return id_refs[i];
3534 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
3535 if (!tmp_id_refs)
3536 return NULL;
3538 id_refs = tmp_id_refs;
3540 for (i = 0; i < refs_size; i++) {
3541 struct ref **tmp;
3543 if (strcmp(id, refs[i].id))
3544 continue;
3546 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
3547 if (!tmp) {
3548 if (ref_list)
3549 free(ref_list);
3550 return NULL;
3551 }
3553 ref_list = tmp;
3554 if (ref_list_size > 0)
3555 ref_list[ref_list_size - 1]->next = 1;
3556 ref_list[ref_list_size] = &refs[i];
3558 /* XXX: The properties of the commit chains ensures that we can
3559 * safely modify the shared ref. The repo references will
3560 * always be similar for the same id. */
3561 ref_list[ref_list_size]->next = 0;
3562 ref_list_size++;
3563 }
3565 if (ref_list)
3566 id_refs[id_refs_size++] = ref_list;
3568 return ref_list;
3569 }
3571 static int
3572 read_ref(char *id, int idlen, char *name, int namelen)
3573 {
3574 struct ref *ref;
3575 bool tag = FALSE;
3577 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
3578 /* Commits referenced by tags has "^{}" appended. */
3579 if (name[namelen - 1] != '}')
3580 return OK;
3582 while (namelen > 0 && name[namelen] != '^')
3583 namelen--;
3585 tag = TRUE;
3586 namelen -= STRING_SIZE("refs/tags/");
3587 name += STRING_SIZE("refs/tags/");
3589 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
3590 namelen -= STRING_SIZE("refs/heads/");
3591 name += STRING_SIZE("refs/heads/");
3593 } else if (!strcmp(name, "HEAD")) {
3594 return OK;
3595 }
3597 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
3598 if (!refs)
3599 return ERR;
3601 ref = &refs[refs_size++];
3602 ref->name = malloc(namelen + 1);
3603 if (!ref->name)
3604 return ERR;
3606 strncpy(ref->name, name, namelen);
3607 ref->name[namelen] = 0;
3608 ref->tag = tag;
3609 string_copy(ref->id, id);
3611 return OK;
3612 }
3614 static int
3615 load_refs(void)
3616 {
3617 const char *cmd_env = getenv("TIG_LS_REMOTE");
3618 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
3620 return read_properties(popen(cmd, "r"), "\t", read_ref);
3621 }
3623 static int
3624 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
3625 {
3626 if (!strcmp(name, "i18n.commitencoding"))
3627 string_copy(opt_encoding, value);
3629 return OK;
3630 }
3632 static int
3633 load_repo_config(void)
3634 {
3635 return read_properties(popen("git repo-config --list", "r"),
3636 "=", read_repo_config_option);
3637 }
3639 static int
3640 read_properties(FILE *pipe, const char *separators,
3641 int (*read_property)(char *, int, char *, int))
3642 {
3643 char buffer[BUFSIZ];
3644 char *name;
3645 int state = OK;
3647 if (!pipe)
3648 return ERR;
3650 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
3651 char *value;
3652 size_t namelen;
3653 size_t valuelen;
3655 name = chomp_string(name);
3656 namelen = strcspn(name, separators);
3658 if (name[namelen]) {
3659 name[namelen] = 0;
3660 value = chomp_string(name + namelen + 1);
3661 valuelen = strlen(value);
3663 } else {
3664 value = "";
3665 valuelen = 0;
3666 }
3668 state = read_property(name, namelen, value, valuelen);
3669 }
3671 if (state != ERR && ferror(pipe))
3672 state = ERR;
3674 pclose(pipe);
3676 return state;
3677 }
3680 /*
3681 * Main
3682 */
3684 static void __NORETURN
3685 quit(int sig)
3686 {
3687 /* XXX: Restore tty modes and let the OS cleanup the rest! */
3688 if (cursed)
3689 endwin();
3690 exit(0);
3691 }
3693 static void __NORETURN
3694 die(const char *err, ...)
3695 {
3696 va_list args;
3698 endwin();
3700 va_start(args, err);
3701 fputs("tig: ", stderr);
3702 vfprintf(stderr, err, args);
3703 fputs("\n", stderr);
3704 va_end(args);
3706 exit(1);
3707 }
3709 int
3710 main(int argc, char *argv[])
3711 {
3712 struct view *view;
3713 enum request request;
3714 size_t i;
3716 signal(SIGINT, quit);
3718 if (setlocale(LC_ALL, "")) {
3719 string_copy(opt_codeset, nl_langinfo(CODESET));
3720 }
3722 if (load_options() == ERR)
3723 die("Failed to load user config.");
3725 /* Load the repo config file so options can be overwritten from
3726 * the command line. */
3727 if (load_repo_config() == ERR)
3728 die("Failed to load repo config.");
3730 if (!parse_options(argc, argv))
3731 return 0;
3733 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
3734 opt_iconv = iconv_open(opt_codeset, opt_encoding);
3735 if (opt_iconv == ICONV_NONE)
3736 die("Failed to initialize character set conversion");
3737 }
3739 if (load_refs() == ERR)
3740 die("Failed to load refs.");
3742 /* Require a git repository unless when running in pager mode. */
3743 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
3744 die("Not a git repository");
3746 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
3747 view->cmd_env = getenv(view->cmd_env);
3749 request = opt_request;
3751 init_display();
3753 while (view_driver(display[current_view], request)) {
3754 int key;
3755 int i;
3757 foreach_view (view, i)
3758 update_view(view);
3760 /* Refresh, accept single keystroke of input */
3761 key = wgetch(status_win);
3763 /* wgetch() with nodelay() enabled returns ERR when there's no
3764 * input. */
3765 if (key == ERR) {
3766 request = REQ_NONE;
3767 continue;
3768 }
3770 request = get_keybinding(display[current_view]->keymap, key);
3772 /* Some low-level request handling. This keeps access to
3773 * status_win restricted. */
3774 switch (request) {
3775 case REQ_PROMPT:
3776 {
3777 char *cmd = read_prompt(":");
3779 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
3780 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
3781 opt_request = REQ_VIEW_DIFF;
3782 } else {
3783 opt_request = REQ_VIEW_PAGER;
3784 }
3785 break;
3786 }
3788 request = REQ_NONE;
3789 break;
3790 }
3791 case REQ_SEARCH:
3792 case REQ_SEARCH_BACK:
3793 {
3794 const char *prompt = request == REQ_SEARCH
3795 ? "/" : "?";
3796 char *search = read_prompt(prompt);
3798 if (search)
3799 string_copy(opt_search, search);
3800 else
3801 request = REQ_NONE;
3802 break;
3803 }
3804 case REQ_SCREEN_RESIZE:
3805 {
3806 int height, width;
3808 getmaxyx(stdscr, height, width);
3810 /* Resize the status view and let the view driver take
3811 * care of resizing the displayed views. */
3812 wresize(status_win, 1, width);
3813 mvwin(status_win, height - 1, 0);
3814 wrefresh(status_win);
3815 break;
3816 }
3817 default:
3818 break;
3819 }
3820 }
3822 quit(0);
3824 return 0;
3825 }