readline: Improve robustness of editing and escape-sequence handling

Replace fragile cursor compensation with full-line redraws where needed,
fix Ctrl+W buffer/screen corruption, properly consume unrecognized CSI
sequences, and always clean up leaked local-echo characters from buggy
serial drivers. These changes make line editing consistent across
terminals while preserving existing behavior.

Signed-off-by: Alan C. Assis <acassis@gmail.com>
Assisted-by: Claude Code (Sonnet 5)
This commit is contained in:
Alan C. Assis 2026-07-23 14:35:12 -03:00
parent 48eb95998b
commit ae237624ee
2 changed files with 135 additions and 49 deletions

View file

@ -61,18 +61,15 @@ config READLINE_EDIT
Build in support for full command-line editing using cursor keys,
Home/End, Delete. Requires a VT100/ANSI-compatible terminal.
if READLINE_EDIT
config READLINE_EDIT_EMACS
bool "Emacs-style control keys"
default n
depends on READLINE_EDIT
---help---
Enable emacs-style control key bindings:
Ctrl+A Home, Ctrl+E End, Ctrl+K kill-to-end,
Ctrl+U kill-to-start, Ctrl+Left/Right word movement.
endif # READLINE_EDIT
config READLINE_CMD_HISTORY
bool "Command line history"
default n

View file

@ -1030,7 +1030,7 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf,
continue;
}
#endif
#endif /* CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH */
/* Are we processing a VT100 escape sequence */
@ -1042,19 +1042,44 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf,
if (escape == 3)
{
escape = 0;
if (ch == '~' && cursor < nch)
if (ch == '~')
{
if (cursor < nch)
{
int k;
for (k = cursor + 1; k < nch; k++)
buf[k - 1] = buf[k];
nch--;
# ifdef CONFIG_READLINE_ECHO
/* Back up 1 — terminal echo of '~' advanced cursor */
}
RL_WRITE(vtbl, g_curleft, sizeof(g_curleft));
redraw_tail(vtbl, buf, nch, cursor);
# ifdef CONFIG_READLINE_ECHO
/* Delete arrives as the 4-byte sequence "ESC [ 3 ~".
* Some serial drivers only know how to suppress
* local echo for fixed 3-byte "ESC [ x" sequences
* and leak the trailing '~' as a literal character,
* silently advancing the terminal's real cursor by
* one column. redraw_line() is immune to this (and
* to any other such drift) because it always
* returns to column 0 with '\r' first, rather than
* assuming where the cursor currently is -- unlike
* a plain erase-and-rewrite-the-tail redraw, which
* only works relative to the cursor's last known
* position and has no way to recover if that
* assumption turns out to be wrong.
*
* This redraw is unconditional -- even when there
* was nothing to delete (cursor already at the true
* end, including an empty line) -- specifically so
* it also cleans up a leaked '~' in that case; only
* skipping the redraw when there is "nothing to do"
* is exactly what left a stray '~' visible on
* screen with an unpatched driver.
*/
redraw_line(vtbl, buf, nch, cursor);
# endif
}
continue;
}
@ -1117,6 +1142,35 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf,
}
#endif /* CONFIG_READLINE_EDIT */
if (escape == 10) /* Unrecognized CSI -- consume to final byte */
{
if (ch >= 0x20 && ch <= 0x3f)
{
continue; /* still a parameter/intermediate byte */
}
/* ch is the final byte (0x40-0x7e), or something
* malformed -- either way the sequence is over now and
* is discarded; nothing was ever inserted into the
* buffer. Still issue a corrective redraw: some serial
* drivers only know how to suppress local echo for
* fixed 3-byte "ESC [ x" sequences and leak bytes from
* any longer, unrecognized sequence as literal
* characters, the same class of issue Delete has for
* its own "ESC [ 3 ~". redraw_line() cleans up any
* such leaked characters even though the buffer itself
* was never affected by them.
*/
escape = 0;
# ifdef CONFIG_READLINE_ECHO
# ifdef CONFIG_READLINE_EDIT
redraw_line(vtbl, buf, nch, cursor);
# endif
# endif
continue;
}
/* Some terminals (e.g. xterm) use the SS3 introducer "ESC O"
* rather than CSI "ESC [" for Home/End (and, in application
* cursor key mode, for the arrow keys too). Recognize the
@ -1315,6 +1369,22 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf,
}
#endif
/* Not one of the specific sequences recognized above. If
* this still looks like a CSI parameter or intermediate
* byte (0x20-0x3f: digits, ';', and similar), this is
* some other CSI sequence this code doesn't specifically
* know about -- keep consuming bytes until the real final
* byte (0x40-0x7e) ends it, rather than treating this
* byte as the end of the sequence and leaking whatever
* comes after it as literal input.
*/
if (ch >= 0x20 && ch <= 0x3f)
{
escape = 10;
continue;
}
escape = 0;
ch = 'a';
}
@ -1450,16 +1520,31 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf,
buf[nch] = '\0';
redraw_line(vtbl, buf, nch, cursor);
}
else if (ch == CTRL_W) /* Ctrl+W = Kill word backward (FIXME) */
else if (ch == CTRL_W) /* Ctrl+W = Kill word backward */
{
int start, k;
start = word_skip(buf, cursor, 0, false);
for (k = start; k < nch; k++)
for (k = cursor; k < nch; k++)
buf[k - (cursor - start)] = buf[k];
nch -= (cursor - start);
cursor = start;
buf[nch] = '\0';
redraw_tail(vtbl, buf, nch, cursor);
/* Unlike Ctrl+K (which never moves the cursor, so erasing
* from wherever it already is is correct) and Backspace
* (which moves it by exactly one column and compensates
* with a single explicit backspace byte), Ctrl+W can move
* the cursor back by any number of columns depending on how
* long the killed word was. redraw_tail() has no way to
* express "also move left by N first"; it just erases from
* wherever the terminal's cursor already happens to be.
* Using redraw_line() instead sidesteps the whole problem
* the same way it does for Delete: it returns to column 0
* with '\r' first, so it is correct regardless of how far
* the cursor moved.
*/
redraw_line(vtbl, buf, nch, cursor);
}
# ifdef CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH
@ -1526,7 +1611,11 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf,
if (nch + 1 < buflen)
{
int j;
for (j = nch; j > cursor; j--) buf[j] = buf[j - 1];
for (j = nch; j > cursor; j--)
{
buf[j] = buf[j - 1];
}
buf[cursor] = (char)ch; nch++; cursor++;
# ifdef CONFIG_READLINE_ECHO
redraw_tail(vtbl, buf, nch, cursor);
@ -1534,7 +1623,7 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf,
}
#else
buf[nch++] = ch;
#endif
#endif /* CONFIG_READLINE_EDIT */
/* Check if there is room for another character and the line's
* null terminator. If not then we have to end the line now.