Improve handling of remotes
[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.5.git"
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 <sys/types.h>
34 #include <regex.h>
35
36 #include <locale.h>
37 #include <langinfo.h>
38 #include <iconv.h>
39
40 #include <curses.h>
41
42 #if __GNUC__ >= 3
43 #define __NORETURN __attribute__((__noreturn__))
44 #else
45 #define __NORETURN
46 #endif
47
48 static void __NORETURN die(const char *err, ...);
49 static void report(const char *msg, ...);
50 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
51 static void set_nonblocking_input(bool loading);
52 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
53
54 #define ABS(x) ((x) >= 0 ? (x) : -(x))
55 #define MIN(x, y) ((x) < (y) ? (x) : (y))
56
57 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
58 #define STRING_SIZE(x) (sizeof(x) - 1)
59
60 #define SIZEOF_STR 1024 /* Default string size. */
61 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
62 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
63 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
64
65 /* This color name can be used to refer to the default term colors. */
66 #define COLOR_DEFAULT (-1)
67
68 #define ICONV_NONE ((iconv_t) -1)
69
70 /* The format and size of the date column in the main view. */
71 #define DATE_FORMAT "%Y-%m-%d %H:%M"
72 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
73
74 #define AUTHOR_COLS 20
75
76 /* The default interval between line numbers. */
77 #define NUMBER_INTERVAL 1
78
79 #define TABSIZE 8
80
81 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
82
83 #define TIG_LS_REMOTE \
84 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
85
86 #define TIG_DIFF_CMD \
87 "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
88
89 #define TIG_LOG_CMD \
90 "git log --cc --stat -n100 %s 2>/dev/null"
91
92 #define TIG_MAIN_CMD \
93 "git log --topo-order --pretty=raw %s 2>/dev/null"
94
95 #define TIG_TREE_CMD \
96 "git ls-tree %s %s"
97
98 #define TIG_BLOB_CMD \
99 "git cat-file blob %s"
100
101 /* XXX: Needs to be defined to the empty string. */
102 #define TIG_HELP_CMD ""
103 #define TIG_PAGER_CMD ""
104
105 /* Some ascii-shorthands fitted into the ncurses namespace. */
106 #define KEY_TAB '\t'
107 #define KEY_RETURN '\r'
108 #define KEY_ESC 27
109
110
111 struct ref {
112 char *name; /* Ref name; tag or head names are shortened. */
113 char id[SIZEOF_REV]; /* Commit SHA1 ID */
114 unsigned int tag:1; /* Is it a tag? */
115 unsigned int remote:1; /* Is it a remote ref? */
116 unsigned int next:1; /* For ref lists: are there more refs? */
117 };
118
119 static struct ref **get_refs(char *id);
120
121 struct int_map {
122 const char *name;
123 int namelen;
124 int value;
125 };
126
127 static int
128 set_from_int_map(struct int_map *map, size_t map_size,
129 int *value, const char *name, int namelen)
130 {
131
132 int i;
133
134 for (i = 0; i < map_size; i++)
135 if (namelen == map[i].namelen &&
136 !strncasecmp(name, map[i].name, namelen)) {
137 *value = map[i].value;
138 return OK;
139 }
140
141 return ERR;
142 }
143
144
145 /*
146 * String helpers
147 */
148
149 static inline void
150 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
151 {
152 if (srclen > dstlen - 1)
153 srclen = dstlen - 1;
154
155 strncpy(dst, src, srclen);
156 dst[srclen] = 0;
157 }
158
159 /* Shorthands for safely copying into a fixed buffer. */
160
161 #define string_copy(dst, src) \
162 string_ncopy_do(dst, sizeof(dst), src, sizeof(dst))
163
164 #define string_ncopy(dst, src, srclen) \
165 string_ncopy_do(dst, sizeof(dst), src, srclen)
166
167 static char *
168 chomp_string(char *name)
169 {
170 int namelen;
171
172 while (isspace(*name))
173 name++;
174
175 namelen = strlen(name) - 1;
176 while (namelen > 0 && isspace(name[namelen]))
177 name[namelen--] = 0;
178
179 return name;
180 }
181
182 static bool
183 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
184 {
185 va_list args;
186 size_t pos = bufpos ? *bufpos : 0;
187
188 va_start(args, fmt);
189 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
190 va_end(args);
191
192 if (bufpos)
193 *bufpos = pos;
194
195 return pos >= bufsize ? FALSE : TRUE;
196 }
197
198 #define string_format(buf, fmt, args...) \
199 string_nformat(buf, sizeof(buf), NULL, fmt, args)
200
201 #define string_format_from(buf, from, fmt, args...) \
202 string_nformat(buf, sizeof(buf), from, fmt, args)
203
204 static int
205 string_enum_compare(const char *str1, const char *str2, int len)
206 {
207 size_t i;
208
209 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
210
211 /* Diff-Header == DIFF_HEADER */
212 for (i = 0; i < len; i++) {
213 if (toupper(str1[i]) == toupper(str2[i]))
214 continue;
215
216 if (string_enum_sep(str1[i]) &&
217 string_enum_sep(str2[i]))
218 continue;
219
220 return str1[i] - str2[i];
221 }
222
223 return 0;
224 }
225
226 /* Shell quoting
227 *
228 * NOTE: The following is a slightly modified copy of the git project's shell
229 * quoting routines found in the quote.c file.
230 *
231 * Help to copy the thing properly quoted for the shell safety. any single
232 * quote is replaced with '\'', any exclamation point is replaced with '\!',
233 * and the whole thing is enclosed in a
234 *
235 * E.g.
236 * original sq_quote result
237 * name ==> name ==> 'name'
238 * a b ==> a b ==> 'a b'
239 * a'b ==> a'\''b ==> 'a'\''b'
240 * a!b ==> a'\!'b ==> 'a'\!'b'
241 */
242
243 static size_t
244 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
245 {
246 char c;
247
248 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
249
250 BUFPUT('\'');
251 while ((c = *src++)) {
252 if (c == '\'' || c == '!') {
253 BUFPUT('\'');
254 BUFPUT('\\');
255 BUFPUT(c);
256 BUFPUT('\'');
257 } else {
258 BUFPUT(c);
259 }
260 }
261 BUFPUT('\'');
262
263 return bufsize;
264 }
265
266
267 /*
268 * User requests
269 */
270
271 #define REQ_INFO \
272 /* XXX: Keep the view request first and in sync with views[]. */ \
273 REQ_GROUP("View switching") \
274 REQ_(VIEW_MAIN, "Show main view"), \
275 REQ_(VIEW_DIFF, "Show diff view"), \
276 REQ_(VIEW_LOG, "Show log view"), \
277 REQ_(VIEW_TREE, "Show tree view"), \
278 REQ_(VIEW_BLOB, "Show blob view"), \
279 REQ_(VIEW_HELP, "Show help page"), \
280 REQ_(VIEW_PAGER, "Show pager view"), \
281 \
282 REQ_GROUP("View manipulation") \
283 REQ_(ENTER, "Enter current line and scroll"), \
284 REQ_(NEXT, "Move to next"), \
285 REQ_(PREVIOUS, "Move to previous"), \
286 REQ_(VIEW_NEXT, "Move focus to next view"), \
287 REQ_(VIEW_CLOSE, "Close the current view"), \
288 REQ_(QUIT, "Close all views and quit"), \
289 \
290 REQ_GROUP("Cursor navigation") \
291 REQ_(MOVE_UP, "Move cursor one line up"), \
292 REQ_(MOVE_DOWN, "Move cursor one line down"), \
293 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
294 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
295 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
296 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
297 \
298 REQ_GROUP("Scrolling") \
299 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
300 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
301 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
302 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
303 \
304 REQ_GROUP("Searching") \
305 REQ_(SEARCH, "Search the view"), \
306 REQ_(SEARCH_BACK, "Search backwards in the view"), \
307 REQ_(FIND_NEXT, "Find next search match"), \
308 REQ_(FIND_PREV, "Find previous search match"), \
309 \
310 REQ_GROUP("Misc") \
311 REQ_(NONE, "Do nothing"), \
312 REQ_(PROMPT, "Bring up the prompt"), \
313 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
314 REQ_(SCREEN_RESIZE, "Resize the screen"), \
315 REQ_(SHOW_VERSION, "Show version information"), \
316 REQ_(STOP_LOADING, "Stop all loading views"), \
317 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
318 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization")
319
320
321 /* User action requests. */
322 enum request {
323 #define REQ_GROUP(help)
324 #define REQ_(req, help) REQ_##req
325
326 /* Offset all requests to avoid conflicts with ncurses getch values. */
327 REQ_OFFSET = KEY_MAX + 1,
328 REQ_INFO,
329 REQ_UNKNOWN,
330
331 #undef REQ_GROUP
332 #undef REQ_
333 };
334
335 struct request_info {
336 enum request request;
337 char *name;
338 int namelen;
339 char *help;
340 };
341
342 static struct request_info req_info[] = {
343 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
344 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
345 REQ_INFO
346 #undef REQ_GROUP
347 #undef REQ_
348 };
349
350 static enum request
351 get_request(const char *name)
352 {
353 int namelen = strlen(name);
354 int i;
355
356 for (i = 0; i < ARRAY_SIZE(req_info); i++)
357 if (req_info[i].namelen == namelen &&
358 !string_enum_compare(req_info[i].name, name, namelen))
359 return req_info[i].request;
360
361 return REQ_UNKNOWN;
362 }
363
364
365 /*
366 * Options
367 */
368
369 static const char usage[] =
370 VERSION " (" __DATE__ ")\n"
371 "\n"
372 "Usage: tig [options]\n"
373 " or: tig [options] [--] [git log options]\n"
374 " or: tig [options] log [git log options]\n"
375 " or: tig [options] diff [git diff options]\n"
376 " or: tig [options] show [git show options]\n"
377 " or: tig [options] < [git command output]\n"
378 "\n"
379 "Options:\n"
380 " -l Start up in log view\n"
381 " -d Start up in diff view\n"
382 " -n[I], --line-number[=I] Show line numbers with given interval\n"
383 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
384 " -- Mark end of tig options\n"
385 " -v, --version Show version and exit\n"
386 " -h, --help Show help message and exit\n";
387
388 /* Option and state variables. */
389 static bool opt_line_number = FALSE;
390 static bool opt_rev_graph = TRUE;
391 static int opt_num_interval = NUMBER_INTERVAL;
392 static int opt_tab_size = TABSIZE;
393 static enum request opt_request = REQ_VIEW_MAIN;
394 static char opt_cmd[SIZEOF_STR] = "";
395 static char opt_path[SIZEOF_STR] = "";
396 static FILE *opt_pipe = NULL;
397 static char opt_encoding[20] = "UTF-8";
398 static bool opt_utf8 = TRUE;
399 static char opt_codeset[20] = "UTF-8";
400 static iconv_t opt_iconv = ICONV_NONE;
401 static char opt_search[SIZEOF_STR] = "";
402
403 enum option_type {
404 OPT_NONE,
405 OPT_INT,
406 };
407
408 static bool
409 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
410 {
411 va_list args;
412 char *value = "";
413 int *number;
414
415 if (opt[0] != '-')
416 return FALSE;
417
418 if (opt[1] == '-') {
419 int namelen = strlen(name);
420
421 opt += 2;
422
423 if (strncmp(opt, name, namelen))
424 return FALSE;
425
426 if (opt[namelen] == '=')
427 value = opt + namelen + 1;
428
429 } else {
430 if (!short_name || opt[1] != short_name)
431 return FALSE;
432 value = opt + 2;
433 }
434
435 va_start(args, type);
436 if (type == OPT_INT) {
437 number = va_arg(args, int *);
438 if (isdigit(*value))
439 *number = atoi(value);
440 }
441 va_end(args);
442
443 return TRUE;
444 }
445
446 /* Returns the index of log or diff command or -1 to exit. */
447 static bool
448 parse_options(int argc, char *argv[])
449 {
450 int i;
451
452 for (i = 1; i < argc; i++) {
453 char *opt = argv[i];
454
455 if (!strcmp(opt, "log") ||
456 !strcmp(opt, "diff") ||
457 !strcmp(opt, "show")) {
458 opt_request = opt[0] == 'l'
459 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
460 break;
461 }
462
463 if (opt[0] && opt[0] != '-')
464 break;
465
466 if (!strcmp(opt, "-l")) {
467 opt_request = REQ_VIEW_LOG;
468 continue;
469 }
470
471 if (!strcmp(opt, "-d")) {
472 opt_request = REQ_VIEW_DIFF;
473 continue;
474 }
475
476 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
477 opt_line_number = TRUE;
478 continue;
479 }
480
481 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
482 opt_tab_size = MIN(opt_tab_size, TABSIZE);
483 continue;
484 }
485
486 if (check_option(opt, 'v', "version", OPT_NONE)) {
487 printf("tig version %s\n", VERSION);
488 return FALSE;
489 }
490
491 if (check_option(opt, 'h', "help", OPT_NONE)) {
492 printf(usage);
493 return FALSE;
494 }
495
496 if (!strcmp(opt, "--")) {
497 i++;
498 break;
499 }
500
501 die("unknown option '%s'\n\n%s", opt, usage);
502 }
503
504 if (!isatty(STDIN_FILENO)) {
505 opt_request = REQ_VIEW_PAGER;
506 opt_pipe = stdin;
507
508 } else if (i < argc) {
509 size_t buf_size;
510
511 if (opt_request == REQ_VIEW_MAIN)
512 /* XXX: This is vulnerable to the user overriding
513 * options required for the main view parser. */
514 string_copy(opt_cmd, "git log --pretty=raw");
515 else
516 string_copy(opt_cmd, "git");
517 buf_size = strlen(opt_cmd);
518
519 while (buf_size < sizeof(opt_cmd) && i < argc) {
520 opt_cmd[buf_size++] = ' ';
521 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
522 }
523
524 if (buf_size >= sizeof(opt_cmd))
525 die("command too long");
526
527 opt_cmd[buf_size] = 0;
528 }
529
530 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
531 opt_utf8 = FALSE;
532
533 return TRUE;
534 }
535
536
537 /*
538 * Line-oriented content detection.
539 */
540
541 #define LINE_INFO \
542 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
543 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
544 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
545 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
546 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
547 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
548 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
549 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
550 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
551 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
552 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
553 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
554 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
555 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
556 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
557 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
558 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
559 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
560 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
561 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
562 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
563 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
564 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
565 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
566 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
567 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
568 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
570 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
571 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
572 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
573 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
574 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
575 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
576 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
577 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
578 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
579 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
580 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
581 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
582 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
583 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL)
584
585 enum line_type {
586 #define LINE(type, line, fg, bg, attr) \
587 LINE_##type
588 LINE_INFO
589 #undef LINE
590 };
591
592 struct line_info {
593 const char *name; /* Option name. */
594 int namelen; /* Size of option name. */
595 const char *line; /* The start of line to match. */
596 int linelen; /* Size of string to match. */
597 int fg, bg, attr; /* Color and text attributes for the lines. */
598 };
599
600 static struct line_info line_info[] = {
601 #define LINE(type, line, fg, bg, attr) \
602 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
603 LINE_INFO
604 #undef LINE
605 };
606
607 static enum line_type
608 get_line_type(char *line)
609 {
610 int linelen = strlen(line);
611 enum line_type type;
612
613 for (type = 0; type < ARRAY_SIZE(line_info); type++)
614 /* Case insensitive search matches Signed-off-by lines better. */
615 if (linelen >= line_info[type].linelen &&
616 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
617 return type;
618
619 return LINE_DEFAULT;
620 }
621
622 static inline int
623 get_line_attr(enum line_type type)
624 {
625 assert(type < ARRAY_SIZE(line_info));
626 return COLOR_PAIR(type) | line_info[type].attr;
627 }
628
629 static struct line_info *
630 get_line_info(char *name, int namelen)
631 {
632 enum line_type type;
633
634 for (type = 0; type < ARRAY_SIZE(line_info); type++)
635 if (namelen == line_info[type].namelen &&
636 !string_enum_compare(line_info[type].name, name, namelen))
637 return &line_info[type];
638
639 return NULL;
640 }
641
642 static void
643 init_colors(void)
644 {
645 int default_bg = COLOR_BLACK;
646 int default_fg = COLOR_WHITE;
647 enum line_type type;
648
649 start_color();
650
651 if (use_default_colors() != ERR) {
652 default_bg = -1;
653 default_fg = -1;
654 }
655
656 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
657 struct line_info *info = &line_info[type];
658 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
659 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
660
661 init_pair(type, fg, bg);
662 }
663 }
664
665 struct line {
666 enum line_type type;
667
668 /* State flags */
669 unsigned int selected:1;
670
671 void *data; /* User data */
672 };
673
674
675 /*
676 * Keys
677 */
678
679 struct keybinding {
680 int alias;
681 enum request request;
682 struct keybinding *next;
683 };
684
685 static struct keybinding default_keybindings[] = {
686 /* View switching */
687 { 'm', REQ_VIEW_MAIN },
688 { 'd', REQ_VIEW_DIFF },
689 { 'l', REQ_VIEW_LOG },
690 { 't', REQ_VIEW_TREE },
691 { 'f', REQ_VIEW_BLOB },
692 { 'p', REQ_VIEW_PAGER },
693 { 'h', REQ_VIEW_HELP },
694
695 /* View manipulation */
696 { 'q', REQ_VIEW_CLOSE },
697 { KEY_TAB, REQ_VIEW_NEXT },
698 { KEY_RETURN, REQ_ENTER },
699 { KEY_UP, REQ_PREVIOUS },
700 { KEY_DOWN, REQ_NEXT },
701
702 /* Cursor navigation */
703 { 'k', REQ_MOVE_UP },
704 { 'j', REQ_MOVE_DOWN },
705 { KEY_HOME, REQ_MOVE_FIRST_LINE },
706 { KEY_END, REQ_MOVE_LAST_LINE },
707 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
708 { ' ', REQ_MOVE_PAGE_DOWN },
709 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
710 { 'b', REQ_MOVE_PAGE_UP },
711 { '-', REQ_MOVE_PAGE_UP },
712
713 /* Scrolling */
714 { KEY_IC, REQ_SCROLL_LINE_UP },
715 { KEY_DC, REQ_SCROLL_LINE_DOWN },
716 { 'w', REQ_SCROLL_PAGE_UP },
717 { 's', REQ_SCROLL_PAGE_DOWN },
718
719 /* Searching */
720 { '/', REQ_SEARCH },
721 { '?', REQ_SEARCH_BACK },
722 { 'n', REQ_FIND_NEXT },
723 { 'N', REQ_FIND_PREV },
724
725 /* Misc */
726 { 'Q', REQ_QUIT },
727 { 'z', REQ_STOP_LOADING },
728 { 'v', REQ_SHOW_VERSION },
729 { 'r', REQ_SCREEN_REDRAW },
730 { '.', REQ_TOGGLE_LINENO },
731 { 'g', REQ_TOGGLE_REV_GRAPH },
732 { ':', REQ_PROMPT },
733
734 /* Using the ncurses SIGWINCH handler. */
735 { KEY_RESIZE, REQ_SCREEN_RESIZE },
736 };
737
738 #define KEYMAP_INFO \
739 KEYMAP_(GENERIC), \
740 KEYMAP_(MAIN), \
741 KEYMAP_(DIFF), \
742 KEYMAP_(LOG), \
743 KEYMAP_(TREE), \
744 KEYMAP_(BLOB), \
745 KEYMAP_(PAGER), \
746 KEYMAP_(HELP) \
747
748 enum keymap {
749 #define KEYMAP_(name) KEYMAP_##name
750 KEYMAP_INFO
751 #undef KEYMAP_
752 };
753
754 static struct int_map keymap_table[] = {
755 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
756 KEYMAP_INFO
757 #undef KEYMAP_
758 };
759
760 #define set_keymap(map, name) \
761 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
762
763 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
764
765 static void
766 add_keybinding(enum keymap keymap, enum request request, int key)
767 {
768 struct keybinding *keybinding;
769
770 keybinding = calloc(1, sizeof(*keybinding));
771 if (!keybinding)
772 die("Failed to allocate keybinding");
773
774 keybinding->alias = key;
775 keybinding->request = request;
776 keybinding->next = keybindings[keymap];
777 keybindings[keymap] = keybinding;
778 }
779
780 /* Looks for a key binding first in the given map, then in the generic map, and
781 * lastly in the default keybindings. */
782 static enum request
783 get_keybinding(enum keymap keymap, int key)
784 {
785 struct keybinding *kbd;
786 int i;
787
788 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
789 if (kbd->alias == key)
790 return kbd->request;
791
792 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
793 if (kbd->alias == key)
794 return kbd->request;
795
796 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
797 if (default_keybindings[i].alias == key)
798 return default_keybindings[i].request;
799
800 return (enum request) key;
801 }
802
803
804 struct key {
805 char *name;
806 int value;
807 };
808
809 static struct key key_table[] = {
810 { "Enter", KEY_RETURN },
811 { "Space", ' ' },
812 { "Backspace", KEY_BACKSPACE },
813 { "Tab", KEY_TAB },
814 { "Escape", KEY_ESC },
815 { "Left", KEY_LEFT },
816 { "Right", KEY_RIGHT },
817 { "Up", KEY_UP },
818 { "Down", KEY_DOWN },
819 { "Insert", KEY_IC },
820 { "Delete", KEY_DC },
821 { "Hash", '#' },
822 { "Home", KEY_HOME },
823 { "End", KEY_END },
824 { "PageUp", KEY_PPAGE },
825 { "PageDown", KEY_NPAGE },
826 { "F1", KEY_F(1) },
827 { "F2", KEY_F(2) },
828 { "F3", KEY_F(3) },
829 { "F4", KEY_F(4) },
830 { "F5", KEY_F(5) },
831 { "F6", KEY_F(6) },
832 { "F7", KEY_F(7) },
833 { "F8", KEY_F(8) },
834 { "F9", KEY_F(9) },
835 { "F10", KEY_F(10) },
836 { "F11", KEY_F(11) },
837 { "F12", KEY_F(12) },
838 };
839
840 static int
841 get_key_value(const char *name)
842 {
843 int i;
844
845 for (i = 0; i < ARRAY_SIZE(key_table); i++)
846 if (!strcasecmp(key_table[i].name, name))
847 return key_table[i].value;
848
849 if (strlen(name) == 1 && isprint(*name))
850 return (int) *name;
851
852 return ERR;
853 }
854
855 static char *
856 get_key(enum request request)
857 {
858 static char buf[BUFSIZ];
859 static char key_char[] = "'X'";
860 size_t pos = 0;
861 char *sep = " ";
862 int i;
863
864 buf[pos] = 0;
865
866 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
867 struct keybinding *keybinding = &default_keybindings[i];
868 char *seq = NULL;
869 int key;
870
871 if (keybinding->request != request)
872 continue;
873
874 for (key = 0; key < ARRAY_SIZE(key_table); key++)
875 if (key_table[key].value == keybinding->alias)
876 seq = key_table[key].name;
877
878 if (seq == NULL &&
879 keybinding->alias < 127 &&
880 isprint(keybinding->alias)) {
881 key_char[1] = (char) keybinding->alias;
882 seq = key_char;
883 }
884
885 if (!seq)
886 seq = "'?'";
887
888 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
889 return "Too many keybindings!";
890 sep = ", ";
891 }
892
893 return buf;
894 }
895
896
897 /*
898 * User config file handling.
899 */
900
901 static struct int_map color_map[] = {
902 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
903 COLOR_MAP(DEFAULT),
904 COLOR_MAP(BLACK),
905 COLOR_MAP(BLUE),
906 COLOR_MAP(CYAN),
907 COLOR_MAP(GREEN),
908 COLOR_MAP(MAGENTA),
909 COLOR_MAP(RED),
910 COLOR_MAP(WHITE),
911 COLOR_MAP(YELLOW),
912 };
913
914 #define set_color(color, name) \
915 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
916
917 static struct int_map attr_map[] = {
918 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
919 ATTR_MAP(NORMAL),
920 ATTR_MAP(BLINK),
921 ATTR_MAP(BOLD),
922 ATTR_MAP(DIM),
923 ATTR_MAP(REVERSE),
924 ATTR_MAP(STANDOUT),
925 ATTR_MAP(UNDERLINE),
926 };
927
928 #define set_attribute(attr, name) \
929 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
930
931 static int config_lineno;
932 static bool config_errors;
933 static char *config_msg;
934
935 /* Wants: object fgcolor bgcolor [attr] */
936 static int
937 option_color_command(int argc, char *argv[])
938 {
939 struct line_info *info;
940
941 if (argc != 3 && argc != 4) {
942 config_msg = "Wrong number of arguments given to color command";
943 return ERR;
944 }
945
946 info = get_line_info(argv[0], strlen(argv[0]));
947 if (!info) {
948 config_msg = "Unknown color name";
949 return ERR;
950 }
951
952 if (set_color(&info->fg, argv[1]) == ERR ||
953 set_color(&info->bg, argv[2]) == ERR) {
954 config_msg = "Unknown color";
955 return ERR;
956 }
957
958 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
959 config_msg = "Unknown attribute";
960 return ERR;
961 }
962
963 return OK;
964 }
965
966 /* Wants: name = value */
967 static int
968 option_set_command(int argc, char *argv[])
969 {
970 if (argc != 3) {
971 config_msg = "Wrong number of arguments given to set command";
972 return ERR;
973 }
974
975 if (strcmp(argv[1], "=")) {
976 config_msg = "No value assigned";
977 return ERR;
978 }
979
980 if (!strcmp(argv[0], "show-rev-graph")) {
981 opt_rev_graph = (!strcmp(argv[2], "1") ||
982 !strcmp(argv[2], "true") ||
983 !strcmp(argv[2], "yes"));
984 return OK;
985 }
986
987 if (!strcmp(argv[0], "line-number-interval")) {
988 opt_num_interval = atoi(argv[2]);
989 return OK;
990 }
991
992 if (!strcmp(argv[0], "tab-size")) {
993 opt_tab_size = atoi(argv[2]);
994 return OK;
995 }
996
997 if (!strcmp(argv[0], "commit-encoding")) {
998 char *arg = argv[2];
999 int delimiter = *arg;
1000 int i;
1001
1002 switch (delimiter) {
1003 case '"':
1004 case '\'':
1005 for (arg++, i = 0; arg[i]; i++)
1006 if (arg[i] == delimiter) {
1007 arg[i] = 0;
1008 break;
1009 }
1010 default:
1011 string_copy(opt_encoding, arg);
1012 return OK;
1013 }
1014 }
1015
1016 config_msg = "Unknown variable name";
1017 return ERR;
1018 }
1019
1020 /* Wants: mode request key */
1021 static int
1022 option_bind_command(int argc, char *argv[])
1023 {
1024 enum request request;
1025 int keymap;
1026 int key;
1027
1028 if (argc != 3) {
1029 config_msg = "Wrong number of arguments given to bind command";
1030 return ERR;
1031 }
1032
1033 if (set_keymap(&keymap, argv[0]) == ERR) {
1034 config_msg = "Unknown key map";
1035 return ERR;
1036 }
1037
1038 key = get_key_value(argv[1]);
1039 if (key == ERR) {
1040 config_msg = "Unknown key";
1041 return ERR;
1042 }
1043
1044 request = get_request(argv[2]);
1045 if (request == REQ_UNKNOWN) {
1046 config_msg = "Unknown request name";
1047 return ERR;
1048 }
1049
1050 add_keybinding(keymap, request, key);
1051
1052 return OK;
1053 }
1054
1055 static int
1056 set_option(char *opt, char *value)
1057 {
1058 char *argv[16];
1059 int valuelen;
1060 int argc = 0;
1061
1062 /* Tokenize */
1063 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1064 argv[argc++] = value;
1065
1066 value += valuelen;
1067 if (!*value)
1068 break;
1069
1070 *value++ = 0;
1071 while (isspace(*value))
1072 value++;
1073 }
1074
1075 if (!strcmp(opt, "color"))
1076 return option_color_command(argc, argv);
1077
1078 if (!strcmp(opt, "set"))
1079 return option_set_command(argc, argv);
1080
1081 if (!strcmp(opt, "bind"))
1082 return option_bind_command(argc, argv);
1083
1084 config_msg = "Unknown option command";
1085 return ERR;
1086 }
1087
1088 static int
1089 read_option(char *opt, int optlen, char *value, int valuelen)
1090 {
1091 int status = OK;
1092
1093 config_lineno++;
1094 config_msg = "Internal error";
1095
1096 /* Check for comment markers, since read_properties() will
1097 * only ensure opt and value are split at first " \t". */
1098 optlen = strcspn(opt, "#");
1099 if (optlen == 0)
1100 return OK;
1101
1102 if (opt[optlen] != 0) {
1103 config_msg = "No option value";
1104 status = ERR;
1105
1106 } else {
1107 /* Look for comment endings in the value. */
1108 int len = strcspn(value, "#");
1109
1110 if (len < valuelen) {
1111 valuelen = len;
1112 value[valuelen] = 0;
1113 }
1114
1115 status = set_option(opt, value);
1116 }
1117
1118 if (status == ERR) {
1119 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1120 config_lineno, optlen, opt, config_msg);
1121 config_errors = TRUE;
1122 }
1123
1124 /* Always keep going if errors are encountered. */
1125 return OK;
1126 }
1127
1128 static int
1129 load_options(void)
1130 {
1131 char *home = getenv("HOME");
1132 char buf[SIZEOF_STR];
1133 FILE *file;
1134
1135 config_lineno = 0;
1136 config_errors = FALSE;
1137
1138 if (!home || !string_format(buf, "%s/.tigrc", home))
1139 return ERR;
1140
1141 /* It's ok that the file doesn't exist. */
1142 file = fopen(buf, "r");
1143 if (!file)
1144 return OK;
1145
1146 if (read_properties(file, " \t", read_option) == ERR ||
1147 config_errors == TRUE)
1148 fprintf(stderr, "Errors while loading %s.\n", buf);
1149
1150 return OK;
1151 }
1152
1153
1154 /*
1155 * The viewer
1156 */
1157
1158 struct view;
1159 struct view_ops;
1160
1161 /* The display array of active views and the index of the current view. */
1162 static struct view *display[2];
1163 static unsigned int current_view;
1164
1165 /* Reading from the prompt? */
1166 static bool input_mode = FALSE;
1167
1168 #define foreach_displayed_view(view, i) \
1169 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1170
1171 #define displayed_views() (display[1] != NULL ? 2 : 1)
1172
1173 /* Current head and commit ID */
1174 static char ref_blob[SIZEOF_REF] = "";
1175 static char ref_commit[SIZEOF_REF] = "HEAD";
1176 static char ref_head[SIZEOF_REF] = "HEAD";
1177
1178 struct view {
1179 const char *name; /* View name */
1180 const char *cmd_fmt; /* Default command line format */
1181 const char *cmd_env; /* Command line set via environment */
1182 const char *id; /* Points to either of ref_{head,commit,blob} */
1183
1184 struct view_ops *ops; /* View operations */
1185
1186 enum keymap keymap; /* What keymap does this view have */
1187
1188 char cmd[SIZEOF_STR]; /* Command buffer */
1189 char ref[SIZEOF_REF]; /* Hovered commit reference */
1190 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1191
1192 int height, width; /* The width and height of the main window */
1193 WINDOW *win; /* The main window */
1194 WINDOW *title; /* The title window living below the main window */
1195
1196 /* Navigation */
1197 unsigned long offset; /* Offset of the window top */
1198 unsigned long lineno; /* Current line number */
1199
1200 /* Searching */
1201 char grep[SIZEOF_STR]; /* Search string */
1202 regex_t *regex; /* Pre-compiled regex */
1203
1204 /* If non-NULL, points to the view that opened this view. If this view
1205 * is closed tig will switch back to the parent view. */
1206 struct view *parent;
1207
1208 /* Buffering */
1209 unsigned long lines; /* Total number of lines */
1210 struct line *line; /* Line index */
1211 unsigned long line_size;/* Total number of allocated lines */
1212 unsigned int digits; /* Number of digits in the lines member. */
1213
1214 /* Loading */
1215 FILE *pipe;
1216 time_t start_time;
1217 };
1218
1219 struct view_ops {
1220 /* What type of content being displayed. Used in the title bar. */
1221 const char *type;
1222 /* Draw one line; @lineno must be < view->height. */
1223 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1224 /* Read one line; updates view->line. */
1225 bool (*read)(struct view *view, char *data);
1226 /* Depending on view, change display based on current line. */
1227 bool (*enter)(struct view *view, struct line *line);
1228 /* Search for regex in a line. */
1229 bool (*grep)(struct view *view, struct line *line);
1230 /* Select line */
1231 void (*select)(struct view *view, struct line *line);
1232 };
1233
1234 static struct view_ops pager_ops;
1235 static struct view_ops main_ops;
1236 static struct view_ops tree_ops;
1237 static struct view_ops blob_ops;
1238
1239 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1240 { name, cmd, #env, ref, ops, map}
1241
1242 #define VIEW_(id, name, ops, ref) \
1243 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1244
1245
1246 static struct view views[] = {
1247 VIEW_(MAIN, "main", &main_ops, ref_head),
1248 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1249 VIEW_(LOG, "log", &pager_ops, ref_head),
1250 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1251 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1252 VIEW_(HELP, "help", &pager_ops, "static"),
1253 VIEW_(PAGER, "pager", &pager_ops, "static"),
1254 };
1255
1256 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1257
1258 #define foreach_view(view, i) \
1259 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1260
1261 #define view_is_displayed(view) \
1262 (view == display[0] || view == display[1])
1263
1264 static bool
1265 draw_view_line(struct view *view, unsigned int lineno)
1266 {
1267 struct line *line;
1268 bool selected = (view->offset + lineno == view->lineno);
1269 bool draw_ok;
1270
1271 assert(view_is_displayed(view));
1272
1273 if (view->offset + lineno >= view->lines)
1274 return FALSE;
1275
1276 line = &view->line[view->offset + lineno];
1277
1278 if (selected) {
1279 line->selected = TRUE;
1280 view->ops->select(view, line);
1281 } else if (line->selected) {
1282 line->selected = FALSE;
1283 wmove(view->win, lineno, 0);
1284 wclrtoeol(view->win);
1285 }
1286
1287 scrollok(view->win, FALSE);
1288 draw_ok = view->ops->draw(view, line, lineno, selected);
1289 scrollok(view->win, TRUE);
1290
1291 return draw_ok;
1292 }
1293
1294 static void
1295 redraw_view_from(struct view *view, int lineno)
1296 {
1297 assert(0 <= lineno && lineno < view->height);
1298
1299 for (; lineno < view->height; lineno++) {
1300 if (!draw_view_line(view, lineno))
1301 break;
1302 }
1303
1304 redrawwin(view->win);
1305 if (input_mode)
1306 wnoutrefresh(view->win);
1307 else
1308 wrefresh(view->win);
1309 }
1310
1311 static void
1312 redraw_view(struct view *view)
1313 {
1314 wclear(view->win);
1315 redraw_view_from(view, 0);
1316 }
1317
1318
1319 static void
1320 update_view_title(struct view *view)
1321 {
1322 char buf[SIZEOF_STR];
1323 char state[SIZEOF_STR];
1324 size_t bufpos = 0, statelen = 0;
1325
1326 assert(view_is_displayed(view));
1327
1328 if (view->lines || view->pipe) {
1329 unsigned int view_lines = view->offset + view->height;
1330 unsigned int lines = view->lines
1331 ? MIN(view_lines, view->lines) * 100 / view->lines
1332 : 0;
1333
1334 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1335 view->ops->type,
1336 view->lineno + 1,
1337 view->lines,
1338 lines);
1339
1340 if (view->pipe) {
1341 time_t secs = time(NULL) - view->start_time;
1342
1343 /* Three git seconds are a long time ... */
1344 if (secs > 2)
1345 string_format_from(state, &statelen, " %lds", secs);
1346 }
1347 }
1348
1349 string_format_from(buf, &bufpos, "[%s]", view->name);
1350 if (*view->ref && bufpos < view->width) {
1351 size_t refsize = strlen(view->ref);
1352 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1353
1354 if (minsize < view->width)
1355 refsize = view->width - minsize + 7;
1356 string_format_from(buf, &bufpos, " %.*s", refsize, view->ref);
1357 }
1358
1359 if (statelen && bufpos < view->width) {
1360 string_format_from(buf, &bufpos, " %s", state);
1361 }
1362
1363 if (view == display[current_view])
1364 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1365 else
1366 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1367
1368 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1369 wclrtoeol(view->title);
1370 wmove(view->title, 0, view->width - 1);
1371
1372 if (input_mode)
1373 wnoutrefresh(view->title);
1374 else
1375 wrefresh(view->title);
1376 }
1377
1378 static void
1379 resize_display(void)
1380 {
1381 int offset, i;
1382 struct view *base = display[0];
1383 struct view *view = display[1] ? display[1] : display[0];
1384
1385 /* Setup window dimensions */
1386
1387 getmaxyx(stdscr, base->height, base->width);
1388
1389 /* Make room for the status window. */
1390 base->height -= 1;
1391
1392 if (view != base) {
1393 /* Horizontal split. */
1394 view->width = base->width;
1395 view->height = SCALE_SPLIT_VIEW(base->height);
1396 base->height -= view->height;
1397
1398 /* Make room for the title bar. */
1399 view->height -= 1;
1400 }
1401
1402 /* Make room for the title bar. */
1403 base->height -= 1;
1404
1405 offset = 0;
1406
1407 foreach_displayed_view (view, i) {
1408 if (!view->win) {
1409 view->win = newwin(view->height, 0, offset, 0);
1410 if (!view->win)
1411 die("Failed to create %s view", view->name);
1412
1413 scrollok(view->win, TRUE);
1414
1415 view->title = newwin(1, 0, offset + view->height, 0);
1416 if (!view->title)
1417 die("Failed to create title window");
1418
1419 } else {
1420 wresize(view->win, view->height, view->width);
1421 mvwin(view->win, offset, 0);
1422 mvwin(view->title, offset + view->height, 0);
1423 }
1424
1425 offset += view->height + 1;
1426 }
1427 }
1428
1429 static void
1430 redraw_display(void)
1431 {
1432 struct view *view;
1433 int i;
1434
1435 foreach_displayed_view (view, i) {
1436 redraw_view(view);
1437 update_view_title(view);
1438 }
1439 }
1440
1441 static void
1442 update_display_cursor(struct view *view)
1443 {
1444 /* Move the cursor to the right-most column of the cursor line.
1445 *
1446 * XXX: This could turn out to be a bit expensive, but it ensures that
1447 * the cursor does not jump around. */
1448 if (view->lines) {
1449 wmove(view->win, view->lineno - view->offset, view->width - 1);
1450 wrefresh(view->win);
1451 }
1452 }
1453
1454 /*
1455 * Navigation
1456 */
1457
1458 /* Scrolling backend */
1459 static void
1460 do_scroll_view(struct view *view, int lines)
1461 {
1462 bool redraw_current_line = FALSE;
1463
1464 /* The rendering expects the new offset. */
1465 view->offset += lines;
1466
1467 assert(0 <= view->offset && view->offset < view->lines);
1468 assert(lines);
1469
1470 /* Move current line into the view. */
1471 if (view->lineno < view->offset) {
1472 view->lineno = view->offset;
1473 redraw_current_line = TRUE;
1474 } else if (view->lineno >= view->offset + view->height) {
1475 view->lineno = view->offset + view->height - 1;
1476 redraw_current_line = TRUE;
1477 }
1478
1479 assert(view->offset <= view->lineno && view->lineno < view->lines);
1480
1481 /* Redraw the whole screen if scrolling is pointless. */
1482 if (view->height < ABS(lines)) {
1483 redraw_view(view);
1484
1485 } else {
1486 int line = lines > 0 ? view->height - lines : 0;
1487 int end = line + ABS(lines);
1488
1489 wscrl(view->win, lines);
1490
1491 for (; line < end; line++) {
1492 if (!draw_view_line(view, line))
1493 break;
1494 }
1495
1496 if (redraw_current_line)
1497 draw_view_line(view, view->lineno - view->offset);
1498 }
1499
1500 redrawwin(view->win);
1501 wrefresh(view->win);
1502 report("");
1503 }
1504
1505 /* Scroll frontend */
1506 static void
1507 scroll_view(struct view *view, enum request request)
1508 {
1509 int lines = 1;
1510
1511 assert(view_is_displayed(view));
1512
1513 switch (request) {
1514 case REQ_SCROLL_PAGE_DOWN:
1515 lines = view->height;
1516 case REQ_SCROLL_LINE_DOWN:
1517 if (view->offset + lines > view->lines)
1518 lines = view->lines - view->offset;
1519
1520 if (lines == 0 || view->offset + view->height >= view->lines) {
1521 report("Cannot scroll beyond the last line");
1522 return;
1523 }
1524 break;
1525
1526 case REQ_SCROLL_PAGE_UP:
1527 lines = view->height;
1528 case REQ_SCROLL_LINE_UP:
1529 if (lines > view->offset)
1530 lines = view->offset;
1531
1532 if (lines == 0) {
1533 report("Cannot scroll beyond the first line");
1534 return;
1535 }
1536
1537 lines = -lines;
1538 break;
1539
1540 default:
1541 die("request %d not handled in switch", request);
1542 }
1543
1544 do_scroll_view(view, lines);
1545 }
1546
1547 /* Cursor moving */
1548 static void
1549 move_view(struct view *view, enum request request)
1550 {
1551 int scroll_steps = 0;
1552 int steps;
1553
1554 switch (request) {
1555 case REQ_MOVE_FIRST_LINE:
1556 steps = -view->lineno;
1557 break;
1558
1559 case REQ_MOVE_LAST_LINE:
1560 steps = view->lines - view->lineno - 1;
1561 break;
1562
1563 case REQ_MOVE_PAGE_UP:
1564 steps = view->height > view->lineno
1565 ? -view->lineno : -view->height;
1566 break;
1567
1568 case REQ_MOVE_PAGE_DOWN:
1569 steps = view->lineno + view->height >= view->lines
1570 ? view->lines - view->lineno - 1 : view->height;
1571 break;
1572
1573 case REQ_MOVE_UP:
1574 steps = -1;
1575 break;
1576
1577 case REQ_MOVE_DOWN:
1578 steps = 1;
1579 break;
1580
1581 default:
1582 die("request %d not handled in switch", request);
1583 }
1584
1585 if (steps <= 0 && view->lineno == 0) {
1586 report("Cannot move beyond the first line");
1587 return;
1588
1589 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1590 report("Cannot move beyond the last line");
1591 return;
1592 }
1593
1594 /* Move the current line */
1595 view->lineno += steps;
1596 assert(0 <= view->lineno && view->lineno < view->lines);
1597
1598 /* Check whether the view needs to be scrolled */
1599 if (view->lineno < view->offset ||
1600 view->lineno >= view->offset + view->height) {
1601 scroll_steps = steps;
1602 if (steps < 0 && -steps > view->offset) {
1603 scroll_steps = -view->offset;
1604
1605 } else if (steps > 0) {
1606 if (view->lineno == view->lines - 1 &&
1607 view->lines > view->height) {
1608 scroll_steps = view->lines - view->offset - 1;
1609 if (scroll_steps >= view->height)
1610 scroll_steps -= view->height - 1;
1611 }
1612 }
1613 }
1614
1615 if (!view_is_displayed(view)) {
1616 view->offset += steps;
1617 view->ops->select(view, &view->line[view->lineno]);
1618 return;
1619 }
1620
1621 /* Repaint the old "current" line if we be scrolling */
1622 if (ABS(steps) < view->height)
1623 draw_view_line(view, view->lineno - steps - view->offset);
1624
1625 if (scroll_steps) {
1626 do_scroll_view(view, scroll_steps);
1627 return;
1628 }
1629
1630 /* Draw the current line */
1631 draw_view_line(view, view->lineno - view->offset);
1632
1633 redrawwin(view->win);
1634 wrefresh(view->win);
1635 report("");
1636 }
1637
1638
1639 /*
1640 * Searching
1641 */
1642
1643 static void search_view(struct view *view, enum request request);
1644
1645 static bool
1646 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1647 {
1648 assert(view_is_displayed(view));
1649
1650 if (!view->ops->grep(view, line))
1651 return FALSE;
1652
1653 if (lineno - view->offset >= view->height) {
1654 view->offset = lineno;
1655 view->lineno = lineno;
1656 redraw_view(view);
1657
1658 } else {
1659 unsigned long old_lineno = view->lineno - view->offset;
1660
1661 view->lineno = lineno;
1662 draw_view_line(view, old_lineno);
1663
1664 draw_view_line(view, view->lineno - view->offset);
1665 redrawwin(view->win);
1666 wrefresh(view->win);
1667 }
1668
1669 report("Line %ld matches '%s'", lineno + 1, view->grep);
1670 return TRUE;
1671 }
1672
1673 static void
1674 find_next(struct view *view, enum request request)
1675 {
1676 unsigned long lineno = view->lineno;
1677 int direction;
1678
1679 if (!*view->grep) {
1680 if (!*opt_search)
1681 report("No previous search");
1682 else
1683 search_view(view, request);
1684 return;
1685 }
1686
1687 switch (request) {
1688 case REQ_SEARCH:
1689 case REQ_FIND_NEXT:
1690 direction = 1;
1691 break;
1692
1693 case REQ_SEARCH_BACK:
1694 case REQ_FIND_PREV:
1695 direction = -1;
1696 break;
1697
1698 default:
1699 return;
1700 }
1701
1702 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1703 lineno += direction;
1704
1705 /* Note, lineno is unsigned long so will wrap around in which case it
1706 * will become bigger than view->lines. */
1707 for (; lineno < view->lines; lineno += direction) {
1708 struct line *line = &view->line[lineno];
1709
1710 if (find_next_line(view, lineno, line))
1711 return;
1712 }
1713
1714 report("No match found for '%s'", view->grep);
1715 }
1716
1717 static void
1718 search_view(struct view *view, enum request request)
1719 {
1720 int regex_err;
1721
1722 if (view->regex) {
1723 regfree(view->regex);
1724 *view->grep = 0;
1725 } else {
1726 view->regex = calloc(1, sizeof(*view->regex));
1727 if (!view->regex)
1728 return;
1729 }
1730
1731 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1732 if (regex_err != 0) {
1733 char buf[SIZEOF_STR] = "unknown error";
1734
1735 regerror(regex_err, view->regex, buf, sizeof(buf));
1736 report("Search failed: %s", buf);
1737 return;
1738 }
1739
1740 string_copy(view->grep, opt_search);
1741
1742 find_next(view, request);
1743 }
1744
1745 /*
1746 * Incremental updating
1747 */
1748
1749 static void
1750 end_update(struct view *view)
1751 {
1752 if (!view->pipe)
1753 return;
1754 set_nonblocking_input(FALSE);
1755 if (view->pipe == stdin)
1756 fclose(view->pipe);
1757 else
1758 pclose(view->pipe);
1759 view->pipe = NULL;
1760 }
1761
1762 static bool
1763 begin_update(struct view *view)
1764 {
1765 const char *id = view->id;
1766
1767 if (view->pipe)
1768 end_update(view);
1769
1770 if (opt_cmd[0]) {
1771 string_copy(view->cmd, opt_cmd);
1772 opt_cmd[0] = 0;
1773 /* When running random commands, the view ref could have become
1774 * invalid so clear it. */
1775 view->ref[0] = 0;
1776
1777 } else if (view == VIEW(REQ_VIEW_TREE)) {
1778 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1779
1780 if (strcmp(view->vid, view->id))
1781 opt_path[0] = 0;
1782
1783 if (!string_format(view->cmd, format, id, opt_path))
1784 return FALSE;
1785
1786 } else {
1787 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1788
1789 if (!string_format(view->cmd, format, id, id, id, id, id))
1790 return FALSE;
1791 }
1792
1793 /* Special case for the pager view. */
1794 if (opt_pipe) {
1795 view->pipe = opt_pipe;
1796 opt_pipe = NULL;
1797 } else {
1798 view->pipe = popen(view->cmd, "r");
1799 }
1800
1801 if (!view->pipe)
1802 return FALSE;
1803
1804 set_nonblocking_input(TRUE);
1805
1806 view->offset = 0;
1807 view->lines = 0;
1808 view->lineno = 0;
1809 string_copy(view->vid, id);
1810
1811 if (view->line) {
1812 int i;
1813
1814 for (i = 0; i < view->lines; i++)
1815 if (view->line[i].data)
1816 free(view->line[i].data);
1817
1818 free(view->line);
1819 view->line = NULL;
1820 }
1821
1822 view->start_time = time(NULL);
1823
1824 return TRUE;
1825 }
1826
1827 static struct line *
1828 realloc_lines(struct view *view, size_t line_size)
1829 {
1830 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1831
1832 if (!tmp)
1833 return NULL;
1834
1835 view->line = tmp;
1836 view->line_size = line_size;
1837 return view->line;
1838 }
1839
1840 static bool
1841 update_view(struct view *view)
1842 {
1843 char in_buffer[BUFSIZ];
1844 char out_buffer[BUFSIZ * 2];
1845 char *line;
1846 /* The number of lines to read. If too low it will cause too much
1847 * redrawing (and possible flickering), if too high responsiveness
1848 * will suffer. */
1849 unsigned long lines = view->height;
1850 int redraw_from = -1;
1851
1852 if (!view->pipe)
1853 return TRUE;
1854
1855 /* Only redraw if lines are visible. */
1856 if (view->offset + view->height >= view->lines)
1857 redraw_from = view->lines - view->offset;
1858
1859 /* FIXME: This is probably not perfect for backgrounded views. */
1860 if (!realloc_lines(view, view->lines + lines))
1861 goto alloc_error;
1862
1863 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1864 size_t linelen = strlen(line);
1865
1866 if (linelen)
1867 line[linelen - 1] = 0;
1868
1869 if (opt_iconv != ICONV_NONE) {
1870 char *inbuf = line;
1871 size_t inlen = linelen;
1872
1873 char *outbuf = out_buffer;
1874 size_t outlen = sizeof(out_buffer);
1875
1876 size_t ret;
1877
1878 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
1879 if (ret != (size_t) -1) {
1880 line = out_buffer;
1881 linelen = strlen(out_buffer);
1882 }
1883 }
1884
1885 if (!view->ops->read(view, line))
1886 goto alloc_error;
1887
1888 if (lines-- == 1)
1889 break;
1890 }
1891
1892 {
1893 int digits;
1894
1895 lines = view->lines;
1896 for (digits = 0; lines; digits++)
1897 lines /= 10;
1898
1899 /* Keep the displayed view in sync with line number scaling. */
1900 if (digits != view->digits) {
1901 view->digits = digits;
1902 redraw_from = 0;
1903 }
1904 }
1905
1906 if (!view_is_displayed(view))
1907 goto check_pipe;
1908
1909 if (view == VIEW(REQ_VIEW_TREE)) {
1910 /* Clear the view and redraw everything since the tree sorting
1911 * might have rearranged things. */
1912 redraw_view(view);
1913
1914 } else if (redraw_from >= 0) {
1915 /* If this is an incremental update, redraw the previous line
1916 * since for commits some members could have changed when
1917 * loading the main view. */
1918 if (redraw_from > 0)
1919 redraw_from--;
1920
1921 /* Incrementally draw avoids flickering. */
1922 redraw_view_from(view, redraw_from);
1923 }
1924
1925 /* Update the title _after_ the redraw so that if the redraw picks up a
1926 * commit reference in view->ref it'll be available here. */
1927 update_view_title(view);
1928
1929 check_pipe:
1930 if (ferror(view->pipe)) {
1931 report("Failed to read: %s", strerror(errno));
1932 goto end;
1933
1934 } else if (feof(view->pipe)) {
1935 report("");
1936 goto end;
1937 }
1938
1939 return TRUE;
1940
1941 alloc_error:
1942 report("Allocation failure");
1943
1944 end:
1945 end_update(view);
1946 return FALSE;
1947 }
1948
1949
1950 /*
1951 * View opening
1952 */
1953
1954 static void open_help_view(struct view *view)
1955 {
1956 char buf[BUFSIZ];
1957 int lines = ARRAY_SIZE(req_info) + 2;
1958 int i;
1959
1960 if (view->lines > 0)
1961 return;
1962
1963 for (i = 0; i < ARRAY_SIZE(req_info); i++)
1964 if (!req_info[i].request)
1965 lines++;
1966
1967 view->line = calloc(lines, sizeof(*view->line));
1968 if (!view->line) {
1969 report("Allocation failure");
1970 return;
1971 }
1972
1973 view->ops->read(view, "Quick reference for tig keybindings:");
1974
1975 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
1976 char *key;
1977
1978 if (!req_info[i].request) {
1979 view->ops->read(view, "");
1980 view->ops->read(view, req_info[i].help);
1981 continue;
1982 }
1983
1984 key = get_key(req_info[i].request);
1985 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
1986 continue;
1987
1988 view->ops->read(view, buf);
1989 }
1990 }
1991
1992 enum open_flags {
1993 OPEN_DEFAULT = 0, /* Use default view switching. */
1994 OPEN_SPLIT = 1, /* Split current view. */
1995 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1996 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1997 };
1998
1999 static void
2000 open_view(struct view *prev, enum request request, enum open_flags flags)
2001 {
2002 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2003 bool split = !!(flags & OPEN_SPLIT);
2004 bool reload = !!(flags & OPEN_RELOAD);
2005 struct view *view = VIEW(request);
2006 int nviews = displayed_views();
2007 struct view *base_view = display[0];
2008
2009 if (view == prev && nviews == 1 && !reload) {
2010 report("Already in %s view", view->name);
2011 return;
2012 }
2013
2014 if (view == VIEW(REQ_VIEW_HELP)) {
2015 open_help_view(view);
2016
2017 } else if ((reload || strcmp(view->vid, view->id)) &&
2018 !begin_update(view)) {
2019 report("Failed to load %s view", view->name);
2020 return;
2021 }
2022
2023 if (split) {
2024 display[1] = view;
2025 if (!backgrounded)
2026 current_view = 1;
2027 } else {
2028 /* Maximize the current view. */
2029 memset(display, 0, sizeof(display));
2030 current_view = 0;
2031 display[current_view] = view;
2032 }
2033
2034 /* Resize the view when switching between split- and full-screen,
2035 * or when switching between two different full-screen views. */
2036 if (nviews != displayed_views() ||
2037 (nviews == 1 && base_view != display[0]))
2038 resize_display();
2039
2040 if (split && prev->lineno - prev->offset >= prev->height) {
2041 /* Take the title line into account. */
2042 int lines = prev->lineno - prev->offset - prev->height + 1;
2043
2044 /* Scroll the view that was split if the current line is
2045 * outside the new limited view. */
2046 do_scroll_view(prev, lines);
2047 }
2048
2049 if (prev && view != prev) {
2050 if (split && !backgrounded) {
2051 /* "Blur" the previous view. */
2052 update_view_title(prev);
2053 }
2054
2055 view->parent = prev;
2056 }
2057
2058 if (view->pipe && view->lines == 0) {
2059 /* Clear the old view and let the incremental updating refill
2060 * the screen. */
2061 wclear(view->win);
2062 report("");
2063 } else {
2064 redraw_view(view);
2065 report("");
2066 }
2067
2068 /* If the view is backgrounded the above calls to report()
2069 * won't redraw the view title. */
2070 if (backgrounded)
2071 update_view_title(view);
2072 }
2073
2074
2075 /*
2076 * User request switch noodle
2077 */
2078
2079 static int
2080 view_driver(struct view *view, enum request request)
2081 {
2082 int i;
2083
2084 switch (request) {
2085 case REQ_MOVE_UP:
2086 case REQ_MOVE_DOWN:
2087 case REQ_MOVE_PAGE_UP:
2088 case REQ_MOVE_PAGE_DOWN:
2089 case REQ_MOVE_FIRST_LINE:
2090 case REQ_MOVE_LAST_LINE:
2091 move_view(view, request);
2092 break;
2093
2094 case REQ_SCROLL_LINE_DOWN:
2095 case REQ_SCROLL_LINE_UP:
2096 case REQ_SCROLL_PAGE_DOWN:
2097 case REQ_SCROLL_PAGE_UP:
2098 scroll_view(view, request);
2099 break;
2100
2101 case REQ_VIEW_BLOB:
2102 if (!ref_blob[0]) {
2103 report("No file chosen, press 't' to open tree view");
2104 break;
2105 }
2106 /* Fall-through */
2107 case REQ_VIEW_MAIN:
2108 case REQ_VIEW_DIFF:
2109 case REQ_VIEW_LOG:
2110 case REQ_VIEW_TREE:
2111 case REQ_VIEW_HELP:
2112 case REQ_VIEW_PAGER:
2113 open_view(view, request, OPEN_DEFAULT);
2114 break;
2115
2116 case REQ_NEXT:
2117 case REQ_PREVIOUS:
2118 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2119
2120 if ((view == VIEW(REQ_VIEW_DIFF) &&
2121 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2122 (view == VIEW(REQ_VIEW_BLOB) &&
2123 view->parent == VIEW(REQ_VIEW_TREE))) {
2124 view = view->parent;
2125 move_view(view, request);
2126 if (view_is_displayed(view))
2127 update_view_title(view);
2128 } else {
2129 move_view(view, request);
2130 break;
2131 }
2132 /* Fall-through */
2133
2134 case REQ_ENTER:
2135 if (!view->lines) {
2136 report("Nothing to enter");
2137 break;
2138 }
2139 return view->ops->enter(view, &view->line[view->lineno]);
2140
2141 case REQ_VIEW_NEXT:
2142 {
2143 int nviews = displayed_views();
2144 int next_view = (current_view + 1) % nviews;
2145
2146 if (next_view == current_view) {
2147 report("Only one view is displayed");
2148 break;
2149 }
2150
2151 current_view = next_view;
2152 /* Blur out the title of the previous view. */
2153 update_view_title(view);
2154 report("");
2155 break;
2156 }
2157 case REQ_TOGGLE_LINENO:
2158 opt_line_number = !opt_line_number;
2159 redraw_display();
2160 break;
2161
2162 case REQ_TOGGLE_REV_GRAPH:
2163 opt_rev_graph = !opt_rev_graph;
2164 redraw_display();
2165 break;
2166
2167 case REQ_PROMPT:
2168 /* Always reload^Wrerun commands from the prompt. */
2169 open_view(view, opt_request, OPEN_RELOAD);
2170 break;
2171
2172 case REQ_SEARCH:
2173 case REQ_SEARCH_BACK:
2174 search_view(view, request);
2175 break;
2176
2177 case REQ_FIND_NEXT:
2178 case REQ_FIND_PREV:
2179 find_next(view, request);
2180 break;
2181
2182 case REQ_STOP_LOADING:
2183 for (i = 0; i < ARRAY_SIZE(views); i++) {
2184 view = &views[i];
2185 if (view->pipe)
2186 report("Stopped loading the %s view", view->name),
2187 end_update(view);
2188 }
2189 break;
2190
2191 case REQ_SHOW_VERSION:
2192 report("%s (built %s)", VERSION, __DATE__);
2193 return TRUE;
2194
2195 case REQ_SCREEN_RESIZE:
2196 resize_display();
2197 /* Fall-through */
2198 case REQ_SCREEN_REDRAW:
2199 redraw_display();
2200 break;
2201
2202 case REQ_NONE:
2203 doupdate();
2204 return TRUE;
2205
2206 case REQ_VIEW_CLOSE:
2207 /* XXX: Mark closed views by letting view->parent point to the
2208 * view itself. Parents to closed view should never be
2209 * followed. */
2210 if (view->parent &&
2211 view->parent->parent != view->parent) {
2212 memset(display, 0, sizeof(display));
2213 current_view = 0;
2214 display[current_view] = view->parent;
2215 view->parent = view;
2216 resize_display();
2217 redraw_display();
2218 break;
2219 }
2220 /* Fall-through */
2221 case REQ_QUIT:
2222 return FALSE;
2223
2224 default:
2225 /* An unknown key will show most commonly used commands. */
2226 report("Unknown key, press 'h' for help");
2227 return TRUE;
2228 }
2229
2230 return TRUE;
2231 }
2232
2233
2234 /*
2235 * Pager backend
2236 */
2237
2238 static bool
2239 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2240 {
2241 char *text = line->data;
2242 enum line_type type = line->type;
2243 int textlen = strlen(text);
2244 int attr;
2245
2246 wmove(view->win, lineno, 0);
2247
2248 if (selected) {
2249 type = LINE_CURSOR;
2250 wchgat(view->win, -1, 0, type, NULL);
2251 }
2252
2253 attr = get_line_attr(type);
2254 wattrset(view->win, attr);
2255
2256 if (opt_line_number || opt_tab_size < TABSIZE) {
2257 static char spaces[] = " ";
2258 int col_offset = 0, col = 0;
2259
2260 if (opt_line_number) {
2261 unsigned long real_lineno = view->offset + lineno + 1;
2262
2263 if (real_lineno == 1 ||
2264 (real_lineno % opt_num_interval) == 0) {
2265 wprintw(view->win, "%.*d", view->digits, real_lineno);
2266
2267 } else {
2268 waddnstr(view->win, spaces,
2269 MIN(view->digits, STRING_SIZE(spaces)));
2270 }
2271 waddstr(view->win, ": ");
2272 col_offset = view->digits + 2;
2273 }
2274
2275 while (text && col_offset + col < view->width) {
2276 int cols_max = view->width - col_offset - col;
2277 char *pos = text;
2278 int cols;
2279
2280 if (*text == '\t') {
2281 text++;
2282 assert(sizeof(spaces) > TABSIZE);
2283 pos = spaces;
2284 cols = opt_tab_size - (col % opt_tab_size);
2285
2286 } else {
2287 text = strchr(text, '\t');
2288 cols = line ? text - pos : strlen(pos);
2289 }
2290
2291 waddnstr(view->win, pos, MIN(cols, cols_max));
2292 col += cols;
2293 }
2294
2295 } else {
2296 int col = 0, pos = 0;
2297
2298 for (; pos < textlen && col < view->width; pos++, col++)
2299 if (text[pos] == '\t')
2300 col += TABSIZE - (col % TABSIZE) - 1;
2301
2302 waddnstr(view->win, text, pos);
2303 }
2304
2305 return TRUE;
2306 }
2307
2308 static bool
2309 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2310 {
2311 char refbuf[SIZEOF_STR];
2312 char *ref = NULL;
2313 FILE *pipe;
2314
2315 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2316 return TRUE;
2317
2318 pipe = popen(refbuf, "r");
2319 if (!pipe)
2320 return TRUE;
2321
2322 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2323 ref = chomp_string(ref);
2324 pclose(pipe);
2325
2326 if (!ref || !*ref)
2327 return TRUE;
2328
2329 /* This is the only fatal call, since it can "corrupt" the buffer. */
2330 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2331 return FALSE;
2332
2333 return TRUE;
2334 }
2335
2336 static void
2337 add_pager_refs(struct view *view, struct line *line)
2338 {
2339 char buf[SIZEOF_STR];
2340 char *commit_id = line->data + STRING_SIZE("commit ");
2341 struct ref **refs;
2342 size_t bufpos = 0, refpos = 0;
2343 const char *sep = "Refs: ";
2344 bool is_tag = FALSE;
2345
2346 assert(line->type == LINE_COMMIT);
2347
2348 refs = get_refs(commit_id);
2349 if (!refs) {
2350 if (view == VIEW(REQ_VIEW_DIFF))
2351 goto try_add_describe_ref;
2352 return;
2353 }
2354
2355 do {
2356 struct ref *ref = refs[refpos];
2357 char *fmt = ref->tag ? "%s[%s]" :
2358 ref->remote ? "%s<%s>" : "%s%s";
2359
2360 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2361 return;
2362 sep = ", ";
2363 if (ref->tag)
2364 is_tag = TRUE;
2365 } while (refs[refpos++]->next);
2366
2367 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2368 try_add_describe_ref:
2369 /* Add <tag>-g<commit_id> "fake" reference. */
2370 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2371 return;
2372 }
2373
2374 if (bufpos == 0)
2375 return;
2376
2377 if (!realloc_lines(view, view->line_size + 1))
2378 return;
2379
2380 line = &view->line[view->lines];
2381 line->data = strdup(buf);
2382 if (!line->data)
2383 return;
2384
2385 line->type = LINE_PP_REFS;
2386 view->lines++;
2387 }
2388
2389 static bool
2390 pager_read(struct view *view, char *data)
2391 {
2392 struct line *line = &view->line[view->lines];
2393
2394 line->data = strdup(data);
2395 if (!line->data)
2396 return FALSE;
2397
2398 line->type = get_line_type(line->data);
2399 view->lines++;
2400
2401 if (line->type == LINE_COMMIT &&
2402 (view == VIEW(REQ_VIEW_DIFF) ||
2403 view == VIEW(REQ_VIEW_LOG)))
2404 add_pager_refs(view, line);
2405
2406 return TRUE;
2407 }
2408
2409 static bool
2410 pager_enter(struct view *view, struct line *line)
2411 {
2412 int split = 0;
2413
2414 if (line->type == LINE_COMMIT &&
2415 (view == VIEW(REQ_VIEW_LOG) ||
2416 view == VIEW(REQ_VIEW_PAGER))) {
2417 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2418 split = 1;
2419 }
2420
2421 /* Always scroll the view even if it was split. That way
2422 * you can use Enter to scroll through the log view and
2423 * split open each commit diff. */
2424 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2425
2426 /* FIXME: A minor workaround. Scrolling the view will call report("")
2427 * but if we are scrolling a non-current view this won't properly
2428 * update the view title. */
2429 if (split)
2430 update_view_title(view);
2431
2432 return TRUE;
2433 }
2434
2435 static bool
2436 pager_grep(struct view *view, struct line *line)
2437 {
2438 regmatch_t pmatch;
2439 char *text = line->data;
2440
2441 if (!*text)
2442 return FALSE;
2443
2444 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2445 return FALSE;
2446
2447 return TRUE;
2448 }
2449
2450 static void
2451 pager_select(struct view *view, struct line *line)
2452 {
2453 if (line->type == LINE_COMMIT) {
2454 char *text = line->data;
2455
2456 string_copy(view->ref, text + STRING_SIZE("commit "));
2457 string_copy(ref_commit, view->ref);
2458 }
2459 }
2460
2461 static struct view_ops pager_ops = {
2462 "line",
2463 pager_draw,
2464 pager_read,
2465 pager_enter,
2466 pager_grep,
2467 pager_select,
2468 };
2469
2470
2471 /*
2472 * Tree backend
2473 */
2474
2475 /* Parse output from git-ls-tree(1):
2476 *
2477 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2478 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2479 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2480 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2481 */
2482
2483 #define SIZEOF_TREE_ATTR \
2484 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2485
2486 #define TREE_UP_FORMAT "040000 tree %s\t.."
2487
2488 static int
2489 tree_compare_entry(enum line_type type1, char *name1,
2490 enum line_type type2, char *name2)
2491 {
2492 if (type1 != type2) {
2493 if (type1 == LINE_TREE_DIR)
2494 return -1;
2495 return 1;
2496 }
2497
2498 return strcmp(name1, name2);
2499 }
2500
2501 static bool
2502 tree_read(struct view *view, char *text)
2503 {
2504 size_t textlen = strlen(text);
2505 char buf[SIZEOF_STR];
2506 unsigned long pos;
2507 enum line_type type;
2508 bool first_read = view->lines == 0;
2509
2510 if (textlen <= SIZEOF_TREE_ATTR)
2511 return FALSE;
2512
2513 type = text[STRING_SIZE("100644 ")] == 't'
2514 ? LINE_TREE_DIR : LINE_TREE_FILE;
2515
2516 if (first_read) {
2517 /* Add path info line */
2518 if (string_format(buf, "Directory path /%s", opt_path) &&
2519 realloc_lines(view, view->line_size + 1) &&
2520 pager_read(view, buf))
2521 view->line[view->lines - 1].type = LINE_DEFAULT;
2522 else
2523 return FALSE;
2524
2525 /* Insert "link" to parent directory. */
2526 if (*opt_path &&
2527 string_format(buf, TREE_UP_FORMAT, view->ref) &&
2528 realloc_lines(view, view->line_size + 1) &&
2529 pager_read(view, buf))
2530 view->line[view->lines - 1].type = LINE_TREE_DIR;
2531 else if (*opt_path)
2532 return FALSE;
2533 }
2534
2535 /* Strip the path part ... */
2536 if (*opt_path) {
2537 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2538 size_t striplen = strlen(opt_path);
2539 char *path = text + SIZEOF_TREE_ATTR;
2540
2541 if (pathlen > striplen)
2542 memmove(path, path + striplen,
2543 pathlen - striplen + 1);
2544 }
2545
2546 /* Skip "Directory ..." and ".." line. */
2547 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2548 struct line *line = &view->line[pos];
2549 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2550 char *path2 = text + SIZEOF_TREE_ATTR;
2551 int cmp = tree_compare_entry(line->type, path1, type, path2);
2552
2553 if (cmp <= 0)
2554 continue;
2555
2556 text = strdup(text);
2557 if (!text)
2558 return FALSE;
2559
2560 if (view->lines > pos)
2561 memmove(&view->line[pos + 1], &view->line[pos],
2562 (view->lines - pos) * sizeof(*line));
2563
2564 line = &view->line[pos];
2565 line->data = text;
2566 line->type = type;
2567 view->lines++;
2568 return TRUE;
2569 }
2570
2571 if (!pager_read(view, text))
2572 return FALSE;
2573
2574 /* Move the current line to the first tree entry. */
2575 if (first_read)
2576 view->lineno++;
2577
2578 view->line[view->lines - 1].type = type;
2579 return TRUE;
2580 }
2581
2582 static bool
2583 tree_enter(struct view *view, struct line *line)
2584 {
2585 enum open_flags flags;
2586 enum request request;
2587
2588 switch (line->type) {
2589 case LINE_TREE_DIR:
2590 /* Depending on whether it is a subdir or parent (updir?) link
2591 * mangle the path buffer. */
2592 if (line == &view->line[1] && *opt_path) {
2593 size_t path_len = strlen(opt_path);
2594 char *dirsep = opt_path + path_len - 1;
2595
2596 while (dirsep > opt_path && dirsep[-1] != '/')
2597 dirsep--;
2598
2599 dirsep[0] = 0;
2600
2601 } else {
2602 size_t pathlen = strlen(opt_path);
2603 size_t origlen = pathlen;
2604 char *data = line->data;
2605 char *basename = data + SIZEOF_TREE_ATTR;
2606
2607 if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
2608 opt_path[origlen] = 0;
2609 return TRUE;
2610 }
2611 }
2612
2613 /* Trees and subtrees share the same ID, so they are not not
2614 * unique like blobs. */
2615 flags = OPEN_RELOAD;
2616 request = REQ_VIEW_TREE;
2617 break;
2618
2619 case LINE_TREE_FILE:
2620 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2621 request = REQ_VIEW_BLOB;
2622 break;
2623
2624 default:
2625 return TRUE;
2626 }
2627
2628 open_view(view, request, flags);
2629
2630 return TRUE;
2631 }
2632
2633 static void
2634 tree_select(struct view *view, struct line *line)
2635 {
2636 char *text = line->data;
2637
2638 text += STRING_SIZE("100644 blob ");
2639
2640 if (line->type == LINE_TREE_FILE) {
2641 string_ncopy(ref_blob, text, 40);
2642 /* Also update the blob view's ref, since all there must always
2643 * be in sync. */
2644 string_copy(VIEW(REQ_VIEW_BLOB)->ref, ref_blob);
2645
2646 } else if (line->type != LINE_TREE_DIR) {
2647 return;
2648 }
2649
2650 string_ncopy(view->ref, text, 40);
2651 }
2652
2653 static struct view_ops tree_ops = {
2654 "file",
2655 pager_draw,
2656 tree_read,
2657 tree_enter,
2658 pager_grep,
2659 tree_select,
2660 };
2661
2662 static bool
2663 blob_read(struct view *view, char *line)
2664 {
2665 bool state = pager_read(view, line);
2666
2667 if (state == TRUE)
2668 view->line[view->lines - 1].type = LINE_DEFAULT;
2669
2670 return state;
2671 }
2672
2673 static struct view_ops blob_ops = {
2674 "line",
2675 pager_draw,
2676 blob_read,
2677 pager_enter,
2678 pager_grep,
2679 pager_select,
2680 };
2681
2682
2683 /*
2684 * Main view backend
2685 */
2686
2687 struct commit {
2688 char id[SIZEOF_REV]; /* SHA1 ID. */
2689 char title[128]; /* First line of the commit message. */
2690 char author[75]; /* Author of the commit. */
2691 struct tm time; /* Date from the author ident. */
2692 struct ref **refs; /* Repository references. */
2693 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
2694 size_t graph_size; /* The width of the graph array. */
2695 };
2696
2697 static bool
2698 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2699 {
2700 char buf[DATE_COLS + 1];
2701 struct commit *commit = line->data;
2702 enum line_type type;
2703 int col = 0;
2704 size_t timelen;
2705 size_t authorlen;
2706 int trimmed = 1;
2707
2708 if (!*commit->author)
2709 return FALSE;
2710
2711 wmove(view->win, lineno, col);
2712
2713 if (selected) {
2714 type = LINE_CURSOR;
2715 wattrset(view->win, get_line_attr(type));
2716 wchgat(view->win, -1, 0, type, NULL);
2717
2718 } else {
2719 type = LINE_MAIN_COMMIT;
2720 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
2721 }
2722
2723 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
2724 waddnstr(view->win, buf, timelen);
2725 waddstr(view->win, " ");
2726
2727 col += DATE_COLS;
2728 wmove(view->win, lineno, col);
2729 if (type != LINE_CURSOR)
2730 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
2731
2732 if (opt_utf8) {
2733 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
2734 } else {
2735 authorlen = strlen(commit->author);
2736 if (authorlen > AUTHOR_COLS - 2) {
2737 authorlen = AUTHOR_COLS - 2;
2738 trimmed = 1;
2739 }
2740 }
2741
2742 if (trimmed) {
2743 waddnstr(view->win, commit->author, authorlen);
2744 if (type != LINE_CURSOR)
2745 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
2746 waddch(view->win, '~');
2747 } else {
2748 waddstr(view->win, commit->author);
2749 }
2750
2751 col += AUTHOR_COLS;
2752 if (type != LINE_CURSOR)
2753 wattrset(view->win, A_NORMAL);
2754
2755 if (opt_rev_graph && commit->graph_size) {
2756 size_t i;
2757
2758 wmove(view->win, lineno, col);
2759 /* Using waddch() instead of waddnstr() ensures that
2760 * they'll be rendered correctly for the cursor line. */
2761 for (i = 0; i < commit->graph_size; i++)
2762 waddch(view->win, commit->graph[i]);
2763
2764 col += commit->graph_size + 1;
2765 }
2766
2767 wmove(view->win, lineno, col);
2768
2769 if (commit->refs) {
2770 size_t i = 0;
2771
2772 do {
2773 if (type == LINE_CURSOR)
2774 ;
2775 else if (commit->refs[i]->tag)
2776 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
2777 else if (commit->refs[i]->remote)
2778 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
2779 else
2780 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
2781 waddstr(view->win, "[");
2782 waddstr(view->win, commit->refs[i]->name);
2783 waddstr(view->win, "]");
2784 if (type != LINE_CURSOR)
2785 wattrset(view->win, A_NORMAL);
2786 waddstr(view->win, " ");
2787 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
2788 } while (commit->refs[i++]->next);
2789 }
2790
2791 if (type != LINE_CURSOR)
2792 wattrset(view->win, get_line_attr(type));
2793
2794 {
2795 int titlelen = strlen(commit->title);
2796
2797 if (col + titlelen > view->width)
2798 titlelen = view->width - col;
2799
2800 waddnstr(view->win, commit->title, titlelen);
2801 }
2802
2803 return TRUE;
2804 }
2805
2806 /* Reads git log --pretty=raw output and parses it into the commit struct. */
2807 static bool
2808 main_read(struct view *view, char *line)
2809 {
2810 enum line_type type = get_line_type(line);
2811 struct commit *commit = view->lines
2812 ? view->line[view->lines - 1].data : NULL;
2813
2814 switch (type) {
2815 case LINE_COMMIT:
2816 commit = calloc(1, sizeof(struct commit));
2817 if (!commit)
2818 return FALSE;
2819
2820 line += STRING_SIZE("commit ");
2821
2822 view->line[view->lines++].data = commit;
2823 string_copy(commit->id, line);
2824 commit->refs = get_refs(commit->id);
2825 commit->graph[commit->graph_size++] = ACS_LTEE;
2826 break;
2827
2828 case LINE_AUTHOR:
2829 {
2830 /* Parse author lines where the name may be empty:
2831 * author <email@address.tld> 1138474660 +0100
2832 */
2833 char *ident = line + STRING_SIZE("author ");
2834 char *nameend = strchr(ident, '<');
2835 char *emailend = strchr(ident, '>');
2836
2837 if (!commit || !nameend || !emailend)
2838 break;
2839
2840 *nameend = *emailend = 0;
2841 ident = chomp_string(ident);
2842 if (!*ident) {
2843 ident = chomp_string(nameend + 1);
2844 if (!*ident)
2845 ident = "Unknown";
2846 }
2847
2848 string_copy(commit->author, ident);
2849
2850 /* Parse epoch and timezone */
2851 if (emailend[1] == ' ') {
2852 char *secs = emailend + 2;
2853 char *zone = strchr(secs, ' ');
2854 time_t time = (time_t) atol(secs);
2855
2856 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
2857 long tz;
2858
2859 zone++;
2860 tz = ('0' - zone[1]) * 60 * 60 * 10;
2861 tz += ('0' - zone[2]) * 60 * 60;
2862 tz += ('0' - zone[3]) * 60;
2863 tz += ('0' - zone[4]) * 60;
2864
2865 if (zone[0] == '-')
2866 tz = -tz;
2867
2868 time -= tz;
2869 }
2870
2871 gmtime_r(&time, &commit->time);
2872 }
2873 break;
2874 }
2875 default:
2876 if (!commit)
2877 break;
2878
2879 /* Fill in the commit title if it has not already been set. */
2880 if (commit->title[0])
2881 break;
2882
2883 /* Require titles to start with a non-space character at the
2884 * offset used by git log. */
2885 if (strncmp(line, " ", 4))
2886 break;
2887 line += 4;
2888 /* Well, if the title starts with a whitespace character,
2889 * try to be forgiving. Otherwise we end up with no title. */
2890 while (isspace(*line))
2891 line++;
2892 if (*line == '\0')
2893 break;
2894 /* FIXME: More graceful handling of titles; append "..." to
2895 * shortened titles, etc. */
2896
2897 string_copy(commit->title, line);
2898 }
2899
2900 return TRUE;
2901 }
2902
2903 static bool
2904 main_enter(struct view *view, struct line *line)
2905 {
2906 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2907
2908 open_view(view, REQ_VIEW_DIFF, flags);
2909 return TRUE;
2910 }
2911
2912 static bool
2913 main_grep(struct view *view, struct line *line)
2914 {
2915 struct commit *commit = line->data;
2916 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
2917 char buf[DATE_COLS + 1];
2918 regmatch_t pmatch;
2919
2920 for (state = S_TITLE; state < S_END; state++) {
2921 char *text;
2922
2923 switch (state) {
2924 case S_TITLE: text = commit->title; break;
2925 case S_AUTHOR: text = commit->author; break;
2926 case S_DATE:
2927 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
2928 continue;
2929 text = buf;
2930 break;
2931
2932 default:
2933 return FALSE;
2934 }
2935
2936 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
2937 return TRUE;
2938 }
2939
2940 return FALSE;
2941 }
2942
2943 static void
2944 main_select(struct view *view, struct line *line)
2945 {
2946 struct commit *commit = line->data;
2947
2948 string_copy(view->ref, commit->id);
2949 string_copy(ref_commit, view->ref);
2950 }
2951
2952 static struct view_ops main_ops = {
2953 "commit",
2954 main_draw,
2955 main_read,
2956 main_enter,
2957 main_grep,
2958 main_select,
2959 };
2960
2961
2962 /*
2963 * Unicode / UTF-8 handling
2964 *
2965 * NOTE: Much of the following code for dealing with unicode is derived from
2966 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2967 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2968 */
2969
2970 /* I've (over)annotated a lot of code snippets because I am not entirely
2971 * confident that the approach taken by this small UTF-8 interface is correct.
2972 * --jonas */
2973
2974 static inline int
2975 unicode_width(unsigned long c)
2976 {
2977 if (c >= 0x1100 &&
2978 (c <= 0x115f /* Hangul Jamo */
2979 || c == 0x2329
2980 || c == 0x232a
2981 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
2982 /* CJK ... Yi */
2983 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2984 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2985 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2986 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2987 || (c >= 0xffe0 && c <= 0xffe6)
2988 || (c >= 0x20000 && c <= 0x2fffd)
2989 || (c >= 0x30000 && c <= 0x3fffd)))
2990 return 2;
2991
2992 return 1;
2993 }
2994
2995 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2996 * Illegal bytes are set one. */
2997 static const unsigned char utf8_bytes[256] = {
2998 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,
2999 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,
3000 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,
3001 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,
3002 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,
3003 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,
3004 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,
3005 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,
3006 };
3007
3008 /* Decode UTF-8 multi-byte representation into a unicode character. */
3009 static inline unsigned long
3010 utf8_to_unicode(const char *string, size_t length)
3011 {
3012 unsigned long unicode;
3013
3014 switch (length) {
3015 case 1:
3016 unicode = string[0];
3017 break;
3018 case 2:
3019 unicode = (string[0] & 0x1f) << 6;
3020 unicode += (string[1] & 0x3f);
3021 break;
3022 case 3:
3023 unicode = (string[0] & 0x0f) << 12;
3024 unicode += ((string[1] & 0x3f) << 6);
3025 unicode += (string[2] & 0x3f);
3026 break;
3027 case 4:
3028 unicode = (string[0] & 0x0f) << 18;
3029 unicode += ((string[1] & 0x3f) << 12);
3030 unicode += ((string[2] & 0x3f) << 6);
3031 unicode += (string[3] & 0x3f);
3032 break;
3033 case 5:
3034 unicode = (string[0] & 0x0f) << 24;
3035 unicode += ((string[1] & 0x3f) << 18);
3036 unicode += ((string[2] & 0x3f) << 12);
3037 unicode += ((string[3] & 0x3f) << 6);
3038 unicode += (string[4] & 0x3f);
3039 break;
3040 case 6:
3041 unicode = (string[0] & 0x01) << 30;
3042 unicode += ((string[1] & 0x3f) << 24);
3043 unicode += ((string[2] & 0x3f) << 18);
3044 unicode += ((string[3] & 0x3f) << 12);
3045 unicode += ((string[4] & 0x3f) << 6);
3046 unicode += (string[5] & 0x3f);
3047 break;
3048 default:
3049 die("Invalid unicode length");
3050 }
3051
3052 /* Invalid characters could return the special 0xfffd value but NUL
3053 * should be just as good. */
3054 return unicode > 0xffff ? 0 : unicode;
3055 }
3056
3057 /* Calculates how much of string can be shown within the given maximum width
3058 * and sets trimmed parameter to non-zero value if all of string could not be
3059 * shown.
3060 *
3061 * Additionally, adds to coloffset how many many columns to move to align with
3062 * the expected position. Takes into account how multi-byte and double-width
3063 * characters will effect the cursor position.
3064 *
3065 * Returns the number of bytes to output from string to satisfy max_width. */
3066 static size_t
3067 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3068 {
3069 const char *start = string;
3070 const char *end = strchr(string, '\0');
3071 size_t mbwidth = 0;
3072 size_t width = 0;
3073
3074 *trimmed = 0;
3075
3076 while (string < end) {
3077 int c = *(unsigned char *) string;
3078 unsigned char bytes = utf8_bytes[c];
3079 size_t ucwidth;
3080 unsigned long unicode;
3081
3082 if (string + bytes > end)
3083 break;
3084
3085 /* Change representation to figure out whether
3086 * it is a single- or double-width character. */
3087
3088 unicode = utf8_to_unicode(string, bytes);
3089 /* FIXME: Graceful handling of invalid unicode character. */
3090 if (!unicode)
3091 break;
3092
3093 ucwidth = unicode_width(unicode);
3094 width += ucwidth;
3095 if (width > max_width) {
3096 *trimmed = 1;
3097 break;
3098 }
3099
3100 /* The column offset collects the differences between the
3101 * number of bytes encoding a character and the number of
3102 * columns will be used for rendering said character.
3103 *
3104 * So if some character A is encoded in 2 bytes, but will be
3105 * represented on the screen using only 1 byte this will and up
3106 * adding 1 to the multi-byte column offset.
3107 *
3108 * Assumes that no double-width character can be encoding in
3109 * less than two bytes. */
3110 if (bytes > ucwidth)
3111 mbwidth += bytes - ucwidth;
3112
3113 string += bytes;
3114 }
3115
3116 *coloffset += mbwidth;
3117
3118 return string - start;
3119 }
3120
3121
3122 /*
3123 * Status management
3124 */
3125
3126 /* Whether or not the curses interface has been initialized. */
3127 static bool cursed = FALSE;
3128
3129 /* The status window is used for polling keystrokes. */
3130 static WINDOW *status_win;
3131
3132 static bool status_empty = TRUE;
3133
3134 /* Update status and title window. */
3135 static void
3136 report(const char *msg, ...)
3137 {
3138 struct view *view = display[current_view];
3139
3140 if (input_mode)
3141 return;
3142
3143 if (!status_empty || *msg) {
3144 va_list args;
3145
3146 va_start(args, msg);
3147
3148 wmove(status_win, 0, 0);
3149 if (*msg) {
3150 vwprintw(status_win, msg, args);
3151 status_empty = FALSE;
3152 } else {
3153 status_empty = TRUE;
3154 }
3155 wclrtoeol(status_win);
3156 wrefresh(status_win);
3157
3158 va_end(args);
3159 }
3160
3161 update_view_title(view);
3162 update_display_cursor(view);
3163 }
3164
3165 /* Controls when nodelay should be in effect when polling user input. */
3166 static void
3167 set_nonblocking_input(bool loading)
3168 {
3169 static unsigned int loading_views;
3170
3171 if ((loading == FALSE && loading_views-- == 1) ||
3172 (loading == TRUE && loading_views++ == 0))
3173 nodelay(status_win, loading);
3174 }
3175
3176 static void
3177 init_display(void)
3178 {
3179 int x, y;
3180
3181 /* Initialize the curses library */
3182 if (isatty(STDIN_FILENO)) {
3183 cursed = !!initscr();
3184 } else {
3185 /* Leave stdin and stdout alone when acting as a pager. */
3186 FILE *io = fopen("/dev/tty", "r+");
3187
3188 if (!io)
3189 die("Failed to open /dev/tty");
3190 cursed = !!newterm(NULL, io, io);
3191 }
3192
3193 if (!cursed)
3194 die("Failed to initialize curses");
3195
3196 nonl(); /* Tell curses not to do NL->CR/NL on output */
3197 cbreak(); /* Take input chars one at a time, no wait for \n */
3198 noecho(); /* Don't echo input */
3199 leaveok(stdscr, TRUE);
3200
3201 if (has_colors())
3202 init_colors();
3203
3204 getmaxyx(stdscr, y, x);
3205 status_win = newwin(1, 0, y - 1, 0);
3206 if (!status_win)
3207 die("Failed to create status window");
3208
3209 /* Enable keyboard mapping */
3210 keypad(status_win, TRUE);
3211 wbkgdset(status_win, get_line_attr(LINE_STATUS));
3212 }
3213
3214 static char *
3215 read_prompt(const char *prompt)
3216 {
3217 enum { READING, STOP, CANCEL } status = READING;
3218 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
3219 int pos = 0;
3220
3221 while (status == READING) {
3222 struct view *view;
3223 int i, key;
3224
3225 input_mode = TRUE;
3226
3227 foreach_view (view, i)
3228 update_view(view);
3229
3230 input_mode = FALSE;
3231
3232 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
3233 wclrtoeol(status_win);
3234
3235 /* Refresh, accept single keystroke of input */
3236 key = wgetch(status_win);
3237 switch (key) {
3238 case KEY_RETURN:
3239 case KEY_ENTER:
3240 case '\n':
3241 status = pos ? STOP : CANCEL;
3242 break;
3243
3244 case KEY_BACKSPACE:
3245 if (pos > 0)
3246 pos--;
3247 else
3248 status = CANCEL;
3249 break;
3250
3251 case KEY_ESC:
3252 status = CANCEL;
3253 break;
3254
3255 case ERR:
3256 break;
3257
3258 default:
3259 if (pos >= sizeof(buf)) {
3260 report("Input string too long");
3261 return NULL;
3262 }
3263
3264 if (isprint(key))
3265 buf[pos++] = (char) key;
3266 }
3267 }
3268
3269 /* Clear the status window */
3270 status_empty = FALSE;
3271 report("");
3272
3273 if (status == CANCEL)
3274 return NULL;
3275
3276 buf[pos++] = 0;
3277
3278 return buf;
3279 }
3280
3281 /*
3282 * Repository references
3283 */
3284
3285 static struct ref *refs;
3286 static size_t refs_size;
3287
3288 /* Id <-> ref store */
3289 static struct ref ***id_refs;
3290 static size_t id_refs_size;
3291
3292 static struct ref **
3293 get_refs(char *id)
3294 {
3295 struct ref ***tmp_id_refs;
3296 struct ref **ref_list = NULL;
3297 size_t ref_list_size = 0;
3298 size_t i;
3299
3300 for (i = 0; i < id_refs_size; i++)
3301 if (!strcmp(id, id_refs[i][0]->id))
3302 return id_refs[i];
3303
3304 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
3305 if (!tmp_id_refs)
3306 return NULL;
3307
3308 id_refs = tmp_id_refs;
3309
3310 for (i = 0; i < refs_size; i++) {
3311 struct ref **tmp;
3312
3313 if (strcmp(id, refs[i].id))
3314 continue;
3315
3316 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
3317 if (!tmp) {
3318 if (ref_list)
3319 free(ref_list);
3320 return NULL;
3321 }
3322
3323 ref_list = tmp;
3324 if (ref_list_size > 0)
3325 ref_list[ref_list_size - 1]->next = 1;
3326 ref_list[ref_list_size] = &refs[i];
3327
3328 /* XXX: The properties of the commit chains ensures that we can
3329 * safely modify the shared ref. The repo references will
3330 * always be similar for the same id. */
3331 ref_list[ref_list_size]->next = 0;
3332 ref_list_size++;
3333 }
3334
3335 if (ref_list)
3336 id_refs[id_refs_size++] = ref_list;
3337
3338 return ref_list;
3339 }
3340
3341 static int
3342 read_ref(char *id, int idlen, char *name, int namelen)
3343 {
3344 struct ref *ref;
3345 bool tag = FALSE;
3346 bool remote = FALSE;
3347
3348 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
3349 /* Commits referenced by tags has "^{}" appended. */
3350 if (name[namelen - 1] != '}')
3351 return OK;
3352
3353 while (namelen > 0 && name[namelen] != '^')
3354 namelen--;
3355
3356 tag = TRUE;
3357 namelen -= STRING_SIZE("refs/tags/");
3358 name += STRING_SIZE("refs/tags/");
3359
3360 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
3361 remote = TRUE;
3362 namelen -= STRING_SIZE("refs/remotes/");
3363 name += STRING_SIZE("refs/remotes/");
3364
3365 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
3366 namelen -= STRING_SIZE("refs/heads/");
3367 name += STRING_SIZE("refs/heads/");
3368
3369 } else if (!strcmp(name, "HEAD")) {
3370 return OK;
3371 }
3372
3373 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
3374 if (!refs)
3375 return ERR;
3376
3377 ref = &refs[refs_size++];
3378 ref->name = malloc(namelen + 1);
3379 if (!ref->name)
3380 return ERR;
3381
3382 strncpy(ref->name, name, namelen);
3383 ref->name[namelen] = 0;
3384 ref->tag = tag;
3385 ref->remote = remote;
3386 string_copy(ref->id, id);
3387
3388 return OK;
3389 }
3390
3391 static int
3392 load_refs(void)
3393 {
3394 const char *cmd_env = getenv("TIG_LS_REMOTE");
3395 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
3396
3397 return read_properties(popen(cmd, "r"), "\t", read_ref);
3398 }
3399
3400 static int
3401 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
3402 {
3403 if (!strcmp(name, "i18n.commitencoding"))
3404 string_copy(opt_encoding, value);
3405
3406 return OK;
3407 }
3408
3409 static int
3410 load_repo_config(void)
3411 {
3412 return read_properties(popen("git repo-config --list", "r"),
3413 "=", read_repo_config_option);
3414 }
3415
3416 static int
3417 read_properties(FILE *pipe, const char *separators,
3418 int (*read_property)(char *, int, char *, int))
3419 {
3420 char buffer[BUFSIZ];
3421 char *name;
3422 int state = OK;
3423
3424 if (!pipe)
3425 return ERR;
3426
3427 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
3428 char *value;
3429 size_t namelen;
3430 size_t valuelen;
3431
3432 name = chomp_string(name);
3433 namelen = strcspn(name, separators);
3434
3435 if (name[namelen]) {
3436 name[namelen] = 0;
3437 value = chomp_string(name + namelen + 1);
3438 valuelen = strlen(value);
3439
3440 } else {
3441 value = "";
3442 valuelen = 0;
3443 }
3444
3445 state = read_property(name, namelen, value, valuelen);
3446 }
3447
3448 if (state != ERR && ferror(pipe))
3449 state = ERR;
3450
3451 pclose(pipe);
3452
3453 return state;
3454 }
3455
3456
3457 /*
3458 * Main
3459 */
3460
3461 static void __NORETURN
3462 quit(int sig)
3463 {
3464 /* XXX: Restore tty modes and let the OS cleanup the rest! */
3465 if (cursed)
3466 endwin();
3467 exit(0);
3468 }
3469
3470 static void __NORETURN
3471 die(const char *err, ...)
3472 {
3473 va_list args;
3474
3475 endwin();
3476
3477 va_start(args, err);
3478 fputs("tig: ", stderr);
3479 vfprintf(stderr, err, args);
3480 fputs("\n", stderr);
3481 va_end(args);
3482
3483 exit(1);
3484 }
3485
3486 int
3487 main(int argc, char *argv[])
3488 {
3489 struct view *view;
3490 enum request request;
3491 size_t i;
3492
3493 signal(SIGINT, quit);
3494
3495 if (setlocale(LC_ALL, "")) {
3496 string_copy(opt_codeset, nl_langinfo(CODESET));
3497 }
3498
3499 if (load_options() == ERR)
3500 die("Failed to load user config.");
3501
3502 /* Load the repo config file so options can be overwritten from
3503 * the command line. */
3504 if (load_repo_config() == ERR)
3505 die("Failed to load repo config.");
3506
3507 if (!parse_options(argc, argv))
3508 return 0;
3509
3510 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
3511 opt_iconv = iconv_open(opt_codeset, opt_encoding);
3512 if (opt_iconv == ICONV_NONE)
3513 die("Failed to initialize character set conversion");
3514 }
3515
3516 if (load_refs() == ERR)
3517 die("Failed to load refs.");
3518
3519 /* Require a git repository unless when running in pager mode. */
3520 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
3521 die("Not a git repository");
3522
3523 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
3524 view->cmd_env = getenv(view->cmd_env);
3525
3526 request = opt_request;
3527
3528 init_display();
3529
3530 while (view_driver(display[current_view], request)) {
3531 int key;
3532 int i;
3533
3534 foreach_view (view, i)
3535 update_view(view);
3536
3537 /* Refresh, accept single keystroke of input */
3538 key = wgetch(status_win);
3539
3540 /* wgetch() with nodelay() enabled returns ERR when there's no
3541 * input. */
3542 if (key == ERR) {
3543 request = REQ_NONE;
3544 continue;
3545 }
3546
3547 request = get_keybinding(display[current_view]->keymap, key);
3548
3549 /* Some low-level request handling. This keeps access to
3550 * status_win restricted. */
3551 switch (request) {
3552 case REQ_PROMPT:
3553 {
3554 char *cmd = read_prompt(":");
3555
3556 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
3557 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
3558 opt_request = REQ_VIEW_DIFF;
3559 } else {
3560 opt_request = REQ_VIEW_PAGER;
3561 }
3562 break;
3563 }
3564
3565 request = REQ_NONE;
3566 break;
3567 }
3568 case REQ_SEARCH:
3569 case REQ_SEARCH_BACK:
3570 {
3571 const char *prompt = request == REQ_SEARCH
3572 ? "/" : "?";
3573 char *search = read_prompt(prompt);
3574
3575 if (search)
3576 string_copy(opt_search, search);
3577 else
3578 request = REQ_NONE;
3579 break;
3580 }
3581 case REQ_SCREEN_RESIZE:
3582 {
3583 int height, width;
3584
3585 getmaxyx(stdscr, height, width);
3586
3587 /* Resize the status view and let the view driver take
3588 * care of resizing the displayed views. */
3589 wresize(status_win, 1, width);
3590 mvwin(status_win, height - 1, 0);
3591 wrefresh(status_win);
3592 break;
3593 }
3594 default:
3595 break;
3596 }
3597 }
3598
3599 quit(0);
3600
3601 return 0;
3602 }