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