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