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