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