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