lineedit: improve multiline PS1 - redraw using last PS1 line. Closes 10381
authorAvi Halachmi <avihpit@yahoo.com>
Thu, 12 Oct 2017 14:38:35 +0000 (16:38 +0200)
committerDenys Vlasenko <vda.linux@googlemail.com>
Thu, 12 Oct 2017 16:26:13 +0000 (18:26 +0200)
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 <avihpit@yahoo.com>
Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
libbb/lineedit.c

index 3a092ffe26a93c5e9c7a7b2713971913817e69ed..c0e35bb2160e3bccee775bb159f7e94bb68f5cf8 100644 (file)
  *
  * 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'):