Started on the main view
[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
13  * tig log  [git log options]
14  * tig diff [git diff options]
15  * tig < [git log or git diff output]
16  *
17  * DESCRIPTION
18  * -----------
19  * Browse changes in a git repository.
20  *
21  * OPTIONS
22  * -------
23  *
24  * None.
25  *
26  **/
27
28 #define DEBUG
29
30 #ifndef DEBUG
31 #define NDEBUG
32 #endif
33
34 #include <assert.h>
35 #include <ctype.h>
36 #include <signal.h>
37 #include <stdarg.h>
38 #include <stdio.h>
39 #include <stdlib.h>
40 #include <string.h>
41
42 #include <curses.h>
43 #include <form.h>
44
45 static void die(const char *err, ...);
46 static void report(const char *msg, ...);
47
48 #define ARRAY_SIZE(x)   (sizeof(x) / sizeof(x[0]))
49
50 #define KEY_TAB         9
51 #define KEY_ESC         27
52 #define KEY_DEL         127
53
54 #define REQ_OFFSET      (MAX_COMMAND + 1)
55
56 /* Requests for switching between the different views. */
57 #define REQ_DIFF        (REQ_OFFSET + 0)
58 #define REQ_LOG         (REQ_OFFSET + 1)
59 #define REQ_MAIN        (REQ_OFFSET + 2)
60 #define REQ_VIEWS       (REQ_OFFSET + 3)
61
62 #define REQ_QUIT        (REQ_OFFSET + 11)
63 #define REQ_VERSION     (REQ_OFFSET + 12)
64 #define REQ_STOP        (REQ_OFFSET + 13)
65 #define REQ_UPDATE      (REQ_OFFSET + 14)
66 #define REQ_REDRAW      (REQ_OFFSET + 15)
67 #define REQ_FIRST_LINE  (REQ_OFFSET + 16)
68 #define REQ_LAST_LINE   (REQ_OFFSET + 17)
69
70 #define COLOR_CURSOR    42
71
72 /**
73  * KEYS
74  * ----
75  *
76  * d::
77  *      diff
78  * l::
79  *      log
80  * q::
81  *      quit
82  * r::
83  *      redraw screen
84  * s::
85  *      stop all background loading
86  * j::
87  *      down
88  * k::
89  *      up
90  * h, ?::
91  *      help
92  * v::
93  *      version
94  *
95  **/
96
97 #define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp"
98
99 struct keymap {
100         int alias;
101         int request;
102 };
103
104 struct keymap keymap[] = {
105         /* Cursor navigation */
106         { KEY_UP,       REQ_PREV_LINE },
107         { 'k',          REQ_PREV_LINE },
108         { KEY_DOWN,     REQ_NEXT_LINE },
109         { 'j',          REQ_NEXT_LINE },
110         { KEY_HOME,     REQ_FIRST_LINE },
111         { KEY_END,      REQ_LAST_LINE },
112
113         /* Scrolling */
114         { KEY_IC,       REQ_SCR_BLINE }, /* scroll field backward a line */
115         { KEY_DC,       REQ_SCR_FLINE }, /* scroll field forward a line */
116         { KEY_NPAGE,    REQ_SCR_FPAGE }, /* scroll field forward a page */
117         { KEY_PPAGE,    REQ_SCR_BPAGE }, /* scroll field backward a page */
118         { 'w',          REQ_SCR_FHPAGE }, /* scroll field forward half page */
119         { 's',          REQ_SCR_BHPAGE }, /* scroll field backward half page */
120
121         { 'd',          REQ_DIFF },
122         { 'l',          REQ_LOG },
123         { 'm',          REQ_MAIN },
124
125         /* No input from wgetch() with nodelay() enabled. */
126         { ERR,          REQ_UPDATE },
127
128         { KEY_ESC,      REQ_QUIT },
129         { 'q',          REQ_QUIT },
130         { 's',          REQ_STOP },
131         { 'v',          REQ_VERSION },
132         { 'r',          REQ_REDRAW },
133 };
134
135 static int
136 get_request(int request)
137 {
138         int i;
139
140         for (i = 0; i < ARRAY_SIZE(keymap); i++)
141                 if (keymap[i].alias == request)
142                         return keymap[i].request;
143
144         return request;
145 }
146
147
148 /*
149  * Viewer
150  */
151
152 struct view {
153         char *name;
154         char *cmd;
155         char *id;
156
157
158         /* Rendering */
159         int (*read)(struct view *, char *);
160         int (*draw)(struct view *, unsigned int);
161         size_t objsize;         /* Size of objects in the line index */
162
163         WINDOW *win;
164         int height, width;
165
166         /* Navigation */
167         unsigned long offset;   /* Offset of the window top */
168         unsigned long lineno;   /* Current line number */
169
170         /* Buffering */
171         unsigned long lines;    /* Total number of lines */
172         void **line;            /* Line index */
173
174         /* Loading */
175         FILE *pipe;
176 };
177
178 struct commit {
179         char id[41];
180         char title[75];
181 };
182
183 static int pager_draw(struct view *view, unsigned int lineno);
184 static int pager_read(struct view *view, char *line);
185
186 static int main_draw(struct view *view, unsigned int lineno);
187 static int main_read(struct view *view, char *line);
188
189 #define DIFF_CMD \
190         "git log --stat -n1 %s ; echo; " \
191         "git diff --find-copies-harder -B -C %s^ %s"
192
193 #define LOG_CMD \
194         "git log --stat -n100 %s"
195
196 #define MAIN_CMD \
197         "git log --stat --pretty=raw %s"
198
199 /* The status window at the bottom. Used for polling keystrokes. */
200 static WINDOW *status_win;
201
202 #define SIZEOF_ID       1024
203 #define SIZEOF_VIEWS    (REQ_VIEWS - REQ_OFFSET)
204
205 char head_id[SIZEOF_ID] = "HEAD";
206 char commit_id[SIZEOF_ID] = "HEAD";
207
208 static unsigned int current_view;
209 static unsigned int nloading;
210
211 static struct view views[];
212 static struct view *display[];
213
214 static struct view views[] = {
215         { "diff",  DIFF_CMD,   commit_id,  pager_read,  pager_draw, sizeof(char) },
216         { "log",   LOG_CMD,    head_id,    pager_read,  pager_draw, sizeof(struct commit) },
217         { "main",  MAIN_CMD,   head_id,    main_read,   main_draw },
218 };
219
220 static struct view *display[ARRAY_SIZE(views)];
221
222
223 #define foreach_view(view, i) \
224         for (i = 0; i < sizeof(display) && (view = display[i]); i++)
225
226 static void
227 redraw_view(struct view *view)
228 {
229         int lineno;
230
231         wclear(view->win);
232         wmove(view->win, 0, 0);
233
234         for (lineno = 0; lineno < view->height; lineno++) {
235                 if (!view->draw(view, lineno))
236                         break;
237         }
238
239         redrawwin(view->win);
240         wrefresh(view->win);
241 }
242
243 /* FIXME: Fix percentage. */
244 static void
245 report_position(struct view *view, int all)
246 {
247         report(all ? "line %d of %d (%d%%) viewing from %d"
248                      : "line %d of %d",
249                view->lineno + 1,
250                view->lines,
251                view->lines ? view->offset * 100 / view->lines : 0,
252                view->offset);
253 }
254
255 static void
256 move_view(struct view *view, int lines)
257 {
258         /* The rendering expects the new offset. */
259         view->offset += lines;
260
261         assert(0 <= view->offset && view->offset < view->lines);
262         assert(lines);
263
264         {
265                 int line = lines > 0 ? view->height - lines : 0;
266                 int end = line + (lines > 0 ? lines : -lines);
267
268                 wscrl(view->win, lines);
269
270                 for (; line < end; line++) {
271                         if (!view->draw(view, line))
272                                 break;
273                 }
274         }
275
276         /* Move current line into the view. */
277         if (view->lineno < view->offset) {
278                 view->lineno = view->offset;
279                 view->draw(view, 0);
280
281         } else if (view->lineno >= view->offset + view->height) {
282                 view->lineno = view->offset + view->height - 1;
283                 view->draw(view, view->lineno - view->offset);
284         }
285
286         assert(view->offset <= view->lineno && view->lineno <= view->lines);
287
288         redrawwin(view->win);
289         wrefresh(view->win);
290
291         report_position(view, lines);
292 }
293 static void
294 scroll_view(struct view *view, int request)
295 {
296         int lines = 1;
297
298         switch (request) {
299         case REQ_SCR_FPAGE:
300                 lines = view->height;
301         case REQ_SCR_FLINE:
302                 if (view->offset + lines > view->lines)
303                         lines = view->lines - view->offset;
304
305                 if (lines == 0 || view->offset + view->height >= view->lines) {
306                         report("already at last line");
307                         return;
308                 }
309                 break;
310
311         case REQ_SCR_BPAGE:
312                 lines = view->height;
313         case REQ_SCR_BLINE:
314                 if (lines > view->offset)
315                         lines = view->offset;
316
317                 if (lines == 0) {
318                         report("already at first line");
319                         return;
320                 }
321
322                 lines = -lines;
323                 break;
324         }
325
326         move_view(view, lines);
327 }
328
329
330 static void
331 navigate_view(struct view *view, int request)
332 {
333         int steps;
334
335         switch (request) {
336         case REQ_PREV_LINE:
337                 if (view->lineno == 0) {
338                         report("already at first line");
339                         return;
340                 }
341                 steps = -1;
342                 break;
343
344         case REQ_NEXT_LINE:
345                 if (view->lineno + 1 >= view->lines) {
346                         report("already at last line");
347                         return;
348                 }
349                 steps = 1;
350                 break;
351
352         case REQ_FIRST_LINE:
353                 steps = -view->lineno;
354                 break;
355
356         case REQ_LAST_LINE:
357                 steps = view->lines - view->lineno - 1;
358         }
359
360         view->lineno += steps;
361         view->draw(view, view->lineno - steps - view->offset);
362
363         if (view->lineno < view->offset ||
364             view->lineno >= view->offset + view->height) {
365                 if (steps < 0 && -steps > view->offset) {
366                         steps = -view->offset;
367                 }
368                 move_view(view, steps);
369                 return;
370         }
371
372         view->draw(view, view->lineno - view->offset);
373
374         redrawwin(view->win);
375         wrefresh(view->win);
376
377         report_position(view, view->height);
378 }
379
380 static void
381 resize_view(struct view *view)
382 {
383         int lines, cols;
384
385         getmaxyx(stdscr, lines, cols);
386
387         if (view->win) {
388                 mvwin(view->win, 0, 0);
389                 wresize(view->win, lines - 1, cols);
390
391         } else {
392                 view->win = newwin(lines - 1, 0, 0, 0);
393                 if (!view->win) {
394                         report("failed to create %s view", view->name);
395                         return;
396                 }
397                 scrollok(view->win, TRUE);
398         }
399
400         getmaxyx(view->win, view->height, view->width);
401 }
402
403
404 static bool
405 begin_update(struct view *view)
406 {
407         char buf[1024];
408
409         if (view->cmd) {
410                 char *id = view->id;
411
412                 if (snprintf(buf, sizeof(buf), view->cmd, id, id, id) < sizeof(buf))
413                         view->pipe = popen(buf, "r");
414
415                 if (!view->pipe)
416                         return FALSE;
417
418                 if (nloading++ == 0)
419                         nodelay(status_win, TRUE);
420         }
421
422         display[current_view] = view;
423
424         view->offset = 0;
425         view->lines  = 0;
426         view->lineno = 0;
427
428         return TRUE;
429 }
430
431 static void
432 end_update(struct view *view)
433 {
434         wattrset(view->win, A_NORMAL);
435         pclose(view->pipe);
436         view->pipe = NULL;
437
438         if (nloading-- == 1)
439                 nodelay(status_win, FALSE);
440 }
441
442 static int
443 update_view(struct view *view)
444 {
445         char buffer[BUFSIZ];
446         char *line;
447         void **tmp;
448         int redraw;
449         int lines = view->height;
450
451         if (!view->pipe)
452                 return TRUE;
453
454         /* Only redraw after the first reading session. */
455         /* FIXME: ... and possibly more. */
456         redraw = view->height > view->lines;
457
458         tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines));
459         if (!tmp)
460                 goto alloc_error;
461
462         view->line = tmp;
463
464         while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
465                 int linelen;
466
467                 linelen = strlen(line);
468                 if (linelen)
469                         line[linelen - 1] = 0;
470
471                 if (!view->read(view, line))
472                         goto alloc_error;
473
474                 if (lines-- == 1)
475                         break;
476         }
477
478         if (redraw)
479                 redraw_view(view);
480
481         if (ferror(view->pipe)) {
482                 report("failed to read %s", view->cmd);
483                 goto end;
484
485         } else if (feof(view->pipe)) {
486                 report_position(view, 0);
487                 goto end;
488         }
489
490         return TRUE;
491
492 alloc_error:
493         report("allocation failure");
494
495 end:
496         end_update(view);
497         return FALSE;
498 }
499
500
501 static struct view *
502 switch_view(struct view *prev, int request)
503 {
504         struct view *view = &views[request - REQ_OFFSET];
505         struct view *displayed;
506         int i;
507
508         if (view == prev) {
509                 foreach_view (displayed, i) ;
510
511                 if (i == 1)
512                         report("already in %s view", view->name);
513                 else
514                         report("FIXME: Maximize");
515
516                 return view;
517
518         } else {
519                 foreach_view (displayed, i) {
520                         if (view == displayed) {
521                                 current_view = i;
522                                 report("new current view");
523                                 return view;
524                         }
525                 }
526         }
527
528         if (!view->win)
529                 resize_view(view);
530
531         /* Reload */
532
533         if (view->line) {
534                 for (i = 0; i < view->lines; i++)
535                         if (view->line[i])
536                                 free(view->line[i]);
537
538                 free(view->line);
539                 view->line = NULL;
540         }
541
542         if (prev && prev->pipe)
543                 end_update(prev);
544
545         if (begin_update(view)) {
546                 if (!view->cmd)
547                         report("%s", HELP);
548                 else
549                         report("loading...");
550         }
551
552         return view;
553 }
554
555
556 /* Process a keystroke */
557 static int
558 view_driver(struct view *view, int key)
559 {
560         int request = get_request(key);
561         int i;
562
563         switch (request) {
564         case REQ_NEXT_LINE:
565         case REQ_PREV_LINE:
566         case REQ_FIRST_LINE:
567         case REQ_LAST_LINE:
568                 if (view)
569                         navigate_view(view, request);
570                 break;
571
572         case REQ_SCR_FLINE:
573         case REQ_SCR_BLINE:
574         case REQ_SCR_FPAGE:
575         case REQ_SCR_BPAGE:
576                 if (view)
577                         scroll_view(view, request);
578                 break;
579
580         case REQ_MAIN:
581         case REQ_LOG:
582         case REQ_DIFF:
583                 view = switch_view(view, request);
584                 break;
585
586         case REQ_REDRAW:
587                 redraw_view(view);
588                 break;
589
590         case REQ_STOP:
591                 foreach_view (view, i) {
592                         if (view->pipe) {
593                                 end_update(view);
594                                 scroll_view(view, 0);
595                         }
596                 }
597                 break;
598
599         case REQ_VERSION:
600                 report("version %s", VERSION);
601                 return TRUE;
602
603         case REQ_UPDATE:
604                 doupdate();
605                 return TRUE;
606
607         case REQ_QUIT:
608                 return FALSE;
609
610         default:
611                 report(HELP);
612                 return TRUE;
613         }
614
615         return TRUE;
616 }
617
618
619 /*
620  * Rendering
621  */
622
623 #define ATTR(line, attr) { (line), sizeof(line) - 1, (attr) }
624
625 struct attr {
626         char *line;
627         int linelen;
628         int attr;
629 };
630
631 static struct attr attrs[] = {
632         ATTR("commit ",         COLOR_PAIR(COLOR_GREEN)),
633         ATTR("Author: ",        COLOR_PAIR(COLOR_CYAN)),
634         ATTR("Date:   ",        COLOR_PAIR(COLOR_YELLOW)),
635         ATTR("diff --git ",     COLOR_PAIR(COLOR_YELLOW)),
636         ATTR("diff-tree ",      COLOR_PAIR(COLOR_BLUE)),
637         ATTR("index ",          COLOR_PAIR(COLOR_BLUE)),
638         ATTR("-",               COLOR_PAIR(COLOR_RED)),
639         ATTR("+",               COLOR_PAIR(COLOR_GREEN)),
640         ATTR("@",               COLOR_PAIR(COLOR_MAGENTA)),
641 };
642
643 static int
644 pager_draw(struct view *view, unsigned int lineno)
645 {
646         char *line;
647         int linelen;
648         int attr = A_NORMAL;
649         int i;
650
651         if (view->offset + lineno >= view->lines)
652                 return FALSE;
653
654         line = view->line[view->offset + lineno];
655         if (!line) return FALSE;
656
657         linelen = strlen(line);
658
659         for (i = 0; i < ARRAY_SIZE(attrs); i++) {
660                 if (linelen < attrs[i].linelen
661                     || strncmp(attrs[i].line, line, attrs[i].linelen))
662                         continue;
663
664                 attr = attrs[i].attr;
665                 break;
666         }
667
668         if (view->offset + lineno == view->lineno) {
669                 if (i == 0)
670                         strncpy(commit_id, line + 7, SIZEOF_ID);
671                 attr = COLOR_PAIR(COLOR_CURSOR) | A_BOLD;
672         }
673
674         wattrset(view->win, attr);
675         //mvwprintw(view->win, lineno, 0, "%4d: %s", view->offset + lineno, line);
676         mvwaddstr(view->win, lineno, 0, line);
677
678         return TRUE;
679 }
680
681 static int
682 pager_read(struct view *view, char *line)
683 {
684         view->line[view->lines] = strdup(line);
685         if (!view->line[view->lines])
686                 return FALSE;
687
688         view->lines++;
689         return TRUE;
690 }
691
692 static int
693 main_draw(struct view *view, unsigned int lineno)
694 {
695         struct commit *commit;
696         int attr = A_NORMAL;
697
698         if (view->offset + lineno >= view->lines)
699                 return FALSE;
700
701         commit = view->line[view->offset + lineno];
702         if (!commit) return FALSE;
703
704         attr = attrs[0].attr;
705
706         if (view->offset + lineno == view->lineno) {
707                 strncpy(commit_id, commit->id, SIZEOF_ID);
708                 attr = COLOR_PAIR(COLOR_CURSOR) | A_BOLD;
709         }
710
711         mvwaddch(view->win, lineno, 0, ACS_LTEE);
712         wattrset(view->win, attr);
713         mvwaddstr(view->win, lineno, 2, commit->title);
714         wattrset(view->win, A_NORMAL);
715
716         return TRUE;
717 }
718
719 static int
720 main_read(struct view *view, char *line)
721 {
722         int linelen = strlen(line);
723         int i;
724
725         for (i = 0; i < ARRAY_SIZE(attrs); i++) {
726                 if (linelen < attrs[i].linelen
727                     || strncmp(attrs[i].line, line, attrs[i].linelen))
728                         continue;
729                 break;
730         }
731
732         if (i == 0) {
733                 struct commit *commit;
734
735                 commit = calloc(1, sizeof(struct commit));
736                 if (!commit)
737                         return FALSE;
738
739                 view->line[view->lines++] = commit;
740
741                 strncpy(commit->id, line + 7, 41);
742
743         } else {
744                 struct commit *commit = view->line[view->lines - 1];
745
746                 if (!commit->title[0] &&
747                     linelen > 5 &&
748                     !strncmp(line, "    ", 4) &&
749                     !isspace(line[5]))
750                         strncpy(commit->title, line + 4, sizeof(commit->title));
751         }
752
753         return TRUE;
754 }
755
756
757 /*
758  * Main
759  */
760
761 static void
762 quit(int sig)
763 {
764         if (status_win)
765                 delwin(status_win);
766         endwin();
767
768         /* FIXME: Shutdown gracefully. */
769
770         exit(0);
771 }
772
773 static void die(const char *err, ...)
774 {
775         va_list args;
776
777         endwin();
778
779         va_start(args, err);
780         fputs("tig: ", stderr);
781         vfprintf(stderr, err, args);
782         fputs("\n", stderr);
783         va_end(args);
784
785         exit(1);
786 }
787
788 static void
789 report(const char *msg, ...)
790 {
791         va_list args;
792
793         va_start(args, msg);
794
795         werase(status_win);
796         wmove(status_win, 0, 0);
797
798 #if 0
799         if (display[current_view])
800                 wprintw(status_win, "%s %4s: ", commit_id, display[current_view]->name);
801 #endif
802         vwprintw(status_win, msg, args);
803         wrefresh(status_win);
804
805         va_end(args);
806 }
807
808 static void
809 init_colors(void)
810 {
811         int bg = COLOR_BLACK;
812
813         start_color();
814
815         if (use_default_colors() != ERR)
816                 bg = -1;
817
818         init_pair(COLOR_BLACK,   COLOR_BLACK,   bg);
819         init_pair(COLOR_GREEN,   COLOR_GREEN,   bg);
820         init_pair(COLOR_RED,     COLOR_RED,     bg);
821         init_pair(COLOR_CYAN,    COLOR_CYAN,    bg);
822         init_pair(COLOR_WHITE,   COLOR_WHITE,   bg);
823         init_pair(COLOR_MAGENTA, COLOR_MAGENTA, bg);
824         init_pair(COLOR_BLUE,    COLOR_BLUE,    bg);
825         init_pair(COLOR_YELLOW,  COLOR_YELLOW,  bg);
826         init_pair(COLOR_CURSOR,  COLOR_WHITE,   COLOR_GREEN);
827 }
828
829 int
830 main(int argc, char *argv[])
831 {
832         int request = REQ_MAIN;
833         int x, y;
834
835         signal(SIGINT, quit);
836
837         initscr();      /* initialize the curses library */
838         nonl();         /* tell curses not to do NL->CR/NL on output */
839         cbreak();       /* take input chars one at a time, no wait for \n */
840         noecho();       /* don't echo input */
841         leaveok(stdscr, TRUE);
842         /* curs_set(0); */
843
844         if (has_colors())
845                 init_colors();
846
847         getmaxyx(stdscr, y, x);
848         status_win = newwin(1, 0, y - 1, 0);
849         if (!status_win)
850                 die("Failed to create status window");
851
852         /* Enable keyboard mapping */
853         keypad(status_win, TRUE);
854         wattrset(status_win, COLOR_PAIR(COLOR_GREEN));
855
856         while (view_driver(display[current_view], request)) {
857                 struct view *view;
858                 int i;
859
860                 foreach_view (view, i) {
861                         if (view->pipe) {
862                                 update_view(view);
863                         }
864                 }
865
866                 /* Refresh, accept single keystroke of input */
867                 request = wgetch(status_win);
868                 if (request == KEY_RESIZE) {
869                         int lines, cols;
870
871                         getmaxyx(stdscr, lines, cols);
872                         mvwin(status_win, lines - 1, 0);
873                         wresize(status_win, 1, cols - 1);
874                 }
875         }
876
877         quit(0);
878
879         return 0;
880 }
881
882 /**
883  * COPYRIGHT
884  * ---------
885  * Copyright (c) Jonas Fonseca, 2006
886  *
887  * This program is free software; you can redistribute it and/or modify
888  * it under the terms of the GNU General Public License as published by
889  * the Free Software Foundation; either version 2 of the License, or
890  * (at your option) any later version.
891  *
892  * SEE ALSO
893  * --------
894  * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
895  * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
896  **/