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