TODO's and TODON'T's ...
[tig] / tig.c
1 /**
2  * TIG(1)
3  * ======
4  *
5  * NAME
6  * ----
7  * tig - text-mode interface for git
8  *
9  * SYNOPSIS
10  * --------
11  * [verse]
12  * tig [options]
13  * tig [options] log  [git log options]
14  * tig [options] diff [git diff options]
15  * tig [options] <    [git log or git diff output]
16  *
17  * DESCRIPTION
18  * -----------
19  * Browse changes in a git repository.
20  **/
21
22 #define DEBUG
23 #ifndef DEBUG
24 #define NDEBUG
25 #endif
26
27 #ifndef VERSION
28 #define VERSION "tig-0.1"
29 #endif
30
31 #include <assert.h>
32 #include <errno.h>
33 #include <ctype.h>
34 #include <signal.h>
35 #include <stdarg.h>
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <string.h>
39 #include <time.h>
40
41 #include <curses.h>
42 #include <form.h>
43
44 static void die(const char *err, ...);
45 static void report(const char *msg, ...);
46
47 /* Some ascii-shorthands that fit into the ncurses namespace. */
48 #define KEY_TAB         9
49 #define KEY_ESC         27
50 #define KEY_DEL         127
51
52 /* View requests */
53 enum request {
54         /* REQ_* values from form.h is used as a basis for user actions.
55          * Offset new values below relative to MAX_COMMAND from form.h. */
56         REQ_OFFSET      = MAX_COMMAND,
57
58         /* Requests for switching between the different views. */
59         REQ_DIFF,
60         REQ_LOG,
61         REQ_MAIN,
62         REQ_VIEWS,
63
64         REQ_QUIT,
65         REQ_VERSION,
66         REQ_STOP,
67         REQ_UPDATE,
68         REQ_REDRAW,
69         REQ_FIRST_LINE,
70         REQ_LAST_LINE,
71         REQ_LINE_NUMBER,
72 };
73
74 /* The request are used for adressing the view array. */
75 #define VIEW_OFFSET(r)  ((r) - REQ_OFFSET - 1)
76
77 #define SIZEOF_ID       1024
78
79 #define COLOR_TRANSP    (-1)
80
81 #define DATE_FORMAT     "%Y-%m-%d %H:%M"
82 #define DATE_COLS       (STRING_SIZE("2006-04-29 14:21") + 1)
83
84 #define ABS(x)          ((x) >= 0 ? (x) : -(x))
85 #define MIN(x, y)       ((x) < (y) ? (x) : (y))
86
87 #define ARRAY_SIZE(x)   (sizeof(x) / sizeof(x[0]))
88 #define STRING_SIZE(x)  (sizeof(x) - 1)
89
90 struct commit {
91         char id[41];
92         char title[75];
93         char author[75];
94         struct tm time;
95 };
96
97
98 /**
99  * OPTIONS
100  * -------
101  **/
102
103 static int opt_line_number;
104 static int opt_request = REQ_MAIN;
105
106 char head_id[SIZEOF_ID] = "HEAD";
107 char commit_id[SIZEOF_ID] = "HEAD";
108
109 /* Returns the index of log or diff command or -1 to exit. */
110 static int
111 parse_options(int argc, char *argv[])
112 {
113         int i;
114
115         for (i = 1; i < argc; i++) {
116                 char *opt = argv[i];
117
118                 /**
119                  * log [options]::
120                  *      git log options.
121                  **/
122                 if (!strcmp(opt, "log")) {
123                         opt_request = REQ_LOG;
124                         return i;
125
126                 /**
127                  * diff [options]::
128                  *      git diff options.
129                  **/
130                 } else if (!strcmp(opt, "diff")) {
131                         opt_request = REQ_DIFF;
132                         return i;
133
134                 /**
135                  * -l::
136                  *      Start up in log view.
137                  **/
138                 } else if (!strcmp(opt, "-l")) {
139                         opt_request = REQ_LOG;
140
141                 /**
142                  * -d::
143                  *      Start up in diff view.
144                  **/
145                 } else if (!strcmp(opt, "-d")) {
146                         opt_request = REQ_DIFF;
147
148                 /**
149                  * -n, --line-number::
150                  *      Prefix line numbers in log and diff view.
151                  **/
152                 } else if (!strcmp(opt, "-n") ||
153                            !strcmp(opt, "--line-number")) {
154                         opt_line_number = 1;
155
156                 /**
157                  * -v, --version::
158                  *      Show version and exit.
159                  **/
160                 } else if (!strcmp(opt, "-v") ||
161                            !strcmp(opt, "--version")) {
162                         printf("tig version %s\n", VERSION);
163                         return -1;
164
165                 /**
166                  * ref::
167                  *      Commit reference, symbolic or raw SHA1 ID.
168                  **/
169                 } else if (opt[0] && opt[0] != '-') {
170                         strncpy(head_id, opt, SIZEOF_ID);
171                         strncpy(commit_id, opt, SIZEOF_ID);
172
173                 } else {
174                         die("unknown command '%s'", opt);
175                 }
176         }
177
178         return i;
179 }
180
181
182 /*
183  * Line-oriented content detection.
184  */
185
186 enum line_type {
187         LINE_DEFAULT,
188         LINE_AUTHOR,
189         LINE_AUTHOR_IDENT,
190         LINE_COMMIT,
191         LINE_COMMITTER,
192         LINE_CURSOR,
193         LINE_DATE,
194         LINE_DIFF,
195         LINE_DIFF_ADD,
196         LINE_DIFF_CHUNK,
197         LINE_DIFF_COPY,
198         LINE_DIFF_DEL,
199         LINE_DIFF_DISSIM,
200         LINE_DIFF_NEWMODE,
201         LINE_DIFF_OLDMODE,
202         LINE_DIFF_RENAME,
203         LINE_DIFF_SIM,
204         LINE_DIFF_TREE,
205         LINE_INDEX,
206         LINE_MAIN_AUTHOR,
207         LINE_MAIN_COMMIT,
208         LINE_MAIN_DATE,
209         LINE_MAIN_DELIM,
210         LINE_MERGE,
211         LINE_PARENT,
212         LINE_SIGNOFF,
213         LINE_STATUS,
214         LINE_TITLE,
215         LINE_TREE,
216 };
217
218 struct line_info {
219         enum line_type type;
220         char *line;
221         int linelen;
222
223         int fg;
224         int bg;
225         int attr;
226 };
227
228 #define LINE(type, line, fg, bg, attr) \
229         { LINE_##type, (line), STRING_SIZE(line), (fg), (bg), (attr) }
230
231 static struct line_info line_info[] = {
232         /* Diff markup */
233         LINE(DIFF,         "diff --git ",       COLOR_YELLOW,   COLOR_TRANSP,   0),
234         LINE(INDEX,        "index ",            COLOR_BLUE,     COLOR_TRANSP,   0),
235         LINE(DIFF_CHUNK,   "@@",                COLOR_MAGENTA,  COLOR_TRANSP,   0),
236         LINE(DIFF_ADD,     "+",                 COLOR_GREEN,    COLOR_TRANSP,   0),
237         LINE(DIFF_DEL,     "-",                 COLOR_RED,      COLOR_TRANSP,   0),
238         LINE(DIFF_OLDMODE, "old mode ",         COLOR_YELLOW,   COLOR_TRANSP,   0),
239         LINE(DIFF_NEWMODE, "new mode ",         COLOR_YELLOW,   COLOR_TRANSP,   0),
240         LINE(DIFF_COPY,    "copy ",             COLOR_YELLOW,   COLOR_TRANSP,   0),
241         LINE(DIFF_RENAME,  "rename ",           COLOR_YELLOW,   COLOR_TRANSP,   0),
242         LINE(DIFF_SIM,     "similarity ",       COLOR_YELLOW,   COLOR_TRANSP,   0),
243         LINE(DIFF_DISSIM,  "dissimilarity ",    COLOR_YELLOW,   COLOR_TRANSP,   0),
244
245         /* Pretty print commit header */
246         LINE(AUTHOR,       "Author: ",          COLOR_CYAN,     COLOR_TRANSP,   0),
247         LINE(MERGE,        "Merge: ",           COLOR_BLUE,     COLOR_TRANSP,   0),
248         LINE(DATE,         "Date:   ",          COLOR_YELLOW,   COLOR_TRANSP,   0),
249
250         /* Raw commit header */
251         LINE(COMMIT,       "commit ",           COLOR_GREEN,    COLOR_TRANSP,   0),
252         LINE(PARENT,       "parent ",           COLOR_BLUE,     COLOR_TRANSP,   0),
253         LINE(TREE,         "tree ",             COLOR_BLUE,     COLOR_TRANSP,   0),
254         LINE(AUTHOR_IDENT, "author ",           COLOR_CYAN,     COLOR_TRANSP,   0),
255         LINE(COMMITTER,    "committer ",        COLOR_MAGENTA,  COLOR_TRANSP,   0),
256
257         /* Misc */
258         LINE(DIFF_TREE,    "diff-tree ",        COLOR_BLUE,     COLOR_TRANSP,   0),
259         LINE(SIGNOFF,      "    Signed-off-by", COLOR_YELLOW,   COLOR_TRANSP,   0),
260
261         /* UI colors */
262         LINE(DEFAULT,      "",  COLOR_TRANSP,   COLOR_TRANSP,   A_NORMAL),
263         LINE(CURSOR,       "",  COLOR_WHITE,    COLOR_GREEN,    A_BOLD),
264         LINE(STATUS,       "",  COLOR_GREEN,    COLOR_TRANSP,   0),
265         LINE(TITLE,        "",  COLOR_YELLOW,   COLOR_BLUE,     A_BOLD),
266         LINE(MAIN_DATE,    "",  COLOR_BLUE,     COLOR_TRANSP,   0),
267         LINE(MAIN_AUTHOR,  "",  COLOR_GREEN,    COLOR_TRANSP,   0),
268         LINE(MAIN_COMMIT,  "",  COLOR_TRANSP,   COLOR_TRANSP,   0),
269         LINE(MAIN_DELIM,   "",  COLOR_MAGENTA,  COLOR_TRANSP,   0),
270 };
271
272 static struct line_info *
273 get_line_info(char *line)
274 {
275         int linelen = strlen(line);
276         int i;
277
278         for (i = 0; i < ARRAY_SIZE(line_info); i++) {
279                 if (linelen < line_info[i].linelen
280                     || strncasecmp(line_info[i].line, line, line_info[i].linelen))
281                         continue;
282
283                 return &line_info[i];
284         }
285
286         return NULL;
287 }
288
289 static enum line_type
290 get_line_type(char *line)
291 {
292         struct line_info *info = get_line_info(line);
293
294         return info ? info->type : LINE_DEFAULT;
295 }
296
297 static int
298 get_line_attr(enum line_type type)
299 {
300         int i;
301
302         for (i = 0; i < ARRAY_SIZE(line_info); i++)
303                 if (line_info[i].type == type)
304                         return COLOR_PAIR(line_info[i].type) | line_info[i].attr;
305
306         return A_NORMAL;
307 }
308
309 static void
310 init_colors(void)
311 {
312         int transparent_bg = COLOR_BLACK;
313         int transparent_fg = COLOR_WHITE;
314         int i;
315
316         start_color();
317
318         if (use_default_colors() != ERR) {
319                 transparent_bg = -1;
320                 transparent_fg = -1;
321         }
322
323         for (i = 0; i < ARRAY_SIZE(line_info); i++) {
324                 struct line_info *info = &line_info[i];
325                 int bg = info->bg == COLOR_TRANSP ? transparent_bg : info->bg;
326                 int fg = info->fg == COLOR_TRANSP ? transparent_fg : info->fg;
327
328                 init_pair(info->type, fg, bg);
329         }
330 }
331
332
333 /**
334  * KEYS
335  * ----
336  *
337  * d::
338  *      diff
339  * l::
340  *      log
341  * q::
342  *      quit
343  * r::
344  *      redraw screen
345  * s::
346  *      stop all background loading
347  * j::
348  *      down
349  * k::
350  *      up
351  * h, ?::
352  *      help
353  * v::
354  *      version
355  **/
356
357 #define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp"
358
359 struct keymap {
360         int alias;
361         int request;
362 };
363
364 struct keymap keymap[] = {
365         /* Cursor navigation */
366         { KEY_UP,       REQ_PREV_LINE },
367         { 'k',          REQ_PREV_LINE },
368         { KEY_DOWN,     REQ_NEXT_LINE },
369         { 'j',          REQ_NEXT_LINE },
370         { KEY_HOME,     REQ_FIRST_LINE },
371         { KEY_END,      REQ_LAST_LINE },
372         { KEY_NPAGE,    REQ_NEXT_PAGE },
373         { KEY_PPAGE,    REQ_PREV_PAGE },
374
375         /* Scrolling */
376         { KEY_IC,       REQ_SCR_BLINE }, /* scroll field backward a line */
377         { KEY_DC,       REQ_SCR_FLINE }, /* scroll field forward a line */
378         { 's',          REQ_SCR_FPAGE }, /* scroll field forward a page */
379         { 'w',          REQ_SCR_BPAGE }, /* scroll field backward a page */
380
381         { 'd',          REQ_DIFF },
382         { 'l',          REQ_LOG },
383         { 'm',          REQ_MAIN },
384
385         { 'n',          REQ_LINE_NUMBER },
386
387         /* No input from wgetch() with nodelay() enabled. */
388         { ERR,          REQ_UPDATE },
389
390         { KEY_ESC,      REQ_QUIT },
391         { 'q',          REQ_QUIT },
392         { 's',          REQ_STOP },
393         { 'v',          REQ_VERSION },
394         { 'r',          REQ_REDRAW },
395 };
396
397 static int
398 get_request(int request)
399 {
400         int i;
401
402         for (i = 0; i < ARRAY_SIZE(keymap); i++)
403                 if (keymap[i].alias == request)
404                         return keymap[i].request;
405
406         return request;
407 }
408
409
410 /*
411  * Viewer
412  */
413
414 struct view {
415         char *name;
416         char *cmd;
417         char *id;
418
419
420         /* Rendering */
421         int (*read)(struct view *, char *);
422         int (*draw)(struct view *, unsigned int);
423         size_t objsize;         /* Size of objects in the line index */
424
425         WINDOW *win;
426         int height, width;
427
428         /* Navigation */
429         unsigned long offset;   /* Offset of the window top */
430         unsigned long lineno;   /* Current line number */
431
432         /* Buffering */
433         unsigned long lines;    /* Total number of lines */
434         void **line;            /* Line index */
435
436         /* Loading */
437         FILE *pipe;
438 };
439
440 static int pager_draw(struct view *view, unsigned int lineno);
441 static int pager_read(struct view *view, char *line);
442
443 static int main_draw(struct view *view, unsigned int lineno);
444 static int main_read(struct view *view, char *line);
445
446 #define DIFF_CMD \
447         "git log --stat -n1 %s ; echo; " \
448         "git diff --find-copies-harder -B -C %s^ %s"
449
450 #define LOG_CMD \
451         "git log --stat -n100 %s"
452
453 #define MAIN_CMD \
454         "git log --stat --pretty=raw %s"
455
456 /* The status window is used for polling keystrokes. */
457 static WINDOW *status_win;
458 static WINDOW *title_win;
459
460 /* The number of loading views. Controls when nodelay should be in effect when
461  * polling user input. */
462 static unsigned int nloading;
463
464 static struct view views[] = {
465         { "diff",  DIFF_CMD,   commit_id,  pager_read,  pager_draw, sizeof(char) },
466         { "log",   LOG_CMD,    head_id,    pager_read,  pager_draw, sizeof(char) },
467         { "main",  MAIN_CMD,   head_id,    main_read,   main_draw,  sizeof(struct commit) },
468 };
469
470 /* The display array of active views and the index of the current view. */
471 static struct view *display[ARRAY_SIZE(views)];
472 static unsigned int current_view;
473
474 #define foreach_view(view, i) \
475         for (i = 0; i < sizeof(display) && (view = display[i]); i++)
476
477
478 static void
479 redraw_view(struct view *view)
480 {
481         int lineno;
482
483         wclear(view->win);
484         wmove(view->win, 0, 0);
485
486         for (lineno = 0; lineno < view->height; lineno++) {
487                 if (!view->draw(view, lineno))
488                         break;
489         }
490
491         redrawwin(view->win);
492         wrefresh(view->win);
493 }
494
495 static void
496 resize_view(struct view *view)
497 {
498         int lines, cols;
499
500         getmaxyx(stdscr, lines, cols);
501
502         if (view->win) {
503                 mvwin(view->win, 0, 0);
504                 wresize(view->win, lines - 2, cols);
505
506         } else {
507                 view->win = newwin(lines - 2, 0, 0, 0);
508                 if (!view->win) {
509                         report("failed to create %s view", view->name);
510                         return;
511                 }
512                 scrollok(view->win, TRUE);
513         }
514
515         getmaxyx(view->win, view->height, view->width);
516 }
517
518 /* FIXME: Fix percentage. */
519 static void
520 report_position(struct view *view, int all)
521 {
522         report(all ? "line %d of %d (%d%%)"
523                      : "line %d of %d",
524                view->lineno + 1,
525                view->lines,
526                view->lines ? (view->lineno + 1) * 100 / view->lines : 0);
527 }
528
529
530 static void
531 move_view(struct view *view, int lines)
532 {
533         /* The rendering expects the new offset. */
534         view->offset += lines;
535
536         assert(0 <= view->offset && view->offset < view->lines);
537         assert(lines);
538
539         if (view->height < ABS(lines)) {
540                 redraw_view(view);
541
542         } else {
543                 int line = lines > 0 ? view->height - lines : 0;
544                 int end = line + (lines > 0 ? lines : -lines);
545
546                 wscrl(view->win, lines);
547
548                 for (; line < end; line++) {
549                         if (!view->draw(view, line))
550                                 break;
551                 }
552         }
553
554         /* Move current line into the view. */
555         if (view->lineno < view->offset) {
556                 view->lineno = view->offset;
557                 view->draw(view, 0);
558
559         } else if (view->lineno >= view->offset + view->height) {
560                 view->lineno = view->offset + view->height - 1;
561                 view->draw(view, view->lineno - view->offset);
562         }
563
564         assert(view->offset <= view->lineno && view->lineno < view->lines);
565
566         redrawwin(view->win);
567         wrefresh(view->win);
568
569         report_position(view, lines);
570 }
571
572 static void
573 scroll_view(struct view *view, int request)
574 {
575         int lines = 1;
576
577         switch (request) {
578         case REQ_SCR_FPAGE:
579                 lines = view->height;
580         case REQ_SCR_FLINE:
581                 if (view->offset + lines > view->lines)
582                         lines = view->lines - view->offset;
583
584                 if (lines == 0 || view->offset + view->height >= view->lines) {
585                         report("already at last line");
586                         return;
587                 }
588                 break;
589
590         case REQ_SCR_BPAGE:
591                 lines = view->height;
592         case REQ_SCR_BLINE:
593                 if (lines > view->offset)
594                         lines = view->offset;
595
596                 if (lines == 0) {
597                         report("already at first line");
598                         return;
599                 }
600
601                 lines = -lines;
602                 break;
603         }
604
605         move_view(view, lines);
606 }
607
608 static void
609 navigate_view(struct view *view, int request)
610 {
611         int steps;
612
613         switch (request) {
614         case REQ_FIRST_LINE:
615                 steps = -view->lineno;
616                 break;
617
618         case REQ_LAST_LINE:
619                 steps = view->lines - view->lineno - 1;
620                 break;
621
622         case REQ_PREV_PAGE:
623                 steps = view->height > view->lineno
624                       ? -view->lineno : -view->height;
625                 break;
626
627         case REQ_NEXT_PAGE:
628                 steps = view->lineno + view->height >= view->lines
629                       ? view->lines - view->lineno - 1 : view->height;
630                 break;
631
632         case REQ_PREV_LINE:
633                 steps = -1;
634                 break;
635
636         case REQ_NEXT_LINE:
637                 steps = 1;
638                 break;
639         }
640
641         if (steps <= 0 && view->lineno == 0) {
642                 report("already at first line");
643                 return;
644
645         } else if (steps >= 0 && view->lineno + 1 == view->lines) {
646                 report("already at last line");
647                 return;
648         }
649
650         /* Move the current line */
651         view->lineno += steps;
652         assert(0 <= view->lineno && view->lineno < view->lines);
653
654         /* Repaint the old "current" line if we be scrolling */
655         if (ABS(steps) < view->height)
656                 view->draw(view, view->lineno - steps - view->offset);
657
658         /* Check whether the view needs to be scrolled */
659         if (view->lineno < view->offset ||
660             view->lineno >= view->offset + view->height) {
661                 if (steps < 0 && -steps > view->offset) {
662                         steps = -view->offset;
663
664                 } else if (steps > 0) {
665                         if (view->lineno == view->lines - 1 &&
666                             view->lines > view->height) {
667                                 steps = view->lines - view->offset - 1;
668                                 if (steps >= view->height)
669                                         steps -= view->height - 1;
670                         }
671                 }
672
673                 move_view(view, steps);
674                 return;
675         }
676
677         /* Draw the current line */
678         view->draw(view, view->lineno - view->offset);
679
680         redrawwin(view->win);
681         wrefresh(view->win);
682
683         report_position(view, view->height);
684 }
685
686
687
688 static bool
689 begin_update(struct view *view)
690 {
691         char buf[1024];
692
693         if (view->cmd) {
694                 char *id = view->id;
695
696                 if (snprintf(buf, sizeof(buf), view->cmd, id, id, id) < sizeof(buf))
697                         view->pipe = popen(buf, "r");
698
699                 if (!view->pipe)
700                         return FALSE;
701
702                 if (nloading++ == 0)
703                         nodelay(status_win, TRUE);
704         }
705
706         display[current_view] = view;
707
708         view->offset = 0;
709         view->lines  = 0;
710         view->lineno = 0;
711
712         return TRUE;
713 }
714
715 static void
716 end_update(struct view *view)
717 {
718         wattrset(view->win, A_NORMAL);
719         pclose(view->pipe);
720         view->pipe = NULL;
721
722         if (nloading-- == 1)
723                 nodelay(status_win, FALSE);
724 }
725
726 static int
727 update_view(struct view *view)
728 {
729         char buffer[BUFSIZ];
730         char *line;
731         void **tmp;
732         int redraw;
733         int lines = view->height;
734
735         if (!view->pipe)
736                 return TRUE;
737
738         /* Only redraw after the first reading session. */
739         /* FIXME: ... and possibly more. */
740         redraw = view->height > view->lines;
741
742         tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines));
743         if (!tmp)
744                 goto alloc_error;
745
746         view->line = tmp;
747
748         while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
749                 int linelen;
750
751                 linelen = strlen(line);
752                 if (linelen)
753                         line[linelen - 1] = 0;
754
755                 if (!view->read(view, line))
756                         goto alloc_error;
757
758                 if (lines-- == 1)
759                         break;
760         }
761
762         if (redraw) {
763                 /* FIXME: This causes flickering. Draw incrementally. */
764                 redraw_view(view);
765         }
766
767         if (ferror(view->pipe)) {
768                 report("failed to read %s: %s", view->cmd, strerror(errno));
769                 goto end;
770
771         } else if (feof(view->pipe)) {
772                 report_position(view, 0);
773                 goto end;
774         }
775
776         return TRUE;
777
778 alloc_error:
779         report("allocation failure");
780
781 end:
782         end_update(view);
783         return FALSE;
784 }
785
786
787 static struct view *
788 switch_view(struct view *prev, int request)
789 {
790         struct view *view = &views[VIEW_OFFSET(request)];
791         struct view *displayed;
792         int i;
793
794         if (view == prev) {
795                 foreach_view (displayed, i) ;
796
797                 if (i == 1)
798                         report("already in %s view", view->name);
799                 else
800                         report("FIXME: Maximize");
801
802                 return view;
803
804         } else {
805                 foreach_view (displayed, i) {
806                         if (view == displayed) {
807                                 current_view = i;
808                                 report("new current view");
809                                 return view;
810                         }
811                 }
812         }
813
814         if (!view->win)
815                 resize_view(view);
816
817         /* Reload */
818
819         if (view->line) {
820                 for (i = 0; i < view->lines; i++)
821                         if (view->line[i])
822                                 free(view->line[i]);
823
824                 free(view->line);
825                 view->line = NULL;
826         }
827
828         if (prev && prev->pipe)
829                 end_update(prev);
830
831         if (begin_update(view)) {
832                 if (!view->cmd)
833                         report("%s", HELP);
834                 else
835                         report("loading...");
836         }
837
838         return view;
839 }
840
841
842 /* Process a keystroke */
843 static int
844 view_driver(struct view *view, int key)
845 {
846         int request = get_request(key);
847         int i;
848
849         switch (request) {
850         case REQ_NEXT_LINE:
851         case REQ_PREV_LINE:
852         case REQ_FIRST_LINE:
853         case REQ_LAST_LINE:
854         case REQ_NEXT_PAGE:
855         case REQ_PREV_PAGE:
856                 if (view)
857                         navigate_view(view, request);
858                 break;
859
860         case REQ_SCR_FLINE:
861         case REQ_SCR_BLINE:
862         case REQ_SCR_FPAGE:
863         case REQ_SCR_BPAGE:
864                 if (view)
865                         scroll_view(view, request);
866                 break;
867
868         case REQ_MAIN:
869         case REQ_LOG:
870         case REQ_DIFF:
871                 view = switch_view(view, request);
872                 break;
873
874         case REQ_LINE_NUMBER:
875                 opt_line_number = !opt_line_number;
876                 redraw_view(view);
877                 break;
878
879         case REQ_REDRAW:
880                 redraw_view(view);
881                 break;
882
883         case REQ_STOP:
884                 foreach_view (view, i) {
885                         if (view->pipe) {
886                                 end_update(view);
887                                 scroll_view(view, 0);
888                         }
889                 }
890                 break;
891
892         case REQ_VERSION:
893                 report("version %s", VERSION);
894                 return TRUE;
895
896         case REQ_UPDATE:
897                 doupdate();
898                 return TRUE;
899
900         case REQ_QUIT:
901                 return FALSE;
902
903         default:
904                 report(HELP);
905                 return TRUE;
906         }
907
908         return TRUE;
909 }
910
911
912 /*
913  * Rendering
914  */
915
916 static int
917 pager_draw(struct view *view, unsigned int lineno)
918 {
919         enum line_type type;
920         char *line;
921         int linelen;
922         int attr;
923
924         if (view->offset + lineno >= view->lines)
925                 return FALSE;
926
927         line = view->line[view->offset + lineno];
928         type = get_line_type(line);
929
930         if (view->offset + lineno == view->lineno) {
931                 if (type == LINE_COMMIT)
932                         strncpy(commit_id, line + 7, SIZEOF_ID);
933                 type = LINE_CURSOR;
934         }
935
936         attr = get_line_attr(type);
937         wattrset(view->win, attr);
938
939         linelen = strlen(line);
940         linelen = MIN(linelen, view->width);
941
942         if (opt_line_number) {
943                 wmove(view->win, lineno, 0);
944                 lineno += view->offset + 1;
945                 if (lineno == 1 || (lineno % 10) == 0)
946                         wprintw(view->win, "%4d: ", lineno);
947                 else
948                         wprintw(view->win, "    : ", lineno);
949
950                 while (line) {
951                         if (*line == '\t') {
952                                 waddstr(view->win, "        ");
953                                 line++;
954                         } else {
955                                 char *tab = strchr(line, '\t');
956
957                                 if (tab)
958                                         waddnstr(view->win, line, tab - line);
959                                 else
960                                         waddstr(view->win, line);
961                                 line = tab;
962                         }
963                 }
964                 waddstr(view->win, line);
965
966         } else {
967                 /* No empty lines makes cursor drawing and clearing implicit. */
968                 if (!*line)
969                         line = " ", linelen = 1;
970                 mvwaddnstr(view->win, lineno, 0, line, linelen);
971         }
972
973         return TRUE;
974 }
975
976 static int
977 pager_read(struct view *view, char *line)
978 {
979         view->line[view->lines] = strdup(line);
980         if (!view->line[view->lines])
981                 return FALSE;
982
983         view->lines++;
984         return TRUE;
985 }
986
987 static int
988 main_draw(struct view *view, unsigned int lineno)
989 {
990         char buf[21];
991         struct commit *commit;
992         enum line_type type;
993         int cols = 0;
994         size_t timelen;
995
996         if (view->offset + lineno >= view->lines)
997                 return FALSE;
998
999         commit = view->line[view->offset + lineno];
1000         if (!*commit->author)
1001                 return FALSE;
1002
1003         if (view->offset + lineno == view->lineno) {
1004                 strncpy(commit_id, commit->id, SIZEOF_ID);
1005                 type = LINE_CURSOR;
1006         } else {
1007                 type = LINE_MAIN_COMMIT;
1008         }
1009
1010         wmove(view->win, lineno, cols);
1011         wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1012
1013         timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1014         waddnstr(view->win, buf, timelen);
1015         waddstr(view->win, " ");
1016
1017         cols += DATE_COLS;
1018         wmove(view->win, lineno, cols);
1019         wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1020
1021         if (strlen(commit->author) > 19) {
1022                 waddnstr(view->win, commit->author, 18);
1023                 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1024                 waddch(view->win, '~');
1025         } else {
1026                 waddstr(view->win, commit->author);
1027         }
1028
1029         cols += 20;
1030         wattrset(view->win, A_NORMAL);
1031         mvwaddch(view->win, lineno, cols, ACS_LTEE);
1032         wattrset(view->win, get_line_attr(type));
1033         mvwaddstr(view->win, lineno, cols + 2, commit->title);
1034         wattrset(view->win, A_NORMAL);
1035
1036         return TRUE;
1037 }
1038
1039 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1040 static int
1041 main_read(struct view *view, char *line)
1042 {
1043         enum line_type type = get_line_type(line);
1044         struct commit *commit;
1045
1046         switch (type) {
1047         case LINE_COMMIT:
1048                 commit = calloc(1, sizeof(struct commit));
1049                 if (!commit)
1050                         return FALSE;
1051
1052                 line += STRING_SIZE("commit ");
1053
1054                 view->line[view->lines++] = commit;
1055                 strncpy(commit->id, line, sizeof(commit->id));
1056                 break;
1057
1058         case LINE_AUTHOR_IDENT:
1059         {
1060                 char *ident = line + STRING_SIZE("author ");
1061                 char *end = strchr(ident, '<');
1062
1063                 if (end) {
1064                         for (; end > ident && isspace(end[-1]); end--) ;
1065                         *end = 0;
1066                 }
1067
1068                 commit = view->line[view->lines - 1];
1069                 strncpy(commit->author, ident, sizeof(commit->author));
1070
1071                 /* Parse epoch and timezone */
1072                 if (end) {
1073                         char *secs = strchr(end + 1, '>');
1074                         char *zone;
1075                         time_t time;
1076
1077                         if (!secs || secs[1] != ' ')
1078                                 break;
1079
1080                         secs += 2;
1081                         time = (time_t) atol(secs);
1082                         zone = strchr(secs, ' ');
1083                         if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1084                                 long tz;
1085
1086                                 zone++;
1087                                 tz  = ('0' - zone[1]) * 60 * 60 * 10;
1088                                 tz += ('0' - zone[2]) * 60 * 60;
1089                                 tz += ('0' - zone[3]) * 60;
1090                                 tz += ('0' - zone[4]) * 60;
1091
1092                                 if (zone[0] == '-')
1093                                         tz = -tz;
1094
1095                                 time -= tz;
1096                         }
1097                         gmtime_r(&time, &commit->time);
1098                 }
1099                 break;
1100         }
1101         default:
1102                 /* Fill in the commit title */
1103                 commit = view->line[view->lines - 1];
1104                 if (!commit->title[0] &&
1105                     !strncmp(line, "    ", 4) &&
1106                     !isspace(line[5]))
1107                         strncpy(commit->title, line + 4, sizeof(commit->title));
1108         }
1109
1110         return TRUE;
1111 }
1112
1113
1114 /*
1115  * Main
1116  */
1117
1118 static void
1119 quit(int sig)
1120 {
1121         if (status_win)
1122                 delwin(status_win);
1123         if (title_win)
1124                 delwin(title_win);
1125         endwin();
1126
1127         /* FIXME: Shutdown gracefully. */
1128
1129         exit(0);
1130 }
1131
1132 static void die(const char *err, ...)
1133 {
1134         va_list args;
1135
1136         endwin();
1137
1138         va_start(args, err);
1139         fputs("tig: ", stderr);
1140         vfprintf(stderr, err, args);
1141         fputs("\n", stderr);
1142         va_end(args);
1143
1144         exit(1);
1145 }
1146
1147 static void
1148 report(const char *msg, ...)
1149 {
1150         va_list args;
1151
1152         werase(title_win);
1153         wmove(title_win, 0, 0);
1154         wprintw(title_win, "commit %s", commit_id);
1155         wrefresh(title_win);
1156
1157         va_start(args, msg);
1158
1159         werase(status_win);
1160         wmove(status_win, 0, 0);
1161
1162 #if 0
1163         if (display[current_view])
1164                 wprintw(status_win, "%s %4s: ", commit_id, display[current_view]->name);
1165 #endif
1166         vwprintw(status_win, msg, args);
1167         wrefresh(status_win);
1168
1169         va_end(args);
1170 }
1171
1172 int
1173 main(int argc, char *argv[])
1174 {
1175         int x, y;
1176         int request;
1177         int git_cmd;
1178
1179         signal(SIGINT, quit);
1180
1181         git_cmd = parse_options(argc, argv);
1182         if (git_cmd < 0)
1183                 return 0;
1184
1185         request = opt_request;
1186
1187         initscr();      /* initialize the curses library */
1188         nonl();         /* tell curses not to do NL->CR/NL on output */
1189         cbreak();       /* take input chars one at a time, no wait for \n */
1190         noecho();       /* don't echo input */
1191         leaveok(stdscr, TRUE);
1192         /* curs_set(0); */
1193
1194         if (has_colors())
1195                 init_colors();
1196
1197         getmaxyx(stdscr, y, x);
1198         status_win = newwin(1, 0, y - 1, 0);
1199         if (!status_win)
1200                 die("Failed to create status window");
1201
1202         title_win = newwin(1, 0, y - 2, 0);
1203         if (!title_win)
1204                 die("Failed to create title window");
1205
1206         /* Enable keyboard mapping */
1207         keypad(status_win, TRUE);
1208         wbkgdset(status_win, get_line_attr(LINE_STATUS));
1209         wbkgdset(title_win, get_line_attr(LINE_TITLE));
1210
1211         while (view_driver(display[current_view], request)) {
1212                 struct view *view;
1213                 int i;
1214
1215                 foreach_view (view, i) {
1216                         if (view->pipe) {
1217                                 update_view(view);
1218                         }
1219                 }
1220
1221                 /* Refresh, accept single keystroke of input */
1222                 request = wgetch(status_win);
1223                 if (request == KEY_RESIZE) {
1224                         int lines, cols;
1225
1226                         getmaxyx(stdscr, lines, cols);
1227
1228                         mvwin(status_win, lines - 1, 0);
1229                         wresize(status_win, 1, cols - 1);
1230
1231                         mvwin(title_win, lines - 2, 0);
1232                         wresize(title_win, 1, cols - 1);
1233                 }
1234         }
1235
1236         quit(0);
1237
1238         return 0;
1239 }
1240
1241 /**
1242  * BUGS
1243  * ----
1244  * Known bugs and problems:
1245  *
1246  * Redrawing of the main view while loading::
1247  *      If only part of a commit has been parsed not all fields will be visible
1248  *      or even redrawn when the whole commit have loaded. This can be
1249  *      triggered when continuously moving to the last line. Use 'r' to redraw
1250  *      the whole screen.
1251  *
1252  * TODO
1253  * ----
1254  * Features that should be explored.
1255  *
1256  *  - Proper command line handling; ability to take the command that should be
1257  *    shown. Example:
1258  *
1259  *      $ tig log -p
1260  *
1261  *  - Internal command line (exmode-inspired) which allows to specify what git
1262  *    log or git diff command to run. Example:
1263  *
1264  *      :log -p
1265  *
1266  * - Proper resizing support. I am yet to figure out whether catching SIGWINCH
1267  *   is preferred over using ncurses' built-in support for resizing.
1268  *
1269  * - Locale support.
1270  *
1271  * COPYRIGHT
1272  * ---------
1273  * Copyright (c) Jonas Fonseca, 2006
1274  *
1275  * This program is free software; you can redistribute it and/or modify
1276  * it under the terms of the GNU General Public License as published by
1277  * the Free Software Foundation; either version 2 of the License, or
1278  * (at your option) any later version.
1279  *
1280  * SEE ALSO
1281  * --------
1282  * [verse]
1283  * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
1284  * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
1285  * gitk(1): git repository browser written using tcl/tk,
1286  * gitview(1): git repository browser written using python/gtk.
1287  **/