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