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