/**************************************************************************** * apps/system/cle/cle.c * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. The * ASF licenses this file to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. * ****************************************************************************/ /**************************************************************************** * Included Files ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "system/cle.h" /**************************************************************************** * Pre-processor Definitions ****************************************************************************/ /* Control characters */ #undef CTRL #define CTRL(a) ((a) & 0x1f) #define CLE_BEL(priv) cle_putch(priv,CTRL('G')) /* Sizes of things */ #define TABSIZE 8 /* A TAB is eight characters */ #define TABMASK 7 /* Mask for TAB alignment */ #define NEXT_TAB(p) (((p) + TABSIZE) & ~TABMASK) /* Debug */ #ifndef CONFIG_SYSTEM_CLE_DEBUGLEVEL # define CONFIG_SYSTEM_CLE_DEBUGLEVEL 0 #endif #if CONFIG_SYSTEM_CLE_DEBUGLEVEL > 0 # define cledbg cle_debug #else # define cledbg _none #endif #if CONFIG_SYSTEM_CLE_DEBUGLEVEL > 1 # define cleinfo cle_debug #else # define cleinfo _none #endif #ifdef CONFIG_SYSTEM_COLOR_CLE # define COLOR_PROMPT VT100_YELLOW # define COLOR_COMMAND VT100_CYAN # define COLOR_OUTPUT VT100_GREEN #endif /**************************************************************************** * Private Types ****************************************************************************/ /* VI Key Bindings */ enum cle_key_e { KEY_BEGINLINE = CTRL('A'), /* Move cursor to start of current line */ KEY_LEFT = CTRL('B'), /* Move left one character */ KEY_DEL = CTRL('D'), /* Delete a single character at the cursor position */ KEY_ENDLINE = CTRL('E'), /* Move cursor to end of current line */ KEY_RIGHT = CTRL('F'), /* Move right one character */ KEY_DELLEFT = CTRL('H'), /* Delete character, left (backspace) */ KEY_DELEOL = CTRL('K'), /* Delete to the end of the line */ KEY_CLRSCR = CTRL('L'), /* Clear the screen */ KEY_DN = CTRL('N'), /* Cursor down */ KEY_UP = CTRL('P'), /* Cursor up */ KEY_DELLINE = CTRL('U'), /* Delete the entire line */ KEY_QUOTE = '\\' /* The next character is quote (use literal value) */ }; /* This structure describes the overall state of the editor */ struct cle_s { int16_t curpos; /* Current cursor position in buffer */ int16_t realpos; /* Current cursor position in terminal */ uint16_t coloffs; /* Left cursor offset */ uint16_t linelen; /* Size of the line buffer */ uint16_t nchars; /* Size of data in the line buffer */ int infd; /* Input file handle */ int outfd; /* Output file handle */ FAR char *line; /* Line buffer */ FAR const char *prompt; /* Prompt, in case we have to re-print it */ }; /**************************************************************************** * Private Function Prototypes ****************************************************************************/ #if CONFIG_SYSTEM_CLE_DEBUGLEVEL > 0 static void cle_debug(FAR const char *fmt, ...) printf_like(1, 2); #endif /* Low-level display and data entry functions */ static void cle_write(FAR struct cle_s *priv, FAR const char *buffer, uint16_t buflen); static void cle_putch(FAR struct cle_s *priv, char ch); static int cle_getch(FAR struct cle_s *priv); static void cle_cursoron(FAR struct cle_s *priv); static void cle_cursoroff(FAR struct cle_s *priv); static void cle_setcursor(FAR struct cle_s *priv, int16_t column); static void cle_clrtoeol(FAR struct cle_s *priv); /* Editor function */ static bool cle_opentext(FAR struct cle_s *priv, uint16_t pos, uint16_t increment); static void cle_closetext(FAR struct cle_s *priv, uint16_t pos, uint16_t size); static void cle_showtext(FAR struct cle_s *priv); static void cle_insertch(FAR struct cle_s *priv, char ch); static int cle_editloop(FAR struct cle_s *priv); /**************************************************************************** * Private Data ****************************************************************************/ #ifdef CONFIG_SYSTEM_CLE_CMD_HISTORY /* Command history * * g_cmd_history[][] Circular buffer * g_cmd_history_head Head of the circular buffer, most recent * command * g_cmd_history_steps_from_head Offset from head * g_cmd_history_len Number of elements in the circular buffer * * REVISIT: These globals will *not* work in an environment where there * are multiple copies if the NSH shell! Use of global variables is not * thread safe! These settings should, at least, be semaphore protected so * that the integrity of the data is assured, even though commands from * different sessions may be intermixed. */ static char g_cmd_history[CONFIG_SYSTEM_CLE_CMD_HISTORY_LEN] [CONFIG_SYSTEM_CLE_CMD_HISTORY_LINELEN]; static int g_cmd_history_head = -1; static int g_cmd_history_steps_from_head = 1; static int g_cmd_history_len = 0; #endif /* CONFIG_SYSTEM_CLE_CMD_HISTORY */ /* VT100 escape sequences */ static const char g_cursoron[] = VT100_CURSORON; static const char g_cursoroff[] = VT100_CURSOROFF; static const char g_erasetoeol[] = VT100_CLEAREOL; static const char g_clrscr[] = VT100_CLEARSCREEN; static const char g_clrline[] = VT100_CLEARLINE; static const char g_home[] = VT100_CURSORHOME; #ifdef CONFIG_SYSTEM_COLOR_CLE static const char g_setcolor[] = VT100_FMT_FORE_COLOR; #endif /**************************************************************************** * Private Functions ****************************************************************************/ /**************************************************************************** * Name: cle_debug * * Description: * Print a debug message to the syslog * ****************************************************************************/ #if CONFIG_SYSTEM_CLE_DEBUGLEVEL > 0 static void cle_debug(FAR const char *fmt, ...) { va_list ap; /* Let vsyslog do the real work */ va_start(ap, fmt); vsyslog(LOG_DEBUG, fmt, ap); va_end(ap); } #endif /**************************************************************************** * Name: cle_write * * Description: * Write a sequence of bytes to the console output device. * ****************************************************************************/ static void cle_write(FAR struct cle_s *priv, FAR const char *buffer, uint16_t buflen) { ssize_t nwritten; /* Loop until all bytes have been successfully written (or until a * unrecoverable error is encountered) */ do { /* Put the next gulp */ nwritten = write(priv->outfd, buffer, buflen); /* Handle write errors. write() should neve return 0. */ if (nwritten <= 0) { /* EINTR is not really an error; it simply means that a signal was * received while waiting for write. */ int errcode = errno; if (nwritten == 0 || errcode != EINTR) { cledbg("ERROR: write to stdout failed: %d\n", errcode); return; } } /* Decrement the count of bytes remaining to be sent (to handle the * case of a partial write) */ else { buffer += nwritten; buflen -= nwritten; } } while (buflen > 0); } /**************************************************************************** * Name: cle_putch * * Description: * Write a single character to the console output device. * ****************************************************************************/ static void cle_putch(FAR struct cle_s *priv, char ch) { cle_write(priv, &ch, 1); } /**************************************************************************** * Name: cle_getch * * Description: * Get a single character from the console input device. * ****************************************************************************/ static int cle_getch(FAR struct cle_s *priv) { char buffer; ssize_t nread; /* Loop until we successfully read a character (or until an unexpected * error occurs). */ do { /* Read one character from the incoming stream */ nread = read(priv->infd, &buffer, 1); /* Check for error or end-of-file. */ if (nread <= 0) { /* EINTR is not really an error; it simply means that a signal we * received while waiting for input. */ int errcode = errno; if (nread == 0 || errcode != EINTR) { cledbg("ERROR: read from stdin failed: %d\n", errcode); return -EIO; } } } while (nread < 1); /* On success, return the character that was read */ cleinfo("Returning: %c[%02x]\n", isprint(buffer) ? buffer : '.', buffer); return buffer; } /**************************************************************************** * Name: cle_cursoron * * Description: * Turn on the cursor * ****************************************************************************/ static void cle_cursoron(FAR struct cle_s *priv) { /* Send the VT100 CURSORON command */ cle_write(priv, g_cursoron, sizeof(g_cursoron)); } /**************************************************************************** * Name: cle_cursoroff * * Description: * Turn off the cursor * ****************************************************************************/ static void cle_cursoroff(FAR struct cle_s *priv) { /* Send the VT100 CURSOROFF command */ cle_write(priv, g_cursoroff, sizeof(g_cursoroff)); } /**************************************************************************** * Name: cle_setcursor * * Description: * Move the current cursor position to position (row,col) * ****************************************************************************/ static void cle_setcursor(FAR struct cle_s *priv, int16_t column) { char buffer[16]; int len; int off; /* Sub prompt offset from real postion to get correct offset to execute */ off = column - (priv->realpos - priv->coloffs); cleinfo("column=%d offset=%d\n", column, off); /* If cursor not move, retrun directly */ if (off == 0) { return; } /* If position after adjustment is belong to promot area, * limit it to edge of the prompt. */ if (off + priv->realpos < priv->coloffs) { off = priv->realpos - priv->coloffs; } /* Format the cursor position command. * Move left or right depends on the current cursor position in buffer. */ len = snprintf(buffer, sizeof(buffer), off < 0 ? VT100_FMT_CURSORLF : VT100_FMT_CURSORRT, off < 0 ? -off : off); /* Send the VT100 CURSORPOS command */ cle_write(priv, buffer, len); /* Update the current cursor position in terminal */ priv->realpos = priv->coloffs + column; } /**************************************************************************** * Name: cle_setcolor * * Description: * Set foreground color * ****************************************************************************/ #ifdef CONFIG_SYSTEM_COLOR_CLE static void cle_setcolor(FAR struct cle_s *priv, uint8_t color) { char buffer[16]; int len; len = snprintf(buffer, 16, g_setcolor, color); cle_write(priv, buffer, len); } #endif /**************************************************************************** * Name: cle_outputprompt * * Description: * Send the prompt to the screen * ****************************************************************************/ static void cle_outputprompt(FAR struct cle_s *priv) { #ifdef CONFIG_SYSTEM_COLOR_CLE cle_setcolor(priv, COLOR_PROMPT); #endif cle_write(priv, priv->prompt, strlen(priv->prompt)); #ifdef CONFIG_SYSTEM_COLOR_CLE cle_setcolor(priv, COLOR_OUTPUT); #endif } /**************************************************************************** * Name: cle_clrscr * * Description: * Clear the screen, and re-establish the prompt * ****************************************************************************/ static void cle_clrscr(FAR struct cle_s *priv) { /* Send the VT100 CLEARSCREEN command */ cle_write(priv, g_clrscr, sizeof(g_clrscr)); cle_write(priv, g_home, sizeof(g_home)); cle_outputprompt(priv); } /**************************************************************************** * Name: cle_clrtoeol * * Description: * Clear the display from the current cursor position to the end of the * current line. * ****************************************************************************/ static void cle_clrtoeol(FAR struct cle_s *priv) { /* Send the VT100 ERASETOEOL command */ cle_write(priv, g_erasetoeol, sizeof(g_erasetoeol)); } /**************************************************************************** * Name: cle_opentext * * Description: * Make space for new text of size 'increment' at the specified cursor * position. This function will not allow text grow beyond ('linelen' - 1) * in size. * ****************************************************************************/ static bool cle_opentext(FAR struct cle_s *priv, uint16_t pos, uint16_t increment) { int i; cleinfo("pos=%ld increment=%ld\n", (long)pos, (long)increment); /* Check if there is space in the line buffer to open up a region the size * of 'increment' */ if (priv->nchars + increment >= priv->linelen) { CLE_BEL(priv); return false; } /* Move text to make space for new text of size 'increment' at the current * cursor position */ for (i = priv->nchars - 1; i >= pos; i--) { priv->line[i + increment] = priv->line[i]; } /* Adjust end of file position */ priv->nchars += increment; return true; } /**************************************************************************** * Name: cle_closetext * * Description: * Delete a region in the line buffer by copying the end of the line buffer * over the deleted region and adjusting the size of the region. * ****************************************************************************/ static void cle_closetext(FAR struct cle_s *priv, uint16_t pos, uint16_t size) { int i; cleinfo("pos=%ld size=%ld\n", (long)pos, (long)size); /* Close up the gap to remove 'size' characters at 'pos' */ for (i = pos + size; i < priv->nchars; i++) { priv->line[i - size] = priv->line[i]; } /* Adjust sizes and positions */ priv->nchars -= size; if (priv->curpos > pos) { /* Check if the cursor position is beyond the deleted region */ if (priv->curpos - pos > size) { /* Yes... just subtract the size of the deleted region */ priv->curpos -= size; } else { /* What if the position is within the deleted region? Set it to * the beginning of the deleted region. */ priv->curpos = pos; } } } /**************************************************************************** * Name: cle_showtext * * Description: * Update the display based on the last operation. This function is * called at the beginning of the editor loop. * ****************************************************************************/ static void cle_showtext(FAR struct cle_s *priv) { uint16_t column; uint16_t tabcol; /* Turn off the cursor during the update. */ cle_cursoroff(priv); /* Set the cursor position to the beginning of this row */ #ifdef CONFIG_SYSTEM_COLOR_CLE cle_setcolor(priv, COLOR_COMMAND); #endif cle_setcursor(priv, 0); cle_clrtoeol(priv); /* Loop for each column */ for (column = 0; column < priv->nchars; ) { /* Perform TAB expansion */ if (priv->line[column] == '\t') { tabcol = NEXT_TAB(column); if (tabcol < priv->linelen) { for (; column < tabcol; column++) { cle_putch(priv, ' '); priv->realpos++; } } else { /* Break out of the loop... there is nothing left on the * line but whitespace. */ break; } } /* Add the normal character to the display */ else { cle_putch(priv, priv->line[column]); column++; priv->realpos++; } } #ifdef CONFIG_SYSTEM_COLOR_CLE cle_setcolor(priv, COLOR_OUTPUT); #endif /* Turn the cursor back on */ cle_cursoron(priv); } /**************************************************************************** * Name: cle_insertch * * Description: * Insert one character into the line buffer * ****************************************************************************/ static void cle_insertch(FAR struct cle_s *priv, char ch) { cleinfo("curpos=%" PRId16 " ch=%c[%02x]\n", priv->curpos, isprint(ch) ? ch : '.', ch); /* Make space in the buffer for the new character */ if (cle_opentext(priv, priv->curpos, 1)) { /* Add the new character to the buffer */ priv->line[priv->curpos++] = ch; } } /**************************************************************************** * Name: cle_editloop * * Description: * Command line editor loop * ****************************************************************************/ static int cle_editloop(FAR struct cle_s *priv) { /* Loop while we are in command mode */ for (; ; ) { #if 1 /* Perhaps here should be a config switch */ char state = 0; #endif int ch; /* Make sure that the display reflects the current state */ cle_showtext(priv); cle_setcursor(priv, priv->curpos); /* Get the next character from the input */ #if 1 /* Perhaps here should be a config switch */ /* Simple decode of some VT100/xterm codes: left/right, up/dn, * home/end, del */ /* loop till we have a ch */ for (; ; ) { ch = cle_getch(priv); if (ch < 0) { return -EIO; } else if (state != 0) { if (state == (char)1) /* Got ESC */ { if (ch == '[' || ch == 'O') { state = ch; } else { break; /* break the for loop */ } } else if (state == '[') { /* Got ESC[ */ switch (ch) { case '3': /* ESC[3~ = DEL */ { state = ch; continue; } case 'A': { ch = KEY_UP; } break; case 'B': { ch = KEY_DN; } break; case 'C': { ch = KEY_RIGHT; } break; case 'D': { ch = KEY_LEFT; } break; case 'F': { ch = KEY_ENDLINE; } break; case 'H': { ch = KEY_BEGINLINE; } break; default: break; } break; /* Break the 'for' loop */ } else if (state == 'O') { /* got ESCO */ if (ch == 'F') { ch = KEY_ENDLINE; } break; /* Break the 'for' loop */ } else if (state == '3') { if (ch == '~') { ch = KEY_DEL; } break; /* Break the 'for' loop */ } else { break; /* Break the 'for' loop */ } } else if (ch == ASCII_ESC) { ++state; } else { break; /* Break the 'for' loop, use the char */ } } #else ch = cle_getch(priv); if (ch < 0) { return -EIO; } #endif /* Then handle the character. */ #ifdef CONFIG_SYSTEM_CLE_CMD_HISTORY if (g_cmd_history_len > 0) { int i = 1; switch (ch) { case KEY_UP: /* Go to the past command in history */ g_cmd_history_steps_from_head--; if (-g_cmd_history_steps_from_head >= g_cmd_history_len) { g_cmd_history_steps_from_head = -(g_cmd_history_len - 1); } break; case KEY_DN: /* Go to the recent command in history */ g_cmd_history_steps_from_head++; if (g_cmd_history_steps_from_head > 1) { g_cmd_history_steps_from_head = 1; } break; default: i = 0; break; } if (i != 0) { priv->nchars = 0; priv->curpos = 0; if (g_cmd_history_steps_from_head != 1) { int idx = g_cmd_history_head + g_cmd_history_steps_from_head; /* Circular buffer wrap around */ if (idx < 0) { idx = idx + CONFIG_SYSTEM_CLE_CMD_HISTORY_LEN; } else if (idx >= CONFIG_SYSTEM_CLE_CMD_HISTORY_LEN) { idx = idx - CONFIG_SYSTEM_CLE_CMD_HISTORY_LEN; } for (i = 0; g_cmd_history[idx][i] != '\0'; i++) { cle_insertch(priv, g_cmd_history[idx][i]); } priv->curpos = priv->nchars; } continue; } } #endif /* CONFIG_SYSTEM_CLE_CMD_HISTORY */ switch (ch) { case KEY_BEGINLINE: /* Move cursor to start of current line */ { priv->curpos = 0; } break; case KEY_LEFT: /* Move the cursor left 1 character */ { if (priv->curpos > 0) { priv->curpos--; } else { CLE_BEL(priv); } } break; case KEY_DEL: /* Delete 1 character at the cursor */ { if (priv->curpos < priv->nchars) { cle_closetext(priv, priv->curpos, 1); } else { CLE_BEL(priv); } } break; case KEY_ENDLINE: /* Move cursor to end of current line */ { priv->curpos = priv->nchars; } break; case KEY_RIGHT: /* Move the cursor right one character */ { if (priv->curpos < priv->nchars) { priv->curpos++; } else { CLE_BEL(priv); } } break; case ASCII_DEL: case KEY_DELLEFT: /* Delete 1 character before the cursor */ { if (priv->curpos > 0) { cle_closetext(priv, --priv->curpos, 1); } else { CLE_BEL(priv); } } break; case KEY_DELEOL: /* Delete to the end of the line */ { priv->nchars = (priv->nchars > 0 ? priv->curpos : 0); } break; case KEY_DELLINE: /* Delete to the end of the line */ { priv->nchars = 0; priv->curpos = 0; } break; case KEY_CLRSCR: /* Clear the screen & return cursor to top */ cle_clrscr(priv); break; case KEY_QUOTE: /* Quoted character follows */ { ch = cle_getch(priv); if (ch < 0) { return -EIO; } /* Insert the next character unconditionally */ cle_insertch(priv, ch); } break; /* Newline terminates editing. But what is a newline? */ case '\n': /* LF terminates line */ { /* Add the newline to the buffer at the end of the line */ priv->curpos = priv->nchars; cle_insertch(priv, '\n'); return OK; } break; /* Text to insert or unimplemented/invalid keypresses */ default: { /* Ignore all control characters except for tab and newline */ if (!iscntrl(ch) || ch == '\t') { /* Insert the filtered character into the buffer */ cle_insertch(priv, ch); /* Printable character will change the cursor position in */ if (ch != '\t') { priv->realpos++; } } else { CLE_BEL(priv); } } break; } } return OK; } /**************************************************************************** * Command line processing ****************************************************************************/ /**************************************************************************** * Public Functions ****************************************************************************/ /**************************************************************************** * Name: cle/cle_fd * * Description: * EMACS-like command line editor. This is actually more like readline * than is the NuttX readline! * ****************************************************************************/ int cle_fd(FAR char *line, FAR const char *prompt, uint16_t linelen, int infd, int outfd) { FAR struct cle_s priv; int ret; /* Initialize the CLE state structure */ memset(&priv, 0, sizeof(struct cle_s)); priv.linelen = linelen; priv.line = line; priv.infd = infd; priv.outfd = outfd; /* Clear line, move cursor to column 1 */ cle_write(&priv, g_clrline, sizeof(g_clrline)); /* Store the prompt in case we need to re-print it */ priv.prompt = prompt; cle_outputprompt(&priv); /* Assumption: * nsh prompt is always shown at line start by clear line. */ priv.coloffs = strlen(prompt); priv.realpos = priv.coloffs; /* The editor loop */ ret = cle_editloop(&priv); /* Make sure that the line is NUL terminated */ line[priv.nchars] = '\0'; #ifdef CONFIG_SYSTEM_CLE_CMD_HISTORY /* Save history of command, only if there was something typed besides * return character. */ if (priv.nchars > 1) { int i; g_cmd_history_head = (g_cmd_history_head + 1) % CONFIG_SYSTEM_CLE_CMD_HISTORY_LEN; for (i = 0; (i < priv.nchars - 1) && i < (CONFIG_SYSTEM_CLE_CMD_HISTORY_LINELEN - 1); i++) { g_cmd_history[g_cmd_history_head][i] = line[i]; } g_cmd_history[g_cmd_history_head][i] = '\0'; g_cmd_history_steps_from_head = 1; if (g_cmd_history_len < CONFIG_SYSTEM_CLE_CMD_HISTORY_LEN) { g_cmd_history_len++; } } #endif /* CONFIG_SYSTEM_CLE_CMD_HISTORY */ return ret; } #ifdef CONFIG_FILE_STREAM int cle(FAR char *line, FAR const char *prompt, uint16_t linelen, FAR FILE *instream, FAR FILE *outstream) { int instream_fd; int outstream_fd; instream_fd = fileno(instream); outstream_fd = fileno(outstream); return cle_fd(line, prompt, linelen, instream_fd, outstream_fd); } #endif