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