From 0fd5dbba8f34e006aa0e999002d31e79daf2fdf3 Mon Sep 17 00:00:00 2001 From: Avi Halachmi Date: Thu, 12 Oct 2017 16:38:35 +0200 Subject: [PATCH] lineedit: improve multiline PS1 - redraw using last PS1 line. Closes 10381 This patch only affects prompts with newlines. We redraw the prompt [+ input] occasionally, e.g. during tab completion, history browsing or search, etc, and we expect it to align with prior redraws, such that the visible effect is that only the input changes. With multi-line PS1, redraw always printed the prompt some lines below the old one, which resulted in terminal scroll during every redraw. Now we only redraw the last PS1 line, so vertical alignment is easier to manage (we already calculated it using only the last line, but re-drew all lines - that was the culprit), which fixes those extra scrolls. Notes: - We now use the full prompt for the initial draw, after clear-screen (^L), and after tab-completion choices are displayed. Everything else now redraws using the last/sole prompt line. - During terminal resize we now only redraw the last[/sole] prompt line, which is arguably better because it's hard to do right (and we never did). - Good side effect for reverse-i-search: its prompt now replaces only the last line of the original prompt - like other shells do. function old new delta put_prompt_custom - 66 +66 draw_custom - 66 +66 parse_and_put_prompt 766 806 +40 read_line_input 3867 3884 +17 input_tab 1069 1076 +7 cmdedit_setwidth 61 63 +2 redraw 59 47 -12 put_prompt 46 - -46 ------------------------------------------------------------------------------ (add/remove: 2/1 grow/shrink: 4/1 up/down: 198/-58) Total: 140 bytes Signed-off-by: Avi Halachmi Signed-off-by: Denys Vlasenko --- libbb/lineedit.c | 88 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/libbb/lineedit.c b/libbb/lineedit.c index 3a092ffe2..c0e35bb21 100644 --- a/libbb/lineedit.c +++ b/libbb/lineedit.c @@ -37,11 +37,6 @@ * * Unicode in PS1 is not fully supported: prompt length calulation is wrong, * resulting in line wrap problems with long (multi-line) input. - * - * Multi-line PS1 (e.g. PS1="\n[\w]\n$ ") has problems with history - * browsing: up/down arrows result in scrolling. - * It stems from simplistic "cmdedit_y = cmdedit_prmt_len / cmdedit_termw" - * calculation of how many lines the prompt takes. */ #include "busybox.h" #include "NUM_APPLETS.h" @@ -133,7 +128,7 @@ struct lineedit_statics { unsigned cmdedit_x; /* real x (col) terminal position */ unsigned cmdedit_y; /* pseudoreal y (row) terminal position */ - unsigned cmdedit_prmt_len; /* length of prompt (without colors etc) */ + unsigned cmdedit_prmt_len; /* on-screen length of last/sole prompt line */ unsigned cursor; int command_len; /* must be signed */ @@ -143,6 +138,7 @@ struct lineedit_statics { CHAR_T *command_ps; const char *cmdedit_prompt; + const char *prompt_last_line; /* last/sole prompt line */ #if ENABLE_USERNAME_OR_HOMEDIR char *user_buf; @@ -185,6 +181,7 @@ extern struct lineedit_statics *const lineedit_ptr_to_statics; #define command_len (S.command_len ) #define command_ps (S.command_ps ) #define cmdedit_prompt (S.cmdedit_prompt ) +#define prompt_last_line (S.prompt_last_line) #define user_buf (S.user_buf ) #define home_pwd_buf (S.home_pwd_buf ) #define matches (S.matches ) @@ -437,14 +434,20 @@ static void beep(void) bb_putchar('\007'); } -static void put_prompt(void) +/* Full or last/sole prompt line, reset edit cursor, calculate terminal cursor. + * cmdedit_y is always calculated for the last/sole prompt line. + */ +static void put_prompt_custom(bool is_full) { - fputs(cmdedit_prompt, stdout); + fputs((is_full ? cmdedit_prompt : prompt_last_line), stdout); cursor = 0; cmdedit_y = cmdedit_prmt_len / cmdedit_termw; /* new quasireal y */ cmdedit_x = cmdedit_prmt_len % cmdedit_termw; } +#define put_prompt_last_line() put_prompt_custom(0) +#define put_prompt() put_prompt_custom(1) + /* Move back one character */ /* (optimized for slow terminals) */ static void input_backward(unsigned num) @@ -509,7 +512,7 @@ static void input_backward(unsigned num) printf("\r" ESC"[%uA", cmdedit_y); cmdedit_y = 0; sv_cursor = cursor; - put_prompt(); /* sets cursor to 0 */ + put_prompt_last_line(); /* sets cursor to 0 */ while (cursor < sv_cursor) put_cur_glyph_and_inc_cursor(); } else { @@ -530,18 +533,27 @@ static void input_backward(unsigned num) } } -/* draw prompt, editor line, and clear tail */ -static void redraw(int y, int back_cursor) +/* See redraw and draw_full below */ +static void draw_custom(int y, int back_cursor, bool is_full) { if (y > 0) /* up y lines */ printf(ESC"[%uA", y); bb_putchar('\r'); - put_prompt(); + put_prompt_custom(is_full); put_till_end_and_adv_cursor(); printf(SEQ_CLEAR_TILL_END_OF_SCREEN); input_backward(back_cursor); } +/* Move y lines up, draw last/sole prompt line, editor line[s], and clear tail. + * goal: redraw the prompt+input+cursor in-place, overwriting the previous */ +#define redraw(y, back_cursor) draw_custom((y), (back_cursor), 0) + +/* Like above, but without moving up, and while using all the prompt lines. + * goal: draw a full prompt+input+cursor unrelated to a previous position. + * note: cmdedit_y always ends up relating to the last/sole prompt line */ +#define draw_full(back_cursor) draw_custom(0, (back_cursor), 1) + /* Delete the char in front of the cursor, optionally saving it * for later putback */ #if !ENABLE_FEATURE_EDITING_VI @@ -1106,7 +1118,7 @@ static NOINLINE void input_tab(smallint *lastWasTab) int sav_cursor = cursor; goto_new_line(); showfiles(); - redraw(0, command_len - sav_cursor); + draw_full(command_len - sav_cursor); } return; } @@ -1782,14 +1794,37 @@ static void ask_terminal(void) #define ask_terminal() ((void)0) #endif +/* Note about multi-line PS1 (e.g. "\n\w \u@\h\n> ") and prompt redrawing: + * + * If the prompt has any newlines, after we print it once we use only its last + * line to redraw in-place, which makes it simpler to calculate how many lines + * we should move the cursor up to align the redraw (cmdedit_y). The earlier + * prompt lines just stay on screen and we redraw below them. + * + * Use cases for all prompt lines beyond the initial draw: + * - After clear-screen (^L) or after displaying tab-completion choices, we + * print the full prompt, as it isn't redrawn in-place. + * - During terminal resize we could try to redraw all lines, but we don't, + * because it requires delicate alignment, it's good enough with only the + * last line, and doing it wrong is arguably worse than not doing it at all. + * + * Terminology wise, if it doesn't mention "full", then it means the last/sole + * prompt line. We use the prompt (last/sole line) while redrawing in-place, + * and the full where we need a fresh one unrelated to an earlier position. + * + * If PS1 is not multiline, the last/sole line and the full are the same string. + */ + /* Called just once at read_line_input() init time */ #if !ENABLE_FEATURE_EDITING_FANCY_PROMPT static void parse_and_put_prompt(const char *prmt_ptr) { const char *p; - cmdedit_prompt = prmt_ptr; + cmdedit_prompt = prompt_last_line = prmt_ptr; p = strrchr(prmt_ptr, '\n'); - cmdedit_prmt_len = unicode_strwidth(p ? p+1 : prmt_ptr); + if (p) + prompt_last_line = p + 1; + cmdedit_prmt_len = unicode_strwidth(prompt_last_line); put_prompt(); } #else @@ -1973,7 +2008,11 @@ static void parse_and_put_prompt(const char *prmt_ptr) if (cwd_buf != (char *)bb_msg_unknown) free(cwd_buf); # endif - cmdedit_prompt = prmt_mem_ptr; + /* see comment (above this function) about multiline prompt redrawing */ + cmdedit_prompt = prompt_last_line = prmt_mem_ptr; + prmt_ptr = strrchr(cmdedit_prompt, '\n'); + if (prmt_ptr) + prompt_last_line = prmt_ptr + 1; put_prompt(); } #endif @@ -2145,7 +2184,7 @@ static int32_t reverse_i_search(int timeout) match_buf[0] = '\0'; /* Save and replace the prompt */ - saved_prompt = cmdedit_prompt; + saved_prompt = prompt_last_line; saved_prmt_len = cmdedit_prmt_len; goto set_prompt; @@ -2218,10 +2257,10 @@ static int32_t reverse_i_search(int timeout) cursor = match - matched_history_line; //FIXME: cursor position for Unicode case - free((char*)cmdedit_prompt); + free((char*)prompt_last_line); set_prompt: - cmdedit_prompt = xasprintf("(reverse-i-search)'%s': ", match_buf); - cmdedit_prmt_len = unicode_strwidth(cmdedit_prompt); + prompt_last_line = xasprintf("(reverse-i-search)'%s': ", match_buf); + cmdedit_prmt_len = unicode_strwidth(prompt_last_line); goto do_redraw; } } @@ -2241,8 +2280,8 @@ static int32_t reverse_i_search(int timeout) if (matched_history_line) command_len = load_string(matched_history_line); - free((char*)cmdedit_prompt); - cmdedit_prompt = saved_prompt; + free((char*)prompt_last_line); + prompt_last_line = saved_prompt; cmdedit_prmt_len = saved_prmt_len; redraw(cmdedit_y, command_len - cursor); @@ -2451,8 +2490,9 @@ int FAST_FUNC read_line_input(line_input_t *st, const char *prompt, char *comman case CTRL('L'): vi_case(CTRL('L')|VI_CMDMODE_BIT:) /* Control-l -- clear screen */ - printf(ESC"[H"); /* cursor to top,left */ - redraw(0, command_len - cursor); + /* cursor to top,left; clear to the end of screen */ + printf(ESC"[H" ESC"[J"); + draw_full(command_len - cursor); break; #if MAX_HISTORY > 0 case CTRL('N'): -- 2.25.1