Implement support for terminal resizing
[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         /* Use the ncurses SIGWINCH handler. */
462         { KEY_RESIZE,   REQ_SCREEN_RESIZE },
463 };
464
465 static enum request
466 get_request(int key)
467 {
468         int i;
469
470         for (i = 0; i < ARRAY_SIZE(keymap); i++)
471                 if (keymap[i].alias == key)
472                         return keymap[i].request;
473
474         return (enum request) key;
475 }
476
477
478 /*
479  * Line-oriented content detection.
480  */
481
482 #define LINE_INFO \
483 /*   Line type     String to match      Foreground      Background      Attributes
484  *   ---------     ---------------      ----------      ----------      ---------- */ \
485 /* Diff markup */ \
486 LINE(DIFF,         "diff --git ",       COLOR_YELLOW,   COLOR_DEFAULT,  0), \
487 LINE(DIFF_INDEX,   "index ",            COLOR_BLUE,     COLOR_DEFAULT,  0), \
488 LINE(DIFF_CHUNK,   "@@",                COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
489 LINE(DIFF_ADD,     "+",                 COLOR_GREEN,    COLOR_DEFAULT,  0), \
490 LINE(DIFF_DEL,     "-",                 COLOR_RED,      COLOR_DEFAULT,  0), \
491 LINE(DIFF_OLDMODE, "old mode ",         COLOR_YELLOW,   COLOR_DEFAULT,  0), \
492 LINE(DIFF_NEWMODE, "new mode ",         COLOR_YELLOW,   COLOR_DEFAULT,  0), \
493 LINE(DIFF_COPY,    "copy ",             COLOR_YELLOW,   COLOR_DEFAULT,  0), \
494 LINE(DIFF_RENAME,  "rename ",           COLOR_YELLOW,   COLOR_DEFAULT,  0), \
495 LINE(DIFF_SIM,     "similarity ",       COLOR_YELLOW,   COLOR_DEFAULT,  0), \
496 LINE(DIFF_DISSIM,  "dissimilarity ",    COLOR_YELLOW,   COLOR_DEFAULT,  0), \
497 /* Pretty print commit header */ \
498 LINE(PP_AUTHOR,    "Author: ",          COLOR_CYAN,     COLOR_DEFAULT,  0), \
499 LINE(PP_COMMIT,    "Commit: ",          COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
500 LINE(PP_MERGE,     "Merge: ",           COLOR_BLUE,     COLOR_DEFAULT,  0), \
501 LINE(PP_DATE,      "Date:   ",          COLOR_YELLOW,   COLOR_DEFAULT,  0), \
502 LINE(PP_ADATE,     "AuthorDate: ",      COLOR_YELLOW,   COLOR_DEFAULT,  0), \
503 LINE(PP_CDATE,     "CommitDate: ",      COLOR_YELLOW,   COLOR_DEFAULT,  0), \
504 /* Raw commit header */ \
505 LINE(COMMIT,       "commit ",           COLOR_GREEN,    COLOR_DEFAULT,  0), \
506 LINE(PARENT,       "parent ",           COLOR_BLUE,     COLOR_DEFAULT,  0), \
507 LINE(TREE,         "tree ",             COLOR_BLUE,     COLOR_DEFAULT,  0), \
508 LINE(AUTHOR,       "author ",           COLOR_CYAN,     COLOR_DEFAULT,  0), \
509 LINE(COMMITTER,    "committer ",        COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
510 /* Misc */ \
511 LINE(DIFF_TREE,    "diff-tree ",        COLOR_BLUE,     COLOR_DEFAULT,  0), \
512 LINE(SIGNOFF,      "    Signed-off-by", COLOR_YELLOW,   COLOR_DEFAULT,  0), \
513 /* UI colors */ \
514 LINE(DEFAULT,      "",                  COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL), \
515 LINE(CURSOR,       "",                  COLOR_WHITE,    COLOR_GREEN,    A_BOLD), \
516 LINE(STATUS,       "",                  COLOR_GREEN,    COLOR_DEFAULT,  0), \
517 LINE(TITLE_BLUR,   "",                  COLOR_WHITE,    COLOR_BLUE,     0), \
518 LINE(TITLE_FOCUS,  "",                  COLOR_WHITE,    COLOR_BLUE,     A_BOLD), \
519 LINE(MAIN_DATE,    "",                  COLOR_BLUE,     COLOR_DEFAULT,  0), \
520 LINE(MAIN_AUTHOR,  "",                  COLOR_GREEN,    COLOR_DEFAULT,  0), \
521 LINE(MAIN_COMMIT,  "",                  COLOR_DEFAULT,  COLOR_DEFAULT,  0), \
522 LINE(MAIN_DELIM,   "",                  COLOR_MAGENTA,  COLOR_DEFAULT,  0),
523
524 enum line_type {
525 #define LINE(type, line, fg, bg, attr) \
526         LINE_##type
527         LINE_INFO
528 #undef  LINE
529 };
530
531 struct line_info {
532         char *line;             /* The start of line to match. */
533         int linelen;            /* Size of string to match. */
534         int fg, bg, attr;       /* Color and text attributes for the lines. */
535 };
536
537 static struct line_info line_info[] = {
538 #define LINE(type, line, fg, bg, attr) \
539         { (line), STRING_SIZE(line), (fg), (bg), (attr) }
540         LINE_INFO
541 #undef  LINE
542 };
543
544 static enum line_type
545 get_line_type(char *line)
546 {
547         int linelen = strlen(line);
548         enum line_type type;
549
550         for (type = 0; type < ARRAY_SIZE(line_info); type++)
551                 /* Case insensitive search matches Signed-off-by lines better. */
552                 if (linelen >= line_info[type].linelen &&
553                     !strncasecmp(line_info[type].line, line, line_info[type].linelen))
554                         return type;
555
556         return LINE_DEFAULT;
557 }
558
559 static inline int
560 get_line_attr(enum line_type type)
561 {
562         assert(type < ARRAY_SIZE(line_info));
563         return COLOR_PAIR(type) | line_info[type].attr;
564 }
565
566 static void
567 init_colors(void)
568 {
569         int default_bg = COLOR_BLACK;
570         int default_fg = COLOR_WHITE;
571         enum line_type type;
572
573         start_color();
574
575         if (use_default_colors() != ERR) {
576                 default_bg = -1;
577                 default_fg = -1;
578         }
579
580         for (type = 0; type < ARRAY_SIZE(line_info); type++) {
581                 struct line_info *info = &line_info[type];
582                 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
583                 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
584
585                 init_pair(type, fg, bg);
586         }
587 }
588
589
590 /**
591  * ENVIRONMENT VARIABLES
592  * ---------------------
593  * It is possible to alter which commands are used for the different views.
594  * If for example you prefer commits in the main to be sorted by date and
595  * only show 500 commits, use:
596  *
597  *      $ TIG_MAIN_CMD="git log --date-order -n500 --pretty=raw %s" tig
598  *
599  * Or set the variable permanently in your environment.
600  *
601  * Notice, how `%s` is used to specify the commit reference. There can
602  * be a maximum of 5 `%s` ref specifications.
603  *
604  * TIG_DIFF_CMD::
605  *      The command used for the diff view. By default, git show is used
606  *      as a backend.
607  *
608  * TIG_LOG_CMD::
609  *      The command used for the log view.
610  *
611  * TIG_MAIN_CMD::
612  *      The command used for the main view. Note, you must always specify
613  *      the option: `--pretty=raw` since the main view parser expects to
614  *      read that format.
615  **/
616
617 #define TIG_DIFF_CMD \
618         "git show --patch-with-stat --find-copies-harder -B -C %s"
619
620 #define TIG_LOG_CMD     \
621         "git log --cc --stat -n100 %s"
622
623 #define TIG_MAIN_CMD \
624         "git log --topo-order --stat --pretty=raw %s"
625
626 /* We silently ignore that the following are also exported. */
627
628 #define TIG_HELP_CMD \
629         "man tig 2> /dev/null"
630
631 #define TIG_PAGER_CMD \
632         ""
633
634
635 /*
636  * Viewer
637  */
638
639 struct view {
640         const char *name;       /* View name */
641         char *cmd_fmt;          /* Default command line format */
642         char *cmd_env;          /* Command line set via environment */
643         char *id;               /* Points to either of ref_{head,commit} */
644         size_t objsize;         /* Size of objects in the line index */
645
646         struct view_ops {
647                 /* Draw one line; @lineno must be < view->height. */
648                 bool (*draw)(struct view *view, unsigned int lineno);
649                 /* Read one line; updates view->line. */
650                 bool (*read)(struct view *view, char *line);
651                 /* Depending on view, change display based on current line. */
652                 bool (*enter)(struct view *view);
653         } *ops;
654
655         char cmd[SIZEOF_CMD];   /* Command buffer */
656         char ref[SIZEOF_REF];   /* Hovered commit reference */
657         char vid[SIZEOF_REF];   /* View ID. Set to id member when updating. */
658
659         int height, width;      /* The width and height of the main window */
660         WINDOW *win;            /* The main window */
661         WINDOW *title;          /* The title window living below the main window */
662
663         /* Navigation */
664         unsigned long offset;   /* Offset of the window top */
665         unsigned long lineno;   /* Current line number */
666
667         /* Buffering */
668         unsigned long lines;    /* Total number of lines */
669         void **line;            /* Line index; each line contains user data */
670         unsigned int digits;    /* Number of digits in the lines member. */
671
672         /* Loading */
673         FILE *pipe;
674         time_t start_time;
675 };
676
677 static struct view_ops pager_ops;
678 static struct view_ops main_ops;
679
680 char ref_head[SIZEOF_REF]       = "HEAD";
681 char ref_commit[SIZEOF_REF]     = "HEAD";
682
683 #define VIEW_STR(name, cmd, env, ref, objsize, ops) \
684         { name, cmd, #env, ref, objsize, ops }
685
686 #define VIEW_(id, name, ops, ref, objsize) \
687         VIEW_STR(name, TIG_##id##_CMD,  TIG_##id##_CMD, ref, objsize, ops)
688
689 static struct view views[] = {
690         VIEW_(MAIN,  "main",  &main_ops,  ref_head,   sizeof(struct commit)),
691         VIEW_(DIFF,  "diff",  &pager_ops, ref_commit, sizeof(char)),
692         VIEW_(LOG,   "log",   &pager_ops, ref_head,   sizeof(char)),
693         VIEW_(HELP,  "help",  &pager_ops, ref_head,   sizeof(char)),
694         VIEW_(PAGER, "pager", &pager_ops, "static",   sizeof(char)),
695 };
696
697 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
698
699 /* The display array of active views and the index of the current view. */
700 static struct view *display[2];
701 static unsigned int current_view;
702
703 #define foreach_view(view, i) \
704         for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
705
706
707 static void
708 redraw_view_from(struct view *view, int lineno)
709 {
710         assert(0 <= lineno && lineno < view->height);
711
712         for (; lineno < view->height; lineno++) {
713                 if (!view->ops->draw(view, lineno))
714                         break;
715         }
716
717         redrawwin(view->win);
718         wrefresh(view->win);
719 }
720
721 static void
722 redraw_view(struct view *view)
723 {
724         wclear(view->win);
725         redraw_view_from(view, 0);
726 }
727
728 static void
729 resize_display(void)
730 {
731         int offset, i;
732         struct view *base = display[0];
733         struct view *view = display[1] ? display[1] : display[0];
734
735         /* Setup window dimensions */
736
737         getmaxyx(stdscr, base->height, base->width);
738
739         /* Make room for the status window. */
740         base->height -= 1;
741
742         if (view != base) {
743                 /* Horizontal split. */
744                 view->width   = base->width;
745                 view->height  = SCALE_SPLIT_VIEW(base->height);
746                 base->height -= view->height;
747
748                 /* Make room for the title bar. */
749                 view->height -= 1;
750         }
751
752         /* Make room for the title bar. */
753         base->height -= 1;
754
755         offset = 0;
756
757         foreach_view (view, i) {
758                 if (!view->win) {
759                         view->win = newwin(view->height, 0, offset, 0);
760                         if (!view->win)
761                                 die("Failed to create %s view", view->name);
762
763                         scrollok(view->win, TRUE);
764
765                         view->title = newwin(1, 0, offset + view->height, 0);
766                         if (!view->title)
767                                 die("Failed to create title window");
768
769                 } else {
770                         wresize(view->win, view->height, view->width);
771                         mvwin(view->win,   offset, 0);
772                         mvwin(view->title, offset + view->height, 0);
773                         wrefresh(view->win);
774                 }
775
776                 offset += view->height + 1;
777         }
778 }
779
780 static void
781 update_view_title(struct view *view)
782 {
783         if (view == display[current_view])
784                 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
785         else
786                 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
787
788         werase(view->title);
789         wmove(view->title, 0, 0);
790
791         /* [main] ref: 334b506... - commit 6 of 4383 (0%) */
792
793         if (*view->ref)
794                 wprintw(view->title, "[%s] ref: %s", view->name, view->ref);
795         else
796                 wprintw(view->title, "[%s]", view->name);
797
798         if (view->lines) {
799                 char *type = view == VIEW(REQ_VIEW_MAIN) ? "commit" : "line";
800
801                 wprintw(view->title, " - %s %d of %d (%d%%)",
802                         type,
803                         view->lineno + 1,
804                         view->lines,
805                         (view->lineno + 1) * 100 / view->lines);
806         }
807
808         wrefresh(view->title);
809 }
810
811 /*
812  * Navigation
813  */
814
815 /* Scrolling backend */
816 static void
817 do_scroll_view(struct view *view, int lines)
818 {
819         /* The rendering expects the new offset. */
820         view->offset += lines;
821
822         assert(0 <= view->offset && view->offset < view->lines);
823         assert(lines);
824
825         /* Redraw the whole screen if scrolling is pointless. */
826         if (view->height < ABS(lines)) {
827                 redraw_view(view);
828
829         } else {
830                 int line = lines > 0 ? view->height - lines : 0;
831                 int end = line + ABS(lines);
832
833                 wscrl(view->win, lines);
834
835                 for (; line < end; line++) {
836                         if (!view->ops->draw(view, line))
837                                 break;
838                 }
839         }
840
841         /* Move current line into the view. */
842         if (view->lineno < view->offset) {
843                 view->lineno = view->offset;
844                 view->ops->draw(view, 0);
845
846         } else if (view->lineno >= view->offset + view->height) {
847                 view->lineno = view->offset + view->height - 1;
848                 view->ops->draw(view, view->lineno - view->offset);
849         }
850
851         assert(view->offset <= view->lineno && view->lineno < view->lines);
852
853         redrawwin(view->win);
854         wrefresh(view->win);
855         report("");
856 }
857
858 /* Scroll frontend */
859 static void
860 scroll_view(struct view *view, enum request request)
861 {
862         int lines = 1;
863
864         switch (request) {
865         case REQ_SCROLL_PAGE_DOWN:
866                 lines = view->height;
867         case REQ_SCROLL_LINE_DOWN:
868                 if (view->offset + lines > view->lines)
869                         lines = view->lines - view->offset;
870
871                 if (lines == 0 || view->offset + view->height >= view->lines) {
872                         report("Cannot scroll beyond the last line");
873                         return;
874                 }
875                 break;
876
877         case REQ_SCROLL_PAGE_UP:
878                 lines = view->height;
879         case REQ_SCROLL_LINE_UP:
880                 if (lines > view->offset)
881                         lines = view->offset;
882
883                 if (lines == 0) {
884                         report("Cannot scroll beyond the first line");
885                         return;
886                 }
887
888                 lines = -lines;
889                 break;
890
891         default:
892                 die("request %d not handled in switch", request);
893         }
894
895         do_scroll_view(view, lines);
896 }
897
898 /* Cursor moving */
899 static void
900 move_view(struct view *view, enum request request)
901 {
902         int steps;
903
904         switch (request) {
905         case REQ_MOVE_FIRST_LINE:
906                 steps = -view->lineno;
907                 break;
908
909         case REQ_MOVE_LAST_LINE:
910                 steps = view->lines - view->lineno - 1;
911                 break;
912
913         case REQ_MOVE_PAGE_UP:
914                 steps = view->height > view->lineno
915                       ? -view->lineno : -view->height;
916                 break;
917
918         case REQ_MOVE_PAGE_DOWN:
919                 steps = view->lineno + view->height >= view->lines
920                       ? view->lines - view->lineno - 1 : view->height;
921                 break;
922
923         case REQ_MOVE_UP:
924                 steps = -1;
925                 break;
926
927         case REQ_MOVE_DOWN:
928                 steps = 1;
929                 break;
930
931         default:
932                 die("request %d not handled in switch", request);
933         }
934
935         if (steps <= 0 && view->lineno == 0) {
936                 report("Cannot move beyond the first line");
937                 return;
938
939         } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
940                 report("Cannot move beyond the last line");
941                 return;
942         }
943
944         /* Move the current line */
945         view->lineno += steps;
946         assert(0 <= view->lineno && view->lineno < view->lines);
947
948         /* Repaint the old "current" line if we be scrolling */
949         if (ABS(steps) < view->height) {
950                 int prev_lineno = view->lineno - steps - view->offset;
951
952                 wmove(view->win, prev_lineno, 0);
953                 wclrtoeol(view->win);
954                 view->ops->draw(view, prev_lineno);
955         }
956
957         /* Check whether the view needs to be scrolled */
958         if (view->lineno < view->offset ||
959             view->lineno >= view->offset + view->height) {
960                 if (steps < 0 && -steps > view->offset) {
961                         steps = -view->offset;
962
963                 } else if (steps > 0) {
964                         if (view->lineno == view->lines - 1 &&
965                             view->lines > view->height) {
966                                 steps = view->lines - view->offset - 1;
967                                 if (steps >= view->height)
968                                         steps -= view->height - 1;
969                         }
970                 }
971
972                 do_scroll_view(view, steps);
973                 return;
974         }
975
976         /* Draw the current line */
977         view->ops->draw(view, view->lineno - view->offset);
978
979         redrawwin(view->win);
980         wrefresh(view->win);
981         report("");
982 }
983
984
985 /*
986  * Incremental updating
987  */
988
989 static bool
990 begin_update(struct view *view)
991 {
992         char *id = view->id;
993
994         if (opt_cmd[0]) {
995                 string_copy(view->cmd, opt_cmd);
996                 opt_cmd[0] = 0;
997                 /* When running random commands, the view ref could have become
998                  * invalid so clear it. */
999                 view->ref[0] = 0;
1000         } else {
1001                 char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1002
1003                 if (snprintf(view->cmd, sizeof(view->cmd), format,
1004                              id, id, id, id, id) >= sizeof(view->cmd))
1005                         return FALSE;
1006         }
1007
1008         /* Special case for the pager view. */
1009         if (opt_pipe) {
1010                 view->pipe = opt_pipe;
1011                 opt_pipe = NULL;
1012         } else {
1013                 view->pipe = popen(view->cmd, "r");
1014         }
1015
1016         if (!view->pipe)
1017                 return FALSE;
1018
1019         set_nonblocking_input(TRUE);
1020
1021         view->offset = 0;
1022         view->lines  = 0;
1023         view->lineno = 0;
1024         string_copy(view->vid, id);
1025
1026         if (view->line) {
1027                 int i;
1028
1029                 for (i = 0; i < view->lines; i++)
1030                         if (view->line[i])
1031                                 free(view->line[i]);
1032
1033                 free(view->line);
1034                 view->line = NULL;
1035         }
1036
1037         view->start_time = time(NULL);
1038
1039         return TRUE;
1040 }
1041
1042 static void
1043 end_update(struct view *view)
1044 {
1045         if (!view->pipe)
1046                 return;
1047         set_nonblocking_input(FALSE);
1048         if (view->pipe == stdin)
1049                 fclose(view->pipe);
1050         else
1051                 pclose(view->pipe);
1052         view->pipe = NULL;
1053 }
1054
1055 static bool
1056 update_view(struct view *view)
1057 {
1058         char buffer[BUFSIZ];
1059         char *line;
1060         void **tmp;
1061         /* The number of lines to read. If too low it will cause too much
1062          * redrawing (and possible flickering), if too high responsiveness
1063          * will suffer. */
1064         unsigned long lines = view->height;
1065         int redraw_from = -1;
1066
1067         if (!view->pipe)
1068                 return TRUE;
1069
1070         /* Only redraw if lines are visible. */
1071         if (view->offset + view->height >= view->lines)
1072                 redraw_from = view->lines - view->offset;
1073
1074         tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines));
1075         if (!tmp)
1076                 goto alloc_error;
1077
1078         view->line = tmp;
1079
1080         while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1081                 int linelen;
1082
1083                 linelen = strlen(line);
1084                 if (linelen)
1085                         line[linelen - 1] = 0;
1086
1087                 if (!view->ops->read(view, line))
1088                         goto alloc_error;
1089
1090                 if (lines-- == 1)
1091                         break;
1092         }
1093
1094         {
1095                 int digits;
1096
1097                 lines = view->lines;
1098                 for (digits = 0; lines; digits++)
1099                         lines /= 10;
1100
1101                 /* Keep the displayed view in sync with line number scaling. */
1102                 if (digits != view->digits) {
1103                         view->digits = digits;
1104                         redraw_from = 0;
1105                 }
1106         }
1107
1108         if (redraw_from >= 0) {
1109                 /* If this is an incremental update, redraw the previous line
1110                  * since for commits some members could have changed when
1111                  * loading the main view. */
1112                 if (redraw_from > 0)
1113                         redraw_from--;
1114
1115                 /* Incrementally draw avoids flickering. */
1116                 redraw_view_from(view, redraw_from);
1117         }
1118
1119         /* Update the title _after_ the redraw so that if the redraw picks up a
1120          * commit reference in view->ref it'll be available here. */
1121         update_view_title(view);
1122
1123         if (ferror(view->pipe)) {
1124                 report("Failed to read: %s", strerror(errno));
1125                 goto end;
1126
1127         } else if (feof(view->pipe)) {
1128                 time_t secs = time(NULL) - view->start_time;
1129
1130                 if (view == VIEW(REQ_VIEW_HELP)) {
1131                         report("%s", HELP);
1132                         goto end;
1133                 }
1134
1135                 report("Loaded %d lines in %ld second%s", view->lines, secs,
1136                        secs == 1 ? "" : "s");
1137                 goto end;
1138         }
1139
1140         return TRUE;
1141
1142 alloc_error:
1143         report("Allocation failure");
1144
1145 end:
1146         end_update(view);
1147         return FALSE;
1148 }
1149
1150 enum open_flags {
1151         OPEN_DEFAULT = 0,       /* Use default view switching. */
1152         OPEN_SPLIT = 1,         /* Split current view. */
1153         OPEN_BACKGROUNDED = 2,  /* Backgrounded. */
1154         OPEN_RELOAD = 4,        /* Reload view even if it is the current. */
1155 };
1156
1157 static void
1158 open_view(struct view *prev, enum request request, enum open_flags flags)
1159 {
1160         bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1161         bool split = !!(flags & OPEN_SPLIT);
1162         bool reload = !!(flags & OPEN_RELOAD);
1163         struct view *view = VIEW(request);
1164         struct view *displayed;
1165         int nviews;
1166
1167         /* Cycle between displayed views and count the views. */
1168         foreach_view (displayed, nviews) {
1169                 if (prev != view &&
1170                     view == displayed &&
1171                     !strcmp(view->vid, prev->vid)) {
1172                         current_view = nviews;
1173                         /* Blur out the title of the previous view. */
1174                         update_view_title(prev);
1175                         report("Switching to %s view", view->name);
1176                         return;
1177                 }
1178         }
1179
1180         if (view == prev && nviews == 1 && !reload) {
1181                 report("Already in %s view", view->name);
1182                 return;
1183         }
1184
1185         if ((reload || strcmp(view->vid, view->id)) &&
1186             !begin_update(view)) {
1187                 report("Failed to load %s view", view->name);
1188                 return;
1189         }
1190
1191         if (split) {
1192                 display[current_view + 1] = view;
1193                 if (!backgrounded)
1194                         current_view++;
1195         } else {
1196                 /* Maximize the current view. */
1197                 memset(display, 0, sizeof(display));
1198                 current_view = 0;
1199                 display[current_view] = view;
1200         }
1201
1202         resize_display();
1203
1204         if (split && prev->lineno - prev->offset >= prev->height) {
1205                 /* Take the title line into account. */
1206                 int lines = prev->lineno - prev->offset - prev->height + 1;
1207
1208                 /* Scroll the view that was split if the current line is
1209                  * outside the new limited view. */
1210                 do_scroll_view(prev, lines);
1211         }
1212
1213         if (prev && view != prev) {
1214                 /* "Blur" the previous view. */
1215                 update_view_title(prev);
1216
1217                 /* Continue loading split views in the background. */
1218                 if (!split)
1219                         end_update(prev);
1220         }
1221
1222         if (view->pipe) {
1223                 /* Clear the old view and let the incremental updating refill
1224                  * the screen. */
1225                 wclear(view->win);
1226                 report("Loading...");
1227         } else {
1228                 redraw_view(view);
1229                 report("");
1230         }
1231 }
1232
1233
1234 /*
1235  * User request switch noodle
1236  */
1237
1238 static int
1239 view_driver(struct view *view, enum request request)
1240 {
1241         int i;
1242
1243         switch (request) {
1244         case REQ_MOVE_UP:
1245         case REQ_MOVE_DOWN:
1246         case REQ_MOVE_PAGE_UP:
1247         case REQ_MOVE_PAGE_DOWN:
1248         case REQ_MOVE_FIRST_LINE:
1249         case REQ_MOVE_LAST_LINE:
1250                 move_view(view, request);
1251                 break;
1252
1253         case REQ_SCROLL_LINE_DOWN:
1254         case REQ_SCROLL_LINE_UP:
1255         case REQ_SCROLL_PAGE_DOWN:
1256         case REQ_SCROLL_PAGE_UP:
1257                 scroll_view(view, request);
1258                 break;
1259
1260         case REQ_VIEW_MAIN:
1261         case REQ_VIEW_DIFF:
1262         case REQ_VIEW_LOG:
1263         case REQ_VIEW_HELP:
1264         case REQ_VIEW_PAGER:
1265                 open_view(view, request, OPEN_DEFAULT);
1266                 break;
1267
1268         case REQ_ENTER:
1269                 if (!view->lines) {
1270                         report("Nothing to enter");
1271                         break;
1272                 }
1273                 return view->ops->enter(view);
1274
1275         case REQ_VIEW_NEXT:
1276         {
1277                 int nviews = display[1] ? 2 : 1;
1278                 int next_view = (current_view + 1) % nviews;
1279
1280                 if (next_view == current_view) {
1281                         report("Only one view is displayed");
1282                         break;
1283                 }
1284
1285                 current_view = next_view;
1286                 /* Blur out the title of the previous view. */
1287                 update_view_title(view);
1288                 report("Switching to %s view", display[current_view]->name);
1289                 break;
1290         }
1291         case REQ_TOGGLE_LINE_NUMBERS:
1292                 opt_line_number = !opt_line_number;
1293                 redraw_view(view);
1294                 break;
1295
1296         case REQ_PROMPT:
1297                 /* Always reload^Wrerun commands from the prompt. */
1298                 open_view(view, opt_request, OPEN_RELOAD);
1299                 break;
1300
1301         case REQ_STOP_LOADING:
1302                 foreach_view (view, i) {
1303                         if (view->pipe)
1304                                 report("Stopped loaded of %s view", view->name),
1305                         end_update(view);
1306                 }
1307                 break;
1308
1309         case REQ_SHOW_VERSION:
1310                 report("Version: %s", VERSION);
1311                 return TRUE;
1312
1313         case REQ_SCREEN_RESIZE:
1314                 resize_display();
1315                 /* Fall-through */
1316         case REQ_SCREEN_REDRAW:
1317                 foreach_view (view, i) {
1318                         redraw_view(view);
1319                         update_view_title(view);
1320                 }
1321                 break;
1322
1323         case REQ_SCREEN_UPDATE:
1324                 doupdate();
1325                 return TRUE;
1326
1327         case REQ_QUIT:
1328                 return FALSE;
1329
1330         default:
1331                 /* An unknown key will show most commonly used commands. */
1332                 report("%s", HELP);
1333                 return TRUE;
1334         }
1335
1336         return TRUE;
1337 }
1338
1339
1340 /*
1341  * View backend handlers
1342  */
1343
1344 static bool
1345 pager_draw(struct view *view, unsigned int lineno)
1346 {
1347         enum line_type type;
1348         char *line;
1349         int linelen;
1350         int attr;
1351
1352         if (view->offset + lineno >= view->lines)
1353                 return FALSE;
1354
1355         line = view->line[view->offset + lineno];
1356         type = get_line_type(line);
1357
1358         if (view->offset + lineno == view->lineno) {
1359                 if (type == LINE_COMMIT) {
1360                         string_copy(view->ref, line + 7);
1361                         string_copy(ref_commit, view->ref);
1362                 }
1363
1364                 type = LINE_CURSOR;
1365         }
1366
1367         attr = get_line_attr(type);
1368         wattrset(view->win, attr);
1369
1370         linelen = strlen(line);
1371         linelen = MIN(linelen, view->width);
1372
1373         if (opt_line_number) {
1374                 static char indent[] = "                    ";
1375                 unsigned long real_lineno = view->offset + lineno + 1;
1376                 int col = 0;
1377
1378                 if (real_lineno == 1 || (real_lineno % opt_num_interval) == 0)
1379                         mvwprintw(view->win, lineno, 0, "%.*d", view->digits, real_lineno);
1380
1381                 else if (view->digits < sizeof(indent))
1382                         mvwaddnstr(view->win, lineno, 0, indent, view->digits);
1383
1384                 waddstr(view->win, ": ");
1385
1386                 while (line) {
1387                         if (*line == '\t') {
1388                                 waddnstr(view->win, "        ", 8 - (col % 8));
1389                                 col += 8 - (col % 8);
1390                                 line++;
1391
1392                         } else {
1393                                 char *tab = strchr(line, '\t');
1394
1395                                 if (tab)
1396                                         waddnstr(view->win, line, tab - line);
1397                                 else
1398                                         waddstr(view->win, line);
1399                                 col += tab - line;
1400                                 line = tab;
1401                         }
1402                 }
1403                 waddstr(view->win, line);
1404
1405         } else {
1406 #if 0
1407                 /* NOTE: Code for only highlighting the text on the cursor line.
1408                  * Kept since I've not yet decided whether to highlight the
1409                  * entire line or not. --fonseca */
1410                 /* No empty lines makes cursor drawing and clearing implicit. */
1411                 if (!*line)
1412                         line = " ", linelen = 1;
1413 #endif
1414                 mvwaddnstr(view->win, lineno, 0, line, linelen);
1415         }
1416
1417         /* Paint the rest of the line if it's the cursor line. */
1418         if (type == LINE_CURSOR)
1419                 wchgat(view->win, -1, 0, type, NULL);
1420
1421         return TRUE;
1422 }
1423
1424 static bool
1425 pager_read(struct view *view, char *line)
1426 {
1427         view->line[view->lines] = strdup(line);
1428         if (!view->line[view->lines])
1429                 return FALSE;
1430
1431         view->lines++;
1432         return TRUE;
1433 }
1434
1435 static bool
1436 pager_enter(struct view *view)
1437 {
1438         char *line = view->line[view->lineno];
1439
1440         if (get_line_type(line) == LINE_COMMIT) {
1441                 open_view(view, REQ_VIEW_DIFF, OPEN_DEFAULT);
1442         }
1443
1444         return TRUE;
1445 }
1446
1447 static struct view_ops pager_ops = {
1448         pager_draw,
1449         pager_read,
1450         pager_enter,
1451 };
1452
1453
1454 static bool
1455 main_draw(struct view *view, unsigned int lineno)
1456 {
1457         char buf[DATE_COLS + 1];
1458         struct commit *commit;
1459         enum line_type type;
1460         int cols = 0;
1461         size_t timelen;
1462
1463         if (view->offset + lineno >= view->lines)
1464                 return FALSE;
1465
1466         commit = view->line[view->offset + lineno];
1467         if (!*commit->author)
1468                 return FALSE;
1469
1470         if (view->offset + lineno == view->lineno) {
1471                 string_copy(view->ref, commit->id);
1472                 string_copy(ref_commit, view->ref);
1473                 type = LINE_CURSOR;
1474         } else {
1475                 type = LINE_MAIN_COMMIT;
1476         }
1477
1478         wmove(view->win, lineno, cols);
1479         wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1480
1481         timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1482         waddnstr(view->win, buf, timelen);
1483         waddstr(view->win, " ");
1484
1485         cols += DATE_COLS;
1486         wmove(view->win, lineno, cols);
1487         wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1488
1489         if (strlen(commit->author) > 19) {
1490                 waddnstr(view->win, commit->author, 18);
1491                 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1492                 waddch(view->win, '~');
1493         } else {
1494                 waddstr(view->win, commit->author);
1495         }
1496
1497         cols += 20;
1498         wattrset(view->win, A_NORMAL);
1499         mvwaddch(view->win, lineno, cols, ACS_LTEE);
1500         wattrset(view->win, get_line_attr(type));
1501         mvwaddstr(view->win, lineno, cols + 2, commit->title);
1502         wattrset(view->win, A_NORMAL);
1503
1504         return TRUE;
1505 }
1506
1507 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1508 static bool
1509 main_read(struct view *view, char *line)
1510 {
1511         enum line_type type = get_line_type(line);
1512         struct commit *commit;
1513
1514         switch (type) {
1515         case LINE_COMMIT:
1516                 commit = calloc(1, sizeof(struct commit));
1517                 if (!commit)
1518                         return FALSE;
1519
1520                 line += STRING_SIZE("commit ");
1521
1522                 view->line[view->lines++] = commit;
1523                 string_copy(commit->id, line);
1524                 break;
1525
1526         case LINE_AUTHOR:
1527         {
1528                 char *ident = line + STRING_SIZE("author ");
1529                 char *end = strchr(ident, '<');
1530
1531                 if (end) {
1532                         for (; end > ident && isspace(end[-1]); end--) ;
1533                         *end = 0;
1534                 }
1535
1536                 commit = view->line[view->lines - 1];
1537                 string_copy(commit->author, ident);
1538
1539                 /* Parse epoch and timezone */
1540                 if (end) {
1541                         char *secs = strchr(end + 1, '>');
1542                         char *zone;
1543                         time_t time;
1544
1545                         if (!secs || secs[1] != ' ')
1546                                 break;
1547
1548                         secs += 2;
1549                         time = (time_t) atol(secs);
1550                         zone = strchr(secs, ' ');
1551                         if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1552                                 long tz;
1553
1554                                 zone++;
1555                                 tz  = ('0' - zone[1]) * 60 * 60 * 10;
1556                                 tz += ('0' - zone[2]) * 60 * 60;
1557                                 tz += ('0' - zone[3]) * 60;
1558                                 tz += ('0' - zone[4]) * 60;
1559
1560                                 if (zone[0] == '-')
1561                                         tz = -tz;
1562
1563                                 time -= tz;
1564                         }
1565                         gmtime_r(&time, &commit->time);
1566                 }
1567                 break;
1568         }
1569         default:
1570                 /* We should only ever end up here if there has already been a
1571                  * commit line, however, be safe. */
1572                 if (view->lines == 0)
1573                         break;
1574
1575                 /* Fill in the commit title if it has not already been set. */
1576                 commit = view->line[view->lines - 1];
1577                 if (commit->title[0])
1578                         break;
1579
1580                 /* Require titles to start with a non-space character at the
1581                  * offset used by git log. */
1582                 /* FIXME: More gracefull handling of titles; append "..." to
1583                  * shortened titles, etc. */
1584                 if (strncmp(line, "    ", 4) ||
1585                     isspace(line[4]))
1586                         break;
1587
1588                 string_copy(commit->title, line + 4);
1589         }
1590
1591         return TRUE;
1592 }
1593
1594 static bool
1595 main_enter(struct view *view)
1596 {
1597         open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT | OPEN_BACKGROUNDED);
1598         return TRUE;
1599 }
1600
1601 static struct view_ops main_ops = {
1602         main_draw,
1603         main_read,
1604         main_enter,
1605 };
1606
1607 /*
1608  * Status management
1609  */
1610
1611 /* Whether or not the curses interface has been initialized. */
1612 bool cursed = FALSE;
1613
1614 /* The status window is used for polling keystrokes. */
1615 static WINDOW *status_win;
1616
1617 /* Update status and title window. */
1618 static void
1619 report(const char *msg, ...)
1620 {
1621         va_list args;
1622
1623         va_start(args, msg);
1624
1625         /* Update the title window first, so the cursor ends up in the status
1626          * window. */
1627         update_view_title(display[current_view]);
1628
1629         werase(status_win);
1630         wmove(status_win, 0, 0);
1631         vwprintw(status_win, msg, args);
1632         wrefresh(status_win);
1633
1634         va_end(args);
1635 }
1636
1637 /* Controls when nodelay should be in effect when polling user input. */
1638 static void
1639 set_nonblocking_input(bool loading)
1640 {
1641         /* The number of loading views. */
1642         static unsigned int nloading;
1643
1644         if ((loading == FALSE && nloading-- == 1) ||
1645             (loading == TRUE  && nloading++ == 0))
1646                 nodelay(status_win, loading);
1647 }
1648
1649 static void
1650 init_display(void)
1651 {
1652         int x, y;
1653
1654         /* Initialize the curses library */
1655         if (isatty(STDIN_FILENO)) {
1656                 cursed = !!initscr();
1657         } else {
1658                 /* Leave stdin and stdout alone when acting as a pager. */
1659                 FILE *io = fopen("/dev/tty", "r+");
1660
1661                 cursed = !!newterm(NULL, io, io);
1662         }
1663
1664         if (!cursed)
1665                 die("Failed to initialize curses");
1666
1667         nonl();         /* Tell curses not to do NL->CR/NL on output */
1668         cbreak();       /* Take input chars one at a time, no wait for \n */
1669         noecho();       /* Don't echo input */
1670         leaveok(stdscr, TRUE);
1671
1672         if (has_colors())
1673                 init_colors();
1674
1675         getmaxyx(stdscr, y, x);
1676         status_win = newwin(1, 0, y - 1, 0);
1677         if (!status_win)
1678                 die("Failed to create status window");
1679
1680         /* Enable keyboard mapping */
1681         keypad(status_win, TRUE);
1682         wbkgdset(status_win, get_line_attr(LINE_STATUS));
1683 }
1684
1685 /*
1686  * Main
1687  */
1688
1689 static void
1690 quit(int sig)
1691 {
1692         /* XXX: Restore tty modes and let the OS cleanup the rest! */
1693         if (cursed)
1694                 endwin();
1695         exit(0);
1696 }
1697
1698 static void die(const char *err, ...)
1699 {
1700         va_list args;
1701
1702         endwin();
1703
1704         va_start(args, err);
1705         fputs("tig: ", stderr);
1706         vfprintf(stderr, err, args);
1707         fputs("\n", stderr);
1708         va_end(args);
1709
1710         exit(1);
1711 }
1712
1713 #include <readline/readline.h>
1714
1715 int
1716 main(int argc, char *argv[])
1717 {
1718         struct view *view;
1719         enum request request;
1720         size_t i;
1721
1722         signal(SIGINT, quit);
1723
1724         if (!parse_options(argc, argv))
1725                 return 0;
1726
1727         for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1728                 view->cmd_env = getenv(view->cmd_env);
1729
1730         request = opt_request;
1731
1732         init_display();
1733
1734         while (view_driver(display[current_view], request)) {
1735                 int key;
1736                 int i;
1737
1738                 foreach_view (view, i)
1739                         update_view(view);
1740
1741                 /* Refresh, accept single keystroke of input */
1742                 key = wgetch(status_win);
1743                 request = get_request(key);
1744
1745                 /* Some low-level request handling. This keeps handling of
1746                  * status_win restricted. */
1747                 switch (request) {
1748                 case REQ_PROMPT:
1749                         report(":");
1750                         /* Temporarily switch to line-oriented and echoed
1751                          * input. */
1752                         nocbreak();
1753                         echo();
1754
1755                         if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
1756                                 memcpy(opt_cmd, "git ", 4);
1757                                 opt_request = REQ_VIEW_PAGER;
1758                         } else {
1759                                 request = ERR;
1760                         }
1761
1762                         noecho();
1763                         cbreak();
1764                         break;
1765
1766                 case REQ_SCREEN_RESIZE:
1767                 {
1768                         int height, width;
1769
1770                         getmaxyx(stdscr, height, width);
1771
1772                         /* Resize the status view and let the view driver take
1773                          * care of resizing the displayed views. */
1774                         wresize(status_win, 1, width);
1775                         mvwin(status_win, height - 1, 0);
1776                         wrefresh(status_win);
1777                         break;
1778                 }
1779                 default:
1780                         break;
1781                 }
1782         }
1783
1784         quit(0);
1785
1786         return 0;
1787 }
1788
1789 /**
1790  * TODO
1791  * ----
1792  * Features that should be explored.
1793  *
1794  * - Searching.
1795  *
1796  * - Locale support.
1797  *
1798  * COPYRIGHT
1799  * ---------
1800  * Copyright (c) Jonas Fonseca <fonseca@diku.dk>, 2006
1801  *
1802  * This program is free software; you can redistribute it and/or modify
1803  * it under the terms of the GNU General Public License as published by
1804  * the Free Software Foundation; either version 2 of the License, or
1805  * (at your option) any later version.
1806  *
1807  * SEE ALSO
1808  * --------
1809  * [verse]
1810  * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
1811  * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
1812  * gitk(1): git repository browser written using tcl/tk,
1813  * gitview(1): git repository browser written using python/gtk.
1814  **/