From 51bfd48e78a55b8b6eab53c5743bbadfe5de2a2a Mon Sep 17 00:00:00 2001 From: "Alan C. Assis" Date: Tue, 14 Jul 2026 21:39:39 -0300 Subject: [PATCH] system/readline: Add support for line movements and editing This commit adds support to nsh> command line editing. This is one of the most missing feature of the NuttShell. If you typed a long command line and make a mistake you need to press Backspace and remove everything until reach that typo. Only the basic editing feature is enabled by default (left/right keys movement, Home/End to move to the beginning or ending of the command line). More advanced features are available when CONFIG_READLINE_EDIT_EMACS is selected. It enables the Emacs-style control keys (Ctrl+A/B/D/E/F/K/U/W) that allow more flexible line editing. Other more advanced feature is enabled when CONFIG_READLINE_CMD_HISTORY and CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH are enabled. It allows the user to press Ctrl+R to do reverse search in the command line history. Signed-off-by: Alan C. Assis Assisted-by: Claude Code --- nshlib/nsh_session.c | 6 + system/readline/Kconfig | 30 + system/readline/readline_common.c | 951 ++++++++++++++++++++++++++++-- 3 files changed, 925 insertions(+), 62 deletions(-) diff --git a/nshlib/nsh_session.c b/nshlib/nsh_session.c index bb83b98fb..4dd316a9d 100644 --- a/nshlib/nsh_session.c +++ b/nshlib/nsh_session.c @@ -223,6 +223,12 @@ int nsh_session(FAR struct console_stdio_s *pstate, write(OUTFD(pstate), nsh_prompt(), strlen(nsh_prompt())); +#ifdef CONFIG_READLINE_TABCOMPLETION + /* Set the prompt so readline can redraw it for editing features */ + + readline_prompt(nsh_prompt()); +#endif + /* readline() normally returns the number of characters read, but * will return EOF on end of file or if an error occurs. EOF * will cause the session to terminate. diff --git a/system/readline/Kconfig b/system/readline/Kconfig index f088fddee..1914fedaa 100644 --- a/system/readline/Kconfig +++ b/system/readline/Kconfig @@ -54,6 +54,25 @@ config READLINE_MAX_EXTCMDS endif # READLINE_TABCOMPLETION +config READLINE_EDIT + bool "Command line editing" + default y if !DEFAULT_SMALL + ---help--- + 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 + ---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 @@ -96,6 +115,17 @@ config READLINE_CMD_HISTORY_LEN will be READLINE_CMD_HISTORY_LINELEN x READLINE_CMD_HISTORY_LEN. Default: 16 +config READLINE_EDIT_EMACS_REVERSE_SEARCH + bool "Reverse incremental search (Ctrl+R)" + default y + depends on READLINE_EDIT_EMACS + ---help--- + Enable bash-style Ctrl+R reverse incremental search through the + command history. Typing narrows the search to the most recent + matching command; Ctrl+R again searches further back; Enter + submits the match; Ctrl+G/Ctrl+C cancels back to the line being + edited before Ctrl+R was pressed. + endif # READLINE_CMD_HISTORY endif # READLINE_ECHO endif # SYSTEM_READLINE diff --git a/system/readline/readline_common.c b/system/readline/readline_common.c index 0fd3b9396..a5bd7b301 100644 --- a/system/readline/readline_common.c +++ b/system/readline/readline_common.c @@ -72,6 +72,27 @@ struct cmdhist_s #ifdef CONFIG_READLINE_ECHO static const char g_erasetoeol[] = VT100_CLEAREOL; #endif +#ifdef CONFIG_READLINE_EDIT +static const char g_curleft[] = {ASCII_ESC, '[', 'D'}; +static const char g_curright[] = {ASCII_ESC, '[', 'C'}; +#endif + +#ifdef CONFIG_READLINE_EDIT_EMACS +/* Emacs-style control key codes */ + +# define CTRL_A 1 /* ^A - Home */ +# define CTRL_B 2 /* ^B - Left */ +# define CTRL_D 4 /* ^D - Delete at cursor */ +# define CTRL_E 5 /* ^E - End */ +# define CTRL_F 6 /* ^F - Right */ +# define CTRL_K 11 /* ^K - Kill to end of line */ +# define CTRL_U 21 /* ^U - Kill to beginning of line */ +# define CTRL_W 23 /* ^W - Kill word backward */ +# ifdef CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH +# define CTRL_G 7 /* ^G - Cancel incremental search */ +# define CTRL_R 18 /* ^R - Reverse incremental search */ +# endif +#endif #ifdef CONFIG_READLINE_TABCOMPLETION /* Prompt string to present at the beginning of the line */ @@ -150,12 +171,18 @@ static int count_builtin_matches(FAR char *buf, FAR int *matches, * nch - the number of characters. * * Returned Value: - * None. + * True if the completion changed 'buf'/'*nch' and/or echoed anything + * to the terminal (a single unambiguous match was appended, or the + * multiple-match list was printed -- which always ends with a full + * reprint of the prompt and buffer, even if the common prefix did + * not grow). False if nothing happened at all (no match, or an + * exact single match with nothing left to add), in which case the + * terminal's cursor never moved. * ****************************************************************************/ #ifdef CONFIG_READLINE_TABCOMPLETION -static void tab_completion(FAR struct rl_common_s *vtbl, char *buf, +static bool tab_completion(FAR struct rl_common_s *vtbl, char *buf, int buflen, int *nch) { FAR const char *name = NULL; @@ -247,7 +274,10 @@ static void tab_completion(FAR struct rl_common_s *vtbl, char *buf, if (len < name_len) { *nch = name_len; + return true; } + + return false; } /* Are there multiple matching names? */ @@ -359,15 +389,292 @@ static void tab_completion(FAR struct rl_common_s *vtbl, char *buf, * if any */ + /* Don't remove extra characters after the completed word, + * if any + */ + if (len < name_len) { *nch = name_len; } + + /* Whether or not the common prefix grew, the prompt and + * buffer were just reprinted from scratch above, so the + * terminal's cursor is now at the end of the line either + * way. + */ + + return true; } } + + return false; } #endif +#ifdef CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH +/**************************************************************************** + * Name: isearch_find + * + * Description: + * Used by Ctrl+R (reverse incremental search). Search backward + * through the command history, starting just before 'startoffset', + * for the most recent entry containing 'search' as a substring (using + * the same head/offset addressing as the up/down arrow history + * recall code above). If a match is found, it is copied into 'buf' + * (updating '*nch'), and its offset is returned via '*foundoffset' so + * that a subsequent call can continue the search further back in + * history. If no match is found, 'buf'/'*nch' are left unmodified. + * + * Returned Value: + * True if a match was found, false otherwise. + * + ****************************************************************************/ + +static bool isearch_find(FAR const char *search, int searchlen, + int startoffset, FAR int *foundoffset, + FAR char *buf, int buflen, FAR int *nch) +{ + int minoffset; + int offset; + int idx; + int len; + int i; + int j; + bool matched; + + if (searchlen == 0 || g_cmdhist.len == 0) + { + return false; + } + + minoffset = -(g_cmdhist.len - 1); + + for (offset = startoffset - 1; offset >= minoffset; offset--) + { + idx = g_cmdhist.head + offset; + + if (idx < 0) + { + idx += RL_CMDHIST_LEN; + } + else if (idx >= RL_CMDHIST_LEN) + { + idx -= RL_CMDHIST_LEN; + } + + len = strlen(g_cmdhist.buf[idx]); + + /* Does this history entry contain 'search' anywhere? 'search' + * is a raw character buffer that is never null-terminated (the + * caller only tracks its length in 'searchlen'), so this cannot + * use strstr() -- do a plain bounded substring search instead. + */ + + matched = false; + + for (i = 0; i + searchlen <= len; i++) + { + for (j = 0; j < searchlen; j++) + { + if (g_cmdhist.buf[idx][i + j] != search[j]) + { + break; + } + } + + if (j == searchlen) + { + matched = true; + break; + } + } + + if (!matched) + { + continue; + } + + if (len > buflen - 1) + { + len = buflen - 1; + } + + for (i = 0; i < len; i++) + { + buf[i] = g_cmdhist.buf[idx][i]; + } + + buf[len] = '\0'; + *nch = len; + *foundoffset = offset; + return true; + } + + return false; +} +#endif + +#ifdef CONFIG_READLINE_ECHO +#ifdef CONFIG_READLINE_EDIT +/**************************************************************************** + * Name: redraw_line + * + * Description: + * Redraw the prompt and the line buffer from scratch: return to the + * true start of the terminal line, erase to the end of line, reprint + * the prompt (if any), reprint the buffer, then move the cursor left + * as many times as needed to visually land on 'cursor'. This does + * not depend on where the terminal's cursor happened to be beforehand + * -- unlike a backspace-based erase, it is correct no matter where + * the cursor was left by whatever came before it. + * + * This is the common tail end of every full-line redraw in this file + * (Home, End, Ctrl+Left/Right, history recall, ...); consolidating it + * here instead of repeating the same half-dozen lines at each call + * site saves a fair amount of code space. + * + ****************************************************************************/ + +static void redraw_line(FAR struct rl_common_s *vtbl, FAR const char *buf, + int nch, int cursor) +{ + int i; + + RL_PUTC(vtbl, '\r'); + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); + +#ifdef CONFIG_READLINE_TABCOMPLETION + if (g_readline_prompt != NULL) + { + for (i = 0; g_readline_prompt[i] != '\0'; i++) + { + RL_PUTC(vtbl, g_readline_prompt[i]); + } + } +#endif + + if (nch > 0) + { + RL_WRITE(vtbl, buf, nch); + } + + for (i = nch; i > cursor; i--) + { + RL_WRITE(vtbl, g_curleft, sizeof(g_curleft)); + } +} +#endif /* CONFIG_READLINE_EDIT */ + +#ifdef CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH +/**************************************************************************** + * Name: isearch_redraw + * + * Description: + * Redraw the reverse-incremental-search prompt line: + * "(reverse-i-search)`': ", or + * "(failed reverse-i-search)`': " if the current + * search string has no match (in which case 'buf'/'nch' still hold + * whatever the last successful match was, exactly as bash does). + * + ****************************************************************************/ + +static void isearch_redraw(FAR struct rl_common_s *vtbl, + FAR const char *search, int searchlen, + FAR const char *buf, int nch, bool failed) +{ + static const char matchlabel[] = "(reverse-i-search)`"; + static const char faillabel[] = "(failed reverse-i-search)`"; + + RL_PUTC(vtbl, '\r'); + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); + + if (failed) + { + RL_WRITE(vtbl, faillabel, sizeof(faillabel) - 1); + } + else + { + RL_WRITE(vtbl, matchlabel, sizeof(matchlabel) - 1); + } + + if (searchlen > 0) + { + RL_WRITE(vtbl, search, searchlen); + } + + RL_PUTC(vtbl, '\''); + RL_PUTC(vtbl, ':'); + RL_PUTC(vtbl, ' '); + + if (nch > 0) + { + RL_WRITE(vtbl, buf, nch); + } +} +#endif /* CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH */ +#endif /* CONFIG_READLINE_ECHO */ + +/**************************************************************************** + * Name: submit_line + * + * Description: + * Finish a line of input: record it in the command history (if + * enabled and it isn't a duplicate of the most recent entry), + * terminate 'buf' with '\n' and a null terminator, and return the + * number of characters in 'buf' (including the trailing '\n'). This + * is shared by the normal Enter key handling and by Ctrl+R + * (reverse-i-search), which also submits the matched line directly + * on Enter. + * + ****************************************************************************/ + +static ssize_t submit_line(FAR char *buf, int nch) +{ +#ifdef CONFIG_READLINE_CMD_HISTORY + int i; + + /* Save history of command, only if there was something typed besides + * the return character. + */ + + if (nch >= 1) + { + /* If this command is the one at the top of the circular buffer, + * don't save it again. + */ + + if (strncmp(buf, g_cmdhist.buf[g_cmdhist.head], nch + 1) != 0) + { + g_cmdhist.head = (g_cmdhist.head + 1) % RL_CMDHIST_LEN; + + for (i = 0; (i < nch) && i < (RL_CMDHIST_LINELEN - 1); i++) + { + g_cmdhist.buf[g_cmdhist.head][i] = buf[i]; + } + + g_cmdhist.buf[g_cmdhist.head][i] = '\0'; + + if (g_cmdhist.len < RL_CMDHIST_LEN) + { + g_cmdhist.len++; + } + } + + g_cmdhist.offset = 1; + } +#endif /* CONFIG_READLINE_CMD_HISTORY */ + + /* The newline is stored in the buffer along with the null + * terminator. + */ + + buf[nch++] = '\n'; + buf[nch] = '\0'; + + return nch; +} + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -481,6 +788,18 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, #ifdef CONFIG_READLINE_CMD_HISTORY int i; #endif +#ifdef CONFIG_READLINE_EDIT + volatile int cursor; +#endif +#ifdef CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH + bool insearch; + char search[RL_CMDHIST_LINELEN]; + int searchlen; + int searchoffset; + char savedbuf[RL_CMDHIST_LINELEN]; + int savednch; + int savedcursor; +#endif /* Sanity checks */ @@ -505,6 +824,13 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, escape = 0; nch = 0; +#ifdef CONFIG_READLINE_EDIT + cursor = 0; +#endif +#ifdef CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH + insearch = false; + searchlen = 0; +#endif for (; ; ) { @@ -534,20 +860,255 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, return EOF; } +#ifdef CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH + /* Are we in reverse incremental search mode (Ctrl+R)? If so, + * every subsequent keystroke is interpreted as part of the + * search until it is accepted, submitted, or cancelled. + */ + + else if (insearch) + { + if (ch == CTRL_R) + { + /* Repeat: search further back for another match of the + * same search string. + */ + + bool found = isearch_find(search, searchlen, searchoffset, + &searchoffset, buf, buflen, &nch); +#ifdef CONFIG_READLINE_ECHO + isearch_redraw(vtbl, search, searchlen, buf, nch, !found); +#endif + } + else if (ch == ASCII_BS || ch == ASCII_DEL) + { + if (searchlen > 0) + { + searchlen--; + searchoffset = 1; + + if (searchlen > 0) + { + isearch_find(search, searchlen, searchoffset, + &searchoffset, buf, buflen, &nch); + } + else + { + nch = 0; + } + } + +#ifdef CONFIG_READLINE_ECHO + isearch_redraw(vtbl, search, searchlen, buf, nch, + searchlen > 0 && nch == 0); +#endif + } + else if (ch == CTRL_G || ch == ASCII_ETX) /* ^G or ^C: cancel */ + { + /* Cancel: restore the line exactly as it was before + * Ctrl+R was pressed. + */ + + insearch = false; + nch = savednch; + cursor = savedcursor; + + for (i = 0; i < nch; i++) + { + buf[i] = savedbuf[i]; + } + + buf[nch] = '\0'; + +#ifdef CONFIG_READLINE_ECHO + redraw_line(vtbl, buf, nch, cursor); +#endif + } + else if (ch == '\n') + { + /* Accept the current match and submit it immediately, + * exactly as bash does. + */ + + insearch = false; + return submit_line(buf, nch); + } + else if (ch == ASCII_ESC) + { + /* Accept the current match into the line, then let the + * escape sequence that follows (if any) be processed + * normally against it on the next iteration(s). + */ + + insearch = false; + cursor = nch; + escape = 1; + } + else if (!iscntrl(ch & 0xff) && searchlen < RL_CMDHIST_LINELEN - 1) + { + search[searchlen++] = (char)ch; + searchoffset = 1; + + bool found = isearch_find(search, searchlen, searchoffset, + &searchoffset, buf, buflen, &nch); +#ifdef CONFIG_READLINE_ECHO + isearch_redraw(vtbl, search, searchlen, buf, nch, !found); +#endif + } + else + { + /* Anything else (an unhandled control character) just + * accepts the current match and returns to normal + * editing. + */ + + insearch = false; + cursor = nch; + +#ifdef CONFIG_READLINE_ECHO + redraw_line(vtbl, buf, nch, cursor); +#endif + } + + continue; + } +#endif + /* Are we processing a VT100 escape sequence */ else if (escape) { +#ifdef CONFIG_READLINE_EDIT + /* Delete key: ESC [ 3 ~ — waiting for '~' */ + + if (escape == 3) + { + escape = 0; + if (ch == '~' && 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)); + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); + if (cursor < nch) + { + RL_WRITE(vtbl, buf + cursor, nch - cursor); + for (k = nch; k > cursor; k--) + RL_WRITE(vtbl, g_curleft, sizeof(g_curleft)); + } +# endif + } + continue; + } + + if (escape == 4 || escape == 5) + { + if (ch == '~') + { + /* Home (1~) or End (4~) */ + + cursor = (escape == 4) ? 0 : nch; + redraw_line(vtbl, buf, nch, cursor); + } + + if (ch == ';') + { + escape = 8; + continue; + } + + escape = 0; + continue; + } +#endif + +#ifdef CONFIG_READLINE_EDIT + if (escape == 8) /* CSI ; — waiting for modifier digit */ + { + if (ch == '5') + { + escape = 9; /* Ready for final char */ + continue; + } + + if (ch == ';') + { + escape = 8; + continue; + } + + escape = 0; + continue; + } + + if (escape == 9) /* CSI ;5 — waiting for D or C */ + { + escape = 0; + + if (ch == 'D') /* Ctrl+Left */ + { + while (cursor > 0 && buf[cursor - 1] == ' ') + cursor--; + while (cursor > 0 && buf[cursor - 1] != ' ') + cursor--; + + redraw_line(vtbl, buf, nch, cursor); + } + else if (ch == 'C') /* Ctrl+Right */ + { + while (cursor < nch && buf[cursor] != ' ') + cursor++; + while (cursor < nch && buf[cursor] == ' ') + cursor++; + + redraw_line(vtbl, buf, nch, cursor); + } + + continue; + } +#endif /* CONFIG_READLINE_EDIT */ + + /* 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 + * "ESC O" prefix here and fall into exactly the same + * "terminator character" handling used for "ESC [ " below, + * by advancing to a dedicated state (6) that is treated the + * same way state 2 (saw "ESC [") is treated once the final + * byte arrives. Without this, the 'O' is silently dropped + * (falling through the unrecognized-sequence path below) and + * the *next* byte (e.g. 'F' for End, 'H' for Home) is left to + * be reprocessed as an ordinary printable character, which is + * what produced the stray inserted "OF"/"OH" text. + */ + + if (escape == 1 && ch == ASCII_O) + { + escape = 6; + continue; + } + /* Yes, is it an [, 3 byte sequence */ - if (ch != ASCII_LBRACKET || escape == 2) + if (ch != ASCII_LBRACKET || escape == 2 || escape == 6) { /* We are finished with the escape sequence */ #ifdef CONFIG_READLINE_CMD_HISTORY - /* Intercept up and down arrow keys */ + /* Intercept up and down arrow keys. This must only run + * for the up/down arrow keys themselves -- previously the + * clear-and-reload logic below ran for *any* completed + * escape sequence (Left, Right, Home, End, Delete, ...) + * as long as some history existed, silently wiping + * whatever the user was currently typing. + */ - if (g_cmdhist.len > 0) + if (g_cmdhist.len > 0 && (ch == 'A' || ch == 'B')) { if (ch == 'A') /* up arrow */ { @@ -560,7 +1121,7 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, g_cmdhist.offset = -(g_cmdhist.len - 1); } } - else if (ch == 'B') /* down arrow */ + else /* down arrow */ { /* Go to the recent command in history */ @@ -572,17 +1133,39 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, } } - /* Clear out current command from the prompt */ + /* Clear out current command from the prompt. + * + * This cannot assume the terminal's cursor is + * sitting at the end of the currently displayed + * text (i.e. at column 'nch') and simply backspace + * 'nch' times -- the cursor can be anywhere in the + * line (e.g. the user pressed Left one or more + * times before pressing Up/Down again), and + * backspacing more times than the cursor's actual + * distance from the end of the prompt walks back + * into and erases part of the prompt itself. + * Instead, return to the true start of the + * terminal line and erase to the end of line, then + * reprint the prompt -- this does not depend on + * where the cursor happened to be. + */ - while (nch > 0) - { - nch--; + nch = 0; #ifdef CONFIG_READLINE_ECHO - RL_PUTC(vtbl, ASCII_BS); - RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); -#endif + RL_PUTC(vtbl, '\r'); + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); +#ifdef CONFIG_READLINE_TABCOMPLETION + if (g_readline_prompt != NULL) + { + int k; + for (k = 0; g_readline_prompt[k] != '\0'; k++) + { + RL_PUTC(vtbl, g_readline_prompt[k]); + } } +#endif +#endif if (g_cmdhist.offset != 1) { @@ -599,7 +1182,10 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, idx = idx - RL_CMDHIST_LEN; } - for (i = 0; g_cmdhist.buf[idx][i] != '\0'; i++) + for (i = 0; + g_cmdhist.buf[idx][i] != '\0' && + nch + 1 < buflen; + i++) { buf[nch++] = g_cmdhist.buf[idx][i]; RL_PUTC(vtbl, g_cmdhist.buf[idx][i]); @@ -607,9 +1193,84 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, buf[nch] = '\0'; } + + /* The line was just wholesale replaced -- the cursor + * must always end up in sync with the new length, or + * later edits (insert/delete) will index into buf[] + * using a stale, out-of-range cursor value. + */ + +#ifdef CONFIG_READLINE_EDIT + cursor = nch; +#endif } #endif /* CONFIG_READLINE_CMD_HISTORY */ +#ifdef CONFIG_READLINE_EDIT + if (ch == '1') + { + escape = 4; + continue; + } + + if (ch == ';') + { + escape = 8; + continue; + } + + if (ch == '3') + { + escape = 3; + continue; + } + + if (ch == '4') + { + escape = 5; + continue; + } + + if (ch == 'F') + { + escape = 0; + cursor = nch; + + /* Redraw full line — cursor ends at end */ + + redraw_line(vtbl, buf, nch, cursor); + continue; + } + + if (ch == 'D' && cursor > 0) + { + cursor--; + escape = 0; + RL_WRITE(vtbl, g_curleft, sizeof(g_curleft)); + continue; + } + + if (ch == 'C' && cursor < nch) + { + cursor++; + escape = 0; + RL_WRITE(vtbl, g_curright, sizeof(g_curright)); + continue; + } + + if (ch == 'H') + { + escape = 0; + cursor = 0; + + /* Redraw full line from column 0 */ + + redraw_line(vtbl, buf, nch, cursor); + + continue; + } +#endif + escape = 0; ch = 'a'; } @@ -636,22 +1297,41 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, else if (ch == ASCII_BS || ch == ASCII_DEL) { - /* Eliminate that last character in the buffer. */ +#ifdef CONFIG_READLINE_EDIT + /* Delete character before cursor */ + if (cursor > 0) + { + int k; + for (k = cursor; k < nch; k++) + buf[k - 1] = buf[k]; + cursor--; + nch--; + +# ifdef CONFIG_READLINE_ECHO + /* Redraw: backspace, clear EOL, redraw tail, restore cursor */ + + RL_PUTC(vtbl, ASCII_BS); + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); + if (cursor < nch) + { + RL_WRITE(vtbl, buf + cursor, nch - cursor); + for (k = nch; k > cursor; k--) + RL_WRITE(vtbl, g_curleft, sizeof(g_curleft)); + } +# endif + } +#else if (nch > 0) { nch--; -#ifdef CONFIG_READLINE_ECHO - /* Echo the backspace character on the console. Always output - * the backspace character because the VT100 terminal doesn't - * understand DEL properly. - */ - +# ifdef CONFIG_READLINE_ECHO RL_PUTC(vtbl, ASCII_BS); RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); -#endif +# endif } +#endif } /* Check for the beginning of a VT100 escape sequence */ @@ -670,55 +1350,175 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, else if (ch == '\n') { -#ifdef CONFIG_READLINE_CMD_HISTORY - /* Save history of command, only if there was something - * typed besides return character. - */ - - if (nch >= 1) - { - /* If this command is the one at the top of the circular - * buffer, don't save it again. - */ - - if (strncmp(buf, g_cmdhist.buf[g_cmdhist.head], nch + 1) != 0) - { - g_cmdhist.head = (g_cmdhist.head + 1) % RL_CMDHIST_LEN; - - for (i = 0; (i < nch) && i < (RL_CMDHIST_LINELEN - 1); i++) - { - g_cmdhist.buf[g_cmdhist.head][i] = buf[i]; - } - - g_cmdhist.buf[g_cmdhist.head][i] = '\0'; - - if (g_cmdhist.len < RL_CMDHIST_LEN) - { - g_cmdhist.len++; - } - } - - g_cmdhist.offset = 1; - } -#endif /* CONFIG_READLINE_CMD_HISTORY */ - - /* The newline is stored in the buffer along with the null - * terminator. - */ - - buf[nch++] = '\n'; - buf[nch] = '\0'; - - return nch; + return submit_line(buf, nch); } + /* Emacs-style control keys */ + +#ifdef CONFIG_READLINE_EDIT_EMACS + else if (ch == CTRL_A) /* Ctrl+A = Home */ + { + cursor = 0; + redraw_line(vtbl, buf, nch, cursor); + } + else if (ch == CTRL_B) /* Ctrl+B = Left */ + { + if (cursor > 0) + { + cursor--; + RL_WRITE(vtbl, g_curleft, sizeof(g_curleft)); + } + } + else if (ch == CTRL_D) /* Ctrl+D = Delete at cursor */ + { + if (cursor < nch) + { + int k; + for (k = cursor + 1; k < nch; k++) + buf[k - 1] = buf[k]; + nch--; + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); + if (cursor < nch) + { + RL_WRITE(vtbl, buf + cursor, nch - cursor); + for (k = nch; k > cursor; k--) + RL_WRITE(vtbl, g_curleft, sizeof(g_curleft)); + } + } + } + else if (ch == CTRL_E) /* Ctrl+E = End */ + { + while (cursor < nch) + { + cursor++; + RL_WRITE(vtbl, g_curright, sizeof(g_curright)); + } + } + else if (ch == CTRL_F) /* Ctrl+F = Right */ + { + if (cursor < nch) + { + cursor++; + RL_WRITE(vtbl, g_curright, sizeof(g_curright)); + } + } + else if (ch == CTRL_K) /* Ctrl+K = Kill to EOL */ + { + nch = cursor; + buf[nch] = '\0'; + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); + } + else if (ch == CTRL_U) /* Ctrl+U = Kill to BOL */ + { + int j; + for (j = cursor; j < nch; j++) + buf[j - cursor] = buf[j]; + nch -= cursor; + cursor = 0; + buf[nch] = '\0'; + redraw_line(vtbl, buf, nch, cursor); + } + else if (ch == CTRL_W) /* Ctrl+W = Kill word backward (FIXME) */ + { + int start, k; + start = cursor; + while (start > 0 && buf[start - 1] == ' ') + start--; + while (start > 0 && buf[start - 1] != ' ') + start--; + for (k = start; k < nch; k++) + buf[k - (cursor - start)] = buf[k]; + nch -= (cursor - start); + cursor = start; + buf[nch] = '\0'; + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); + if (cursor < nch) + { + RL_WRITE(vtbl, buf + cursor, nch - cursor); + for (k = nch; k > cursor; k--) + RL_WRITE(vtbl, g_curleft, sizeof(g_curleft)); + } + } + +#ifdef CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH + else if (ch == CTRL_R && g_cmdhist.len > 0) + { + /* Ctrl+R = start a reverse incremental search through the + * command history, bash-style. Save the line currently + * being edited so it can be restored if the search is + * cancelled (Ctrl+G). + */ + + insearch = true; + searchlen = 0; + searchoffset = 1; + + savednch = nch; + if (savednch > RL_CMDHIST_LINELEN - 1) + { + savednch = RL_CMDHIST_LINELEN - 1; + } + + for (i = 0; i < savednch; i++) + { + savedbuf[i] = buf[i]; + } + + savedcursor = cursor; + nch = 0; + +#ifdef CONFIG_READLINE_ECHO + isearch_redraw(vtbl, search, searchlen, buf, nch, false); +#endif + } +#endif /* CONFIG_READLINE_EDIT_EMACS_REVERSE_SEARCH */ +#endif /* CONFIG_READLINE_EDIT_EMACS */ + /* Otherwise, put the character in the line buffer if the * character is not a control byte */ else if (!iscntrl(ch & 0xff)) { +#ifdef CONFIG_READLINE_EDIT + /* Defensive check: 'cursor' must always satisfy + * 0 <= cursor <= nch. It should never be able to get out of + * that range now, but clamp it rather than trust it blindly + * -- indexing buf[] with an out-of-range cursor is a + * memory-safety bug (a stale cursor previously let this + * write arbitrarily far past the end of the caller's + * buffer). + */ + + if (cursor < 0 || cursor > nch) + { + cursor = nch; + } + + /* Only insert if there is room for the new character plus + * the line's null terminator. Checking this before writing + * (rather than only after, as the non-editing path below + * does) is what actually prevents the out-of-bounds write. + */ + + if (nch + 1 < buflen) + { + int j; + for (j = nch; j > cursor; j--) buf[j] = buf[j - 1]; + buf[cursor] = (char)ch; nch++; cursor++; +# ifdef CONFIG_READLINE_ECHO + if (cursor < nch) + { + RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol)); + RL_WRITE(vtbl, buf + cursor, nch - cursor); + for (j = nch; j > cursor; j--) + RL_WRITE(vtbl, g_curleft, sizeof(g_curleft)); + } +# endif + } +#else buf[nch++] = ch; +#endif /* Check if there is room for another character and the line's * null terminator. If not then we have to end the line now. @@ -733,7 +1533,34 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR char *buf, #ifdef CONFIG_READLINE_TABCOMPLETION else if (ch == '\t') /* TAB character */ { + /* When tab_completion() finds a match (or lists several), it + * always leaves the terminal's real cursor at the end of the + * (possibly completed) line -- either by echoing the + * appended characters one at a time, or, when it lists + * multiple matches, by reprinting the prompt and the whole + * buffer from scratch. 'cursor' has to be resynced to + * match in that case, or the very next Left/Right/Home/End + * keypress will move the terminal's cursor from column + * 'nch' while still believing it is moving from wherever + * 'cursor' was left (typically the length of the word + * before completion) -- the two then stay out of sync for + * the rest of the line. + * + * But if there was no match at all, tab_completion() does + * not touch the terminal or the buffer, and 'cursor' must + * be left exactly where it was -- unconditionally resyncing + * it here would be just as wrong as never resyncing it, + * only in the opposite direction. + */ + +#ifdef CONFIG_READLINE_EDIT + if (tab_completion(vtbl, buf, buflen, &nch)) + { + cursor = nch; + } +#else tab_completion(vtbl, buf, buflen, &nch); +#endif } #endif }