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