utf8_length: add reserve flag for reserving a trailing character
[tig] / tig.c
diff --git a/tig.c b/tig.c
index 14f73e6..25cf31b 100644 (file)
--- a/tig.c
+++ b/tig.c
@@ -42,6 +42,9 @@
 #include <langinfo.h>
 #include <iconv.h>
 
+/* ncurses(3): Must be defined to have extended wide-character functions. */
+#define _XOPEN_SOURCE_EXTENDED
+
 #include <curses.h>
 
 #if __GNUC__ >= 3
 #endif
 
 static void __NORETURN die(const char *err, ...);
+static void warn(const char *msg, ...);
 static void report(const char *msg, ...);
 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
 static void set_nonblocking_input(bool loading);
-static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
+static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
 
 #define ABS(x)         ((x) >= 0  ? (x) : -(x))
 #define MIN(x, y)      ((x) < (y) ? (x) :  (y))
@@ -106,7 +110,7 @@ static size_t utf8_length(const char *string, size_t max_width, int *coloffset,
        "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
 
 #define TIG_DIFF_CMD \
-       "git show --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
+       "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
 
 #define TIG_LOG_CMD    \
        "git log --no-color --cc --stat -n100 %s 2>/dev/null"
@@ -408,22 +412,14 @@ get_request(const char *name)
 static const char usage[] =
 "tig " TIG_VERSION " (" __DATE__ ")\n"
 "\n"
-"Usage: tig [options]\n"
-"   or: tig [options] [--] [git log options]\n"
-"   or: tig [options] log  [git log options]\n"
-"   or: tig [options] diff [git diff options]\n"
-"   or: tig [options] show [git show options]\n"
-"   or: tig [options] <    [git command output]\n"
+"Usage: tig        [options] [revs] [--] [paths]\n"
+"   or: tig show   [options] [revs] [--] [paths]\n"
+"   or: tig status\n"
+"   or: tig <      [git command output]\n"
 "\n"
 "Options:\n"
-"  -l                          Start up in log view\n"
-"  -d                          Start up in diff view\n"
-"  -S                          Start up in status view\n"
-"  -n[I], --line-number[=I]    Show line numbers with given interval\n"
-"  -b[N], --tab-size[=N]       Set number of spaces for tab expansion\n"
-"  --                          Mark end of tig options\n"
-"  -v, --version               Show version and exit\n"
-"  -h, --help                  Show help message and exit\n";
+"  -v, --version   Show version and exit\n"
+"  -h, --help      Show help message and exit\n";
 
 /* Option and state variables. */
 static bool opt_line_number            = FALSE;
@@ -441,7 +437,7 @@ static iconv_t opt_iconv            = ICONV_NONE;
 static char opt_search[SIZEOF_STR]     = "";
 static char opt_cdup[SIZEOF_STR]       = "";
 static char opt_git_dir[SIZEOF_STR]    = "";
-static char opt_is_inside_work_tree    = -1; /* set to TRUE or FALSE */
+static signed char opt_is_inside_work_tree     = -1; /* set to TRUE or FALSE */
 static char opt_editor[SIZEOF_STR]     = "";
 
 enum option_type {
@@ -491,16 +487,32 @@ check_option(char *opt, char short_name, char *name, enum option_type type, ...)
 static bool
 parse_options(int argc, char *argv[])
 {
+       char *altargv[1024];
+       int altargc = 0;
+       char *subcommand = NULL;
        int i;
 
        for (i = 1; i < argc; i++) {
                char *opt = argv[i];
 
                if (!strcmp(opt, "log") ||
-                   !strcmp(opt, "diff") ||
-                   !strcmp(opt, "show")) {
+                   !strcmp(opt, "diff")) {
+                       subcommand = opt;
                        opt_request = opt[0] == 'l'
                                    ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
+                       warn("`tig %s' has been deprecated", opt);
+                       break;
+               }
+
+               if (!strcmp(opt, "show")) {
+                       subcommand = opt;
+                       opt_request = REQ_VIEW_DIFF;
+                       break;
+               }
+
+               if (!strcmp(opt, "status")) {
+                       subcommand = opt;
+                       opt_request = REQ_VIEW_STATUS;
                        break;
                }
 
@@ -523,38 +535,43 @@ parse_options(int argc, char *argv[])
                }
 
                if (!strcmp(opt, "-S")) {
+                       warn("`%s' has been deprecated; use `tig status' instead", opt);
                        opt_request = REQ_VIEW_STATUS;
                        continue;
                }
 
                if (!strcmp(opt, "-l")) {
                        opt_request = REQ_VIEW_LOG;
-                       continue;
-               }
-
-               if (!strcmp(opt, "-d")) {
+               } else if (!strcmp(opt, "-d")) {
                        opt_request = REQ_VIEW_DIFF;
-                       continue;
-               }
-
-               if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
+               } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
                        opt_line_number = TRUE;
-                       continue;
-               }
-
-               if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
+               } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
                        opt_tab_size = MIN(opt_tab_size, TABSIZE);
