This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git

commit 51bfd48e78a55b8b6eab53c5743bbadfe5de2a2a
Author: Alan C. Assis <[email protected]>
AuthorDate: Tue Jul 14 21:39:39 2026 -0300

    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 <[email protected]>
    Assisted-by: Claude Code
---
 nshlib/nsh_session.c              |   6 +
 system/readline/Kconfig           |  30 ++
 system/readline/readline_common.c | 931 +++++++++++++++++++++++++++++++++++---
 3 files changed, 915 insertions(+), 52 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)`<search>': <match>", or
+ *   "(failed reverse-i-search)`<search>': <match>" 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 [ <x>" 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 <esc>[, 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 (nch > 0)
+          if (cursor > 0)
             {
+              int k;
+              for (k = cursor; k < nch; k++)
+                buf[k - 1] = buf[k];
+              cursor--;
               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
+              /* Redraw: backspace, clear EOL, redraw tail, restore cursor */
 
               RL_PUTC(vtbl, ASCII_BS);
               RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol));
-#endif
+              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
+              RL_PUTC(vtbl, ASCII_BS);
+              RL_WRITE(vtbl, g_erasetoeol, sizeof(g_erasetoeol));
+#  endif
             }
+#endif
         }
 
       /* Check for the beginning of a VT100 escape sequence */
@@ -670,47 +1350,129 @@ 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.
-           */
+          return submit_line(buf, nch);
+        }
 
-          if (nch >= 1)
-            {
-              /* If this command is the one at the top of the circular
-               * buffer, don't save it again.
-               */
+      /* Emacs-style control keys */
 
-              if (strncmp(buf, g_cmdhist.buf[g_cmdhist.head], nch + 1) != 0)
+#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)
                 {
-                  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];
-                    }
+                  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));
+            }
+        }
 
-                  g_cmdhist.buf[g_cmdhist.head][i] = '\0';
+#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).
+           */
 
-                  if (g_cmdhist.len < RL_CMDHIST_LEN)
-                    {
-                      g_cmdhist.len++;
-                    }
-                }
+          insearch     = true;
+          searchlen    = 0;
+          searchoffset = 1;
 
-              g_cmdhist.offset = 1;
+          savednch = nch;
+          if (savednch > RL_CMDHIST_LINELEN - 1)
+            {
+              savednch = RL_CMDHIST_LINELEN - 1;
             }
-#endif /* CONFIG_READLINE_CMD_HISTORY */
 
-          /* The newline is stored in the buffer along with the null
-           * terminator.
-           */
+          for (i = 0; i < savednch; i++)
+            {
+              savedbuf[i] = buf[i];
+            }
 
-          buf[nch++] = '\n';
-          buf[nch]   = '\0';
+          savedcursor = cursor;
+          nch         = 0;
 
-          return nch;
+#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
@@ -718,7 +1480,45 @@ ssize_t readline_common(FAR struct rl_common_s *vtbl, FAR 
char *buf,
 
       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
     }


Reply via email to