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