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