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