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