+ report("");
+}
+
+
+/*
+ * Incremental updating
+ */
+
+static void
+end_update(struct view *view)
+{
+ if (!view->pipe)
+ return;
+ set_nonblocking_input(FALSE);
+ if (view->pipe == stdin)
+ fclose(view->pipe);
+ else
+ pclose(view->pipe);
+ view->pipe = NULL;
+}
+
+static bool
+begin_update(struct view *view)
+{
+ const char *id = view->id;
+
+ if (view->pipe)
+ end_update(view);
+
+ if (opt_cmd[0]) {
+ string_copy(view->cmd, opt_cmd);
+ opt_cmd[0] = 0;
+ /* When running random commands, the view ref could have become
+ * invalid so clear it. */
+ view->ref[0] = 0;
+ } else {
+ const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
+
+ if (snprintf(view->cmd, sizeof(view->cmd), format,
+ id, id, id, id, id) >= sizeof(view->cmd))
+ return FALSE;
+ }
+
+ /* Special case for the pager view. */
+ if (opt_pipe) {
+ view->pipe = opt_pipe;
+ opt_pipe = NULL;
+ } else {
+ view->pipe = popen(view->cmd, "r");
+ }
+
+ if (!view->pipe)
+ return FALSE;
+
+ set_nonblocking_input(TRUE);
+
+ view->offset = 0;
+ view->lines = 0;
+ view->lineno = 0;
+ string_copy(view->vid, id);
+
+ if (view->line) {
+ int i;
+
+ for (i = 0; i < view->lines; i++)
+ if (view->line[i].data)
+ free(view->line[i].data);
+
+ free(view->line);
+ view->line = NULL;
+ }
+
+ view->start_time = time(NULL);
+
+ return TRUE;
+}
+
+static bool
+update_view(struct view *view)
+{
+ char buffer[BUFSIZ];
+ char *line;
+ struct line *tmp;
+ /* The number of lines to read. If too low it will cause too much
+ * redrawing (and possible flickering), if too high responsiveness
+ * will suffer. */
+ unsigned long lines = view->height;
+ int redraw_from = -1;
+
+ if (!view->pipe)
+ return TRUE;
+
+ /* Only redraw if lines are visible. */
+ if (view->offset + view->height >= view->lines)
+ redraw_from = view->lines - view->offset;
+
+ tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines));
+ if (!tmp)
+ goto alloc_error;
+
+ view->line = tmp;
+
+ while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
+ int linelen = strlen(line);
+
+ struct line *prev = view->lines
+ ? &view->line[view->lines - 1]
+ : NULL;
+
+ if (linelen)
+ line[linelen - 1] = 0;
+
+ if (!view->ops->read(view, prev, line))
+ goto alloc_error;
+
+ if (lines-- == 1)
+ break;
+ }
+
+ {
+ int digits;
+
+ lines = view->lines;
+ for (digits = 0; lines; digits++)
+ lines /= 10;
+
+ /* Keep the displayed view in sync with line number scaling. */
+ if (digits != view->digits) {
+ view->digits = digits;
+ redraw_from = 0;
+ }
+ }
+
+ if (redraw_from >= 0) {
+ /* If this is an incremental update, redraw the previous line
+ * since for commits some members could have changed when
+ * loading the main view. */
+ if (redraw_from > 0)
+ redraw_from--;
+
+ /* Incrementally draw avoids flickering. */
+ redraw_view_from(view, redraw_from);
+ }
+
+ /* Update the title _after_ the redraw so that if the redraw picks up a
+ * commit reference in view->ref it'll be available here. */
+ update_view_title(view);
+
+ if (ferror(view->pipe)) {
+ report("Failed to read: %s", strerror(errno));
+ goto end;
+
+ } else if (feof(view->pipe)) {
+ if (view == VIEW(REQ_VIEW_HELP)) {
+ const char *msg = TIG_HELP;
+
+ if (view->lines == 0) {
+ /* Slightly ugly, but abusing view->ref keeps
+ * the error message. */
+ string_copy(view->ref, "No help available");
+ msg = "The tig(1) manpage is not installed";
+ }
+
+ report("%s", msg);
+ goto end;
+ }
+
+ report("");
+ goto end;
+ }
+
+ return TRUE;
+
+alloc_error:
+ report("Allocation failure");
+
+end:
+ end_update(view);
+ return FALSE;
+}
+
+enum open_flags {
+ OPEN_DEFAULT = 0, /* Use default view switching. */
+ OPEN_SPLIT = 1, /* Split current view. */
+ OPEN_BACKGROUNDED = 2, /* Backgrounded. */
+ OPEN_RELOAD = 4, /* Reload view even if it is the current. */
+};
+
+static void
+open_view(struct view *prev, enum request request, enum open_flags flags)
+{
+ bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
+ bool split = !!(flags & OPEN_SPLIT);
+ bool reload = !!(flags & OPEN_RELOAD);
+ struct view *view = VIEW(request);
+ int nviews = displayed_views();
+ struct view *base_view = display[0];
+
+ if (view == prev && nviews == 1 && !reload) {
+ report("Already in %s view", view->name);
+ return;
+ }
+
+ 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 the view when switching between split- and full-screen,
+ * or when switching between two different full-screen views. */
+ if (nviews != displayed_views() ||
+ (nviews == 1 && base_view != display[0]))
+ 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) {
+ if (split && !backgrounded) {
+ /* "Blur" the previous view. */
+ update_view_title(prev);
+ }
+
+ view->parent = prev;
+ }
+
+ if (view->pipe && view->lines == 0) {
+ /* Clear the old view and let the incremental updating refill
+ * the screen. */
+ wclear(view->win);
+ report("");
+ } 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[1] == view;
+
+ view = view->parent;
+ move_view(view, request, redraw);
+ if (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, &view->line[view->lineno]);
+
+ case REQ_VIEW_NEXT:
+ {
+ int nviews = displayed_views();
+ 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:
+ for (i = 0; i < ARRAY_SIZE(views); i++) {
+ view = &views[i];
+ if (view->pipe)
+ report("Stopped loading 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:
+ /* XXX: Mark closed views by letting view->parent point to the
+ * view itself. Parents to closed view should never be
+ * followed. */
+ if (view->parent &&
+ view->parent->parent != view->parent) {
+ memset(display, 0, sizeof(display));
+ current_view = 0;
+ display[current_view] = view->parent;
+ view->parent = view;
+ 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, struct line *line, unsigned int lineno)
+{
+ char *text = line->data;
+ enum line_type type = line->type;
+ int textlen = strlen(text);
+ int attr;
+
+ wmove(view->win, lineno, 0);
+
+ if (view->offset + lineno == view->lineno) {
+ if (type == LINE_COMMIT) {
+ string_copy(view->ref, text + 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);
+
+ 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 (text && col_offset + col < view->width) {
+ int cols_max = view->width - col_offset - col;
+ char *pos = text;
+ int cols;
+
+ if (*text == '\t') {
+ text++;
+ assert(sizeof(spaces) > TABSIZE);
+ pos = spaces;
+ cols = opt_tab_size - (col % opt_tab_size);
+
+ } else {
+ text = strchr(text, '\t');
+ cols = line ? text - pos : strlen(pos);
+ }
+
+ waddnstr(view->win, pos, MIN(cols, cols_max));
+ col += cols;
+ }
+
+ } else {
+ int col = 0, pos = 0;
+
+ for (; pos < textlen && col < view->width; pos++, col++)
+ if (text[pos] == '\t')
+ col += TABSIZE - (col % TABSIZE) - 1;
+
+ waddnstr(view->win, text, pos);
+ }
+
+ return TRUE;
+}
+
+static bool
+pager_read(struct view *view, struct line *prev, char *line)
+{
+ /* Compress empty lines in the help view. */
+ if (view == VIEW(REQ_VIEW_HELP) &&
+ !*line && prev && !*((char *) prev->data))
+ return TRUE;
+
+ view->line[view->lines].data = strdup(line);
+ if (!view->line[view->lines].data)
+ return FALSE;
+
+ view->line[view->lines].type = get_line_type(line);
+
+ view->lines++;
+ return TRUE;
+}
+
+static bool
+pager_enter(struct view *view, struct line *line)
+{
+ int split = 0;
+
+ if (line->type == LINE_COMMIT &&
+ (view == VIEW(REQ_VIEW_LOG) ||
+ view == VIEW(REQ_VIEW_PAGER))) {
+ 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 scrolling 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, struct line *line, unsigned int lineno)
+{
+ char buf[DATE_COLS + 1];
+ struct commit *commit = line->data;
+ enum line_type type;
+ int col = 0;
+ size_t timelen;
+ size_t authorlen;
+ int trimmed = 1;
+
+ 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));
+
+ if (opt_utf8) {
+ authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
+ } else {
+ authorlen = strlen(commit->author);
+ if (authorlen > AUTHOR_COLS - 2) {
+ authorlen = AUTHOR_COLS - 2;
+ trimmed = 1;
+ }
+ }
+
+ 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, struct line *prev, 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++].data = 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 (!prev)
+ break;
+
+ commit = prev->data;
+
+ if (end) {
+ for (; end > ident && isspace(end[-1]); end--) ;
+ *end = 0;
+ }
+
+ 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:
+ if (!prev)
+ break;
+
+ commit = prev->data;
+
+ /* Fill in the commit title if it has not already been set. */
+ 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, struct line *line)
+{
+ 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;
+}
+
+
+/*
+ * Unicode / UTF-8 handling
+ *
+ * NOTE: Much of the following code for dealing with unicode is derived from
+ * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
+ * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
+ */
+
+/* I've (over)annotated a lot of code snippets because I am not entirely
+ * confident that the approach taken by this small UTF-8 interface is correct.
+ * --jonas */
+
+static inline int
+unicode_width(unsigned long c)
+{
+ if (c >= 0x1100 &&
+ (c <= 0x115f /* Hangul Jamo */
+ || c == 0x2329
+ || c == 0x232a
+ || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
+ /* CJK ... Yi */
+ || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
+ || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
+ || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
+ || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
+ || (c >= 0xffe0 && c <= 0xffe6)
+ || (c >= 0x20000 && c <= 0x2fffd)
+ || (c >= 0x30000 && c <= 0x3fffd)))
+ return 2;
+
+ return 1;
+}
+
+/* Number of bytes used for encoding a UTF-8 character indexed by first byte.
+ * Illegal bytes are set one. */
+static const unsigned char utf8_bytes[256] = {
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+};
+
+/* Decode UTF-8 multi-byte representation into a unicode character. */
+static inline unsigned long
+utf8_to_unicode(const char *string, size_t length)
+{
+ unsigned long unicode;
+
+ switch (length) {
+ case 1:
+ unicode = string[0];
+ break;
+ case 2:
+ unicode = (string[0] & 0x1f) << 6;
+ unicode += (string[1] & 0x3f);
+ break;
+ case 3:
+ unicode = (string[0] & 0x0f) << 12;
+ unicode += ((string[1] & 0x3f) << 6);
+ unicode += (string[2] & 0x3f);
+ break;
+ case 4:
+ unicode = (string[0] & 0x0f) << 18;
+ unicode += ((string[1] & 0x3f) << 12);
+ unicode += ((string[2] & 0x3f) << 6);
+ unicode += (string[3] & 0x3f);
+ break;
+ case 5:
+ unicode = (string[0] & 0x0f) << 24;
+ unicode += ((string[1] & 0x3f) << 18);
+ unicode += ((string[2] & 0x3f) << 12);
+ unicode += ((string[3] & 0x3f) << 6);
+ unicode += (string[4] & 0x3f);
+ break;
+ case 6:
+ unicode = (string[0] & 0x01) << 30;
+ unicode += ((string[1] & 0x3f) << 24);
+ unicode += ((string[2] & 0x3f) << 18);
+ unicode += ((string[3] & 0x3f) << 12);
+ unicode += ((string[4] & 0x3f) << 6);
+ unicode += (string[5] & 0x3f);
+ break;
+ default:
+ die("Invalid unicode length");
+ }