+               } else {
+                       if (altargc >= ARRAY_SIZE(altargv))
+                               die("maximum number of arguments exceeded");
+                       altargv[altargc++] = opt;
                        continue;
                }
 
-               die("unknown option '%s'\n\n%s", opt, usage);
+               warn("`%s' has been deprecated", opt);
        }
 
+       /* Check that no 'alt' arguments occured before a subcommand. */
+       if (subcommand && i < argc && altargc > 0)
+               die("unknown arguments before `%s'", argv[i]);
+
        if (!isatty(STDIN_FILENO)) {
                opt_request = REQ_VIEW_PAGER;
                opt_pipe = stdin;
 
-       } else if (i < argc) {
+       } else if (opt_request == REQ_VIEW_STATUS) {
+               if (argc - i > 1)
+                       warn("ignoring arguments after `%s'", argv[i]);
+
+       } else if (i < argc || altargc > 0) {
+               int alti = 0;
                size_t buf_size;
 
                if (opt_request == REQ_VIEW_MAIN)
@@ -565,6 +582,11 @@ parse_options(int argc, char *argv[])
                        string_copy(opt_cmd, "git");
                buf_size = strlen(opt_cmd);
 
+               while (buf_size < sizeof(opt_cmd) && alti < altargc) {
+                       opt_cmd[buf_size++] = ' ';
+                       buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
+               }
+
                while (buf_size < sizeof(opt_cmd) && i < argc) {
                        opt_cmd[buf_size++] = ' ';
                        buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
@@ -697,15 +719,15 @@ get_line_info(char *name, int namelen)
 static void
 init_colors(void)
 {
-       int default_bg = COLOR_BLACK;
-       int default_fg = COLOR_WHITE;
+       int default_bg = line_info[LINE_DEFAULT].bg;
+       int default_fg = line_info[LINE_DEFAULT].fg;
        enum line_type type;
 
        start_color();
 
-       if (use_default_colors() != ERR) {
-               default_bg = -1;
-               default_fg = -1;
+       if (assume_default_colors(default_fg, default_bg) == ERR) {
+               default_bg = COLOR_BLACK;
+               default_fg = COLOR_WHITE;
        }
 
        for (type = 0; type < ARRAY_SIZE(line_info); type++) {
@@ -1271,29 +1293,47 @@ read_option(char *opt, size_t optlen, char *value, size_t valuelen)
        return OK;
 }
 
-static int
-load_options(void)
+static void
+load_option_file(const char *path)
 {
-       char *home = getenv("HOME");
-       char buf[SIZEOF_STR];
        FILE *file;
 
+       /* It's ok that the file doesn't exist. */
+       file = fopen(path, "r");
+       if (!file)
+               return;
+
        config_lineno = 0;
        config_errors = FALSE;
 
-       add_builtin_run_requests();
+       if (read_properties(file, " \t", read_option) == ERR ||
+           config_errors == TRUE)
+               fprintf(stderr, "Errors while loading %s.\n", path);
+}
 
-       if (!home || !string_format(buf, "%s/.tigrc", home))
-               return ERR;
+static int
+load_options(void)
+{
+       char *home = getenv("HOME");
+       char *tigrc_user = getenv("TIGRC_USER");
+       char *tigrc_system = getenv("TIGRC_SYSTEM");
+       char buf[SIZEOF_STR];
 
-       /* It's ok that the file doesn't exist. */
-       file = fopen(buf, "r");
-       if (!file)
-               return OK;
+       add_builtin_run_requests();
 
-       if (read_properties(file, " \t", read_option) == ERR ||
-           config_errors == TRUE)
-               fprintf(stderr, "Errors while loading %s.\n", buf);
+       if (!tigrc_system) {
+               if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
+                       return ERR;
+               tigrc_system = buf;
+       }
+       load_option_file(tigrc_system);
+
+       if (!tigrc_user) {
+               if (!home || !string_format(buf, "%s/.tigrc", home))
+                       return ERR;
+               tigrc_user = buf;
+       }
+       load_option_file(tigrc_user);
 
        return OK;
 }
@@ -1416,6 +1456,43 @@ static struct view views[] = {
 #define view_is_displayed(view) \
        (view == display[0] || view == display[1])
 
+static int
+draw_text(struct view *view, const char *string, int max_len, int col,
+         bool use_tilde, int tilde_attr)
+{
+       int n;
+
+       n = 0;
+       if (max_len > 0) {
+               int len;
+               int trimmed = FALSE;
+
+               if (opt_utf8) {
+                       len = utf8_length(string, max_len, &trimmed, use_tilde);
+                       n = len;
+               } else {
+                       len = strlen(string);
+                       if (len > max_len) {
+                               if (use_tilde) {
+                                       max_len -= 1;
+                               }
+                               len = max_len;
+                               trimmed = TRUE;
+                       }
+                       n = len;
+               }
+               waddnstr(view->win, string, n);
+               if (trimmed && use_tilde) {
+                       if (tilde_attr != -1)
+                               wattrset(view->win, tilde_attr);
+                       waddch(view->win, '~');
+                       n++;
+               }
+       }
+
+       return n;
+}
+
 static bool
 draw_view_line(struct view *view, unsigned int lineno)
 {
@@ -2553,7 +2630,6 @@ pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selec
 {
        char *text = line->data;
        enum line_type type = line->type;
-       int textlen = strlen(text);
        int attr;
 
        wmove(view->win, lineno, 0);
@@ -2606,13 +2682,9 @@ pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selec
                }
 
        } else {
-               int col = 0, pos = 0;
+               int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
 
-               for (; pos < textlen && col < view->width; pos++, col++)
-                       if (text[pos] == '\t')
-                               col += TABSIZE - (col % TABSIZE) - 1;
-
-               waddnstr(view->win, text, pos);
+               draw_text(view, text, view->width, 0, TRUE, tilde_attr);
        }
 
        return TRUE;
@@ -2650,7 +2722,7 @@ static void
 add_pager_refs(struct view *view, struct line *line)
 {
        char buf[SIZEOF_STR];
-       char *commit_id = line->data + STRING_SIZE("commit ");
+       char *commit_id = (char *)line->data + STRING_SIZE("commit ");
        struct ref **refs;
        size_t bufpos = 0, refpos = 0;
        const char *sep = "Refs: ";
@@ -2761,7 +2833,7 @@ static void
 pager_select(struct view *view, struct line *line)
 {
        if (line->type == LINE_COMMIT) {
-               char *text = line->data + STRING_SIZE("commit ");
+               char *text = (char *)line->data + STRING_SIZE("commit ");
 
                if (view != VIEW(REQ_VIEW_PAGER))
                        string_copy_rev(view->ref, text);
@@ -3080,7 +3152,7 @@ tree_request(struct view *view, enum request request, struct line *line)
 static void
 tree_select(struct view *view, struct line *line)
 {
-       char *text = line->data + STRING_SIZE("100644 blob ");
+       char *text = (char *)line->data + STRING_SIZE("100644 blob ");
 
        if (line->type == LINE_TREE_FILE) {
                string_copy_rev(ref_blob, text);
@@ -3128,12 +3200,13 @@ struct status {
        struct {
                mode_t mode;
                char rev[SIZEOF_REV];
+               char name[SIZEOF_STR];
        } old;
        struct {
                mode_t mode;
                char rev[SIZEOF_REV];
+               char name[SIZEOF_STR];
        } new;
-       char name[SIZEOF_STR];
 };
 
 static struct status stage_status;
@@ -3151,7 +3224,7 @@ status_get_diff(struct status *file, char *buf, size_t bufsize)
        char *new_rev  = buf + 56;
        char *status   = buf + 97;
 
-       if (bufsize != 99 ||
+       if (bufsize < 99 ||
            old_mode[-1] != ':' ||
            new_mode[-1] != ' ' ||
            old_rev[-1]  != ' ' ||
@@ -3167,7 +3240,7 @@ status_get_diff(struct status *file, char *buf, size_t bufsize)
        file->old.mode = strtoul(old_mode, NULL, 8);
        file->new.mode = strtoul(new_mode, NULL, 8);
 
-       file->name[0] = 0;
+       file->old.name[0] = file->new.name[0] = 0;
 
        return TRUE;
 }
@@ -3234,7 +3307,7 @@ status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
                                        unmerged = file;
 
                                } else if (unmerged) {
-                                       int collapse = !strcmp(buf, unmerged->name);
+                                       int collapse = !strcmp(buf, unmerged->new.name);
 
                                        unmerged = NULL;
                                        if (collapse) {
@@ -3245,10 +3318,26 @@ status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
                                }
                        }
 
+                       /* Grab the old name for rename/copy. */
+                       if (!*file->old.name &&
+                           (file->status == 'R' || file->status == 'C')) {
+                               sepsize = sep - buf + 1;
+                               string_ncopy(file->old.name, buf, sepsize);
+                               bufsize -= sepsize;
+                               memmove(buf, sep + 1, bufsize);
+
+                               sep = memchr(buf, 0, bufsize);
+                               if (!sep)
+                                       break;
+                               sepsize = sep - buf + 1;
+                       }
+
                        /* git-ls-files just delivers a NUL separated
                         * list of file names similar to the second half
                         * of the git-diff-* output. */
-                       string_ncopy(file->name, buf, sepsize);
+                       string_ncopy(file->new.name, buf, sepsize);
+                       if (!*file->old.name)
+                               string_copy(file->old.name, file->new.name);
                        bufsize -= sepsize;
                        memmove(buf, sep + 1, bufsize);
                        file = NULL;
@@ -3269,16 +3358,16 @@ error_out:
 }
 
 /* Don't show unmerged entries in the staged section. */
-#define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached HEAD"
-#define STATUS_DIFF_FILES_CMD "git diff-files -z"
+#define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
+#define STATUS_DIFF_FILES_CMD "git update-index -q --refresh && git diff-files -z"
 #define STATUS_LIST_OTHER_CMD \
        "git ls-files -z --others --exclude-per-directory=.gitignore"
 
 #define STATUS_DIFF_INDEX_SHOW_CMD \
-       "git diff-index --root --patch-with-stat --find-copies-harder -C --cached HEAD -- %s 2>/dev/null"
+       "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
 
 #define STATUS_DIFF_FILES_SHOW_CMD \
-       "git diff-files --root --patch-with-stat --find-copies-harder -C -- %s 2>/dev/null"
+       "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
 
 /* First parse staged info using git-diff-index(1), then parse unstaged
  * info using git-diff-files(1), and finally untracked files using
@@ -3333,12 +3422,14 @@ static bool
 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
 {
        struct status *status = line->data;
+       int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
 
        wmove(view->win, lineno, 0);
 
        if (selected) {
                wattrset(view->win, get_line_attr(LINE_CURSOR));
                wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
+               tilde_attr = -1;
 
        } else if (!status && line->type != LINE_STAT_NONE) {
                wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
@@ -3372,7 +3463,7 @@ status_draw(struct view *view, struct line *line, unsigned int lineno, bool sele
                        return FALSE;
                }
 
-               waddstr(view->win, text);
+               draw_text(view, text, view->width, 0, TRUE, tilde_attr);
                return TRUE;
        }
 
@@ -3380,8 +3471,10 @@ status_draw(struct view *view, struct line *line, unsigned int lineno, bool sele
        if (!selected)
                wattrset(view->win, A_NORMAL);
        wmove(view->win, lineno, 4);
-       waddstr(view->win, status->name);
+       if (view->width < 5)
+               return TRUE;
 
+       draw_text(view, status->new.name, view->width - 5, 5, TRUE, tilde_attr);
        return TRUE;
 }
 
@@ -3389,7 +3482,8 @@ static enum request
 status_enter(struct view *view, struct line *line)
 {
        struct status *status = line->data;
-       char path[SIZEOF_STR] = "";
+       char oldpath[SIZEOF_STR] = "";
+       char newpath[SIZEOF_STR] = "";
        char *info;
        size_t cmdsize = 0;
 
@@ -3399,8 +3493,15 @@ status_enter(struct view *view, struct line *line)
                return REQ_NONE;
        }
 
-       if (status && sq_quote(path, 0, status->name) >= sizeof(path))
-               return REQ_QUIT;
+       if (status) {
+               if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
+                       return REQ_QUIT;
+               /* Diffs for unmerged entries are empty when pasing the
+                * new path, so leave it empty. */
+               if (status->status != 'U' &&
+                   sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
+                       return REQ_QUIT;
+       }
 
        if (opt_cdup[0] &&
            line->type != LINE_STAT_UNTRACKED &&
@@ -3410,7 +3511,7 @@ status_enter(struct view *view, struct line *line)
        switch (line->type) {
        case LINE_STAT_STAGED:
                if (!string_format_from(opt_cmd, &cmdsize,
-                                       STATUS_DIFF_INDEX_SHOW_CMD, path))
+                                       STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
                        return REQ_QUIT;
                if (status)
                        info = "Staged changes to %s";
@@ -3420,7 +3521,7 @@ status_enter(struct view *view, struct line *line)
 
        case LINE_STAT_UNSTAGED:
                if (!string_format_from(opt_cmd, &cmdsize,
-                                       STATUS_DIFF_FILES_SHOW_CMD, path))
+                                       STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
                        return REQ_QUIT;
                if (status)
                        info = "Unstaged changes to %s";
@@ -3438,7 +3539,7 @@ status_enter(struct view *view, struct line *line)
                        return REQ_NONE;
                }
 
-               opt_pipe = fopen(status->name, "r");
+               opt_pipe = fopen(status->new.name, "r");
                info = "Untracked file %s";
                break;
 
@@ -3455,7 +3556,7 @@ status_enter(struct view *view, struct line *line)
                }
 
                stage_line_type = line->type;
-               string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.name);
+               string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
        }
 
        return REQ_NONE;
@@ -3482,7 +3583,7 @@ status_update_file(struct view *view, struct status *status, enum line_type type
                if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
                                        status->old.mode,
                                        status->old.rev,
-                                       status->name, 0))
+                                       status->old.name, 0))
                        return FALSE;
 
                string_add(cmd, cmdsize, "git update-index -z --index-info");
@@ -3490,7 +3591,7 @@ status_update_file(struct view *view, struct status *status, enum line_type type
 
        case LINE_STAT_UNSTAGED:
        case LINE_STAT_UNTRACKED:
-               if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
+               if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
                        return FALSE;
 
                string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
@@ -3554,14 +3655,14 @@ status_request(struct view *view, enum request request, struct line *line)
                        report("Merging only possible for files with unmerged status ('U').");
                        return REQ_NONE;
                }
-               open_mergetool(status->name);
+               open_mergetool(status->new.name);
                break;
 
        case REQ_EDIT:
                if (!status)
                        return request;
 
-               open_editor(status->status != '?', status->name);
+               open_editor(status->status != '?', status->new.name);
                break;
 
        case REQ_ENTER:
@@ -3592,7 +3693,7 @@ status_select(struct view *view, struct line *line)
        char *text;
        char *key;
 
-       if (status && !string_format(file, "'%s'", status->name))
+       if (status && !string_format(file, "'%s'", status->new.name))
                return;
 
        if (!status && line[1].type == LINE_STAT_NONE)
@@ -3645,7 +3746,7 @@ status_grep(struct view *view, struct line *line)
                char *text;
 
                switch (state) {
-               case S_NAME:    text = status->name;    break;
+               case S_NAME:    text = status->new.name;        break;
                case S_STATUS:
                        buf[0] = status->status;
                        text = buf;
@@ -3810,10 +3911,10 @@ stage_request(struct view *view, enum request request, struct line *line)
                break;
 
        case REQ_EDIT:
-               if (!stage_status.name[0])
+               if (!stage_status.new.name[0])
                        return request;
 
-               open_editor(stage_status.status != '?', stage_status.name);
+               open_editor(stage_status.status != '?', stage_status.new.name);
                break;
 
        case REQ_ENTER:
@@ -4055,67 +4156,75 @@ main_draw(struct view *view, struct line *line, unsigned int lineno, bool select
        enum line_type type;
        int col = 0;
        size_t timelen;
-       size_t authorlen;
-       int trimmed = 1;
+       int tilde_attr;
+       int space;
 
        if (!*commit->author)
                return FALSE;
 
+       space = view->width;
        wmove(view->win, lineno, col);
 
        if (selected) {
                type = LINE_CURSOR;
                wattrset(view->win, get_line_attr(type));
                wchgat(view->win, -1, 0, type, NULL);
-
+               tilde_attr = -1;
        } else {
                type = LINE_MAIN_COMMIT;
                wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
+               tilde_attr = get_line_attr(LINE_MAIN_DELIM);
        }
 
-       timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
-       waddnstr(view->win, buf, timelen);
-       waddstr(view->win, " ");
+       {
+               int n;
 
-       col += DATE_COLS;
-       wmove(view->win, lineno, col);
-       if (type != LINE_CURSOR)
-               wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
+               timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
+               n = draw_text(
+                       view, buf, view->width - col, col, FALSE, tilde_attr);
+               draw_text(
+                       view, " ", view->width - col - n, col + n, FALSE,
+                       tilde_attr);
 
-       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;
-               }
+               col += DATE_COLS;
+               wmove(view->win, lineno, col);
+               if (col >= view->width)
+                       return TRUE;
        }
+       if (type != LINE_CURSOR)
+               wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
 
-       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);
+       {
+               int max_len;
+
+               max_len = view->width - col;
+               if (max_len > AUTHOR_COLS - 1)
+                       max_len = AUTHOR_COLS - 1;
+               draw_text(
+                       view, commit->author, max_len, col, TRUE, tilde_attr);
+               col += AUTHOR_COLS;
+               if (col >= view->width)
+                       return TRUE;
        }
 
-       col += AUTHOR_COLS;
-
        if (opt_rev_graph && commit->graph_size) {
+               size_t graph_size = view->width - col;
                size_t i;
 
                if (type != LINE_CURSOR)
                        wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
                wmove(view->win, lineno, col);
+               if (graph_size > commit->graph_size)
+                       graph_size = commit->graph_size;
                /* Using waddch() instead of waddnstr() ensures that
                 * they'll be rendered correctly for the cursor line. */
-               for (i = 0; i < commit->graph_size; i++)
+               for (i = 0; i < graph_size; i++)
                        waddch(view->win, commit->graph[i]);
 
-               waddch(view->win, ' ');
                col += commit->graph_size + 1;
+               if (col >= view->width)
+                       return TRUE;
+               waddch(view->win, ' ');
        }
        if (type != LINE_CURSOR)
                wattrset(view->win, A_NORMAL);
@@ -4134,27 +4243,31 @@ main_draw(struct view *view, struct line *line, unsigned int lineno, bool select
                                wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
                        else
                                wattrset(view->win, get_line_attr(LINE_MAIN_REF));
-                       waddstr(view->win, "[");
-                       waddstr(view->win, commit->refs[i]->name);
-                       waddstr(view->win, "]");
+
+                       col += draw_text(
+                               view, "[", view->width - col, col, TRUE,
+                               tilde_attr);
+                       col += draw_text(
+                               view, commit->refs[i]->name, view->width - col,
+                               col, TRUE, tilde_attr);
+                       col += draw_text(
+                               view, "]", view->width - col, col, TRUE,
+                               tilde_attr);
                        if (type != LINE_CURSOR)
                                wattrset(view->win, A_NORMAL);
-                       waddstr(view->win, " ");
-                       col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
+                       col += draw_text(
+                               view, " ", view->width - col, col, TRUE,
+                               tilde_attr);
+                       if (col >= view->width)
+                               return TRUE;
                } 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);
-       }
+       col += draw_text(
+               view, commit->title, view->width - col, col, TRUE, tilde_attr);
 
        return TRUE;
 }
@@ -4369,6 +4482,9 @@ unicode_width(unsigned long c)
            || (c >= 0x30000 && c <= 0x3fffd)))
                return 2;
 
