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