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.3"
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 <curses.h>
35 static void die(const char *err, ...);
36 static void report(const char *msg, ...);
37 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
38 static void set_nonblocking_input(bool loading);
39 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
40 static void load_help_page(void);
42 #define ABS(x) ((x) >= 0 ? (x) : -(x))
43 #define MIN(x, y) ((x) < (y) ? (x) : (y))
45 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
46 #define STRING_SIZE(x) (sizeof(x) - 1)
48 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
49 #define SIZEOF_CMD 1024 /* Size of command buffer. */
51 /* This color name can be used to refer to the default term colors. */
52 #define COLOR_DEFAULT (-1)
54 /* The format and size of the date column in the main view. */
55 #define DATE_FORMAT "%Y-%m-%d %H:%M"
56 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
58 #define AUTHOR_COLS 20
60 /* The default interval between line numbers. */
61 #define NUMBER_INTERVAL 1
63 #define TABSIZE 8
65 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
67 #define TIG_LS_REMOTE \
68 "git ls-remote . 2>/dev/null"
70 #define TIG_DIFF_CMD \
71 "git show --patch-with-stat --find-copies-harder -B -C %s"
73 #define TIG_LOG_CMD \
74 "git log --cc --stat -n100 %s"
76 #define TIG_MAIN_CMD \
77 "git log --topo-order --stat --pretty=raw %s"
79 /* XXX: Needs to be defined to the empty string. */
80 #define TIG_HELP_CMD ""
81 #define TIG_PAGER_CMD ""
83 /* Some ascii-shorthands fitted into the ncurses namespace. */
84 #define KEY_TAB '\t'
85 #define KEY_RETURN '\r'
86 #define KEY_ESC 27
89 struct ref {
90 char *name; /* Ref name; tag or head names are shortened. */
91 char id[41]; /* Commit SHA1 ID */
92 unsigned int tag:1; /* Is it a tag? */
93 unsigned int next:1; /* For ref lists: are there more refs? */
94 };
96 static struct ref **get_refs(char *id);
98 struct int_map {
99 const char *name;
100 int namelen;
101 int value;
102 };
104 static int
105 set_from_int_map(struct int_map *map, size_t map_size,
106 int *value, const char *name, int namelen)
107 {
109 int i;
111 for (i = 0; i < map_size; i++)
112 if (namelen == map[i].namelen &&
113 !strncasecmp(name, map[i].name, namelen)) {
114 *value = map[i].value;
115 return OK;
116 }
118 return ERR;
119 }
122 /*
123 * String helpers
124 */
126 static inline void
127 string_ncopy(char *dst, const char *src, int dstlen)
128 {
129 strncpy(dst, src, dstlen - 1);
130 dst[dstlen - 1] = 0;
132 }
134 /* Shorthand for safely copying into a fixed buffer. */
135 #define string_copy(dst, src) \
136 string_ncopy(dst, src, sizeof(dst))
138 static char *
139 chomp_string(char *name)
140 {
141 int namelen;
143 while (isspace(*name))
144 name++;
146 namelen = strlen(name) - 1;
147 while (namelen > 0 && isspace(name[namelen]))
148 name[namelen--] = 0;
150 return name;
151 }
153 static bool
154 string_nformat(char *buf, size_t bufsize, int *bufpos, const char *fmt, ...)
155 {
156 va_list args;
157 int pos = bufpos ? *bufpos : 0;
159 va_start(args, fmt);
160 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
161 va_end(args);
163 if (bufpos)
164 *bufpos = pos;
166 return pos >= bufsize ? FALSE : TRUE;
167 }
169 #define string_format(buf, fmt, args...) \
170 string_nformat(buf, sizeof(buf), NULL, fmt, args)
172 #define string_format_from(buf, from, fmt, args...) \
173 string_nformat(buf, sizeof(buf), from, fmt, args)
175 /* Shell quoting
176 *
177 * NOTE: The following is a slightly modified copy of the git project's shell
178 * quoting routines found in the quote.c file.
179 *
180 * Help to copy the thing properly quoted for the shell safety. any single
181 * quote is replaced with '\'', any exclamation point is replaced with '\!',
182 * and the whole thing is enclosed in a
183 *
184 * E.g.
185 * original sq_quote result
186 * name ==> name ==> 'name'
187 * a b ==> a b ==> 'a b'
188 * a'b ==> a'\''b ==> 'a'\''b'
189 * a!b ==> a'\!'b ==> 'a'\!'b'
190 */
192 static size_t
193 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
194 {
195 char c;
197 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
199 BUFPUT('\'');
200 while ((c = *src++)) {
201 if (c == '\'' || c == '!') {
202 BUFPUT('\'');
203 BUFPUT('\\');
204 BUFPUT(c);
205 BUFPUT('\'');
206 } else {
207 BUFPUT(c);
208 }
209 }
210 BUFPUT('\'');
212 return bufsize;
213 }
216 /*
217 * User requests
218 */
220 #define REQ_INFO \
221 /* XXX: Keep the view request first and in sync with views[]. */ \
222 REQ_GROUP("View switching") \
223 REQ_(VIEW_MAIN, "Show main view"), \
224 REQ_(VIEW_DIFF, "Show diff view"), \
225 REQ_(VIEW_LOG, "Show log view"), \
226 REQ_(VIEW_HELP, "Show help page"), \
227 REQ_(VIEW_PAGER, "Show pager view"), \
228 \
229 REQ_GROUP("View manipulation") \
230 REQ_(ENTER, "Enter current line and scroll"), \
231 REQ_(NEXT, "Move to next"), \
232 REQ_(PREVIOUS, "Move to previous"), \
233 REQ_(VIEW_NEXT, "Move focus to next view"), \
234 REQ_(VIEW_CLOSE, "Close the current view"), \
235 REQ_(QUIT, "Close all views and quit"), \
236 \
237 REQ_GROUP("Cursor navigation") \
238 REQ_(MOVE_UP, "Move cursor one line up"), \
239 REQ_(MOVE_DOWN, "Move cursor one line down"), \
240 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
241 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
242 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
243 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
244 \
245 REQ_GROUP("Scrolling") \
246 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
247 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
248 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
249 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
250 \
251 REQ_GROUP("Misc") \
252 REQ_(PROMPT, "Bring up the prompt"), \
253 REQ_(SCREEN_UPDATE, "Update the screen"), \
254 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
255 REQ_(SCREEN_RESIZE, "Resize the screen"), \
256 REQ_(SHOW_VERSION, "Show version information"), \
257 REQ_(STOP_LOADING, "Stop all loading views"), \
258 REQ_(TOGGLE_LINENO, "Toggle line numbers"),
261 /* User action requests. */
262 enum request {
263 #define REQ_GROUP(help)
264 #define REQ_(req, help) REQ_##req
266 /* Offset all requests to avoid conflicts with ncurses getch values. */
267 REQ_OFFSET = KEY_MAX + 1,
268 REQ_INFO
270 #undef REQ_GROUP
271 #undef REQ_
272 };
274 struct request_info {
275 enum request request;
276 char *help;
277 };
279 static struct request_info req_info[] = {
280 #define REQ_GROUP(help) { 0, (help) },
281 #define REQ_(req, help) { REQ_##req, (help) }
282 REQ_INFO
283 #undef REQ_GROUP
284 #undef REQ_
285 };
287 /*
288 * Options
289 */
291 static const char usage[] =
292 VERSION " (" __DATE__ ")\n"
293 "\n"
294 "Usage: tig [options]\n"
295 " or: tig [options] [--] [git log options]\n"
296 " or: tig [options] log [git log options]\n"
297 " or: tig [options] diff [git diff options]\n"
298 " or: tig [options] show [git show options]\n"
299 " or: tig [options] < [git command output]\n"
300 "\n"
301 "Options:\n"
302 " -l Start up in log view\n"
303 " -d Start up in diff view\n"
304 " -n[I], --line-number[=I] Show line numbers with given interval\n"
305 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
306 " -- Mark end of tig options\n"
307 " -v, --version Show version and exit\n"
308 " -h, --help Show help message and exit\n";
310 /* Option and state variables. */
311 static bool opt_line_number = FALSE;
312 static int opt_num_interval = NUMBER_INTERVAL;
313 static int opt_tab_size = TABSIZE;
314 static enum request opt_request = REQ_VIEW_MAIN;
315 static char opt_cmd[SIZEOF_CMD] = "";
316 static char opt_encoding[20] = "";
317 static bool opt_utf8 = TRUE;
318 static FILE *opt_pipe = NULL;
320 enum option_type {
321 OPT_NONE,
322 OPT_INT,
323 };
325 static bool
326 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
327 {
328 va_list args;
329 char *value = "";
330 int *number;
332 if (opt[0] != '-')
333 return FALSE;
335 if (opt[1] == '-') {
336 int namelen = strlen(name);
338 opt += 2;
340 if (strncmp(opt, name, namelen))
341 return FALSE;
343 if (opt[namelen] == '=')
344 value = opt + namelen + 1;
346 } else {
347 if (!short_name || opt[1] != short_name)
348 return FALSE;
349 value = opt + 2;
350 }
352 va_start(args, type);
353 if (type == OPT_INT) {
354 number = va_arg(args, int *);
355 if (isdigit(*value))
356 *number = atoi(value);
357 }
358 va_end(args);
360 return TRUE;
361 }
363 /* Returns the index of log or diff command or -1 to exit. */
364 static bool
365 parse_options(int argc, char *argv[])
366 {
367 int i;
369 for (i = 1; i < argc; i++) {
370 char *opt = argv[i];
372 if (!strcmp(opt, "-l")) {
373 opt_request = REQ_VIEW_LOG;
374 continue;
375 }
377 if (!strcmp(opt, "-d")) {
378 opt_request = REQ_VIEW_DIFF;
379 continue;
380 }
382 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
383 opt_line_number = TRUE;
384 continue;
385 }
387 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
388 opt_tab_size = MIN(opt_tab_size, TABSIZE);
389 continue;
390 }
392 if (check_option(opt, 'v', "version", OPT_NONE)) {
393 printf("tig version %s\n", VERSION);
394 return FALSE;
395 }
397 if (check_option(opt, 'h', "help", OPT_NONE)) {
398 printf(usage);
399 return FALSE;
400 }
402 if (!strcmp(opt, "--")) {
403 i++;
404 break;
405 }
407 if (!strcmp(opt, "log") ||
408 !strcmp(opt, "diff") ||
409 !strcmp(opt, "show")) {
410 opt_request = opt[0] == 'l'
411 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
412 break;
413 }
415 if (opt[0] && opt[0] != '-')
416 break;
418 die("unknown command '%s'", opt);
419 }
421 if (!isatty(STDIN_FILENO)) {
422 opt_request = REQ_VIEW_PAGER;
423 opt_pipe = stdin;
425 } else if (i < argc) {
426 size_t buf_size;
428 if (opt_request == REQ_VIEW_MAIN)
429 /* XXX: This is vulnerable to the user overriding
430 * options required for the main view parser. */
431 string_copy(opt_cmd, "git log --stat --pretty=raw");
432 else
433 string_copy(opt_cmd, "git");
434 buf_size = strlen(opt_cmd);
436 while (buf_size < sizeof(opt_cmd) && i < argc) {
437 opt_cmd[buf_size++] = ' ';
438 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
439 }
441 if (buf_size >= sizeof(opt_cmd))
442 die("command too long");
444 opt_cmd[buf_size] = 0;
446 }
448 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
449 opt_utf8 = FALSE;
451 return TRUE;
452 }
455 static struct int_map color_map[] = {
456 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
457 COLOR_MAP(DEFAULT),
458 COLOR_MAP(BLACK),
459 COLOR_MAP(BLUE),
460 COLOR_MAP(CYAN),
461 COLOR_MAP(GREEN),
462 COLOR_MAP(MAGENTA),
463 COLOR_MAP(RED),
464 COLOR_MAP(WHITE),
465 COLOR_MAP(YELLOW),
466 };
468 static struct int_map attr_map[] = {
469 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
470 ATTR_MAP(NORMAL),
471 ATTR_MAP(BLINK),
472 ATTR_MAP(BOLD),
473 ATTR_MAP(DIM),
474 ATTR_MAP(REVERSE),
475 ATTR_MAP(STANDOUT),
476 ATTR_MAP(UNDERLINE),
477 };
479 #define LINE_INFO \
480 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
481 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
482 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
483 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
484 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
485 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
486 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
487 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
488 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
489 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
490 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
491 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
492 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
493 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
494 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
495 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
496 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
497 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
498 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
499 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
500 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
501 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
502 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
503 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
504 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
505 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
506 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
507 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
508 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
509 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
510 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
511 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
512 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
513 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
514 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
515 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
516 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
517 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
520 /*
521 * Line-oriented content detection.
522 */
524 enum line_type {
525 #define LINE(type, line, fg, bg, attr) \
526 LINE_##type
527 LINE_INFO
528 #undef LINE
529 };
531 struct line_info {
532 const char *name; /* Option name. */
533 int namelen; /* Size of option name. */
534 const char *line; /* The start of line to match. */
535 int linelen; /* Size of string to match. */
536 int fg, bg, attr; /* Color and text attributes for the lines. */
537 };
539 static struct line_info line_info[] = {
540 #define LINE(type, line, fg, bg, attr) \
541 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
542 LINE_INFO
543 #undef LINE
544 };
546 static enum line_type
547 get_line_type(char *line)
548 {
549 int linelen = strlen(line);
550 enum line_type type;
552 for (type = 0; type < ARRAY_SIZE(line_info); type++)
553 /* Case insensitive search matches Signed-off-by lines better. */
554 if (linelen >= line_info[type].linelen &&
555 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
556 return type;
558 return LINE_DEFAULT;
559 }
561 static inline int
562 get_line_attr(enum line_type type)
563 {
564 assert(type < ARRAY_SIZE(line_info));
565 return COLOR_PAIR(type) | line_info[type].attr;
566 }
568 static struct line_info *
569 get_line_info(char *name, int namelen)
570 {
571 enum line_type type;
572 int i;
574 /* Diff-Header -> DIFF_HEADER */
575 for (i = 0; i < namelen; i++) {
576 if (name[i] == '-')
577 name[i] = '_';
578 else if (name[i] == '.')
579 name[i] = '_';
580 }
582 for (type = 0; type < ARRAY_SIZE(line_info); type++)
583 if (namelen == line_info[type].namelen &&
584 !strncasecmp(line_info[type].name, name, namelen))
585 return &line_info[type];
587 return NULL;
588 }
590 static void
591 init_colors(void)
592 {
593 int default_bg = COLOR_BLACK;
594 int default_fg = COLOR_WHITE;
595 enum line_type type;
597 start_color();
599 if (use_default_colors() != ERR) {
600 default_bg = -1;
601 default_fg = -1;
602 }
604 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
605 struct line_info *info = &line_info[type];
606 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
607 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
609 init_pair(type, fg, bg);
610 }
611 }
613 struct line {
614 enum line_type type;
615 void *data; /* User data */
616 };
619 /*
620 * User config file handling.
621 */
623 #define set_color(color, name, namelen) \
624 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
626 #define set_attribute(attr, name, namelen) \
627 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
629 static int config_lineno;
630 static bool config_errors;
631 static char *config_msg;
633 static int
634 set_option(char *opt, int optlen, char *value, int valuelen)
635 {
636 /* Reads: "color" object fgcolor bgcolor [attr] */
637 if (!strcmp(opt, "color")) {
638 struct line_info *info;
640 value = chomp_string(value);
641 valuelen = strcspn(value, " \t");
642 info = get_line_info(value, valuelen);
643 if (!info) {
644 config_msg = "Unknown color name";
645 return ERR;
646 }
648 value = chomp_string(value + valuelen);
649 valuelen = strcspn(value, " \t");
650 if (set_color(&info->fg, value, valuelen) == ERR) {
651 config_msg = "Unknown color";
652 return ERR;
653 }
655 value = chomp_string(value + valuelen);
656 valuelen = strcspn(value, " \t");
657 if (set_color(&info->bg, value, valuelen) == ERR) {
658 config_msg = "Unknown color";
659 return ERR;
660 }
662 value = chomp_string(value + valuelen);
663 if (*value &&
664 set_attribute(&info->attr, value, strlen(value)) == ERR) {
665 config_msg = "Unknown attribute";
666 return ERR;
667 }
669 return OK;
670 }
672 return ERR;
673 }
675 static int
676 read_option(char *opt, int optlen, char *value, int valuelen)
677 {
678 config_lineno++;
679 config_msg = "Internal error";
681 optlen = strcspn(opt, "#;");
682 if (optlen == 0) {
683 /* The whole line is a commend or empty. */
684 return OK;
686 } else if (opt[optlen] != 0) {
687 /* Part of the option name is a comment, so the value part
688 * should be ignored. */
689 valuelen = 0;
690 opt[optlen] = value[valuelen] = 0;
691 } else {
692 /* Else look for comment endings in the value. */
693 valuelen = strcspn(value, "#;");
694 value[valuelen] = 0;
695 }
697 if (set_option(opt, optlen, value, valuelen) == ERR) {
698 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
699 config_lineno, optlen, opt, config_msg);
700 config_errors = TRUE;
701 }
703 /* Always keep going if errors are encountered. */
704 return OK;
705 }
707 static int
708 load_options(void)
709 {
710 char *home = getenv("HOME");
711 char buf[1024];
712 FILE *file;
714 config_lineno = 0;
715 config_errors = FALSE;
717 if (!home || !string_format(buf, "%s/.tigrc", home))
718 return ERR;
720 /* It's ok that the file doesn't exist. */
721 file = fopen(buf, "r");
722 if (!file)
723 return OK;
725 if (read_properties(file, " \t", read_option) == ERR ||
726 config_errors == TRUE)
727 fprintf(stderr, "Errors while loading %s.\n", buf);
729 return OK;
730 }
733 /*
734 * The viewer
735 */
737 struct view;
738 struct view_ops;
740 /* The display array of active views and the index of the current view. */
741 static struct view *display[2];
742 static unsigned int current_view;
744 #define foreach_view(view, i) \
745 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
747 #define displayed_views() (display[1] != NULL ? 2 : 1)
749 /* Current head and commit ID */
750 static char ref_commit[SIZEOF_REF] = "HEAD";
751 static char ref_head[SIZEOF_REF] = "HEAD";
753 struct view {
754 const char *name; /* View name */
755 const char *cmd_fmt; /* Default command line format */
756 const char *cmd_env; /* Command line set via environment */
757 const char *id; /* Points to either of ref_{head,commit} */
759 struct view_ops *ops; /* View operations */
761 char cmd[SIZEOF_CMD]; /* Command buffer */
762 char ref[SIZEOF_REF]; /* Hovered commit reference */
763 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
765 int height, width; /* The width and height of the main window */
766 WINDOW *win; /* The main window */
767 WINDOW *title; /* The title window living below the main window */
769 /* Navigation */
770 unsigned long offset; /* Offset of the window top */
771 unsigned long lineno; /* Current line number */
773 /* If non-NULL, points to the view that opened this view. If this view
774 * is closed tig will switch back to the parent view. */
775 struct view *parent;
777 /* Buffering */
778 unsigned long lines; /* Total number of lines */
779 struct line *line; /* Line index */
780 unsigned long line_size;/* Total number of allocated lines */
781 unsigned int digits; /* Number of digits in the lines member. */
783 /* Loading */
784 FILE *pipe;
785 time_t start_time;
786 };
788 struct view_ops {
789 /* What type of content being displayed. Used in the title bar. */
790 const char *type;
791 /* Draw one line; @lineno must be < view->height. */
792 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
793 /* Read one line; updates view->line. */
794 bool (*read)(struct view *view, char *data);
795 /* Depending on view, change display based on current line. */
796 bool (*enter)(struct view *view, struct line *line);
797 };
799 static struct view_ops pager_ops;
800 static struct view_ops main_ops;
802 #define VIEW_STR(name, cmd, env, ref, ops) \
803 { name, cmd, #env, ref, ops }
805 #define VIEW_(id, name, ops, ref) \
806 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
809 static struct view views[] = {
810 VIEW_(MAIN, "main", &main_ops, ref_head),
811 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
812 VIEW_(LOG, "log", &pager_ops, ref_head),
813 VIEW_(HELP, "help", &pager_ops, "static"),
814 VIEW_(PAGER, "pager", &pager_ops, "static"),
815 };
817 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
820 static bool
821 draw_view_line(struct view *view, unsigned int lineno)
822 {
823 if (view->offset + lineno >= view->lines)
824 return FALSE;
826 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
827 }
829 static void
830 redraw_view_from(struct view *view, int lineno)
831 {
832 assert(0 <= lineno && lineno < view->height);
834 for (; lineno < view->height; lineno++) {
835 if (!draw_view_line(view, lineno))
836 break;
837 }
839 redrawwin(view->win);
840 wrefresh(view->win);
841 }
843 static void
844 redraw_view(struct view *view)
845 {
846 wclear(view->win);
847 redraw_view_from(view, 0);
848 }
851 static void
852 update_view_title(struct view *view)
853 {
854 if (view == display[current_view])
855 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
856 else
857 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
859 werase(view->title);
860 wmove(view->title, 0, 0);
862 if (*view->ref)
863 wprintw(view->title, "[%s] %s", view->name, view->ref);
864 else
865 wprintw(view->title, "[%s]", view->name);
867 if (view->lines || view->pipe) {
868 unsigned int lines = view->lines
869 ? (view->lineno + 1) * 100 / view->lines
870 : 0;
872 wprintw(view->title, " - %s %d of %d (%d%%)",
873 view->ops->type,
874 view->lineno + 1,
875 view->lines,
876 lines);
877 }
879 if (view->pipe) {
880 time_t secs = time(NULL) - view->start_time;
882 /* Three git seconds are a long time ... */
883 if (secs > 2)
884 wprintw(view->title, " %lds", secs);
885 }
887 wmove(view->title, 0, view->width - 1);
888 wrefresh(view->title);
889 }
891 static void
892 resize_display(void)
893 {
894 int offset, i;
895 struct view *base = display[0];
896 struct view *view = display[1] ? display[1] : display[0];
898 /* Setup window dimensions */
900 getmaxyx(stdscr, base->height, base->width);
902 /* Make room for the status window. */
903 base->height -= 1;
905 if (view != base) {
906 /* Horizontal split. */
907 view->width = base->width;
908 view->height = SCALE_SPLIT_VIEW(base->height);
909 base->height -= view->height;
911 /* Make room for the title bar. */
912 view->height -= 1;
913 }
915 /* Make room for the title bar. */
916 base->height -= 1;
918 offset = 0;
920 foreach_view (view, i) {
921 if (!view->win) {
922 view->win = newwin(view->height, 0, offset, 0);
923 if (!view->win)
924 die("Failed to create %s view", view->name);
926 scrollok(view->win, TRUE);
928 view->title = newwin(1, 0, offset + view->height, 0);
929 if (!view->title)
930 die("Failed to create title window");
932 } else {
933 wresize(view->win, view->height, view->width);
934 mvwin(view->win, offset, 0);
935 mvwin(view->title, offset + view->height, 0);
936 }
938 offset += view->height + 1;
939 }
940 }
942 static void
943 redraw_display(void)
944 {
945 struct view *view;
946 int i;
948 foreach_view (view, i) {
949 redraw_view(view);
950 update_view_title(view);
951 }
952 }
954 static void
955 update_display_cursor(void)
956 {
957 struct view *view = display[current_view];
959 /* Move the cursor to the right-most column of the cursor line.
960 *
961 * XXX: This could turn out to be a bit expensive, but it ensures that
962 * the cursor does not jump around. */
963 if (view->lines) {
964 wmove(view->win, view->lineno - view->offset, view->width - 1);
965 wrefresh(view->win);
966 }
967 }
969 /*
970 * Navigation
971 */
973 /* Scrolling backend */
974 static void
975 do_scroll_view(struct view *view, int lines, bool redraw)
976 {
977 /* The rendering expects the new offset. */
978 view->offset += lines;
980 assert(0 <= view->offset && view->offset < view->lines);
981 assert(lines);
983 /* Redraw the whole screen if scrolling is pointless. */
984 if (view->height < ABS(lines)) {
985 redraw_view(view);
987 } else {
988 int line = lines > 0 ? view->height - lines : 0;
989 int end = line + ABS(lines);
991 wscrl(view->win, lines);
993 for (; line < end; line++) {
994 if (!draw_view_line(view, line))
995 break;
996 }
997 }
999 /* Move current line into the view. */
1000 if (view->lineno < view->offset) {
1001 view->lineno = view->offset;
1002 draw_view_line(view, 0);
1004 } else if (view->lineno >= view->offset + view->height) {
1005 if (view->lineno == view->offset + view->height) {
1006 /* Clear the hidden line so it doesn't show if the view
1007 * is scrolled up. */
1008 wmove(view->win, view->height, 0);
1009 wclrtoeol(view->win);
1010 }
1011 view->lineno = view->offset + view->height - 1;
1012 draw_view_line(view, view->lineno - view->offset);
1013 }
1015 assert(view->offset <= view->lineno && view->lineno < view->lines);
1017 if (!redraw)
1018 return;
1020 redrawwin(view->win);
1021 wrefresh(view->win);
1022 report("");
1023 }
1025 /* Scroll frontend */
1026 static void
1027 scroll_view(struct view *view, enum request request)
1028 {
1029 int lines = 1;
1031 switch (request) {
1032 case REQ_SCROLL_PAGE_DOWN:
1033 lines = view->height;
1034 case REQ_SCROLL_LINE_DOWN:
1035 if (view->offset + lines > view->lines)
1036 lines = view->lines - view->offset;
1038 if (lines == 0 || view->offset + view->height >= view->lines) {
1039 report("Cannot scroll beyond the last line");
1040 return;
1041 }
1042 break;
1044 case REQ_SCROLL_PAGE_UP:
1045 lines = view->height;
1046 case REQ_SCROLL_LINE_UP:
1047 if (lines > view->offset)
1048 lines = view->offset;
1050 if (lines == 0) {
1051 report("Cannot scroll beyond the first line");
1052 return;
1053 }
1055 lines = -lines;
1056 break;
1058 default:
1059 die("request %d not handled in switch", request);
1060 }
1062 do_scroll_view(view, lines, TRUE);
1063 }
1065 /* Cursor moving */
1066 static void
1067 move_view(struct view *view, enum request request, bool redraw)
1068 {
1069 int steps;
1071 switch (request) {
1072 case REQ_MOVE_FIRST_LINE:
1073 steps = -view->lineno;
1074 break;
1076 case REQ_MOVE_LAST_LINE:
1077 steps = view->lines - view->lineno - 1;
1078 break;
1080 case REQ_MOVE_PAGE_UP:
1081 steps = view->height > view->lineno
1082 ? -view->lineno : -view->height;
1083 break;
1085 case REQ_MOVE_PAGE_DOWN:
1086 steps = view->lineno + view->height >= view->lines
1087 ? view->lines - view->lineno - 1 : view->height;
1088 break;
1090 case REQ_MOVE_UP:
1091 steps = -1;
1092 break;
1094 case REQ_MOVE_DOWN:
1095 steps = 1;
1096 break;
1098 default:
1099 die("request %d not handled in switch", request);
1100 }
1102 if (steps <= 0 && view->lineno == 0) {
1103 report("Cannot move beyond the first line");
1104 return;
1106 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1107 report("Cannot move beyond the last line");
1108 return;
1109 }
1111 /* Move the current line */
1112 view->lineno += steps;
1113 assert(0 <= view->lineno && view->lineno < view->lines);
1115 /* Repaint the old "current" line if we be scrolling */
1116 if (ABS(steps) < view->height) {
1117 int prev_lineno = view->lineno - steps - view->offset;
1119 wmove(view->win, prev_lineno, 0);
1120 wclrtoeol(view->win);
1121 draw_view_line(view, prev_lineno);
1122 }
1124 /* Check whether the view needs to be scrolled */
1125 if (view->lineno < view->offset ||
1126 view->lineno >= view->offset + view->height) {
1127 if (steps < 0 && -steps > view->offset) {
1128 steps = -view->offset;
1130 } else if (steps > 0) {
1131 if (view->lineno == view->lines - 1 &&
1132 view->lines > view->height) {
1133 steps = view->lines - view->offset - 1;
1134 if (steps >= view->height)
1135 steps -= view->height - 1;
1136 }
1137 }
1139 do_scroll_view(view, steps, redraw);
1140 return;
1141 }
1143 /* Draw the current line */
1144 draw_view_line(view, view->lineno - view->offset);
1146 if (!redraw)
1147 return;
1149 redrawwin(view->win);
1150 wrefresh(view->win);
1151 report("");
1152 }
1155 /*
1156 * Incremental updating
1157 */
1159 static void
1160 end_update(struct view *view)
1161 {
1162 if (!view->pipe)
1163 return;
1164 set_nonblocking_input(FALSE);
1165 if (view->pipe == stdin)
1166 fclose(view->pipe);
1167 else
1168 pclose(view->pipe);
1169 view->pipe = NULL;
1170 }
1172 static bool
1173 begin_update(struct view *view)
1174 {
1175 const char *id = view->id;
1177 if (view->pipe)
1178 end_update(view);
1180 if (opt_cmd[0]) {
1181 string_copy(view->cmd, opt_cmd);
1182 opt_cmd[0] = 0;
1183 /* When running random commands, the view ref could have become
1184 * invalid so clear it. */
1185 view->ref[0] = 0;
1186 } else {
1187 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1189 if (!string_format(view->cmd, format, id, id, id, id, id))
1190 return FALSE;
1191 }
1193 /* Special case for the pager view. */
1194 if (opt_pipe) {
1195 view->pipe = opt_pipe;
1196 opt_pipe = NULL;
1197 } else {
1198 view->pipe = popen(view->cmd, "r");
1199 }
1201 if (!view->pipe)
1202 return FALSE;
1204 set_nonblocking_input(TRUE);
1206 view->offset = 0;
1207 view->lines = 0;
1208 view->lineno = 0;
1209 string_copy(view->vid, id);
1211 if (view->line) {
1212 int i;
1214 for (i = 0; i < view->lines; i++)
1215 if (view->line[i].data)
1216 free(view->line[i].data);
1218 free(view->line);
1219 view->line = NULL;
1220 }
1222 view->start_time = time(NULL);
1224 return TRUE;
1225 }
1227 static struct line *
1228 realloc_lines(struct view *view, size_t line_size)
1229 {
1230 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1232 if (!tmp)
1233 return NULL;
1235 view->line = tmp;
1236 view->line_size = line_size;
1237 return view->line;
1238 }
1240 static bool
1241 update_view(struct view *view)
1242 {
1243 char buffer[BUFSIZ];
1244 char *line;
1245 /* The number of lines to read. If too low it will cause too much
1246 * redrawing (and possible flickering), if too high responsiveness
1247 * will suffer. */
1248 unsigned long lines = view->height;
1249 int redraw_from = -1;
1251 if (!view->pipe)
1252 return TRUE;
1254 /* Only redraw if lines are visible. */
1255 if (view->offset + view->height >= view->lines)
1256 redraw_from = view->lines - view->offset;
1258 if (!realloc_lines(view, view->lines + lines))
1259 goto alloc_error;
1261 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1262 int linelen = strlen(line);
1264 if (linelen)
1265 line[linelen - 1] = 0;
1267 if (!view->ops->read(view, line))
1268 goto alloc_error;
1270 if (lines-- == 1)
1271 break;
1272 }
1274 {
1275 int digits;
1277 lines = view->lines;
1278 for (digits = 0; lines; digits++)
1279 lines /= 10;
1281 /* Keep the displayed view in sync with line number scaling. */
1282 if (digits != view->digits) {
1283 view->digits = digits;
1284 redraw_from = 0;
1285 }
1286 }
1288 if (redraw_from >= 0) {
1289 /* If this is an incremental update, redraw the previous line
1290 * since for commits some members could have changed when
1291 * loading the main view. */
1292 if (redraw_from > 0)
1293 redraw_from--;
1295 /* Incrementally draw avoids flickering. */
1296 redraw_view_from(view, redraw_from);
1297 }
1299 /* Update the title _after_ the redraw so that if the redraw picks up a
1300 * commit reference in view->ref it'll be available here. */
1301 update_view_title(view);
1303 if (ferror(view->pipe)) {
1304 report("Failed to read: %s", strerror(errno));
1305 goto end;
1307 } else if (feof(view->pipe)) {
1308 report("");
1309 goto end;
1310 }
1312 return TRUE;
1314 alloc_error:
1315 report("Allocation failure");
1317 end:
1318 end_update(view);
1319 return FALSE;
1320 }
1322 enum open_flags {
1323 OPEN_DEFAULT = 0, /* Use default view switching. */
1324 OPEN_SPLIT = 1, /* Split current view. */
1325 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1326 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1327 };
1329 static void
1330 open_view(struct view *prev, enum request request, enum open_flags flags)
1331 {
1332 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1333 bool split = !!(flags & OPEN_SPLIT);
1334 bool reload = !!(flags & OPEN_RELOAD);
1335 struct view *view = VIEW(request);
1336 int nviews = displayed_views();
1337 struct view *base_view = display[0];
1339 if (view == prev && nviews == 1 && !reload) {
1340 report("Already in %s view", view->name);
1341 return;
1342 }
1344 if ((reload || strcmp(view->vid, view->id)) &&
1345 !begin_update(view)) {
1346 report("Failed to load %s view", view->name);
1347 return;
1348 }
1350 if (split) {
1351 display[1] = view;
1352 if (!backgrounded)
1353 current_view = 1;
1354 } else {
1355 /* Maximize the current view. */
1356 memset(display, 0, sizeof(display));
1357 current_view = 0;
1358 display[current_view] = view;
1359 }
1361 /* Resize the view when switching between split- and full-screen,
1362 * or when switching between two different full-screen views. */
1363 if (nviews != displayed_views() ||
1364 (nviews == 1 && base_view != display[0]))
1365 resize_display();
1367 if (split && prev->lineno - prev->offset >= prev->height) {
1368 /* Take the title line into account. */
1369 int lines = prev->lineno - prev->offset - prev->height + 1;
1371 /* Scroll the view that was split if the current line is
1372 * outside the new limited view. */
1373 do_scroll_view(prev, lines, TRUE);
1374 }
1376 if (prev && view != prev) {
1377 if (split && !backgrounded) {
1378 /* "Blur" the previous view. */
1379 update_view_title(prev);
1380 }
1382 view->parent = prev;
1383 }
1385 if (view == VIEW(REQ_VIEW_HELP))
1386 load_help_page();
1388 if (view->pipe && view->lines == 0) {
1389 /* Clear the old view and let the incremental updating refill
1390 * the screen. */
1391 wclear(view->win);
1392 report("");
1393 } else {
1394 redraw_view(view);
1395 report("");
1396 }
1398 /* If the view is backgrounded the above calls to report()
1399 * won't redraw the view title. */
1400 if (backgrounded)
1401 update_view_title(view);
1402 }
1405 /*
1406 * User request switch noodle
1407 */
1409 static int
1410 view_driver(struct view *view, enum request request)
1411 {
1412 int i;
1414 switch (request) {
1415 case REQ_MOVE_UP:
1416 case REQ_MOVE_DOWN:
1417 case REQ_MOVE_PAGE_UP:
1418 case REQ_MOVE_PAGE_DOWN:
1419 case REQ_MOVE_FIRST_LINE:
1420 case REQ_MOVE_LAST_LINE:
1421 move_view(view, request, TRUE);
1422 break;
1424 case REQ_SCROLL_LINE_DOWN:
1425 case REQ_SCROLL_LINE_UP:
1426 case REQ_SCROLL_PAGE_DOWN:
1427 case REQ_SCROLL_PAGE_UP:
1428 scroll_view(view, request);
1429 break;
1431 case REQ_VIEW_MAIN:
1432 case REQ_VIEW_DIFF:
1433 case REQ_VIEW_LOG:
1434 case REQ_VIEW_HELP:
1435 case REQ_VIEW_PAGER:
1436 open_view(view, request, OPEN_DEFAULT);
1437 break;
1439 case REQ_NEXT:
1440 case REQ_PREVIOUS:
1441 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1443 if (view == VIEW(REQ_VIEW_DIFF) &&
1444 view->parent == VIEW(REQ_VIEW_MAIN)) {
1445 bool redraw = display[1] == view;
1447 view = view->parent;
1448 move_view(view, request, redraw);
1449 if (redraw)
1450 update_view_title(view);
1451 } else {
1452 move_view(view, request, TRUE);
1453 break;
1454 }
1455 /* Fall-through */
1457 case REQ_ENTER:
1458 if (!view->lines) {
1459 report("Nothing to enter");
1460 break;
1461 }
1462 return view->ops->enter(view, &view->line[view->lineno]);
1464 case REQ_VIEW_NEXT:
1465 {
1466 int nviews = displayed_views();
1467 int next_view = (current_view + 1) % nviews;
1469 if (next_view == current_view) {
1470 report("Only one view is displayed");
1471 break;
1472 }
1474 current_view = next_view;
1475 /* Blur out the title of the previous view. */
1476 update_view_title(view);
1477 report("");
1478 break;
1479 }
1480 case REQ_TOGGLE_LINENO:
1481 opt_line_number = !opt_line_number;
1482 redraw_display();
1483 break;
1485 case REQ_PROMPT:
1486 /* Always reload^Wrerun commands from the prompt. */
1487 open_view(view, opt_request, OPEN_RELOAD);
1488 break;
1490 case REQ_STOP_LOADING:
1491 for (i = 0; i < ARRAY_SIZE(views); i++) {
1492 view = &views[i];
1493 if (view->pipe)
1494 report("Stopped loading the %s view", view->name),
1495 end_update(view);
1496 }
1497 break;
1499 case REQ_SHOW_VERSION:
1500 report("%s (built %s)", VERSION, __DATE__);
1501 return TRUE;
1503 case REQ_SCREEN_RESIZE:
1504 resize_display();
1505 /* Fall-through */
1506 case REQ_SCREEN_REDRAW:
1507 redraw_display();
1508 break;
1510 case REQ_SCREEN_UPDATE:
1511 doupdate();
1512 return TRUE;
1514 case REQ_VIEW_CLOSE:
1515 /* XXX: Mark closed views by letting view->parent point to the
1516 * view itself. Parents to closed view should never be
1517 * followed. */
1518 if (view->parent &&
1519 view->parent->parent != view->parent) {
1520 memset(display, 0, sizeof(display));
1521 current_view = 0;
1522 display[current_view] = view->parent;
1523 view->parent = view;
1524 resize_display();
1525 redraw_display();
1526 break;
1527 }
1528 /* Fall-through */
1529 case REQ_QUIT:
1530 return FALSE;
1532 default:
1533 /* An unknown key will show most commonly used commands. */
1534 report("Unknown key, press 'h' for help");
1535 return TRUE;
1536 }
1538 return TRUE;
1539 }
1542 /*
1543 * Pager backend
1544 */
1546 static bool
1547 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1548 {
1549 char *text = line->data;
1550 enum line_type type = line->type;
1551 int textlen = strlen(text);
1552 int attr;
1554 wmove(view->win, lineno, 0);
1556 if (view->offset + lineno == view->lineno) {
1557 if (type == LINE_COMMIT) {
1558 string_copy(view->ref, text + 7);
1559 string_copy(ref_commit, view->ref);
1560 }
1562 type = LINE_CURSOR;
1563 wchgat(view->win, -1, 0, type, NULL);
1564 }
1566 attr = get_line_attr(type);
1567 wattrset(view->win, attr);
1569 if (opt_line_number || opt_tab_size < TABSIZE) {
1570 static char spaces[] = " ";
1571 int col_offset = 0, col = 0;
1573 if (opt_line_number) {
1574 unsigned long real_lineno = view->offset + lineno + 1;
1576 if (real_lineno == 1 ||
1577 (real_lineno % opt_num_interval) == 0) {
1578 wprintw(view->win, "%.*d", view->digits, real_lineno);
1580 } else {
1581 waddnstr(view->win, spaces,
1582 MIN(view->digits, STRING_SIZE(spaces)));
1583 }
1584 waddstr(view->win, ": ");
1585 col_offset = view->digits + 2;
1586 }
1588 while (text && col_offset + col < view->width) {
1589 int cols_max = view->width - col_offset - col;
1590 char *pos = text;
1591 int cols;
1593 if (*text == '\t') {
1594 text++;
1595 assert(sizeof(spaces) > TABSIZE);
1596 pos = spaces;
1597 cols = opt_tab_size - (col % opt_tab_size);
1599 } else {
1600 text = strchr(text, '\t');
1601 cols = line ? text - pos : strlen(pos);
1602 }
1604 waddnstr(view->win, pos, MIN(cols, cols_max));
1605 col += cols;
1606 }
1608 } else {
1609 int col = 0, pos = 0;
1611 for (; pos < textlen && col < view->width; pos++, col++)
1612 if (text[pos] == '\t')
1613 col += TABSIZE - (col % TABSIZE) - 1;
1615 waddnstr(view->win, text, pos);
1616 }
1618 return TRUE;
1619 }
1621 static void
1622 add_pager_refs(struct view *view, struct line *line)
1623 {
1624 char buf[1024];
1625 char *data = line->data;
1626 struct ref **refs;
1627 int bufpos = 0, refpos = 0;
1628 const char *sep = "Refs: ";
1630 assert(line->type == LINE_COMMIT);
1632 refs = get_refs(data + STRING_SIZE("commit "));
1633 if (!refs)
1634 return;
1636 do {
1637 struct ref *ref = refs[refpos];
1638 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1640 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1641 return;
1642 sep = ", ";
1643 } while (refs[refpos++]->next);
1645 if (!realloc_lines(view, view->line_size + 1))
1646 return;
1648 line = &view->line[view->lines];
1649 line->data = strdup(buf);
1650 if (!line->data)
1651 return;
1653 line->type = LINE_PP_REFS;
1654 view->lines++;
1655 }
1657 static bool
1658 pager_read(struct view *view, char *data)
1659 {
1660 struct line *line = &view->line[view->lines];
1662 line->data = strdup(data);
1663 if (!line->data)
1664 return FALSE;
1666 line->type = get_line_type(line->data);
1667 view->lines++;
1669 if (line->type == LINE_COMMIT &&
1670 (view == VIEW(REQ_VIEW_DIFF) ||
1671 view == VIEW(REQ_VIEW_LOG)))
1672 add_pager_refs(view, line);
1674 return TRUE;
1675 }
1677 static bool
1678 pager_enter(struct view *view, struct line *line)
1679 {
1680 int split = 0;
1682 if (line->type == LINE_COMMIT &&
1683 (view == VIEW(REQ_VIEW_LOG) ||
1684 view == VIEW(REQ_VIEW_PAGER))) {
1685 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1686 split = 1;
1687 }
1689 /* Always scroll the view even if it was split. That way
1690 * you can use Enter to scroll through the log view and
1691 * split open each commit diff. */
1692 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1694 /* FIXME: A minor workaround. Scrolling the view will call report("")
1695 * but if we are scrolling a non-current view this won't properly
1696 * update the view title. */
1697 if (split)
1698 update_view_title(view);
1700 return TRUE;
1701 }
1703 static struct view_ops pager_ops = {
1704 "line",
1705 pager_draw,
1706 pager_read,
1707 pager_enter,
1708 };
1711 /*
1712 * Main view backend
1713 */
1715 struct commit {
1716 char id[41]; /* SHA1 ID. */
1717 char title[75]; /* The first line of the commit message. */
1718 char author[75]; /* The author of the commit. */
1719 struct tm time; /* Date from the author ident. */
1720 struct ref **refs; /* Repository references; tags & branch heads. */
1721 };
1723 static bool
1724 main_draw(struct view *view, struct line *line, unsigned int lineno)
1725 {
1726 char buf[DATE_COLS + 1];
1727 struct commit *commit = line->data;
1728 enum line_type type;
1729 int col = 0;
1730 size_t timelen;
1731 size_t authorlen;
1732 int trimmed = 1;
1734 if (!*commit->author)
1735 return FALSE;
1737 wmove(view->win, lineno, col);
1739 if (view->offset + lineno == view->lineno) {
1740 string_copy(view->ref, commit->id);
1741 string_copy(ref_commit, view->ref);
1742 type = LINE_CURSOR;
1743 wattrset(view->win, get_line_attr(type));
1744 wchgat(view->win, -1, 0, type, NULL);
1746 } else {
1747 type = LINE_MAIN_COMMIT;
1748 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1749 }
1751 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1752 waddnstr(view->win, buf, timelen);
1753 waddstr(view->win, " ");
1755 col += DATE_COLS;
1756 wmove(view->win, lineno, col);
1757 if (type != LINE_CURSOR)
1758 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1760 if (opt_utf8) {
1761 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1762 } else {
1763 authorlen = strlen(commit->author);
1764 if (authorlen > AUTHOR_COLS - 2) {
1765 authorlen = AUTHOR_COLS - 2;
1766 trimmed = 1;
1767 }
1768 }
1770 if (trimmed) {
1771 waddnstr(view->win, commit->author, authorlen);
1772 if (type != LINE_CURSOR)
1773 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1774 waddch(view->win, '~');
1775 } else {
1776 waddstr(view->win, commit->author);
1777 }
1779 col += AUTHOR_COLS;
1780 if (type != LINE_CURSOR)
1781 wattrset(view->win, A_NORMAL);
1783 mvwaddch(view->win, lineno, col, ACS_LTEE);
1784 wmove(view->win, lineno, col + 2);
1785 col += 2;
1787 if (commit->refs) {
1788 size_t i = 0;
1790 do {
1791 if (type == LINE_CURSOR)
1792 ;
1793 else if (commit->refs[i]->tag)
1794 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1795 else
1796 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1797 waddstr(view->win, "[");
1798 waddstr(view->win, commit->refs[i]->name);
1799 waddstr(view->win, "]");
1800 if (type != LINE_CURSOR)
1801 wattrset(view->win, A_NORMAL);
1802 waddstr(view->win, " ");
1803 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1804 } while (commit->refs[i++]->next);
1805 }
1807 if (type != LINE_CURSOR)
1808 wattrset(view->win, get_line_attr(type));
1810 {
1811 int titlelen = strlen(commit->title);
1813 if (col + titlelen > view->width)
1814 titlelen = view->width - col;
1816 waddnstr(view->win, commit->title, titlelen);
1817 }
1819 return TRUE;
1820 }
1822 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1823 static bool
1824 main_read(struct view *view, char *line)
1825 {
1826 enum line_type type = get_line_type(line);
1827 struct commit *commit = view->lines
1828 ? view->line[view->lines - 1].data : NULL;
1830 switch (type) {
1831 case LINE_COMMIT:
1832 commit = calloc(1, sizeof(struct commit));
1833 if (!commit)
1834 return FALSE;
1836 line += STRING_SIZE("commit ");
1838 view->line[view->lines++].data = commit;
1839 string_copy(commit->id, line);
1840 commit->refs = get_refs(commit->id);
1841 break;
1843 case LINE_AUTHOR:
1844 {
1845 char *ident = line + STRING_SIZE("author ");
1846 char *end = strchr(ident, '<');
1848 if (!commit)
1849 break;
1851 if (end) {
1852 for (; end > ident && isspace(end[-1]); end--) ;
1853 *end = 0;
1854 }
1856 string_copy(commit->author, ident);
1858 /* Parse epoch and timezone */
1859 if (end) {
1860 char *secs = strchr(end + 1, '>');
1861 char *zone;
1862 time_t time;
1864 if (!secs || secs[1] != ' ')
1865 break;
1867 secs += 2;
1868 time = (time_t) atol(secs);
1869 zone = strchr(secs, ' ');
1870 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1871 long tz;
1873 zone++;
1874 tz = ('0' - zone[1]) * 60 * 60 * 10;
1875 tz += ('0' - zone[2]) * 60 * 60;
1876 tz += ('0' - zone[3]) * 60;
1877 tz += ('0' - zone[4]) * 60;
1879 if (zone[0] == '-')
1880 tz = -tz;
1882 time -= tz;
1883 }
1884 gmtime_r(&time, &commit->time);
1885 }
1886 break;
1887 }
1888 default:
1889 if (!commit)
1890 break;
1892 /* Fill in the commit title if it has not already been set. */
1893 if (commit->title[0])
1894 break;
1896 /* Require titles to start with a non-space character at the
1897 * offset used by git log. */
1898 /* FIXME: More gracefull handling of titles; append "..." to
1899 * shortened titles, etc. */
1900 if (strncmp(line, " ", 4) ||
1901 isspace(line[4]))
1902 break;
1904 string_copy(commit->title, line + 4);
1905 }
1907 return TRUE;
1908 }
1910 static bool
1911 main_enter(struct view *view, struct line *line)
1912 {
1913 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
1915 open_view(view, REQ_VIEW_DIFF, flags);
1916 return TRUE;
1917 }
1919 static struct view_ops main_ops = {
1920 "commit",
1921 main_draw,
1922 main_read,
1923 main_enter,
1924 };
1927 /*
1928 * Keys
1929 */
1931 struct keymap {
1932 int alias;
1933 int request;
1934 };
1936 static struct keymap keymap[] = {
1937 /* View switching */
1938 { 'm', REQ_VIEW_MAIN },
1939 { 'd', REQ_VIEW_DIFF },
1940 { 'l', REQ_VIEW_LOG },
1941 { 'p', REQ_VIEW_PAGER },
1942 { 'h', REQ_VIEW_HELP },
1943 { '?', REQ_VIEW_HELP },
1945 /* View manipulation */
1946 { 'q', REQ_VIEW_CLOSE },
1947 { KEY_TAB, REQ_VIEW_NEXT },
1948 { KEY_RETURN, REQ_ENTER },
1949 { KEY_UP, REQ_PREVIOUS },
1950 { KEY_DOWN, REQ_NEXT },
1952 /* Cursor navigation */
1953 { 'k', REQ_MOVE_UP },
1954 { 'j', REQ_MOVE_DOWN },
1955 { KEY_HOME, REQ_MOVE_FIRST_LINE },
1956 { KEY_END, REQ_MOVE_LAST_LINE },
1957 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
1958 { ' ', REQ_MOVE_PAGE_DOWN },
1959 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
1960 { 'b', REQ_MOVE_PAGE_UP },
1961 { '-', REQ_MOVE_PAGE_UP },
1963 /* Scrolling */
1964 { KEY_IC, REQ_SCROLL_LINE_UP },
1965 { KEY_DC, REQ_SCROLL_LINE_DOWN },
1966 { 'w', REQ_SCROLL_PAGE_UP },
1967 { 's', REQ_SCROLL_PAGE_DOWN },
1969 /* Misc */
1970 { 'Q', REQ_QUIT },
1971 { 'z', REQ_STOP_LOADING },
1972 { 'v', REQ_SHOW_VERSION },
1973 { 'r', REQ_SCREEN_REDRAW },
1974 { 'n', REQ_TOGGLE_LINENO },
1975 { ':', REQ_PROMPT },
1977 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
1978 { ERR, REQ_SCREEN_UPDATE },
1980 /* Use the ncurses SIGWINCH handler. */
1981 { KEY_RESIZE, REQ_SCREEN_RESIZE },
1982 };
1984 static enum request
1985 get_request(int key)
1986 {
1987 int i;
1989 for (i = 0; i < ARRAY_SIZE(keymap); i++)
1990 if (keymap[i].alias == key)
1991 return keymap[i].request;
1993 return (enum request) key;
1994 }
1996 struct key {
1997 char *name;
1998 int value;
1999 };
2001 static struct key key_table[] = {
2002 { "Enter", KEY_RETURN },
2003 { "Space", ' ' },
2004 { "Backspace", KEY_BACKSPACE },
2005 { "Tab", KEY_TAB },
2006 { "Escape", KEY_ESC },
2007 { "Left", KEY_LEFT },
2008 { "Right", KEY_RIGHT },
2009 { "Up", KEY_UP },
2010 { "Down", KEY_DOWN },
2011 { "Insert", KEY_IC },
2012 { "Delete", KEY_DC },
2013 { "Home", KEY_HOME },
2014 { "End", KEY_END },
2015 { "PageUp", KEY_PPAGE },
2016 { "PageDown", KEY_NPAGE },
2017 { "F1", KEY_F(1) },
2018 { "F2", KEY_F(2) },
2019 { "F3", KEY_F(3) },
2020 { "F4", KEY_F(4) },
2021 { "F5", KEY_F(5) },
2022 { "F6", KEY_F(6) },
2023 { "F7", KEY_F(7) },
2024 { "F8", KEY_F(8) },
2025 { "F9", KEY_F(9) },
2026 { "F10", KEY_F(10) },
2027 { "F11", KEY_F(11) },
2028 { "F12", KEY_F(12) },
2029 };
2031 static char *
2032 get_key(enum request request)
2033 {
2034 static char buf[BUFSIZ];
2035 static char key_char[] = "'X'";
2036 int pos = 0;
2037 char *sep = " ";
2038 int i;
2040 buf[pos] = 0;
2042 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2043 char *seq = NULL;
2044 int key;
2046 if (keymap[i].request != request)
2047 continue;
2049 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2050 if (key_table[key].value == keymap[i].alias)
2051 seq = key_table[key].name;
2053 if (seq == NULL &&
2054 keymap[i].alias < 127 &&
2055 isprint(keymap[i].alias)) {
2056 key_char[1] = (char) keymap[i].alias;
2057 seq = key_char;
2058 }
2060 if (!seq)
2061 seq = "'?'";
2063 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
2064 return "Too many keybindings!";
2065 sep = ", ";
2066 }
2068 return buf;
2069 }
2071 static void load_help_page(void)
2072 {
2073 char buf[BUFSIZ];
2074 struct view *view = VIEW(REQ_VIEW_HELP);
2075 int lines = ARRAY_SIZE(req_info) + 2;
2076 int i;
2078 if (view->lines > 0)
2079 return;
2081 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2082 if (!req_info[i].request)
2083 lines++;
2085 view->line = calloc(lines, sizeof(*view->line));
2086 if (!view->line) {
2087 report("Allocation failure");
2088 return;
2089 }
2091 pager_read(view, "Quick reference for tig keybindings:");
2093 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2094 char *key;
2096 if (!req_info[i].request) {
2097 pager_read(view, "");
2098 pager_read(view, req_info[i].help);
2099 continue;
2100 }
2102 key = get_key(req_info[i].request);
2103 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
2104 continue;
2106 pager_read(view, buf);
2107 }
2108 }
2111 /*
2112 * Unicode / UTF-8 handling
2113 *
2114 * NOTE: Much of the following code for dealing with unicode is derived from
2115 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2116 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2117 */
2119 /* I've (over)annotated a lot of code snippets because I am not entirely
2120 * confident that the approach taken by this small UTF-8 interface is correct.
2121 * --jonas */
2123 static inline int
2124 unicode_width(unsigned long c)
2125 {
2126 if (c >= 0x1100 &&
2127 (c <= 0x115f /* Hangul Jamo */
2128 || c == 0x2329
2129 || c == 0x232a
2130 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2131 /* CJK ... Yi */
2132 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2133 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2134 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2135 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2136 || (c >= 0xffe0 && c <= 0xffe6)
2137 || (c >= 0x20000 && c <= 0x2fffd)
2138 || (c >= 0x30000 && c <= 0x3fffd)))
2139 return 2;
2141 return 1;
2142 }
2144 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2145 * Illegal bytes are set one. */
2146 static const unsigned char utf8_bytes[256] = {
2147 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,
2148 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,
2149 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,
2150 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,
2151 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,
2152 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,
2153 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,
2154 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,
2155 };
2157 /* Decode UTF-8 multi-byte representation into a unicode character. */
2158 static inline unsigned long
2159 utf8_to_unicode(const char *string, size_t length)
2160 {
2161 unsigned long unicode;
2163 switch (length) {
2164 case 1:
2165 unicode = string[0];
2166 break;
2167 case 2:
2168 unicode = (string[0] & 0x1f) << 6;
2169 unicode += (string[1] & 0x3f);
2170 break;
2171 case 3:
2172 unicode = (string[0] & 0x0f) << 12;
2173 unicode += ((string[1] & 0x3f) << 6);
2174 unicode += (string[2] & 0x3f);
2175 break;
2176 case 4:
2177 unicode = (string[0] & 0x0f) << 18;
2178 unicode += ((string[1] & 0x3f) << 12);
2179 unicode += ((string[2] & 0x3f) << 6);
2180 unicode += (string[3] & 0x3f);
2181 break;
2182 case 5:
2183 unicode = (string[0] & 0x0f) << 24;
2184 unicode += ((string[1] & 0x3f) << 18);
2185 unicode += ((string[2] & 0x3f) << 12);
2186 unicode += ((string[3] & 0x3f) << 6);
2187 unicode += (string[4] & 0x3f);
2188 break;
2189 case 6:
2190 unicode = (string[0] & 0x01) << 30;
2191 unicode += ((string[1] & 0x3f) << 24);
2192 unicode += ((string[2] & 0x3f) << 18);
2193 unicode += ((string[3] & 0x3f) << 12);
2194 unicode += ((string[4] & 0x3f) << 6);
2195 unicode += (string[5] & 0x3f);
2196 break;
2197 default:
2198 die("Invalid unicode length");
2199 }
2201 /* Invalid characters could return the special 0xfffd value but NUL
2202 * should be just as good. */
2203 return unicode > 0xffff ? 0 : unicode;
2204 }
2206 /* Calculates how much of string can be shown within the given maximum width
2207 * and sets trimmed parameter to non-zero value if all of string could not be
2208 * shown.
2209 *
2210 * Additionally, adds to coloffset how many many columns to move to align with
2211 * the expected position. Takes into account how multi-byte and double-width
2212 * characters will effect the cursor position.
2213 *
2214 * Returns the number of bytes to output from string to satisfy max_width. */
2215 static size_t
2216 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2217 {
2218 const char *start = string;
2219 const char *end = strchr(string, '\0');
2220 size_t mbwidth = 0;
2221 size_t width = 0;
2223 *trimmed = 0;
2225 while (string < end) {
2226 int c = *(unsigned char *) string;
2227 unsigned char bytes = utf8_bytes[c];
2228 size_t ucwidth;
2229 unsigned long unicode;
2231 if (string + bytes > end)
2232 break;
2234 /* Change representation to figure out whether
2235 * it is a single- or double-width character. */
2237 unicode = utf8_to_unicode(string, bytes);
2238 /* FIXME: Graceful handling of invalid unicode character. */
2239 if (!unicode)
2240 break;
2242 ucwidth = unicode_width(unicode);
2243 width += ucwidth;
2244 if (width > max_width) {
2245 *trimmed = 1;
2246 break;
2247 }
2249 /* The column offset collects the differences between the
2250 * number of bytes encoding a character and the number of
2251 * columns will be used for rendering said character.
2252 *
2253 * So if some character A is encoded in 2 bytes, but will be
2254 * represented on the screen using only 1 byte this will and up
2255 * adding 1 to the multi-byte column offset.
2256 *
2257 * Assumes that no double-width character can be encoding in
2258 * less than two bytes. */
2259 if (bytes > ucwidth)
2260 mbwidth += bytes - ucwidth;
2262 string += bytes;
2263 }
2265 *coloffset += mbwidth;
2267 return string - start;
2268 }
2271 /*
2272 * Status management
2273 */
2275 /* Whether or not the curses interface has been initialized. */
2276 static bool cursed = FALSE;
2278 /* The status window is used for polling keystrokes. */
2279 static WINDOW *status_win;
2281 /* Update status and title window. */
2282 static void
2283 report(const char *msg, ...)
2284 {
2285 static bool empty = TRUE;
2286 struct view *view = display[current_view];
2288 if (!empty || *msg) {
2289 va_list args;
2291 va_start(args, msg);
2293 werase(status_win);
2294 wmove(status_win, 0, 0);
2295 if (*msg) {
2296 vwprintw(status_win, msg, args);
2297 empty = FALSE;
2298 } else {
2299 empty = TRUE;
2300 }
2301 wrefresh(status_win);
2303 va_end(args);
2304 }
2306 update_view_title(view);
2307 update_display_cursor();
2308 }
2310 /* Controls when nodelay should be in effect when polling user input. */
2311 static void
2312 set_nonblocking_input(bool loading)
2313 {
2314 static unsigned int loading_views;
2316 if ((loading == FALSE && loading_views-- == 1) ||
2317 (loading == TRUE && loading_views++ == 0))
2318 nodelay(status_win, loading);
2319 }
2321 static void
2322 init_display(void)
2323 {
2324 int x, y;
2326 /* Initialize the curses library */
2327 if (isatty(STDIN_FILENO)) {
2328 cursed = !!initscr();
2329 } else {
2330 /* Leave stdin and stdout alone when acting as a pager. */
2331 FILE *io = fopen("/dev/tty", "r+");
2333 cursed = !!newterm(NULL, io, io);
2334 }
2336 if (!cursed)
2337 die("Failed to initialize curses");
2339 nonl(); /* Tell curses not to do NL->CR/NL on output */
2340 cbreak(); /* Take input chars one at a time, no wait for \n */
2341 noecho(); /* Don't echo input */
2342 leaveok(stdscr, TRUE);
2344 if (has_colors())
2345 init_colors();
2347 getmaxyx(stdscr, y, x);
2348 status_win = newwin(1, 0, y - 1, 0);
2349 if (!status_win)
2350 die("Failed to create status window");
2352 /* Enable keyboard mapping */
2353 keypad(status_win, TRUE);
2354 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2355 }
2358 /*
2359 * Repository references
2360 */
2362 static struct ref *refs;
2363 static size_t refs_size;
2365 /* Id <-> ref store */
2366 static struct ref ***id_refs;
2367 static size_t id_refs_size;
2369 static struct ref **
2370 get_refs(char *id)
2371 {
2372 struct ref ***tmp_id_refs;
2373 struct ref **ref_list = NULL;
2374 size_t ref_list_size = 0;
2375 size_t i;
2377 for (i = 0; i < id_refs_size; i++)
2378 if (!strcmp(id, id_refs[i][0]->id))
2379 return id_refs[i];
2381 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2382 if (!tmp_id_refs)
2383 return NULL;
2385 id_refs = tmp_id_refs;
2387 for (i = 0; i < refs_size; i++) {
2388 struct ref **tmp;
2390 if (strcmp(id, refs[i].id))
2391 continue;
2393 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2394 if (!tmp) {
2395 if (ref_list)
2396 free(ref_list);
2397 return NULL;
2398 }
2400 ref_list = tmp;
2401 if (ref_list_size > 0)
2402 ref_list[ref_list_size - 1]->next = 1;
2403 ref_list[ref_list_size] = &refs[i];
2405 /* XXX: The properties of the commit chains ensures that we can
2406 * safely modify the shared ref. The repo references will
2407 * always be similar for the same id. */
2408 ref_list[ref_list_size]->next = 0;
2409 ref_list_size++;
2410 }
2412 if (ref_list)
2413 id_refs[id_refs_size++] = ref_list;
2415 return ref_list;
2416 }
2418 static int
2419 read_ref(char *id, int idlen, char *name, int namelen)
2420 {
2421 struct ref *ref;
2422 bool tag = FALSE;
2423 bool tag_commit = FALSE;
2425 /* Commits referenced by tags has "^{}" appended. */
2426 if (name[namelen - 1] == '}') {
2427 while (namelen > 0 && name[namelen] != '^')
2428 namelen--;
2429 if (namelen > 0)
2430 tag_commit = TRUE;
2431 name[namelen] = 0;
2432 }
2434 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2435 if (!tag_commit)
2436 return OK;
2437 name += STRING_SIZE("refs/tags/");
2438 tag = TRUE;
2440 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2441 name += STRING_SIZE("refs/heads/");
2443 } else if (!strcmp(name, "HEAD")) {
2444 return OK;
2445 }
2447 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2448 if (!refs)
2449 return ERR;
2451 ref = &refs[refs_size++];
2452 ref->name = strdup(name);
2453 if (!ref->name)
2454 return ERR;
2456 ref->tag = tag;
2457 string_copy(ref->id, id);
2459 return OK;
2460 }
2462 static int
2463 load_refs(void)
2464 {
2465 const char *cmd_env = getenv("TIG_LS_REMOTE");
2466 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2468 return read_properties(popen(cmd, "r"), "\t", read_ref);
2469 }
2471 static int
2472 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2473 {
2474 if (!strcmp(name, "i18n.commitencoding"))
2475 string_copy(opt_encoding, value);
2477 return OK;
2478 }
2480 static int
2481 load_repo_config(void)
2482 {
2483 return read_properties(popen("git repo-config --list", "r"),
2484 "=", read_repo_config_option);
2485 }
2487 static int
2488 read_properties(FILE *pipe, const char *separators,
2489 int (*read_property)(char *, int, char *, int))
2490 {
2491 char buffer[BUFSIZ];
2492 char *name;
2493 int state = OK;
2495 if (!pipe)
2496 return ERR;
2498 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2499 char *value;
2500 size_t namelen;
2501 size_t valuelen;
2503 name = chomp_string(name);
2504 namelen = strcspn(name, separators);
2506 if (name[namelen]) {
2507 name[namelen] = 0;
2508 value = chomp_string(name + namelen + 1);
2509 valuelen = strlen(value);
2511 } else {
2512 value = "";
2513 valuelen = 0;
2514 }
2516 state = read_property(name, namelen, value, valuelen);
2517 }
2519 if (state != ERR && ferror(pipe))
2520 state = ERR;
2522 pclose(pipe);
2524 return state;
2525 }
2528 /*
2529 * Main
2530 */
2532 #if __GNUC__ >= 3
2533 #define __NORETURN __attribute__((__noreturn__))
2534 #else
2535 #define __NORETURN
2536 #endif
2538 static void __NORETURN
2539 quit(int sig)
2540 {
2541 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2542 if (cursed)
2543 endwin();
2544 exit(0);
2545 }
2547 static void __NORETURN
2548 die(const char *err, ...)
2549 {
2550 va_list args;
2552 endwin();
2554 va_start(args, err);
2555 fputs("tig: ", stderr);
2556 vfprintf(stderr, err, args);
2557 fputs("\n", stderr);
2558 va_end(args);
2560 exit(1);
2561 }
2563 int
2564 main(int argc, char *argv[])
2565 {
2566 struct view *view;
2567 enum request request;
2568 size_t i;
2570 signal(SIGINT, quit);
2572 if (load_options() == ERR)
2573 die("Failed to load user config.");
2575 /* Load the repo config file so options can be overwritten from
2576 * the command line. */
2577 if (load_repo_config() == ERR)
2578 die("Failed to load repo config.");
2580 if (!parse_options(argc, argv))
2581 return 0;
2583 if (load_refs() == ERR)
2584 die("Failed to load refs.");
2586 /* Require a git repository unless when running in pager mode. */
2587 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2588 die("Not a git repository");
2590 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2591 view->cmd_env = getenv(view->cmd_env);
2593 request = opt_request;
2595 init_display();
2597 while (view_driver(display[current_view], request)) {
2598 int key;
2599 int i;
2601 foreach_view (view, i)
2602 update_view(view);
2604 /* Refresh, accept single keystroke of input */
2605 key = wgetch(status_win);
2606 request = get_request(key);
2608 /* Some low-level request handling. This keeps access to
2609 * status_win restricted. */
2610 switch (request) {
2611 case REQ_PROMPT:
2612 report(":");
2613 /* Temporarily switch to line-oriented and echoed
2614 * input. */
2615 nocbreak();
2616 echo();
2618 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2619 memcpy(opt_cmd, "git ", 4);
2620 opt_request = REQ_VIEW_PAGER;
2621 } else {
2622 report("Prompt interrupted by loading view, "
2623 "press 'z' to stop loading views");
2624 request = REQ_SCREEN_UPDATE;
2625 }
2627 noecho();
2628 cbreak();
2629 break;
2631 case REQ_SCREEN_RESIZE:
2632 {
2633 int height, width;
2635 getmaxyx(stdscr, height, width);
2637 /* Resize the status view and let the view driver take
2638 * care of resizing the displayed views. */
2639 wresize(status_win, 1, width);
2640 mvwin(status_win, height - 1, 0);
2641 wrefresh(status_win);
2642 break;
2643 }
2644 default:
2645 break;
2646 }
2647 }
2649 quit(0);
2651 return 0;
2652 }