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