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