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 if (view == VIEW(REQ_VIEW_PAGER))
1822 string_copy(view->ref, view->cmd);
1823 else
1824 view->ref[0] = 0;
1826 } else if (view == VIEW(REQ_VIEW_TREE)) {
1827 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1828 char path[SIZEOF_STR];
1830 if (strcmp(view->vid, view->id))
1831 opt_path[0] = path[0] = 0;
1832 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1833 return FALSE;
1835 if (!string_format(view->cmd, format, view->id, path))
1836 return FALSE;
1838 } else {
1839 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1840 const char *id = view->id;
1842 if (!string_format(view->cmd, format, id, id, id, id, id))
1843 return FALSE;
1845 /* Put the current ref_* value to the view title ref
1846 * member. This is needed by the blob view. Most other
1847 * views sets it automatically after loading because the
1848 * first line is a commit line. */
1849 string_copy_rev(view->ref, view->id);
1850 }
1852 /* Special case for the pager view. */
1853 if (opt_pipe) {
1854 view->pipe = opt_pipe;
1855 opt_pipe = NULL;
1856 } else {
1857 view->pipe = popen(view->cmd, "r");
1858 }
1860 if (!view->pipe)
1861 return FALSE;
1863 set_nonblocking_input(TRUE);
1865 view->offset = 0;
1866 view->lines = 0;
1867 view->lineno = 0;
1868 string_copy_rev(view->vid, view->id);
1870 if (view->line) {
1871 int i;
1873 for (i = 0; i < view->lines; i++)
1874 if (view->line[i].data)
1875 free(view->line[i].data);
1877 free(view->line);
1878 view->line = NULL;
1879 }
1881 view->start_time = time(NULL);
1883 return TRUE;
1884 }
1886 static struct line *
1887 realloc_lines(struct view *view, size_t line_size)
1888 {
1889 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1891 if (!tmp)
1892 return NULL;
1894 view->line = tmp;
1895 view->line_size = line_size;
1896 return view->line;
1897 }
1899 static bool
1900 update_view(struct view *view)
1901 {
1902 char in_buffer[BUFSIZ];
1903 char out_buffer[BUFSIZ * 2];
1904 char *line;
1905 /* The number of lines to read. If too low it will cause too much
1906 * redrawing (and possible flickering), if too high responsiveness
1907 * will suffer. */
1908 unsigned long lines = view->height;
1909 int redraw_from = -1;
1911 if (!view->pipe)
1912 return TRUE;
1914 /* Only redraw if lines are visible. */
1915 if (view->offset + view->height >= view->lines)
1916 redraw_from = view->lines - view->offset;
1918 /* FIXME: This is probably not perfect for backgrounded views. */
1919 if (!realloc_lines(view, view->lines + lines))
1920 goto alloc_error;
1922 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1923 size_t linelen = strlen(line);
1925 if (linelen)
1926 line[linelen - 1] = 0;
1928 if (opt_iconv != ICONV_NONE) {
1929 char *inbuf = line;
1930 size_t inlen = linelen;
1932 char *outbuf = out_buffer;
1933 size_t outlen = sizeof(out_buffer);
1935 size_t ret;
1937 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
1938 if (ret != (size_t) -1) {
1939 line = out_buffer;
1940 linelen = strlen(out_buffer);
1941 }
1942 }
1944 if (!view->ops->read(view, line))
1945 goto alloc_error;
1947 if (lines-- == 1)
1948 break;
1949 }
1951 {
1952 int digits;
1954 lines = view->lines;
1955 for (digits = 0; lines; digits++)
1956 lines /= 10;
1958 /* Keep the displayed view in sync with line number scaling. */
1959 if (digits != view->digits) {
1960 view->digits = digits;
1961 redraw_from = 0;
1962 }
1963 }
1965 if (!view_is_displayed(view))
1966 goto check_pipe;
1968 if (view == VIEW(REQ_VIEW_TREE)) {
1969 /* Clear the view and redraw everything since the tree sorting
1970 * might have rearranged things. */
1971 redraw_view(view);
1973 } else if (redraw_from >= 0) {
1974 /* If this is an incremental update, redraw the previous line
1975 * since for commits some members could have changed when
1976 * loading the main view. */
1977 if (redraw_from > 0)
1978 redraw_from--;
1980 /* Since revision graph visualization requires knowledge
1981 * about the parent commit, it causes a further one-off
1982 * needed to be redrawn for incremental updates. */
1983 if (redraw_from > 0 && opt_rev_graph)
1984 redraw_from--;
1986 /* Incrementally draw avoids flickering. */
1987 redraw_view_from(view, redraw_from);
1988 }
1990 /* Update the title _after_ the redraw so that if the redraw picks up a
1991 * commit reference in view->ref it'll be available here. */
1992 update_view_title(view);
1994 check_pipe:
1995 if (ferror(view->pipe)) {
1996 report("Failed to read: %s", strerror(errno));
1997 goto end;
1999 } else if (feof(view->pipe)) {
2000 report("");
2001 goto end;
2002 }
2004 return TRUE;
2006 alloc_error:
2007 report("Allocation failure");
2009 end:
2010 view->ops->read(view, NULL);
2011 end_update(view);
2012 return FALSE;
2013 }
2015 static struct line *
2016 add_line_data(struct view *view, void *data, enum line_type type)
2017 {
2018 struct line *line = &view->line[view->lines++];
2020 memset(line, 0, sizeof(*line));
2021 line->type = type;
2022 line->data = data;
2024 return line;
2025 }
2027 static struct line *
2028 add_line_text(struct view *view, char *data, enum line_type type)
2029 {
2030 if (data)
2031 data = strdup(data);
2033 return data ? add_line_data(view, data, type) : NULL;
2034 }
2037 /*
2038 * View opening
2039 */
2041 enum open_flags {
2042 OPEN_DEFAULT = 0, /* Use default view switching. */
2043 OPEN_SPLIT = 1, /* Split current view. */
2044 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2045 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2046 };
2048 static void
2049 open_view(struct view *prev, enum request request, enum open_flags flags)
2050 {
2051 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2052 bool split = !!(flags & OPEN_SPLIT);
2053 bool reload = !!(flags & OPEN_RELOAD);
2054 struct view *view = VIEW(request);
2055 int nviews = displayed_views();
2056 struct view *base_view = display[0];
2058 if (view == prev && nviews == 1 && !reload) {
2059 report("Already in %s view", view->name);
2060 return;
2061 }
2063 if (view->ops->open) {
2064 if (!view->ops->open(view)) {
2065 report("Failed to load %s view", view->name);
2066 return;
2067 }
2069 } else if ((reload || strcmp(view->vid, view->id)) &&
2070 !begin_update(view)) {
2071 report("Failed to load %s view", view->name);
2072 return;
2073 }
2075 if (split) {
2076 display[1] = view;
2077 if (!backgrounded)
2078 current_view = 1;
2079 } else {
2080 /* Maximize the current view. */
2081 memset(display, 0, sizeof(display));
2082 current_view = 0;
2083 display[current_view] = view;
2084 }
2086 /* Resize the view when switching between split- and full-screen,
2087 * or when switching between two different full-screen views. */
2088 if (nviews != displayed_views() ||
2089 (nviews == 1 && base_view != display[0]))
2090 resize_display();
2092 if (split && prev->lineno - prev->offset >= prev->height) {
2093 /* Take the title line into account. */
2094 int lines = prev->lineno - prev->offset - prev->height + 1;
2096 /* Scroll the view that was split if the current line is
2097 * outside the new limited view. */
2098 do_scroll_view(prev, lines);
2099 }
2101 if (prev && view != prev) {
2102 if (split && !backgrounded) {
2103 /* "Blur" the previous view. */
2104 update_view_title(prev);
2105 }
2107 view->parent = prev;
2108 }
2110 if (view->pipe && view->lines == 0) {
2111 /* Clear the old view and let the incremental updating refill
2112 * the screen. */
2113 wclear(view->win);
2114 report("");
2115 } else {
2116 redraw_view(view);
2117 report("");
2118 }
2120 /* If the view is backgrounded the above calls to report()
2121 * won't redraw the view title. */
2122 if (backgrounded)
2123 update_view_title(view);
2124 }
2126 static void
2127 open_editor(struct view *view, char *file)
2128 {
2129 char cmd[SIZEOF_STR];
2130 char file_sq[SIZEOF_STR];
2131 char *editor;
2133 editor = getenv("GIT_EDITOR");
2134 if (!editor && *opt_editor)
2135 editor = opt_editor;
2136 if (!editor)
2137 editor = getenv("VISUAL");
2138 if (!editor)
2139 editor = getenv("EDITOR");
2140 if (!editor)
2141 editor = "vi";
2143 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2144 string_format(cmd, "%s %s", editor, file_sq)) {
2145 def_prog_mode(); /* save current tty modes */
2146 endwin(); /* restore original tty modes */
2147 system(cmd);
2148 reset_prog_mode();
2149 redraw_display();
2150 }
2151 }
2153 /*
2154 * User request switch noodle
2155 */
2157 static int
2158 view_driver(struct view *view, enum request request)
2159 {
2160 int i;
2162 if (view && view->lines) {
2163 request = view->ops->request(view, request, &view->line[view->lineno]);
2164 if (request == REQ_NONE)
2165 return TRUE;
2166 }
2168 switch (request) {
2169 case REQ_MOVE_UP:
2170 case REQ_MOVE_DOWN:
2171 case REQ_MOVE_PAGE_UP:
2172 case REQ_MOVE_PAGE_DOWN:
2173 case REQ_MOVE_FIRST_LINE:
2174 case REQ_MOVE_LAST_LINE:
2175 move_view(view, request);
2176 break;
2178 case REQ_SCROLL_LINE_DOWN:
2179 case REQ_SCROLL_LINE_UP:
2180 case REQ_SCROLL_PAGE_DOWN:
2181 case REQ_SCROLL_PAGE_UP:
2182 scroll_view(view, request);
2183 break;
2185 case REQ_VIEW_BLOB:
2186 if (!ref_blob[0]) {
2187 report("No file chosen, press %s to open tree view",
2188 get_key(REQ_VIEW_TREE));
2189 break;
2190 }
2191 open_view(view, request, OPEN_DEFAULT);
2192 break;
2194 case REQ_VIEW_PAGER:
2195 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2196 report("No pager content, press %s to run command from prompt",
2197 get_key(REQ_PROMPT));
2198 break;
2199 }
2200 open_view(view, request, OPEN_DEFAULT);
2201 break;
2203 case REQ_VIEW_MAIN:
2204 case REQ_VIEW_DIFF:
2205 case REQ_VIEW_LOG:
2206 case REQ_VIEW_TREE:
2207 case REQ_VIEW_HELP:
2208 case REQ_VIEW_STATUS:
2209 open_view(view, request, OPEN_DEFAULT);
2210 break;
2212 case REQ_NEXT:
2213 case REQ_PREVIOUS:
2214 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2216 if ((view == VIEW(REQ_VIEW_DIFF) &&
2217 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2218 (view == VIEW(REQ_VIEW_DIFF) &&
2219 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2220 (view == VIEW(REQ_VIEW_BLOB) &&
2221 view->parent == VIEW(REQ_VIEW_TREE))) {
2222 int line;
2224 view = view->parent;
2225 line = view->lineno;
2226 move_view(view, request);
2227 if (view_is_displayed(view))
2228 update_view_title(view);
2229 if (line != view->lineno)
2230 view->ops->request(view, REQ_ENTER,
2231 &view->line[view->lineno]);
2233 } else {
2234 move_view(view, request);
2235 }
2236 break;
2238 case REQ_VIEW_NEXT:
2239 {
2240 int nviews = displayed_views();
2241 int next_view = (current_view + 1) % nviews;
2243 if (next_view == current_view) {
2244 report("Only one view is displayed");
2245 break;
2246 }
2248 current_view = next_view;
2249 /* Blur out the title of the previous view. */
2250 update_view_title(view);
2251 report("");
2252 break;
2253 }
2254 case REQ_TOGGLE_LINENO:
2255 opt_line_number = !opt_line_number;
2256 redraw_display();
2257 break;
2259 case REQ_TOGGLE_REV_GRAPH:
2260 opt_rev_graph = !opt_rev_graph;
2261 redraw_display();
2262 break;
2264 case REQ_PROMPT:
2265 /* Always reload^Wrerun commands from the prompt. */
2266 open_view(view, opt_request, OPEN_RELOAD);
2267 break;
2269 case REQ_SEARCH:
2270 case REQ_SEARCH_BACK:
2271 search_view(view, request);
2272 break;
2274 case REQ_FIND_NEXT:
2275 case REQ_FIND_PREV:
2276 find_next(view, request);
2277 break;
2279 case REQ_STOP_LOADING:
2280 for (i = 0; i < ARRAY_SIZE(views); i++) {
2281 view = &views[i];
2282 if (view->pipe)
2283 report("Stopped loading the %s view", view->name),
2284 end_update(view);
2285 }
2286 break;
2288 case REQ_SHOW_VERSION:
2289 report("tig-%s (built %s)", VERSION, __DATE__);
2290 return TRUE;
2292 case REQ_SCREEN_RESIZE:
2293 resize_display();
2294 /* Fall-through */
2295 case REQ_SCREEN_REDRAW:
2296 redraw_display();
2297 break;
2299 case REQ_EDIT:
2300 report("Nothing to edit");
2301 break;
2303 case REQ_ENTER:
2304 report("Nothing to enter");
2305 break;
2307 case REQ_NONE:
2308 doupdate();
2309 return TRUE;
2311 case REQ_VIEW_CLOSE:
2312 /* XXX: Mark closed views by letting view->parent point to the
2313 * view itself. Parents to closed view should never be
2314 * followed. */
2315 if (view->parent &&
2316 view->parent->parent != view->parent) {
2317 memset(display, 0, sizeof(display));
2318 current_view = 0;
2319 display[current_view] = view->parent;
2320 view->parent = view;
2321 resize_display();
2322 redraw_display();
2323 break;
2324 }
2325 /* Fall-through */
2326 case REQ_QUIT:
2327 return FALSE;
2329 default:
2330 /* An unknown key will show most commonly used commands. */
2331 report("Unknown key, press 'h' for help");
2332 return TRUE;
2333 }
2335 return TRUE;
2336 }
2339 /*
2340 * Pager backend
2341 */
2343 static bool
2344 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2345 {
2346 char *text = line->data;
2347 enum line_type type = line->type;
2348 int textlen = strlen(text);
2349 int attr;
2351 wmove(view->win, lineno, 0);
2353 if (selected) {
2354 type = LINE_CURSOR;
2355 wchgat(view->win, -1, 0, type, NULL);
2356 }
2358 attr = get_line_attr(type);
2359 wattrset(view->win, attr);
2361 if (opt_line_number || opt_tab_size < TABSIZE) {
2362 static char spaces[] = " ";
2363 int col_offset = 0, col = 0;
2365 if (opt_line_number) {
2366 unsigned long real_lineno = view->offset + lineno + 1;
2368 if (real_lineno == 1 ||
2369 (real_lineno % opt_num_interval) == 0) {
2370 wprintw(view->win, "%.*d", view->digits, real_lineno);
2372 } else {
2373 waddnstr(view->win, spaces,
2374 MIN(view->digits, STRING_SIZE(spaces)));
2375 }
2376 waddstr(view->win, ": ");
2377 col_offset = view->digits + 2;
2378 }
2380 while (text && col_offset + col < view->width) {
2381 int cols_max = view->width - col_offset - col;
2382 char *pos = text;
2383 int cols;
2385 if (*text == '\t') {
2386 text++;
2387 assert(sizeof(spaces) > TABSIZE);
2388 pos = spaces;
2389 cols = opt_tab_size - (col % opt_tab_size);
2391 } else {
2392 text = strchr(text, '\t');
2393 cols = line ? text - pos : strlen(pos);
2394 }
2396 waddnstr(view->win, pos, MIN(cols, cols_max));
2397 col += cols;
2398 }
2400 } else {
2401 int col = 0, pos = 0;
2403 for (; pos < textlen && col < view->width; pos++, col++)
2404 if (text[pos] == '\t')
2405 col += TABSIZE - (col % TABSIZE) - 1;
2407 waddnstr(view->win, text, pos);
2408 }
2410 return TRUE;
2411 }
2413 static bool
2414 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2415 {
2416 char refbuf[SIZEOF_STR];
2417 char *ref = NULL;
2418 FILE *pipe;
2420 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2421 return TRUE;
2423 pipe = popen(refbuf, "r");
2424 if (!pipe)
2425 return TRUE;
2427 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2428 ref = chomp_string(ref);
2429 pclose(pipe);
2431 if (!ref || !*ref)
2432 return TRUE;
2434 /* This is the only fatal call, since it can "corrupt" the buffer. */
2435 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2436 return FALSE;
2438 return TRUE;
2439 }
2441 static void
2442 add_pager_refs(struct view *view, struct line *line)
2443 {
2444 char buf[SIZEOF_STR];
2445 char *commit_id = line->data + STRING_SIZE("commit ");
2446 struct ref **refs;
2447 size_t bufpos = 0, refpos = 0;
2448 const char *sep = "Refs: ";
2449 bool is_tag = FALSE;
2451 assert(line->type == LINE_COMMIT);
2453 refs = get_refs(commit_id);
2454 if (!refs) {
2455 if (view == VIEW(REQ_VIEW_DIFF))
2456 goto try_add_describe_ref;
2457 return;
2458 }
2460 do {
2461 struct ref *ref = refs[refpos];
2462 char *fmt = ref->tag ? "%s[%s]" :
2463 ref->remote ? "%s<%s>" : "%s%s";
2465 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2466 return;
2467 sep = ", ";
2468 if (ref->tag)
2469 is_tag = TRUE;
2470 } while (refs[refpos++]->next);
2472 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2473 try_add_describe_ref:
2474 /* Add <tag>-g<commit_id> "fake" reference. */
2475 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2476 return;
2477 }
2479 if (bufpos == 0)
2480 return;
2482 if (!realloc_lines(view, view->line_size + 1))
2483 return;
2485 add_line_text(view, buf, LINE_PP_REFS);
2486 }
2488 static bool
2489 pager_read(struct view *view, char *data)
2490 {
2491 struct line *line;
2493 if (!data)
2494 return TRUE;
2496 line = add_line_text(view, data, get_line_type(data));
2497 if (!line)
2498 return FALSE;
2500 if (line->type == LINE_COMMIT &&
2501 (view == VIEW(REQ_VIEW_DIFF) ||
2502 view == VIEW(REQ_VIEW_LOG)))
2503 add_pager_refs(view, line);
2505 return TRUE;
2506 }
2508 static enum request
2509 pager_request(struct view *view, enum request request, struct line *line)
2510 {
2511 int split = 0;
2513 if (request != REQ_ENTER)
2514 return request;
2516 if (line->type == LINE_COMMIT &&
2517 (view == VIEW(REQ_VIEW_LOG) ||
2518 view == VIEW(REQ_VIEW_PAGER))) {
2519 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2520 split = 1;
2521 }
2523 /* Always scroll the view even if it was split. That way
2524 * you can use Enter to scroll through the log view and
2525 * split open each commit diff. */
2526 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2528 /* FIXME: A minor workaround. Scrolling the view will call report("")
2529 * but if we are scrolling a non-current view this won't properly
2530 * update the view title. */
2531 if (split)
2532 update_view_title(view);
2534 return REQ_NONE;
2535 }
2537 static bool
2538 pager_grep(struct view *view, struct line *line)
2539 {
2540 regmatch_t pmatch;
2541 char *text = line->data;
2543 if (!*text)
2544 return FALSE;
2546 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2547 return FALSE;
2549 return TRUE;
2550 }
2552 static void
2553 pager_select(struct view *view, struct line *line)
2554 {
2555 if (line->type == LINE_COMMIT) {
2556 char *text = line->data + STRING_SIZE("commit ");
2558 if (view != VIEW(REQ_VIEW_PAGER))
2559 string_copy_rev(view->ref, text);
2560 string_copy_rev(ref_commit, text);
2561 }
2562 }
2564 static struct view_ops pager_ops = {
2565 "line",
2566 NULL,
2567 pager_read,
2568 pager_draw,
2569 pager_request,
2570 pager_grep,
2571 pager_select,
2572 };
2575 /*
2576 * Help backend
2577 */
2579 static bool
2580 help_open(struct view *view)
2581 {
2582 char buf[BUFSIZ];
2583 int lines = ARRAY_SIZE(req_info) + 2;
2584 int i;
2586 if (view->lines > 0)
2587 return TRUE;
2589 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2590 if (!req_info[i].request)
2591 lines++;
2593 view->line = calloc(lines, sizeof(*view->line));
2594 if (!view->line)
2595 return FALSE;
2597 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2599 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2600 char *key;
2602 if (!req_info[i].request) {
2603 add_line_text(view, "", LINE_DEFAULT);
2604 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2605 continue;
2606 }
2608 key = get_key(req_info[i].request);
2609 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2610 continue;
2612 add_line_text(view, buf, LINE_DEFAULT);
2613 }
2615 return TRUE;
2616 }
2618 static struct view_ops help_ops = {
2619 "line",
2620 help_open,
2621 NULL,
2622 pager_draw,
2623 pager_request,
2624 pager_grep,
2625 pager_select,
2626 };
2629 /*
2630 * Tree backend
2631 */
2633 /* Parse output from git-ls-tree(1):
2634 *
2635 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2636 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2637 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2638 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2639 */
2641 #define SIZEOF_TREE_ATTR \
2642 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2644 #define TREE_UP_FORMAT "040000 tree %s\t.."
2646 static int
2647 tree_compare_entry(enum line_type type1, char *name1,
2648 enum line_type type2, char *name2)
2649 {
2650 if (type1 != type2) {
2651 if (type1 == LINE_TREE_DIR)
2652 return -1;
2653 return 1;
2654 }
2656 return strcmp(name1, name2);
2657 }
2659 static bool
2660 tree_read(struct view *view, char *text)
2661 {
2662 size_t textlen = text ? strlen(text) : 0;
2663 char buf[SIZEOF_STR];
2664 unsigned long pos;
2665 enum line_type type;
2666 bool first_read = view->lines == 0;
2668 if (textlen <= SIZEOF_TREE_ATTR)
2669 return FALSE;
2671 type = text[STRING_SIZE("100644 ")] == 't'
2672 ? LINE_TREE_DIR : LINE_TREE_FILE;
2674 if (first_read) {
2675 /* Add path info line */
2676 if (!string_format(buf, "Directory path /%s", opt_path) ||
2677 !realloc_lines(view, view->line_size + 1) ||
2678 !add_line_text(view, buf, LINE_DEFAULT))
2679 return FALSE;
2681 /* Insert "link" to parent directory. */
2682 if (*opt_path) {
2683 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2684 !realloc_lines(view, view->line_size + 1) ||
2685 !add_line_text(view, buf, LINE_TREE_DIR))
2686 return FALSE;
2687 }
2688 }
2690 /* Strip the path part ... */
2691 if (*opt_path) {
2692 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2693 size_t striplen = strlen(opt_path);
2694 char *path = text + SIZEOF_TREE_ATTR;
2696 if (pathlen > striplen)
2697 memmove(path, path + striplen,
2698 pathlen - striplen + 1);
2699 }
2701 /* Skip "Directory ..." and ".." line. */
2702 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2703 struct line *line = &view->line[pos];
2704 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2705 char *path2 = text + SIZEOF_TREE_ATTR;
2706 int cmp = tree_compare_entry(line->type, path1, type, path2);
2708 if (cmp <= 0)
2709 continue;
2711 text = strdup(text);
2712 if (!text)
2713 return FALSE;
2715 if (view->lines > pos)
2716 memmove(&view->line[pos + 1], &view->line[pos],
2717 (view->lines - pos) * sizeof(*line));
2719 line = &view->line[pos];
2720 line->data = text;
2721 line->type = type;
2722 view->lines++;
2723 return TRUE;
2724 }
2726 if (!add_line_text(view, text, type))
2727 return FALSE;
2729 /* Move the current line to the first tree entry. */
2730 if (first_read)
2731 view->lineno++;
2733 return TRUE;
2734 }
2736 static enum request
2737 tree_request(struct view *view, enum request request, struct line *line)
2738 {
2739 enum open_flags flags;
2741 if (request != REQ_ENTER)
2742 return request;
2744 switch (line->type) {
2745 case LINE_TREE_DIR:
2746 /* Depending on whether it is a subdir or parent (updir?) link
2747 * mangle the path buffer. */
2748 if (line == &view->line[1] && *opt_path) {
2749 size_t path_len = strlen(opt_path);
2750 char *dirsep = opt_path + path_len - 1;
2752 while (dirsep > opt_path && dirsep[-1] != '/')
2753 dirsep--;
2755 dirsep[0] = 0;
2757 } else {
2758 size_t pathlen = strlen(opt_path);
2759 size_t origlen = pathlen;
2760 char *data = line->data;
2761 char *basename = data + SIZEOF_TREE_ATTR;
2763 if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
2764 opt_path[origlen] = 0;
2765 return TRUE;
2766 }
2767 }
2769 /* Trees and subtrees share the same ID, so they are not not
2770 * unique like blobs. */
2771 flags = OPEN_RELOAD;
2772 request = REQ_VIEW_TREE;
2773 break;
2775 case LINE_TREE_FILE:
2776 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2777 request = REQ_VIEW_BLOB;
2778 break;
2780 default:
2781 return TRUE;
2782 }
2784 open_view(view, request, flags);
2786 return REQ_NONE;
2787 }
2789 static void
2790 tree_select(struct view *view, struct line *line)
2791 {
2792 char *text = line->data + STRING_SIZE("100644 blob ");
2794 if (line->type == LINE_TREE_FILE) {
2795 string_copy_rev(ref_blob, text);
2797 } else if (line->type != LINE_TREE_DIR) {
2798 return;
2799 }
2801 string_copy_rev(view->ref, text);
2802 }
2804 static struct view_ops tree_ops = {
2805 "file",
2806 NULL,
2807 tree_read,
2808 pager_draw,
2809 tree_request,
2810 pager_grep,
2811 tree_select,
2812 };
2814 static bool
2815 blob_read(struct view *view, char *line)
2816 {
2817 return add_line_text(view, line, LINE_DEFAULT);
2818 }
2820 static struct view_ops blob_ops = {
2821 "line",
2822 NULL,
2823 blob_read,
2824 pager_draw,
2825 pager_request,
2826 pager_grep,
2827 pager_select,
2828 };
2831 /*
2832 * Status backend
2833 */
2835 struct status {
2836 char status;
2837 struct {
2838 mode_t mode;
2839 char rev[SIZEOF_REV];
2840 } old;
2841 struct {
2842 mode_t mode;
2843 char rev[SIZEOF_REV];
2844 } new;
2845 char name[SIZEOF_STR];
2846 };
2848 /* Get fields from the diff line:
2849 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
2850 */
2851 static inline bool
2852 status_get_diff(struct status *file, char *buf, size_t bufsize)
2853 {
2854 char *old_mode = buf + 1;
2855 char *new_mode = buf + 8;
2856 char *old_rev = buf + 15;
2857 char *new_rev = buf + 56;
2858 char *status = buf + 97;
2860 if (bufsize != 99 ||
2861 old_mode[-1] != ':' ||
2862 new_mode[-1] != ' ' ||
2863 old_rev[-1] != ' ' ||
2864 new_rev[-1] != ' ' ||
2865 status[-1] != ' ')
2866 return FALSE;
2868 file->status = *status;
2870 string_copy_rev(file->old.rev, old_rev);
2871 string_copy_rev(file->new.rev, new_rev);
2873 file->old.mode = strtoul(old_mode, NULL, 8);
2874 file->new.mode = strtoul(new_mode, NULL, 8);
2876 file->name[0] = 0;
2878 return TRUE;
2879 }
2881 static bool
2882 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
2883 {
2884 struct status *file = NULL;
2885 char buf[SIZEOF_STR * 4];
2886 size_t bufsize = 0;
2887 FILE *pipe;
2889 pipe = popen(cmd, "r");
2890 if (!pipe)
2891 return FALSE;
2893 add_line_data(view, NULL, type);
2895 while (!feof(pipe) && !ferror(pipe)) {
2896 char *sep;
2897 size_t readsize;
2899 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
2900 if (!readsize)
2901 break;
2902 bufsize += readsize;
2904 /* Process while we have NUL chars. */
2905 while ((sep = memchr(buf, 0, bufsize))) {
2906 size_t sepsize = sep - buf + 1;
2908 if (!file) {
2909 if (!realloc_lines(view, view->line_size + 1))
2910 goto error_out;
2912 file = calloc(1, sizeof(*file));
2913 if (!file)
2914 goto error_out;
2916 add_line_data(view, file, type);
2917 }
2919 /* Parse diff info part. */
2920 if (!diff) {
2921 file->status = '?';
2923 } else if (!file->status) {
2924 if (!status_get_diff(file, buf, sepsize))
2925 goto error_out;
2927 bufsize -= sepsize;
2928 memmove(buf, sep + 1, bufsize);
2930 sep = memchr(buf, 0, bufsize);
2931 if (!sep)
2932 break;
2933 sepsize = sep - buf + 1;
2934 }
2936 /* git-ls-files just delivers a NUL separated
2937 * list of file names similar to the second half
2938 * of the git-diff-* output. */
2939 string_ncopy(file->name, buf, sepsize);
2940 bufsize -= sepsize;
2941 memmove(buf, sep + 1, bufsize);
2942 file = NULL;
2943 }
2944 }
2946 if (ferror(pipe)) {
2947 error_out:
2948 pclose(pipe);
2949 return FALSE;
2950 }
2952 if (!view->line[view->lines - 1].data)
2953 add_line_data(view, NULL, LINE_STAT_NONE);
2955 pclose(pipe);
2956 return TRUE;
2957 }
2959 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --cached HEAD"
2960 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
2961 #define STATUS_LIST_OTHER_CMD \
2962 "git ls-files -z --others --exclude-per-directory=.gitignore"
2964 #define STATUS_DIFF_SHOW_CMD \
2965 "git diff --root --patch-with-stat --find-copies-harder -B -C %s -- %s 2>/dev/null"
2967 /* First parse staged info using git-diff-index(1), then parse unstaged
2968 * info using git-diff-files(1), and finally untracked files using
2969 * git-ls-files(1). */
2970 static bool
2971 status_open(struct view *view)
2972 {
2973 struct stat statbuf;
2974 char exclude[SIZEOF_STR];
2975 char cmd[SIZEOF_STR];
2976 size_t i;
2978 for (i = 0; i < view->lines; i++)
2979 free(view->line[i].data);
2980 free(view->line);
2981 view->lines = view->line_size = 0;
2982 view->line = NULL;
2984 if (!realloc_lines(view, view->line_size + 6))
2985 return FALSE;
2987 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
2988 return FALSE;
2990 string_copy(cmd, STATUS_LIST_OTHER_CMD);
2992 if (stat(exclude, &statbuf) >= 0) {
2993 size_t cmdsize = strlen(cmd);
2995 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
2996 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
2997 return FALSE;
2998 }
3000 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3001 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3002 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3003 return FALSE;
3005 return TRUE;
3006 }
3008 static bool
3009 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3010 {
3011 struct status *status = line->data;
3013 wmove(view->win, lineno, 0);
3015 if (selected) {
3016 wattrset(view->win, get_line_attr(LINE_CURSOR));
3017 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3019 } else if (!status && line->type != LINE_STAT_NONE) {
3020 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3021 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3023 } else {
3024 wattrset(view->win, get_line_attr(line->type));
3025 }
3027 if (!status) {
3028 char *text;
3030 switch (line->type) {
3031 case LINE_STAT_STAGED:
3032 text = "Changes to be committed:";
3033 break;
3035 case LINE_STAT_UNSTAGED:
3036 text = "Changed but not updated:";
3037 break;
3039 case LINE_STAT_UNTRACKED:
3040 text = "Untracked files:";
3041 break;
3043 case LINE_STAT_NONE:
3044 text = " (no files)";
3045 break;
3047 default:
3048 return FALSE;
3049 }
3051 waddstr(view->win, text);
3052 return TRUE;
3053 }
3055 waddch(view->win, status->status);
3056 if (!selected)
3057 wattrset(view->win, A_NORMAL);
3058 wmove(view->win, lineno, 4);
3059 waddstr(view->win, status->name);
3061 return TRUE;
3062 }
3064 static enum request
3065 status_enter(struct view *view, struct line *line)
3066 {
3067 struct status *status = line->data;
3068 char path[SIZEOF_STR] = "";
3069 char *info;
3070 size_t cmdsize = 0;
3072 if (line->type == LINE_STAT_NONE ||
3073 (!status && line[1].type == LINE_STAT_NONE)) {
3074 report("No file to diff");
3075 return REQ_NONE;
3076 }
3078 if (status && sq_quote(path, 0, status->name) >= sizeof(path))
3079 return REQ_QUIT;
3081 if (opt_cdup[0] &&
3082 line->type != LINE_STAT_UNTRACKED &&
3083 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3084 return REQ_QUIT;
3086 switch (line->type) {
3087 case LINE_STAT_STAGED:
3088 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3089 "--cached", path))
3090 return REQ_QUIT;
3091 if (status)
3092 info = "Staged changes to %s";
3093 else
3094 info = "Staged changes";
3095 break;
3097 case LINE_STAT_UNSTAGED:
3098 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3099 "", path))
3100 return REQ_QUIT;
3101 if (status)
3102 info = "Unstaged changes to %s";
3103 else
3104 info = "Unstaged changes";
3105 break;
3107 case LINE_STAT_UNTRACKED:
3108 if (opt_pipe)
3109 return REQ_QUIT;
3112 if (!status) {
3113 report("No file to show");
3114 return REQ_NONE;
3115 }
3117 opt_pipe = fopen(status->name, "r");
3118 info = "Untracked file %s";
3119 break;
3121 default:
3122 die("w00t");
3123 }
3125 open_view(view, REQ_VIEW_DIFF, OPEN_RELOAD | OPEN_SPLIT);
3126 if (view_is_displayed(VIEW(REQ_VIEW_DIFF))) {
3127 string_format(VIEW(REQ_VIEW_DIFF)->ref, info, status->name);
3128 }
3130 return REQ_NONE;
3131 }
3134 static bool
3135 status_update_file(struct view *view, struct status *status, enum line_type type)
3136 {
3137 char cmd[SIZEOF_STR];
3138 char buf[SIZEOF_STR];
3139 size_t cmdsize = 0;
3140 size_t bufsize = 0;
3141 size_t written = 0;
3142 FILE *pipe;
3144 if (opt_cdup[0] &&
3145 type != LINE_STAT_UNTRACKED &&
3146 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3147 return FALSE;
3149 switch (type) {
3150 case LINE_STAT_STAGED:
3151 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3152 status->old.mode,
3153 status->old.rev,
3154 status->name, 0))
3155 return FALSE;
3157 string_add(cmd, cmdsize, "git update-index -z --index-info");
3158 break;
3160 case LINE_STAT_UNSTAGED:
3161 case LINE_STAT_UNTRACKED:
3162 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3163 return FALSE;
3165 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3166 break;
3168 default:
3169 die("w00t");
3170 }
3172 pipe = popen(cmd, "w");
3173 if (!pipe)
3174 return FALSE;
3176 while (!ferror(pipe) && written < bufsize) {
3177 written += fwrite(buf + written, 1, bufsize - written, pipe);
3178 }
3180 pclose(pipe);
3182 if (written != bufsize)
3183 return FALSE;
3185 return TRUE;
3186 }
3188 static void
3189 status_update(struct view *view)
3190 {
3191 struct line *line = &view->line[view->lineno];
3193 assert(view->lines);
3195 if (!line->data) {
3196 while (++line < view->line + view->lines && line->data) {
3197 if (!status_update_file(view, line->data, line->type))
3198 report("Failed to update file status");
3199 }
3201 if (!line[-1].data) {
3202 report("Nothing to update");
3203 return;
3204 }
3206 } else if (!status_update_file(view, line->data, line->type)) {
3207 report("Failed to update file status");
3208 }
3210 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3211 }
3213 static enum request
3214 status_request(struct view *view, enum request request, struct line *line)
3215 {
3216 struct status *status = line->data;
3218 switch (request) {
3219 case REQ_STATUS_UPDATE:
3220 status_update(view);
3221 break;
3223 case REQ_EDIT:
3224 if (!status)
3225 return request;
3227 open_editor(view, status->name);
3228 break;
3230 case REQ_ENTER:
3231 status_enter(view, line);
3232 break;
3234 default:
3235 return request;
3236 }
3238 return REQ_NONE;
3239 }
3241 static void
3242 status_select(struct view *view, struct line *line)
3243 {
3244 struct status *status = line->data;
3245 char file[SIZEOF_STR] = "all files";
3246 char *text;
3248 if (status && !string_format(file, "'%s'", status->name))
3249 return;
3251 if (!status && line[1].type == LINE_STAT_NONE)
3252 line++;
3254 switch (line->type) {
3255 case LINE_STAT_STAGED:
3256 text = "Press %s to unstage %s for commit";
3257 break;
3259 case LINE_STAT_UNSTAGED:
3260 text = "Press %s to stage %s for commit";
3261 break;
3263 case LINE_STAT_UNTRACKED:
3264 text = "Press %s to stage %s for addition";
3265 break;
3267 case LINE_STAT_NONE:
3268 text = "Nothing to update";
3269 break;
3271 default:
3272 die("w00t");
3273 }
3275 string_format(view->ref, text, get_key(REQ_STATUS_UPDATE), file);
3276 }
3278 static bool
3279 status_grep(struct view *view, struct line *line)
3280 {
3281 struct status *status = line->data;
3282 enum { S_STATUS, S_NAME, S_END } state;
3283 char buf[2] = "?";
3284 regmatch_t pmatch;
3286 if (!status)
3287 return FALSE;
3289 for (state = S_STATUS; state < S_END; state++) {
3290 char *text;
3292 switch (state) {
3293 case S_NAME: text = status->name; break;
3294 case S_STATUS:
3295 buf[0] = status->status;
3296 text = buf;
3297 break;
3299 default:
3300 return FALSE;
3301 }
3303 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3304 return TRUE;
3305 }
3307 return FALSE;
3308 }
3310 static struct view_ops status_ops = {
3311 "file",
3312 status_open,
3313 NULL,
3314 status_draw,
3315 status_request,
3316 status_grep,
3317 status_select,
3318 };
3321 /*
3322 * Revision graph
3323 */
3325 struct commit {
3326 char id[SIZEOF_REV]; /* SHA1 ID. */
3327 char title[128]; /* First line of the commit message. */
3328 char author[75]; /* Author of the commit. */
3329 struct tm time; /* Date from the author ident. */
3330 struct ref **refs; /* Repository references. */
3331 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3332 size_t graph_size; /* The width of the graph array. */
3333 };
3335 /* Size of rev graph with no "padding" columns */
3336 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3338 struct rev_graph {
3339 struct rev_graph *prev, *next, *parents;
3340 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3341 size_t size;
3342 struct commit *commit;
3343 size_t pos;
3344 };
3346 /* Parents of the commit being visualized. */
3347 static struct rev_graph graph_parents[4];
3349 /* The current stack of revisions on the graph. */
3350 static struct rev_graph graph_stacks[4] = {
3351 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3352 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3353 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3354 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3355 };
3357 static inline bool
3358 graph_parent_is_merge(struct rev_graph *graph)
3359 {
3360 return graph->parents->size > 1;
3361 }
3363 static inline void
3364 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3365 {
3366 struct commit *commit = graph->commit;
3368 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3369 commit->graph[commit->graph_size++] = symbol;
3370 }
3372 static void
3373 done_rev_graph(struct rev_graph *graph)
3374 {
3375 if (graph_parent_is_merge(graph) &&
3376 graph->pos < graph->size - 1 &&
3377 graph->next->size == graph->size + graph->parents->size - 1) {
3378 size_t i = graph->pos + graph->parents->size - 1;
3380 graph->commit->graph_size = i * 2;
3381 while (i < graph->next->size - 1) {
3382 append_to_rev_graph(graph, ' ');
3383 append_to_rev_graph(graph, '\\');
3384 i++;
3385 }
3386 }
3388 graph->size = graph->pos = 0;
3389 graph->commit = NULL;
3390 memset(graph->parents, 0, sizeof(*graph->parents));
3391 }
3393 static void
3394 push_rev_graph(struct rev_graph *graph, char *parent)
3395 {
3396 int i;
3398 /* "Collapse" duplicate parents lines.
3399 *
3400 * FIXME: This needs to also update update the drawn graph but
3401 * for now it just serves as a method for pruning graph lines. */
3402 for (i = 0; i < graph->size; i++)
3403 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3404 return;
3406 if (graph->size < SIZEOF_REVITEMS) {
3407 string_copy_rev(graph->rev[graph->size++], parent);
3408 }
3409 }
3411 static chtype
3412 get_rev_graph_symbol(struct rev_graph *graph)
3413 {
3414 chtype symbol;
3416 if (graph->parents->size == 0)
3417 symbol = REVGRAPH_INIT;
3418 else if (graph_parent_is_merge(graph))
3419 symbol = REVGRAPH_MERGE;
3420 else if (graph->pos >= graph->size)
3421 symbol = REVGRAPH_BRANCH;
3422 else
3423 symbol = REVGRAPH_COMMIT;
3425 return symbol;
3426 }
3428 static void
3429 draw_rev_graph(struct rev_graph *graph)
3430 {
3431 struct rev_filler {
3432 chtype separator, line;
3433 };
3434 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3435 static struct rev_filler fillers[] = {
3436 { ' ', REVGRAPH_LINE },
3437 { '`', '.' },
3438 { '\'', ' ' },
3439 { '/', ' ' },
3440 };
3441 chtype symbol = get_rev_graph_symbol(graph);
3442 struct rev_filler *filler;
3443 size_t i;
3445 filler = &fillers[DEFAULT];
3447 for (i = 0; i < graph->pos; i++) {
3448 append_to_rev_graph(graph, filler->line);
3449 if (graph_parent_is_merge(graph->prev) &&
3450 graph->prev->pos == i)
3451 filler = &fillers[RSHARP];
3453 append_to_rev_graph(graph, filler->separator);
3454 }
3456 /* Place the symbol for this revision. */
3457 append_to_rev_graph(graph, symbol);
3459 if (graph->prev->size > graph->size)
3460 filler = &fillers[RDIAG];
3461 else
3462 filler = &fillers[DEFAULT];
3464 i++;
3466 for (; i < graph->size; i++) {
3467 append_to_rev_graph(graph, filler->separator);
3468 append_to_rev_graph(graph, filler->line);
3469 if (graph_parent_is_merge(graph->prev) &&
3470 i < graph->prev->pos + graph->parents->size)
3471 filler = &fillers[RSHARP];
3472 if (graph->prev->size > graph->size)
3473 filler = &fillers[LDIAG];
3474 }
3476 if (graph->prev->size > graph->size) {
3477 append_to_rev_graph(graph, filler->separator);
3478 if (filler->line != ' ')
3479 append_to_rev_graph(graph, filler->line);
3480 }
3481 }
3483 /* Prepare the next rev graph */
3484 static void
3485 prepare_rev_graph(struct rev_graph *graph)
3486 {
3487 size_t i;
3489 /* First, traverse all lines of revisions up to the active one. */
3490 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
3491 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
3492 break;
3494 push_rev_graph(graph->next, graph->rev[graph->pos]);
3495 }
3497 /* Interleave the new revision parent(s). */
3498 for (i = 0; i < graph->parents->size; i++)
3499 push_rev_graph(graph->next, graph->parents->rev[i]);
3501 /* Lastly, put any remaining revisions. */
3502 for (i = graph->pos + 1; i < graph->size; i++)
3503 push_rev_graph(graph->next, graph->rev[i]);
3504 }
3506 static void
3507 update_rev_graph(struct rev_graph *graph)
3508 {
3509 /* If this is the finalizing update ... */
3510 if (graph->commit)
3511 prepare_rev_graph(graph);
3513 /* Graph visualization needs a one rev look-ahead,
3514 * so the first update doesn't visualize anything. */
3515 if (!graph->prev->commit)
3516 return;
3518 draw_rev_graph(graph->prev);
3519 done_rev_graph(graph->prev->prev);
3520 }
3523 /*
3524 * Main view backend
3525 */
3527 static bool
3528 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3529 {
3530 char buf[DATE_COLS + 1];
3531 struct commit *commit = line->data;
3532 enum line_type type;
3533 int col = 0;
3534 size_t timelen;
3535 size_t authorlen;
3536 int trimmed = 1;
3538 if (!*commit->author)
3539 return FALSE;
3541 wmove(view->win, lineno, col);
3543 if (selected) {
3544 type = LINE_CURSOR;
3545 wattrset(view->win, get_line_attr(type));
3546 wchgat(view->win, -1, 0, type, NULL);
3548 } else {
3549 type = LINE_MAIN_COMMIT;
3550 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3551 }
3553 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
3554 waddnstr(view->win, buf, timelen);
3555 waddstr(view->win, " ");
3557 col += DATE_COLS;
3558 wmove(view->win, lineno, col);
3559 if (type != LINE_CURSOR)
3560 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3562 if (opt_utf8) {
3563 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
3564 } else {
3565 authorlen = strlen(commit->author);
3566 if (authorlen > AUTHOR_COLS - 2) {
3567 authorlen = AUTHOR_COLS - 2;
3568 trimmed = 1;
3569 }
3570 }
3572 if (trimmed) {
3573 waddnstr(view->win, commit->author, authorlen);
3574 if (type != LINE_CURSOR)
3575 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
3576 waddch(view->win, '~');
3577 } else {
3578 waddstr(view->win, commit->author);
3579 }
3581 col += AUTHOR_COLS;
3582 if (type != LINE_CURSOR)
3583 wattrset(view->win, A_NORMAL);
3585 if (opt_rev_graph && commit->graph_size) {
3586 size_t i;
3588 wmove(view->win, lineno, col);
3589 /* Using waddch() instead of waddnstr() ensures that
3590 * they'll be rendered correctly for the cursor line. */
3591 for (i = 0; i < commit->graph_size; i++)
3592 waddch(view->win, commit->graph[i]);
3594 waddch(view->win, ' ');
3595 col += commit->graph_size + 1;
3596 }
3598 wmove(view->win, lineno, col);
3600 if (commit->refs) {
3601 size_t i = 0;
3603 do {
3604 if (type == LINE_CURSOR)
3605 ;
3606 else if (commit->refs[i]->tag)
3607 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
3608 else if (commit->refs[i]->remote)
3609 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
3610 else
3611 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
3612 waddstr(view->win, "[");
3613 waddstr(view->win, commit->refs[i]->name);
3614 waddstr(view->win, "]");
3615 if (type != LINE_CURSOR)
3616 wattrset(view->win, A_NORMAL);
3617 waddstr(view->win, " ");
3618 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
3619 } while (commit->refs[i++]->next);
3620 }
3622 if (type != LINE_CURSOR)
3623 wattrset(view->win, get_line_attr(type));
3625 {
3626 int titlelen = strlen(commit->title);
3628 if (col + titlelen > view->width)
3629 titlelen = view->width - col;
3631 waddnstr(view->win, commit->title, titlelen);
3632 }
3634 return TRUE;
3635 }
3637 /* Reads git log --pretty=raw output and parses it into the commit struct. */
3638 static bool
3639 main_read(struct view *view, char *line)
3640 {
3641 static struct rev_graph *graph = graph_stacks;
3642 enum line_type type;
3643 struct commit *commit;
3645 if (!line) {
3646 update_rev_graph(graph);
3647 return TRUE;
3648 }
3650 type = get_line_type(line);
3651 if (type == LINE_COMMIT) {
3652 commit = calloc(1, sizeof(struct commit));
3653 if (!commit)
3654 return FALSE;
3656 string_copy_rev(commit->id, line + STRING_SIZE("commit "));
3657 commit->refs = get_refs(commit->id);
3658 graph->commit = commit;
3659 add_line_data(view, commit, LINE_MAIN_COMMIT);
3660 return TRUE;
3661 }
3663 if (!view->lines)
3664 return TRUE;
3665 commit = view->line[view->lines - 1].data;
3667 switch (type) {
3668 case LINE_PARENT:
3669 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
3670 break;
3672 case LINE_AUTHOR:
3673 {
3674 /* Parse author lines where the name may be empty:
3675 * author <email@address.tld> 1138474660 +0100
3676 */
3677 char *ident = line + STRING_SIZE("author ");
3678 char *nameend = strchr(ident, '<');
3679 char *emailend = strchr(ident, '>');
3681 if (!nameend || !emailend)
3682 break;
3684 update_rev_graph(graph);
3685 graph = graph->next;
3687 *nameend = *emailend = 0;
3688 ident = chomp_string(ident);
3689 if (!*ident) {
3690 ident = chomp_string(nameend + 1);
3691 if (!*ident)
3692 ident = "Unknown";
3693 }
3695 string_ncopy(commit->author, ident, strlen(ident));
3697 /* Parse epoch and timezone */
3698 if (emailend[1] == ' ') {
3699 char *secs = emailend + 2;
3700 char *zone = strchr(secs, ' ');
3701 time_t time = (time_t) atol(secs);
3703 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
3704 long tz;
3706 zone++;
3707 tz = ('0' - zone[1]) * 60 * 60 * 10;
3708 tz += ('0' - zone[2]) * 60 * 60;
3709 tz += ('0' - zone[3]) * 60;
3710 tz += ('0' - zone[4]) * 60;
3712 if (zone[0] == '-')
3713 tz = -tz;
3715 time -= tz;
3716 }
3718 gmtime_r(&time, &commit->time);
3719 }
3720 break;
3721 }
3722 default:
3723 /* Fill in the commit title if it has not already been set. */
3724 if (commit->title[0])
3725 break;
3727 /* Require titles to start with a non-space character at the
3728 * offset used by git log. */
3729 if (strncmp(line, " ", 4))
3730 break;
3731 line += 4;
3732 /* Well, if the title starts with a whitespace character,
3733 * try to be forgiving. Otherwise we end up with no title. */
3734 while (isspace(*line))
3735 line++;
3736 if (*line == '\0')
3737 break;
3738 /* FIXME: More graceful handling of titles; append "..." to
3739 * shortened titles, etc. */
3741 string_ncopy(commit->title, line, strlen(line));
3742 }
3744 return TRUE;
3745 }
3747 static enum request
3748 main_request(struct view *view, enum request request, struct line *line)
3749 {
3750 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3752 if (request == REQ_ENTER)
3753 open_view(view, REQ_VIEW_DIFF, flags);
3754 else
3755 return request;
3757 return REQ_NONE;
3758 }
3760 static bool
3761 main_grep(struct view *view, struct line *line)
3762 {
3763 struct commit *commit = line->data;
3764 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
3765 char buf[DATE_COLS + 1];
3766 regmatch_t pmatch;
3768 for (state = S_TITLE; state < S_END; state++) {
3769 char *text;
3771 switch (state) {
3772 case S_TITLE: text = commit->title; break;
3773 case S_AUTHOR: text = commit->author; break;
3774 case S_DATE:
3775 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
3776 continue;
3777 text = buf;
3778 break;
3780 default:
3781 return FALSE;
3782 }
3784 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3785 return TRUE;
3786 }
3788 return FALSE;
3789 }
3791 static void
3792 main_select(struct view *view, struct line *line)
3793 {
3794 struct commit *commit = line->data;
3796 string_copy_rev(view->ref, commit->id);
3797 string_copy_rev(ref_commit, view->ref);
3798 }
3800 static struct view_ops main_ops = {
3801 "commit",
3802 NULL,
3803 main_read,
3804 main_draw,
3805 main_request,
3806 main_grep,
3807 main_select,
3808 };
3811 /*
3812 * Unicode / UTF-8 handling
3813 *
3814 * NOTE: Much of the following code for dealing with unicode is derived from
3815 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
3816 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
3817 */
3819 /* I've (over)annotated a lot of code snippets because I am not entirely
3820 * confident that the approach taken by this small UTF-8 interface is correct.
3821 * --jonas */
3823 static inline int
3824 unicode_width(unsigned long c)
3825 {
3826 if (c >= 0x1100 &&
3827 (c <= 0x115f /* Hangul Jamo */
3828 || c == 0x2329
3829 || c == 0x232a
3830 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
3831 /* CJK ... Yi */
3832 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
3833 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
3834 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
3835 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
3836 || (c >= 0xffe0 && c <= 0xffe6)
3837 || (c >= 0x20000 && c <= 0x2fffd)
3838 || (c >= 0x30000 && c <= 0x3fffd)))
3839 return 2;
3841 return 1;
3842 }
3844 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
3845 * Illegal bytes are set one. */
3846 static const unsigned char utf8_bytes[256] = {
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 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,
3851 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,
3852 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,
3853 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,
3854 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,
3855 };
3857 /* Decode UTF-8 multi-byte representation into a unicode character. */
3858 static inline unsigned long
3859 utf8_to_unicode(const char *string, size_t length)
3860 {
3861 unsigned long unicode;
3863 switch (length) {
3864 case 1:
3865 unicode = string[0];
3866 break;
3867 case 2:
3868 unicode = (string[0] & 0x1f) << 6;
3869 unicode += (string[1] & 0x3f);
3870 break;
3871 case 3:
3872 unicode = (string[0] & 0x0f) << 12;
3873 unicode += ((string[1] & 0x3f) << 6);
3874 unicode += (string[2] & 0x3f);
3875 break;
3876 case 4:
3877 unicode = (string[0] & 0x0f) << 18;
3878 unicode += ((string[1] & 0x3f) << 12);
3879 unicode += ((string[2] & 0x3f) << 6);
3880 unicode += (string[3] & 0x3f);
3881 break;
3882 case 5:
3883 unicode = (string[0] & 0x0f) << 24;
3884 unicode += ((string[1] & 0x3f) << 18);
3885 unicode += ((string[2] & 0x3f) << 12);
3886 unicode += ((string[3] & 0x3f) << 6);
3887 unicode += (string[4] & 0x3f);
3888 break;
3889 case 6:
3890 unicode = (string[0] & 0x01) << 30;
3891 unicode += ((string[1] & 0x3f) << 24);
3892 unicode += ((string[2] & 0x3f) << 18);
3893 unicode += ((string[3] & 0x3f) << 12);
3894 unicode += ((string[4] & 0x3f) << 6);
3895 unicode += (string[5] & 0x3f);
3896 break;
3897 default:
3898 die("Invalid unicode length");
3899 }
3901 /* Invalid characters could return the special 0xfffd value but NUL
3902 * should be just as good. */
3903 return unicode > 0xffff ? 0 : unicode;
3904 }
3906 /* Calculates how much of string can be shown within the given maximum width
3907 * and sets trimmed parameter to non-zero value if all of string could not be
3908 * shown.
3909 *
3910 * Additionally, adds to coloffset how many many columns to move to align with
3911 * the expected position. Takes into account how multi-byte and double-width
3912 * characters will effect the cursor position.
3913 *
3914 * Returns the number of bytes to output from string to satisfy max_width. */
3915 static size_t
3916 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3917 {
3918 const char *start = string;
3919 const char *end = strchr(string, '\0');
3920 size_t mbwidth = 0;
3921 size_t width = 0;
3923 *trimmed = 0;
3925 while (string < end) {
3926 int c = *(unsigned char *) string;
3927 unsigned char bytes = utf8_bytes[c];
3928 size_t ucwidth;
3929 unsigned long unicode;
3931 if (string + bytes > end)
3932 break;
3934 /* Change representation to figure out whether
3935 * it is a single- or double-width character. */
3937 unicode = utf8_to_unicode(string, bytes);
3938 /* FIXME: Graceful handling of invalid unicode character. */
3939 if (!unicode)
3940 break;
3942 ucwidth = unicode_width(unicode);
3943 width += ucwidth;
3944 if (width > max_width) {
3945 *trimmed = 1;
3946 break;
3947 }
3949 /* The column offset collects the differences between the
3950 * number of bytes encoding a character and the number of
3951 * columns will be used for rendering said character.
3952 *
3953 * So if some character A is encoded in 2 bytes, but will be
3954 * represented on the screen using only 1 byte this will and up
3955 * adding 1 to the multi-byte column offset.
3956 *
3957 * Assumes that no double-width character can be encoding in
3958 * less than two bytes. */
3959 if (bytes > ucwidth)
3960 mbwidth += bytes - ucwidth;
3962 string += bytes;
3963 }
3965 *coloffset += mbwidth;
3967 return string - start;
3968 }
3971 /*
3972 * Status management
3973 */
3975 /* Whether or not the curses interface has been initialized. */
3976 static bool cursed = FALSE;
3978 /* The status window is used for polling keystrokes. */
3979 static WINDOW *status_win;
3981 static bool status_empty = TRUE;
3983 /* Update status and title window. */
3984 static void
3985 report(const char *msg, ...)
3986 {
3987 struct view *view = display[current_view];
3989 if (input_mode)
3990 return;
3992 if (!status_empty || *msg) {
3993 va_list args;
3995 va_start(args, msg);
3997 wmove(status_win, 0, 0);
3998 if (*msg) {
3999 vwprintw(status_win, msg, args);
4000 status_empty = FALSE;
4001 } else {
4002 status_empty = TRUE;
4003 }
4004 wclrtoeol(status_win);
4005 wrefresh(status_win);
4007 va_end(args);
4008 }
4010 update_view_title(view);
4011 update_display_cursor(view);
4012 }
4014 /* Controls when nodelay should be in effect when polling user input. */
4015 static void
4016 set_nonblocking_input(bool loading)
4017 {
4018 static unsigned int loading_views;
4020 if ((loading == FALSE && loading_views-- == 1) ||
4021 (loading == TRUE && loading_views++ == 0))
4022 nodelay(status_win, loading);
4023 }
4025 static void
4026 init_display(void)
4027 {
4028 int x, y;
4030 /* Initialize the curses library */
4031 if (isatty(STDIN_FILENO)) {
4032 cursed = !!initscr();
4033 } else {
4034 /* Leave stdin and stdout alone when acting as a pager. */
4035 FILE *io = fopen("/dev/tty", "r+");
4037 if (!io)
4038 die("Failed to open /dev/tty");
4039 cursed = !!newterm(NULL, io, io);
4040 }
4042 if (!cursed)
4043 die("Failed to initialize curses");
4045 nonl(); /* Tell curses not to do NL->CR/NL on output */
4046 cbreak(); /* Take input chars one at a time, no wait for \n */
4047 noecho(); /* Don't echo input */
4048 leaveok(stdscr, TRUE);
4050 if (has_colors())
4051 init_colors();
4053 getmaxyx(stdscr, y, x);
4054 status_win = newwin(1, 0, y - 1, 0);
4055 if (!status_win)
4056 die("Failed to create status window");
4058 /* Enable keyboard mapping */
4059 keypad(status_win, TRUE);
4060 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4061 }
4063 static char *
4064 read_prompt(const char *prompt)
4065 {
4066 enum { READING, STOP, CANCEL } status = READING;
4067 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4068 int pos = 0;
4070 while (status == READING) {
4071 struct view *view;
4072 int i, key;
4074 input_mode = TRUE;
4076 foreach_view (view, i)
4077 update_view(view);
4079 input_mode = FALSE;
4081 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4082 wclrtoeol(status_win);
4084 /* Refresh, accept single keystroke of input */
4085 key = wgetch(status_win);
4086 switch (key) {
4087 case KEY_RETURN:
4088 case KEY_ENTER:
4089 case '\n':
4090 status = pos ? STOP : CANCEL;
4091 break;
4093 case KEY_BACKSPACE:
4094 if (pos > 0)
4095 pos--;
4096 else
4097 status = CANCEL;
4098 break;
4100 case KEY_ESC:
4101 status = CANCEL;
4102 break;
4104 case ERR:
4105 break;
4107 default:
4108 if (pos >= sizeof(buf)) {
4109 report("Input string too long");
4110 return NULL;
4111 }
4113 if (isprint(key))
4114 buf[pos++] = (char) key;
4115 }
4116 }
4118 /* Clear the status window */
4119 status_empty = FALSE;
4120 report("");
4122 if (status == CANCEL)
4123 return NULL;
4125 buf[pos++] = 0;
4127 return buf;
4128 }
4130 /*
4131 * Repository references
4132 */
4134 static struct ref *refs;
4135 static size_t refs_size;
4137 /* Id <-> ref store */
4138 static struct ref ***id_refs;
4139 static size_t id_refs_size;
4141 static struct ref **
4142 get_refs(char *id)
4143 {
4144 struct ref ***tmp_id_refs;
4145 struct ref **ref_list = NULL;
4146 size_t ref_list_size = 0;
4147 size_t i;
4149 for (i = 0; i < id_refs_size; i++)
4150 if (!strcmp(id, id_refs[i][0]->id))
4151 return id_refs[i];
4153 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4154 if (!tmp_id_refs)
4155 return NULL;
4157 id_refs = tmp_id_refs;
4159 for (i = 0; i < refs_size; i++) {
4160 struct ref **tmp;
4162 if (strcmp(id, refs[i].id))
4163 continue;
4165 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
4166 if (!tmp) {
4167 if (ref_list)
4168 free(ref_list);
4169 return NULL;
4170 }
4172 ref_list = tmp;
4173 if (ref_list_size > 0)
4174 ref_list[ref_list_size - 1]->next = 1;
4175 ref_list[ref_list_size] = &refs[i];
4177 /* XXX: The properties of the commit chains ensures that we can
4178 * safely modify the shared ref. The repo references will
4179 * always be similar for the same id. */
4180 ref_list[ref_list_size]->next = 0;
4181 ref_list_size++;
4182 }
4184 if (ref_list)
4185 id_refs[id_refs_size++] = ref_list;
4187 return ref_list;
4188 }
4190 static int
4191 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4192 {
4193 struct ref *ref;
4194 bool tag = FALSE;
4195 bool remote = FALSE;
4197 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4198 /* Commits referenced by tags has "^{}" appended. */
4199 if (name[namelen - 1] != '}')
4200 return OK;
4202 while (namelen > 0 && name[namelen] != '^')
4203 namelen--;
4205 tag = TRUE;
4206 namelen -= STRING_SIZE("refs/tags/");
4207 name += STRING_SIZE("refs/tags/");
4209 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4210 remote = TRUE;
4211 namelen -= STRING_SIZE("refs/remotes/");
4212 name += STRING_SIZE("refs/remotes/");
4214 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4215 namelen -= STRING_SIZE("refs/heads/");
4216 name += STRING_SIZE("refs/heads/");
4218 } else if (!strcmp(name, "HEAD")) {
4219 return OK;
4220 }
4222 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4223 if (!refs)
4224 return ERR;
4226 ref = &refs[refs_size++];
4227 ref->name = malloc(namelen + 1);
4228 if (!ref->name)
4229 return ERR;
4231 strncpy(ref->name, name, namelen);
4232 ref->name[namelen] = 0;
4233 ref->tag = tag;
4234 ref->remote = remote;
4235 string_copy_rev(ref->id, id);
4237 return OK;
4238 }
4240 static int
4241 load_refs(void)
4242 {
4243 const char *cmd_env = getenv("TIG_LS_REMOTE");
4244 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4246 return read_properties(popen(cmd, "r"), "\t", read_ref);
4247 }
4249 static int
4250 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4251 {
4252 if (!strcmp(name, "i18n.commitencoding"))
4253 string_ncopy(opt_encoding, value, valuelen);
4255 if (!strcmp(name, "core.editor"))
4256 string_ncopy(opt_editor, value, valuelen);
4258 return OK;
4259 }
4261 static int
4262 load_repo_config(void)
4263 {
4264 return read_properties(popen("git repo-config --list", "r"),
4265 "=", read_repo_config_option);
4266 }
4268 static int
4269 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4270 {
4271 if (!opt_git_dir[0])
4272 string_ncopy(opt_git_dir, name, namelen);
4273 else
4274 string_ncopy(opt_cdup, name, namelen);
4275 return OK;
4276 }
4278 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4279 * must be the last one! */
4280 static int
4281 load_repo_info(void)
4282 {
4283 return read_properties(popen("git rev-parse --git-dir --show-cdup 2>/dev/null", "r"),
4284 "=", read_repo_info);
4285 }
4287 static int
4288 read_properties(FILE *pipe, const char *separators,
4289 int (*read_property)(char *, size_t, char *, size_t))
4290 {
4291 char buffer[BUFSIZ];
4292 char *name;
4293 int state = OK;
4295 if (!pipe)
4296 return ERR;
4298 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4299 char *value;
4300 size_t namelen;
4301 size_t valuelen;
4303 name = chomp_string(name);
4304 namelen = strcspn(name, separators);
4306 if (name[namelen]) {
4307 name[namelen] = 0;
4308 value = chomp_string(name + namelen + 1);
4309 valuelen = strlen(value);
4311 } else {
4312 value = "";
4313 valuelen = 0;
4314 }
4316 state = read_property(name, namelen, value, valuelen);
4317 }
4319 if (state != ERR && ferror(pipe))
4320 state = ERR;
4322 pclose(pipe);
4324 return state;
4325 }
4328 /*
4329 * Main
4330 */
4332 static void __NORETURN
4333 quit(int sig)
4334 {
4335 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4336 if (cursed)
4337 endwin();
4338 exit(0);
4339 }
4341 static void __NORETURN
4342 die(const char *err, ...)
4343 {
4344 va_list args;
4346 endwin();
4348 va_start(args, err);
4349 fputs("tig: ", stderr);
4350 vfprintf(stderr, err, args);
4351 fputs("\n", stderr);
4352 va_end(args);
4354 exit(1);
4355 }
4357 int
4358 main(int argc, char *argv[])
4359 {
4360 struct view *view;
4361 enum request request;
4362 size_t i;
4364 signal(SIGINT, quit);
4366 if (setlocale(LC_ALL, "")) {
4367 char *codeset = nl_langinfo(CODESET);
4369 string_ncopy(opt_codeset, codeset, strlen(codeset));
4370 }
4372 if (load_repo_info() == ERR)
4373 die("Failed to load repo info.");
4375 /* Require a git repository unless when running in pager mode. */
4376 if (!opt_git_dir[0])
4377 die("Not a git repository");
4379 if (load_options() == ERR)
4380 die("Failed to load user config.");
4382 /* Load the repo config file so options can be overwritten from
4383 * the command line. */
4384 if (load_repo_config() == ERR)
4385 die("Failed to load repo config.");
4387 if (!parse_options(argc, argv))
4388 return 0;
4390 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4391 opt_iconv = iconv_open(opt_codeset, opt_encoding);
4392 if (opt_iconv == ICONV_NONE)
4393 die("Failed to initialize character set conversion");
4394 }
4396 if (load_refs() == ERR)
4397 die("Failed to load refs.");
4399 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4400 view->cmd_env = getenv(view->cmd_env);
4402 request = opt_request;
4404 init_display();
4406 while (view_driver(display[current_view], request)) {
4407 int key;
4408 int i;
4410 foreach_view (view, i)
4411 update_view(view);
4413 /* Refresh, accept single keystroke of input */
4414 key = wgetch(status_win);
4416 /* wgetch() with nodelay() enabled returns ERR when there's no
4417 * input. */
4418 if (key == ERR) {
4419 request = REQ_NONE;
4420 continue;
4421 }
4423 request = get_keybinding(display[current_view]->keymap, key);
4425 /* Some low-level request handling. This keeps access to
4426 * status_win restricted. */
4427 switch (request) {
4428 case REQ_PROMPT:
4429 {
4430 char *cmd = read_prompt(":");
4432 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4433 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4434 opt_request = REQ_VIEW_DIFF;
4435 } else {
4436 opt_request = REQ_VIEW_PAGER;
4437 }
4438 break;
4439 }
4441 request = REQ_NONE;
4442 break;
4443 }
4444 case REQ_SEARCH:
4445 case REQ_SEARCH_BACK:
4446 {
4447 const char *prompt = request == REQ_SEARCH
4448 ? "/" : "?";
4449 char *search = read_prompt(prompt);
4451 if (search)
4452 string_ncopy(opt_search, search, strlen(search));
4453 else
4454 request = REQ_NONE;
4455 break;
4456 }
4457 case REQ_SCREEN_RESIZE:
4458 {
4459 int height, width;
4461 getmaxyx(stdscr, height, width);
4463 /* Resize the status view and let the view driver take
4464 * care of resizing the displayed views. */
4465 wresize(status_win, 1, width);
4466 mvwin(status_win, height - 1, 0);
4467 wrefresh(status_win);
4468 break;
4469 }
4470 default:
4471 break;
4472 }
4473 }
4475 quit(0);
4477 return 0;
4478 }