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