TODO's and TODON'T's ...
[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 [options]
13 * tig [options] log [git log options]
14 * tig [options] diff [git diff options]
15 * tig [options] < [git log or git diff output]
16 *
17 * DESCRIPTION
18 * -----------
19 * Browse changes in a git repository.
20 **/
21
22 #define DEBUG
23 #ifndef DEBUG
24 #define NDEBUG
25 #endif
26
27 #ifndef VERSION
28 #define VERSION "tig-0.1"
29 #endif
30
31 #include <assert.h>
32 #include <errno.h>
33 #include <ctype.h>
34 #include <signal.h>
35 #include <stdarg.h>
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <string.h>
39 #include <time.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 /* Some ascii-shorthands that fit into the ncurses namespace. */
48 #define KEY_TAB 9
49 #define KEY_ESC 27
50 #define KEY_DEL 127
51
52 /* View requests */
53 enum request {
54 /* REQ_* values from form.h is used as a basis for user actions.
55 * Offset new values below relative to MAX_COMMAND from form.h. */
56 REQ_OFFSET = MAX_COMMAND,
57
58 /* Requests for switching between the different views. */
59 REQ_DIFF,
60 REQ_LOG,
61 REQ_MAIN,
62 REQ_VIEWS,
63
64 REQ_QUIT,
65 REQ_VERSION,
66 REQ_STOP,
67 REQ_UPDATE,
68 REQ_REDRAW,
69 REQ_FIRST_LINE,
70 REQ_LAST_LINE,
71 REQ_LINE_NUMBER,
72 };
73
74 /* The request are used for adressing the view array. */
75 #define VIEW_OFFSET(r) ((r) - REQ_OFFSET - 1)
76
77 #define SIZEOF_ID 1024
78
79 #define COLOR_TRANSP (-1)
80
81 #define DATE_FORMAT "%Y-%m-%d %H:%M"
82 #define DATE_COLS (STRING_SIZE("2006-04-29 14:21") + 1)
83
84 #define ABS(x) ((x) >= 0 ? (x) : -(x))
85 #define MIN(x, y) ((x) < (y) ? (x) : (y))
86
87 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
88 #define STRING_SIZE(x) (sizeof(x) - 1)
89
90 struct commit {
91 char id[41];
92 char title[75];
93 char author[75];
94 struct tm time;
95 };
96
97
98 /**
99 * OPTIONS
100 * -------
101 **/
102
103 static int opt_line_number;
104 static int opt_request = REQ_MAIN;
105
106 char head_id[SIZEOF_ID] = "HEAD";
107 char commit_id[SIZEOF_ID] = "HEAD";
108
109 /* Returns the index of log or diff command or -1 to exit. */
110 static int
111 parse_options(int argc, char *argv[])
112 {
113 int i;
114
115 for (i = 1; i < argc; i++) {
116 char *opt = argv[i];
117
118 /**
119 * log [options]::
120 * git log options.
121 **/
122 if (!strcmp(opt, "log")) {
123 opt_request = REQ_LOG;
124 return i;
125
126 /**
127 * diff [options]::
128 * git diff options.
129 **/
130 } else if (!strcmp(opt, "diff")) {
131 opt_request = REQ_DIFF;
132 return i;
133
134 /**
135 * -l::
136 * Start up in log view.
137 **/
138 } else if (!strcmp(opt, "-l")) {
139 opt_request = REQ_LOG;
140
141 /**
142 * -d::
143 * Start up in diff view.
144 **/
145 } else if (!strcmp(opt, "-d")) {
146 opt_request = REQ_DIFF;
147
148 /**
149 * -n, --line-number::
150 * Prefix line numbers in log and diff view.
151 **/
152 } else if (!strcmp(opt, "-n") ||
153 !strcmp(opt, "--line-number")) {
154 opt_line_number = 1;
155
156 /**
157 * -v, --version::
158 * Show version and exit.
159 **/
160 } else if (!strcmp(opt, "-v") ||
161 !strcmp(opt, "--version")) {
162 printf("tig version %s\n", VERSION);
163 return -1;
164
165 /**
166 * ref::
167 * Commit reference, symbolic or raw SHA1 ID.
168 **/
169 } else if (opt[0] && opt[0] != '-') {
170 strncpy(head_id, opt, SIZEOF_ID);
171 strncpy(commit_id, opt, SIZEOF_ID);
172
173 } else {
174 die("unknown command '%s'", opt);
175 }
176 }
177
178 return i;
179 }
180
181
182 /*
183 * Line-oriented content detection.
184 */
185
186 enum line_type {
187 LINE_DEFAULT,
188 LINE_AUTHOR,
189 LINE_AUTHOR_IDENT,
190 LINE_COMMIT,
191 LINE_COMMITTER,
192 LINE_CURSOR,
193 LINE_DATE,
194 LINE_DIFF,
195 LINE_DIFF_ADD,
196 LINE_DIFF_CHUNK,
197 LINE_DIFF_COPY,
198 LINE_DIFF_DEL,
199 LINE_DIFF_DISSIM,
200 LINE_DIFF_NEWMODE,
201 LINE_DIFF_OLDMODE,
202 LINE_DIFF_RENAME,
203 LINE_DIFF_SIM,
204 LINE_DIFF_TREE,
205 LINE_INDEX,
206 LINE_MAIN_AUTHOR,
207 LINE_MAIN_COMMIT,
208 LINE_MAIN_DATE,
209 LINE_MAIN_DELIM,
210 LINE_MERGE,
211 LINE_PARENT,
212 LINE_SIGNOFF,
213 LINE_STATUS,
214 LINE_TITLE,
215 LINE_TREE,
216 };
217
218 struct line_info {
219 enum line_type type;
220 char *line;
221 int linelen;
222
223 int fg;
224 int bg;
225 int attr;
226 };
227
228 #define LINE(type, line, fg, bg, attr) \
229 { LINE_##type, (line), STRING_SIZE(line), (fg), (bg), (attr) }
230
231 static struct line_info line_info[] = {
232 /* Diff markup */
233 LINE(DIFF, "diff --git ", COLOR_YELLOW, COLOR_TRANSP, 0),
234 LINE(INDEX, "index ", COLOR_BLUE, COLOR_TRANSP, 0),
235 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_TRANSP, 0),
236 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_TRANSP, 0),
237 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_TRANSP, 0),
238 LINE(DIFF_OLDMODE, "old mode ", COLOR_YELLOW, COLOR_TRANSP, 0),
239 LINE(DIFF_NEWMODE, "new mode ", COLOR_YELLOW, COLOR_TRANSP, 0),
240 LINE(DIFF_COPY, "copy ", COLOR_YELLOW, COLOR_TRANSP, 0),
241 LINE(DIFF_RENAME, "rename ", COLOR_YELLOW, COLOR_TRANSP, 0),
242 LINE(DIFF_SIM, "similarity ", COLOR_YELLOW, COLOR_TRANSP, 0),
243 LINE(DIFF_DISSIM, "dissimilarity ", COLOR_YELLOW, COLOR_TRANSP, 0),
244
245 /* Pretty print commit header */
246 LINE(AUTHOR, "Author: ", COLOR_CYAN, COLOR_TRANSP, 0),
247 LINE(MERGE, "Merge: ", COLOR_BLUE, COLOR_TRANSP, 0),
248 LINE(DATE, "Date: ", COLOR_YELLOW, COLOR_TRANSP, 0),
249
250 /* Raw commit header */
251 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_TRANSP, 0),
252 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_TRANSP, 0),
253 LINE(TREE, "tree ", COLOR_BLUE, COLOR_TRANSP, 0),
254 LINE(AUTHOR_IDENT, "author ", COLOR_CYAN, COLOR_TRANSP, 0),
255 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_TRANSP, 0),
256
257 /* Misc */
258 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_TRANSP, 0),
259 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_TRANSP, 0),
260
261 /* UI colors */
262 LINE(DEFAULT, "", COLOR_TRANSP, COLOR_TRANSP, A_NORMAL),
263 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD),
264 LINE(STATUS, "", COLOR_GREEN, COLOR_TRANSP, 0),
265 LINE(TITLE, "", COLOR_YELLOW, COLOR_BLUE, A_BOLD),
266 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_TRANSP, 0),
267 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_TRANSP, 0),
268 LINE(MAIN_COMMIT, "", COLOR_TRANSP, COLOR_TRANSP, 0),
269 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_TRANSP, 0),
270 };
271
272 static struct line_info *
273 get_line_info(char *line)
274 {
275 int linelen = strlen(line);
276 int i;
277
278 for (i = 0; i < ARRAY_SIZE(line_info); i++) {
279 if (linelen < line_info[i].linelen
280 || strncasecmp(line_info[i].line, line, line_info[i].linelen))
281 continue;
282
283 return &line_info[i];
284 }
285
286 return NULL;
287 }
288
289 static enum line_type
290 get_line_type(char *line)
291 {
292 struct line_info *info = get_line_info(line);
293
294 return info ? info->type : LINE_DEFAULT;
295 }
296
297 static int
298 get_line_attr(enum line_type type)
299 {
300 int i;
301
302 for (i = 0; i < ARRAY_SIZE(line_info); i++)
303 if (line_info[i].type == type)
304 return COLOR_PAIR(line_info[i].type) | line_info[i].attr;
305
306 return A_NORMAL;
307 }
308
309 static void
310 init_colors(void)
311 {
312 int transparent_bg = COLOR_BLACK;
313 int transparent_fg = COLOR_WHITE;
314 int i;
315
316 start_color();
317
318 if (use_default_colors() != ERR) {
319 transparent_bg = -1;
320 transparent_fg = -1;
321 }
322
323 for (i = 0; i < ARRAY_SIZE(line_info); i++) {
324 struct line_info *info = &line_info[i];
325 int bg = info->bg == COLOR_TRANSP ? transparent_bg : info->bg;
326 int fg = info->fg == COLOR_TRANSP ? transparent_fg : info->fg;
327
328 init_pair(info->type, fg, bg);
329 }
330 }
331
332
333 /**
334 * KEYS
335 * ----
336 *
337 * d::
338 * diff
339 * l::
340 * log
341 * q::
342 * quit
343 * r::
344 * redraw screen
345 * s::
346 * stop all background loading
347 * j::
348 * down
349 * k::
350 * up
351 * h, ?::
352 * help
353 * v::
354 * version
355 **/
356
357 #define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp"
358
359 struct keymap {
360 int alias;
361 int request;
362 };
363
364 struct keymap keymap[] = {
365 /* Cursor navigation */
366 { KEY_UP, REQ_PREV_LINE },
367 { 'k', REQ_PREV_LINE },
368 { KEY_DOWN, REQ_NEXT_LINE },
369 { 'j', REQ_NEXT_LINE },
370 { KEY_HOME, REQ_FIRST_LINE },
371 { KEY_END, REQ_LAST_LINE },
372 { KEY_NPAGE, REQ_NEXT_PAGE },
373 { KEY_PPAGE, REQ_PREV_PAGE },
374
375 /* Scrolling */
376 { KEY_IC, REQ_SCR_BLINE }, /* scroll field backward a line */
377 { KEY_DC, REQ_SCR_FLINE }, /* scroll field forward a line */
378 { 's', REQ_SCR_FPAGE }, /* scroll field forward a page */
379 { 'w', REQ_SCR_BPAGE }, /* scroll field backward a page */
380
381 { 'd', REQ_DIFF },
382 { 'l', REQ_LOG },
383 { 'm', REQ_MAIN },
384
385 { 'n', REQ_LINE_NUMBER },
386
387 /* No input from wgetch() with nodelay() enabled. */
388 { ERR, REQ_UPDATE },
389
390 { KEY_ESC, REQ_QUIT },
391 { 'q', REQ_QUIT },
392 { 's', REQ_STOP },
393 { 'v', REQ_VERSION },
394 { 'r', REQ_REDRAW },
395 };
396
397 static int
398 get_request(int request)
399 {
400 int i;
401
402 for (i = 0; i < ARRAY_SIZE(keymap); i++)
403 if (keymap[i].alias == request)
404 return keymap[i].request;
405
406 return request;
407 }
408
409
410 /*
411 * Viewer
412 */
413
414 struct view {
415 char *name;
416 char *cmd;
417 char *id;
418
419
420 /* Rendering */
421 int (*read)(struct view *, char *);
422 int (*draw)(struct view *, unsigned int);
423 size_t objsize; /* Size of objects in the line index */
424
425 WINDOW *win;
426 int height, width;
427
428 /* Navigation */
429 unsigned long offset; /* Offset of the window top */
430 unsigned long lineno; /* Current line number */
431
432 /* Buffering */
433 unsigned long lines; /* Total number of lines */
434 void **line; /* Line index */
435
436 /* Loading */
437 FILE *pipe;
438 };
439
440 static int pager_draw(struct view *view, unsigned int lineno);
441 static int pager_read(struct view *view, char *line);
442
443 static int main_draw(struct view *view, unsigned int lineno);
444 static int main_read(struct view *view, char *line);
445
446 #define DIFF_CMD \
447 "git log --stat -n1 %s ; echo; " \
448 "git diff --find-copies-harder -B -C %s^ %s"
449
450 #define LOG_CMD \
451 "git log --stat -n100 %s"
452
453 #define MAIN_CMD \
454 "git log --stat --pretty=raw %s"
455
456 /* The status window is used for polling keystrokes. */
457 static WINDOW *status_win;
458 static WINDOW *title_win;
459
460 /* The number of loading views. Controls when nodelay should be in effect when
461 * polling user input. */
462 static unsigned int nloading;
463
464 static struct view views[] = {
465 { "diff", DIFF_CMD, commit_id, pager_read, pager_draw, sizeof(char) },
466 { "log", LOG_CMD, head_id, pager_read, pager_draw, sizeof(char) },
467 { "main", MAIN_CMD, head_id, main_read, main_draw, sizeof(struct commit) },
468 };
469
470 /* The display array of active views and the index of the current view. */
471 static struct view *display[ARRAY_SIZE(views)];
472 static unsigned int current_view;
473
474 #define foreach_view(view, i) \
475 for (i = 0; i < sizeof(display) && (view = display[i]); i++)
476
477
478 static void
479 redraw_view(struct view *view)
480 {
481 int lineno;
482
483 wclear(view->win);
484 wmove(view->win, 0, 0);
485
486 for (lineno = 0; lineno < view->height; lineno++) {
487 if (!view->draw(view, lineno))
488 break;
489 }
490
491 redrawwin(view->win);
492 wrefresh(view->win);
493 }
494
495 static void
496 resize_view(struct view *view)
497 {
498 int lines, cols;
499
500 getmaxyx(stdscr, lines, cols);
501
502 if (view->win) {
503 mvwin(view->win, 0, 0);
504 wresize(view->win, lines - 2, cols);
505
506 } else {
507 view->win = newwin(lines - 2, 0, 0, 0);
508 if (!view->win) {
509 report("failed to create %s view", view->name);
510 return;
511 }
512 scrollok(view->win, TRUE);
513 }
514
515 getmaxyx(view->win, view->height, view->width);
516 }
517
518 /* FIXME: Fix percentage. */
519 static void
520 report_position(struct view *view, int all)
521 {
522 report(all ? "line %d of %d (%d%%)"
523 : "line %d of %d",
524 view->lineno + 1,
525 view->lines,
526 view->lines ? (view->lineno + 1) * 100 / view->lines : 0);
527 }
528
529
530 static void
531 move_view(struct view *view, int lines)
532 {
533 /* The rendering expects the new offset. */
534 view->offset += lines;
535
536 assert(0 <= view->offset && view->offset < view->lines);
537 assert(lines);
538
539 if (view->height < ABS(lines)) {
540 redraw_view(view);
541
542 } else {
543 int line = lines > 0 ? view->height - lines : 0;
544 int end = line + (lines > 0 ? lines : -lines);
545
546 wscrl(view->win, lines);
547
548 for (; line < end; line++) {
549 if (!view->draw(view, line))
550 break;
551 }
552 }
553
554 /* Move current line into the view. */
555 if (view->lineno < view->offset) {
556 view->lineno = view->offset;
557 view->draw(view, 0);
558
559 } else if (view->lineno >= view->offset + view->height) {
560 view->lineno = view->offset + view->height - 1;
561 view->draw(view, view->lineno - view->offset);
562 }
563
564 assert(view->offset <= view->lineno && view->lineno < view->lines);
565
566 redrawwin(view->win);
567 wrefresh(view->win);
568
569 report_position(view, lines);
570 }
571
572 static void
573 scroll_view(struct view *view, int request)
574 {
575 int lines = 1;
576
577 switch (request) {
578 case REQ_SCR_FPAGE:
579 lines = view->height;
580 case REQ_SCR_FLINE:
581 if (view->offset + lines > view->lines)
582 lines = view->lines - view->offset;
583
584 if (lines == 0 || view->offset + view->height >= view->lines) {
585 report("already at last line");
586 return;
587 }
588 break;
589
590 case REQ_SCR_BPAGE:
591 lines = view->height;
592 case REQ_SCR_BLINE:
593 if (lines > view->offset)
594 lines = view->offset;
595
596 if (lines == 0) {
597 report("already at first line");
598 return;
599 }
600
601 lines = -lines;
602 break;
603 }
604
605 move_view(view, lines);
606 }
607
608 static void
609 navigate_view(struct view *view, int request)
610 {
611 int steps;
612
613 switch (request) {
614 case REQ_FIRST_LINE:
615 steps = -view->lineno;
616 break;
617
618 case REQ_LAST_LINE:
619 steps = view->lines - view->lineno - 1;
620 break;
621
622 case REQ_PREV_PAGE:
623 steps = view->height > view->lineno
624 ? -view->lineno : -view->height;
625 break;
626
627 case REQ_NEXT_PAGE:
628 steps = view->lineno + view->height >= view->lines
629 ? view->lines - view->lineno - 1 : view->height;
630 break;
631
632 case REQ_PREV_LINE:
633 steps = -1;
634 break;
635
636 case REQ_NEXT_LINE:
637 steps = 1;
638 break;
639 }
640
641 if (steps <= 0 && view->lineno == 0) {
642 report("already at first line");
643 return;
644
645 } else if (steps >= 0 && view->lineno + 1 == view->lines) {
646 report("already at last line");
647 return;
648 }
649
650 /* Move the current line */
651 view->lineno += steps;
652 assert(0 <= view->lineno && view->lineno < view->lines);
653
654 /* Repaint the old "current" line if we be scrolling */
655 if (ABS(steps) < view->height)
656 view->draw(view, view->lineno - steps - view->offset);
657
658 /* Check whether the view needs to be scrolled */
659 if (view->lineno < view->offset ||
660 view->lineno >= view->offset + view->height) {
661 if (steps < 0 && -steps > view->offset) {
662 steps = -view->offset;
663
664 } else if (steps > 0) {
665 if (view->lineno == view->lines - 1 &&
666 view->lines > view->height) {
667 steps = view->lines - view->offset - 1;
668 if (steps >= view->height)
669 steps -= view->height - 1;
670 }
671 }
672
673 move_view(view, steps);
674 return;
675 }
676
677 /* Draw the current line */
678 view->draw(view, view->lineno - view->offset);
679
680 redrawwin(view->win);
681 wrefresh(view->win);
682
683 report_position(view, view->height);
684 }
685
686
687
688 static bool
689 begin_update(struct view *view)
690 {
691 char buf[1024];
692
693 if (view->cmd) {
694 char *id = view->id;
695
696 if (snprintf(buf, sizeof(buf), view->cmd, id, id, id) < sizeof(buf))
697 view->pipe = popen(buf, "r");
698
699 if (!view->pipe)
700 return FALSE;
701
702 if (nloading++ == 0)
703 nodelay(status_win, TRUE);
704 }
705
706 display[current_view] = view;
707
708 view->offset = 0;
709 view->lines = 0;
710 view->lineno = 0;
711
712 return TRUE;
713 }
714
715 static void
716 end_update(struct view *view)
717 {
718 wattrset(view->win, A_NORMAL);
719 pclose(view->pipe);
720 view->pipe = NULL;
721
722 if (nloading-- == 1)
723 nodelay(status_win, FALSE);
724 }
725
726 static int
727 update_view(struct view *view)
728 {
729 char buffer[BUFSIZ];
730 char *line;
731 void **tmp;
732 int redraw;
733 int lines = view->height;
734
735 if (!view->pipe)
736 return TRUE;
737
738 /* Only redraw after the first reading session. */
739 /* FIXME: ... and possibly more. */
740 redraw = view->height > view->lines;
741
742 tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines));
743 if (!tmp)
744 goto alloc_error;
745
746 view->line = tmp;
747
748 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
749 int linelen;
750
751 linelen = strlen(line);
752 if (linelen)
753 line[linelen - 1] = 0;
754
755 if (!view->read(view, line))
756 goto alloc_error;
757
758 if (lines-- == 1)
759 break;
760 }
761
762 if (redraw) {
763 /* FIXME: This causes flickering. Draw incrementally. */
764 redraw_view(view);
765 }
766
767 if (ferror(view->pipe)) {
768 report("failed to read %s: %s", view->cmd, strerror(errno));
769 goto end;
770
771 } else if (feof(view->pipe)) {
772 report_position(view, 0);
773 goto end;
774 }
775
776 return TRUE;
777
778 alloc_error:
779 report("allocation failure");
780
781 end:
782 end_update(view);
783 return FALSE;
784 }
785
786
787 static struct view *
788 switch_view(struct view *prev, int request)
789 {
790 struct view *view = &views[VIEW_OFFSET(request)];
791 struct view *displayed;
792 int i;
793
794 if (view == prev) {
795 foreach_view (displayed, i) ;
796
797 if (i == 1)
798 report("already in %s view", view->name);
799 else
800 report("FIXME: Maximize");
801
802 return view;
803
804 } else {
805 foreach_view (displayed, i) {
806 if (view == displayed) {
807 current_view = i;
808 report("new current view");
809 return view;
810 }
811 }
812 }
813
814 if (!view->win)
815 resize_view(view);
816
817 /* Reload */
818
819 if (view->line) {
820 for (i = 0; i < view->lines; i++)
821 if (view->line[i])
822 free(view->line[i]);
823
824 free(view->line);
825 view->line = NULL;
826 }
827
828 if (prev && prev->pipe)
829 end_update(prev);
830
831 if (begin_update(view)) {
832 if (!view->cmd)
833 report("%s", HELP);
834 else
835 report("loading...");
836 }
837
838 return view;
839 }
840
841
842 /* Process a keystroke */
843 static int
844 view_driver(struct view *view, int key)
845 {
846 int request = get_request(key);
847 int i;
848
849 switch (request) {
850 case REQ_NEXT_LINE:
851 case REQ_PREV_LINE:
852 case REQ_FIRST_LINE:
853 case REQ_LAST_LINE:
854 case REQ_NEXT_PAGE:
855 case REQ_PREV_PAGE:
856 if (view)
857 navigate_view(view, request);
858 break;
859
860 case REQ_SCR_FLINE:
861 case REQ_SCR_BLINE:
862 case REQ_SCR_FPAGE:
863 case REQ_SCR_BPAGE:
864 if (view)
865 scroll_view(view, request);
866 break;
867
868 case REQ_MAIN:
869 case REQ_LOG:
870 case REQ_DIFF:
871 view = switch_view(view, request);
872 break;
873
874 case REQ_LINE_NUMBER:
875 opt_line_number = !opt_line_number;
876 redraw_view(view);
877 break;
878
879 case REQ_REDRAW:
880 redraw_view(view);
881 break;
882
883 case REQ_STOP:
884 foreach_view (view, i) {
885 if (view->pipe) {
886 end_update(view);
887 scroll_view(view, 0);
888 }
889 }
890 break;
891
892 case REQ_VERSION:
893 report("version %s", VERSION);
894 return TRUE;
895
896 case REQ_UPDATE:
897 doupdate();
898 return TRUE;
899
900 case REQ_QUIT:
901 return FALSE;
902
903 default:
904 report(HELP);
905 return TRUE;
906 }
907
908 return TRUE;
909 }
910
911
912 /*
913 * Rendering
914 */
915
916 static int
917 pager_draw(struct view *view, unsigned int lineno)
918 {
919 enum line_type type;
920 char *line;
921 int linelen;
922 int attr;
923
924 if (view->offset + lineno >= view->lines)
925 return FALSE;
926
927 line = view->line[view->offset + lineno];
928 type = get_line_type(line);
929
930 if (view->offset + lineno == view->lineno) {
931 if (type == LINE_COMMIT)
932 strncpy(commit_id, line + 7, SIZEOF_ID);
933 type = LINE_CURSOR;
934 }
935
936 attr = get_line_attr(type);
937 wattrset(view->win, attr);
938
939 linelen = strlen(line);
940 linelen = MIN(linelen, view->width);
941
942 if (opt_line_number) {
943 wmove(view->win, lineno, 0);
944 lineno += view->offset + 1;
945 if (lineno == 1 || (lineno % 10) == 0)
946 wprintw(view->win, "%4d: ", lineno);
947 else
948 wprintw(view->win, " : ", lineno);
949
950 while (line) {
951 if (*line == '\t') {
952 waddstr(view->win, " ");
953 line++;
954 } else {
955 char *tab = strchr(line, '\t');
956
957 if (tab)
958 waddnstr(view->win, line, tab - line);
959 else
960 waddstr(view->win, line);
961 line = tab;
962 }
963 }
964 waddstr(view->win, line);
965
966 } else {
967 /* No empty lines makes cursor drawing and clearing implicit. */
968 if (!*line)
969 line = " ", linelen = 1;
970 mvwaddnstr(view->win, lineno, 0, line, linelen);
971 }
972
973 return TRUE;
974 }
975
976 static int
977 pager_read(struct view *view, char *line)
978 {
979 view->line[view->lines] = strdup(line);
980 if (!view->line[view->lines])
981 return FALSE;
982
983 view->lines++;
984 return TRUE;
985 }
986
987 static int
988 main_draw(struct view *view, unsigned int lineno)
989 {
990 char buf[21];
991 struct commit *commit;
992 enum line_type type;
993 int cols = 0;
994 size_t timelen;
995
996 if (view->offset + lineno >= view->lines)
997 return FALSE;
998
999 commit = view->line[view->offset + lineno];
1000 if (!*commit->author)
1001 return FALSE;
1002
1003 if (view->offset + lineno == view->lineno) {
1004 strncpy(commit_id, commit->id, SIZEOF_ID);
1005 type = LINE_CURSOR;
1006 } else {
1007 type = LINE_MAIN_COMMIT;
1008 }
1009
1010 wmove(view->win, lineno, cols);
1011 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1012
1013 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1014 waddnstr(view->win, buf, timelen);
1015 waddstr(view->win, " ");
1016
1017 cols += DATE_COLS;
1018 wmove(view->win, lineno, cols);
1019 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1020
1021 if (strlen(commit->author) > 19) {
1022 waddnstr(view->win, commit->author, 18);
1023 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1024 waddch(view->win, '~');
1025 } else {
1026 waddstr(view->win, commit->author);
1027 }
1028
1029 cols += 20;
1030 wattrset(view->win, A_NORMAL);
1031 mvwaddch(view->win, lineno, cols, ACS_LTEE);
1032 wattrset(view->win, get_line_attr(type));
1033 mvwaddstr(view->win, lineno, cols + 2, commit->title);
1034 wattrset(view->win, A_NORMAL);
1035
1036 return TRUE;
1037 }
1038
1039 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1040 static int
1041 main_read(struct view *view, char *line)
1042 {
1043 enum line_type type = get_line_type(line);
1044 struct commit *commit;
1045
1046 switch (type) {
1047 case LINE_COMMIT:
1048 commit = calloc(1, sizeof(struct commit));
1049 if (!commit)
1050 return FALSE;
1051
1052 line += STRING_SIZE("commit ");
1053
1054 view->line[view->lines++] = commit;
1055 strncpy(commit->id, line, sizeof(commit->id));
1056 break;
1057
1058 case LINE_AUTHOR_IDENT:
1059 {
1060 char *ident = line + STRING_SIZE("author ");
1061 char *end = strchr(ident, '<');
1062
1063 if (end) {
1064 for (; end > ident && isspace(end[-1]); end--) ;
1065 *end = 0;
1066 }
1067
1068 commit = view->line[view->lines - 1];
1069 strncpy(commit->author, ident, sizeof(commit->author));
1070
1071 /* Parse epoch and timezone */
1072 if (end) {
1073 char *secs = strchr(end + 1, '>');
1074 char *zone;
1075 time_t time;
1076
1077 if (!secs || secs[1] != ' ')
1078 break;
1079
1080 secs += 2;
1081 time = (time_t) atol(secs);
1082 zone = strchr(secs, ' ');
1083 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1084 long tz;
1085
1086 zone++;
1087 tz = ('0' - zone[1]) * 60 * 60 * 10;
1088 tz += ('0' - zone[2]) * 60 * 60;
1089 tz += ('0' - zone[3]) * 60;
1090 tz += ('0' - zone[4]) * 60;
1091
1092 if (zone[0] == '-')
1093 tz = -tz;
1094
1095 time -= tz;
1096 }
1097 gmtime_r(&time, &commit->time);
1098 }
1099 break;
1100 }
1101 default:
1102 /* Fill in the commit title */
1103 commit = view->line[view->lines - 1];
1104 if (!commit->title[0] &&
1105 !strncmp(line, " ", 4) &&
1106 !isspace(line[5]))
1107 strncpy(commit->title, line + 4, sizeof(commit->title));
1108 }
1109
1110 return TRUE;
1111 }
1112
1113
1114 /*
1115 * Main
1116 */
1117
1118 static void
1119 quit(int sig)
1120 {
1121 if (status_win)
1122 delwin(status_win);
1123 if (title_win)
1124 delwin(title_win);
1125 endwin();
1126
1127 /* FIXME: Shutdown gracefully. */
1128
1129 exit(0);
1130 }
1131
1132 static void die(const char *err, ...)
1133 {
1134 va_list args;
1135
1136 endwin();
1137
1138 va_start(args, err);
1139 fputs("tig: ", stderr);
1140 vfprintf(stderr, err, args);
1141 fputs("\n", stderr);
1142 va_end(args);
1143
1144 exit(1);
1145 }
1146
1147 static void
1148 report(const char *msg, ...)
1149 {
1150 va_list args;
1151
1152 werase(title_win);
1153 wmove(title_win, 0, 0);
1154 wprintw(title_win, "commit %s", commit_id);
1155 wrefresh(title_win);
1156
1157 va_start(args, msg);
1158
1159 werase(status_win);
1160 wmove(status_win, 0, 0);
1161
1162 #if 0
1163 if (display[current_view])
1164 wprintw(status_win, "%s %4s: ", commit_id, display[current_view]->name);
1165 #endif
1166 vwprintw(status_win, msg, args);
1167 wrefresh(status_win);
1168
1169 va_end(args);
1170 }
1171
1172 int
1173 main(int argc, char *argv[])
1174 {
1175 int x, y;
1176 int request;
1177 int git_cmd;
1178
1179 signal(SIGINT, quit);
1180
1181 git_cmd = parse_options(argc, argv);
1182 if (git_cmd < 0)
1183 return 0;
1184
1185 request = opt_request;
1186
1187 initscr(); /* initialize the curses library */
1188 nonl(); /* tell curses not to do NL->CR/NL on output */
1189 cbreak(); /* take input chars one at a time, no wait for \n */
1190 noecho(); /* don't echo input */
1191 leaveok(stdscr, TRUE);
1192 /* curs_set(0); */
1193
1194 if (has_colors())
1195 init_colors();
1196
1197 getmaxyx(stdscr, y, x);
1198 status_win = newwin(1, 0, y - 1, 0);
1199 if (!status_win)
1200 die("Failed to create status window");
1201
1202 title_win = newwin(1, 0, y - 2, 0);
1203 if (!title_win)
1204 die("Failed to create title window");
1205
1206 /* Enable keyboard mapping */
1207 keypad(status_win, TRUE);
1208 wbkgdset(status_win, get_line_attr(LINE_STATUS));
1209 wbkgdset(title_win, get_line_attr(LINE_TITLE));
1210
1211 while (view_driver(display[current_view], request)) {
1212 struct view *view;
1213 int i;
1214
1215 foreach_view (view, i) {
1216 if (view->pipe) {
1217 update_view(view);
1218 }
1219 }
1220
1221 /* Refresh, accept single keystroke of input */
1222 request = wgetch(status_win);
1223 if (request == KEY_RESIZE) {
1224 int lines, cols;
1225
1226 getmaxyx(stdscr, lines, cols);
1227
1228 mvwin(status_win, lines - 1, 0);
1229 wresize(status_win, 1, cols - 1);
1230
1231 mvwin(title_win, lines - 2, 0);
1232 wresize(title_win, 1, cols - 1);
1233 }
1234 }
1235
1236 quit(0);
1237
1238 return 0;
1239 }
1240
1241 /**
1242 * BUGS
1243 * ----
1244 * Known bugs and problems:
1245 *
1246 * Redrawing of the main view while loading::
1247 * If only part of a commit has been parsed not all fields will be visible
1248 * or even redrawn when the whole commit have loaded. This can be
1249 * triggered when continuously moving to the last line. Use 'r' to redraw
1250 * the whole screen.
1251 *
1252 * TODO
1253 * ----
1254 * Features that should be explored.
1255 *
1256 * - Proper command line handling; ability to take the command that should be
1257 * shown. Example:
1258 *
1259 * $ tig log -p
1260 *
1261 * - Internal command line (exmode-inspired) which allows to specify what git
1262 * log or git diff command to run. Example:
1263 *
1264 * :log -p
1265 *
1266 * - Proper resizing support. I am yet to figure out whether catching SIGWINCH
1267 * is preferred over using ncurses' built-in support for resizing.
1268 *
1269 * - Locale support.
1270 *
1271 * COPYRIGHT
1272 * ---------
1273 * Copyright (c) Jonas Fonseca, 2006
1274 *
1275 * This program is free software; you can redistribute it and/or modify
1276 * it under the terms of the GNU General Public License as published by
1277 * the Free Software Foundation; either version 2 of the License, or
1278 * (at your option) any later version.
1279 *
1280 * SEE ALSO
1281 * --------
1282 * [verse]
1283 * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
1284 * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
1285 * gitk(1): git repository browser written using tcl/tk,
1286 * gitview(1): git repository browser written using python/gtk.
1287 **/