X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=linenoise.c;h=c06d88e3782450f7c99eedf7e0dcd46522c62caa;hb=e91e48354750d28a76f65966443a8fa4f098d72d;hp=ab61d34a9569df7f02f5fd50edb641b37cc72769;hpb=85f6003a0ee9d443966c43f99403bb319c555cd5;p=linenoise.git diff --git a/linenoise.c b/linenoise.c index ab61d34..c06d88e 100644 --- a/linenoise.c +++ b/linenoise.c @@ -3,7 +3,8 @@ * * You can find the latest source code at: * - * http://github.com/antirez/linenoise + * http://github.com/msteveb/linenoise + * (forked from http://github.com/antirez/linenoise) * * Does a number of crazy assumptions that happen to be true in 99.9999% of * the 2010 UNIX computers around. @@ -12,6 +13,7 @@ * * Copyright (c) 2010, Salvatore Sanfilippo * Copyright (c) 2010, Pieter Noordhuis + * Copyright (c) 2011, Steve Bennett * * All rights reserved. * @@ -44,22 +46,16 @@ * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html * - * Todo list: - * - Win32 support - * - Save and load history containing newlines - * * Bloat: * - Completion? * + * Unix/termios + * ------------ * List of escape sequences used by this program, we do everything just * a few sequences. In order to be so cheap we may have some * flickering effect with some slow terminal, but the lesser sequences * the more compatible. * - * CHA (Cursor Horizontal Absolute) - * Sequence: ESC [ n G - * Effect: moves cursor to column n (1 based) - * * EL (Erase Line) * Sequence: ESC [ n K * Effect: if n is 0 or missing, clear from cursor to end of line @@ -70,6 +66,10 @@ * Sequence: ESC [ n C * Effect: moves cursor forward of n chars * + * CR (Carriage Return) + * Sequence: \r + * Effect: moves cursor to column 1 + * * The following are used to clear the screen: ESC [ H ESC [ 2 J * This is actually composed of two sequences: * @@ -94,20 +94,36 @@ * DSR/CPR (Report cursor position) * Sequence: ESC [ 6 n * Effect: reports current cursor position as ESC [ NNN ; MMM R + * + * win32/console + * ------------- + * If __MINGW32__ is defined, the win32 console API is used. + * This could probably be made to work for the msvc compiler too. + * This support based in part on work by Jon Griffiths. */ -#ifdef __MINGW32__ +#ifdef _WIN32 /* Windows platform, either MinGW or Visual Studio (MSVC) */ #include #include #define USE_WINCONSOLE +#ifdef __MINGW32__ +#define HAVE_UNISTD_H +#else +/* Microsoft headers don't like old POSIX names */ +#define strdup _strdup +#define snprintf _snprintf +#endif #else #include #include #include #define USE_TERMIOS +#define HAVE_UNISTD_H #endif +#ifdef HAVE_UNISTD_H #include +#endif #include #include #include @@ -115,7 +131,6 @@ #include #include #include -#include #include "linenoise.h" #include "utf8.h" @@ -135,6 +150,9 @@ enum { SPECIAL_DELETE = -24, SPECIAL_HOME = -25, SPECIAL_END = -26, + SPECIAL_INSERT = -27, + SPECIAL_PAGE_UP = -28, + SPECIAL_PAGE_DOWN = -29 }; static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; @@ -150,6 +168,7 @@ struct current { int pos; /* Cursor position, measured in chars */ int cols; /* Size of the window, in chars */ const char *prompt; + char *capture; /* Allocated capture buffer, or NULL for none. Always null terminated */ #if defined(USE_TERMIOS) int fd; /* Terminal fd */ #elif defined(USE_WINCONSOLE) @@ -172,6 +191,7 @@ void linenoiseHistoryFree(void) { free(history[j]); free(history); history = NULL; + history_len = 0; } } @@ -253,7 +273,10 @@ static void linenoiseAtExit(void) { linenoiseHistoryFree(); } -/* gcc/glibc insists that we care about the return code of write! */ +/* gcc/glibc insists that we care about the return code of write! + * Clarification: This means that a void-cast like "(void) (EXPR)" + * does not work. + */ #define IGNORE_RC(EXPR) if (EXPR) {} /* This is fdprintf() on some systems, but use a different @@ -278,7 +301,7 @@ static void clearScreen(struct current *current) static void cursorToLeft(struct current *current) { - fd_printf(current->fd, "\x1b[1G"); + fd_printf(current->fd, "\r"); } static int outputChars(struct current *current, const char *buf, int len) @@ -288,7 +311,7 @@ static int outputChars(struct current *current, const char *buf, int len) static void outputControlChar(struct current *current, char ch) { - fd_printf(current->fd, "\033[7m^%c\033[0m", ch); + fd_printf(current->fd, "\x1b[7m^%c\x1b[0m", ch); } static void eraseEol(struct current *current) @@ -298,7 +321,7 @@ static void eraseEol(struct current *current) static void setCursorPos(struct current *current, int x) { - fd_printf(current->fd, "\x1b[1G\x1b[%dC", x); + fd_printf(current->fd, "\r\x1b[%dC", x); } /** @@ -359,6 +382,70 @@ static int fd_read(struct current *current) #endif } +static int countColorControlChars(const char* prompt, int plen) +{ + /* ANSI color control sequences have the form: + * "\x1b" "[" [0-9;]+ "m" + * We parse them with a simple state machine. + */ + + enum { + search_esc, + expect_bracket, + expect_inner, + expect_trail + } state = search_esc; + int len, found = 0; + char ch; + + /* XXX: Strictly we should be checking utf8 chars rather than + * bytes in case of the extremely unlikely scenario where + * an ANSI sequence is part of a utf8 sequence. + */ + for (; plen ; plen--, prompt++) { + ch = *prompt; + + switch (state) { + case search_esc: + len = 0; + if (ch == '\x1b') { + state = expect_bracket; + len++; + } + break; + case expect_bracket: + if (ch == '[') { + state = expect_inner; + len++; + } else { + state = search_esc; + } + break; + case expect_inner: + if (ch >= '0' && ch <= '9') { + len++; + state = expect_trail; + } else { + state = search_esc; + } + break; + case expect_trail: + if (ch == 'm') { + len++; + found += len; + state = search_esc; + } else if ((ch != ';') && ((ch < '0') || (ch > '9'))) { + state = search_esc; + } + /* 0-9, or semicolon */ + len++; + break; + } + } + + return found; +} + static int getWindowSize(struct current *current) { struct winsize ws; @@ -377,8 +464,8 @@ static int getWindowSize(struct current *current) if (current->cols == 0) { current->cols = 80; - /* Move cursor far right and report cursor position */ - fd_printf(current->fd, "\x1b[999G" "\x1b[6n"); + /* Move cursor far right and report cursor position, then back to the left */ + fd_printf(current->fd, "\x1b[999C" "\x1b[6n"); /* Parse the response: ESC [ rows ; cols R */ if (fd_read_char(current->fd, 100) == 0x1b && fd_read_char(current->fd, 100) == '[') { @@ -447,16 +534,28 @@ static int check_special(int fd) return SPECIAL_HOME; } } - if (c == '[' && c2 >= '1' && c2 <= '6') { + if (c == '[' && c2 >= '1' && c2 <= '8') { /* extended escape */ - int c3 = fd_read_char(fd, 50); - if (c2 == '3' && c3 == '~') { - /* delete char under cursor */ - return SPECIAL_DELETE; + c = fd_read_char(fd, 50); + if (c == '~') { + switch (c2) { + case '2': + return SPECIAL_INSERT; + case '3': + return SPECIAL_DELETE; + case '5': + return SPECIAL_PAGE_UP; + case '6': + return SPECIAL_PAGE_DOWN; + case '7': + return SPECIAL_HOME; + case '8': + return SPECIAL_END; + } } - while (c3 != -1 && c3 != '~') { + while (c != -1 && c != '~') { /* .e.g \e[12~ or '\e[11;2~ discard the complete sequence */ - c3 = fd_read_char(fd, 50); + c = fd_read_char(fd, 50); } } @@ -505,7 +604,7 @@ static void clearScreen(struct current *current) static void cursorToLeft(struct current *current) { - COORD pos = { 0, current->y }; + COORD pos = { 0, (SHORT)current->y }; DWORD n; FillConsoleOutputAttribute(current->outh, @@ -515,15 +614,17 @@ static void cursorToLeft(struct current *current) static int outputChars(struct current *current, const char *buf, int len) { - COORD pos = { current->x, current->y }; - WriteConsoleOutputCharacter(current->outh, buf, len, pos, 0); + COORD pos = { (SHORT)current->x, (SHORT)current->y }; + DWORD n; + + WriteConsoleOutputCharacter(current->outh, buf, len, pos, &n); current->x += len; return 0; } static void outputControlChar(struct current *current, char ch) { - COORD pos = { current->x, current->y }; + COORD pos = { (SHORT)current->x, (SHORT)current->y }; DWORD n; FillConsoleOutputAttribute(current->outh, BACKGROUND_INTENSITY, 2, pos, &n); @@ -533,7 +634,7 @@ static void outputControlChar(struct current *current, char ch) static void eraseEol(struct current *current) { - COORD pos = { current->x, current->y }; + COORD pos = { (SHORT)current->x, (SHORT)current->y }; DWORD n; FillConsoleOutputCharacter(current->outh, ' ', current->cols - current->x, pos, &n); @@ -541,7 +642,7 @@ static void eraseEol(struct current *current) static void setCursorPos(struct current *current, int x) { - COORD pos = { x, current->y }; + COORD pos = { (SHORT)x, (SHORT)current->y }; SetConsoleCursorPosition(current->outh, pos); current->x = x; @@ -570,12 +671,18 @@ static int fd_read(struct current *current) return SPECIAL_UP; case VK_DOWN: return SPECIAL_DOWN; + case VK_INSERT: + return SPECIAL_INSERT; case VK_DELETE: return SPECIAL_DELETE; case VK_HOME: return SPECIAL_HOME; case VK_END: return SPECIAL_END; + case VK_PRIOR: + return SPECIAL_PAGE_UP; + case VK_NEXT: + return SPECIAL_PAGE_DOWN; } } /* Note that control characters are already translated in AsciiChar */ @@ -591,6 +698,14 @@ static int fd_read(struct current *current) return -1; } +static int countColorControlChars(char* prompt, int plen) +{ + /* For windows we assume that there are no embedded ansi color + * control sequences. + */ + return 0; +} + static int getWindowSize(struct current *current) { CONSOLE_SCREEN_BUFFER_INFO info; @@ -653,6 +768,11 @@ static void refreshLine(const char *prompt, struct current *current) plen = strlen(prompt); pchars = utf8_strlen(prompt, plen); + /* Scan the prompt for embedded ansi color control sequences and + * discount them as characters/columns. + */ + pchars -= countColorControlChars(prompt, plen); + /* Account for a line which is too long to fit in the window. * Note that control chars require an extra column */ @@ -669,7 +789,7 @@ static void refreshLine(const char *prompt, struct current *current) } } - /* If too many are need, strip chars off the front of 'buf' + /* If too many are needed, strip chars off the front of 'buf' * until it fits. Note that if the current char is a control character, * we need one extra col. */ @@ -677,7 +797,7 @@ static void refreshLine(const char *prompt, struct current *current) n++; } - while (n >= current->cols) { + while (n >= current->cols && pos > 0) { b = utf8_tounicode(buf, &ch); if (ch < ' ') { n--; @@ -820,16 +940,64 @@ static int insert_char(struct current *current, int pos, int ch) } /** + * Captures up to 'n' characters starting at 'pos' for the cut buffer. + * + * This replaces any existing characters in the cut buffer. + */ +static void capture_chars(struct current *current, int pos, int n) +{ + if (pos >= 0 && (pos + n - 1) < current->chars) { + int p1 = utf8_index(current->buf, pos); + int nbytes = utf8_index(current->buf + p1, n); + + if (nbytes) { + free(current->capture); + /* Include space for the null terminator */ + current->capture = (char *)malloc(nbytes + 1); + memcpy(current->capture, current->buf + p1, nbytes); + current->capture[nbytes] = '\0'; + } + } +} + +/** + * Removes up to 'n' characters at cursor position 'pos'. + * * Returns 0 if no chars were removed or non-zero otherwise. */ static int remove_chars(struct current *current, int pos, int n) { int removed = 0; + + /* First save any chars which will be removed */ + capture_chars(current, pos, n); + while (n-- && remove_char(current, pos)) { removed++; } return removed; } +/** + * Inserts the characters (string) 'chars' at the cursor position 'pos'. + * + * Returns 0 if no chars were inserted or non-zero otherwise. + */ +static int insert_chars(struct current *current, int pos, const char *chars) +{ + int inserted = 0; + + while (*chars) { + int ch; + int n = utf8_tounicode(chars, &ch); + if (insert_char(current, pos, ch) == 0) { + break; + } + inserted++; + pos++; + chars += n; + } + return inserted; +} #ifndef NO_COMPLETION static linenoiseCompletionCallback *completionCallback = NULL; @@ -932,7 +1100,7 @@ static int linenoisePrompt(struct current *current) { /* Only autocomplete when the callback is set. It returns < 0 when * there was an error reading from fd. Otherwise it will return the * character that should be handled next. */ - if (c == 9 && completionCallback != NULL) { + if (c == '\t' && current->pos == current->chars && completionCallback != NULL) { c = completeLine(current); /* Return on errors */ if (c < 0) return current->len; @@ -969,12 +1137,18 @@ process_char: free(history[history_len]); return -1; } - /* Otherwise delete char to right of cursor */ - if (remove_char(current, current->pos)) { + /* Otherwise fall through to delete char to right of cursor */ + case SPECIAL_DELETE: + if (remove_char(current, current->pos) == 1) { refreshLine(current->prompt, current); } break; - case ctrl('W'): /* ctrl-w */ + case SPECIAL_INSERT: + /* Ignore. Expansion Hook. + * Future possibility: Toggle Insert/Overwrite Modes + */ + break; + case ctrl('W'): /* ctrl-w, delete word at left. save deleted chars */ /* eat any spaces on the left */ { int pos = current->pos; @@ -1094,9 +1268,11 @@ process_char: } break; case ctrl('T'): /* ctrl-t */ - if (current->pos > 0 && current->pos < current->chars) { - c = get_char(current, current->pos); - remove_char(current, current->pos); + if (current->pos > 0 && current->pos <= current->chars) { + /* If cursor is at end, transpose the previous two chars */ + int fixer = (current->pos == current->chars); + c = get_char(current, current->pos - fixer); + remove_char(current, current->pos - fixer); insert_char(current, current->pos - 1, c); refreshLine(current->prompt, current); } @@ -1133,81 +1309,90 @@ process_char: refreshLine(current->prompt, current); } break; + case SPECIAL_PAGE_UP: + dir = history_len - history_index - 1; /* move to start of history */ + goto history_navigation; + case SPECIAL_PAGE_DOWN: + dir = -history_index; /* move to 0 == end of history, i.e. current */ + goto history_navigation; case ctrl('P'): case SPECIAL_UP: dir = 1; + goto history_navigation; case ctrl('N'): case SPECIAL_DOWN: +history_navigation: if (history_len > 1) { /* Update the current history entry before to * overwrite it with tne next one. */ - free(history[history_len-1-history_index]); - history[history_len-1-history_index] = strdup(current->buf); + free(history[history_len - 1 - history_index]); + history[history_len - 1 - history_index] = strdup(current->buf); /* Show the new entry */ history_index += dir; if (history_index < 0) { history_index = 0; break; } else if (history_index >= history_len) { - history_index = history_len-1; + history_index = history_len - 1; break; } - set_current(current, history[history_len-1-history_index]); - refreshLine(current->prompt, current); - } - break; - - case SPECIAL_DELETE: - if (remove_char(current, current->pos) == 1) { + set_current(current, history[history_len - 1 - history_index]); refreshLine(current->prompt, current); } break; + case ctrl('A'): /* Ctrl+a, go to the start of the line */ case SPECIAL_HOME: current->pos = 0; refreshLine(current->prompt, current); break; + case ctrl('E'): /* ctrl+e, go to the end of the line */ case SPECIAL_END: current->pos = current->chars; refreshLine(current->prompt, current); break; - default: - /* Only tab is allowed without ^V */ - if (c == '\t' || c >= ' ') { - if (insert_char(current, current->pos, c) == 1) { - refreshLine(current->prompt, current); - } - } - break; - case ctrl('U'): /* Ctrl+u, delete to beginning of line. */ + case ctrl('U'): /* Ctrl+u, delete to beginning of line, save deleted chars. */ if (remove_chars(current, 0, current->pos)) { refreshLine(current->prompt, current); } break; - case ctrl('K'): /* Ctrl+k, delete from current to end of line. */ + case ctrl('K'): /* Ctrl+k, delete from current to end of line, save deleted chars. */ if (remove_chars(current, current->pos, current->chars - current->pos)) { refreshLine(current->prompt, current); } break; - case ctrl('A'): /* Ctrl+a, go to the start of the line */ - current->pos = 0; - refreshLine(current->prompt, current); - break; - case ctrl('E'): /* ctrl+e, go to the end of the line */ - current->pos = current->chars; - refreshLine(current->prompt, current); + case ctrl('Y'): /* Ctrl+y, insert saved chars at current position */ + if (current->capture && insert_chars(current, current->pos, current->capture)) { + refreshLine(current->prompt, current); + } break; case ctrl('L'): /* Ctrl+L, clear screen */ - /* clear screen */ clearScreen(current); /* Force recalc of window size for serial terminals */ current->cols = 0; refreshLine(current->prompt, current); break; + default: + /* Only tab is allowed without ^V */ + if (c == '\t' || c >= ' ') { + if (insert_char(current, current->pos, c) == 1) { + refreshLine(current->prompt, current); + } + } + break; } } return current->len; } +int linenoiseColumns(void) +{ + struct current current; + enableRawMode (¤t); + getWindowSize (¤t); + disableRawMode (¤t); + return current.cols; +} + char *linenoise(const char *prompt) { int count; @@ -1215,10 +1400,10 @@ char *linenoise(const char *prompt) char buf[LINENOISE_MAX_LINE]; if (enableRawMode(¤t) == -1) { - printf("%s", prompt); + printf("%s", prompt); fflush(stdout); if (fgets(buf, sizeof(buf), stdin) == NULL) { - return NULL; + return NULL; } count = strlen(buf); if (count && buf[count-1] == '\n') { @@ -1234,10 +1419,14 @@ char *linenoise(const char *prompt) current.chars = 0; current.pos = 0; current.prompt = prompt; + current.capture = NULL; count = linenoisePrompt(¤t); + disableRawMode(¤t); printf("\n"); + + free(current.capture); if (count == -1) { return NULL; } @@ -1255,6 +1444,12 @@ int linenoiseHistoryAdd(const char *line) { if (history == NULL) return 0; memset(history,0,(sizeof(char*)*history_max_len)); } + + /* do not insert duplicate lines into history */ + if (history_len > 0 && strcmp(line, history[history_len - 1]) == 0) { + return 0; + } + linecopy = strdup(line); if (!linecopy) return 0; if (history_len == history_max_len) { @@ -1267,6 +1462,10 @@ int linenoiseHistoryAdd(const char *line) { return 1; } +int linenoiseHistoryGetMaxLen(void) { + return history_max_len; +} + int linenoiseHistorySetMaxLen(int len) { char **newHistory;