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