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