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