Simplify the update_rev_graph end-game
[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
9e43b9cd
JF
2810static inline bool
2811graph_parent_is_merge(struct rev_stack *graph)
2812{
2813 return graph->parents->size > 1;
2814}
2815
c65a501a
JF
2816static void
2817reset_rev_graph(struct rev_stack *graph)
2818{
2819 graph->size = graph->pos = 0;
2820 graph->commit = NULL;
2821 memset(graph->parents, 0, sizeof(*graph->parents));
2822}
2b757533 2823
88757ebd
JF
2824static inline void
2825append_to_rev_graph(struct rev_stack *stack, chtype symbol)
2826{
c65a501a
JF
2827 if (stack->commit->graph_size < ARRAY_SIZE(stack->commit->graph) - 1)
2828 stack->commit->graph[stack->commit->graph_size++] = symbol;
88757ebd
JF
2829}
2830
2b757533
JF
2831static void
2832push_rev_stack(struct rev_stack *stack, char *parent)
2833{
2b757533
JF
2834 /* Combine duplicate parents lines. */
2835 if (stack->size > 0 &&
2836 !strncmp(stack->rev[stack->size - 1], parent, SIZEOF_REV))
2837 return;
2838
2839 if (stack->size < SIZEOF_REVITEMS) {
2840 string_ncopy(stack->rev[stack->size++], parent, SIZEOF_REV);
2841 }
2842}
2843
b5d8f208 2844static void
c65a501a 2845draw_rev_graph(struct rev_stack *graph)
2b757533 2846{
110e948e 2847 chtype symbol, separator, line;
2b757533
JF
2848 size_t i;
2849
2b757533 2850 /* Place the symbol for this commit. */
c65a501a 2851 if (graph->parents->size == 0)
c8d60a25 2852 symbol = REVGRAPH_INIT;
c65a501a 2853 else if (graph->parents->size > 1)
c8d60a25 2854 symbol = REVGRAPH_MERGE;
c65a501a 2855 else if (graph->pos >= graph->size)
c8d60a25 2856 symbol = REVGRAPH_BRANCH;
2b757533 2857 else
c8d60a25 2858 symbol = REVGRAPH_COMMIT;
1dcb3bec 2859
110e948e 2860 separator = ' ';
c8d60a25 2861 line = REVGRAPH_LINE;
110e948e 2862
c65a501a
JF
2863 for (i = 0; i < graph->pos; i++) {
2864 append_to_rev_graph(graph, line);
9e43b9cd 2865 if (graph_parent_is_merge(graph->prev) &&
c65a501a 2866 graph->prev->pos == i) {
110e948e
JF
2867 separator = '`';
2868 line = '.';
2869 }
c65a501a 2870 append_to_rev_graph(graph, separator);
110e948e
JF
2871 }
2872
c65a501a 2873 append_to_rev_graph(graph, symbol);
2b757533 2874
c65a501a
JF
2875 if (graph->prev->size > graph->size) {
2876 separator = '\'';
2877 line = ' ';
2878 } else {
2879 separator = ' ';
2880 line = REVGRAPH_LINE;
2881 }
c8d60a25 2882 i++;
2b757533 2883
c65a501a
JF
2884 for (; i < graph->size; i++) {
2885 append_to_rev_graph(graph, separator);
2886 append_to_rev_graph(graph, line);
9e43b9cd 2887 if (graph_parent_is_merge(graph->prev)) {
c65a501a 2888 if (i < graph->prev->pos + graph->parents->size) {
c8d60a25
JF
2889 separator = '`';
2890 line = '.';
2891 }
110e948e 2892 }
c65a501a
JF
2893 if (graph->prev->size > graph->size) {
2894 separator = '/';
2895 line = ' ';
2896 }
2897 }
2898
2899 if (graph->prev->size > graph->size) {
2900 append_to_rev_graph(graph, separator);
2901 if (line != ' ')
2902 append_to_rev_graph(graph, line);
2b757533 2903 }
b5d8f208
JF
2904}
2905
2906void
c65a501a 2907update_rev_graph(struct rev_stack *graph)
b5d8f208 2908{
b5d8f208
JF
2909 size_t i;
2910
b5d8f208 2911 /* First traverse all lines of revisions up to the active one. */
c65a501a
JF
2912 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
2913 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
b5d8f208 2914 break;
b5d8f208 2915
c65a501a 2916 push_rev_stack(graph->next, graph->rev[graph->pos]);
b5d8f208
JF
2917 }
2918
c65a501a
JF
2919 for (i = 0; i < graph->parents->size; i++)
2920 push_rev_stack(graph->next, graph->parents->rev[i]);
b5d8f208 2921
b5d8f208 2922 /* FIXME: Moving branches left and right when collapsing a branch. */
c65a501a
JF
2923 for (i = graph->pos + 1; i < graph->size; i++)
2924 push_rev_stack(graph->next, graph->rev[i]);
2925
2926 draw_rev_graph(graph);
9e43b9cd 2927
375650cf
JF
2928 graph = graph->prev;
2929
2930 if (graph_parent_is_merge(graph) &&
2931 graph->pos < graph->size - 1 &&
2932 graph->next->size == graph->size + graph->parents->size - 1) {
2933 i = graph->pos + graph->parents->size - 1;
2934 graph->commit->graph_size = i * 2;
2935 while (i < graph->next->size - 1) {
2936 append_to_rev_graph(graph, ' ');
2937 append_to_rev_graph(graph, '\\');
c65a501a
JF
2938 i++;
2939 }
2940 }
9e43b9cd 2941
375650cf 2942 reset_rev_graph(graph);
2b757533
JF
2943}
2944
4c6fabc2 2945/* Reads git log --pretty=raw output and parses it into the commit struct. */
6b161b31 2946static bool
701e4f5d 2947main_read(struct view *view, char *line)
22f66b0a 2948{
c65a501a 2949 static struct rev_stack *graph = graph_stacks;
78c70acd 2950 enum line_type type = get_line_type(line);
701e4f5d
JF
2951 struct commit *commit = view->lines
2952 ? view->line[view->lines - 1].data : NULL;
22f66b0a 2953
78c70acd
JF
2954 switch (type) {
2955 case LINE_COMMIT:
22f66b0a
JF
2956 commit = calloc(1, sizeof(struct commit));
2957 if (!commit)
2958 return FALSE;
2959
4c6fabc2 2960 line += STRING_SIZE("commit ");
b76c2afc 2961
fe7233c3 2962 view->line[view->lines++].data = commit;
82e78006 2963 string_copy(commit->id, line);
c34d9c9f 2964 commit->refs = get_refs(commit->id);
c65a501a 2965 graph->commit = commit;
2b757533
JF
2966 break;
2967
2968 case LINE_PARENT:
2969 if (commit) {
2970 line += STRING_SIZE("parent ");
c65a501a 2971 push_rev_stack(graph->parents, line);
2b757533 2972 }
78c70acd 2973 break;
22f66b0a 2974
8855ada4 2975 case LINE_AUTHOR:
b76c2afc 2976 {
4c6fabc2 2977 char *ident = line + STRING_SIZE("author ");
b76c2afc
JF
2978 char *end = strchr(ident, '<');
2979
701e4f5d 2980 if (!commit)
fe7233c3
JF
2981 break;
2982
c65a501a
JF
2983 update_rev_graph(graph);
2984 graph = graph->next;
2b757533 2985
b76c2afc 2986 if (end) {
cbbf2d1b
JF
2987 char *email = end + 1;
2988
b76c2afc 2989 for (; end > ident && isspace(end[-1]); end--) ;
cbbf2d1b
JF
2990
2991 if (end == ident && *email) {
2992 ident = email;
2993 end = strchr(ident, '>');
2994 for (; end > ident && isspace(end[-1]); end--) ;
2995 }
b76c2afc
JF
2996 *end = 0;
2997 }
2998
cbbf2d1b
JF
2999 /* End is NULL or ident meaning there's no author. */
3000 if (end <= ident)
3001 ident = "Unknown";
3002
82e78006 3003 string_copy(commit->author, ident);
b76c2afc 3004
4c6fabc2 3005 /* Parse epoch and timezone */
b76c2afc
JF
3006 if (end) {
3007 char *secs = strchr(end + 1, '>');
3008 char *zone;
3009 time_t time;
3010
3011 if (!secs || secs[1] != ' ')
3012 break;
3013
3014 secs += 2;
3015 time = (time_t) atol(secs);
3016 zone = strchr(secs, ' ');
4c6fabc2 3017 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
b76c2afc
JF
3018 long tz;
3019
3020 zone++;
3021 tz = ('0' - zone[1]) * 60 * 60 * 10;
3022 tz += ('0' - zone[2]) * 60 * 60;
3023 tz += ('0' - zone[3]) * 60;
3024 tz += ('0' - zone[4]) * 60;
3025
3026 if (zone[0] == '-')
3027 tz = -tz;
3028
3029 time -= tz;
3030 }
3031 gmtime_r(&time, &commit->time);
3032 }
3033 break;
3034 }
78c70acd 3035 default:
701e4f5d 3036 if (!commit)
2e8488b4
JF
3037 break;
3038
3039 /* Fill in the commit title if it has not already been set. */
2e8488b4
JF
3040 if (commit->title[0])
3041 break;
3042
3043 /* Require titles to start with a non-space character at the
3044 * offset used by git log. */
eb98559e
JF
3045 /* FIXME: More gracefull handling of titles; append "..." to
3046 * shortened titles, etc. */
2e8488b4 3047 if (strncmp(line, " ", 4) ||
eb98559e 3048 isspace(line[4]))
82e78006
JF
3049 break;
3050
3051 string_copy(commit->title, line + 4);
22f66b0a
JF
3052 }
3053
3054 return TRUE;
3055}
3056
6b161b31 3057static bool
fe7233c3 3058main_enter(struct view *view, struct line *line)
b801d8b2 3059{
b3a54cba
JF
3060 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3061
3062 open_view(view, REQ_VIEW_DIFF, flags);
6b161b31 3063 return TRUE;
b801d8b2
JF
3064}
3065
4af34daa
JF
3066static bool
3067main_grep(struct view *view, struct line *line)
3068{
3069 struct commit *commit = line->data;
3070 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
3071 char buf[DATE_COLS + 1];
3072 regmatch_t pmatch;
3073
3074 for (state = S_TITLE; state < S_END; state++) {
3075 char *text;
3076
3077 switch (state) {
3078 case S_TITLE: text = commit->title; break;
3079 case S_AUTHOR: text = commit->author; break;
3080 case S_DATE:
3081 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
3082 continue;
3083 text = buf;
3084 break;
3085
3086 default:
3087 return FALSE;
3088 }
3089
b77b2cb8 3090 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4af34daa
JF
3091 return TRUE;
3092 }
3093
3094 return FALSE;
3095}
3096
d720de4b
JF
3097static void
3098main_select(struct view *view, struct line *line)
3099{
3100 struct commit *commit = line->data;
3101
3102 string_copy(view->ref, commit->id);
3103 string_copy(ref_commit, view->ref);
3104}
3105
6b161b31 3106static struct view_ops main_ops = {
6734f6b9 3107 "commit",
6b161b31
JF
3108 main_draw,
3109 main_read,
3110 main_enter,
4af34daa 3111 main_grep,
d720de4b 3112 main_select,
6b161b31 3113};
2e8488b4 3114
c34d9c9f 3115
6b161b31 3116/*
10e290ee
JF
3117 * Unicode / UTF-8 handling
3118 *
3119 * NOTE: Much of the following code for dealing with unicode is derived from
3120 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
3121 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
3122 */
3123
3124/* I've (over)annotated a lot of code snippets because I am not entirely
3125 * confident that the approach taken by this small UTF-8 interface is correct.
3126 * --jonas */
3127
3128static inline int
3129unicode_width(unsigned long c)
3130{
3131 if (c >= 0x1100 &&
3132 (c <= 0x115f /* Hangul Jamo */
3133 || c == 0x2329
3134 || c == 0x232a
3135 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
f97f4012 3136 /* CJK ... Yi */
10e290ee
JF
3137 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
3138 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
3139 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
3140 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
3141 || (c >= 0xffe0 && c <= 0xffe6)
3142 || (c >= 0x20000 && c <= 0x2fffd)
3143 || (c >= 0x30000 && c <= 0x3fffd)))
3144 return 2;
3145
3146 return 1;
3147}
3148
3149/* Number of bytes used for encoding a UTF-8 character indexed by first byte.
3150 * Illegal bytes are set one. */
3151static const unsigned char utf8_bytes[256] = {
3152 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,
3153 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,
3154 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,
3155 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,
3156 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,
3157 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,
3158 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,
3159 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,
3160};
3161
3162/* Decode UTF-8 multi-byte representation into a unicode character. */
3163static inline unsigned long
3164utf8_to_unicode(const char *string, size_t length)
3165{
3166 unsigned long unicode;
3167
3168 switch (length) {
3169 case 1:
3170 unicode = string[0];
3171 break;
3172 case 2:
3173 unicode = (string[0] & 0x1f) << 6;
3174 unicode += (string[1] & 0x3f);
3175 break;
3176 case 3:
3177 unicode = (string[0] & 0x0f) << 12;
3178 unicode += ((string[1] & 0x3f) << 6);
3179 unicode += (string[2] & 0x3f);
3180 break;
3181 case 4:
3182 unicode = (string[0] & 0x0f) << 18;
3183 unicode += ((string[1] & 0x3f) << 12);
3184 unicode += ((string[2] & 0x3f) << 6);
3185 unicode += (string[3] & 0x3f);
3186 break;
3187 case 5:
3188 unicode = (string[0] & 0x0f) << 24;
3189 unicode += ((string[1] & 0x3f) << 18);
3190 unicode += ((string[2] & 0x3f) << 12);
3191 unicode += ((string[3] & 0x3f) << 6);
3192 unicode += (string[4] & 0x3f);
3193 break;
68b6e0eb 3194 case 6:
10e290ee
JF
3195 unicode = (string[0] & 0x01) << 30;
3196 unicode += ((string[1] & 0x3f) << 24);
3197 unicode += ((string[2] & 0x3f) << 18);
3198 unicode += ((string[3] & 0x3f) << 12);
3199 unicode += ((string[4] & 0x3f) << 6);
3200 unicode += (string[5] & 0x3f);
3201 break;
3202 default:
3203 die("Invalid unicode length");
3204 }
3205
3206 /* Invalid characters could return the special 0xfffd value but NUL
3207 * should be just as good. */
3208 return unicode > 0xffff ? 0 : unicode;
3209}
3210
3211/* Calculates how much of string can be shown within the given maximum width
3212 * and sets trimmed parameter to non-zero value if all of string could not be
3213 * shown.
3214 *
3215 * Additionally, adds to coloffset how many many columns to move to align with
3216 * the expected position. Takes into account how multi-byte and double-width
3217 * characters will effect the cursor position.
3218 *
3219 * Returns the number of bytes to output from string to satisfy max_width. */
3220static size_t
3221utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3222{
3223 const char *start = string;
3224 const char *end = strchr(string, '\0');
3225 size_t mbwidth = 0;
3226 size_t width = 0;
3227
3228 *trimmed = 0;
3229
3230 while (string < end) {
3231 int c = *(unsigned char *) string;
3232 unsigned char bytes = utf8_bytes[c];
3233 size_t ucwidth;
3234 unsigned long unicode;
3235
3236 if (string + bytes > end)
3237 break;
3238
3239 /* Change representation to figure out whether
3240 * it is a single- or double-width character. */
3241
3242 unicode = utf8_to_unicode(string, bytes);
3243 /* FIXME: Graceful handling of invalid unicode character. */
3244 if (!unicode)
3245 break;
3246
3247 ucwidth = unicode_width(unicode);
3248 width += ucwidth;
3249 if (width > max_width) {
3250 *trimmed = 1;
3251 break;
3252 }
3253
3254 /* The column offset collects the differences between the
3255 * number of bytes encoding a character and the number of
3256 * columns will be used for rendering said character.
3257 *
3258 * So if some character A is encoded in 2 bytes, but will be
3259 * represented on the screen using only 1 byte this will and up
3260 * adding 1 to the multi-byte column offset.
3261 *
3262 * Assumes that no double-width character can be encoding in
3263 * less than two bytes. */
3264 if (bytes > ucwidth)
3265 mbwidth += bytes - ucwidth;
3266
3267 string += bytes;
3268 }
3269
3270 *coloffset += mbwidth;
3271
3272 return string - start;
3273}
3274
3275
3276/*
6b161b31
JF
3277 * Status management
3278 */
2e8488b4 3279
8855ada4 3280/* Whether or not the curses interface has been initialized. */
68b6e0eb 3281static bool cursed = FALSE;
8855ada4 3282
6b161b31
JF
3283/* The status window is used for polling keystrokes. */
3284static WINDOW *status_win;
4a2909a7 3285
2e8488b4 3286/* Update status and title window. */
4a2909a7
JF
3287static void
3288report(const char *msg, ...)
3289{
6706b2ba
JF
3290 static bool empty = TRUE;
3291 struct view *view = display[current_view];
b76c2afc 3292
6706b2ba
JF
3293 if (!empty || *msg) {
3294 va_list args;
4a2909a7 3295
6706b2ba 3296 va_start(args, msg);
4b76734f 3297
6706b2ba
JF
3298 werase(status_win);
3299 wmove(status_win, 0, 0);
3300 if (*msg) {
3301 vwprintw(status_win, msg, args);
3302 empty = FALSE;
3303 } else {
3304 empty = TRUE;
3305 }
3306 wrefresh(status_win);
b801d8b2 3307
6706b2ba
JF
3308 va_end(args);
3309 }
3310
3311 update_view_title(view);
85af6284 3312 update_display_cursor();
b801d8b2
JF
3313}
3314
6b161b31
JF
3315/* Controls when nodelay should be in effect when polling user input. */
3316static void
1ba2ae4b 3317set_nonblocking_input(bool loading)
b801d8b2 3318{
6706b2ba 3319 static unsigned int loading_views;
b801d8b2 3320
6706b2ba
JF
3321 if ((loading == FALSE && loading_views-- == 1) ||
3322 (loading == TRUE && loading_views++ == 0))
1ba2ae4b 3323 nodelay(status_win, loading);
6b161b31
JF
3324}
3325
3326static void
3327init_display(void)
3328{
3329 int x, y;
b76c2afc 3330
6908bdbd
JF
3331 /* Initialize the curses library */
3332 if (isatty(STDIN_FILENO)) {
8855ada4 3333 cursed = !!initscr();
6908bdbd
JF
3334 } else {
3335 /* Leave stdin and stdout alone when acting as a pager. */
3336 FILE *io = fopen("/dev/tty", "r+");
3337
e6f60674
JF
3338 if (!io)
3339 die("Failed to open /dev/tty");
8855ada4 3340 cursed = !!newterm(NULL, io, io);
6908bdbd
JF
3341 }
3342
8855ada4
JF
3343 if (!cursed)
3344 die("Failed to initialize curses");
3345
2e8488b4
JF
3346 nonl(); /* Tell curses not to do NL->CR/NL on output */
3347 cbreak(); /* Take input chars one at a time, no wait for \n */
3348 noecho(); /* Don't echo input */
b801d8b2 3349 leaveok(stdscr, TRUE);
b801d8b2
JF
3350
3351 if (has_colors())
3352 init_colors();
3353
3354 getmaxyx(stdscr, y, x);
3355 status_win = newwin(1, 0, y - 1, 0);
3356 if (!status_win)
3357 die("Failed to create status window");
3358
3359 /* Enable keyboard mapping */
3360 keypad(status_win, TRUE);
78c70acd 3361 wbkgdset(status_win, get_line_attr(LINE_STATUS));
6b161b31
JF
3362}
3363
4af34daa 3364static char *
cb9e48c1 3365read_prompt(const char *prompt)
ef5404a4
JF
3366{
3367 enum { READING, STOP, CANCEL } status = READING;
9e21ce5c 3368 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
ef5404a4
JF
3369 int pos = 0;
3370
3371 while (status == READING) {
3372 struct view *view;
3373 int i, key;
3374
699ae55b 3375 foreach_view (view, i)
ef5404a4
JF
3376 update_view(view);
3377
cb9e48c1 3378 report("%s%.*s", prompt, pos, buf);
ef5404a4
JF
3379 /* Refresh, accept single keystroke of input */
3380 key = wgetch(status_win);
3381 switch (key) {
3382 case KEY_RETURN:
3383 case KEY_ENTER:
3384 case '\n':
3385 status = pos ? STOP : CANCEL;
3386 break;
3387
3388 case KEY_BACKSPACE:
3389 if (pos > 0)
3390 pos--;
3391 else
3392 status = CANCEL;
3393 break;
3394
3395 case KEY_ESC:
3396 status = CANCEL;
3397 break;
3398
3399 case ERR:
3400 break;
3401
3402 default:
3403 if (pos >= sizeof(buf)) {
3404 report("Input string too long");
9e21ce5c 3405 return NULL;
ef5404a4
JF
3406 }
3407
3408 if (isprint(key))
3409 buf[pos++] = (char) key;
3410 }
3411 }
3412
3413 if (status == CANCEL) {
3414 /* Clear the status window */
3415 report("");
9e21ce5c 3416 return NULL;
ef5404a4
JF
3417 }
3418
3419 buf[pos++] = 0;
ef5404a4 3420
9e21ce5c 3421 return buf;
ef5404a4 3422}
c34d9c9f
JF
3423
3424/*
3425 * Repository references
3426 */
3427
3428static struct ref *refs;
3a91b75e 3429static size_t refs_size;
c34d9c9f 3430
1307df1a
JF
3431/* Id <-> ref store */
3432static struct ref ***id_refs;
3433static size_t id_refs_size;
3434
c34d9c9f
JF
3435static struct ref **
3436get_refs(char *id)
3437{
1307df1a
JF
3438 struct ref ***tmp_id_refs;
3439 struct ref **ref_list = NULL;
3440 size_t ref_list_size = 0;
c34d9c9f
JF
3441 size_t i;
3442
1307df1a
JF
3443 for (i = 0; i < id_refs_size; i++)
3444 if (!strcmp(id, id_refs[i][0]->id))
3445 return id_refs[i];
3446
3447 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
3448 if (!tmp_id_refs)
3449 return NULL;
3450
3451 id_refs = tmp_id_refs;
3452
c34d9c9f
JF
3453 for (i = 0; i < refs_size; i++) {
3454 struct ref **tmp;
3455
3456 if (strcmp(id, refs[i].id))
3457 continue;
3458
1307df1a 3459 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
c34d9c9f 3460 if (!tmp) {
1307df1a
JF
3461 if (ref_list)
3462 free(ref_list);
c34d9c9f
JF
3463 return NULL;
3464 }
3465
1307df1a
JF
3466 ref_list = tmp;
3467 if (ref_list_size > 0)
3468 ref_list[ref_list_size - 1]->next = 1;
3469 ref_list[ref_list_size] = &refs[i];
3af8774e
JF
3470
3471 /* XXX: The properties of the commit chains ensures that we can
3472 * safely modify the shared ref. The repo references will
3473 * always be similar for the same id. */
1307df1a
JF
3474 ref_list[ref_list_size]->next = 0;
3475 ref_list_size++;
c34d9c9f
JF
3476 }
3477
1307df1a
JF
3478 if (ref_list)
3479 id_refs[id_refs_size++] = ref_list;
3480
3481 return ref_list;
c34d9c9f
JF
3482}
3483
3484static int
d0cea5f9 3485read_ref(char *id, int idlen, char *name, int namelen)
c34d9c9f 3486{
d0cea5f9
JF
3487 struct ref *ref;
3488 bool tag = FALSE;
d0cea5f9 3489
8b0297ae
JF
3490 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
3491 /* Commits referenced by tags has "^{}" appended. */
3492 if (name[namelen - 1] != '}')
3493 return OK;
3494
d0cea5f9
JF
3495 while (namelen > 0 && name[namelen] != '^')
3496 namelen--;
c34d9c9f 3497
d0cea5f9 3498 tag = TRUE;
8b0297ae
JF
3499 namelen -= STRING_SIZE("refs/tags/");
3500 name += STRING_SIZE("refs/tags/");
c34d9c9f 3501
d0cea5f9 3502 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
8b0297ae
JF
3503 namelen -= STRING_SIZE("refs/heads/");
3504 name += STRING_SIZE("refs/heads/");
c34d9c9f 3505
d0cea5f9
JF
3506 } else if (!strcmp(name, "HEAD")) {
3507 return OK;
3508 }
6706b2ba 3509
d0cea5f9
JF
3510 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
3511 if (!refs)
3512 return ERR;
c34d9c9f 3513
d0cea5f9 3514 ref = &refs[refs_size++];
8b0297ae 3515 ref->name = malloc(namelen + 1);
d0cea5f9
JF
3516 if (!ref->name)
3517 return ERR;
3af8774e 3518
8b0297ae
JF
3519 strncpy(ref->name, name, namelen);
3520 ref->name[namelen] = 0;
d0cea5f9
JF
3521 ref->tag = tag;
3522 string_copy(ref->id, id);
3af8774e 3523
d0cea5f9
JF
3524 return OK;
3525}
c34d9c9f 3526
d0cea5f9
JF
3527static int
3528load_refs(void)
3529{
3530 const char *cmd_env = getenv("TIG_LS_REMOTE");
3531 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
c34d9c9f 3532
4a63c884 3533 return read_properties(popen(cmd, "r"), "\t", read_ref);
d0cea5f9 3534}
c34d9c9f 3535
d0cea5f9 3536static int
14c778a6 3537read_repo_config_option(char *name, int namelen, char *value, int valuelen)
d0cea5f9 3538{
22913179 3539 if (!strcmp(name, "i18n.commitencoding"))
d0cea5f9 3540 string_copy(opt_encoding, value);
c34d9c9f 3541
c34d9c9f
JF
3542 return OK;
3543}
3544
4670cf89 3545static int
14c778a6 3546load_repo_config(void)
4670cf89 3547{
66749723 3548 return read_properties(popen("git repo-config --list", "r"),
14c778a6 3549 "=", read_repo_config_option);
d0cea5f9
JF
3550}
3551
3552static int
4a63c884 3553read_properties(FILE *pipe, const char *separators,
d0cea5f9
JF
3554 int (*read_property)(char *, int, char *, int))
3555{
4670cf89
JF
3556 char buffer[BUFSIZ];
3557 char *name;
d0cea5f9 3558 int state = OK;
4670cf89
JF
3559
3560 if (!pipe)
3561 return ERR;
3562
d0cea5f9 3563 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4a63c884
JF
3564 char *value;
3565 size_t namelen;
3566 size_t valuelen;
4670cf89 3567
4a63c884
JF
3568 name = chomp_string(name);
3569 namelen = strcspn(name, separators);
3570
3571 if (name[namelen]) {
3572 name[namelen] = 0;
3573 value = chomp_string(name + namelen + 1);
d0cea5f9 3574 valuelen = strlen(value);
4670cf89 3575
d0cea5f9 3576 } else {
d0cea5f9
JF
3577 value = "";
3578 valuelen = 0;
4670cf89 3579 }
d0cea5f9 3580
3c3801c2 3581 state = read_property(name, namelen, value, valuelen);
4670cf89
JF
3582 }
3583
d0cea5f9
JF
3584 if (state != ERR && ferror(pipe))
3585 state = ERR;
4670cf89
JF
3586
3587 pclose(pipe);
3588
d0cea5f9 3589 return state;
4670cf89
JF
3590}
3591
d0cea5f9 3592
6b161b31
JF
3593/*
3594 * Main
3595 */
3596
b5c9e67f 3597static void __NORETURN
6b161b31
JF
3598quit(int sig)
3599{
8855ada4
JF
3600 /* XXX: Restore tty modes and let the OS cleanup the rest! */
3601 if (cursed)
3602 endwin();
6b161b31
JF
3603 exit(0);
3604}
3605
c6704a4e
JF
3606static void __NORETURN
3607die(const char *err, ...)
6b161b31
JF
3608{
3609 va_list args;
3610
3611 endwin();
3612
3613 va_start(args, err);
3614 fputs("tig: ", stderr);
3615 vfprintf(stderr, err, args);
3616 fputs("\n", stderr);
3617 va_end(args);
3618
3619 exit(1);
3620}
3621
3622int
3623main(int argc, char *argv[])
3624{
1ba2ae4b 3625 struct view *view;
6b161b31 3626 enum request request;
1ba2ae4b 3627 size_t i;
6b161b31
JF
3628
3629 signal(SIGINT, quit);
3630
6b68fd24
JF
3631 if (setlocale(LC_ALL, "")) {
3632 string_copy(opt_codeset, nl_langinfo(CODESET));
3633 }
3634
660e09ad
JF
3635 if (load_options() == ERR)
3636 die("Failed to load user config.");
3637
3638 /* Load the repo config file so options can be overwritten from
afdc35b3 3639 * the command line. */
14c778a6 3640 if (load_repo_config() == ERR)
afdc35b3
JF
3641 die("Failed to load repo config.");
3642
8855ada4 3643 if (!parse_options(argc, argv))
6b161b31
JF
3644 return 0;
3645
6b68fd24
JF
3646 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
3647 opt_iconv = iconv_open(opt_codeset, opt_encoding);
20f4b4a3 3648 if (opt_iconv == ICONV_NONE)
6b68fd24
JF
3649 die("Failed to initialize character set conversion");
3650 }
3651
c34d9c9f
JF
3652 if (load_refs() == ERR)
3653 die("Failed to load refs.");
3654
7bb55251
JF
3655 /* Require a git repository unless when running in pager mode. */
3656 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
3657 die("Not a git repository");
3658
1ba2ae4b
JF
3659 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
3660 view->cmd_env = getenv(view->cmd_env);
3661
6b161b31
JF
3662 request = opt_request;
3663
3664 init_display();
b801d8b2
JF
3665
3666 while (view_driver(display[current_view], request)) {
6b161b31 3667 int key;
b801d8b2
JF
3668 int i;
3669
699ae55b 3670 foreach_view (view, i)
6b161b31 3671 update_view(view);
b801d8b2
JF
3672
3673 /* Refresh, accept single keystroke of input */
6b161b31 3674 key = wgetch(status_win);
04e2b7b2
JF
3675
3676 request = get_keybinding(display[current_view]->keymap, key);
03a93dbb 3677
6706b2ba 3678 /* Some low-level request handling. This keeps access to
fac7db6c
JF
3679 * status_win restricted. */
3680 switch (request) {
3681 case REQ_PROMPT:
9e21ce5c
JF
3682 {
3683 char *cmd = read_prompt(":");
3684
3685 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
3686 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
3687 opt_request = REQ_VIEW_DIFF;
3688 } else {
3689 opt_request = REQ_VIEW_PAGER;
3690 }
3691 break;
3692 }
fac7db6c 3693
1d754561 3694 request = REQ_NONE;
9e21ce5c
JF
3695 break;
3696 }
4af34daa
JF
3697 case REQ_SEARCH:
3698 case REQ_SEARCH_BACK:
3699 {
3700 const char *prompt = request == REQ_SEARCH
3701 ? "/" : "?";
3702 char *search = read_prompt(prompt);
3703
3704 if (search)
3705 string_copy(opt_search, search);
3706 else
3707 request = REQ_NONE;
3708 break;
3709 }
fac7db6c
JF
3710 case REQ_SCREEN_RESIZE:
3711 {
3712 int height, width;
3713
3714 getmaxyx(stdscr, height, width);
3715
3716 /* Resize the status view and let the view driver take
3717 * care of resizing the displayed views. */
3718 wresize(status_win, 1, width);
3719 mvwin(status_win, height - 1, 0);
3720 wrefresh(status_win);
3721 break;
3722 }
3723 default:
3724 break;
03a93dbb 3725 }
b801d8b2
JF
3726 }
3727
3728 quit(0);
3729
3730 return 0;
3731}