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