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