+       if (c == '\t')
+               return opt_tab_size;
+
        return 1;
 }
 
@@ -4436,19 +4552,16 @@ utf8_to_unicode(const char *string, size_t length)
 
 /* Calculates how much of string can be shown within the given maximum width
  * and sets trimmed parameter to non-zero value if all of string could not be
- * shown.
- *
- * Additionally, adds to coloffset how many many columns to move to align with
- * the expected position. Takes into account how multi-byte and double-width
- * characters will effect the cursor position.
+ * shown. If the reserve flag is TRUE, it will reserve at least one
+ * trailing character, which can be useful when drawing a delimiter.
  *
  * Returns the number of bytes to output from string to satisfy max_width. */
 static size_t
-utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
+utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
 {
        const char *start = string;
        const char *end = strchr(string, '\0');
-       size_t mbwidth = 0;
+       unsigned char last_bytes = 0;
        size_t width = 0;
 
        *trimmed = 0;
@@ -4474,27 +4587,16 @@ utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
                width  += ucwidth;
                if (width > max_width) {
                        *trimmed = 1;
+                       if (reserve && width - ucwidth == max_width) {
+                               string -= last_bytes;
+                       }
                        break;
                }
 
-               /* The column offset collects the differences between the
-                * number of bytes encoding a character and the number of
-                * columns will be used for rendering said character.
-                *
-                * So if some character A is encoded in 2 bytes, but will be
-                * represented on the screen using only 1 byte this will and up
-                * adding 1 to the multi-byte column offset.
-                *
-                * Assumes that no double-width character can be encoding in
-                * less than two bytes. */
-               if (bytes > ucwidth)
-                       mbwidth += bytes - ucwidth;
-
                string  += bytes;
+               last_bytes = bytes;
        }
 
-       *coloffset += mbwidth;
-
        return string - start;
 }
 
@@ -4911,6 +5013,18 @@ die(const char *err, ...)
        exit(1);
 }
 
+static void
+warn(const char *msg, ...)
+{
+       va_list args;
+
+       va_start(args, msg);
+       fputs("tig warning: ", stderr);
+       vfprintf(stderr, msg, args);
+       fputs("\n", stderr);
+       va_end(args);
+}
+
 int
 main(int argc, char *argv[])
 {