Add support for showing tags and other repo refs in the diff and log view
[tig] / tig.c
1 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
13 /**
14 * TIG(1)
15 * ======
16 *
17 * NAME
18 * ----
19 * tig - text-mode interface for git
20 *
21 * SYNOPSIS
22 * --------
23 * [verse]
24 * tig [options]
25 * tig [options] [--] [git log options]
26 * tig [options] log [git log options]
27 * tig [options] diff [git diff options]
28 * tig [options] show [git show options]
29 * tig [options] < [git command output]
30 *
31 * DESCRIPTION
32 * -----------
33 * Browse changes in a git repository. Additionally, tig(1) can also act
34 * as a pager for output of various git commands.
35 *
36 * When browsing repositories, tig(1) uses the underlying git commands
37 * to present the user with various views, such as summarized commit log
38 * and showing the commit with the log message, diffstat, and the diff.
39 *
40 * Using tig(1) as a pager, it will display input from stdin and try
41 * to colorize it.
42 **/
43
44 #ifndef VERSION
45 #define VERSION "tig-0.3"
46 #endif
47
48 #ifndef DEBUG
49 #define NDEBUG
50 #endif
51
52 #include <assert.h>
53 #include <errno.h>
54 #include <ctype.h>
55 #include <signal.h>
56 #include <stdarg.h>
57 #include <stdio.h>
58 #include <stdlib.h>
59 #include <string.h>
60 #include <unistd.h>
61 #include <time.h>
62
63 #include <curses.h>
64
65 static void die(const char *err, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
70 static void load_help_page(void);
71
72 #define ABS(x) ((x) >= 0 ? (x) : -(x))
73 #define MIN(x, y) ((x) < (y) ? (x) : (y))
74
75 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
76 #define STRING_SIZE(x) (sizeof(x) - 1)
77
78 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
79 #define SIZEOF_CMD 1024 /* Size of command buffer. */
80
81 /* This color name can be used to refer to the default term colors. */
82 #define COLOR_DEFAULT (-1)
83
84 /* The format and size of the date column in the main view. */
85 #define DATE_FORMAT "%Y-%m-%d %H:%M"
86 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
87
88 #define AUTHOR_COLS 20
89
90 /* The default interval between line numbers. */
91 #define NUMBER_INTERVAL 1
92
93 #define TABSIZE 8
94
95 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
96
97 /* Some ascii-shorthands fitted into the ncurses namespace. */
98 #define KEY_TAB '\t'
99 #define KEY_RETURN '\r'
100 #define KEY_ESC 27
101
102
103 struct ref {
104 char *name; /* Ref name; tag or head names are shortened. */
105 char id[41]; /* Commit SHA1 ID */
106 unsigned int tag:1; /* Is it a tag? */
107 unsigned int next:1; /* For ref lists: are there more refs? */
108 };
109
110 static struct ref **get_refs(char *id);
111
112 struct int_map {
113 const char *name;
114 int namelen;
115 int value;
116 };
117
118 static int
119 set_from_int_map(struct int_map *map, size_t map_size,
120 int *value, const char *name, int namelen)
121 {
122
123 int i;
124
125 for (i = 0; i < map_size; i++)
126 if (namelen == map[i].namelen &&
127 !strncasecmp(name, map[i].name, namelen)) {
128 *value = map[i].value;
129 return OK;
130 }
131
132 return ERR;
133 }
134
135
136 /*
137 * String helpers
138 */
139
140 static inline void
141 string_ncopy(char *dst, const char *src, int dstlen)
142 {
143 strncpy(dst, src, dstlen - 1);
144 dst[dstlen - 1] = 0;
145
146 }
147
148 /* Shorthand for safely copying into a fixed buffer. */
149 #define string_copy(dst, src) \
150 string_ncopy(dst, src, sizeof(dst))
151
152 static char *
153 chomp_string(char *name)
154 {
155 int namelen;
156
157 while (isspace(*name))
158 name++;
159
160 namelen = strlen(name) - 1;
161 while (namelen > 0 && isspace(name[namelen]))
162 name[namelen--] = 0;
163
164 return name;
165 }
166
167
168 /* Shell quoting
169 *
170 * NOTE: The following is a slightly modified copy of the git project's shell
171 * quoting routines found in the quote.c file.
172 *
173 * Help to copy the thing properly quoted for the shell safety. any single
174 * quote is replaced with '\'', any exclamation point is replaced with '\!',
175 * and the whole thing is enclosed in a
176 *
177 * E.g.
178 * original sq_quote result
179 * name ==> name ==> 'name'
180 * a b ==> a b ==> 'a b'
181 * a'b ==> a'\''b ==> 'a'\''b'
182 * a!b ==> a'\!'b ==> 'a'\!'b'
183 */
184
185 static size_t
186 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
187 {
188 char c;
189
190 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
191
192 BUFPUT('\'');
193 while ((c = *src++)) {
194 if (c == '\'' || c == '!') {
195 BUFPUT('\'');
196 BUFPUT('\\');
197 BUFPUT(c);
198 BUFPUT('\'');
199 } else {
200 BUFPUT(c);
201 }
202 }
203 BUFPUT('\'');
204
205 return bufsize;
206 }
207
208
209 /*
210 * User requests
211 */
212
213 #define REQ_INFO \
214 /* XXX: Keep the view request first and in sync with views[]. */ \
215 REQ_GROUP("View switching") \
216 REQ_(VIEW_MAIN, "Show main view"), \
217 REQ_(VIEW_DIFF, "Show diff view"), \
218 REQ_(VIEW_LOG, "Show log view"), \
219 REQ_(VIEW_HELP, "Show help page"), \
220 REQ_(VIEW_PAGER, "Show pager view"), \
221 \
222 REQ_GROUP("View manipulation") \
223 REQ_(ENTER, "Enter current line and scroll"), \
224 REQ_(NEXT, "Move to next"), \
225 REQ_(PREVIOUS, "Move to previous"), \
226 REQ_(VIEW_NEXT, "Move focus to next view"), \
227 REQ_(VIEW_CLOSE, "Close the current view"), \
228 REQ_(QUIT, "Close all views and quit"), \
229 \
230 REQ_GROUP("Cursor navigation") \
231 REQ_(MOVE_UP, "Move cursor one line up"), \
232 REQ_(MOVE_DOWN, "Move cursor one line down"), \
233 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
234 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
235 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
236 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
237 \
238 REQ_GROUP("Scrolling") \
239 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
240 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
241 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
242 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
243 \
244 REQ_GROUP("Misc") \
245 REQ_(PROMPT, "Bring up the prompt"), \
246 REQ_(SCREEN_UPDATE, "Update the screen"), \
247 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
248 REQ_(SCREEN_RESIZE, "Resize the screen"), \
249 REQ_(SHOW_VERSION, "Show version information"), \
250 REQ_(STOP_LOADING, "Stop all loading views"), \
251 REQ_(TOGGLE_LINENO, "Toggle line numbers"),
252
253
254 /* User action requests. */
255 enum request {
256 #define REQ_GROUP(help)
257 #define REQ_(req, help) REQ_##req
258
259 /* Offset all requests to avoid conflicts with ncurses getch values. */
260 REQ_OFFSET = KEY_MAX + 1,
261 REQ_INFO
262
263 #undef REQ_GROUP
264 #undef REQ_
265 };
266
267 struct request_info {
268 enum request request;
269 char *help;
270 };
271
272 static struct request_info req_info[] = {
273 #define REQ_GROUP(help) { 0, (help) },
274 #define REQ_(req, help) { REQ_##req, (help) }
275 REQ_INFO
276 #undef REQ_GROUP
277 #undef REQ_
278 };
279
280 /**
281 * OPTIONS
282 * -------
283 **/
284
285 static const char usage[] =
286 VERSION " (" __DATE__ ")\n"
287 "\n"
288 "Usage: tig [options]\n"
289 " or: tig [options] [--] [git log options]\n"
290 " or: tig [options] log [git log options]\n"
291 " or: tig [options] diff [git diff options]\n"
292 " or: tig [options] show [git show options]\n"
293 " or: tig [options] < [git command output]\n"
294 "\n"
295 "Options:\n"
296 " -l Start up in log view\n"
297 " -d Start up in diff view\n"
298 " -n[I], --line-number[=I] Show line numbers with given interval\n"
299 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
300 " -- Mark end of tig options\n"
301 " -v, --version Show version and exit\n"
302 " -h, --help Show help message and exit\n";
303
304 /* Option and state variables. */
305 static bool opt_line_number = FALSE;
306 static int opt_num_interval = NUMBER_INTERVAL;
307 static int opt_tab_size = TABSIZE;
308 static enum request opt_request = REQ_VIEW_MAIN;
309 static char opt_cmd[SIZEOF_CMD] = "";
310 static char opt_encoding[20] = "";
311 static bool opt_utf8 = TRUE;
312 static FILE *opt_pipe = NULL;
313
314 /* Returns the index of log or diff command or -1 to exit. */
315 static bool
316 parse_options(int argc, char *argv[])
317 {
318 int i;
319
320 for (i = 1; i < argc; i++) {
321 char *opt = argv[i];
322
323 /**
324 * -l::
325 * Start up in log view using the internal log command.
326 **/
327 if (!strcmp(opt, "-l")) {
328 opt_request = REQ_VIEW_LOG;
329 continue;
330 }
331
332 /**
333 * -d::
334 * Start up in diff view using the internal diff command.
335 **/
336 if (!strcmp(opt, "-d")) {
337 opt_request = REQ_VIEW_DIFF;
338 continue;
339 }
340
341 /**
342 * -n[INTERVAL], --line-number[=INTERVAL]::
343 * Prefix line numbers in log and diff view.
344 * Optionally, with interval different than each line.
345 **/
346 if (!strncmp(opt, "-n", 2) ||
347 !strncmp(opt, "--line-number", 13)) {
348 char *num = opt;
349
350 if (opt[1] == 'n') {
351 num = opt + 2;
352
353 } else if (opt[STRING_SIZE("--line-number")] == '=') {
354 num = opt + STRING_SIZE("--line-number=");
355 }
356
357 if (isdigit(*num))
358 opt_num_interval = atoi(num);
359
360 opt_line_number = TRUE;
361 continue;
362 }
363
364 /**
365 * -b[NSPACES], --tab-size[=NSPACES]::
366 * Set the number of spaces tabs should be expanded to.
367 **/
368 if (!strncmp(opt, "-b", 2) ||
369 !strncmp(opt, "--tab-size", 10)) {
370 char *num = opt;
371
372 if (opt[1] == 'b') {
373 num = opt + 2;
374
375 } else if (opt[STRING_SIZE("--tab-size")] == '=') {
376 num = opt + STRING_SIZE("--tab-size=");
377 }
378
379 if (isdigit(*num))
380 opt_tab_size = MIN(atoi(num), TABSIZE);
381 continue;
382 }
383
384 /**
385 * -v, --version::
386 * Show version and exit.
387 **/
388 if (!strcmp(opt, "-v") ||
389 !strcmp(opt, "--version")) {
390 printf("tig version %s\n", VERSION);
391 return FALSE;
392 }
393
394 /**
395 * -h, --help::
396 * Show help message and exit.
397 **/
398 if (!strcmp(opt, "-h") ||
399 !strcmp(opt, "--help")) {
400 printf(usage);
401 return FALSE;
402 }
403
404 /**
405 * \--::
406 * End of tig(1) options. Useful when specifying command
407 * options for the main view. Example:
408 *
409 * $ tig -- --since=1.month
410 **/
411 if (!strcmp(opt, "--")) {
412 i++;
413 break;
414 }
415
416 /**
417 * log [git log options]::
418 * Open log view using the given git log options.
419 *
420 * diff [git diff options]::
421 * Open diff view using the given git diff options.
422 *
423 * show [git show options]::
424 * Open diff view using the given git show options.
425 **/
426 if (!strcmp(opt, "log") ||
427 !strcmp(opt, "diff") ||
428 !strcmp(opt, "show")) {
429 opt_request = opt[0] == 'l'
430 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
431 break;
432 }
433
434 /**
435 * [git log options]::
436 * tig(1) will stop the option parsing when the first
437 * command line parameter not starting with "-" is
438 * encountered. All options including this one will be
439 * passed to git log when loading the main view.
440 * This makes it possible to say:
441 *
442 * $ tig tag-1.0..HEAD
443 **/
444 if (opt[0] && opt[0] != '-')
445 break;
446
447 die("unknown command '%s'", opt);
448 }
449
450 if (!isatty(STDIN_FILENO)) {
451 opt_request = REQ_VIEW_PAGER;
452 opt_pipe = stdin;
453
454 } else if (i < argc) {
455 size_t buf_size;
456
457 if (opt_request == REQ_VIEW_MAIN)
458 /* XXX: This is vulnerable to the user overriding
459 * options required for the main view parser. */
460 string_copy(opt_cmd, "git log --stat --pretty=raw");
461 else
462 string_copy(opt_cmd, "git");
463 buf_size = strlen(opt_cmd);
464
465 while (buf_size < sizeof(opt_cmd) && i < argc) {
466 opt_cmd[buf_size++] = ' ';
467 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
468 }
469
470 if (buf_size >= sizeof(opt_cmd))
471 die("command too long");
472
473 opt_cmd[buf_size] = 0;
474
475 }
476
477 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
478 opt_utf8 = FALSE;
479
480 return TRUE;
481 }
482
483
484 /**
485 * ENVIRONMENT VARIABLES
486 * ---------------------
487 * TIG_LS_REMOTE::
488 * Set command for retrieving all repository references. The command
489 * should output data in the same format as git-ls-remote(1).
490 **/
491
492 #define TIG_LS_REMOTE \
493 "git ls-remote . 2>/dev/null"
494
495 /**
496 * TIG_DIFF_CMD::
497 * The command used for the diff view. By default, git show is used
498 * as a backend.
499 *
500 * TIG_LOG_CMD::
501 * The command used for the log view. If you prefer to have both
502 * author and committer shown in the log view be sure to pass
503 * `--pretty=fuller` to git log.
504 *
505 * TIG_MAIN_CMD::
506 * The command used for the main view. Note, you must always specify
507 * the option: `--pretty=raw` since the main view parser expects to
508 * read that format.
509 **/
510
511 #define TIG_DIFF_CMD \
512 "git show --patch-with-stat --find-copies-harder -B -C %s"
513
514 #define TIG_LOG_CMD \
515 "git log --cc --stat -n100 %s"
516
517 #define TIG_MAIN_CMD \
518 "git log --topo-order --stat --pretty=raw %s"
519
520 /* ... silently ignore that the following are also exported. */
521
522 #define TIG_HELP_CMD \
523 ""
524
525 #define TIG_PAGER_CMD \
526 ""
527
528
529 /**
530 * FILES
531 * -----
532 * '~/.tigrc'::
533 * User configuration file. See tigrc(5) for examples.
534 *
535 * '.git/config'::
536 * Repository config file. Read on startup with the help of
537 * git-repo-config(1).
538 **/
539
540 static struct int_map color_map[] = {
541 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
542 COLOR_MAP(DEFAULT),
543 COLOR_MAP(BLACK),
544 COLOR_MAP(BLUE),
545 COLOR_MAP(CYAN),
546 COLOR_MAP(GREEN),
547 COLOR_MAP(MAGENTA),
548 COLOR_MAP(RED),
549 COLOR_MAP(WHITE),
550 COLOR_MAP(YELLOW),
551 };
552
553 static struct int_map attr_map[] = {
554 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
555 ATTR_MAP(NORMAL),
556 ATTR_MAP(BLINK),
557 ATTR_MAP(BOLD),
558 ATTR_MAP(DIM),
559 ATTR_MAP(REVERSE),
560 ATTR_MAP(STANDOUT),
561 ATTR_MAP(UNDERLINE),
562 };
563
564 #define LINE_INFO \
565 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
568 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
569 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
570 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
586 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
587 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
590 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
593 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
594 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
595 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
596 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
597 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
598 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
599 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
600 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
601 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
602 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
603
604
605 /*
606 * Line-oriented content detection.
607 */
608
609 enum line_type {
610 #define LINE(type, line, fg, bg, attr) \
611 LINE_##type
612 LINE_INFO
613 #undef LINE
614 };
615
616 struct line_info {
617 const char *name; /* Option name. */
618 int namelen; /* Size of option name. */
619 const char *line; /* The start of line to match. */
620 int linelen; /* Size of string to match. */
621 int fg, bg, attr; /* Color and text attributes for the lines. */
622 };
623
624 static struct line_info line_info[] = {
625 #define LINE(type, line, fg, bg, attr) \
626 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
627 LINE_INFO
628 #undef LINE
629 };
630
631 static enum line_type
632 get_line_type(char *line)
633 {
634 int linelen = strlen(line);
635 enum line_type type;
636
637 for (type = 0; type < ARRAY_SIZE(line_info); type++)
638 /* Case insensitive search matches Signed-off-by lines better. */
639 if (linelen >= line_info[type].linelen &&
640 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
641 return type;
642
643 return LINE_DEFAULT;
644 }
645
646 static inline int
647 get_line_attr(enum line_type type)
648 {
649 assert(type < ARRAY_SIZE(line_info));
650 return COLOR_PAIR(type) | line_info[type].attr;
651 }
652
653 static struct line_info *
654 get_line_info(char *name, int namelen)
655 {
656 enum line_type type;
657 int i;
658
659 /* Diff-Header -> DIFF_HEADER */
660 for (i = 0; i < namelen; i++) {
661 if (name[i] == '-')
662 name[i] = '_';
663 else if (name[i] == '.')
664 name[i] = '_';
665 }
666
667 for (type = 0; type < ARRAY_SIZE(line_info); type++)
668 if (namelen == line_info[type].namelen &&
669 !strncasecmp(line_info[type].name, name, namelen))
670 return &line_info[type];
671
672 return NULL;
673 }
674
675 static void
676 init_colors(void)
677 {
678 int default_bg = COLOR_BLACK;
679 int default_fg = COLOR_WHITE;
680 enum line_type type;
681
682 start_color();
683
684 if (use_default_colors() != ERR) {
685 default_bg = -1;
686 default_fg = -1;
687 }
688
689 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
690 struct line_info *info = &line_info[type];
691 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
692 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
693
694 init_pair(type, fg, bg);
695 }
696 }
697
698 struct line {
699 enum line_type type;
700 void *data; /* User data */
701 };
702
703
704 /*
705 * User config file handling.
706 */
707
708 #define set_color(color, name, namelen) \
709 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen)
710
711 #define set_attribute(attr, name, namelen) \
712 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen)
713
714 static int config_lineno;
715 static bool config_errors;
716 static char *config_msg;
717
718 static int
719 set_option(char *opt, int optlen, char *value, int valuelen)
720 {
721 /* Reads: "color" object fgcolor bgcolor [attr] */
722 if (!strcmp(opt, "color")) {
723 struct line_info *info;
724
725 value = chomp_string(value);
726 valuelen = strcspn(value, " \t");
727 info = get_line_info(value, valuelen);
728 if (!info) {
729 config_msg = "Unknown color name";
730 return ERR;
731 }
732
733 value = chomp_string(value + valuelen);
734 valuelen = strcspn(value, " \t");
735 if (set_color(&info->fg, value, valuelen) == ERR) {
736 config_msg = "Unknown color";
737 return ERR;
738 }
739
740 value = chomp_string(value + valuelen);
741 valuelen = strcspn(value, " \t");
742 if (set_color(&info->bg, value, valuelen) == ERR) {
743 config_msg = "Unknown color";
744 return ERR;
745 }
746
747 value = chomp_string(value + valuelen);
748 if (*value &&
749 set_attribute(&info->attr, value, strlen(value)) == ERR) {
750 config_msg = "Unknown attribute";
751 return ERR;
752 }
753
754 return OK;
755 }
756
757 return ERR;
758 }
759
760 static int
761 read_option(char *opt, int optlen, char *value, int valuelen)
762 {
763 config_lineno++;
764 config_msg = "Internal error";
765
766 optlen = strcspn(opt, "#;");
767 if (optlen == 0) {
768 /* The whole line is a commend or empty. */
769 return OK;
770
771 } else if (opt[optlen] != 0) {
772 /* Part of the option name is a comment, so the value part
773 * should be ignored. */
774 valuelen = 0;
775 opt[optlen] = value[valuelen] = 0;
776 } else {
777 /* Else look for comment endings in the value. */
778 valuelen = strcspn(value, "#;");
779 value[valuelen] = 0;
780 }
781
782 if (set_option(opt, optlen, value, valuelen) == ERR) {
783 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
784 config_lineno, optlen, opt, config_msg);
785 config_errors = TRUE;
786 }
787
788 /* Always keep going if errors are encountered. */
789 return OK;
790 }
791
792 static int
793 load_options(void)
794 {
795 char *home = getenv("HOME");
796 char buf[1024];
797 FILE *file;
798
799 config_lineno = 0;
800 config_errors = FALSE;
801
802 if (!home ||
803 snprintf(buf, sizeof(buf), "%s/.tigrc", home) >= sizeof(buf))
804 return ERR;
805
806 /* It's ok that the file doesn't exist. */
807 file = fopen(buf, "r");
808 if (!file)
809 return OK;
810
811 if (read_properties(file, " \t", read_option) == ERR ||
812 config_errors == TRUE)
813 fprintf(stderr, "Errors while loading %s.\n", buf);
814
815 return OK;
816 }
817
818
819 /*
820 * The viewer
821 */
822
823 struct view;
824 struct view_ops;
825
826 /* The display array of active views and the index of the current view. */
827 static struct view *display[2];
828 static unsigned int current_view;
829
830 #define foreach_view(view, i) \
831 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
832
833 #define displayed_views() (display[1] != NULL ? 2 : 1)
834
835 /* Current head and commit ID */
836 static char ref_commit[SIZEOF_REF] = "HEAD";
837 static char ref_head[SIZEOF_REF] = "HEAD";
838
839 struct view {
840 const char *name; /* View name */
841 const char *cmd_fmt; /* Default command line format */
842 const char *cmd_env; /* Command line set via environment */
843 const char *id; /* Points to either of ref_{head,commit} */
844
845 struct view_ops *ops; /* View operations */
846
847 char cmd[SIZEOF_CMD]; /* Command buffer */
848 char ref[SIZEOF_REF]; /* Hovered commit reference */
849 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
850
851 int height, width; /* The width and height of the main window */
852 WINDOW *win; /* The main window */
853 WINDOW *title; /* The title window living below the main window */
854
855 /* Navigation */
856 unsigned long offset; /* Offset of the window top */
857 unsigned long lineno; /* Current line number */
858
859 /* If non-NULL, points to the view that opened this view. If this view
860 * is closed tig will switch back to the parent view. */
861 struct view *parent;
862
863 /* Buffering */
864 unsigned long lines; /* Total number of lines */
865 struct line *line; /* Line index */
866 unsigned long line_size;/* Total number of allocated lines */
867 unsigned int digits; /* Number of digits in the lines member. */
868
869 /* Loading */
870 FILE *pipe;
871 time_t start_time;
872 };
873
874 struct view_ops {
875 /* What type of content being displayed. Used in the title bar. */
876 const char *type;
877 /* Draw one line; @lineno must be < view->height. */
878 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
879 /* Read one line; updates view->line. */
880 bool (*read)(struct view *view, struct line *prev, char *data);
881 /* Depending on view, change display based on current line. */
882 bool (*enter)(struct view *view, struct line *line);
883 };
884
885 static struct view_ops pager_ops;
886 static struct view_ops main_ops;
887
888 #define VIEW_STR(name, cmd, env, ref, ops) \
889 { name, cmd, #env, ref, ops }
890
891 #define VIEW_(id, name, ops, ref) \
892 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
893
894
895 static struct view views[] = {
896 VIEW_(MAIN, "main", &main_ops, ref_head),
897 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
898 VIEW_(LOG, "log", &pager_ops, ref_head),
899 VIEW_(HELP, "help", &pager_ops, "static"),
900 VIEW_(PAGER, "pager", &pager_ops, "static"),
901 };
902
903 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
904
905
906 static bool
907 draw_view_line(struct view *view, unsigned int lineno)
908 {
909 if (view->offset + lineno >= view->lines)
910 return FALSE;
911
912 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
913 }
914
915 static void
916 redraw_view_from(struct view *view, int lineno)
917 {
918 assert(0 <= lineno && lineno < view->height);
919
920 for (; lineno < view->height; lineno++) {
921 if (!draw_view_line(view, lineno))
922 break;
923 }
924
925 redrawwin(view->win);
926 wrefresh(view->win);
927 }
928
929 static void
930 redraw_view(struct view *view)
931 {
932 wclear(view->win);
933 redraw_view_from(view, 0);
934 }
935
936
937 static void
938 update_view_title(struct view *view)
939 {
940 if (view == display[current_view])
941 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
942 else
943 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
944
945 werase(view->title);
946 wmove(view->title, 0, 0);
947
948 if (*view->ref)
949 wprintw(view->title, "[%s] %s", view->name, view->ref);
950 else
951 wprintw(view->title, "[%s]", view->name);
952
953 if (view->lines || view->pipe) {
954 unsigned int lines = view->lines
955 ? (view->lineno + 1) * 100 / view->lines
956 : 0;
957
958 wprintw(view->title, " - %s %d of %d (%d%%)",
959 view->ops->type,
960 view->lineno + 1,
961 view->lines,
962 lines);
963 }
964
965 if (view->pipe) {
966 time_t secs = time(NULL) - view->start_time;
967
968 /* Three git seconds are a long time ... */
969 if (secs > 2)
970 wprintw(view->title, " %lds", secs);
971 }
972
973 wmove(view->title, 0, view->width - 1);
974 wrefresh(view->title);
975 }
976
977 static void
978 resize_display(void)
979 {
980 int offset, i;
981 struct view *base = display[0];
982 struct view *view = display[1] ? display[1] : display[0];
983
984 /* Setup window dimensions */
985
986 getmaxyx(stdscr, base->height, base->width);
987
988 /* Make room for the status window. */
989 base->height -= 1;
990
991 if (view != base) {
992 /* Horizontal split. */
993 view->width = base->width;
994 view->height = SCALE_SPLIT_VIEW(base->height);
995 base->height -= view->height;
996
997 /* Make room for the title bar. */
998 view->height -= 1;
999 }
1000
1001 /* Make room for the title bar. */
1002 base->height -= 1;
1003
1004 offset = 0;
1005
1006 foreach_view (view, i) {
1007 if (!view->win) {
1008 view->win = newwin(view->height, 0, offset, 0);
1009 if (!view->win)
1010 die("Failed to create %s view", view->name);
1011
1012 scrollok(view->win, TRUE);
1013
1014 view->title = newwin(1, 0, offset + view->height, 0);
1015 if (!view->title)
1016 die("Failed to create title window");
1017
1018 } else {
1019 wresize(view->win, view->height, view->width);
1020 mvwin(view->win, offset, 0);
1021 mvwin(view->title, offset + view->height, 0);
1022 }
1023
1024 offset += view->height + 1;
1025 }
1026 }
1027
1028 static void
1029 redraw_display(void)
1030 {
1031 struct view *view;
1032 int i;
1033
1034 foreach_view (view, i) {
1035 redraw_view(view);
1036 update_view_title(view);
1037 }
1038 }
1039
1040 static void
1041 update_display_cursor(void)
1042 {
1043 struct view *view = display[current_view];
1044
1045 /* Move the cursor to the right-most column of the cursor line.
1046 *
1047 * XXX: This could turn out to be a bit expensive, but it ensures that
1048 * the cursor does not jump around. */
1049 if (view->lines) {
1050 wmove(view->win, view->lineno - view->offset, view->width - 1);
1051 wrefresh(view->win);
1052 }
1053 }
1054
1055 /*
1056 * Navigation
1057 */
1058
1059 /* Scrolling backend */
1060 static void
1061 do_scroll_view(struct view *view, int lines, bool redraw)
1062 {
1063 /* The rendering expects the new offset. */
1064 view->offset += lines;
1065
1066 assert(0 <= view->offset && view->offset < view->lines);
1067 assert(lines);
1068
1069 /* Redraw the whole screen if scrolling is pointless. */
1070 if (view->height < ABS(lines)) {
1071 redraw_view(view);
1072
1073 } else {
1074 int line = lines > 0 ? view->height - lines : 0;
1075 int end = line + ABS(lines);
1076
1077 wscrl(view->win, lines);
1078
1079 for (; line < end; line++) {
1080 if (!draw_view_line(view, line))
1081 break;
1082 }
1083 }
1084
1085 /* Move current line into the view. */
1086 if (view->lineno < view->offset) {
1087 view->lineno = view->offset;
1088 draw_view_line(view, 0);
1089
1090 } else if (view->lineno >= view->offset + view->height) {
1091 if (view->lineno == view->offset + view->height) {
1092 /* Clear the hidden line so it doesn't show if the view
1093 * is scrolled up. */
1094 wmove(view->win, view->height, 0);
1095 wclrtoeol(view->win);
1096 }
1097 view->lineno = view->offset + view->height - 1;
1098 draw_view_line(view, view->lineno - view->offset);
1099 }
1100
1101 assert(view->offset <= view->lineno && view->lineno < view->lines);
1102
1103 if (!redraw)
1104 return;
1105
1106 redrawwin(view->win);
1107 wrefresh(view->win);
1108 report("");
1109 }
1110
1111 /* Scroll frontend */
1112 static void
1113 scroll_view(struct view *view, enum request request)
1114 {
1115 int lines = 1;
1116
1117 switch (request) {
1118 case REQ_SCROLL_PAGE_DOWN:
1119 lines = view->height;
1120 case REQ_SCROLL_LINE_DOWN:
1121 if (view->offset + lines > view->lines)
1122 lines = view->lines - view->offset;
1123
1124 if (lines == 0 || view->offset + view->height >= view->lines) {
1125 report("Cannot scroll beyond the last line");
1126 return;
1127 }
1128 break;
1129
1130 case REQ_SCROLL_PAGE_UP:
1131 lines = view->height;
1132 case REQ_SCROLL_LINE_UP:
1133 if (lines > view->offset)
1134 lines = view->offset;
1135
1136 if (lines == 0) {
1137 report("Cannot scroll beyond the first line");
1138 return;
1139 }
1140
1141 lines = -lines;
1142 break;
1143
1144 default:
1145 die("request %d not handled in switch", request);
1146 }
1147
1148 do_scroll_view(view, lines, TRUE);
1149 }
1150
1151 /* Cursor moving */
1152 static void
1153 move_view(struct view *view, enum request request, bool redraw)
1154 {
1155 int steps;
1156
1157 switch (request) {
1158 case REQ_MOVE_FIRST_LINE:
1159 steps = -view->lineno;
1160 break;
1161
1162 case REQ_MOVE_LAST_LINE:
1163 steps = view->lines - view->lineno - 1;
1164 break;
1165
1166 case REQ_MOVE_PAGE_UP:
1167 steps = view->height > view->lineno
1168 ? -view->lineno : -view->height;
1169 break;
1170
1171 case REQ_MOVE_PAGE_DOWN:
1172 steps = view->lineno + view->height >= view->lines
1173 ? view->lines - view->lineno - 1 : view->height;
1174 break;
1175
1176 case REQ_MOVE_UP:
1177 steps = -1;
1178 break;
1179
1180 case REQ_MOVE_DOWN:
1181 steps = 1;
1182 break;
1183
1184 default:
1185 die("request %d not handled in switch", request);
1186 }
1187
1188 if (steps <= 0 && view->lineno == 0) {
1189 report("Cannot move beyond the first line");
1190 return;
1191
1192 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1193 report("Cannot move beyond the last line");
1194 return;
1195 }
1196
1197 /* Move the current line */
1198 view->lineno += steps;
1199 assert(0 <= view->lineno && view->lineno < view->lines);
1200
1201 /* Repaint the old "current" line if we be scrolling */
1202 if (ABS(steps) < view->height) {
1203 int prev_lineno = view->lineno - steps - view->offset;
1204
1205 wmove(view->win, prev_lineno, 0);
1206 wclrtoeol(view->win);
1207 draw_view_line(view, prev_lineno);
1208 }
1209
1210 /* Check whether the view needs to be scrolled */
1211 if (view->lineno < view->offset ||
1212 view->lineno >= view->offset + view->height) {
1213 if (steps < 0 && -steps > view->offset) {
1214 steps = -view->offset;
1215
1216 } else if (steps > 0) {
1217 if (view->lineno == view->lines - 1 &&
1218 view->lines > view->height) {
1219 steps = view->lines - view->offset - 1;
1220 if (steps >= view->height)
1221 steps -= view->height - 1;
1222 }
1223 }
1224
1225 do_scroll_view(view, steps, redraw);
1226 return;
1227 }
1228
1229 /* Draw the current line */
1230 draw_view_line(view, view->lineno - view->offset);
1231
1232 if (!redraw)
1233 return;
1234
1235 redrawwin(view->win);
1236 wrefresh(view->win);
1237 report("");
1238 }
1239
1240
1241 /*
1242 * Incremental updating
1243 */
1244
1245 static void
1246 end_update(struct view *view)
1247 {
1248 if (!view->pipe)
1249 return;
1250 set_nonblocking_input(FALSE);
1251 if (view->pipe == stdin)
1252 fclose(view->pipe);
1253 else
1254 pclose(view->pipe);
1255 view->pipe = NULL;
1256 }
1257
1258 static bool
1259 begin_update(struct view *view)
1260 {
1261 const char *id = view->id;
1262
1263 if (view->pipe)
1264 end_update(view);
1265
1266 if (opt_cmd[0]) {
1267 string_copy(view->cmd, opt_cmd);
1268 opt_cmd[0] = 0;
1269 /* When running random commands, the view ref could have become
1270 * invalid so clear it. */
1271 view->ref[0] = 0;
1272 } else {
1273 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1274
1275 if (snprintf(view->cmd, sizeof(view->cmd), format,
1276 id, id, id, id, id) >= sizeof(view->cmd))
1277 return FALSE;
1278 }
1279
1280 /* Special case for the pager view. */
1281 if (opt_pipe) {
1282 view->pipe = opt_pipe;
1283 opt_pipe = NULL;
1284 } else {
1285 view->pipe = popen(view->cmd, "r");
1286 }
1287
1288 if (!view->pipe)
1289 return FALSE;
1290
1291 set_nonblocking_input(TRUE);
1292
1293 view->offset = 0;
1294 view->lines = 0;
1295 view->lineno = 0;
1296 string_copy(view->vid, id);
1297
1298 if (view->line) {
1299 int i;
1300
1301 for (i = 0; i < view->lines; i++)
1302 if (view->line[i].data)
1303 free(view->line[i].data);
1304
1305 free(view->line);
1306 view->line = NULL;
1307 }
1308
1309 view->start_time = time(NULL);
1310
1311 return TRUE;
1312 }
1313
1314 static struct line *
1315 realloc_lines(struct view *view, size_t line_size)
1316 {
1317 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1318
1319 if (!tmp)
1320 return NULL;
1321
1322 view->line = tmp;
1323 view->line_size = line_size;
1324 return view->line;
1325 }
1326
1327 static bool
1328 update_view(struct view *view)
1329 {
1330 char buffer[BUFSIZ];
1331 char *line;
1332 /* The number of lines to read. If too low it will cause too much
1333 * redrawing (and possible flickering), if too high responsiveness
1334 * will suffer. */
1335 unsigned long lines = view->height;
1336 int redraw_from = -1;
1337
1338 if (!view->pipe)
1339 return TRUE;
1340
1341 /* Only redraw if lines are visible. */
1342 if (view->offset + view->height >= view->lines)
1343 redraw_from = view->lines - view->offset;
1344
1345 if (!realloc_lines(view, view->lines + lines))
1346 goto alloc_error;
1347
1348 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1349 int linelen = strlen(line);
1350
1351 struct line *prev = view->lines
1352 ? &view->line[view->lines - 1]
1353 : NULL;
1354
1355 if (linelen)
1356 line[linelen - 1] = 0;
1357
1358 if (!view->ops->read(view, prev, line))
1359 goto alloc_error;
1360
1361 if (lines-- == 1)
1362 break;
1363 }
1364
1365 {
1366 int digits;
1367
1368 lines = view->lines;
1369 for (digits = 0; lines; digits++)
1370 lines /= 10;
1371
1372 /* Keep the displayed view in sync with line number scaling. */
1373 if (digits != view->digits) {
1374 view->digits = digits;
1375 redraw_from = 0;
1376 }
1377 }
1378
1379 if (redraw_from >= 0) {
1380 /* If this is an incremental update, redraw the previous line
1381 * since for commits some members could have changed when
1382 * loading the main view. */
1383 if (redraw_from > 0)
1384 redraw_from--;
1385
1386 /* Incrementally draw avoids flickering. */
1387 redraw_view_from(view, redraw_from);
1388 }
1389
1390 /* Update the title _after_ the redraw so that if the redraw picks up a
1391 * commit reference in view->ref it'll be available here. */
1392 update_view_title(view);
1393
1394 if (ferror(view->pipe)) {
1395 report("Failed to read: %s", strerror(errno));
1396 goto end;
1397
1398 } else if (feof(view->pipe)) {
1399 report("");
1400 goto end;
1401 }
1402
1403 return TRUE;
1404
1405 alloc_error:
1406 report("Allocation failure");
1407
1408 end:
1409 end_update(view);
1410 return FALSE;
1411 }
1412
1413 enum open_flags {
1414 OPEN_DEFAULT = 0, /* Use default view switching. */
1415 OPEN_SPLIT = 1, /* Split current view. */
1416 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1417 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1418 };
1419
1420 static void
1421 open_view(struct view *prev, enum request request, enum open_flags flags)
1422 {
1423 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1424 bool split = !!(flags & OPEN_SPLIT);
1425 bool reload = !!(flags & OPEN_RELOAD);
1426 struct view *view = VIEW(request);
1427 int nviews = displayed_views();
1428 struct view *base_view = display[0];
1429
1430 if (view == prev && nviews == 1 && !reload) {
1431 report("Already in %s view", view->name);
1432 return;
1433 }
1434
1435 if ((reload || strcmp(view->vid, view->id)) &&
1436 !begin_update(view)) {
1437 report("Failed to load %s view", view->name);
1438 return;
1439 }
1440
1441 if (split) {
1442 display[1] = view;
1443 if (!backgrounded)
1444 current_view = 1;
1445 } else {
1446 /* Maximize the current view. */
1447 memset(display, 0, sizeof(display));
1448 current_view = 0;
1449 display[current_view] = view;
1450 }
1451
1452 /* Resize the view when switching between split- and full-screen,
1453 * or when switching between two different full-screen views. */
1454 if (nviews != displayed_views() ||
1455 (nviews == 1 && base_view != display[0]))
1456 resize_display();
1457
1458 if (split && prev->lineno - prev->offset >= prev->height) {
1459 /* Take the title line into account. */
1460 int lines = prev->lineno - prev->offset - prev->height + 1;
1461
1462 /* Scroll the view that was split if the current line is
1463 * outside the new limited view. */
1464 do_scroll_view(prev, lines, TRUE);
1465 }
1466
1467 if (prev && view != prev) {
1468 if (split && !backgrounded) {
1469 /* "Blur" the previous view. */
1470 update_view_title(prev);
1471 }
1472
1473 view->parent = prev;
1474 }
1475
1476 if (view == VIEW(REQ_VIEW_HELP))
1477 load_help_page();
1478
1479 if (view->pipe && view->lines == 0) {
1480 /* Clear the old view and let the incremental updating refill
1481 * the screen. */
1482 wclear(view->win);
1483 report("");
1484 } else {
1485 redraw_view(view);
1486 report("");
1487 }
1488
1489 /* If the view is backgrounded the above calls to report()
1490 * won't redraw the view title. */
1491 if (backgrounded)
1492 update_view_title(view);
1493 }
1494
1495
1496 /*
1497 * User request switch noodle
1498 */
1499
1500 static int
1501 view_driver(struct view *view, enum request request)
1502 {
1503 int i;
1504
1505 switch (request) {
1506 case REQ_MOVE_UP:
1507 case REQ_MOVE_DOWN:
1508 case REQ_MOVE_PAGE_UP:
1509 case REQ_MOVE_PAGE_DOWN:
1510 case REQ_MOVE_FIRST_LINE:
1511 case REQ_MOVE_LAST_LINE:
1512 move_view(view, request, TRUE);
1513 break;
1514
1515 case REQ_SCROLL_LINE_DOWN:
1516 case REQ_SCROLL_LINE_UP:
1517 case REQ_SCROLL_PAGE_DOWN:
1518 case REQ_SCROLL_PAGE_UP:
1519 scroll_view(view, request);
1520 break;
1521
1522 case REQ_VIEW_MAIN:
1523 case REQ_VIEW_DIFF:
1524 case REQ_VIEW_LOG:
1525 case REQ_VIEW_HELP:
1526 case REQ_VIEW_PAGER:
1527 open_view(view, request, OPEN_DEFAULT);
1528 break;
1529
1530 case REQ_NEXT:
1531 case REQ_PREVIOUS:
1532 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1533
1534 if (view == VIEW(REQ_VIEW_DIFF) &&
1535 view->parent == VIEW(REQ_VIEW_MAIN)) {
1536 bool redraw = display[1] == view;
1537
1538 view = view->parent;
1539 move_view(view, request, redraw);
1540 if (redraw)
1541 update_view_title(view);
1542 } else {
1543 move_view(view, request, TRUE);
1544 break;
1545 }
1546 /* Fall-through */
1547
1548 case REQ_ENTER:
1549 if (!view->lines) {
1550 report("Nothing to enter");
1551 break;
1552 }
1553 return view->ops->enter(view, &view->line[view->lineno]);
1554
1555 case REQ_VIEW_NEXT:
1556 {
1557 int nviews = displayed_views();
1558 int next_view = (current_view + 1) % nviews;
1559
1560 if (next_view == current_view) {
1561 report("Only one view is displayed");
1562 break;
1563 }
1564
1565 current_view = next_view;
1566 /* Blur out the title of the previous view. */
1567 update_view_title(view);
1568 report("");
1569 break;
1570 }
1571 case REQ_TOGGLE_LINENO:
1572 opt_line_number = !opt_line_number;
1573 redraw_display();
1574 break;
1575
1576 case REQ_PROMPT:
1577 /* Always reload^Wrerun commands from the prompt. */
1578 open_view(view, opt_request, OPEN_RELOAD);
1579 break;
1580
1581 case REQ_STOP_LOADING:
1582 for (i = 0; i < ARRAY_SIZE(views); i++) {
1583 view = &views[i];
1584 if (view->pipe)
1585 report("Stopped loading the %s view", view->name),
1586 end_update(view);
1587 }
1588 break;
1589
1590 case REQ_SHOW_VERSION:
1591 report("%s (built %s)", VERSION, __DATE__);
1592 return TRUE;
1593
1594 case REQ_SCREEN_RESIZE:
1595 resize_display();
1596 /* Fall-through */
1597 case REQ_SCREEN_REDRAW:
1598 redraw_display();
1599 break;
1600
1601 case REQ_SCREEN_UPDATE:
1602 doupdate();
1603 return TRUE;
1604
1605 case REQ_VIEW_CLOSE:
1606 /* XXX: Mark closed views by letting view->parent point to the
1607 * view itself. Parents to closed view should never be
1608 * followed. */
1609 if (view->parent &&
1610 view->parent->parent != view->parent) {
1611 memset(display, 0, sizeof(display));
1612 current_view = 0;
1613 display[current_view] = view->parent;
1614 view->parent = view;
1615 resize_display();
1616 redraw_display();
1617 break;
1618 }
1619 /* Fall-through */
1620 case REQ_QUIT:
1621 return FALSE;
1622
1623 default:
1624 /* An unknown key will show most commonly used commands. */
1625 report("Unknown key, press 'h' for help");
1626 return TRUE;
1627 }
1628
1629 return TRUE;
1630 }
1631
1632
1633 /*
1634 * Pager backend
1635 */
1636
1637 static bool
1638 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1639 {
1640 char *text = line->data;
1641 enum line_type type = line->type;
1642 int textlen = strlen(text);
1643 int attr;
1644
1645 wmove(view->win, lineno, 0);
1646
1647 if (view->offset + lineno == view->lineno) {
1648 if (type == LINE_COMMIT) {
1649 string_copy(view->ref, text + 7);
1650 string_copy(ref_commit, view->ref);
1651 }
1652
1653 type = LINE_CURSOR;
1654 wchgat(view->win, -1, 0, type, NULL);
1655 }
1656
1657 attr = get_line_attr(type);
1658 wattrset(view->win, attr);
1659
1660 if (opt_line_number || opt_tab_size < TABSIZE) {
1661 static char spaces[] = " ";
1662 int col_offset = 0, col = 0;
1663
1664 if (opt_line_number) {
1665 unsigned long real_lineno = view->offset + lineno + 1;
1666
1667 if (real_lineno == 1 ||
1668 (real_lineno % opt_num_interval) == 0) {
1669 wprintw(view->win, "%.*d", view->digits, real_lineno);
1670
1671 } else {
1672 waddnstr(view->win, spaces,
1673 MIN(view->digits, STRING_SIZE(spaces)));
1674 }
1675 waddstr(view->win, ": ");
1676 col_offset = view->digits + 2;
1677 }
1678
1679 while (text && col_offset + col < view->width) {
1680 int cols_max = view->width - col_offset - col;
1681 char *pos = text;
1682 int cols;
1683
1684 if (*text == '\t') {
1685 text++;
1686 assert(sizeof(spaces) > TABSIZE);
1687 pos = spaces;
1688 cols = opt_tab_size - (col % opt_tab_size);
1689
1690 } else {
1691 text = strchr(text, '\t');
1692 cols = line ? text - pos : strlen(pos);
1693 }
1694
1695 waddnstr(view->win, pos, MIN(cols, cols_max));
1696 col += cols;
1697 }
1698
1699 } else {
1700 int col = 0, pos = 0;
1701
1702 for (; pos < textlen && col < view->width; pos++, col++)
1703 if (text[pos] == '\t')
1704 col += TABSIZE - (col % TABSIZE) - 1;
1705
1706 waddnstr(view->win, text, pos);
1707 }
1708
1709 return TRUE;
1710 }
1711
1712 static void
1713 add_pager_refs(struct view *view, struct line *line)
1714 {
1715 char buf[1024];
1716 char *data = line->data;
1717 struct ref **refs;
1718 int bufpos = 0, refpos = 0;
1719 const char *sep = "Refs: ";
1720
1721 assert(line->type == LINE_COMMIT);
1722
1723 refs = get_refs(data + STRING_SIZE("commit "));
1724 if (!refs)
1725 return;
1726
1727 do {
1728 char *begin = "", *end = "";
1729
1730 if (refs[refpos]->tag) {
1731 begin = "[";
1732 end = "]";
1733 }
1734
1735 bufpos += snprintf(buf + bufpos, sizeof(buf) - bufpos,
1736 "%s%s%s%s", sep, begin, refs[refpos]->name,
1737 end);
1738 if (bufpos >= sizeof(buf))
1739 break;
1740 sep = ", ";
1741 } while (refs[refpos++]->next);
1742
1743 if (!bufpos ||
1744 bufpos >= sizeof(buf) ||
1745 !realloc_lines(view, view->line_size + 1))
1746 return;
1747
1748 line = &view->line[view->lines];
1749 line->data = strdup(buf);
1750 if (!line->data)
1751 return;
1752
1753 line->type = LINE_PP_REFS;
1754 view->lines++;
1755 }
1756
1757 static bool
1758 pager_read(struct view *view, struct line *prev, char *data)
1759 {
1760 struct line *line = &view->line[view->lines];
1761
1762 line->data = strdup(data);
1763 if (!line->data)
1764 return FALSE;
1765
1766 line->type = get_line_type(line->data);
1767 view->lines++;
1768
1769 if (line->type == LINE_COMMIT &&
1770 (view == VIEW(REQ_VIEW_DIFF) ||
1771 view == VIEW(REQ_VIEW_LOG)))
1772 add_pager_refs(view, line);
1773
1774 return TRUE;
1775 }
1776
1777 static bool
1778 pager_enter(struct view *view, struct line *line)
1779 {
1780 int split = 0;
1781
1782 if (line->type == LINE_COMMIT &&
1783 (view == VIEW(REQ_VIEW_LOG) ||
1784 view == VIEW(REQ_VIEW_PAGER))) {
1785 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1786 split = 1;
1787 }
1788
1789 /* Always scroll the view even if it was split. That way
1790 * you can use Enter to scroll through the log view and
1791 * split open each commit diff. */
1792 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1793
1794 /* FIXME: A minor workaround. Scrolling the view will call report("")
1795 * but if we are scrolling a non-current view this won't properly
1796 * update the view title. */
1797 if (split)
1798 update_view_title(view);
1799
1800 return TRUE;
1801 }
1802
1803 static struct view_ops pager_ops = {
1804 "line",
1805 pager_draw,
1806 pager_read,
1807 pager_enter,
1808 };
1809
1810
1811 /*
1812 * Main view backend
1813 */
1814
1815 struct commit {
1816 char id[41]; /* SHA1 ID. */
1817 char title[75]; /* The first line of the commit message. */
1818 char author[75]; /* The author of the commit. */
1819 struct tm time; /* Date from the author ident. */
1820 struct ref **refs; /* Repository references; tags & branch heads. */
1821 };
1822
1823 static bool
1824 main_draw(struct view *view, struct line *line, unsigned int lineno)
1825 {
1826 char buf[DATE_COLS + 1];
1827 struct commit *commit = line->data;
1828 enum line_type type;
1829 int col = 0;
1830 size_t timelen;
1831 size_t authorlen;
1832 int trimmed = 1;
1833
1834 if (!*commit->author)
1835 return FALSE;
1836
1837 wmove(view->win, lineno, col);
1838
1839 if (view->offset + lineno == view->lineno) {
1840 string_copy(view->ref, commit->id);
1841 string_copy(ref_commit, view->ref);
1842 type = LINE_CURSOR;
1843 wattrset(view->win, get_line_attr(type));
1844 wchgat(view->win, -1, 0, type, NULL);
1845
1846 } else {
1847 type = LINE_MAIN_COMMIT;
1848 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1849 }
1850
1851 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1852 waddnstr(view->win, buf, timelen);
1853 waddstr(view->win, " ");
1854
1855 col += DATE_COLS;
1856 wmove(view->win, lineno, col);
1857 if (type != LINE_CURSOR)
1858 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1859
1860 if (opt_utf8) {
1861 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1862 } else {
1863 authorlen = strlen(commit->author);
1864 if (authorlen > AUTHOR_COLS - 2) {
1865 authorlen = AUTHOR_COLS - 2;
1866 trimmed = 1;
1867 }
1868 }
1869
1870 if (trimmed) {
1871 waddnstr(view->win, commit->author, authorlen);
1872 if (type != LINE_CURSOR)
1873 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1874 waddch(view->win, '~');
1875 } else {
1876 waddstr(view->win, commit->author);
1877 }
1878
1879 col += AUTHOR_COLS;
1880 if (type != LINE_CURSOR)
1881 wattrset(view->win, A_NORMAL);
1882
1883 mvwaddch(view->win, lineno, col, ACS_LTEE);
1884 wmove(view->win, lineno, col + 2);
1885 col += 2;
1886
1887 if (commit->refs) {
1888 size_t i = 0;
1889
1890 do {
1891 if (type == LINE_CURSOR)
1892 ;
1893 else if (commit->refs[i]->tag)
1894 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1895 else
1896 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1897 waddstr(view->win, "[");
1898 waddstr(view->win, commit->refs[i]->name);
1899 waddstr(view->win, "]");
1900 if (type != LINE_CURSOR)
1901 wattrset(view->win, A_NORMAL);
1902 waddstr(view->win, " ");
1903 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
1904 } while (commit->refs[i++]->next);
1905 }
1906
1907 if (type != LINE_CURSOR)
1908 wattrset(view->win, get_line_attr(type));
1909
1910 {
1911 int titlelen = strlen(commit->title);
1912
1913 if (col + titlelen > view->width)
1914 titlelen = view->width - col;
1915
1916 waddnstr(view->win, commit->title, titlelen);
1917 }
1918
1919 return TRUE;
1920 }
1921
1922 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1923 static bool
1924 main_read(struct view *view, struct line *prev, char *line)
1925 {
1926 enum line_type type = get_line_type(line);
1927 struct commit *commit;
1928
1929 switch (type) {
1930 case LINE_COMMIT:
1931 commit = calloc(1, sizeof(struct commit));
1932 if (!commit)
1933 return FALSE;
1934
1935 line += STRING_SIZE("commit ");
1936
1937 view->line[view->lines++].data = commit;
1938 string_copy(commit->id, line);
1939 commit->refs = get_refs(commit->id);
1940 break;
1941
1942 case LINE_AUTHOR:
1943 {
1944 char *ident = line + STRING_SIZE("author ");
1945 char *end = strchr(ident, '<');
1946
1947 if (!prev)
1948 break;
1949
1950 commit = prev->data;
1951
1952 if (end) {
1953 for (; end > ident && isspace(end[-1]); end--) ;
1954 *end = 0;
1955 }
1956
1957 string_copy(commit->author, ident);
1958
1959 /* Parse epoch and timezone */
1960 if (end) {
1961 char *secs = strchr(end + 1, '>');
1962 char *zone;
1963 time_t time;
1964
1965 if (!secs || secs[1] != ' ')
1966 break;
1967
1968 secs += 2;
1969 time = (time_t) atol(secs);
1970 zone = strchr(secs, ' ');
1971 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1972 long tz;
1973
1974 zone++;
1975 tz = ('0' - zone[1]) * 60 * 60 * 10;
1976 tz += ('0' - zone[2]) * 60 * 60;
1977 tz += ('0' - zone[3]) * 60;
1978 tz += ('0' - zone[4]) * 60;
1979
1980 if (zone[0] == '-')
1981 tz = -tz;
1982
1983 time -= tz;
1984 }
1985 gmtime_r(&time, &commit->time);
1986 }
1987 break;
1988 }
1989 default:
1990 if (!prev)
1991 break;
1992
1993 commit = prev->data;
1994
1995 /* Fill in the commit title if it has not already been set. */
1996 if (commit->title[0])
1997 break;
1998
1999 /* Require titles to start with a non-space character at the
2000 * offset used by git log. */
2001 /* FIXME: More gracefull handling of titles; append "..." to
2002 * shortened titles, etc. */
2003 if (strncmp(line, " ", 4) ||
2004 isspace(line[4]))
2005 break;
2006
2007 string_copy(commit->title, line + 4);
2008 }
2009
2010 return TRUE;
2011 }
2012
2013 static bool
2014 main_enter(struct view *view, struct line *line)
2015 {
2016 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2017
2018 open_view(view, REQ_VIEW_DIFF, flags);
2019 return TRUE;
2020 }
2021
2022 static struct view_ops main_ops = {
2023 "commit",
2024 main_draw,
2025 main_read,
2026 main_enter,
2027 };
2028
2029
2030 /*
2031 * Keys
2032 */
2033
2034 struct keymap {
2035 int alias;
2036 int request;
2037 };
2038
2039 static struct keymap keymap[] = {
2040 /* View switching */
2041 { 'm', REQ_VIEW_MAIN },
2042 { 'd', REQ_VIEW_DIFF },
2043 { 'l', REQ_VIEW_LOG },
2044 { 'p', REQ_VIEW_PAGER },
2045 { 'h', REQ_VIEW_HELP },
2046 { '?', REQ_VIEW_HELP },
2047
2048 /* View manipulation */
2049 { 'q', REQ_VIEW_CLOSE },
2050 { KEY_TAB, REQ_VIEW_NEXT },
2051 { KEY_RETURN, REQ_ENTER },
2052 { KEY_UP, REQ_PREVIOUS },
2053 { KEY_DOWN, REQ_NEXT },
2054
2055 /* Cursor navigation */
2056 { 'k', REQ_MOVE_UP },
2057 { 'j', REQ_MOVE_DOWN },
2058 { KEY_HOME, REQ_MOVE_FIRST_LINE },
2059 { KEY_END, REQ_MOVE_LAST_LINE },
2060 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
2061 { ' ', REQ_MOVE_PAGE_DOWN },
2062 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
2063 { 'b', REQ_MOVE_PAGE_UP },
2064 { '-', REQ_MOVE_PAGE_UP },
2065
2066 /* Scrolling */
2067 { KEY_IC, REQ_SCROLL_LINE_UP },
2068 { KEY_DC, REQ_SCROLL_LINE_DOWN },
2069 { 'w', REQ_SCROLL_PAGE_UP },
2070 { 's', REQ_SCROLL_PAGE_DOWN },
2071
2072 /* Misc */
2073 { 'Q', REQ_QUIT },
2074 { 'z', REQ_STOP_LOADING },
2075 { 'v', REQ_SHOW_VERSION },
2076 { 'r', REQ_SCREEN_REDRAW },
2077 { 'n', REQ_TOGGLE_LINENO },
2078 { ':', REQ_PROMPT },
2079
2080 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2081 { ERR, REQ_SCREEN_UPDATE },
2082
2083 /* Use the ncurses SIGWINCH handler. */
2084 { KEY_RESIZE, REQ_SCREEN_RESIZE },
2085 };
2086
2087 static enum request
2088 get_request(int key)
2089 {
2090 int i;
2091
2092 for (i = 0; i < ARRAY_SIZE(keymap); i++)
2093 if (keymap[i].alias == key)
2094 return keymap[i].request;
2095
2096 return (enum request) key;
2097 }
2098
2099 struct key {
2100 char *name;
2101 int value;
2102 };
2103
2104 static struct key key_table[] = {
2105 { "Enter", KEY_RETURN },
2106 { "Space", ' ' },
2107 { "Backspace", KEY_BACKSPACE },
2108 { "Tab", KEY_TAB },
2109 { "Escape", KEY_ESC },
2110 { "Left", KEY_LEFT },
2111 { "Right", KEY_RIGHT },
2112 { "Up", KEY_UP },
2113 { "Down", KEY_DOWN },
2114 { "Insert", KEY_IC },
2115 { "Delete", KEY_DC },
2116 { "Home", KEY_HOME },
2117 { "End", KEY_END },
2118 { "PageUp", KEY_PPAGE },
2119 { "PageDown", KEY_NPAGE },
2120 { "F1", KEY_F(1) },
2121 { "F2", KEY_F(2) },
2122 { "F3", KEY_F(3) },
2123 { "F4", KEY_F(4) },
2124 { "F5", KEY_F(5) },
2125 { "F6", KEY_F(6) },
2126 { "F7", KEY_F(7) },
2127 { "F8", KEY_F(8) },
2128 { "F9", KEY_F(9) },
2129 { "F10", KEY_F(10) },
2130 { "F11", KEY_F(11) },
2131 { "F12", KEY_F(12) },
2132 };
2133
2134 static char *
2135 get_key(enum request request)
2136 {
2137 static char buf[BUFSIZ];
2138 static char key_char[] = "'X'";
2139 int pos = 0;
2140 char *sep = " ";
2141 int i;
2142
2143 buf[pos] = 0;
2144
2145 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2146 char *seq = NULL;
2147 int key;
2148
2149 if (keymap[i].request != request)
2150 continue;
2151
2152 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2153 if (key_table[key].value == keymap[i].alias)
2154 seq = key_table[key].name;
2155
2156 if (seq == NULL &&
2157 keymap[i].alias < 127 &&
2158 isprint(keymap[i].alias)) {
2159 key_char[1] = (char) keymap[i].alias;
2160 seq = key_char;
2161 }
2162
2163 if (!seq)
2164 seq = "'?'";
2165
2166 pos += snprintf(buf + pos, sizeof(buf) - pos, "%s%s", sep, seq);
2167 if (pos >= sizeof(buf))
2168 return "Too many keybindings!";
2169 sep = ", ";
2170 }
2171
2172 return buf;
2173 }
2174
2175 static void load_help_page(void)
2176 {
2177 char buf[BUFSIZ];
2178 struct view *view = VIEW(REQ_VIEW_HELP);
2179 int lines = ARRAY_SIZE(req_info) + 2;
2180 int i;
2181
2182 if (view->lines > 0)
2183 return;
2184
2185 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2186 if (!req_info[i].request)
2187 lines++;
2188
2189 view->line = calloc(lines, sizeof(*view->line));
2190 if (!view->line) {
2191 report("Allocation failure");
2192 return;
2193 }
2194
2195 pager_read(view, NULL, "Quick reference for tig keybindings:");
2196
2197 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2198 char *key;
2199
2200 if (!req_info[i].request) {
2201 pager_read(view, NULL, "");
2202 pager_read(view, NULL, req_info[i].help);
2203 continue;
2204 }
2205
2206 key = get_key(req_info[i].request);
2207 if (snprintf(buf, sizeof(buf), "%-25s %s", key, req_info[i].help)
2208 >= sizeof(buf))
2209 continue;
2210
2211 pager_read(view, NULL, buf);
2212 }
2213 }
2214
2215
2216 /*
2217 * Unicode / UTF-8 handling
2218 *
2219 * NOTE: Much of the following code for dealing with unicode is derived from
2220 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2221 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2222 */
2223
2224 /* I've (over)annotated a lot of code snippets because I am not entirely
2225 * confident that the approach taken by this small UTF-8 interface is correct.
2226 * --jonas */
2227
2228 static inline int
2229 unicode_width(unsigned long c)
2230 {
2231 if (c >= 0x1100 &&
2232 (c <= 0x115f /* Hangul Jamo */
2233 || c == 0x2329
2234 || c == 0x232a
2235 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2236 /* CJK ... Yi */
2237 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2238 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2239 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2240 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2241 || (c >= 0xffe0 && c <= 0xffe6)
2242 || (c >= 0x20000 && c <= 0x2fffd)
2243 || (c >= 0x30000 && c <= 0x3fffd)))
2244 return 2;
2245
2246 return 1;
2247 }
2248
2249 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2250 * Illegal bytes are set one. */
2251 static const unsigned char utf8_bytes[256] = {
2252 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2253 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2254 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2255 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2256 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2257 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
2258 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,
2259 3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4, 5,5,5,5,6,6,1,1,
2260 };
2261
2262 /* Decode UTF-8 multi-byte representation into a unicode character. */
2263 static inline unsigned long
2264 utf8_to_unicode(const char *string, size_t length)
2265 {
2266 unsigned long unicode;
2267
2268 switch (length) {
2269 case 1:
2270 unicode = string[0];
2271 break;
2272 case 2:
2273 unicode = (string[0] & 0x1f) << 6;
2274 unicode += (string[1] & 0x3f);
2275 break;
2276 case 3:
2277 unicode = (string[0] & 0x0f) << 12;
2278 unicode += ((string[1] & 0x3f) << 6);
2279 unicode += (string[2] & 0x3f);
2280 break;
2281 case 4:
2282 unicode = (string[0] & 0x0f) << 18;
2283 unicode += ((string[1] & 0x3f) << 12);
2284 unicode += ((string[2] & 0x3f) << 6);
2285 unicode += (string[3] & 0x3f);
2286 break;
2287 case 5:
2288 unicode = (string[0] & 0x0f) << 24;
2289 unicode += ((string[1] & 0x3f) << 18);
2290 unicode += ((string[2] & 0x3f) << 12);
2291 unicode += ((string[3] & 0x3f) << 6);
2292 unicode += (string[4] & 0x3f);
2293 break;
2294 case 6:
2295 unicode = (string[0] & 0x01) << 30;
2296 unicode += ((string[1] & 0x3f) << 24);
2297 unicode += ((string[2] & 0x3f) << 18);
2298 unicode += ((string[3] & 0x3f) << 12);
2299 unicode += ((string[4] & 0x3f) << 6);
2300 unicode += (string[5] & 0x3f);
2301 break;
2302 default:
2303 die("Invalid unicode length");
2304 }
2305
2306 /* Invalid characters could return the special 0xfffd value but NUL
2307 * should be just as good. */
2308 return unicode > 0xffff ? 0 : unicode;
2309 }
2310
2311 /* Calculates how much of string can be shown within the given maximum width
2312 * and sets trimmed parameter to non-zero value if all of string could not be
2313 * shown.
2314 *
2315 * Additionally, adds to coloffset how many many columns to move to align with
2316 * the expected position. Takes into account how multi-byte and double-width
2317 * characters will effect the cursor position.
2318 *
2319 * Returns the number of bytes to output from string to satisfy max_width. */
2320 static size_t
2321 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2322 {
2323 const char *start = string;
2324 const char *end = strchr(string, '\0');
2325 size_t mbwidth = 0;
2326 size_t width = 0;
2327
2328 *trimmed = 0;
2329
2330 while (string < end) {
2331 int c = *(unsigned char *) string;
2332 unsigned char bytes = utf8_bytes[c];
2333 size_t ucwidth;
2334 unsigned long unicode;
2335
2336 if (string + bytes > end)
2337 break;
2338
2339 /* Change representation to figure out whether
2340 * it is a single- or double-width character. */
2341
2342 unicode = utf8_to_unicode(string, bytes);
2343 /* FIXME: Graceful handling of invalid unicode character. */
2344 if (!unicode)
2345 break;
2346
2347 ucwidth = unicode_width(unicode);
2348 width += ucwidth;
2349 if (width > max_width) {
2350 *trimmed = 1;
2351 break;
2352 }
2353
2354 /* The column offset collects the differences between the
2355 * number of bytes encoding a character and the number of
2356 * columns will be used for rendering said character.
2357 *
2358 * So if some character A is encoded in 2 bytes, but will be
2359 * represented on the screen using only 1 byte this will and up
2360 * adding 1 to the multi-byte column offset.
2361 *
2362 * Assumes that no double-width character can be encoding in
2363 * less than two bytes. */
2364 if (bytes > ucwidth)
2365 mbwidth += bytes - ucwidth;
2366
2367 string += bytes;
2368 }
2369
2370 *coloffset += mbwidth;
2371
2372 return string - start;
2373 }
2374
2375
2376 /*
2377 * Status management
2378 */
2379
2380 /* Whether or not the curses interface has been initialized. */
2381 static bool cursed = FALSE;
2382
2383 /* The status window is used for polling keystrokes. */
2384 static WINDOW *status_win;
2385
2386 /* Update status and title window. */
2387 static void
2388 report(const char *msg, ...)
2389 {
2390 static bool empty = TRUE;
2391 struct view *view = display[current_view];
2392
2393 if (!empty || *msg) {
2394 va_list args;
2395
2396 va_start(args, msg);
2397
2398 werase(status_win);
2399 wmove(status_win, 0, 0);
2400 if (*msg) {
2401 vwprintw(status_win, msg, args);
2402 empty = FALSE;
2403 } else {
2404 empty = TRUE;
2405 }
2406 wrefresh(status_win);
2407
2408 va_end(args);
2409 }
2410
2411 update_view_title(view);
2412 update_display_cursor();
2413 }
2414
2415 /* Controls when nodelay should be in effect when polling user input. */
2416 static void
2417 set_nonblocking_input(bool loading)
2418 {
2419 static unsigned int loading_views;
2420
2421 if ((loading == FALSE && loading_views-- == 1) ||
2422 (loading == TRUE && loading_views++ == 0))
2423 nodelay(status_win, loading);
2424 }
2425
2426 static void
2427 init_display(void)
2428 {
2429 int x, y;
2430
2431 /* Initialize the curses library */
2432 if (isatty(STDIN_FILENO)) {
2433 cursed = !!initscr();
2434 } else {
2435 /* Leave stdin and stdout alone when acting as a pager. */
2436 FILE *io = fopen("/dev/tty", "r+");
2437
2438 cursed = !!newterm(NULL, io, io);
2439 }
2440
2441 if (!cursed)
2442 die("Failed to initialize curses");
2443
2444 nonl(); /* Tell curses not to do NL->CR/NL on output */
2445 cbreak(); /* Take input chars one at a time, no wait for \n */
2446 noecho(); /* Don't echo input */
2447 leaveok(stdscr, TRUE);
2448
2449 if (has_colors())
2450 init_colors();
2451
2452 getmaxyx(stdscr, y, x);
2453 status_win = newwin(1, 0, y - 1, 0);
2454 if (!status_win)
2455 die("Failed to create status window");
2456
2457 /* Enable keyboard mapping */
2458 keypad(status_win, TRUE);
2459 wbkgdset(status_win, get_line_attr(LINE_STATUS));
2460 }
2461
2462
2463 /*
2464 * Repository references
2465 */
2466
2467 static struct ref *refs;
2468 static size_t refs_size;
2469
2470 /* Id <-> ref store */
2471 static struct ref ***id_refs;
2472 static size_t id_refs_size;
2473
2474 static struct ref **
2475 get_refs(char *id)
2476 {
2477 struct ref ***tmp_id_refs;
2478 struct ref **ref_list = NULL;
2479 size_t ref_list_size = 0;
2480 size_t i;
2481
2482 for (i = 0; i < id_refs_size; i++)
2483 if (!strcmp(id, id_refs[i][0]->id))
2484 return id_refs[i];
2485
2486 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2487 if (!tmp_id_refs)
2488 return NULL;
2489
2490 id_refs = tmp_id_refs;
2491
2492 for (i = 0; i < refs_size; i++) {
2493 struct ref **tmp;
2494
2495 if (strcmp(id, refs[i].id))
2496 continue;
2497
2498 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
2499 if (!tmp) {
2500 if (ref_list)
2501 free(ref_list);
2502 return NULL;
2503 }
2504
2505 ref_list = tmp;
2506 if (ref_list_size > 0)
2507 ref_list[ref_list_size - 1]->next = 1;
2508 ref_list[ref_list_size] = &refs[i];
2509
2510 /* XXX: The properties of the commit chains ensures that we can
2511 * safely modify the shared ref. The repo references will
2512 * always be similar for the same id. */
2513 ref_list[ref_list_size]->next = 0;
2514 ref_list_size++;
2515 }
2516
2517 if (ref_list)
2518 id_refs[id_refs_size++] = ref_list;
2519
2520 return ref_list;
2521 }
2522
2523 static int
2524 read_ref(char *id, int idlen, char *name, int namelen)
2525 {
2526 struct ref *ref;
2527 bool tag = FALSE;
2528 bool tag_commit = FALSE;
2529
2530 /* Commits referenced by tags has "^{}" appended. */
2531 if (name[namelen - 1] == '}') {
2532 while (namelen > 0 && name[namelen] != '^')
2533 namelen--;
2534 if (namelen > 0)
2535 tag_commit = TRUE;
2536 name[namelen] = 0;
2537 }
2538
2539 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2540 if (!tag_commit)
2541 return OK;
2542 name += STRING_SIZE("refs/tags/");
2543 tag = TRUE;
2544
2545 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2546 name += STRING_SIZE("refs/heads/");
2547
2548 } else if (!strcmp(name, "HEAD")) {
2549 return OK;
2550 }
2551
2552 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2553 if (!refs)
2554 return ERR;
2555
2556 ref = &refs[refs_size++];
2557 ref->name = strdup(name);
2558 if (!ref->name)
2559 return ERR;
2560
2561 ref->tag = tag;
2562 string_copy(ref->id, id);
2563
2564 return OK;
2565 }
2566
2567 static int
2568 load_refs(void)
2569 {
2570 const char *cmd_env = getenv("TIG_LS_REMOTE");
2571 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2572
2573 return read_properties(popen(cmd, "r"), "\t", read_ref);
2574 }
2575
2576 static int
2577 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2578 {
2579 if (!strcmp(name, "i18n.commitencoding")) {
2580 string_copy(opt_encoding, value);
2581 }
2582
2583 return OK;
2584 }
2585
2586 static int
2587 load_repo_config(void)
2588 {
2589 return read_properties(popen("git repo-config --list", "r"),
2590 "=", read_repo_config_option);
2591 }
2592
2593 static int
2594 read_properties(FILE *pipe, const char *separators,
2595 int (*read_property)(char *, int, char *, int))
2596 {
2597 char buffer[BUFSIZ];
2598 char *name;
2599 int state = OK;
2600
2601 if (!pipe)
2602 return ERR;
2603
2604 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2605 char *value;
2606 size_t namelen;
2607 size_t valuelen;
2608
2609 name = chomp_string(name);
2610 namelen = strcspn(name, separators);
2611
2612 if (name[namelen]) {
2613 name[namelen] = 0;
2614 value = chomp_string(name + namelen + 1);
2615 valuelen = strlen(value);
2616
2617 } else {
2618 value = "";
2619 valuelen = 0;
2620 }
2621
2622 state = read_property(name, namelen, value, valuelen);
2623 }
2624
2625 if (state != ERR && ferror(pipe))
2626 state = ERR;
2627
2628 pclose(pipe);
2629
2630 return state;
2631 }
2632
2633
2634 /*
2635 * Main
2636 */
2637
2638 #if __GNUC__ >= 3
2639 #define __NORETURN __attribute__((__noreturn__))
2640 #else
2641 #define __NORETURN
2642 #endif
2643
2644 static void __NORETURN
2645 quit(int sig)
2646 {
2647 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2648 if (cursed)
2649 endwin();
2650 exit(0);
2651 }
2652
2653 static void __NORETURN
2654 die(const char *err, ...)
2655 {
2656 va_list args;
2657
2658 endwin();
2659
2660 va_start(args, err);
2661 fputs("tig: ", stderr);
2662 vfprintf(stderr, err, args);
2663 fputs("\n", stderr);
2664 va_end(args);
2665
2666 exit(1);
2667 }
2668
2669 int
2670 main(int argc, char *argv[])
2671 {
2672 struct view *view;
2673 enum request request;
2674 size_t i;
2675
2676 signal(SIGINT, quit);
2677
2678 if (load_options() == ERR)
2679 die("Failed to load user config.");
2680
2681 /* Load the repo config file so options can be overwritten from
2682 * the command line. */
2683 if (load_repo_config() == ERR)
2684 die("Failed to load repo config.");
2685
2686 if (!parse_options(argc, argv))
2687 return 0;
2688
2689 if (load_refs() == ERR)
2690 die("Failed to load refs.");
2691
2692 /* Require a git repository unless when running in pager mode. */
2693 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2694 die("Not a git repository");
2695
2696 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2697 view->cmd_env = getenv(view->cmd_env);
2698
2699 request = opt_request;
2700
2701 init_display();
2702
2703 while (view_driver(display[current_view], request)) {
2704 int key;
2705 int i;
2706
2707 foreach_view (view, i)
2708 update_view(view);
2709
2710 /* Refresh, accept single keystroke of input */
2711 key = wgetch(status_win);
2712 request = get_request(key);
2713
2714 /* Some low-level request handling. This keeps access to
2715 * status_win restricted. */
2716 switch (request) {
2717 case REQ_PROMPT:
2718 report(":");
2719 /* Temporarily switch to line-oriented and echoed
2720 * input. */
2721 nocbreak();
2722 echo();
2723
2724 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2725 memcpy(opt_cmd, "git ", 4);
2726 opt_request = REQ_VIEW_PAGER;
2727 } else {
2728 report("Prompt interrupted by loading view, "
2729 "press 'z' to stop loading views");
2730 request = REQ_SCREEN_UPDATE;
2731 }
2732
2733 noecho();
2734 cbreak();
2735 break;
2736
2737 case REQ_SCREEN_RESIZE:
2738 {
2739 int height, width;
2740
2741 getmaxyx(stdscr, height, width);
2742
2743 /* Resize the status view and let the view driver take
2744 * care of resizing the displayed views. */
2745 wresize(status_win, 1, width);
2746 mvwin(status_win, height - 1, 0);
2747 wrefresh(status_win);
2748 break;
2749 }
2750 default:
2751 break;
2752 }
2753 }
2754
2755 quit(0);
2756
2757 return 0;
2758 }
2759
2760 /**
2761 * include::BUGS[]
2762 *
2763 * COPYRIGHT
2764 * ---------
2765 * Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2766 *
2767 * This program is free software; you can redistribute it and/or modify
2768 * it under the terms of the GNU General Public License as published by
2769 * the Free Software Foundation; either version 2 of the License, or
2770 * (at your option) any later version.
2771 *
2772 * SEE ALSO
2773 * --------
2774 * - link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
2775 * - link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
2776 *
2777 * Other git repository browsers:
2778 *
2779 * - gitk(1)
2780 * - qgit(1)
2781 * - gitview(1)
2782 *
2783 * Sites:
2784 *
2785 * include::SITES[]
2786 **/