Make it possible to overwrite the default (terminal) colors
[tig] / tig.c
CommitLineData
53924375 1/* Copyright (c) 2006-2007 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
776bf2ac
JF
14#ifdef HAVE_CONFIG_H
15#include "config.h"
16#endif
17
ec31d0d0
SG
18#ifndef TIG_VERSION
19#define TIG_VERSION "unknown-version"
b76c2afc
JF
20#endif
21
8855ada4
JF
22#ifndef DEBUG
23#define NDEBUG
24#endif
25
22f66b0a 26#include <assert.h>
4c6fabc2 27#include <errno.h>
22f66b0a
JF
28#include <ctype.h>
29#include <signal.h>
b801d8b2 30#include <stdarg.h>
b801d8b2 31#include <stdio.h>
22f66b0a 32#include <stdlib.h>
b801d8b2 33#include <string.h>
810f0078
JF
34#include <sys/types.h>
35#include <sys/stat.h>
6908bdbd 36#include <unistd.h>
b76c2afc 37#include <time.h>
b801d8b2 38
4af34daa
JF
39#include <regex.h>
40
6b68fd24
JF
41#include <locale.h>
42#include <langinfo.h>
43#include <iconv.h>
44
b801d8b2 45#include <curses.h>
b801d8b2 46
e2da526d
JF
47#if __GNUC__ >= 3
48#define __NORETURN __attribute__((__noreturn__))
49#else
50#define __NORETURN
51#endif
52
53static void __NORETURN die(const char *err, ...);
b801d8b2 54static void report(const char *msg, ...);
5699e0cf 55static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
1ba2ae4b 56static void set_nonblocking_input(bool loading);
10e290ee 57static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
6b161b31
JF
58
59#define ABS(x) ((x) >= 0 ? (x) : -(x))
60#define MIN(x, y) ((x) < (y) ? (x) : (y))
61
62#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
63#define STRING_SIZE(x) (sizeof(x) - 1)
b76c2afc 64
17482b11 65#define SIZEOF_STR 1024 /* Default string size. */
2e8488b4 66#define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
10446330 67#define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
c8d60a25
JF
68
69/* Revision graph */
70
71#define REVGRAPH_INIT 'I'
72#define REVGRAPH_MERGE 'M'
73#define REVGRAPH_BRANCH '+'
74#define REVGRAPH_COMMIT '*'
e81e9c2c 75#define REVGRAPH_BOUND '^'
c8d60a25
JF
76#define REVGRAPH_LINE '|'
77
54efb62b 78#define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
b801d8b2 79
82e78006
JF
80/* This color name can be used to refer to the default term colors. */
81#define COLOR_DEFAULT (-1)
78c70acd 82
6b68fd24 83#define ICONV_NONE ((iconv_t) -1)
58a5e4ea
JF
84#ifndef ICONV_CONST
85#define ICONV_CONST /* nothing */
86#endif
6b68fd24 87
82e78006 88/* The format and size of the date column in the main view. */
4c6fabc2 89#define DATE_FORMAT "%Y-%m-%d %H:%M"
6b161b31 90#define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
4c6fabc2 91
10e290ee
JF
92#define AUTHOR_COLS 20
93
a28bcc22 94/* The default interval between line numbers. */
4a2909a7 95#define NUMBER_INTERVAL 1
82e78006 96
6706b2ba
JF
97#define TABSIZE 8
98
a28bcc22
JF
99#define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
100
96e58f5b 101#ifndef GIT_CONFIG
da633326 102#define GIT_CONFIG "git config"
96e58f5b
JF
103#endif
104
8eb62770 105#define TIG_LS_REMOTE \
337d7377 106 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
8eb62770
JF
107
108#define TIG_DIFF_CMD \
4ef1bf12 109 "git show --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
8eb62770
JF
110
111#define TIG_LOG_CMD \
8897e66a 112 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
8eb62770
JF
113
114#define TIG_MAIN_CMD \
e81e9c2c 115 "git log --no-color --topo-order --boundary --pretty=raw %s 2>/dev/null"
8eb62770 116
e733ee54
JF
117#define TIG_TREE_CMD \
118 "git ls-tree %s %s"
119
120#define TIG_BLOB_CMD \
121 "git cat-file blob %s"
122
8eb62770
JF
123/* XXX: Needs to be defined to the empty string. */
124#define TIG_HELP_CMD ""
125#define TIG_PAGER_CMD ""
173d76ea 126#define TIG_STATUS_CMD ""
3e634113 127#define TIG_STAGE_CMD ""
8eb62770 128
8855ada4 129/* Some ascii-shorthands fitted into the ncurses namespace. */
a28bcc22
JF
130#define KEY_TAB '\t'
131#define KEY_RETURN '\r'
4a2909a7
JF
132#define KEY_ESC 27
133
6706b2ba 134
c34d9c9f 135struct ref {
468876c9 136 char *name; /* Ref name; tag or head names are shortened. */
10446330 137 char id[SIZEOF_REV]; /* Commit SHA1 ID */
468876c9 138 unsigned int tag:1; /* Is it a tag? */
e15ec88e 139 unsigned int remote:1; /* Is it a remote ref? */
468876c9 140 unsigned int next:1; /* For ref lists: are there more refs? */
c34d9c9f
JF
141};
142
ff26aa29 143static struct ref **get_refs(char *id);
4c6fabc2 144
660e09ad
JF
145struct int_map {
146 const char *name;
147 int namelen;
148 int value;
149};
150
151static int
152set_from_int_map(struct int_map *map, size_t map_size,
153 int *value, const char *name, int namelen)
154{
155
156 int i;
157
158 for (i = 0; i < map_size; i++)
159 if (namelen == map[i].namelen &&
160 !strncasecmp(name, map[i].name, namelen)) {
161 *value = map[i].value;
162 return OK;
163 }
164
165 return ERR;
166}
167
6706b2ba 168
03a93dbb
JF
169/*
170 * String helpers
171 */
78c70acd 172
82e78006 173static inline void
9a48919b 174string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
82e78006 175{
9a48919b
JF
176 if (srclen > dstlen - 1)
177 srclen = dstlen - 1;
03a93dbb 178
9a48919b
JF
179 strncpy(dst, src, srclen);
180 dst[srclen] = 0;
82e78006
JF
181}
182
9a48919b
JF
183/* Shorthands for safely copying into a fixed buffer. */
184
82e78006 185#define string_copy(dst, src) \
751e27c9 186 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
9a48919b
JF
187
188#define string_ncopy(dst, src, srclen) \
189 string_ncopy_do(dst, sizeof(dst), src, srclen)
82e78006 190
2463b4ea
JF
191#define string_copy_rev(dst, src) \
192 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
193
91c5d983
JF
194#define string_add(dst, from, src) \
195 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
196
4a63c884
JF
197static char *
198chomp_string(char *name)
199{
200 int namelen;
201
202 while (isspace(*name))
203 name++;
204
205 namelen = strlen(name) - 1;
206 while (namelen > 0 && isspace(name[namelen]))
207 name[namelen--] = 0;
208
209 return name;
210}
211
cc2d1364 212static bool
d65ced0d 213string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
cc2d1364
JF
214{
215 va_list args;
d65ced0d 216 size_t pos = bufpos ? *bufpos : 0;
cc2d1364
JF
217
218 va_start(args, fmt);
219 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
220 va_end(args);
221
222 if (bufpos)
223 *bufpos = pos;
224
225 return pos >= bufsize ? FALSE : TRUE;
226}
227
228#define string_format(buf, fmt, args...) \
229 string_nformat(buf, sizeof(buf), NULL, fmt, args)
230
231#define string_format_from(buf, from, fmt, args...) \
232 string_nformat(buf, sizeof(buf), from, fmt, args)
6706b2ba 233
201f5a18
JF
234static int
235string_enum_compare(const char *str1, const char *str2, int len)
236{
237 size_t i;
238
239#define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
240
241 /* Diff-Header == DIFF_HEADER */
242 for (i = 0; i < len; i++) {
243 if (toupper(str1[i]) == toupper(str2[i]))
244 continue;
245
246 if (string_enum_sep(str1[i]) &&
247 string_enum_sep(str2[i]))
248 continue;
249
250 return str1[i] - str2[i];
251 }
252
253 return 0;
254}
255
03a93dbb
JF
256/* Shell quoting
257 *
258 * NOTE: The following is a slightly modified copy of the git project's shell
259 * quoting routines found in the quote.c file.
260 *
261 * Help to copy the thing properly quoted for the shell safety. any single
262 * quote is replaced with '\'', any exclamation point is replaced with '\!',
263 * and the whole thing is enclosed in a
264 *
265 * E.g.
266 * original sq_quote result
267 * name ==> name ==> 'name'
268 * a b ==> a b ==> 'a b'
269 * a'b ==> a'\''b ==> 'a'\''b'
270 * a!b ==> a'\!'b ==> 'a'\!'b'
271 */
272
273static size_t
17482b11 274sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
03a93dbb
JF
275{
276 char c;
277
17482b11 278#define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
03a93dbb
JF
279
280 BUFPUT('\'');
281 while ((c = *src++)) {
282 if (c == '\'' || c == '!') {
283 BUFPUT('\'');
284 BUFPUT('\\');
285 BUFPUT(c);
286 BUFPUT('\'');
287 } else {
288 BUFPUT(c);
289 }
290 }
291 BUFPUT('\'');
292
f0f114ac
JF
293 if (bufsize < SIZEOF_STR)
294 buf[bufsize] = 0;
295
03a93dbb
JF
296 return bufsize;
297}
298
82e78006 299
24b5b3e0
JF
300/*
301 * User requests
302 */
303
304#define REQ_INFO \
305 /* XXX: Keep the view request first and in sync with views[]. */ \
306 REQ_GROUP("View switching") \
307 REQ_(VIEW_MAIN, "Show main view"), \
308 REQ_(VIEW_DIFF, "Show diff view"), \
309 REQ_(VIEW_LOG, "Show log view"), \
e733ee54
JF
310 REQ_(VIEW_TREE, "Show tree view"), \
311 REQ_(VIEW_BLOB, "Show blob view"), \
24b5b3e0
JF
312 REQ_(VIEW_HELP, "Show help page"), \
313 REQ_(VIEW_PAGER, "Show pager view"), \
173d76ea 314 REQ_(VIEW_STATUS, "Show status view"), \
3e634113 315 REQ_(VIEW_STAGE, "Show stage view"), \
24b5b3e0
JF
316 \
317 REQ_GROUP("View manipulation") \
318 REQ_(ENTER, "Enter current line and scroll"), \
319 REQ_(NEXT, "Move to next"), \
320 REQ_(PREVIOUS, "Move to previous"), \
321 REQ_(VIEW_NEXT, "Move focus to next view"), \
acaef3b3 322 REQ_(REFRESH, "Reload and refresh"), \
24b5b3e0
JF
323 REQ_(VIEW_CLOSE, "Close the current view"), \
324 REQ_(QUIT, "Close all views and quit"), \
325 \
326 REQ_GROUP("Cursor navigation") \
327 REQ_(MOVE_UP, "Move cursor one line up"), \
328 REQ_(MOVE_DOWN, "Move cursor one line down"), \
329 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
330 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
331 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
332 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
333 \
334 REQ_GROUP("Scrolling") \
335 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
336 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
337 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
338 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
339 \
4af34daa
JF
340 REQ_GROUP("Searching") \
341 REQ_(SEARCH, "Search the view"), \
342 REQ_(SEARCH_BACK, "Search backwards in the view"), \
343 REQ_(FIND_NEXT, "Find next search match"), \
344 REQ_(FIND_PREV, "Find previous search match"), \
345 \
24b5b3e0
JF
346 REQ_GROUP("Misc") \
347 REQ_(PROMPT, "Bring up the prompt"), \
24b5b3e0
JF
348 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
349 REQ_(SCREEN_RESIZE, "Resize the screen"), \
350 REQ_(SHOW_VERSION, "Show version information"), \
351 REQ_(STOP_LOADING, "Stop all loading views"), \
54efb62b 352 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
ca1d71ea 353 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
0cea0d43 354 REQ_(STATUS_UPDATE, "Update file status"), \
b5c18d9d 355 REQ_(STATUS_MERGE, "Merge file using external tool"), \
c509eed2 356 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
226da94b 357 REQ_(EDIT, "Open in editor"), \
d31a629d 358 REQ_(NONE, "Do nothing")
24b5b3e0
JF
359
360
361/* User action requests. */
362enum request {
363#define REQ_GROUP(help)
364#define REQ_(req, help) REQ_##req
365
366 /* Offset all requests to avoid conflicts with ncurses getch values. */
367 REQ_OFFSET = KEY_MAX + 1,
d31a629d 368 REQ_INFO
24b5b3e0
JF
369
370#undef REQ_GROUP
371#undef REQ_
372};
373
374struct request_info {
375 enum request request;
04e2b7b2
JF
376 char *name;
377 int namelen;
24b5b3e0
JF
378 char *help;
379};
380
381static struct request_info req_info[] = {
04e2b7b2
JF
382#define REQ_GROUP(help) { 0, NULL, 0, (help) },
383#define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
24b5b3e0
JF
384 REQ_INFO
385#undef REQ_GROUP
386#undef REQ_
387};
388
04e2b7b2
JF
389static enum request
390get_request(const char *name)
391{
392 int namelen = strlen(name);
393 int i;
394
395 for (i = 0; i < ARRAY_SIZE(req_info); i++)
396 if (req_info[i].namelen == namelen &&
397 !string_enum_compare(req_info[i].name, name, namelen))
398 return req_info[i].request;
399
d31a629d 400 return REQ_NONE;
04e2b7b2
JF
401}
402
403
8eb62770
JF
404/*
405 * Options
406 */
b76c2afc 407
4b8c01a3 408static const char usage[] =
ec31d0d0 409"tig " TIG_VERSION " (" __DATE__ ")\n"
4b8c01a3
JF
410"\n"
411"Usage: tig [options]\n"
412" or: tig [options] [--] [git log options]\n"
413" or: tig [options] log [git log options]\n"
414" or: tig [options] diff [git diff options]\n"
415" or: tig [options] show [git show options]\n"
416" or: tig [options] < [git command output]\n"
417"\n"
418"Options:\n"
419" -l Start up in log view\n"
420" -d Start up in diff view\n"
173d76ea 421" -S Start up in status view\n"
4b8c01a3 422" -n[I], --line-number[=I] Show line numbers with given interval\n"
b3c965c9 423" -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
4b8c01a3
JF
424" -- Mark end of tig options\n"
425" -v, --version Show version and exit\n"
426" -h, --help Show help message and exit\n";
427
6706b2ba 428/* Option and state variables. */
92d30f5c 429static bool opt_line_number = FALSE;
11ce319e 430static bool opt_rev_graph = FALSE;
92d30f5c
JF
431static int opt_num_interval = NUMBER_INTERVAL;
432static int opt_tab_size = TABSIZE;
433static enum request opt_request = REQ_VIEW_MAIN;
434static char opt_cmd[SIZEOF_STR] = "";
e733ee54 435static char opt_path[SIZEOF_STR] = "";
92d30f5c
JF
436static FILE *opt_pipe = NULL;
437static char opt_encoding[20] = "UTF-8";
438static bool opt_utf8 = TRUE;
439static char opt_codeset[20] = "UTF-8";
440static iconv_t opt_iconv = ICONV_NONE;
441static char opt_search[SIZEOF_STR] = "";
91c5d983 442static char opt_cdup[SIZEOF_STR] = "";
810f0078 443static char opt_git_dir[SIZEOF_STR] = "";
c38c64bb 444static char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
0cea0d43 445static char opt_editor[SIZEOF_STR] = "";
b76c2afc 446
6dbf6c19
JF
447enum option_type {
448 OPT_NONE,
449 OPT_INT,
450};
451
452static bool
453check_option(char *opt, char short_name, char *name, enum option_type type, ...)
454{
455 va_list args;
456 char *value = "";
457 int *number;
458
459 if (opt[0] != '-')
460 return FALSE;
461
462 if (opt[1] == '-') {
463 int namelen = strlen(name);
464
465 opt += 2;
466
467 if (strncmp(opt, name, namelen))
468 return FALSE;
469
470 if (opt[namelen] == '=')
471 value = opt + namelen + 1;
472
473 } else {
474 if (!short_name || opt[1] != short_name)
475 return FALSE;
476 value = opt + 2;
477 }
478
479 va_start(args, type);
480 if (type == OPT_INT) {
481 number = va_arg(args, int *);
482 if (isdigit(*value))
483 *number = atoi(value);
484 }
485 va_end(args);
486
487 return TRUE;
488}
489
b76c2afc 490/* Returns the index of log or diff command or -1 to exit. */
8855ada4 491static bool
b76c2afc
JF
492parse_options(int argc, char *argv[])
493{
494 int i;
495
496 for (i = 1; i < argc; i++) {
497 char *opt = argv[i];
498
3621d94e
JF
499 if (!strcmp(opt, "log") ||
500 !strcmp(opt, "diff") ||
501 !strcmp(opt, "show")) {
502 opt_request = opt[0] == 'l'
503 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
504 break;
505 }
506
8d27b36e
JF
507 if (opt[0] && opt[0] != '-')
508 break;
509
bfe97931
JF
510 if (!strcmp(opt, "--")) {
511 i++;
512 break;
513 }
514
515 if (check_option(opt, 'v', "version", OPT_NONE)) {
516 printf("tig version %s\n", TIG_VERSION);
517 return FALSE;
518 }
519
520 if (check_option(opt, 'h', "help", OPT_NONE)) {
521 printf(usage);
522 return FALSE;
523 }
524
525 if (!strcmp(opt, "-S")) {
526 opt_request = REQ_VIEW_STATUS;
527 continue;
528 }
529
6b161b31 530 if (!strcmp(opt, "-l")) {
4a2909a7 531 opt_request = REQ_VIEW_LOG;
6b161b31
JF
532 continue;
533 }
b76c2afc 534
6b161b31 535 if (!strcmp(opt, "-d")) {
4a2909a7 536 opt_request = REQ_VIEW_DIFF;
6b161b31
JF
537 continue;
538 }
b76c2afc 539
6dbf6c19 540 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
6b161b31
JF
541 opt_line_number = TRUE;
542 continue;
543 }
b76c2afc 544
6dbf6c19
JF
545 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
546 opt_tab_size = MIN(opt_tab_size, TABSIZE);
6706b2ba
JF
547 continue;
548 }
549
bf174187 550 die("unknown option '%s'\n\n%s", opt, usage);
b76c2afc
JF
551 }
552
6908bdbd 553 if (!isatty(STDIN_FILENO)) {
6908bdbd
JF
554 opt_request = REQ_VIEW_PAGER;
555 opt_pipe = stdin;
556
557 } else if (i < argc) {
558 size_t buf_size;
559
6908bdbd 560 if (opt_request == REQ_VIEW_MAIN)
8855ada4
JF
561 /* XXX: This is vulnerable to the user overriding
562 * options required for the main view parser. */
e81e9c2c 563 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary");
6908bdbd
JF
564 else
565 string_copy(opt_cmd, "git");
566 buf_size = strlen(opt_cmd);
567
568 while (buf_size < sizeof(opt_cmd) && i < argc) {
569 opt_cmd[buf_size++] = ' ';
570 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
571 }
572
573 if (buf_size >= sizeof(opt_cmd))
574 die("command too long");
575
576 opt_cmd[buf_size] = 0;
6908bdbd
JF
577 }
578
afdc35b3
JF
579 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
580 opt_utf8 = FALSE;
581
8855ada4 582 return TRUE;
b76c2afc
JF
583}
584
585
54efb62b
JF
586/*
587 * Line-oriented content detection.
588 */
589
2e8488b4 590#define LINE_INFO \
660e09ad 591LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
a28bcc22
JF
592LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
593LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
594LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
660e09ad
JF
595LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
596LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
597LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
599LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
600LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
601LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
602LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
603LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
604LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
6908bdbd 605LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
8855ada4 606LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
6908bdbd
JF
607LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
608LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
8855ada4
JF
609LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
610LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
7b99a34c 611LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
a28bcc22
JF
612LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
613LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
614LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
8855ada4 615LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
a28bcc22 616LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
a28bcc22 617LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
d4d8de8f 618LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
a28bcc22
JF
619LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
620LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
621LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
6b161b31
JF
622LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
623LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
a28bcc22
JF
624LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
625LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
626LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
c34d9c9f
JF
627LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
628LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
e15ec88e 629LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
660e09ad 630LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
f83b1c33 631LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
e733ee54 632LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
173d76ea 633LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
53924375 634LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
173d76ea 635LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
53924375
JF
636LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
637LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
173d76ea 638LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
660e09ad 639
78c70acd 640enum line_type {
2e8488b4
JF
641#define LINE(type, line, fg, bg, attr) \
642 LINE_##type
643 LINE_INFO
644#undef LINE
78c70acd
JF
645};
646
647struct line_info {
660e09ad
JF
648 const char *name; /* Option name. */
649 int namelen; /* Size of option name. */
4685845e 650 const char *line; /* The start of line to match. */
2e8488b4
JF
651 int linelen; /* Size of string to match. */
652 int fg, bg, attr; /* Color and text attributes for the lines. */
78c70acd
JF
653};
654
2e8488b4 655static struct line_info line_info[] = {
78c70acd 656#define LINE(type, line, fg, bg, attr) \
660e09ad 657 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
2e8488b4
JF
658 LINE_INFO
659#undef LINE
78c70acd
JF
660};
661
2e8488b4
JF
662static enum line_type
663get_line_type(char *line)
78c70acd
JF
664{
665 int linelen = strlen(line);
a28bcc22 666 enum line_type type;
78c70acd 667
a28bcc22 668 for (type = 0; type < ARRAY_SIZE(line_info); type++)
2e8488b4 669 /* Case insensitive search matches Signed-off-by lines better. */
a28bcc22
JF
670 if (linelen >= line_info[type].linelen &&
671 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
672 return type;
78c70acd 673
2e8488b4 674 return LINE_DEFAULT;
78c70acd
JF
675}
676
2e8488b4 677static inline int
78c70acd
JF
678get_line_attr(enum line_type type)
679{
2e8488b4
JF
680 assert(type < ARRAY_SIZE(line_info));
681 return COLOR_PAIR(type) | line_info[type].attr;
78c70acd
JF
682}
683
660e09ad
JF
684static struct line_info *
685get_line_info(char *name, int namelen)
686{
687 enum line_type type;
660e09ad
JF
688
689 for (type = 0; type < ARRAY_SIZE(line_info); type++)
690 if (namelen == line_info[type].namelen &&
201f5a18 691 !string_enum_compare(line_info[type].name, name, namelen))
660e09ad
JF
692 return &line_info[type];
693
694 return NULL;
695}
696
78c70acd
JF
697static void
698init_colors(void)
699{
62c7d1a7
JF
700 int default_bg = line_info[LINE_DEFAULT].bg;
701 int default_fg = line_info[LINE_DEFAULT].fg;
a28bcc22 702 enum line_type type;
78c70acd
JF
703
704 start_color();
705
62c7d1a7
JF
706 if (assume_default_colors(default_fg, default_bg) == ERR) {
707 default_bg = COLOR_BLACK;
708 default_fg = COLOR_WHITE;
78c70acd
JF
709 }
710
a28bcc22
JF
711 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
712 struct line_info *info = &line_info[type];
82e78006
JF
713 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
714 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
78c70acd 715
a28bcc22 716 init_pair(type, fg, bg);
78c70acd
JF
717 }
718}
719
fe7233c3
JF
720struct line {
721 enum line_type type;
3c571d67
JF
722
723 /* State flags */
724 unsigned int selected:1;
725
fe7233c3
JF
726 void *data; /* User data */
727};
728
78c70acd 729
1899507c 730/*
37157fa0
JF
731 * Keys
732 */
733
93a97d86 734struct keybinding {
37157fa0 735 int alias;
93a97d86 736 enum request request;
04e2b7b2 737 struct keybinding *next;
37157fa0
JF
738};
739
93a97d86 740static struct keybinding default_keybindings[] = {
37157fa0
JF
741 /* View switching */
742 { 'm', REQ_VIEW_MAIN },
743 { 'd', REQ_VIEW_DIFF },
744 { 'l', REQ_VIEW_LOG },
e733ee54 745 { 't', REQ_VIEW_TREE },
0001fc34 746 { 'f', REQ_VIEW_BLOB },
37157fa0
JF
747 { 'p', REQ_VIEW_PAGER },
748 { 'h', REQ_VIEW_HELP },
173d76ea 749 { 'S', REQ_VIEW_STATUS },
3e634113 750 { 'c', REQ_VIEW_STAGE },
37157fa0
JF
751
752 /* View manipulation */
753 { 'q', REQ_VIEW_CLOSE },
754 { KEY_TAB, REQ_VIEW_NEXT },
755 { KEY_RETURN, REQ_ENTER },
756 { KEY_UP, REQ_PREVIOUS },
757 { KEY_DOWN, REQ_NEXT },
acaef3b3 758 { 'R', REQ_REFRESH },
37157fa0
JF
759
760 /* Cursor navigation */
761 { 'k', REQ_MOVE_UP },
762 { 'j', REQ_MOVE_DOWN },
763 { KEY_HOME, REQ_MOVE_FIRST_LINE },
764 { KEY_END, REQ_MOVE_LAST_LINE },
765 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
766 { ' ', REQ_MOVE_PAGE_DOWN },
767 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
768 { 'b', REQ_MOVE_PAGE_UP },
769 { '-', REQ_MOVE_PAGE_UP },
770
771 /* Scrolling */
772 { KEY_IC, REQ_SCROLL_LINE_UP },
773 { KEY_DC, REQ_SCROLL_LINE_DOWN },
774 { 'w', REQ_SCROLL_PAGE_UP },
775 { 's', REQ_SCROLL_PAGE_DOWN },
776
4af34daa
JF
777 /* Searching */
778 { '/', REQ_SEARCH },
779 { '?', REQ_SEARCH_BACK },
780 { 'n', REQ_FIND_NEXT },
781 { 'N', REQ_FIND_PREV },
782
37157fa0
JF
783 /* Misc */
784 { 'Q', REQ_QUIT },
785 { 'z', REQ_STOP_LOADING },
786 { 'v', REQ_SHOW_VERSION },
787 { 'r', REQ_SCREEN_REDRAW },
904e68d8 788 { '.', REQ_TOGGLE_LINENO },
73fb51d5 789 { 'g', REQ_TOGGLE_REV_GRAPH },
37157fa0 790 { ':', REQ_PROMPT },
ca1d71ea 791 { 'u', REQ_STATUS_UPDATE },
b5c18d9d 792 { 'M', REQ_STATUS_MERGE },
c509eed2 793 { ',', REQ_TREE_PARENT },
0cea0d43 794 { 'e', REQ_EDIT },
37157fa0 795
1d754561 796 /* Using the ncurses SIGWINCH handler. */
37157fa0
JF
797 { KEY_RESIZE, REQ_SCREEN_RESIZE },
798};
799
04e2b7b2
JF
800#define KEYMAP_INFO \
801 KEYMAP_(GENERIC), \
802 KEYMAP_(MAIN), \
803 KEYMAP_(DIFF), \
804 KEYMAP_(LOG), \
e733ee54
JF
805 KEYMAP_(TREE), \
806 KEYMAP_(BLOB), \
04e2b7b2 807 KEYMAP_(PAGER), \
173d76ea 808 KEYMAP_(HELP), \
3e634113
JF
809 KEYMAP_(STATUS), \
810 KEYMAP_(STAGE)
04e2b7b2
JF
811
812enum keymap {
813#define KEYMAP_(name) KEYMAP_##name
814 KEYMAP_INFO
815#undef KEYMAP_
816};
817
818static struct int_map keymap_table[] = {
819#define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
820 KEYMAP_INFO
821#undef KEYMAP_
822};
823
824#define set_keymap(map, name) \
825 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
826
827static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
828
829static void
830add_keybinding(enum keymap keymap, enum request request, int key)
831{
832 struct keybinding *keybinding;
833
834 keybinding = calloc(1, sizeof(*keybinding));
835 if (!keybinding)
836 die("Failed to allocate keybinding");
837
838 keybinding->alias = key;
839 keybinding->request = request;
840 keybinding->next = keybindings[keymap];
841 keybindings[keymap] = keybinding;
842}
843
844/* Looks for a key binding first in the given map, then in the generic map, and
845 * lastly in the default keybindings. */
37157fa0 846static enum request
04e2b7b2 847get_keybinding(enum keymap keymap, int key)
37157fa0 848{
04e2b7b2 849 struct keybinding *kbd;
37157fa0
JF
850 int i;
851
04e2b7b2
JF
852 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
853 if (kbd->alias == key)
854 return kbd->request;
855
856 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
857 if (kbd->alias == key)
858 return kbd->request;
859
93a97d86
JF
860 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
861 if (default_keybindings[i].alias == key)
862 return default_keybindings[i].request;
37157fa0
JF
863
864 return (enum request) key;
865}
866
93a97d86 867
37157fa0
JF
868struct key {
869 char *name;
870 int value;
871};
872
873static struct key key_table[] = {
874 { "Enter", KEY_RETURN },
875 { "Space", ' ' },
876 { "Backspace", KEY_BACKSPACE },
877 { "Tab", KEY_TAB },
878 { "Escape", KEY_ESC },
879 { "Left", KEY_LEFT },
880 { "Right", KEY_RIGHT },
881 { "Up", KEY_UP },
882 { "Down", KEY_DOWN },
883 { "Insert", KEY_IC },
884 { "Delete", KEY_DC },
74f83ee6 885 { "Hash", '#' },
37157fa0
JF
886 { "Home", KEY_HOME },
887 { "End", KEY_END },
888 { "PageUp", KEY_PPAGE },
889 { "PageDown", KEY_NPAGE },
890 { "F1", KEY_F(1) },
891 { "F2", KEY_F(2) },
892 { "F3", KEY_F(3) },
893 { "F4", KEY_F(4) },
894 { "F5", KEY_F(5) },
895 { "F6", KEY_F(6) },
896 { "F7", KEY_F(7) },
897 { "F8", KEY_F(8) },
898 { "F9", KEY_F(9) },
899 { "F10", KEY_F(10) },
900 { "F11", KEY_F(11) },
901 { "F12", KEY_F(12) },
902};
903
04e2b7b2
JF
904static int
905get_key_value(const char *name)
906{
907 int i;
908
909 for (i = 0; i < ARRAY_SIZE(key_table); i++)
910 if (!strcasecmp(key_table[i].name, name))
911 return key_table[i].value;
912
913 if (strlen(name) == 1 && isprint(*name))
914 return (int) *name;
915
916 return ERR;
917}
918
37157fa0 919static char *
9eb14b72
JF
920get_key_name(int key_value)
921{
922 static char key_char[] = "'X'";
923 char *seq = NULL;
924 int key;
925
926 for (key = 0; key < ARRAY_SIZE(key_table); key++)
927 if (key_table[key].value == key_value)
928 seq = key_table[key].name;
929
930 if (seq == NULL &&
931 key_value < 127 &&
932 isprint(key_value)) {
933 key_char[1] = (char) key_value;
934 seq = key_char;
935 }
936
937 return seq ? seq : "'?'";
938}
939
940static char *
37157fa0
JF
941get_key(enum request request)
942{
943 static char buf[BUFSIZ];
d65ced0d 944 size_t pos = 0;
520094b4 945 char *sep = "";
37157fa0
JF
946 int i;
947
948 buf[pos] = 0;
949
93a97d86
JF
950 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
951 struct keybinding *keybinding = &default_keybindings[i];
37157fa0 952
93a97d86 953 if (keybinding->request != request)
37157fa0
JF
954 continue;
955
738bee4a
JF
956 if (!string_format_from(buf, &pos, "%s%s", sep,
957 get_key_name(keybinding->alias)))
37157fa0
JF
958 return "Too many keybindings!";
959 sep = ", ";
960 }
961
962 return buf;
963}
964
9eb14b72
JF
965struct run_request {
966 enum keymap keymap;
967 int key;
968 char cmd[SIZEOF_STR];
969};
970
971static struct run_request *run_request;
972static size_t run_requests;
973
974static enum request
975add_run_request(enum keymap keymap, int key, int argc, char **argv)
976{
977 struct run_request *tmp;
978 struct run_request req = { keymap, key };
979 size_t bufpos;
980
981 for (bufpos = 0; argc > 0; argc--, argv++)
982 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
983 return REQ_NONE;
984
985 req.cmd[bufpos - 1] = 0;
986
987 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
988 if (!tmp)
989 return REQ_NONE;
990
991 run_request = tmp;
992 run_request[run_requests++] = req;
993
994 return REQ_NONE + run_requests;
995}
996
997static struct run_request *
998get_run_request(enum request request)
999{
1000 if (request <= REQ_NONE)
1001 return NULL;
1002 return &run_request[request - REQ_NONE - 1];
1003}
37157fa0 1004
f655964f
JF
1005static void
1006add_builtin_run_requests(void)
1007{
1008 struct {
1009 enum keymap keymap;
1010 int key;
1011 char *argv[1];
1012 } reqs[] = {
1013 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1014 { KEYMAP_GENERIC, 'G', { "git gc" } },
1015 };
1016 int i;
1017
1018 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1019 enum request req;
1020
1021 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1022 if (req != REQ_NONE)
1023 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1024 }
1025}
1026
37157fa0 1027/*
1899507c
JF
1028 * User config file handling.
1029 */
1030
5dc795f2
JF
1031static struct int_map color_map[] = {
1032#define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1033 COLOR_MAP(DEFAULT),
1034 COLOR_MAP(BLACK),
1035 COLOR_MAP(BLUE),
1036 COLOR_MAP(CYAN),
1037 COLOR_MAP(GREEN),
1038 COLOR_MAP(MAGENTA),
1039 COLOR_MAP(RED),
1040 COLOR_MAP(WHITE),
1041 COLOR_MAP(YELLOW),
1042};
1043
9256ab05
JF
1044#define set_color(color, name) \
1045 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
660e09ad 1046
5dc795f2
JF
1047static struct int_map attr_map[] = {
1048#define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1049 ATTR_MAP(NORMAL),
1050 ATTR_MAP(BLINK),
1051 ATTR_MAP(BOLD),
1052 ATTR_MAP(DIM),
1053 ATTR_MAP(REVERSE),
1054 ATTR_MAP(STANDOUT),
1055 ATTR_MAP(UNDERLINE),
1056};
1057
9256ab05
JF
1058#define set_attribute(attr, name) \
1059 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
660e09ad 1060
3c3801c2
JF
1061static int config_lineno;
1062static bool config_errors;
1063static char *config_msg;
1064
5bfd96c7 1065/* Wants: object fgcolor bgcolor [attr] */
660e09ad 1066static int
5bfd96c7 1067option_color_command(int argc, char *argv[])
660e09ad 1068{
bca8fcaa
JF
1069 struct line_info *info;
1070
9256ab05
JF
1071 if (argc != 3 && argc != 4) {
1072 config_msg = "Wrong number of arguments given to color command";
1073 return ERR;
1074 }
1075
1076 info = get_line_info(argv[0], strlen(argv[0]));
bca8fcaa
JF
1077 if (!info) {
1078 config_msg = "Unknown color name";
1079 return ERR;
1080 }
660e09ad 1081
a3653368
JF
1082 if (set_color(&info->fg, argv[1]) == ERR ||
1083 set_color(&info->bg, argv[2]) == ERR) {
bca8fcaa
JF
1084 config_msg = "Unknown color";
1085 return ERR;
1086 }
660e09ad 1087
9256ab05 1088 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
bca8fcaa
JF
1089 config_msg = "Unknown attribute";
1090 return ERR;
660e09ad
JF
1091 }
1092
bca8fcaa
JF
1093 return OK;
1094}
1095
5bfd96c7
JF
1096/* Wants: name = value */
1097static int
1098option_set_command(int argc, char *argv[])
1099{
1100 if (argc != 3) {
1101 config_msg = "Wrong number of arguments given to set command";
1102 return ERR;
1103 }
1104
1105 if (strcmp(argv[1], "=")) {
1106 config_msg = "No value assigned";
1107 return ERR;
1108 }
1109
1110 if (!strcmp(argv[0], "show-rev-graph")) {
1111 opt_rev_graph = (!strcmp(argv[2], "1") ||
1112 !strcmp(argv[2], "true") ||
1113 !strcmp(argv[2], "yes"));
1114 return OK;
1115 }
1116
1117 if (!strcmp(argv[0], "line-number-interval")) {
1118 opt_num_interval = atoi(argv[2]);
1119 return OK;
1120 }
1121
1122 if (!strcmp(argv[0], "tab-size")) {
1123 opt_tab_size = atoi(argv[2]);
1124 return OK;
1125 }
1126
cb7267ee 1127 if (!strcmp(argv[0], "commit-encoding")) {
3cc9a4d4
JF
1128 char *arg = argv[2];
1129 int delimiter = *arg;
1130 int i;
1131
1132 switch (delimiter) {
1133 case '"':
1134 case '\'':
1135 for (arg++, i = 0; arg[i]; i++)
1136 if (arg[i] == delimiter) {
1137 arg[i] = 0;
1138 break;
1139 }
1140 default:
739e81de 1141 string_ncopy(opt_encoding, arg, strlen(arg));
3cc9a4d4
JF
1142 return OK;
1143 }
5bfd96c7
JF
1144 }
1145
a3653368 1146 config_msg = "Unknown variable name";
5bfd96c7
JF
1147 return ERR;
1148}
1149
04e2b7b2
JF
1150/* Wants: mode request key */
1151static int
1152option_bind_command(int argc, char *argv[])
1153{
1154 enum request request;
1155 int keymap;
1156 int key;
1157
9eb14b72 1158 if (argc < 3) {
04e2b7b2
JF
1159 config_msg = "Wrong number of arguments given to bind command";
1160 return ERR;
1161 }
1162
1163 if (set_keymap(&keymap, argv[0]) == ERR) {
1164 config_msg = "Unknown key map";
1165 return ERR;
1166 }
1167
1168 key = get_key_value(argv[1]);
1169 if (key == ERR) {
1170 config_msg = "Unknown key";
1171 return ERR;
1172 }
1173
1174 request = get_request(argv[2]);
f40385ae 1175 if (request == REQ_NONE) {
f655964f
JF
1176 const char *obsolete[] = { "cherry-pick" };
1177 size_t namelen = strlen(argv[2]);
1178 int i;
1179
1180 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1181 if (namelen == strlen(obsolete[i]) &&
1182 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1183 config_msg = "Obsolete request name";
1184 return ERR;
1185 }
1186 }
1187 }
9eb14b72
JF
1188 if (request == REQ_NONE && *argv[2]++ == '!')
1189 request = add_run_request(keymap, key, argc - 2, argv + 2);
d31a629d 1190 if (request == REQ_NONE) {
04e2b7b2
JF
1191 config_msg = "Unknown request name";
1192 return ERR;
1193 }
1194
1195 add_keybinding(keymap, request, key);
1196
1197 return OK;
1198}
1199
bca8fcaa 1200static int
9256ab05 1201set_option(char *opt, char *value)
bca8fcaa 1202{
9256ab05
JF
1203 char *argv[16];
1204 int valuelen;
1205 int argc = 0;
1206
1207 /* Tokenize */
1208 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1209 argv[argc++] = value;
9256ab05 1210 value += valuelen;
b86250da
JF
1211
1212 /* Nothing more to tokenize or last available token. */
1213 if (!*value || argc >= ARRAY_SIZE(argv))
9256ab05
JF
1214 break;
1215
1216 *value++ = 0;
1217 while (isspace(*value))
1218 value++;
1219 }
1220
1221 if (!strcmp(opt, "color"))
5bfd96c7
JF
1222 return option_color_command(argc, argv);
1223
1224 if (!strcmp(opt, "set"))
1225 return option_set_command(argc, argv);
bca8fcaa 1226
04e2b7b2
JF
1227 if (!strcmp(opt, "bind"))
1228 return option_bind_command(argc, argv);
1229
a3653368 1230 config_msg = "Unknown option command";
660e09ad
JF
1231 return ERR;
1232}
1233
1234static int
5699e0cf 1235read_option(char *opt, size_t optlen, char *value, size_t valuelen)
3c3801c2 1236{
a3653368
JF
1237 int status = OK;
1238
3c3801c2
JF
1239 config_lineno++;
1240 config_msg = "Internal error";
1241
a3653368
JF
1242 /* Check for comment markers, since read_properties() will
1243 * only ensure opt and value are split at first " \t". */
74f83ee6 1244 optlen = strcspn(opt, "#");
a3653368 1245 if (optlen == 0)
3c3801c2
JF
1246 return OK;
1247
a3653368
JF
1248 if (opt[optlen] != 0) {
1249 config_msg = "No option value";
1250 status = ERR;
1251
1252 } else {
1253 /* Look for comment endings in the value. */
5699e0cf 1254 size_t len = strcspn(value, "#");
a3653368
JF
1255
1256 if (len < valuelen) {
1257 valuelen = len;
1258 value[valuelen] = 0;
1259 }
1260
1261 status = set_option(opt, value);
3c3801c2
JF
1262 }
1263
a3653368
JF
1264 if (status == ERR) {
1265 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
07c3971e 1266 config_lineno, (int) optlen, opt, config_msg);
3c3801c2
JF
1267 config_errors = TRUE;
1268 }
1269
1270 /* Always keep going if errors are encountered. */
1271 return OK;
1272}
1273
1274static int
660e09ad
JF
1275load_options(void)
1276{
1277 char *home = getenv("HOME");
17482b11 1278 char buf[SIZEOF_STR];
660e09ad
JF
1279 FILE *file;
1280
3c3801c2
JF
1281 config_lineno = 0;
1282 config_errors = FALSE;
1283
f655964f
JF
1284 add_builtin_run_requests();
1285
cc2d1364 1286 if (!home || !string_format(buf, "%s/.tigrc", home))
660e09ad
JF
1287 return ERR;
1288
1289 /* It's ok that the file doesn't exist. */
1290 file = fopen(buf, "r");
1291 if (!file)
1292 return OK;
1293
3c3801c2
JF
1294 if (read_properties(file, " \t", read_option) == ERR ||
1295 config_errors == TRUE)
1296 fprintf(stderr, "Errors while loading %s.\n", buf);
1297
1298 return OK;
660e09ad
JF
1299}
1300
1301
d839253b 1302/*
468876c9 1303 * The viewer
d839253b 1304 */
c2124ccd
JF
1305
1306struct view;
fe7233c3 1307struct view_ops;
c2124ccd
JF
1308
1309/* The display array of active views and the index of the current view. */
1310static struct view *display[2];
1311static unsigned int current_view;
1312
ab4af23e
JF
1313/* Reading from the prompt? */
1314static bool input_mode = FALSE;
1315
33c4f9ea 1316#define foreach_displayed_view(view, i) \
c2124ccd
JF
1317 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1318
9f41488f 1319#define displayed_views() (display[1] != NULL ? 2 : 1)
c2124ccd 1320
d839253b 1321/* Current head and commit ID */
e733ee54 1322static char ref_blob[SIZEOF_REF] = "";
c2124ccd
JF
1323static char ref_commit[SIZEOF_REF] = "HEAD";
1324static char ref_head[SIZEOF_REF] = "HEAD";
1325
b801d8b2 1326struct view {
03a93dbb 1327 const char *name; /* View name */
4685845e
TH
1328 const char *cmd_fmt; /* Default command line format */
1329 const char *cmd_env; /* Command line set via environment */
e733ee54 1330 const char *id; /* Points to either of ref_{head,commit,blob} */
6b161b31 1331
fe7233c3 1332 struct view_ops *ops; /* View operations */
22f66b0a 1333
04e2b7b2
JF
1334 enum keymap keymap; /* What keymap does this view have */
1335
17482b11 1336 char cmd[SIZEOF_STR]; /* Command buffer */
49f2b43f
JF
1337 char ref[SIZEOF_REF]; /* Hovered commit reference */
1338 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
2e8488b4 1339
8855ada4
JF
1340 int height, width; /* The width and height of the main window */
1341 WINDOW *win; /* The main window */
1342 WINDOW *title; /* The title window living below the main window */
b801d8b2
JF
1343
1344 /* Navigation */
1345 unsigned long offset; /* Offset of the window top */
1346 unsigned long lineno; /* Current line number */
1347
4af34daa
JF
1348 /* Searching */
1349 char grep[SIZEOF_STR]; /* Search string */
b77b2cb8 1350 regex_t *regex; /* Pre-compiled regex */
4af34daa 1351
f6da0b66
JF
1352 /* If non-NULL, points to the view that opened this view. If this view
1353 * is closed tig will switch back to the parent view. */
1354 struct view *parent;
1355
b801d8b2
JF
1356 /* Buffering */
1357 unsigned long lines; /* Total number of lines */
fe7233c3 1358 struct line *line; /* Line index */
e2c01617 1359 unsigned long line_size;/* Total number of allocated lines */
8855ada4 1360 unsigned int digits; /* Number of digits in the lines member. */
b801d8b2
JF
1361
1362 /* Loading */
1363 FILE *pipe;
2e8488b4 1364 time_t start_time;
b801d8b2
JF
1365};
1366
fe7233c3
JF
1367struct view_ops {
1368 /* What type of content being displayed. Used in the title bar. */
1369 const char *type;
f098944b
JF
1370 /* Open and reads in all view content. */
1371 bool (*open)(struct view *view);
fe7233c3 1372 /* Read one line; updates view->line. */
701e4f5d 1373 bool (*read)(struct view *view, char *data);
f098944b
JF
1374 /* Draw one line; @lineno must be < view->height. */
1375 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
586c423d
JF
1376 /* Depending on view handle a special requests. */
1377 enum request (*request)(struct view *view, enum request request, struct line *line);
4af34daa
JF
1378 /* Search for regex in a line. */
1379 bool (*grep)(struct view *view, struct line *line);
d720de4b
JF
1380 /* Select line */
1381 void (*select)(struct view *view, struct line *line);
fe7233c3
JF
1382};
1383
6b161b31
JF
1384static struct view_ops pager_ops;
1385static struct view_ops main_ops;
e733ee54
JF
1386static struct view_ops tree_ops;
1387static struct view_ops blob_ops;
f098944b 1388static struct view_ops help_ops;
173d76ea 1389static struct view_ops status_ops;
3e634113 1390static struct view_ops stage_ops;
a28bcc22 1391
04e2b7b2
JF
1392#define VIEW_STR(name, cmd, env, ref, ops, map) \
1393 { name, cmd, #env, ref, ops, map}
1ba2ae4b 1394
95d7ddcd 1395#define VIEW_(id, name, ops, ref) \
04e2b7b2 1396 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1ba2ae4b 1397
c2124ccd 1398
b801d8b2 1399static struct view views[] = {
173d76ea
JF
1400 VIEW_(MAIN, "main", &main_ops, ref_head),
1401 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1402 VIEW_(LOG, "log", &pager_ops, ref_head),
1403 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1404 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
b64c5b75
JF
1405 VIEW_(HELP, "help", &help_ops, ""),
1406 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
173d76ea 1407 VIEW_(STATUS, "status", &status_ops, ""),
3e634113 1408 VIEW_(STAGE, "stage", &stage_ops, ""),
b801d8b2
JF
1409};
1410
a28bcc22
JF
1411#define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1412
699ae55b
JF
1413#define foreach_view(view, i) \
1414 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1415
1416#define view_is_displayed(view) \
1417 (view == display[0] || view == display[1])
4c6fabc2 1418
fe7233c3
JF
1419static bool
1420draw_view_line(struct view *view, unsigned int lineno)
1421{
d720de4b 1422 struct line *line;
5dcf8064 1423 bool selected = (view->offset + lineno == view->lineno);
4887d44e 1424 bool draw_ok;
d720de4b 1425
699ae55b
JF
1426 assert(view_is_displayed(view));
1427
fe7233c3
JF
1428 if (view->offset + lineno >= view->lines)
1429 return FALSE;
1430
d720de4b
JF
1431 line = &view->line[view->offset + lineno];
1432
3c571d67
JF
1433 if (selected) {
1434 line->selected = TRUE;
d720de4b 1435 view->ops->select(view, line);
3c571d67
JF
1436 } else if (line->selected) {
1437 line->selected = FALSE;
1438 wmove(view->win, lineno, 0);
1439 wclrtoeol(view->win);
1440 }
d720de4b 1441
4887d44e
JF
1442 scrollok(view->win, FALSE);
1443 draw_ok = view->ops->draw(view, line, lineno, selected);
1444 scrollok(view->win, TRUE);
1445
1446 return draw_ok;
fe7233c3
JF
1447}
1448
b801d8b2 1449static void
82e78006 1450redraw_view_from(struct view *view, int lineno)
b801d8b2 1451{
82e78006 1452 assert(0 <= lineno && lineno < view->height);
b801d8b2 1453
82e78006 1454 for (; lineno < view->height; lineno++) {
fe7233c3 1455 if (!draw_view_line(view, lineno))
fd85fef1 1456 break;
b801d8b2
JF
1457 }
1458
1459 redrawwin(view->win);
ab4af23e
JF
1460 if (input_mode)
1461 wnoutrefresh(view->win);
1462 else
1463 wrefresh(view->win);
b801d8b2
JF
1464}
1465
b76c2afc 1466static void
82e78006
JF
1467redraw_view(struct view *view)
1468{
1469 wclear(view->win);
1470 redraw_view_from(view, 0);
1471}
1472
c2124ccd 1473
6b161b31 1474static void
81030ec8
JF
1475update_view_title(struct view *view)
1476{
3c112a88 1477 char buf[SIZEOF_STR];
71d1c7db
JF
1478 char state[SIZEOF_STR];
1479 size_t bufpos = 0, statelen = 0;
81030ec8 1480
3c112a88 1481 assert(view_is_displayed(view));
81030ec8 1482
249016a6 1483 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
6d9c07af 1484 unsigned int view_lines = view->offset + view->height;
c19f8017 1485 unsigned int lines = view->lines
6d9c07af 1486 ? MIN(view_lines, view->lines) * 100 / view->lines
c19f8017
JF
1487 : 0;
1488
71d1c7db 1489 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
3c112a88
JF
1490 view->ops->type,
1491 view->lineno + 1,
1492 view->lines,
1493 lines);
81030ec8 1494
5becf244
JF
1495 if (view->pipe) {
1496 time_t secs = time(NULL) - view->start_time;
f97f4012 1497
5becf244
JF
1498 /* Three git seconds are a long time ... */
1499 if (secs > 2)
71d1c7db 1500 string_format_from(state, &statelen, " %lds", secs);
5becf244 1501 }
81030ec8
JF
1502 }
1503
71d1c7db
JF
1504 string_format_from(buf, &bufpos, "[%s]", view->name);
1505 if (*view->ref && bufpos < view->width) {
1506 size_t refsize = strlen(view->ref);
1507 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1508
1509 if (minsize < view->width)
1510 refsize = view->width - minsize + 7;
d1858deb 1511 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
71d1c7db 1512 }
f97f4012 1513
71d1c7db
JF
1514 if (statelen && bufpos < view->width) {
1515 string_format_from(buf, &bufpos, " %s", state);
f97f4012
JF
1516 }
1517
3c112a88
JF
1518 if (view == display[current_view])
1519 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1520 else
1521 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1522
3c112a88 1523 mvwaddnstr(view->title, 0, 0, buf, bufpos);
390a8262 1524 wclrtoeol(view->title);
976447f8 1525 wmove(view->title, 0, view->width - 1);
ab4af23e
JF
1526
1527 if (input_mode)
1528 wnoutrefresh(view->title);
1529 else
1530 wrefresh(view->title);
81030ec8
JF
1531}
1532
1533static void
6b161b31 1534resize_display(void)
b76c2afc 1535{
03a93dbb 1536 int offset, i;
6b161b31
JF
1537 struct view *base = display[0];
1538 struct view *view = display[1] ? display[1] : display[0];
b76c2afc 1539
6b161b31 1540 /* Setup window dimensions */
b76c2afc 1541
03a93dbb 1542 getmaxyx(stdscr, base->height, base->width);
b76c2afc 1543
6b161b31 1544 /* Make room for the status window. */
03a93dbb 1545 base->height -= 1;
6b161b31
JF
1546
1547 if (view != base) {
03a93dbb
JF
1548 /* Horizontal split. */
1549 view->width = base->width;
6b161b31
JF
1550 view->height = SCALE_SPLIT_VIEW(base->height);
1551 base->height -= view->height;
1552
1553 /* Make room for the title bar. */
1554 view->height -= 1;
1555 }
1556
1557 /* Make room for the title bar. */
1558 base->height -= 1;
1559
1560 offset = 0;
1561
33c4f9ea 1562 foreach_displayed_view (view, i) {
b76c2afc 1563 if (!view->win) {
c19f8017 1564 view->win = newwin(view->height, 0, offset, 0);
6b161b31
JF
1565 if (!view->win)
1566 die("Failed to create %s view", view->name);
1567
1568 scrollok(view->win, TRUE);
1569
1570 view->title = newwin(1, 0, offset + view->height, 0);
1571 if (!view->title)
1572 die("Failed to create title window");
1573
1574 } else {
c19f8017 1575 wresize(view->win, view->height, view->width);
6b161b31
JF
1576 mvwin(view->win, offset, 0);
1577 mvwin(view->title, offset + view->height, 0);
a28bcc22 1578 }
a28bcc22 1579
6b161b31 1580 offset += view->height + 1;
b76c2afc 1581 }
6b161b31 1582}
b76c2afc 1583
6b161b31 1584static void
20bb5e18
JF
1585redraw_display(void)
1586{
1587 struct view *view;
1588 int i;
1589
33c4f9ea 1590 foreach_displayed_view (view, i) {
20bb5e18
JF
1591 redraw_view(view);
1592 update_view_title(view);
1593 }
1594}
1595
85af6284 1596static void
2bee3bde 1597update_display_cursor(struct view *view)
85af6284 1598{
85af6284
JF
1599 /* Move the cursor to the right-most column of the cursor line.
1600 *
1601 * XXX: This could turn out to be a bit expensive, but it ensures that
1602 * the cursor does not jump around. */
1603 if (view->lines) {
1604 wmove(view->win, view->lineno - view->offset, view->width - 1);
1605 wrefresh(view->win);
1606 }
1607}
20bb5e18 1608
2e8488b4
JF
1609/*
1610 * Navigation
1611 */
1612
4a2909a7 1613/* Scrolling backend */
b801d8b2 1614static void
8c317212 1615do_scroll_view(struct view *view, int lines)
b801d8b2 1616{
a0087dd5
JF
1617 bool redraw_current_line = FALSE;
1618
fd85fef1
JF
1619 /* The rendering expects the new offset. */
1620 view->offset += lines;
1621
1622 assert(0 <= view->offset && view->offset < view->lines);
1623 assert(lines);
b801d8b2 1624
a0087dd5
JF
1625 /* Move current line into the view. */
1626 if (view->lineno < view->offset) {
1627 view->lineno = view->offset;
1628 redraw_current_line = TRUE;
1629 } else if (view->lineno >= view->offset + view->height) {
1630 view->lineno = view->offset + view->height - 1;
1631 redraw_current_line = TRUE;
1632 }
1633
1634 assert(view->offset <= view->lineno && view->lineno < view->lines);
1635
82e78006 1636 /* Redraw the whole screen if scrolling is pointless. */
4c6fabc2 1637 if (view->height < ABS(lines)) {
b76c2afc
JF
1638 redraw_view(view);
1639
1640 } else {
22f66b0a 1641 int line = lines > 0 ? view->height - lines : 0;
82e78006 1642 int end = line + ABS(lines);
fd85fef1
JF
1643
1644 wscrl(view->win, lines);
1645
22f66b0a 1646 for (; line < end; line++) {
fe7233c3 1647 if (!draw_view_line(view, line))
fd85fef1
JF
1648 break;
1649 }
fd85fef1 1650
a0087dd5
JF
1651 if (redraw_current_line)
1652 draw_view_line(view, view->lineno - view->offset);
fd85fef1
JF
1653 }
1654
fd85fef1
JF
1655 redrawwin(view->win);
1656 wrefresh(view->win);
9d3f5834 1657 report("");
fd85fef1 1658}
78c70acd 1659
4a2909a7 1660/* Scroll frontend */
fd85fef1 1661static void
6b161b31 1662scroll_view(struct view *view, enum request request)
fd85fef1
JF
1663{
1664 int lines = 1;
b801d8b2 1665
8c317212
JF
1666 assert(view_is_displayed(view));
1667
b801d8b2 1668 switch (request) {
4a2909a7 1669 case REQ_SCROLL_PAGE_DOWN:
fd85fef1 1670 lines = view->height;
4a2909a7 1671 case REQ_SCROLL_LINE_DOWN:
b801d8b2 1672 if (view->offset + lines > view->lines)
bde3653a 1673 lines = view->lines - view->offset;
b801d8b2 1674
fd85fef1 1675 if (lines == 0 || view->offset + view->height >= view->lines) {
eb98559e 1676 report("Cannot scroll beyond the last line");
b801d8b2
JF
1677 return;
1678 }
1679 break;
1680
4a2909a7 1681 case REQ_SCROLL_PAGE_UP:
fd85fef1 1682 lines = view->height;
4a2909a7 1683 case REQ_SCROLL_LINE_UP:
b801d8b2
JF
1684 if (lines > view->offset)
1685 lines = view->offset;
1686
1687 if (lines == 0) {
eb98559e 1688 report("Cannot scroll beyond the first line");
b801d8b2
JF
1689 return;
1690 }
1691
fd85fef1 1692 lines = -lines;
b801d8b2 1693 break;
03a93dbb 1694
6b161b31
JF
1695 default:
1696 die("request %d not handled in switch", request);
b801d8b2
JF
1697 }
1698
8c317212 1699 do_scroll_view(view, lines);
fd85fef1 1700}
b801d8b2 1701
4a2909a7 1702/* Cursor moving */
fd85fef1 1703static void
8522ecc7 1704move_view(struct view *view, enum request request)
fd85fef1 1705{
dfaa6c81 1706 int scroll_steps = 0;
fd85fef1 1707 int steps;
b801d8b2 1708
fd85fef1 1709 switch (request) {
4a2909a7 1710 case REQ_MOVE_FIRST_LINE:
78c70acd
JF
1711 steps = -view->lineno;
1712 break;
1713
4a2909a7 1714 case REQ_MOVE_LAST_LINE:
78c70acd
JF
1715 steps = view->lines - view->lineno - 1;
1716 break;
1717
4a2909a7 1718 case REQ_MOVE_PAGE_UP:
78c70acd
JF
1719 steps = view->height > view->lineno
1720 ? -view->lineno : -view->height;
1721 break;
1722
4a2909a7 1723 case REQ_MOVE_PAGE_DOWN:
78c70acd
JF
1724 steps = view->lineno + view->height >= view->lines
1725 ? view->lines - view->lineno - 1 : view->height;
1726 break;
1727
4a2909a7 1728 case REQ_MOVE_UP:
fd85fef1
JF
1729 steps = -1;
1730 break;
b801d8b2 1731
4a2909a7 1732 case REQ_MOVE_DOWN:
fd85fef1
JF
1733 steps = 1;
1734 break;
6b161b31
JF
1735
1736 default:
1737 die("request %d not handled in switch", request);
78c70acd 1738 }
b801d8b2 1739
4c6fabc2 1740 if (steps <= 0 && view->lineno == 0) {
eb98559e 1741 report("Cannot move beyond the first line");
78c70acd 1742 return;
b801d8b2 1743
6908bdbd 1744 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
eb98559e 1745 report("Cannot move beyond the last line");
78c70acd 1746 return;
fd85fef1
JF
1747 }
1748
4c6fabc2 1749 /* Move the current line */
fd85fef1 1750 view->lineno += steps;
4c6fabc2
JF
1751 assert(0 <= view->lineno && view->lineno < view->lines);
1752
4c6fabc2 1753 /* Check whether the view needs to be scrolled */
fd85fef1
JF
1754 if (view->lineno < view->offset ||
1755 view->lineno >= view->offset + view->height) {
dfaa6c81 1756 scroll_steps = steps;
fd85fef1 1757 if (steps < 0 && -steps > view->offset) {
dfaa6c81 1758 scroll_steps = -view->offset;
b76c2afc
JF
1759
1760 } else if (steps > 0) {
1761 if (view->lineno == view->lines - 1 &&
1762 view->lines > view->height) {
dfaa6c81
JF
1763 scroll_steps = view->lines - view->offset - 1;
1764 if (scroll_steps >= view->height)
1765 scroll_steps -= view->height - 1;
b76c2afc 1766 }
b801d8b2 1767 }
8522ecc7
JF
1768 }
1769
1770 if (!view_is_displayed(view)) {
a3965365
JF
1771 view->offset += scroll_steps;
1772 assert(0 <= view->offset && view->offset < view->lines);
8522ecc7
JF
1773 view->ops->select(view, &view->line[view->lineno]);
1774 return;
1775 }
1776
1777 /* Repaint the old "current" line if we be scrolling */
1778 if (ABS(steps) < view->height)
1779 draw_view_line(view, view->lineno - steps - view->offset);
1780
dfaa6c81
JF
1781 if (scroll_steps) {
1782 do_scroll_view(view, scroll_steps);
fd85fef1 1783 return;
b801d8b2
JF
1784 }
1785
4c6fabc2 1786 /* Draw the current line */
fe7233c3 1787 draw_view_line(view, view->lineno - view->offset);
fd85fef1 1788
b801d8b2
JF
1789 redrawwin(view->win);
1790 wrefresh(view->win);
9d3f5834 1791 report("");
b801d8b2
JF
1792}
1793
b801d8b2 1794
2e8488b4 1795/*
4af34daa
JF
1796 * Searching
1797 */
1798
c02d8fce 1799static void search_view(struct view *view, enum request request);
4af34daa
JF
1800
1801static bool
1802find_next_line(struct view *view, unsigned long lineno, struct line *line)
1803{
699ae55b
JF
1804 assert(view_is_displayed(view));
1805
4af34daa
JF
1806 if (!view->ops->grep(view, line))
1807 return FALSE;
1808
1809 if (lineno - view->offset >= view->height) {
1810 view->offset = lineno;
1811 view->lineno = lineno;
1812 redraw_view(view);
1813
1814 } else {
1815 unsigned long old_lineno = view->lineno - view->offset;
1816
1817 view->lineno = lineno;
4af34daa
JF
1818 draw_view_line(view, old_lineno);
1819
1820 draw_view_line(view, view->lineno - view->offset);
1821 redrawwin(view->win);
1822 wrefresh(view->win);
1823 }
1824
1825 report("Line %ld matches '%s'", lineno + 1, view->grep);
1826 return TRUE;
1827}
1828
1829static void
1830find_next(struct view *view, enum request request)
1831{
1832 unsigned long lineno = view->lineno;
1833 int direction;
1834
1835 if (!*view->grep) {
1836 if (!*opt_search)
1837 report("No previous search");
1838 else
c02d8fce 1839 search_view(view, request);
4af34daa
JF
1840 return;
1841 }
1842
1843 switch (request) {
1844 case REQ_SEARCH:
1845 case REQ_FIND_NEXT:
1846 direction = 1;
1847 break;
1848
1849 case REQ_SEARCH_BACK:
1850 case REQ_FIND_PREV:
1851 direction = -1;
1852 break;
1853
1854 default:
1855 return;
1856 }
1857
1858 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1859 lineno += direction;
1860
1861 /* Note, lineno is unsigned long so will wrap around in which case it
1862 * will become bigger than view->lines. */
1863 for (; lineno < view->lines; lineno += direction) {
1864 struct line *line = &view->line[lineno];
1865
1866 if (find_next_line(view, lineno, line))
1867 return;
1868 }
1869
1870 report("No match found for '%s'", view->grep);
1871}
1872
1873static void
c02d8fce 1874search_view(struct view *view, enum request request)
4af34daa
JF
1875{
1876 int regex_err;
1877
b77b2cb8
JF
1878 if (view->regex) {
1879 regfree(view->regex);
4af34daa 1880 *view->grep = 0;
b77b2cb8
JF
1881 } else {
1882 view->regex = calloc(1, sizeof(*view->regex));
1883 if (!view->regex)
1884 return;
4af34daa
JF
1885 }
1886
c02d8fce 1887 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
4af34daa
JF
1888 if (regex_err != 0) {
1889 char buf[SIZEOF_STR] = "unknown error";
1890
b77b2cb8 1891 regerror(regex_err, view->regex, buf, sizeof(buf));
e9cacd58 1892 report("Search failed: %s", buf);
4af34daa
JF
1893 return;
1894 }
1895
c02d8fce 1896 string_copy(view->grep, opt_search);
4af34daa
JF
1897
1898 find_next(view, request);
1899}
1900
1901/*
2e8488b4
JF
1902 * Incremental updating
1903 */
b801d8b2 1904
199d1288
JF
1905static void
1906end_update(struct view *view)
1907{
1908 if (!view->pipe)
1909 return;
1910 set_nonblocking_input(FALSE);
1911 if (view->pipe == stdin)
1912 fclose(view->pipe);
1913 else
1914 pclose(view->pipe);
1915 view->pipe = NULL;
1916}
1917
03a93dbb 1918static bool
b801d8b2
JF
1919begin_update(struct view *view)
1920{
199d1288
JF
1921 if (view->pipe)
1922 end_update(view);
1923
03a93dbb
JF
1924 if (opt_cmd[0]) {
1925 string_copy(view->cmd, opt_cmd);
1926 opt_cmd[0] = 0;
035ba11f
JF
1927 /* When running random commands, initially show the
1928 * command in the title. However, it maybe later be
1929 * overwritten if a commit line is selected. */
809a9f48
JF
1930 if (view == VIEW(REQ_VIEW_PAGER))
1931 string_copy(view->ref, view->cmd);
1932 else
1933 view->ref[0] = 0;
e733ee54
JF
1934
1935 } else if (view == VIEW(REQ_VIEW_TREE)) {
1936 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
f0f114ac 1937 char path[SIZEOF_STR];
e733ee54
JF
1938
1939 if (strcmp(view->vid, view->id))
f0f114ac
JF
1940 opt_path[0] = path[0] = 0;
1941 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1942 return FALSE;
e733ee54 1943
739e81de 1944 if (!string_format(view->cmd, format, view->id, path))
e733ee54
JF
1945 return FALSE;
1946
03a93dbb 1947 } else {
4685845e 1948 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
739e81de 1949 const char *id = view->id;
1ba2ae4b 1950
cc2d1364 1951 if (!string_format(view->cmd, format, id, id, id, id, id))
03a93dbb 1952 return FALSE;
035ba11f
JF
1953
1954 /* Put the current ref_* value to the view title ref
1955 * member. This is needed by the blob view. Most other
1956 * views sets it automatically after loading because the
1957 * first line is a commit line. */
739e81de 1958 string_copy_rev(view->ref, view->id);
03a93dbb 1959 }
b801d8b2 1960
6908bdbd
JF
1961 /* Special case for the pager view. */
1962 if (opt_pipe) {
1963 view->pipe = opt_pipe;
1964 opt_pipe = NULL;
1965 } else {
1966 view->pipe = popen(view->cmd, "r");
1967 }
1968
2e8488b4
JF
1969 if (!view->pipe)
1970 return FALSE;
b801d8b2 1971
6b161b31 1972 set_nonblocking_input(TRUE);
b801d8b2
JF
1973
1974 view->offset = 0;
1975 view->lines = 0;
1976 view->lineno = 0;
739e81de 1977 string_copy_rev(view->vid, view->id);
b801d8b2 1978
2e8488b4
JF
1979 if (view->line) {
1980 int i;
1981
1982 for (i = 0; i < view->lines; i++)
fe7233c3
JF
1983 if (view->line[i].data)
1984 free(view->line[i].data);
2e8488b4
JF
1985
1986 free(view->line);
1987 view->line = NULL;
1988 }
1989
1990 view->start_time = time(NULL);
1991
b801d8b2
JF
1992 return TRUE;
1993}
1994
e2c01617
JF
1995static struct line *
1996realloc_lines(struct view *view, size_t line_size)
1997{
1998 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1999
2000 if (!tmp)
2001 return NULL;
2002
2003 view->line = tmp;
2004 view->line_size = line_size;
2005 return view->line;
2006}
2007
03a93dbb 2008static bool
b801d8b2
JF
2009update_view(struct view *view)
2010{
6b68fd24
JF
2011 char in_buffer[BUFSIZ];
2012 char out_buffer[BUFSIZ * 2];
b801d8b2 2013 char *line;
82e78006
JF
2014 /* The number of lines to read. If too low it will cause too much
2015 * redrawing (and possible flickering), if too high responsiveness
2016 * will suffer. */
8855ada4 2017 unsigned long lines = view->height;
82e78006 2018 int redraw_from = -1;
b801d8b2
JF
2019
2020 if (!view->pipe)
2021 return TRUE;
2022
82e78006
JF
2023 /* Only redraw if lines are visible. */
2024 if (view->offset + view->height >= view->lines)
2025 redraw_from = view->lines - view->offset;
b801d8b2 2026
699ae55b 2027 /* FIXME: This is probably not perfect for backgrounded views. */
e2c01617 2028 if (!realloc_lines(view, view->lines + lines))
b801d8b2
JF
2029 goto alloc_error;
2030
6b68fd24
JF
2031 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2032 size_t linelen = strlen(line);
b801d8b2 2033
b801d8b2
JF
2034 if (linelen)
2035 line[linelen - 1] = 0;
2036
6b68fd24 2037 if (opt_iconv != ICONV_NONE) {
e47afdf2 2038 ICONV_CONST char *inbuf = line;
6b68fd24
JF
2039 size_t inlen = linelen;
2040
2041 char *outbuf = out_buffer;
2042 size_t outlen = sizeof(out_buffer);
2043
2044 size_t ret;
2045
7361622d 2046 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
6b68fd24
JF
2047 if (ret != (size_t) -1) {
2048 line = out_buffer;
2049 linelen = strlen(out_buffer);
2050 }
2051 }
2052
701e4f5d 2053 if (!view->ops->read(view, line))
b801d8b2 2054 goto alloc_error;
fd85fef1
JF
2055
2056 if (lines-- == 1)
2057 break;
b801d8b2
JF
2058 }
2059
8855ada4
JF
2060 {
2061 int digits;
2062
2063 lines = view->lines;
2064 for (digits = 0; lines; digits++)
2065 lines /= 10;
2066
2067 /* Keep the displayed view in sync with line number scaling. */
2068 if (digits != view->digits) {
2069 view->digits = digits;
2070 redraw_from = 0;
2071 }
2072 }
2073
699ae55b
JF
2074 if (!view_is_displayed(view))
2075 goto check_pipe;
2076
e733ee54
JF
2077 if (view == VIEW(REQ_VIEW_TREE)) {
2078 /* Clear the view and redraw everything since the tree sorting
2079 * might have rearranged things. */
2080 redraw_view(view);
2081
2082 } else if (redraw_from >= 0) {
82e78006 2083 /* If this is an incremental update, redraw the previous line
a28bcc22
JF
2084 * since for commits some members could have changed when
2085 * loading the main view. */
82e78006
JF
2086 if (redraw_from > 0)
2087 redraw_from--;
2088
9eded379
JF
2089 /* Since revision graph visualization requires knowledge
2090 * about the parent commit, it causes a further one-off
2091 * needed to be redrawn for incremental updates. */
2092 if (redraw_from > 0 && opt_rev_graph)
2093 redraw_from--;
2094
82e78006
JF
2095 /* Incrementally draw avoids flickering. */
2096 redraw_view_from(view, redraw_from);
4c6fabc2 2097 }
b801d8b2 2098
eb98559e
JF
2099 /* Update the title _after_ the redraw so that if the redraw picks up a
2100 * commit reference in view->ref it'll be available here. */
2101 update_view_title(view);
2102
699ae55b 2103check_pipe:
b801d8b2 2104 if (ferror(view->pipe)) {
03a93dbb 2105 report("Failed to read: %s", strerror(errno));
b801d8b2
JF
2106 goto end;
2107
2108 } else if (feof(view->pipe)) {
f97f4012 2109 report("");
b801d8b2
JF
2110 goto end;
2111 }
2112
2113 return TRUE;
2114
2115alloc_error:
2e8488b4 2116 report("Allocation failure");
b801d8b2
JF
2117
2118end:
be04d936 2119 view->ops->read(view, NULL);
b801d8b2
JF
2120 end_update(view);
2121 return FALSE;
2122}
2123
0a0d8910 2124static struct line *
e314c36d 2125add_line_data(struct view *view, void *data, enum line_type type)
0a0d8910 2126{
e314c36d 2127 struct line *line = &view->line[view->lines++];
0a0d8910 2128
e314c36d 2129 memset(line, 0, sizeof(*line));
0a0d8910 2130 line->type = type;
e314c36d 2131 line->data = data;
0a0d8910
JF
2132
2133 return line;
2134}
2135
e314c36d
JF
2136static struct line *
2137add_line_text(struct view *view, char *data, enum line_type type)
2138{
2139 if (data)
2140 data = strdup(data);
2141
2142 return data ? add_line_data(view, data, type) : NULL;
2143}
2144
79d445ca 2145
e10154d5
JF
2146/*
2147 * View opening
2148 */
2149
49f2b43f
JF
2150enum open_flags {
2151 OPEN_DEFAULT = 0, /* Use default view switching. */
2152 OPEN_SPLIT = 1, /* Split current view. */
2153 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2154 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2155};
2156
6b161b31 2157static void
49f2b43f 2158open_view(struct view *prev, enum request request, enum open_flags flags)
b801d8b2 2159{
49f2b43f
JF
2160 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2161 bool split = !!(flags & OPEN_SPLIT);
2162 bool reload = !!(flags & OPEN_RELOAD);
a28bcc22 2163 struct view *view = VIEW(request);
9f41488f 2164 int nviews = displayed_views();
6e950a52 2165 struct view *base_view = display[0];
b801d8b2 2166
49f2b43f 2167 if (view == prev && nviews == 1 && !reload) {
6b161b31
JF
2168 report("Already in %s view", view->name);
2169 return;
2170 }
b801d8b2 2171
f098944b
JF
2172 if (view->ops->open) {
2173 if (!view->ops->open(view)) {
2174 report("Failed to load %s view", view->name);
2175 return;
2176 }
2509b112
JF
2177
2178 } else if ((reload || strcmp(view->vid, view->id)) &&
2179 !begin_update(view)) {
6b161b31
JF
2180 report("Failed to load %s view", view->name);
2181 return;
2182 }
a28bcc22 2183
6b161b31 2184 if (split) {
8d741c06 2185 display[1] = view;
6b161b31 2186 if (!backgrounded)
8d741c06 2187 current_view = 1;
6b161b31
JF
2188 } else {
2189 /* Maximize the current view. */
2190 memset(display, 0, sizeof(display));
2191 current_view = 0;
2192 display[current_view] = view;
a28bcc22 2193 }
b801d8b2 2194
6e950a52
JF
2195 /* Resize the view when switching between split- and full-screen,
2196 * or when switching between two different full-screen views. */
2197 if (nviews != displayed_views() ||
2198 (nviews == 1 && base_view != display[0]))
a006db63 2199 resize_display();
b801d8b2 2200
a8891802 2201 if (split && prev->lineno - prev->offset >= prev->height) {
03a93dbb 2202 /* Take the title line into account. */
eb98559e 2203 int lines = prev->lineno - prev->offset - prev->height + 1;
03a93dbb
JF
2204
2205 /* Scroll the view that was split if the current line is
2206 * outside the new limited view. */
8c317212 2207 do_scroll_view(prev, lines);
03a93dbb
JF
2208 }
2209
6b161b31 2210 if (prev && view != prev) {
9b995f0c 2211 if (split && !backgrounded) {
f0b3ab80
JF
2212 /* "Blur" the previous view. */
2213 update_view_title(prev);
9f396969 2214 }
f0b3ab80 2215
f6da0b66 2216 view->parent = prev;
b801d8b2
JF
2217 }
2218
9f396969 2219 if (view->pipe && view->lines == 0) {
03a93dbb
JF
2220 /* Clear the old view and let the incremental updating refill
2221 * the screen. */
2222 wclear(view->win);
f97f4012 2223 report("");
03a93dbb
JF
2224 } else {
2225 redraw_view(view);
24b5b3e0 2226 report("");
03a93dbb 2227 }
6706b2ba
JF
2228
2229 /* If the view is backgrounded the above calls to report()
2230 * won't redraw the view title. */
2231 if (backgrounded)
2232 update_view_title(view);
b801d8b2
JF
2233}
2234
0cea0d43 2235static void
d24ef76c
JF
2236open_external_viewer(const char *cmd)
2237{
2238 def_prog_mode(); /* save current tty modes */
2239 endwin(); /* restore original tty modes */
2240 system(cmd);
2241 fprintf(stderr, "Press Enter to continue");
2242 getc(stdin);
2243 reset_prog_mode();
2244 redraw_display();
2245}
2246
2247static void
b5c18d9d
JF
2248open_mergetool(const char *file)
2249{
2250 char cmd[SIZEOF_STR];
2251 char file_sq[SIZEOF_STR];
2252
2253 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2254 string_format(cmd, "git mergetool %s", file_sq)) {
2255 open_external_viewer(cmd);
2256 }
2257}
2258
2259static void
d24ef76c 2260open_editor(bool from_root, const char *file)
0cea0d43
JF
2261{
2262 char cmd[SIZEOF_STR];
2263 char file_sq[SIZEOF_STR];
2264 char *editor;
7d31b059 2265 char *prefix = from_root ? opt_cdup : "";
0cea0d43
JF
2266
2267 editor = getenv("GIT_EDITOR");
2268 if (!editor && *opt_editor)
2269 editor = opt_editor;
2270 if (!editor)
2271 editor = getenv("VISUAL");
2272 if (!editor)
2273 editor = getenv("EDITOR");
2274 if (!editor)
2275 editor = "vi";
2276
2277 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
7d31b059 2278 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
d24ef76c 2279 open_external_viewer(cmd);
0cea0d43
JF
2280 }
2281}
b801d8b2 2282
9eb14b72
JF
2283static void
2284open_run_request(enum request request)
2285{
2286 struct run_request *req = get_run_request(request);
2287 char buf[SIZEOF_STR * 2];
2288 size_t bufpos;
2289 char *cmd;
2290
2291 if (!req) {
2292 report("Unknown run request");
2293 return;
2294 }
2295
2296 bufpos = 0;
2297 cmd = req->cmd;
2298
2299 while (cmd) {
2300 char *next = strstr(cmd, "%(");
2301 int len = next - cmd;
2302 char *value;
2303
2304 if (!next) {
2305 len = strlen(cmd);
2306 value = "";
2307
2308 } else if (!strncmp(next, "%(head)", 7)) {
2309 value = ref_head;
2310
2311 } else if (!strncmp(next, "%(commit)", 9)) {
2312 value = ref_commit;
2313
2314 } else if (!strncmp(next, "%(blob)", 7)) {
2315 value = ref_blob;
2316
2317 } else {
2318 report("Unknown replacement in run request: `%s`", req->cmd);
2319 return;
2320 }
2321
2322 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2323 return;
2324
2325 if (next)
2326 next = strchr(next, ')') + 1;
2327 cmd = next;
2328 }
2329
2330 open_external_viewer(buf);
2331}
2332
6b161b31
JF
2333/*
2334 * User request switch noodle
2335 */
2336
b801d8b2 2337static int
6b161b31 2338view_driver(struct view *view, enum request request)
b801d8b2 2339{
b801d8b2
JF
2340 int i;
2341
1bace428
JF
2342 if (request == REQ_NONE) {
2343 doupdate();
2344 return TRUE;
2345 }
2346
9eb14b72
JF
2347 if (request > REQ_NONE) {
2348 open_run_request(request);
2349 return TRUE;
2350 }
2351
586c423d
JF
2352 if (view && view->lines) {
2353 request = view->ops->request(view, request, &view->line[view->lineno]);
2354 if (request == REQ_NONE)
2355 return TRUE;
2356 }
2357
b801d8b2 2358 switch (request) {
4a2909a7
JF
2359 case REQ_MOVE_UP:
2360 case REQ_MOVE_DOWN:
2361 case REQ_MOVE_PAGE_UP:
2362 case REQ_MOVE_PAGE_DOWN:
2363 case REQ_MOVE_FIRST_LINE:
2364 case REQ_MOVE_LAST_LINE:
8522ecc7 2365 move_view(view, request);
fd85fef1
JF
2366 break;
2367
4a2909a7
JF
2368 case REQ_SCROLL_LINE_DOWN:
2369 case REQ_SCROLL_LINE_UP:
2370 case REQ_SCROLL_PAGE_DOWN:
2371 case REQ_SCROLL_PAGE_UP:
a28bcc22 2372 scroll_view(view, request);
b801d8b2
JF
2373 break;
2374
e733ee54
JF
2375 case REQ_VIEW_BLOB:
2376 if (!ref_blob[0]) {
550cd4b5
JF
2377 report("No file chosen, press %s to open tree view",
2378 get_key(REQ_VIEW_TREE));
e733ee54
JF
2379 break;
2380 }
5c4358d1
JF
2381 open_view(view, request, OPEN_DEFAULT);
2382 break;
2383
2384 case REQ_VIEW_PAGER:
b64c5b75 2385 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
5c4358d1
JF
2386 report("No pager content, press %s to run command from prompt",
2387 get_key(REQ_PROMPT));
2388 break;
2389 }
2390 open_view(view, request, OPEN_DEFAULT);
2391 break;
2392
3e634113
JF
2393 case REQ_VIEW_STAGE:
2394 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2395 report("No stage content, press %s to open the status view and choose file",
2396 get_key(REQ_VIEW_STATUS));
2397 break;
2398 }
2399 open_view(view, request, OPEN_DEFAULT);
2400 break;
2401
c38c64bb
JF
2402 case REQ_VIEW_STATUS:
2403 if (opt_is_inside_work_tree == FALSE) {
2404 report("The status view requires a working tree");
2405 break;
2406 }
2407 open_view(view, request, OPEN_DEFAULT);
2408 break;
2409
4a2909a7 2410 case REQ_VIEW_MAIN:
4a2909a7 2411 case REQ_VIEW_DIFF:
2e8488b4 2412 case REQ_VIEW_LOG:
e733ee54 2413 case REQ_VIEW_TREE:
2e8488b4 2414 case REQ_VIEW_HELP:
49f2b43f 2415 open_view(view, request, OPEN_DEFAULT);
b801d8b2
JF
2416 break;
2417
b3a54cba
JF
2418 case REQ_NEXT:
2419 case REQ_PREVIOUS:
2420 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2421
e733ee54
JF
2422 if ((view == VIEW(REQ_VIEW_DIFF) &&
2423 view->parent == VIEW(REQ_VIEW_MAIN)) ||
3e634113 2424 (view == VIEW(REQ_VIEW_STAGE) &&
b9b5b4cd 2425 view->parent == VIEW(REQ_VIEW_STATUS)) ||
e733ee54
JF
2426 (view == VIEW(REQ_VIEW_BLOB) &&
2427 view->parent == VIEW(REQ_VIEW_TREE))) {
03400136
WF
2428 int line;
2429
b3a54cba 2430 view = view->parent;
03400136 2431 line = view->lineno;
8522ecc7
JF
2432 move_view(view, request);
2433 if (view_is_displayed(view))
f0b3ab80 2434 update_view_title(view);
328d27f7
JF
2435 if (line != view->lineno)
2436 view->ops->request(view, REQ_ENTER,
2437 &view->line[view->lineno]);
2438
b3a54cba 2439 } else {
8522ecc7 2440 move_view(view, request);
b3a54cba 2441 }
328d27f7 2442 break;
6b161b31 2443
03a93dbb
JF
2444 case REQ_VIEW_NEXT:
2445 {
9f41488f 2446 int nviews = displayed_views();
03a93dbb
JF
2447 int next_view = (current_view + 1) % nviews;
2448
2449 if (next_view == current_view) {
2450 report("Only one view is displayed");
2451 break;
2452 }
2453
2454 current_view = next_view;
2455 /* Blur out the title of the previous view. */
2456 update_view_title(view);
6734f6b9 2457 report("");
03a93dbb
JF
2458 break;
2459 }
acaef3b3
JF
2460 case REQ_REFRESH:
2461 report("Refreshing is not yet supported for the %s view", view->name);
2462 break;
2463
24b5b3e0 2464 case REQ_TOGGLE_LINENO:
b76c2afc 2465 opt_line_number = !opt_line_number;
20bb5e18 2466 redraw_display();
b801d8b2
JF
2467 break;
2468
54efb62b
JF
2469 case REQ_TOGGLE_REV_GRAPH:
2470 opt_rev_graph = !opt_rev_graph;
2471 redraw_display();
2472 break;
2473
03a93dbb 2474 case REQ_PROMPT:
8855ada4 2475 /* Always reload^Wrerun commands from the prompt. */
49f2b43f 2476 open_view(view, opt_request, OPEN_RELOAD);
03a93dbb
JF
2477 break;
2478
4af34daa
JF
2479 case REQ_SEARCH:
2480 case REQ_SEARCH_BACK:
c02d8fce 2481 search_view(view, request);
4af34daa
JF
2482 break;
2483
2484 case REQ_FIND_NEXT:
2485 case REQ_FIND_PREV:
2486 find_next(view, request);
2487 break;
2488
4a2909a7 2489 case REQ_STOP_LOADING:
59a45d3a
JF
2490 for (i = 0; i < ARRAY_SIZE(views); i++) {
2491 view = &views[i];
2e8488b4 2492 if (view->pipe)
6a7bb912 2493 report("Stopped loading the %s view", view->name),
03a93dbb
JF
2494 end_update(view);
2495 }
b801d8b2
JF
2496 break;
2497
4a2909a7 2498 case REQ_SHOW_VERSION:
ec31d0d0 2499 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
b801d8b2
JF
2500 return TRUE;
2501
fac7db6c
JF
2502 case REQ_SCREEN_RESIZE:
2503 resize_display();
2504 /* Fall-through */
4a2909a7 2505 case REQ_SCREEN_REDRAW:
20bb5e18 2506 redraw_display();
4a2909a7
JF
2507 break;
2508
0cea0d43
JF
2509 case REQ_EDIT:
2510 report("Nothing to edit");
531c6c69
JF
2511 break;
2512
226da94b 2513
531c6c69
JF
2514 case REQ_ENTER:
2515 report("Nothing to enter");
2516 break;
ca1d71ea 2517
b801d8b2 2518
4f9b667a 2519 case REQ_VIEW_CLOSE:
2fcf5401
JF
2520 /* XXX: Mark closed views by letting view->parent point to the
2521 * view itself. Parents to closed view should never be
2522 * followed. */
2523 if (view->parent &&
2524 view->parent->parent != view->parent) {
4f9b667a
JF
2525 memset(display, 0, sizeof(display));
2526 current_view = 0;
f6da0b66 2527 display[current_view] = view->parent;
2fcf5401 2528 view->parent = view;
4f9b667a
JF
2529 resize_display();
2530 redraw_display();
2531 break;
2532 }
2533 /* Fall-through */
b801d8b2
JF
2534 case REQ_QUIT:
2535 return FALSE;
2536
2537 default:
2e8488b4 2538 /* An unknown key will show most commonly used commands. */
468876c9 2539 report("Unknown key, press 'h' for help");
b801d8b2
JF
2540 return TRUE;
2541 }
2542
2543 return TRUE;
2544}
2545
2546
2547/*
ff26aa29 2548 * Pager backend
b801d8b2
JF
2549 */
2550
6b161b31 2551static bool
5dcf8064 2552pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
b801d8b2 2553{
fe7233c3
JF
2554 char *text = line->data;
2555 enum line_type type = line->type;
2556 int textlen = strlen(text);
78c70acd 2557 int attr;
b801d8b2 2558
6706b2ba
JF
2559 wmove(view->win, lineno, 0);
2560
5dcf8064 2561 if (selected) {
78c70acd 2562 type = LINE_CURSOR;
6706b2ba 2563 wchgat(view->win, -1, 0, type, NULL);
fd85fef1
JF
2564 }
2565
78c70acd 2566 attr = get_line_attr(type);
b801d8b2 2567 wattrset(view->win, attr);
b76c2afc 2568
6706b2ba
JF
2569 if (opt_line_number || opt_tab_size < TABSIZE) {
2570 static char spaces[] = " ";
2571 int col_offset = 0, col = 0;
2572
2573 if (opt_line_number) {
2574 unsigned long real_lineno = view->offset + lineno + 1;
82e78006 2575
6706b2ba
JF
2576 if (real_lineno == 1 ||
2577 (real_lineno % opt_num_interval) == 0) {
2578 wprintw(view->win, "%.*d", view->digits, real_lineno);
8855ada4 2579
6706b2ba
JF
2580 } else {
2581 waddnstr(view->win, spaces,
2582 MIN(view->digits, STRING_SIZE(spaces)));
2583 }
2584 waddstr(view->win, ": ");
2585 col_offset = view->digits + 2;
2586 }
8855ada4 2587
fe7233c3 2588 while (text && col_offset + col < view->width) {
6706b2ba 2589 int cols_max = view->width - col_offset - col;
fe7233c3 2590 char *pos = text;
6706b2ba 2591 int cols;
4c6fabc2 2592
fe7233c3
JF
2593 if (*text == '\t') {
2594 text++;
6706b2ba 2595 assert(sizeof(spaces) > TABSIZE);
fe7233c3 2596 pos = spaces;
6706b2ba 2597 cols = opt_tab_size - (col % opt_tab_size);
82e78006 2598
b76c2afc 2599 } else {
fe7233c3
JF
2600 text = strchr(text, '\t');
2601 cols = line ? text - pos : strlen(pos);
b76c2afc 2602 }
6706b2ba 2603
fe7233c3 2604 waddnstr(view->win, pos, MIN(cols, cols_max));
6706b2ba 2605 col += cols;
b76c2afc 2606 }
b76c2afc
JF
2607
2608 } else {
6706b2ba 2609 int col = 0, pos = 0;
b801d8b2 2610
fe7233c3
JF
2611 for (; pos < textlen && col < view->width; pos++, col++)
2612 if (text[pos] == '\t')
6706b2ba
JF
2613 col += TABSIZE - (col % TABSIZE) - 1;
2614
fe7233c3 2615 waddnstr(view->win, text, pos);
6706b2ba 2616 }
2e8488b4 2617
b801d8b2
JF
2618 return TRUE;
2619}
2620
dc23c0e3 2621static bool
d65ced0d 2622add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
dc23c0e3 2623{
17482b11 2624 char refbuf[SIZEOF_STR];
dc23c0e3
JF
2625 char *ref = NULL;
2626 FILE *pipe;
2627
d3c345f7 2628 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
dc23c0e3
JF
2629 return TRUE;
2630
2631 pipe = popen(refbuf, "r");
2632 if (!pipe)
2633 return TRUE;
2634
2635 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2636 ref = chomp_string(ref);
2637 pclose(pipe);
2638
2639 if (!ref || !*ref)
2640 return TRUE;
2641
2642 /* This is the only fatal call, since it can "corrupt" the buffer. */
17482b11 2643 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
dc23c0e3
JF
2644 return FALSE;
2645
2646 return TRUE;
2647}
2648
7b99a34c
JF
2649static void
2650add_pager_refs(struct view *view, struct line *line)
2651{
17482b11 2652 char buf[SIZEOF_STR];
c9ca1ec3 2653 char *commit_id = line->data + STRING_SIZE("commit ");
7b99a34c 2654 struct ref **refs;
d65ced0d 2655 size_t bufpos = 0, refpos = 0;
7b99a34c 2656 const char *sep = "Refs: ";
dc23c0e3 2657 bool is_tag = FALSE;
7b99a34c
JF
2658
2659 assert(line->type == LINE_COMMIT);
2660
c9ca1ec3 2661 refs = get_refs(commit_id);
dc23c0e3
JF
2662 if (!refs) {
2663 if (view == VIEW(REQ_VIEW_DIFF))
2664 goto try_add_describe_ref;
7b99a34c 2665 return;
dc23c0e3 2666 }
7b99a34c
JF
2667
2668 do {
cc2d1364 2669 struct ref *ref = refs[refpos];
e15ec88e
JF
2670 char *fmt = ref->tag ? "%s[%s]" :
2671 ref->remote ? "%s<%s>" : "%s%s";
7b99a34c 2672
cc2d1364
JF
2673 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2674 return;
7b99a34c 2675 sep = ", ";
dc23c0e3
JF
2676 if (ref->tag)
2677 is_tag = TRUE;
7b99a34c
JF
2678 } while (refs[refpos++]->next);
2679
dc23c0e3
JF
2680 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2681try_add_describe_ref:
d42c8a35 2682 /* Add <tag>-g<commit_id> "fake" reference. */
dc23c0e3
JF
2683 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2684 return;
2685 }
2686
d42c8a35
JF
2687 if (bufpos == 0)
2688 return;
2689
cc2d1364 2690 if (!realloc_lines(view, view->line_size + 1))
7b99a34c
JF
2691 return;
2692
0a0d8910 2693 add_line_text(view, buf, LINE_PP_REFS);
7b99a34c
JF
2694}
2695
6b161b31 2696static bool
701e4f5d 2697pager_read(struct view *view, char *data)
22f66b0a 2698{
0a0d8910 2699 struct line *line;
22f66b0a 2700
be04d936
JF
2701 if (!data)
2702 return TRUE;
2703
0a0d8910
JF
2704 line = add_line_text(view, data, get_line_type(data));
2705 if (!line)
7b99a34c 2706 return FALSE;
fe7233c3 2707
7b99a34c
JF
2708 if (line->type == LINE_COMMIT &&
2709 (view == VIEW(REQ_VIEW_DIFF) ||
2710 view == VIEW(REQ_VIEW_LOG)))
2711 add_pager_refs(view, line);
2712
22f66b0a
JF
2713 return TRUE;
2714}
2715
586c423d
JF
2716static enum request
2717pager_request(struct view *view, enum request request, struct line *line)
6b161b31 2718{
91e8e277 2719 int split = 0;
6b161b31 2720
586c423d
JF
2721 if (request != REQ_ENTER)
2722 return request;
2723
9fbbd28f
JF
2724 if (line->type == LINE_COMMIT &&
2725 (view == VIEW(REQ_VIEW_LOG) ||
2726 view == VIEW(REQ_VIEW_PAGER))) {
91e8e277
JF
2727 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2728 split = 1;
67e48ac5
JF
2729 }
2730
91e8e277
JF
2731 /* Always scroll the view even if it was split. That way
2732 * you can use Enter to scroll through the log view and
2733 * split open each commit diff. */
2734 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2735
2736 /* FIXME: A minor workaround. Scrolling the view will call report("")
9d82d824
JF
2737 * but if we are scrolling a non-current view this won't properly
2738 * update the view title. */
91e8e277
JF
2739 if (split)
2740 update_view_title(view);
6b161b31 2741
586c423d 2742 return REQ_NONE;
6b161b31
JF
2743}
2744
4af34daa
JF
2745static bool
2746pager_grep(struct view *view, struct line *line)
2747{
2748 regmatch_t pmatch;
2749 char *text = line->data;
2750
2751 if (!*text)
2752 return FALSE;
2753
b77b2cb8 2754 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
4af34daa
JF
2755 return FALSE;
2756
2757 return TRUE;
2758}
2759
d720de4b
JF
2760static void
2761pager_select(struct view *view, struct line *line)
2762{
2763 if (line->type == LINE_COMMIT) {
035ba11f 2764 char *text = line->data + STRING_SIZE("commit ");
d720de4b 2765
035ba11f 2766 if (view != VIEW(REQ_VIEW_PAGER))
2463b4ea
JF
2767 string_copy_rev(view->ref, text);
2768 string_copy_rev(ref_commit, text);
d720de4b
JF
2769 }
2770}
2771
6b161b31 2772static struct view_ops pager_ops = {
6734f6b9 2773 "line",
f098944b 2774 NULL,
6b161b31 2775 pager_read,
f098944b 2776 pager_draw,
586c423d 2777 pager_request,
f098944b
JF
2778 pager_grep,
2779 pager_select,
2780};
2781
2782
2783/*
2784 * Help backend
2785 */
2786
2787static bool
2788help_open(struct view *view)
2789{
2790 char buf[BUFSIZ];
2791 int lines = ARRAY_SIZE(req_info) + 2;
2792 int i;
2793
2794 if (view->lines > 0)
2795 return TRUE;
2796
2797 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2798 if (!req_info[i].request)
2799 lines++;
2800
9eb14b72
JF
2801 lines += run_requests + 1;
2802
f098944b
JF
2803 view->line = calloc(lines, sizeof(*view->line));
2804 if (!view->line)
2805 return FALSE;
2806
2807 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2808
2809 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2810 char *key;
2811
0e4360b6
JF
2812 if (req_info[i].request == REQ_NONE)
2813 continue;
2814
f098944b
JF
2815 if (!req_info[i].request) {
2816 add_line_text(view, "", LINE_DEFAULT);
2817 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2818 continue;
2819 }
2820
2821 key = get_key(req_info[i].request);
0e4360b6
JF
2822 if (!*key)
2823 key = "(no key defined)";
2824
f098944b
JF
2825 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2826 continue;
2827
2828 add_line_text(view, buf, LINE_DEFAULT);
2829 }
2830
9eb14b72
JF
2831 if (run_requests) {
2832 add_line_text(view, "", LINE_DEFAULT);
2833 add_line_text(view, "External commands:", LINE_DEFAULT);
2834 }
2835
2836 for (i = 0; i < run_requests; i++) {
2837 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2838 char *key;
2839
2840 if (!req)
2841 continue;
2842
2843 key = get_key_name(req->key);
2844 if (!*key)
2845 key = "(no key defined)";
2846
2847 if (!string_format(buf, " %-10s %-14s `%s`",
2848 keymap_table[req->keymap].name,
2849 key, req->cmd))
2850 continue;
2851
2852 add_line_text(view, buf, LINE_DEFAULT);
2853 }
2854
f098944b
JF
2855 return TRUE;
2856}
2857
2858static struct view_ops help_ops = {
2859 "line",
2860 help_open,
2861 NULL,
2862 pager_draw,
586c423d 2863 pager_request,
4af34daa 2864 pager_grep,
d720de4b 2865 pager_select,
6b161b31
JF
2866};
2867
80ce96ea 2868
ff26aa29 2869/*
e733ee54
JF
2870 * Tree backend
2871 */
2872
69efc854
JF
2873struct tree_stack_entry {
2874 struct tree_stack_entry *prev; /* Entry below this in the stack */
2875 unsigned long lineno; /* Line number to restore */
2876 char *name; /* Position of name in opt_path */
2877};
2878
2879/* The top of the path stack. */
2880static struct tree_stack_entry *tree_stack = NULL;
2881unsigned long tree_lineno = 0;
2882
2883static void
2884pop_tree_stack_entry(void)
2885{
2886 struct tree_stack_entry *entry = tree_stack;
2887
2888 tree_lineno = entry->lineno;
2889 entry->name[0] = 0;
2890 tree_stack = entry->prev;
2891 free(entry);
2892}
2893
2894static void
2895push_tree_stack_entry(char *name, unsigned long lineno)
2896{
2897 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
2898 size_t pathlen = strlen(opt_path);
2899
2900 if (!entry)
2901 return;
2902
2903 entry->prev = tree_stack;
2904 entry->name = opt_path + pathlen;
2905 tree_stack = entry;
2906
2907 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
2908 pop_tree_stack_entry();
2909 return;
2910 }
2911
2912 /* Move the current line to the first tree entry. */
2913 tree_lineno = 1;
2914 entry->lineno = lineno;
2915}
2916
4795d620 2917/* Parse output from git-ls-tree(1):
e733ee54
JF
2918 *
2919 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2920 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2921 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2922 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2923 */
2924
2925#define SIZEOF_TREE_ATTR \
2926 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2927
2928#define TREE_UP_FORMAT "040000 tree %s\t.."
2929
2930static int
2931tree_compare_entry(enum line_type type1, char *name1,
2932 enum line_type type2, char *name2)
2933{
2934 if (type1 != type2) {
2935 if (type1 == LINE_TREE_DIR)
2936 return -1;
2937 return 1;
2938 }
2939
2940 return strcmp(name1, name2);
2941}
2942
2943static bool
2944tree_read(struct view *view, char *text)
2945{
be04d936 2946 size_t textlen = text ? strlen(text) : 0;
e733ee54
JF
2947 char buf[SIZEOF_STR];
2948 unsigned long pos;
2949 enum line_type type;
f88a5319 2950 bool first_read = view->lines == 0;
e733ee54
JF
2951
2952 if (textlen <= SIZEOF_TREE_ATTR)
2953 return FALSE;
2954
2955 type = text[STRING_SIZE("100644 ")] == 't'
2956 ? LINE_TREE_DIR : LINE_TREE_FILE;
2957
f88a5319 2958 if (first_read) {
e733ee54 2959 /* Add path info line */
0a0d8910
JF
2960 if (!string_format(buf, "Directory path /%s", opt_path) ||
2961 !realloc_lines(view, view->line_size + 1) ||
2962 !add_line_text(view, buf, LINE_DEFAULT))
e733ee54
JF
2963 return FALSE;
2964
2965 /* Insert "link" to parent directory. */
0a0d8910
JF
2966 if (*opt_path) {
2967 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2968 !realloc_lines(view, view->line_size + 1) ||
2969 !add_line_text(view, buf, LINE_TREE_DIR))
2970 return FALSE;
2971 }
e733ee54
JF
2972 }
2973
2974 /* Strip the path part ... */
2975 if (*opt_path) {
2976 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2977 size_t striplen = strlen(opt_path);
2978 char *path = text + SIZEOF_TREE_ATTR;
2979
2980 if (pathlen > striplen)
2981 memmove(path, path + striplen,
2982 pathlen - striplen + 1);
2983 }
2984
2985 /* Skip "Directory ..." and ".." line. */
2986 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2987 struct line *line = &view->line[pos];
2988 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2989 char *path2 = text + SIZEOF_TREE_ATTR;
2990 int cmp = tree_compare_entry(line->type, path1, type, path2);
2991
2992 if (cmp <= 0)
2993 continue;
2994
2995 text = strdup(text);
2996 if (!text)
2997 return FALSE;
2998
2999 if (view->lines > pos)
3000 memmove(&view->line[pos + 1], &view->line[pos],
3001 (view->lines - pos) * sizeof(*line));
3002
3003 line = &view->line[pos];
3004 line->data = text;
3005 line->type = type;
3006 view->lines++;
3007 return TRUE;
3008 }
3009
0a0d8910 3010 if (!add_line_text(view, text, type))
e733ee54
JF
3011 return FALSE;
3012
69efc854
JF
3013 if (tree_lineno > view->lineno) {
3014 view->lineno = tree_lineno;
3015 tree_lineno = 0;
3016 }
f88a5319 3017
e733ee54
JF
3018 return TRUE;
3019}
3020
586c423d
JF
3021static enum request
3022tree_request(struct view *view, enum request request, struct line *line)
e733ee54 3023{
aac64c17 3024 enum open_flags flags;
586c423d 3025
c509eed2
DV
3026 if (request == REQ_TREE_PARENT) {
3027 if (*opt_path) {
3028 /* fake 'cd ..' */
3029 request = REQ_ENTER;
3030 line = &view->line[1];
3031 } else {
3032 /* quit view if at top of tree */
3033 return REQ_VIEW_CLOSE;
3034 }
3035 }
586c423d
JF
3036 if (request != REQ_ENTER)
3037 return request;
e733ee54 3038
69efc854
JF
3039 /* Cleanup the stack if the tree view is at a different tree. */
3040 while (!*opt_path && tree_stack)
3041 pop_tree_stack_entry();
3042
e733ee54
JF
3043 switch (line->type) {
3044 case LINE_TREE_DIR:
3045 /* Depending on whether it is a subdir or parent (updir?) link
3046 * mangle the path buffer. */
3047 if (line == &view->line[1] && *opt_path) {
69efc854 3048 pop_tree_stack_entry();
e733ee54
JF
3049
3050 } else {
4795d620 3051 char *data = line->data;
e733ee54
JF
3052 char *basename = data + SIZEOF_TREE_ATTR;
3053
69efc854 3054 push_tree_stack_entry(basename, view->lineno);
e733ee54
JF
3055 }
3056
3057 /* Trees and subtrees share the same ID, so they are not not
3058 * unique like blobs. */
aac64c17 3059 flags = OPEN_RELOAD;
e733ee54
JF
3060 request = REQ_VIEW_TREE;
3061 break;
3062
3063 case LINE_TREE_FILE:
aac64c17 3064 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
e733ee54
JF
3065 request = REQ_VIEW_BLOB;
3066 break;
3067
3068 default:
3069 return TRUE;
3070 }
3071
3072 open_view(view, request, flags);
69efc854
JF
3073 if (request == REQ_VIEW_TREE) {
3074 view->lineno = tree_lineno;
3075 }
e733ee54 3076
586c423d 3077 return REQ_NONE;
e733ee54
JF
3078}
3079
d720de4b
JF
3080static void
3081tree_select(struct view *view, struct line *line)
3082{
035ba11f 3083 char *text = line->data + STRING_SIZE("100644 blob ");
73c76ef5
JF
3084
3085 if (line->type == LINE_TREE_FILE) {
2463b4ea 3086 string_copy_rev(ref_blob, text);
d720de4b 3087
ebbaf4fe
JF
3088 } else if (line->type != LINE_TREE_DIR) {
3089 return;
d720de4b 3090 }
ebbaf4fe 3091
2463b4ea 3092 string_copy_rev(view->ref, text);
d720de4b
JF
3093}
3094
e733ee54
JF
3095static struct view_ops tree_ops = {
3096 "file",
f098944b 3097 NULL,
e733ee54 3098 tree_read,
f098944b 3099 pager_draw,
586c423d 3100 tree_request,
e733ee54 3101 pager_grep,
d720de4b 3102 tree_select,
e733ee54
JF
3103};
3104
3105static bool
3106blob_read(struct view *view, char *line)
3107{
c115e7ac 3108 return add_line_text(view, line, LINE_DEFAULT) != NULL;
e733ee54
JF
3109}
3110
3111static struct view_ops blob_ops = {
3112 "line",
f098944b 3113 NULL,
e733ee54 3114 blob_read,
f098944b 3115 pager_draw,
586c423d 3116 pager_request,
e733ee54 3117 pager_grep,
d720de4b 3118 pager_select,
e733ee54
JF
3119};
3120
3121
3122/*
173d76ea
JF
3123 * Status backend
3124 */
3125
3126struct status {
3127 char status;
3128 struct {
3129 mode_t mode;
3130 char rev[SIZEOF_REV];
3131 } old;
3132 struct {
3133 mode_t mode;
3134 char rev[SIZEOF_REV];
3135 } new;
3136 char name[SIZEOF_STR];
3137};
3138
b33611d8
JF
3139static struct status stage_status;
3140static enum line_type stage_line_type;
3141
173d76ea
JF
3142/* Get fields from the diff line:
3143 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3144 */
3145static inline bool
3146status_get_diff(struct status *file, char *buf, size_t bufsize)
3147{
3148 char *old_mode = buf + 1;
3149 char *new_mode = buf + 8;
3150 char *old_rev = buf + 15;
3151 char *new_rev = buf + 56;
3152 char *status = buf + 97;
3153
3154 if (bufsize != 99 ||
3155 old_mode[-1] != ':' ||
3156 new_mode[-1] != ' ' ||
3157 old_rev[-1] != ' ' ||
3158 new_rev[-1] != ' ' ||
3159 status[-1] != ' ')
3160 return FALSE;
3161
3162 file->status = *status;
3163
3164 string_copy_rev(file->old.rev, old_rev);
3165 string_copy_rev(file->new.rev, new_rev);
3166
3167 file->old.mode = strtoul(old_mode, NULL, 8);
3168 file->new.mode = strtoul(new_mode, NULL, 8);
3169
3170 file->name[0] = 0;
3171
3172 return TRUE;
3173}
3174
3175static bool
3176status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3177{
3178 struct status *file = NULL;
b5c18d9d 3179 struct status *unmerged = NULL;
173d76ea
JF
3180 char buf[SIZEOF_STR * 4];
3181 size_t bufsize = 0;
3182 FILE *pipe;
3183
3184 pipe = popen(cmd, "r");
3185 if (!pipe)
3186 return FALSE;
3187
3188 add_line_data(view, NULL, type);
3189
3190 while (!feof(pipe) && !ferror(pipe)) {
3191 char *sep;
3192 size_t readsize;
3193
3194 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3195 if (!readsize)
3196 break;
3197 bufsize += readsize;
3198
3199 /* Process while we have NUL chars. */
3200 while ((sep = memchr(buf, 0, bufsize))) {
3201 size_t sepsize = sep - buf + 1;
3202
3203 if (!file) {
3204 if (!realloc_lines(view, view->line_size + 1))
3205 goto error_out;
3206
3207 file = calloc(1, sizeof(*file));
3208 if (!file)
3209 goto error_out;
3210
3211 add_line_data(view, file, type);
3212 }
3213
3214 /* Parse diff info part. */
3215 if (!diff) {
3216 file->status = '?';
3217
3218 } else if (!file->status) {
3219 if (!status_get_diff(file, buf, sepsize))
3220 goto error_out;
3221
3222 bufsize -= sepsize;
3223 memmove(buf, sep + 1, bufsize);
3224
3225 sep = memchr(buf, 0, bufsize);
3226 if (!sep)
3227 break;
3228 sepsize = sep - buf + 1;
b5c18d9d
JF
3229
3230 /* Collapse all 'M'odified entries that
3231 * follow a associated 'U'nmerged entry.
3232 */
3233 if (file->status == 'U') {
3234 unmerged = file;
3235
3236 } else if (unmerged) {
3237 int collapse = !strcmp(buf, unmerged->name);
3238
3239 unmerged = NULL;
3240 if (collapse) {
3241 free(file);
3242 view->lines--;
3243 continue;
3244 }
3245 }
173d76ea
JF
3246 }
3247
3248 /* git-ls-files just delivers a NUL separated
3249 * list of file names similar to the second half
3250 * of the git-diff-* output. */
3251 string_ncopy(file->name, buf, sepsize);
3252 bufsize -= sepsize;
3253 memmove(buf, sep + 1, bufsize);
3254 file = NULL;
3255 }
3256 }
3257
3258 if (ferror(pipe)) {
3259error_out:
3260 pclose(pipe);
3261 return FALSE;
3262 }
3263
3264 if (!view->line[view->lines - 1].data)
3265 add_line_data(view, NULL, LINE_STAT_NONE);
3266
3267 pclose(pipe);
3268 return TRUE;
3269}
3270
b5c18d9d
JF
3271/* Don't show unmerged entries in the staged section. */
3272#define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached HEAD"
173d76ea
JF
3273#define STATUS_DIFF_FILES_CMD "git diff-files -z"
3274#define STATUS_LIST_OTHER_CMD \
810f0078 3275 "git ls-files -z --others --exclude-per-directory=.gitignore"
173d76ea 3276
f99c6095 3277#define STATUS_DIFF_INDEX_SHOW_CMD \
4ef1bf12 3278 "git diff-index --root --patch-with-stat --find-copies-harder -C --cached HEAD -- %s 2>/dev/null"
f99c6095
JF
3279
3280#define STATUS_DIFF_FILES_SHOW_CMD \
4ef1bf12 3281 "git diff-files --root --patch-with-stat --find-copies-harder -C -- %s 2>/dev/null"
89d917a2 3282
173d76ea
JF
3283/* First parse staged info using git-diff-index(1), then parse unstaged
3284 * info using git-diff-files(1), and finally untracked files using
3285 * git-ls-files(1). */
3286static bool
3287status_open(struct view *view)
3288{
810f0078
JF
3289 struct stat statbuf;
3290 char exclude[SIZEOF_STR];
3291 char cmd[SIZEOF_STR];
12e8c2be 3292 unsigned long prev_lineno = view->lineno;
173d76ea
JF
3293 size_t i;
3294
3295 for (i = 0; i < view->lines; i++)
3296 free(view->line[i].data);
3297 free(view->line);
12e8c2be 3298 view->lines = view->line_size = view->lineno = 0;
173d76ea
JF
3299 view->line = NULL;
3300
3301 if (!realloc_lines(view, view->line_size + 6))
3302 return FALSE;
3303
810f0078
JF
3304 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3305 return FALSE;
3306
3307 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3308
3309 if (stat(exclude, &statbuf) >= 0) {
3310 size_t cmdsize = strlen(cmd);
3311
3312 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3313 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3314 return FALSE;
3315 }
3316
173d76ea
JF
3317 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3318 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
810f0078 3319 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
173d76ea
JF
3320 return FALSE;
3321
12e8c2be
JF
3322 /* If all went well restore the previous line number to stay in
3323 * the context. */
3324 if (prev_lineno < view->lines)
3325 view->lineno = prev_lineno;
3326 else
3327 view->lineno = view->lines - 1;
3328
173d76ea
JF
3329 return TRUE;
3330}
3331
3332static bool
3333status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3334{
3335 struct status *status = line->data;
3336
3337 wmove(view->win, lineno, 0);
3338
3339 if (selected) {
3340 wattrset(view->win, get_line_attr(LINE_CURSOR));
3341 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3342
3343 } else if (!status && line->type != LINE_STAT_NONE) {
3344 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3345 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3346
3347 } else {
3348 wattrset(view->win, get_line_attr(line->type));
3349 }
3350
3351 if (!status) {
3352 char *text;
3353
3354 switch (line->type) {
3355 case LINE_STAT_STAGED:
3356 text = "Changes to be committed:";
3357 break;
3358
3359 case LINE_STAT_UNSTAGED:
3360 text = "Changed but not updated:";
3361 break;
3362
3363 case LINE_STAT_UNTRACKED:
3364 text = "Untracked files:";
3365 break;
3366
3367 case LINE_STAT_NONE:
3368 text = " (no files)";
3369 break;
3370
3371 default:
3372 return FALSE;
3373 }
3374
3375 waddstr(view->win, text);
3376 return TRUE;
3377 }
3378
3379 waddch(view->win, status->status);
3380 if (!selected)
3381 wattrset(view->win, A_NORMAL);
3382 wmove(view->win, lineno, 4);
3383 waddstr(view->win, status->name);
3384
3385 return TRUE;
3386}
3387
586c423d 3388static enum request
88f66e2d 3389status_enter(struct view *view, struct line *line)
173d76ea 3390{
89d917a2 3391 struct status *status = line->data;
4e8159cf 3392 char path[SIZEOF_STR] = "";
89d917a2
JF
3393 char *info;
3394 size_t cmdsize = 0;
3395
4e8159cf
JF
3396 if (line->type == LINE_STAT_NONE ||
3397 (!status && line[1].type == LINE_STAT_NONE)) {
3398 report("No file to diff");
586c423d 3399 return REQ_NONE;
89d917a2
JF
3400 }
3401
4e8159cf 3402 if (status && sq_quote(path, 0, status->name) >= sizeof(path))
586c423d 3403 return REQ_QUIT;
89d917a2
JF
3404
3405 if (opt_cdup[0] &&
3406 line->type != LINE_STAT_UNTRACKED &&
3407 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
586c423d 3408 return REQ_QUIT;
89d917a2
JF
3409
3410 switch (line->type) {
3411 case LINE_STAT_STAGED:
f99c6095
JF
3412 if (!string_format_from(opt_cmd, &cmdsize,
3413 STATUS_DIFF_INDEX_SHOW_CMD, path))
586c423d 3414 return REQ_QUIT;
4e8159cf
JF
3415 if (status)
3416 info = "Staged changes to %s";
3417 else
3418 info = "Staged changes";
89d917a2
JF
3419 break;
3420
3421 case LINE_STAT_UNSTAGED:
f99c6095
JF
3422 if (!string_format_from(opt_cmd, &cmdsize,
3423 STATUS_DIFF_FILES_SHOW_CMD, path))
586c423d 3424 return REQ_QUIT;
4e8159cf
JF
3425 if (status)
3426 info = "Unstaged changes to %s";
3427 else
3428 info = "Unstaged changes";
89d917a2
JF
3429 break;
3430
3431 case LINE_STAT_UNTRACKED:
3432 if (opt_pipe)
586c423d
JF
3433 return REQ_QUIT;
3434
4e8159cf
JF
3435
3436 if (!status) {
3437 report("No file to show");
586c423d 3438 return REQ_NONE;
4e8159cf
JF
3439 }
3440
89d917a2
JF
3441 opt_pipe = fopen(status->name, "r");
3442 info = "Untracked file %s";
3443 break;
3444
3445 default:
e77aa656 3446 die("line type %d not handled in switch", line->type);
89d917a2
JF
3447 }
3448
3e634113
JF
3449 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3450 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
b33611d8
JF
3451 if (status) {
3452 stage_status = *status;
3453 } else {
3454 memset(&stage_status, 0, sizeof(stage_status));
3455 }
3456
3457 stage_line_type = line->type;
3458 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.name);
89d917a2
JF
3459 }
3460
586c423d 3461 return REQ_NONE;
ca1d71ea
JF
3462}
3463
88f66e2d 3464
ca1d71ea
JF
3465static bool
3466status_update_file(struct view *view, struct status *status, enum line_type type)
3467{
91c5d983 3468 char cmd[SIZEOF_STR];
173d76ea 3469 char buf[SIZEOF_STR];
91c5d983 3470 size_t cmdsize = 0;
173d76ea
JF
3471 size_t bufsize = 0;
3472 size_t written = 0;
3473 FILE *pipe;
3474
91c5d983 3475 if (opt_cdup[0] &&
ca1d71ea 3476 type != LINE_STAT_UNTRACKED &&
91c5d983
JF
3477 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3478 return FALSE;
3479
ca1d71ea 3480 switch (type) {
173d76ea
JF
3481 case LINE_STAT_STAGED:
3482 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3483 status->old.mode,
3484 status->old.rev,
3485 status->name, 0))
3486 return FALSE;
91c5d983
JF
3487
3488 string_add(cmd, cmdsize, "git update-index -z --index-info");
173d76ea
JF
3489 break;
3490
3491 case LINE_STAT_UNSTAGED:
3492 case LINE_STAT_UNTRACKED:
3493 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3494 return FALSE;
91c5d983
JF
3495
3496 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
173d76ea
JF
3497 break;
3498
3499 default:
e77aa656 3500 die("line type %d not handled in switch", type);
173d76ea
JF
3501 }
3502
3503 pipe = popen(cmd, "w");
3504 if (!pipe)
3505 return FALSE;
3506
3507 while (!ferror(pipe) && written < bufsize) {
3508 written += fwrite(buf + written, 1, bufsize - written, pipe);
3509 }
3510
3511 pclose(pipe);
3512
3513 if (written != bufsize)
3514 return FALSE;
3515
173d76ea
JF
3516 return TRUE;
3517}
3518
3519static void
ca1d71ea
JF
3520status_update(struct view *view)
3521{
dec7b437 3522 struct line *line = &view->line[view->lineno];
351917f8 3523
dec7b437 3524 assert(view->lines);
11359638 3525
dec7b437
JF
3526 if (!line->data) {
3527 while (++line < view->line + view->lines && line->data) {
3528 if (!status_update_file(view, line->data, line->type))
3529 report("Failed to update file status");
3530 }
11359638 3531
dec7b437
JF
3532 if (!line[-1].data) {
3533 report("Nothing to update");
3534 return;
93e4c4f6
JF
3535 }
3536
dec7b437
JF
3537 } else if (!status_update_file(view, line->data, line->type)) {
3538 report("Failed to update file status");
ca1d71ea
JF
3539 }
3540}
3541
88f66e2d
JF
3542static enum request
3543status_request(struct view *view, enum request request, struct line *line)
3544{
0cea0d43
JF
3545 struct status *status = line->data;
3546
88f66e2d
JF
3547 switch (request) {
3548 case REQ_STATUS_UPDATE:
3549 status_update(view);
3550 break;
3551
b5c18d9d 3552 case REQ_STATUS_MERGE:
91521b9d
JF
3553 if (!status || status->status != 'U') {
3554 report("Merging only possible for files with unmerged status ('U').");
3555 return REQ_NONE;
3556 }
b5c18d9d 3557 open_mergetool(status->name);
b5c18d9d
JF
3558 break;
3559
0cea0d43
JF
3560 case REQ_EDIT:
3561 if (!status)
3562 return request;
3563
3dd65d95 3564 open_editor(status->status != '?', status->name);
0cea0d43
JF
3565 break;
3566
88f66e2d 3567 case REQ_ENTER:
17a27c16
JF
3568 /* After returning the status view has been split to
3569 * show the stage view. No further reloading is
3570 * necessary. */
88f66e2d 3571 status_enter(view, line);
17a27c16 3572 return REQ_NONE;
88f66e2d 3573
acaef3b3 3574 case REQ_REFRESH:
17a27c16 3575 /* Simply reload the view. */
acaef3b3
JF
3576 break;
3577
88f66e2d
JF
3578 default:
3579 return request;
3580 }
3581
17a27c16
JF
3582 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3583
88f66e2d
JF
3584 return REQ_NONE;
3585}
3586
ca1d71ea 3587static void
173d76ea
JF
3588status_select(struct view *view, struct line *line)
3589{
11359638
JF
3590 struct status *status = line->data;
3591 char file[SIZEOF_STR] = "all files";
173d76ea 3592 char *text;
b5c18d9d 3593 char *key;
173d76ea 3594
11359638
JF
3595 if (status && !string_format(file, "'%s'", status->name))
3596 return;
3597
3598 if (!status && line[1].type == LINE_STAT_NONE)
3599 line++;
3600
173d76ea
JF
3601 switch (line->type) {
3602 case LINE_STAT_STAGED:
11359638 3603 text = "Press %s to unstage %s for commit";
173d76ea
JF
3604 break;
3605
3606 case LINE_STAT_UNSTAGED:
11359638 3607 text = "Press %s to stage %s for commit";
173d76ea
JF
3608 break;
3609
3610 case LINE_STAT_UNTRACKED:
11359638 3611 text = "Press %s to stage %s for addition";
173d76ea
JF
3612 break;
3613
3614 case LINE_STAT_NONE:
11359638
JF
3615 text = "Nothing to update";
3616 break;
173d76ea
JF
3617
3618 default:
e77aa656 3619 die("line type %d not handled in switch", line->type);
173d76ea
JF
3620 }
3621
b5c18d9d
JF
3622 if (status && status->status == 'U') {
3623 text = "Press %s to resolve conflict in %s";
3624 key = get_key(REQ_STATUS_MERGE);
3625
3626 } else {
3627 key = get_key(REQ_STATUS_UPDATE);
3628 }
3629
3630 string_format(view->ref, text, key, file);
173d76ea
JF
3631}
3632
3633static bool
3634status_grep(struct view *view, struct line *line)
3635{
3636 struct status *status = line->data;
3637 enum { S_STATUS, S_NAME, S_END } state;
3638 char buf[2] = "?";
3639 regmatch_t pmatch;
3640
3641 if (!status)
3642 return FALSE;
3643
3644 for (state = S_STATUS; state < S_END; state++) {
3645 char *text;
3646
3647 switch (state) {
3648 case S_NAME: text = status->name; break;
3649 case S_STATUS:
3650 buf[0] = status->status;
3651 text = buf;
3652 break;
3653
3654 default:
3655 return FALSE;
3656 }
3657
3658 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3659 return TRUE;
3660 }
3661
3662 return FALSE;
3663}
3664
3665static struct view_ops status_ops = {
3666 "file",
3667 status_open,
3668 NULL,
3669 status_draw,
586c423d 3670 status_request,
173d76ea
JF
3671 status_grep,
3672 status_select,
3673};
3674
b33611d8
JF
3675
3676static bool
3677stage_diff_line(FILE *pipe, struct line *line)
3678{
3679 char *buf = line->data;
3680 size_t bufsize = strlen(buf);
3681 size_t written = 0;
3682
3683 while (!ferror(pipe) && written < bufsize) {
3684 written += fwrite(buf + written, 1, bufsize - written, pipe);
3685 }
3686
3687 fputc('\n', pipe);
3688
3689 return written == bufsize;
3690}
3691
3692static struct line *
3693stage_diff_hdr(struct view *view, struct line *line)
3694{
3695 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3696 struct line *diff_hdr;
3697
3698 if (line->type == LINE_DIFF_CHUNK)
3699 diff_hdr = line - 1;
3700 else
3701 diff_hdr = view->line + 1;
3702
3703 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3704 if (diff_hdr->type == LINE_DIFF_HEADER)
3705 return diff_hdr;
3706
3707 diff_hdr += diff_hdr_dir;
3708 }
3709
3710 return NULL;
3711}
3712
3713static bool
3714stage_update_chunk(struct view *view, struct line *line)
3715{
3716 char cmd[SIZEOF_STR];
3717 size_t cmdsize = 0;
3718 struct line *diff_hdr, *diff_chunk, *diff_end;
3719 FILE *pipe;
3720
3721 diff_hdr = stage_diff_hdr(view, line);
3722 if (!diff_hdr)
3723 return FALSE;
3724
3725 if (opt_cdup[0] &&
3726 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3727 return FALSE;
3728
3729 if (!string_format_from(cmd, &cmdsize,
3730 "git apply --cached %s - && "
3731 "git update-index -q --unmerged --refresh 2>/dev/null",
3732 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3733 return FALSE;
3734
3735 pipe = popen(cmd, "w");
3736 if (!pipe)
3737 return FALSE;
3738
3739 diff_end = view->line + view->lines;
3740 if (line->type != LINE_DIFF_CHUNK) {
3741 diff_chunk = diff_hdr;
3742
3743 } else {
3744 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3745 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3746 diff_chunk->type == LINE_DIFF_HEADER)
3747 diff_end = diff_chunk;
3748
3749 diff_chunk = line;
3750
3751 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3752 switch (diff_hdr->type) {
3753 case LINE_DIFF_HEADER:
3754 case LINE_DIFF_INDEX:
3755 case LINE_DIFF_ADD:
3756 case LINE_DIFF_DEL:
3757 break;
3758
3759 default:
3760 diff_hdr++;
3761 continue;
3762 }
3763
3764 if (!stage_diff_line(pipe, diff_hdr++)) {
3765 pclose(pipe);
3766 return FALSE;
3767 }
3768 }
3769 }
3770
3771 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3772 diff_chunk++;
3773
3774 pclose(pipe);
3775
3776 if (diff_chunk != diff_end)
3777 return FALSE;
3778
3779 return TRUE;
3780}
3781
3782static void
3783stage_update(struct view *view, struct line *line)
3784{
3785 if (stage_line_type != LINE_STAT_UNTRACKED &&
3786 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3787 if (!stage_update_chunk(view, line)) {
3788 report("Failed to apply chunk");
3789 return;
3790 }
3791
3792 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3793 report("Failed to update file");
3794 return;
3795 }
3796
3797 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3798
3799 view = VIEW(REQ_VIEW_STATUS);
3800 if (view_is_displayed(view))
3801 status_enter(view, &view->line[view->lineno]);
3802}
3803
3804static enum request
3805stage_request(struct view *view, enum request request, struct line *line)
3806{
3807 switch (request) {
3808 case REQ_STATUS_UPDATE:
3809 stage_update(view, line);
3810 break;
3811
3812 case REQ_EDIT:
3813 if (!stage_status.name[0])
3814 return request;
3815
3dd65d95 3816 open_editor(stage_status.status != '?', stage_status.name);
b33611d8
JF
3817 break;
3818
3819 case REQ_ENTER:
3820 pager_request(view, request, line);
3821 break;
3822
3823 default:
3824 return request;
3825 }
3826
3827 return REQ_NONE;
3828}
3829
3e634113
JF
3830static struct view_ops stage_ops = {
3831 "line",
3832 NULL,
3833 pager_read,
3834 pager_draw,
b33611d8 3835 stage_request,
3e634113
JF
3836 pager_grep,
3837 pager_select,
3838};
173d76ea 3839
b33611d8 3840
173d76ea 3841/*
ccc33449 3842 * Revision graph
ff26aa29
JF
3843 */
3844
3845struct commit {
10446330 3846 char id[SIZEOF_REV]; /* SHA1 ID. */
aea510c8 3847 char title[128]; /* First line of the commit message. */
54efb62b
JF
3848 char author[75]; /* Author of the commit. */
3849 struct tm time; /* Date from the author ident. */
3850 struct ref **refs; /* Repository references. */
3851 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3852 size_t graph_size; /* The width of the graph array. */
ff26aa29 3853};
c34d9c9f 3854
ccc33449
JF
3855/* Size of rev graph with no "padding" columns */
3856#define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
2b757533 3857
2ce5c87c
JF
3858struct rev_graph {
3859 struct rev_graph *prev, *next, *parents;
2b757533
JF
3860 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3861 size_t size;
88757ebd
JF
3862 struct commit *commit;
3863 size_t pos;
e81e9c2c 3864 unsigned int boundary:1;
2b757533
JF
3865};
3866
2b757533 3867/* Parents of the commit being visualized. */
446a5c36 3868static struct rev_graph graph_parents[4];
c8d60a25 3869
c65a501a 3870/* The current stack of revisions on the graph. */
446a5c36
JF
3871static struct rev_graph graph_stacks[4] = {
3872 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
c65a501a 3873 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
446a5c36
JF
3874 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3875 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
c65a501a
JF
3876};
3877
9e43b9cd 3878static inline bool
2ce5c87c 3879graph_parent_is_merge(struct rev_graph *graph)
9e43b9cd
JF
3880{
3881 return graph->parents->size > 1;
3882}
3883
88757ebd 3884static inline void
2ce5c87c 3885append_to_rev_graph(struct rev_graph *graph, chtype symbol)
88757ebd 3886{
2c27faac
JF
3887 struct commit *commit = graph->commit;
3888
3889 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3890 commit->graph[commit->graph_size++] = symbol;
88757ebd
JF
3891}
3892
2b757533 3893static void
2ce5c87c 3894done_rev_graph(struct rev_graph *graph)
987890af
JF
3895{
3896 if (graph_parent_is_merge(graph) &&
3897 graph->pos < graph->size - 1 &&
3898 graph->next->size == graph->size + graph->parents->size - 1) {
3899 size_t i = graph->pos + graph->parents->size - 1;
3900
3901 graph->commit->graph_size = i * 2;
3902 while (i < graph->next->size - 1) {
3903 append_to_rev_graph(graph, ' ');
3904 append_to_rev_graph(graph, '\\');
3905 i++;
3906 }
3907 }
3908
3909 graph->size = graph->pos = 0;
3910 graph->commit = NULL;
3911 memset(graph->parents, 0, sizeof(*graph->parents));
3912}
3913
3914static void
2ce5c87c 3915push_rev_graph(struct rev_graph *graph, char *parent)
2b757533 3916{
2fe894e6
JF
3917 int i;
3918
3919 /* "Collapse" duplicate parents lines.
3920 *
3921 * FIXME: This needs to also update update the drawn graph but
3922 * for now it just serves as a method for pruning graph lines. */
3923 for (i = 0; i < graph->size; i++)
3924 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3925 return;
2b757533 3926
2ce5c87c 3927 if (graph->size < SIZEOF_REVITEMS) {
739e81de 3928 string_copy_rev(graph->rev[graph->size++], parent);
2b757533
JF
3929 }
3930}
3931
92507a24
JF
3932static chtype
3933get_rev_graph_symbol(struct rev_graph *graph)
2b757533 3934{
92507a24 3935 chtype symbol;
2b757533 3936
e81e9c2c
JF
3937 if (graph->boundary)
3938 symbol = REVGRAPH_BOUND;
3939 else if (graph->parents->size == 0)
c8d60a25 3940 symbol = REVGRAPH_INIT;
18ffaa23 3941 else if (graph_parent_is_merge(graph))
c8d60a25 3942 symbol = REVGRAPH_MERGE;
c65a501a 3943 else if (graph->pos >= graph->size)
c8d60a25 3944 symbol = REVGRAPH_BRANCH;
2b757533 3945 else
c8d60a25 3946 symbol = REVGRAPH_COMMIT;
1dcb3bec 3947
92507a24
JF
3948 return symbol;
3949}
3950
3951static void
3952draw_rev_graph(struct rev_graph *graph)
3953{
e937c2c8
JF
3954 struct rev_filler {
3955 chtype separator, line;
3956 };
3957 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3958 static struct rev_filler fillers[] = {
3959 { ' ', REVGRAPH_LINE },
3960 { '`', '.' },
3961 { '\'', ' ' },
3962 { '/', ' ' },
e937c2c8 3963 };
92507a24 3964 chtype symbol = get_rev_graph_symbol(graph);
e937c2c8 3965 struct rev_filler *filler;
92507a24
JF
3966 size_t i;
3967
e937c2c8 3968 filler = &fillers[DEFAULT];
110e948e 3969
c65a501a 3970 for (i = 0; i < graph->pos; i++) {
e937c2c8 3971 append_to_rev_graph(graph, filler->line);
9e43b9cd 3972 if (graph_parent_is_merge(graph->prev) &&
e937c2c8
JF
3973 graph->prev->pos == i)
3974 filler = &fillers[RSHARP];
3975
3976 append_to_rev_graph(graph, filler->separator);
110e948e
JF
3977 }
3978
92507a24 3979 /* Place the symbol for this revision. */
c65a501a 3980 append_to_rev_graph(graph, symbol);
2b757533 3981
e937c2c8
JF
3982 if (graph->prev->size > graph->size)
3983 filler = &fillers[RDIAG];
3984 else
3985 filler = &fillers[DEFAULT];
3986
c8d60a25 3987 i++;
2b757533 3988
c65a501a 3989 for (; i < graph->size; i++) {
e937c2c8
JF
3990 append_to_rev_graph(graph, filler->separator);
3991 append_to_rev_graph(graph, filler->line);
3992 if (graph_parent_is_merge(graph->prev) &&
3993 i < graph->prev->pos + graph->parents->size)
3994 filler = &fillers[RSHARP];
3995 if (graph->prev->size > graph->size)
3996 filler = &fillers[LDIAG];
c65a501a
JF
3997 }
3998
3999 if (graph->prev->size > graph->size) {
e937c2c8
JF
4000 append_to_rev_graph(graph, filler->separator);
4001 if (filler->line != ' ')
4002 append_to_rev_graph(graph, filler->line);
2b757533 4003 }
b5d8f208
JF
4004}
4005
61eed810
JF
4006/* Prepare the next rev graph */
4007static void
4008prepare_rev_graph(struct rev_graph *graph)
b5d8f208 4009{
b5d8f208
JF
4010 size_t i;
4011
320df4ea 4012 /* First, traverse all lines of revisions up to the active one. */
c65a501a
JF
4013 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4014 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
b5d8f208 4015 break;
b5d8f208 4016
2ce5c87c 4017 push_rev_graph(graph->next, graph->rev[graph->pos]);
b5d8f208
JF
4018 }
4019
320df4ea 4020 /* Interleave the new revision parent(s). */
e81e9c2c 4021 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
2ce5c87c 4022 push_rev_graph(graph->next, graph->parents->rev[i]);
b5d8f208 4023
320df4ea 4024 /* Lastly, put any remaining revisions. */
c65a501a 4025 for (i = graph->pos + 1; i < graph->size; i++)
2ce5c87c 4026 push_rev_graph(graph->next, graph->rev[i]);
61eed810
JF
4027}
4028
4029static void
4030update_rev_graph(struct rev_graph *graph)
4031{
446a5c36
JF
4032 /* If this is the finalizing update ... */
4033 if (graph->commit)
4034 prepare_rev_graph(graph);
4035
4036 /* Graph visualization needs a one rev look-ahead,
4037 * so the first update doesn't visualize anything. */
4038 if (!graph->prev->commit)
4039 return;
c65a501a 4040
61eed810
JF
4041 draw_rev_graph(graph->prev);
4042 done_rev_graph(graph->prev->prev);
2b757533
JF
4043}
4044
ccc33449
JF
4045
4046/*
4047 * Main view backend
4048 */
4049
4050static bool
4051main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4052{
4053 char buf[DATE_COLS + 1];
4054 struct commit *commit = line->data;
4055 enum line_type type;
4056 int col = 0;
4057 size_t timelen;
4058 size_t authorlen;
4059 int trimmed = 1;
4060
4061 if (!*commit->author)
4062 return FALSE;
4063
4064 wmove(view->win, lineno, col);
4065
4066 if (selected) {
4067 type = LINE_CURSOR;
4068 wattrset(view->win, get_line_attr(type));
4069 wchgat(view->win, -1, 0, type, NULL);
4070
4071 } else {
4072 type = LINE_MAIN_COMMIT;
4073 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4074 }
4075
4076 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4077 waddnstr(view->win, buf, timelen);
4078 waddstr(view->win, " ");
4079
4080 col += DATE_COLS;
4081 wmove(view->win, lineno, col);
4082 if (type != LINE_CURSOR)
4083 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4084
4085 if (opt_utf8) {
4086 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
4087 } else {
4088 authorlen = strlen(commit->author);
4089 if (authorlen > AUTHOR_COLS - 2) {
4090 authorlen = AUTHOR_COLS - 2;
4091 trimmed = 1;
4092 }
4093 }
4094
4095 if (trimmed) {
4096 waddnstr(view->win, commit->author, authorlen);
4097 if (type != LINE_CURSOR)
4098 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
4099 waddch(view->win, '~');
4100 } else {
4101 waddstr(view->win, commit->author);
4102 }
4103
4104 col += AUTHOR_COLS;
ccc33449
JF
4105
4106 if (opt_rev_graph && commit->graph_size) {
4107 size_t i;
4108
f83b1c33
DV
4109 if (type != LINE_CURSOR)
4110 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
ccc33449
JF
4111 wmove(view->win, lineno, col);
4112 /* Using waddch() instead of waddnstr() ensures that
4113 * they'll be rendered correctly for the cursor line. */
4114 for (i = 0; i < commit->graph_size; i++)
4115 waddch(view->win, commit->graph[i]);
4116
8716b9ed 4117 waddch(view->win, ' ');
ccc33449
JF
4118 col += commit->graph_size + 1;
4119 }
f83b1c33
DV
4120 if (type != LINE_CURSOR)
4121 wattrset(view->win, A_NORMAL);
ccc33449
JF
4122
4123 wmove(view->win, lineno, col);
4124
4125 if (commit->refs) {
4126 size_t i = 0;
4127
4128 do {
4129 if (type == LINE_CURSOR)
4130 ;
4131 else if (commit->refs[i]->tag)
4132 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
e15ec88e
JF
4133 else if (commit->refs[i]->remote)
4134 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
ccc33449
JF
4135 else
4136 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4137 waddstr(view->win, "[");
4138 waddstr(view->win, commit->refs[i]->name);
4139 waddstr(view->win, "]");
4140 if (type != LINE_CURSOR)
4141 wattrset(view->win, A_NORMAL);
4142 waddstr(view->win, " ");
4143 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
4144 } while (commit->refs[i++]->next);
4145 }
4146
4147 if (type != LINE_CURSOR)
4148 wattrset(view->win, get_line_attr(type));
4149
4150 {
4151 int titlelen = strlen(commit->title);
4152
4153 if (col + titlelen > view->width)
4154 titlelen = view->width - col;
4155
4156 waddnstr(view->win, commit->title, titlelen);
4157 }
4158
4159 return TRUE;
4160}
4161
4c6fabc2 4162/* Reads git log --pretty=raw output and parses it into the commit struct. */
6b161b31 4163static bool
701e4f5d 4164main_read(struct view *view, char *line)
22f66b0a 4165{
2ce5c87c 4166 static struct rev_graph *graph = graph_stacks;
be04d936 4167 enum line_type type;
0ff3b97c 4168 struct commit *commit;
22f66b0a 4169
be04d936 4170 if (!line) {
446a5c36 4171 update_rev_graph(graph);
be04d936
JF
4172 return TRUE;
4173 }
4174
4175 type = get_line_type(line);
0ff3b97c 4176 if (type == LINE_COMMIT) {
22f66b0a
JF
4177 commit = calloc(1, sizeof(struct commit));
4178 if (!commit)
4179 return FALSE;
4180
e81e9c2c
JF
4181 line += STRING_SIZE("commit ");
4182 if (*line == '-') {
4183 graph->boundary = 1;
4184 line++;
4185 }
4186
4187 string_copy_rev(commit->id, line);
c34d9c9f 4188 commit->refs = get_refs(commit->id);
c65a501a 4189 graph->commit = commit;
e314c36d 4190 add_line_data(view, commit, LINE_MAIN_COMMIT);
0ff3b97c
JF
4191 return TRUE;
4192 }
2b757533 4193
0ff3b97c
JF
4194 if (!view->lines)
4195 return TRUE;
4196 commit = view->line[view->lines - 1].data;
4197
4198 switch (type) {
2b757533 4199 case LINE_PARENT:
0ff3b97c 4200 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
78c70acd 4201 break;
22f66b0a 4202
8855ada4 4203 case LINE_AUTHOR:
b76c2afc 4204 {
19c3ac60
JF
4205 /* Parse author lines where the name may be empty:
4206 * author <email@address.tld> 1138474660 +0100
4207 */
4c6fabc2 4208 char *ident = line + STRING_SIZE("author ");
19c3ac60
JF
4209 char *nameend = strchr(ident, '<');
4210 char *emailend = strchr(ident, '>');
b76c2afc 4211
0ff3b97c 4212 if (!nameend || !emailend)
fe7233c3
JF
4213 break;
4214
c65a501a
JF
4215 update_rev_graph(graph);
4216 graph = graph->next;
2b757533 4217
19c3ac60
JF
4218 *nameend = *emailend = 0;
4219 ident = chomp_string(ident);
4220 if (!*ident) {
4221 ident = chomp_string(nameend + 1);
4222 if (!*ident)
4223 ident = "Unknown";
b76c2afc
JF
4224 }
4225
739e81de 4226 string_ncopy(commit->author, ident, strlen(ident));
b76c2afc 4227
4c6fabc2 4228 /* Parse epoch and timezone */
19c3ac60
JF
4229 if (emailend[1] == ' ') {
4230 char *secs = emailend + 2;
4231 char *zone = strchr(secs, ' ');
4232 time_t time = (time_t) atol(secs);
b76c2afc 4233
4c6fabc2 4234 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
b76c2afc
JF
4235 long tz;
4236
4237 zone++;
4238 tz = ('0' - zone[1]) * 60 * 60 * 10;
4239 tz += ('0' - zone[2]) * 60 * 60;
4240 tz += ('0' - zone[3]) * 60;
4241 tz += ('0' - zone[4]) * 60;
4242
4243 if (zone[0] == '-')
4244 tz = -tz;
4245
4246 time -= tz;
4247 }
19c3ac60 4248
b76c2afc
JF
4249 gmtime_r(&time, &commit->time);
4250 }
4251 break;
4252 }
78c70acd 4253 default:
2e8488b4 4254 /* Fill in the commit title if it has not already been set. */
2e8488b4
JF
4255 if (commit->title[0])
4256 break;
4257
4258 /* Require titles to start with a non-space character at the
4259 * offset used by git log. */
9073c64a
JF
4260 if (strncmp(line, " ", 4))
4261 break;
4262 line += 4;
4263 /* Well, if the title starts with a whitespace character,
4264 * try to be forgiving. Otherwise we end up with no title. */
4265 while (isspace(*line))
4266 line++;
4267 if (*line == '\0')
82e78006 4268 break;
9073c64a
JF
4269 /* FIXME: More graceful handling of titles; append "..." to
4270 * shortened titles, etc. */
82e78006 4271
739e81de 4272 string_ncopy(commit->title, line, strlen(line));
22f66b0a
JF
4273 }
4274
4275 return TRUE;
4276}
4277
586c423d
JF
4278static enum request
4279main_request(struct view *view, enum request request, struct line *line)
b801d8b2 4280{
b3a54cba
JF
4281 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4282
586c423d
JF
4283 if (request == REQ_ENTER)
4284 open_view(view, REQ_VIEW_DIFF, flags);
4285 else
4286 return request;
4287
4288 return REQ_NONE;
b801d8b2
JF
4289}
4290
4af34daa
JF
4291static bool
4292main_grep(struct view *view, struct line *line)
4293{
4294 struct commit *commit = line->data;
4295 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4296 char buf[DATE_COLS + 1];
4297 regmatch_t pmatch;
4298
4299 for (state = S_TITLE; state < S_END; state++) {
4300 char *text;
4301
4302 switch (state) {
4303 case S_TITLE: text = commit->title; break;
4304 case S_AUTHOR: text = commit->author; break;
4305 case S_DATE:
4306 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4307 continue;
4308 text = buf;
4309 break;
4310
4311 default:
4312 return FALSE;
4313 }
4314
b77b2cb8 4315 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4af34daa
JF
4316 return TRUE;
4317 }
4318
4319 return FALSE;
4320}
4321
d720de4b
JF
4322static void
4323main_select(struct view *view, struct line *line)
4324{
4325 struct commit *commit = line->data;
4326
2463b4ea
JF
4327 string_copy_rev(view->ref, commit->id);
4328 string_copy_rev(ref_commit, view->ref);
d720de4b
JF
4329}
4330
6b161b31 4331static struct view_ops main_ops = {
6734f6b9 4332 "commit",
f098944b 4333 NULL,
6b161b31 4334 main_read,
f098944b 4335 main_draw,
586c423d 4336 main_request,
4af34daa 4337 main_grep,
d720de4b 4338 main_select,
6b161b31 4339};
2e8488b4 4340
c34d9c9f 4341
6b161b31 4342/*
10e290ee
JF
4343 * Unicode / UTF-8 handling
4344 *
4345 * NOTE: Much of the following code for dealing with unicode is derived from
4346 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4347 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4348 */
4349
4350/* I've (over)annotated a lot of code snippets because I am not entirely
4351 * confident that the approach taken by this small UTF-8 interface is correct.
4352 * --jonas */
4353
4354static inline int
4355unicode_width(unsigned long c)
4356{
4357 if (c >= 0x1100 &&
4358 (c <= 0x115f /* Hangul Jamo */
4359 || c == 0x2329
4360 || c == 0x232a
4361 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
f97f4012 4362 /* CJK ... Yi */
10e290ee
JF
4363 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4364 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4365 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4366 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4367 || (c >= 0xffe0 && c <= 0xffe6)
4368 || (c >= 0x20000 && c <= 0x2fffd)
4369 || (c >= 0x30000 && c <= 0x3fffd)))
4370 return 2;
4371
4372 return 1;
4373}
4374
4375/* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4376 * Illegal bytes are set one. */
4377static const unsigned char utf8_bytes[256] = {
4378 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,
4379 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,
4380 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,
4381 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,
4382 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,
4383 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,
4384 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,
4385 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,
4386};
4387
4388/* Decode UTF-8 multi-byte representation into a unicode character. */
4389static inline unsigned long
4390utf8_to_unicode(const char *string, size_t length)
4391{
4392 unsigned long unicode;
4393
4394 switch (length) {
4395 case 1:
4396 unicode = string[0];
4397 break;
4398 case 2:
4399 unicode = (string[0] & 0x1f) << 6;
4400 unicode += (string[1] & 0x3f);
4401 break;
4402 case 3:
4403 unicode = (string[0] & 0x0f) << 12;
4404 unicode += ((string[1] & 0x3f) << 6);
4405 unicode += (string[2] & 0x3f);
4406 break;
4407 case 4:
4408 unicode = (string[0] & 0x0f) << 18;
4409 unicode += ((string[1] & 0x3f) << 12);
4410 unicode += ((string[2] & 0x3f) << 6);
4411 unicode += (string[3] & 0x3f);
4412 break;
4413 case 5:
4414 unicode = (string[0] & 0x0f) << 24;
4415 unicode += ((string[1] & 0x3f) << 18);
4416 unicode += ((string[2] & 0x3f) << 12);
4417 unicode += ((string[3] & 0x3f) << 6);
4418 unicode += (string[4] & 0x3f);
4419 break;
68b6e0eb 4420 case 6:
10e290ee
JF
4421 unicode = (string[0] & 0x01) << 30;
4422 unicode += ((string[1] & 0x3f) << 24);
4423 unicode += ((string[2] & 0x3f) << 18);
4424 unicode += ((string[3] & 0x3f) << 12);
4425 unicode += ((string[4] & 0x3f) << 6);
4426 unicode += (string[5] & 0x3f);
4427 break;
4428 default:
4429 die("Invalid unicode length");
4430 }
4431
4432 /* Invalid characters could return the special 0xfffd value but NUL
4433 * should be just as good. */
4434 return unicode > 0xffff ? 0 : unicode;
4435}
4436
4437/* Calculates how much of string can be shown within the given maximum width
4438 * and sets trimmed parameter to non-zero value if all of string could not be
4439 * shown.
4440 *
4441 * Additionally, adds to coloffset how many many columns to move to align with
4442 * the expected position. Takes into account how multi-byte and double-width
4443 * characters will effect the cursor position.
4444 *
4445 * Returns the number of bytes to output from string to satisfy max_width. */
4446static size_t
4447utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
4448{
4449 const char *start = string;
4450 const char *end = strchr(string, '\0');
4451 size_t mbwidth = 0;
4452 size_t width = 0;
4453
4454 *trimmed = 0;
4455
4456 while (string < end) {
4457 int c = *(unsigned char *) string;
4458 unsigned char bytes = utf8_bytes[c];
4459 size_t ucwidth;
4460 unsigned long unicode;
4461
4462 if (string + bytes > end)
4463 break;
4464
4465 /* Change representation to figure out whether
4466 * it is a single- or double-width character. */
4467
4468 unicode = utf8_to_unicode(string, bytes);
4469 /* FIXME: Graceful handling of invalid unicode character. */
4470 if (!unicode)
4471 break;
4472
4473 ucwidth = unicode_width(unicode);
4474 width += ucwidth;
4475 if (width > max_width) {
4476 *trimmed = 1;
4477 break;
4478 }
4479
4480 /* The column offset collects the differences between the
4481 * number of bytes encoding a character and the number of
4482 * columns will be used for rendering said character.
4483 *
4484 * So if some character A is encoded in 2 bytes, but will be
4485 * represented on the screen using only 1 byte this will and up
4486 * adding 1 to the multi-byte column offset.
4487 *
4488 * Assumes that no double-width character can be encoding in
4489 * less than two bytes. */
4490 if (bytes > ucwidth)
4491 mbwidth += bytes - ucwidth;
4492
4493 string += bytes;
4494 }
4495
4496 *coloffset += mbwidth;
4497
4498 return string - start;
4499}
4500
4501
4502/*
6b161b31
JF
4503 * Status management
4504 */
2e8488b4 4505
8855ada4 4506/* Whether or not the curses interface has been initialized. */
68b6e0eb 4507static bool cursed = FALSE;
8855ada4 4508
6b161b31
JF
4509/* The status window is used for polling keystrokes. */
4510static WINDOW *status_win;
4a2909a7 4511
21be28fb
JF
4512static bool status_empty = TRUE;
4513
2e8488b4 4514/* Update status and title window. */
4a2909a7
JF
4515static void
4516report(const char *msg, ...)
4517{
6706b2ba 4518 struct view *view = display[current_view];
b76c2afc 4519
ab4af23e
JF
4520 if (input_mode)
4521 return;
4522
c38c64bb
JF
4523 if (!view) {
4524 char buf[SIZEOF_STR];
4525 va_list args;
4526
4527 va_start(args, msg);
4528 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4529 buf[sizeof(buf) - 1] = 0;
4530 buf[sizeof(buf) - 2] = '.';
4531 buf[sizeof(buf) - 3] = '.';
4532 buf[sizeof(buf) - 4] = '.';
4533 }
4534 va_end(args);
4535 die("%s", buf);
4536 }
4537
21be28fb 4538 if (!status_empty || *msg) {
6706b2ba 4539 va_list args;
4a2909a7 4540
6706b2ba 4541 va_start(args, msg);
4b76734f 4542
6706b2ba
JF
4543 wmove(status_win, 0, 0);
4544 if (*msg) {
4545 vwprintw(status_win, msg, args);
21be28fb 4546 status_empty = FALSE;
6706b2ba 4547 } else {
21be28fb 4548 status_empty = TRUE;
6706b2ba 4549 }
390a8262 4550 wclrtoeol(status_win);
6706b2ba 4551 wrefresh(status_win);
b801d8b2 4552
6706b2ba
JF
4553 va_end(args);
4554 }
4555
4556 update_view_title(view);
2bee3bde 4557 update_display_cursor(view);
b801d8b2
JF
4558}
4559
6b161b31
JF
4560/* Controls when nodelay should be in effect when polling user input. */
4561static void
1ba2ae4b 4562set_nonblocking_input(bool loading)
b801d8b2 4563{
6706b2ba 4564 static unsigned int loading_views;
b801d8b2 4565
6706b2ba
JF
4566 if ((loading == FALSE && loading_views-- == 1) ||
4567 (loading == TRUE && loading_views++ == 0))
1ba2ae4b 4568 nodelay(status_win, loading);
6b161b31
JF
4569}
4570
4571static void
4572init_display(void)
4573{
4574 int x, y;
b76c2afc 4575
6908bdbd
JF
4576 /* Initialize the curses library */
4577 if (isatty(STDIN_FILENO)) {
8855ada4 4578 cursed = !!initscr();
6908bdbd
JF
4579 } else {
4580 /* Leave stdin and stdout alone when acting as a pager. */
4581 FILE *io = fopen("/dev/tty", "r+");
4582
e6f60674
JF
4583 if (!io)
4584 die("Failed to open /dev/tty");
8855ada4 4585 cursed = !!newterm(NULL, io, io);
6908bdbd
JF
4586 }
4587
8855ada4
JF
4588 if (!cursed)
4589 die("Failed to initialize curses");
4590
2e8488b4
JF
4591 nonl(); /* Tell curses not to do NL->CR/NL on output */
4592 cbreak(); /* Take input chars one at a time, no wait for \n */
4593 noecho(); /* Don't echo input */
b801d8b2 4594 leaveok(stdscr, TRUE);
b801d8b2
JF
4595
4596 if (has_colors())
4597 init_colors();
4598
4599 getmaxyx(stdscr, y, x);
4600 status_win = newwin(1, 0, y - 1, 0);
4601 if (!status_win)
4602 die("Failed to create status window");
4603
4604 /* Enable keyboard mapping */
4605 keypad(status_win, TRUE);
78c70acd 4606 wbkgdset(status_win, get_line_attr(LINE_STATUS));
6b161b31
JF
4607}
4608
4af34daa 4609static char *
cb9e48c1 4610read_prompt(const char *prompt)
ef5404a4
JF
4611{
4612 enum { READING, STOP, CANCEL } status = READING;
9e21ce5c 4613 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
ef5404a4
JF
4614 int pos = 0;
4615
4616 while (status == READING) {
4617 struct view *view;
4618 int i, key;
4619
ab4af23e
JF
4620 input_mode = TRUE;
4621
699ae55b 4622 foreach_view (view, i)
ef5404a4
JF
4623 update_view(view);
4624
ab4af23e
JF
4625 input_mode = FALSE;
4626
4627 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4628 wclrtoeol(status_win);
4629
ef5404a4
JF
4630 /* Refresh, accept single keystroke of input */
4631 key = wgetch(status_win);
4632 switch (key) {
4633 case KEY_RETURN:
4634 case KEY_ENTER:
4635 case '\n':
4636 status = pos ? STOP : CANCEL;
4637 break;
4638
4639 case KEY_BACKSPACE:
4640 if (pos > 0)
4641 pos--;
4642 else
4643 status = CANCEL;
4644 break;
4645
4646 case KEY_ESC:
4647 status = CANCEL;
4648 break;
4649
4650 case ERR:
4651 break;
4652
4653 default:
4654 if (pos >= sizeof(buf)) {
4655 report("Input string too long");
9e21ce5c 4656 return NULL;
ef5404a4
JF
4657 }
4658
4659 if (isprint(key))
4660 buf[pos++] = (char) key;
4661 }
4662 }
4663
7a06ebdf
JF
4664 /* Clear the status window */
4665 status_empty = FALSE;
4666 report("");
4667
4668 if (status == CANCEL)
9e21ce5c 4669 return NULL;
ef5404a4
JF
4670
4671 buf[pos++] = 0;
ef5404a4 4672
9e21ce5c 4673 return buf;
ef5404a4 4674}
c34d9c9f
JF
4675
4676/*
4677 * Repository references
4678 */
4679
4680static struct ref *refs;
3a91b75e 4681static size_t refs_size;
c34d9c9f 4682
1307df1a
JF
4683/* Id <-> ref store */
4684static struct ref ***id_refs;
4685static size_t id_refs_size;
4686
c34d9c9f
JF
4687static struct ref **
4688get_refs(char *id)
4689{
1307df1a
JF
4690 struct ref ***tmp_id_refs;
4691 struct ref **ref_list = NULL;
4692 size_t ref_list_size = 0;
c34d9c9f
JF
4693 size_t i;
4694
1307df1a
JF
4695 for (i = 0; i < id_refs_size; i++)
4696 if (!strcmp(id, id_refs[i][0]->id))
4697 return id_refs[i];
4698
4699 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4700 if (!tmp_id_refs)
4701 return NULL;
4702
4703 id_refs = tmp_id_refs;
4704
c34d9c9f
JF
4705 for (i = 0; i < refs_size; i++) {
4706 struct ref **tmp;
4707
4708 if (strcmp(id, refs[i].id))
4709 continue;
4710
1307df1a 4711 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
c34d9c9f 4712 if (!tmp) {
1307df1a
JF
4713 if (ref_list)
4714 free(ref_list);
c34d9c9f
JF
4715 return NULL;
4716 }
4717
1307df1a
JF
4718 ref_list = tmp;
4719 if (ref_list_size > 0)
4720 ref_list[ref_list_size - 1]->next = 1;
4721 ref_list[ref_list_size] = &refs[i];
3af8774e
JF
4722
4723 /* XXX: The properties of the commit chains ensures that we can
4724 * safely modify the shared ref. The repo references will
4725 * always be similar for the same id. */
1307df1a
JF
4726 ref_list[ref_list_size]->next = 0;
4727 ref_list_size++;
c34d9c9f
JF
4728 }
4729
1307df1a
JF
4730 if (ref_list)
4731 id_refs[id_refs_size++] = ref_list;
4732
4733 return ref_list;
c34d9c9f
JF
4734}
4735
4736static int
5699e0cf 4737read_ref(char *id, size_t idlen, char *name, size_t namelen)
c34d9c9f 4738{
d0cea5f9
JF
4739 struct ref *ref;
4740 bool tag = FALSE;
e15ec88e 4741 bool remote = FALSE;
d0cea5f9 4742
8b0297ae
JF
4743 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4744 /* Commits referenced by tags has "^{}" appended. */
4745 if (name[namelen - 1] != '}')
4746 return OK;
4747
d0cea5f9
JF
4748 while (namelen > 0 && name[namelen] != '^')
4749 namelen--;
c34d9c9f 4750
d0cea5f9 4751 tag = TRUE;
8b0297ae
JF
4752 namelen -= STRING_SIZE("refs/tags/");
4753 name += STRING_SIZE("refs/tags/");
c34d9c9f 4754
e15ec88e
JF
4755 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4756 remote = TRUE;
4757 namelen -= STRING_SIZE("refs/remotes/");
4758 name += STRING_SIZE("refs/remotes/");
4759
d0cea5f9 4760 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
8b0297ae
JF
4761 namelen -= STRING_SIZE("refs/heads/");
4762 name += STRING_SIZE("refs/heads/");
c34d9c9f 4763
d0cea5f9
JF
4764 } else if (!strcmp(name, "HEAD")) {
4765 return OK;
4766 }
6706b2ba 4767
d0cea5f9
JF
4768 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4769 if (!refs)
4770 return ERR;
c34d9c9f 4771
d0cea5f9 4772 ref = &refs[refs_size++];
8b0297ae 4773 ref->name = malloc(namelen + 1);
d0cea5f9
JF
4774 if (!ref->name)
4775 return ERR;
3af8774e 4776
8b0297ae
JF
4777 strncpy(ref->name, name, namelen);
4778 ref->name[namelen] = 0;
d0cea5f9 4779 ref->tag = tag;
e15ec88e 4780 ref->remote = remote;
2463b4ea 4781 string_copy_rev(ref->id, id);
3af8774e 4782
d0cea5f9
JF
4783 return OK;
4784}
c34d9c9f 4785
d0cea5f9
JF
4786static int
4787load_refs(void)
4788{
4789 const char *cmd_env = getenv("TIG_LS_REMOTE");
4790 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
c34d9c9f 4791
4a63c884 4792 return read_properties(popen(cmd, "r"), "\t", read_ref);
d0cea5f9 4793}
c34d9c9f 4794
d0cea5f9 4795static int
5699e0cf 4796read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
d0cea5f9 4797{
22913179 4798 if (!strcmp(name, "i18n.commitencoding"))
739e81de 4799 string_ncopy(opt_encoding, value, valuelen);
c34d9c9f 4800
0cea0d43
JF
4801 if (!strcmp(name, "core.editor"))
4802 string_ncopy(opt_editor, value, valuelen);
4803
c34d9c9f
JF
4804 return OK;
4805}
4806
4670cf89 4807static int
14c778a6 4808load_repo_config(void)
4670cf89 4809{
96e58f5b 4810 return read_properties(popen(GIT_CONFIG " --list", "r"),
14c778a6 4811 "=", read_repo_config_option);
d0cea5f9
JF
4812}
4813
4814static int
5699e0cf 4815read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
91c5d983 4816{
c38c64bb 4817 if (!opt_git_dir[0]) {
810f0078 4818 string_ncopy(opt_git_dir, name, namelen);
c38c64bb
JF
4819
4820 } else if (opt_is_inside_work_tree == -1) {
4821 /* This can be 3 different values depending on the
4822 * version of git being used. If git-rev-parse does not
4823 * understand --is-inside-work-tree it will simply echo
4824 * the option else either "true" or "false" is printed.
4825 * Default to true for the unknown case. */
4826 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
4827
4828 } else {
739e81de 4829 string_ncopy(opt_cdup, name, namelen);
c38c64bb
JF
4830 }
4831
91c5d983
JF
4832 return OK;
4833}
4834
810f0078
JF
4835/* XXX: The line outputted by "--show-cdup" can be empty so the option
4836 * must be the last one! */
91c5d983
JF
4837static int
4838load_repo_info(void)
4839{
c38c64bb 4840 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
91c5d983
JF
4841 "=", read_repo_info);
4842}
4843
4844static int
4a63c884 4845read_properties(FILE *pipe, const char *separators,
5699e0cf 4846 int (*read_property)(char *, size_t, char *, size_t))
d0cea5f9 4847{
4670cf89
JF
4848 char buffer[BUFSIZ];
4849 char *name;
d0cea5f9 4850 int state = OK;
4670cf89
JF
4851
4852 if (!pipe)
4853 return ERR;
4854
d0cea5f9 4855 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4a63c884
JF
4856 char *value;
4857 size_t namelen;
4858 size_t valuelen;
4670cf89 4859
4a63c884
JF
4860 name = chomp_string(name);
4861 namelen = strcspn(name, separators);
4862
4863 if (name[namelen]) {
4864 name[namelen] = 0;
4865 value = chomp_string(name + namelen + 1);
d0cea5f9 4866 valuelen = strlen(value);
4670cf89 4867
d0cea5f9 4868 } else {
d0cea5f9
JF
4869 value = "";
4870 valuelen = 0;
4670cf89 4871 }
d0cea5f9 4872
3c3801c2 4873 state = read_property(name, namelen, value, valuelen);
4670cf89
JF
4874 }
4875
d0cea5f9
JF
4876 if (state != ERR && ferror(pipe))
4877 state = ERR;
4670cf89
JF
4878
4879 pclose(pipe);
4880
d0cea5f9 4881 return state;
4670cf89
JF
4882}
4883
d0cea5f9 4884
6b161b31
JF
4885/*
4886 * Main
4887 */
4888
b5c9e67f 4889static void __NORETURN
6b161b31
JF
4890quit(int sig)
4891{
8855ada4
JF
4892 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4893 if (cursed)
4894 endwin();
6b161b31
JF
4895 exit(0);
4896}
4897
c6704a4e
JF
4898static void __NORETURN
4899die(const char *err, ...)
6b161b31
JF
4900{
4901 va_list args;
4902
4903 endwin();
4904
4905 va_start(args, err);
4906 fputs("tig: ", stderr);
4907 vfprintf(stderr, err, args);
4908 fputs("\n", stderr);
4909 va_end(args);
4910
4911 exit(1);
4912}
4913
4914int
4915main(int argc, char *argv[])
4916{
1ba2ae4b 4917 struct view *view;
6b161b31 4918 enum request request;
1ba2ae4b 4919 size_t i;
6b161b31
JF
4920
4921 signal(SIGINT, quit);
4922
6b68fd24 4923 if (setlocale(LC_ALL, "")) {
739e81de
JF
4924 char *codeset = nl_langinfo(CODESET);
4925
4926 string_ncopy(opt_codeset, codeset, strlen(codeset));
6b68fd24
JF
4927 }
4928
e0f50df0
JF
4929 if (load_repo_info() == ERR)
4930 die("Failed to load repo info.");
4931
660e09ad
JF
4932 if (load_options() == ERR)
4933 die("Failed to load user config.");
4934
4935 /* Load the repo config file so options can be overwritten from
739e81de 4936 * the command line. */
14c778a6 4937 if (load_repo_config() == ERR)
afdc35b3 4938 die("Failed to load repo config.");
91c5d983 4939
8855ada4 4940 if (!parse_options(argc, argv))
6b161b31
JF
4941 return 0;
4942
58a5e4ea
JF
4943 /* Require a git repository unless when running in pager mode. */
4944 if (!opt_git_dir[0])
4945 die("Not a git repository");
4946
6b68fd24
JF
4947 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4948 opt_iconv = iconv_open(opt_codeset, opt_encoding);
20f4b4a3 4949 if (opt_iconv == ICONV_NONE)
6b68fd24
JF
4950 die("Failed to initialize character set conversion");
4951 }
4952
c34d9c9f
JF
4953 if (load_refs() == ERR)
4954 die("Failed to load refs.");
4955
1ba2ae4b
JF
4956 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4957 view->cmd_env = getenv(view->cmd_env);
4958
6b161b31
JF
4959 request = opt_request;
4960
4961 init_display();
b801d8b2
JF
4962
4963 while (view_driver(display[current_view], request)) {
6b161b31 4964 int key;
b801d8b2
JF
4965 int i;
4966
699ae55b 4967 foreach_view (view, i)
6b161b31 4968 update_view(view);
b801d8b2
JF
4969
4970 /* Refresh, accept single keystroke of input */
6b161b31 4971 key = wgetch(status_win);
04e2b7b2 4972
cf4d82e6
JF
4973 /* wgetch() with nodelay() enabled returns ERR when there's no
4974 * input. */
4975 if (key == ERR) {
4976 request = REQ_NONE;
8b534a13 4977 continue;
cf4d82e6 4978 }
04e2b7b2
JF
4979
4980 request = get_keybinding(display[current_view]->keymap, key);
03a93dbb 4981
6706b2ba 4982 /* Some low-level request handling. This keeps access to
fac7db6c
JF
4983 * status_win restricted. */
4984 switch (request) {
4985 case REQ_PROMPT:
9e21ce5c
JF
4986 {
4987 char *cmd = read_prompt(":");
4988
4989 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4990 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4991 opt_request = REQ_VIEW_DIFF;
4992 } else {
4993 opt_request = REQ_VIEW_PAGER;
4994 }
4995 break;
4996 }
fac7db6c 4997
1d754561 4998 request = REQ_NONE;
9e21ce5c
JF
4999 break;
5000 }
4af34daa
JF
5001 case REQ_SEARCH:
5002 case REQ_SEARCH_BACK:
5003 {
5004 const char *prompt = request == REQ_SEARCH
5005 ? "/" : "?";
5006 char *search = read_prompt(prompt);
5007
5008 if (search)
739e81de 5009 string_ncopy(opt_search, search, strlen(search));
4af34daa
JF
5010 else
5011 request = REQ_NONE;
5012 break;
5013 }
fac7db6c
JF
5014 case REQ_SCREEN_RESIZE:
5015 {
5016 int height, width;
5017
5018 getmaxyx(stdscr, height, width);
5019
5020 /* Resize the status view and let the view driver take
5021 * care of resizing the displayed views. */
5022 wresize(status_win, 1, width);
5023 mvwin(status_win, height - 1, 0);
5024 wrefresh(status_win);
5025 break;
5026 }
5027 default:
5028 break;
03a93dbb 5029 }
b801d8b2
JF
5030 }
5031
5032 quit(0);
5033
5034 return 0;
5035}