Code

Add support for showing tags and other repo refs in the diff and log view
[tig.git] / tig.c
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)
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;
136 /*
137  * String helpers
138  */
140 static inline void
141 string_ncopy(char *dst, const char *src, int dstlen)
143         strncpy(dst, src, dstlen - 1);
144         dst[dstlen - 1] = 0;
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)
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;
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)
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;
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[])
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;
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(PP_REFS,      "Refs: ",            COLOR_RED,      COLOR_DEFAULT,  0), \
586 LINE(COMMIT,       "commit ",           COLOR_GREEN,    COLOR_DEFAULT,  0), \
587 LINE(PARENT,       "parent ",           COLOR_BLUE,     COLOR_DEFAULT,  0), \
588 LINE(TREE,         "tree ",             COLOR_BLUE,     COLOR_DEFAULT,  0), \
589 LINE(AUTHOR,       "author ",           COLOR_CYAN,     COLOR_DEFAULT,  0), \
590 LINE(COMMITTER,    "committer ",        COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
591 LINE(SIGNOFF,      "    Signed-off-by", COLOR_YELLOW,   COLOR_DEFAULT,  0), \
592 LINE(DEFAULT,      "",                  COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL), \
593 LINE(CURSOR,       "",                  COLOR_WHITE,    COLOR_GREEN,    A_BOLD), \
594 LINE(STATUS,       "",                  COLOR_GREEN,    COLOR_DEFAULT,  0), \
595 LINE(TITLE_BLUR,   "",                  COLOR_WHITE,    COLOR_BLUE,     0), \
596 LINE(TITLE_FOCUS,  "",                  COLOR_WHITE,    COLOR_BLUE,     A_BOLD), \
597 LINE(MAIN_DATE,    "",                  COLOR_BLUE,     COLOR_DEFAULT,  0), \
598 LINE(MAIN_AUTHOR,  "",                  COLOR_GREEN,    COLOR_DEFAULT,  0), \
599 LINE(MAIN_COMMIT,  "",                  COLOR_DEFAULT,  COLOR_DEFAULT,  0), \
600 LINE(MAIN_DELIM,   "",                  COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
601 LINE(MAIN_TAG,     "",                  COLOR_MAGENTA,  COLOR_DEFAULT,  A_BOLD), \
602 LINE(MAIN_REF,     "",                  COLOR_CYAN,     COLOR_DEFAULT,  A_BOLD), \
605 /*
606  * Line-oriented content detection.
607  */
609 enum line_type {
610 #define LINE(type, line, fg, bg, attr) \
611         LINE_##type
612         LINE_INFO
613 #undef  LINE
614 };
616 struct line_info {
617         const char *name;       /* Option name. */
618         int namelen;            /* Size of option name. */
619         const char *line;       /* The start of line to match. */
620         int linelen;            /* Size of string to match. */
621         int fg, bg, attr;       /* Color and text attributes for the lines. */
622 };
624 static struct line_info line_info[] = {
625 #define LINE(type, line, fg, bg, attr) \
626         { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
627         LINE_INFO
628 #undef  LINE
629 };
631 static enum line_type
632 get_line_type(char *line)
634         int linelen = strlen(line);
635         enum line_type type;
637         for (type = 0; type < ARRAY_SIZE(line_info); type++)
638                 /* Case insensitive search matches Signed-off-by lines better. */
639                 if (linelen >= line_info[type].linelen &&
640                     !strncasecmp(line_info[type].line, line, line_info[type].linelen))
641                         return type;
643         return LINE_DEFAULT;
646 static inline int
647 get_line_attr(enum line_type type)
649         assert(type < ARRAY_SIZE(line_info));
650         return COLOR_PAIR(type) | line_info[type].attr;
653 static struct line_info *
654 get_line_info(char *name, int namelen)
656         enum line_type type;
657         int i;
659         /* Diff-Header -> DIFF_HEADER */
660         for (i = 0; i < namelen; i++) {
661                 if (name[i] == '-')
662                         name[i] = '_';
663                 else if (name[i] == '.')
664                         name[i] = '_';
665         }
667         for (type = 0; type < ARRAY_SIZE(line_info); type++)
668                 if (namelen == line_info[type].namelen &&
669                     !strncasecmp(line_info[type].name, name, namelen))
670                         return &line_info[type];
672         return NULL;
675 static void
676 init_colors(void)
678         int default_bg = COLOR_BLACK;
679         int default_fg = COLOR_WHITE;
680         enum line_type type;
682         start_color();
684         if (use_default_colors() != ERR) {
685                 default_bg = -1;
686                 default_fg = -1;
687         }
689         for (type = 0; type < ARRAY_SIZE(line_info); type++) {
690                 struct line_info *info = &line_info[type];
691                 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
692                 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
694                 init_pair(type, fg, bg);
695         }
698 struct line {
699         enum line_type type;
700         void *data;             /* User data */
701 };
704 /*
705  * User config file handling.
706  */
708 #define set_color(color, name, namelen) \
709         set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
711 #define set_attribute(attr, name, namelen) \
712         set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
714 static int   config_lineno;
715 static bool  config_errors;
716 static char *config_msg;
718 static int
719 set_option(char *opt, int optlen, char *value, int valuelen)
721         /* Reads: "color" object fgcolor bgcolor [attr] */
722         if (!strcmp(opt, "color")) {
723                 struct line_info *info;
725                 value = chomp_string(value);
726                 valuelen = strcspn(value, " \t");
727                 info = get_line_info(value, valuelen);
728                 if (!info) {
729                         config_msg = "Unknown color name";
730                         return ERR;
731                 }
733                 value = chomp_string(value + valuelen);
734                 valuelen = strcspn(value, " \t");
735                 if (set_color(&info->fg, value, valuelen) == ERR) {
736                         config_msg = "Unknown color";
737                         return ERR;
738                 }
740                 value = chomp_string(value + valuelen);
741                 valuelen = strcspn(value, " \t");
742                 if (set_color(&info->bg, value, valuelen) == ERR) {
743                         config_msg = "Unknown color";
744                         return ERR;
745                 }
747                 value = chomp_string(value + valuelen);
748                 if (*value &&
749                     set_attribute(&info->attr, value, strlen(value)) == ERR) {
750                         config_msg = "Unknown attribute";
751                         return ERR;
752                 }
754                 return OK;
755         }
757         return ERR;
760 static int
761 read_option(char *opt, int optlen, char *value, int valuelen)
763         config_lineno++;
764         config_msg = "Internal error";
766         optlen = strcspn(opt, "#;");
767         if (optlen == 0) {
768                 /* The whole line is a commend or empty. */
769                 return OK;
771         } else if (opt[optlen] != 0) {
772                 /* Part of the option name is a comment, so the value part
773                  * should be ignored. */
774                 valuelen = 0;
775                 opt[optlen] = value[valuelen] = 0;
776         } else {
777                 /* Else look for comment endings in the value. */
778                 valuelen = strcspn(value, "#;");
779                 value[valuelen] = 0;
780         }
782         if (set_option(opt, optlen, value, valuelen) == ERR) {
783                 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
784                         config_lineno, optlen, opt, config_msg);
785                 config_errors = TRUE;
786         }
788         /* Always keep going if errors are encountered. */
789         return OK;
792 static int
793 load_options(void)
795         char *home = getenv("HOME");
796         char buf[1024];
797         FILE *file;
799         config_lineno = 0;
800         config_errors = FALSE;
802         if (!home ||
803             snprintf(buf, sizeof(buf), "%s/.tigrc", home) >= sizeof(buf))
804                 return ERR;
806         /* It's ok that the file doesn't exist. */
807         file = fopen(buf, "r");
808         if (!file)
809                 return OK;
811         if (read_properties(file, " \t", read_option) == ERR ||
812             config_errors == TRUE)
813                 fprintf(stderr, "Errors while loading %s.\n", buf);
815         return OK;
819 /*
820  * The viewer
821  */
823 struct view;
824 struct view_ops;
826 /* The display array of active views and the index of the current view. */
827 static struct view *display[2];
828 static unsigned int current_view;
830 #define foreach_view(view, i) \
831         for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
833 #define displayed_views()       (display[1] != NULL ? 2 : 1)
835 /* Current head and commit ID */
836 static char ref_commit[SIZEOF_REF]      = "HEAD";
837 static char ref_head[SIZEOF_REF]        = "HEAD";
839 struct view {
840         const char *name;       /* View name */
841         const char *cmd_fmt;    /* Default command line format */
842         const char *cmd_env;    /* Command line set via environment */
843         const char *id;         /* Points to either of ref_{head,commit} */
845         struct view_ops *ops;   /* View operations */
847         char cmd[SIZEOF_CMD];   /* Command buffer */
848         char ref[SIZEOF_REF];   /* Hovered commit reference */
849         char vid[SIZEOF_REF];   /* View ID. Set to id member when updating. */
851         int height, width;      /* The width and height of the main window */
852         WINDOW *win;            /* The main window */
853         WINDOW *title;          /* The title window living below the main window */
855         /* Navigation */
856         unsigned long offset;   /* Offset of the window top */
857         unsigned long lineno;   /* Current line number */
859         /* If non-NULL, points to the view that opened this view. If this view
860          * is closed tig will switch back to the parent view. */
861         struct view *parent;
863         /* Buffering */
864         unsigned long lines;    /* Total number of lines */
865         struct line *line;      /* Line index */
866         unsigned long line_size;/* Total number of allocated lines */
867         unsigned int digits;    /* Number of digits in the lines member. */
869         /* Loading */
870         FILE *pipe;
871         time_t start_time;
872 };
874 struct view_ops {
875         /* What type of content being displayed. Used in the title bar. */
876         const char *type;
877         /* Draw one line; @lineno must be < view->height. */
878         bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
879         /* Read one line; updates view->line. */
880         bool (*read)(struct view *view, struct line *prev, char *data);
881         /* Depending on view, change display based on current line. */
882         bool (*enter)(struct view *view, struct line *line);
883 };
885 static struct view_ops pager_ops;
886 static struct view_ops main_ops;
888 #define VIEW_STR(name, cmd, env, ref, ops) \
889         { name, cmd, #env, ref, ops }
891 #define VIEW_(id, name, ops, ref) \
892         VIEW_STR(name, TIG_##id##_CMD,  TIG_##id##_CMD, ref, ops)
895 static struct view views[] = {
896         VIEW_(MAIN,  "main",  &main_ops,  ref_head),
897         VIEW_(DIFF,  "diff",  &pager_ops, ref_commit),
898         VIEW_(LOG,   "log",   &pager_ops, ref_head),
899         VIEW_(HELP,  "help",  &pager_ops, "static"),
900         VIEW_(PAGER, "pager", &pager_ops, "static"),
901 };
903 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
906 static bool
907 draw_view_line(struct view *view, unsigned int lineno)
909         if (view->offset + lineno >= view->lines)
910                 return FALSE;
912         return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
915 static void
916 redraw_view_from(struct view *view, int lineno)
918         assert(0 <= lineno && lineno < view->height);
920         for (; lineno < view->height; lineno++) {
921                 if (!draw_view_line(view, lineno))
922                         break;
923         }
925         redrawwin(view->win);
926         wrefresh(view->win);
929 static void
930 redraw_view(struct view *view)
932         wclear(view->win);
933         redraw_view_from(view, 0);
937 static void
938 update_view_title(struct view *view)
940         if (view == display[current_view])
941                 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
942         else
943                 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
945         werase(view->title);
946         wmove(view->title, 0, 0);
948         if (*view->ref)
949                 wprintw(view->title, "[%s] %s", view->name, view->ref);
950         else
951                 wprintw(view->title, "[%s]", view->name);
953         if (view->lines || view->pipe) {
954                 unsigned int lines = view->lines
955                                    ? (view->lineno + 1) * 100 / view->lines
956                                    : 0;
958                 wprintw(view->title, " - %s %d of %d (%d%%)",
959                         view->ops->type,
960                         view->lineno + 1,
961                         view->lines,
962                         lines);
963         }
965         if (view->pipe) {
966                 time_t secs = time(NULL) - view->start_time;
968                 /* Three git seconds are a long time ... */
969                 if (secs > 2)
970                         wprintw(view->title, " %lds", secs);
971         }
973         wmove(view->title, 0, view->width - 1);
974         wrefresh(view->title);
977 static void
978 resize_display(void)
980         int offset, i;
981         struct view *base = display[0];
982         struct view *view = display[1] ? display[1] : display[0];
984         /* Setup window dimensions */
986         getmaxyx(stdscr, base->height, base->width);
988         /* Make room for the status window. */
989         base->height -= 1;
991         if (view != base) {
992                 /* Horizontal split. */
993                 view->width   = base->width;
994                 view->height  = SCALE_SPLIT_VIEW(base->height);
995                 base->height -= view->height;
997                 /* Make room for the title bar. */
998                 view->height -= 1;
999         }
1001         /* Make room for the title bar. */
1002         base->height -= 1;
1004         offset = 0;
1006         foreach_view (view, i) {
1007                 if (!view->win) {
1008                         view->win = newwin(view->height, 0, offset, 0);
1009                         if (!view->win)
1010                                 die("Failed to create %s view", view->name);
1012                         scrollok(view->win, TRUE);
1014                         view->title = newwin(1, 0, offset + view->height, 0);
1015                         if (!view->title)
1016                                 die("Failed to create title window");
1018                 } else {
1019                         wresize(view->win, view->height, view->width);
1020                         mvwin(view->win,   offset, 0);
1021                         mvwin(view->title, offset + view->height, 0);
1022                 }
1024                 offset += view->height + 1;
1025         }
1028 static void
1029 redraw_display(void)
1031         struct view *view;
1032         int i;
1034         foreach_view (view, i) {
1035                 redraw_view(view);
1036                 update_view_title(view);
1037         }
1040 static void
1041 update_display_cursor(void)
1043         struct view *view = display[current_view];
1045         /* Move the cursor to the right-most column of the cursor line.
1046          *
1047          * XXX: This could turn out to be a bit expensive, but it ensures that
1048          * the cursor does not jump around. */
1049         if (view->lines) {
1050                 wmove(view->win, view->lineno - view->offset, view->width - 1);
1051                 wrefresh(view->win);
1052         }
1055 /*
1056  * Navigation
1057  */
1059 /* Scrolling backend */
1060 static void
1061 do_scroll_view(struct view *view, int lines, bool redraw)
1063         /* The rendering expects the new offset. */
1064         view->offset += lines;
1066         assert(0 <= view->offset && view->offset < view->lines);
1067         assert(lines);
1069         /* Redraw the whole screen if scrolling is pointless. */
1070         if (view->height < ABS(lines)) {
1071                 redraw_view(view);
1073         } else {
1074                 int line = lines > 0 ? view->height - lines : 0;
1075                 int end = line + ABS(lines);
1077                 wscrl(view->win, lines);
1079                 for (; line < end; line++) {
1080                         if (!draw_view_line(view, line))
1081                                 break;
1082                 }
1083         }
1085         /* Move current line into the view. */
1086         if (view->lineno < view->offset) {
1087                 view->lineno = view->offset;
1088                 draw_view_line(view, 0);
1090         } else if (view->lineno >= view->offset + view->height) {
1091                 if (view->lineno == view->offset + view->height) {
1092                         /* Clear the hidden line so it doesn't show if the view
1093                          * is scrolled up. */
1094                         wmove(view->win, view->height, 0);
1095                         wclrtoeol(view->win);
1096                 }
1097                 view->lineno = view->offset + view->height - 1;
1098                 draw_view_line(view, view->lineno - view->offset);
1099         }
1101         assert(view->offset <= view->lineno && view->lineno < view->lines);
1103         if (!redraw)
1104                 return;
1106         redrawwin(view->win);
1107         wrefresh(view->win);
1108         report("");
1111 /* Scroll frontend */
1112 static void
1113 scroll_view(struct view *view, enum request request)
1115         int lines = 1;
1117         switch (request) {
1118         case REQ_SCROLL_PAGE_DOWN:
1119                 lines = view->height;
1120         case REQ_SCROLL_LINE_DOWN:
1121                 if (view->offset + lines > view->lines)
1122                         lines = view->lines - view->offset;
1124                 if (lines == 0 || view->offset + view->height >= view->lines) {
1125                         report("Cannot scroll beyond the last line");
1126                         return;
1127                 }
1128                 break;
1130         case REQ_SCROLL_PAGE_UP:
1131                 lines = view->height;
1132         case REQ_SCROLL_LINE_UP:
1133                 if (lines > view->offset)
1134                         lines = view->offset;
1136                 if (lines == 0) {
1137                         report("Cannot scroll beyond the first line");
1138                         return;
1139                 }
1141                 lines = -lines;
1142                 break;
1144         default:
1145                 die("request %d not handled in switch", request);
1146         }
1148         do_scroll_view(view, lines, TRUE);
1151 /* Cursor moving */
1152 static void
1153 move_view(struct view *view, enum request request, bool redraw)
1155         int steps;
1157         switch (request) {
1158         case REQ_MOVE_FIRST_LINE:
1159                 steps = -view->lineno;
1160                 break;
1162         case REQ_MOVE_LAST_LINE:
1163                 steps = view->lines - view->lineno - 1;
1164                 break;
1166         case REQ_MOVE_PAGE_UP:
1167                 steps = view->height > view->lineno
1168                       ? -view->lineno : -view->height;
1169                 break;
1171         case REQ_MOVE_PAGE_DOWN:
1172                 steps = view->lineno + view->height >= view->lines
1173                       ? view->lines - view->lineno - 1 : view->height;
1174                 break;
1176         case REQ_MOVE_UP:
1177                 steps = -1;
1178                 break;
1180         case REQ_MOVE_DOWN:
1181                 steps = 1;
1182                 break;
1184         default:
1185                 die("request %d not handled in switch", request);
1186         }
1188         if (steps <= 0 && view->lineno == 0) {
1189                 report("Cannot move beyond the first line");
1190                 return;
1192         } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1193                 report("Cannot move beyond the last line");
1194                 return;
1195         }
1197         /* Move the current line */
1198         view->lineno += steps;
1199         assert(0 <= view->lineno && view->lineno < view->lines);
1201         /* Repaint the old "current" line if we be scrolling */
1202         if (ABS(steps) < view->height) {
1203                 int prev_lineno = view->lineno - steps - view->offset;
1205                 wmove(view->win, prev_lineno, 0);
1206                 wclrtoeol(view->win);
1207                 draw_view_line(view,  prev_lineno);
1208         }
1210         /* Check whether the view needs to be scrolled */
1211         if (view->lineno < view->offset ||
1212             view->lineno >= view->offset + view->height) {
1213                 if (steps < 0 && -steps > view->offset) {
1214                         steps = -view->offset;
1216                 } else if (steps > 0) {
1217                         if (view->lineno == view->lines - 1 &&
1218                             view->lines > view->height) {
1219                                 steps = view->lines - view->offset - 1;
1220                                 if (steps >= view->height)
1221                                         steps -= view->height - 1;
1222                         }
1223                 }
1225                 do_scroll_view(view, steps, redraw);
1226                 return;
1227         }
1229         /* Draw the current line */
1230         draw_view_line(view, view->lineno - view->offset);
1232         if (!redraw)
1233                 return;
1235         redrawwin(view->win);
1236         wrefresh(view->win);
1237         report("");
1241 /*
1242  * Incremental updating
1243  */
1245 static void
1246 end_update(struct view *view)
1248         if (!view->pipe)
1249                 return;
1250         set_nonblocking_input(FALSE);
1251         if (view->pipe == stdin)
1252                 fclose(view->pipe);
1253         else
1254                 pclose(view->pipe);
1255         view->pipe = NULL;
1258 static bool
1259 begin_update(struct view *view)
1261         const char *id = view->id;
1263         if (view->pipe)
1264                 end_update(view);
1266         if (opt_cmd[0]) {
1267                 string_copy(view->cmd, opt_cmd);
1268                 opt_cmd[0] = 0;
1269                 /* When running random commands, the view ref could have become
1270                  * invalid so clear it. */
1271                 view->ref[0] = 0;
1272         } else {
1273                 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1275                 if (snprintf(view->cmd, sizeof(view->cmd), format,
1276                              id, id, id, id, id) >= sizeof(view->cmd))
1277                         return FALSE;
1278         }
1280         /* Special case for the pager view. */
1281         if (opt_pipe) {
1282                 view->pipe = opt_pipe;
1283                 opt_pipe = NULL;
1284         } else {
1285                 view->pipe = popen(view->cmd, "r");
1286         }
1288         if (!view->pipe)
1289                 return FALSE;
1291         set_nonblocking_input(TRUE);
1293         view->offset = 0;
1294         view->lines  = 0;
1295         view->lineno = 0;
1296         string_copy(view->vid, id);
1298         if (view->line) {
1299                 int i;
1301                 for (i = 0; i < view->lines; i++)
1302                         if (view->line[i].data)
1303                                 free(view->line[i].data);
1305                 free(view->line);
1306                 view->line = NULL;
1307         }
1309         view->start_time = time(NULL);
1311         return TRUE;
1314 static struct line *
1315 realloc_lines(struct view *view, size_t line_size)
1317         struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1319         if (!tmp)
1320                 return NULL;
1322         view->line = tmp;
1323         view->line_size = line_size;
1324         return view->line;
1327 static bool
1328 update_view(struct view *view)
1330         char buffer[BUFSIZ];
1331         char *line;
1332         /* The number of lines to read. If too low it will cause too much
1333          * redrawing (and possible flickering), if too high responsiveness
1334          * will suffer. */
1335         unsigned long lines = view->height;
1336         int redraw_from = -1;
1338         if (!view->pipe)
1339                 return TRUE;
1341         /* Only redraw if lines are visible. */
1342         if (view->offset + view->height >= view->lines)
1343                 redraw_from = view->lines - view->offset;
1345         if (!realloc_lines(view, view->lines + lines))
1346                 goto alloc_error;
1348         while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1349                 int linelen = strlen(line);
1351                 struct line *prev = view->lines
1352                                   ? &view->line[view->lines - 1]
1353                                   : NULL;
1355                 if (linelen)
1356                         line[linelen - 1] = 0;
1358                 if (!view->ops->read(view, prev, line))
1359                         goto alloc_error;
1361                 if (lines-- == 1)
1362                         break;
1363         }
1365         {
1366                 int digits;
1368                 lines = view->lines;
1369                 for (digits = 0; lines; digits++)
1370                         lines /= 10;
1372                 /* Keep the displayed view in sync with line number scaling. */
1373                 if (digits != view->digits) {
1374                         view->digits = digits;
1375                         redraw_from = 0;
1376                 }
1377         }
1379         if (redraw_from >= 0) {
1380                 /* If this is an incremental update, redraw the previous line
1381                  * since for commits some members could have changed when
1382                  * loading the main view. */
1383                 if (redraw_from > 0)
1384                         redraw_from--;
1386                 /* Incrementally draw avoids flickering. */
1387                 redraw_view_from(view, redraw_from);
1388         }
1390         /* Update the title _after_ the redraw so that if the redraw picks up a
1391          * commit reference in view->ref it'll be available here. */
1392         update_view_title(view);
1394         if (ferror(view->pipe)) {
1395                 report("Failed to read: %s", strerror(errno));
1396                 goto end;
1398         } else if (feof(view->pipe)) {
1399                 report("");
1400                 goto end;
1401         }
1403         return TRUE;
1405 alloc_error:
1406         report("Allocation failure");
1408 end:
1409         end_update(view);
1410         return FALSE;
1413 enum open_flags {
1414         OPEN_DEFAULT = 0,       /* Use default view switching. */
1415         OPEN_SPLIT = 1,         /* Split current view. */
1416         OPEN_BACKGROUNDED = 2,  /* Backgrounded. */
1417         OPEN_RELOAD = 4,        /* Reload view even if it is the current. */
1418 };
1420 static void
1421 open_view(struct view *prev, enum request request, enum open_flags flags)
1423         bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1424         bool split = !!(flags & OPEN_SPLIT);
1425         bool reload = !!(flags & OPEN_RELOAD);
1426         struct view *view = VIEW(request);
1427         int nviews = displayed_views();
1428         struct view *base_view = display[0];
1430         if (view == prev && nviews == 1 && !reload) {
1431                 report("Already in %s view", view->name);
1432                 return;
1433         }
1435         if ((reload || strcmp(view->vid, view->id)) &&
1436             !begin_update(view)) {
1437                 report("Failed to load %s view", view->name);
1438                 return;
1439         }
1441         if (split) {
1442                 display[1] = view;
1443                 if (!backgrounded)
1444                         current_view = 1;
1445         } else {
1446                 /* Maximize the current view. */
1447                 memset(display, 0, sizeof(display));
1448                 current_view = 0;
1449                 display[current_view] = view;
1450         }
1452         /* Resize the view when switching between split- and full-screen,
1453          * or when switching between two different full-screen views. */
1454         if (nviews != displayed_views() ||
1455             (nviews == 1 && base_view != display[0]))
1456                 resize_display();
1458         if (split && prev->lineno - prev->offset >= prev->height) {
1459                 /* Take the title line into account. */
1460                 int lines = prev->lineno - prev->offset - prev->height + 1;
1462                 /* Scroll the view that was split if the current line is
1463                  * outside the new limited view. */
1464                 do_scroll_view(prev, lines, TRUE);
1465         }
1467         if (prev && view != prev) {
1468                 if (split && !backgrounded) {
1469                         /* "Blur" the previous view. */
1470                         update_view_title(prev);
1471                 }
1473                 view->parent = prev;
1474         }
1476         if (view == VIEW(REQ_VIEW_HELP))
1477                 load_help_page();
1479         if (view->pipe && view->lines == 0) {
1480                 /* Clear the old view and let the incremental updating refill
1481                  * the screen. */
1482                 wclear(view->win);
1483                 report("");
1484         } else {
1485                 redraw_view(view);
1486                 report("");
1487         }
1489         /* If the view is backgrounded the above calls to report()
1490          * won't redraw the view title. */
1491         if (backgrounded)
1492                 update_view_title(view);
1496 /*
1497  * User request switch noodle
1498  */
1500 static int
1501 view_driver(struct view *view, enum request request)
1503         int i;
1505         switch (request) {
1506         case REQ_MOVE_UP:
1507         case REQ_MOVE_DOWN:
1508         case REQ_MOVE_PAGE_UP:
1509         case REQ_MOVE_PAGE_DOWN:
1510         case REQ_MOVE_FIRST_LINE:
1511         case REQ_MOVE_LAST_LINE:
1512                 move_view(view, request, TRUE);
1513                 break;
1515         case REQ_SCROLL_LINE_DOWN:
1516         case REQ_SCROLL_LINE_UP:
1517         case REQ_SCROLL_PAGE_DOWN:
1518         case REQ_SCROLL_PAGE_UP:
1519                 scroll_view(view, request);
1520                 break;
1522         case REQ_VIEW_MAIN:
1523         case REQ_VIEW_DIFF:
1524         case REQ_VIEW_LOG:
1525         case REQ_VIEW_HELP:
1526         case REQ_VIEW_PAGER:
1527                 open_view(view, request, OPEN_DEFAULT);
1528                 break;
1530         case REQ_NEXT:
1531         case REQ_PREVIOUS:
1532                 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1534                 if (view == VIEW(REQ_VIEW_DIFF) &&
1535                     view->parent == VIEW(REQ_VIEW_MAIN)) {
1536                         bool redraw = display[1] == view;
1538                         view = view->parent;
1539                         move_view(view, request, redraw);
1540                         if (redraw)
1541                                 update_view_title(view);
1542                 } else {
1543                         move_view(view, request, TRUE);
1544                         break;
1545                 }
1546                 /* Fall-through */
1548         case REQ_ENTER:
1549                 if (!view->lines) {
1550                         report("Nothing to enter");
1551                         break;
1552                 }
1553                 return view->ops->enter(view, &view->line[view->lineno]);
1555         case REQ_VIEW_NEXT:
1556         {
1557                 int nviews = displayed_views();
1558                 int next_view = (current_view + 1) % nviews;
1560                 if (next_view == current_view) {
1561                         report("Only one view is displayed");
1562                         break;
1563                 }
1565                 current_view = next_view;
1566                 /* Blur out the title of the previous view. */
1567                 update_view_title(view);
1568                 report("");
1569                 break;
1570         }
1571         case REQ_TOGGLE_LINENO:
1572                 opt_line_number = !opt_line_number;
1573                 redraw_display();
1574                 break;
1576         case REQ_PROMPT:
1577                 /* Always reload^Wrerun commands from the prompt. */
1578                 open_view(view, opt_request, OPEN_RELOAD);
1579                 break;
1581         case REQ_STOP_LOADING:
1582                 for (i = 0; i < ARRAY_SIZE(views); i++) {
1583                         view = &views[i];
1584                         if (view->pipe)
1585                                 report("Stopped loading the %s view", view->name),
1586                         end_update(view);
1587                 }
1588                 break;
1590         case REQ_SHOW_VERSION:
1591                 report("%s (built %s)", VERSION, __DATE__);
1592                 return TRUE;
1594         case REQ_SCREEN_RESIZE:
1595                 resize_display();
1596                 /* Fall-through */
1597         case REQ_SCREEN_REDRAW:
1598                 redraw_display();
1599                 break;
1601         case REQ_SCREEN_UPDATE:
1602                 doupdate();
1603                 return TRUE;
1605         case REQ_VIEW_CLOSE:
1606                 /* XXX: Mark closed views by letting view->parent point to the
1607                  * view itself. Parents to closed view should never be
1608                  * followed. */
1609                 if (view->parent &&
1610                     view->parent->parent != view->parent) {
1611                         memset(display, 0, sizeof(display));
1612                         current_view = 0;
1613                         display[current_view] = view->parent;
1614                         view->parent = view;
1615                         resize_display();
1616                         redraw_display();
1617                         break;
1618                 }
1619                 /* Fall-through */
1620         case REQ_QUIT:
1621                 return FALSE;
1623         default:
1624                 /* An unknown key will show most commonly used commands. */
1625                 report("Unknown key, press 'h' for help");
1626                 return TRUE;
1627         }
1629         return TRUE;
1633 /*
1634  * Pager backend
1635  */
1637 static bool
1638 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1640         char *text = line->data;
1641         enum line_type type = line->type;
1642         int textlen = strlen(text);
1643         int attr;
1645         wmove(view->win, lineno, 0);
1647         if (view->offset + lineno == view->lineno) {
1648                 if (type == LINE_COMMIT) {
1649                         string_copy(view->ref, text + 7);
1650                         string_copy(ref_commit, view->ref);
1651                 }
1653                 type = LINE_CURSOR;
1654                 wchgat(view->win, -1, 0, type, NULL);
1655         }
1657         attr = get_line_attr(type);
1658         wattrset(view->win, attr);
1660         if (opt_line_number || opt_tab_size < TABSIZE) {
1661                 static char spaces[] = "                    ";
1662                 int col_offset = 0, col = 0;
1664                 if (opt_line_number) {
1665                         unsigned long real_lineno = view->offset + lineno + 1;
1667                         if (real_lineno == 1 ||
1668                             (real_lineno % opt_num_interval) == 0) {
1669                                 wprintw(view->win, "%.*d", view->digits, real_lineno);
1671                         } else {
1672                                 waddnstr(view->win, spaces,
1673                                          MIN(view->digits, STRING_SIZE(spaces)));
1674                         }
1675                         waddstr(view->win, ": ");
1676                         col_offset = view->digits + 2;
1677                 }
1679                 while (text && col_offset + col < view->width) {
1680                         int cols_max = view->width - col_offset - col;
1681                         char *pos = text;
1682                         int cols;
1684                         if (*text == '\t') {
1685                                 text++;
1686                                 assert(sizeof(spaces) > TABSIZE);
1687                                 pos = spaces;
1688                                 cols = opt_tab_size - (col % opt_tab_size);
1690                         } else {
1691                                 text = strchr(text, '\t');
1692                                 cols = line ? text - pos : strlen(pos);
1693                         }
1695                         waddnstr(view->win, pos, MIN(cols, cols_max));
1696                         col += cols;
1697                 }
1699         } else {
1700                 int col = 0, pos = 0;
1702                 for (; pos < textlen && col < view->width; pos++, col++)
1703                         if (text[pos] == '\t')
1704                                 col += TABSIZE - (col % TABSIZE) - 1;
1706                 waddnstr(view->win, text, pos);
1707         }
1709         return TRUE;
1712 static void
1713 add_pager_refs(struct view *view, struct line *line)
1715         char buf[1024];
1716         char *data = line->data;
1717         struct ref **refs;
1718         int bufpos = 0, refpos = 0;
1719         const char *sep = "Refs: ";
1721         assert(line->type == LINE_COMMIT);
1723         refs = get_refs(data + STRING_SIZE("commit "));
1724         if (!refs)
1725                 return;
1727         do {
1728                 char *begin = "", *end = "";
1730                 if (refs[refpos]->tag) {
1731                         begin = "[";
1732                         end = "]";
1733                 }
1735                 bufpos += snprintf(buf + bufpos, sizeof(buf) - bufpos,
1736                                    "%s%s%s%s", sep, begin, refs[refpos]->name,
1737                                    end);
1738                 if (bufpos >= sizeof(buf))
1739                         break;
1740                 sep = ", ";
1741         } while (refs[refpos++]->next);
1743         if (!bufpos ||
1744             bufpos >= sizeof(buf) ||
1745             !realloc_lines(view, view->line_size + 1))
1746                 return;
1748         line = &view->line[view->lines];
1749         line->data = strdup(buf);
1750         if (!line->data)
1751                 return;
1753         line->type = LINE_PP_REFS;
1754         view->lines++;
1757 static bool
1758 pager_read(struct view *view, struct line *prev, char *data)
1760         struct line *line = &view->line[view->lines];
1762         line->data = strdup(data);
1763         if (!line->data)
1764                 return FALSE;
1766         line->type = get_line_type(line->data);
1767         view->lines++;
1769         if (line->type == LINE_COMMIT &&
1770             (view == VIEW(REQ_VIEW_DIFF) ||
1771              view == VIEW(REQ_VIEW_LOG)))
1772                 add_pager_refs(view, line);
1774         return TRUE;
1777 static bool
1778 pager_enter(struct view *view, struct line *line)
1780         int split = 0;
1782         if (line->type == LINE_COMMIT &&
1783            (view == VIEW(REQ_VIEW_LOG) ||
1784             view == VIEW(REQ_VIEW_PAGER))) {
1785                 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1786                 split = 1;
1787         }
1789         /* Always scroll the view even if it was split. That way
1790          * you can use Enter to scroll through the log view and
1791          * split open each commit diff. */
1792         scroll_view(view, REQ_SCROLL_LINE_DOWN);
1794         /* FIXME: A minor workaround. Scrolling the view will call report("")
1795          * but if we are scrolling a non-current view this won't properly
1796          * update the view title. */
1797         if (split)
1798                 update_view_title(view);
1800         return TRUE;
1803 static struct view_ops pager_ops = {
1804         "line",
1805         pager_draw,
1806         pager_read,
1807         pager_enter,
1808 };
1811 /*
1812  * Main view backend
1813  */
1815 struct commit {
1816         char id[41];            /* SHA1 ID. */
1817         char title[75];         /* The first line of the commit message. */
1818         char author[75];        /* The author of the commit. */
1819         struct tm time;         /* Date from the author ident. */
1820         struct ref **refs;      /* Repository references; tags & branch heads. */
1821 };
1823 static bool
1824 main_draw(struct view *view, struct line *line, unsigned int lineno)
1826         char buf[DATE_COLS + 1];
1827         struct commit *commit = line->data;
1828         enum line_type type;
1829         int col = 0;
1830         size_t timelen;
1831         size_t authorlen;
1832         int trimmed = 1;
1834         if (!*commit->author)
1835                 return FALSE;
1837         wmove(view->win, lineno, col);
1839         if (view->offset + lineno == view->lineno) {
1840                 string_copy(view->ref, commit->id);
1841                 string_copy(ref_commit, view->ref);
1842                 type = LINE_CURSOR;
1843                 wattrset(view->win, get_line_attr(type));
1844                 wchgat(view->win, -1, 0, type, NULL);
1846         } else {
1847                 type = LINE_MAIN_COMMIT;
1848                 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1849         }
1851         timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1852         waddnstr(view->win, buf, timelen);
1853         waddstr(view->win, " ");
1855         col += DATE_COLS;
1856         wmove(view->win, lineno, col);
1857         if (type != LINE_CURSOR)
1858                 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1860         if (opt_utf8) {
1861                 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1862         } else {
1863                 authorlen = strlen(commit->author);
1864                 if (authorlen > AUTHOR_COLS - 2) {
1865                         authorlen = AUTHOR_COLS - 2;
1866                         trimmed = 1;
1867                 }
1868         }
1870         if (trimmed) {
1871                 waddnstr(view->win, commit->author, authorlen);
1872                 if (type != LINE_CURSOR)
1873                         wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1874                 waddch(view->win, '~');
1875         } else {
1876                 waddstr(view->win, commit->author);
1877         }
1879         col += AUTHOR_COLS;
1880         if (type != LINE_CURSOR)
1881                 wattrset(view->win, A_NORMAL);
1883         mvwaddch(view->win, lineno, col, ACS_LTEE);
1884         wmove(view->win, lineno, col + 2);
1885         col += 2;
1887         if (commit->refs) {
1888                 size_t i = 0;
1890                 do {
1891                         if (type == LINE_CURSOR)
1892                                 ;
1893                         else if (commit->refs[i]->tag)
1894                                 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1895                         else
1896                                 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1897                         waddstr(view->win, "[");
1898                         waddstr(view->win, commit->refs[i]->name);
1899                         waddstr(view->win, "]");
1900                         if (type != LINE_CURSOR)
1901                                 wattrset(view->win, A_NORMAL);
1902                         waddstr(view->win, " ");
1903                         col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1904                 } while (commit->refs[i++]->next);
1905         }
1907         if (type != LINE_CURSOR)
1908                 wattrset(view->win, get_line_attr(type));
1910         {
1911                 int titlelen = strlen(commit->title);
1913                 if (col + titlelen > view->width)
1914                         titlelen = view->width - col;
1916                 waddnstr(view->win, commit->title, titlelen);
1917         }
1919         return TRUE;
1922 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1923 static bool
1924 main_read(struct view *view, struct line *prev, char *line)
1926         enum line_type type = get_line_type(line);
1927         struct commit *commit;
1929         switch (type) {
1930         case LINE_COMMIT:
1931                 commit = calloc(1, sizeof(struct commit));
1932                 if (!commit)
1933                         return FALSE;
1935                 line += STRING_SIZE("commit ");
1937                 view->line[view->lines++].data = commit;
1938                 string_copy(commit->id, line);
1939                 commit->refs = get_refs(commit->id);
1940                 break;
1942         case LINE_AUTHOR:
1943         {
1944                 char *ident = line + STRING_SIZE("author ");
1945                 char *end = strchr(ident, '<');
1947                 if (!prev)
1948                         break;
1950                 commit = prev->data;
1952                 if (end) {
1953                         for (; end > ident && isspace(end[-1]); end--) ;
1954                         *end = 0;
1955                 }
1957                 string_copy(commit->author, ident);
1959                 /* Parse epoch and timezone */
1960                 if (end) {
1961                         char *secs = strchr(end + 1, '>');
1962                         char *zone;
1963                         time_t time;
1965                         if (!secs || secs[1] != ' ')
1966                                 break;
1968                         secs += 2;
1969                         time = (time_t) atol(secs);
1970                         zone = strchr(secs, ' ');
1971                         if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1972                                 long tz;
1974                                 zone++;
1975                                 tz  = ('0' - zone[1]) * 60 * 60 * 10;
1976                                 tz += ('0' - zone[2]) * 60 * 60;
1977                                 tz += ('0' - zone[3]) * 60;
1978                                 tz += ('0' - zone[4]) * 60;
1980                                 if (zone[0] == '-')
1981                                         tz = -tz;
1983                                 time -= tz;
1984                         }
1985                         gmtime_r(&time, &commit->time);
1986                 }
1987                 break;
1988         }
1989         default:
1990                 if (!prev)
1991                         break;
1993                 commit = prev->data;
1995                 /* Fill in the commit title if it has not already been set. */
1996                 if (commit->title[0])
1997                         break;
1999                 /* Require titles to start with a non-space character at the
2000                  * offset used by git log. */
2001                 /* FIXME: More gracefull handling of titles; append "..." to
2002                  * shortened titles, etc. */
2003                 if (strncmp(line, "    ", 4) ||
2004                     isspace(line[4]))
2005                         break;
2007                 string_copy(commit->title, line + 4);
2008         }
2010         return TRUE;
2013 static bool
2014 main_enter(struct view *view, struct line *line)
2016         enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2018         open_view(view, REQ_VIEW_DIFF, flags);
2019         return TRUE;
2022 static struct view_ops main_ops = {
2023         "commit",
2024         main_draw,
2025         main_read,
2026         main_enter,
2027 };
2030 /*
2031  * Keys
2032  */
2034 struct keymap {
2035         int alias;
2036         int request;
2037 };
2039 static struct keymap keymap[] = {
2040         /* View switching */
2041         { 'm',          REQ_VIEW_MAIN },
2042         { 'd',          REQ_VIEW_DIFF },
2043         { 'l',          REQ_VIEW_LOG },
2044         { 'p',          REQ_VIEW_PAGER },
2045         { 'h',          REQ_VIEW_HELP },
2046         { '?',          REQ_VIEW_HELP },
2048         /* View manipulation */
2049         { 'q',          REQ_VIEW_CLOSE },
2050         { KEY_TAB,      REQ_VIEW_NEXT },
2051         { KEY_RETURN,   REQ_ENTER },
2052         { KEY_UP,       REQ_PREVIOUS },
2053         { KEY_DOWN,     REQ_NEXT },
2055         /* Cursor navigation */
2056         { 'k',          REQ_MOVE_UP },
2057         { 'j',          REQ_MOVE_DOWN },
2058         { KEY_HOME,     REQ_MOVE_FIRST_LINE },
2059         { KEY_END,      REQ_MOVE_LAST_LINE },
2060         { KEY_NPAGE,    REQ_MOVE_PAGE_DOWN },
2061         { ' ',          REQ_MOVE_PAGE_DOWN },
2062         { KEY_PPAGE,    REQ_MOVE_PAGE_UP },
2063         { 'b',          REQ_MOVE_PAGE_UP },
2064         { '-',          REQ_MOVE_PAGE_UP },
2066         /* Scrolling */
2067         { KEY_IC,       REQ_SCROLL_LINE_UP },
2068         { KEY_DC,       REQ_SCROLL_LINE_DOWN },
2069         { 'w',          REQ_SCROLL_PAGE_UP },
2070         { 's',          REQ_SCROLL_PAGE_DOWN },
2072         /* Misc */
2073         { 'Q',          REQ_QUIT },
2074         { 'z',          REQ_STOP_LOADING },
2075         { 'v',          REQ_SHOW_VERSION },
2076         { 'r',          REQ_SCREEN_REDRAW },
2077         { 'n',          REQ_TOGGLE_LINENO },
2078         { ':',          REQ_PROMPT },
2080         /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2081         { ERR,          REQ_SCREEN_UPDATE },
2083         /* Use the ncurses SIGWINCH handler. */
2084         { KEY_RESIZE,   REQ_SCREEN_RESIZE },
2085 };
2087 static enum request
2088 get_request(int key)
2090         int i;
2092         for (i = 0; i < ARRAY_SIZE(keymap); i++)
2093                 if (keymap[i].alias == key)
2094                         return keymap[i].request;
2096         return (enum request) key;
2099 struct key {
2100         char *name;
2101         int value;
2102 };
2104 static struct key key_table[] = {
2105         { "Enter",      KEY_RETURN },
2106         { "Space",      ' ' },
2107         { "Backspace",  KEY_BACKSPACE },
2108         { "Tab",        KEY_TAB },
2109         { "Escape",     KEY_ESC },
2110         { "Left",       KEY_LEFT },
2111         { "Right",      KEY_RIGHT },
2112         { "Up",         KEY_UP },
2113         { "Down",       KEY_DOWN },
2114         { "Insert",     KEY_IC },
2115         { "Delete",     KEY_DC },
2116         { "Home",       KEY_HOME },
2117         { "End",        KEY_END },
2118         { "PageUp",     KEY_PPAGE },
2119         { "PageDown",   KEY_NPAGE },
2120         { "F1",         KEY_F(1) },
2121         { "F2",         KEY_F(2) },
2122         { "F3",         KEY_F(3) },
2123         { "F4",         KEY_F(4) },
2124         { "F5",         KEY_F(5) },
2125         { "F6",         KEY_F(6) },
2126         { "F7",         KEY_F(7) },
2127         { "F8",         KEY_F(8) },
2128         { "F9",         KEY_F(9) },
2129         { "F10",        KEY_F(10) },
2130         { "F11",        KEY_F(11) },
2131         { "F12",        KEY_F(12) },
2132 };
2134 static char *
2135 get_key(enum request request)
2137         static char buf[BUFSIZ];
2138         static char key_char[] = "'X'";
2139         int pos = 0;
2140         char *sep = "    ";
2141         int i;
2143         buf[pos] = 0;
2145         for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2146                 char *seq = NULL;
2147                 int key;
2149                 if (keymap[i].request != request)
2150                         continue;
2152                 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2153                         if (key_table[key].value == keymap[i].alias)
2154                                 seq = key_table[key].name;
2156                 if (seq == NULL &&
2157                     keymap[i].alias < 127 &&
2158                     isprint(keymap[i].alias)) {
2159                         key_char[1] = (char) keymap[i].alias;
2160                         seq = key_char;
2161                 }
2163                 if (!seq)
2164                         seq = "'?'";
2166                 pos += snprintf(buf + pos, sizeof(buf) - pos, "%s%s", sep, seq);
2167                 if (pos >= sizeof(buf))
2168                         return "Too many keybindings!";
2169                 sep = ", ";
2170         }
2172         return buf;
2175 static void load_help_page(void)
2177         char buf[BUFSIZ];
2178         struct view *view = VIEW(REQ_VIEW_HELP);
2179         int lines = ARRAY_SIZE(req_info) + 2;
2180         int i;
2182         if (view->lines > 0)
2183                 return;
2185         for (i = 0; i < ARRAY_SIZE(req_info); i++)
2186                 if (!req_info[i].request)
2187                         lines++;
2189         view->line = calloc(lines, sizeof(*view->line));
2190         if (!view->line) {
2191                 report("Allocation failure");
2192                 return;
2193         }
2195         pager_read(view, NULL, "Quick reference for tig keybindings:");
2197         for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2198                 char *key;
2200                 if (!req_info[i].request) {
2201                         pager_read(view, NULL, "");
2202                         pager_read(view, NULL, req_info[i].help);
2203                         continue;
2204                 }
2206                 key = get_key(req_info[i].request);
2207                 if (snprintf(buf, sizeof(buf), "%-25s %s", key, req_info[i].help)
2208                     >= sizeof(buf))
2209                         continue;
2211                 pager_read(view, NULL, buf);
2212         }
2216 /*
2217  * Unicode / UTF-8 handling
2218  *
2219  * NOTE: Much of the following code for dealing with unicode is derived from
2220  * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2221  * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2222  */
2224 /* I've (over)annotated a lot of code snippets because I am not entirely
2225  * confident that the approach taken by this small UTF-8 interface is correct.
2226  * --jonas */
2228 static inline int
2229 unicode_width(unsigned long c)
2231         if (c >= 0x1100 &&
2232            (c <= 0x115f                         /* Hangul Jamo */
2233             || c == 0x2329
2234             || c == 0x232a
2235             || (c >= 0x2e80  && c <= 0xa4cf && c != 0x303f)
2236                                                 /* CJK ... Yi */
2237             || (c >= 0xac00  && c <= 0xd7a3)    /* Hangul Syllables */
2238             || (c >= 0xf900  && c <= 0xfaff)    /* CJK Compatibility Ideographs */
2239             || (c >= 0xfe30  && c <= 0xfe6f)    /* CJK Compatibility Forms */
2240             || (c >= 0xff00  && c <= 0xff60)    /* Fullwidth Forms */
2241             || (c >= 0xffe0  && c <= 0xffe6)
2242             || (c >= 0x20000 && c <= 0x2fffd)
2243             || (c >= 0x30000 && c <= 0x3fffd)))
2244                 return 2;
2246         return 1;
2249 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2250  * Illegal bytes are set one. */
2251 static const unsigned char utf8_bytes[256] = {
2252         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,
2253         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,
2254         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,
2255         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,
2256         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,
2257         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,
2258         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,
2259         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,
2260 };
2262 /* Decode UTF-8 multi-byte representation into a unicode character. */
2263 static inline unsigned long
2264 utf8_to_unicode(const char *string, size_t length)
2266         unsigned long unicode;
2268         switch (length) {
2269         case 1:
2270                 unicode  =   string[0];
2271                 break;
2272         case 2:
2273                 unicode  =  (string[0] & 0x1f) << 6;
2274                 unicode +=  (string[1] & 0x3f);
2275                 break;
2276         case 3:
2277                 unicode  =  (string[0] & 0x0f) << 12;
2278                 unicode += ((string[1] & 0x3f) << 6);
2279                 unicode +=  (string[2] & 0x3f);
2280                 break;
2281         case 4:
2282                 unicode  =  (string[0] & 0x0f) << 18;
2283                 unicode += ((string[1] & 0x3f) << 12);
2284                 unicode += ((string[2] & 0x3f) << 6);
2285                 unicode +=  (string[3] & 0x3f);
2286                 break;
2287         case 5:
2288                 unicode  =  (string[0] & 0x0f) << 24;
2289                 unicode += ((string[1] & 0x3f) << 18);
2290                 unicode += ((string[2] & 0x3f) << 12);
2291                 unicode += ((string[3] & 0x3f) << 6);
2292                 unicode +=  (string[4] & 0x3f);
2293                 break;
2294         case 6:
2295                 unicode  =  (string[0] & 0x01) << 30;
2296                 unicode += ((string[1] & 0x3f) << 24);
2297                 unicode += ((string[2] & 0x3f) << 18);
2298                 unicode += ((string[3] & 0x3f) << 12);
2299                 unicode += ((string[4] & 0x3f) << 6);
2300                 unicode +=  (string[5] & 0x3f);
2301                 break;
2302         default:
2303                 die("Invalid unicode length");
2304         }
2306         /* Invalid characters could return the special 0xfffd value but NUL
2307          * should be just as good. */
2308         return unicode > 0xffff ? 0 : unicode;
2311 /* Calculates how much of string can be shown within the given maximum width
2312  * and sets trimmed parameter to non-zero value if all of string could not be
2313  * shown.
2314  *
2315  * Additionally, adds to coloffset how many many columns to move to align with
2316  * the expected position. Takes into account how multi-byte and double-width
2317  * characters will effect the cursor position.
2318  *
2319  * Returns the number of bytes to output from string to satisfy max_width. */
2320 static size_t
2321 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2323         const char *start = string;
2324         const char *end = strchr(string, '\0');
2325         size_t mbwidth = 0;
2326         size_t width = 0;
2328         *trimmed = 0;
2330         while (string < end) {
2331                 int c = *(unsigned char *) string;
2332                 unsigned char bytes = utf8_bytes[c];
2333                 size_t ucwidth;
2334                 unsigned long unicode;
2336                 if (string + bytes > end)
2337                         break;
2339                 /* Change representation to figure out whether
2340                  * it is a single- or double-width character. */
2342                 unicode = utf8_to_unicode(string, bytes);
2343                 /* FIXME: Graceful handling of invalid unicode character. */
2344                 if (!unicode)
2345                         break;
2347                 ucwidth = unicode_width(unicode);
2348                 width  += ucwidth;
2349                 if (width > max_width) {
2350                         *trimmed = 1;
2351                         break;
2352                 }
2354                 /* The column offset collects the differences between the
2355                  * number of bytes encoding a character and the number of
2356                  * columns will be used for rendering said character.
2357                  *
2358                  * So if some character A is encoded in 2 bytes, but will be
2359                  * represented on the screen using only 1 byte this will and up
2360                  * adding 1 to the multi-byte column offset.
2361                  *
2362                  * Assumes that no double-width character can be encoding in
2363                  * less than two bytes. */
2364                 if (bytes > ucwidth)
2365                         mbwidth += bytes - ucwidth;
2367                 string  += bytes;
2368         }
2370         *coloffset += mbwidth;
2372         return string - start;
2376 /*
2377  * Status management
2378  */
2380 /* Whether or not the curses interface has been initialized. */
2381 static bool cursed = FALSE;
2383 /* The status window is used for polling keystrokes. */
2384 static WINDOW *status_win;
2386 /* Update status and title window. */
2387 static void
2388 report(const char *msg, ...)
2390         static bool empty = TRUE;
2391         struct view *view = display[current_view];
2393         if (!empty || *msg) {
2394                 va_list args;
2396                 va_start(args, msg);
2398                 werase(status_win);
2399                 wmove(status_win, 0, 0);
2400                 if (*msg) {
2401                         vwprintw(status_win, msg, args);
2402                         empty = FALSE;
2403                 } else {
2404                         empty = TRUE;
2405                 }
2406                 wrefresh(status_win);
2408                 va_end(args);
2409         }
2411         update_view_title(view);
2412         update_display_cursor();
2415 /* Controls when nodelay should be in effect when polling user input. */
2416 static void
2417 set_nonblocking_input(bool loading)
2419         static unsigned int loading_views;
2421         if ((loading == FALSE && loading_views-- == 1) ||
2422             (loading == TRUE  && loading_views++ == 0))
2423                 nodelay(status_win, loading);
2426 static void
2427 init_display(void)
2429         int x, y;
2431         /* Initialize the curses library */
2432         if (isatty(STDIN_FILENO)) {
2433                 cursed = !!initscr();
2434         } else {
2435                 /* Leave stdin and stdout alone when acting as a pager. */
2436                 FILE *io = fopen("/dev/tty", "r+");
2438                 cursed = !!newterm(NULL, io, io);
2439         }
2441         if (!cursed)
2442                 die("Failed to initialize curses");
2444         nonl();         /* Tell curses not to do NL->CR/NL on output */
2445         cbreak();       /* Take input chars one at a time, no wait for \n */
2446         noecho();       /* Don't echo input */
2447         leaveok(stdscr, TRUE);
2449         if (has_colors())
2450                 init_colors();
2452         getmaxyx(stdscr, y, x);
2453         status_win = newwin(1, 0, y - 1, 0);
2454         if (!status_win)
2455                 die("Failed to create status window");
2457         /* Enable keyboard mapping */
2458         keypad(status_win, TRUE);
2459         wbkgdset(status_win, get_line_attr(LINE_STATUS));
2463 /*
2464  * Repository references
2465  */
2467 static struct ref *refs;
2468 static size_t refs_size;
2470 /* Id <-> ref store */
2471 static struct ref ***id_refs;
2472 static size_t id_refs_size;
2474 static struct ref **
2475 get_refs(char *id)
2477         struct ref ***tmp_id_refs;
2478         struct ref **ref_list = NULL;
2479         size_t ref_list_size = 0;
2480         size_t i;
2482         for (i = 0; i < id_refs_size; i++)
2483                 if (!strcmp(id, id_refs[i][0]->id))
2484                         return id_refs[i];
2486         tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2487         if (!tmp_id_refs)
2488                 return NULL;
2490         id_refs = tmp_id_refs;
2492         for (i = 0; i < refs_size; i++) {
2493                 struct ref **tmp;
2495                 if (strcmp(id, refs[i].id))
2496                         continue;
2498                 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2499                 if (!tmp) {
2500                         if (ref_list)
2501                                 free(ref_list);
2502                         return NULL;
2503                 }
2505                 ref_list = tmp;
2506                 if (ref_list_size > 0)
2507                         ref_list[ref_list_size - 1]->next = 1;
2508                 ref_list[ref_list_size] = &refs[i];
2510                 /* XXX: The properties of the commit chains ensures that we can
2511                  * safely modify the shared ref. The repo references will
2512                  * always be similar for the same id. */
2513                 ref_list[ref_list_size]->next = 0;
2514                 ref_list_size++;
2515         }
2517         if (ref_list)
2518                 id_refs[id_refs_size++] = ref_list;
2520         return ref_list;
2523 static int
2524 read_ref(char *id, int idlen, char *name, int namelen)
2526         struct ref *ref;
2527         bool tag = FALSE;
2528         bool tag_commit = FALSE;
2530         /* Commits referenced by tags has "^{}" appended. */
2531         if (name[namelen - 1] == '}') {
2532                 while (namelen > 0 && name[namelen] != '^')
2533                         namelen--;
2534                 if (namelen > 0)
2535                         tag_commit = TRUE;
2536                 name[namelen] = 0;
2537         }
2539         if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2540                 if (!tag_commit)
2541                         return OK;
2542                 name += STRING_SIZE("refs/tags/");
2543                 tag = TRUE;
2545         } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2546                 name += STRING_SIZE("refs/heads/");
2548         } else if (!strcmp(name, "HEAD")) {
2549                 return OK;
2550         }
2552         refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2553         if (!refs)
2554                 return ERR;
2556         ref = &refs[refs_size++];
2557         ref->name = strdup(name);
2558         if (!ref->name)
2559                 return ERR;
2561         ref->tag = tag;
2562         string_copy(ref->id, id);
2564         return OK;
2567 static int
2568 load_refs(void)
2570         const char *cmd_env = getenv("TIG_LS_REMOTE");
2571         const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2573         return read_properties(popen(cmd, "r"), "\t", read_ref);
2576 static int
2577 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2579         if (!strcmp(name, "i18n.commitencoding")) {
2580                 string_copy(opt_encoding, value);
2581         }
2583         return OK;
2586 static int
2587 load_repo_config(void)
2589         return read_properties(popen("git repo-config --list", "r"),
2590                                "=", read_repo_config_option);
2593 static int
2594 read_properties(FILE *pipe, const char *separators,
2595                 int (*read_property)(char *, int, char *, int))
2597         char buffer[BUFSIZ];
2598         char *name;
2599         int state = OK;
2601         if (!pipe)
2602                 return ERR;
2604         while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2605                 char *value;
2606                 size_t namelen;
2607                 size_t valuelen;
2609                 name = chomp_string(name);
2610                 namelen = strcspn(name, separators);
2612                 if (name[namelen]) {
2613                         name[namelen] = 0;
2614                         value = chomp_string(name + namelen + 1);
2615                         valuelen = strlen(value);
2617                 } else {
2618                         value = "";
2619                         valuelen = 0;
2620                 }
2622                 state = read_property(name, namelen, value, valuelen);
2623         }
2625         if (state != ERR && ferror(pipe))
2626                 state = ERR;
2628         pclose(pipe);
2630         return state;
2634 /*
2635  * Main
2636  */
2638 #if __GNUC__ >= 3
2639 #define __NORETURN __attribute__((__noreturn__))
2640 #else
2641 #define __NORETURN
2642 #endif
2644 static void __NORETURN
2645 quit(int sig)
2647         /* XXX: Restore tty modes and let the OS cleanup the rest! */
2648         if (cursed)
2649                 endwin();
2650         exit(0);
2653 static void __NORETURN
2654 die(const char *err, ...)
2656         va_list args;
2658         endwin();
2660         va_start(args, err);
2661         fputs("tig: ", stderr);
2662         vfprintf(stderr, err, args);
2663         fputs("\n", stderr);
2664         va_end(args);
2666         exit(1);
2669 int
2670 main(int argc, char *argv[])
2672         struct view *view;
2673         enum request request;
2674         size_t i;
2676         signal(SIGINT, quit);
2678         if (load_options() == ERR)
2679                 die("Failed to load user config.");
2681         /* Load the repo config file so options can be overwritten from
2682          * the command line.  */
2683         if (load_repo_config() == ERR)
2684                 die("Failed to load repo config.");
2686         if (!parse_options(argc, argv))
2687                 return 0;
2689         if (load_refs() == ERR)
2690                 die("Failed to load refs.");
2692         /* Require a git repository unless when running in pager mode. */
2693         if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2694                 die("Not a git repository");
2696         for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2697                 view->cmd_env = getenv(view->cmd_env);
2699         request = opt_request;
2701         init_display();
2703         while (view_driver(display[current_view], request)) {
2704                 int key;
2705                 int i;
2707                 foreach_view (view, i)
2708                         update_view(view);
2710                 /* Refresh, accept single keystroke of input */
2711                 key = wgetch(status_win);
2712                 request = get_request(key);
2714                 /* Some low-level request handling. This keeps access to
2715                  * status_win restricted. */
2716                 switch (request) {
2717                 case REQ_PROMPT:
2718                         report(":");
2719                         /* Temporarily switch to line-oriented and echoed
2720                          * input. */
2721                         nocbreak();
2722                         echo();
2724                         if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2725                                 memcpy(opt_cmd, "git ", 4);
2726                                 opt_request = REQ_VIEW_PAGER;
2727                         } else {
2728                                 report("Prompt interrupted by loading view, "
2729                                        "press 'z' to stop loading views");
2730                                 request = REQ_SCREEN_UPDATE;
2731                         }
2733                         noecho();
2734                         cbreak();
2735                         break;
2737                 case REQ_SCREEN_RESIZE:
2738                 {
2739                         int height, width;
2741                         getmaxyx(stdscr, height, width);
2743                         /* Resize the status view and let the view driver take
2744                          * care of resizing the displayed views. */
2745                         wresize(status_win, 1, width);
2746                         mvwin(status_win, height - 1, 0);
2747                         wrefresh(status_win);
2748                         break;
2749                 }
2750                 default:
2751                         break;
2752                 }
2753         }
2755         quit(0);
2757         return 0;
2760 /**
2761  * include::BUGS[]
2762  *
2763  * COPYRIGHT
2764  * ---------
2765  * Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2766  *
2767  * This program is free software; you can redistribute it and/or modify
2768  * it under the terms of the GNU General Public License as published by
2769  * the Free Software Foundation; either version 2 of the License, or
2770  * (at your option) any later version.
2771  *
2772  * SEE ALSO
2773  * --------
2774  * - link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
2775  * - link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
2776  *
2777  * Other git repository browsers:
2778  *
2779  *  - gitk(1)
2780  *  - qgit(1)
2781  *  - gitview(1)
2782  *
2783  * Sites:
2784  *
2785  * include::SITES[]
2786  **/