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 */
13 /**
14 * TIG(1)
15 * ======
16 *
17 * NAME
18 * ----
19 * tig - text-mode interface for git
20 *
21 * SYNOPSIS
22 * --------
23 * [verse]
24 * tig [options]
25 * tig [options] [--] [git log options]
26 * tig [options] log [git log options]
27 * tig [options] diff [git diff options]
28 * tig [options] show [git show options]
29 * tig [options] < [git command output]
30 *
31 * DESCRIPTION
32 * -----------
33 * Browse changes in a git repository. Additionally, tig(1) can also act
34 * as a pager for output of various git commands.
35 *
36 * When browsing repositories, tig(1) uses the underlying git commands
37 * to present the user with various views, such as summarized commit log
38 * and showing the commit with the log message, diffstat, and the diff.
39 *
40 * Using tig(1) as a pager, it will display input from stdin and try
41 * to colorize it.
42 **/
44 #ifndef VERSION
45 #define VERSION "tig-0.3"
46 #endif
48 #ifndef DEBUG
49 #define NDEBUG
50 #endif
52 #include <assert.h>
53 #include <errno.h>
54 #include <ctype.h>
55 #include <signal.h>
56 #include <stdarg.h>
57 #include <stdio.h>
58 #include <stdlib.h>
59 #include <string.h>
60 #include <unistd.h>
61 #include <time.h>
63 #include <curses.h>
65 static void die(const char *err, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
70 static void load_help_page(void);
72 #define ABS(x) ((x) >= 0 ? (x) : -(x))
73 #define MIN(x, y) ((x) < (y) ? (x) : (y))
75 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
76 #define STRING_SIZE(x) (sizeof(x) - 1)
78 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
79 #define SIZEOF_CMD 1024 /* Size of command buffer. */
81 /* This color name can be used to refer to the default term colors. */
82 #define COLOR_DEFAULT (-1)
84 /* The format and size of the date column in the main view. */
85 #define DATE_FORMAT "%Y-%m-%d %H:%M"
86 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
88 #define AUTHOR_COLS 20
90 /* The default interval between line numbers. */
91 #define NUMBER_INTERVAL 1
93 #define TABSIZE 8
95 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
97 /* Some ascii-shorthands fitted into the ncurses namespace. */
98 #define KEY_TAB '\t'
99 #define KEY_RETURN '\r'
100 #define KEY_ESC 27
103 struct ref {
104 char *name; /* Ref name; tag or head names are shortened. */
105 char id[41]; /* Commit SHA1 ID */
106 unsigned int tag:1; /* Is it a tag? */
107 unsigned int next:1; /* For ref lists: are there more refs? */
108 };
110 static struct ref **get_refs(char *id);
112 struct int_map {
113 const char *name;
114 int namelen;
115 int value;
116 };
118 static int
119 set_from_int_map(struct int_map *map, size_t map_size,
120 int *value, const char *name, int namelen)
121 {
123 int i;
125 for (i = 0; i < map_size; i++)
126 if (namelen == map[i].namelen &&
127 !strncasecmp(name, map[i].name, namelen)) {
128 *value = map[i].value;
129 return OK;
130 }
132 return ERR;
133 }
136 /*
137 * String helpers
138 */
140 static inline void
141 string_ncopy(char *dst, const char *src, int dstlen)
142 {
143 strncpy(dst, src, dstlen - 1);
144 dst[dstlen - 1] = 0;
146 }
148 /* Shorthand for safely copying into a fixed buffer. */
149 #define string_copy(dst, src) \
150 string_ncopy(dst, src, sizeof(dst))
152 static char *
153 chomp_string(char *name)
154 {
155 int namelen;
157 while (isspace(*name))
158 name++;
160 namelen = strlen(name) - 1;
161 while (namelen > 0 && isspace(name[namelen]))
162 name[namelen--] = 0;
164 return name;
165 }
168 /* Shell quoting
169 *
170 * NOTE: The following is a slightly modified copy of the git project's shell
171 * quoting routines found in the quote.c file.
172 *
173 * Help to copy the thing properly quoted for the shell safety. any single
174 * quote is replaced with '\'', any exclamation point is replaced with '\!',
175 * and the whole thing is enclosed in a
176 *
177 * E.g.
178 * original sq_quote result
179 * name ==> name ==> 'name'
180 * a b ==> a b ==> 'a b'
181 * a'b ==> a'\''b ==> 'a'\''b'
182 * a!b ==> a'\!'b ==> 'a'\!'b'
183 */
185 static size_t
186 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
187 {
188 char c;
190 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
192 BUFPUT('\'');
193 while ((c = *src++)) {
194 if (c == '\'' || c == '!') {
195 BUFPUT('\'');
196 BUFPUT('\\');
197 BUFPUT(c);
198 BUFPUT('\'');
199 } else {
200 BUFPUT(c);
201 }
202 }
203 BUFPUT('\'');
205 return bufsize;
206 }
209 /*
210 * User requests
211 */
213 #define REQ_INFO \
214 /* XXX: Keep the view request first and in sync with views[]. */ \
215 REQ_GROUP("View switching") \
216 REQ_(VIEW_MAIN, "Show main view"), \
217 REQ_(VIEW_DIFF, "Show diff view"), \
218 REQ_(VIEW_LOG, "Show log view"), \
219 REQ_(VIEW_HELP, "Show help page"), \
220 REQ_(VIEW_PAGER, "Show pager view"), \
221 \
222 REQ_GROUP("View manipulation") \
223 REQ_(ENTER, "Enter current line and scroll"), \
224 REQ_(NEXT, "Move to next"), \
225 REQ_(PREVIOUS, "Move to previous"), \
226 REQ_(VIEW_NEXT, "Move focus to next view"), \
227 REQ_(VIEW_CLOSE, "Close the current view"), \
228 REQ_(QUIT, "Close all views and quit"), \
229 \
230 REQ_GROUP("Cursor navigation") \
231 REQ_(MOVE_UP, "Move cursor one line up"), \
232 REQ_(MOVE_DOWN, "Move cursor one line down"), \
233 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
234 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
235 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
236 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
237 \
238 REQ_GROUP("Scrolling") \
239 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
240 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
241 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
242 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
243 \
244 REQ_GROUP("Misc") \
245 REQ_(PROMPT, "Bring up the prompt"), \
246 REQ_(SCREEN_UPDATE, "Update the screen"), \
247 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
248 REQ_(SCREEN_RESIZE, "Resize the screen"), \
249 REQ_(SHOW_VERSION, "Show version information"), \
250 REQ_(STOP_LOADING, "Stop all loading views"), \
251 REQ_(TOGGLE_LINENO, "Toggle line numbers"),
254 /* User action requests. */
255 enum request {
256 #define REQ_GROUP(help)
257 #define REQ_(req, help) REQ_##req
259 /* Offset all requests to avoid conflicts with ncurses getch values. */
260 REQ_OFFSET = KEY_MAX + 1,
261 REQ_INFO
263 #undef REQ_GROUP
264 #undef REQ_
265 };
267 struct request_info {
268 enum request request;
269 char *help;
270 };
272 static struct request_info req_info[] = {
273 #define REQ_GROUP(help) { 0, (help) },
274 #define REQ_(req, help) { REQ_##req, (help) }
275 REQ_INFO
276 #undef REQ_GROUP
277 #undef REQ_
278 };
280 /**
281 * OPTIONS
282 * -------
283 **/
285 static const char usage[] =
286 VERSION " (" __DATE__ ")\n"
287 "\n"
288 "Usage: tig [options]\n"
289 " or: tig [options] [--] [git log options]\n"
290 " or: tig [options] log [git log options]\n"
291 " or: tig [options] diff [git diff options]\n"
292 " or: tig [options] show [git show options]\n"
293 " or: tig [options] < [git command output]\n"
294 "\n"
295 "Options:\n"
296 " -l Start up in log view\n"
297 " -d Start up in diff view\n"
298 " -n[I], --line-number[=I] Show line numbers with given interval\n"
299 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
300 " -- Mark end of tig options\n"
301 " -v, --version Show version and exit\n"
302 " -h, --help Show help message and exit\n";
304 /* Option and state variables. */
305 static bool opt_line_number = FALSE;
306 static int opt_num_interval = NUMBER_INTERVAL;
307 static int opt_tab_size = TABSIZE;
308 static enum request opt_request = REQ_VIEW_MAIN;
309 static char opt_cmd[SIZEOF_CMD] = "";
310 static char opt_encoding[20] = "";
311 static bool opt_utf8 = TRUE;
312 static FILE *opt_pipe = NULL;
314 /* Returns the index of log or diff command or -1 to exit. */
315 static bool
316 parse_options(int argc, char *argv[])
317 {
318 int i;
320 for (i = 1; i < argc; i++) {
321 char *opt = argv[i];
323 /**
324 * -l::
325 * Start up in log view using the internal log command.
326 **/
327 if (!strcmp(opt, "-l")) {
328 opt_request = REQ_VIEW_LOG;
329 continue;
330 }
332 /**
333 * -d::
334 * Start up in diff view using the internal diff command.
335 **/
336 if (!strcmp(opt, "-d")) {
337 opt_request = REQ_VIEW_DIFF;
338 continue;
339 }
341 /**
342 * -n[INTERVAL], --line-number[=INTERVAL]::
343 * Prefix line numbers in log and diff view.
344 * Optionally, with interval different than each line.
345 **/
346 if (!strncmp(opt, "-n", 2) ||
347 !strncmp(opt, "--line-number", 13)) {
348 char *num = opt;
350 if (opt[1] == 'n') {
351 num = opt + 2;
353 } else if (opt[STRING_SIZE("--line-number")] == '=') {
354 num = opt + STRING_SIZE("--line-number=");
355 }
357 if (isdigit(*num))
358 opt_num_interval = atoi(num);
360 opt_line_number = TRUE;
361 continue;
362 }
364 /**
365 * -b[NSPACES], --tab-size[=NSPACES]::
366 * Set the number of spaces tabs should be expanded to.
367 **/
368 if (!strncmp(opt, "-b", 2) ||
369 !strncmp(opt, "--tab-size", 10)) {
370 char *num = opt;
372 if (opt[1] == 'b') {
373 num = opt + 2;
375 } else if (opt[STRING_SIZE("--tab-size")] == '=') {
376 num = opt + STRING_SIZE("--tab-size=");
377 }
379 if (isdigit(*num))
380 opt_tab_size = MIN(atoi(num), TABSIZE);
381 continue;
382 }
384 /**
385 * -v, --version::
386 * Show version and exit.
387 **/
388 if (!strcmp(opt, "-v") ||
389 !strcmp(opt, "--version")) {
390 printf("tig version %s\n", VERSION);
391 return FALSE;
392 }
394 /**
395 * -h, --help::
396 * Show help message and exit.
397 **/
398 if (!strcmp(opt, "-h") ||
399 !strcmp(opt, "--help")) {
400 printf(usage);
401 return FALSE;
402 }
404 /**
405 * \--::
406 * End of tig(1) options. Useful when specifying command
407 * options for the main view. Example:
408 *
409 * $ tig -- --since=1.month
410 **/
411 if (!strcmp(opt, "--")) {
412 i++;
413 break;
414 }
416 /**
417 * log [git log options]::
418 * Open log view using the given git log options.
419 *
420 * diff [git diff options]::
421 * Open diff view using the given git diff options.
422 *
423 * show [git show options]::
424 * Open diff view using the given git show options.
425 **/
426 if (!strcmp(opt, "log") ||
427 !strcmp(opt, "diff") ||
428 !strcmp(opt, "show")) {
429 opt_request = opt[0] == 'l'
430 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
431 break;
432 }
434 /**
435 * [git log options]::
436 * tig(1) will stop the option parsing when the first
437 * command line parameter not starting with "-" is
438 * encountered. All options including this one will be
439 * passed to git log when loading the main view.
440 * This makes it possible to say:
441 *
442 * $ tig tag-1.0..HEAD
443 **/
444 if (opt[0] && opt[0] != '-')
445 break;
447 die("unknown command '%s'", opt);
448 }
450 if (!isatty(STDIN_FILENO)) {
451 opt_request = REQ_VIEW_PAGER;
452 opt_pipe = stdin;
454 } else if (i < argc) {
455 size_t buf_size;
457 if (opt_request == REQ_VIEW_MAIN)
458 /* XXX: This is vulnerable to the user overriding
459 * options required for the main view parser. */
460 string_copy(opt_cmd, "git log --stat --pretty=raw");
461 else
462 string_copy(opt_cmd, "git");
463 buf_size = strlen(opt_cmd);
465 while (buf_size < sizeof(opt_cmd) && i < argc) {
466 opt_cmd[buf_size++] = ' ';
467 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
468 }
470 if (buf_size >= sizeof(opt_cmd))
471 die("command too long");
473 opt_cmd[buf_size] = 0;
475 }
477 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
478 opt_utf8 = FALSE;
480 return TRUE;
481 }
484 /**
485 * ENVIRONMENT VARIABLES
486 * ---------------------
487 * TIG_LS_REMOTE::
488 * Set command for retrieving all repository references. The command
489 * should output data in the same format as git-ls-remote(1).
490 **/
492 #define TIG_LS_REMOTE \
493 "git ls-remote . 2>/dev/null"
495 /**
496 * TIG_DIFF_CMD::
497 * The command used for the diff view. By default, git show is used
498 * as a backend.
499 *
500 * TIG_LOG_CMD::
501 * The command used for the log view. If you prefer to have both
502 * author and committer shown in the log view be sure to pass
503 * `--pretty=fuller` to git log.
504 *
505 * TIG_MAIN_CMD::
506 * The command used for the main view. Note, you must always specify
507 * the option: `--pretty=raw` since the main view parser expects to
508 * read that format.
509 **/
511 #define TIG_DIFF_CMD \
512 "git show --patch-with-stat --find-copies-harder -B -C %s"
514 #define TIG_LOG_CMD \
515 "git log --cc --stat -n100 %s"
517 #define TIG_MAIN_CMD \
518 "git log --topo-order --stat --pretty=raw %s"
520 /* ... silently ignore that the following are also exported. */
522 #define TIG_HELP_CMD \
523 ""
525 #define TIG_PAGER_CMD \
526 ""
529 /**
530 * FILES
531 * -----
532 * '~/.tigrc'::
533 * User configuration file. See tigrc(5) for examples.
534 *
535 * '.git/config'::
536 * Repository config file. Read on startup with the help of
537 * git-repo-config(1).
538 **/
540 static struct int_map color_map[] = {
541 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
542 COLOR_MAP(DEFAULT),
543 COLOR_MAP(BLACK),
544 COLOR_MAP(BLUE),
545 COLOR_MAP(CYAN),
546 COLOR_MAP(GREEN),
547 COLOR_MAP(MAGENTA),
548 COLOR_MAP(RED),
549 COLOR_MAP(WHITE),
550 COLOR_MAP(YELLOW),
551 };
553 static struct int_map attr_map[] = {
554 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
555 ATTR_MAP(NORMAL),
556 ATTR_MAP(BLINK),
557 ATTR_MAP(BOLD),
558 ATTR_MAP(DIM),
559 ATTR_MAP(REVERSE),
560 ATTR_MAP(STANDOUT),
561 ATTR_MAP(UNDERLINE),
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(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
586 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
587 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
589 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
590 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
591 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
592 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
593 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
594 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
595 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
596 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
597 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
598 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
599 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
600 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
601 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
604 /*
605 * Line-oriented content detection.
606 */
608 enum line_type {
609 #define LINE(type, line, fg, bg, attr) \
610 LINE_##type
611 LINE_INFO
612 #undef LINE
613 };
615 struct line_info {
616 const char *name; /* Option name. */
617 int namelen; /* Size of option name. */
618 const char *line; /* The start of line to match. */
619 int linelen; /* Size of string to match. */
620 int fg, bg, attr; /* Color and text attributes for the lines. */
621 };
623 static struct line_info line_info[] = {
624 #define LINE(type, line, fg, bg, attr) \
625 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
626 LINE_INFO
627 #undef LINE
628 };
630 static enum line_type
631 get_line_type(char *line)
632 {
633 int linelen = strlen(line);
634 enum line_type type;
636 for (type = 0; type < ARRAY_SIZE(line_info); type++)
637 /* Case insensitive search matches Signed-off-by lines better. */
638 if (linelen >= line_info[type].linelen &&
639 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
640 return type;
642 return LINE_DEFAULT;
643 }
645 static inline int
646 get_line_attr(enum line_type type)
647 {
648 assert(type < ARRAY_SIZE(line_info));
649 return COLOR_PAIR(type) | line_info[type].attr;
650 }
652 static struct line_info *
653 get_line_info(char *name, int namelen)
654 {
655 enum line_type type;
656 int i;
658 /* Diff-Header -> DIFF_HEADER */
659 for (i = 0; i < namelen; i++) {
660 if (name[i] == '-')
661 name[i] = '_';
662 else if (name[i] == '.')
663 name[i] = '_';
664 }
666 for (type = 0; type < ARRAY_SIZE(line_info); type++)
667 if (namelen == line_info[type].namelen &&
668 !strncasecmp(line_info[type].name, name, namelen))
669 return &line_info[type];
671 return NULL;
672 }
674 static void
675 init_colors(void)
676 {
677 int default_bg = COLOR_BLACK;
678 int default_fg = COLOR_WHITE;
679 enum line_type type;
681 start_color();
683 if (use_default_colors() != ERR) {
684 default_bg = -1;
685 default_fg = -1;
686 }
688 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
689 struct line_info *info = &line_info[type];
690 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
691 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
693 init_pair(type, fg, bg);
694 }
695 }
697 struct line {
698 enum line_type type;
699 void *data; /* User data */
700 };
703 /*
704 * User config file handling.
705 */
707 #define set_color(color, name, namelen) \
708 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
710 #define set_attribute(attr, name, namelen) \
711 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
713 static int config_lineno;
714 static bool config_errors;
715 static char *config_msg;
717 static int
718 set_option(char *opt, int optlen, char *value, int valuelen)
719 {
720 /* Reads: "color" object fgcolor bgcolor [attr] */
721 if (!strcmp(opt, "color")) {
722 struct line_info *info;
724 value = chomp_string(value);
725 valuelen = strcspn(value, " \t");
726 info = get_line_info(value, valuelen);
727 if (!info) {
728 config_msg = "Unknown color name";
729 return ERR;
730 }
732 value = chomp_string(value + valuelen);
733 valuelen = strcspn(value, " \t");
734 if (set_color(&info->fg, value, valuelen) == ERR) {
735 config_msg = "Unknown color";
736 return ERR;
737 }
739 value = chomp_string(value + valuelen);
740 valuelen = strcspn(value, " \t");
741 if (set_color(&info->bg, value, valuelen) == ERR) {
742 config_msg = "Unknown color";
743 return ERR;
744 }
746 value = chomp_string(value + valuelen);
747 if (*value &&
748 set_attribute(&info->attr, value, strlen(value)) == ERR) {
749 config_msg = "Unknown attribute";
750 return ERR;
751 }
753 return OK;
754 }
756 return ERR;
757 }
759 static int
760 read_option(char *opt, int optlen, char *value, int valuelen)
761 {
762 config_lineno++;
763 config_msg = "Internal error";
765 optlen = strcspn(opt, "#;");
766 if (optlen == 0) {
767 /* The whole line is a commend or empty. */
768 return OK;
770 } else if (opt[optlen] != 0) {
771 /* Part of the option name is a comment, so the value part
772 * should be ignored. */
773 valuelen = 0;
774 opt[optlen] = value[valuelen] = 0;
775 } else {
776 /* Else look for comment endings in the value. */
777 valuelen = strcspn(value, "#;");
778 value[valuelen] = 0;
779 }
781 if (set_option(opt, optlen, value, valuelen) == ERR) {
782 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
783 config_lineno, optlen, opt, config_msg);
784 config_errors = TRUE;
785 }
787 /* Always keep going if errors are encountered. */
788 return OK;
789 }
791 static int
792 load_options(void)
793 {
794 char *home = getenv("HOME");
795 char buf[1024];
796 FILE *file;
798 config_lineno = 0;
799 config_errors = FALSE;
801 if (!home ||
802 snprintf(buf, sizeof(buf), "%s/.tigrc", home) >= sizeof(buf))
803 return ERR;
805 /* It's ok that the file doesn't exist. */
806 file = fopen(buf, "r");
807 if (!file)
808 return OK;
810 if (read_properties(file, " \t", read_option) == ERR ||
811 config_errors == TRUE)
812 fprintf(stderr, "Errors while loading %s.\n", buf);
814 return OK;
815 }
818 /*
819 * The viewer
820 */
822 struct view;
823 struct view_ops;
825 /* The display array of active views and the index of the current view. */
826 static struct view *display[2];
827 static unsigned int current_view;
829 #define foreach_view(view, i) \
830 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
832 #define displayed_views() (display[1] != NULL ? 2 : 1)
834 /* Current head and commit ID */
835 static char ref_commit[SIZEOF_REF] = "HEAD";
836 static char ref_head[SIZEOF_REF] = "HEAD";
838 struct view {
839 const char *name; /* View name */
840 const char *cmd_fmt; /* Default command line format */
841 const char *cmd_env; /* Command line set via environment */
842 const char *id; /* Points to either of ref_{head,commit} */
844 struct view_ops *ops; /* View operations */
846 char cmd[SIZEOF_CMD]; /* Command buffer */
847 char ref[SIZEOF_REF]; /* Hovered commit reference */
848 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
850 int height, width; /* The width and height of the main window */
851 WINDOW *win; /* The main window */
852 WINDOW *title; /* The title window living below the main window */
854 /* Navigation */
855 unsigned long offset; /* Offset of the window top */
856 unsigned long lineno; /* Current line number */
858 /* If non-NULL, points to the view that opened this view. If this view
859 * is closed tig will switch back to the parent view. */
860 struct view *parent;
862 /* Buffering */
863 unsigned long lines; /* Total number of lines */
864 struct line *line; /* Line index */
865 unsigned int digits; /* Number of digits in the lines member. */
867 /* Loading */
868 FILE *pipe;
869 time_t start_time;
870 };
872 struct view_ops {
873 /* What type of content being displayed. Used in the title bar. */
874 const char *type;
875 /* Draw one line; @lineno must be < view->height. */
876 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
877 /* Read one line; updates view->line. */
878 bool (*read)(struct view *view, struct line *prev, char *data);
879 /* Depending on view, change display based on current line. */
880 bool (*enter)(struct view *view, struct line *line);
881 };
883 static struct view_ops pager_ops;
884 static struct view_ops main_ops;
886 #define VIEW_STR(name, cmd, env, ref, ops) \
887 { name, cmd, #env, ref, ops }
889 #define VIEW_(id, name, ops, ref) \
890 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
893 static struct view views[] = {
894 VIEW_(MAIN, "main", &main_ops, ref_head),
895 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
896 VIEW_(LOG, "log", &pager_ops, ref_head),
897 VIEW_(HELP, "help", &pager_ops, "static"),
898 VIEW_(PAGER, "pager", &pager_ops, "static"),
899 };
901 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
904 static bool
905 draw_view_line(struct view *view, unsigned int lineno)
906 {
907 if (view->offset + lineno >= view->lines)
908 return FALSE;
910 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
911 }
913 static void
914 redraw_view_from(struct view *view, int lineno)
915 {
916 assert(0 <= lineno && lineno < view->height);
918 for (; lineno < view->height; lineno++) {
919 if (!draw_view_line(view, lineno))
920 break;
921 }
923 redrawwin(view->win);
924 wrefresh(view->win);
925 }
927 static void
928 redraw_view(struct view *view)
929 {
930 wclear(view->win);
931 redraw_view_from(view, 0);
932 }
935 static void
936 update_view_title(struct view *view)
937 {
938 if (view == display[current_view])
939 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
940 else
941 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
943 werase(view->title);
944 wmove(view->title, 0, 0);
946 if (*view->ref)
947 wprintw(view->title, "[%s] %s", view->name, view->ref);
948 else
949 wprintw(view->title, "[%s]", view->name);
951 if (view->lines || view->pipe) {
952 unsigned int lines = view->lines
953 ? (view->lineno + 1) * 100 / view->lines
954 : 0;
956 wprintw(view->title, " - %s %d of %d (%d%%)",
957 view->ops->type,
958 view->lineno + 1,
959 view->lines,
960 lines);
961 }
963 if (view->pipe) {
964 time_t secs = time(NULL) - view->start_time;
966 /* Three git seconds are a long time ... */
967 if (secs > 2)
968 wprintw(view->title, " %lds", secs);
969 }
971 wmove(view->title, 0, view->width - 1);
972 wrefresh(view->title);
973 }
975 static void
976 resize_display(void)
977 {
978 int offset, i;
979 struct view *base = display[0];
980 struct view *view = display[1] ? display[1] : display[0];
982 /* Setup window dimensions */
984 getmaxyx(stdscr, base->height, base->width);
986 /* Make room for the status window. */
987 base->height -= 1;
989 if (view != base) {
990 /* Horizontal split. */
991 view->width = base->width;
992 view->height = SCALE_SPLIT_VIEW(base->height);
993 base->height -= view->height;
995 /* Make room for the title bar. */
996 view->height -= 1;
997 }
999 /* Make room for the title bar. */
1000 base->height -= 1;
1002 offset = 0;
1004 foreach_view (view, i) {
1005 if (!view->win) {
1006 view->win = newwin(view->height, 0, offset, 0);
1007 if (!view->win)
1008 die("Failed to create %s view", view->name);
1010 scrollok(view->win, TRUE);
1012 view->title = newwin(1, 0, offset + view->height, 0);
1013 if (!view->title)
1014 die("Failed to create title window");
1016 } else {
1017 wresize(view->win, view->height, view->width);
1018 mvwin(view->win, offset, 0);
1019 mvwin(view->title, offset + view->height, 0);
1020 }
1022 offset += view->height + 1;
1023 }
1024 }
1026 static void
1027 redraw_display(void)
1028 {
1029 struct view *view;
1030 int i;
1032 foreach_view (view, i) {
1033 redraw_view(view);
1034 update_view_title(view);
1035 }
1036 }
1038 static void
1039 update_display_cursor(void)
1040 {
1041 struct view *view = display[current_view];
1043 /* Move the cursor to the right-most column of the cursor line.
1044 *
1045 * XXX: This could turn out to be a bit expensive, but it ensures that
1046 * the cursor does not jump around. */
1047 if (view->lines) {
1048 wmove(view->win, view->lineno - view->offset, view->width - 1);
1049 wrefresh(view->win);
1050 }
1051 }
1053 /*
1054 * Navigation
1055 */
1057 /* Scrolling backend */
1058 static void
1059 do_scroll_view(struct view *view, int lines, bool redraw)
1060 {
1061 /* The rendering expects the new offset. */
1062 view->offset += lines;
1064 assert(0 <= view->offset && view->offset < view->lines);
1065 assert(lines);
1067 /* Redraw the whole screen if scrolling is pointless. */
1068 if (view->height < ABS(lines)) {
1069 redraw_view(view);
1071 } else {
1072 int line = lines > 0 ? view->height - lines : 0;
1073 int end = line + ABS(lines);
1075 wscrl(view->win, lines);
1077 for (; line < end; line++) {
1078 if (!draw_view_line(view, line))
1079 break;
1080 }
1081 }
1083 /* Move current line into the view. */
1084 if (view->lineno < view->offset) {
1085 view->lineno = view->offset;
1086 draw_view_line(view, 0);
1088 } else if (view->lineno >= view->offset + view->height) {
1089 if (view->lineno == view->offset + view->height) {
1090 /* Clear the hidden line so it doesn't show if the view
1091 * is scrolled up. */
1092 wmove(view->win, view->height, 0);
1093 wclrtoeol(view->win);
1094 }
1095 view->lineno = view->offset + view->height - 1;
1096 draw_view_line(view, view->lineno - view->offset);
1097 }
1099 assert(view->offset <= view->lineno && view->lineno < view->lines);
1101 if (!redraw)
1102 return;
1104 redrawwin(view->win);
1105 wrefresh(view->win);
1106 report("");
1107 }
1109 /* Scroll frontend */
1110 static void
1111 scroll_view(struct view *view, enum request request)
1112 {
1113 int lines = 1;
1115 switch (request) {
1116 case REQ_SCROLL_PAGE_DOWN:
1117 lines = view->height;
1118 case REQ_SCROLL_LINE_DOWN:
1119 if (view->offset + lines > view->lines)
1120 lines = view->lines - view->offset;
1122 if (lines == 0 || view->offset + view->height >= view->lines) {
1123 report("Cannot scroll beyond the last line");
1124 return;
1125 }
1126 break;
1128 case REQ_SCROLL_PAGE_UP:
1129 lines = view->height;
1130 case REQ_SCROLL_LINE_UP:
1131 if (lines > view->offset)
1132 lines = view->offset;
1134 if (lines == 0) {
1135 report("Cannot scroll beyond the first line");
1136 return;
1137 }
1139 lines = -lines;
1140 break;
1142 default:
1143 die("request %d not handled in switch", request);
1144 }
1146 do_scroll_view(view, lines, TRUE);
1147 }
1149 /* Cursor moving */
1150 static void
1151 move_view(struct view *view, enum request request, bool redraw)
1152 {
1153 int steps;
1155 switch (request) {
1156 case REQ_MOVE_FIRST_LINE:
1157 steps = -view->lineno;
1158 break;
1160 case REQ_MOVE_LAST_LINE:
1161 steps = view->lines - view->lineno - 1;
1162 break;
1164 case REQ_MOVE_PAGE_UP:
1165 steps = view->height > view->lineno
1166 ? -view->lineno : -view->height;
1167 break;
1169 case REQ_MOVE_PAGE_DOWN:
1170 steps = view->lineno + view->height >= view->lines
1171 ? view->lines - view->lineno - 1 : view->height;
1172 break;
1174 case REQ_MOVE_UP:
1175 steps = -1;
1176 break;
1178 case REQ_MOVE_DOWN:
1179 steps = 1;
1180 break;
1182 default:
1183 die("request %d not handled in switch", request);
1184 }
1186 if (steps <= 0 && view->lineno == 0) {
1187 report("Cannot move beyond the first line");
1188 return;
1190 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1191 report("Cannot move beyond the last line");
1192 return;
1193 }
1195 /* Move the current line */
1196 view->lineno += steps;
1197 assert(0 <= view->lineno && view->lineno < view->lines);
1199 /* Repaint the old "current" line if we be scrolling */
1200 if (ABS(steps) < view->height) {
1201 int prev_lineno = view->lineno - steps - view->offset;
1203 wmove(view->win, prev_lineno, 0);
1204 wclrtoeol(view->win);
1205 draw_view_line(view, prev_lineno);
1206 }
1208 /* Check whether the view needs to be scrolled */
1209 if (view->lineno < view->offset ||
1210 view->lineno >= view->offset + view->height) {
1211 if (steps < 0 && -steps > view->offset) {
1212 steps = -view->offset;
1214 } else if (steps > 0) {
1215 if (view->lineno == view->lines - 1 &&
1216 view->lines > view->height) {
1217 steps = view->lines - view->offset - 1;
1218 if (steps >= view->height)
1219 steps -= view->height - 1;
1220 }
1221 }
1223 do_scroll_view(view, steps, redraw);
1224 return;
1225 }
1227 /* Draw the current line */
1228 draw_view_line(view, view->lineno - view->offset);
1230 if (!redraw)
1231 return;
1233 redrawwin(view->win);
1234 wrefresh(view->win);
1235 report("");
1236 }
1239 /*
1240 * Incremental updating
1241 */
1243 static void
1244 end_update(struct view *view)
1245 {
1246 if (!view->pipe)
1247 return;
1248 set_nonblocking_input(FALSE);
1249 if (view->pipe == stdin)
1250 fclose(view->pipe);
1251 else
1252 pclose(view->pipe);
1253 view->pipe = NULL;
1254 }
1256 static bool
1257 begin_update(struct view *view)
1258 {
1259 const char *id = view->id;
1261 if (view->pipe)
1262 end_update(view);
1264 if (opt_cmd[0]) {
1265 string_copy(view->cmd, opt_cmd);
1266 opt_cmd[0] = 0;
1267 /* When running random commands, the view ref could have become
1268 * invalid so clear it. */
1269 view->ref[0] = 0;
1270 } else {
1271 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1273 if (snprintf(view->cmd, sizeof(view->cmd), format,
1274 id, id, id, id, id) >= sizeof(view->cmd))
1275 return FALSE;
1276 }
1278 /* Special case for the pager view. */
1279 if (opt_pipe) {
1280 view->pipe = opt_pipe;
1281 opt_pipe = NULL;
1282 } else {
1283 view->pipe = popen(view->cmd, "r");
1284 }
1286 if (!view->pipe)
1287 return FALSE;
1289 set_nonblocking_input(TRUE);
1291 view->offset = 0;
1292 view->lines = 0;
1293 view->lineno = 0;
1294 string_copy(view->vid, id);
1296 if (view->line) {
1297 int i;
1299 for (i = 0; i < view->lines; i++)
1300 if (view->line[i].data)
1301 free(view->line[i].data);
1303 free(view->line);
1304 view->line = NULL;
1305 }
1307 view->start_time = time(NULL);
1309 return TRUE;
1310 }
1312 static bool
1313 update_view(struct view *view)
1314 {
1315 char buffer[BUFSIZ];
1316 char *line;
1317 struct line *tmp;
1318 /* The number of lines to read. If too low it will cause too much
1319 * redrawing (and possible flickering), if too high responsiveness
1320 * will suffer. */
1321 unsigned long lines = view->height;
1322 int redraw_from = -1;
1324 if (!view->pipe)
1325 return TRUE;
1327 /* Only redraw if lines are visible. */
1328 if (view->offset + view->height >= view->lines)
1329 redraw_from = view->lines - view->offset;
1331 tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines));
1332 if (!tmp)
1333 goto alloc_error;
1335 view->line = tmp;
1337 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1338 int linelen = strlen(line);
1340 struct line *prev = view->lines
1341 ? &view->line[view->lines - 1]
1342 : NULL;
1344 if (linelen)
1345 line[linelen - 1] = 0;
1347 if (!view->ops->read(view, prev, line))
1348 goto alloc_error;
1350 if (lines-- == 1)
1351 break;
1352 }
1354 {
1355 int digits;
1357 lines = view->lines;
1358 for (digits = 0; lines; digits++)
1359 lines /= 10;
1361 /* Keep the displayed view in sync with line number scaling. */
1362 if (digits != view->digits) {
1363 view->digits = digits;
1364 redraw_from = 0;
1365 }
1366 }
1368 if (redraw_from >= 0) {
1369 /* If this is an incremental update, redraw the previous line
1370 * since for commits some members could have changed when
1371 * loading the main view. */
1372 if (redraw_from > 0)
1373 redraw_from--;
1375 /* Incrementally draw avoids flickering. */
1376 redraw_view_from(view, redraw_from);
1377 }
1379 /* Update the title _after_ the redraw so that if the redraw picks up a
1380 * commit reference in view->ref it'll be available here. */
1381 update_view_title(view);
1383 if (ferror(view->pipe)) {
1384 report("Failed to read: %s", strerror(errno));
1385 goto end;
1387 } else if (feof(view->pipe)) {
1388 report("");
1389 goto end;
1390 }
1392 return TRUE;
1394 alloc_error:
1395 report("Allocation failure");
1397 end:
1398 end_update(view);
1399 return FALSE;
1400 }
1402 enum open_flags {
1403 OPEN_DEFAULT = 0, /* Use default view switching. */
1404 OPEN_SPLIT = 1, /* Split current view. */
1405 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1406 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1407 };
1409 static void
1410 open_view(struct view *prev, enum request request, enum open_flags flags)
1411 {
1412 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1413 bool split = !!(flags & OPEN_SPLIT);
1414 bool reload = !!(flags & OPEN_RELOAD);
1415 struct view *view = VIEW(request);
1416 int nviews = displayed_views();
1417 struct view *base_view = display[0];
1419 if (view == prev && nviews == 1 && !reload) {
1420 report("Already in %s view", view->name);
1421 return;
1422 }
1424 if ((reload || strcmp(view->vid, view->id)) &&
1425 !begin_update(view)) {
1426 report("Failed to load %s view", view->name);
1427 return;
1428 }
1430 if (split) {
1431 display[1] = view;
1432 if (!backgrounded)
1433 current_view = 1;
1434 } else {
1435 /* Maximize the current view. */
1436 memset(display, 0, sizeof(display));
1437 current_view = 0;
1438 display[current_view] = view;
1439 }
1441 /* Resize the view when switching between split- and full-screen,
1442 * or when switching between two different full-screen views. */
1443 if (nviews != displayed_views() ||
1444 (nviews == 1 && base_view != display[0]))
1445 resize_display();
1447 if (split && prev->lineno - prev->offset >= prev->height) {
1448 /* Take the title line into account. */
1449 int lines = prev->lineno - prev->offset - prev->height + 1;
1451 /* Scroll the view that was split if the current line is
1452 * outside the new limited view. */
1453 do_scroll_view(prev, lines, TRUE);
1454 }
1456 if (prev && view != prev) {
1457 if (split && !backgrounded) {
1458 /* "Blur" the previous view. */
1459 update_view_title(prev);
1460 }
1462 view->parent = prev;
1463 }
1465 if (view == VIEW(REQ_VIEW_HELP))
1466 load_help_page();
1468 if (view->pipe && view->lines == 0) {
1469 /* Clear the old view and let the incremental updating refill
1470 * the screen. */
1471 wclear(view->win);
1472 report("");
1473 } else {
1474 redraw_view(view);
1475 report("");
1476 }
1478 /* If the view is backgrounded the above calls to report()
1479 * won't redraw the view title. */
1480 if (backgrounded)
1481 update_view_title(view);
1482 }
1485 /*
1486 * User request switch noodle
1487 */
1489 static int
1490 view_driver(struct view *view, enum request request)
1491 {
1492 int i;
1494 switch (request) {
1495 case REQ_MOVE_UP:
1496 case REQ_MOVE_DOWN:
1497 case REQ_MOVE_PAGE_UP:
1498 case REQ_MOVE_PAGE_DOWN:
1499 case REQ_MOVE_FIRST_LINE:
1500 case REQ_MOVE_LAST_LINE:
1501 move_view(view, request, TRUE);
1502 break;
1504 case REQ_SCROLL_LINE_DOWN:
1505 case REQ_SCROLL_LINE_UP:
1506 case REQ_SCROLL_PAGE_DOWN:
1507 case REQ_SCROLL_PAGE_UP:
1508 scroll_view(view, request);
1509 break;
1511 case REQ_VIEW_MAIN:
1512 case REQ_VIEW_DIFF:
1513 case REQ_VIEW_LOG:
1514 case REQ_VIEW_HELP:
1515 case REQ_VIEW_PAGER:
1516 open_view(view, request, OPEN_DEFAULT);
1517 break;
1519 case REQ_NEXT:
1520 case REQ_PREVIOUS:
1521 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1523 if (view == VIEW(REQ_VIEW_DIFF) &&
1524 view->parent == VIEW(REQ_VIEW_MAIN)) {
1525 bool redraw = display[1] == view;
1527 view = view->parent;
1528 move_view(view, request, redraw);
1529 if (redraw)
1530 update_view_title(view);
1531 } else {
1532 move_view(view, request, TRUE);
1533 break;
1534 }
1535 /* Fall-through */
1537 case REQ_ENTER:
1538 if (!view->lines) {
1539 report("Nothing to enter");
1540 break;
1541 }
1542 return view->ops->enter(view, &view->line[view->lineno]);
1544 case REQ_VIEW_NEXT:
1545 {
1546 int nviews = displayed_views();
1547 int next_view = (current_view + 1) % nviews;
1549 if (next_view == current_view) {
1550 report("Only one view is displayed");
1551 break;
1552 }
1554 current_view = next_view;
1555 /* Blur out the title of the previous view. */
1556 update_view_title(view);
1557 report("");
1558 break;
1559 }
1560 case REQ_TOGGLE_LINENO:
1561 opt_line_number = !opt_line_number;
1562 redraw_display();
1563 break;
1565 case REQ_PROMPT:
1566 /* Always reload^Wrerun commands from the prompt. */
1567 open_view(view, opt_request, OPEN_RELOAD);
1568 break;
1570 case REQ_STOP_LOADING:
1571 for (i = 0; i < ARRAY_SIZE(views); i++) {
1572 view = &views[i];
1573 if (view->pipe)
1574 report("Stopped loading the %s view", view->name),
1575 end_update(view);
1576 }
1577 break;
1579 case REQ_SHOW_VERSION:
1580 report("%s (built %s)", VERSION, __DATE__);
1581 return TRUE;
1583 case REQ_SCREEN_RESIZE:
1584 resize_display();
1585 /* Fall-through */
1586 case REQ_SCREEN_REDRAW:
1587 redraw_display();
1588 break;
1590 case REQ_SCREEN_UPDATE:
1591 doupdate();
1592 return TRUE;
1594 case REQ_VIEW_CLOSE:
1595 /* XXX: Mark closed views by letting view->parent point to the
1596 * view itself. Parents to closed view should never be
1597 * followed. */
1598 if (view->parent &&
1599 view->parent->parent != view->parent) {
1600 memset(display, 0, sizeof(display));
1601 current_view = 0;
1602 display[current_view] = view->parent;
1603 view->parent = view;
1604 resize_display();
1605 redraw_display();
1606 break;
1607 }
1608 /* Fall-through */
1609 case REQ_QUIT:
1610 return FALSE;
1612 default:
1613 /* An unknown key will show most commonly used commands. */
1614 report("Unknown key, press 'h' for help");
1615 return TRUE;
1616 }
1618 return TRUE;
1619 }
1622 /*
1623 * Pager backend
1624 */
1626 static bool
1627 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1628 {
1629 char *text = line->data;
1630 enum line_type type = line->type;
1631 int textlen = strlen(text);
1632 int attr;
1634 wmove(view->win, lineno, 0);
1636 if (view->offset + lineno == view->lineno) {
1637 if (type == LINE_COMMIT) {
1638 string_copy(view->ref, text + 7);
1639 string_copy(ref_commit, view->ref);
1640 }
1642 type = LINE_CURSOR;
1643 wchgat(view->win, -1, 0, type, NULL);
1644 }
1646 attr = get_line_attr(type);
1647 wattrset(view->win, attr);
1649 if (opt_line_number || opt_tab_size < TABSIZE) {
1650 static char spaces[] = " ";
1651 int col_offset = 0, col = 0;
1653 if (opt_line_number) {
1654 unsigned long real_lineno = view->offset + lineno + 1;
1656 if (real_lineno == 1 ||
1657 (real_lineno % opt_num_interval) == 0) {
1658 wprintw(view->win, "%.*d", view->digits, real_lineno);
1660 } else {
1661 waddnstr(view->win, spaces,
1662 MIN(view->digits, STRING_SIZE(spaces)));
1663 }
1664 waddstr(view->win, ": ");
1665 col_offset = view->digits + 2;
1666 }
1668 while (text && col_offset + col < view->width) {
1669 int cols_max = view->width - col_offset - col;
1670 char *pos = text;
1671 int cols;
1673 if (*text == '\t') {
1674 text++;
1675 assert(sizeof(spaces) > TABSIZE);
1676 pos = spaces;
1677 cols = opt_tab_size - (col % opt_tab_size);
1679 } else {
1680 text = strchr(text, '\t');
1681 cols = line ? text - pos : strlen(pos);
1682 }
1684 waddnstr(view->win, pos, MIN(cols, cols_max));
1685 col += cols;
1686 }
1688 } else {
1689 int col = 0, pos = 0;
1691 for (; pos < textlen && col < view->width; pos++, col++)
1692 if (text[pos] == '\t')
1693 col += TABSIZE - (col % TABSIZE) - 1;
1695 waddnstr(view->win, text, pos);
1696 }
1698 return TRUE;
1699 }
1701 static bool
1702 pager_read(struct view *view, struct line *prev, char *line)
1703 {
1704 view->line[view->lines].data = strdup(line);
1705 if (!view->line[view->lines].data)
1706 return FALSE;
1708 view->line[view->lines].type = get_line_type(line);
1710 view->lines++;
1711 return TRUE;
1712 }
1714 static bool
1715 pager_enter(struct view *view, struct line *line)
1716 {
1717 int split = 0;
1719 if (line->type == LINE_COMMIT &&
1720 (view == VIEW(REQ_VIEW_LOG) ||
1721 view == VIEW(REQ_VIEW_PAGER))) {
1722 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1723 split = 1;
1724 }
1726 /* Always scroll the view even if it was split. That way
1727 * you can use Enter to scroll through the log view and
1728 * split open each commit diff. */
1729 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1731 /* FIXME: A minor workaround. Scrolling the view will call report("")
1732 * but if we are scrolling a non-current view this won't properly
1733 * update the view title. */
1734 if (split)
1735 update_view_title(view);
1737 return TRUE;
1738 }
1740 static struct view_ops pager_ops = {
1741 "line",
1742 pager_draw,
1743 pager_read,
1744 pager_enter,
1745 };
1748 /*
1749 * Main view backend
1750 */
1752 struct commit {
1753 char id[41]; /* SHA1 ID. */
1754 char title[75]; /* The first line of the commit message. */
1755 char author[75]; /* The author of the commit. */
1756 struct tm time; /* Date from the author ident. */
1757 struct ref **refs; /* Repository references; tags & branch heads. */
1758 };
1760 static bool
1761 main_draw(struct view *view, struct line *line, unsigned int lineno)
1762 {
1763 char buf[DATE_COLS + 1];
1764 struct commit *commit = line->data;
1765 enum line_type type;
1766 int col = 0;
1767 size_t timelen;
1768 size_t authorlen;
1769 int trimmed = 1;
1771 if (!*commit->author)
1772 return FALSE;
1774 wmove(view->win, lineno, col);
1776 if (view->offset + lineno == view->lineno) {
1777 string_copy(view->ref, commit->id);
1778 string_copy(ref_commit, view->ref);
1779 type = LINE_CURSOR;
1780 wattrset(view->win, get_line_attr(type));
1781 wchgat(view->win, -1, 0, type, NULL);
1783 } else {
1784 type = LINE_MAIN_COMMIT;
1785 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1786 }
1788 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1789 waddnstr(view->win, buf, timelen);
1790 waddstr(view->win, " ");
1792 col += DATE_COLS;
1793 wmove(view->win, lineno, col);
1794 if (type != LINE_CURSOR)
1795 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1797 if (opt_utf8) {
1798 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1799 } else {
1800 authorlen = strlen(commit->author);
1801 if (authorlen > AUTHOR_COLS - 2) {
1802 authorlen = AUTHOR_COLS - 2;
1803 trimmed = 1;
1804 }
1805 }
1807 if (trimmed) {
1808 waddnstr(view->win, commit->author, authorlen);
1809 if (type != LINE_CURSOR)
1810 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1811 waddch(view->win, '~');
1812 } else {
1813 waddstr(view->win, commit->author);
1814 }
1816 col += AUTHOR_COLS;
1817 if (type != LINE_CURSOR)
1818 wattrset(view->win, A_NORMAL);
1820 mvwaddch(view->win, lineno, col, ACS_LTEE);
1821 wmove(view->win, lineno, col + 2);
1822 col += 2;
1824 if (commit->refs) {
1825 size_t i = 0;
1827 do {
1828 if (type == LINE_CURSOR)
1829 ;
1830 else if (commit->refs[i]->tag)
1831 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1832 else
1833 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1834 waddstr(view->win, "[");
1835 waddstr(view->win, commit->refs[i]->name);
1836 waddstr(view->win, "]");
1837 if (type != LINE_CURSOR)
1838 wattrset(view->win, A_NORMAL);
1839 waddstr(view->win, " ");
1840 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1841 } while (commit->refs[i++]->next);
1842 }
1844 if (type != LINE_CURSOR)
1845 wattrset(view->win, get_line_attr(type));
1847 {
1848 int titlelen = strlen(commit->title);
1850 if (col + titlelen > view->width)
1851 titlelen = view->width - col;
1853 waddnstr(view->win, commit->title, titlelen);
1854 }
1856 return TRUE;
1857 }
1859 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1860 static bool
1861 main_read(struct view *view, struct line *prev, char *line)
1862 {
1863 enum line_type type = get_line_type(line);
1864 struct commit *commit;
1866 switch (type) {
1867 case LINE_COMMIT:
1868 commit = calloc(1, sizeof(struct commit));
1869 if (!commit)
1870 return FALSE;
1872 line += STRING_SIZE("commit ");
1874 view->line[view->lines++].data = commit;
1875 string_copy(commit->id, line);
1876 commit->refs = get_refs(commit->id);
1877 break;
1879 case LINE_AUTHOR:
1880 {
1881 char *ident = line + STRING_SIZE("author ");
1882 char *end = strchr(ident, '<');
1884 if (!prev)
1885 break;
1887 commit = prev->data;
1889 if (end) {
1890 for (; end > ident && isspace(end[-1]); end--) ;
1891 *end = 0;
1892 }
1894 string_copy(commit->author, ident);
1896 /* Parse epoch and timezone */
1897 if (end) {
1898 char *secs = strchr(end + 1, '>');
1899 char *zone;
1900 time_t time;
1902 if (!secs || secs[1] != ' ')
1903 break;
1905 secs += 2;
1906 time = (time_t) atol(secs);
1907 zone = strchr(secs, ' ');
1908 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1909 long tz;
1911 zone++;
1912 tz = ('0' - zone[1]) * 60 * 60 * 10;
1913 tz += ('0' - zone[2]) * 60 * 60;
1914 tz += ('0' - zone[3]) * 60;
1915 tz += ('0' - zone[4]) * 60;
1917 if (zone[0] == '-')
1918 tz = -tz;
1920 time -= tz;
1921 }
1922 gmtime_r(&time, &commit->time);
1923 }
1924 break;
1925 }
1926 default:
1927 if (!prev)
1928 break;
1930 commit = prev->data;
1932 /* Fill in the commit title if it has not already been set. */
1933 if (commit->title[0])
1934 break;
1936 /* Require titles to start with a non-space character at the
1937 * offset used by git log. */
1938 /* FIXME: More gracefull handling of titles; append "..." to
1939 * shortened titles, etc. */
1940 if (strncmp(line, " ", 4) ||
1941 isspace(line[4]))
1942 break;
1944 string_copy(commit->title, line + 4);
1945 }
1947 return TRUE;
1948 }
1950 static bool
1951 main_enter(struct view *view, struct line *line)
1952 {
1953 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
1955 open_view(view, REQ_VIEW_DIFF, flags);
1956 return TRUE;
1957 }
1959 static struct view_ops main_ops = {
1960 "commit",
1961 main_draw,
1962 main_read,
1963 main_enter,
1964 };
1967 /*
1968 * Keys
1969 */
1971 struct keymap {
1972 int alias;
1973 int request;
1974 };
1976 static struct keymap keymap[] = {
1977 /* View switching */
1978 { 'm', REQ_VIEW_MAIN },
1979 { 'd', REQ_VIEW_DIFF },
1980 { 'l', REQ_VIEW_LOG },
1981 { 'p', REQ_VIEW_PAGER },
1982 { 'h', REQ_VIEW_HELP },
1983 { '?', REQ_VIEW_HELP },
1985 /* View manipulation */
1986 { 'q', REQ_VIEW_CLOSE },
1987 { KEY_TAB, REQ_VIEW_NEXT },
1988 { KEY_RETURN, REQ_ENTER },
1989 { KEY_UP, REQ_PREVIOUS },
1990 { KEY_DOWN, REQ_NEXT },
1992 /* Cursor navigation */
1993 { 'k', REQ_MOVE_UP },
1994 { 'j', REQ_MOVE_DOWN },
1995 { KEY_HOME, REQ_MOVE_FIRST_LINE },
1996 { KEY_END, REQ_MOVE_LAST_LINE },
1997 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
1998 { ' ', REQ_MOVE_PAGE_DOWN },
1999 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
2000 { 'b', REQ_MOVE_PAGE_UP },
2001 { '-', REQ_MOVE_PAGE_UP },
2003 /* Scrolling */
2004 { KEY_IC, REQ_SCROLL_LINE_UP },
2005 { KEY_DC, REQ_SCROLL_LINE_DOWN },
2006 { 'w', REQ_SCROLL_PAGE_UP },
2007 { 's', REQ_SCROLL_PAGE_DOWN },
2009 /* Misc */
2010 { 'Q', REQ_QUIT },
2011 { 'z', REQ_STOP_LOADING },
2012 { 'v', REQ_SHOW_VERSION },
2013 { 'r', REQ_SCREEN_REDRAW },
2014 { 'n', REQ_TOGGLE_LINENO },
2015 { ':', REQ_PROMPT },
2017 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2018 { ERR, REQ_SCREEN_UPDATE },
2020 /* Use the ncurses SIGWINCH handler. */
2021 { KEY_RESIZE, REQ_SCREEN_RESIZE },
2022 };
2024 static enum request
2025 get_request(int key)
2026 {
2027 int i;
2029 for (i = 0; i < ARRAY_SIZE(keymap); i++)
2030 if (keymap[i].alias == key)
2031 return keymap[i].request;
2033 return (enum request) key;
2034 }
2036 struct key {
2037 char *name;
2038 int value;
2039 };
2041 static struct key key_table[] = {
2042 { "Enter", KEY_RETURN },
2043 { "Space", ' ' },
2044 { "Backspace", KEY_BACKSPACE },
2045 { "Tab", KEY_TAB },
2046 { "Escape", KEY_ESC },
2047 { "Left", KEY_LEFT },
2048 { "Right", KEY_RIGHT },
2049 { "Up", KEY_UP },
2050 { "Down", KEY_DOWN },
2051 { "Insert", KEY_IC },
2052 { "Delete", KEY_DC },
2053 { "Home", KEY_HOME },
2054 { "End", KEY_END },
2055 { "PageUp", KEY_PPAGE },
2056 { "PageDown", KEY_NPAGE },
2057 { "F1", KEY_F(1) },
2058 { "F2", KEY_F(2) },
2059 { "F3", KEY_F(3) },
2060 { "F4", KEY_F(4) },
2061 { "F5", KEY_F(5) },
2062 { "F6", KEY_F(6) },
2063 { "F7", KEY_F(7) },
2064 { "F8", KEY_F(8) },
2065 { "F9", KEY_F(9) },
2066 { "F10", KEY_F(10) },
2067 { "F11", KEY_F(11) },
2068 { "F12", KEY_F(12) },
2069 };
2071 static char *
2072 get_key(enum request request)
2073 {
2074 static char buf[BUFSIZ];
2075 static char key_char[] = "'X'";
2076 int pos = 0;
2077 char *sep = " ";
2078 int i;
2080 buf[pos] = 0;
2082 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2083 char *seq = NULL;
2084 int key;
2086 if (keymap[i].request != request)
2087 continue;
2089 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2090 if (key_table[key].value == keymap[i].alias)
2091 seq = key_table[key].name;
2093 if (seq == NULL &&
2094 keymap[i].alias < 127 &&
2095 isprint(keymap[i].alias)) {
2096 key_char[1] = (char) keymap[i].alias;
2097 seq = key_char;
2098 }
2100 if (!seq)
2101 seq = "'?'";
2103 pos += snprintf(buf + pos, sizeof(buf) - pos, "%s%s", sep, seq);
2104 if (pos >= sizeof(buf))
2105 return "Too many keybindings!";
2106 sep = ", ";
2107 }
2109 return buf;
2110 }
2112 static void load_help_page(void)
2113 {
2114 char buf[BUFSIZ];
2115 struct view *view = VIEW(REQ_VIEW_HELP);
2116 int lines = ARRAY_SIZE(req_info) + 2;
2117 int i;
2119 if (view->lines > 0)
2120 return;
2122 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2123 if (!req_info[i].request)
2124 lines++;
2126 view->line = calloc(lines, sizeof(*view->line));
2127 if (!view->line) {
2128 report("Allocation failure");
2129 return;
2130 }
2132 pager_read(view, NULL, "Quick reference for tig keybindings:");
2134 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2135 char *key;
2137 if (!req_info[i].request) {
2138 pager_read(view, NULL, "");
2139 pager_read(view, NULL, req_info[i].help);
2140 continue;
2141 }
2143 key = get_key(req_info[i].request);
2144 if (snprintf(buf, sizeof(buf), "%-25s %s", key, req_info[i].help)
2145 >= sizeof(buf))
2146 continue;
2148 pager_read(view, NULL, buf);
2149 }
2150 }
2153 /*
2154 * Unicode / UTF-8 handling
2155 *
2156 * NOTE: Much of the following code for dealing with unicode is derived from
2157 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2158 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2159 */
2161 /* I've (over)annotated a lot of code snippets because I am not entirely
2162 * confident that the approach taken by this small UTF-8 interface is correct.
2163 * --jonas */
2165 static inline int
2166 unicode_width(unsigned long c)
2167 {
2168 if (c >= 0x1100 &&
2169 (c <= 0x115f /* Hangul Jamo */
2170 || c == 0x2329
2171 || c == 0x232a
2172 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2173 /* CJK ... Yi */
2174 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2175 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2176 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2177 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2178 || (c >= 0xffe0 && c <= 0xffe6)
2179 || (c >= 0x20000 && c <= 0x2fffd)
2180 || (c >= 0x30000 && c <= 0x3fffd)))
2181 return 2;
2183 return 1;
2184 }
2186 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2187 * Illegal bytes are set one. */
2188 static const unsigned char utf8_bytes[256] = {
2189 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,
2190 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,
2191 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,
2192 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,
2193 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,
2194 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,
2195 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,
2196 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,
2197 };
2199 /* Decode UTF-8 multi-byte representation into a unicode character. */
2200 static inline unsigned long
2201 utf8_to_unicode(const char *string, size_t length)
2202 {
2203 unsigned long unicode;
2205 switch (length) {
2206 case 1:
2207 unicode = string[0];
2208 break;
2209 case 2:
2210 unicode = (string[0] & 0x1f) << 6;
2211 unicode += (string[1] & 0x3f);
2212 break;
2213 case 3:
2214 unicode = (string[0] & 0x0f) << 12;
2215 unicode += ((string[1] & 0x3f) << 6);
2216 unicode += (string[2] & 0x3f);
2217 break;
2218 case 4:
2219 unicode = (string[0] & 0x0f) << 18;
2220 unicode += ((string[1] & 0x3f) << 12);
2221 unicode += ((string[2] & 0x3f) << 6);
2222 unicode += (string[3] & 0x3f);
2223 break;
2224 case 5:
2225 unicode = (string[0] & 0x0f) << 24;
2226 unicode += ((string[1] & 0x3f) << 18);
2227 unicode += ((string[2] & 0x3f) << 12);
2228 unicode += ((string[3] & 0x3f) << 6);
2229 unicode += (string[4] & 0x3f);
2230 break;
2231 case 6:
2232 unicode = (string[0] & 0x01) << 30;
2233 unicode += ((string[1] & 0x3f) << 24);
2234 unicode += ((string[2] & 0x3f) << 18);
2235 unicode += ((string[3] & 0x3f) << 12);
2236 unicode += ((string[4] & 0x3f) << 6);
2237 unicode += (string[5] & 0x3f);
2238 break;
2239 default:
2240 die("Invalid unicode length");
2241 }
2243 /* Invalid characters could return the special 0xfffd value but NUL
2244 * should be just as good. */
2245 return unicode > 0xffff ? 0 : unicode;
2246 }
2248 /* Calculates how much of string can be shown within the given maximum width
2249 * and sets trimmed parameter to non-zero value if all of string could not be
2250 * shown.
2251 *
2252 * Additionally, adds to coloffset how many many columns to move to align with
2253 * the expected position. Takes into account how multi-byte and double-width
2254 * characters will effect the cursor position.
2255 *
2256 * Returns the number of bytes to output from string to satisfy max_width. */
2257 static size_t
2258 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2259 {
2260 const char *start = string;
2261 const char *end = strchr(string, '\0');
2262 size_t mbwidth = 0;
2263 size_t width = 0;
2265 *trimmed = 0;
2267 while (string < end) {
2268 int c = *(unsigned char *) string;
2269 unsigned char bytes = utf8_bytes[c];
2270 size_t ucwidth;
2271 unsigned long unicode;
2273 if (string + bytes > end)
2274 break;
2276 /* Change representation to figure out whether
2277 * it is a single- or double-width character. */
2279 unicode = utf8_to_unicode(string, bytes);
2280 /* FIXME: Graceful handling of invalid unicode character. */
2281 if (!unicode)
2282 break;
2284 ucwidth = unicode_width(unicode);
2285 width += ucwidth;
2286 if (width > max_width) {
2287 *trimmed = 1;
2288 break;
2289 }
2291 /* The column offset collects the differences between the
2292 * number of bytes encoding a character and the number of
2293 * columns will be used for rendering said character.
2294 *
2295 * So if some character A is encoded in 2 bytes, but will be
2296 * represented on the screen using only 1 byte this will and up
2297 * adding 1 to the multi-byte column offset.
2298 *
2299 * Assumes that no double-width character can be encoding in
2300 * less than two bytes. */
2301 if (bytes > ucwidth)
2302 mbwidth += bytes - ucwidth;
2304 string += bytes;
2305 }
2307 *coloffset += mbwidth;
2309 return string - start;
2310 }
2313 /*
2314 * Status management
2315 */
2317 /* Whether or not the curses interface has been initialized. */
2318 static bool cursed = FALSE;
2320 /* The status window is used for polling keystrokes. */
2321 static WINDOW *status_win;
2323 /* Update status and title window. */
2324 static void
2325 report(const char *msg, ...)
2326 {
2327 static bool empty = TRUE;
2328 struct view *view = display[current_view];
2330 if (!empty || *msg) {
2331 va_list args;
2333 va_start(args, msg);
2335 werase(status_win);
2336 wmove(status_win, 0, 0);
2337 if (*msg) {
2338 vwprintw(status_win, msg, args);
2339 empty = FALSE;
2340 } else {
2341 empty = TRUE;
2342 }
2343 wrefresh(status_win);
2345 va_end(args);
2346 }
2348 update_view_title(view);
2349 update_display_cursor();
2350 }
2352 /* Controls when nodelay should be in effect when polling user input. */
2353 static void
2354 set_nonblocking_input(bool loading)
2355 {
2356 static unsigned int loading_views;
2358 if ((loading == FALSE && loading_views-- == 1) ||
2359 (loading == TRUE && loading_views++ == 0))
2360 nodelay(status_win, loading);
2361 }
2363 static void
2364 init_display(void)
2365 {
2366 int x, y;
2368 /* Initialize the curses library */
2369 if (isatty(STDIN_FILENO)) {
2370 cursed = !!initscr();
2371 } else {
2372 /* Leave stdin and stdout alone when acting as a pager. */
2373 FILE *io = fopen("/dev/tty", "r+");
2375 cursed = !!newterm(NULL, io, io);
2376 }
2378 if (!cursed)
2379 die("Failed to initialize curses");
2381 nonl(); /* Tell curses not to do NL->CR/NL on output */
2382 cbreak(); /* Take input chars one at a time, no wait for \n */
2383 noecho(); /* Don't echo input */
2384 leaveok(stdscr, TRUE);
2386 if (has_colors())
2387 init_colors();
2389 getmaxyx(stdscr, y, x);
2390 status_win = newwin(1, 0, y - 1, 0);
2391 if (!status_win)
2392 die("Failed to create status window");
2394 /* Enable keyboard mapping */
2395 keypad(status_win, TRUE);
2396 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2397 }
2400 /*
2401 * Repository references
2402 */
2404 static struct ref *refs;
2405 static size_t refs_size;
2407 /* Id <-> ref store */
2408 static struct ref ***id_refs;
2409 static size_t id_refs_size;
2411 static struct ref **
2412 get_refs(char *id)
2413 {
2414 struct ref ***tmp_id_refs;
2415 struct ref **ref_list = NULL;
2416 size_t ref_list_size = 0;
2417 size_t i;
2419 for (i = 0; i < id_refs_size; i++)
2420 if (!strcmp(id, id_refs[i][0]->id))
2421 return id_refs[i];
2423 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2424 if (!tmp_id_refs)
2425 return NULL;
2427 id_refs = tmp_id_refs;
2429 for (i = 0; i < refs_size; i++) {
2430 struct ref **tmp;
2432 if (strcmp(id, refs[i].id))
2433 continue;
2435 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2436 if (!tmp) {
2437 if (ref_list)
2438 free(ref_list);
2439 return NULL;
2440 }
2442 ref_list = tmp;
2443 if (ref_list_size > 0)
2444 ref_list[ref_list_size - 1]->next = 1;
2445 ref_list[ref_list_size] = &refs[i];
2447 /* XXX: The properties of the commit chains ensures that we can
2448 * safely modify the shared ref. The repo references will
2449 * always be similar for the same id. */
2450 ref_list[ref_list_size]->next = 0;
2451 ref_list_size++;
2452 }
2454 if (ref_list)
2455 id_refs[id_refs_size++] = ref_list;
2457 return ref_list;
2458 }
2460 static int
2461 read_ref(char *id, int idlen, char *name, int namelen)
2462 {
2463 struct ref *ref;
2464 bool tag = FALSE;
2465 bool tag_commit = FALSE;
2467 /* Commits referenced by tags has "^{}" appended. */
2468 if (name[namelen - 1] == '}') {
2469 while (namelen > 0 && name[namelen] != '^')
2470 namelen--;
2471 if (namelen > 0)
2472 tag_commit = TRUE;
2473 name[namelen] = 0;
2474 }
2476 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2477 if (!tag_commit)
2478 return OK;
2479 name += STRING_SIZE("refs/tags/");
2480 tag = TRUE;
2482 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2483 name += STRING_SIZE("refs/heads/");
2485 } else if (!strcmp(name, "HEAD")) {
2486 return OK;
2487 }
2489 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2490 if (!refs)
2491 return ERR;
2493 ref = &refs[refs_size++];
2494 ref->name = strdup(name);
2495 if (!ref->name)
2496 return ERR;
2498 ref->tag = tag;
2499 string_copy(ref->id, id);
2501 return OK;
2502 }
2504 static int
2505 load_refs(void)
2506 {
2507 const char *cmd_env = getenv("TIG_LS_REMOTE");
2508 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2510 return read_properties(popen(cmd, "r"), "\t", read_ref);
2511 }
2513 static int
2514 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2515 {
2516 if (!strcmp(name, "i18n.commitencoding")) {
2517 string_copy(opt_encoding, value);
2518 }
2520 return OK;
2521 }
2523 static int
2524 load_repo_config(void)
2525 {
2526 return read_properties(popen("git repo-config --list", "r"),
2527 "=", read_repo_config_option);
2528 }
2530 static int
2531 read_properties(FILE *pipe, const char *separators,
2532 int (*read_property)(char *, int, char *, int))
2533 {
2534 char buffer[BUFSIZ];
2535 char *name;
2536 int state = OK;
2538 if (!pipe)
2539 return ERR;
2541 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2542 char *value;
2543 size_t namelen;
2544 size_t valuelen;
2546 name = chomp_string(name);
2547 namelen = strcspn(name, separators);
2549 if (name[namelen]) {
2550 name[namelen] = 0;
2551 value = chomp_string(name + namelen + 1);
2552 valuelen = strlen(value);
2554 } else {
2555 value = "";
2556 valuelen = 0;
2557 }
2559 state = read_property(name, namelen, value, valuelen);
2560 }
2562 if (state != ERR && ferror(pipe))
2563 state = ERR;
2565 pclose(pipe);
2567 return state;
2568 }
2571 /*
2572 * Main
2573 */
2575 #if __GNUC__ >= 3
2576 #define __NORETURN __attribute__((__noreturn__))
2577 #else
2578 #define __NORETURN
2579 #endif
2581 static void __NORETURN
2582 quit(int sig)
2583 {
2584 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2585 if (cursed)
2586 endwin();
2587 exit(0);
2588 }
2590 static void __NORETURN
2591 die(const char *err, ...)
2592 {
2593 va_list args;
2595 endwin();
2597 va_start(args, err);
2598 fputs("tig: ", stderr);
2599 vfprintf(stderr, err, args);
2600 fputs("\n", stderr);
2601 va_end(args);
2603 exit(1);
2604 }
2606 int
2607 main(int argc, char *argv[])
2608 {
2609 struct view *view;
2610 enum request request;
2611 size_t i;
2613 signal(SIGINT, quit);
2615 if (load_options() == ERR)
2616 die("Failed to load user config.");
2618 /* Load the repo config file so options can be overwritten from
2619 * the command line. */
2620 if (load_repo_config() == ERR)
2621 die("Failed to load repo config.");
2623 if (!parse_options(argc, argv))
2624 return 0;
2626 if (load_refs() == ERR)
2627 die("Failed to load refs.");
2629 /* Require a git repository unless when running in pager mode. */
2630 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2631 die("Not a git repository");
2633 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2634 view->cmd_env = getenv(view->cmd_env);
2636 request = opt_request;
2638 init_display();
2640 while (view_driver(display[current_view], request)) {
2641 int key;
2642 int i;
2644 foreach_view (view, i)
2645 update_view(view);
2647 /* Refresh, accept single keystroke of input */
2648 key = wgetch(status_win);
2649 request = get_request(key);
2651 /* Some low-level request handling. This keeps access to
2652 * status_win restricted. */
2653 switch (request) {
2654 case REQ_PROMPT:
2655 report(":");
2656 /* Temporarily switch to line-oriented and echoed
2657 * input. */
2658 nocbreak();
2659 echo();
2661 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2662 memcpy(opt_cmd, "git ", 4);
2663 opt_request = REQ_VIEW_PAGER;
2664 } else {
2665 report("Prompt interrupted by loading view, "
2666 "press 'z' to stop loading views");
2667 request = REQ_SCREEN_UPDATE;
2668 }
2670 noecho();
2671 cbreak();
2672 break;
2674 case REQ_SCREEN_RESIZE:
2675 {
2676 int height, width;
2678 getmaxyx(stdscr, height, width);
2680 /* Resize the status view and let the view driver take
2681 * care of resizing the displayed views. */
2682 wresize(status_win, 1, width);
2683 mvwin(status_win, height - 1, 0);
2684 wrefresh(status_win);
2685 break;
2686 }
2687 default:
2688 break;
2689 }
2690 }
2692 quit(0);
2694 return 0;
2695 }
2697 /**
2698 * include::BUGS[]
2699 *
2700 * COPYRIGHT
2701 * ---------
2702 * Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2703 *
2704 * This program is free software; you can redistribute it and/or modify
2705 * it under the terms of the GNU General Public License as published by
2706 * the Free Software Foundation; either version 2 of the License, or
2707 * (at your option) any later version.
2708 *
2709 * SEE ALSO
2710 * --------
2711 * - link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
2712 * - link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
2713 *
2714 * Other git repository browsers:
2715 *
2716 * - gitk(1)
2717 * - qgit(1)
2718 * - gitview(1)
2719 *
2720 * Sites:
2721 *
2722 * include::SITES[]
2723 **/