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