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