The pager is begining to work. :)
[tig] / tig.c
CommitLineData
b801d8b2
JF
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
44static void die(const char *err, ...);
45static 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
93struct keymap {
94 int alias;
95 int request;
96};
97
98struct 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
120static int
121get_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
137struct 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
157static 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. */
167static WINDOW *status_win;
168
169static struct view views[] = {
170 { "diff", DIFF_CMD, default_renderer },
171 { "log", LOG_CMD, default_renderer },
172 { "main", NULL },
173};
174
175static struct view *display[ARRAY_SIZE(views)];
176static unsigned int current_view;
177static unsigned int nloading;
178
179#define foreach_view(view, i) \
180 for (i = 0; i < sizeof(display) && (view = display[i]); i++)
181
182static void
183redraw_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. */
202static void
203report_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
213static void
214scroll_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 - 1;
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
285static void
286resize_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
307static bool
308begin_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
332static void
333end_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
343static int
344update_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
395alloc_error:
396 report("Allocation failure");
397
398end:
399 end_update(view);
400 return FALSE;
401}
402
403
404static struct view *
405switch_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 */
460static int
461view_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
520struct attr {
521 char *line;
522 int linelen;
523 int attr;
524};
525
526static 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
538static int
539default_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
570static void
571quit(int sig)
572{
573 if (status_win)
574 delwin(status_win);
575 endwin();
576
577 /* FIXME: Shutdown gracefully. */
578
579 exit(0);
580}
581
582static 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
597static void
598report(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
616static void
617init_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
636int
637main(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 **/