Try to improve report("")
[tig] / tig.c
1 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2  * See license info at the bottom. */
3 /**
4  * TIG(1)
5  * ======
6  *
7  * NAME
8  * ----
9  * tig - text-mode interface for git
10  *
11  * SYNOPSIS
12  * --------
13  * [verse]
14  * tig [options]
15  * tig [options] [--] [git log options]
16  * tig [options] log  [git log options]
17  * tig [options] diff [git diff options]
18  * tig [options] show [git show options]
19  * tig [options] <    [git command output]
20  *
21  * DESCRIPTION
22  * -----------
23  * Browse changes in a git repository.
24  **/
25
26 #ifndef VERSION
27 #define VERSION "tig-0.1"
28 #endif
29
30 #ifndef DEBUG
31 #define NDEBUG
32 #endif
33
34 #include <assert.h>
35 #include <errno.h>
36 #include <ctype.h>
37 #include <signal.h>
38 #include <stdarg.h>
39 #include <stdio.h>
40 #include <stdlib.h>
41 #include <string.h>
42 #include <unistd.h>
43 #include <time.h>
44
45 #include <curses.h>
46
47 static void die(const char *err, ...);
48 static void report(const char *msg, ...);
49 static void set_nonblocking_input(bool loading);
50
51 #define ABS(x)          ((x) >= 0  ? (x) : -(x))
52 #define MIN(x, y)       ((x) < (y) ? (x) :  (y))
53
54 #define ARRAY_SIZE(x)   (sizeof(x) / sizeof(x[0]))
55 #define STRING_SIZE(x)  (sizeof(x) - 1)
56
57 #define SIZEOF_REF      256     /* Size of symbolic or SHA1 ID. */
58 #define SIZEOF_CMD      1024    /* Size of command buffer. */
59
60 /* This color name can be used to refer to the default term colors. */
61 #define COLOR_DEFAULT   (-1)
62
63 /* The format and size of the date column in the main view. */
64 #define DATE_FORMAT     "%Y-%m-%d %H:%M"
65 #define DATE_COLS       STRING_SIZE("2006-04-29 14:21 ")
66
67 /* The default interval between line numbers. */
68 #define NUMBER_INTERVAL 1
69
70 #define SCALE_SPLIT_VIEW(height)        ((height) * 2 / 3)
71
72 /* Some ascii-shorthands fitted into the ncurses namespace. */
73 #define KEY_TAB         '\t'
74 #define KEY_RETURN      '\r'
75 #define KEY_ESC         27
76
77 /* User action requests. */
78 enum request {
79         /* Offset all requests to avoid conflicts with ncurses getch values. */
80         REQ_OFFSET = KEY_MAX + 1,
81
82         /* XXX: Keep the view request first and in sync with views[]. */
83         REQ_VIEW_MAIN,
84         REQ_VIEW_DIFF,
85         REQ_VIEW_LOG,
86         REQ_VIEW_HELP,
87         REQ_VIEW_PAGER,
88
89         REQ_ENTER,
90         REQ_QUIT,
91         REQ_PROMPT,
92         REQ_SCREEN_REDRAW,
93         REQ_SCREEN_RESIZE,
94         REQ_SCREEN_UPDATE,
95         REQ_SHOW_VERSION,
96         REQ_STOP_LOADING,
97         REQ_TOGGLE_LINE_NUMBERS,
98         REQ_VIEW_NEXT,
99
100         REQ_MOVE_UP,
101         REQ_MOVE_DOWN,
102         REQ_MOVE_PAGE_UP,
103         REQ_MOVE_PAGE_DOWN,
104         REQ_MOVE_FIRST_LINE,
105         REQ_MOVE_LAST_LINE,
106
107         REQ_SCROLL_LINE_UP,
108         REQ_SCROLL_LINE_DOWN,
109         REQ_SCROLL_PAGE_UP,
110         REQ_SCROLL_PAGE_DOWN,
111 };
112
113 struct commit {
114         char id[41];            /* SHA1 ID. */
115         char title[75];         /* The first line of the commit message. */
116         char author[75];        /* The author of the commit. */
117         struct tm time;         /* Date from the author ident. */
118 };
119
120 /*
121  * String helpers
122  */
123
124 static inline void
125 string_ncopy(char *dst, char *src, int dstlen)
126 {
127         strncpy(dst, src, dstlen - 1);
128         dst[dstlen - 1] = 0;
129
130 }
131
132 /* Shorthand for safely copying into a fixed buffer. */
133 #define string_copy(dst, src) \
134         string_ncopy(dst, src, sizeof(dst))
135
136 /* Shell quoting
137  *
138  * NOTE: The following is a slightly modified copy of the git project's shell
139  * quoting routines found in the quote.c file.
140  *
141  * Help to copy the thing properly quoted for the shell safety.  any single
142  * quote is replaced with '\'', any exclamation point is replaced with '\!',
143  * and the whole thing is enclosed in a
144  *
145  * E.g.
146  *  original     sq_quote     result
147  *  name     ==> name      ==> 'name'
148  *  a b      ==> a b       ==> 'a b'
149  *  a'b      ==> a'\''b    ==> 'a'\''b'
150  *  a!b      ==> a'\!'b    ==> 'a'\!'b'
151  */
152
153 static size_t
154 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
155 {
156         char c;
157
158 #define BUFPUT(x) ( (bufsize < SIZEOF_CMD) && (buf[bufsize++] = (x)) )
159
160         BUFPUT('\'');
161         while ((c = *src++)) {
162                 if (c == '\'' || c == '!') {
163                         BUFPUT('\'');
164                         BUFPUT('\\');
165                         BUFPUT(c);
166                         BUFPUT('\'');
167                 } else {
168                         BUFPUT(c);
169                 }
170         }
171         BUFPUT('\'');
172
173         return bufsize;
174 }
175
176
177 /**
178  * OPTIONS
179  * -------
180  **/
181
182 static int opt_line_number      = FALSE;
183 static int opt_num_interval     = NUMBER_INTERVAL;
184 static enum request opt_request = REQ_VIEW_MAIN;
185 static char opt_cmd[SIZEOF_CMD] = "";
186 static FILE *opt_pipe           = NULL;
187
188 /* Returns the index of log or diff command or -1 to exit. */
189 static bool
190 parse_options(int argc, char *argv[])
191 {
192         int i;
193
194         for (i = 1; i < argc; i++) {
195                 char *opt = argv[i];
196
197                 /**
198                  * -l::
199                  *      Start up in log view using the internal log command.
200                  **/
201                 if (!strcmp(opt, "-l")) {
202                         opt_request = REQ_VIEW_LOG;
203                         continue;
204                 }
205
206                 /**
207                  * -d::
208                  *      Start up in diff view using the internal diff command.
209                  **/
210                 if (!strcmp(opt, "-d")) {
211                         opt_request = REQ_VIEW_DIFF;
212                         continue;
213                 }
214
215                 /**
216                  * -n[INTERVAL], --line-number[=INTERVAL]::
217                  *      Prefix line numbers in log and diff view.
218                  *      Optionally, with interval different than each line.
219                  **/
220                 if (!strncmp(opt, "-n", 2) ||
221                     !strncmp(opt, "--line-number", 13)) {
222                         char *num = opt;
223
224                         if (opt[1] == 'n') {
225                                 num = opt + 2;
226
227                         } else if (opt[STRING_SIZE("--line-number")] == '=') {
228                                 num = opt + STRING_SIZE("--line-number=");
229                         }
230
231                         if (isdigit(*num))
232                                 opt_num_interval = atoi(num);
233
234                         opt_line_number = TRUE;
235                         continue;
236                 }
237
238                 /**
239                  * -v, --version::
240                  *      Show version and exit.
241                  **/
242                 if (!strcmp(opt, "-v") ||
243                     !strcmp(opt, "--version")) {
244                         printf("tig version %s\n", VERSION);
245                         return FALSE;
246                 }
247
248                 /**
249                  * \--::
250                  *      End of tig(1) options. Useful when specifying commands
251                  *      for the main view. Example:
252                  *
253                  *              $ tig -- --since=1.month
254                  **/
255                 if (!strcmp(opt, "--")) {
256                         i++;
257                         break;
258                 }
259
260                 /**
261                  * log [options]::
262                  *      Open log view using the given git log options.
263                  *
264                  * diff [options]::
265                  *      Open diff view using the given git diff options.
266                  *
267                  * show [options]::
268                  *      Open diff view using the given git show options.
269                  **/
270                 if (!strcmp(opt, "log") ||
271                     !strcmp(opt, "diff") ||
272                     !strcmp(opt, "show")) {
273                         opt_request = opt[0] == 'l'
274                                     ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
275                         break;
276                 }
277
278                 /* Make stuff like:
279                  *
280                  *      $ tig tag-1.0..HEAD
281                  *
282                  * work. */
283                 if (opt[0] && opt[0] != '-')
284                         break;
285
286                 die("unknown command '%s'", opt);
287         }
288
289         if (!isatty(STDIN_FILENO)) {
290                 /**
291                  * Pager mode
292                  * ~~~~~~~~~~
293                  * If stdin is a pipe, any log or diff options will be ignored and the
294                  * pager view will be opened loading data from stdin. The pager mode
295                  * can be used for colorizing output from various git commands.
296                  *
297                  * Example on how to colorize the output of git-show(1):
298                  *
299                  *      $ git show | tig
300                  **/
301                 opt_request = REQ_VIEW_PAGER;
302                 opt_pipe = stdin;
303
304         } else if (i < argc) {
305                 size_t buf_size;
306
307                 /**
308                  * Git command options
309                  * ~~~~~~~~~~~~~~~~~~~
310                  * All git command options specified on the command line will
311                  * be passed to the given command and all will be shell quoted
312                  * before used.
313                  *
314                  * NOTE: It is possible to specify options even for the main
315                  * view. If doing this you should not touch the `--pretty`
316                  * option.
317                  *
318                  * Example on how to open the log view and show both author and
319                  * committer information:
320                  *
321                  *      $ tig log --pretty=fuller
322                  **/
323
324                 if (opt_request == REQ_VIEW_MAIN)
325                         /* XXX: This is vulnerable to the user overriding
326                          * options required for the main view parser. */
327                         string_copy(opt_cmd, "git log --stat --pretty=raw");
328                 else
329                         string_copy(opt_cmd, "git");
330                 buf_size = strlen(opt_cmd);
331
332                 while (buf_size < sizeof(opt_cmd) && i < argc) {
333                         opt_cmd[buf_size++] = ' ';
334                         buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
335                 }
336
337                 if (buf_size >= sizeof(opt_cmd))
338                         die("command too long");
339
340                 opt_cmd[buf_size] = 0;
341
342         }
343
344         return TRUE;
345 }
346
347
348 /**
349  * KEYS
350  * ----
351  **/
352
353 #define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp"
354
355 struct keymap {
356         int alias;
357         int request;
358 };
359
360 struct keymap keymap[] = {
361         /**
362          * View switching
363          * ~~~~~~~~~~~~~~
364          * d::
365          *      Switch to diff view.
366          * l::
367          *      Switch to log view.
368          * m::
369          *      Switch to main view.
370          * p::
371          *      Switch to pager view.
372          * h::
373          *      Show man page.
374          * Return::
375          *      If in main view split the view
376          *      and show the diff in the bottom view.
377          * Tab::
378          *      Switch to next view.
379          **/
380         { 'm',          REQ_VIEW_MAIN },
381         { 'd',          REQ_VIEW_DIFF },
382         { 'l',          REQ_VIEW_LOG },
383         { 'p',          REQ_VIEW_PAGER },
384         { 'h',          REQ_VIEW_HELP },
385
386         { KEY_TAB,      REQ_VIEW_NEXT },
387         { KEY_RETURN,   REQ_ENTER },
388
389         /**
390          * Cursor navigation
391          * ~~~~~~~~~~~~~~~~~
392          * Up, k::
393          *      Move curser one line up.
394          * Down, j::
395          *      Move cursor one line down.
396          * Page Up::
397          *      Move curser one page up.
398          * Page Down::
399          *      Move cursor one page down.
400          * Home::
401          *      Jump to first line.
402          * End::
403          *      Jump to last line.
404          **/
405         { KEY_UP,       REQ_MOVE_UP },
406         { 'k',          REQ_MOVE_UP },
407         { KEY_DOWN,     REQ_MOVE_DOWN },
408         { 'j',          REQ_MOVE_DOWN },
409         { KEY_HOME,     REQ_MOVE_FIRST_LINE },
410         { KEY_END,      REQ_MOVE_LAST_LINE },
411         { KEY_NPAGE,    REQ_MOVE_PAGE_DOWN },
412         { KEY_PPAGE,    REQ_MOVE_PAGE_UP },
413
414         /**
415          * Scrolling
416          * ~~~~~~~~~
417          * Insert::
418          *      Scroll view one line up.
419          * Delete::
420          *      Scroll view one line down.
421          * w::
422          *      Scroll view one page up.
423          * s::
424          *      Scroll view one page down.
425          **/
426         { KEY_IC,       REQ_SCROLL_LINE_UP },
427         { KEY_DC,       REQ_SCROLL_LINE_DOWN },
428         { 'w',          REQ_SCROLL_PAGE_UP },
429         { 's',          REQ_SCROLL_PAGE_DOWN },
430
431         /**
432          * Misc
433          * ~~~~
434          * q, Escape::
435          *      Quit
436          * r::
437          *      Redraw screen.
438          * z::
439          *      Stop all background loading.
440          * v::
441          *      Show version.
442          * n::
443          *      Toggle line numbers on/off.
444          * ':'::
445          *      Open prompt. This allows you to specify what git command to run.
446          *      Example:
447          *
448          *      :log -p
449          *
450          **/
451         { KEY_ESC,      REQ_QUIT },
452         { 'q',          REQ_QUIT },
453         { 'z',          REQ_STOP_LOADING },
454         { 'v',          REQ_SHOW_VERSION },
455         { 'r',          REQ_SCREEN_REDRAW },
456         { 'n',          REQ_TOGGLE_LINE_NUMBERS },
457         { ':',          REQ_PROMPT },
458
459         /* wgetch() with nodelay() enabled returns ERR when there's no input. */
460         { ERR,          REQ_SCREEN_UPDATE },
461
462         /* Use the ncurses SIGWINCH handler. */
463         { KEY_RESIZE,   REQ_SCREEN_RESIZE },
464 };
465
466 static enum request
467 get_request(int key)
468 {
469         int i;
470
471         for (i = 0; i < ARRAY_SIZE(keymap); i++)
472                 if (keymap[i].alias == key)
473                         return keymap[i].request;
474
475         return (enum request) key;
476 }
477
478
479 /*
480  * Line-oriented content detection.
481  */
482
483 #define LINE_INFO \
484 /*   Line type     String to match      Foreground      Background      Attributes
485  *   ---------     ---------------      ----------      ----------      ---------- */ \
486 /* Diff markup */ \
487 LINE(DIFF,         "diff --git ",       COLOR_YELLOW,   COLOR_DEFAULT,  0), \
488 LINE(DIFF_INDEX,   "index ",            COLOR_BLUE,     COLOR_DEFAULT,  0), \
489 LINE(DIFF_CHUNK,   "@@",                COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
490 LINE(DIFF_ADD,     "+",                 COLOR_GREEN,    COLOR_DEFAULT,  0), \
491 LINE(DIFF_DEL,     "-",                 COLOR_RED,      COLOR_DEFAULT,  0), \
492 LINE(DIFF_OLDMODE, "old mode ",         COLOR_YELLOW,   COLOR_DEFAULT,  0), \
493 LINE(DIFF_NEWMODE, "new mode ",         COLOR_YELLOW,   COLOR_DEFAULT,  0), \
494 LINE(DIFF_COPY,    "copy ",             COLOR_YELLOW,   COLOR_DEFAULT,  0), \
495 LINE(DIFF_RENAME,  "rename ",           COLOR_YELLOW,   COLOR_DEFAULT,  0), \
496 LINE(DIFF_SIM,     "similarity ",       COLOR_YELLOW,   COLOR_DEFAULT,  0), \
497 LINE(DIFF_DISSIM,  "dissimilarity ",    COLOR_YELLOW,   COLOR_DEFAULT,  0), \
498 /* Pretty print commit header */ \
499 LINE(PP_AUTHOR,    "Author: ",          COLOR_CYAN,     COLOR_DEFAULT,  0), \
500 LINE(PP_COMMIT,    "Commit: ",          COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
501 LINE(PP_MERGE,     "Merge: ",           COLOR_BLUE,     COLOR_DEFAULT,  0), \
502 LINE(PP_DATE,      "Date:   ",          COLOR_YELLOW,   COLOR_DEFAULT,  0), \
503 LINE(PP_ADATE,     "AuthorDate: ",      COLOR_YELLOW,   COLOR_DEFAULT,  0), \
504 LINE(PP_CDATE,     "CommitDate: ",      COLOR_YELLOW,   COLOR_DEFAULT,  0), \
505 /* Raw commit header */ \
506 LINE(COMMIT,       "commit ",           COLOR_GREEN,    COLOR_DEFAULT,  0), \
507 LINE(PARENT,       "parent ",           COLOR_BLUE,     COLOR_DEFAULT,  0), \
508 LINE(TREE,         "tree ",             COLOR_BLUE,     COLOR_DEFAULT,  0), \
509 LINE(AUTHOR,       "author ",           COLOR_CYAN,     COLOR_DEFAULT,  0), \
510 LINE(COMMITTER,    "committer ",        COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
511 /* Misc */ \
512 LINE(DIFF_TREE,    "diff-tree ",        COLOR_BLUE,     COLOR_DEFAULT,  0), \
513 LINE(SIGNOFF,      "    Signed-off-by", COLOR_YELLOW,   COLOR_DEFAULT,  0), \
514 /* UI colors */ \
515 LINE(DEFAULT,      "",                  COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL), \
516 LINE(CURSOR,       "",                  COLOR_WHITE,    COLOR_GREEN,    A_BOLD), \
517 LINE(STATUS,       "",                  COLOR_GREEN,    COLOR_DEFAULT,  0), \
518 LINE(TITLE_BLUR,   "",                  COLOR_WHITE,    COLOR_BLUE,     0), \
519 LINE(TITLE_FOCUS,  "",                  COLOR_WHITE,    COLOR_BLUE,     A_BOLD), \
520 LINE(MAIN_DATE,    "",                  COLOR_BLUE,     COLOR_DEFAULT,  0), \
521 LINE(MAIN_AUTHOR,  "",                  COLOR_GREEN,    COLOR_DEFAULT,  0), \
522 LINE(MAIN_COMMIT,  "",                  COLOR_DEFAULT,  COLOR_DEFAULT,  0), \
523 LINE(MAIN_DELIM,   "",                  COLOR_MAGENTA,  COLOR_DEFAULT,  0),
524
525 enum line_type {
526 #define LINE(type, line, fg, bg, attr) \
527         LINE_##type
528         LINE_INFO
529 #undef  LINE
530 };
531
532 struct line_info {
533         char *line;             /* The start of line to match. */
534         int linelen;            /* Size of string to match. */
535         int fg, bg, attr;       /* Color and text attributes for the lines. */
536 };
537
538 static struct line_info line_info[] = {
539 #define LINE(type, line, fg, bg, attr) \
540         { (line), STRING_SIZE(line), (fg), (bg), (attr) }
541         LINE_INFO
542 #undef  LINE
543 };
544
545 static enum line_type
546 get_line_type(char *line)
547 {
548         int linelen = strlen(line);
549         enum line_type type;
550
551         for (type = 0; type < ARRAY_SIZE(line_info); type++)
552                 /* Case insensitive search matches Signed-off-by lines better. */
553                 if (linelen >= line_info[type].linelen &&
554                     !strncasecmp(line_info[type].line, line, line_info[type].linelen))
555                         return type;
556
557         return LINE_DEFAULT;
558 }
559
560 static inline int
561 get_line_attr(enum line_type type)
562 {
563         assert(type < ARRAY_SIZE(line_info));
564         return COLOR_PAIR(type) | line_info[type].attr;
565 }
566
567 static void
568 init_colors(void)
569 {
570         int default_bg = COLOR_BLACK;
571         int default_fg = COLOR_WHITE;
572         enum line_type type;
573
574         start_color();
575
576         if (use_default_colors() != ERR) {
577                 default_bg = -1;
578                 default_fg = -1;
579         }
580
581         for (type = 0; type < ARRAY_SIZE(line_info); type++) {
582                 struct line_info *info = &line_info[type];
583                 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
584                 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
585
586                 init_pair(type, fg, bg);
587         }
588 }
589
590
591 /**
592  * ENVIRONMENT VARIABLES
593  * ---------------------
594  * It is possible to alter which commands are used for the different views.
595  * If for example you prefer commits in the main to be sorted by date and
596  * only show 500 commits, use:
597  *
598  *      $ TIG_MAIN_CMD="git log --date-order -n500 --pretty=raw %s" tig
599  *
600  * Or set the variable permanently in your environment.
601  *
602  * Notice, how `%s` is used to specify the commit reference. There can
603  * be a maximum of 5 `%s` ref specifications.
604  *
605  * TIG_DIFF_CMD::
606  *      The command used for the diff view. By default, git show is used
607  *      as a backend.
608  *
609  * TIG_LOG_CMD::
610  *      The command used for the log view.
611  *
612  * TIG_MAIN_CMD::
613  *      The command used for the main view. Note, you must always specify
614  *      the option: `--pretty=raw` since the main view parser expects to
615  *      read that format.
616  **/
617
618 #define TIG_DIFF_CMD \
619         "git show --patch-with-stat --find-copies-harder -B -C %s"
620
621 #define TIG_LOG_CMD     \
622         "git log --cc --stat -n100 %s"
623
624 #define TIG_MAIN_CMD \
625         "git log --topo-order --stat --pretty=raw %s"
626
627 /* We silently ignore that the following are also exported. */
628
629 #define TIG_HELP_CMD \
630         "man tig 2> /dev/null"
631
632 #define TIG_PAGER_CMD \
633         ""
634
635
636 /*
637  * Viewer
638  */
639
640 struct view {
641         const char *name;       /* View name */
642         char *cmd_fmt;          /* Default command line format */
643         char *cmd_env;          /* Command line set via environment */
644         char *id;               /* Points to either of ref_{head,commit} */
645         size_t objsize;         /* Size of objects in the line index */
646
647         struct view_ops {
648                 /* Draw one line; @lineno must be < view->height. */
649                 bool (*draw)(struct view *view, unsigned int lineno);
650                 /* Read one line; updates view->line. */
651                 bool (*read)(struct view *view, char *line);
652                 /* Depending on view, change display based on current line. */
653                 bool (*enter)(struct view *view);
654         } *ops;
655
656         char cmd[SIZEOF_CMD];   /* Command buffer */
657         char ref[SIZEOF_REF];   /* Hovered commit reference */
658         char vid[SIZEOF_REF];   /* View ID. Set to id member when updating. */
659
660         int height, width;      /* The width and height of the main window */
661         WINDOW *win;            /* The main window */
662         WINDOW *title;          /* The title window living below the main window */
663
664         /* Navigation */
665         unsigned long offset;   /* Offset of the window top */
666         unsigned long lineno;   /* Current line number */
667
668         /* Buffering */
669         unsigned long lines;    /* Total number of lines */
670         void **line;            /* Line index; each line contains user data */
671         unsigned int digits;    /* Number of digits in the lines member. */
672
673         /* Loading */
674         FILE *pipe;
675         time_t start_time;
676 };
677
678 static struct view_ops pager_ops;
679 static struct view_ops main_ops;
680
681 char ref_head[SIZEOF_REF]       = "HEAD";
682 char ref_commit[SIZEOF_REF]     = "HEAD";
683
684 #define VIEW_STR(name, cmd, env, ref, objsize, ops) \
685         { name, cmd, #env, ref, objsize, ops }
686
687 #define VIEW_(id, name, ops, ref, objsize) \
688         VIEW_STR(name, TIG_##id##_CMD,  TIG_##id##_CMD, ref, objsize, ops)
689
690 static struct view views[] = {
691         VIEW_(MAIN,  "main",  &main_ops,  ref_head,   sizeof(struct commit)),
692         VIEW_(DIFF,  "diff",  &pager_ops, ref_commit, sizeof(char)),
693         VIEW_(LOG,   "log",   &pager_ops, ref_head,   sizeof(char)),
694         VIEW_(HELP,  "help",  &pager_ops, ref_head,   sizeof(char)),
695         VIEW_(PAGER, "pager", &pager_ops, "static",   sizeof(char)),
696 };
697
698 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
699
700 /* The display array of active views and the index of the current view. */
701 static struct view *display[2];
702 static unsigned int current_view;
703
704 #define foreach_view(view, i) \
705         for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
706
707
708 static void
709 redraw_view_from(struct view *view, int lineno)
710 {
711         assert(0 <= lineno && lineno < view->height);
712
713         for (; lineno < view->height; lineno++) {
714                 if (!view->ops->draw(view, lineno))
715                         break;
716         }
717
718         redrawwin(view->win);
719         wrefresh(view->win);
720 }
721
722 static void
723 redraw_view(struct view *view)
724 {
725         wclear(view->win);
726         redraw_view_from(view, 0);
727 }
728
729 static void
730 resize_display(void)
731 {
732         int offset, i;
733         struct view *base = display[0];
734         struct view *view = display[1] ? display[1] : display[0];
735
736         /* Setup window dimensions */
737
738         getmaxyx(stdscr, base->height, base->width);
739
740         /* Make room for the status window. */
741         base->height -= 1;
742
743         if (view != base) {
744                 /* Horizontal split. */
745                 view->width   = base->width;
746                 view->height  = SCALE_SPLIT_VIEW(base->height);
747                 base->height -= view->height;
748
749                 /* Make room for the title bar. */
750                 view->height -= 1;
751         }
752
753         /* Make room for the title bar. */
754         base->height -= 1;
755
756         offset = 0;
757
758         foreach_view (view, i) {
759                 if (!view->win) {
760                         view->win = newwin(view->height, 0, offset, 0);
761                         if (!view->win)
762                                 die("Failed to create %s view", view->name);
763
764                         scrollok(view->win, TRUE);
765
766                         view->title = newwin(1, 0, offset + view->height, 0);
767                         if (!view->title)
768                                 die("Failed to create title window");
769
770                 } else {
771                         wresize(view->win, view->height, view->width);
772                         mvwin(view->win,   offset, 0);
773                         mvwin(view->title, offset + view->height, 0);
774                         wrefresh(view->win);
775                 }
776
777                 offset += view->height + 1;
778         }
779 }
780
781 static void
782 update_view_title(struct view *view)
783 {
784         if (view == display[current_view])
785                 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
786         else
787                 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
788
789         werase(view->title);
790         wmove(view->title, 0, 0);
791
792         /* [main] ref: 334b506... - commit 6 of 4383 (0%) */
793
794         if (*view->ref)
795                 wprintw(view->title, "[%s] ref: %s", view->name, view->ref);
796         else
797                 wprintw(view->title, "[%s]", view->name);
798
799         if (view->lines) {
800                 char *type = view == VIEW(REQ_VIEW_MAIN) ? "commit" : "line";
801
802                 wprintw(view->title, " - %s %d of %d (%d%%)",
803                         type,
804                         view->lineno + 1,
805                         view->lines,
806                         (view->lineno + 1) * 100 / view->lines);
807         }
808
809         wrefresh(view->title);
810 }
811
812 /*
813  * Navigation
814  */
815
816 /* Scrolling backend */
817 static void
818 do_scroll_view(struct view *view, int lines)
819 {
820         /* The rendering expects the new offset. */
821         view->offset += lines;
822
823         assert(0 <= view->offset && view->offset < view->lines);
824         assert(lines);
825
826         /* Redraw the whole screen if scrolling is pointless. */
827         if (view->height < ABS(lines)) {
828                 redraw_view(view);
829
830         } else {
831                 int line = lines > 0 ? view->height - lines : 0;
832                 int end = line + ABS(lines);
833
834                 wscrl(view->win, lines);
835
836                 for (; line < end; line++) {
837                         if (!view->ops->draw(view, line))
838                                 break;
839                 }
840         }
841
842         /* Move current line into the view. */
843         if (view->lineno < view->offset) {
844                 view->lineno = view->offset;
845                 view->ops->draw(view, 0);
846
847         } else if (view->lineno >= view->offset + view->height) {
848                 view->lineno = view->offset + view->height - 1;
849                 view->ops->draw(view, view->lineno - view->offset);
850         }
851
852         assert(view->offset <= view->lineno && view->lineno < view->lines);
853
854         redrawwin(view->win);
855         wrefresh(view->win);
856         report("");
857 }
858
859 /* Scroll frontend */
860 static void
861 scroll_view(struct view *view, enum request request)
862 {
863         int lines = 1;
864
865         switch (request) {
866         case REQ_SCROLL_PAGE_DOWN:
867                 lines = view->height;
868         case REQ_SCROLL_LINE_DOWN:
869                 if (view->offset + lines > view->lines)
870                         lines = view->lines - view->offset;
871
872                 if (lines == 0 || view->offset + view->height >= view->lines) {
873                         report("Cannot scroll beyond the last line");
874                         return;
875                 }
876                 break;
877
878         case REQ_SCROLL_PAGE_UP:
879                 lines = view->height;
880         case REQ_SCROLL_LINE_UP:
881                 if (lines > view->offset)
882                         lines = view->offset;
883
884                 if (lines == 0) {
885                         report("Cannot scroll beyond the first line");
886                         return;
887                 }
888
889                 lines = -lines;
890                 break;
891
892         default:
893                 die("request %d not handled in switch", request);
894         }
895
896         do_scroll_view(view, lines);
897 }
898
899 /* Cursor moving */
900 static void
901 move_view(struct view *view, enum request request)
902 {
903         int steps;
904
905         switch (request) {
906         case REQ_MOVE_FIRST_LINE:
907                 steps = -view->lineno;
908                 break;
909
910         case REQ_MOVE_LAST_LINE:
911                 steps = view->lines - view->lineno - 1;
912                 break;
913
914         case REQ_MOVE_PAGE_UP:
915                 steps = view->height > view->lineno
916                       ? -view->lineno : -view->height;
917                 break;
918
919         case REQ_MOVE_PAGE_DOWN:
920                 steps = view->lineno + view->height >= view->lines
921                       ? view->lines - view->lineno - 1 : view->height;
922                 break;
923
924         case REQ_MOVE_UP:
925                 steps = -1;
926                 break;
927
928         case REQ_MOVE_DOWN:
929                 steps = 1;
930                 break;
931
932         default:
933                 die("request %d not handled in switch", request);
934         }
935
936         if (steps <= 0 && view->lineno == 0) {
937                 report("Cannot move beyond the first line");
938                 return;
939
940         } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
941                 report("Cannot move beyond the last line");
942                 return;
943         }
944
945         /* Move the current line */
946         view->lineno += steps;
947         assert(0 <= view->lineno && view->lineno < view->lines);
948
949         /* Repaint the old "current" line if we be scrolling */
950         if (ABS(steps) < view->height) {
951                 int prev_lineno = view->lineno - steps - view->offset;
952
953                 wmove(view->win, prev_lineno, 0);
954                 wclrtoeol(view->win);
955                 view->ops->draw(view, prev_lineno);
956         }
957
958         /* Check whether the view needs to be scrolled */
959         if (view->lineno < view->offset ||
960             view->lineno >= view->offset + view->height) {
961                 if (steps < 0 && -steps > view->offset) {
962                         steps = -view->offset;
963
964                 } else if (steps > 0) {
965                         if (view->lineno == view->lines - 1 &&
966                             view->lines > view->height) {
967                                 steps = view->lines - view->offset - 1;
968                                 if (steps >= view->height)
969                                         steps -= view->height - 1;
970                         }
971                 }
972
973                 do_scroll_view(view, steps);
974                 return;
975         }
976
977         /* Draw the current line */
978         view->ops->draw(view, view->lineno - view->offset);
979
980         redrawwin(view->win);
981         wrefresh(view->win);
982         report("");
983 }
984
985
986 /*
987  * Incremental updating
988  */
989
990 static bool
991 begin_update(struct view *view)
992 {
993         char *id = view->id;
994
995         if (opt_cmd[0]) {
996                 string_copy(view->cmd, opt_cmd);
997                 opt_cmd[0] = 0;
998                 /* When running random commands, the view ref could have become
999                  * invalid so clear it. */
1000                 view->ref[0] = 0;
1001         } else {
1002                 char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1003
1004                 if (snprintf(view->cmd, sizeof(view->cmd), format,
1005                              id, id, id, id, id) >= sizeof(view->cmd))
1006                         return FALSE;
1007         }
1008
1009         /* Special case for the pager view. */
1010         if (opt_pipe) {
1011                 view->pipe = opt_pipe;
1012                 opt_pipe = NULL;
1013         } else {
1014                 view->pipe = popen(view->cmd, "r");
1015         }
1016
1017         if (!view->pipe)
1018                 return FALSE;
1019
1020         set_nonblocking_input(TRUE);
1021
1022         view->offset = 0;
1023         view->lines  = 0;
1024         view->lineno = 0;
1025         string_copy(view->vid, id);
1026
1027         if (view->line) {
1028                 int i;
1029
1030                 for (i = 0; i < view->lines; i++)
1031                         if (view->line[i])
1032                                 free(view->line[i]);
1033
1034                 free(view->line);
1035                 view->line = NULL;
1036         }
1037
1038         view->start_time = time(NULL);
1039
1040         return TRUE;
1041 }
1042
1043 static void
1044 end_update(struct view *view)
1045 {
1046         if (!view->pipe)
1047                 return;
1048         set_nonblocking_input(FALSE);
1049         if (view->pipe == stdin)
1050                 fclose(view->pipe);
1051         else
1052                 pclose(view->pipe);
1053         view->pipe = NULL;
1054 }
1055
1056 static bool
1057 update_view(struct view *view)
1058 {
1059         char buffer[BUFSIZ];
1060         char *line;
1061         void **tmp;
1062         /* The number of lines to read. If too low it will cause too much
1063          * redrawing (and possible flickering), if too high responsiveness
1064          * will suffer. */
1065         unsigned long lines = view->height;
1066         int redraw_from = -1;
1067
1068         if (!view->pipe)
1069                 return TRUE;
1070
1071         /* Only redraw if lines are visible. */
1072         if (view->offset + view->height >= view->lines)
1073                 redraw_from = view->lines - view->offset;
1074
1075         tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines));
1076         if (!tmp)
1077                 goto alloc_error;
1078
1079         view->line = tmp;
1080
1081         while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1082                 int linelen;
1083
1084                 linelen = strlen(line);
1085                 if (linelen)
1086                         line[linelen - 1] = 0;
1087
1088                 if (!view->ops->read(view, line))
1089                         goto alloc_error;
1090
1091                 if (lines-- == 1)
1092                         break;
1093         }
1094
1095         {
1096                 int digits;
1097
1098                 lines = view->lines;
1099                 for (digits = 0; lines; digits++)
1100                         lines /= 10;
1101
1102                 /* Keep the displayed view in sync with line number scaling. */
1103                 if (digits != view->digits) {
1104                         view->digits = digits;
1105                         redraw_from = 0;
1106                 }
1107         }
1108
1109         if (redraw_from >= 0) {
1110                 /* If this is an incremental update, redraw the previous line
1111                  * since for commits some members could have changed when
1112                  * loading the main view. */
1113                 if (redraw_from > 0)
1114                         redraw_from--;
1115
1116                 /* Incrementally draw avoids flickering. */
1117                 redraw_view_from(view, redraw_from);
1118         }
1119
1120         /* Update the title _after_ the redraw so that if the redraw picks up a
1121          * commit reference in view->ref it'll be available here. */
1122         update_view_title(view);
1123
1124         if (ferror(view->pipe)) {
1125                 report("Failed to read: %s", strerror(errno));
1126                 goto end;
1127
1128         } else if (feof(view->pipe)) {
1129                 time_t secs = time(NULL) - view->start_time;
1130
1131                 if (view == VIEW(REQ_VIEW_HELP)) {
1132                         report("%s", HELP);
1133                         goto end;
1134                 }
1135
1136                 report("Loaded %d lines in %ld second%s", view->lines, secs,
1137                        secs == 1 ? "" : "s");
1138                 goto end;
1139         }
1140
1141         return TRUE;
1142
1143 alloc_error:
1144         report("Allocation failure");
1145
1146 end:
1147         end_update(view);
1148         return FALSE;
1149 }
1150
1151 enum open_flags {
1152         OPEN_DEFAULT = 0,       /* Use default view switching. */
1153         OPEN_SPLIT = 1,         /* Split current view. */
1154         OPEN_BACKGROUNDED = 2,  /* Backgrounded. */
1155         OPEN_RELOAD = 4,        /* Reload view even if it is the current. */
1156 };
1157
1158 static void
1159 open_view(struct view *prev, enum request request, enum open_flags flags)
1160 {
1161         bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1162         bool split = !!(flags & OPEN_SPLIT);
1163         bool reload = !!(flags & OPEN_RELOAD);
1164         struct view *view = VIEW(request);
1165         struct view *displayed;
1166         int nviews;
1167
1168         /* Cycle between displayed views and count the views. */
1169         foreach_view (displayed, nviews) {
1170                 if (prev != view &&
1171                     view == displayed &&
1172                     !strcmp(view->vid, prev->vid)) {
1173                         current_view = nviews;
1174                         /* Blur out the title of the previous view. */
1175                         update_view_title(prev);
1176                         report("Switching to %s view", view->name);
1177                         return;
1178                 }
1179         }
1180
1181         if (view == prev && nviews == 1 && !reload) {
1182                 report("Already in %s view", view->name);
1183                 return;
1184         }
1185
1186         if ((reload || strcmp(view->vid, view->id)) &&
1187             !begin_update(view)) {
1188                 report("Failed to load %s view", view->name);
1189                 return;
1190         }
1191
1192         if (split) {
1193                 display[current_view + 1] = view;
1194                 if (!backgrounded)
1195                         current_view++;
1196         } else {
1197                 /* Maximize the current view. */
1198                 memset(display, 0, sizeof(display));
1199                 current_view = 0;
1200                 display[current_view] = view;
1201         }
1202
1203         resize_display();
1204
1205         if (split && prev->lineno - prev->offset >= prev->height) {
1206                 /* Take the title line into account. */
1207                 int lines = prev->lineno - prev->offset - prev->height + 1;
1208
1209                 /* Scroll the view that was split if the current line is
1210                  * outside the new limited view. */
1211                 do_scroll_view(prev, lines);
1212         }
1213
1214         if (prev && view != prev) {
1215                 /* "Blur" the previous view. */
1216                 update_view_title(prev);
1217
1218                 /* Continue loading split views in the background. */
1219                 if (!split)
1220                         end_update(prev);
1221         }
1222
1223         if (view->pipe) {
1224                 /* Clear the old view and let the incremental updating refill
1225                  * the screen. */
1226                 wclear(view->win);
1227                 report("Loading...");
1228         } else {
1229                 redraw_view(view);
1230                 report("");
1231         }
1232 }
1233
1234
1235 /*
1236  * User request switch noodle
1237  */
1238
1239 static int
1240 view_driver(struct view *view, enum request request)
1241 {
1242         int i;
1243
1244         switch (request) {
1245         case REQ_MOVE_UP:
1246         case REQ_MOVE_DOWN:
1247         case REQ_MOVE_PAGE_UP:
1248         case REQ_MOVE_PAGE_DOWN:
1249         case REQ_MOVE_FIRST_LINE:
1250         case REQ_MOVE_LAST_LINE:
1251                 move_view(view, request);
1252                 break;
1253
1254         case REQ_SCROLL_LINE_DOWN:
1255         case REQ_SCROLL_LINE_UP:
1256         case REQ_SCROLL_PAGE_DOWN:
1257         case REQ_SCROLL_PAGE_UP:
1258                 scroll_view(view, request);
1259                 break;
1260
1261         case REQ_VIEW_MAIN:
1262         case REQ_VIEW_DIFF:
1263         case REQ_VIEW_LOG:
1264         case REQ_VIEW_HELP:
1265         case REQ_VIEW_PAGER:
1266                 open_view(view, request, OPEN_DEFAULT);
1267                 break;
1268
1269         case REQ_ENTER:
1270                 if (!view->lines) {
1271                         report("Nothing to enter");
1272                         break;
1273                 }
1274                 return view->ops->enter(view);
1275
1276         case REQ_VIEW_NEXT:
1277         {
1278                 int nviews = display[1] ? 2 : 1;
1279                 int next_view = (current_view + 1) % nviews;
1280
1281                 if (next_view == current_view) {
1282                         report("Only one view is displayed");
1283                         break;
1284                 }
1285
1286                 current_view = next_view;
1287                 /* Blur out the title of the previous view. */
1288                 update_view_title(view);
1289                 report("Switching to %s view", display[current_view]->name);
1290                 break;
1291         }
1292         case REQ_TOGGLE_LINE_NUMBERS:
1293                 opt_line_number = !opt_line_number;
1294                 redraw_view(view);
1295                 break;
1296
1297         case REQ_PROMPT:
1298                 /* Always reload^Wrerun commands from the prompt. */
1299                 open_view(view, opt_request, OPEN_RELOAD);
1300                 break;
1301
1302         case REQ_STOP_LOADING:
1303                 foreach_view (view, i) {
1304                         if (view->pipe)
1305                                 report("Stopped loaded of %s view", view->name),
1306                         end_update(view);
1307                 }
1308                 break;
1309
1310         case REQ_SHOW_VERSION:
1311                 report("Version: %s", VERSION);
1312                 return TRUE;
1313
1314         case REQ_SCREEN_RESIZE:
1315                 resize_display();
1316                 /* Fall-through */
1317         case REQ_SCREEN_REDRAW:
1318                 foreach_view (view, i) {
1319                         redraw_view(view);
1320                         update_view_title(view);
1321                 }
1322                 break;
1323
1324         case REQ_SCREEN_UPDATE:
1325                 doupdate();
1326                 return TRUE;
1327
1328         case REQ_QUIT:
1329                 return FALSE;
1330
1331         default:
1332                 /* An unknown key will show most commonly used commands. */
1333                 report("%s", HELP);
1334                 return TRUE;
1335         }
1336
1337         return TRUE;
1338 }
1339
1340
1341 /*
1342  * View backend handlers
1343  */
1344
1345 static bool
1346 pager_draw(struct view *view, unsigned int lineno)
1347 {
1348         enum line_type type;
1349         char *line;
1350         int linelen;
1351         int attr;
1352
1353         if (view->offset + lineno >= view->lines)
1354                 return FALSE;
1355
1356         line = view->line[view->offset + lineno];
1357         type = get_line_type(line);
1358
1359         if (view->offset + lineno == view->lineno) {
1360                 if (type == LINE_COMMIT) {
1361                         string_copy(view->ref, line + 7);
1362                         string_copy(ref_commit, view->ref);
1363                 }
1364
1365                 type = LINE_CURSOR;
1366         }
1367
1368         attr = get_line_attr(type);
1369         wattrset(view->win, attr);
1370
1371         linelen = strlen(line);
1372         linelen = MIN(linelen, view->width);
1373
1374         if (opt_line_number) {
1375                 static char indent[] = "                    ";
1376                 unsigned long real_lineno = view->offset + lineno + 1;
1377                 int col = 0;
1378
1379                 if (real_lineno == 1 || (real_lineno % opt_num_interval) == 0)
1380                         mvwprintw(view->win, lineno, 0, "%.*d", view->digits, real_lineno);
1381
1382                 else if (view->digits < sizeof(indent))
1383                         mvwaddnstr(view->win, lineno, 0, indent, view->digits);
1384
1385                 waddstr(view->win, ": ");
1386
1387                 while (line) {
1388                         if (*line == '\t') {
1389                                 waddnstr(view->win, "        ", 8 - (col % 8));
1390                                 col += 8 - (col % 8);
1391                                 line++;
1392
1393                         } else {
1394                                 char *tab = strchr(line, '\t');
1395
1396                                 if (tab)
1397                                         waddnstr(view->win, line, tab - line);
1398                                 else
1399                                         waddstr(view->win, line);
1400                                 col += tab - line;
1401                                 line = tab;
1402                         }
1403                 }
1404                 waddstr(view->win, line);
1405
1406         } else {
1407 #if 0
1408                 /* NOTE: Code for only highlighting the text on the cursor line.
1409                  * Kept since I've not yet decided whether to highlight the
1410                  * entire line or not. --fonseca */
1411                 /* No empty lines makes cursor drawing and clearing implicit. */
1412                 if (!*line)
1413                         line = " ", linelen = 1;
1414 #endif
1415                 mvwaddnstr(view->win, lineno, 0, line, linelen);
1416         }
1417
1418         /* Paint the rest of the line if it's the cursor line. */
1419         if (type == LINE_CURSOR)
1420                 wchgat(view->win, -1, 0, type, NULL);
1421
1422         return TRUE;
1423 }
1424
1425 static bool
1426 pager_read(struct view *view, char *line)
1427 {
1428         view->line[view->lines] = strdup(line);
1429         if (!view->line[view->lines])
1430                 return FALSE;
1431
1432         view->lines++;
1433         return TRUE;
1434 }
1435
1436 static bool
1437 pager_enter(struct view *view)
1438 {
1439         char *line = view->line[view->lineno];
1440
1441         if (get_line_type(line) == LINE_COMMIT) {
1442                 open_view(view, REQ_VIEW_DIFF, OPEN_DEFAULT);
1443         }
1444
1445         return TRUE;
1446 }
1447
1448 static struct view_ops pager_ops = {
1449         pager_draw,
1450         pager_read,
1451         pager_enter,
1452 };
1453
1454
1455 static bool
1456 main_draw(struct view *view, unsigned int lineno)
1457 {
1458         char buf[DATE_COLS + 1];
1459         struct commit *commit;
1460         enum line_type type;
1461         int cols = 0;
1462         size_t timelen;
1463
1464         if (view->offset + lineno >= view->lines)
1465                 return FALSE;
1466
1467         commit = view->line[view->offset + lineno];
1468         if (!*commit->author)
1469                 return FALSE;
1470
1471         if (view->offset + lineno == view->lineno) {
1472                 string_copy(view->ref, commit->id);
1473                 string_copy(ref_commit, view->ref);
1474                 type = LINE_CURSOR;
1475         } else {
1476                 type = LINE_MAIN_COMMIT;
1477         }
1478
1479         wmove(view->win, lineno, cols);
1480         wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1481
1482         timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1483         waddnstr(view->win, buf, timelen);
1484         waddstr(view->win, " ");
1485
1486         cols += DATE_COLS;
1487         wmove(view->win, lineno, cols);
1488         wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1489
1490         if (strlen(commit->author) > 19) {
1491                 waddnstr(view->win, commit->author, 18);
1492                 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1493                 waddch(view->win, '~');
1494         } else {
1495                 waddstr(view->win, commit->author);
1496         }
1497
1498         cols += 20;
1499         wattrset(view->win, A_NORMAL);
1500         mvwaddch(view->win, lineno, cols, ACS_LTEE);
1501         wattrset(view->win, get_line_attr(type));
1502         mvwaddstr(view->win, lineno, cols + 2, commit->title);
1503         wattrset(view->win, A_NORMAL);
1504
1505         return TRUE;
1506 }
1507
1508 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1509 static bool
1510 main_read(struct view *view, char *line)
1511 {
1512         enum line_type type = get_line_type(line);
1513         struct commit *commit;
1514
1515         switch (type) {
1516         case LINE_COMMIT:
1517                 commit = calloc(1, sizeof(struct commit));
1518                 if (!commit)
1519                         return FALSE;
1520
1521                 line += STRING_SIZE("commit ");
1522
1523                 view->line[view->lines++] = commit;
1524                 string_copy(commit->id, line);
1525                 break;
1526
1527         case LINE_AUTHOR:
1528         {
1529                 char *ident = line + STRING_SIZE("author ");
1530                 char *end = strchr(ident, '<');
1531
1532                 if (end) {
1533                         for (; end > ident && isspace(end[-1]); end--) ;
1534                         *end = 0;
1535                 }
1536
1537                 commit = view->line[view->lines - 1];
1538                 string_copy(commit->author, ident);
1539
1540                 /* Parse epoch and timezone */
1541                 if (end) {
1542                         char *secs = strchr(end + 1, '>');
1543                         char *zone;
1544                         time_t time;
1545
1546                         if (!secs || secs[1] != ' ')
1547                                 break;
1548
1549                         secs += 2;
1550                         time = (time_t) atol(secs);
1551                         zone = strchr(secs, ' ');
1552                         if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1553                                 long tz;
1554
1555                                 zone++;
1556                                 tz  = ('0' - zone[1]) * 60 * 60 * 10;
1557                                 tz += ('0' - zone[2]) * 60 * 60;
1558                                 tz += ('0' - zone[3]) * 60;
1559                                 tz += ('0' - zone[4]) * 60;
1560
1561                                 if (zone[0] == '-')
1562                                         tz = -tz;
1563
1564                                 time -= tz;
1565                         }
1566                         gmtime_r(&time, &commit->time);
1567                 }
1568                 break;
1569         }
1570         default:
1571                 /* We should only ever end up here if there has already been a
1572                  * commit line, however, be safe. */
1573                 if (view->lines == 0)
1574                         break;
1575
1576                 /* Fill in the commit title if it has not already been set. */
1577                 commit = view->line[view->lines - 1];
1578                 if (commit->title[0])
1579                         break;
1580
1581                 /* Require titles to start with a non-space character at the
1582                  * offset used by git log. */
1583                 /* FIXME: More gracefull handling of titles; append "..." to
1584                  * shortened titles, etc. */
1585                 if (strncmp(line, "    ", 4) ||
1586                     isspace(line[4]))
1587                         break;
1588
1589                 string_copy(commit->title, line + 4);
1590         }
1591
1592         return TRUE;
1593 }
1594
1595 static bool
1596 main_enter(struct view *view)
1597 {
1598         open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT | OPEN_BACKGROUNDED);
1599         return TRUE;
1600 }
1601
1602 static struct view_ops main_ops = {
1603         main_draw,
1604         main_read,
1605         main_enter,
1606 };
1607
1608 /*
1609  * Status management
1610  */
1611
1612 /* Whether or not the curses interface has been initialized. */
1613 bool cursed = FALSE;
1614
1615 /* The status window is used for polling keystrokes. */
1616 static WINDOW *status_win;
1617
1618 /* Update status and title window. */
1619 static void
1620 report(const char *msg, ...)
1621 {
1622         va_list args;
1623
1624         /* Update the title window first, so the cursor ends up in the status
1625          * window. */
1626         update_view_title(display[current_view]);
1627
1628         va_start(args, msg);
1629
1630         werase(status_win);
1631         wmove(status_win, 0, 0);
1632         if (*msg)
1633                 vwprintw(status_win, msg, args);
1634         wrefresh(status_win);
1635
1636         va_end(args);
1637 }
1638
1639 /* Controls when nodelay should be in effect when polling user input. */
1640 static void
1641 set_nonblocking_input(bool loading)
1642 {
1643         /* The number of loading views. */
1644         static unsigned int nloading;
1645
1646         if ((loading == FALSE && nloading-- == 1) ||
1647             (loading == TRUE  && nloading++ == 0))
1648                 nodelay(status_win, loading);
1649 }
1650
1651 static void
1652 init_display(void)
1653 {
1654         int x, y;
1655
1656         /* Initialize the curses library */
1657         if (isatty(STDIN_FILENO)) {
1658                 cursed = !!initscr();
1659         } else {
1660                 /* Leave stdin and stdout alone when acting as a pager. */
1661                 FILE *io = fopen("/dev/tty", "r+");
1662
1663                 cursed = !!newterm(NULL, io, io);
1664         }
1665
1666         if (!cursed)
1667                 die("Failed to initialize curses");
1668
1669         nonl();         /* Tell curses not to do NL->CR/NL on output */
1670         cbreak();       /* Take input chars one at a time, no wait for \n */
1671         noecho();       /* Don't echo input */
1672         leaveok(stdscr, TRUE);
1673
1674         if (has_colors())
1675                 init_colors();
1676
1677         getmaxyx(stdscr, y, x);
1678         status_win = newwin(1, 0, y - 1, 0);
1679         if (!status_win)
1680                 die("Failed to create status window");
1681
1682         /* Enable keyboard mapping */
1683         keypad(status_win, TRUE);
1684         wbkgdset(status_win, get_line_attr(LINE_STATUS));
1685 }
1686
1687 /*
1688  * Main
1689  */
1690
1691 static void
1692 quit(int sig)
1693 {
1694         /* XXX: Restore tty modes and let the OS cleanup the rest! */
1695         if (cursed)
1696                 endwin();
1697         exit(0);
1698 }
1699
1700 static void die(const char *err, ...)
1701 {
1702         va_list args;
1703
1704         endwin();
1705
1706         va_start(args, err);
1707         fputs("tig: ", stderr);
1708         vfprintf(stderr, err, args);
1709         fputs("\n", stderr);
1710         va_end(args);
1711
1712         exit(1);
1713 }
1714
1715 #include <readline/readline.h>
1716
1717 int
1718 main(int argc, char *argv[])
1719 {
1720         struct view *view;
1721         enum request request;
1722         size_t i;
1723
1724         signal(SIGINT, quit);
1725
1726         if (!parse_options(argc, argv))
1727                 return 0;
1728
1729         for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1730                 view->cmd_env = getenv(view->cmd_env);
1731
1732         request = opt_request;
1733
1734         init_display();
1735
1736         while (view_driver(display[current_view], request)) {
1737                 int key;
1738                 int i;
1739
1740                 foreach_view (view, i)
1741                         update_view(view);
1742
1743                 /* Refresh, accept single keystroke of input */
1744                 key = wgetch(status_win);
1745                 request = get_request(key);
1746
1747                 /* Some low-level request handling. This keeps handling of
1748                  * status_win restricted. */
1749                 switch (request) {
1750                 case REQ_PROMPT:
1751                         report(":");
1752                         /* Temporarily switch to line-oriented and echoed
1753                          * input. */
1754                         nocbreak();
1755                         echo();
1756
1757                         if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
1758                                 memcpy(opt_cmd, "git ", 4);
1759                                 opt_request = REQ_VIEW_PAGER;
1760                         } else {
1761                                 request = ERR;
1762                         }
1763
1764                         noecho();
1765                         cbreak();
1766                         break;
1767
1768                 case REQ_SCREEN_RESIZE:
1769                 {
1770                         int height, width;
1771
1772                         getmaxyx(stdscr, height, width);
1773
1774                         /* Resize the status view and let the view driver take
1775                          * care of resizing the displayed views. */
1776                         wresize(status_win, 1, width);
1777                         mvwin(status_win, height - 1, 0);
1778                         wrefresh(status_win);
1779                         break;
1780                 }
1781                 default:
1782                         break;
1783                 }
1784         }
1785
1786         quit(0);
1787
1788         return 0;
1789 }
1790
1791 /**
1792  * TODO
1793  * ----
1794  * Features that should be explored.
1795  *
1796  * - Searching.
1797  *
1798  * - Locale support.
1799  *
1800  * COPYRIGHT
1801  * ---------
1802  * Copyright (c) Jonas Fonseca <fonseca@diku.dk>, 2006
1803  *
1804  * This program is free software; you can redistribute it and/or modify
1805  * it under the terms of the GNU General Public License as published by
1806  * the Free Software Foundation; either version 2 of the License, or
1807  * (at your option) any later version.
1808  *
1809  * SEE ALSO
1810  * --------
1811  * [verse]
1812  * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
1813  * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
1814  * gitk(1): git repository browser written using tcl/tk,
1815  * gitview(1): git repository browser written using python/gtk.
1816  **/