TODO's and TODON'T's ...
[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]
b76c2afc
JF
12 * tig [options]
13 * tig [options] log [git log options]
14 * tig [options] diff [git diff options]
4c6fabc2 15 * tig [options] < [git log or git diff output]
b801d8b2
JF
16 *
17 * DESCRIPTION
18 * -----------
19 * Browse changes in a git repository.
b801d8b2
JF
20 **/
21
22#define DEBUG
b801d8b2
JF
23#ifndef DEBUG
24#define NDEBUG
25#endif
26
b76c2afc
JF
27#ifndef VERSION
28#define VERSION "tig-0.1"
29#endif
30
22f66b0a 31#include <assert.h>
4c6fabc2 32#include <errno.h>
22f66b0a
JF
33#include <ctype.h>
34#include <signal.h>
b801d8b2 35#include <stdarg.h>
b801d8b2 36#include <stdio.h>
22f66b0a 37#include <stdlib.h>
b801d8b2 38#include <string.h>
b76c2afc 39#include <time.h>
b801d8b2
JF
40
41#include <curses.h>
42#include <form.h>
43
44static void die(const char *err, ...);
45static void report(const char *msg, ...);
46
4c6fabc2 47/* Some ascii-shorthands that fit into the ncurses namespace. */
b801d8b2 48#define KEY_TAB 9
fd85fef1
JF
49#define KEY_ESC 27
50#define KEY_DEL 127
b801d8b2 51
4c6fabc2
JF
52/* View requests */
53enum 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};
b801d8b2 73
4c6fabc2
JF
74/* The request are used for adressing the view array. */
75#define VIEW_OFFSET(r) ((r) - REQ_OFFSET - 1)
b76c2afc 76
b76c2afc 77#define SIZEOF_ID 1024
b801d8b2 78
78c70acd
JF
79#define COLOR_TRANSP (-1)
80
4c6fabc2
JF
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
90struct commit {
91 char id[41];
92 char title[75];
93 char author[75];
94 struct tm time;
95};
96
78c70acd 97
b76c2afc
JF
98/**
99 * OPTIONS
100 * -------
101 **/
102
103static int opt_line_number;
104static int opt_request = REQ_MAIN;
105
106char head_id[SIZEOF_ID] = "HEAD";
107char commit_id[SIZEOF_ID] = "HEAD";
108
b76c2afc
JF
109/* Returns the index of log or diff command or -1 to exit. */
110static int
111parse_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 {
4c6fabc2 174 die("unknown command '%s'", opt);
b76c2afc
JF
175 }
176 }
177
178 return i;
179}
180
181
182/*
183 * Line-oriented content detection.
184 */
185
78c70acd 186enum line_type {
b76c2afc 187 LINE_DEFAULT,
78c70acd 188 LINE_AUTHOR,
b76c2afc 189 LINE_AUTHOR_IDENT,
78c70acd 190 LINE_COMMIT,
b76c2afc
JF
191 LINE_COMMITTER,
192 LINE_CURSOR,
78c70acd
JF
193 LINE_DATE,
194 LINE_DIFF,
195 LINE_DIFF_ADD,
78c70acd 196 LINE_DIFF_CHUNK,
b76c2afc
JF
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,
78c70acd
JF
204 LINE_DIFF_TREE,
205 LINE_INDEX,
b76c2afc
JF
206 LINE_MAIN_AUTHOR,
207 LINE_MAIN_COMMIT,
208 LINE_MAIN_DATE,
209 LINE_MAIN_DELIM,
210 LINE_MERGE,
78c70acd 211 LINE_PARENT,
4c6fabc2 212 LINE_SIGNOFF,
78c70acd
JF
213 LINE_STATUS,
214 LINE_TITLE,
b76c2afc 215 LINE_TREE,
78c70acd
JF
216};
217
218struct 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) \
4c6fabc2 229 { LINE_##type, (line), STRING_SIZE(line), (fg), (bg), (attr) }
78c70acd
JF
230
231static struct line_info line_info[] = {
b76c2afc
JF
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
4c6fabc2 257 /* Misc */
b76c2afc 258 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_TRANSP, 0),
4c6fabc2 259 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_TRANSP, 0),
b76c2afc
JF
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),
78c70acd
JF
270};
271
272static struct line_info *
273get_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
4c6fabc2 280 || strncasecmp(line_info[i].line, line, line_info[i].linelen))
78c70acd
JF
281 continue;
282
283 return &line_info[i];
284 }
285
286 return NULL;
287}
288
289static enum line_type
290get_line_type(char *line)
291{
292 struct line_info *info = get_line_info(line);
293
b76c2afc 294 return info ? info->type : LINE_DEFAULT;
78c70acd
JF
295}
296
297static int
298get_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
309static void
310init_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
b801d8b2
JF
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
b801d8b2
JF
355 **/
356
357#define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp"
358
359struct keymap {
360 int alias;
361 int request;
362};
363
364struct keymap keymap[] = {
fd85fef1 365 /* Cursor navigation */
b801d8b2
JF
366 { KEY_UP, REQ_PREV_LINE },
367 { 'k', REQ_PREV_LINE },
368 { KEY_DOWN, REQ_NEXT_LINE },
369 { 'j', REQ_NEXT_LINE },
fd85fef1
JF
370 { KEY_HOME, REQ_FIRST_LINE },
371 { KEY_END, REQ_LAST_LINE },
78c70acd
JF
372 { KEY_NPAGE, REQ_NEXT_PAGE },
373 { KEY_PPAGE, REQ_PREV_PAGE },
fd85fef1
JF
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 */
78c70acd
JF
378 { 's', REQ_SCR_FPAGE }, /* scroll field forward a page */
379 { 'w', REQ_SCR_BPAGE }, /* scroll field backward a page */
b801d8b2
JF
380
381 { 'd', REQ_DIFF },
382 { 'l', REQ_LOG },
383 { 'm', REQ_MAIN },
384
b76c2afc
JF
385 { 'n', REQ_LINE_NUMBER },
386
b801d8b2
JF
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
397static int
398get_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
414struct view {
415 char *name;
416 char *cmd;
fd85fef1 417 char *id;
b801d8b2 418
22f66b0a 419
b801d8b2 420 /* Rendering */
22f66b0a
JF
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
b801d8b2 425 WINDOW *win;
fd85fef1 426 int height, width;
b801d8b2
JF
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 */
22f66b0a 434 void **line; /* Line index */
b801d8b2
JF
435
436 /* Loading */
437 FILE *pipe;
438};
439
22f66b0a
JF
440static int pager_draw(struct view *view, unsigned int lineno);
441static int pager_read(struct view *view, char *line);
442
443static int main_draw(struct view *view, unsigned int lineno);
444static int main_read(struct view *view, char *line);
b801d8b2 445
fd85fef1 446#define DIFF_CMD \
b801d8b2
JF
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
fd85fef1
JF
453#define MAIN_CMD \
454 "git log --stat --pretty=raw %s"
455
4c6fabc2 456/* The status window is used for polling keystrokes. */
b801d8b2 457static WINDOW *status_win;
78c70acd
JF
458static WINDOW *title_win;
459
4c6fabc2
JF
460/* The number of loading views. Controls when nodelay should be in effect when
461 * polling user input. */
fd85fef1
JF
462static unsigned int nloading;
463
b801d8b2 464static struct view views[] = {
22f66b0a 465 { "diff", DIFF_CMD, commit_id, pager_read, pager_draw, sizeof(char) },
78c70acd
JF
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) },
b801d8b2
JF
468};
469
4c6fabc2 470/* The display array of active views and the index of the current view. */
b801d8b2 471static struct view *display[ARRAY_SIZE(views)];
4c6fabc2 472static unsigned int current_view;
22f66b0a 473
b801d8b2
JF
474#define foreach_view(view, i) \
475 for (i = 0; i < sizeof(display) && (view = display[i]); i++)
476
4c6fabc2 477
b801d8b2
JF
478static void
479redraw_view(struct view *view)
480{
481 int lineno;
b801d8b2
JF
482
483 wclear(view->win);
484 wmove(view->win, 0, 0);
485
fd85fef1 486 for (lineno = 0; lineno < view->height; lineno++) {
22f66b0a 487 if (!view->draw(view, lineno))
fd85fef1 488 break;
b801d8b2
JF
489 }
490
491 redrawwin(view->win);
492 wrefresh(view->win);
493}
494
b76c2afc
JF
495static void
496resize_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
b801d8b2
JF
518/* FIXME: Fix percentage. */
519static void
520report_position(struct view *view, int all)
521{
4c6fabc2 522 report(all ? "line %d of %d (%d%%)"
b801d8b2
JF
523 : "line %d of %d",
524 view->lineno + 1,
525 view->lines,
4c6fabc2 526 view->lines ? (view->lineno + 1) * 100 / view->lines : 0);
b801d8b2
JF
527}
528
78c70acd 529
b801d8b2 530static void
fd85fef1 531move_view(struct view *view, int lines)
b801d8b2 532{
fd85fef1
JF
533 /* The rendering expects the new offset. */
534 view->offset += lines;
535
536 assert(0 <= view->offset && view->offset < view->lines);
537 assert(lines);
b801d8b2 538
4c6fabc2 539 if (view->height < ABS(lines)) {
b76c2afc
JF
540 redraw_view(view);
541
542 } else {
22f66b0a
JF
543 int line = lines > 0 ? view->height - lines : 0;
544 int end = line + (lines > 0 ? lines : -lines);
fd85fef1
JF
545
546 wscrl(view->win, lines);
547
22f66b0a
JF
548 for (; line < end; line++) {
549 if (!view->draw(view, line))
fd85fef1
JF
550 break;
551 }
552 }
553
554 /* Move current line into the view. */
555 if (view->lineno < view->offset) {
556 view->lineno = view->offset;
22f66b0a 557 view->draw(view, 0);
fd85fef1
JF
558
559 } else if (view->lineno >= view->offset + view->height) {
560 view->lineno = view->offset + view->height - 1;
22f66b0a 561 view->draw(view, view->lineno - view->offset);
fd85fef1
JF
562 }
563
4c6fabc2 564 assert(view->offset <= view->lineno && view->lineno < view->lines);
fd85fef1
JF
565
566 redrawwin(view->win);
567 wrefresh(view->win);
568
569 report_position(view, lines);
570}
78c70acd 571
fd85fef1
JF
572static void
573scroll_view(struct view *view, int request)
574{
575 int lines = 1;
b801d8b2
JF
576
577 switch (request) {
fd85fef1
JF
578 case REQ_SCR_FPAGE:
579 lines = view->height;
580 case REQ_SCR_FLINE:
b801d8b2 581 if (view->offset + lines > view->lines)
bde3653a 582 lines = view->lines - view->offset;
b801d8b2 583
fd85fef1 584 if (lines == 0 || view->offset + view->height >= view->lines) {
b801d8b2
JF
585 report("already at last line");
586 return;
587 }
588 break;
589
fd85fef1
JF
590 case REQ_SCR_BPAGE:
591 lines = view->height;
592 case REQ_SCR_BLINE:
b801d8b2
JF
593 if (lines > view->offset)
594 lines = view->offset;
595
596 if (lines == 0) {
597 report("already at first line");
598 return;
599 }
600
fd85fef1 601 lines = -lines;
b801d8b2 602 break;
b801d8b2
JF
603 }
604
fd85fef1
JF
605 move_view(view, lines);
606}
b801d8b2 607
fd85fef1
JF
608static void
609navigate_view(struct view *view, int request)
610{
611 int steps;
b801d8b2 612
fd85fef1 613 switch (request) {
78c70acd
JF
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
fd85fef1 632 case REQ_PREV_LINE:
fd85fef1
JF
633 steps = -1;
634 break;
b801d8b2 635
fd85fef1 636 case REQ_NEXT_LINE:
fd85fef1
JF
637 steps = 1;
638 break;
78c70acd 639 }
b801d8b2 640
4c6fabc2 641 if (steps <= 0 && view->lineno == 0) {
78c70acd
JF
642 report("already at first line");
643 return;
b801d8b2 644
4c6fabc2 645 } else if (steps >= 0 && view->lineno + 1 == view->lines) {
78c70acd
JF
646 report("already at last line");
647 return;
fd85fef1
JF
648 }
649
4c6fabc2 650 /* Move the current line */
fd85fef1 651 view->lineno += steps;
4c6fabc2
JF
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);
fd85fef1 657
4c6fabc2 658 /* Check whether the view needs to be scrolled */
fd85fef1
JF
659 if (view->lineno < view->offset ||
660 view->lineno >= view->offset + view->height) {
661 if (steps < 0 && -steps > view->offset) {
662 steps = -view->offset;
b76c2afc
JF
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 }
b801d8b2 671 }
78c70acd 672
fd85fef1
JF
673 move_view(view, steps);
674 return;
b801d8b2
JF
675 }
676
4c6fabc2 677 /* Draw the current line */
22f66b0a 678 view->draw(view, view->lineno - view->offset);
fd85fef1 679
b801d8b2
JF
680 redrawwin(view->win);
681 wrefresh(view->win);
682
fd85fef1 683 report_position(view, view->height);
b801d8b2
JF
684}
685
b801d8b2
JF
686
687
688static bool
689begin_update(struct view *view)
690{
691 char buf[1024];
692
693 if (view->cmd) {
fd85fef1
JF
694 char *id = view->id;
695
696 if (snprintf(buf, sizeof(buf), view->cmd, id, id, id) < sizeof(buf))
b801d8b2
JF
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
715static void
716end_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
726static int
727update_view(struct view *view)
728{
729 char buffer[BUFSIZ];
730 char *line;
22f66b0a 731 void **tmp;
b801d8b2 732 int redraw;
fd85fef1 733 int lines = view->height;
b801d8b2
JF
734
735 if (!view->pipe)
736 return TRUE;
737
fd85fef1 738 /* Only redraw after the first reading session. */
22f66b0a
JF
739 /* FIXME: ... and possibly more. */
740 redraw = view->height > view->lines;
b801d8b2
JF
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
b801d8b2
JF
751 linelen = strlen(line);
752 if (linelen)
753 line[linelen - 1] = 0;
754
22f66b0a 755 if (!view->read(view, line))
b801d8b2 756 goto alloc_error;
fd85fef1
JF
757
758 if (lines-- == 1)
759 break;
b801d8b2
JF
760 }
761
4c6fabc2
JF
762 if (redraw) {
763 /* FIXME: This causes flickering. Draw incrementally. */
b801d8b2 764 redraw_view(view);
4c6fabc2 765 }
b801d8b2
JF
766
767 if (ferror(view->pipe)) {
4c6fabc2 768 report("failed to read %s: %s", view->cmd, strerror(errno));
b801d8b2
JF
769 goto end;
770
771 } else if (feof(view->pipe)) {
772 report_position(view, 0);
773 goto end;
774 }
775
776 return TRUE;
777
778alloc_error:
fd85fef1 779 report("allocation failure");
b801d8b2
JF
780
781end:
782 end_update(view);
783 return FALSE;
784}
785
786
787static struct view *
788switch_view(struct view *prev, int request)
789{
4c6fabc2 790 struct view *view = &views[VIEW_OFFSET(request)];
b801d8b2
JF
791 struct view *displayed;
792 int i;
793
794 if (view == prev) {
795 foreach_view (displayed, i) ;
796
797 if (i == 1)
fd85fef1 798 report("already in %s view", view->name);
b801d8b2
JF
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;
fd85fef1 808 report("new current view");
b801d8b2
JF
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 */
843static int
844view_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:
b801d8b2 851 case REQ_PREV_LINE:
fd85fef1
JF
852 case REQ_FIRST_LINE:
853 case REQ_LAST_LINE:
78c70acd
JF
854 case REQ_NEXT_PAGE:
855 case REQ_PREV_PAGE:
fd85fef1
JF
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:
b801d8b2
JF
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
b76c2afc
JF
874 case REQ_LINE_NUMBER:
875 opt_line_number = !opt_line_number;
876 redraw_view(view);
877 break;
878
b801d8b2
JF
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
b801d8b2 916static int
22f66b0a 917pager_draw(struct view *view, unsigned int lineno)
b801d8b2 918{
78c70acd 919 enum line_type type;
b801d8b2 920 char *line;
4c6fabc2 921 int linelen;
78c70acd 922 int attr;
b801d8b2 923
fd85fef1
JF
924 if (view->offset + lineno >= view->lines)
925 return FALSE;
926
b801d8b2 927 line = view->line[view->offset + lineno];
78c70acd 928 type = get_line_type(line);
b801d8b2 929
fd85fef1 930 if (view->offset + lineno == view->lineno) {
78c70acd 931 if (type == LINE_COMMIT)
fd85fef1 932 strncpy(commit_id, line + 7, SIZEOF_ID);
78c70acd 933 type = LINE_CURSOR;
fd85fef1
JF
934 }
935
78c70acd 936 attr = get_line_attr(type);
b801d8b2 937 wattrset(view->win, attr);
b76c2afc 938
4c6fabc2
JF
939 linelen = strlen(line);
940 linelen = MIN(linelen, view->width);
941
b76c2afc 942 if (opt_line_number) {
4c6fabc2
JF
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
b76c2afc
JF
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)
4c6fabc2
JF
969 line = " ", linelen = 1;
970 mvwaddnstr(view->win, lineno, 0, line, linelen);
b76c2afc 971 }
b801d8b2
JF
972
973 return TRUE;
974}
975
22f66b0a
JF
976static int
977pager_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
987static int
988main_draw(struct view *view, unsigned int lineno)
989{
b76c2afc 990 char buf[21];
22f66b0a 991 struct commit *commit;
78c70acd 992 enum line_type type;
b76c2afc
JF
993 int cols = 0;
994 size_t timelen;
22f66b0a
JF
995
996 if (view->offset + lineno >= view->lines)
997 return FALSE;
998
999 commit = view->line[view->offset + lineno];
4c6fabc2
JF
1000 if (!*commit->author)
1001 return FALSE;
22f66b0a 1002
22f66b0a
JF
1003 if (view->offset + lineno == view->lineno) {
1004 strncpy(commit_id, commit->id, SIZEOF_ID);
78c70acd
JF
1005 type = LINE_CURSOR;
1006 } else {
b76c2afc
JF
1007 type = LINE_MAIN_COMMIT;
1008 }
1009
1010 wmove(view->win, lineno, cols);
1011 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1012
4c6fabc2 1013 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
b76c2afc 1014 waddnstr(view->win, buf, timelen);
4c6fabc2 1015 waddstr(view->win, " ");
b76c2afc 1016
4c6fabc2 1017 cols += DATE_COLS;
b76c2afc
JF
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);
22f66b0a
JF
1027 }
1028
b76c2afc
JF
1029 cols += 20;
1030 wattrset(view->win, A_NORMAL);
1031 mvwaddch(view->win, lineno, cols, ACS_LTEE);
78c70acd 1032 wattrset(view->win, get_line_attr(type));
b76c2afc 1033 mvwaddstr(view->win, lineno, cols + 2, commit->title);
22f66b0a
JF
1034 wattrset(view->win, A_NORMAL);
1035
1036 return TRUE;
1037}
1038
4c6fabc2 1039/* Reads git log --pretty=raw output and parses it into the commit struct. */
22f66b0a
JF
1040static int
1041main_read(struct view *view, char *line)
1042{
78c70acd
JF
1043 enum line_type type = get_line_type(line);
1044 struct commit *commit;
22f66b0a 1045
78c70acd
JF
1046 switch (type) {
1047 case LINE_COMMIT:
22f66b0a
JF
1048 commit = calloc(1, sizeof(struct commit));
1049 if (!commit)
1050 return FALSE;
1051
4c6fabc2 1052 line += STRING_SIZE("commit ");
b76c2afc 1053
22f66b0a 1054 view->line[view->lines++] = commit;
b76c2afc 1055 strncpy(commit->id, line, sizeof(commit->id));
78c70acd 1056 break;
22f66b0a 1057
b76c2afc
JF
1058 case LINE_AUTHOR_IDENT:
1059 {
4c6fabc2 1060 char *ident = line + STRING_SIZE("author ");
b76c2afc
JF
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
4c6fabc2 1071 /* Parse epoch and timezone */
b76c2afc
JF
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, ' ');
4c6fabc2 1083 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
b76c2afc
JF
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 }
78c70acd 1101 default:
4c6fabc2 1102 /* Fill in the commit title */
78c70acd 1103 commit = view->line[view->lines - 1];
22f66b0a 1104 if (!commit->title[0] &&
22f66b0a
JF
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
b801d8b2
JF
1114/*
1115 * Main
1116 */
1117
1118static void
1119quit(int sig)
1120{
1121 if (status_win)
1122 delwin(status_win);
78c70acd
JF
1123 if (title_win)
1124 delwin(title_win);
b801d8b2
JF
1125 endwin();
1126
1127 /* FIXME: Shutdown gracefully. */
1128
1129 exit(0);
1130}
1131
1132static 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
1147static void
1148report(const char *msg, ...)
1149{
1150 va_list args;
1151
b76c2afc
JF
1152 werase(title_win);
1153 wmove(title_win, 0, 0);
1154 wprintw(title_win, "commit %s", commit_id);
1155 wrefresh(title_win);
1156
b801d8b2
JF
1157 va_start(args, msg);
1158
1159 werase(status_win);
1160 wmove(status_win, 0, 0);
1161
fd85fef1 1162#if 0
b801d8b2 1163 if (display[current_view])
fd85fef1
JF
1164 wprintw(status_win, "%s %4s: ", commit_id, display[current_view]->name);
1165#endif
b801d8b2
JF
1166 vwprintw(status_win, msg, args);
1167 wrefresh(status_win);
1168
1169 va_end(args);
b801d8b2
JF
1170}
1171
1172int
1173main(int argc, char *argv[])
1174{
b801d8b2 1175 int x, y;
b76c2afc
JF
1176 int request;
1177 int git_cmd;
b801d8b2
JF
1178
1179 signal(SIGINT, quit);
1180
b76c2afc
JF
1181 git_cmd = parse_options(argc, argv);
1182 if (git_cmd < 0)
1183 return 0;
1184
1185 request = opt_request;
1186
b801d8b2
JF
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
78c70acd
JF
1202 title_win = newwin(1, 0, y - 2, 0);
1203 if (!title_win)
1204 die("Failed to create title window");
1205
b801d8b2
JF
1206 /* Enable keyboard mapping */
1207 keypad(status_win, TRUE);
78c70acd
JF
1208 wbkgdset(status_win, get_line_attr(LINE_STATUS));
1209 wbkgdset(title_win, get_line_attr(LINE_TITLE));
b801d8b2
JF
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);
78c70acd 1227
b801d8b2
JF
1228 mvwin(status_win, lines - 1, 0);
1229 wresize(status_win, 1, cols - 1);
78c70acd
JF
1230
1231 mvwin(title_win, lines - 2, 0);
1232 wresize(title_win, 1, cols - 1);
b801d8b2
JF
1233 }
1234 }
1235
1236 quit(0);
1237
1238 return 0;
1239}
1240
1241/**
4c6fabc2
JF
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 *
b801d8b2
JF
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 * --------
4c6fabc2 1282 * [verse]
b801d8b2
JF
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)]
4c6fabc2
JF
1285 * gitk(1): git repository browser written using tcl/tk,
1286 * gitview(1): git repository browser written using python/gtk.
b801d8b2 1287 **/