+ if ((reload || strcmp(view->vid, view->id)) &&
+ !begin_update(view)) {
+ report("Failed to load %s view", view->name);
+ return;
+ }
+
+ if (split) {
+ display[current_view + 1] = view;
+ if (!backgrounded)
+ current_view++;
+ } else {
+ /* Maximize the current view. */
+ memset(display, 0, sizeof(display));
+ current_view = 0;
+ display[current_view] = view;
+ }
+
+ resize_display();
+
+ if (split && prev->lineno - prev->offset >= prev->height) {
+ /* Take the title line into account. */
+ int lines = prev->lineno - prev->offset - prev->height + 1;
+
+ /* Scroll the view that was split if the current line is
+ * outside the new limited view. */
+ do_scroll_view(prev, lines, TRUE);
+ }
+
+ if (prev && view != prev) {
+ /* "Blur" the previous view. */
+ if (!backgrounded)
+ update_view_title(prev);
+
+ /* Continue loading split views in the background. */
+ if (!split)
+ end_update(prev);
+ view->parent = prev;
+ }
+
+ if (view->pipe) {
+ /* Clear the old view and let the incremental updating refill
+ * the screen. */
+ wclear(view->win);
+ report("Loading...");
+ } else {
+ redraw_view(view);
+ if (view == VIEW(REQ_VIEW_HELP))
+ report("%s", TIG_HELP);
+ else
+ report("");
+ }
+
+ /* If the view is backgrounded the above calls to report()
+ * won't redraw the view title. */
+ if (backgrounded)
+ update_view_title(view);
+}
+
+
+/*
+ * User request switch noodle
+ */
+
+static int
+view_driver(struct view *view, enum request request)
+{
+ int i;
+
+ switch (request) {
+ case REQ_MOVE_UP:
+ case REQ_MOVE_DOWN:
+ case REQ_MOVE_PAGE_UP:
+ case REQ_MOVE_PAGE_DOWN:
+ case REQ_MOVE_FIRST_LINE:
+ case REQ_MOVE_LAST_LINE:
+ move_view(view, request, TRUE);
+ break;
+
+ case REQ_SCROLL_LINE_DOWN:
+ case REQ_SCROLL_LINE_UP:
+ case REQ_SCROLL_PAGE_DOWN:
+ case REQ_SCROLL_PAGE_UP:
+ scroll_view(view, request);
+ break;
+
+ case REQ_VIEW_MAIN:
+ case REQ_VIEW_DIFF:
+ case REQ_VIEW_LOG:
+ case REQ_VIEW_HELP:
+ case REQ_VIEW_PAGER:
+ open_view(view, request, OPEN_DEFAULT);
+ break;
+
+ case REQ_NEXT:
+ case REQ_PREVIOUS:
+ request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
+
+ if (view == VIEW(REQ_VIEW_DIFF) &&
+ view->parent == VIEW(REQ_VIEW_MAIN)) {
+ bool redraw = display[0] == VIEW(REQ_VIEW_MAIN);
+
+ view = view->parent;
+ move_view(view, request, redraw);
+ update_view_title(view);
+ } else {
+ move_view(view, request, TRUE);
+ break;
+ }
+ /* Fall-through */
+
+ case REQ_ENTER:
+ if (!view->lines) {
+ report("Nothing to enter");
+ break;
+ }
+ return view->ops->enter(view);
+
+ case REQ_VIEW_NEXT:
+ {
+ int nviews = display[1] ? 2 : 1;
+ int next_view = (current_view + 1) % nviews;
+
+ if (next_view == current_view) {
+ report("Only one view is displayed");
+ break;
+ }
+
+ current_view = next_view;
+ /* Blur out the title of the previous view. */
+ update_view_title(view);
+ report("");
+ break;
+ }
+ case REQ_TOGGLE_LINE_NUMBERS:
+ opt_line_number = !opt_line_number;
+ redraw_display();
+ break;
+
+ case REQ_PROMPT:
+ /* Always reload^Wrerun commands from the prompt. */
+ open_view(view, opt_request, OPEN_RELOAD);
+ break;
+
+ case REQ_STOP_LOADING:
+ foreach_view (view, i) {
+ if (view->pipe)
+ report("Stopped loaded the %s view", view->name),
+ end_update(view);
+ }
+ break;
+
+ case REQ_SHOW_VERSION:
+ report("%s (built %s)", VERSION, __DATE__);
+ return TRUE;
+
+ case REQ_SCREEN_RESIZE:
+ resize_display();
+ /* Fall-through */
+ case REQ_SCREEN_REDRAW:
+ redraw_display();
+ break;
+
+ case REQ_SCREEN_UPDATE:
+ doupdate();
+ return TRUE;
+
+ case REQ_VIEW_CLOSE:
+ if (view->parent) {
+ memset(display, 0, sizeof(display));
+ current_view = 0;
+ display[current_view] = view->parent;
+ view->parent = NULL;
+ resize_display();
+ redraw_display();
+ break;
+ }
+ /* Fall-through */
+ case REQ_QUIT:
+ return FALSE;
+
+ default:
+ /* An unknown key will show most commonly used commands. */
+ report("Unknown key, press 'h' for help");
+ return TRUE;
+ }
+
+ return TRUE;
+}
+
+
+/*
+ * Pager backend
+ */
+
+static bool
+pager_draw(struct view *view, unsigned int lineno)
+{
+ enum line_type type;
+ char *line;
+ int linelen;
+ int attr;
+
+ if (view->offset + lineno >= view->lines)
+ return FALSE;
+
+ line = view->line[view->offset + lineno];
+ type = get_line_type(line);
+
+ wmove(view->win, lineno, 0);
+
+ if (view->offset + lineno == view->lineno) {
+ if (type == LINE_COMMIT) {
+ string_copy(view->ref, line + 7);
+ string_copy(ref_commit, view->ref);
+ }
+
+ type = LINE_CURSOR;
+ wchgat(view->win, -1, 0, type, NULL);
+ }
+
+ attr = get_line_attr(type);
+ wattrset(view->win, attr);
+
+ linelen = strlen(line);
+
+ if (opt_line_number || opt_tab_size < TABSIZE) {
+ static char spaces[] = " ";
+ int col_offset = 0, col = 0;
+
+ if (opt_line_number) {
+ unsigned long real_lineno = view->offset + lineno + 1;
+
+ if (real_lineno == 1 ||
+ (real_lineno % opt_num_interval) == 0) {
+ wprintw(view->win, "%.*d", view->digits, real_lineno);
+
+ } else {
+ waddnstr(view->win, spaces,
+ MIN(view->digits, STRING_SIZE(spaces)));
+ }
+ waddstr(view->win, ": ");
+ col_offset = view->digits + 2;
+ }
+
+ while (line && col_offset + col < view->width) {
+ int cols_max = view->width - col_offset - col;
+ char *text = line;
+ int cols;
+
+ if (*line == '\t') {
+ assert(sizeof(spaces) > TABSIZE);
+ line++;
+ text = spaces;
+ cols = opt_tab_size - (col % opt_tab_size);
+
+ } else {
+ line = strchr(line, '\t');
+ cols = line ? line - text : strlen(text);
+ }
+
+ waddnstr(view->win, text, MIN(cols, cols_max));
+ col += cols;
+ }
+
+ } else {
+ int col = 0, pos = 0;
+
+ for (; pos < linelen && col < view->width; pos++, col++)
+ if (line[pos] == '\t')
+ col += TABSIZE - (col % TABSIZE) - 1;
+
+ waddnstr(view->win, line, pos);
+ }
+
+ return TRUE;
+}
+
+static bool
+pager_read(struct view *view, char *line)
+{
+ /* Compress empty lines in the help view. */
+ if (view == VIEW(REQ_VIEW_HELP) &&
+ !*line &&
+ view->lines &&
+ !*((char *) view->line[view->lines - 1]))
+ return TRUE;
+
+ view->line[view->lines] = strdup(line);
+ if (!view->line[view->lines])
+ return FALSE;
+
+ view->lines++;
+ return TRUE;
+}
+
+static bool
+pager_enter(struct view *view)
+{
+ char *line = view->line[view->lineno];
+ int split = 0;
+
+ if ((view == VIEW(REQ_VIEW_LOG) ||
+ view == VIEW(REQ_VIEW_PAGER)) &&
+ get_line_type(line) == LINE_COMMIT) {
+ open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
+ split = 1;
+ }
+
+ /* Always scroll the view even if it was split. That way
+ * you can use Enter to scroll through the log view and
+ * split open each commit diff. */
+ scroll_view(view, REQ_SCROLL_LINE_DOWN);
+
+ /* FIXME: A minor workaround. Scrolling the view will call report("")
+ * but if we are scolling a non-current view this won't properly update
+ * the view title. */
+ if (split)
+ update_view_title(view);
+
+ return TRUE;
+}
+
+static struct view_ops pager_ops = {
+ "line",
+ pager_draw,
+ pager_read,
+ pager_enter,
+};
+
+
+/*
+ * Main view backend
+ */
+
+struct commit {
+ char id[41]; /* SHA1 ID. */
+ char title[75]; /* The first line of the commit message. */
+ char author[75]; /* The author of the commit. */
+ struct tm time; /* Date from the author ident. */
+ struct ref **refs; /* Repository references; tags & branch heads. */
+};
+
+static bool
+main_draw(struct view *view, unsigned int lineno)
+{
+ char buf[DATE_COLS + 1];
+ struct commit *commit;
+ enum line_type type;
+ int col = 0;
+ size_t timelen;
+ size_t authorlen;
+ int trimmed;
+
+ if (view->offset + lineno >= view->lines)
+ return FALSE;
+
+ commit = view->line[view->offset + lineno];
+ if (!*commit->author)
+ return FALSE;
+
+ wmove(view->win, lineno, col);
+
+ if (view->offset + lineno == view->lineno) {
+ string_copy(view->ref, commit->id);
+ string_copy(ref_commit, view->ref);
+ type = LINE_CURSOR;
+ wattrset(view->win, get_line_attr(type));
+ wchgat(view->win, -1, 0, type, NULL);
+
+ } else {
+ type = LINE_MAIN_COMMIT;
+ wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
+ }
+
+ timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
+ waddnstr(view->win, buf, timelen);
+ waddstr(view->win, " ");
+
+ col += DATE_COLS;
+ wmove(view->win, lineno, col);
+ if (type != LINE_CURSOR)
+ wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
+
+ /* FIXME: Make this optional, and add i18n.commitEncoding support. */
+ authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
+
+ if (trimmed) {
+ waddnstr(view->win, commit->author, authorlen);
+ if (type != LINE_CURSOR)
+ wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
+ waddch(view->win, '~');
+ } else {
+ waddstr(view->win, commit->author);
+ }
+
+ col += AUTHOR_COLS;
+ if (type != LINE_CURSOR)
+ wattrset(view->win, A_NORMAL);
+
+ mvwaddch(view->win, lineno, col, ACS_LTEE);
+ wmove(view->win, lineno, col + 2);
+ col += 2;
+
+ if (commit->refs) {
+ size_t i = 0;
+
+ do {
+ if (type == LINE_CURSOR)
+ ;
+ else if (commit->refs[i]->tag)
+ wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
+ else
+ wattrset(view->win, get_line_attr(LINE_MAIN_REF));
+ waddstr(view->win, "[");
+ waddstr(view->win, commit->refs[i]->name);
+ waddstr(view->win, "]");
+ if (type != LINE_CURSOR)
+ wattrset(view->win, A_NORMAL);
+ waddstr(view->win, " ");
+ col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
+ } while (commit->refs[i++]->next);
+ }
+
+ if (type != LINE_CURSOR)
+ wattrset(view->win, get_line_attr(type));
+
+ {
+ int titlelen = strlen(commit->title);
+
+ if (col + titlelen > view->width)
+ titlelen = view->width - col;
+
+ waddnstr(view->win, commit->title, titlelen);
+ }
+
+ return TRUE;
+}
+
+/* Reads git log --pretty=raw output and parses it into the commit struct. */
+static bool
+main_read(struct view *view, char *line)
+{
+ enum line_type type = get_line_type(line);
+ struct commit *commit;
+
+ switch (type) {
+ case LINE_COMMIT:
+ commit = calloc(1, sizeof(struct commit));
+ if (!commit)
+ return FALSE;
+
+ line += STRING_SIZE("commit ");
+
+ view->line[view->lines++] = commit;
+ string_copy(commit->id, line);
+ commit->refs = get_refs(commit->id);
+ break;
+
+ case LINE_AUTHOR:
+ {
+ char *ident = line + STRING_SIZE("author ");
+ char *end = strchr(ident, '<');
+
+ if (end) {
+ for (; end > ident && isspace(end[-1]); end--) ;
+ *end = 0;
+ }
+
+ commit = view->line[view->lines - 1];
+ string_copy(commit->author, ident);
+
+ /* Parse epoch and timezone */
+ if (end) {
+ char *secs = strchr(end + 1, '>');
+ char *zone;
+ time_t time;
+
+ if (!secs || secs[1] != ' ')
+ break;
+
+ secs += 2;
+ time = (time_t) atol(secs);
+ zone = strchr(secs, ' ');
+ if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
+ long tz;
+
+ zone++;
+ tz = ('0' - zone[1]) * 60 * 60 * 10;
+ tz += ('0' - zone[2]) * 60 * 60;
+ tz += ('0' - zone[3]) * 60;
+ tz += ('0' - zone[4]) * 60;
+
+ if (zone[0] == '-')
+ tz = -tz;
+
+ time -= tz;
+ }
+ gmtime_r(&time, &commit->time);
+ }
+ break;
+ }
+ default:
+ /* We should only ever end up here if there has already been a
+ * commit line, however, be safe. */
+ if (view->lines == 0)
+ break;
+
+ /* Fill in the commit title if it has not already been set. */
+ commit = view->line[view->lines - 1];
+ if (commit->title[0])
+ break;
+
+ /* Require titles to start with a non-space character at the
+ * offset used by git log. */
+ /* FIXME: More gracefull handling of titles; append "..." to
+ * shortened titles, etc. */
+ if (strncmp(line, " ", 4) ||
+ isspace(line[4]))
+ break;
+
+ string_copy(commit->title, line + 4);
+ }
+
+ return TRUE;
+}
+
+static bool
+main_enter(struct view *view)
+{
+ enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
+
+ open_view(view, REQ_VIEW_DIFF, flags);
+ return TRUE;
+}
+
+static struct view_ops main_ops = {
+ "commit",
+ main_draw,
+ main_read,
+ main_enter,
+};
+
+
+/**
+ * KEYS
+ * ----
+ * Below the default key bindings are shown.
+ **/
+
+struct keymap {
+ int alias;
+ int request;
+};
+
+static struct keymap keymap[] = {
+ /**
+ * View switching
+ * ~~~~~~~~~~~~~~
+ * m::
+ * Switch to main view.
+ * d::
+ * Switch to diff view.
+ * l::
+ * Switch to log view.
+ * p::
+ * Switch to pager view.
+ * h::
+ * Show man page.
+ **/
+ { 'm', REQ_VIEW_MAIN },
+ { 'd', REQ_VIEW_DIFF },
+ { 'l', REQ_VIEW_LOG },
+ { 'p', REQ_VIEW_PAGER },
+ { 'h', REQ_VIEW_HELP },
+
+ /**
+ * View manipulation
+ * ~~~~~~~~~~~~~~~~~
+ * q::
+ * Close view, if multiple views are open it will jump back to the
+ * previous view in the view stack. If it is the last open view it
+ * will quit. Use 'Q' to quit all views at once.
+ * Enter::
+ * This key is "context sensitive" depending on what view you are
+ * currently in. When in log view on a commit line or in the main
+ * view, split the view and show the commit diff. In the diff view
+ * pressing Enter will simply scroll the view one line down.
+ * Tab::
+ * Switch to next view.
+ * Up::
+ * This key is "context sensitive" and will move the cursor one
+ * line up. However, uf you opened a diff view from the main view
+ * (split- or full-screen) it will change the cursor to point to
+ * the previous commit in the main view and update the diff view
+ * to display it.
+ * Down::
+ * Similar to 'Up' but will move down.
+ **/
+ { 'q', REQ_VIEW_CLOSE },
+ { KEY_TAB, REQ_VIEW_NEXT },
+ { KEY_RETURN, REQ_ENTER },
+ { KEY_UP, REQ_PREVIOUS },
+ { KEY_DOWN, REQ_NEXT },
+
+ /**
+ * Cursor navigation
+ * ~~~~~~~~~~~~~~~~~
+ * j::
+ * Move cursor one line up.
+ * k::
+ * Move cursor one line down.
+ * PgUp::
+ * b::
+ * -::
+ * Move cursor one page up.
+ * PgDown::
+ * Space::
+ * Move cursor one page down.
+ * Home::
+ * Jump to first line.
+ * End::
+ * Jump to last line.
+ **/
+ { 'k', REQ_MOVE_UP },
+ { 'j', REQ_MOVE_DOWN },
+ { KEY_HOME, REQ_MOVE_FIRST_LINE },
+ { KEY_END, REQ_MOVE_LAST_LINE },
+ { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
+ { ' ', REQ_MOVE_PAGE_DOWN },
+ { KEY_PPAGE, REQ_MOVE_PAGE_UP },
+ { 'b', REQ_MOVE_PAGE_UP },
+ { '-', REQ_MOVE_PAGE_UP },
+
+ /**
+ * Scrolling
+ * ~~~~~~~~~
+ * Insert::
+ * Scroll view one line up.
+ * Delete::
+ * Scroll view one line down.
+ * w::
+ * Scroll view one page up.
+ * s::
+ * Scroll view one page down.
+ **/
+ { KEY_IC, REQ_SCROLL_LINE_UP },
+ { KEY_DC, REQ_SCROLL_LINE_DOWN },
+ { 'w', REQ_SCROLL_PAGE_UP },
+ { 's', REQ_SCROLL_PAGE_DOWN },
+
+ /**
+ * Misc
+ * ~~~~
+ * Q::
+ * Quit.
+ * r::
+ * Redraw screen.
+ * z::
+ * Stop all background loading. This can be useful if you use
+ * tig(1) in a repository with a long history without limiting
+ * the revision log.
+ * v::
+ * Show version.
+ * n::
+ * Toggle line numbers on/off.
+ * ':'::
+ * Open prompt. This allows you to specify what git command
+ * to run. Example:
+ *
+ * :log -p
+ **/
+ { 'Q', REQ_QUIT },
+ { 'z', REQ_STOP_LOADING },
+ { 'v', REQ_SHOW_VERSION },
+ { 'r', REQ_SCREEN_REDRAW },
+ { 'n', REQ_TOGGLE_LINE_NUMBERS },
+ { ':', REQ_PROMPT },
+
+ /* wgetch() with nodelay() enabled returns ERR when there's no input. */
+ { ERR, REQ_SCREEN_UPDATE },
+
+ /* Use the ncurses SIGWINCH handler. */
+ { KEY_RESIZE, REQ_SCREEN_RESIZE },
+};
+
+static enum request
+get_request(int key)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(keymap); i++)
+ if (keymap[i].alias == key)
+ return keymap[i].request;
+
+ return (enum request) key;
+}