From 6706b2ba1d81bd757263292174f830abe791e68a Mon Sep 17 00:00:00 2001 From: Jonas Fonseca Date: Mon, 15 May 2006 03:50:32 +0200 Subject: [PATCH] Improve documentation; optional tab size; move up/down + enter req; fix wrapping --- tig.c | 345 ++++++++++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 251 insertions(+), 94 deletions(-) diff --git a/tig.c b/tig.c index ae7dce9..5190213 100644 --- a/tig.c +++ b/tig.c @@ -20,7 +20,40 @@ * * DESCRIPTION * ----------- - * Browse changes in a git repository. + * Browse changes in a git repository. Additionally, tig(1) can also act + * as a pager for output of various git commands. + * + * Commit limiting + * ~~~~~~~~~~~~~~~ + * To speed up interaction with git, you can limit the amount of commits + * to show both for the log and main view. Either limit by date using + * e.g. `--since=1.month` or limit by the number of commits using `-n400`. + * + * Alternatively, commits can be limited to a specific range, such as + * "all commits between tag-1.0 and tag-2.0". For example: + * + * $ tig log tag-1.0..tag-2.0 + * + * Git interprets this as "all commits reachable from commit-2 + * but not from commit-1". The above can also be written: + * + * $ tig log tag-2.0 ^tag-1.0 + * + * You can think of '^' as a negator. Using this alternate syntax, + * it is possible to furthur prune commits by specifying multiple + * negators. + * + * This way of commit limiting makes it trivial to only browse the commit + * which hasn't been pushed to a remote branch. Assuming origin is your + * upstream remote branch, using: + * + * $ tig log origin..HEAD + * + * Optionally, with "HEAD" left out, will list what will be pushed to + * the remote branch. + * + * See the section on environment variables, on how to further tune the + * interaction with git. **/ #ifndef VERSION @@ -67,6 +100,8 @@ static void set_nonblocking_input(bool loading); /* The default interval between line numbers. */ #define NUMBER_INTERVAL 1 +#define TABSIZE 8 + #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3) /* Some ascii-shorthands fitted into the ncurses namespace. */ @@ -74,6 +109,7 @@ static void set_nonblocking_input(bool loading); #define KEY_RETURN '\r' #define KEY_ESC 27 + /* User action requests. */ enum request { /* Offset all requests to avoid conflicts with ncurses getch values. */ @@ -98,7 +134,9 @@ enum request { REQ_VIEW_NEXT, REQ_MOVE_UP, + REQ_MOVE_UP_ENTER, REQ_MOVE_DOWN, + REQ_MOVE_DOWN_ENTER, REQ_MOVE_PAGE_UP, REQ_MOVE_PAGE_DOWN, REQ_MOVE_FIRST_LINE, @@ -125,6 +163,7 @@ struct commit { struct ref **refs; /* Repository references; tags & branch heads. */ }; + /* * String helpers */ @@ -141,6 +180,7 @@ string_ncopy(char *dst, char *src, int dstlen) #define string_copy(dst, src) \ string_ncopy(dst, src, sizeof(dst)) + /* Shell quoting * * NOTE: The following is a slightly modified copy of the git project's shell @@ -187,8 +227,10 @@ sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src) * ------- **/ -static int opt_line_number = FALSE; +/* Option and state variables. */ +static bool opt_line_number = FALSE; static int opt_num_interval = NUMBER_INTERVAL; +static int opt_tab_size = TABSIZE; static enum request opt_request = REQ_VIEW_MAIN; static char opt_cmd[SIZEOF_CMD] = ""; static FILE *opt_pipe = NULL; @@ -244,6 +286,26 @@ parse_options(int argc, char *argv[]) } /** + * -t[NSPACES], --tab-size[=NSPACES]:: + * Set the number of spaces tabs should be expanded to. + **/ + if (!strncmp(opt, "-t", 2) || + !strncmp(opt, "--tab-size", 10)) { + char *num = opt; + + if (opt[1] == 't') { + num = opt + 2; + + } else if (opt[STRING_SIZE("--tab-size")] == '=') { + num = opt + STRING_SIZE("--tab-size="); + } + + if (isdigit(*num)) + opt_tab_size = MIN(atoi(num), TABSIZE); + continue; + } + + /** * -v, --version:: * Show version and exit. **/ @@ -356,6 +418,7 @@ parse_options(int argc, char *argv[]) /** * KEYS * ---- + * Below the default key bindings are shown. **/ #define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp" @@ -380,8 +443,10 @@ struct keymap keymap[] = { * h:: * Show man page. * Return:: - * If in main view split the view - * and show the diff in the bottom view. + * If on a commit line show the commit diff. Addiionally, if in + * main or log view this will split the view. To open the commit + * diff in full size view either use 'd' or press Return twice. + * * Tab:: * Switch to next view. **/ @@ -397,13 +462,21 @@ struct keymap keymap[] = { /** * Cursor navigation * ~~~~~~~~~~~~~~~~~ - * Up, k:: + * Up:: * Move curser one line up. - * Down, j:: + * Down:: * Move cursor one line down. - * Page Up:: + * k:: + * + * Move curser one line up and enter. When used in the main view + * this will always show the diff of the current commit in the + * split diff view. + * + * j:: + * Move cursor one line down and enter. + * PgUp:: * Move curser one page up. - * Page Down:: + * PgDown:: * Move cursor one page down. * Home:: * Jump to first line. @@ -411,9 +484,9 @@ struct keymap keymap[] = { * Jump to last line. **/ { KEY_UP, REQ_MOVE_UP }, - { 'k', REQ_MOVE_UP }, { KEY_DOWN, REQ_MOVE_DOWN }, - { 'j', REQ_MOVE_DOWN }, + { 'k', REQ_MOVE_UP_ENTER }, + { 'j', REQ_MOVE_DOWN_ENTER }, { KEY_HOME, REQ_MOVE_FIRST_LINE }, { KEY_END, REQ_MOVE_LAST_LINE }, { KEY_NPAGE, REQ_MOVE_PAGE_DOWN }, @@ -439,24 +512,25 @@ struct keymap keymap[] = { /** * Misc * ~~~~ - * q, Escape:: + * q:: * Quit * r:: * Redraw screen. * z:: - * Stop all background loading. + * Stop all background loading. This can be useful if you ran + * tig(1) in a repository with a long history without limiting + * the log output. * v:: * Show version. * n:: * Toggle line numbers on/off. * ':':: - * Open prompt. This allows you to specify what git command to run. - * Example: + * Open prompt. This allows you to specify what git command + * to run. Example: * * :log -p * **/ - { KEY_ESC, REQ_QUIT }, { 'q', REQ_QUIT }, { 'z', REQ_STOP_LOADING }, { 'v', REQ_SHOW_VERSION }, @@ -614,12 +688,13 @@ init_colors(void) * If you want to filter out certain directories under `.git/refs/`, say * `tmp` you can do it by setting the following variable: * - * $ TIG_LS_REMOTE="git ls-remote | sed '/\/tmp\//d'" + * $ TIG_LS_REMOTE="git ls-remote . | sed /\/tmp\//d" tig * * Or set the variable permanently in your environment. * * TIG_LS_REMOTE:: - * Set command for retrieving all repository references.private + * Set command for retrieving all repository references. The command + * should output data in the same format as git-ls-remote(1). **/ #define TIG_LS_REMOTE \ @@ -629,8 +704,8 @@ init_colors(void) * View commands * ~~~~~~~~~~~~~ * It is possible to alter which commands are used for the different views. - * If for example you prefer commits in the main to be sorted by date and - * only show 500 commits, use: + * If for example you prefer commits in the main view to be sorted by date + * and only show 500 commits, use: * * $ TIG_MAIN_CMD="git log --date-order -n500 --pretty=raw %s" tig * @@ -644,7 +719,9 @@ init_colors(void) * as a backend. * * TIG_LOG_CMD:: - * The command used for the log view. + * The command used for the log view. If you prefer to have both + * author and committer shown in the log view be sure to pass + * `--pretty=fuller` to git log. * * TIG_MAIN_CMD:: * The command used for the main view. Note, you must always specify @@ -796,8 +873,11 @@ resize_display(void) offset = 0; foreach_view (view, i) { + /* Keep the size of the all view windows one lager than is + * required. This makes current line management easier when the + * cursor will go outside the window. */ if (!view->win) { - view->win = newwin(view->height, 0, offset, 0); + view->win = newwin(view->height + 1, 0, offset, 0); if (!view->win) die("Failed to create %s view", view->name); @@ -808,7 +888,7 @@ resize_display(void) die("Failed to create title window"); } else { - wresize(view->win, view->height, view->width); + wresize(view->win, view->height + 1, view->width); mvwin(view->win, offset, 0); mvwin(view->title, offset + view->height, 0); wrefresh(view->win); @@ -883,6 +963,12 @@ do_scroll_view(struct view *view, int lines) view->ops->draw(view, 0); } else if (view->lineno >= view->offset + view->height) { + if (view->lineno == view->offset + view->height) { + /* Clear the hidden line so it doesn't show if the view + * is scrolled up. */ + wmove(view->win, view->height, 0); + wclrtoeol(view->win); + } view->lineno = view->offset + view->height - 1; view->ops->draw(view, view->lineno - view->offset); } @@ -960,10 +1046,12 @@ move_view(struct view *view, enum request request) break; case REQ_MOVE_UP: + case REQ_MOVE_UP_ENTER: steps = -1; break; case REQ_MOVE_DOWN: + case REQ_MOVE_DOWN_ENTER: steps = 1; break; @@ -1250,7 +1338,8 @@ open_view(struct view *prev, enum request request, enum open_flags flags) if (prev && view != prev) { /* "Blur" the previous view. */ - update_view_title(prev); + if (!backgrounded) + update_view_title(prev); /* Continue loading split views in the background. */ if (!split) @@ -1266,6 +1355,11 @@ open_view(struct view *prev, enum request request, enum open_flags flags) redraw_view(view); report(""); } + + /* If the view is backgrounded the above calls to report() + * won't redraw the view title. */ + if (backgrounded) + update_view_title(view); } @@ -1303,6 +1397,11 @@ view_driver(struct view *view, enum request request) open_view(view, request, OPEN_DEFAULT); break; + case REQ_MOVE_UP_ENTER: + case REQ_MOVE_DOWN_ENTER: + move_view(view, request); + /* Fall-through */ + case REQ_ENTER: if (!view->lines) { report("Nothing to enter"); @@ -1329,6 +1428,7 @@ view_driver(struct view *view, enum request request) case REQ_TOGGLE_LINE_NUMBERS: opt_line_number = !opt_line_number; redraw_view(view); + update_view_title(view); break; case REQ_PROMPT: @@ -1339,7 +1439,7 @@ view_driver(struct view *view, enum request request) case REQ_STOP_LOADING: foreach_view (view, i) { if (view->pipe) - report("Stopped loaded of %s view", view->name), + report("Stopped loaded the %s view", view->name), end_update(view); } break; @@ -1393,6 +1493,8 @@ pager_draw(struct view *view, unsigned int lineno) 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); @@ -1400,61 +1502,62 @@ pager_draw(struct view *view, unsigned int lineno) } type = LINE_CURSOR; + wchgat(view->win, -1, 0, type, NULL); } attr = get_line_attr(type); wattrset(view->win, attr); linelen = strlen(line); - linelen = MIN(linelen, view->width); - if (opt_line_number) { - static char indent[] = " "; - unsigned long real_lineno = view->offset + lineno + 1; - int col = 0; + 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) - mvwprintw(view->win, lineno, 0, "%.*d", view->digits, real_lineno); + if (real_lineno == 1 || + (real_lineno % opt_num_interval) == 0) { + wprintw(view->win, "%.*d", view->digits, real_lineno); - else if (view->digits < sizeof(indent)) - mvwaddnstr(view->win, lineno, 0, indent, view->digits); + } else { + waddnstr(view->win, spaces, + MIN(view->digits, STRING_SIZE(spaces))); + } + waddstr(view->win, ": "); + col_offset = view->digits + 2; + } - waddstr(view->win, ": "); + while (line && col_offset + col < view->width) { + int cols_max = view->width - col_offset - col; + char *text = line; + int cols; - while (line) { if (*line == '\t') { - waddnstr(view->win, " ", 8 - (col % 8)); - col += 8 - (col % 8); + assert(sizeof(spaces) > TABSIZE); line++; + text = spaces; + cols = opt_tab_size - (col % opt_tab_size); } else { - char *tab = strchr(line, '\t'); - - if (tab) - waddnstr(view->win, line, tab - line); - else - waddstr(view->win, line); - col += tab - line; - line = tab; + line = strchr(line, '\t'); + cols = line ? line - text : strlen(text); } + + waddnstr(view->win, text, MIN(cols, cols_max)); + col += cols; } - waddstr(view->win, line); } else { -#if 0 - /* NOTE: Code for only highlighting the text on the cursor line. - * Kept since I've not yet decided whether to highlight the - * entire line or not. --fonseca */ - /* No empty lines makes cursor drawing and clearing implicit. */ - if (!*line) - line = " ", linelen = 1; -#endif - mvwaddnstr(view->win, lineno, 0, line, linelen); - } + int col = 0, pos = 0; - /* Paint the rest of the line if it's the cursor line. */ - if (type == LINE_CURSOR) - wchgat(view->win, -1, 0, type, NULL); + for (; pos < linelen && col < view->width; pos++, col++) + if (line[pos] == '\t') + col += TABSIZE - (col % TABSIZE) - 1; + + waddnstr(view->win, line, pos); + } return TRUE; } @@ -1462,6 +1565,13 @@ pager_draw(struct view *view, unsigned int lineno) 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; @@ -1476,7 +1586,10 @@ pager_enter(struct view *view) char *line = view->line[view->lineno]; if (get_line_type(line) == LINE_COMMIT) { - open_view(view, REQ_VIEW_DIFF, OPEN_DEFAULT); + if (view == VIEW(REQ_VIEW_LOG)) + open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT | OPEN_BACKGROUNDED); + else + open_view(view, REQ_VIEW_DIFF, OPEN_DEFAULT); } return TRUE; @@ -1498,7 +1611,7 @@ main_draw(struct view *view, unsigned int lineno) char buf[DATE_COLS + 1]; struct commit *commit; enum line_type type; - int cols = 0; + int col = 0; size_t timelen; if (view->offset + lineno >= view->lines) @@ -1508,57 +1621,77 @@ main_draw(struct view *view, unsigned int 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)); } - wmove(view->win, lineno, cols); - 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, " "); - cols += DATE_COLS; - wmove(view->win, lineno, cols); - wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR)); + col += DATE_COLS; + wmove(view->win, lineno, col); + if (type != LINE_CURSOR) + wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR)); if (strlen(commit->author) > 19) { waddnstr(view->win, commit->author, 18); - wattrset(view->win, get_line_attr(LINE_MAIN_DELIM)); + if (type != LINE_CURSOR) + wattrset(view->win, get_line_attr(LINE_MAIN_DELIM)); waddch(view->win, '~'); } else { waddstr(view->win, commit->author); } - cols += 20; - wattrset(view->win, A_NORMAL); - mvwaddch(view->win, lineno, cols, ACS_LTEE); - wmove(view->win, lineno, cols + 2); + col += 20; + 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 (commit->refs[i]->tag) + 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, "]"); - wattrset(view->win, A_NORMAL); + 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); } - wattrset(view->win, get_line_attr(type)); - waddstr(view->win, commit->title); - wattrset(view->win, A_NORMAL); + 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; } @@ -1680,32 +1813,47 @@ static WINDOW *status_win; static void report(const char *msg, ...) { - va_list args; + static bool empty = TRUE; + struct view *view = display[current_view]; - /* Update the title window first, so the cursor ends up in the status - * window. */ - update_view_title(display[current_view]); + if (!empty || *msg) { + va_list args; - va_start(args, msg); + va_start(args, msg); - werase(status_win); - wmove(status_win, 0, 0); - if (*msg) - vwprintw(status_win, msg, args); - wrefresh(status_win); + werase(status_win); + wmove(status_win, 0, 0); + if (*msg) { + vwprintw(status_win, msg, args); + empty = FALSE; + } else { + empty = TRUE; + } + wrefresh(status_win); - va_end(args); + va_end(args); + } + + update_view_title(view); + + /* Move the cursor to the right-most column of the cursor line. + * + * XXX: This could turn out to be a bit expensive, but it ensures that + * the cursor does not jump around. */ + if (view->lines) { + wmove(view->win, view->lineno - view->offset, view->width - 1); + wrefresh(view->win); + } } /* Controls when nodelay should be in effect when polling user input. */ static void set_nonblocking_input(bool loading) { - /* The number of loading views. */ - static unsigned int nloading; + static unsigned int loading_views; - if ((loading == FALSE && nloading-- == 1) || - (loading == TRUE && nloading++ == 0)) + if ((loading == FALSE && loading_views-- == 1) || + (loading == TRUE && loading_views++ == 0)) nodelay(status_win, loading); } @@ -1792,7 +1940,7 @@ static int load_refs(void) { char *cmd_env = getenv("TIG_LS_REMOTE"); - char *cmd = cmd_env ? cmd_env : TIG_LS_REMOTE; + char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE; FILE *pipe = popen(cmd, "r"); char buffer[BUFSIZ]; char *line; @@ -1812,6 +1960,8 @@ load_refs(void) *name++ = 0; namelen = strlen(name) - 1; + + /* Commits referenced by tags has "^{}" appended. */ if (name[namelen - 1] == '}') { while (namelen > 0 && name[namelen] != '^') namelen--; @@ -1915,7 +2065,7 @@ main(int argc, char *argv[]) key = wgetch(status_win); request = get_request(key); - /* Some low-level request handling. This keeps handling of + /* Some low-level request handling. This keeps access to * status_win restricted. */ switch (request) { case REQ_PROMPT: @@ -1960,6 +2110,13 @@ main(int argc, char *argv[]) } /** + * BUGS + * ---- + * Known bugs and problems: + * + * - If the screen width is very small the main view can draw + * outside the current view causing bad wrapping. + * * TODO * ---- * Features that should be explored. -- 2.11.0