Sync. Home/End seems to finally 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
b801d8b2 49#define KEY_TAB 9
fd85fef1
JF
50#define KEY_ESC 27
51#define KEY_DEL 127
b801d8b2
JF
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)
fd85fef1
JF
65#define REQ_FIRST_LINE (REQ_OFFSET + 16)
66#define REQ_LAST_LINE (REQ_OFFSET + 17)
b801d8b2 67
fd85fef1 68#define COLOR_CURSOR 42
b801d8b2
JF
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
97struct keymap {
98 int alias;
99 int request;
100};
101
102struct keymap keymap[] = {
fd85fef1 103 /* Cursor navigation */
b801d8b2
JF
104 { KEY_UP, REQ_PREV_LINE },
105 { 'k', REQ_PREV_LINE },
106 { KEY_DOWN, REQ_NEXT_LINE },
107 { 'j', REQ_NEXT_LINE },
fd85fef1
JF
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 */
b801d8b2
JF
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
133static int
134get_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
150struct view {
151 char *name;
152 char *cmd;
fd85fef1 153 char *id;
b801d8b2
JF
154
155 /* Rendering */
fd85fef1 156 int (*render)(struct view *, unsigned int);
b801d8b2 157 WINDOW *win;
fd85fef1 158 int height, width;
b801d8b2
JF
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
fd85fef1 172static int default_renderer(struct view *view, unsigned int lineno);
b801d8b2 173
fd85fef1 174#define DIFF_CMD \
b801d8b2
JF
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
fd85fef1
JF
181#define MAIN_CMD \
182 "git log --stat --pretty=raw %s"
183
b801d8b2
JF
184/* The status window at the bottom. Used for polling keystrokes. */
185static WINDOW *status_win;
186
fd85fef1
JF
187#define SIZEOF_ID 1024
188
189char head_id[SIZEOF_ID] = "HEAD";
190char commit_id[SIZEOF_ID] = "HEAD";
191
192static unsigned int current_view;
193static unsigned int nloading;
194
b801d8b2 195static struct view views[] = {
fd85fef1
JF
196 { "diff", DIFF_CMD, commit_id, default_renderer },
197 { "log", LOG_CMD, head_id, default_renderer },
198 { "main", MAIN_CMD, head_id, default_renderer },
b801d8b2
JF
199};
200
201static struct view *display[ARRAY_SIZE(views)];
b801d8b2
JF
202
203#define foreach_view(view, i) \
204 for (i = 0; i < sizeof(display) && (view = display[i]); i++)
205
206static void
207redraw_view(struct view *view)
208{
209 int lineno;
b801d8b2
JF
210
211 wclear(view->win);
212 wmove(view->win, 0, 0);
213
fd85fef1
JF
214 for (lineno = 0; lineno < view->height; lineno++) {
215 if (!view->render(view, lineno))
216 break;
b801d8b2
JF
217 }
218
219 redrawwin(view->win);
220 wrefresh(view->win);
221}
222
223/* FIXME: Fix percentage. */
224static void
225report_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
235static void
fd85fef1 236move_view(struct view *view, int lines)
b801d8b2 237{
fd85fef1
JF
238 /* The rendering expects the new offset. */
239 view->offset += lines;
240
241 assert(0 <= view->offset && view->offset < view->lines);
242 assert(lines);
b801d8b2 243
fd85fef1
JF
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}
273static void
274scroll_view(struct view *view, int request)
275{
276 int lines = 1;
b801d8b2
JF
277
278 switch (request) {
fd85fef1
JF
279 case REQ_SCR_FPAGE:
280 lines = view->height;
281 case REQ_SCR_FLINE:
b801d8b2 282 if (view->offset + lines > view->lines)
bde3653a 283 lines = view->lines - view->offset;
b801d8b2 284
fd85fef1 285 if (lines == 0 || view->offset + view->height >= view->lines) {
b801d8b2
JF
286 report("already at last line");
287 return;
288 }
289 break;
290
fd85fef1
JF
291 case REQ_SCR_BPAGE:
292 lines = view->height;
293 case REQ_SCR_BLINE:
b801d8b2
JF
294 if (lines > view->offset)
295 lines = view->offset;
296
297 if (lines == 0) {
298 report("already at first line");
299 return;
300 }
301
fd85fef1 302 lines = -lines;
b801d8b2 303 break;
b801d8b2
JF
304 }
305
fd85fef1
JF
306 move_view(view, lines);
307}
b801d8b2 308
b801d8b2 309
fd85fef1
JF
310static void
311navigate_view(struct view *view, int request)
312{
313 int steps;
b801d8b2 314
fd85fef1
JF
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;
b801d8b2 323
fd85fef1
JF
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;
b801d8b2 331
fd85fef1
JF
332 case REQ_FIRST_LINE:
333 steps = -view->lineno;
334 break;
b801d8b2 335
fd85fef1
JF
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;
b801d8b2 347 }
fd85fef1
JF
348 move_view(view, steps);
349 return;
b801d8b2
JF
350 }
351
fd85fef1
JF
352 view->render(view, view->lineno - view->offset);
353
b801d8b2
JF
354 redrawwin(view->win);
355 wrefresh(view->win);
356
fd85fef1 357 report_position(view, view->height);
b801d8b2
JF
358}
359
360static void
361resize_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) {
fd85fef1 374 report("failed to create %s view", view->name);
b801d8b2
JF
375 return;
376 }
377 scrollok(view->win, TRUE);
378 }
fd85fef1
JF
379
380 getmaxyx(view->win, view->height, view->width);
b801d8b2
JF
381}
382
383
384static bool
385begin_update(struct view *view)
386{
387 char buf[1024];
388
389 if (view->cmd) {
fd85fef1
JF
390 char *id = view->id;
391
392 if (snprintf(buf, sizeof(buf), view->cmd, id, id, id) < sizeof(buf))
b801d8b2
JF
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
411static void
412end_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
422static int
423update_view(struct view *view)
424{
425 char buffer[BUFSIZ];
426 char *line;
b801d8b2
JF
427 char **tmp;
428 int redraw;
fd85fef1 429 int lines = view->height;
b801d8b2
JF
430
431 if (!view->pipe)
432 return TRUE;
433
fd85fef1 434 /* Only redraw after the first reading session. */
b801d8b2
JF
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
b801d8b2
JF
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++;
fd85fef1
JF
454
455 if (lines-- == 1)
456 break;
b801d8b2
JF
457 }
458
459 if (redraw)
460 redraw_view(view);
461
462 if (ferror(view->pipe)) {
fd85fef1 463 report("failed to read %s", view->cmd);
b801d8b2
JF
464 goto end;
465
466 } else if (feof(view->pipe)) {
467 report_position(view, 0);
468 goto end;
469 }
470
471 return TRUE;
472
473alloc_error:
fd85fef1 474 report("allocation failure");
b801d8b2
JF
475
476end:
477 end_update(view);
478 return FALSE;
479}
480
481
482static struct view *
483switch_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)
fd85fef1 493 report("already in %s view", view->name);
b801d8b2
JF
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;
fd85fef1 503 report("new current view");
b801d8b2
JF
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 */
538static int
539view_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:
b801d8b2 546 case REQ_PREV_LINE:
fd85fef1
JF
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:
b801d8b2
JF
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
606struct attr {
607 char *line;
608 int linelen;
609 int attr;
610};
611
612static 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
624static int
fd85fef1 625default_renderer(struct view *view, unsigned int lineno)
b801d8b2
JF
626{
627 char *line;
628 int linelen;
629 int attr = A_NORMAL;
630 int i;
631
fd85fef1
JF
632 if (view->offset + lineno >= view->lines)
633 return FALSE;
634
b801d8b2
JF
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
fd85fef1
JF
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
b801d8b2 655 wattrset(view->win, attr);
fd85fef1
JF
656 //mvwprintw(view->win, lineno, 0, "%4d: %s", view->offset + lineno, line);
657 mvwaddstr(view->win, lineno, 0, line);
b801d8b2
JF
658
659 return TRUE;
660}
661
662/*
663 * Main
664 */
665
666static void
667quit(int sig)
668{
669 if (status_win)
670 delwin(status_win);
671 endwin();
672
673 /* FIXME: Shutdown gracefully. */
674
675 exit(0);
676}
677
678static 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
693static void
694report(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
fd85fef1 703#if 0
b801d8b2 704 if (display[current_view])
fd85fef1
JF
705 wprintw(status_win, "%s %4s: ", commit_id, display[current_view]->name);
706#endif
b801d8b2
JF
707 vwprintw(status_win, msg, args);
708 wrefresh(status_win);
709
710 va_end(args);
711}
712
713static void
714init_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);
fd85fef1 731 init_pair(COLOR_CURSOR, COLOR_WHITE, COLOR_GREEN);
b801d8b2
JF
732}
733
734int
735main(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 **/