1 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
14 #ifndef VERSION
15 #define VERSION "tig-0.6.git"
16 #endif
18 #ifndef DEBUG
19 #define NDEBUG
20 #endif
22 #include <assert.h>
23 #include <errno.h>
24 #include <ctype.h>
25 #include <signal.h>
26 #include <stdarg.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <unistd.h>
31 #include <time.h>
33 #include <sys/types.h>
34 #include <regex.h>
36 #include <locale.h>
37 #include <langinfo.h>
38 #include <iconv.h>
40 #include <curses.h>
42 #if __GNUC__ >= 3
43 #define __NORETURN __attribute__((__noreturn__))
44 #else
45 #define __NORETURN
46 #endif
48 static void __NORETURN die(const char *err, ...);
49 static void report(const char *msg, ...);
50 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
51 static void set_nonblocking_input(bool loading);
52 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
54 #define ABS(x) ((x) >= 0 ? (x) : -(x))
55 #define MIN(x, y) ((x) < (y) ? (x) : (y))
57 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
58 #define STRING_SIZE(x) (sizeof(x) - 1)
60 #define SIZEOF_STR 1024 /* Default string size. */
61 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
62 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
64 /* Revision graph */
66 #define REVGRAPH_INIT 'I'
67 #define REVGRAPH_MERGE 'M'
68 #define REVGRAPH_BRANCH '+'
69 #define REVGRAPH_COMMIT '*'
70 #define REVGRAPH_LINE '|'
72 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
74 /* This color name can be used to refer to the default term colors. */
75 #define COLOR_DEFAULT (-1)
77 #define ICONV_NONE ((iconv_t) -1)
79 /* The format and size of the date column in the main view. */
80 #define DATE_FORMAT "%Y-%m-%d %H:%M"
81 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
83 #define AUTHOR_COLS 20
85 /* The default interval between line numbers. */
86 #define NUMBER_INTERVAL 1
88 #define TABSIZE 8
90 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
92 #define TIG_LS_REMOTE \
93 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
95 #define TIG_DIFF_CMD \
96 "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
98 #define TIG_LOG_CMD \
99 "git log --cc --stat -n100 %s 2>/dev/null"
101 #define TIG_MAIN_CMD \
102 "git log --topo-order --pretty=raw %s 2>/dev/null"
104 #define TIG_TREE_CMD \
105 "git ls-tree %s %s"
107 #define TIG_BLOB_CMD \
108 "git cat-file blob %s"
110 /* XXX: Needs to be defined to the empty string. */
111 #define TIG_HELP_CMD ""
112 #define TIG_PAGER_CMD ""
113 #define TIG_STATUS_CMD ""
115 /* Some ascii-shorthands fitted into the ncurses namespace. */
116 #define KEY_TAB '\t'
117 #define KEY_RETURN '\r'
118 #define KEY_ESC 27
121 struct ref {
122 char *name; /* Ref name; tag or head names are shortened. */
123 char id[SIZEOF_REV]; /* Commit SHA1 ID */
124 unsigned int tag:1; /* Is it a tag? */
125 unsigned int remote:1; /* Is it a remote ref? */
126 unsigned int next:1; /* For ref lists: are there more refs? */
127 };
129 static struct ref **get_refs(char *id);
131 struct int_map {
132 const char *name;
133 int namelen;
134 int value;
135 };
137 static int
138 set_from_int_map(struct int_map *map, size_t map_size,
139 int *value, const char *name, int namelen)
140 {
142 int i;
144 for (i = 0; i < map_size; i++)
145 if (namelen == map[i].namelen &&
146 !strncasecmp(name, map[i].name, namelen)) {
147 *value = map[i].value;
148 return OK;
149 }
151 return ERR;
152 }
155 /*
156 * String helpers
157 */
159 static inline void
160 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
161 {
162 if (srclen > dstlen - 1)
163 srclen = dstlen - 1;
165 strncpy(dst, src, srclen);
166 dst[srclen] = 0;
167 }
169 /* Shorthands for safely copying into a fixed buffer. */
171 #define string_copy(dst, src) \
172 string_ncopy_do(dst, sizeof(dst), src, sizeof(dst))
174 #define string_ncopy(dst, src, srclen) \
175 string_ncopy_do(dst, sizeof(dst), src, srclen)
177 #define string_copy_rev(dst, src) \
178 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
180 static char *
181 chomp_string(char *name)
182 {
183 int namelen;
185 while (isspace(*name))
186 name++;
188 namelen = strlen(name) - 1;
189 while (namelen > 0 && isspace(name[namelen]))
190 name[namelen--] = 0;
192 return name;
193 }
195 static bool
196 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
197 {
198 va_list args;
199 size_t pos = bufpos ? *bufpos : 0;
201 va_start(args, fmt);
202 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
203 va_end(args);
205 if (bufpos)
206 *bufpos = pos;
208 return pos >= bufsize ? FALSE : TRUE;
209 }
211 #define string_format(buf, fmt, args...) \
212 string_nformat(buf, sizeof(buf), NULL, fmt, args)
214 #define string_format_from(buf, from, fmt, args...) \
215 string_nformat(buf, sizeof(buf), from, fmt, args)
217 static int
218 string_enum_compare(const char *str1, const char *str2, int len)
219 {
220 size_t i;
222 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
224 /* Diff-Header == DIFF_HEADER */
225 for (i = 0; i < len; i++) {
226 if (toupper(str1[i]) == toupper(str2[i]))
227 continue;
229 if (string_enum_sep(str1[i]) &&
230 string_enum_sep(str2[i]))
231 continue;
233 return str1[i] - str2[i];
234 }
236 return 0;
237 }
239 /* Shell quoting
240 *
241 * NOTE: The following is a slightly modified copy of the git project's shell
242 * quoting routines found in the quote.c file.
243 *
244 * Help to copy the thing properly quoted for the shell safety. any single
245 * quote is replaced with '\'', any exclamation point is replaced with '\!',
246 * and the whole thing is enclosed in a
247 *
248 * E.g.
249 * original sq_quote result
250 * name ==> name ==> 'name'
251 * a b ==> a b ==> 'a b'
252 * a'b ==> a'\''b ==> 'a'\''b'
253 * a!b ==> a'\!'b ==> 'a'\!'b'
254 */
256 static size_t
257 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
258 {
259 char c;
261 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
263 BUFPUT('\'');
264 while ((c = *src++)) {
265 if (c == '\'' || c == '!') {
266 BUFPUT('\'');
267 BUFPUT('\\');
268 BUFPUT(c);
269 BUFPUT('\'');
270 } else {
271 BUFPUT(c);
272 }
273 }
274 BUFPUT('\'');
276 if (bufsize < SIZEOF_STR)
277 buf[bufsize] = 0;
279 return bufsize;
280 }
283 /*
284 * User requests
285 */
287 #define REQ_INFO \
288 /* XXX: Keep the view request first and in sync with views[]. */ \
289 REQ_GROUP("View switching") \
290 REQ_(VIEW_MAIN, "Show main view"), \
291 REQ_(VIEW_DIFF, "Show diff view"), \
292 REQ_(VIEW_LOG, "Show log view"), \
293 REQ_(VIEW_TREE, "Show tree view"), \
294 REQ_(VIEW_BLOB, "Show blob view"), \
295 REQ_(VIEW_HELP, "Show help page"), \
296 REQ_(VIEW_PAGER, "Show pager view"), \
297 REQ_(VIEW_STATUS, "Show status view"), \
298 \
299 REQ_GROUP("View manipulation") \
300 REQ_(ENTER, "Enter current line and scroll"), \
301 REQ_(NEXT, "Move to next"), \
302 REQ_(PREVIOUS, "Move to previous"), \
303 REQ_(VIEW_NEXT, "Move focus to next view"), \
304 REQ_(VIEW_CLOSE, "Close the current view"), \
305 REQ_(QUIT, "Close all views and quit"), \
306 \
307 REQ_GROUP("Cursor navigation") \
308 REQ_(MOVE_UP, "Move cursor one line up"), \
309 REQ_(MOVE_DOWN, "Move cursor one line down"), \
310 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
311 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
312 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
313 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
314 \
315 REQ_GROUP("Scrolling") \
316 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
317 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
318 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
319 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
320 \
321 REQ_GROUP("Searching") \
322 REQ_(SEARCH, "Search the view"), \
323 REQ_(SEARCH_BACK, "Search backwards in the view"), \
324 REQ_(FIND_NEXT, "Find next search match"), \
325 REQ_(FIND_PREV, "Find previous search match"), \
326 \
327 REQ_GROUP("Misc") \
328 REQ_(NONE, "Do nothing"), \
329 REQ_(PROMPT, "Bring up the prompt"), \
330 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
331 REQ_(SCREEN_RESIZE, "Resize the screen"), \
332 REQ_(SHOW_VERSION, "Show version information"), \
333 REQ_(STOP_LOADING, "Stop all loading views"), \
334 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
335 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization")
338 /* User action requests. */
339 enum request {
340 #define REQ_GROUP(help)
341 #define REQ_(req, help) REQ_##req
343 /* Offset all requests to avoid conflicts with ncurses getch values. */
344 REQ_OFFSET = KEY_MAX + 1,
345 REQ_INFO,
346 REQ_UNKNOWN,
348 #undef REQ_GROUP
349 #undef REQ_
350 };
352 struct request_info {
353 enum request request;
354 char *name;
355 int namelen;
356 char *help;
357 };
359 static struct request_info req_info[] = {
360 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
361 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
362 REQ_INFO
363 #undef REQ_GROUP
364 #undef REQ_
365 };
367 static enum request
368 get_request(const char *name)
369 {
370 int namelen = strlen(name);
371 int i;
373 for (i = 0; i < ARRAY_SIZE(req_info); i++)
374 if (req_info[i].namelen == namelen &&
375 !string_enum_compare(req_info[i].name, name, namelen))
376 return req_info[i].request;
378 return REQ_UNKNOWN;
379 }
382 /*
383 * Options
384 */
386 static const char usage[] =
387 VERSION " (" __DATE__ ")\n"
388 "\n"
389 "Usage: tig [options]\n"
390 " or: tig [options] [--] [git log options]\n"
391 " or: tig [options] log [git log options]\n"
392 " or: tig [options] diff [git diff options]\n"
393 " or: tig [options] show [git show options]\n"
394 " or: tig [options] < [git command output]\n"
395 "\n"
396 "Options:\n"
397 " -l Start up in log view\n"
398 " -d Start up in diff view\n"
399 " -S Start up in status view\n"
400 " -n[I], --line-number[=I] Show line numbers with given interval\n"
401 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
402 " -- Mark end of tig options\n"
403 " -v, --version Show version and exit\n"
404 " -h, --help Show help message and exit\n";
406 /* Option and state variables. */
407 static bool opt_line_number = FALSE;
408 static bool opt_rev_graph = FALSE;
409 static int opt_num_interval = NUMBER_INTERVAL;
410 static int opt_tab_size = TABSIZE;
411 static enum request opt_request = REQ_VIEW_MAIN;
412 static char opt_cmd[SIZEOF_STR] = "";
413 static char opt_path[SIZEOF_STR] = "";
414 static FILE *opt_pipe = NULL;
415 static char opt_encoding[20] = "UTF-8";
416 static bool opt_utf8 = TRUE;
417 static char opt_codeset[20] = "UTF-8";
418 static iconv_t opt_iconv = ICONV_NONE;
419 static char opt_search[SIZEOF_STR] = "";
421 enum option_type {
422 OPT_NONE,
423 OPT_INT,
424 };
426 static bool
427 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
428 {
429 va_list args;
430 char *value = "";
431 int *number;
433 if (opt[0] != '-')
434 return FALSE;
436 if (opt[1] == '-') {
437 int namelen = strlen(name);
439 opt += 2;
441 if (strncmp(opt, name, namelen))
442 return FALSE;
444 if (opt[namelen] == '=')
445 value = opt + namelen + 1;
447 } else {
448 if (!short_name || opt[1] != short_name)
449 return FALSE;
450 value = opt + 2;
451 }
453 va_start(args, type);
454 if (type == OPT_INT) {
455 number = va_arg(args, int *);
456 if (isdigit(*value))
457 *number = atoi(value);
458 }
459 va_end(args);
461 return TRUE;
462 }
464 /* Returns the index of log or diff command or -1 to exit. */
465 static bool
466 parse_options(int argc, char *argv[])
467 {
468 int i;
470 for (i = 1; i < argc; i++) {
471 char *opt = argv[i];
473 if (!strcmp(opt, "log") ||
474 !strcmp(opt, "diff") ||
475 !strcmp(opt, "show")) {
476 opt_request = opt[0] == 'l'
477 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
478 break;
479 }
481 if (opt[0] && opt[0] != '-')
482 break;
484 if (!strcmp(opt, "-l")) {
485 opt_request = REQ_VIEW_LOG;
486 continue;
487 }
489 if (!strcmp(opt, "-d")) {
490 opt_request = REQ_VIEW_DIFF;
491 continue;
492 }
494 if (!strcmp(opt, "-S")) {
495 opt_request = REQ_VIEW_STATUS;
496 break;
497 }
499 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
500 opt_line_number = TRUE;
501 continue;
502 }
504 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
505 opt_tab_size = MIN(opt_tab_size, TABSIZE);
506 continue;
507 }
509 if (check_option(opt, 'v', "version", OPT_NONE)) {
510 printf("tig version %s\n", VERSION);
511 return FALSE;
512 }
514 if (check_option(opt, 'h', "help", OPT_NONE)) {
515 printf(usage);
516 return FALSE;
517 }
519 if (!strcmp(opt, "--")) {
520 i++;
521 break;
522 }
524 die("unknown option '%s'\n\n%s", opt, usage);
525 }
527 if (!isatty(STDIN_FILENO)) {
528 opt_request = REQ_VIEW_PAGER;
529 opt_pipe = stdin;
531 } else if (i < argc) {
532 size_t buf_size;
534 if (opt_request == REQ_VIEW_MAIN)
535 /* XXX: This is vulnerable to the user overriding
536 * options required for the main view parser. */
537 string_copy(opt_cmd, "git log --pretty=raw");
538 else
539 string_copy(opt_cmd, "git");
540 buf_size = strlen(opt_cmd);
542 while (buf_size < sizeof(opt_cmd) && i < argc) {
543 opt_cmd[buf_size++] = ' ';
544 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
545 }
547 if (buf_size >= sizeof(opt_cmd))
548 die("command too long");
550 opt_cmd[buf_size] = 0;
551 }
553 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
554 opt_utf8 = FALSE;
556 return TRUE;
557 }
560 /*
561 * Line-oriented content detection.
562 */
564 #define LINE_INFO \
565 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
568 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
569 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
570 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
586 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
587 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
590 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
594 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
595 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
596 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
597 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
598 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
599 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
600 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
601 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
602 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
603 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
604 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
605 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
606 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
607 LINE(STAT_SECTION, "", COLOR_DEFAULT, COLOR_BLUE, A_BOLD), \
608 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
609 LINE(STAT_STAGED, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
610 LINE(STAT_UNSTAGED,"", COLOR_YELLOW, COLOR_DEFAULT, 0), \
611 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
613 enum line_type {
614 #define LINE(type, line, fg, bg, attr) \
615 LINE_##type
616 LINE_INFO
617 #undef LINE
618 };
620 struct line_info {
621 const char *name; /* Option name. */
622 int namelen; /* Size of option name. */
623 const char *line; /* The start of line to match. */
624 int linelen; /* Size of string to match. */
625 int fg, bg, attr; /* Color and text attributes for the lines. */
626 };
628 static struct line_info line_info[] = {
629 #define LINE(type, line, fg, bg, attr) \
630 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
631 LINE_INFO
632 #undef LINE
633 };
635 static enum line_type
636 get_line_type(char *line)
637 {
638 int linelen = strlen(line);
639 enum line_type type;
641 for (type = 0; type < ARRAY_SIZE(line_info); type++)
642 /* Case insensitive search matches Signed-off-by lines better. */
643 if (linelen >= line_info[type].linelen &&
644 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
645 return type;
647 return LINE_DEFAULT;
648 }
650 static inline int
651 get_line_attr(enum line_type type)
652 {
653 assert(type < ARRAY_SIZE(line_info));
654 return COLOR_PAIR(type) | line_info[type].attr;
655 }
657 static struct line_info *
658 get_line_info(char *name, int namelen)
659 {
660 enum line_type type;
662 for (type = 0; type < ARRAY_SIZE(line_info); type++)
663 if (namelen == line_info[type].namelen &&
664 !string_enum_compare(line_info[type].name, name, namelen))
665 return &line_info[type];
667 return NULL;
668 }
670 static void
671 init_colors(void)
672 {
673 int default_bg = COLOR_BLACK;
674 int default_fg = COLOR_WHITE;
675 enum line_type type;
677 start_color();
679 if (use_default_colors() != ERR) {
680 default_bg = -1;
681 default_fg = -1;
682 }
684 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
685 struct line_info *info = &line_info[type];
686 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
687 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
689 init_pair(type, fg, bg);
690 }
691 }
693 struct line {
694 enum line_type type;
696 /* State flags */
697 unsigned int selected:1;
699 void *data; /* User data */
700 };
703 /*
704 * Keys
705 */
707 struct keybinding {
708 int alias;
709 enum request request;
710 struct keybinding *next;
711 };
713 static struct keybinding default_keybindings[] = {
714 /* View switching */
715 { 'm', REQ_VIEW_MAIN },
716 { 'd', REQ_VIEW_DIFF },
717 { 'l', REQ_VIEW_LOG },
718 { 't', REQ_VIEW_TREE },
719 { 'f', REQ_VIEW_BLOB },
720 { 'p', REQ_VIEW_PAGER },
721 { 'h', REQ_VIEW_HELP },
722 { 'S', REQ_VIEW_STATUS },
724 /* View manipulation */
725 { 'q', REQ_VIEW_CLOSE },
726 { KEY_TAB, REQ_VIEW_NEXT },
727 { KEY_RETURN, REQ_ENTER },
728 { KEY_UP, REQ_PREVIOUS },
729 { KEY_DOWN, REQ_NEXT },
731 /* Cursor navigation */
732 { 'k', REQ_MOVE_UP },
733 { 'j', REQ_MOVE_DOWN },
734 { KEY_HOME, REQ_MOVE_FIRST_LINE },
735 { KEY_END, REQ_MOVE_LAST_LINE },
736 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
737 { ' ', REQ_MOVE_PAGE_DOWN },
738 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
739 { 'b', REQ_MOVE_PAGE_UP },
740 { '-', REQ_MOVE_PAGE_UP },
742 /* Scrolling */
743 { KEY_IC, REQ_SCROLL_LINE_UP },
744 { KEY_DC, REQ_SCROLL_LINE_DOWN },
745 { 'w', REQ_SCROLL_PAGE_UP },
746 { 's', REQ_SCROLL_PAGE_DOWN },
748 /* Searching */
749 { '/', REQ_SEARCH },
750 { '?', REQ_SEARCH_BACK },
751 { 'n', REQ_FIND_NEXT },
752 { 'N', REQ_FIND_PREV },
754 /* Misc */
755 { 'Q', REQ_QUIT },
756 { 'z', REQ_STOP_LOADING },
757 { 'v', REQ_SHOW_VERSION },
758 { 'r', REQ_SCREEN_REDRAW },
759 { '.', REQ_TOGGLE_LINENO },
760 { 'g', REQ_TOGGLE_REV_GRAPH },
761 { ':', REQ_PROMPT },
763 /* Using the ncurses SIGWINCH handler. */
764 { KEY_RESIZE, REQ_SCREEN_RESIZE },
765 };
767 #define KEYMAP_INFO \
768 KEYMAP_(GENERIC), \
769 KEYMAP_(MAIN), \
770 KEYMAP_(DIFF), \
771 KEYMAP_(LOG), \
772 KEYMAP_(TREE), \
773 KEYMAP_(BLOB), \
774 KEYMAP_(PAGER), \
775 KEYMAP_(HELP), \
776 KEYMAP_(STATUS)
778 enum keymap {
779 #define KEYMAP_(name) KEYMAP_##name
780 KEYMAP_INFO
781 #undef KEYMAP_
782 };
784 static struct int_map keymap_table[] = {
785 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
786 KEYMAP_INFO
787 #undef KEYMAP_
788 };
790 #define set_keymap(map, name) \
791 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
793 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
795 static void
796 add_keybinding(enum keymap keymap, enum request request, int key)
797 {
798 struct keybinding *keybinding;
800 keybinding = calloc(1, sizeof(*keybinding));
801 if (!keybinding)
802 die("Failed to allocate keybinding");
804 keybinding->alias = key;
805 keybinding->request = request;
806 keybinding->next = keybindings[keymap];
807 keybindings[keymap] = keybinding;
808 }
810 /* Looks for a key binding first in the given map, then in the generic map, and
811 * lastly in the default keybindings. */
812 static enum request
813 get_keybinding(enum keymap keymap, int key)
814 {
815 struct keybinding *kbd;
816 int i;
818 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
819 if (kbd->alias == key)
820 return kbd->request;
822 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
823 if (kbd->alias == key)
824 return kbd->request;
826 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
827 if (default_keybindings[i].alias == key)
828 return default_keybindings[i].request;
830 return (enum request) key;
831 }
834 struct key {
835 char *name;
836 int value;
837 };
839 static struct key key_table[] = {
840 { "Enter", KEY_RETURN },
841 { "Space", ' ' },
842 { "Backspace", KEY_BACKSPACE },
843 { "Tab", KEY_TAB },
844 { "Escape", KEY_ESC },
845 { "Left", KEY_LEFT },
846 { "Right", KEY_RIGHT },
847 { "Up", KEY_UP },
848 { "Down", KEY_DOWN },
849 { "Insert", KEY_IC },
850 { "Delete", KEY_DC },
851 { "Hash", '#' },
852 { "Home", KEY_HOME },
853 { "End", KEY_END },
854 { "PageUp", KEY_PPAGE },
855 { "PageDown", KEY_NPAGE },
856 { "F1", KEY_F(1) },
857 { "F2", KEY_F(2) },
858 { "F3", KEY_F(3) },
859 { "F4", KEY_F(4) },
860 { "F5", KEY_F(5) },
861 { "F6", KEY_F(6) },
862 { "F7", KEY_F(7) },
863 { "F8", KEY_F(8) },
864 { "F9", KEY_F(9) },
865 { "F10", KEY_F(10) },
866 { "F11", KEY_F(11) },
867 { "F12", KEY_F(12) },
868 };
870 static int
871 get_key_value(const char *name)
872 {
873 int i;
875 for (i = 0; i < ARRAY_SIZE(key_table); i++)
876 if (!strcasecmp(key_table[i].name, name))
877 return key_table[i].value;
879 if (strlen(name) == 1 && isprint(*name))
880 return (int) *name;
882 return ERR;
883 }
885 static char *
886 get_key(enum request request)
887 {
888 static char buf[BUFSIZ];
889 static char key_char[] = "'X'";
890 size_t pos = 0;
891 char *sep = "";
892 int i;
894 buf[pos] = 0;
896 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
897 struct keybinding *keybinding = &default_keybindings[i];
898 char *seq = NULL;
899 int key;
901 if (keybinding->request != request)
902 continue;
904 for (key = 0; key < ARRAY_SIZE(key_table); key++)
905 if (key_table[key].value == keybinding->alias)
906 seq = key_table[key].name;
908 if (seq == NULL &&
909 keybinding->alias < 127 &&
910 isprint(keybinding->alias)) {
911 key_char[1] = (char) keybinding->alias;
912 seq = key_char;
913 }
915 if (!seq)
916 seq = "'?'";
918 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
919 return "Too many keybindings!";
920 sep = ", ";
921 }
923 return buf;
924 }
927 /*
928 * User config file handling.
929 */
931 static struct int_map color_map[] = {
932 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
933 COLOR_MAP(DEFAULT),
934 COLOR_MAP(BLACK),
935 COLOR_MAP(BLUE),
936 COLOR_MAP(CYAN),
937 COLOR_MAP(GREEN),
938 COLOR_MAP(MAGENTA),
939 COLOR_MAP(RED),
940 COLOR_MAP(WHITE),
941 COLOR_MAP(YELLOW),
942 };
944 #define set_color(color, name) \
945 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
947 static struct int_map attr_map[] = {
948 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
949 ATTR_MAP(NORMAL),
950 ATTR_MAP(BLINK),
951 ATTR_MAP(BOLD),
952 ATTR_MAP(DIM),
953 ATTR_MAP(REVERSE),
954 ATTR_MAP(STANDOUT),
955 ATTR_MAP(UNDERLINE),
956 };
958 #define set_attribute(attr, name) \
959 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
961 static int config_lineno;
962 static bool config_errors;
963 static char *config_msg;
965 /* Wants: object fgcolor bgcolor [attr] */
966 static int
967 option_color_command(int argc, char *argv[])
968 {
969 struct line_info *info;
971 if (argc != 3 && argc != 4) {
972 config_msg = "Wrong number of arguments given to color command";
973 return ERR;
974 }
976 info = get_line_info(argv[0], strlen(argv[0]));
977 if (!info) {
978 config_msg = "Unknown color name";
979 return ERR;
980 }
982 if (set_color(&info->fg, argv[1]) == ERR ||
983 set_color(&info->bg, argv[2]) == ERR) {
984 config_msg = "Unknown color";
985 return ERR;
986 }
988 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
989 config_msg = "Unknown attribute";
990 return ERR;
991 }
993 return OK;
994 }
996 /* Wants: name = value */
997 static int
998 option_set_command(int argc, char *argv[])
999 {
1000 if (argc != 3) {
1001 config_msg = "Wrong number of arguments given to set command";
1002 return ERR;
1003 }
1005 if (strcmp(argv[1], "=")) {
1006 config_msg = "No value assigned";
1007 return ERR;
1008 }
1010 if (!strcmp(argv[0], "show-rev-graph")) {
1011 opt_rev_graph = (!strcmp(argv[2], "1") ||
1012 !strcmp(argv[2], "true") ||
1013 !strcmp(argv[2], "yes"));
1014 return OK;
1015 }
1017 if (!strcmp(argv[0], "line-number-interval")) {
1018 opt_num_interval = atoi(argv[2]);
1019 return OK;
1020 }
1022 if (!strcmp(argv[0], "tab-size")) {
1023 opt_tab_size = atoi(argv[2]);
1024 return OK;
1025 }
1027 if (!strcmp(argv[0], "commit-encoding")) {
1028 char *arg = argv[2];
1029 int delimiter = *arg;
1030 int i;
1032 switch (delimiter) {
1033 case '"':
1034 case '\'':
1035 for (arg++, i = 0; arg[i]; i++)
1036 if (arg[i] == delimiter) {
1037 arg[i] = 0;
1038 break;
1039 }
1040 default:
1041 string_copy(opt_encoding, arg);
1042 return OK;
1043 }
1044 }
1046 config_msg = "Unknown variable name";
1047 return ERR;
1048 }
1050 /* Wants: mode request key */
1051 static int
1052 option_bind_command(int argc, char *argv[])
1053 {
1054 enum request request;
1055 int keymap;
1056 int key;
1058 if (argc != 3) {
1059 config_msg = "Wrong number of arguments given to bind command";
1060 return ERR;
1061 }
1063 if (set_keymap(&keymap, argv[0]) == ERR) {
1064 config_msg = "Unknown key map";
1065 return ERR;
1066 }
1068 key = get_key_value(argv[1]);
1069 if (key == ERR) {
1070 config_msg = "Unknown key";
1071 return ERR;
1072 }
1074 request = get_request(argv[2]);
1075 if (request == REQ_UNKNOWN) {
1076 config_msg = "Unknown request name";
1077 return ERR;
1078 }
1080 add_keybinding(keymap, request, key);
1082 return OK;
1083 }
1085 static int
1086 set_option(char *opt, char *value)
1087 {
1088 char *argv[16];
1089 int valuelen;
1090 int argc = 0;
1092 /* Tokenize */
1093 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1094 argv[argc++] = value;
1096 value += valuelen;
1097 if (!*value)
1098 break;
1100 *value++ = 0;
1101 while (isspace(*value))
1102 value++;
1103 }
1105 if (!strcmp(opt, "color"))
1106 return option_color_command(argc, argv);
1108 if (!strcmp(opt, "set"))
1109 return option_set_command(argc, argv);
1111 if (!strcmp(opt, "bind"))
1112 return option_bind_command(argc, argv);
1114 config_msg = "Unknown option command";
1115 return ERR;
1116 }
1118 static int
1119 read_option(char *opt, int optlen, char *value, int valuelen)
1120 {
1121 int status = OK;
1123 config_lineno++;
1124 config_msg = "Internal error";
1126 /* Check for comment markers, since read_properties() will
1127 * only ensure opt and value are split at first " \t". */
1128 optlen = strcspn(opt, "#");
1129 if (optlen == 0)
1130 return OK;
1132 if (opt[optlen] != 0) {
1133 config_msg = "No option value";
1134 status = ERR;
1136 } else {
1137 /* Look for comment endings in the value. */
1138 int len = strcspn(value, "#");
1140 if (len < valuelen) {
1141 valuelen = len;
1142 value[valuelen] = 0;
1143 }
1145 status = set_option(opt, value);
1146 }
1148 if (status == ERR) {
1149 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1150 config_lineno, optlen, opt, config_msg);
1151 config_errors = TRUE;
1152 }
1154 /* Always keep going if errors are encountered. */
1155 return OK;
1156 }
1158 static int
1159 load_options(void)
1160 {
1161 char *home = getenv("HOME");
1162 char buf[SIZEOF_STR];
1163 FILE *file;
1165 config_lineno = 0;
1166 config_errors = FALSE;
1168 if (!home || !string_format(buf, "%s/.tigrc", home))
1169 return ERR;
1171 /* It's ok that the file doesn't exist. */
1172 file = fopen(buf, "r");
1173 if (!file)
1174 return OK;
1176 if (read_properties(file, " \t", read_option) == ERR ||
1177 config_errors == TRUE)
1178 fprintf(stderr, "Errors while loading %s.\n", buf);
1180 return OK;
1181 }
1184 /*
1185 * The viewer
1186 */
1188 struct view;
1189 struct view_ops;
1191 /* The display array of active views and the index of the current view. */
1192 static struct view *display[2];
1193 static unsigned int current_view;
1195 /* Reading from the prompt? */
1196 static bool input_mode = FALSE;
1198 #define foreach_displayed_view(view, i) \
1199 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1201 #define displayed_views() (display[1] != NULL ? 2 : 1)
1203 /* Current head and commit ID */
1204 static char ref_blob[SIZEOF_REF] = "";
1205 static char ref_commit[SIZEOF_REF] = "HEAD";
1206 static char ref_head[SIZEOF_REF] = "HEAD";
1208 struct view {
1209 const char *name; /* View name */
1210 const char *cmd_fmt; /* Default command line format */
1211 const char *cmd_env; /* Command line set via environment */
1212 const char *id; /* Points to either of ref_{head,commit,blob} */
1214 struct view_ops *ops; /* View operations */
1216 enum keymap keymap; /* What keymap does this view have */
1218 char cmd[SIZEOF_STR]; /* Command buffer */
1219 char ref[SIZEOF_REF]; /* Hovered commit reference */
1220 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1222 int height, width; /* The width and height of the main window */
1223 WINDOW *win; /* The main window */
1224 WINDOW *title; /* The title window living below the main window */
1226 /* Navigation */
1227 unsigned long offset; /* Offset of the window top */
1228 unsigned long lineno; /* Current line number */
1230 /* Searching */
1231 char grep[SIZEOF_STR]; /* Search string */
1232 regex_t *regex; /* Pre-compiled regex */
1234 /* If non-NULL, points to the view that opened this view. If this view
1235 * is closed tig will switch back to the parent view. */
1236 struct view *parent;
1238 /* Buffering */
1239 unsigned long lines; /* Total number of lines */
1240 struct line *line; /* Line index */
1241 unsigned long line_size;/* Total number of allocated lines */
1242 unsigned int digits; /* Number of digits in the lines member. */
1244 /* Loading */
1245 FILE *pipe;
1246 time_t start_time;
1247 };
1249 struct view_ops {
1250 /* What type of content being displayed. Used in the title bar. */
1251 const char *type;
1252 /* Open and reads in all view content. */
1253 bool (*open)(struct view *view);
1254 /* Read one line; updates view->line. */
1255 bool (*read)(struct view *view, char *data);
1256 /* Draw one line; @lineno must be < view->height. */
1257 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1258 /* Depending on view, change display based on current line. */
1259 bool (*enter)(struct view *view, struct line *line);
1260 /* Search for regex in a line. */
1261 bool (*grep)(struct view *view, struct line *line);
1262 /* Select line */
1263 void (*select)(struct view *view, struct line *line);
1264 };
1266 static struct view_ops pager_ops;
1267 static struct view_ops main_ops;
1268 static struct view_ops tree_ops;
1269 static struct view_ops blob_ops;
1270 static struct view_ops help_ops;
1271 static struct view_ops status_ops;
1273 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1274 { name, cmd, #env, ref, ops, map}
1276 #define VIEW_(id, name, ops, ref) \
1277 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1280 static struct view views[] = {
1281 VIEW_(MAIN, "main", &main_ops, ref_head),
1282 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1283 VIEW_(LOG, "log", &pager_ops, ref_head),
1284 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1285 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1286 VIEW_(HELP, "help", &help_ops, ""),
1287 VIEW_(PAGER, "pager", &pager_ops, ""),
1288 VIEW_(STATUS, "status", &status_ops, ""),
1289 };
1291 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1293 #define foreach_view(view, i) \
1294 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1296 #define view_is_displayed(view) \
1297 (view == display[0] || view == display[1])
1299 static bool
1300 draw_view_line(struct view *view, unsigned int lineno)
1301 {
1302 struct line *line;
1303 bool selected = (view->offset + lineno == view->lineno);
1304 bool draw_ok;
1306 assert(view_is_displayed(view));
1308 if (view->offset + lineno >= view->lines)
1309 return FALSE;
1311 line = &view->line[view->offset + lineno];
1313 if (selected) {
1314 line->selected = TRUE;
1315 view->ops->select(view, line);
1316 } else if (line->selected) {
1317 line->selected = FALSE;
1318 wmove(view->win, lineno, 0);
1319 wclrtoeol(view->win);
1320 }
1322 scrollok(view->win, FALSE);
1323 draw_ok = view->ops->draw(view, line, lineno, selected);
1324 scrollok(view->win, TRUE);
1326 return draw_ok;
1327 }
1329 static void
1330 redraw_view_from(struct view *view, int lineno)
1331 {
1332 assert(0 <= lineno && lineno < view->height);
1334 for (; lineno < view->height; lineno++) {
1335 if (!draw_view_line(view, lineno))
1336 break;
1337 }
1339 redrawwin(view->win);
1340 if (input_mode)
1341 wnoutrefresh(view->win);
1342 else
1343 wrefresh(view->win);
1344 }
1346 static void
1347 redraw_view(struct view *view)
1348 {
1349 wclear(view->win);
1350 redraw_view_from(view, 0);
1351 }
1354 static void
1355 update_view_title(struct view *view)
1356 {
1357 char buf[SIZEOF_STR];
1358 char state[SIZEOF_STR];
1359 size_t bufpos = 0, statelen = 0;
1361 assert(view_is_displayed(view));
1363 if (view->lines || view->pipe) {
1364 unsigned int view_lines = view->offset + view->height;
1365 unsigned int lines = view->lines
1366 ? MIN(view_lines, view->lines) * 100 / view->lines
1367 : 0;
1369 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1370 view->ops->type,
1371 view->lineno + 1,
1372 view->lines,
1373 lines);
1375 if (view->pipe) {
1376 time_t secs = time(NULL) - view->start_time;
1378 /* Three git seconds are a long time ... */
1379 if (secs > 2)
1380 string_format_from(state, &statelen, " %lds", secs);
1381 }
1382 }
1384 string_format_from(buf, &bufpos, "[%s]", view->name);
1385 if (*view->ref && bufpos < view->width) {
1386 size_t refsize = strlen(view->ref);
1387 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1389 if (minsize < view->width)
1390 refsize = view->width - minsize + 7;
1391 string_format_from(buf, &bufpos, " %.*s", refsize, view->ref);
1392 }
1394 if (statelen && bufpos < view->width) {
1395 string_format_from(buf, &bufpos, " %s", state);
1396 }
1398 if (view == display[current_view])
1399 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1400 else
1401 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1403 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1404 wclrtoeol(view->title);
1405 wmove(view->title, 0, view->width - 1);
1407 if (input_mode)
1408 wnoutrefresh(view->title);
1409 else
1410 wrefresh(view->title);
1411 }
1413 static void
1414 resize_display(void)
1415 {
1416 int offset, i;
1417 struct view *base = display[0];
1418 struct view *view = display[1] ? display[1] : display[0];
1420 /* Setup window dimensions */
1422 getmaxyx(stdscr, base->height, base->width);
1424 /* Make room for the status window. */
1425 base->height -= 1;
1427 if (view != base) {
1428 /* Horizontal split. */
1429 view->width = base->width;
1430 view->height = SCALE_SPLIT_VIEW(base->height);
1431 base->height -= view->height;
1433 /* Make room for the title bar. */
1434 view->height -= 1;
1435 }
1437 /* Make room for the title bar. */
1438 base->height -= 1;
1440 offset = 0;
1442 foreach_displayed_view (view, i) {
1443 if (!view->win) {
1444 view->win = newwin(view->height, 0, offset, 0);
1445 if (!view->win)
1446 die("Failed to create %s view", view->name);
1448 scrollok(view->win, TRUE);
1450 view->title = newwin(1, 0, offset + view->height, 0);
1451 if (!view->title)
1452 die("Failed to create title window");
1454 } else {
1455 wresize(view->win, view->height, view->width);
1456 mvwin(view->win, offset, 0);
1457 mvwin(view->title, offset + view->height, 0);
1458 }
1460 offset += view->height + 1;
1461 }
1462 }
1464 static void
1465 redraw_display(void)
1466 {
1467 struct view *view;
1468 int i;
1470 foreach_displayed_view (view, i) {
1471 redraw_view(view);
1472 update_view_title(view);
1473 }
1474 }
1476 static void
1477 update_display_cursor(struct view *view)
1478 {
1479 /* Move the cursor to the right-most column of the cursor line.
1480 *
1481 * XXX: This could turn out to be a bit expensive, but it ensures that
1482 * the cursor does not jump around. */
1483 if (view->lines) {
1484 wmove(view->win, view->lineno - view->offset, view->width - 1);
1485 wrefresh(view->win);
1486 }
1487 }
1489 /*
1490 * Navigation
1491 */
1493 /* Scrolling backend */
1494 static void
1495 do_scroll_view(struct view *view, int lines)
1496 {
1497 bool redraw_current_line = FALSE;
1499 /* The rendering expects the new offset. */
1500 view->offset += lines;
1502 assert(0 <= view->offset && view->offset < view->lines);
1503 assert(lines);
1505 /* Move current line into the view. */
1506 if (view->lineno < view->offset) {
1507 view->lineno = view->offset;
1508 redraw_current_line = TRUE;
1509 } else if (view->lineno >= view->offset + view->height) {
1510 view->lineno = view->offset + view->height - 1;
1511 redraw_current_line = TRUE;
1512 }
1514 assert(view->offset <= view->lineno && view->lineno < view->lines);
1516 /* Redraw the whole screen if scrolling is pointless. */
1517 if (view->height < ABS(lines)) {
1518 redraw_view(view);
1520 } else {
1521 int line = lines > 0 ? view->height - lines : 0;
1522 int end = line + ABS(lines);
1524 wscrl(view->win, lines);
1526 for (; line < end; line++) {
1527 if (!draw_view_line(view, line))
1528 break;
1529 }
1531 if (redraw_current_line)
1532 draw_view_line(view, view->lineno - view->offset);
1533 }
1535 redrawwin(view->win);
1536 wrefresh(view->win);
1537 report("");
1538 }
1540 /* Scroll frontend */
1541 static void
1542 scroll_view(struct view *view, enum request request)
1543 {
1544 int lines = 1;
1546 assert(view_is_displayed(view));
1548 switch (request) {
1549 case REQ_SCROLL_PAGE_DOWN:
1550 lines = view->height;
1551 case REQ_SCROLL_LINE_DOWN:
1552 if (view->offset + lines > view->lines)
1553 lines = view->lines - view->offset;
1555 if (lines == 0 || view->offset + view->height >= view->lines) {
1556 report("Cannot scroll beyond the last line");
1557 return;
1558 }
1559 break;
1561 case REQ_SCROLL_PAGE_UP:
1562 lines = view->height;
1563 case REQ_SCROLL_LINE_UP:
1564 if (lines > view->offset)
1565 lines = view->offset;
1567 if (lines == 0) {
1568 report("Cannot scroll beyond the first line");
1569 return;
1570 }
1572 lines = -lines;
1573 break;
1575 default:
1576 die("request %d not handled in switch", request);
1577 }
1579 do_scroll_view(view, lines);
1580 }
1582 /* Cursor moving */
1583 static void
1584 move_view(struct view *view, enum request request)
1585 {
1586 int scroll_steps = 0;
1587 int steps;
1589 switch (request) {
1590 case REQ_MOVE_FIRST_LINE:
1591 steps = -view->lineno;
1592 break;
1594 case REQ_MOVE_LAST_LINE:
1595 steps = view->lines - view->lineno - 1;
1596 break;
1598 case REQ_MOVE_PAGE_UP:
1599 steps = view->height > view->lineno
1600 ? -view->lineno : -view->height;
1601 break;
1603 case REQ_MOVE_PAGE_DOWN:
1604 steps = view->lineno + view->height >= view->lines
1605 ? view->lines - view->lineno - 1 : view->height;
1606 break;
1608 case REQ_MOVE_UP:
1609 steps = -1;
1610 break;
1612 case REQ_MOVE_DOWN:
1613 steps = 1;
1614 break;
1616 default:
1617 die("request %d not handled in switch", request);
1618 }
1620 if (steps <= 0 && view->lineno == 0) {
1621 report("Cannot move beyond the first line");
1622 return;
1624 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1625 report("Cannot move beyond the last line");
1626 return;
1627 }
1629 /* Move the current line */
1630 view->lineno += steps;
1631 assert(0 <= view->lineno && view->lineno < view->lines);
1633 /* Check whether the view needs to be scrolled */
1634 if (view->lineno < view->offset ||
1635 view->lineno >= view->offset + view->height) {
1636 scroll_steps = steps;
1637 if (steps < 0 && -steps > view->offset) {
1638 scroll_steps = -view->offset;
1640 } else if (steps > 0) {
1641 if (view->lineno == view->lines - 1 &&
1642 view->lines > view->height) {
1643 scroll_steps = view->lines - view->offset - 1;
1644 if (scroll_steps >= view->height)
1645 scroll_steps -= view->height - 1;
1646 }
1647 }
1648 }
1650 if (!view_is_displayed(view)) {
1651 view->offset += scroll_steps;
1652 assert(0 <= view->offset && view->offset < view->lines);
1653 view->ops->select(view, &view->line[view->lineno]);
1654 return;
1655 }
1657 /* Repaint the old "current" line if we be scrolling */
1658 if (ABS(steps) < view->height)
1659 draw_view_line(view, view->lineno - steps - view->offset);
1661 if (scroll_steps) {
1662 do_scroll_view(view, scroll_steps);
1663 return;
1664 }
1666 /* Draw the current line */
1667 draw_view_line(view, view->lineno - view->offset);
1669 redrawwin(view->win);
1670 wrefresh(view->win);
1671 report("");
1672 }
1675 /*
1676 * Searching
1677 */
1679 static void search_view(struct view *view, enum request request);
1681 static bool
1682 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1683 {
1684 assert(view_is_displayed(view));
1686 if (!view->ops->grep(view, line))
1687 return FALSE;
1689 if (lineno - view->offset >= view->height) {
1690 view->offset = lineno;
1691 view->lineno = lineno;
1692 redraw_view(view);
1694 } else {
1695 unsigned long old_lineno = view->lineno - view->offset;
1697 view->lineno = lineno;
1698 draw_view_line(view, old_lineno);
1700 draw_view_line(view, view->lineno - view->offset);
1701 redrawwin(view->win);
1702 wrefresh(view->win);
1703 }
1705 report("Line %ld matches '%s'", lineno + 1, view->grep);
1706 return TRUE;
1707 }
1709 static void
1710 find_next(struct view *view, enum request request)
1711 {
1712 unsigned long lineno = view->lineno;
1713 int direction;
1715 if (!*view->grep) {
1716 if (!*opt_search)
1717 report("No previous search");
1718 else
1719 search_view(view, request);
1720 return;
1721 }
1723 switch (request) {
1724 case REQ_SEARCH:
1725 case REQ_FIND_NEXT:
1726 direction = 1;
1727 break;
1729 case REQ_SEARCH_BACK:
1730 case REQ_FIND_PREV:
1731 direction = -1;
1732 break;
1734 default:
1735 return;
1736 }
1738 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1739 lineno += direction;
1741 /* Note, lineno is unsigned long so will wrap around in which case it
1742 * will become bigger than view->lines. */
1743 for (; lineno < view->lines; lineno += direction) {
1744 struct line *line = &view->line[lineno];
1746 if (find_next_line(view, lineno, line))
1747 return;
1748 }
1750 report("No match found for '%s'", view->grep);
1751 }
1753 static void
1754 search_view(struct view *view, enum request request)
1755 {
1756 int regex_err;
1758 if (view->regex) {
1759 regfree(view->regex);
1760 *view->grep = 0;
1761 } else {
1762 view->regex = calloc(1, sizeof(*view->regex));
1763 if (!view->regex)
1764 return;
1765 }
1767 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1768 if (regex_err != 0) {
1769 char buf[SIZEOF_STR] = "unknown error";
1771 regerror(regex_err, view->regex, buf, sizeof(buf));
1772 report("Search failed: %s", buf);
1773 return;
1774 }
1776 string_copy(view->grep, opt_search);
1778 find_next(view, request);
1779 }
1781 /*
1782 * Incremental updating
1783 */
1785 static void
1786 end_update(struct view *view)
1787 {
1788 if (!view->pipe)
1789 return;
1790 set_nonblocking_input(FALSE);
1791 if (view->pipe == stdin)
1792 fclose(view->pipe);
1793 else
1794 pclose(view->pipe);
1795 view->pipe = NULL;
1796 }
1798 static bool
1799 begin_update(struct view *view)
1800 {
1801 const char *id = view->id;
1803 if (view->pipe)
1804 end_update(view);
1806 if (opt_cmd[0]) {
1807 string_copy(view->cmd, opt_cmd);
1808 opt_cmd[0] = 0;
1809 /* When running random commands, initially show the
1810 * command in the title. However, it maybe later be
1811 * overwritten if a commit line is selected. */
1812 string_copy(view->ref, view->cmd);
1814 } else if (view == VIEW(REQ_VIEW_TREE)) {
1815 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1816 char path[SIZEOF_STR];
1818 if (strcmp(view->vid, view->id))
1819 opt_path[0] = path[0] = 0;
1820 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1821 return FALSE;
1823 if (!string_format(view->cmd, format, id, path))
1824 return FALSE;
1826 } else {
1827 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1829 if (!string_format(view->cmd, format, id, id, id, id, id))
1830 return FALSE;
1832 /* Put the current ref_* value to the view title ref
1833 * member. This is needed by the blob view. Most other
1834 * views sets it automatically after loading because the
1835 * first line is a commit line. */
1836 string_copy(view->ref, id);
1837 }
1839 /* Special case for the pager view. */
1840 if (opt_pipe) {
1841 view->pipe = opt_pipe;
1842 opt_pipe = NULL;
1843 } else {
1844 view->pipe = popen(view->cmd, "r");
1845 }
1847 if (!view->pipe)
1848 return FALSE;
1850 set_nonblocking_input(TRUE);
1852 view->offset = 0;
1853 view->lines = 0;
1854 view->lineno = 0;
1855 string_copy_rev(view->vid, id);
1857 if (view->line) {
1858 int i;
1860 for (i = 0; i < view->lines; i++)
1861 if (view->line[i].data)
1862 free(view->line[i].data);
1864 free(view->line);
1865 view->line = NULL;
1866 }
1868 view->start_time = time(NULL);
1870 return TRUE;
1871 }
1873 static struct line *
1874 realloc_lines(struct view *view, size_t line_size)
1875 {
1876 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1878 if (!tmp)
1879 return NULL;
1881 view->line = tmp;
1882 view->line_size = line_size;
1883 return view->line;
1884 }
1886 static bool
1887 update_view(struct view *view)
1888 {
1889 char in_buffer[BUFSIZ];
1890 char out_buffer[BUFSIZ * 2];
1891 char *line;
1892 /* The number of lines to read. If too low it will cause too much
1893 * redrawing (and possible flickering), if too high responsiveness
1894 * will suffer. */
1895 unsigned long lines = view->height;
1896 int redraw_from = -1;
1898 if (!view->pipe)
1899 return TRUE;
1901 /* Only redraw if lines are visible. */
1902 if (view->offset + view->height >= view->lines)
1903 redraw_from = view->lines - view->offset;
1905 /* FIXME: This is probably not perfect for backgrounded views. */
1906 if (!realloc_lines(view, view->lines + lines))
1907 goto alloc_error;
1909 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1910 size_t linelen = strlen(line);
1912 if (linelen)
1913 line[linelen - 1] = 0;
1915 if (opt_iconv != ICONV_NONE) {
1916 char *inbuf = line;
1917 size_t inlen = linelen;
1919 char *outbuf = out_buffer;
1920 size_t outlen = sizeof(out_buffer);
1922 size_t ret;
1924 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
1925 if (ret != (size_t) -1) {
1926 line = out_buffer;
1927 linelen = strlen(out_buffer);
1928 }
1929 }
1931 if (!view->ops->read(view, line))
1932 goto alloc_error;
1934 if (lines-- == 1)
1935 break;
1936 }
1938 {
1939 int digits;
1941 lines = view->lines;
1942 for (digits = 0; lines; digits++)
1943 lines /= 10;
1945 /* Keep the displayed view in sync with line number scaling. */
1946 if (digits != view->digits) {
1947 view->digits = digits;
1948 redraw_from = 0;
1949 }
1950 }
1952 if (!view_is_displayed(view))
1953 goto check_pipe;
1955 if (view == VIEW(REQ_VIEW_TREE)) {
1956 /* Clear the view and redraw everything since the tree sorting
1957 * might have rearranged things. */
1958 redraw_view(view);
1960 } else if (redraw_from >= 0) {
1961 /* If this is an incremental update, redraw the previous line
1962 * since for commits some members could have changed when
1963 * loading the main view. */
1964 if (redraw_from > 0)
1965 redraw_from--;
1967 /* Since revision graph visualization requires knowledge
1968 * about the parent commit, it causes a further one-off
1969 * needed to be redrawn for incremental updates. */
1970 if (redraw_from > 0 && opt_rev_graph)
1971 redraw_from--;
1973 /* Incrementally draw avoids flickering. */
1974 redraw_view_from(view, redraw_from);
1975 }
1977 /* Update the title _after_ the redraw so that if the redraw picks up a
1978 * commit reference in view->ref it'll be available here. */
1979 update_view_title(view);
1981 check_pipe:
1982 if (ferror(view->pipe)) {
1983 report("Failed to read: %s", strerror(errno));
1984 goto end;
1986 } else if (feof(view->pipe)) {
1987 report("");
1988 goto end;
1989 }
1991 return TRUE;
1993 alloc_error:
1994 report("Allocation failure");
1996 end:
1997 view->ops->read(view, NULL);
1998 end_update(view);
1999 return FALSE;
2000 }
2002 static struct line *
2003 add_line_data(struct view *view, void *data, enum line_type type)
2004 {
2005 struct line *line = &view->line[view->lines++];
2007 memset(line, 0, sizeof(*line));
2008 line->type = type;
2009 line->data = data;
2011 return line;
2012 }
2014 static struct line *
2015 add_line_text(struct view *view, char *data, enum line_type type)
2016 {
2017 if (data)
2018 data = strdup(data);
2020 return data ? add_line_data(view, data, type) : NULL;
2021 }
2024 /*
2025 * View opening
2026 */
2028 enum open_flags {
2029 OPEN_DEFAULT = 0, /* Use default view switching. */
2030 OPEN_SPLIT = 1, /* Split current view. */
2031 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2032 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2033 };
2035 static void
2036 open_view(struct view *prev, enum request request, enum open_flags flags)
2037 {
2038 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2039 bool split = !!(flags & OPEN_SPLIT);
2040 bool reload = !!(flags & OPEN_RELOAD);
2041 struct view *view = VIEW(request);
2042 int nviews = displayed_views();
2043 struct view *base_view = display[0];
2045 if (view == prev && nviews == 1 && !reload) {
2046 report("Already in %s view", view->name);
2047 return;
2048 }
2050 if (view->ops->open) {
2051 if (!view->ops->open(view)) {
2052 report("Failed to load %s view", view->name);
2053 return;
2054 }
2056 } else if ((reload || strcmp(view->vid, view->id)) &&
2057 !begin_update(view)) {
2058 report("Failed to load %s view", view->name);
2059 return;
2060 }
2062 if (split) {
2063 display[1] = view;
2064 if (!backgrounded)
2065 current_view = 1;
2066 } else {
2067 /* Maximize the current view. */
2068 memset(display, 0, sizeof(display));
2069 current_view = 0;
2070 display[current_view] = view;
2071 }
2073 /* Resize the view when switching between split- and full-screen,
2074 * or when switching between two different full-screen views. */
2075 if (nviews != displayed_views() ||
2076 (nviews == 1 && base_view != display[0]))
2077 resize_display();
2079 if (split && prev->lineno - prev->offset >= prev->height) {
2080 /* Take the title line into account. */
2081 int lines = prev->lineno - prev->offset - prev->height + 1;
2083 /* Scroll the view that was split if the current line is
2084 * outside the new limited view. */
2085 do_scroll_view(prev, lines);
2086 }
2088 if (prev && view != prev) {
2089 if (split && !backgrounded) {
2090 /* "Blur" the previous view. */
2091 update_view_title(prev);
2092 }
2094 view->parent = prev;
2095 }
2097 if (view->pipe && view->lines == 0) {
2098 /* Clear the old view and let the incremental updating refill
2099 * the screen. */
2100 wclear(view->win);
2101 report("");
2102 } else {
2103 redraw_view(view);
2104 report("");
2105 }
2107 /* If the view is backgrounded the above calls to report()
2108 * won't redraw the view title. */
2109 if (backgrounded)
2110 update_view_title(view);
2111 }
2114 /*
2115 * User request switch noodle
2116 */
2118 static int
2119 view_driver(struct view *view, enum request request)
2120 {
2121 int i;
2123 switch (request) {
2124 case REQ_MOVE_UP:
2125 case REQ_MOVE_DOWN:
2126 case REQ_MOVE_PAGE_UP:
2127 case REQ_MOVE_PAGE_DOWN:
2128 case REQ_MOVE_FIRST_LINE:
2129 case REQ_MOVE_LAST_LINE:
2130 move_view(view, request);
2131 break;
2133 case REQ_SCROLL_LINE_DOWN:
2134 case REQ_SCROLL_LINE_UP:
2135 case REQ_SCROLL_PAGE_DOWN:
2136 case REQ_SCROLL_PAGE_UP:
2137 scroll_view(view, request);
2138 break;
2140 case REQ_VIEW_BLOB:
2141 if (!ref_blob[0]) {
2142 report("No file chosen, press %s to open tree view",
2143 get_key(REQ_VIEW_TREE));
2144 break;
2145 }
2146 open_view(view, request, OPEN_DEFAULT);
2147 break;
2149 case REQ_VIEW_PAGER:
2150 if (!VIEW(REQ_VIEW_PAGER)->lines) {
2151 report("No pager content, press %s to run command from prompt",
2152 get_key(REQ_PROMPT));
2153 break;
2154 }
2155 open_view(view, request, OPEN_DEFAULT);
2156 break;
2158 case REQ_VIEW_MAIN:
2159 case REQ_VIEW_DIFF:
2160 case REQ_VIEW_LOG:
2161 case REQ_VIEW_TREE:
2162 case REQ_VIEW_HELP:
2163 case REQ_VIEW_STATUS:
2164 open_view(view, request, OPEN_DEFAULT);
2165 break;
2167 case REQ_NEXT:
2168 case REQ_PREVIOUS:
2169 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2171 if ((view == VIEW(REQ_VIEW_DIFF) &&
2172 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2173 (view == VIEW(REQ_VIEW_BLOB) &&
2174 view->parent == VIEW(REQ_VIEW_TREE))) {
2175 view = view->parent;
2176 move_view(view, request);
2177 if (view_is_displayed(view))
2178 update_view_title(view);
2179 } else {
2180 move_view(view, request);
2181 break;
2182 }
2183 /* Fall-through */
2185 case REQ_ENTER:
2186 if (!view->lines) {
2187 report("Nothing to enter");
2188 break;
2189 }
2190 return view->ops->enter(view, &view->line[view->lineno]);
2192 case REQ_VIEW_NEXT:
2193 {
2194 int nviews = displayed_views();
2195 int next_view = (current_view + 1) % nviews;
2197 if (next_view == current_view) {
2198 report("Only one view is displayed");
2199 break;
2200 }
2202 current_view = next_view;
2203 /* Blur out the title of the previous view. */
2204 update_view_title(view);
2205 report("");
2206 break;
2207 }
2208 case REQ_TOGGLE_LINENO:
2209 opt_line_number = !opt_line_number;
2210 redraw_display();
2211 break;
2213 case REQ_TOGGLE_REV_GRAPH:
2214 opt_rev_graph = !opt_rev_graph;
2215 redraw_display();
2216 break;
2218 case REQ_PROMPT:
2219 /* Always reload^Wrerun commands from the prompt. */
2220 open_view(view, opt_request, OPEN_RELOAD);
2221 break;
2223 case REQ_SEARCH:
2224 case REQ_SEARCH_BACK:
2225 search_view(view, request);
2226 break;
2228 case REQ_FIND_NEXT:
2229 case REQ_FIND_PREV:
2230 find_next(view, request);
2231 break;
2233 case REQ_STOP_LOADING:
2234 for (i = 0; i < ARRAY_SIZE(views); i++) {
2235 view = &views[i];
2236 if (view->pipe)
2237 report("Stopped loading the %s view", view->name),
2238 end_update(view);
2239 }
2240 break;
2242 case REQ_SHOW_VERSION:
2243 report("%s (built %s)", VERSION, __DATE__);
2244 return TRUE;
2246 case REQ_SCREEN_RESIZE:
2247 resize_display();
2248 /* Fall-through */
2249 case REQ_SCREEN_REDRAW:
2250 redraw_display();
2251 break;
2253 case REQ_NONE:
2254 doupdate();
2255 return TRUE;
2257 case REQ_VIEW_CLOSE:
2258 /* XXX: Mark closed views by letting view->parent point to the
2259 * view itself. Parents to closed view should never be
2260 * followed. */
2261 if (view->parent &&
2262 view->parent->parent != view->parent) {
2263 memset(display, 0, sizeof(display));
2264 current_view = 0;
2265 display[current_view] = view->parent;
2266 view->parent = view;
2267 resize_display();
2268 redraw_display();
2269 break;
2270 }
2271 /* Fall-through */
2272 case REQ_QUIT:
2273 return FALSE;
2275 default:
2276 /* An unknown key will show most commonly used commands. */
2277 report("Unknown key, press 'h' for help");
2278 return TRUE;
2279 }
2281 return TRUE;
2282 }
2285 /*
2286 * Pager backend
2287 */
2289 static bool
2290 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2291 {
2292 char *text = line->data;
2293 enum line_type type = line->type;
2294 int textlen = strlen(text);
2295 int attr;
2297 wmove(view->win, lineno, 0);
2299 if (selected) {
2300 type = LINE_CURSOR;
2301 wchgat(view->win, -1, 0, type, NULL);
2302 }
2304 attr = get_line_attr(type);
2305 wattrset(view->win, attr);
2307 if (opt_line_number || opt_tab_size < TABSIZE) {
2308 static char spaces[] = " ";
2309 int col_offset = 0, col = 0;
2311 if (opt_line_number) {
2312 unsigned long real_lineno = view->offset + lineno + 1;
2314 if (real_lineno == 1 ||
2315 (real_lineno % opt_num_interval) == 0) {
2316 wprintw(view->win, "%.*d", view->digits, real_lineno);
2318 } else {
2319 waddnstr(view->win, spaces,
2320 MIN(view->digits, STRING_SIZE(spaces)));
2321 }
2322 waddstr(view->win, ": ");
2323 col_offset = view->digits + 2;
2324 }
2326 while (text && col_offset + col < view->width) {
2327 int cols_max = view->width - col_offset - col;
2328 char *pos = text;
2329 int cols;
2331 if (*text == '\t') {
2332 text++;
2333 assert(sizeof(spaces) > TABSIZE);
2334 pos = spaces;
2335 cols = opt_tab_size - (col % opt_tab_size);
2337 } else {
2338 text = strchr(text, '\t');
2339 cols = line ? text - pos : strlen(pos);
2340 }
2342 waddnstr(view->win, pos, MIN(cols, cols_max));
2343 col += cols;
2344 }
2346 } else {
2347 int col = 0, pos = 0;
2349 for (; pos < textlen && col < view->width; pos++, col++)
2350 if (text[pos] == '\t')
2351 col += TABSIZE - (col % TABSIZE) - 1;
2353 waddnstr(view->win, text, pos);
2354 }
2356 return TRUE;
2357 }
2359 static bool
2360 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2361 {
2362 char refbuf[SIZEOF_STR];
2363 char *ref = NULL;
2364 FILE *pipe;
2366 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2367 return TRUE;
2369 pipe = popen(refbuf, "r");
2370 if (!pipe)
2371 return TRUE;
2373 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2374 ref = chomp_string(ref);
2375 pclose(pipe);
2377 if (!ref || !*ref)
2378 return TRUE;
2380 /* This is the only fatal call, since it can "corrupt" the buffer. */
2381 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2382 return FALSE;
2384 return TRUE;
2385 }
2387 static void
2388 add_pager_refs(struct view *view, struct line *line)
2389 {
2390 char buf[SIZEOF_STR];
2391 char *commit_id = line->data + STRING_SIZE("commit ");
2392 struct ref **refs;
2393 size_t bufpos = 0, refpos = 0;
2394 const char *sep = "Refs: ";
2395 bool is_tag = FALSE;
2397 assert(line->type == LINE_COMMIT);
2399 refs = get_refs(commit_id);
2400 if (!refs) {
2401 if (view == VIEW(REQ_VIEW_DIFF))
2402 goto try_add_describe_ref;
2403 return;
2404 }
2406 do {
2407 struct ref *ref = refs[refpos];
2408 char *fmt = ref->tag ? "%s[%s]" :
2409 ref->remote ? "%s<%s>" : "%s%s";
2411 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2412 return;
2413 sep = ", ";
2414 if (ref->tag)
2415 is_tag = TRUE;
2416 } while (refs[refpos++]->next);
2418 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2419 try_add_describe_ref:
2420 /* Add <tag>-g<commit_id> "fake" reference. */
2421 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2422 return;
2423 }
2425 if (bufpos == 0)
2426 return;
2428 if (!realloc_lines(view, view->line_size + 1))
2429 return;
2431 add_line_text(view, buf, LINE_PP_REFS);
2432 }
2434 static bool
2435 pager_read(struct view *view, char *data)
2436 {
2437 struct line *line;
2439 if (!data)
2440 return TRUE;
2442 line = add_line_text(view, data, get_line_type(data));
2443 if (!line)
2444 return FALSE;
2446 if (line->type == LINE_COMMIT &&
2447 (view == VIEW(REQ_VIEW_DIFF) ||
2448 view == VIEW(REQ_VIEW_LOG)))
2449 add_pager_refs(view, line);
2451 return TRUE;
2452 }
2454 static bool
2455 pager_enter(struct view *view, struct line *line)
2456 {
2457 int split = 0;
2459 if (line->type == LINE_COMMIT &&
2460 (view == VIEW(REQ_VIEW_LOG) ||
2461 view == VIEW(REQ_VIEW_PAGER))) {
2462 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2463 split = 1;
2464 }
2466 /* Always scroll the view even if it was split. That way
2467 * you can use Enter to scroll through the log view and
2468 * split open each commit diff. */
2469 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2471 /* FIXME: A minor workaround. Scrolling the view will call report("")
2472 * but if we are scrolling a non-current view this won't properly
2473 * update the view title. */
2474 if (split)
2475 update_view_title(view);
2477 return TRUE;
2478 }
2480 static bool
2481 pager_grep(struct view *view, struct line *line)
2482 {
2483 regmatch_t pmatch;
2484 char *text = line->data;
2486 if (!*text)
2487 return FALSE;
2489 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2490 return FALSE;
2492 return TRUE;
2493 }
2495 static void
2496 pager_select(struct view *view, struct line *line)
2497 {
2498 if (line->type == LINE_COMMIT) {
2499 char *text = line->data + STRING_SIZE("commit ");
2501 if (view != VIEW(REQ_VIEW_PAGER))
2502 string_copy_rev(view->ref, text);
2503 string_copy_rev(ref_commit, text);
2504 }
2505 }
2507 static struct view_ops pager_ops = {
2508 "line",
2509 NULL,
2510 pager_read,
2511 pager_draw,
2512 pager_enter,
2513 pager_grep,
2514 pager_select,
2515 };
2518 /*
2519 * Help backend
2520 */
2522 static bool
2523 help_open(struct view *view)
2524 {
2525 char buf[BUFSIZ];
2526 int lines = ARRAY_SIZE(req_info) + 2;
2527 int i;
2529 if (view->lines > 0)
2530 return TRUE;
2532 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2533 if (!req_info[i].request)
2534 lines++;
2536 view->line = calloc(lines, sizeof(*view->line));
2537 if (!view->line)
2538 return FALSE;
2540 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2542 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2543 char *key;
2545 if (!req_info[i].request) {
2546 add_line_text(view, "", LINE_DEFAULT);
2547 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2548 continue;
2549 }
2551 key = get_key(req_info[i].request);
2552 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2553 continue;
2555 add_line_text(view, buf, LINE_DEFAULT);
2556 }
2558 return TRUE;
2559 }
2561 static struct view_ops help_ops = {
2562 "line",
2563 help_open,
2564 NULL,
2565 pager_draw,
2566 pager_enter,
2567 pager_grep,
2568 pager_select,
2569 };
2572 /*
2573 * Tree backend
2574 */
2576 /* Parse output from git-ls-tree(1):
2577 *
2578 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2579 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2580 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2581 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2582 */
2584 #define SIZEOF_TREE_ATTR \
2585 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2587 #define TREE_UP_FORMAT "040000 tree %s\t.."
2589 static int
2590 tree_compare_entry(enum line_type type1, char *name1,
2591 enum line_type type2, char *name2)
2592 {
2593 if (type1 != type2) {
2594 if (type1 == LINE_TREE_DIR)
2595 return -1;
2596 return 1;
2597 }
2599 return strcmp(name1, name2);
2600 }
2602 static bool
2603 tree_read(struct view *view, char *text)
2604 {
2605 size_t textlen = text ? strlen(text) : 0;
2606 char buf[SIZEOF_STR];
2607 unsigned long pos;
2608 enum line_type type;
2609 bool first_read = view->lines == 0;
2611 if (textlen <= SIZEOF_TREE_ATTR)
2612 return FALSE;
2614 type = text[STRING_SIZE("100644 ")] == 't'
2615 ? LINE_TREE_DIR : LINE_TREE_FILE;
2617 if (first_read) {
2618 /* Add path info line */
2619 if (!string_format(buf, "Directory path /%s", opt_path) ||
2620 !realloc_lines(view, view->line_size + 1) ||
2621 !add_line_text(view, buf, LINE_DEFAULT))
2622 return FALSE;
2624 /* Insert "link" to parent directory. */
2625 if (*opt_path) {
2626 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2627 !realloc_lines(view, view->line_size + 1) ||
2628 !add_line_text(view, buf, LINE_TREE_DIR))
2629 return FALSE;
2630 }
2631 }
2633 /* Strip the path part ... */
2634 if (*opt_path) {
2635 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2636 size_t striplen = strlen(opt_path);
2637 char *path = text + SIZEOF_TREE_ATTR;
2639 if (pathlen > striplen)
2640 memmove(path, path + striplen,
2641 pathlen - striplen + 1);
2642 }
2644 /* Skip "Directory ..." and ".." line. */
2645 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2646 struct line *line = &view->line[pos];
2647 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2648 char *path2 = text + SIZEOF_TREE_ATTR;
2649 int cmp = tree_compare_entry(line->type, path1, type, path2);
2651 if (cmp <= 0)
2652 continue;
2654 text = strdup(text);
2655 if (!text)
2656 return FALSE;
2658 if (view->lines > pos)
2659 memmove(&view->line[pos + 1], &view->line[pos],
2660 (view->lines - pos) * sizeof(*line));
2662 line = &view->line[pos];
2663 line->data = text;
2664 line->type = type;
2665 view->lines++;
2666 return TRUE;
2667 }
2669 if (!add_line_text(view, text, type))
2670 return FALSE;
2672 /* Move the current line to the first tree entry. */
2673 if (first_read)
2674 view->lineno++;
2676 return TRUE;
2677 }
2679 static bool
2680 tree_enter(struct view *view, struct line *line)
2681 {
2682 enum open_flags flags;
2683 enum request request;
2685 switch (line->type) {
2686 case LINE_TREE_DIR:
2687 /* Depending on whether it is a subdir or parent (updir?) link
2688 * mangle the path buffer. */
2689 if (line == &view->line[1] && *opt_path) {
2690 size_t path_len = strlen(opt_path);
2691 char *dirsep = opt_path + path_len - 1;
2693 while (dirsep > opt_path && dirsep[-1] != '/')
2694 dirsep--;
2696 dirsep[0] = 0;
2698 } else {
2699 size_t pathlen = strlen(opt_path);
2700 size_t origlen = pathlen;
2701 char *data = line->data;
2702 char *basename = data + SIZEOF_TREE_ATTR;
2704 if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
2705 opt_path[origlen] = 0;
2706 return TRUE;
2707 }
2708 }
2710 /* Trees and subtrees share the same ID, so they are not not
2711 * unique like blobs. */
2712 flags = OPEN_RELOAD;
2713 request = REQ_VIEW_TREE;
2714 break;
2716 case LINE_TREE_FILE:
2717 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2718 request = REQ_VIEW_BLOB;
2719 break;
2721 default:
2722 return TRUE;
2723 }
2725 open_view(view, request, flags);
2727 return TRUE;
2728 }
2730 static void
2731 tree_select(struct view *view, struct line *line)
2732 {
2733 char *text = line->data + STRING_SIZE("100644 blob ");
2735 if (line->type == LINE_TREE_FILE) {
2736 string_copy_rev(ref_blob, text);
2738 } else if (line->type != LINE_TREE_DIR) {
2739 return;
2740 }
2742 string_copy_rev(view->ref, text);
2743 }
2745 static struct view_ops tree_ops = {
2746 "file",
2747 NULL,
2748 tree_read,
2749 pager_draw,
2750 tree_enter,
2751 pager_grep,
2752 tree_select,
2753 };
2755 static bool
2756 blob_read(struct view *view, char *line)
2757 {
2758 return add_line_text(view, line, LINE_DEFAULT);
2759 }
2761 static struct view_ops blob_ops = {
2762 "line",
2763 NULL,
2764 blob_read,
2765 pager_draw,
2766 pager_enter,
2767 pager_grep,
2768 pager_select,
2769 };
2772 /*
2773 * Status backend
2774 */
2776 struct status {
2777 char status;
2778 struct {
2779 mode_t mode;
2780 char rev[SIZEOF_REV];
2781 } old;
2782 struct {
2783 mode_t mode;
2784 char rev[SIZEOF_REV];
2785 } new;
2786 char name[SIZEOF_STR];
2787 };
2789 /* Get fields from the diff line:
2790 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
2791 */
2792 static inline bool
2793 status_get_diff(struct status *file, char *buf, size_t bufsize)
2794 {
2795 char *old_mode = buf + 1;
2796 char *new_mode = buf + 8;
2797 char *old_rev = buf + 15;
2798 char *new_rev = buf + 56;
2799 char *status = buf + 97;
2801 if (bufsize != 99 ||
2802 old_mode[-1] != ':' ||
2803 new_mode[-1] != ' ' ||
2804 old_rev[-1] != ' ' ||
2805 new_rev[-1] != ' ' ||
2806 status[-1] != ' ')
2807 return FALSE;
2809 file->status = *status;
2811 string_copy_rev(file->old.rev, old_rev);
2812 string_copy_rev(file->new.rev, new_rev);
2814 file->old.mode = strtoul(old_mode, NULL, 8);
2815 file->new.mode = strtoul(new_mode, NULL, 8);
2817 file->name[0] = 0;
2819 return TRUE;
2820 }
2822 static bool
2823 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
2824 {
2825 struct status *file = NULL;
2826 char buf[SIZEOF_STR * 4];
2827 size_t bufsize = 0;
2828 FILE *pipe;
2830 pipe = popen(cmd, "r");
2831 if (!pipe)
2832 return FALSE;
2834 add_line_data(view, NULL, type);
2836 while (!feof(pipe) && !ferror(pipe)) {
2837 char *sep;
2838 size_t readsize;
2840 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
2841 if (!readsize)
2842 break;
2843 bufsize += readsize;
2845 /* Process while we have NUL chars. */
2846 while ((sep = memchr(buf, 0, bufsize))) {
2847 size_t sepsize = sep - buf + 1;
2849 if (!file) {
2850 if (!realloc_lines(view, view->line_size + 1))
2851 goto error_out;
2853 file = calloc(1, sizeof(*file));
2854 if (!file)
2855 goto error_out;
2857 add_line_data(view, file, type);
2858 }
2860 /* Parse diff info part. */
2861 if (!diff) {
2862 file->status = '?';
2864 } else if (!file->status) {
2865 if (!status_get_diff(file, buf, sepsize))
2866 goto error_out;
2868 bufsize -= sepsize;
2869 memmove(buf, sep + 1, bufsize);
2871 sep = memchr(buf, 0, bufsize);
2872 if (!sep)
2873 break;
2874 sepsize = sep - buf + 1;
2875 }
2877 /* git-ls-files just delivers a NUL separated
2878 * list of file names similar to the second half
2879 * of the git-diff-* output. */
2880 string_ncopy(file->name, buf, sepsize);
2881 bufsize -= sepsize;
2882 memmove(buf, sep + 1, bufsize);
2883 file = NULL;
2884 }
2885 }
2887 if (ferror(pipe)) {
2888 error_out:
2889 pclose(pipe);
2890 return FALSE;
2891 }
2893 if (!view->line[view->lines - 1].data)
2894 add_line_data(view, NULL, LINE_STAT_NONE);
2896 pclose(pipe);
2897 return TRUE;
2898 }
2900 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --cached HEAD"
2901 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
2902 #define STATUS_LIST_OTHER_CMD \
2903 "_git_exclude=$(git rev-parse --git-dir)/info/exclude;" \
2904 "test -f \"$_git_exclude\" && exclude=\"--exclude-from=$_git_exclude\";" \
2905 "git ls-files -z --others --exclude-per-directory=.gitignore \"$exclude\"" \
2907 /* First parse staged info using git-diff-index(1), then parse unstaged
2908 * info using git-diff-files(1), and finally untracked files using
2909 * git-ls-files(1). */
2910 static bool
2911 status_open(struct view *view)
2912 {
2913 size_t i;
2915 for (i = 0; i < view->lines; i++)
2916 free(view->line[i].data);
2917 free(view->line);
2918 view->lines = view->line_size = 0;
2919 view->line = NULL;
2921 if (!realloc_lines(view, view->line_size + 6))
2922 return FALSE;
2924 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
2925 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
2926 !status_run(view, STATUS_LIST_OTHER_CMD, FALSE, LINE_STAT_UNTRACKED))
2927 return FALSE;
2929 return TRUE;
2930 }
2932 static bool
2933 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2934 {
2935 struct status *status = line->data;
2937 wmove(view->win, lineno, 0);
2939 if (selected) {
2940 wattrset(view->win, get_line_attr(LINE_CURSOR));
2941 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
2943 } else if (!status && line->type != LINE_STAT_NONE) {
2944 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
2945 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
2947 } else {
2948 wattrset(view->win, get_line_attr(line->type));
2949 }
2951 if (!status) {
2952 char *text;
2954 switch (line->type) {
2955 case LINE_STAT_STAGED:
2956 text = "Changes to be committed:";
2957 break;
2959 case LINE_STAT_UNSTAGED:
2960 text = "Changed but not updated:";
2961 break;
2963 case LINE_STAT_UNTRACKED:
2964 text = "Untracked files:";
2965 break;
2967 case LINE_STAT_NONE:
2968 text = " (no files)";
2969 break;
2971 default:
2972 return FALSE;
2973 }
2975 waddstr(view->win, text);
2976 return TRUE;
2977 }
2979 waddch(view->win, status->status);
2980 if (!selected)
2981 wattrset(view->win, A_NORMAL);
2982 wmove(view->win, lineno, 4);
2983 waddstr(view->win, status->name);
2985 return TRUE;
2986 }
2988 static bool
2989 status_enter(struct view *view, struct line *line)
2990 {
2991 struct status *status = line->data;
2992 const char *cmd;
2993 char buf[SIZEOF_STR];
2994 size_t bufsize = 0;
2995 size_t written = 0;
2996 FILE *pipe;
2998 if (!status)
2999 return TRUE;
3001 switch (line->type) {
3002 case LINE_STAT_STAGED:
3003 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3004 status->old.mode,
3005 status->old.rev,
3006 status->name, 0))
3007 return FALSE;
3008 cmd = "git update-index -z --index-info";
3009 break;
3011 case LINE_STAT_UNSTAGED:
3012 case LINE_STAT_UNTRACKED:
3013 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3014 return FALSE;
3015 cmd = "git update-index -z --add --remove --stdin";
3016 break;
3018 default:
3019 die("w00t");
3020 }
3022 pipe = popen(cmd, "w");
3023 if (!pipe)
3024 return FALSE;
3026 while (!ferror(pipe) && written < bufsize) {
3027 written += fwrite(buf + written, 1, bufsize - written, pipe);
3028 }
3030 pclose(pipe);
3032 if (written != bufsize)
3033 return FALSE;
3035 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3036 return TRUE;
3037 }
3039 static void
3040 status_select(struct view *view, struct line *line)
3041 {
3042 char *text;
3044 switch (line->type) {
3045 case LINE_STAT_STAGED:
3046 text = "Press Enter to unstage file for commit";
3047 break;
3049 case LINE_STAT_UNSTAGED:
3050 text = "Press Enter to stage file for commit ";
3051 break;
3053 case LINE_STAT_UNTRACKED:
3054 text = "Press Enter to stage file for addition";
3055 break;
3057 case LINE_STAT_NONE:
3058 return;
3060 default:
3061 die("w00t");
3062 }
3064 string_ncopy(view->ref, text, strlen(text));
3065 }
3067 static bool
3068 status_grep(struct view *view, struct line *line)
3069 {
3070 struct status *status = line->data;
3071 enum { S_STATUS, S_NAME, S_END } state;
3072 char buf[2] = "?";
3073 regmatch_t pmatch;
3075 if (!status)
3076 return FALSE;
3078 for (state = S_STATUS; state < S_END; state++) {
3079 char *text;
3081 switch (state) {
3082 case S_NAME: text = status->name; break;
3083 case S_STATUS:
3084 buf[0] = status->status;
3085 text = buf;
3086 break;
3088 default:
3089 return FALSE;
3090 }
3092 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3093 return TRUE;
3094 }
3096 return FALSE;
3097 }
3099 static struct view_ops status_ops = {
3100 "file",
3101 status_open,
3102 NULL,
3103 status_draw,
3104 status_enter,
3105 status_grep,
3106 status_select,
3107 };
3110 /*
3111 * Revision graph
3112 */
3114 struct commit {
3115 char id[SIZEOF_REV]; /* SHA1 ID. */
3116 char title[128]; /* First line of the commit message. */
3117 char author[75]; /* Author of the commit. */
3118 struct tm time; /* Date from the author ident. */
3119 struct ref **refs; /* Repository references. */
3120 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3121 size_t graph_size; /* The width of the graph array. */
3122 };
3124 /* Size of rev graph with no "padding" columns */
3125 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3127 struct rev_graph {
3128 struct rev_graph *prev, *next, *parents;
3129 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3130 size_t size;
3131 struct commit *commit;
3132 size_t pos;
3133 };
3135 /* Parents of the commit being visualized. */
3136 static struct rev_graph graph_parents[4];
3138 /* The current stack of revisions on the graph. */
3139 static struct rev_graph graph_stacks[4] = {
3140 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3141 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3142 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3143 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3144 };
3146 static inline bool
3147 graph_parent_is_merge(struct rev_graph *graph)
3148 {
3149 return graph->parents->size > 1;
3150 }
3152 static inline void
3153 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3154 {
3155 struct commit *commit = graph->commit;
3157 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3158 commit->graph[commit->graph_size++] = symbol;
3159 }
3161 static void
3162 done_rev_graph(struct rev_graph *graph)
3163 {
3164 if (graph_parent_is_merge(graph) &&
3165 graph->pos < graph->size - 1 &&
3166 graph->next->size == graph->size + graph->parents->size - 1) {
3167 size_t i = graph->pos + graph->parents->size - 1;
3169 graph->commit->graph_size = i * 2;
3170 while (i < graph->next->size - 1) {
3171 append_to_rev_graph(graph, ' ');
3172 append_to_rev_graph(graph, '\\');
3173 i++;
3174 }
3175 }
3177 graph->size = graph->pos = 0;
3178 graph->commit = NULL;
3179 memset(graph->parents, 0, sizeof(*graph->parents));
3180 }
3182 static void
3183 push_rev_graph(struct rev_graph *graph, char *parent)
3184 {
3185 int i;
3187 /* "Collapse" duplicate parents lines.
3188 *
3189 * FIXME: This needs to also update update the drawn graph but
3190 * for now it just serves as a method for pruning graph lines. */
3191 for (i = 0; i < graph->size; i++)
3192 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3193 return;
3195 if (graph->size < SIZEOF_REVITEMS) {
3196 string_ncopy(graph->rev[graph->size++], parent, SIZEOF_REV);
3197 }
3198 }
3200 static chtype
3201 get_rev_graph_symbol(struct rev_graph *graph)
3202 {
3203 chtype symbol;
3205 if (graph->parents->size == 0)
3206 symbol = REVGRAPH_INIT;
3207 else if (graph_parent_is_merge(graph))
3208 symbol = REVGRAPH_MERGE;
3209 else if (graph->pos >= graph->size)
3210 symbol = REVGRAPH_BRANCH;
3211 else
3212 symbol = REVGRAPH_COMMIT;
3214 return symbol;
3215 }
3217 static void
3218 draw_rev_graph(struct rev_graph *graph)
3219 {
3220 struct rev_filler {
3221 chtype separator, line;
3222 };
3223 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3224 static struct rev_filler fillers[] = {
3225 { ' ', REVGRAPH_LINE },
3226 { '`', '.' },
3227 { '\'', ' ' },
3228 { '/', ' ' },
3229 };
3230 chtype symbol = get_rev_graph_symbol(graph);
3231 struct rev_filler *filler;
3232 size_t i;
3234 filler = &fillers[DEFAULT];
3236 for (i = 0; i < graph->pos; i++) {
3237 append_to_rev_graph(graph, filler->line);
3238 if (graph_parent_is_merge(graph->prev) &&
3239 graph->prev->pos == i)
3240 filler = &fillers[RSHARP];
3242 append_to_rev_graph(graph, filler->separator);
3243 }
3245 /* Place the symbol for this revision. */
3246 append_to_rev_graph(graph, symbol);
3248 if (graph->prev->size > graph->size)
3249 filler = &fillers[RDIAG];
3250 else
3251 filler = &fillers[DEFAULT];
3253 i++;
3255 for (; i < graph->size; i++) {
3256 append_to_rev_graph(graph, filler->separator);
3257 append_to_rev_graph(graph, filler->line);
3258 if (graph_parent_is_merge(graph->prev) &&
3259 i < graph->prev->pos + graph->parents->size)
3260 filler = &fillers[RSHARP];
3261 if (graph->prev->size > graph->size)
3262 filler = &fillers[LDIAG];
3263 }
3265 if (graph->prev->size > graph->size) {
3266 append_to_rev_graph(graph, filler->separator);
3267 if (filler->line != ' ')
3268 append_to_rev_graph(graph, filler->line);
3269 }
3270 }
3272 /* Prepare the next rev graph */
3273 static void
3274 prepare_rev_graph(struct rev_graph *graph)
3275 {
3276 size_t i;
3278 /* First, traverse all lines of revisions up to the active one. */
3279 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
3280 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
3281 break;
3283 push_rev_graph(graph->next, graph->rev[graph->pos]);
3284 }
3286 /* Interleave the new revision parent(s). */
3287 for (i = 0; i < graph->parents->size; i++)
3288 push_rev_graph(graph->next, graph->parents->rev[i]);
3290 /* Lastly, put any remaining revisions. */
3291 for (i = graph->pos + 1; i < graph->size; i++)
3292 push_rev_graph(graph->next, graph->rev[i]);
3293 }
3295 static void
3296 update_rev_graph(struct rev_graph *graph)
3297 {
3298 /* If this is the finalizing update ... */
3299 if (graph->commit)
3300 prepare_rev_graph(graph);
3302 /* Graph visualization needs a one rev look-ahead,
3303 * so the first update doesn't visualize anything. */
3304 if (!graph->prev->commit)
3305 return;
3307 draw_rev_graph(graph->prev);
3308 done_rev_graph(graph->prev->prev);
3309 }
3312 /*
3313 * Main view backend
3314 */
3316 static bool
3317 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3318 {
3319 char buf[DATE_COLS + 1];
3320 struct commit *commit = line->data;
3321 enum line_type type;
3322 int col = 0;
3323 size_t timelen;
3324 size_t authorlen;
3325 int trimmed = 1;
3327 if (!*commit->author)
3328 return FALSE;
3330 wmove(view->win, lineno, col);
3332 if (selected) {
3333 type = LINE_CURSOR;
3334 wattrset(view->win, get_line_attr(type));
3335 wchgat(view->win, -1, 0, type, NULL);
3337 } else {
3338 type = LINE_MAIN_COMMIT;
3339 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3340 }
3342 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
3343 waddnstr(view->win, buf, timelen);
3344 waddstr(view->win, " ");
3346 col += DATE_COLS;
3347 wmove(view->win, lineno, col);
3348 if (type != LINE_CURSOR)
3349 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3351 if (opt_utf8) {
3352 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
3353 } else {
3354 authorlen = strlen(commit->author);
3355 if (authorlen > AUTHOR_COLS - 2) {
3356 authorlen = AUTHOR_COLS - 2;
3357 trimmed = 1;
3358 }
3359 }
3361 if (trimmed) {
3362 waddnstr(view->win, commit->author, authorlen);
3363 if (type != LINE_CURSOR)
3364 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
3365 waddch(view->win, '~');
3366 } else {
3367 waddstr(view->win, commit->author);
3368 }
3370 col += AUTHOR_COLS;
3371 if (type != LINE_CURSOR)
3372 wattrset(view->win, A_NORMAL);
3374 if (opt_rev_graph && commit->graph_size) {
3375 size_t i;
3377 wmove(view->win, lineno, col);
3378 /* Using waddch() instead of waddnstr() ensures that
3379 * they'll be rendered correctly for the cursor line. */
3380 for (i = 0; i < commit->graph_size; i++)
3381 waddch(view->win, commit->graph[i]);
3383 waddch(view->win, ' ');
3384 col += commit->graph_size + 1;
3385 }
3387 wmove(view->win, lineno, col);
3389 if (commit->refs) {
3390 size_t i = 0;
3392 do {
3393 if (type == LINE_CURSOR)
3394 ;
3395 else if (commit->refs[i]->tag)
3396 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
3397 else if (commit->refs[i]->remote)
3398 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
3399 else
3400 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
3401 waddstr(view->win, "[");
3402 waddstr(view->win, commit->refs[i]->name);
3403 waddstr(view->win, "]");
3404 if (type != LINE_CURSOR)
3405 wattrset(view->win, A_NORMAL);
3406 waddstr(view->win, " ");
3407 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
3408 } while (commit->refs[i++]->next);
3409 }
3411 if (type != LINE_CURSOR)
3412 wattrset(view->win, get_line_attr(type));
3414 {
3415 int titlelen = strlen(commit->title);
3417 if (col + titlelen > view->width)
3418 titlelen = view->width - col;
3420 waddnstr(view->win, commit->title, titlelen);
3421 }
3423 return TRUE;
3424 }
3426 /* Reads git log --pretty=raw output and parses it into the commit struct. */
3427 static bool
3428 main_read(struct view *view, char *line)
3429 {
3430 static struct rev_graph *graph = graph_stacks;
3431 enum line_type type;
3432 struct commit *commit;
3434 if (!line) {
3435 update_rev_graph(graph);
3436 return TRUE;
3437 }
3439 type = get_line_type(line);
3440 if (type == LINE_COMMIT) {
3441 commit = calloc(1, sizeof(struct commit));
3442 if (!commit)
3443 return FALSE;
3445 string_copy_rev(commit->id, line + STRING_SIZE("commit "));
3446 commit->refs = get_refs(commit->id);
3447 graph->commit = commit;
3448 add_line_data(view, commit, LINE_MAIN_COMMIT);
3449 return TRUE;
3450 }
3452 if (!view->lines)
3453 return TRUE;
3454 commit = view->line[view->lines - 1].data;
3456 switch (type) {
3457 case LINE_PARENT:
3458 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
3459 break;
3461 case LINE_AUTHOR:
3462 {
3463 /* Parse author lines where the name may be empty:
3464 * author <email@address.tld> 1138474660 +0100
3465 */
3466 char *ident = line + STRING_SIZE("author ");
3467 char *nameend = strchr(ident, '<');
3468 char *emailend = strchr(ident, '>');
3470 if (!nameend || !emailend)
3471 break;
3473 update_rev_graph(graph);
3474 graph = graph->next;
3476 *nameend = *emailend = 0;
3477 ident = chomp_string(ident);
3478 if (!*ident) {
3479 ident = chomp_string(nameend + 1);
3480 if (!*ident)
3481 ident = "Unknown";
3482 }
3484 string_copy(commit->author, ident);
3486 /* Parse epoch and timezone */
3487 if (emailend[1] == ' ') {
3488 char *secs = emailend + 2;
3489 char *zone = strchr(secs, ' ');
3490 time_t time = (time_t) atol(secs);
3492 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
3493 long tz;
3495 zone++;
3496 tz = ('0' - zone[1]) * 60 * 60 * 10;
3497 tz += ('0' - zone[2]) * 60 * 60;
3498 tz += ('0' - zone[3]) * 60;
3499 tz += ('0' - zone[4]) * 60;
3501 if (zone[0] == '-')
3502 tz = -tz;
3504 time -= tz;
3505 }
3507 gmtime_r(&time, &commit->time);
3508 }
3509 break;
3510 }
3511 default:
3512 /* Fill in the commit title if it has not already been set. */
3513 if (commit->title[0])
3514 break;
3516 /* Require titles to start with a non-space character at the
3517 * offset used by git log. */
3518 if (strncmp(line, " ", 4))
3519 break;
3520 line += 4;
3521 /* Well, if the title starts with a whitespace character,
3522 * try to be forgiving. Otherwise we end up with no title. */
3523 while (isspace(*line))
3524 line++;
3525 if (*line == '\0')
3526 break;
3527 /* FIXME: More graceful handling of titles; append "..." to
3528 * shortened titles, etc. */
3530 string_copy(commit->title, line);
3531 }
3533 return TRUE;
3534 }
3536 static bool
3537 main_enter(struct view *view, struct line *line)
3538 {
3539 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3541 open_view(view, REQ_VIEW_DIFF, flags);
3542 return TRUE;
3543 }
3545 static bool
3546 main_grep(struct view *view, struct line *line)
3547 {
3548 struct commit *commit = line->data;
3549 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
3550 char buf[DATE_COLS + 1];
3551 regmatch_t pmatch;
3553 for (state = S_TITLE; state < S_END; state++) {
3554 char *text;
3556 switch (state) {
3557 case S_TITLE: text = commit->title; break;
3558 case S_AUTHOR: text = commit->author; break;
3559 case S_DATE:
3560 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
3561 continue;
3562 text = buf;
3563 break;
3565 default:
3566 return FALSE;
3567 }
3569 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3570 return TRUE;
3571 }
3573 return FALSE;
3574 }
3576 static void
3577 main_select(struct view *view, struct line *line)
3578 {
3579 struct commit *commit = line->data;
3581 string_copy_rev(view->ref, commit->id);
3582 string_copy_rev(ref_commit, view->ref);
3583 }
3585 static struct view_ops main_ops = {
3586 "commit",
3587 NULL,
3588 main_read,
3589 main_draw,
3590 main_enter,
3591 main_grep,
3592 main_select,
3593 };
3596 /*
3597 * Unicode / UTF-8 handling
3598 *
3599 * NOTE: Much of the following code for dealing with unicode is derived from
3600 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
3601 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
3602 */
3604 /* I've (over)annotated a lot of code snippets because I am not entirely
3605 * confident that the approach taken by this small UTF-8 interface is correct.
3606 * --jonas */
3608 static inline int
3609 unicode_width(unsigned long c)
3610 {
3611 if (c >= 0x1100 &&
3612 (c <= 0x115f /* Hangul Jamo */
3613 || c == 0x2329
3614 || c == 0x232a
3615 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
3616 /* CJK ... Yi */
3617 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
3618 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
3619 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
3620 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
3621 || (c >= 0xffe0 && c <= 0xffe6)
3622 || (c >= 0x20000 && c <= 0x2fffd)
3623 || (c >= 0x30000 && c <= 0x3fffd)))
3624 return 2;
3626 return 1;
3627 }
3629 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
3630 * Illegal bytes are set one. */
3631 static const unsigned char utf8_bytes[256] = {
3632 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
3633 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
3634 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
3635 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
3636 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
3637 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
3638 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,
3639 3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4, 5,5,5,5,6,6,1,1,
3640 };
3642 /* Decode UTF-8 multi-byte representation into a unicode character. */
3643 static inline unsigned long
3644 utf8_to_unicode(const char *string, size_t length)
3645 {
3646 unsigned long unicode;
3648 switch (length) {
3649 case 1:
3650 unicode = string[0];
3651 break;
3652 case 2:
3653 unicode = (string[0] & 0x1f) << 6;
3654 unicode += (string[1] & 0x3f);
3655 break;
3656 case 3:
3657 unicode = (string[0] & 0x0f) << 12;
3658 unicode += ((string[1] & 0x3f) << 6);
3659 unicode += (string[2] & 0x3f);
3660 break;
3661 case 4:
3662 unicode = (string[0] & 0x0f) << 18;
3663 unicode += ((string[1] & 0x3f) << 12);
3664 unicode += ((string[2] & 0x3f) << 6);
3665 unicode += (string[3] & 0x3f);
3666 break;
3667 case 5:
3668 unicode = (string[0] & 0x0f) << 24;
3669 unicode += ((string[1] & 0x3f) << 18);
3670 unicode += ((string[2] & 0x3f) << 12);
3671 unicode += ((string[3] & 0x3f) << 6);
3672 unicode += (string[4] & 0x3f);
3673 break;
3674 case 6:
3675 unicode = (string[0] & 0x01) << 30;
3676 unicode += ((string[1] & 0x3f) << 24);
3677 unicode += ((string[2] & 0x3f) << 18);
3678 unicode += ((string[3] & 0x3f) << 12);
3679 unicode += ((string[4] & 0x3f) << 6);
3680 unicode += (string[5] & 0x3f);
3681 break;
3682 default:
3683 die("Invalid unicode length");
3684 }
3686 /* Invalid characters could return the special 0xfffd value but NUL
3687 * should be just as good. */
3688 return unicode > 0xffff ? 0 : unicode;
3689 }
3691 /* Calculates how much of string can be shown within the given maximum width
3692 * and sets trimmed parameter to non-zero value if all of string could not be
3693 * shown.
3694 *
3695 * Additionally, adds to coloffset how many many columns to move to align with
3696 * the expected position. Takes into account how multi-byte and double-width
3697 * characters will effect the cursor position.
3698 *
3699 * Returns the number of bytes to output from string to satisfy max_width. */
3700 static size_t
3701 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3702 {
3703 const char *start = string;
3704 const char *end = strchr(string, '\0');
3705 size_t mbwidth = 0;
3706 size_t width = 0;
3708 *trimmed = 0;
3710 while (string < end) {
3711 int c = *(unsigned char *) string;
3712 unsigned char bytes = utf8_bytes[c];
3713 size_t ucwidth;
3714 unsigned long unicode;
3716 if (string + bytes > end)
3717 break;
3719 /* Change representation to figure out whether
3720 * it is a single- or double-width character. */
3722 unicode = utf8_to_unicode(string, bytes);
3723 /* FIXME: Graceful handling of invalid unicode character. */
3724 if (!unicode)
3725 break;
3727 ucwidth = unicode_width(unicode);
3728 width += ucwidth;
3729 if (width > max_width) {
3730 *trimmed = 1;
3731 break;
3732 }
3734 /* The column offset collects the differences between the
3735 * number of bytes encoding a character and the number of
3736 * columns will be used for rendering said character.
3737 *
3738 * So if some character A is encoded in 2 bytes, but will be
3739 * represented on the screen using only 1 byte this will and up
3740 * adding 1 to the multi-byte column offset.
3741 *
3742 * Assumes that no double-width character can be encoding in
3743 * less than two bytes. */
3744 if (bytes > ucwidth)
3745 mbwidth += bytes - ucwidth;
3747 string += bytes;
3748 }
3750 *coloffset += mbwidth;
3752 return string - start;
3753 }
3756 /*
3757 * Status management
3758 */
3760 /* Whether or not the curses interface has been initialized. */
3761 static bool cursed = FALSE;
3763 /* The status window is used for polling keystrokes. */
3764 static WINDOW *status_win;
3766 static bool status_empty = TRUE;
3768 /* Update status and title window. */
3769 static void
3770 report(const char *msg, ...)
3771 {
3772 struct view *view = display[current_view];
3774 if (input_mode)
3775 return;
3777 if (!status_empty || *msg) {
3778 va_list args;
3780 va_start(args, msg);
3782 wmove(status_win, 0, 0);
3783 if (*msg) {
3784 vwprintw(status_win, msg, args);
3785 status_empty = FALSE;
3786 } else {
3787 status_empty = TRUE;
3788 }
3789 wclrtoeol(status_win);
3790 wrefresh(status_win);
3792 va_end(args);
3793 }
3795 update_view_title(view);
3796 update_display_cursor(view);
3797 }
3799 /* Controls when nodelay should be in effect when polling user input. */
3800 static void
3801 set_nonblocking_input(bool loading)
3802 {
3803 static unsigned int loading_views;
3805 if ((loading == FALSE && loading_views-- == 1) ||
3806 (loading == TRUE && loading_views++ == 0))
3807 nodelay(status_win, loading);
3808 }
3810 static void
3811 init_display(void)
3812 {
3813 int x, y;
3815 /* Initialize the curses library */
3816 if (isatty(STDIN_FILENO)) {
3817 cursed = !!initscr();
3818 } else {
3819 /* Leave stdin and stdout alone when acting as a pager. */
3820 FILE *io = fopen("/dev/tty", "r+");
3822 if (!io)
3823 die("Failed to open /dev/tty");
3824 cursed = !!newterm(NULL, io, io);
3825 }
3827 if (!cursed)
3828 die("Failed to initialize curses");
3830 nonl(); /* Tell curses not to do NL->CR/NL on output */
3831 cbreak(); /* Take input chars one at a time, no wait for \n */
3832 noecho(); /* Don't echo input */
3833 leaveok(stdscr, TRUE);
3835 if (has_colors())
3836 init_colors();
3838 getmaxyx(stdscr, y, x);
3839 status_win = newwin(1, 0, y - 1, 0);
3840 if (!status_win)
3841 die("Failed to create status window");
3843 /* Enable keyboard mapping */
3844 keypad(status_win, TRUE);
3845 wbkgdset(status_win, get_line_attr(LINE_STATUS));
3846 }
3848 static char *
3849 read_prompt(const char *prompt)
3850 {
3851 enum { READING, STOP, CANCEL } status = READING;
3852 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
3853 int pos = 0;
3855 while (status == READING) {
3856 struct view *view;
3857 int i, key;
3859 input_mode = TRUE;
3861 foreach_view (view, i)
3862 update_view(view);
3864 input_mode = FALSE;
3866 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
3867 wclrtoeol(status_win);
3869 /* Refresh, accept single keystroke of input */
3870 key = wgetch(status_win);
3871 switch (key) {
3872 case KEY_RETURN:
3873 case KEY_ENTER:
3874 case '\n':
3875 status = pos ? STOP : CANCEL;
3876 break;
3878 case KEY_BACKSPACE:
3879 if (pos > 0)
3880 pos--;
3881 else
3882 status = CANCEL;
3883 break;
3885 case KEY_ESC:
3886 status = CANCEL;
3887 break;
3889 case ERR:
3890 break;
3892 default:
3893 if (pos >= sizeof(buf)) {
3894 report("Input string too long");
3895 return NULL;
3896 }
3898 if (isprint(key))
3899 buf[pos++] = (char) key;
3900 }
3901 }
3903 /* Clear the status window */
3904 status_empty = FALSE;
3905 report("");
3907 if (status == CANCEL)
3908 return NULL;
3910 buf[pos++] = 0;
3912 return buf;
3913 }
3915 /*
3916 * Repository references
3917 */
3919 static struct ref *refs;
3920 static size_t refs_size;
3922 /* Id <-> ref store */
3923 static struct ref ***id_refs;
3924 static size_t id_refs_size;
3926 static struct ref **
3927 get_refs(char *id)
3928 {
3929 struct ref ***tmp_id_refs;
3930 struct ref **ref_list = NULL;
3931 size_t ref_list_size = 0;
3932 size_t i;
3934 for (i = 0; i < id_refs_size; i++)
3935 if (!strcmp(id, id_refs[i][0]->id))
3936 return id_refs[i];
3938 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
3939 if (!tmp_id_refs)
3940 return NULL;
3942 id_refs = tmp_id_refs;
3944 for (i = 0; i < refs_size; i++) {
3945 struct ref **tmp;
3947 if (strcmp(id, refs[i].id))
3948 continue;
3950 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
3951 if (!tmp) {
3952 if (ref_list)
3953 free(ref_list);
3954 return NULL;
3955 }
3957 ref_list = tmp;
3958 if (ref_list_size > 0)
3959 ref_list[ref_list_size - 1]->next = 1;
3960 ref_list[ref_list_size] = &refs[i];
3962 /* XXX: The properties of the commit chains ensures that we can
3963 * safely modify the shared ref. The repo references will
3964 * always be similar for the same id. */
3965 ref_list[ref_list_size]->next = 0;
3966 ref_list_size++;
3967 }
3969 if (ref_list)
3970 id_refs[id_refs_size++] = ref_list;
3972 return ref_list;
3973 }
3975 static int
3976 read_ref(char *id, int idlen, char *name, int namelen)
3977 {
3978 struct ref *ref;
3979 bool tag = FALSE;
3980 bool remote = FALSE;
3982 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
3983 /* Commits referenced by tags has "^{}" appended. */
3984 if (name[namelen - 1] != '}')
3985 return OK;
3987 while (namelen > 0 && name[namelen] != '^')
3988 namelen--;
3990 tag = TRUE;
3991 namelen -= STRING_SIZE("refs/tags/");
3992 name += STRING_SIZE("refs/tags/");
3994 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
3995 remote = TRUE;
3996 namelen -= STRING_SIZE("refs/remotes/");
3997 name += STRING_SIZE("refs/remotes/");
3999 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4000 namelen -= STRING_SIZE("refs/heads/");
4001 name += STRING_SIZE("refs/heads/");
4003 } else if (!strcmp(name, "HEAD")) {
4004 return OK;
4005 }
4007 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4008 if (!refs)
4009 return ERR;
4011 ref = &refs[refs_size++];
4012 ref->name = malloc(namelen + 1);
4013 if (!ref->name)
4014 return ERR;
4016 strncpy(ref->name, name, namelen);
4017 ref->name[namelen] = 0;
4018 ref->tag = tag;
4019 ref->remote = remote;
4020 string_copy_rev(ref->id, id);
4022 return OK;
4023 }
4025 static int
4026 load_refs(void)
4027 {
4028 const char *cmd_env = getenv("TIG_LS_REMOTE");
4029 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4031 return read_properties(popen(cmd, "r"), "\t", read_ref);
4032 }
4034 static int
4035 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
4036 {
4037 if (!strcmp(name, "i18n.commitencoding"))
4038 string_copy(opt_encoding, value);
4040 return OK;
4041 }
4043 static int
4044 load_repo_config(void)
4045 {
4046 return read_properties(popen("git repo-config --list", "r"),
4047 "=", read_repo_config_option);
4048 }
4050 static int
4051 read_properties(FILE *pipe, const char *separators,
4052 int (*read_property)(char *, int, char *, int))
4053 {
4054 char buffer[BUFSIZ];
4055 char *name;
4056 int state = OK;
4058 if (!pipe)
4059 return ERR;
4061 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4062 char *value;
4063 size_t namelen;
4064 size_t valuelen;
4066 name = chomp_string(name);
4067 namelen = strcspn(name, separators);
4069 if (name[namelen]) {
4070 name[namelen] = 0;
4071 value = chomp_string(name + namelen + 1);
4072 valuelen = strlen(value);
4074 } else {
4075 value = "";
4076 valuelen = 0;
4077 }
4079 state = read_property(name, namelen, value, valuelen);
4080 }
4082 if (state != ERR && ferror(pipe))
4083 state = ERR;
4085 pclose(pipe);
4087 return state;
4088 }
4091 /*
4092 * Main
4093 */
4095 static void __NORETURN
4096 quit(int sig)
4097 {
4098 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4099 if (cursed)
4100 endwin();
4101 exit(0);
4102 }
4104 static void __NORETURN
4105 die(const char *err, ...)
4106 {
4107 va_list args;
4109 endwin();
4111 va_start(args, err);
4112 fputs("tig: ", stderr);
4113 vfprintf(stderr, err, args);
4114 fputs("\n", stderr);
4115 va_end(args);
4117 exit(1);
4118 }
4120 int
4121 main(int argc, char *argv[])
4122 {
4123 struct view *view;
4124 enum request request;
4125 size_t i;
4127 signal(SIGINT, quit);
4129 if (setlocale(LC_ALL, "")) {
4130 string_copy(opt_codeset, nl_langinfo(CODESET));
4131 }
4133 if (load_options() == ERR)
4134 die("Failed to load user config.");
4136 /* Load the repo config file so options can be overwritten from
4137 * the command line. */
4138 if (load_repo_config() == ERR)
4139 die("Failed to load repo config.");
4141 if (!parse_options(argc, argv))
4142 return 0;
4144 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4145 opt_iconv = iconv_open(opt_codeset, opt_encoding);
4146 if (opt_iconv == ICONV_NONE)
4147 die("Failed to initialize character set conversion");
4148 }
4150 if (load_refs() == ERR)
4151 die("Failed to load refs.");
4153 /* Require a git repository unless when running in pager mode. */
4154 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
4155 die("Not a git repository");
4157 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4158 view->cmd_env = getenv(view->cmd_env);
4160 request = opt_request;
4162 init_display();
4164 while (view_driver(display[current_view], request)) {
4165 int key;
4166 int i;
4168 foreach_view (view, i)
4169 update_view(view);
4171 /* Refresh, accept single keystroke of input */
4172 key = wgetch(status_win);
4174 /* wgetch() with nodelay() enabled returns ERR when there's no
4175 * input. */
4176 if (key == ERR) {
4177 request = REQ_NONE;
4178 continue;
4179 }
4181 request = get_keybinding(display[current_view]->keymap, key);
4183 /* Some low-level request handling. This keeps access to
4184 * status_win restricted. */
4185 switch (request) {
4186 case REQ_PROMPT:
4187 {
4188 char *cmd = read_prompt(":");
4190 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4191 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4192 opt_request = REQ_VIEW_DIFF;
4193 } else {
4194 opt_request = REQ_VIEW_PAGER;
4195 }
4196 break;
4197 }
4199 request = REQ_NONE;
4200 break;
4201 }
4202 case REQ_SEARCH:
4203 case REQ_SEARCH_BACK:
4204 {
4205 const char *prompt = request == REQ_SEARCH
4206 ? "/" : "?";
4207 char *search = read_prompt(prompt);
4209 if (search)
4210 string_copy(opt_search, search);
4211 else
4212 request = REQ_NONE;
4213 break;
4214 }
4215 case REQ_SCREEN_RESIZE:
4216 {
4217 int height, width;
4219 getmaxyx(stdscr, height, width);
4221 /* Resize the status view and let the view driver take
4222 * care of resizing the displayed views. */
4223 wresize(status_win, 1, width);
4224 mvwin(status_win, height - 1, 0);
4225 wrefresh(status_win);
4226 break;
4227 }
4228 default:
4229 break;
4230 }
4231 }
4233 quit(0);
4235 return 0;
4236 }