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