626fcd5fd1ca49e8dfc89d1ae29c09f5a0e10edd
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 */
14 #ifndef VERSION
15 #define VERSION "tig-0.3"
16 #endif
18 #ifndef DEBUG
19 #define NDEBUG
20 #endif
22 #include <assert.h>
23 #include <errno.h>
24 #include <ctype.h>
25 #include <signal.h>
26 #include <stdarg.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <unistd.h>
31 #include <time.h>
33 #include <curses.h>
35 #if __GNUC__ >= 3
36 #define __NORETURN __attribute__((__noreturn__))
37 #else
38 #define __NORETURN
39 #endif
41 static void __NORETURN die(const char *err, ...);
42 static void report(const char *msg, ...);
43 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
44 static void set_nonblocking_input(bool loading);
45 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
47 #define ABS(x) ((x) >= 0 ? (x) : -(x))
48 #define MIN(x, y) ((x) < (y) ? (x) : (y))
50 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
51 #define STRING_SIZE(x) (sizeof(x) - 1)
53 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
54 #define SIZEOF_CMD 1024 /* Size of command buffer. */
55 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
57 /* This color name can be used to refer to the default term colors. */
58 #define COLOR_DEFAULT (-1)
60 /* The format and size of the date column in the main view. */
61 #define DATE_FORMAT "%Y-%m-%d %H:%M"
62 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
64 #define AUTHOR_COLS 20
66 /* The default interval between line numbers. */
67 #define NUMBER_INTERVAL 1
69 #define TABSIZE 8
71 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
73 #define TIG_LS_REMOTE \
74 "git ls-remote . 2>/dev/null"
76 #define TIG_DIFF_CMD \
77 "git show --patch-with-stat --find-copies-harder -B -C %s"
79 #define TIG_LOG_CMD \
80 "git log --cc --stat -n100 %s"
82 #define TIG_MAIN_CMD \
83 "git log --topo-order --stat --pretty=raw %s"
85 /* XXX: Needs to be defined to the empty string. */
86 #define TIG_HELP_CMD ""
87 #define TIG_PAGER_CMD ""
89 /* Some ascii-shorthands fitted into the ncurses namespace. */
90 #define KEY_TAB '\t'
91 #define KEY_RETURN '\r'
92 #define KEY_ESC 27
95 struct ref {
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? */
100 };
102 static struct ref **get_refs(char *id);
104 struct int_map {
105 const char *name;
106 int namelen;
107 int value;
108 };
110 static int
111 set_from_int_map(struct int_map *map, size_t map_size,
112 int *value, const char *name, int namelen)
113 {
115 int i;
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 }
124 return ERR;
125 }
128 /*
129 * String helpers
130 */
132 static inline void
133 string_ncopy(char *dst, const char *src, int dstlen)
134 {
135 strncpy(dst, src, dstlen - 1);
136 dst[dstlen - 1] = 0;
138 }
140 /* Shorthand for safely copying into a fixed buffer. */
141 #define string_copy(dst, src) \
142 string_ncopy(dst, src, sizeof(dst))
144 static char *
145 chomp_string(char *name)
146 {
147 int namelen;
149 while (isspace(*name))
150 name++;
152 namelen = strlen(name) - 1;
153 while (namelen > 0 && isspace(name[namelen]))
154 name[namelen--] = 0;
156 return name;
157 }
159 static bool
160 string_nformat(char *buf, size_t bufsize, int *bufpos, const char *fmt, ...)
161 {
162 va_list args;
163 int pos = bufpos ? *bufpos : 0;
165 va_start(args, fmt);
166 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
167 va_end(args);
169 if (bufpos)
170 *bufpos = pos;
172 return pos >= bufsize ? FALSE : TRUE;
173 }
175 #define string_format(buf, fmt, args...) \
176 string_nformat(buf, sizeof(buf), NULL, fmt, args)
178 #define string_format_from(buf, from, fmt, args...) \
179 string_nformat(buf, sizeof(buf), from, fmt, args)
181 static int
182 string_enum_compare(const char *str1, const char *str2, int len)
183 {
184 size_t i;
186 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
188 /* Diff-Header == DIFF_HEADER */
189 for (i = 0; i < len; i++) {
190 if (toupper(str1[i]) == toupper(str2[i]))
191 continue;
193 if (string_enum_sep(str1[i]) &&
194 string_enum_sep(str2[i]))
195 continue;
197 return str1[i] - str2[i];
198 }
200 return 0;
201 }
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 */
220 static size_t
221 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
222 {
223 char c;
225 #define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
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('\'');
240 return bufsize;
241 }
244 /*
245 * User requests
246 */
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"), \
286 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
287 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"),
290 /* User action requests. */
291 enum request {
292 #define REQ_GROUP(help)
293 #define REQ_(req, help) REQ_##req
295 /* Offset all requests to avoid conflicts with ncurses getch values. */
296 REQ_OFFSET = KEY_MAX + 1,
297 REQ_INFO
299 #undef REQ_GROUP
300 #undef REQ_
301 };
303 struct request_info {
304 enum request request;
305 char *help;
306 };
308 static struct request_info req_info[] = {
309 #define REQ_GROUP(help) { 0, (help) },
310 #define REQ_(req, help) { REQ_##req, (help) }
311 REQ_INFO
312 #undef REQ_GROUP
313 #undef REQ_
314 };
316 /*
317 * Options
318 */
320 static const char usage[] =
321 VERSION " (" __DATE__ ")\n"
322 "\n"
323 "Usage: tig [options]\n"
324 " or: tig [options] [--] [git log options]\n"
325 " or: tig [options] log [git log options]\n"
326 " or: tig [options] diff [git diff options]\n"
327 " or: tig [options] show [git show options]\n"
328 " or: tig [options] < [git command output]\n"
329 "\n"
330 "Options:\n"
331 " -l Start up in log view\n"
332 " -d Start up in diff view\n"
333 " -n[I], --line-number[=I] Show line numbers with given interval\n"
334 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
335 " -- Mark end of tig options\n"
336 " -v, --version Show version and exit\n"
337 " -h, --help Show help message and exit\n";
339 /* Option and state variables. */
340 static bool opt_line_number = FALSE;
341 static bool opt_rev_graph = TRUE;
342 static int opt_num_interval = NUMBER_INTERVAL;
343 static int opt_tab_size = TABSIZE;
344 static enum request opt_request = REQ_VIEW_MAIN;
345 static char opt_cmd[SIZEOF_CMD] = "";
346 static char opt_encoding[20] = "";
347 static bool opt_utf8 = TRUE;
348 static FILE *opt_pipe = NULL;
350 enum option_type {
351 OPT_NONE,
352 OPT_INT,
353 };
355 static bool
356 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
357 {
358 va_list args;
359 char *value = "";
360 int *number;
362 if (opt[0] != '-')
363 return FALSE;
365 if (opt[1] == '-') {
366 int namelen = strlen(name);
368 opt += 2;
370 if (strncmp(opt, name, namelen))
371 return FALSE;
373 if (opt[namelen] == '=')
374 value = opt + namelen + 1;
376 } else {
377 if (!short_name || opt[1] != short_name)
378 return FALSE;
379 value = opt + 2;
380 }
382 va_start(args, type);
383 if (type == OPT_INT) {
384 number = va_arg(args, int *);
385 if (isdigit(*value))
386 *number = atoi(value);
387 }
388 va_end(args);
390 return TRUE;
391 }
393 /* Returns the index of log or diff command or -1 to exit. */
394 static bool
395 parse_options(int argc, char *argv[])
396 {
397 int i;
399 for (i = 1; i < argc; i++) {
400 char *opt = argv[i];
402 if (!strcmp(opt, "-l")) {
403 opt_request = REQ_VIEW_LOG;
404 continue;
405 }
407 if (!strcmp(opt, "-d")) {
408 opt_request = REQ_VIEW_DIFF;
409 continue;
410 }
412 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
413 opt_line_number = TRUE;
414 continue;
415 }
417 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
418 opt_tab_size = MIN(opt_tab_size, TABSIZE);
419 continue;
420 }
422 if (check_option(opt, 'v', "version", OPT_NONE)) {
423 printf("tig version %s\n", VERSION);
424 return FALSE;
425 }
427 if (check_option(opt, 'h', "help", OPT_NONE)) {
428 printf(usage);
429 return FALSE;
430 }
432 if (!strcmp(opt, "--")) {
433 i++;
434 break;
435 }
437 if (!strcmp(opt, "log") ||
438 !strcmp(opt, "diff") ||
439 !strcmp(opt, "show")) {
440 opt_request = opt[0] == 'l'
441 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
442 break;
443 }
445 if (opt[0] && opt[0] != '-')
446 break;
448 die("unknown option '%s'\n\n%s", opt, usage);
449 }
451 if (!isatty(STDIN_FILENO)) {
452 opt_request = REQ_VIEW_PAGER;
453 opt_pipe = stdin;
455 } else if (i < argc) {
456 size_t buf_size;
458 if (opt_request == REQ_VIEW_MAIN)
459 /* XXX: This is vulnerable to the user overriding
460 * options required for the main view parser. */
461 string_copy(opt_cmd, "git log --stat --pretty=raw");
462 else
463 string_copy(opt_cmd, "git");
464 buf_size = strlen(opt_cmd);
466 while (buf_size < sizeof(opt_cmd) && i < argc) {
467 opt_cmd[buf_size++] = ' ';
468 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
469 }
471 if (buf_size >= sizeof(opt_cmd))
472 die("command too long");
474 opt_cmd[buf_size] = 0;
476 }
478 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
479 opt_utf8 = FALSE;
481 return TRUE;
482 }
485 /*
486 * Line-oriented content detection.
487 */
489 #define LINE_INFO \
490 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
491 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
492 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
493 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
494 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
495 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
496 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
497 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
498 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
499 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
500 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
501 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
502 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
503 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
504 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
505 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
506 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
507 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
508 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
509 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
510 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
511 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
512 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
513 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
514 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
515 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
516 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
517 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
518 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
519 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
520 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
521 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
522 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
523 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
524 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
525 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
526 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
527 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
529 enum line_type {
530 #define LINE(type, line, fg, bg, attr) \
531 LINE_##type
532 LINE_INFO
533 #undef LINE
534 };
536 struct line_info {
537 const char *name; /* Option name. */
538 int namelen; /* Size of option name. */
539 const char *line; /* The start of line to match. */
540 int linelen; /* Size of string to match. */
541 int fg, bg, attr; /* Color and text attributes for the lines. */
542 };
544 static struct line_info line_info[] = {
545 #define LINE(type, line, fg, bg, attr) \
546 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
547 LINE_INFO
548 #undef LINE
549 };
551 static enum line_type
552 get_line_type(char *line)
553 {
554 int linelen = strlen(line);
555 enum line_type type;
557 for (type = 0; type < ARRAY_SIZE(line_info); type++)
558 /* Case insensitive search matches Signed-off-by lines better. */
559 if (linelen >= line_info[type].linelen &&
560 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
561 return type;
563 return LINE_DEFAULT;
564 }
566 static inline int
567 get_line_attr(enum line_type type)
568 {
569 assert(type < ARRAY_SIZE(line_info));
570 return COLOR_PAIR(type) | line_info[type].attr;
571 }
573 static struct line_info *
574 get_line_info(char *name, int namelen)
575 {
576 enum line_type type;
578 for (type = 0; type < ARRAY_SIZE(line_info); type++)
579 if (namelen == line_info[type].namelen &&
580 !string_enum_compare(line_info[type].name, name, namelen))
581 return &line_info[type];
583 return NULL;
584 }
586 static void
587 init_colors(void)
588 {
589 int default_bg = COLOR_BLACK;
590 int default_fg = COLOR_WHITE;
591 enum line_type type;
593 start_color();
595 if (use_default_colors() != ERR) {
596 default_bg = -1;
597 default_fg = -1;
598 }
600 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
601 struct line_info *info = &line_info[type];
602 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
603 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
605 init_pair(type, fg, bg);
606 }
607 }
609 struct line {
610 enum line_type type;
611 void *data; /* User data */
612 };
615 /*
616 * Keys
617 */
619 struct keymap {
620 int alias;
621 int request;
622 };
624 static struct keymap keymap[] = {
625 /* View switching */
626 { 'm', REQ_VIEW_MAIN },
627 { 'd', REQ_VIEW_DIFF },
628 { 'l', REQ_VIEW_LOG },
629 { 'p', REQ_VIEW_PAGER },
630 { 'h', REQ_VIEW_HELP },
631 { '?', REQ_VIEW_HELP },
633 /* View manipulation */
634 { 'q', REQ_VIEW_CLOSE },
635 { KEY_TAB, REQ_VIEW_NEXT },
636 { KEY_RETURN, REQ_ENTER },
637 { KEY_UP, REQ_PREVIOUS },
638 { KEY_DOWN, REQ_NEXT },
640 /* Cursor navigation */
641 { 'k', REQ_MOVE_UP },
642 { 'j', REQ_MOVE_DOWN },
643 { KEY_HOME, REQ_MOVE_FIRST_LINE },
644 { KEY_END, REQ_MOVE_LAST_LINE },
645 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
646 { ' ', REQ_MOVE_PAGE_DOWN },
647 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
648 { 'b', REQ_MOVE_PAGE_UP },
649 { '-', REQ_MOVE_PAGE_UP },
651 /* Scrolling */
652 { KEY_IC, REQ_SCROLL_LINE_UP },
653 { KEY_DC, REQ_SCROLL_LINE_DOWN },
654 { 'w', REQ_SCROLL_PAGE_UP },
655 { 's', REQ_SCROLL_PAGE_DOWN },
657 /* Misc */
658 { 'Q', REQ_QUIT },
659 { 'z', REQ_STOP_LOADING },
660 { 'v', REQ_SHOW_VERSION },
661 { 'r', REQ_SCREEN_REDRAW },
662 { 'n', REQ_TOGGLE_LINENO },
663 { 'g', REQ_TOGGLE_REV_GRAPH},
664 { ':', REQ_PROMPT },
666 /* wgetch() with nodelay() enabled returns ERR when there's no input. */
667 { ERR, REQ_SCREEN_UPDATE },
669 /* Use the ncurses SIGWINCH handler. */
670 { KEY_RESIZE, REQ_SCREEN_RESIZE },
671 };
673 static enum request
674 get_request(int key)
675 {
676 int i;
678 for (i = 0; i < ARRAY_SIZE(keymap); i++)
679 if (keymap[i].alias == key)
680 return keymap[i].request;
682 return (enum request) key;
683 }
685 struct key {
686 char *name;
687 int value;
688 };
690 static struct key key_table[] = {
691 { "Enter", KEY_RETURN },
692 { "Space", ' ' },
693 { "Backspace", KEY_BACKSPACE },
694 { "Tab", KEY_TAB },
695 { "Escape", KEY_ESC },
696 { "Left", KEY_LEFT },
697 { "Right", KEY_RIGHT },
698 { "Up", KEY_UP },
699 { "Down", KEY_DOWN },
700 { "Insert", KEY_IC },
701 { "Delete", KEY_DC },
702 { "Home", KEY_HOME },
703 { "End", KEY_END },
704 { "PageUp", KEY_PPAGE },
705 { "PageDown", KEY_NPAGE },
706 { "F1", KEY_F(1) },
707 { "F2", KEY_F(2) },
708 { "F3", KEY_F(3) },
709 { "F4", KEY_F(4) },
710 { "F5", KEY_F(5) },
711 { "F6", KEY_F(6) },
712 { "F7", KEY_F(7) },
713 { "F8", KEY_F(8) },
714 { "F9", KEY_F(9) },
715 { "F10", KEY_F(10) },
716 { "F11", KEY_F(11) },
717 { "F12", KEY_F(12) },
718 };
720 static char *
721 get_key(enum request request)
722 {
723 static char buf[BUFSIZ];
724 static char key_char[] = "'X'";
725 int pos = 0;
726 char *sep = " ";
727 int i;
729 buf[pos] = 0;
731 for (i = 0; i < ARRAY_SIZE(keymap); i++) {
732 char *seq = NULL;
733 int key;
735 if (keymap[i].request != request)
736 continue;
738 for (key = 0; key < ARRAY_SIZE(key_table); key++)
739 if (key_table[key].value == keymap[i].alias)
740 seq = key_table[key].name;
742 if (seq == NULL &&
743 keymap[i].alias < 127 &&
744 isprint(keymap[i].alias)) {
745 key_char[1] = (char) keymap[i].alias;
746 seq = key_char;
747 }
749 if (!seq)
750 seq = "'?'";
752 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
753 return "Too many keybindings!";
754 sep = ", ";
755 }
757 return buf;
758 }
761 /*
762 * User config file handling.
763 */
765 static struct int_map color_map[] = {
766 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
767 COLOR_MAP(DEFAULT),
768 COLOR_MAP(BLACK),
769 COLOR_MAP(BLUE),
770 COLOR_MAP(CYAN),
771 COLOR_MAP(GREEN),
772 COLOR_MAP(MAGENTA),
773 COLOR_MAP(RED),
774 COLOR_MAP(WHITE),
775 COLOR_MAP(YELLOW),
776 };
778 #define set_color(color, name) \
779 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
781 static struct int_map attr_map[] = {
782 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
783 ATTR_MAP(NORMAL),
784 ATTR_MAP(BLINK),
785 ATTR_MAP(BOLD),
786 ATTR_MAP(DIM),
787 ATTR_MAP(REVERSE),
788 ATTR_MAP(STANDOUT),
789 ATTR_MAP(UNDERLINE),
790 };
792 #define set_attribute(attr, name) \
793 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
795 static int config_lineno;
796 static bool config_errors;
797 static char *config_msg;
799 /* Wants: object fgcolor bgcolor [attr] */
800 static int
801 option_color_command(int argc, char *argv[])
802 {
803 struct line_info *info;
805 if (argc != 3 && argc != 4) {
806 config_msg = "Wrong number of arguments given to color command";
807 return ERR;
808 }
810 info = get_line_info(argv[0], strlen(argv[0]));
811 if (!info) {
812 config_msg = "Unknown color name";
813 return ERR;
814 }
816 if (set_color(&info->fg, argv[1]) == ERR) {
817 config_msg = "Unknown color";
818 return ERR;
819 }
821 if (set_color(&info->bg, argv[2]) == ERR) {
822 config_msg = "Unknown color";
823 return ERR;
824 }
826 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
827 config_msg = "Unknown attribute";
828 return ERR;
829 }
831 return OK;
832 }
834 /* Wants: name = value */
835 static int
836 option_set_command(int argc, char *argv[])
837 {
838 if (argc != 3) {
839 config_msg = "Wrong number of arguments given to set command";
840 return ERR;
841 }
843 if (strcmp(argv[1], "=")) {
844 config_msg = "No value assigned";
845 return ERR;
846 }
848 if (!strcmp(argv[0], "show-rev-graph")) {
849 opt_rev_graph = (!strcmp(argv[2], "1") ||
850 !strcmp(argv[2], "true") ||
851 !strcmp(argv[2], "yes"));
852 return OK;
853 }
855 if (!strcmp(argv[0], "line-number-interval")) {
856 opt_num_interval = atoi(argv[2]);
857 return OK;
858 }
860 if (!strcmp(argv[0], "tab-size")) {
861 opt_tab_size = atoi(argv[2]);
862 return OK;
863 }
865 if (!strcmp(argv[0], "encoding")) {
866 string_copy(opt_encoding, argv[2]);
867 return OK;
868 }
870 return ERR;
871 }
873 static int
874 set_option(char *opt, char *value)
875 {
876 char *argv[16];
877 int valuelen;
878 int argc = 0;
880 /* Tokenize */
881 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
882 argv[argc++] = value;
884 value += valuelen;
885 if (!*value)
886 break;
888 *value++ = 0;
889 while (isspace(*value))
890 value++;
891 }
893 if (!strcmp(opt, "color"))
894 return option_color_command(argc, argv);
896 if (!strcmp(opt, "set"))
897 return option_set_command(argc, argv);
899 return ERR;
900 }
902 static int
903 read_option(char *opt, int optlen, char *value, int valuelen)
904 {
905 config_lineno++;
906 config_msg = "Internal error";
908 optlen = strcspn(opt, "#;");
909 if (optlen == 0) {
910 /* The whole line is a commend or empty. */
911 return OK;
913 } else if (opt[optlen] != 0) {
914 /* Part of the option name is a comment, so the value part
915 * should be ignored. */
916 valuelen = 0;
917 opt[optlen] = value[valuelen] = 0;
918 } else {
919 /* Else look for comment endings in the value. */
920 valuelen = strcspn(value, "#;");
921 value[valuelen] = 0;
922 }
924 if (set_option(opt, value) == ERR) {
925 fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n",
926 config_lineno, optlen, opt, config_msg);
927 config_errors = TRUE;
928 }
930 /* Always keep going if errors are encountered. */
931 return OK;
932 }
934 static int
935 load_options(void)
936 {
937 char *home = getenv("HOME");
938 char buf[1024];
939 FILE *file;
941 config_lineno = 0;
942 config_errors = FALSE;
944 if (!home || !string_format(buf, "%s/.tigrc", home))
945 return ERR;
947 /* It's ok that the file doesn't exist. */
948 file = fopen(buf, "r");
949 if (!file)
950 return OK;
952 if (read_properties(file, " \t", read_option) == ERR ||
953 config_errors == TRUE)
954 fprintf(stderr, "Errors while loading %s.\n", buf);
956 return OK;
957 }
960 /*
961 * The viewer
962 */
964 struct view;
965 struct view_ops;
967 /* The display array of active views and the index of the current view. */
968 static struct view *display[2];
969 static unsigned int current_view;
971 #define foreach_view(view, i) \
972 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
974 #define displayed_views() (display[1] != NULL ? 2 : 1)
976 /* Current head and commit ID */
977 static char ref_commit[SIZEOF_REF] = "HEAD";
978 static char ref_head[SIZEOF_REF] = "HEAD";
980 struct view {
981 const char *name; /* View name */
982 const char *cmd_fmt; /* Default command line format */
983 const char *cmd_env; /* Command line set via environment */
984 const char *id; /* Points to either of ref_{head,commit} */
986 struct view_ops *ops; /* View operations */
988 char cmd[SIZEOF_CMD]; /* Command buffer */
989 char ref[SIZEOF_REF]; /* Hovered commit reference */
990 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
992 int height, width; /* The width and height of the main window */
993 WINDOW *win; /* The main window */
994 WINDOW *title; /* The title window living below the main window */
996 /* Navigation */
997 unsigned long offset; /* Offset of the window top */
998 unsigned long lineno; /* Current line number */
1000 /* If non-NULL, points to the view that opened this view. If this view
1001 * is closed tig will switch back to the parent view. */
1002 struct view *parent;
1004 /* Buffering */
1005 unsigned long lines; /* Total number of lines */
1006 struct line *line; /* Line index */
1007 unsigned long line_size;/* Total number of allocated lines */
1008 unsigned int digits; /* Number of digits in the lines member. */
1010 /* Loading */
1011 FILE *pipe;
1012 time_t start_time;
1013 };
1015 struct view_ops {
1016 /* What type of content being displayed. Used in the title bar. */
1017 const char *type;
1018 /* Draw one line; @lineno must be < view->height. */
1019 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1020 /* Read one line; updates view->line. */
1021 bool (*read)(struct view *view, char *data);
1022 /* Depending on view, change display based on current line. */
1023 bool (*enter)(struct view *view, struct line *line);
1024 };
1026 static struct view_ops pager_ops;
1027 static struct view_ops main_ops;
1029 #define VIEW_STR(name, cmd, env, ref, ops) \
1030 { name, cmd, #env, ref, ops }
1032 #define VIEW_(id, name, ops, ref) \
1033 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops)
1036 static struct view views[] = {
1037 VIEW_(MAIN, "main", &main_ops, ref_head),
1038 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1039 VIEW_(LOG, "log", &pager_ops, ref_head),
1040 VIEW_(HELP, "help", &pager_ops, "static"),
1041 VIEW_(PAGER, "pager", &pager_ops, "static"),
1042 };
1044 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1047 static bool
1048 draw_view_line(struct view *view, unsigned int lineno)
1049 {
1050 if (view->offset + lineno >= view->lines)
1051 return FALSE;
1053 return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
1054 }
1056 static void
1057 redraw_view_from(struct view *view, int lineno)
1058 {
1059 assert(0 <= lineno && lineno < view->height);
1061 for (; lineno < view->height; lineno++) {
1062 if (!draw_view_line(view, lineno))
1063 break;
1064 }
1066 redrawwin(view->win);
1067 wrefresh(view->win);
1068 }
1070 static void
1071 redraw_view(struct view *view)
1072 {
1073 wclear(view->win);
1074 redraw_view_from(view, 0);
1075 }
1078 static void
1079 update_view_title(struct view *view)
1080 {
1081 if (view == display[current_view])
1082 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1083 else
1084 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1086 werase(view->title);
1087 wmove(view->title, 0, 0);
1089 if (*view->ref)
1090 wprintw(view->title, "[%s] %s", view->name, view->ref);
1091 else
1092 wprintw(view->title, "[%s]", view->name);
1094 if (view->lines || view->pipe) {
1095 unsigned int view_lines = view->offset + view->height;
1096 unsigned int lines = view->lines
1097 ? MIN(view_lines, view->lines) * 100 / view->lines
1098 : 0;
1100 wprintw(view->title, " - %s %d of %d (%d%%)",
1101 view->ops->type,
1102 view->lineno + 1,
1103 view->lines,
1104 lines);
1105 }
1107 if (view->pipe) {
1108 time_t secs = time(NULL) - view->start_time;
1110 /* Three git seconds are a long time ... */
1111 if (secs > 2)
1112 wprintw(view->title, " %lds", secs);
1113 }
1115 wmove(view->title, 0, view->width - 1);
1116 wrefresh(view->title);
1117 }
1119 static void
1120 resize_display(void)
1121 {
1122 int offset, i;
1123 struct view *base = display[0];
1124 struct view *view = display[1] ? display[1] : display[0];
1126 /* Setup window dimensions */
1128 getmaxyx(stdscr, base->height, base->width);
1130 /* Make room for the status window. */
1131 base->height -= 1;
1133 if (view != base) {
1134 /* Horizontal split. */
1135 view->width = base->width;
1136 view->height = SCALE_SPLIT_VIEW(base->height);
1137 base->height -= view->height;
1139 /* Make room for the title bar. */
1140 view->height -= 1;
1141 }
1143 /* Make room for the title bar. */
1144 base->height -= 1;
1146 offset = 0;
1148 foreach_view (view, i) {
1149 if (!view->win) {
1150 view->win = newwin(view->height, 0, offset, 0);
1151 if (!view->win)
1152 die("Failed to create %s view", view->name);
1154 scrollok(view->win, TRUE);
1156 view->title = newwin(1, 0, offset + view->height, 0);
1157 if (!view->title)
1158 die("Failed to create title window");
1160 } else {
1161 wresize(view->win, view->height, view->width);
1162 mvwin(view->win, offset, 0);
1163 mvwin(view->title, offset + view->height, 0);
1164 }
1166 offset += view->height + 1;
1167 }
1168 }
1170 static void
1171 redraw_display(void)
1172 {
1173 struct view *view;
1174 int i;
1176 foreach_view (view, i) {
1177 redraw_view(view);
1178 update_view_title(view);
1179 }
1180 }
1182 static void
1183 update_display_cursor(void)
1184 {
1185 struct view *view = display[current_view];
1187 /* Move the cursor to the right-most column of the cursor line.
1188 *
1189 * XXX: This could turn out to be a bit expensive, but it ensures that
1190 * the cursor does not jump around. */
1191 if (view->lines) {
1192 wmove(view->win, view->lineno - view->offset, view->width - 1);
1193 wrefresh(view->win);
1194 }
1195 }
1197 /*
1198 * Navigation
1199 */
1201 /* Scrolling backend */
1202 static void
1203 do_scroll_view(struct view *view, int lines, bool redraw)
1204 {
1205 /* The rendering expects the new offset. */
1206 view->offset += lines;
1208 assert(0 <= view->offset && view->offset < view->lines);
1209 assert(lines);
1211 /* Redraw the whole screen if scrolling is pointless. */
1212 if (view->height < ABS(lines)) {
1213 redraw_view(view);
1215 } else {
1216 int line = lines > 0 ? view->height - lines : 0;
1217 int end = line + ABS(lines);
1219 wscrl(view->win, lines);
1221 for (; line < end; line++) {
1222 if (!draw_view_line(view, line))
1223 break;
1224 }
1225 }
1227 /* Move current line into the view. */
1228 if (view->lineno < view->offset) {
1229 view->lineno = view->offset;
1230 draw_view_line(view, 0);
1232 } else if (view->lineno >= view->offset + view->height) {
1233 if (view->lineno == view->offset + view->height) {
1234 /* Clear the hidden line so it doesn't show if the view
1235 * is scrolled up. */
1236 wmove(view->win, view->height, 0);
1237 wclrtoeol(view->win);
1238 }
1239 view->lineno = view->offset + view->height - 1;
1240 draw_view_line(view, view->lineno - view->offset);
1241 }
1243 assert(view->offset <= view->lineno && view->lineno < view->lines);
1245 if (!redraw)
1246 return;
1248 redrawwin(view->win);
1249 wrefresh(view->win);
1250 report("");
1251 }
1253 /* Scroll frontend */
1254 static void
1255 scroll_view(struct view *view, enum request request)
1256 {
1257 int lines = 1;
1259 switch (request) {
1260 case REQ_SCROLL_PAGE_DOWN:
1261 lines = view->height;
1262 case REQ_SCROLL_LINE_DOWN:
1263 if (view->offset + lines > view->lines)
1264 lines = view->lines - view->offset;
1266 if (lines == 0 || view->offset + view->height >= view->lines) {
1267 report("Cannot scroll beyond the last line");
1268 return;
1269 }
1270 break;
1272 case REQ_SCROLL_PAGE_UP:
1273 lines = view->height;
1274 case REQ_SCROLL_LINE_UP:
1275 if (lines > view->offset)
1276 lines = view->offset;
1278 if (lines == 0) {
1279 report("Cannot scroll beyond the first line");
1280 return;
1281 }
1283 lines = -lines;
1284 break;
1286 default:
1287 die("request %d not handled in switch", request);
1288 }
1290 do_scroll_view(view, lines, TRUE);
1291 }
1293 /* Cursor moving */
1294 static void
1295 move_view(struct view *view, enum request request, bool redraw)
1296 {
1297 int steps;
1299 switch (request) {
1300 case REQ_MOVE_FIRST_LINE:
1301 steps = -view->lineno;
1302 break;
1304 case REQ_MOVE_LAST_LINE:
1305 steps = view->lines - view->lineno - 1;
1306 break;
1308 case REQ_MOVE_PAGE_UP:
1309 steps = view->height > view->lineno
1310 ? -view->lineno : -view->height;
1311 break;
1313 case REQ_MOVE_PAGE_DOWN:
1314 steps = view->lineno + view->height >= view->lines
1315 ? view->lines - view->lineno - 1 : view->height;
1316 break;
1318 case REQ_MOVE_UP:
1319 steps = -1;
1320 break;
1322 case REQ_MOVE_DOWN:
1323 steps = 1;
1324 break;
1326 default:
1327 die("request %d not handled in switch", request);
1328 }
1330 if (steps <= 0 && view->lineno == 0) {
1331 report("Cannot move beyond the first line");
1332 return;
1334 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1335 report("Cannot move beyond the last line");
1336 return;
1337 }
1339 /* Move the current line */
1340 view->lineno += steps;
1341 assert(0 <= view->lineno && view->lineno < view->lines);
1343 /* Repaint the old "current" line if we be scrolling */
1344 if (ABS(steps) < view->height) {
1345 int prev_lineno = view->lineno - steps - view->offset;
1347 wmove(view->win, prev_lineno, 0);
1348 wclrtoeol(view->win);
1349 draw_view_line(view, prev_lineno);
1350 }
1352 /* Check whether the view needs to be scrolled */
1353 if (view->lineno < view->offset ||
1354 view->lineno >= view->offset + view->height) {
1355 if (steps < 0 && -steps > view->offset) {
1356 steps = -view->offset;
1358 } else if (steps > 0) {
1359 if (view->lineno == view->lines - 1 &&
1360 view->lines > view->height) {
1361 steps = view->lines - view->offset - 1;
1362 if (steps >= view->height)
1363 steps -= view->height - 1;
1364 }
1365 }
1367 do_scroll_view(view, steps, redraw);
1368 return;
1369 }
1371 /* Draw the current line */
1372 draw_view_line(view, view->lineno - view->offset);
1374 if (!redraw)
1375 return;
1377 redrawwin(view->win);
1378 wrefresh(view->win);
1379 report("");
1380 }
1383 /*
1384 * Incremental updating
1385 */
1387 static void
1388 end_update(struct view *view)
1389 {
1390 if (!view->pipe)
1391 return;
1392 set_nonblocking_input(FALSE);
1393 if (view->pipe == stdin)
1394 fclose(view->pipe);
1395 else
1396 pclose(view->pipe);
1397 view->pipe = NULL;
1398 }
1400 static bool
1401 begin_update(struct view *view)
1402 {
1403 const char *id = view->id;
1405 if (view->pipe)
1406 end_update(view);
1408 if (opt_cmd[0]) {
1409 string_copy(view->cmd, opt_cmd);
1410 opt_cmd[0] = 0;
1411 /* When running random commands, the view ref could have become
1412 * invalid so clear it. */
1413 view->ref[0] = 0;
1414 } else {
1415 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1417 if (!string_format(view->cmd, format, id, id, id, id, id))
1418 return FALSE;
1419 }
1421 /* Special case for the pager view. */
1422 if (opt_pipe) {
1423 view->pipe = opt_pipe;
1424 opt_pipe = NULL;
1425 } else {
1426 view->pipe = popen(view->cmd, "r");
1427 }
1429 if (!view->pipe)
1430 return FALSE;
1432 set_nonblocking_input(TRUE);
1434 view->offset = 0;
1435 view->lines = 0;
1436 view->lineno = 0;
1437 string_copy(view->vid, id);
1439 if (view->line) {
1440 int i;
1442 for (i = 0; i < view->lines; i++)
1443 if (view->line[i].data)
1444 free(view->line[i].data);
1446 free(view->line);
1447 view->line = NULL;
1448 }
1450 view->start_time = time(NULL);
1452 return TRUE;
1453 }
1455 static struct line *
1456 realloc_lines(struct view *view, size_t line_size)
1457 {
1458 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1460 if (!tmp)
1461 return NULL;
1463 view->line = tmp;
1464 view->line_size = line_size;
1465 return view->line;
1466 }
1468 static bool
1469 update_view(struct view *view)
1470 {
1471 char buffer[BUFSIZ];
1472 char *line;
1473 /* The number of lines to read. If too low it will cause too much
1474 * redrawing (and possible flickering), if too high responsiveness
1475 * will suffer. */
1476 unsigned long lines = view->height;
1477 int redraw_from = -1;
1479 if (!view->pipe)
1480 return TRUE;
1482 /* Only redraw if lines are visible. */
1483 if (view->offset + view->height >= view->lines)
1484 redraw_from = view->lines - view->offset;
1486 if (!realloc_lines(view, view->lines + lines))
1487 goto alloc_error;
1489 while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
1490 int linelen = strlen(line);
1492 if (linelen)
1493 line[linelen - 1] = 0;
1495 if (!view->ops->read(view, line))
1496 goto alloc_error;
1498 if (lines-- == 1)
1499 break;
1500 }
1502 {
1503 int digits;
1505 lines = view->lines;
1506 for (digits = 0; lines; digits++)
1507 lines /= 10;
1509 /* Keep the displayed view in sync with line number scaling. */
1510 if (digits != view->digits) {
1511 view->digits = digits;
1512 redraw_from = 0;
1513 }
1514 }
1516 if (redraw_from >= 0) {
1517 /* If this is an incremental update, redraw the previous line
1518 * since for commits some members could have changed when
1519 * loading the main view. */
1520 if (redraw_from > 0)
1521 redraw_from--;
1523 /* Incrementally draw avoids flickering. */
1524 redraw_view_from(view, redraw_from);
1525 }
1527 /* Update the title _after_ the redraw so that if the redraw picks up a
1528 * commit reference in view->ref it'll be available here. */
1529 update_view_title(view);
1531 if (ferror(view->pipe)) {
1532 report("Failed to read: %s", strerror(errno));
1533 goto end;
1535 } else if (feof(view->pipe)) {
1536 report("");
1537 goto end;
1538 }
1540 return TRUE;
1542 alloc_error:
1543 report("Allocation failure");
1545 end:
1546 end_update(view);
1547 return FALSE;
1548 }
1551 /*
1552 * View opening
1553 */
1555 static void open_help_view(struct view *view)
1556 {
1557 char buf[BUFSIZ];
1558 int lines = ARRAY_SIZE(req_info) + 2;
1559 int i;
1561 if (view->lines > 0)
1562 return;
1564 for (i = 0; i < ARRAY_SIZE(req_info); i++)
1565 if (!req_info[i].request)
1566 lines++;
1568 view->line = calloc(lines, sizeof(*view->line));
1569 if (!view->line) {
1570 report("Allocation failure");
1571 return;
1572 }
1574 view->ops->read(view, "Quick reference for tig keybindings:");
1576 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
1577 char *key;
1579 if (!req_info[i].request) {
1580 view->ops->read(view, "");
1581 view->ops->read(view, req_info[i].help);
1582 continue;
1583 }
1585 key = get_key(req_info[i].request);
1586 if (!string_format(buf, "%-25s %s", key, req_info[i].help))
1587 continue;
1589 view->ops->read(view, buf);
1590 }
1591 }
1593 enum open_flags {
1594 OPEN_DEFAULT = 0, /* Use default view switching. */
1595 OPEN_SPLIT = 1, /* Split current view. */
1596 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
1597 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
1598 };
1600 static void
1601 open_view(struct view *prev, enum request request, enum open_flags flags)
1602 {
1603 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
1604 bool split = !!(flags & OPEN_SPLIT);
1605 bool reload = !!(flags & OPEN_RELOAD);
1606 struct view *view = VIEW(request);
1607 int nviews = displayed_views();
1608 struct view *base_view = display[0];
1610 if (view == prev && nviews == 1 && !reload) {
1611 report("Already in %s view", view->name);
1612 return;
1613 }
1615 if (view == VIEW(REQ_VIEW_HELP)) {
1616 open_help_view(view);
1618 } else if ((reload || strcmp(view->vid, view->id)) &&
1619 !begin_update(view)) {
1620 report("Failed to load %s view", view->name);
1621 return;
1622 }
1624 if (split) {
1625 display[1] = view;
1626 if (!backgrounded)
1627 current_view = 1;
1628 } else {
1629 /* Maximize the current view. */
1630 memset(display, 0, sizeof(display));
1631 current_view = 0;
1632 display[current_view] = view;
1633 }
1635 /* Resize the view when switching between split- and full-screen,
1636 * or when switching between two different full-screen views. */
1637 if (nviews != displayed_views() ||
1638 (nviews == 1 && base_view != display[0]))
1639 resize_display();
1641 if (split && prev->lineno - prev->offset >= prev->height) {
1642 /* Take the title line into account. */
1643 int lines = prev->lineno - prev->offset - prev->height + 1;
1645 /* Scroll the view that was split if the current line is
1646 * outside the new limited view. */
1647 do_scroll_view(prev, lines, TRUE);
1648 }
1650 if (prev && view != prev) {
1651 if (split && !backgrounded) {
1652 /* "Blur" the previous view. */
1653 update_view_title(prev);
1654 }
1656 view->parent = prev;
1657 }
1659 if (view->pipe && view->lines == 0) {
1660 /* Clear the old view and let the incremental updating refill
1661 * the screen. */
1662 wclear(view->win);
1663 report("");
1664 } else {
1665 redraw_view(view);
1666 report("");
1667 }
1669 /* If the view is backgrounded the above calls to report()
1670 * won't redraw the view title. */
1671 if (backgrounded)
1672 update_view_title(view);
1673 }
1676 /*
1677 * User request switch noodle
1678 */
1680 static int
1681 view_driver(struct view *view, enum request request)
1682 {
1683 int i;
1685 switch (request) {
1686 case REQ_MOVE_UP:
1687 case REQ_MOVE_DOWN:
1688 case REQ_MOVE_PAGE_UP:
1689 case REQ_MOVE_PAGE_DOWN:
1690 case REQ_MOVE_FIRST_LINE:
1691 case REQ_MOVE_LAST_LINE:
1692 move_view(view, request, TRUE);
1693 break;
1695 case REQ_SCROLL_LINE_DOWN:
1696 case REQ_SCROLL_LINE_UP:
1697 case REQ_SCROLL_PAGE_DOWN:
1698 case REQ_SCROLL_PAGE_UP:
1699 scroll_view(view, request);
1700 break;
1702 case REQ_VIEW_MAIN:
1703 case REQ_VIEW_DIFF:
1704 case REQ_VIEW_LOG:
1705 case REQ_VIEW_HELP:
1706 case REQ_VIEW_PAGER:
1707 open_view(view, request, OPEN_DEFAULT);
1708 break;
1710 case REQ_NEXT:
1711 case REQ_PREVIOUS:
1712 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
1714 if (view == VIEW(REQ_VIEW_DIFF) &&
1715 view->parent == VIEW(REQ_VIEW_MAIN)) {
1716 bool redraw = display[1] == view;
1718 view = view->parent;
1719 move_view(view, request, redraw);
1720 if (redraw)
1721 update_view_title(view);
1722 } else {
1723 move_view(view, request, TRUE);
1724 break;
1725 }
1726 /* Fall-through */
1728 case REQ_ENTER:
1729 if (!view->lines) {
1730 report("Nothing to enter");
1731 break;
1732 }
1733 return view->ops->enter(view, &view->line[view->lineno]);
1735 case REQ_VIEW_NEXT:
1736 {
1737 int nviews = displayed_views();
1738 int next_view = (current_view + 1) % nviews;
1740 if (next_view == current_view) {
1741 report("Only one view is displayed");
1742 break;
1743 }
1745 current_view = next_view;
1746 /* Blur out the title of the previous view. */
1747 update_view_title(view);
1748 report("");
1749 break;
1750 }
1751 case REQ_TOGGLE_LINENO:
1752 opt_line_number = !opt_line_number;
1753 redraw_display();
1754 break;
1756 case REQ_TOGGLE_REV_GRAPH:
1757 opt_rev_graph = !opt_rev_graph;
1758 redraw_display();
1759 break;
1761 case REQ_PROMPT:
1762 /* Always reload^Wrerun commands from the prompt. */
1763 open_view(view, opt_request, OPEN_RELOAD);
1764 break;
1766 case REQ_STOP_LOADING:
1767 for (i = 0; i < ARRAY_SIZE(views); i++) {
1768 view = &views[i];
1769 if (view->pipe)
1770 report("Stopped loading the %s view", view->name),
1771 end_update(view);
1772 }
1773 break;
1775 case REQ_SHOW_VERSION:
1776 report("%s (built %s)", VERSION, __DATE__);
1777 return TRUE;
1779 case REQ_SCREEN_RESIZE:
1780 resize_display();
1781 /* Fall-through */
1782 case REQ_SCREEN_REDRAW:
1783 redraw_display();
1784 break;
1786 case REQ_SCREEN_UPDATE:
1787 doupdate();
1788 return TRUE;
1790 case REQ_VIEW_CLOSE:
1791 /* XXX: Mark closed views by letting view->parent point to the
1792 * view itself. Parents to closed view should never be
1793 * followed. */
1794 if (view->parent &&
1795 view->parent->parent != view->parent) {
1796 memset(display, 0, sizeof(display));
1797 current_view = 0;
1798 display[current_view] = view->parent;
1799 view->parent = view;
1800 resize_display();
1801 redraw_display();
1802 break;
1803 }
1804 /* Fall-through */
1805 case REQ_QUIT:
1806 return FALSE;
1808 default:
1809 /* An unknown key will show most commonly used commands. */
1810 report("Unknown key, press 'h' for help");
1811 return TRUE;
1812 }
1814 return TRUE;
1815 }
1818 /*
1819 * Pager backend
1820 */
1822 static bool
1823 pager_draw(struct view *view, struct line *line, unsigned int lineno)
1824 {
1825 char *text = line->data;
1826 enum line_type type = line->type;
1827 int textlen = strlen(text);
1828 int attr;
1830 wmove(view->win, lineno, 0);
1832 if (view->offset + lineno == view->lineno) {
1833 if (type == LINE_COMMIT) {
1834 string_copy(view->ref, text + 7);
1835 string_copy(ref_commit, view->ref);
1836 }
1838 type = LINE_CURSOR;
1839 wchgat(view->win, -1, 0, type, NULL);
1840 }
1842 attr = get_line_attr(type);
1843 wattrset(view->win, attr);
1845 if (opt_line_number || opt_tab_size < TABSIZE) {
1846 static char spaces[] = " ";
1847 int col_offset = 0, col = 0;
1849 if (opt_line_number) {
1850 unsigned long real_lineno = view->offset + lineno + 1;
1852 if (real_lineno == 1 ||
1853 (real_lineno % opt_num_interval) == 0) {
1854 wprintw(view->win, "%.*d", view->digits, real_lineno);
1856 } else {
1857 waddnstr(view->win, spaces,
1858 MIN(view->digits, STRING_SIZE(spaces)));
1859 }
1860 waddstr(view->win, ": ");
1861 col_offset = view->digits + 2;
1862 }
1864 while (text && col_offset + col < view->width) {
1865 int cols_max = view->width - col_offset - col;
1866 char *pos = text;
1867 int cols;
1869 if (*text == '\t') {
1870 text++;
1871 assert(sizeof(spaces) > TABSIZE);
1872 pos = spaces;
1873 cols = opt_tab_size - (col % opt_tab_size);
1875 } else {
1876 text = strchr(text, '\t');
1877 cols = line ? text - pos : strlen(pos);
1878 }
1880 waddnstr(view->win, pos, MIN(cols, cols_max));
1881 col += cols;
1882 }
1884 } else {
1885 int col = 0, pos = 0;
1887 for (; pos < textlen && col < view->width; pos++, col++)
1888 if (text[pos] == '\t')
1889 col += TABSIZE - (col % TABSIZE) - 1;
1891 waddnstr(view->win, text, pos);
1892 }
1894 return TRUE;
1895 }
1897 static void
1898 add_pager_refs(struct view *view, struct line *line)
1899 {
1900 char buf[1024];
1901 char *data = line->data;
1902 struct ref **refs;
1903 int bufpos = 0, refpos = 0;
1904 const char *sep = "Refs: ";
1906 assert(line->type == LINE_COMMIT);
1908 refs = get_refs(data + STRING_SIZE("commit "));
1909 if (!refs)
1910 return;
1912 do {
1913 struct ref *ref = refs[refpos];
1914 char *fmt = ref->tag ? "%s[%s]" : "%s%s";
1916 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
1917 return;
1918 sep = ", ";
1919 } while (refs[refpos++]->next);
1921 if (!realloc_lines(view, view->line_size + 1))
1922 return;
1924 line = &view->line[view->lines];
1925 line->data = strdup(buf);
1926 if (!line->data)
1927 return;
1929 line->type = LINE_PP_REFS;
1930 view->lines++;
1931 }
1933 static bool
1934 pager_read(struct view *view, char *data)
1935 {
1936 struct line *line = &view->line[view->lines];
1938 line->data = strdup(data);
1939 if (!line->data)
1940 return FALSE;
1942 line->type = get_line_type(line->data);
1943 view->lines++;
1945 if (line->type == LINE_COMMIT &&
1946 (view == VIEW(REQ_VIEW_DIFF) ||
1947 view == VIEW(REQ_VIEW_LOG)))
1948 add_pager_refs(view, line);
1950 return TRUE;
1951 }
1953 static bool
1954 pager_enter(struct view *view, struct line *line)
1955 {
1956 int split = 0;
1958 if (line->type == LINE_COMMIT &&
1959 (view == VIEW(REQ_VIEW_LOG) ||
1960 view == VIEW(REQ_VIEW_PAGER))) {
1961 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
1962 split = 1;
1963 }
1965 /* Always scroll the view even if it was split. That way
1966 * you can use Enter to scroll through the log view and
1967 * split open each commit diff. */
1968 scroll_view(view, REQ_SCROLL_LINE_DOWN);
1970 /* FIXME: A minor workaround. Scrolling the view will call report("")
1971 * but if we are scrolling a non-current view this won't properly
1972 * update the view title. */
1973 if (split)
1974 update_view_title(view);
1976 return TRUE;
1977 }
1979 static struct view_ops pager_ops = {
1980 "line",
1981 pager_draw,
1982 pager_read,
1983 pager_enter,
1984 };
1987 /*
1988 * Main view backend
1989 */
1991 struct commit {
1992 char id[41]; /* SHA1 ID. */
1993 char title[75]; /* First line of the commit message. */
1994 char author[75]; /* Author of the commit. */
1995 struct tm time; /* Date from the author ident. */
1996 struct ref **refs; /* Repository references. */
1997 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
1998 size_t graph_size; /* The width of the graph array. */
1999 };
2001 static bool
2002 main_draw(struct view *view, struct line *line, unsigned int lineno)
2003 {
2004 char buf[DATE_COLS + 1];
2005 struct commit *commit = line->data;
2006 enum line_type type;
2007 int col = 0;
2008 size_t timelen;
2009 size_t authorlen;
2010 int trimmed = 1;
2012 if (!*commit->author)
2013 return FALSE;
2015 wmove(view->win, lineno, col);
2017 if (view->offset + lineno == view->lineno) {
2018 string_copy(view->ref, commit->id);
2019 string_copy(ref_commit, view->ref);
2020 type = LINE_CURSOR;
2021 wattrset(view->win, get_line_attr(type));
2022 wchgat(view->win, -1, 0, type, NULL);
2024 } else {
2025 type = LINE_MAIN_COMMIT;
2026 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
2027 }
2029 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
2030 waddnstr(view->win, buf, timelen);
2031 waddstr(view->win, " ");
2033 col += DATE_COLS;
2034 wmove(view->win, lineno, col);
2035 if (type != LINE_CURSOR)
2036 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
2038 if (opt_utf8) {
2039 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
2040 } else {
2041 authorlen = strlen(commit->author);
2042 if (authorlen > AUTHOR_COLS - 2) {
2043 authorlen = AUTHOR_COLS - 2;
2044 trimmed = 1;
2045 }
2046 }
2048 if (trimmed) {
2049 waddnstr(view->win, commit->author, authorlen);
2050 if (type != LINE_CURSOR)
2051 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
2052 waddch(view->win, '~');
2053 } else {
2054 waddstr(view->win, commit->author);
2055 }
2057 col += AUTHOR_COLS;
2058 if (type != LINE_CURSOR)
2059 wattrset(view->win, A_NORMAL);
2061 if (opt_rev_graph && commit->graph_size) {
2062 size_t i;
2064 wmove(view->win, lineno, col);
2065 /* Using waddch() instead of waddnstr() ensures that
2066 * they'll be rendered correctly for the cursor line. */
2067 for (i = 0; i < commit->graph_size; i++)
2068 waddch(view->win, commit->graph[i]);
2070 col += commit->graph_size + 1;
2071 }
2073 wmove(view->win, lineno, col);
2075 if (commit->refs) {
2076 size_t i = 0;
2078 do {
2079 if (type == LINE_CURSOR)
2080 ;
2081 else if (commit->refs[i]->tag)
2082 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
2083 else
2084 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
2085 waddstr(view->win, "[");
2086 waddstr(view->win, commit->refs[i]->name);
2087 waddstr(view->win, "]");
2088 if (type != LINE_CURSOR)
2089 wattrset(view->win, A_NORMAL);
2090 waddstr(view->win, " ");
2091 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
2092 } while (commit->refs[i++]->next);
2093 }
2095 if (type != LINE_CURSOR)
2096 wattrset(view->win, get_line_attr(type));
2098 {
2099 int titlelen = strlen(commit->title);
2101 if (col + titlelen > view->width)
2102 titlelen = view->width - col;
2104 waddnstr(view->win, commit->title, titlelen);
2105 }
2107 return TRUE;
2108 }
2110 /* Reads git log --pretty=raw output and parses it into the commit struct. */
2111 static bool
2112 main_read(struct view *view, char *line)
2113 {
2114 enum line_type type = get_line_type(line);
2115 struct commit *commit = view->lines
2116 ? view->line[view->lines - 1].data : NULL;
2118 switch (type) {
2119 case LINE_COMMIT:
2120 commit = calloc(1, sizeof(struct commit));
2121 if (!commit)
2122 return FALSE;
2124 line += STRING_SIZE("commit ");
2126 view->line[view->lines++].data = commit;
2127 string_copy(commit->id, line);
2128 commit->refs = get_refs(commit->id);
2129 commit->graph[commit->graph_size++] = ACS_LTEE;
2130 break;
2132 case LINE_AUTHOR:
2133 {
2134 char *ident = line + STRING_SIZE("author ");
2135 char *end = strchr(ident, '<');
2137 if (!commit)
2138 break;
2140 if (end) {
2141 for (; end > ident && isspace(end[-1]); end--) ;
2142 *end = 0;
2143 }
2145 string_copy(commit->author, ident);
2147 /* Parse epoch and timezone */
2148 if (end) {
2149 char *secs = strchr(end + 1, '>');
2150 char *zone;
2151 time_t time;
2153 if (!secs || secs[1] != ' ')
2154 break;
2156 secs += 2;
2157 time = (time_t) atol(secs);
2158 zone = strchr(secs, ' ');
2159 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
2160 long tz;
2162 zone++;
2163 tz = ('0' - zone[1]) * 60 * 60 * 10;
2164 tz += ('0' - zone[2]) * 60 * 60;
2165 tz += ('0' - zone[3]) * 60;
2166 tz += ('0' - zone[4]) * 60;
2168 if (zone[0] == '-')
2169 tz = -tz;
2171 time -= tz;
2172 }
2173 gmtime_r(&time, &commit->time);
2174 }
2175 break;
2176 }
2177 default:
2178 if (!commit)
2179 break;
2181 /* Fill in the commit title if it has not already been set. */
2182 if (commit->title[0])
2183 break;
2185 /* Require titles to start with a non-space character at the
2186 * offset used by git log. */
2187 /* FIXME: More gracefull handling of titles; append "..." to
2188 * shortened titles, etc. */
2189 if (strncmp(line, " ", 4) ||
2190 isspace(line[4]))
2191 break;
2193 string_copy(commit->title, line + 4);
2194 }
2196 return TRUE;
2197 }
2199 static bool
2200 main_enter(struct view *view, struct line *line)
2201 {
2202 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2204 open_view(view, REQ_VIEW_DIFF, flags);
2205 return TRUE;
2206 }
2208 static struct view_ops main_ops = {
2209 "commit",
2210 main_draw,
2211 main_read,
2212 main_enter,
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;
2529 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
2530 /* Commits referenced by tags has "^{}" appended. */
2531 if (name[namelen - 1] != '}')
2532 return OK;
2534 while (namelen > 0 && name[namelen] != '^')
2535 namelen--;
2537 tag = TRUE;
2538 namelen -= STRING_SIZE("refs/tags/");
2539 name += STRING_SIZE("refs/tags/");
2541 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
2542 namelen -= STRING_SIZE("refs/heads/");
2543 name += STRING_SIZE("refs/heads/");
2545 } else if (!strcmp(name, "HEAD")) {
2546 return OK;
2547 }
2549 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
2550 if (!refs)
2551 return ERR;
2553 ref = &refs[refs_size++];
2554 ref->name = malloc(namelen + 1);
2555 if (!ref->name)
2556 return ERR;
2558 strncpy(ref->name, name, namelen);
2559 ref->name[namelen] = 0;
2560 ref->tag = tag;
2561 string_copy(ref->id, id);
2563 return OK;
2564 }
2566 static int
2567 load_refs(void)
2568 {
2569 const char *cmd_env = getenv("TIG_LS_REMOTE");
2570 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
2572 return read_properties(popen(cmd, "r"), "\t", read_ref);
2573 }
2575 static int
2576 read_repo_config_option(char *name, int namelen, char *value, int valuelen)
2577 {
2578 if (!strcmp(name, "i18n.commitencoding"))
2579 string_copy(opt_encoding, value);
2581 return OK;
2582 }
2584 static int
2585 load_repo_config(void)
2586 {
2587 return read_properties(popen("git repo-config --list", "r"),
2588 "=", read_repo_config_option);
2589 }
2591 static int
2592 read_properties(FILE *pipe, const char *separators,
2593 int (*read_property)(char *, int, char *, int))
2594 {
2595 char buffer[BUFSIZ];
2596 char *name;
2597 int state = OK;
2599 if (!pipe)
2600 return ERR;
2602 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
2603 char *value;
2604 size_t namelen;
2605 size_t valuelen;
2607 name = chomp_string(name);
2608 namelen = strcspn(name, separators);
2610 if (name[namelen]) {
2611 name[namelen] = 0;
2612 value = chomp_string(name + namelen + 1);
2613 valuelen = strlen(value);
2615 } else {
2616 value = "";
2617 valuelen = 0;
2618 }
2620 state = read_property(name, namelen, value, valuelen);
2621 }
2623 if (state != ERR && ferror(pipe))
2624 state = ERR;
2626 pclose(pipe);
2628 return state;
2629 }
2632 /*
2633 * Main
2634 */
2636 static void __NORETURN
2637 quit(int sig)
2638 {
2639 /* XXX: Restore tty modes and let the OS cleanup the rest! */
2640 if (cursed)
2641 endwin();
2642 exit(0);
2643 }
2645 static void __NORETURN
2646 die(const char *err, ...)
2647 {
2648 va_list args;
2650 endwin();
2652 va_start(args, err);
2653 fputs("tig: ", stderr);
2654 vfprintf(stderr, err, args);
2655 fputs("\n", stderr);
2656 va_end(args);
2658 exit(1);
2659 }
2661 int
2662 main(int argc, char *argv[])
2663 {
2664 struct view *view;
2665 enum request request;
2666 size_t i;
2668 signal(SIGINT, quit);
2670 if (load_options() == ERR)
2671 die("Failed to load user config.");
2673 /* Load the repo config file so options can be overwritten from
2674 * the command line. */
2675 if (load_repo_config() == ERR)
2676 die("Failed to load repo config.");
2678 if (!parse_options(argc, argv))
2679 return 0;
2681 if (load_refs() == ERR)
2682 die("Failed to load refs.");
2684 /* Require a git repository unless when running in pager mode. */
2685 if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
2686 die("Not a git repository");
2688 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
2689 view->cmd_env = getenv(view->cmd_env);
2691 request = opt_request;
2693 init_display();
2695 while (view_driver(display[current_view], request)) {
2696 int key;
2697 int i;
2699 foreach_view (view, i)
2700 update_view(view);
2702 /* Refresh, accept single keystroke of input */
2703 key = wgetch(status_win);
2704 request = get_request(key);
2706 /* Some low-level request handling. This keeps access to
2707 * status_win restricted. */
2708 switch (request) {
2709 case REQ_PROMPT:
2710 report(":");
2711 /* Temporarily switch to line-oriented and echoed
2712 * input. */
2713 nocbreak();
2714 echo();
2716 if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) {
2717 memcpy(opt_cmd, "git ", 4);
2718 opt_request = REQ_VIEW_PAGER;
2719 } else {
2720 report("Prompt interrupted by loading view, "
2721 "press 'z' to stop loading views");
2722 request = REQ_SCREEN_UPDATE;
2723 }
2725 noecho();
2726 cbreak();
2727 break;
2729 case REQ_SCREEN_RESIZE:
2730 {
2731 int height, width;
2733 getmaxyx(stdscr, height, width);
2735 /* Resize the status view and let the view driver take
2736 * care of resizing the displayed views. */
2737 wresize(status_win, 1, width);
2738 mvwin(status_win, height - 1, 0);
2739 wrefresh(status_win);
2740 break;
2741 }
2742 default:
2743 break;
2744 }
2745 }
2747 quit(0);
2749 return 0;
2750 }