Fix git-describe reference adding when there are no tags and thus no output
[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:
d42c8a35 2336 /* Add <tag>-g<commit_id> "fake" reference. */
dc23c0e3
JF
2337 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2338 return;
2339 }
2340
d42c8a35
JF
2341 if (bufpos == 0)
2342 return;
2343
cc2d1364 2344 if (!realloc_lines(view, view->line_size + 1))
7b99a34c
JF
2345 return;
2346
2347 line = &view->line[view->lines];
2348 line->data = strdup(buf);
2349 if (!line->data)
2350 return;
2351
2352 line->type = LINE_PP_REFS;
2353 view->lines++;
2354}
2355
6b161b31 2356static bool
701e4f5d 2357pager_read(struct view *view, char *data)
22f66b0a 2358{
7b99a34c 2359 struct line *line = &view->line[view->lines];
22f66b0a 2360
7b99a34c
JF
2361 line->data = strdup(data);
2362 if (!line->data)
2363 return FALSE;
fe7233c3 2364
7b99a34c 2365 line->type = get_line_type(line->data);
22f66b0a 2366 view->lines++;
7b99a34c
JF
2367
2368 if (line->type == LINE_COMMIT &&
2369 (view == VIEW(REQ_VIEW_DIFF) ||
2370 view == VIEW(REQ_VIEW_LOG)))
2371 add_pager_refs(view, line);
2372
22f66b0a
JF
2373 return TRUE;
2374}
2375
6b161b31 2376static bool
fe7233c3 2377pager_enter(struct view *view, struct line *line)
6b161b31 2378{
91e8e277 2379 int split = 0;
6b161b31 2380
9fbbd28f
JF
2381 if (line->type == LINE_COMMIT &&
2382 (view == VIEW(REQ_VIEW_LOG) ||
2383 view == VIEW(REQ_VIEW_PAGER))) {
91e8e277
JF
2384 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2385 split = 1;
67e48ac5
JF
2386 }
2387
91e8e277
JF
2388 /* Always scroll the view even if it was split. That way
2389 * you can use Enter to scroll through the log view and
2390 * split open each commit diff. */
2391 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2392
2393 /* FIXME: A minor workaround. Scrolling the view will call report("")
9d82d824
JF
2394 * but if we are scrolling a non-current view this won't properly
2395 * update the view title. */
91e8e277
JF
2396 if (split)
2397 update_view_title(view);
6b161b31
JF
2398
2399 return TRUE;
2400}
2401
4af34daa
JF
2402static bool
2403pager_grep(struct view *view, struct line *line)
2404{
2405 regmatch_t pmatch;
2406 char *text = line->data;
2407
2408 if (!*text)
2409 return FALSE;
2410
2411 if (regexec(&view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2412 return FALSE;
2413
2414 return TRUE;
2415}
2416
6b161b31 2417static struct view_ops pager_ops = {
6734f6b9 2418 "line",
6b161b31
JF
2419 pager_draw,
2420 pager_read,
2421 pager_enter,
4af34daa 2422 pager_grep,
6b161b31
JF
2423};
2424
80ce96ea 2425
ff26aa29 2426/*
e733ee54
JF
2427 * Tree backend
2428 */
2429
2430/* Parse output from git ls-tree:
2431 *
2432 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2433 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2434 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2435 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2436 */
2437
2438#define SIZEOF_TREE_ATTR \
2439 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2440
2441#define TREE_UP_FORMAT "040000 tree %s\t.."
2442
2443static int
2444tree_compare_entry(enum line_type type1, char *name1,
2445 enum line_type type2, char *name2)
2446{
2447 if (type1 != type2) {
2448 if (type1 == LINE_TREE_DIR)
2449 return -1;
2450 return 1;
2451 }
2452
2453 return strcmp(name1, name2);
2454}
2455
2456static bool
2457tree_read(struct view *view, char *text)
2458{
2459 size_t textlen = strlen(text);
2460 char buf[SIZEOF_STR];
2461 unsigned long pos;
2462 enum line_type type;
f88a5319 2463 bool first_read = view->lines == 0;
e733ee54
JF
2464
2465 if (textlen <= SIZEOF_TREE_ATTR)
2466 return FALSE;
2467
2468 type = text[STRING_SIZE("100644 ")] == 't'
2469 ? LINE_TREE_DIR : LINE_TREE_FILE;
2470
f88a5319 2471 if (first_read) {
e733ee54 2472 /* Add path info line */
4ea4ce91 2473 if (string_format(buf, "Directory path /%s", opt_path) &&
e733ee54
JF
2474 realloc_lines(view, view->line_size + 1) &&
2475 pager_read(view, buf))
2476 view->line[view->lines - 1].type = LINE_DEFAULT;
2477 else
2478 return FALSE;
2479
2480 /* Insert "link" to parent directory. */
2481 if (*opt_path &&
4ea4ce91 2482 string_format(buf, TREE_UP_FORMAT, view->ref) &&
e733ee54
JF
2483 realloc_lines(view, view->line_size + 1) &&
2484 pager_read(view, buf))
2485 view->line[view->lines - 1].type = LINE_TREE_DIR;
2486 else if (*opt_path)
2487 return FALSE;
2488 }
2489
2490 /* Strip the path part ... */
2491 if (*opt_path) {
2492 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2493 size_t striplen = strlen(opt_path);
2494 char *path = text + SIZEOF_TREE_ATTR;
2495
2496 if (pathlen > striplen)
2497 memmove(path, path + striplen,
2498 pathlen - striplen + 1);
2499 }
2500
2501 /* Skip "Directory ..." and ".." line. */
2502 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2503 struct line *line = &view->line[pos];
2504 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2505 char *path2 = text + SIZEOF_TREE_ATTR;
2506 int cmp = tree_compare_entry(line->type, path1, type, path2);
2507
2508 if (cmp <= 0)
2509 continue;
2510
2511 text = strdup(text);
2512 if (!text)
2513 return FALSE;
2514
2515 if (view->lines > pos)
2516 memmove(&view->line[pos + 1], &view->line[pos],
2517 (view->lines - pos) * sizeof(*line));
2518
2519 line = &view->line[pos];
2520 line->data = text;
2521 line->type = type;
2522 view->lines++;
2523 return TRUE;
2524 }
2525
2526 if (!pager_read(view, text))
2527 return FALSE;
2528
f88a5319
JF
2529 /* Move the current line to the first tree entry. */
2530 if (first_read)
2531 view->lineno++;
2532
e733ee54
JF
2533 view->line[view->lines - 1].type = type;
2534 return TRUE;
2535}
2536
2537static bool
2538tree_enter(struct view *view, struct line *line)
2539{
2540 enum open_flags flags = OPEN_DEFAULT;
2541 char *data = line->data;
2542 enum request request;
2543
2544 switch (line->type) {
2545 case LINE_TREE_DIR:
2546 /* Depending on whether it is a subdir or parent (updir?) link
2547 * mangle the path buffer. */
2548 if (line == &view->line[1] && *opt_path) {
2549 size_t path_len = strlen(opt_path);
2550 char *dirsep = opt_path + path_len - 1;
2551
2552 while (dirsep > opt_path && dirsep[-1] != '/')
2553 dirsep--;
2554
2555 dirsep[0] = 0;
2556
2557 } else {
d65ced0d 2558 size_t pathlen = strlen(opt_path);
4ea4ce91 2559 size_t origlen = pathlen;
e733ee54
JF
2560 char *basename = data + SIZEOF_TREE_ATTR;
2561
4ea4ce91
JF
2562 if (string_format_from(opt_path, &pathlen, "%s/", basename)) {
2563 opt_path[origlen] = 0;
2564 return TRUE;
2565 }
e733ee54
JF
2566 }
2567
2568 /* Trees and subtrees share the same ID, so they are not not
2569 * unique like blobs. */
2570 flags |= OPEN_RELOAD;
2571 request = REQ_VIEW_TREE;
2572 break;
2573
2574 case LINE_TREE_FILE:
2575 /* This causes the blob view to become split, and not having it
2576 * in the tree dir case will make the blob view automatically
2577 * disappear when moving to a different directory. */
2578 flags |= OPEN_SPLIT;
2579 request = REQ_VIEW_BLOB;
2580 break;
2581
2582 default:
2583 return TRUE;
2584 }
2585
2586 open_view(view, request, flags);
2587
2588 if (!VIEW(request)->pipe)
2589 return TRUE;
2590
2591 /* For tree views insert the path to the parent as the first line. */
2592 if (request == REQ_VIEW_BLOB) {
2593 /* Mirror what is showed in the title bar. */
2594 string_ncopy(ref_blob, data + STRING_SIZE("100644 blob "), 40);
2595 string_copy(VIEW(REQ_VIEW_BLOB)->ref, ref_blob);
2596 return TRUE;
2597 }
2598
2599 return TRUE;
2600}
2601
2602static struct view_ops tree_ops = {
2603 "file",
2604 pager_draw,
2605 tree_read,
2606 tree_enter,
2607 pager_grep,
2608};
2609
2610static bool
2611blob_read(struct view *view, char *line)
2612{
2613 bool state = pager_read(view, line);
2614
2615 if (state == TRUE)
2616 view->line[view->lines - 1].type = LINE_DEFAULT;
2617
2618 return state;
2619}
2620
2621static struct view_ops blob_ops = {
2622 "line",
2623 pager_draw,
2624 blob_read,
2625 pager_enter,
2626 pager_grep,
2627};
2628
2629
2630/*
ff26aa29
JF
2631 * Main view backend
2632 */
2633
2634struct commit {
54efb62b
JF
2635 char id[41]; /* SHA1 ID. */
2636 char title[75]; /* First line of the commit message. */
2637 char author[75]; /* Author of the commit. */
2638 struct tm time; /* Date from the author ident. */
2639 struct ref **refs; /* Repository references. */
2640 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
2641 size_t graph_size; /* The width of the graph array. */
ff26aa29 2642};
c34d9c9f 2643
6b161b31 2644static bool
fe7233c3 2645main_draw(struct view *view, struct line *line, unsigned int lineno)
22f66b0a 2646{
2e8488b4 2647 char buf[DATE_COLS + 1];
fe7233c3 2648 struct commit *commit = line->data;
78c70acd 2649 enum line_type type;
6706b2ba 2650 int col = 0;
b76c2afc 2651 size_t timelen;
10e290ee 2652 size_t authorlen;
9989bf60 2653 int trimmed = 1;
22f66b0a 2654
4c6fabc2
JF
2655 if (!*commit->author)
2656 return FALSE;
22f66b0a 2657
6706b2ba
JF
2658 wmove(view->win, lineno, col);
2659
22f66b0a 2660 if (view->offset + lineno == view->lineno) {
03a93dbb 2661 string_copy(view->ref, commit->id);
49f2b43f 2662 string_copy(ref_commit, view->ref);
78c70acd 2663 type = LINE_CURSOR;
6706b2ba
JF
2664 wattrset(view->win, get_line_attr(type));
2665 wchgat(view->win, -1, 0, type, NULL);
2666
78c70acd 2667 } else {
b76c2afc 2668 type = LINE_MAIN_COMMIT;
6706b2ba 2669 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
b76c2afc
JF
2670 }
2671
4c6fabc2 2672 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
b76c2afc 2673 waddnstr(view->win, buf, timelen);
4c6fabc2 2674 waddstr(view->win, " ");
b76c2afc 2675
6706b2ba
JF
2676 col += DATE_COLS;
2677 wmove(view->win, lineno, col);
2678 if (type != LINE_CURSOR)
2679 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
b76c2afc 2680
9989bf60
JF
2681 if (opt_utf8) {
2682 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
2683 } else {
2684 authorlen = strlen(commit->author);
2685 if (authorlen > AUTHOR_COLS - 2) {
2686 authorlen = AUTHOR_COLS - 2;
2687 trimmed = 1;
2688 }
2689 }
10e290ee
JF
2690
2691 if (trimmed) {
2692 waddnstr(view->win, commit->author, authorlen);
6706b2ba
JF
2693 if (type != LINE_CURSOR)
2694 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
b76c2afc
JF
2695 waddch(view->win, '~');
2696 } else {
2697 waddstr(view->win, commit->author);
22f66b0a
JF
2698 }
2699
10e290ee 2700 col += AUTHOR_COLS;
6706b2ba
JF
2701 if (type != LINE_CURSOR)
2702 wattrset(view->win, A_NORMAL);
2703
54efb62b
JF
2704 if (opt_rev_graph && commit->graph_size) {
2705 size_t i;
2706
2707 wmove(view->win, lineno, col);
2708 /* Using waddch() instead of waddnstr() ensures that
2709 * they'll be rendered correctly for the cursor line. */
2710 for (i = 0; i < commit->graph_size; i++)
2711 waddch(view->win, commit->graph[i]);
2712
2713 col += commit->graph_size + 1;
2714 }
2715
2716 wmove(view->win, lineno, col);
c34d9c9f
JF
2717
2718 if (commit->refs) {
2719 size_t i = 0;
2720
2721 do {
6706b2ba
JF
2722 if (type == LINE_CURSOR)
2723 ;
2724 else if (commit->refs[i]->tag)
c34d9c9f
JF
2725 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
2726 else
2727 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
2728 waddstr(view->win, "[");
2729 waddstr(view->win, commit->refs[i]->name);
2730 waddstr(view->win, "]");
6706b2ba
JF
2731 if (type != LINE_CURSOR)
2732 wattrset(view->win, A_NORMAL);
c34d9c9f 2733 waddstr(view->win, " ");
6706b2ba 2734 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
c34d9c9f
JF
2735 } while (commit->refs[i++]->next);
2736 }
2737
6706b2ba
JF
2738 if (type != LINE_CURSOR)
2739 wattrset(view->win, get_line_attr(type));
2740
2741 {
2742 int titlelen = strlen(commit->title);
2743
2744 if (col + titlelen > view->width)
2745 titlelen = view->width - col;
2746
2747 waddnstr(view->win, commit->title, titlelen);
2748 }
22f66b0a
JF
2749
2750 return TRUE;
2751}
2752
4c6fabc2 2753/* Reads git log --pretty=raw output and parses it into the commit struct. */
6b161b31 2754static bool
701e4f5d 2755main_read(struct view *view, char *line)
22f66b0a 2756{
78c70acd 2757 enum line_type type = get_line_type(line);
701e4f5d
JF
2758 struct commit *commit = view->lines
2759 ? view->line[view->lines - 1].data : NULL;
22f66b0a 2760
78c70acd
JF
2761 switch (type) {
2762 case LINE_COMMIT:
22f66b0a
JF
2763 commit = calloc(1, sizeof(struct commit));
2764 if (!commit)
2765 return FALSE;
2766
4c6fabc2 2767 line += STRING_SIZE("commit ");
b76c2afc 2768
fe7233c3 2769 view->line[view->lines++].data = commit;
82e78006 2770 string_copy(commit->id, line);
c34d9c9f 2771 commit->refs = get_refs(commit->id);
54efb62b 2772 commit->graph[commit->graph_size++] = ACS_LTEE;
78c70acd 2773 break;
22f66b0a 2774
8855ada4 2775 case LINE_AUTHOR:
b76c2afc 2776 {
4c6fabc2 2777 char *ident = line + STRING_SIZE("author ");
b76c2afc
JF
2778 char *end = strchr(ident, '<');
2779
701e4f5d 2780 if (!commit)
fe7233c3
JF
2781 break;
2782
b76c2afc 2783 if (end) {
cbbf2d1b
JF
2784 char *email = end + 1;
2785
b76c2afc 2786 for (; end > ident && isspace(end[-1]); end--) ;
cbbf2d1b
JF
2787
2788 if (end == ident && *email) {
2789 ident = email;
2790 end = strchr(ident, '>');
2791 for (; end > ident && isspace(end[-1]); end--) ;
2792 }
b76c2afc
JF
2793 *end = 0;
2794 }
2795
cbbf2d1b
JF
2796 /* End is NULL or ident meaning there's no author. */
2797 if (end <= ident)
2798 ident = "Unknown";
2799
82e78006 2800 string_copy(commit->author, ident);
b76c2afc 2801
4c6fabc2 2802 /* Parse epoch and timezone */
b76c2afc
JF
2803 if (end) {
2804 char *secs = strchr(end + 1, '>');
2805 char *zone;
2806 time_t time;
2807
2808 if (!secs || secs[1] != ' ')
2809 break;
2810
2811 secs += 2;
2812 time = (time_t) atol(secs);
2813 zone = strchr(secs, ' ');
4c6fabc2 2814 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
b76c2afc
JF
2815 long tz;
2816
2817 zone++;
2818 tz = ('0' - zone[1]) * 60 * 60 * 10;
2819 tz += ('0' - zone[2]) * 60 * 60;
2820 tz += ('0' - zone[3]) * 60;
2821 tz += ('0' - zone[4]) * 60;
2822
2823 if (zone[0] == '-')
2824 tz = -tz;
2825
2826 time -= tz;
2827 }
2828 gmtime_r(&time, &commit->time);
2829 }
2830 break;
2831 }
78c70acd 2832 default:
701e4f5d 2833 if (!commit)
2e8488b4
JF
2834 break;
2835
2836 /* Fill in the commit title if it has not already been set. */
2e8488b4
JF
2837 if (commit->title[0])
2838 break;
2839
2840 /* Require titles to start with a non-space character at the
2841 * offset used by git log. */
eb98559e
JF
2842 /* FIXME: More gracefull handling of titles; append "..." to
2843 * shortened titles, etc. */
2e8488b4 2844 if (strncmp(line, " ", 4) ||
eb98559e 2845 isspace(line[4]))
82e78006
JF
2846 break;
2847
2848 string_copy(commit->title, line + 4);
22f66b0a
JF
2849 }
2850
2851 return TRUE;
2852}
2853
6b161b31 2854static bool
fe7233c3 2855main_enter(struct view *view, struct line *line)
b801d8b2 2856{
b3a54cba
JF
2857 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2858
2859 open_view(view, REQ_VIEW_DIFF, flags);
6b161b31 2860 return TRUE;
b801d8b2
JF
2861}
2862
4af34daa
JF
2863static bool
2864main_grep(struct view *view, struct line *line)
2865{
2866 struct commit *commit = line->data;
2867 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
2868 char buf[DATE_COLS + 1];
2869 regmatch_t pmatch;
2870
2871 for (state = S_TITLE; state < S_END; state++) {
2872 char *text;
2873
2874 switch (state) {
2875 case S_TITLE: text = commit->title; break;
2876 case S_AUTHOR: text = commit->author; break;
2877 case S_DATE:
2878 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
2879 continue;
2880 text = buf;
2881 break;
2882
2883 default:
2884 return FALSE;
2885 }
2886
2887 if (regexec(&view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
2888 return TRUE;
2889 }
2890
2891 return FALSE;
2892}
2893
6b161b31 2894static struct view_ops main_ops = {
6734f6b9 2895 "commit",
6b161b31
JF
2896 main_draw,
2897 main_read,
2898 main_enter,
4af34daa 2899 main_grep,
6b161b31 2900};
2e8488b4 2901
c34d9c9f 2902
6b161b31 2903/*
10e290ee
JF
2904 * Unicode / UTF-8 handling
2905 *
2906 * NOTE: Much of the following code for dealing with unicode is derived from
2907 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2908 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2909 */
2910
2911/* I've (over)annotated a lot of code snippets because I am not entirely
2912 * confident that the approach taken by this small UTF-8 interface is correct.
2913 * --jonas */
2914
2915static inline int
2916unicode_width(unsigned long c)
2917{
2918 if (c >= 0x1100 &&
2919 (c <= 0x115f /* Hangul Jamo */
2920 || c == 0x2329
2921 || c == 0x232a
2922 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
f97f4012 2923 /* CJK ... Yi */
10e290ee
JF
2924 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2925 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2926 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2927 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2928 || (c >= 0xffe0 && c <= 0xffe6)
2929 || (c >= 0x20000 && c <= 0x2fffd)
2930 || (c >= 0x30000 && c <= 0x3fffd)))
2931 return 2;
2932
2933 return 1;
2934}
2935
2936/* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2937 * Illegal bytes are set one. */
2938static const unsigned char utf8_bytes[256] = {
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 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,
2942 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,
2943 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,
2944 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,
2945 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,
2946 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,
2947};
2948
2949/* Decode UTF-8 multi-byte representation into a unicode character. */
2950static inline unsigned long
2951utf8_to_unicode(const char *string, size_t length)
2952{
2953 unsigned long unicode;
2954
2955 switch (length) {
2956 case 1:
2957 unicode = string[0];
2958 break;
2959 case 2:
2960 unicode = (string[0] & 0x1f) << 6;
2961 unicode += (string[1] & 0x3f);
2962 break;
2963 case 3:
2964 unicode = (string[0] & 0x0f) << 12;
2965 unicode += ((string[1] & 0x3f) << 6);
2966 unicode += (string[2] & 0x3f);
2967 break;
2968 case 4:
2969 unicode = (string[0] & 0x0f) << 18;
2970 unicode += ((string[1] & 0x3f) << 12);
2971 unicode += ((string[2] & 0x3f) << 6);
2972 unicode += (string[3] & 0x3f);
2973 break;
2974 case 5:
2975 unicode = (string[0] & 0x0f) << 24;
2976 unicode += ((string[1] & 0x3f) << 18);
2977 unicode += ((string[2] & 0x3f) << 12);
2978 unicode += ((string[3] & 0x3f) << 6);
2979 unicode += (string[4] & 0x3f);
2980 break;
68b6e0eb 2981 case 6:
10e290ee
JF
2982 unicode = (string[0] & 0x01) << 30;
2983 unicode += ((string[1] & 0x3f) << 24);
2984 unicode += ((string[2] & 0x3f) << 18);
2985 unicode += ((string[3] & 0x3f) << 12);
2986 unicode += ((string[4] & 0x3f) << 6);
2987 unicode += (string[5] & 0x3f);
2988 break;
2989 default:
2990 die("Invalid unicode length");
2991 }
2992
2993 /* Invalid characters could return the special 0xfffd value but NUL
2994 * should be just as good. */
2995 return unicode > 0xffff ? 0 : unicode;
2996}
2997
2998/* Calculates how much of string can be shown within the given maximum width
2999 * and sets trimmed parameter to non-zero value if all of string could not be
3000 * shown.
3001 *
3002 * Additionally, adds to coloffset how many many columns to move to align with
3003 * the expected position. Takes into account how multi-byte and double-width
3004 * characters will effect the cursor position.
3005 *
3006 * Returns the number of bytes to output from string to satisfy max_width. */
3007static size_t
3008utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3009{
3010 const char *start = string;
3011 const char *end = strchr(string, '\0');
3012 size_t mbwidth = 0;
3013 size_t width = 0;
3014
3015 *trimmed = 0;
3016
3017 while (string < end) {
3018 int c = *(unsigned char *) string;
3019 unsigned char bytes = utf8_bytes[c];
3020 size_t ucwidth;
3021 unsigned long unicode;
3022
3023 if (string + bytes > end)
3024 break;
3025
3026 /* Change representation to figure out whether
3027 * it is a single- or double-width character. */
3028
3029 unicode = utf8_to_unicode(string, bytes);
3030 /* FIXME: Graceful handling of invalid unicode character. */
3031 if (!unicode)
3032 break;
3033
3034 ucwidth = unicode_width(unicode);
3035 width += ucwidth;
3036 if (width > max_width) {
3037 *trimmed = 1;
3038 break;
3039 }
3040
3041 /* The column offset collects the differences between the
3042 * number of bytes encoding a character and the number of
3043 * columns will be used for rendering said character.
3044 *
3045 * So if some character A is encoded in 2 bytes, but will be
3046 * represented on the screen using only 1 byte this will and up
3047 * adding 1 to the multi-byte column offset.
3048 *
3049 * Assumes that no double-width character can be encoding in
3050 * less than two bytes. */
3051 if (bytes > ucwidth)
3052 mbwidth += bytes - ucwidth;
3053
3054 string += bytes;
3055 }
3056
3057 *coloffset += mbwidth;
3058
3059 return string - start;
3060}
3061
3062
3063/*
6b161b31
JF
3064 * Status management
3065 */
2e8488b4 3066
8855ada4 3067/* Whether or not the curses interface has been initialized. */
68b6e0eb 3068static bool cursed = FALSE;
8855ada4 3069
6b161b31
JF
3070/* The status window is used for polling keystrokes. */
3071static WINDOW *status_win;
4a2909a7 3072
2e8488b4 3073/* Update status and title window. */
4a2909a7
JF
3074static void
3075report(const char *msg, ...)
3076{
6706b2ba
JF
3077 static bool empty = TRUE;
3078 struct view *view = display[current_view];
b76c2afc 3079
6706b2ba
JF
3080 if (!empty || *msg) {
3081 va_list args;
4a2909a7 3082
6706b2ba 3083 va_start(args, msg);
4b76734f 3084
6706b2ba
JF
3085 werase(status_win);
3086 wmove(status_win, 0, 0);
3087 if (*msg) {
3088 vwprintw(status_win, msg, args);
3089 empty = FALSE;
3090 } else {
3091 empty = TRUE;
3092 }
3093 wrefresh(status_win);
b801d8b2 3094
6706b2ba
JF
3095 va_end(args);
3096 }
3097
3098 update_view_title(view);
85af6284 3099 update_display_cursor();
b801d8b2
JF
3100}
3101
6b161b31
JF
3102/* Controls when nodelay should be in effect when polling user input. */
3103static void
1ba2ae4b 3104set_nonblocking_input(bool loading)
b801d8b2 3105{
6706b2ba 3106 static unsigned int loading_views;
b801d8b2 3107
6706b2ba
JF
3108 if ((loading == FALSE && loading_views-- == 1) ||
3109 (loading == TRUE && loading_views++ == 0))
1ba2ae4b 3110 nodelay(status_win, loading);
6b161b31
JF
3111}
3112
3113static void
3114init_display(void)
3115{
3116 int x, y;
b76c2afc 3117
6908bdbd
JF
3118 /* Initialize the curses library */
3119 if (isatty(STDIN_FILENO)) {
8855ada4 3120 cursed = !!initscr();
6908bdbd
JF
3121 } else {
3122 /* Leave stdin and stdout alone when acting as a pager. */
3123 FILE *io = fopen("/dev/tty", "r+");
3124
e6f60674
JF
3125 if (!io)
3126 die("Failed to open /dev/tty");
8855ada4 3127 cursed = !!newterm(NULL, io, io);
6908bdbd
JF
3128 }
3129
8855ada4
JF
3130 if (!cursed)
3131 die("Failed to initialize curses");
3132
2e8488b4
JF
3133 nonl(); /* Tell curses not to do NL->CR/NL on output */
3134 cbreak(); /* Take input chars one at a time, no wait for \n */
3135 noecho(); /* Don't echo input */
b801d8b2 3136 leaveok(stdscr, TRUE);
b801d8b2
JF
3137
3138 if (has_colors())
3139 init_colors();
3140
3141 getmaxyx(stdscr, y, x);
3142 status_win = newwin(1, 0, y - 1, 0);
3143 if (!status_win)
3144 die("Failed to create status window");
3145
3146 /* Enable keyboard mapping */
3147 keypad(status_win, TRUE);
78c70acd 3148 wbkgdset(status_win, get_line_attr(LINE_STATUS));
6b161b31
JF
3149}
3150
4af34daa 3151static char *
cb9e48c1 3152read_prompt(const char *prompt)
ef5404a4
JF
3153{
3154 enum { READING, STOP, CANCEL } status = READING;
9e21ce5c 3155 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
ef5404a4
JF
3156 int pos = 0;
3157
3158 while (status == READING) {
3159 struct view *view;
3160 int i, key;
3161
699ae55b 3162 foreach_view (view, i)
ef5404a4
JF
3163 update_view(view);
3164
cb9e48c1 3165 report("%s%.*s", prompt, pos, buf);
ef5404a4
JF
3166 /* Refresh, accept single keystroke of input */
3167 key = wgetch(status_win);
3168 switch (key) {
3169 case KEY_RETURN:
3170 case KEY_ENTER:
3171 case '\n':
3172 status = pos ? STOP : CANCEL;
3173 break;
3174
3175 case KEY_BACKSPACE:
3176 if (pos > 0)
3177 pos--;
3178 else
3179 status = CANCEL;
3180 break;
3181
3182 case KEY_ESC:
3183 status = CANCEL;
3184 break;
3185
3186 case ERR:
3187 break;
3188
3189 default:
3190 if (pos >= sizeof(buf)) {
3191 report("Input string too long");
9e21ce5c 3192 return NULL;
ef5404a4
JF
3193 }
3194
3195 if (isprint(key))
3196 buf[pos++] = (char) key;
3197 }
3198 }
3199
3200 if (status == CANCEL) {
3201 /* Clear the status window */
3202 report("");
9e21ce5c 3203 return NULL;
ef5404a4
JF
3204 }
3205
3206 buf[pos++] = 0;
ef5404a4 3207
9e21ce5c 3208 return buf;
ef5404a4 3209}
c34d9c9f
JF
3210
3211/*
3212 * Repository references
3213 */
3214
3215static struct ref *refs;
3a91b75e 3216static size_t refs_size;
c34d9c9f 3217
1307df1a
JF
3218/* Id <-> ref store */
3219static struct ref ***id_refs;
3220static size_t id_refs_size;
3221
c34d9c9f
JF
3222static struct ref **
3223get_refs(char *id)
3224{
1307df1a
JF
3225 struct ref ***tmp_id_refs;
3226 struct ref **ref_list = NULL;
3227 size_t ref_list_size = 0;
c34d9c9f
JF
3228 size_t i;
3229
1307df1a
JF
3230 for (i = 0; i < id_refs_size; i++)
3231 if (!strcmp(id, id_refs[i][0]->id))
3232 return id_refs[i];
3233
3234 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
3235 if (!tmp_id_refs)
3236 return NULL;
3237
3238 id_refs = tmp_id_refs;
3239
c34d9c9f
JF
3240 for (i = 0; i < refs_size; i++) {
3241 struct ref **tmp;
3242
3243 if (strcmp(id, refs[i].id))
3244 continue;
3245
1307df1a 3246 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
c34d9c9f 3247 if (!tmp) {
1307df1a
JF
3248 if (ref_list)
3249 free(ref_list);
c34d9c9f
JF
3250 return NULL;
3251 }
3252
1307df1a
JF
3253 ref_list = tmp;
3254 if (ref_list_size > 0)
3255 ref_list[ref_list_size - 1]->next = 1;
3256 ref_list[ref_list_size] = &refs[i];
3af8774e
JF
3257
3258 /* XXX: The properties of the commit chains ensures that we can
3259 * safely modify the shared ref. The repo references will
3260 * always be similar for the same id. */
1307df1a
JF
3261 ref_list[ref_list_size]->next = 0;
3262 ref_list_size++;
c34d9c9f
JF
3263 }
3264
1307df1a
JF
3265 if (ref_list)
3266 id_refs[id_refs_size++] = ref_list;
3267
3268 return ref_list;
c34d9c9f
JF
3269}
3270
3271static int
d0cea5f9 3272read_ref(char *id, int idlen, char *name, int namelen)
c34d9c9f 3273{
d0cea5f9
JF
3274 struct ref *ref;
3275 bool tag = FALSE;
d0cea5f9 3276
8b0297ae
JF
3277 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
3278 /* Commits referenced by tags has "^{}" appended. */
3279 if (name[namelen - 1] != '}')
3280 return OK;
3281
d0cea5f9
JF
3282 while (namelen > 0 && name[namelen] != '^')
3283 namelen--;
c34d9c9f 3284
d0cea5f9 3285 tag = TRUE;
8b0297ae
JF
3286 namelen -= STRING_SIZE("refs/tags/");
3287 name += STRING_SIZE("refs/tags/");
c34d9c9f 3288
d0cea5f9 3289 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
8b0297ae
JF
3290 namelen -= STRING_SIZE("refs/heads/");
3291 name += STRING_SIZE("refs/heads/");
c34d9c9f 3292
d0cea5f9
JF
3293 } else if (!strcmp(name, "HEAD")) {
3294 return OK;
3295 }
6706b2ba 3296
d0cea5f9
JF
3297 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
3298 if (!refs)
3299 return ERR;
c34d9c9f 3300
d0cea5f9 3301 ref = &refs[refs_size++];
8b0297ae 3302 ref->name = malloc(namelen + 1);
d0cea5f9
JF
3303 if (!ref->name)
3304 return ERR;
3af8774e 3305
8b0297ae
JF
3306 strncpy(ref->name, name, namelen);
3307 ref->name[namelen] = 0;
d0cea5f9
JF
3308 ref->tag = tag;
3309 string_copy(ref->id, id);
3af8774e 3310
d0cea5f9
JF
3311 return OK;
3312}
c34d9c9f 3313
d0cea5f9
JF
3314static int
3315load_refs(void)
3316{
3317 const char *cmd_env = getenv("TIG_LS_REMOTE");
3318 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
c34d9c9f 3319
4a63c884 3320 return read_properties(popen(cmd, "r"), "\t", read_ref);
d0cea5f9 3321}
c34d9c9f 3322
d0cea5f9 3323static int
14c778a6 3324read_repo_config_option(char *name, int namelen, char *value, int valuelen)
d0cea5f9 3325{
22913179 3326 if (!strcmp(name, "i18n.commitencoding"))
d0cea5f9 3327 string_copy(opt_encoding, value);
c34d9c9f 3328
c34d9c9f
JF
3329 return OK;
3330}
3331
4670cf89 3332static int
14c778a6 3333load_repo_config(void)
4670cf89 3334{
66749723 3335 return read_properties(popen("git repo-config --list", "r"),
14c778a6 3336 "=", read_repo_config_option);
d0cea5f9
JF
3337}
3338
3339static int
4a63c884 3340read_properties(FILE *pipe, const char *separators,
d0cea5f9
JF
3341 int (*read_property)(char *, int, char *, int))
3342{
4670cf89
JF
3343 char buffer[BUFSIZ];
3344 char *name;
d0cea5f9 3345 int state = OK;
4670cf89
JF
3346
3347 if (!pipe)
3348 return ERR;
3349
d0cea5f9 3350 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4a63c884
JF
3351 char *value;
3352 size_t namelen;
3353 size_t valuelen;
4670cf89 3354
4a63c884
JF
3355 name = chomp_string(name);
3356 namelen = strcspn(name, separators);
3357
3358 if (name[namelen]) {
3359 name[namelen] = 0;
3360 value = chomp_string(name + namelen + 1);
d0cea5f9 3361 valuelen = strlen(value);
4670cf89 3362
d0cea5f9 3363 } else {
d0cea5f9
JF
3364 value = "";
3365 valuelen = 0;
4670cf89 3366 }
d0cea5f9 3367
3c3801c2 3368 state = read_property(name, namelen, value, valuelen);
4670cf89
JF
3369 }
3370
d0cea5f9
JF
3371 if (state != ERR && ferror(pipe))
3372 state = ERR;
4670cf89
JF
3373
3374 pclose(pipe);
3375
d0cea5f9 3376 return state;
4670cf89
JF
3377}
3378
d0cea5f9 3379
6b161b31
JF
3380/*
3381 * Main
3382 */
3383
b5c9e67f 3384static void __NORETURN
6b161b31
JF
3385quit(int sig)
3386{
8855ada4
JF
3387 /* XXX: Restore tty modes and let the OS cleanup the rest! */
3388 if (cursed)
3389 endwin();
6b161b31
JF
3390 exit(0);
3391}
3392
c6704a4e
JF
3393static void __NORETURN
3394die(const char *err, ...)
6b161b31
JF
3395{
3396 va_list args;
3397
3398 endwin();
3399
3400 va_start(args, err);
3401 fputs("tig: ", stderr);
3402 vfprintf(stderr, err, args);
3403 fputs("\n", stderr);
3404 va_end(args);
3405
3406 exit(1);
3407}
3408
3409int
3410main(int argc, char *argv[])
3411{
1ba2ae4b 3412 struct view *view;
6b161b31 3413 enum request request;
1ba2ae4b 3414 size_t i;
6b161b31
JF
3415
3416 signal(SIGINT, quit);
3417
6b68fd24
JF
3418 if (setlocale(LC_ALL, "")) {
3419 string_copy(opt_codeset, nl_langinfo(CODESET));
3420 }
3421
660e09ad
JF
3422 if (load_options() == ERR)
3423 die("Failed to load user config.");
3424
3425 /* Load the repo config file so options can be overwritten from
afdc35b3 3426 * the command line. */
14c778a6 3427 if (load_repo_config() == ERR)
afdc35b3
JF
3428 die("Failed to load repo config.");
3429
8855ada4 3430 if (!parse_options(argc, argv))
6b161b31
JF
3431 return 0;
3432
6b68fd24
JF
3433 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
3434 opt_iconv = iconv_open(opt_codeset, opt_encoding);
20f4b4a3 3435 if (opt_iconv == ICONV_NONE)
6b68fd24
JF
3436 die("Failed to initialize character set conversion");
3437 }
3438
c34d9c9f
JF
3439 if (load_refs() == ERR)
3440 die("Failed to load refs.");
3441
7bb55251
JF
3442 /* Require a git repository unless when running in pager mode. */
3443 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
3444 die("Not a git repository");
3445
1ba2ae4b
JF
3446 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
3447 view->cmd_env = getenv(view->cmd_env);
3448
6b161b31
JF
3449 request = opt_request;
3450
3451 init_display();
b801d8b2
JF
3452
3453 while (view_driver(display[current_view], request)) {
6b161b31 3454 int key;
b801d8b2
JF
3455 int i;
3456
699ae55b 3457 foreach_view (view, i)
6b161b31 3458 update_view(view);
b801d8b2
JF
3459
3460 /* Refresh, accept single keystroke of input */
6b161b31 3461 key = wgetch(status_win);
04e2b7b2
JF
3462
3463 request = get_keybinding(display[current_view]->keymap, key);
03a93dbb 3464
6706b2ba 3465 /* Some low-level request handling. This keeps access to
fac7db6c
JF
3466 * status_win restricted. */
3467 switch (request) {
3468 case REQ_PROMPT:
9e21ce5c
JF
3469 {
3470 char *cmd = read_prompt(":");
3471
3472 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
3473 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
3474 opt_request = REQ_VIEW_DIFF;
3475 } else {
3476 opt_request = REQ_VIEW_PAGER;
3477 }
3478 break;
3479 }
fac7db6c 3480
1d754561 3481 request = REQ_NONE;
9e21ce5c
JF
3482 break;
3483 }
4af34daa
JF
3484 case REQ_SEARCH:
3485 case REQ_SEARCH_BACK:
3486 {
3487 const char *prompt = request == REQ_SEARCH
3488 ? "/" : "?";
3489 char *search = read_prompt(prompt);
3490
3491 if (search)
3492 string_copy(opt_search, search);
3493 else
3494 request = REQ_NONE;
3495 break;
3496 }
fac7db6c
JF
3497 case REQ_SCREEN_RESIZE:
3498 {
3499 int height, width;
3500
3501 getmaxyx(stdscr, height, width);
3502
3503 /* Resize the status view and let the view driver take
3504 * care of resizing the displayed views. */
3505 wresize(status_win, 1, width);
3506 mvwin(status_win, height - 1, 0);
3507 wrefresh(status_win);
3508 break;
3509 }
3510 default:
3511 break;
03a93dbb 3512 }
b801d8b2
JF
3513 }
3514
3515 quit(0);
3516
3517 return 0;
3518}