Correct error checking
[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. */
701e4f5d 900 bool (*read)(struct view *view, char *data);
fe7233c3
JF
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
b801d8b2
JF
1370 if (linelen)
1371 line[linelen - 1] = 0;
1372
701e4f5d 1373 if (!view->ops->read(view, line))
b801d8b2 1374 goto alloc_error;
fd85fef1
JF
1375
1376 if (lines-- == 1)
1377 break;
b801d8b2
JF
1378 }
1379
8855ada4
JF
1380 {
1381 int digits;
1382
1383 lines = view->lines;
1384 for (digits = 0; lines; digits++)
1385 lines /= 10;
1386
1387 /* Keep the displayed view in sync with line number scaling. */
1388 if (digits != view->digits) {
1389 view->digits = digits;
1390 redraw_from = 0;
1391 }
1392 }
1393
82e78006
JF
1394 if (redraw_from >= 0) {
1395 /* If this is an incremental update, redraw the previous line
a28bcc22
JF
1396 * since for commits some members could have changed when
1397 * loading the main view. */
82e78006
JF
1398 if (redraw_from > 0)
1399 redraw_from--;
1400
1401 /* Incrementally draw avoids flickering. */
1402 redraw_view_from(view, redraw_from);
4c6fabc2 1403 }
b801d8b2 1404
eb98559e
JF
1405 /* Update the title _after_ the redraw so that if the redraw picks up a
1406 * commit reference in view->ref it'll be available here. */
1407 update_view_title(view);
1408
b801d8b2 1409 if (ferror(view->pipe)) {
03a93dbb 1410 report("Failed to read: %s", strerror(errno));
b801d8b2
JF
1411 goto end;
1412
1413 } else if (feof(view->pipe)) {
f97f4012 1414 report("");
b801d8b2
JF
1415 goto end;
1416 }
1417
1418 return TRUE;
1419
1420alloc_error:
2e8488b4 1421 report("Allocation failure");
b801d8b2
JF
1422
1423end:
1424 end_update(view);
1425 return FALSE;
1426}
1427
49f2b43f
JF
1428enum open_flags {
1429 OPEN_DEFAULT = 0, /* Use default view switching. */
1430 OPEN_SPLIT = 1, /* Split current view. */
1431 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1432 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1433};
1434
6b161b31 1435static void
49f2b43f 1436open_view(struct view *prev, enum request request, enum open_flags flags)
b801d8b2 1437{
49f2b43f
JF
1438 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1439 bool split = !!(flags & OPEN_SPLIT);
1440 bool reload = !!(flags & OPEN_RELOAD);
a28bcc22 1441 struct view *view = VIEW(request);
9f41488f 1442 int nviews = displayed_views();
6e950a52 1443 struct view *base_view = display[0];
b801d8b2 1444
49f2b43f 1445 if (view == prev && nviews == 1 && !reload) {
6b161b31
JF
1446 report("Already in %s view", view->name);
1447 return;
1448 }
b801d8b2 1449
8855ada4 1450 if ((reload || strcmp(view->vid, view->id)) &&
03a93dbb 1451 !begin_update(view)) {
6b161b31
JF
1452 report("Failed to load %s view", view->name);
1453 return;
1454 }
a28bcc22 1455
6b161b31 1456 if (split) {
8d741c06 1457 display[1] = view;
6b161b31 1458 if (!backgrounded)
8d741c06 1459 current_view = 1;
6b161b31
JF
1460 } else {
1461 /* Maximize the current view. */
1462 memset(display, 0, sizeof(display));
1463 current_view = 0;
1464 display[current_view] = view;
a28bcc22 1465 }
b801d8b2 1466
6e950a52
JF
1467 /* Resize the view when switching between split- and full-screen,
1468 * or when switching between two different full-screen views. */
1469 if (nviews != displayed_views() ||
1470 (nviews == 1 && base_view != display[0]))
a006db63 1471 resize_display();
b801d8b2 1472
a8891802 1473 if (split && prev->lineno - prev->offset >= prev->height) {
03a93dbb 1474 /* Take the title line into account. */
eb98559e 1475 int lines = prev->lineno - prev->offset - prev->height + 1;
03a93dbb
JF
1476
1477 /* Scroll the view that was split if the current line is
1478 * outside the new limited view. */
b3a54cba 1479 do_scroll_view(prev, lines, TRUE);
03a93dbb
JF
1480 }
1481
6b161b31 1482 if (prev && view != prev) {
9b995f0c 1483 if (split && !backgrounded) {
f0b3ab80
JF
1484 /* "Blur" the previous view. */
1485 update_view_title(prev);
9f396969 1486 }
f0b3ab80 1487
f6da0b66 1488 view->parent = prev;
b801d8b2
JF
1489 }
1490
24b5b3e0
JF
1491 if (view == VIEW(REQ_VIEW_HELP))
1492 load_help_page();
1493
9f396969 1494 if (view->pipe && view->lines == 0) {
03a93dbb
JF
1495 /* Clear the old view and let the incremental updating refill
1496 * the screen. */
1497 wclear(view->win);
f97f4012 1498 report("");
03a93dbb
JF
1499 } else {
1500 redraw_view(view);
24b5b3e0 1501 report("");
03a93dbb 1502 }
6706b2ba
JF
1503
1504 /* If the view is backgrounded the above calls to report()
1505 * won't redraw the view title. */
1506 if (backgrounded)
1507 update_view_title(view);
b801d8b2
JF
1508}
1509
1510
6b161b31
JF
1511/*
1512 * User request switch noodle
1513 */
1514
b801d8b2 1515static int
6b161b31 1516view_driver(struct view *view, enum request request)
b801d8b2 1517{
b801d8b2
JF
1518 int i;
1519
1520 switch (request) {
4a2909a7
JF
1521 case REQ_MOVE_UP:
1522 case REQ_MOVE_DOWN:
1523 case REQ_MOVE_PAGE_UP:
1524 case REQ_MOVE_PAGE_DOWN:
1525 case REQ_MOVE_FIRST_LINE:
1526 case REQ_MOVE_LAST_LINE:
b3a54cba 1527 move_view(view, request, TRUE);
fd85fef1
JF
1528 break;
1529
4a2909a7
JF
1530 case REQ_SCROLL_LINE_DOWN:
1531 case REQ_SCROLL_LINE_UP:
1532 case REQ_SCROLL_PAGE_DOWN:
1533 case REQ_SCROLL_PAGE_UP:
a28bcc22 1534 scroll_view(view, request);
b801d8b2
JF
1535 break;
1536
4a2909a7 1537 case REQ_VIEW_MAIN:
4a2909a7 1538 case REQ_VIEW_DIFF:
2e8488b4
JF
1539 case REQ_VIEW_LOG:
1540 case REQ_VIEW_HELP:
6908bdbd 1541 case REQ_VIEW_PAGER:
49f2b43f 1542 open_view(view, request, OPEN_DEFAULT);
b801d8b2
JF
1543 break;
1544
b3a54cba
JF
1545 case REQ_NEXT:
1546 case REQ_PREVIOUS:
1547 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1548
1549 if (view == VIEW(REQ_VIEW_DIFF) &&
1550 view->parent == VIEW(REQ_VIEW_MAIN)) {
f0b3ab80 1551 bool redraw = display[1] == view;
b3a54cba
JF
1552
1553 view = view->parent;
1554 move_view(view, request, redraw);
f0b3ab80
JF
1555 if (redraw)
1556 update_view_title(view);
b3a54cba
JF
1557 } else {
1558 move_view(view, request, TRUE);
1559 break;
1560 }
6706b2ba
JF
1561 /* Fall-through */
1562
6b161b31 1563 case REQ_ENTER:
6908bdbd
JF
1564 if (!view->lines) {
1565 report("Nothing to enter");
1566 break;
1567 }
fe7233c3 1568 return view->ops->enter(view, &view->line[view->lineno]);
6b161b31 1569
03a93dbb
JF
1570 case REQ_VIEW_NEXT:
1571 {
9f41488f 1572 int nviews = displayed_views();
03a93dbb
JF
1573 int next_view = (current_view + 1) % nviews;
1574
1575 if (next_view == current_view) {
1576 report("Only one view is displayed");
1577 break;
1578 }
1579
1580 current_view = next_view;
1581 /* Blur out the title of the previous view. */
1582 update_view_title(view);
6734f6b9 1583 report("");
03a93dbb
JF
1584 break;
1585 }
24b5b3e0 1586 case REQ_TOGGLE_LINENO:
b76c2afc 1587 opt_line_number = !opt_line_number;
20bb5e18 1588 redraw_display();
b801d8b2
JF
1589 break;
1590
03a93dbb 1591 case REQ_PROMPT:
8855ada4 1592 /* Always reload^Wrerun commands from the prompt. */
49f2b43f 1593 open_view(view, opt_request, OPEN_RELOAD);
03a93dbb
JF
1594 break;
1595
4a2909a7 1596 case REQ_STOP_LOADING:
59a45d3a
JF
1597 for (i = 0; i < ARRAY_SIZE(views); i++) {
1598 view = &views[i];
2e8488b4 1599 if (view->pipe)
6a7bb912 1600 report("Stopped loading the %s view", view->name),
03a93dbb
JF
1601 end_update(view);
1602 }
b801d8b2
JF
1603 break;
1604
4a2909a7 1605 case REQ_SHOW_VERSION:
6cb291b7 1606 report("%s (built %s)", VERSION, __DATE__);
b801d8b2
JF
1607 return TRUE;
1608
fac7db6c
JF
1609 case REQ_SCREEN_RESIZE:
1610 resize_display();
1611 /* Fall-through */
4a2909a7 1612 case REQ_SCREEN_REDRAW:
20bb5e18 1613 redraw_display();
4a2909a7
JF
1614 break;
1615
1616 case REQ_SCREEN_UPDATE:
b801d8b2
JF
1617 doupdate();
1618 return TRUE;
1619
4f9b667a 1620 case REQ_VIEW_CLOSE:
2fcf5401
JF
1621 /* XXX: Mark closed views by letting view->parent point to the
1622 * view itself. Parents to closed view should never be
1623 * followed. */
1624 if (view->parent &&
1625 view->parent->parent != view->parent) {
4f9b667a
JF
1626 memset(display, 0, sizeof(display));
1627 current_view = 0;
f6da0b66 1628 display[current_view] = view->parent;
2fcf5401 1629 view->parent = view;
4f9b667a
JF
1630 resize_display();
1631 redraw_display();
1632 break;
1633 }
1634 /* Fall-through */
b801d8b2
JF
1635 case REQ_QUIT:
1636 return FALSE;
1637
1638 default:
2e8488b4 1639 /* An unknown key will show most commonly used commands. */
468876c9 1640 report("Unknown key, press 'h' for help");
b801d8b2
JF
1641 return TRUE;
1642 }
1643
1644 return TRUE;
1645}
1646
1647
1648/*
ff26aa29 1649 * Pager backend
b801d8b2
JF
1650 */
1651
6b161b31 1652static bool
fe7233c3 1653pager_draw(struct view *view, struct line *line, unsigned int lineno)
b801d8b2 1654{
fe7233c3
JF
1655 char *text = line->data;
1656 enum line_type type = line->type;
1657 int textlen = strlen(text);
78c70acd 1658 int attr;
b801d8b2 1659
6706b2ba
JF
1660 wmove(view->win, lineno, 0);
1661
fd85fef1 1662 if (view->offset + lineno == view->lineno) {
8855ada4 1663 if (type == LINE_COMMIT) {
fe7233c3 1664 string_copy(view->ref, text + 7);
03a93dbb
JF
1665 string_copy(ref_commit, view->ref);
1666 }
8855ada4 1667
78c70acd 1668 type = LINE_CURSOR;
6706b2ba 1669 wchgat(view->win, -1, 0, type, NULL);
fd85fef1
JF
1670 }
1671
78c70acd 1672 attr = get_line_attr(type);
b801d8b2 1673 wattrset(view->win, attr);
b76c2afc 1674
6706b2ba
JF
1675 if (opt_line_number || opt_tab_size < TABSIZE) {
1676 static char spaces[] = " ";
1677 int col_offset = 0, col = 0;
1678
1679 if (opt_line_number) {
1680 unsigned long real_lineno = view->offset + lineno + 1;
82e78006 1681
6706b2ba
JF
1682 if (real_lineno == 1 ||
1683 (real_lineno % opt_num_interval) == 0) {
1684 wprintw(view->win, "%.*d", view->digits, real_lineno);
8855ada4 1685
6706b2ba
JF
1686 } else {
1687 waddnstr(view->win, spaces,
1688 MIN(view->digits, STRING_SIZE(spaces)));
1689 }
1690 waddstr(view->win, ": ");
1691 col_offset = view->digits + 2;
1692 }
8855ada4 1693
fe7233c3 1694 while (text && col_offset + col < view->width) {
6706b2ba 1695 int cols_max = view->width - col_offset - col;
fe7233c3 1696 char *pos = text;
6706b2ba 1697 int cols;
4c6fabc2 1698
fe7233c3
JF
1699 if (*text == '\t') {
1700 text++;
6706b2ba 1701 assert(sizeof(spaces) > TABSIZE);
fe7233c3 1702 pos = spaces;
6706b2ba 1703 cols = opt_tab_size - (col % opt_tab_size);
82e78006 1704
b76c2afc 1705 } else {
fe7233c3
JF
1706 text = strchr(text, '\t');
1707 cols = line ? text - pos : strlen(pos);
b76c2afc 1708 }
6706b2ba 1709
fe7233c3 1710 waddnstr(view->win, pos, MIN(cols, cols_max));
6706b2ba 1711 col += cols;
b76c2afc 1712 }
b76c2afc
JF
1713
1714 } else {
6706b2ba 1715 int col = 0, pos = 0;
b801d8b2 1716
fe7233c3
JF
1717 for (; pos < textlen && col < view->width; pos++, col++)
1718 if (text[pos] == '\t')
6706b2ba
JF
1719 col += TABSIZE - (col % TABSIZE) - 1;
1720
fe7233c3 1721 waddnstr(view->win, text, pos);
6706b2ba 1722 }
2e8488b4 1723
b801d8b2
JF
1724 return TRUE;
1725}
1726
7b99a34c
JF
1727static void
1728add_pager_refs(struct view *view, struct line *line)
1729{
1730 char buf[1024];
1731 char *data = line->data;
1732 struct ref **refs;
1733 int bufpos = 0, refpos = 0;
1734 const char *sep = "Refs: ";
1735
1736 assert(line->type == LINE_COMMIT);
1737
1738 refs = get_refs(data + STRING_SIZE("commit "));
1739 if (!refs)
1740 return;
1741
1742 do {
cc2d1364
JF
1743 struct ref *ref = refs[refpos];
1744 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
7b99a34c 1745
cc2d1364
JF
1746 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1747 return;
7b99a34c
JF
1748 sep = ", ";
1749 } while (refs[refpos++]->next);
1750
cc2d1364 1751 if (!realloc_lines(view, view->line_size + 1))
7b99a34c
JF
1752 return;
1753
1754 line = &view->line[view->lines];
1755 line->data = strdup(buf);
1756 if (!line->data)
1757 return;
1758
1759 line->type = LINE_PP_REFS;
1760 view->lines++;
1761}
1762
6b161b31 1763static bool
701e4f5d 1764pager_read(struct view *view, char *data)
22f66b0a 1765{
7b99a34c 1766 struct line *line = &view->line[view->lines];
22f66b0a 1767
7b99a34c
JF
1768 line->data = strdup(data);
1769 if (!line->data)
1770 return FALSE;
fe7233c3 1771
7b99a34c 1772 line->type = get_line_type(line->data);
22f66b0a 1773 view->lines++;
7b99a34c
JF
1774
1775 if (line->type == LINE_COMMIT &&
1776 (view == VIEW(REQ_VIEW_DIFF) ||
1777 view == VIEW(REQ_VIEW_LOG)))
1778 add_pager_refs(view, line);
1779
22f66b0a
JF
1780 return TRUE;
1781}
1782
6b161b31 1783static bool
fe7233c3 1784pager_enter(struct view *view, struct line *line)
6b161b31 1785{
91e8e277 1786 int split = 0;
6b161b31 1787
9fbbd28f
JF
1788 if (line->type == LINE_COMMIT &&
1789 (view == VIEW(REQ_VIEW_LOG) ||
1790 view == VIEW(REQ_VIEW_PAGER))) {
91e8e277
JF
1791 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1792 split = 1;
67e48ac5
JF
1793 }
1794
91e8e277
JF
1795 /* Always scroll the view even if it was split. That way
1796 * you can use Enter to scroll through the log view and
1797 * split open each commit diff. */
1798 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1799
1800 /* FIXME: A minor workaround. Scrolling the view will call report("")
9d82d824
JF
1801 * but if we are scrolling a non-current view this won't properly
1802 * update the view title. */
91e8e277
JF
1803 if (split)
1804 update_view_title(view);
6b161b31
JF
1805
1806 return TRUE;
1807}
1808
6b161b31 1809static struct view_ops pager_ops = {
6734f6b9 1810 "line",
6b161b31
JF
1811 pager_draw,
1812 pager_read,
1813 pager_enter,
1814};
1815
80ce96ea 1816
ff26aa29
JF
1817/*
1818 * Main view backend
1819 */
1820
1821struct commit {
1822 char id[41]; /* SHA1 ID. */
1823 char title[75]; /* The first line of the commit message. */
1824 char author[75]; /* The author of the commit. */
1825 struct tm time; /* Date from the author ident. */
1826 struct ref **refs; /* Repository references; tags & branch heads. */
1827};
c34d9c9f 1828
6b161b31 1829static bool
fe7233c3 1830main_draw(struct view *view, struct line *line, unsigned int lineno)
22f66b0a 1831{
2e8488b4 1832 char buf[DATE_COLS + 1];
fe7233c3 1833 struct commit *commit = line->data;
78c70acd 1834 enum line_type type;
6706b2ba 1835 int col = 0;
b76c2afc 1836 size_t timelen;
10e290ee 1837 size_t authorlen;
9989bf60 1838 int trimmed = 1;
22f66b0a 1839
4c6fabc2
JF
1840 if (!*commit->author)
1841 return FALSE;
22f66b0a 1842
6706b2ba
JF
1843 wmove(view->win, lineno, col);
1844
22f66b0a 1845 if (view->offset + lineno == view->lineno) {
03a93dbb 1846 string_copy(view->ref, commit->id);
49f2b43f 1847 string_copy(ref_commit, view->ref);
78c70acd 1848 type = LINE_CURSOR;
6706b2ba
JF
1849 wattrset(view->win, get_line_attr(type));
1850 wchgat(view->win, -1, 0, type, NULL);
1851
78c70acd 1852 } else {
b76c2afc 1853 type = LINE_MAIN_COMMIT;
6706b2ba 1854 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
b76c2afc
JF
1855 }
1856
4c6fabc2 1857 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
b76c2afc 1858 waddnstr(view->win, buf, timelen);
4c6fabc2 1859 waddstr(view->win, " ");
b76c2afc 1860
6706b2ba
JF
1861 col += DATE_COLS;
1862 wmove(view->win, lineno, col);
1863 if (type != LINE_CURSOR)
1864 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
b76c2afc 1865
9989bf60
JF
1866 if (opt_utf8) {
1867 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
1868 } else {
1869 authorlen = strlen(commit->author);
1870 if (authorlen > AUTHOR_COLS - 2) {
1871 authorlen = AUTHOR_COLS - 2;
1872 trimmed = 1;
1873 }
1874 }
10e290ee
JF
1875
1876 if (trimmed) {
1877 waddnstr(view->win, commit->author, authorlen);
6706b2ba
JF
1878 if (type != LINE_CURSOR)
1879 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
b76c2afc
JF
1880 waddch(view->win, '~');
1881 } else {
1882 waddstr(view->win, commit->author);
22f66b0a
JF
1883 }
1884
10e290ee 1885 col += AUTHOR_COLS;
6706b2ba
JF
1886 if (type != LINE_CURSOR)
1887 wattrset(view->win, A_NORMAL);
1888
1889 mvwaddch(view->win, lineno, col, ACS_LTEE);
1890 wmove(view->win, lineno, col + 2);
1891 col += 2;
c34d9c9f
JF
1892
1893 if (commit->refs) {
1894 size_t i = 0;
1895
1896 do {
6706b2ba
JF
1897 if (type == LINE_CURSOR)
1898 ;
1899 else if (commit->refs[i]->tag)
c34d9c9f
JF
1900 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
1901 else
1902 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
1903 waddstr(view->win, "[");
1904 waddstr(view->win, commit->refs[i]->name);
1905 waddstr(view->win, "]");
6706b2ba
JF
1906 if (type != LINE_CURSOR)
1907 wattrset(view->win, A_NORMAL);
c34d9c9f 1908 waddstr(view->win, " ");
6706b2ba 1909 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
c34d9c9f
JF
1910 } while (commit->refs[i++]->next);
1911 }
1912
6706b2ba
JF
1913 if (type != LINE_CURSOR)
1914 wattrset(view->win, get_line_attr(type));
1915
1916 {
1917 int titlelen = strlen(commit->title);
1918
1919 if (col + titlelen > view->width)
1920 titlelen = view->width - col;
1921
1922 waddnstr(view->win, commit->title, titlelen);
1923 }
22f66b0a
JF
1924
1925 return TRUE;
1926}
1927
4c6fabc2 1928/* Reads git log --pretty=raw output and parses it into the commit struct. */
6b161b31 1929static bool
701e4f5d 1930main_read(struct view *view, char *line)
22f66b0a 1931{
78c70acd 1932 enum line_type type = get_line_type(line);
701e4f5d
JF
1933 struct commit *commit = view->lines
1934 ? view->line[view->lines - 1].data : NULL;
22f66b0a 1935
78c70acd
JF
1936 switch (type) {
1937 case LINE_COMMIT:
22f66b0a
JF
1938 commit = calloc(1, sizeof(struct commit));
1939 if (!commit)
1940 return FALSE;
1941
4c6fabc2 1942 line += STRING_SIZE("commit ");
b76c2afc 1943
fe7233c3 1944 view->line[view->lines++].data = commit;
82e78006 1945 string_copy(commit->id, line);
c34d9c9f 1946 commit->refs = get_refs(commit->id);
78c70acd 1947 break;
22f66b0a 1948
8855ada4 1949 case LINE_AUTHOR:
b76c2afc 1950 {
4c6fabc2 1951 char *ident = line + STRING_SIZE("author ");
b76c2afc
JF
1952 char *end = strchr(ident, '<');
1953
701e4f5d 1954 if (!commit)
fe7233c3
JF
1955 break;
1956
b76c2afc
JF
1957 if (end) {
1958 for (; end > ident && isspace(end[-1]); end--) ;
1959 *end = 0;
1960 }
1961
82e78006 1962 string_copy(commit->author, ident);
b76c2afc 1963
4c6fabc2 1964 /* Parse epoch and timezone */
b76c2afc
JF
1965 if (end) {
1966 char *secs = strchr(end + 1, '>');
1967 char *zone;
1968 time_t time;
1969
1970 if (!secs || secs[1] != ' ')
1971 break;
1972
1973 secs += 2;
1974 time = (time_t) atol(secs);
1975 zone = strchr(secs, ' ');
4c6fabc2 1976 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
b76c2afc
JF
1977 long tz;
1978
1979 zone++;
1980 tz = ('0' - zone[1]) * 60 * 60 * 10;
1981 tz += ('0' - zone[2]) * 60 * 60;
1982 tz += ('0' - zone[3]) * 60;
1983 tz += ('0' - zone[4]) * 60;
1984
1985 if (zone[0] == '-')
1986 tz = -tz;
1987
1988 time -= tz;
1989 }
1990 gmtime_r(&time, &commit->time);
1991 }
1992 break;
1993 }
78c70acd 1994 default:
701e4f5d 1995 if (!commit)
2e8488b4
JF
1996 break;
1997
1998 /* Fill in the commit title if it has not already been set. */
2e8488b4
JF
1999 if (commit->title[0])
2000 break;
2001
2002 /* Require titles to start with a non-space character at the
2003 * offset used by git log. */
eb98559e
JF
2004 /* FIXME: More gracefull handling of titles; append "..." to
2005 * shortened titles, etc. */
2e8488b4 2006 if (strncmp(line, " ", 4) ||
eb98559e 2007 isspace(line[4]))
82e78006
JF
2008 break;
2009
2010 string_copy(commit->title, line + 4);
22f66b0a
JF
2011 }
2012
2013 return TRUE;
2014}
2015
6b161b31 2016static bool
fe7233c3 2017main_enter(struct view *view, struct line *line)
b801d8b2 2018{
b3a54cba
JF
2019 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2020
2021 open_view(view, REQ_VIEW_DIFF, flags);
6b161b31 2022 return TRUE;
b801d8b2
JF
2023}
2024
6b161b31 2025static struct view_ops main_ops = {
6734f6b9 2026 "commit",
6b161b31
JF
2027 main_draw,
2028 main_read,
2029 main_enter,
2030};
2e8488b4 2031
c34d9c9f 2032
d839253b
JF
2033/*
2034 * Keys
2035 */
468876c9
JF
2036
2037struct keymap {
2038 int alias;
2039 int request;
2040};
2041
3a91b75e 2042static struct keymap keymap[] = {
d839253b 2043 /* View switching */
b3a54cba
JF
2044 { 'm', REQ_VIEW_MAIN },
2045 { 'd', REQ_VIEW_DIFF },
2046 { 'l', REQ_VIEW_LOG },
2047 { 'p', REQ_VIEW_PAGER },
2048 { 'h', REQ_VIEW_HELP },
24b5b3e0 2049 { '?', REQ_VIEW_HELP },
b3a54cba 2050
d839253b 2051 /* View manipulation */
4f9b667a 2052 { 'q', REQ_VIEW_CLOSE },
468876c9
JF
2053 { KEY_TAB, REQ_VIEW_NEXT },
2054 { KEY_RETURN, REQ_ENTER },
b3a54cba
JF
2055 { KEY_UP, REQ_PREVIOUS },
2056 { KEY_DOWN, REQ_NEXT },
468876c9 2057
d839253b 2058 /* Cursor navigation */
b3a54cba
JF
2059 { 'k', REQ_MOVE_UP },
2060 { 'j', REQ_MOVE_DOWN },
468876c9
JF
2061 { KEY_HOME, REQ_MOVE_FIRST_LINE },
2062 { KEY_END, REQ_MOVE_LAST_LINE },
2063 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
c622eefa 2064 { ' ', REQ_MOVE_PAGE_DOWN },
468876c9 2065 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
c622eefa 2066 { 'b', REQ_MOVE_PAGE_UP },
0df811cb 2067 { '-', REQ_MOVE_PAGE_UP },
468876c9 2068
d839253b 2069 /* Scrolling */
468876c9
JF
2070 { KEY_IC, REQ_SCROLL_LINE_UP },
2071 { KEY_DC, REQ_SCROLL_LINE_DOWN },
2072 { 'w', REQ_SCROLL_PAGE_UP },
2073 { 's', REQ_SCROLL_PAGE_DOWN },
2074
d839253b 2075 /* Misc */
d9d1c722 2076 { 'Q', REQ_QUIT },
468876c9
JF
2077 { 'z', REQ_STOP_LOADING },
2078 { 'v', REQ_SHOW_VERSION },
2079 { 'r', REQ_SCREEN_REDRAW },
24b5b3e0 2080 { 'n', REQ_TOGGLE_LINENO },
468876c9
JF
2081 { ':', REQ_PROMPT },
2082
2083 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
2084 { ERR, REQ_SCREEN_UPDATE },
2085
2086 /* Use the ncurses SIGWINCH handler. */
2087 { KEY_RESIZE, REQ_SCREEN_RESIZE },
2088};
2089
2090static enum request
2091get_request(int key)
2092{
2093 int i;
2094
2095 for (i = 0; i < ARRAY_SIZE(keymap); i++)
2096 if (keymap[i].alias == key)
2097 return keymap[i].request;
2098
2099 return (enum request) key;
2100}
2101
24b5b3e0
JF
2102struct key {
2103 char *name;
2104 int value;
2105};
2106
2107static struct key key_table[] = {
2108 { "Enter", KEY_RETURN },
2109 { "Space", ' ' },
2110 { "Backspace", KEY_BACKSPACE },
2111 { "Tab", KEY_TAB },
2112 { "Escape", KEY_ESC },
2113 { "Left", KEY_LEFT },
2114 { "Right", KEY_RIGHT },
2115 { "Up", KEY_UP },
2116 { "Down", KEY_DOWN },
2117 { "Insert", KEY_IC },
2118 { "Delete", KEY_DC },
2119 { "Home", KEY_HOME },
2120 { "End", KEY_END },
2121 { "PageUp", KEY_PPAGE },
2122 { "PageDown", KEY_NPAGE },
2123 { "F1", KEY_F(1) },
2124 { "F2", KEY_F(2) },
2125 { "F3", KEY_F(3) },
2126 { "F4", KEY_F(4) },
2127 { "F5", KEY_F(5) },
2128 { "F6", KEY_F(6) },
2129 { "F7", KEY_F(7) },
2130 { "F8", KEY_F(8) },
2131 { "F9", KEY_F(9) },
2132 { "F10", KEY_F(10) },
2133 { "F11", KEY_F(11) },
2134 { "F12", KEY_F(12) },
2135};
2136
2137static char *
2138get_key(enum request request)
2139{
2140 static char buf[BUFSIZ];
2141 static char key_char[] = "'X'";
2142 int pos = 0;
2143 char *sep = " ";
2144 int i;
2145
2146 buf[pos] = 0;
2147
2148 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
2149 char *seq = NULL;
2150 int key;
2151
2152 if (keymap[i].request != request)
2153 continue;
2154
2155 for (key = 0; key < ARRAY_SIZE(key_table); key++)
2156 if (key_table[key].value == keymap[i].alias)
2157 seq = key_table[key].name;
2158
2159 if (seq == NULL &&
2160 keymap[i].alias < 127 &&
2161 isprint(keymap[i].alias)) {
2162 key_char[1] = (char) keymap[i].alias;
2163 seq = key_char;
2164 }
2165
2166 if (!seq)
2167 seq = "'?'";
2168
cc2d1364 2169 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
24b5b3e0
JF
2170 return "Too many keybindings!";
2171 sep = ", ";
2172 }
2173
2174 return buf;
2175}
2176
2177static void load_help_page(void)
2178{
2179 char buf[BUFSIZ];
2180 struct view *view = VIEW(REQ_VIEW_HELP);
2181 int lines = ARRAY_SIZE(req_info) + 2;
2182 int i;
2183
2184 if (view->lines > 0)
2185 return;
2186
2187 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2188 if (!req_info[i].request)
2189 lines++;
2190
2191 view->line = calloc(lines, sizeof(*view->line));
2192 if (!view->line) {
2193 report("Allocation failure");
2194 return;
2195 }
2196
701e4f5d 2197 pager_read(view, "Quick reference for tig keybindings:");
24b5b3e0
JF
2198
2199 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2200 char *key;
2201
2202 if (!req_info[i].request) {
701e4f5d
JF
2203 pager_read(view, "");
2204 pager_read(view, req_info[i].help);
24b5b3e0
JF
2205 continue;
2206 }
2207
2208 key = get_key(req_info[i].request);
fff1780f 2209 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
24b5b3e0
JF
2210 continue;
2211
701e4f5d 2212 pager_read(view, buf);
24b5b3e0
JF
2213 }
2214}
2215
468876c9 2216
6b161b31 2217/*
10e290ee
JF
2218 * Unicode / UTF-8 handling
2219 *
2220 * NOTE: Much of the following code for dealing with unicode is derived from
2221 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
2222 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
2223 */
2224
2225/* I've (over)annotated a lot of code snippets because I am not entirely
2226 * confident that the approach taken by this small UTF-8 interface is correct.
2227 * --jonas */
2228
2229static inline int
2230unicode_width(unsigned long c)
2231{
2232 if (c >= 0x1100 &&
2233 (c <= 0x115f /* Hangul Jamo */
2234 || c == 0x2329
2235 || c == 0x232a
2236 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
f97f4012 2237 /* CJK ... Yi */
10e290ee
JF
2238 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
2239 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
2240 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
2241 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
2242 || (c >= 0xffe0 && c <= 0xffe6)
2243 || (c >= 0x20000 && c <= 0x2fffd)
2244 || (c >= 0x30000 && c <= 0x3fffd)))
2245 return 2;
2246
2247 return 1;
2248}
2249
2250/* Number of bytes used for encoding a UTF-8 character indexed by first byte.
2251 * Illegal bytes are set one. */
2252static const unsigned char utf8_bytes[256] = {
2253 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,
2254 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,
2255 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,
2256 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,
2257 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,
2258 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,
2259 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,
2260 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,
2261};
2262
2263/* Decode UTF-8 multi-byte representation into a unicode character. */
2264static inline unsigned long
2265utf8_to_unicode(const char *string, size_t length)
2266{
2267 unsigned long unicode;
2268
2269 switch (length) {
2270 case 1:
2271 unicode = string[0];
2272 break;
2273 case 2:
2274 unicode = (string[0] & 0x1f) << 6;
2275 unicode += (string[1] & 0x3f);
2276 break;
2277 case 3:
2278 unicode = (string[0] & 0x0f) << 12;
2279 unicode += ((string[1] & 0x3f) << 6);
2280 unicode += (string[2] & 0x3f);
2281 break;
2282 case 4:
2283 unicode = (string[0] & 0x0f) << 18;
2284 unicode += ((string[1] & 0x3f) << 12);
2285 unicode += ((string[2] & 0x3f) << 6);
2286 unicode += (string[3] & 0x3f);
2287 break;
2288 case 5:
2289 unicode = (string[0] & 0x0f) << 24;
2290 unicode += ((string[1] & 0x3f) << 18);
2291 unicode += ((string[2] & 0x3f) << 12);
2292 unicode += ((string[3] & 0x3f) << 6);
2293 unicode += (string[4] & 0x3f);
2294 break;
68b6e0eb 2295 case 6:
10e290ee
JF
2296 unicode = (string[0] & 0x01) << 30;
2297 unicode += ((string[1] & 0x3f) << 24);
2298 unicode += ((string[2] & 0x3f) << 18);
2299 unicode += ((string[3] & 0x3f) << 12);
2300 unicode += ((string[4] & 0x3f) << 6);
2301 unicode += (string[5] & 0x3f);
2302 break;
2303 default:
2304 die("Invalid unicode length");
2305 }
2306
2307 /* Invalid characters could return the special 0xfffd value but NUL
2308 * should be just as good. */
2309 return unicode > 0xffff ? 0 : unicode;
2310}
2311
2312/* Calculates how much of string can be shown within the given maximum width
2313 * and sets trimmed parameter to non-zero value if all of string could not be
2314 * shown.
2315 *
2316 * Additionally, adds to coloffset how many many columns to move to align with
2317 * the expected position. Takes into account how multi-byte and double-width
2318 * characters will effect the cursor position.
2319 *
2320 * Returns the number of bytes to output from string to satisfy max_width. */
2321static size_t
2322utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
2323{
2324 const char *start = string;
2325 const char *end = strchr(string, '\0');
2326 size_t mbwidth = 0;
2327 size_t width = 0;
2328
2329 *trimmed = 0;
2330
2331 while (string < end) {
2332 int c = *(unsigned char *) string;
2333 unsigned char bytes = utf8_bytes[c];
2334 size_t ucwidth;
2335 unsigned long unicode;
2336
2337 if (string + bytes > end)
2338 break;
2339
2340 /* Change representation to figure out whether
2341 * it is a single- or double-width character. */
2342
2343 unicode = utf8_to_unicode(string, bytes);
2344 /* FIXME: Graceful handling of invalid unicode character. */
2345 if (!unicode)
2346 break;
2347
2348 ucwidth = unicode_width(unicode);
2349 width += ucwidth;
2350 if (width > max_width) {
2351 *trimmed = 1;
2352 break;
2353 }
2354
2355 /* The column offset collects the differences between the
2356 * number of bytes encoding a character and the number of
2357 * columns will be used for rendering said character.
2358 *
2359 * So if some character A is encoded in 2 bytes, but will be
2360 * represented on the screen using only 1 byte this will and up
2361 * adding 1 to the multi-byte column offset.
2362 *
2363 * Assumes that no double-width character can be encoding in
2364 * less than two bytes. */
2365 if (bytes > ucwidth)
2366 mbwidth += bytes - ucwidth;
2367
2368 string += bytes;
2369 }
2370
2371 *coloffset += mbwidth;
2372
2373 return string - start;
2374}
2375
2376
2377/*
6b161b31
JF
2378 * Status management
2379 */
2e8488b4 2380
8855ada4 2381/* Whether or not the curses interface has been initialized. */
68b6e0eb 2382static bool cursed = FALSE;
8855ada4 2383
6b161b31
JF
2384/* The status window is used for polling keystrokes. */
2385static WINDOW *status_win;
4a2909a7 2386
2e8488b4 2387/* Update status and title window. */
4a2909a7
JF
2388static void
2389report(const char *msg, ...)
2390{
6706b2ba
JF
2391 static bool empty = TRUE;
2392 struct view *view = display[current_view];
b76c2afc 2393
6706b2ba
JF
2394 if (!empty || *msg) {
2395 va_list args;
4a2909a7 2396
6706b2ba 2397 va_start(args, msg);
4b76734f 2398
6706b2ba
JF
2399 werase(status_win);
2400 wmove(status_win, 0, 0);
2401 if (*msg) {
2402 vwprintw(status_win, msg, args);
2403 empty = FALSE;
2404 } else {
2405 empty = TRUE;
2406 }
2407 wrefresh(status_win);
b801d8b2 2408
6706b2ba
JF
2409 va_end(args);
2410 }
2411
2412 update_view_title(view);
85af6284 2413 update_display_cursor();
b801d8b2
JF
2414}
2415
6b161b31
JF
2416/* Controls when nodelay should be in effect when polling user input. */
2417static void
1ba2ae4b 2418set_nonblocking_input(bool loading)
b801d8b2 2419{
6706b2ba 2420 static unsigned int loading_views;
b801d8b2 2421
6706b2ba
JF
2422 if ((loading == FALSE && loading_views-- == 1) ||
2423 (loading == TRUE && loading_views++ == 0))
1ba2ae4b 2424 nodelay(status_win, loading);
6b161b31
JF
2425}
2426
2427static void
2428init_display(void)
2429{
2430 int x, y;
b76c2afc 2431
6908bdbd
JF
2432 /* Initialize the curses library */
2433 if (isatty(STDIN_FILENO)) {
8855ada4 2434 cursed = !!initscr();
6908bdbd
JF
2435 } else {
2436 /* Leave stdin and stdout alone when acting as a pager. */
2437 FILE *io = fopen("/dev/tty", "r+");
2438
8855ada4 2439 cursed = !!newterm(NULL, io, io);
6908bdbd
JF
2440 }
2441
8855ada4
JF
2442 if (!cursed)
2443 die("Failed to initialize curses");
2444
2e8488b4
JF
2445 nonl(); /* Tell curses not to do NL->CR/NL on output */
2446 cbreak(); /* Take input chars one at a time, no wait for \n */
2447 noecho(); /* Don't echo input */
b801d8b2 2448 leaveok(stdscr, TRUE);
b801d8b2
JF
2449
2450 if (has_colors())
2451 init_colors();
2452
2453 getmaxyx(stdscr, y, x);
2454 status_win = newwin(1, 0, y - 1, 0);
2455 if (!status_win)
2456 die("Failed to create status window");
2457
2458 /* Enable keyboard mapping */
2459 keypad(status_win, TRUE);
78c70acd 2460 wbkgdset(status_win, get_line_attr(LINE_STATUS));
6b161b31
JF
2461}
2462
c34d9c9f
JF
2463
2464/*
2465 * Repository references
2466 */
2467
2468static struct ref *refs;
3a91b75e 2469static size_t refs_size;
c34d9c9f 2470
1307df1a
JF
2471/* Id <-> ref store */
2472static struct ref ***id_refs;
2473static size_t id_refs_size;
2474
c34d9c9f
JF
2475static struct ref **
2476get_refs(char *id)
2477{
1307df1a
JF
2478 struct ref ***tmp_id_refs;
2479 struct ref **ref_list = NULL;
2480 size_t ref_list_size = 0;
c34d9c9f
JF
2481 size_t i;
2482
1307df1a
JF
2483 for (i = 0; i < id_refs_size; i++)
2484 if (!strcmp(id, id_refs[i][0]->id))
2485 return id_refs[i];
2486
2487 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
2488 if (!tmp_id_refs)
2489 return NULL;
2490
2491 id_refs = tmp_id_refs;
2492
c34d9c9f
JF
2493 for (i = 0; i < refs_size; i++) {
2494 struct ref **tmp;
2495
2496 if (strcmp(id, refs[i].id))
2497 continue;
2498
1307df1a 2499 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
c34d9c9f 2500 if (!tmp) {
1307df1a
JF
2501 if (ref_list)
2502 free(ref_list);
c34d9c9f
JF
2503 return NULL;
2504 }
2505
1307df1a
JF
2506 ref_list = tmp;
2507 if (ref_list_size > 0)
2508 ref_list[ref_list_size - 1]->next = 1;
2509 ref_list[ref_list_size] = &refs[i];
3af8774e
JF
2510
2511 /* XXX: The properties of the commit chains ensures that we can
2512 * safely modify the shared ref. The repo references will
2513 * always be similar for the same id. */
1307df1a
JF
2514 ref_list[ref_list_size]->next = 0;
2515 ref_list_size++;
c34d9c9f
JF
2516 }
2517
1307df1a
JF
2518 if (ref_list)
2519 id_refs[id_refs_size++] = ref_list;
2520
2521 return ref_list;
c34d9c9f
JF
2522}
2523
2524static int
d0cea5f9 2525read_ref(char *id, int idlen, char *name, int namelen)
c34d9c9f 2526{
d0cea5f9
JF
2527 struct ref *ref;
2528 bool tag = FALSE;
2529 bool tag_commit = FALSE;
2530
2531 /* Commits referenced by tags has "^{}" appended. */
2532 if (name[namelen - 1] == '}') {
2533 while (namelen > 0 && name[namelen] != '^')
2534 namelen--;
2535 if (namelen > 0)
2536 tag_commit = TRUE;
2537 name[namelen] = 0;
2538 }
c34d9c9f 2539
d0cea5f9
JF
2540 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2541 if (!tag_commit)
2542 return OK;
2543 name += STRING_SIZE("refs/tags/");
2544 tag = TRUE;
c34d9c9f 2545
d0cea5f9
JF
2546 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2547 name += STRING_SIZE("refs/heads/");
c34d9c9f 2548
d0cea5f9
JF
2549 } else if (!strcmp(name, "HEAD")) {
2550 return OK;
2551 }
6706b2ba 2552
d0cea5f9
JF
2553 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2554 if (!refs)
2555 return ERR;
c34d9c9f 2556
d0cea5f9
JF
2557 ref = &refs[refs_size++];
2558 ref->name = strdup(name);
2559 if (!ref->name)
2560 return ERR;
3af8774e 2561
d0cea5f9
JF
2562 ref->tag = tag;
2563 string_copy(ref->id, id);
3af8774e 2564
d0cea5f9
JF
2565 return OK;
2566}
c34d9c9f 2567
d0cea5f9
JF
2568static int
2569load_refs(void)
2570{
2571 const char *cmd_env = getenv("TIG_LS_REMOTE");
2572 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
c34d9c9f 2573
4a63c884 2574 return read_properties(popen(cmd, "r"), "\t", read_ref);
d0cea5f9 2575}
c34d9c9f 2576
d0cea5f9 2577static int
14c778a6 2578read_repo_config_option(char *name, int namelen, char *value, int valuelen)
d0cea5f9
JF
2579{
2580 if (!strcmp(name, "i18n.commitencoding")) {
2581 string_copy(opt_encoding, value);
c34d9c9f
JF
2582 }
2583
c34d9c9f
JF
2584 return OK;
2585}
2586
4670cf89 2587static int
14c778a6 2588load_repo_config(void)
4670cf89 2589{
66749723 2590 return read_properties(popen("git repo-config --list", "r"),
14c778a6 2591 "=", read_repo_config_option);
d0cea5f9
JF
2592}
2593
2594static int
4a63c884 2595read_properties(FILE *pipe, const char *separators,
d0cea5f9
JF
2596 int (*read_property)(char *, int, char *, int))
2597{
4670cf89
JF
2598 char buffer[BUFSIZ];
2599 char *name;
d0cea5f9 2600 int state = OK;
4670cf89
JF
2601
2602 if (!pipe)
2603 return ERR;
2604
d0cea5f9 2605 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4a63c884
JF
2606 char *value;
2607 size_t namelen;
2608 size_t valuelen;
4670cf89 2609
4a63c884
JF
2610 name = chomp_string(name);
2611 namelen = strcspn(name, separators);
2612
2613 if (name[namelen]) {
2614 name[namelen] = 0;
2615 value = chomp_string(name + namelen + 1);
d0cea5f9 2616 valuelen = strlen(value);
4670cf89 2617
d0cea5f9 2618 } else {
d0cea5f9
JF
2619 value = "";
2620 valuelen = 0;
4670cf89 2621 }
d0cea5f9 2622
3c3801c2 2623 state = read_property(name, namelen, value, valuelen);
4670cf89
JF
2624 }
2625
d0cea5f9
JF
2626 if (state != ERR && ferror(pipe))
2627 state = ERR;
4670cf89
JF
2628
2629 pclose(pipe);
2630
d0cea5f9 2631 return state;
4670cf89
JF
2632}
2633
d0cea5f9 2634
6b161b31
JF
2635/*
2636 * Main
2637 */
2638
b5c9e67f
TH
2639#if __GNUC__ >= 3
2640#define __NORETURN __attribute__((__noreturn__))
2641#else
2642#define __NORETURN
2643#endif
2644
2645static void __NORETURN
6b161b31
JF
2646quit(int sig)
2647{
8855ada4
JF
2648 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2649 if (cursed)
2650 endwin();
6b161b31
JF
2651 exit(0);
2652}
2653
c6704a4e
JF
2654static void __NORETURN
2655die(const char *err, ...)
6b161b31
JF
2656{
2657 va_list args;
2658
2659 endwin();
2660
2661 va_start(args, err);
2662 fputs("tig: ", stderr);
2663 vfprintf(stderr, err, args);
2664 fputs("\n", stderr);
2665 va_end(args);
2666
2667 exit(1);
2668}
2669
2670int
2671main(int argc, char *argv[])
2672{
1ba2ae4b 2673 struct view *view;
6b161b31 2674 enum request request;
1ba2ae4b 2675 size_t i;
6b161b31
JF
2676
2677 signal(SIGINT, quit);
2678
660e09ad
JF
2679 if (load_options() == ERR)
2680 die("Failed to load user config.");
2681
2682 /* Load the repo config file so options can be overwritten from
afdc35b3 2683 * the command line. */
14c778a6 2684 if (load_repo_config() == ERR)
afdc35b3
JF
2685 die("Failed to load repo config.");
2686
8855ada4 2687 if (!parse_options(argc, argv))
6b161b31
JF
2688 return 0;
2689
c34d9c9f
JF
2690 if (load_refs() == ERR)
2691 die("Failed to load refs.");
2692
7bb55251
JF
2693 /* Require a git repository unless when running in pager mode. */
2694 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2695 die("Not a git repository");
2696
1ba2ae4b
JF
2697 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2698 view->cmd_env = getenv(view->cmd_env);
2699
6b161b31
JF
2700 request = opt_request;
2701
2702 init_display();
b801d8b2
JF
2703
2704 while (view_driver(display[current_view], request)) {
6b161b31 2705 int key;
b801d8b2
JF
2706 int i;
2707
6b161b31
JF
2708 foreach_view (view, i)
2709 update_view(view);
b801d8b2
JF
2710
2711 /* Refresh, accept single keystroke of input */
6b161b31
JF
2712 key = wgetch(status_win);
2713 request = get_request(key);
03a93dbb 2714
6706b2ba 2715 /* Some low-level request handling. This keeps access to
fac7db6c
JF
2716 * status_win restricted. */
2717 switch (request) {
2718 case REQ_PROMPT:
6908bdbd
JF
2719 report(":");
2720 /* Temporarily switch to line-oriented and echoed
2721 * input. */
03a93dbb
JF
2722 nocbreak();
2723 echo();
49f2b43f
JF
2724
2725 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2726 memcpy(opt_cmd, "git ", 4);
2727 opt_request = REQ_VIEW_PAGER;
2728 } else {
6a7bb912
JF
2729 report("Prompt interrupted by loading view, "
2730 "press 'z' to stop loading views");
2731 request = REQ_SCREEN_UPDATE;
49f2b43f
JF
2732 }
2733
6908bdbd
JF
2734 noecho();
2735 cbreak();
fac7db6c
JF
2736 break;
2737
2738 case REQ_SCREEN_RESIZE:
2739 {
2740 int height, width;
2741
2742 getmaxyx(stdscr, height, width);
2743
2744 /* Resize the status view and let the view driver take
2745 * care of resizing the displayed views. */
2746 wresize(status_win, 1, width);
2747 mvwin(status_win, height - 1, 0);
2748 wrefresh(status_win);
2749 break;
2750 }
2751 default:
2752 break;
03a93dbb 2753 }
b801d8b2
JF
2754 }
2755
2756 quit(0);
2757
2758 return 0;
2759}
2760
2761/**
e34f45d4 2762 * include::BUGS[]
965537fa 2763 *
b801d8b2
JF
2764 * COPYRIGHT
2765 * ---------
192d9a60 2766 * Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
b801d8b2
JF
2767 *
2768 * This program is free software; you can redistribute it and/or modify
2769 * it under the terms of the GNU General Public License as published by
2770 * the Free Software Foundation; either version 2 of the License, or
2771 * (at your option) any later version.
2772 *
2773 * SEE ALSO
2774 * --------
f84f9d28
JF
2775 * - link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
2776 * - link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
2777 *
2778 * Other git repository browsers:
5cfbde75 2779 *
f84f9d28
JF
2780 * - gitk(1)
2781 * - qgit(1)
2782 * - gitview(1)
d839253b
JF
2783 *
2784 * Sites:
2785 *
2786 * include::SITES[]
b801d8b2 2787 **/