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