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 "unknown-version"
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 <sys/types.h>
31 #include <sys/stat.h>
32 #include <unistd.h>
33 #include <time.h>
35 #include <regex.h>
37 #include <locale.h>
38 #include <langinfo.h>
39 #include <iconv.h>
41 #include <curses.h>
43 #if __GNUC__ >= 3
44 #define __NORETURN __attribute__((__noreturn__))
45 #else
46 #define __NORETURN
47 #endif
49 static void __NORETURN die(const char *err, ...);
50 static void report(const char *msg, ...);
51 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
52 static void set_nonblocking_input(bool loading);
53 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
55 #define ABS(x) ((x) >= 0 ? (x) : -(x))
56 #define MIN(x, y) ((x) < (y) ? (x) : (y))
58 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
59 #define STRING_SIZE(x) (sizeof(x) - 1)
61 #define SIZEOF_STR 1024 /* Default string size. */
62 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
63 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
65 /* Revision graph */
67 #define REVGRAPH_INIT 'I'
68 #define REVGRAPH_MERGE 'M'
69 #define REVGRAPH_BRANCH '+'
70 #define REVGRAPH_COMMIT '*'
71 #define REVGRAPH_LINE '|'
73 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
75 /* This color name can be used to refer to the default term colors. */
76 #define COLOR_DEFAULT (-1)
78 #define ICONV_NONE ((iconv_t) -1)
80 /* The format and size of the date column in the main view. */
81 #define DATE_FORMAT "%Y-%m-%d %H:%M"
82 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
84 #define AUTHOR_COLS 20
86 /* The default interval between line numbers. */
87 #define NUMBER_INTERVAL 1
89 #define TABSIZE 8
91 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
93 #define TIG_LS_REMOTE \
94 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
96 #define TIG_DIFF_CMD \
97 "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
99 #define TIG_LOG_CMD \
100 "git log --cc --stat -n100 %s 2>/dev/null"
102 #define TIG_MAIN_CMD \
103 "git log --topo-order --pretty=raw %s 2>/dev/null"
105 #define TIG_TREE_CMD \
106 "git ls-tree %s %s"
108 #define TIG_BLOB_CMD \
109 "git cat-file blob %s"
111 /* XXX: Needs to be defined to the empty string. */
112 #define TIG_HELP_CMD ""
113 #define TIG_PAGER_CMD ""
114 #define TIG_STATUS_CMD ""
116 /* Some ascii-shorthands fitted into the ncurses namespace. */
117 #define KEY_TAB '\t'
118 #define KEY_RETURN '\r'
119 #define KEY_ESC 27
122 struct ref {
123 char *name; /* Ref name; tag or head names are shortened. */
124 char id[SIZEOF_REV]; /* Commit SHA1 ID */
125 unsigned int tag:1; /* Is it a tag? */
126 unsigned int remote:1; /* Is it a remote ref? */
127 unsigned int next:1; /* For ref lists: are there more refs? */
128 };
130 static struct ref **get_refs(char *id);
132 struct int_map {
133 const char *name;
134 int namelen;
135 int value;
136 };
138 static int
139 set_from_int_map(struct int_map *map, size_t map_size,
140 int *value, const char *name, int namelen)
141 {
143 int i;
145 for (i = 0; i < map_size; i++)
146 if (namelen == map[i].namelen &&
147 !strncasecmp(name, map[i].name, namelen)) {
148 *value = map[i].value;
149 return OK;
150 }
152 return ERR;
153 }
156 /*
157 * String helpers
158 */
160 static inline void
161 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
162 {
163 if (srclen > dstlen - 1)
164 srclen = dstlen - 1;
166 strncpy(dst, src, srclen);
167 dst[srclen] = 0;
168 }
170 /* Shorthands for safely copying into a fixed buffer. */
172 #define string_copy(dst, src) \
173 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
175 #define string_ncopy(dst, src, srclen) \
176 string_ncopy_do(dst, sizeof(dst), src, srclen)
178 #define string_copy_rev(dst, src) \
179 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
181 #define string_add(dst, from, src) \
182 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
184 static char *
185 chomp_string(char *name)
186 {
187 int namelen;
189 while (isspace(*name))
190 name++;
192 namelen = strlen(name) - 1;
193 while (namelen > 0 && isspace(name[namelen]))
194 name[namelen--] = 0;
196 return name;
197 }
199 static bool
200 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
201 {
202 va_list args;
203 size_t pos = bufpos ? *bufpos : 0;
205 va_start(args, fmt);
206 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
207 va_end(args);
209 if (bufpos)
210 *bufpos = pos;
212 return pos >= bufsize ? FALSE : TRUE;
213 }
215 #define string_format(buf, fmt, args...) \
216 string_nformat(buf, sizeof(buf), NULL, fmt, args)
218 #define string_format_from(buf, from, fmt, args...) \
219 string_nformat(buf, sizeof(buf), from, fmt, args)
221 static int
222 string_enum_compare(const char *str1, const char *str2, int len)
223 {
224 size_t i;
226 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
228 /* Diff-Header == DIFF_HEADER */
229 for (i = 0; i < len; i++) {
230 if (toupper(str1[i]) == toupper(str2[i]))
231 continue;
233 if (string_enum_sep(str1[i]) &&
234 string_enum_sep(str2[i]))
235 continue;
237 return str1[i] - str2[i];
238 }
240 return 0;
241 }
243 /* Shell quoting
244 *
245 * NOTE: The following is a slightly modified copy of the git project's shell
246 * quoting routines found in the quote.c file.
247 *
248 * Help to copy the thing properly quoted for the shell safety. any single
249 * quote is replaced with '\'', any exclamation point is replaced with '\!',
250 * and the whole thing is enclosed in a
251 *
252 * E.g.
253 * original sq_quote result
254 * name ==> name ==> 'name'
255 * a b ==> a b ==> 'a b'
256 * a'b ==> a'\''b ==> 'a'\''b'
257 * a!b ==> a'\!'b ==> 'a'\!'b'
258 */
260 static size_t
261 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
262 {
263 char c;
265 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
267 BUFPUT('\'');
268 while ((c = *src++)) {
269 if (c == '\'' || c == '!') {
270 BUFPUT('\'');
271 BUFPUT('\\');
272 BUFPUT(c);
273 BUFPUT('\'');
274 } else {
275 BUFPUT(c);
276 }
277 }
278 BUFPUT('\'');
280 if (bufsize < SIZEOF_STR)
281 buf[bufsize] = 0;
283 return bufsize;
284 }
287 /*
288 * User requests
289 */
291 #define REQ_INFO \
292 /* XXX: Keep the view request first and in sync with views[]. */ \
293 REQ_GROUP("View switching") \
294 REQ_(VIEW_MAIN, "Show main view"), \
295 REQ_(VIEW_DIFF, "Show diff view"), \
296 REQ_(VIEW_LOG, "Show log view"), \
297 REQ_(VIEW_TREE, "Show tree view"), \
298 REQ_(VIEW_BLOB, "Show blob view"), \
299 REQ_(VIEW_HELP, "Show help page"), \
300 REQ_(VIEW_PAGER, "Show pager view"), \
301 REQ_(VIEW_STATUS, "Show status view"), \
302 \
303 REQ_GROUP("View manipulation") \
304 REQ_(ENTER, "Enter current line and scroll"), \
305 REQ_(NEXT, "Move to next"), \
306 REQ_(PREVIOUS, "Move to previous"), \
307 REQ_(VIEW_NEXT, "Move focus to next view"), \
308 REQ_(VIEW_CLOSE, "Close the current view"), \
309 REQ_(QUIT, "Close all views and quit"), \
310 \
311 REQ_GROUP("Cursor navigation") \
312 REQ_(MOVE_UP, "Move cursor one line up"), \
313 REQ_(MOVE_DOWN, "Move cursor one line down"), \
314 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
315 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
316 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
317 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
318 \
319 REQ_GROUP("Scrolling") \
320 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
321 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
322 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
323 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
324 \
325 REQ_GROUP("Searching") \
326 REQ_(SEARCH, "Search the view"), \
327 REQ_(SEARCH_BACK, "Search backwards in the view"), \
328 REQ_(FIND_NEXT, "Find next search match"), \
329 REQ_(FIND_PREV, "Find previous search match"), \
330 \
331 REQ_GROUP("Misc") \
332 REQ_(NONE, "Do nothing"), \
333 REQ_(PROMPT, "Bring up the prompt"), \
334 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
335 REQ_(SCREEN_RESIZE, "Resize the screen"), \
336 REQ_(SHOW_VERSION, "Show version information"), \
337 REQ_(STOP_LOADING, "Stop all loading views"), \
338 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
339 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
340 REQ_(STATUS_UPDATE, "Update file status"), \
341 REQ_(EDIT, "Open in editor")
344 /* User action requests. */
345 enum request {
346 #define REQ_GROUP(help)
347 #define REQ_(req, help) REQ_##req
349 /* Offset all requests to avoid conflicts with ncurses getch values. */
350 REQ_OFFSET = KEY_MAX + 1,
351 REQ_INFO,
352 REQ_UNKNOWN,
354 #undef REQ_GROUP
355 #undef REQ_
356 };
358 struct request_info {
359 enum request request;
360 char *name;
361 int namelen;
362 char *help;
363 };
365 static struct request_info req_info[] = {
366 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
367 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
368 REQ_INFO
369 #undef REQ_GROUP
370 #undef REQ_
371 };
373 static enum request
374 get_request(const char *name)
375 {
376 int namelen = strlen(name);
377 int i;
379 for (i = 0; i < ARRAY_SIZE(req_info); i++)
380 if (req_info[i].namelen == namelen &&
381 !string_enum_compare(req_info[i].name, name, namelen))
382 return req_info[i].request;
384 return REQ_UNKNOWN;
385 }
388 /*
389 * Options
390 */
392 static const char usage[] =
393 "tig " VERSION " (" __DATE__ ")\n"
394 "\n"
395 "Usage: tig [options]\n"
396 " or: tig [options] [--] [git log options]\n"
397 " or: tig [options] log [git log options]\n"
398 " or: tig [options] diff [git diff options]\n"
399 " or: tig [options] show [git show options]\n"
400 " or: tig [options] < [git command output]\n"
401 "\n"
402 "Options:\n"
403 " -l Start up in log view\n"
404 " -d Start up in diff view\n"
405 " -S Start up in status view\n"
406 " -n[I], --line-number[=I] Show line numbers with given interval\n"
407 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
408 " -- Mark end of tig options\n"
409 " -v, --version Show version and exit\n"
410 " -h, --help Show help message and exit\n";
412 /* Option and state variables. */
413 static bool opt_line_number = FALSE;
414 static bool opt_rev_graph = FALSE;
415 static int opt_num_interval = NUMBER_INTERVAL;
416 static int opt_tab_size = TABSIZE;
417 static enum request opt_request = REQ_VIEW_MAIN;
418 static char opt_cmd[SIZEOF_STR] = "";
419 static char opt_path[SIZEOF_STR] = "";
420 static FILE *opt_pipe = NULL;
421 static char opt_encoding[20] = "UTF-8";
422 static bool opt_utf8 = TRUE;
423 static char opt_codeset[20] = "UTF-8";
424 static iconv_t opt_iconv = ICONV_NONE;
425 static char opt_search[SIZEOF_STR] = "";
426 static char opt_cdup[SIZEOF_STR] = "";
427 static char opt_git_dir[SIZEOF_STR] = "";
428 static char opt_editor[SIZEOF_STR] = "";
430 enum option_type {
431 OPT_NONE,
432 OPT_INT,
433 };
435 static bool
436 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
437 {
438 va_list args;
439 char *value = "";
440 int *number;
442 if (opt[0] != '-')
443 return FALSE;
445 if (opt[1] == '-') {
446 int namelen = strlen(name);
448 opt += 2;
450 if (strncmp(opt, name, namelen))
451 return FALSE;
453 if (opt[namelen] == '=')
454 value = opt + namelen + 1;
456 } else {
457 if (!short_name || opt[1] != short_name)
458 return FALSE;
459 value = opt + 2;
460 }
462 va_start(args, type);
463 if (type == OPT_INT) {
464 number = va_arg(args, int *);
465 if (isdigit(*value))
466 *number = atoi(value);
467 }
468 va_end(args);
470 return TRUE;
471 }
473 /* Returns the index of log or diff command or -1 to exit. */
474 static bool
475 parse_options(int argc, char *argv[])
476 {
477 int i;
479 for (i = 1; i < argc; i++) {
480 char *opt = argv[i];
482 if (!strcmp(opt, "log") ||
483 !strcmp(opt, "diff") ||
484 !strcmp(opt, "show")) {
485 opt_request = opt[0] == 'l'
486 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
487 break;
488 }
490 if (opt[0] && opt[0] != '-')
491 break;
493 if (!strcmp(opt, "-l")) {
494 opt_request = REQ_VIEW_LOG;
495 continue;
496 }
498 if (!strcmp(opt, "-d")) {
499 opt_request = REQ_VIEW_DIFF;
500 continue;
501 }
503 if (!strcmp(opt, "-S")) {
504 opt_request = REQ_VIEW_STATUS;
505 continue;
506 }
508 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
509 opt_line_number = TRUE;
510 continue;
511 }
513 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
514 opt_tab_size = MIN(opt_tab_size, TABSIZE);
515 continue;
516 }
518 if (check_option(opt, 'v', "version", OPT_NONE)) {
519 printf("tig version %s\n", VERSION);
520 return FALSE;
521 }
523 if (check_option(opt, 'h', "help", OPT_NONE)) {
524 printf(usage);
525 return FALSE;
526 }
528 if (!strcmp(opt, "--")) {
529 i++;
530 break;
531 }
533 die("unknown option '%s'\n\n%s", opt, usage);
534 }
536 if (!isatty(STDIN_FILENO)) {
537 opt_request = REQ_VIEW_PAGER;
538 opt_pipe = stdin;
540 } else if (i < argc) {
541 size_t buf_size;
543 if (opt_request == REQ_VIEW_MAIN)
544 /* XXX: This is vulnerable to the user overriding
545 * options required for the main view parser. */
546 string_copy(opt_cmd, "git log --pretty=raw");
547 else
548 string_copy(opt_cmd, "git");
549 buf_size = strlen(opt_cmd);
551 while (buf_size < sizeof(opt_cmd) && i < argc) {
552 opt_cmd[buf_size++] = ' ';
553 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
554 }
556 if (buf_size >= sizeof(opt_cmd))
557 die("command too long");
559 opt_cmd[buf_size] = 0;
560 }
562 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
563 opt_utf8 = FALSE;
565 return TRUE;
566 }
569 /*
570 * Line-oriented content detection.
571 */
573 #define LINE_INFO \
574 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
576 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
577 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
578 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
580 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
581 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
582 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
586 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
587 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
589 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
590 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
591 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
594 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
595 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
596 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
597 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
598 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
599 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
600 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
601 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
602 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
603 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
604 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
605 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
606 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
607 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
608 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
609 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
610 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
611 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
612 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
613 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
614 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
615 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
616 LINE(STAT_SECTION, "", COLOR_DEFAULT, COLOR_BLUE, A_BOLD), \
617 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
618 LINE(STAT_STAGED, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
619 LINE(STAT_UNSTAGED,"", COLOR_YELLOW, COLOR_DEFAULT, 0), \
620 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
622 enum line_type {
623 #define LINE(type, line, fg, bg, attr) \
624 LINE_##type
625 LINE_INFO
626 #undef LINE
627 };
629 struct line_info {
630 const char *name; /* Option name. */
631 int namelen; /* Size of option name. */
632 const char *line; /* The start of line to match. */
633 int linelen; /* Size of string to match. */
634 int fg, bg, attr; /* Color and text attributes for the lines. */
635 };
637 static struct line_info line_info[] = {
638 #define LINE(type, line, fg, bg, attr) \
639 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
640 LINE_INFO
641 #undef LINE
642 };
644 static enum line_type
645 get_line_type(char *line)
646 {
647 int linelen = strlen(line);
648 enum line_type type;
650 for (type = 0; type < ARRAY_SIZE(line_info); type++)
651 /* Case insensitive search matches Signed-off-by lines better. */
652 if (linelen >= line_info[type].linelen &&
653 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
654 return type;
656 return LINE_DEFAULT;
657 }
659 static inline int
660 get_line_attr(enum line_type type)
661 {
662 assert(type < ARRAY_SIZE(line_info));
663 return COLOR_PAIR(type) | line_info[type].attr;
664 }
666 static struct line_info *
667 get_line_info(char *name, int namelen)
668 {
669 enum line_type type;
671 for (type = 0; type < ARRAY_SIZE(line_info); type++)
672 if (namelen == line_info[type].namelen &&
673 !string_enum_compare(line_info[type].name, name, namelen))
674 return &line_info[type];
676 return NULL;
677 }
679 static void
680 init_colors(void)
681 {
682 int default_bg = COLOR_BLACK;
683 int default_fg = COLOR_WHITE;
684 enum line_type type;
686 start_color();
688 if (use_default_colors() != ERR) {
689 default_bg = -1;
690 default_fg = -1;
691 }
693 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
694 struct line_info *info = &line_info[type];
695 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
696 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
698 init_pair(type, fg, bg);
699 }
700 }
702 struct line {
703 enum line_type type;
705 /* State flags */
706 unsigned int selected:1;
708 void *data; /* User data */
709 };
712 /*
713 * Keys
714 */
716 struct keybinding {
717 int alias;
718 enum request request;
719 struct keybinding *next;
720 };
722 static struct keybinding default_keybindings[] = {
723 /* View switching */
724 { 'm', REQ_VIEW_MAIN },
725 { 'd', REQ_VIEW_DIFF },
726 { 'l', REQ_VIEW_LOG },
727 { 't', REQ_VIEW_TREE },
728 { 'f', REQ_VIEW_BLOB },
729 { 'p', REQ_VIEW_PAGER },
730 { 'h', REQ_VIEW_HELP },
731 { 'S', REQ_VIEW_STATUS },
733 /* View manipulation */
734 { 'q', REQ_VIEW_CLOSE },
735 { KEY_TAB, REQ_VIEW_NEXT },
736 { KEY_RETURN, REQ_ENTER },
737 { KEY_UP, REQ_PREVIOUS },
738 { KEY_DOWN, REQ_NEXT },
740 /* Cursor navigation */
741 { 'k', REQ_MOVE_UP },
742 { 'j', REQ_MOVE_DOWN },
743 { KEY_HOME, REQ_MOVE_FIRST_LINE },
744 { KEY_END, REQ_MOVE_LAST_LINE },
745 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
746 { ' ', REQ_MOVE_PAGE_DOWN },
747 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
748 { 'b', REQ_MOVE_PAGE_UP },
749 { '-', REQ_MOVE_PAGE_UP },
751 /* Scrolling */
752 { KEY_IC, REQ_SCROLL_LINE_UP },
753 { KEY_DC, REQ_SCROLL_LINE_DOWN },
754 { 'w', REQ_SCROLL_PAGE_UP },
755 { 's', REQ_SCROLL_PAGE_DOWN },
757 /* Searching */
758 { '/', REQ_SEARCH },
759 { '?', REQ_SEARCH_BACK },
760 { 'n', REQ_FIND_NEXT },
761 { 'N', REQ_FIND_PREV },
763 /* Misc */
764 { 'Q', REQ_QUIT },
765 { 'z', REQ_STOP_LOADING },
766 { 'v', REQ_SHOW_VERSION },
767 { 'r', REQ_SCREEN_REDRAW },
768 { '.', REQ_TOGGLE_LINENO },
769 { 'g', REQ_TOGGLE_REV_GRAPH },
770 { ':', REQ_PROMPT },
771 { 'u', REQ_STATUS_UPDATE },
772 { 'e', REQ_EDIT },
774 /* Using the ncurses SIGWINCH handler. */
775 { KEY_RESIZE, REQ_SCREEN_RESIZE },
776 };
778 #define KEYMAP_INFO \
779 KEYMAP_(GENERIC), \
780 KEYMAP_(MAIN), \
781 KEYMAP_(DIFF), \
782 KEYMAP_(LOG), \
783 KEYMAP_(TREE), \
784 KEYMAP_(BLOB), \
785 KEYMAP_(PAGER), \
786 KEYMAP_(HELP), \
787 KEYMAP_(STATUS)
789 enum keymap {
790 #define KEYMAP_(name) KEYMAP_##name
791 KEYMAP_INFO
792 #undef KEYMAP_
793 };
795 static struct int_map keymap_table[] = {
796 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
797 KEYMAP_INFO
798 #undef KEYMAP_
799 };
801 #define set_keymap(map, name) \
802 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
804 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
806 static void
807 add_keybinding(enum keymap keymap, enum request request, int key)
808 {
809 struct keybinding *keybinding;
811 keybinding = calloc(1, sizeof(*keybinding));
812 if (!keybinding)
813 die("Failed to allocate keybinding");
815 keybinding->alias = key;
816 keybinding->request = request;
817 keybinding->next = keybindings[keymap];
818 keybindings[keymap] = keybinding;
819 }
821 /* Looks for a key binding first in the given map, then in the generic map, and
822 * lastly in the default keybindings. */
823 static enum request
824 get_keybinding(enum keymap keymap, int key)
825 {
826 struct keybinding *kbd;
827 int i;
829 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
830 if (kbd->alias == key)
831 return kbd->request;
833 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
834 if (kbd->alias == key)
835 return kbd->request;
837 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
838 if (default_keybindings[i].alias == key)
839 return default_keybindings[i].request;
841 return (enum request) key;
842 }
845 struct key {
846 char *name;
847 int value;
848 };
850 static struct key key_table[] = {
851 { "Enter", KEY_RETURN },
852 { "Space", ' ' },
853 { "Backspace", KEY_BACKSPACE },
854 { "Tab", KEY_TAB },
855 { "Escape", KEY_ESC },
856 { "Left", KEY_LEFT },
857 { "Right", KEY_RIGHT },
858 { "Up", KEY_UP },
859 { "Down", KEY_DOWN },
860 { "Insert", KEY_IC },
861 { "Delete", KEY_DC },
862 { "Hash", '#' },
863 { "Home", KEY_HOME },
864 { "End", KEY_END },
865 { "PageUp", KEY_PPAGE },
866 { "PageDown", KEY_NPAGE },
867 { "F1", KEY_F(1) },
868 { "F2", KEY_F(2) },
869 { "F3", KEY_F(3) },
870 { "F4", KEY_F(4) },
871 { "F5", KEY_F(5) },
872 { "F6", KEY_F(6) },
873 { "F7", KEY_F(7) },
874 { "F8", KEY_F(8) },
875 { "F9", KEY_F(9) },
876 { "F10", KEY_F(10) },
877 { "F11", KEY_F(11) },
878 { "F12", KEY_F(12) },
879 };
881 static int
882 get_key_value(const char *name)
883 {
884 int i;
886 for (i = 0; i < ARRAY_SIZE(key_table); i++)
887 if (!strcasecmp(key_table[i].name, name))
888 return key_table[i].value;
890 if (strlen(name) == 1 && isprint(*name))
891 return (int) *name;
893 return ERR;
894 }
896 static char *
897 get_key(enum request request)
898 {
899 static char buf[BUFSIZ];
900 static char key_char[] = "'X'";
901 size_t pos = 0;
902 char *sep = "";
903 int i;
905 buf[pos] = 0;
907 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
908 struct keybinding *keybinding = &default_keybindings[i];
909 char *seq = NULL;
910 int key;
912 if (keybinding->request != request)
913 continue;
915 for (key = 0; key < ARRAY_SIZE(key_table); key++)
916 if (key_table[key].value == keybinding->alias)
917 seq = key_table[key].name;
919 if (seq == NULL &&
920 keybinding->alias < 127 &&
921 isprint(keybinding->alias)) {
922 key_char[1] = (char) keybinding->alias;
923 seq = key_char;
924 }
926 if (!seq)
927 seq = "'?'";
929 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
930 return "Too many keybindings!";
931 sep = ", ";
932 }
934 return buf;
935 }
938 /*
939 * User config file handling.
940 */
942 static struct int_map color_map[] = {
943 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
944 COLOR_MAP(DEFAULT),
945 COLOR_MAP(BLACK),
946 COLOR_MAP(BLUE),
947 COLOR_MAP(CYAN),
948 COLOR_MAP(GREEN),
949 COLOR_MAP(MAGENTA),
950 COLOR_MAP(RED),
951 COLOR_MAP(WHITE),
952 COLOR_MAP(YELLOW),
953 };
955 #define set_color(color, name) \
956 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
958 static struct int_map attr_map[] = {
959 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
960 ATTR_MAP(NORMAL),
961 ATTR_MAP(BLINK),
962 ATTR_MAP(BOLD),
963 ATTR_MAP(DIM),
964 ATTR_MAP(REVERSE),
965 ATTR_MAP(STANDOUT),
966 ATTR_MAP(UNDERLINE),
967 };
969 #define set_attribute(attr, name) \
970 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
972 static int config_lineno;
973 static bool config_errors;
974 static char *config_msg;
976 /* Wants: object fgcolor bgcolor [attr] */
977 static int
978 option_color_command(int argc, char *argv[])
979 {
980 struct line_info *info;
982 if (argc != 3 && argc != 4) {
983 config_msg = "Wrong number of arguments given to color command";
984 return ERR;
985 }
987 info = get_line_info(argv[0], strlen(argv[0]));
988 if (!info) {
989 config_msg = "Unknown color name";
990 return ERR;
991 }
993 if (set_color(&info->fg, argv[1]) == ERR ||
994 set_color(&info->bg, argv[2]) == ERR) {
995 config_msg = "Unknown color";
996 return ERR;
997 }
999 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1000 config_msg = "Unknown attribute";
1001 return ERR;
1002 }
1004 return OK;
1005 }
1007 /* Wants: name = value */
1008 static int
1009 option_set_command(int argc, char *argv[])
1010 {
1011 if (argc != 3) {
1012 config_msg = "Wrong number of arguments given to set command";
1013 return ERR;
1014 }
1016 if (strcmp(argv[1], "=")) {
1017 config_msg = "No value assigned";
1018 return ERR;
1019 }
1021 if (!strcmp(argv[0], "show-rev-graph")) {
1022 opt_rev_graph = (!strcmp(argv[2], "1") ||
1023 !strcmp(argv[2], "true") ||
1024 !strcmp(argv[2], "yes"));
1025 return OK;
1026 }
1028 if (!strcmp(argv[0], "line-number-interval")) {
1029 opt_num_interval = atoi(argv[2]);
1030 return OK;
1031 }
1033 if (!strcmp(argv[0], "tab-size")) {
1034 opt_tab_size = atoi(argv[2]);
1035 return OK;
1036 }
1038 if (!strcmp(argv[0], "commit-encoding")) {
1039 char *arg = argv[2];
1040 int delimiter = *arg;
1041 int i;
1043 switch (delimiter) {
1044 case '"':
1045 case '\'':
1046 for (arg++, i = 0; arg[i]; i++)
1047 if (arg[i] == delimiter) {
1048 arg[i] = 0;
1049 break;
1050 }
1051 default:
1052 string_ncopy(opt_encoding, arg, strlen(arg));
1053 return OK;
1054 }
1055 }
1057 config_msg = "Unknown variable name";
1058 return ERR;
1059 }
1061 /* Wants: mode request key */
1062 static int
1063 option_bind_command(int argc, char *argv[])
1064 {
1065 enum request request;
1066 int keymap;
1067 int key;
1069 if (argc != 3) {
1070 config_msg = "Wrong number of arguments given to bind command";
1071 return ERR;
1072 }
1074 if (set_keymap(&keymap, argv[0]) == ERR) {
1075 config_msg = "Unknown key map";
1076 return ERR;
1077 }
1079 key = get_key_value(argv[1]);
1080 if (key == ERR) {
1081 config_msg = "Unknown key";
1082 return ERR;
1083 }
1085 request = get_request(argv[2]);
1086 if (request == REQ_UNKNOWN) {
1087 config_msg = "Unknown request name";
1088 return ERR;
1089 }
1091 add_keybinding(keymap, request, key);
1093 return OK;
1094 }
1096 static int
1097 set_option(char *opt, char *value)
1098 {
1099 char *argv[16];
1100 int valuelen;
1101 int argc = 0;
1103 /* Tokenize */
1104 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1105 argv[argc++] = value;
1107 value += valuelen;
1108 if (!*value)
1109 break;
1111 *value++ = 0;
1112 while (isspace(*value))
1113 value++;
1114 }
1116 if (!strcmp(opt, "color"))
1117 return option_color_command(argc, argv);
1119 if (!strcmp(opt, "set"))
1120 return option_set_command(argc, argv);
1122 if (!strcmp(opt, "bind"))
1123 return option_bind_command(argc, argv);
1125 config_msg = "Unknown option command";
1126 return ERR;
1127 }
1129 static int
1130 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1131 {
1132 int status = OK;
1134 config_lineno++;
1135 config_msg = "Internal error";
1137 /* Check for comment markers, since read_properties() will
1138 * only ensure opt and value are split at first " \t". */
1139 optlen = strcspn(opt, "#");
1140 if (optlen == 0)
1141 return OK;
1143 if (opt[optlen] != 0) {
1144 config_msg = "No option value";
1145 status = ERR;
1147 } else {
1148 /* Look for comment endings in the value. */
1149 size_t len = strcspn(value, "#");
1151 if (len < valuelen) {
1152 valuelen = len;
1153 value[valuelen] = 0;
1154 }
1156 status = set_option(opt, value);
1157 }
1159 if (status == ERR) {
1160 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1161 config_lineno, (int) optlen, opt, config_msg);
1162 config_errors = TRUE;
1163 }
1165 /* Always keep going if errors are encountered. */
1166 return OK;
1167 }
1169 static int
1170 load_options(void)
1171 {
1172 char *home = getenv("HOME");
1173 char buf[SIZEOF_STR];
1174 FILE *file;
1176 config_lineno = 0;
1177 config_errors = FALSE;
1179 if (!home || !string_format(buf, "%s/.tigrc", home))
1180 return ERR;
1182 /* It's ok that the file doesn't exist. */
1183 file = fopen(buf, "r");
1184 if (!file)
1185 return OK;
1187 if (read_properties(file, " \t", read_option) == ERR ||
1188 config_errors == TRUE)
1189 fprintf(stderr, "Errors while loading %s.\n", buf);
1191 return OK;
1192 }
1195 /*
1196 * The viewer
1197 */
1199 struct view;
1200 struct view_ops;
1202 /* The display array of active views and the index of the current view. */
1203 static struct view *display[2];
1204 static unsigned int current_view;
1206 /* Reading from the prompt? */
1207 static bool input_mode = FALSE;
1209 #define foreach_displayed_view(view, i) \
1210 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1212 #define displayed_views() (display[1] != NULL ? 2 : 1)
1214 /* Current head and commit ID */
1215 static char ref_blob[SIZEOF_REF] = "";
1216 static char ref_commit[SIZEOF_REF] = "HEAD";
1217 static char ref_head[SIZEOF_REF] = "HEAD";
1219 struct view {
1220 const char *name; /* View name */
1221 const char *cmd_fmt; /* Default command line format */
1222 const char *cmd_env; /* Command line set via environment */
1223 const char *id; /* Points to either of ref_{head,commit,blob} */
1225 struct view_ops *ops; /* View operations */
1227 enum keymap keymap; /* What keymap does this view have */
1229 char cmd[SIZEOF_STR]; /* Command buffer */
1230 char ref[SIZEOF_REF]; /* Hovered commit reference */
1231 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1233 int height, width; /* The width and height of the main window */
1234 WINDOW *win; /* The main window */
1235 WINDOW *title; /* The title window living below the main window */
1237 /* Navigation */
1238 unsigned long offset; /* Offset of the window top */
1239 unsigned long lineno; /* Current line number */
1241 /* Searching */
1242 char grep[SIZEOF_STR]; /* Search string */
1243 regex_t *regex; /* Pre-compiled regex */
1245 /* If non-NULL, points to the view that opened this view. If this view
1246 * is closed tig will switch back to the parent view. */
1247 struct view *parent;
1249 /* Buffering */
1250 unsigned long lines; /* Total number of lines */
1251 struct line *line; /* Line index */
1252 unsigned long line_size;/* Total number of allocated lines */
1253 unsigned int digits; /* Number of digits in the lines member. */
1255 /* Loading */
1256 FILE *pipe;
1257 time_t start_time;
1258 };
1260 struct view_ops {
1261 /* What type of content being displayed. Used in the title bar. */
1262 const char *type;
1263 /* Open and reads in all view content. */
1264 bool (*open)(struct view *view);
1265 /* Read one line; updates view->line. */
1266 bool (*read)(struct view *view, char *data);
1267 /* Draw one line; @lineno must be < view->height. */
1268 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1269 /* Depending on view handle a special requests. */
1270 enum request (*request)(struct view *view, enum request request, struct line *line);
1271 /* Search for regex in a line. */
1272 bool (*grep)(struct view *view, struct line *line);
1273 /* Select line */
1274 void (*select)(struct view *view, struct line *line);
1275 };
1277 static struct view_ops pager_ops;
1278 static struct view_ops main_ops;
1279 static struct view_ops tree_ops;
1280 static struct view_ops blob_ops;
1281 static struct view_ops help_ops;
1282 static struct view_ops status_ops;
1284 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1285 { name, cmd, #env, ref, ops, map}
1287 #define VIEW_(id, name, ops, ref) \
1288 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1291 static struct view views[] = {
1292 VIEW_(MAIN, "main", &main_ops, ref_head),
1293 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1294 VIEW_(LOG, "log", &pager_ops, ref_head),
1295 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1296 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1297 VIEW_(HELP, "help", &help_ops, ""),
1298 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1299 VIEW_(STATUS, "status", &status_ops, ""),
1300 };
1302 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1304 #define foreach_view(view, i) \
1305 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1307 #define view_is_displayed(view) \
1308 (view == display[0] || view == display[1])
1310 static bool
1311 draw_view_line(struct view *view, unsigned int lineno)
1312 {
1313 struct line *line;
1314 bool selected = (view->offset + lineno == view->lineno);
1315 bool draw_ok;
1317 assert(view_is_displayed(view));
1319 if (view->offset + lineno >= view->lines)
1320 return FALSE;
1322 line = &view->line[view->offset + lineno];
1324 if (selected) {
1325 line->selected = TRUE;
1326 view->ops->select(view, line);
1327 } else if (line->selected) {
1328 line->selected = FALSE;
1329 wmove(view->win, lineno, 0);
1330 wclrtoeol(view->win);
1331 }
1333 scrollok(view->win, FALSE);
1334 draw_ok = view->ops->draw(view, line, lineno, selected);
1335 scrollok(view->win, TRUE);
1337 return draw_ok;
1338 }
1340 static void
1341 redraw_view_from(struct view *view, int lineno)
1342 {
1343 assert(0 <= lineno && lineno < view->height);
1345 for (; lineno < view->height; lineno++) {
1346 if (!draw_view_line(view, lineno))
1347 break;
1348 }
1350 redrawwin(view->win);
1351 if (input_mode)
1352 wnoutrefresh(view->win);
1353 else
1354 wrefresh(view->win);
1355 }
1357 static void
1358 redraw_view(struct view *view)
1359 {
1360 wclear(view->win);
1361 redraw_view_from(view, 0);
1362 }
1365 static void
1366 update_view_title(struct view *view)
1367 {
1368 char buf[SIZEOF_STR];
1369 char state[SIZEOF_STR];
1370 size_t bufpos = 0, statelen = 0;
1372 assert(view_is_displayed(view));
1374 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1375 unsigned int view_lines = view->offset + view->height;
1376 unsigned int lines = view->lines
1377 ? MIN(view_lines, view->lines) * 100 / view->lines
1378 : 0;
1380 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1381 view->ops->type,
1382 view->lineno + 1,
1383 view->lines,
1384 lines);
1386 if (view->pipe) {
1387 time_t secs = time(NULL) - view->start_time;
1389 /* Three git seconds are a long time ... */
1390 if (secs > 2)
1391 string_format_from(state, &statelen, " %lds", secs);
1392 }
1393 }
1395 string_format_from(buf, &bufpos, "[%s]", view->name);
1396 if (*view->ref && bufpos < view->width) {
1397 size_t refsize = strlen(view->ref);
1398 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1400 if (minsize < view->width)
1401 refsize = view->width - minsize + 7;
1402 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1403 }
1405 if (statelen && bufpos < view->width) {
1406 string_format_from(buf, &bufpos, " %s", state);
1407 }
1409 if (view == display[current_view])
1410 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1411 else
1412 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1414 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1415 wclrtoeol(view->title);
1416 wmove(view->title, 0, view->width - 1);
1418 if (input_mode)
1419 wnoutrefresh(view->title);
1420 else
1421 wrefresh(view->title);
1422 }
1424 static void
1425 resize_display(void)
1426 {
1427 int offset, i;
1428 struct view *base = display[0];
1429 struct view *view = display[1] ? display[1] : display[0];
1431 /* Setup window dimensions */
1433 getmaxyx(stdscr, base->height, base->width);
1435 /* Make room for the status window. */
1436 base->height -= 1;
1438 if (view != base) {
1439 /* Horizontal split. */
1440 view->width = base->width;
1441 view->height = SCALE_SPLIT_VIEW(base->height);
1442 base->height -= view->height;
1444 /* Make room for the title bar. */
1445 view->height -= 1;
1446 }
1448 /* Make room for the title bar. */
1449 base->height -= 1;
1451 offset = 0;
1453 foreach_displayed_view (view, i) {
1454 if (!view->win) {
1455 view->win = newwin(view->height, 0, offset, 0);
1456 if (!view->win)
1457 die("Failed to create %s view", view->name);
1459 scrollok(view->win, TRUE);
1461 view->title = newwin(1, 0, offset + view->height, 0);
1462 if (!view->title)
1463 die("Failed to create title window");
1465 } else {
1466 wresize(view->win, view->height, view->width);
1467 mvwin(view->win, offset, 0);
1468 mvwin(view->title, offset + view->height, 0);
1469 }
1471 offset += view->height + 1;
1472 }
1473 }
1475 static void
1476 redraw_display(void)
1477 {
1478 struct view *view;
1479 int i;
1481 foreach_displayed_view (view, i) {
1482 redraw_view(view);
1483 update_view_title(view);
1484 }
1485 }
1487 static void
1488 update_display_cursor(struct view *view)
1489 {
1490 /* Move the cursor to the right-most column of the cursor line.
1491 *
1492 * XXX: This could turn out to be a bit expensive, but it ensures that
1493 * the cursor does not jump around. */
1494 if (view->lines) {
1495 wmove(view->win, view->lineno - view->offset, view->width - 1);
1496 wrefresh(view->win);
1497 }
1498 }
1500 /*
1501 * Navigation
1502 */
1504 /* Scrolling backend */
1505 static void
1506 do_scroll_view(struct view *view, int lines)
1507 {
1508 bool redraw_current_line = FALSE;
1510 /* The rendering expects the new offset. */
1511 view->offset += lines;
1513 assert(0 <= view->offset && view->offset < view->lines);
1514 assert(lines);
1516 /* Move current line into the view. */
1517 if (view->lineno < view->offset) {
1518 view->lineno = view->offset;
1519 redraw_current_line = TRUE;
1520 } else if (view->lineno >= view->offset + view->height) {
1521 view->lineno = view->offset + view->height - 1;
1522 redraw_current_line = TRUE;
1523 }
1525 assert(view->offset <= view->lineno && view->lineno < view->lines);
1527 /* Redraw the whole screen if scrolling is pointless. */
1528 if (view->height < ABS(lines)) {
1529 redraw_view(view);
1531 } else {
1532 int line = lines > 0 ? view->height - lines : 0;
1533 int end = line + ABS(lines);
1535 wscrl(view->win, lines);
1537 for (; line < end; line++) {
1538 if (!draw_view_line(view, line))
1539 break;
1540 }
1542 if (redraw_current_line)
1543 draw_view_line(view, view->lineno - view->offset);
1544 }
1546 redrawwin(view->win);
1547 wrefresh(view->win);
1548 report("");
1549 }
1551 /* Scroll frontend */
1552 static void
1553 scroll_view(struct view *view, enum request request)
1554 {
1555 int lines = 1;
1557 assert(view_is_displayed(view));
1559 switch (request) {
1560 case REQ_SCROLL_PAGE_DOWN:
1561 lines = view->height;
1562 case REQ_SCROLL_LINE_DOWN:
1563 if (view->offset + lines > view->lines)
1564 lines = view->lines - view->offset;
1566 if (lines == 0 || view->offset + view->height >= view->lines) {
1567 report("Cannot scroll beyond the last line");
1568 return;
1569 }
1570 break;
1572 case REQ_SCROLL_PAGE_UP:
1573 lines = view->height;
1574 case REQ_SCROLL_LINE_UP:
1575 if (lines > view->offset)
1576 lines = view->offset;
1578 if (lines == 0) {
1579 report("Cannot scroll beyond the first line");
1580 return;
1581 }
1583 lines = -lines;
1584 break;
1586 default:
1587 die("request %d not handled in switch", request);
1588 }
1590 do_scroll_view(view, lines);
1591 }
1593 /* Cursor moving */
1594 static void
1595 move_view(struct view *view, enum request request)
1596 {
1597 int scroll_steps = 0;
1598 int steps;
1600 switch (request) {
1601 case REQ_MOVE_FIRST_LINE:
1602 steps = -view->lineno;
1603 break;
1605 case REQ_MOVE_LAST_LINE:
1606 steps = view->lines - view->lineno - 1;
1607 break;
1609 case REQ_MOVE_PAGE_UP:
1610 steps = view->height > view->lineno
1611 ? -view->lineno : -view->height;
1612 break;
1614 case REQ_MOVE_PAGE_DOWN:
1615 steps = view->lineno + view->height >= view->lines
1616 ? view->lines - view->lineno - 1 : view->height;
1617 break;
1619 case REQ_MOVE_UP:
1620 steps = -1;
1621 break;
1623 case REQ_MOVE_DOWN:
1624 steps = 1;
1625 break;
1627 default:
1628 die("request %d not handled in switch", request);
1629 }
1631 if (steps <= 0 && view->lineno == 0) {
1632 report("Cannot move beyond the first line");
1633 return;
1635 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1636 report("Cannot move beyond the last line");
1637 return;
1638 }
1640 /* Move the current line */
1641 view->lineno += steps;
1642 assert(0 <= view->lineno && view->lineno < view->lines);
1644 /* Check whether the view needs to be scrolled */
1645 if (view->lineno < view->offset ||
1646 view->lineno >= view->offset + view->height) {
1647 scroll_steps = steps;
1648 if (steps < 0 && -steps > view->offset) {
1649 scroll_steps = -view->offset;
1651 } else if (steps > 0) {
1652 if (view->lineno == view->lines - 1 &&
1653 view->lines > view->height) {
1654 scroll_steps = view->lines - view->offset - 1;
1655 if (scroll_steps >= view->height)
1656 scroll_steps -= view->height - 1;
1657 }
1658 }
1659 }
1661 if (!view_is_displayed(view)) {
1662 view->offset += scroll_steps;
1663 assert(0 <= view->offset && view->offset < view->lines);
1664 view->ops->select(view, &view->line[view->lineno]);
1665 return;
1666 }
1668 /* Repaint the old "current" line if we be scrolling */
1669 if (ABS(steps) < view->height)
1670 draw_view_line(view, view->lineno - steps - view->offset);
1672 if (scroll_steps) {
1673 do_scroll_view(view, scroll_steps);
1674 return;
1675 }
1677 /* Draw the current line */
1678 draw_view_line(view, view->lineno - view->offset);
1680 redrawwin(view->win);
1681 wrefresh(view->win);
1682 report("");
1683 }
1686 /*
1687 * Searching
1688 */
1690 static void search_view(struct view *view, enum request request);
1692 static bool
1693 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1694 {
1695 assert(view_is_displayed(view));
1697 if (!view->ops->grep(view, line))
1698 return FALSE;
1700 if (lineno - view->offset >= view->height) {
1701 view->offset = lineno;
1702 view->lineno = lineno;
1703 redraw_view(view);
1705 } else {
1706 unsigned long old_lineno = view->lineno - view->offset;
1708 view->lineno = lineno;
1709 draw_view_line(view, old_lineno);
1711 draw_view_line(view, view->lineno - view->offset);
1712 redrawwin(view->win);
1713 wrefresh(view->win);
1714 }
1716 report("Line %ld matches '%s'", lineno + 1, view->grep);
1717 return TRUE;
1718 }
1720 static void
1721 find_next(struct view *view, enum request request)
1722 {
1723 unsigned long lineno = view->lineno;
1724 int direction;
1726 if (!*view->grep) {
1727 if (!*opt_search)
1728 report("No previous search");
1729 else
1730 search_view(view, request);
1731 return;
1732 }
1734 switch (request) {
1735 case REQ_SEARCH:
1736 case REQ_FIND_NEXT:
1737 direction = 1;
1738 break;
1740 case REQ_SEARCH_BACK:
1741 case REQ_FIND_PREV:
1742 direction = -1;
1743 break;
1745 default:
1746 return;
1747 }
1749 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1750 lineno += direction;
1752 /* Note, lineno is unsigned long so will wrap around in which case it
1753 * will become bigger than view->lines. */
1754 for (; lineno < view->lines; lineno += direction) {
1755 struct line *line = &view->line[lineno];
1757 if (find_next_line(view, lineno, line))
1758 return;
1759 }
1761 report("No match found for '%s'", view->grep);
1762 }
1764 static void
1765 search_view(struct view *view, enum request request)
1766 {
1767 int regex_err;
1769 if (view->regex) {
1770 regfree(view->regex);
1771 *view->grep = 0;
1772 } else {
1773 view->regex = calloc(1, sizeof(*view->regex));
1774 if (!view->regex)
1775 return;
1776 }
1778 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1779 if (regex_err != 0) {
1780 char buf[SIZEOF_STR] = "unknown error";
1782 regerror(regex_err, view->regex, buf, sizeof(buf));
1783 report("Search failed: %s", buf);
1784 return;
1785 }
1787 string_copy(view->grep, opt_search);
1789 find_next(view, request);
1790 }
1792 /*
1793 * Incremental updating
1794 */
1796 static void
1797 end_update(struct view *view)
1798 {
1799 if (!view->pipe)
1800 return;
1801 set_nonblocking_input(FALSE);
1802 if (view->pipe == stdin)
1803 fclose(view->pipe);
1804 else
1805 pclose(view->pipe);
1806 view->pipe = NULL;
1807 }
1809 static bool
1810 begin_update(struct view *view)
1811 {
1812 if (view->pipe)
1813 end_update(view);
1815 if (opt_cmd[0]) {
1816 string_copy(view->cmd, opt_cmd);
1817 opt_cmd[0] = 0;
1818 /* When running random commands, initially show the
1819 * command in the title. However, it maybe later be
1820 * overwritten if a commit line is selected. */
1821 string_copy(view->ref, view->cmd);
1823 } else if (view == VIEW(REQ_VIEW_TREE)) {
1824 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1825 char path[SIZEOF_STR];
1827 if (strcmp(view->vid, view->id))
1828 opt_path[0] = path[0] = 0;
1829 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1830 return FALSE;
1832 if (!string_format(view->cmd, format, view->id, path))
1833 return FALSE;
1835 } else {
1836 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1837 const char *id = view->id;
1839 if (!string_format(view->cmd, format, id, id, id, id, id))
1840 return FALSE;
1842 /* Put the current ref_* value to the view title ref
1843 * member. This is needed by the blob view. Most other
1844 * views sets it automatically after loading because the
1845 * first line is a commit line. */
1846 string_copy_rev(view->ref, view->id);
1847 }
1849 /* Special case for the pager view. */
1850 if (opt_pipe) {
1851 view->pipe = opt_pipe;
1852 opt_pipe = NULL;
1853 } else {
1854 view->pipe = popen(view->cmd, "r");
1855 }
1857 if (!view->pipe)
1858 return FALSE;
1860 set_nonblocking_input(TRUE);
1862 view->offset = 0;
1863 view->lines = 0;
1864 view->lineno = 0;
1865 string_copy_rev(view->vid, view->id);
1867 if (view->line) {
1868 int i;
1870 for (i = 0; i < view->lines; i++)
1871 if (view->line[i].data)
1872 free(view->line[i].data);
1874 free(view->line);
1875 view->line = NULL;
1876 }
1878 view->start_time = time(NULL);
1880 return TRUE;
1881 }
1883 static struct line *
1884 realloc_lines(struct view *view, size_t line_size)
1885 {
1886 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1888 if (!tmp)
1889 return NULL;
1891 view->line = tmp;
1892 view->line_size = line_size;
1893 return view->line;
1894 }
1896 static bool
1897 update_view(struct view *view)
1898 {
1899 char in_buffer[BUFSIZ];
1900 char out_buffer[BUFSIZ * 2];
1901 char *line;
1902 /* The number of lines to read. If too low it will cause too much
1903 * redrawing (and possible flickering), if too high responsiveness
1904 * will suffer. */
1905 unsigned long lines = view->height;
1906 int redraw_from = -1;
1908 if (!view->pipe)
1909 return TRUE;
1911 /* Only redraw if lines are visible. */
1912 if (view->offset + view->height >= view->lines)
1913 redraw_from = view->lines - view->offset;
1915 /* FIXME: This is probably not perfect for backgrounded views. */
1916 if (!realloc_lines(view, view->lines + lines))
1917 goto alloc_error;
1919 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1920 size_t linelen = strlen(line);
1922 if (linelen)
1923 line[linelen - 1] = 0;
1925 if (opt_iconv != ICONV_NONE) {
1926 char *inbuf = line;
1927 size_t inlen = linelen;
1929 char *outbuf = out_buffer;
1930 size_t outlen = sizeof(out_buffer);
1932 size_t ret;
1934 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
1935 if (ret != (size_t) -1) {
1936 line = out_buffer;
1937 linelen = strlen(out_buffer);
1938 }
1939 }
1941 if (!view->ops->read(view, line))
1942 goto alloc_error;
1944 if (lines-- == 1)
1945 break;
1946 }
1948 {
1949 int digits;
1951 lines = view->lines;
1952 for (digits = 0; lines; digits++)
1953 lines /= 10;
1955 /* Keep the displayed view in sync with line number scaling. */
1956 if (digits != view->digits) {
1957 view->digits = digits;
1958 redraw_from = 0;
1959 }
1960 }
1962 if (!view_is_displayed(view))
1963 goto check_pipe;
1965 if (view == VIEW(REQ_VIEW_TREE)) {
1966 /* Clear the view and redraw everything since the tree sorting
1967 * might have rearranged things. */
1968 redraw_view(view);
1970 } else if (redraw_from >= 0) {
1971 /* If this is an incremental update, redraw the previous line
1972 * since for commits some members could have changed when
1973 * loading the main view. */
1974 if (redraw_from > 0)
1975 redraw_from--;
1977 /* Since revision graph visualization requires knowledge
1978 * about the parent commit, it causes a further one-off
1979 * needed to be redrawn for incremental updates. */
1980 if (redraw_from > 0 && opt_rev_graph)
1981 redraw_from--;
1983 /* Incrementally draw avoids flickering. */
1984 redraw_view_from(view, redraw_from);
1985 }
1987 /* Update the title _after_ the redraw so that if the redraw picks up a
1988 * commit reference in view->ref it'll be available here. */
1989 update_view_title(view);
1991 check_pipe:
1992 if (ferror(view->pipe)) {
1993 report("Failed to read: %s", strerror(errno));
1994 goto end;
1996 } else if (feof(view->pipe)) {
1997 report("");
1998 goto end;
1999 }
2001 return TRUE;
2003 alloc_error:
2004 report("Allocation failure");
2006 end:
2007 view->ops->read(view, NULL);
2008 end_update(view);
2009 return FALSE;
2010 }
2012 static struct line *
2013 add_line_data(struct view *view, void *data, enum line_type type)
2014 {
2015 struct line *line = &view->line[view->lines++];
2017 memset(line, 0, sizeof(*line));
2018 line->type = type;
2019 line->data = data;
2021 return line;
2022 }
2024 static struct line *
2025 add_line_text(struct view *view, char *data, enum line_type type)
2026 {
2027 if (data)
2028 data = strdup(data);
2030 return data ? add_line_data(view, data, type) : NULL;
2031 }
2034 /*
2035 * View opening
2036 */
2038 enum open_flags {
2039 OPEN_DEFAULT = 0, /* Use default view switching. */
2040 OPEN_SPLIT = 1, /* Split current view. */
2041 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2042 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2043 };
2045 static void
2046 open_view(struct view *prev, enum request request, enum open_flags flags)
2047 {
2048 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2049 bool split = !!(flags & OPEN_SPLIT);
2050 bool reload = !!(flags & OPEN_RELOAD);
2051 struct view *view = VIEW(request);
2052 int nviews = displayed_views();
2053 struct view *base_view = display[0];
2055 if (view == prev && nviews == 1 && !reload) {
2056 report("Already in %s view", view->name);
2057 return;
2058 }
2060 if (view->ops->open) {
2061 if (!view->ops->open(view)) {
2062 report("Failed to load %s view", view->name);
2063 return;
2064 }
2066 } else if ((reload || strcmp(view->vid, view->id)) &&
2067 !begin_update(view)) {
2068 report("Failed to load %s view", view->name);
2069 return;
2070 }
2072 if (split) {
2073 display[1] = view;
2074 if (!backgrounded)
2075 current_view = 1;
2076 } else {
2077 /* Maximize the current view. */
2078 memset(display, 0, sizeof(display));
2079 current_view = 0;
2080 display[current_view] = view;
2081 }
2083 /* Resize the view when switching between split- and full-screen,
2084 * or when switching between two different full-screen views. */
2085 if (nviews != displayed_views() ||
2086 (nviews == 1 && base_view != display[0]))
2087 resize_display();
2089 if (split && prev->lineno - prev->offset >= prev->height) {
2090 /* Take the title line into account. */
2091 int lines = prev->lineno - prev->offset - prev->height + 1;
2093 /* Scroll the view that was split if the current line is
2094 * outside the new limited view. */
2095 do_scroll_view(prev, lines);
2096 }
2098 if (prev && view != prev) {
2099 if (split && !backgrounded) {
2100 /* "Blur" the previous view. */
2101 update_view_title(prev);
2102 }
2104 view->parent = prev;
2105 }
2107 if (view->pipe && view->lines == 0) {
2108 /* Clear the old view and let the incremental updating refill
2109 * the screen. */
2110 wclear(view->win);
2111 report("");
2112 } else {
2113 redraw_view(view);
2114 report("");
2115 }
2117 /* If the view is backgrounded the above calls to report()
2118 * won't redraw the view title. */
2119 if (backgrounded)
2120 update_view_title(view);
2121 }
2123 static void
2124 open_editor(struct view *view, char *file)
2125 {
2126 char cmd[SIZEOF_STR];
2127 char file_sq[SIZEOF_STR];
2128 char *editor;
2130 editor = getenv("GIT_EDITOR");
2131 if (!editor && *opt_editor)
2132 editor = opt_editor;
2133 if (!editor)
2134 editor = getenv("VISUAL");
2135 if (!editor)
2136 editor = getenv("EDITOR");
2137 if (!editor)
2138 editor = "vi";
2140 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2141 string_format(cmd, "%s %s", editor, file_sq)) {
2142 def_prog_mode(); /* save current tty modes */
2143 endwin(); /* restore original tty modes */
2144 system(cmd);
2145 reset_prog_mode();
2146 redraw_display();
2147 }
2148 }
2150 /*
2151 * User request switch noodle
2152 */
2154 static int
2155 view_driver(struct view *view, enum request request)
2156 {
2157 int i;
2159 if (view && view->lines) {
2160 request = view->ops->request(view, request, &view->line[view->lineno]);
2161 if (request == REQ_NONE)
2162 return TRUE;
2163 }
2165 switch (request) {
2166 case REQ_MOVE_UP:
2167 case REQ_MOVE_DOWN:
2168 case REQ_MOVE_PAGE_UP:
2169 case REQ_MOVE_PAGE_DOWN:
2170 case REQ_MOVE_FIRST_LINE:
2171 case REQ_MOVE_LAST_LINE:
2172 move_view(view, request);
2173 break;
2175 case REQ_SCROLL_LINE_DOWN:
2176 case REQ_SCROLL_LINE_UP:
2177 case REQ_SCROLL_PAGE_DOWN:
2178 case REQ_SCROLL_PAGE_UP:
2179 scroll_view(view, request);
2180 break;
2182 case REQ_VIEW_BLOB:
2183 if (!ref_blob[0]) {
2184 report("No file chosen, press %s to open tree view",
2185 get_key(REQ_VIEW_TREE));
2186 break;
2187 }
2188 open_view(view, request, OPEN_DEFAULT);
2189 break;
2191 case REQ_VIEW_PAGER:
2192 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2193 report("No pager content, press %s to run command from prompt",
2194 get_key(REQ_PROMPT));
2195 break;
2196 }
2197 open_view(view, request, OPEN_DEFAULT);
2198 break;
2200 case REQ_VIEW_MAIN:
2201 case REQ_VIEW_DIFF:
2202 case REQ_VIEW_LOG:
2203 case REQ_VIEW_TREE:
2204 case REQ_VIEW_HELP:
2205 case REQ_VIEW_STATUS:
2206 open_view(view, request, OPEN_DEFAULT);
2207 break;
2209 case REQ_NEXT:
2210 case REQ_PREVIOUS:
2211 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2213 if ((view == VIEW(REQ_VIEW_DIFF) &&
2214 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2215 (view == VIEW(REQ_VIEW_DIFF) &&
2216 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2217 (view == VIEW(REQ_VIEW_BLOB) &&
2218 view->parent == VIEW(REQ_VIEW_TREE))) {
2219 int line;
2221 view = view->parent;
2222 line = view->lineno;
2223 move_view(view, request);
2224 if (view_is_displayed(view))
2225 update_view_title(view);
2226 if (line != view->lineno)
2227 view->ops->request(view, REQ_ENTER,
2228 &view->line[view->lineno]);
2230 } else {
2231 move_view(view, request);
2232 }
2233 break;
2235 case REQ_VIEW_NEXT:
2236 {
2237 int nviews = displayed_views();
2238 int next_view = (current_view + 1) % nviews;
2240 if (next_view == current_view) {
2241 report("Only one view is displayed");
2242 break;
2243 }
2245 current_view = next_view;
2246 /* Blur out the title of the previous view. */
2247 update_view_title(view);
2248 report("");
2249 break;
2250 }
2251 case REQ_TOGGLE_LINENO:
2252 opt_line_number = !opt_line_number;
2253 redraw_display();
2254 break;
2256 case REQ_TOGGLE_REV_GRAPH:
2257 opt_rev_graph = !opt_rev_graph;
2258 redraw_display();
2259 break;
2261 case REQ_PROMPT:
2262 /* Always reload^Wrerun commands from the prompt. */
2263 open_view(view, opt_request, OPEN_RELOAD);
2264 break;
2266 case REQ_SEARCH:
2267 case REQ_SEARCH_BACK:
2268 search_view(view, request);
2269 break;
2271 case REQ_FIND_NEXT:
2272 case REQ_FIND_PREV:
2273 find_next(view, request);
2274 break;
2276 case REQ_STOP_LOADING:
2277 for (i = 0; i < ARRAY_SIZE(views); i++) {
2278 view = &views[i];
2279 if (view->pipe)
2280 report("Stopped loading the %s view", view->name),
2281 end_update(view);
2282 }
2283 break;
2285 case REQ_SHOW_VERSION:
2286 report("tig-%s (built %s)", VERSION, __DATE__);
2287 return TRUE;
2289 case REQ_SCREEN_RESIZE:
2290 resize_display();
2291 /* Fall-through */
2292 case REQ_SCREEN_REDRAW:
2293 redraw_display();
2294 break;
2296 case REQ_EDIT:
2297 report("Nothing to edit");
2298 break;
2300 case REQ_ENTER:
2301 report("Nothing to enter");
2302 break;
2304 case REQ_NONE:
2305 doupdate();
2306 return TRUE;
2308 case REQ_VIEW_CLOSE:
2309 /* XXX: Mark closed views by letting view->parent point to the
2310 * view itself. Parents to closed view should never be
2311 * followed. */
2312 if (view->parent &&
2313 view->parent->parent != view->parent) {
2314 memset(display, 0, sizeof(display));
2315 current_view = 0;
2316 display[current_view] = view->parent;
2317 view->parent = view;
2318 resize_display();
2319 redraw_display();
2320 break;
2321 }
2322 /* Fall-through */
2323 case REQ_QUIT:
2324 return FALSE;
2326 default:
2327 /* An unknown key will show most commonly used commands. */
2328 report("Unknown key, press 'h' for help");
2329 return TRUE;
2330 }
2332 return TRUE;
2333 }
2336 /*
2337 * Pager backend
2338 */
2340 static bool
2341 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2342 {
2343 char *text = line->data;
2344 enum line_type type = line->type;
2345 int textlen = strlen(text);
2346 int attr;
2348 wmove(view->win, lineno, 0);
2350 if (selected) {
2351 type = LINE_CURSOR;
2352 wchgat(view->win, -1, 0, type, NULL);
2353 }
2355 attr = get_line_attr(type);
2356 wattrset(view->win, attr);
2358 if (opt_line_number || opt_tab_size < TABSIZE) {
2359 static char spaces[] = " ";
2360 int col_offset = 0, col = 0;
2362 if (opt_line_number) {
2363 unsigned long real_lineno = view->offset + lineno + 1;
2365 if (real_lineno == 1 ||
2366 (real_lineno % opt_num_interval) == 0) {
2367 wprintw(view->win, "%.*d", view->digits, real_lineno);
2369 } else {
2370 waddnstr(view->win, spaces,
2371 MIN(view->digits, STRING_SIZE(spaces)));
2372 }
2373 waddstr(view->win, ": ");
2374 col_offset = view->digits + 2;
2375 }
2377 while (text && col_offset + col < view->width) {
2378 int cols_max = view->width - col_offset - col;
2379 char *pos = text;
2380 int cols;
2382 if (*text == '\t') {
2383 text++;
2384 assert(sizeof(spaces) > TABSIZE);
2385 pos = spaces;
2386 cols = opt_tab_size - (col % opt_tab_size);
2388 } else {
2389 text = strchr(text, '\t');
2390 cols = line ? text - pos : strlen(pos);
2391 }
2393 waddnstr(view->win, pos, MIN(cols, cols_max));
2394 col += cols;
2395 }
2397 } else {
2398 int col = 0, pos = 0;
2400 for (; pos < textlen && col < view->width; pos++, col++)
2401 if (text[pos] == '\t')
2402 col += TABSIZE - (col % TABSIZE) - 1;
2404 waddnstr(view->win, text, pos);
2405 }
2407 return TRUE;
2408 }
2410 static bool
2411 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2412 {
2413 char refbuf[SIZEOF_STR];
2414 char *ref = NULL;
2415 FILE *pipe;
2417 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2418 return TRUE;
2420 pipe = popen(refbuf, "r");
2421 if (!pipe)
2422 return TRUE;
2424 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2425 ref = chomp_string(ref);
2426 pclose(pipe);
2428 if (!ref || !*ref)
2429 return TRUE;
2431 /* This is the only fatal call, since it can "corrupt" the buffer. */
2432 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2433 return FALSE;
2435 return TRUE;
2436 }
2438 static void
2439 add_pager_refs(struct view *view, struct line *line)
2440 {
2441 char buf[SIZEOF_STR];
2442 char *commit_id = line->data + STRING_SIZE("commit ");
2443 struct ref **refs;
2444 size_t bufpos = 0, refpos = 0;
2445 const char *sep = "Refs: ";
2446 bool is_tag = FALSE;
2448 assert(line->type == LINE_COMMIT);
2450 refs = get_refs(commit_id);
2451 if (!refs) {
2452 if (view == VIEW(REQ_VIEW_DIFF))
2453 goto try_add_describe_ref;
2454 return;
2455 }
2457 do {
2458 struct ref *ref = refs[refpos];
2459 char *fmt = ref->tag ? "%s[%s]" :
2460 ref->remote ? "%s<%s>" : "%s%s";
2462 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2463 return;
2464 sep = ", ";
2465 if (ref->tag)
2466 is_tag = TRUE;
2467 } while (refs[refpos++]->next);
2469 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2470 try_add_describe_ref:
2471 /* Add <tag>-g<commit_id> "fake" reference. */
2472 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2473 return;
2474 }
2476 if (bufpos == 0)
2477 return;
2479 if (!realloc_lines(view, view->line_size + 1))
2480 return;
2482 add_line_text(view, buf, LINE_PP_REFS);
2483 }
2485 static bool
2486 pager_read(struct view *view, char *data)
2487 {
2488 struct line *line;
2490 if (!data)
2491 return TRUE;
2493 line = add_line_text(view, data, get_line_type(data));
2494 if (!line)
2495 return FALSE;
2497 if (line->type == LINE_COMMIT &&
2498 (view == VIEW(REQ_VIEW_DIFF) ||
2499 view == VIEW(REQ_VIEW_LOG)))
2500 add_pager_refs(view, line);
2502 return TRUE;
2503 }
2505 static enum request
2506 pager_request(struct view *view, enum request request, struct line *line)
2507 {
2508 int split = 0;
2510 if (request != REQ_ENTER)
2511 return request;
2513 if (line->type == LINE_COMMIT &&
2514 (view == VIEW(REQ_VIEW_LOG) ||
2515 view == VIEW(REQ_VIEW_PAGER))) {
2516 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2517 split = 1;
2518 }
2520 /* Always scroll the view even if it was split. That way
2521 * you can use Enter to scroll through the log view and
2522 * split open each commit diff. */
2523 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2525 /* FIXME: A minor workaround. Scrolling the view will call report("")
2526 * but if we are scrolling a non-current view this won't properly
2527 * update the view title. */
2528 if (split)
2529 update_view_title(view);
2531 return REQ_NONE;
2532 }
2534 static bool
2535 pager_grep(struct view *view, struct line *line)
2536 {
2537 regmatch_t pmatch;
2538 char *text = line->data;
2540 if (!*text)
2541 return FALSE;
2543 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2544 return FALSE;
2546 return TRUE;
2547 }
2549 static void
2550 pager_select(struct view *view, struct line *line)
2551 {
2552 if (line->type == LINE_COMMIT) {
2553 char *text = line->data + STRING_SIZE("commit ");
2555 if (view != VIEW(REQ_VIEW_PAGER))
2556 string_copy_rev(view->ref, text);
2557 string_copy_rev(ref_commit, text);
2558 }
2559 }
2561 static struct view_ops pager_ops = {
2562 "line",
2563 NULL,
2564 pager_read,
2565 pager_draw,
2566 pager_request,
2567 pager_grep,
2568 pager_select,
2569 };
2572 /*
2573 * Help backend
2574 */
2576 static bool
2577 help_open(struct view *view)
2578 {
2579 char buf[BUFSIZ];
2580 int lines = ARRAY_SIZE(req_info) + 2;
2581 int i;
2583 if (view->lines > 0)
2584 return TRUE;
2586 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2587 if (!req_info[i].request)
2588 lines++;
2590 view->line = calloc(lines, sizeof(*view->line));
2591 if (!view->line)
2592 return FALSE;
2594 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2596 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2597 char *key;
2599 if (!req_info[i].request) {
2600 add_line_text(view, "", LINE_DEFAULT);
2601 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2602 continue;
2603 }
2605 key = get_key(req_info[i].request);
2606 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2607 continue;
2609 add_line_text(view, buf, LINE_DEFAULT);
2610 }
2612 return TRUE;
2613 }
2615 static struct view_ops help_ops = {
2616 "line",
2617 help_open,
2618 NULL,
2619 pager_draw,
2620 pager_request,
2621 pager_grep,
2622 pager_select,
2623 };
2626 /*
2627 * Tree backend
2628 */
2630 /* Parse output from git-ls-tree(1):
2631 *
2632 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2633 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2634 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2635 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2636 */
2638 #define SIZEOF_TREE_ATTR \
2639 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2641 #define TREE_UP_FORMAT "040000 tree %s\t.."
2643 static int
2644 tree_compare_entry(enum line_type type1, char *name1,
2645 enum line_type type2, char *name2)
2646 {
2647 if (type1 != type2) {
2648 if (type1 == LINE_TREE_DIR)
2649 return -1;
2650 return 1;
2651 }
2653 return strcmp(name1, name2);
2654 }
2656 static bool
2657 tree_read(struct view *view, char *text)
2658 {
2659 size_t textlen = text ? strlen(text) : 0;
2660 char buf[SIZEOF_STR];
2661 unsigned long pos;
2662 enum line_type type;
2663 bool first_read = view->lines == 0;
2665 if (textlen <= SIZEOF_TREE_ATTR)
2666 return FALSE;
2668 type = text[STRING_SIZE("100644 ")] == 't'
2669 ? LINE_TREE_DIR : LINE_TREE_FILE;
2671 if (first_read) {
2672 /* Add path info line */
2673 if (!string_format(buf, "Directory path /%s", opt_path) ||
2674 !realloc_lines(view, view->line_size + 1) ||
2675 !add_line_text(view, buf, LINE_DEFAULT))
2676 return FALSE;
2678 /* Insert "link" to parent directory. */
2679 if (*opt_path) {
2680 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2681 !realloc_lines(view, view->line_size + 1) ||
2682 !add_line_text(view, buf, LINE_TREE_DIR))
2683 return FALSE;
2684 }
2685 }
2687 /* Strip the path part ... */
2688 if (*opt_path) {
2689 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2690 size_t striplen = strlen(opt_path);
2691 char *path = text + SIZEOF_TREE_ATTR;
2693 if (pathlen > striplen)
2694 memmove(path, path + striplen,
2695 pathlen - striplen + 1);
2696 }
2698 /* Skip "Directory ..." and ".." line. */
2699 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2700 struct line *line = &view->line[pos];
2701 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2702 char *path2 = text + SIZEOF_TREE_ATTR;
2703 int cmp = tree_compare_entry(line->type, path1, type, path2);
2705 if (cmp <= 0)
2706 continue;
2708 text = strdup(text);
2709 if (!text)
2710 return FALSE;
2712 if (view->lines > pos)
2713 memmove(&view->line[pos + 1], &view->line[pos],
2714 (view->lines - pos) * sizeof(*line));
2716 line = &view->line[pos];
2717 line->data = text;
2718 line->type = type;
2719 view->lines++;
2720 return TRUE;
2721 }
2723 if (!add_line_text(view, text, type))
2724 return FALSE;
2726 /* Move the current line to the first tree entry. */
2727 if (first_read)
2728 view->lineno++;
2730 return TRUE;
2731 }
2733 static enum request
2734 tree_request(struct view *view, enum request request, struct line *line)
2735 {
2736 enum open_flags flags;
2738 if (request != REQ_ENTER)
2739 return request;
2741 switch (line->type) {
2742 case LINE_TREE_DIR:
2743 /* Depending on whether it is a subdir or parent (updir?) link
2744 * mangle the path buffer. */
2745 if (line == &view->line[1] && *opt_path) {
2746 size_t path_len = strlen(opt_path);
2747 char *dirsep = opt_path + path_len - 1;
2749 while (dirsep > opt_path && dirsep[-1] != '/')
2750 dirsep--;
2752 dirsep[0] = 0;
2754 } else {
2755 size_t pathlen = strlen(opt_path);
2756 size_t origlen = pathlen;
2757 char *data = line->data;
2758 char *basename = data + SIZEOF_TREE_ATTR;
2760 if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
2761 opt_path[origlen] = 0;
2762 return TRUE;
2763 }
2764 }
2766 /* Trees and subtrees share the same ID, so they are not not
2767 * unique like blobs. */
2768 flags = OPEN_RELOAD;
2769 request = REQ_VIEW_TREE;
2770 break;
2772 case LINE_TREE_FILE:
2773 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2774 request = REQ_VIEW_BLOB;
2775 break;
2777 default:
2778 return TRUE;
2779 }
2781 open_view(view, request, flags);
2783 return REQ_NONE;
2784 }
2786 static void
2787 tree_select(struct view *view, struct line *line)
2788 {
2789 char *text = line->data + STRING_SIZE("100644 blob ");
2791 if (line->type == LINE_TREE_FILE) {
2792 string_copy_rev(ref_blob, text);
2794 } else if (line->type != LINE_TREE_DIR) {
2795 return;
2796 }
2798 string_copy_rev(view->ref, text);
2799 }
2801 static struct view_ops tree_ops = {
2802 "file",
2803 NULL,
2804 tree_read,
2805 pager_draw,
2806 tree_request,
2807 pager_grep,
2808 tree_select,
2809 };
2811 static bool
2812 blob_read(struct view *view, char *line)
2813 {
2814 return add_line_text(view, line, LINE_DEFAULT);
2815 }
2817 static struct view_ops blob_ops = {
2818 "line",
2819 NULL,
2820 blob_read,
2821 pager_draw,
2822 pager_request,
2823 pager_grep,
2824 pager_select,
2825 };
2828 /*
2829 * Status backend
2830 */
2832 struct status {
2833 char status;
2834 struct {
2835 mode_t mode;
2836 char rev[SIZEOF_REV];
2837 } old;
2838 struct {
2839 mode_t mode;
2840 char rev[SIZEOF_REV];
2841 } new;
2842 char name[SIZEOF_STR];
2843 };
2845 /* Get fields from the diff line:
2846 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
2847 */
2848 static inline bool
2849 status_get_diff(struct status *file, char *buf, size_t bufsize)
2850 {
2851 char *old_mode = buf + 1;
2852 char *new_mode = buf + 8;
2853 char *old_rev = buf + 15;
2854 char *new_rev = buf + 56;
2855 char *status = buf + 97;
2857 if (bufsize != 99 ||
2858 old_mode[-1] != ':' ||
2859 new_mode[-1] != ' ' ||
2860 old_rev[-1] != ' ' ||
2861 new_rev[-1] != ' ' ||
2862 status[-1] != ' ')
2863 return FALSE;
2865 file->status = *status;
2867 string_copy_rev(file->old.rev, old_rev);
2868 string_copy_rev(file->new.rev, new_rev);
2870 file->old.mode = strtoul(old_mode, NULL, 8);
2871 file->new.mode = strtoul(new_mode, NULL, 8);
2873 file->name[0] = 0;
2875 return TRUE;
2876 }
2878 static bool
2879 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
2880 {
2881 struct status *file = NULL;
2882 char buf[SIZEOF_STR * 4];
2883 size_t bufsize = 0;
2884 FILE *pipe;
2886 pipe = popen(cmd, "r");
2887 if (!pipe)
2888 return FALSE;
2890 add_line_data(view, NULL, type);
2892 while (!feof(pipe) && !ferror(pipe)) {
2893 char *sep;
2894 size_t readsize;
2896 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
2897 if (!readsize)
2898 break;
2899 bufsize += readsize;
2901 /* Process while we have NUL chars. */
2902 while ((sep = memchr(buf, 0, bufsize))) {
2903 size_t sepsize = sep - buf + 1;
2905 if (!file) {
2906 if (!realloc_lines(view, view->line_size + 1))
2907 goto error_out;
2909 file = calloc(1, sizeof(*file));
2910 if (!file)
2911 goto error_out;
2913 add_line_data(view, file, type);
2914 }
2916 /* Parse diff info part. */
2917 if (!diff) {
2918 file->status = '?';
2920 } else if (!file->status) {
2921 if (!status_get_diff(file, buf, sepsize))
2922 goto error_out;
2924 bufsize -= sepsize;
2925 memmove(buf, sep + 1, bufsize);
2927 sep = memchr(buf, 0, bufsize);
2928 if (!sep)
2929 break;
2930 sepsize = sep - buf + 1;
2931 }
2933 /* git-ls-files just delivers a NUL separated
2934 * list of file names similar to the second half
2935 * of the git-diff-* output. */
2936 string_ncopy(file->name, buf, sepsize);
2937 bufsize -= sepsize;
2938 memmove(buf, sep + 1, bufsize);
2939 file = NULL;
2940 }
2941 }
2943 if (ferror(pipe)) {
2944 error_out:
2945 pclose(pipe);
2946 return FALSE;
2947 }
2949 if (!view->line[view->lines - 1].data)
2950 add_line_data(view, NULL, LINE_STAT_NONE);
2952 pclose(pipe);
2953 return TRUE;
2954 }
2956 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --cached HEAD"
2957 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
2958 #define STATUS_LIST_OTHER_CMD \
2959 "git ls-files -z --others --exclude-per-directory=.gitignore"
2961 #define STATUS_DIFF_SHOW_CMD \
2962 "git diff --root --patch-with-stat --find-copies-harder -B -C %s -- %s 2>/dev/null"
2964 /* First parse staged info using git-diff-index(1), then parse unstaged
2965 * info using git-diff-files(1), and finally untracked files using
2966 * git-ls-files(1). */
2967 static bool
2968 status_open(struct view *view)
2969 {
2970 struct stat statbuf;
2971 char exclude[SIZEOF_STR];
2972 char cmd[SIZEOF_STR];
2973 size_t i;
2975 for (i = 0; i < view->lines; i++)
2976 free(view->line[i].data);
2977 free(view->line);
2978 view->lines = view->line_size = 0;
2979 view->line = NULL;
2981 if (!realloc_lines(view, view->line_size + 6))
2982 return FALSE;
2984 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
2985 return FALSE;
2987 string_copy(cmd, STATUS_LIST_OTHER_CMD);
2989 if (stat(exclude, &statbuf) >= 0) {
2990 size_t cmdsize = strlen(cmd);
2992 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
2993 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
2994 return FALSE;
2995 }
2997 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
2998 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
2999 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3000 return FALSE;
3002 return TRUE;
3003 }
3005 static bool
3006 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3007 {
3008 struct status *status = line->data;
3010 wmove(view->win, lineno, 0);
3012 if (selected) {
3013 wattrset(view->win, get_line_attr(LINE_CURSOR));
3014 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3016 } else if (!status && line->type != LINE_STAT_NONE) {
3017 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3018 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3020 } else {
3021 wattrset(view->win, get_line_attr(line->type));
3022 }
3024 if (!status) {
3025 char *text;
3027 switch (line->type) {
3028 case LINE_STAT_STAGED:
3029 text = "Changes to be committed:";
3030 break;
3032 case LINE_STAT_UNSTAGED:
3033 text = "Changed but not updated:";
3034 break;
3036 case LINE_STAT_UNTRACKED:
3037 text = "Untracked files:";
3038 break;
3040 case LINE_STAT_NONE:
3041 text = " (no files)";
3042 break;
3044 default:
3045 return FALSE;
3046 }
3048 waddstr(view->win, text);
3049 return TRUE;
3050 }
3052 waddch(view->win, status->status);
3053 if (!selected)
3054 wattrset(view->win, A_NORMAL);
3055 wmove(view->win, lineno, 4);
3056 waddstr(view->win, status->name);
3058 return TRUE;
3059 }
3061 static enum request
3062 status_enter(struct view *view, struct line *line)
3063 {
3064 struct status *status = line->data;
3065 char path[SIZEOF_STR] = "";
3066 char *info;
3067 size_t cmdsize = 0;
3069 if (line->type == LINE_STAT_NONE ||
3070 (!status && line[1].type == LINE_STAT_NONE)) {
3071 report("No file to diff");
3072 return REQ_NONE;
3073 }
3075 if (status && sq_quote(path, 0, status->name) >= sizeof(path))
3076 return REQ_QUIT;
3078 if (opt_cdup[0] &&
3079 line->type != LINE_STAT_UNTRACKED &&
3080 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3081 return REQ_QUIT;
3083 switch (line->type) {
3084 case LINE_STAT_STAGED:
3085 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3086 "--cached", path))
3087 return REQ_QUIT;
3088 if (status)
3089 info = "Staged changes to %s";
3090 else
3091 info = "Staged changes";
3092 break;
3094 case LINE_STAT_UNSTAGED:
3095 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3096 "", path))
3097 return REQ_QUIT;
3098 if (status)
3099 info = "Unstaged changes to %s";
3100 else
3101 info = "Unstaged changes";
3102 break;
3104 case LINE_STAT_UNTRACKED:
3105 if (opt_pipe)
3106 return REQ_QUIT;
3109 if (!status) {
3110 report("No file to show");
3111 return REQ_NONE;
3112 }
3114 opt_pipe = fopen(status->name, "r");
3115 info = "Untracked file %s";
3116 break;
3118 default:
3119 die("w00t");
3120 }
3122 open_view(view, REQ_VIEW_DIFF, OPEN_RELOAD | OPEN_SPLIT);
3123 if (view_is_displayed(VIEW(REQ_VIEW_DIFF))) {
3124 string_format(VIEW(REQ_VIEW_DIFF)->ref, info, status->name);
3125 }
3127 return REQ_NONE;
3128 }
3131 static bool
3132 status_update_file(struct view *view, struct status *status, enum line_type type)
3133 {
3134 char cmd[SIZEOF_STR];
3135 char buf[SIZEOF_STR];
3136 size_t cmdsize = 0;
3137 size_t bufsize = 0;
3138 size_t written = 0;
3139 FILE *pipe;
3141 if (opt_cdup[0] &&
3142 type != LINE_STAT_UNTRACKED &&
3143 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3144 return FALSE;
3146 switch (type) {
3147 case LINE_STAT_STAGED:
3148 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3149 status->old.mode,
3150 status->old.rev,
3151 status->name, 0))
3152 return FALSE;
3154 string_add(cmd, cmdsize, "git update-index -z --index-info");
3155 break;
3157 case LINE_STAT_UNSTAGED:
3158 case LINE_STAT_UNTRACKED:
3159 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3160 return FALSE;
3162 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3163 break;
3165 default:
3166 die("w00t");
3167 }
3169 pipe = popen(cmd, "w");
3170 if (!pipe)
3171 return FALSE;
3173 while (!ferror(pipe) && written < bufsize) {
3174 written += fwrite(buf + written, 1, bufsize - written, pipe);
3175 }
3177 pclose(pipe);
3179 if (written != bufsize)
3180 return FALSE;
3182 return TRUE;
3183 }
3185 static void
3186 status_update(struct view *view)
3187 {
3188 struct line *line = &view->line[view->lineno];
3190 assert(view->lines);
3192 if (!line->data) {
3193 while (++line < view->line + view->lines && line->data) {
3194 if (!status_update_file(view, line->data, line->type))
3195 report("Failed to update file status");
3196 }
3198 if (!line[-1].data) {
3199 report("Nothing to update");
3200 return;
3201 }
3203 } else if (!status_update_file(view, line->data, line->type)) {
3204 report("Failed to update file status");
3205 }
3207 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3208 }
3210 static enum request
3211 status_request(struct view *view, enum request request, struct line *line)
3212 {
3213 struct status *status = line->data;
3215 switch (request) {
3216 case REQ_STATUS_UPDATE:
3217 status_update(view);
3218 break;
3220 case REQ_EDIT:
3221 if (!status)
3222 return request;
3224 open_editor(view, status->name);
3225 break;
3227 case REQ_ENTER:
3228 status_enter(view, line);
3229 break;
3231 default:
3232 return request;
3233 }
3235 return REQ_NONE;
3236 }
3238 static void
3239 status_select(struct view *view, struct line *line)
3240 {
3241 struct status *status = line->data;
3242 char file[SIZEOF_STR] = "all files";
3243 char *text;
3245 if (status && !string_format(file, "'%s'", status->name))
3246 return;
3248 if (!status && line[1].type == LINE_STAT_NONE)
3249 line++;
3251 switch (line->type) {
3252 case LINE_STAT_STAGED:
3253 text = "Press %s to unstage %s for commit";
3254 break;
3256 case LINE_STAT_UNSTAGED:
3257 text = "Press %s to stage %s for commit";
3258 break;
3260 case LINE_STAT_UNTRACKED:
3261 text = "Press %s to stage %s for addition";
3262 break;
3264 case LINE_STAT_NONE:
3265 text = "Nothing to update";
3266 break;
3268 default:
3269 die("w00t");
3270 }
3272 string_format(view->ref, text, get_key(REQ_STATUS_UPDATE), file);
3273 }
3275 static bool
3276 status_grep(struct view *view, struct line *line)
3277 {
3278 struct status *status = line->data;
3279 enum { S_STATUS, S_NAME, S_END } state;
3280 char buf[2] = "?";
3281 regmatch_t pmatch;
3283 if (!status)
3284 return FALSE;
3286 for (state = S_STATUS; state < S_END; state++) {
3287 char *text;
3289 switch (state) {
3290 case S_NAME: text = status->name; break;
3291 case S_STATUS:
3292 buf[0] = status->status;
3293 text = buf;
3294 break;
3296 default:
3297 return FALSE;
3298 }
3300 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3301 return TRUE;
3302 }
3304 return FALSE;
3305 }
3307 static struct view_ops status_ops = {
3308 "file",
3309 status_open,
3310 NULL,
3311 status_draw,
3312 status_request,
3313 status_grep,
3314 status_select,
3315 };
3318 /*
3319 * Revision graph
3320 */
3322 struct commit {
3323 char id[SIZEOF_REV]; /* SHA1 ID. */
3324 char title[128]; /* First line of the commit message. */
3325 char author[75]; /* Author of the commit. */
3326 struct tm time; /* Date from the author ident. */
3327 struct ref **refs; /* Repository references. */
3328 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3329 size_t graph_size; /* The width of the graph array. */
3330 };
3332 /* Size of rev graph with no "padding" columns */
3333 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3335 struct rev_graph {
3336 struct rev_graph *prev, *next, *parents;
3337 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3338 size_t size;
3339 struct commit *commit;
3340 size_t pos;
3341 };
3343 /* Parents of the commit being visualized. */
3344 static struct rev_graph graph_parents[4];
3346 /* The current stack of revisions on the graph. */
3347 static struct rev_graph graph_stacks[4] = {
3348 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3349 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3350 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3351 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3352 };
3354 static inline bool
3355 graph_parent_is_merge(struct rev_graph *graph)
3356 {
3357 return graph->parents->size > 1;
3358 }
3360 static inline void
3361 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3362 {
3363 struct commit *commit = graph->commit;
3365 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3366 commit->graph[commit->graph_size++] = symbol;
3367 }
3369 static void
3370 done_rev_graph(struct rev_graph *graph)
3371 {
3372 if (graph_parent_is_merge(graph) &&
3373 graph->pos < graph->size - 1 &&
3374 graph->next->size == graph->size + graph->parents->size - 1) {
3375 size_t i = graph->pos + graph->parents->size - 1;
3377 graph->commit->graph_size = i * 2;
3378 while (i < graph->next->size - 1) {
3379 append_to_rev_graph(graph, ' ');
3380 append_to_rev_graph(graph, '\\');
3381 i++;
3382 }
3383 }
3385 graph->size = graph->pos = 0;
3386 graph->commit = NULL;
3387 memset(graph->parents, 0, sizeof(*graph->parents));
3388 }
3390 static void
3391 push_rev_graph(struct rev_graph *graph, char *parent)
3392 {
3393 int i;
3395 /* "Collapse" duplicate parents lines.
3396 *
3397 * FIXME: This needs to also update update the drawn graph but
3398 * for now it just serves as a method for pruning graph lines. */
3399 for (i = 0; i < graph->size; i++)
3400 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3401 return;
3403 if (graph->size < SIZEOF_REVITEMS) {
3404 string_copy_rev(graph->rev[graph->size++], parent);
3405 }
3406 }
3408 static chtype
3409 get_rev_graph_symbol(struct rev_graph *graph)
3410 {
3411 chtype symbol;
3413 if (graph->parents->size == 0)
3414 symbol = REVGRAPH_INIT;
3415 else if (graph_parent_is_merge(graph))
3416 symbol = REVGRAPH_MERGE;
3417 else if (graph->pos >= graph->size)
3418 symbol = REVGRAPH_BRANCH;
3419 else
3420 symbol = REVGRAPH_COMMIT;
3422 return symbol;
3423 }
3425 static void
3426 draw_rev_graph(struct rev_graph *graph)
3427 {
3428 struct rev_filler {
3429 chtype separator, line;
3430 };
3431 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3432 static struct rev_filler fillers[] = {
3433 { ' ', REVGRAPH_LINE },
3434 { '`', '.' },
3435 { '\'', ' ' },
3436 { '/', ' ' },
3437 };
3438 chtype symbol = get_rev_graph_symbol(graph);
3439 struct rev_filler *filler;
3440 size_t i;
3442 filler = &fillers[DEFAULT];
3444 for (i = 0; i < graph->pos; i++) {
3445 append_to_rev_graph(graph, filler->line);
3446 if (graph_parent_is_merge(graph->prev) &&
3447 graph->prev->pos == i)
3448 filler = &fillers[RSHARP];
3450 append_to_rev_graph(graph, filler->separator);
3451 }
3453 /* Place the symbol for this revision. */
3454 append_to_rev_graph(graph, symbol);
3456 if (graph->prev->size > graph->size)
3457 filler = &fillers[RDIAG];
3458 else
3459 filler = &fillers[DEFAULT];
3461 i++;
3463 for (; i < graph->size; i++) {
3464 append_to_rev_graph(graph, filler->separator);
3465 append_to_rev_graph(graph, filler->line);
3466 if (graph_parent_is_merge(graph->prev) &&
3467 i < graph->prev->pos + graph->parents->size)
3468 filler = &fillers[RSHARP];
3469 if (graph->prev->size > graph->size)
3470 filler = &fillers[LDIAG];
3471 }
3473 if (graph->prev->size > graph->size) {
3474 append_to_rev_graph(graph, filler->separator);
3475 if (filler->line != ' ')
3476 append_to_rev_graph(graph, filler->line);
3477 }
3478 }
3480 /* Prepare the next rev graph */
3481 static void
3482 prepare_rev_graph(struct rev_graph *graph)
3483 {
3484 size_t i;
3486 /* First, traverse all lines of revisions up to the active one. */
3487 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
3488 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
3489 break;
3491 push_rev_graph(graph->next, graph->rev[graph->pos]);
3492 }
3494 /* Interleave the new revision parent(s). */
3495 for (i = 0; i < graph->parents->size; i++)
3496 push_rev_graph(graph->next, graph->parents->rev[i]);
3498 /* Lastly, put any remaining revisions. */
3499 for (i = graph->pos + 1; i < graph->size; i++)
3500 push_rev_graph(graph->next, graph->rev[i]);
3501 }
3503 static void
3504 update_rev_graph(struct rev_graph *graph)
3505 {
3506 /* If this is the finalizing update ... */
3507 if (graph->commit)
3508 prepare_rev_graph(graph);
3510 /* Graph visualization needs a one rev look-ahead,
3511 * so the first update doesn't visualize anything. */
3512 if (!graph->prev->commit)
3513 return;
3515 draw_rev_graph(graph->prev);
3516 done_rev_graph(graph->prev->prev);
3517 }
3520 /*
3521 * Main view backend
3522 */
3524 static bool
3525 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3526 {
3527 char buf[DATE_COLS + 1];
3528 struct commit *commit = line->data;
3529 enum line_type type;
3530 int col = 0;
3531 size_t timelen;
3532 size_t authorlen;
3533 int trimmed = 1;
3535 if (!*commit->author)
3536 return FALSE;
3538 wmove(view->win, lineno, col);
3540 if (selected) {
3541 type = LINE_CURSOR;
3542 wattrset(view->win, get_line_attr(type));
3543 wchgat(view->win, -1, 0, type, NULL);
3545 } else {
3546 type = LINE_MAIN_COMMIT;
3547 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3548 }
3550 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
3551 waddnstr(view->win, buf, timelen);
3552 waddstr(view->win, " ");
3554 col += DATE_COLS;
3555 wmove(view->win, lineno, col);
3556 if (type != LINE_CURSOR)
3557 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3559 if (opt_utf8) {
3560 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
3561 } else {
3562 authorlen = strlen(commit->author);
3563 if (authorlen > AUTHOR_COLS - 2) {
3564 authorlen = AUTHOR_COLS - 2;
3565 trimmed = 1;
3566 }
3567 }
3569 if (trimmed) {
3570 waddnstr(view->win, commit->author, authorlen);
3571 if (type != LINE_CURSOR)
3572 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
3573 waddch(view->win, '~');
3574 } else {
3575 waddstr(view->win, commit->author);
3576 }
3578 col += AUTHOR_COLS;
3579 if (type != LINE_CURSOR)
3580 wattrset(view->win, A_NORMAL);
3582 if (opt_rev_graph && commit->graph_size) {
3583 size_t i;
3585 wmove(view->win, lineno, col);
3586 /* Using waddch() instead of waddnstr() ensures that
3587 * they'll be rendered correctly for the cursor line. */
3588 for (i = 0; i < commit->graph_size; i++)
3589 waddch(view->win, commit->graph[i]);
3591 waddch(view->win, ' ');
3592 col += commit->graph_size + 1;
3593 }
3595 wmove(view->win, lineno, col);
3597 if (commit->refs) {
3598 size_t i = 0;
3600 do {
3601 if (type == LINE_CURSOR)
3602 ;
3603 else if (commit->refs[i]->tag)
3604 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
3605 else if (commit->refs[i]->remote)
3606 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
3607 else
3608 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
3609 waddstr(view->win, "[");
3610 waddstr(view->win, commit->refs[i]->name);
3611 waddstr(view->win, "]");
3612 if (type != LINE_CURSOR)
3613 wattrset(view->win, A_NORMAL);
3614 waddstr(view->win, " ");
3615 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
3616 } while (commit->refs[i++]->next);
3617 }
3619 if (type != LINE_CURSOR)
3620 wattrset(view->win, get_line_attr(type));
3622 {
3623 int titlelen = strlen(commit->title);
3625 if (col + titlelen > view->width)
3626 titlelen = view->width - col;
3628 waddnstr(view->win, commit->title, titlelen);
3629 }
3631 return TRUE;
3632 }
3634 /* Reads git log --pretty=raw output and parses it into the commit struct. */
3635 static bool
3636 main_read(struct view *view, char *line)
3637 {
3638 static struct rev_graph *graph = graph_stacks;
3639 enum line_type type;
3640 struct commit *commit;
3642 if (!line) {
3643 update_rev_graph(graph);
3644 return TRUE;
3645 }
3647 type = get_line_type(line);
3648 if (type == LINE_COMMIT) {
3649 commit = calloc(1, sizeof(struct commit));
3650 if (!commit)
3651 return FALSE;
3653 string_copy_rev(commit->id, line + STRING_SIZE("commit "));
3654 commit->refs = get_refs(commit->id);
3655 graph->commit = commit;
3656 add_line_data(view, commit, LINE_MAIN_COMMIT);
3657 return TRUE;
3658 }
3660 if (!view->lines)
3661 return TRUE;
3662 commit = view->line[view->lines - 1].data;
3664 switch (type) {
3665 case LINE_PARENT:
3666 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
3667 break;
3669 case LINE_AUTHOR:
3670 {
3671 /* Parse author lines where the name may be empty:
3672 * author <email@address.tld> 1138474660 +0100
3673 */
3674 char *ident = line + STRING_SIZE("author ");
3675 char *nameend = strchr(ident, '<');
3676 char *emailend = strchr(ident, '>');
3678 if (!nameend || !emailend)
3679 break;
3681 update_rev_graph(graph);
3682 graph = graph->next;
3684 *nameend = *emailend = 0;
3685 ident = chomp_string(ident);
3686 if (!*ident) {
3687 ident = chomp_string(nameend + 1);
3688 if (!*ident)
3689 ident = "Unknown";
3690 }
3692 string_ncopy(commit->author, ident, strlen(ident));
3694 /* Parse epoch and timezone */
3695 if (emailend[1] == ' ') {
3696 char *secs = emailend + 2;
3697 char *zone = strchr(secs, ' ');
3698 time_t time = (time_t) atol(secs);
3700 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
3701 long tz;
3703 zone++;
3704 tz = ('0' - zone[1]) * 60 * 60 * 10;
3705 tz += ('0' - zone[2]) * 60 * 60;
3706 tz += ('0' - zone[3]) * 60;
3707 tz += ('0' - zone[4]) * 60;
3709 if (zone[0] == '-')
3710 tz = -tz;
3712 time -= tz;
3713 }
3715 gmtime_r(&time, &commit->time);
3716 }
3717 break;
3718 }
3719 default:
3720 /* Fill in the commit title if it has not already been set. */
3721 if (commit->title[0])
3722 break;
3724 /* Require titles to start with a non-space character at the
3725 * offset used by git log. */
3726 if (strncmp(line, " ", 4))
3727 break;
3728 line += 4;
3729 /* Well, if the title starts with a whitespace character,
3730 * try to be forgiving. Otherwise we end up with no title. */
3731 while (isspace(*line))
3732 line++;
3733 if (*line == '\0')
3734 break;
3735 /* FIXME: More graceful handling of titles; append "..." to
3736 * shortened titles, etc. */
3738 string_ncopy(commit->title, line, strlen(line));
3739 }
3741 return TRUE;
3742 }
3744 static enum request
3745 main_request(struct view *view, enum request request, struct line *line)
3746 {
3747 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3749 if (request == REQ_ENTER)
3750 open_view(view, REQ_VIEW_DIFF, flags);
3751 else
3752 return request;
3754 return REQ_NONE;
3755 }
3757 static bool
3758 main_grep(struct view *view, struct line *line)
3759 {
3760 struct commit *commit = line->data;
3761 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
3762 char buf[DATE_COLS + 1];
3763 regmatch_t pmatch;
3765 for (state = S_TITLE; state < S_END; state++) {
3766 char *text;
3768 switch (state) {
3769 case S_TITLE: text = commit->title; break;
3770 case S_AUTHOR: text = commit->author; break;
3771 case S_DATE:
3772 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
3773 continue;
3774 text = buf;
3775 break;
3777 default:
3778 return FALSE;
3779 }
3781 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3782 return TRUE;
3783 }
3785 return FALSE;
3786 }
3788 static void
3789 main_select(struct view *view, struct line *line)
3790 {
3791 struct commit *commit = line->data;
3793 string_copy_rev(view->ref, commit->id);
3794 string_copy_rev(ref_commit, view->ref);
3795 }
3797 static struct view_ops main_ops = {
3798 "commit",
3799 NULL,
3800 main_read,
3801 main_draw,
3802 main_request,
3803 main_grep,
3804 main_select,
3805 };
3808 /*
3809 * Unicode / UTF-8 handling
3810 *
3811 * NOTE: Much of the following code for dealing with unicode is derived from
3812 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
3813 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
3814 */
3816 /* I've (over)annotated a lot of code snippets because I am not entirely
3817 * confident that the approach taken by this small UTF-8 interface is correct.
3818 * --jonas */
3820 static inline int
3821 unicode_width(unsigned long c)
3822 {
3823 if (c >= 0x1100 &&
3824 (c <= 0x115f /* Hangul Jamo */
3825 || c == 0x2329
3826 || c == 0x232a
3827 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
3828 /* CJK ... Yi */
3829 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
3830 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
3831 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
3832 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
3833 || (c >= 0xffe0 && c <= 0xffe6)
3834 || (c >= 0x20000 && c <= 0x2fffd)
3835 || (c >= 0x30000 && c <= 0x3fffd)))
3836 return 2;
3838 return 1;
3839 }
3841 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
3842 * Illegal bytes are set one. */
3843 static const unsigned char utf8_bytes[256] = {
3844 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,
3845 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,
3846 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,
3847 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,
3848 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,
3849 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,
3850 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,
3851 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,
3852 };
3854 /* Decode UTF-8 multi-byte representation into a unicode character. */
3855 static inline unsigned long
3856 utf8_to_unicode(const char *string, size_t length)
3857 {
3858 unsigned long unicode;
3860 switch (length) {
3861 case 1:
3862 unicode = string[0];
3863 break;
3864 case 2:
3865 unicode = (string[0] & 0x1f) << 6;
3866 unicode += (string[1] & 0x3f);
3867 break;
3868 case 3:
3869 unicode = (string[0] & 0x0f) << 12;
3870 unicode += ((string[1] & 0x3f) << 6);
3871 unicode += (string[2] & 0x3f);
3872 break;
3873 case 4:
3874 unicode = (string[0] & 0x0f) << 18;
3875 unicode += ((string[1] & 0x3f) << 12);
3876 unicode += ((string[2] & 0x3f) << 6);
3877 unicode += (string[3] & 0x3f);
3878 break;
3879 case 5:
3880 unicode = (string[0] & 0x0f) << 24;
3881 unicode += ((string[1] & 0x3f) << 18);
3882 unicode += ((string[2] & 0x3f) << 12);
3883 unicode += ((string[3] & 0x3f) << 6);
3884 unicode += (string[4] & 0x3f);
3885 break;
3886 case 6:
3887 unicode = (string[0] & 0x01) << 30;
3888 unicode += ((string[1] & 0x3f) << 24);
3889 unicode += ((string[2] & 0x3f) << 18);
3890 unicode += ((string[3] & 0x3f) << 12);
3891 unicode += ((string[4] & 0x3f) << 6);
3892 unicode += (string[5] & 0x3f);
3893 break;
3894 default:
3895 die("Invalid unicode length");
3896 }
3898 /* Invalid characters could return the special 0xfffd value but NUL
3899 * should be just as good. */
3900 return unicode > 0xffff ? 0 : unicode;
3901 }
3903 /* Calculates how much of string can be shown within the given maximum width
3904 * and sets trimmed parameter to non-zero value if all of string could not be
3905 * shown.
3906 *
3907 * Additionally, adds to coloffset how many many columns to move to align with
3908 * the expected position. Takes into account how multi-byte and double-width
3909 * characters will effect the cursor position.
3910 *
3911 * Returns the number of bytes to output from string to satisfy max_width. */
3912 static size_t
3913 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3914 {
3915 const char *start = string;
3916 const char *end = strchr(string, '\0');
3917 size_t mbwidth = 0;
3918 size_t width = 0;
3920 *trimmed = 0;
3922 while (string < end) {
3923 int c = *(unsigned char *) string;
3924 unsigned char bytes = utf8_bytes[c];
3925 size_t ucwidth;
3926 unsigned long unicode;
3928 if (string + bytes > end)
3929 break;
3931 /* Change representation to figure out whether
3932 * it is a single- or double-width character. */
3934 unicode = utf8_to_unicode(string, bytes);
3935 /* FIXME: Graceful handling of invalid unicode character. */
3936 if (!unicode)
3937 break;
3939 ucwidth = unicode_width(unicode);
3940 width += ucwidth;
3941 if (width > max_width) {
3942 *trimmed = 1;
3943 break;
3944 }
3946 /* The column offset collects the differences between the
3947 * number of bytes encoding a character and the number of
3948 * columns will be used for rendering said character.
3949 *
3950 * So if some character A is encoded in 2 bytes, but will be
3951 * represented on the screen using only 1 byte this will and up
3952 * adding 1 to the multi-byte column offset.
3953 *
3954 * Assumes that no double-width character can be encoding in
3955 * less than two bytes. */
3956 if (bytes > ucwidth)
3957 mbwidth += bytes - ucwidth;
3959 string += bytes;
3960 }
3962 *coloffset += mbwidth;
3964 return string - start;
3965 }
3968 /*
3969 * Status management
3970 */
3972 /* Whether or not the curses interface has been initialized. */
3973 static bool cursed = FALSE;
3975 /* The status window is used for polling keystrokes. */
3976 static WINDOW *status_win;
3978 static bool status_empty = TRUE;
3980 /* Update status and title window. */
3981 static void
3982 report(const char *msg, ...)
3983 {
3984 struct view *view = display[current_view];
3986 if (input_mode)
3987 return;
3989 if (!status_empty || *msg) {
3990 va_list args;
3992 va_start(args, msg);
3994 wmove(status_win, 0, 0);
3995 if (*msg) {
3996 vwprintw(status_win, msg, args);
3997 status_empty = FALSE;
3998 } else {
3999 status_empty = TRUE;
4000 }
4001 wclrtoeol(status_win);
4002 wrefresh(status_win);
4004 va_end(args);
4005 }
4007 update_view_title(view);
4008 update_display_cursor(view);
4009 }
4011 /* Controls when nodelay should be in effect when polling user input. */
4012 static void
4013 set_nonblocking_input(bool loading)
4014 {
4015 static unsigned int loading_views;
4017 if ((loading == FALSE && loading_views-- == 1) ||
4018 (loading == TRUE && loading_views++ == 0))
4019 nodelay(status_win, loading);
4020 }
4022 static void
4023 init_display(void)
4024 {
4025 int x, y;
4027 /* Initialize the curses library */
4028 if (isatty(STDIN_FILENO)) {
4029 cursed = !!initscr();
4030 } else {
4031 /* Leave stdin and stdout alone when acting as a pager. */
4032 FILE *io = fopen("/dev/tty", "r+");
4034 if (!io)
4035 die("Failed to open /dev/tty");
4036 cursed = !!newterm(NULL, io, io);
4037 }
4039 if (!cursed)
4040 die("Failed to initialize curses");
4042 nonl(); /* Tell curses not to do NL->CR/NL on output */
4043 cbreak(); /* Take input chars one at a time, no wait for \n */
4044 noecho(); /* Don't echo input */
4045 leaveok(stdscr, TRUE);
4047 if (has_colors())
4048 init_colors();
4050 getmaxyx(stdscr, y, x);
4051 status_win = newwin(1, 0, y - 1, 0);
4052 if (!status_win)
4053 die("Failed to create status window");
4055 /* Enable keyboard mapping */
4056 keypad(status_win, TRUE);
4057 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4058 }
4060 static char *
4061 read_prompt(const char *prompt)
4062 {
4063 enum { READING, STOP, CANCEL } status = READING;
4064 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4065 int pos = 0;
4067 while (status == READING) {
4068 struct view *view;
4069 int i, key;
4071 input_mode = TRUE;
4073 foreach_view (view, i)
4074 update_view(view);
4076 input_mode = FALSE;
4078 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4079 wclrtoeol(status_win);
4081 /* Refresh, accept single keystroke of input */
4082 key = wgetch(status_win);
4083 switch (key) {
4084 case KEY_RETURN:
4085 case KEY_ENTER:
4086 case '\n':
4087 status = pos ? STOP : CANCEL;
4088 break;
4090 case KEY_BACKSPACE:
4091 if (pos > 0)
4092 pos--;
4093 else
4094 status = CANCEL;
4095 break;
4097 case KEY_ESC:
4098 status = CANCEL;
4099 break;
4101 case ERR:
4102 break;
4104 default:
4105 if (pos >= sizeof(buf)) {
4106 report("Input string too long");
4107 return NULL;
4108 }
4110 if (isprint(key))
4111 buf[pos++] = (char) key;
4112 }
4113 }
4115 /* Clear the status window */
4116 status_empty = FALSE;
4117 report("");
4119 if (status == CANCEL)
4120 return NULL;
4122 buf[pos++] = 0;
4124 return buf;
4125 }
4127 /*
4128 * Repository references
4129 */
4131 static struct ref *refs;
4132 static size_t refs_size;
4134 /* Id <-> ref store */
4135 static struct ref ***id_refs;
4136 static size_t id_refs_size;
4138 static struct ref **
4139 get_refs(char *id)
4140 {
4141 struct ref ***tmp_id_refs;
4142 struct ref **ref_list = NULL;
4143 size_t ref_list_size = 0;
4144 size_t i;
4146 for (i = 0; i < id_refs_size; i++)
4147 if (!strcmp(id, id_refs[i][0]->id))
4148 return id_refs[i];
4150 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4151 if (!tmp_id_refs)
4152 return NULL;
4154 id_refs = tmp_id_refs;
4156 for (i = 0; i < refs_size; i++) {
4157 struct ref **tmp;
4159 if (strcmp(id, refs[i].id))
4160 continue;
4162 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
4163 if (!tmp) {
4164 if (ref_list)
4165 free(ref_list);
4166 return NULL;
4167 }
4169 ref_list = tmp;
4170 if (ref_list_size > 0)
4171 ref_list[ref_list_size - 1]->next = 1;
4172 ref_list[ref_list_size] = &refs[i];
4174 /* XXX: The properties of the commit chains ensures that we can
4175 * safely modify the shared ref. The repo references will
4176 * always be similar for the same id. */
4177 ref_list[ref_list_size]->next = 0;
4178 ref_list_size++;
4179 }
4181 if (ref_list)
4182 id_refs[id_refs_size++] = ref_list;
4184 return ref_list;
4185 }
4187 static int
4188 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4189 {
4190 struct ref *ref;
4191 bool tag = FALSE;
4192 bool remote = FALSE;
4194 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4195 /* Commits referenced by tags has "^{}" appended. */
4196 if (name[namelen - 1] != '}')
4197 return OK;
4199 while (namelen > 0 && name[namelen] != '^')
4200 namelen--;
4202 tag = TRUE;
4203 namelen -= STRING_SIZE("refs/tags/");
4204 name += STRING_SIZE("refs/tags/");
4206 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4207 remote = TRUE;
4208 namelen -= STRING_SIZE("refs/remotes/");
4209 name += STRING_SIZE("refs/remotes/");
4211 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4212 namelen -= STRING_SIZE("refs/heads/");
4213 name += STRING_SIZE("refs/heads/");
4215 } else if (!strcmp(name, "HEAD")) {
4216 return OK;
4217 }
4219 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4220 if (!refs)
4221 return ERR;
4223 ref = &refs[refs_size++];
4224 ref->name = malloc(namelen + 1);
4225 if (!ref->name)
4226 return ERR;
4228 strncpy(ref->name, name, namelen);
4229 ref->name[namelen] = 0;
4230 ref->tag = tag;
4231 ref->remote = remote;
4232 string_copy_rev(ref->id, id);
4234 return OK;
4235 }
4237 static int
4238 load_refs(void)
4239 {
4240 const char *cmd_env = getenv("TIG_LS_REMOTE");
4241 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4243 return read_properties(popen(cmd, "r"), "\t", read_ref);
4244 }
4246 static int
4247 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4248 {
4249 if (!strcmp(name, "i18n.commitencoding"))
4250 string_ncopy(opt_encoding, value, valuelen);
4252 if (!strcmp(name, "core.editor"))
4253 string_ncopy(opt_editor, value, valuelen);
4255 return OK;
4256 }
4258 static int
4259 load_repo_config(void)
4260 {
4261 return read_properties(popen("git repo-config --list", "r"),
4262 "=", read_repo_config_option);
4263 }
4265 static int
4266 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4267 {
4268 if (!opt_git_dir[0])
4269 string_ncopy(opt_git_dir, name, namelen);
4270 else
4271 string_ncopy(opt_cdup, name, namelen);
4272 return OK;
4273 }
4275 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4276 * must be the last one! */
4277 static int
4278 load_repo_info(void)
4279 {
4280 return read_properties(popen("git rev-parse --git-dir --show-cdup 2>/dev/null", "r"),
4281 "=", read_repo_info);
4282 }
4284 static int
4285 read_properties(FILE *pipe, const char *separators,
4286 int (*read_property)(char *, size_t, char *, size_t))
4287 {
4288 char buffer[BUFSIZ];
4289 char *name;
4290 int state = OK;
4292 if (!pipe)
4293 return ERR;
4295 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4296 char *value;
4297 size_t namelen;
4298 size_t valuelen;
4300 name = chomp_string(name);
4301 namelen = strcspn(name, separators);
4303 if (name[namelen]) {
4304 name[namelen] = 0;
4305 value = chomp_string(name + namelen + 1);
4306 valuelen = strlen(value);
4308 } else {
4309 value = "";
4310 valuelen = 0;
4311 }
4313 state = read_property(name, namelen, value, valuelen);
4314 }
4316 if (state != ERR && ferror(pipe))
4317 state = ERR;
4319 pclose(pipe);
4321 return state;
4322 }
4325 /*
4326 * Main
4327 */
4329 static void __NORETURN
4330 quit(int sig)
4331 {
4332 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4333 if (cursed)
4334 endwin();
4335 exit(0);
4336 }
4338 static void __NORETURN
4339 die(const char *err, ...)
4340 {
4341 va_list args;
4343 endwin();
4345 va_start(args, err);
4346 fputs("tig: ", stderr);
4347 vfprintf(stderr, err, args);
4348 fputs("\n", stderr);
4349 va_end(args);
4351 exit(1);
4352 }
4354 int
4355 main(int argc, char *argv[])
4356 {
4357 struct view *view;
4358 enum request request;
4359 size_t i;
4361 signal(SIGINT, quit);
4363 if (setlocale(LC_ALL, "")) {
4364 char *codeset = nl_langinfo(CODESET);
4366 string_ncopy(opt_codeset, codeset, strlen(codeset));
4367 }
4369 if (load_repo_info() == ERR)
4370 die("Failed to load repo info.");
4372 /* Require a git repository unless when running in pager mode. */
4373 if (!opt_git_dir[0])
4374 die("Not a git repository");
4376 if (load_options() == ERR)
4377 die("Failed to load user config.");
4379 /* Load the repo config file so options can be overwritten from
4380 * the command line. */
4381 if (load_repo_config() == ERR)
4382 die("Failed to load repo config.");
4384 if (!parse_options(argc, argv))
4385 return 0;
4387 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4388 opt_iconv = iconv_open(opt_codeset, opt_encoding);
4389 if (opt_iconv == ICONV_NONE)
4390 die("Failed to initialize character set conversion");
4391 }
4393 if (load_refs() == ERR)
4394 die("Failed to load refs.");
4396 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4397 view->cmd_env = getenv(view->cmd_env);
4399 request = opt_request;
4401 init_display();
4403 while (view_driver(display[current_view], request)) {
4404 int key;
4405 int i;
4407 foreach_view (view, i)
4408 update_view(view);
4410 /* Refresh, accept single keystroke of input */
4411 key = wgetch(status_win);
4413 /* wgetch() with nodelay() enabled returns ERR when there's no
4414 * input. */
4415 if (key == ERR) {
4416 request = REQ_NONE;
4417 continue;
4418 }
4420 request = get_keybinding(display[current_view]->keymap, key);
4422 /* Some low-level request handling. This keeps access to
4423 * status_win restricted. */
4424 switch (request) {
4425 case REQ_PROMPT:
4426 {
4427 char *cmd = read_prompt(":");
4429 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4430 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4431 opt_request = REQ_VIEW_DIFF;
4432 } else {
4433 opt_request = REQ_VIEW_PAGER;
4434 }
4435 break;
4436 }
4438 request = REQ_NONE;
4439 break;
4440 }
4441 case REQ_SEARCH:
4442 case REQ_SEARCH_BACK:
4443 {
4444 const char *prompt = request == REQ_SEARCH
4445 ? "/" : "?";
4446 char *search = read_prompt(prompt);
4448 if (search)
4449 string_ncopy(opt_search, search, strlen(search));
4450 else
4451 request = REQ_NONE;
4452 break;
4453 }
4454 case REQ_SCREEN_RESIZE:
4455 {
4456 int height, width;
4458 getmaxyx(stdscr, height, width);
4460 /* Resize the status view and let the view driver take
4461 * care of resizing the displayed views. */
4462 wresize(status_win, 1, width);
4463 mvwin(status_win, height - 1, 0);
4464 wrefresh(status_win);
4465 break;
4466 }
4467 default:
4468 break;
4469 }
4470 }
4472 quit(0);
4474 return 0;
4475 }