diff --git a/mk/rt.mk b/mk/rt.mk index 17f9b8b2f7b..35b8426c555 100644 --- a/mk/rt.mk +++ b/mk/rt.mk @@ -78,7 +78,7 @@ RUNTIME_CXXS_$(1) := \ rt/arch/$$(HOST_$(1))/context.cpp \ rt/arch/$$(HOST_$(1))/gpr.cpp -RUNTIME_CS_$(1) := rt/linenoise/linenoise.c +RUNTIME_CS_$(1) := rt/linenoise/linenoise.c rt/linenoise/utf8.c RUNTIME_S_$(1) := rt/arch/$$(HOST_$(1))/_context.S \ rt/arch/$$(HOST_$(1))/ccall.S \ diff --git a/src/libstd/rl.rs b/src/libstd/rl.rs index cbfa9777b29..dc098586382 100644 --- a/src/libstd/rl.rs +++ b/src/libstd/rl.rs @@ -9,7 +9,6 @@ extern mod rustrt { fn linenoiseHistoryLoad(file: *c_char) -> c_int; fn linenoiseSetCompletionCallback(callback: *u8); fn linenoiseAddCompletion(completions: *(), line: *c_char); - fn linenoiseClearScreen(); } /// Add a line to history @@ -48,11 +47,6 @@ pub fn read(prompt: ~str) -> Option<~str> { } } -/// Clear the screen -pub fn clear() { - rustrt::linenoiseClearScreen(); -} - pub type CompletionCb = fn~(~str, fn(~str)); fn complete_key(_v: @CompletionCb) {} diff --git a/src/rt/linenoise/README.markdown b/src/rt/linenoise/README.markdown new file mode 100644 index 00000000000..f008d2d3d5e --- /dev/null +++ b/src/rt/linenoise/README.markdown @@ -0,0 +1,47 @@ +# Linenoise + +A minimal, zero-config, BSD licensed, readline replacement. + +News: linenoise now includes minimal completion support, thanks to Pieter Noordhuis (@pnoordhuis). + +News: linenoise is now part of [Android](http://android.git.kernel.org/?p=platform/system/core.git;a=tree;f=liblinenoise;h=56450eaed7f783760e5e6a5993ef75cde2e29dea;hb=HEAD Android)! + +## Can a line editing library be 20k lines of code? + +Line editing with some support for history is a really important feature for command line utilities. Instead of retyping almost the same stuff again and again it's just much better to hit the up arrow and edit on syntax errors, or in order to try a slightly different command. But apparently code dealing with terminals is some sort of Black Magic: readline is 30k lines of code, libedit 20k. Is it reasonable to link small utilities to huge libraries just to get a minimal support for line editing? + +So what usually happens is either: + + * Large programs with configure scripts disabling line editing if readline is not present in the system, or not supporting it at all since readline is GPL licensed and libedit (the BSD clone) is not as known and available as readline is (Real world example of this problem: Tclsh). + * Smaller programs not using a configure script not supporting line editing at all (A problem we had with Redis-cli for instance). + +The result is a pollution of binaries without line editing support. + +So I spent more or less two hours doing a reality check resulting in this little library: is it *really* needed for a line editing library to be 20k lines of code? Apparently not, it is possibe to get a very small, zero configuration, trivial to embed library, that solves the problem. Smaller programs will just include this, supporing line editing out of the box. Larger programs may use this little library or just checking with configure if readline/libedit is available and resorting to linenoise if not. + +## Terminals, in 2010. + +Apparently almost every terminal you can happen to use today has some kind of support for VT100 alike escape sequences. So I tried to write a lib using just very basic VT100 features. The resulting library appears to work everywhere I tried to use it. + +Since it's so young I guess there are a few bugs, or the lib may not compile or work with some operating system, but it's a matter of a few weeks and eventually we'll get it right, and there will be no excuses for not shipping command line tools without built-in line editing support. + +The library is currently less than 400 lines of code. In order to use it in your project just look at the *example.c* file in the source distribution, it is trivial. Linenoise is BSD code, so you can use both in free software and commercial software. + +## Tested with... + + * Linux text only console ($TERM = linux) + * Linux KDE terminal application ($TERM = xterm) + * Linux xterm ($TERM = xterm) + * Mac OS X iTerm ($TERM = xterm) + * Mac OS X default Terminal.app ($TERM = xterm) + * OpenBSD 4.5 through an OSX Terminal.app ($TERM = screen) + * IBM AIX 6.1 + * FreeBSD xterm ($TERM = xterm) + +Please test it everywhere you can and report back! + +## Let's push this forward! + +Please fork it and add something interesting and send me a pull request. What's especially interesting are fixes, new key bindings, completion. + +Send feedbacks to antirez at gmail diff --git a/src/rt/linenoise/example.c b/src/rt/linenoise/example.c new file mode 100644 index 00000000000..cb51a0af8f9 --- /dev/null +++ b/src/rt/linenoise/example.c @@ -0,0 +1,30 @@ +#include +#include +#include "linenoise.h" + +#ifndef NO_COMPLETION +void completion(const char *buf, linenoiseCompletions *lc) { + if (buf[0] == 'h') { + linenoiseAddCompletion(lc,"hello"); + linenoiseAddCompletion(lc,"hello there"); + } +} +#endif + +int main(void) { + char *line; + +#ifndef NO_COMPLETION + linenoiseSetCompletionCallback(completion); +#endif + linenoiseHistoryLoad("history.txt"); /* Load the history at startup */ + while((line = linenoise("hello> ")) != NULL) { + if (line[0] != '\0') { + printf("echo: '%s'\n", line); + linenoiseHistoryAdd(line); + linenoiseHistorySave("history.txt"); /* Save every new entry */ + } + free(line); + } + return 0; +} diff --git a/src/rt/linenoise/linenoise.c b/src/rt/linenoise/linenoise.c index 4632f7de818..5e3216e4591 100644 --- a/src/rt/linenoise/linenoise.c +++ b/src/rt/linenoise/linenoise.c @@ -2,8 +2,9 @@ * line editing lib needs to be 20,000 lines of C code. * * 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,20 +13,21 @@ * * Copyright (c) 2010, Salvatore Sanfilippo * Copyright (c) 2010, Pieter Noordhuis + * Copyright (c) 2011, Steve Bennett * * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: - * + * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR @@ -37,31 +39,23 @@ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + * * ------------------------------------------------------------------------ * * References: * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html * - * Todo list: - * - Switch to gets() if $TERM is something we can't support. - * - Filter bogus Ctrl+ combinations. - * - Win32 support - * * Bloat: * - Completion? - * - History search like Ctrl+r in readline? * + * Unix/termios + * ------------ * List of escape sequences used by this program, we do everything just - * with three sequences. In order to be so cheap we may have some + * 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 - * * EL (Erase Line) * Sequence: ESC [ n K * Effect: if n is 0 or missing, clear from cursor to end of line @@ -72,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: * @@ -82,65 +80,154 @@ * ED2 (Clear entire screen) * Sequence: ESC [ 2 J * Effect: clear the whole screen - * + * + * == For highlighting control characters, we also use the following two == + * SO (enter StandOut) + * Sequence: ESC [ 7 m + * Effect: Uses some standout mode such as reverse video + * + * SE (Standout End) + * Sequence: ESC [ 0 m + * Effect: Exit standout mode + * + * == Only used if TIOCGWINSZ fails == + * 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 _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 #include #include #include #include -#include -#include + #include "linenoise.h" +#include "utf8.h" #define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 #define LINENOISE_MAX_LINE 4096 -static char *unsupported_term[] = {"dumb","cons25",NULL}; -static linenoiseCompletionCallback *completionCallback = NULL; -static struct termios orig_termios; /* in order to restore at exit */ -static int rawmode = 0; /* for atexit() function to check if restore is needed*/ -static int atexit_registered = 0; /* register atexit just 1 time */ +#define ctrl(C) ((C) - '@') + +/* Use -ve numbers here to co-exist with normal unicode chars */ +enum { + SPECIAL_NONE, + SPECIAL_UP = -20, + SPECIAL_DOWN = -21, + SPECIAL_LEFT = -22, + SPECIAL_RIGHT = -23, + SPECIAL_DELETE = -24, + SPECIAL_HOME = -25, + SPECIAL_END = -26, +}; + static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; static int history_len = 0; -char **history = NULL; +static char **history = NULL; -static void linenoiseAtExit(void); -int linenoiseHistoryAdd(const char *line); +/* Structure to contain the status of the current (being edited) line */ +struct current { + char *buf; /* Current buffer. Always null terminated */ + int bufmax; /* Size of the buffer, including space for the null termination */ + int len; /* Number of bytes in 'buf' */ + int chars; /* Number of chars in 'buf' (utf-8 chars) */ + int pos; /* Cursor position, measured in chars */ + int cols; /* Size of the window, in chars */ + const char *prompt; +#if defined(USE_TERMIOS) + int fd; /* Terminal fd */ +#elif defined(USE_WINCONSOLE) + HANDLE outh; /* Console output handle */ + HANDLE inh; /* Console input handle */ + int rows; /* Screen rows */ + int x; /* Current column during output */ + int y; /* Current row */ +#endif +}; -static int isUnsupportedTerm(void) { - char *term = getenv("TERM"); - int j; +static int fd_read(struct current *current); +static int getWindowSize(struct current *current); - if (term == NULL) return 0; - for (j = 0; unsupported_term[j]; j++) - if (!strcasecmp(term,unsupported_term[j])) return 1; - return 0; -} - -static void freeHistory(void) { +void linenoiseHistoryFree(void) { if (history) { int j; for (j = 0; j < history_len; j++) free(history[j]); free(history); + history = NULL; } } -static int enableRawMode(int fd) { +#if defined(USE_TERMIOS) +static void linenoiseAtExit(void); +static struct termios orig_termios; /* in order to restore at exit */ +static int rawmode = 0; /* for atexit() function to check if restore is needed*/ +static int atexit_registered = 0; /* register atexit just 1 time */ + +static const char *unsupported_term[] = {"dumb","cons25",NULL}; + +static int isUnsupportedTerm(void) { + char *term = getenv("TERM"); + + if (term) { + int j; + for (j = 0; unsupported_term[j]; j++) { + if (strcasecmp(term, unsupported_term[j]) == 0) { + return 1; + } + } + } + return 0; +} + +static int enableRawMode(struct current *current) { struct termios raw; - if (!isatty(STDIN_FILENO)) goto fatal; + current->fd = STDIN_FILENO; + + if (!isatty(current->fd) || isUnsupportedTerm() || + tcgetattr(current->fd, &orig_termios) == -1) { +fatal: + errno = ENOTTY; + return -1; + } + if (!atexit_registered) { atexit(linenoiseAtExit); atexit_registered = 1; } - if (tcgetattr(fd,&orig_termios) == -1) goto fatal; raw = orig_termios; /* modify the original mode */ /* input modes: no break, no CR to NL, no parity check, no strip char, @@ -158,118 +245,675 @@ static int enableRawMode(int fd) { raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ /* put terminal in raw mode after flushing */ - if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; + if (tcsetattr(current->fd,TCSADRAIN,&raw) < 0) { + goto fatal; + } rawmode = 1; - return 0; -fatal: - errno = ENOTTY; - return -1; + current->cols = 0; + return 0; } -static void disableRawMode(int fd) { +static void disableRawMode(struct current *current) { /* Don't even check the return value as it's too late. */ - if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1) + if (rawmode && tcsetattr(current->fd,TCSADRAIN,&orig_termios) != -1) rawmode = 0; } /* At exit we'll try to fix the terminal to the initial conditions. */ static void linenoiseAtExit(void) { - disableRawMode(STDIN_FILENO); - freeHistory(); + if (rawmode) { + tcsetattr(STDIN_FILENO, TCSADRAIN, &orig_termios); + } + linenoiseHistoryFree(); } -static int getColumns(void) { +/* gcc/glibc insists that we care about the return code of write! */ +#define IGNORE_RC(EXPR) if (EXPR) {} + +/* This is fdprintf() on some systems, but use a different + * name to avoid conflicts + */ +static void fd_printf(int fd, const char *format, ...) +{ + va_list args; + char buf[64]; + int n; + + va_start(args, format); + n = vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + IGNORE_RC(write(fd, buf, n)); +} + +static void clearScreen(struct current *current) +{ + fd_printf(current->fd, "\x1b[H\x1b[2J"); +} + +static void cursorToLeft(struct current *current) +{ + fd_printf(current->fd, "\r"); +} + +static int outputChars(struct current *current, const char *buf, int len) +{ + return write(current->fd, buf, len); +} + +static void outputControlChar(struct current *current, char ch) +{ + fd_printf(current->fd, "\x1b[7m^%c\x1b[0m", ch); +} + +static void eraseEol(struct current *current) +{ + fd_printf(current->fd, "\x1b[0K"); +} + +static void setCursorPos(struct current *current, int x) +{ + fd_printf(current->fd, "\r\x1b[%dC", x); +} + +/** + * Reads a char from 'fd', waiting at most 'timeout' milliseconds. + * + * A timeout of -1 means to wait forever. + * + * Returns -1 if no char is received within the time or an error occurs. + */ +static int fd_read_char(int fd, int timeout) +{ + struct pollfd p; + unsigned char c; + + p.fd = fd; + p.events = POLLIN; + + if (poll(&p, 1, timeout) == 0) { + /* timeout */ + return -1; + } + if (read(fd, &c, 1) != 1) { + return -1; + } + return c; +} + +/** + * Reads a complete utf-8 character + * and returns the unicode value, or -1 on error. + */ +static int fd_read(struct current *current) +{ +#ifdef USE_UTF8 + char buf[4]; + int n; + int i; + int c; + + if (read(current->fd, &buf[0], 1) != 1) { + return -1; + } + n = utf8_charlen(buf[0]); + if (n < 1 || n > 3) { + return -1; + } + for (i = 1; i < n; i++) { + if (read(current->fd, &buf[i], 1) != 1) { + return -1; + } + } + buf[n] = 0; + /* decode and return the character */ + utf8_tounicode(buf, &c); + return c; +#else + return fd_read_char(current->fd, -1); +#endif +} + +static int getWindowSize(struct current *current) +{ struct winsize ws; - if (ioctl(1, TIOCGWINSZ, &ws) == -1) return 80; - return ws.ws_col; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col != 0) { + current->cols = ws.ws_col; + return 0; + } + + /* Failed to query the window size. Perhaps we are on a serial terminal. + * Try to query the width by sending the cursor as far to the right + * and reading back the cursor position. + * Note that this is only done once per call to linenoise rather than + * every time the line is refreshed for efficiency reasons. + */ + if (current->cols == 0) { + current->cols = 80; + + /* 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) == '[') { + int n = 0; + while (1) { + int ch = fd_read_char(current->fd, 100); + if (ch == ';') { + /* Ignore rows */ + n = 0; + } + else if (ch == 'R') { + /* Got cols */ + if (n != 0 && n < 1000) { + current->cols = n; + } + break; + } + else if (ch >= 0 && ch <= '9') { + n = n * 10 + ch - '0'; + } + else { + break; + } + } + } + } + return 0; } -static void refreshLine(int fd, const char *prompt, char *buf, size_t len, size_t pos, size_t cols) { - char seq[64]; - size_t plen = strlen(prompt); - - while((plen+pos) >= cols) { - buf++; - len--; +/** + * If escape (27) was received, reads subsequent + * chars to determine if this is a known special key. + * + * Returns SPECIAL_NONE if unrecognised, or -1 if EOF. + * + * If no additional char is received within a short time, + * 27 is returned. + */ +static int check_special(int fd) +{ + int c = fd_read_char(fd, 50); + int c2; + + if (c < 0) { + return 27; + } + + c2 = fd_read_char(fd, 50); + if (c2 < 0) { + return c2; + } + if (c == '[' || c == 'O') { + /* Potential arrow key */ + switch (c2) { + case 'A': + return SPECIAL_UP; + case 'B': + return SPECIAL_DOWN; + case 'C': + return SPECIAL_RIGHT; + case 'D': + return SPECIAL_LEFT; + case 'F': + return SPECIAL_END; + case 'H': + return SPECIAL_HOME; + } + } + if (c == '[' && c2 >= '1' && c2 <= '8') { + /* extended escape */ + c = fd_read_char(fd, 50); + if (c == '~') { + switch (c2) { + case '3': + return SPECIAL_DELETE; + case '7': + return SPECIAL_HOME; + case '8': + return SPECIAL_END; + } + } + while (c != -1 && c != '~') { + /* .e.g \e[12~ or '\e[11;2~ discard the complete sequence */ + c = fd_read_char(fd, 50); + } + } + + return SPECIAL_NONE; +} +#elif defined(USE_WINCONSOLE) + +static DWORD orig_consolemode = 0; + +static int enableRawMode(struct current *current) { + DWORD n; + INPUT_RECORD irec; + + current->outh = GetStdHandle(STD_OUTPUT_HANDLE); + current->inh = GetStdHandle(STD_INPUT_HANDLE); + + if (!PeekConsoleInput(current->inh, &irec, 1, &n)) { + return -1; + } + if (getWindowSize(current) != 0) { + return -1; + } + if (GetConsoleMode(current->inh, &orig_consolemode)) { + SetConsoleMode(current->inh, ENABLE_PROCESSED_INPUT); + } + return 0; +} + +static void disableRawMode(struct current *current) +{ + SetConsoleMode(current->inh, orig_consolemode); +} + +static void clearScreen(struct current *current) +{ + COORD topleft = { 0, 0 }; + DWORD n; + + FillConsoleOutputCharacter(current->outh, ' ', + current->cols * current->rows, topleft, &n); + FillConsoleOutputAttribute(current->outh, + FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN, + current->cols * current->rows, topleft, &n); + SetConsoleCursorPosition(current->outh, topleft); +} + +static void cursorToLeft(struct current *current) +{ + COORD pos = { 0, (SHORT)current->y }; + DWORD n; + + FillConsoleOutputAttribute(current->outh, + FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN, current->cols, pos, &n); + current->x = 0; +} + +static int outputChars(struct current *current, const char *buf, int len) +{ + 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 = { (SHORT)current->x, (SHORT)current->y }; + DWORD n; + + FillConsoleOutputAttribute(current->outh, BACKGROUND_INTENSITY, 2, pos, &n); + outputChars(current, "^", 1); + outputChars(current, &ch, 1); +} + +static void eraseEol(struct current *current) +{ + COORD pos = { (SHORT)current->x, (SHORT)current->y }; + DWORD n; + + FillConsoleOutputCharacter(current->outh, ' ', current->cols - current->x, pos, &n); +} + +static void setCursorPos(struct current *current, int x) +{ + COORD pos = { (SHORT)x, (SHORT)current->y }; + + SetConsoleCursorPosition(current->outh, pos); + current->x = x; +} + +static int fd_read(struct current *current) +{ + while (1) { + INPUT_RECORD irec; + DWORD n; + if (WaitForSingleObject(current->inh, INFINITE) != WAIT_OBJECT_0) { + break; + } + if (!ReadConsoleInput (current->inh, &irec, 1, &n)) { + break; + } + if (irec.EventType == KEY_EVENT && irec.Event.KeyEvent.bKeyDown) { + KEY_EVENT_RECORD *k = &irec.Event.KeyEvent; + if (k->dwControlKeyState & ENHANCED_KEY) { + switch (k->wVirtualKeyCode) { + case VK_LEFT: + return SPECIAL_LEFT; + case VK_RIGHT: + return SPECIAL_RIGHT; + case VK_UP: + return SPECIAL_UP; + case VK_DOWN: + return SPECIAL_DOWN; + case VK_DELETE: + return SPECIAL_DELETE; + case VK_HOME: + return SPECIAL_HOME; + case VK_END: + return SPECIAL_END; + } + } + /* Note that control characters are already translated in AsciiChar */ + else { +#ifdef USE_UTF8 + return k->uChar.UnicodeChar; +#else + return k->uChar.AsciiChar; +#endif + } + } + } + return -1; +} + +static int getWindowSize(struct current *current) +{ + CONSOLE_SCREEN_BUFFER_INFO info; + if (!GetConsoleScreenBufferInfo(current->outh, &info)) { + return -1; + } + current->cols = info.dwSize.X; + current->rows = info.dwSize.Y; + if (current->cols <= 0 || current->rows <= 0) { + current->cols = 80; + return -1; + } + current->y = info.dwCursorPosition.Y; + current->x = info.dwCursorPosition.X; + return 0; +} +#endif + +static int utf8_getchars(char *buf, int c) +{ +#ifdef USE_UTF8 + return utf8_fromunicode(buf, c); +#else + *buf = c; + return 1; +#endif +} + +/** + * Returns the unicode character at the given offset, + * or -1 if none. + */ +static int get_char(struct current *current, int pos) +{ + if (pos >= 0 && pos < current->chars) { + int c; + int i = utf8_index(current->buf, pos); + (void)utf8_tounicode(current->buf + i, &c); + return c; + } + return -1; +} + +static void refreshLine(const char *prompt, struct current *current) +{ + int plen; + int pchars; + int backup = 0; + int i; + const char *buf = current->buf; + int chars = current->chars; + int pos = current->pos; + int b; + int ch; + int n; + + /* Should intercept SIGWINCH. For now, just get the size every time */ + getWindowSize(current); + + plen = strlen(prompt); + pchars = utf8_strlen(prompt, plen); + + /* Account for a line which is too long to fit in the window. + * Note that control chars require an extra column + */ + + /* How many cols are required to the left of 'pos'? + * The prompt, plus one extra for each control char + */ + n = pchars + utf8_strlen(buf, current->len); + b = 0; + for (i = 0; i < pos; i++) { + b += utf8_tounicode(buf + b, &ch); + if (ch < ' ') { + n++; + } + } + + /* If too many are need, 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. + */ + if (current->pos < current->chars && get_char(current, current->pos) < ' ') { + n++; + } + + while (n >= current->cols && pos > 0) { + b = utf8_tounicode(buf, &ch); + if (ch < ' ') { + n--; + } + n--; + buf += b; pos--; - } - while (plen+len > cols) { - len--; + chars--; } - /* Cursor to left edge */ - snprintf(seq,64,"\x1b[0G"); - if (write(fd,seq,strlen(seq)) == -1) return; - /* Write the prompt and the current buffer content */ - if (write(fd,prompt,strlen(prompt)) == -1) return; - if (write(fd,buf,len) == -1) return; - /* Erase to right */ - snprintf(seq,64,"\x1b[0K"); - if (write(fd,seq,strlen(seq)) == -1) return; - /* Move cursor to original position. */ - snprintf(seq,64,"\x1b[0G\x1b[%dC", (int)(pos+plen)); - if (write(fd,seq,strlen(seq)) == -1) return; + /* Cursor to left edge, then the prompt */ + cursorToLeft(current); + outputChars(current, prompt, plen); + + /* Now the current buffer content */ + + /* Need special handling for control characters. + * If we hit 'cols', stop. + */ + b = 0; /* unwritted bytes */ + n = 0; /* How many control chars were written */ + for (i = 0; i < chars; i++) { + int ch; + int w = utf8_tounicode(buf + b, &ch); + if (ch < ' ') { + n++; + } + if (pchars + i + n >= current->cols) { + break; + } + if (ch < ' ') { + /* A control character, so write the buffer so far */ + outputChars(current, buf, b); + buf += b + w; + b = 0; + outputControlChar(current, ch + '@'); + if (i < pos) { + backup++; + } + } + else { + b += w; + } + } + outputChars(current, buf, b); + + /* Erase to right, move cursor to original position */ + eraseEol(current); + setCursorPos(current, pos + pchars + backup); } +static void set_current(struct current *current, const char *str) +{ + strncpy(current->buf, str, current->bufmax); + current->buf[current->bufmax - 1] = 0; + current->len = strlen(current->buf); + current->pos = current->chars = utf8_strlen(current->buf, current->len); +} + +static int has_room(struct current *current, int bytes) +{ + return current->len + bytes < current->bufmax - 1; +} + +/** + * Removes the char at 'pos'. + * + * Returns 1 if the line needs to be refreshed, 2 if not + * and 0 if nothing was removed + */ +static int remove_char(struct current *current, int pos) +{ + if (pos >= 0 && pos < current->chars) { + int p1, p2; + int ret = 1; + p1 = utf8_index(current->buf, pos); + p2 = p1 + utf8_index(current->buf + p1, 1); + +#ifdef USE_TERMIOS + /* optimise remove char in the case of removing the last char */ + if (current->pos == pos + 1 && current->pos == current->chars) { + if (current->buf[pos] >= ' ' && utf8_strlen(current->prompt, -1) + utf8_strlen(current->buf, current->len) < current->cols - 1) { + ret = 2; + fd_printf(current->fd, "\b \b"); + } + } +#endif + + /* Move the null char too */ + memmove(current->buf + p1, current->buf + p2, current->len - p2 + 1); + current->len -= (p2 - p1); + current->chars--; + + if (current->pos > pos) { + current->pos--; + } + return ret; + } + return 0; +} + +/** + * Insert 'ch' at position 'pos' + * + * Returns 1 if the line needs to be refreshed, 2 if not + * and 0 if nothing was inserted (no room) + */ +static int insert_char(struct current *current, int pos, int ch) +{ + char buf[3]; + int n = utf8_getchars(buf, ch); + + if (has_room(current, n) && pos >= 0 && pos <= current->chars) { + int p1, p2; + int ret = 1; + p1 = utf8_index(current->buf, pos); + p2 = p1 + n; + +#ifdef USE_TERMIOS + /* optimise the case where adding a single char to the end and no scrolling is needed */ + if (current->pos == pos && current->chars == pos) { + if (ch >= ' ' && utf8_strlen(current->prompt, -1) + utf8_strlen(current->buf, current->len) < current->cols - 1) { + IGNORE_RC(write(current->fd, buf, n)); + ret = 2; + } + } +#endif + + memmove(current->buf + p2, current->buf + p1, current->len - p1); + memcpy(current->buf + p1, buf, n); + current->len += n; + + current->chars++; + if (current->pos >= pos) { + current->pos++; + } + return ret; + } + return 0; +} + +/** + * 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; + while (n-- && remove_char(current, pos)) { + removed++; + } + return removed; +} + +#ifndef NO_COMPLETION +static linenoiseCompletionCallback *completionCallback = NULL; + static void beep() { +#ifdef USE_TERMIOS fprintf(stderr, "\x7"); fflush(stderr); +#endif } static void freeCompletions(linenoiseCompletions *lc) { size_t i; for (i = 0; i < lc->len; i++) free(lc->cvec[i]); - if (lc->cvec != NULL) - free(lc->cvec); + free(lc->cvec); } -static int completeLine(int fd, const char *prompt, char *buf, size_t buflen, size_t *len, size_t *pos, size_t cols) { +static int completeLine(struct current *current) { linenoiseCompletions lc = { 0, NULL }; - int nread, nwritten; - char c = 0; + int c = 0; - completionCallback(buf,&lc); + completionCallback(current->buf,&lc); if (lc.len == 0) { beep(); } else { size_t stop = 0, i = 0; - size_t clen; while(!stop) { /* Show completion or original buffer */ if (i < lc.len) { - clen = strlen(lc.cvec[i]); - refreshLine(fd,prompt,lc.cvec[i],clen,clen,cols); + struct current tmp = *current; + tmp.buf = lc.cvec[i]; + tmp.pos = tmp.len = strlen(tmp.buf); + tmp.chars = utf8_strlen(tmp.buf, tmp.len); + refreshLine(current->prompt, &tmp); } else { - refreshLine(fd,prompt,buf,*len,*pos,cols); + refreshLine(current->prompt, current); } - nread = read(fd,&c,1); - if (nread <= 0) { - freeCompletions(&lc); - return -1; + c = fd_read(current); + if (c == -1) { + break; } switch(c) { - case 9: /* tab */ + case '\t': /* tab */ i = (i+1) % (lc.len+1); if (i == lc.len) beep(); break; case 27: /* escape */ /* Re-show original buffer */ if (i < lc.len) { - refreshLine(fd,prompt,buf,*len,*pos,cols); + refreshLine(current->prompt, current); } stop = 1; break; default: /* Update buffer and return */ if (i < lc.len) { - nwritten = snprintf(buf,buflen,"%s",lc.cvec[i]); - *len = *pos = nwritten; + set_current(current,lc.cvec[i]); } stop = 1; break; @@ -281,270 +925,335 @@ static int completeLine(int fd, const char *prompt, char *buf, size_t buflen, si return c; /* Return last read character */ } -void linenoiseClearScreen(void) { - if (write(STDIN_FILENO,"\x1b[H\x1b[2J",7) <= 0) { - /* nothing to do, just to avoid warning. */ - } -} - -static int linenoisePrompt(int fd, char *buf, size_t buflen, const char *prompt) { - size_t plen = strlen(prompt); - size_t pos = 0; - size_t len = 0; - size_t cols = getColumns(); - int history_index = 0; - size_t old_pos; - size_t diff; - - buf[0] = '\0'; - buflen--; /* Make sure there is always space for the nulterm */ - - /* The latest history entry is always our current buffer, that - * initially is just an empty string. */ - linenoiseHistoryAdd(""); - - if (write(fd,prompt,plen) == -1) return -1; - while(1) { - char c; - int nread; - char seq[2], seq2[2]; - - nread = read(fd,&c,1); - if (nread <= 0) return len; - - /* 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) { - c = completeLine(fd,prompt,buf,buflen,&len,&pos,cols); - /* Return on errors */ - if (c < 0) return len; - /* Read next character when 0 */ - if (c == 0) continue; - } - - switch(c) { - case 13: /* enter */ - history_len--; - free(history[history_len]); - return (int)len; - case 3: /* ctrl-c */ - errno = EAGAIN; - return -1; - case 127: /* backspace */ - case 8: /* ctrl-h */ - if (pos > 0 && len > 0) { - memmove(buf+pos-1,buf+pos,len-pos); - pos--; - len--; - buf[len] = '\0'; - refreshLine(fd,prompt,buf,len,pos,cols); - } - break; - case 4: /* ctrl-d, remove char at right of cursor */ - if (len > 1 && pos < (len-1)) { - memmove(buf+pos,buf+pos+1,len-pos); - len--; - buf[len] = '\0'; - refreshLine(fd,prompt,buf,len,pos,cols); - } else if (len == 0) { - history_len--; - free(history[history_len]); - return -1; - } - break; - case 20: /* ctrl-t */ - if (pos > 0 && pos < len) { - int aux = buf[pos-1]; - buf[pos-1] = buf[pos]; - buf[pos] = aux; - if (pos != len-1) pos++; - refreshLine(fd,prompt,buf,len,pos,cols); - } - break; - case 2: /* ctrl-b */ - goto left_arrow; - case 6: /* ctrl-f */ - goto right_arrow; - case 16: /* ctrl-p */ - seq[1] = 65; - goto up_down_arrow; - case 14: /* ctrl-n */ - seq[1] = 66; - goto up_down_arrow; - break; - case 27: /* escape sequence */ - if (read(fd,seq,2) == -1) break; - if (seq[0] == 91 && seq[1] == 68) { -left_arrow: - /* left arrow */ - if (pos > 0) { - pos--; - refreshLine(fd,prompt,buf,len,pos,cols); - } - } else if (seq[0] == 91 && seq[1] == 67) { -right_arrow: - /* right arrow */ - if (pos != len) { - pos++; - refreshLine(fd,prompt,buf,len,pos,cols); - } - } else if (seq[0] == 91 && (seq[1] == 65 || seq[1] == 66)) { -up_down_arrow: - /* up and down arrow: history */ - 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(buf); - /* Show the new entry */ - history_index += (seq[1] == 65) ? 1 : -1; - if (history_index < 0) { - history_index = 0; - break; - } else if (history_index >= history_len) { - history_index = history_len-1; - break; - } - strncpy(buf,history[history_len-1-history_index],buflen); - buf[buflen] = '\0'; - len = pos = strlen(buf); - refreshLine(fd,prompt,buf,len,pos,cols); - } - } else if (seq[0] == 91 && seq[1] > 48 && seq[1] < 55) { - /* extended escape */ - if (read(fd,seq2,2) == -1) break; - if (seq[1] == 51 && seq2[0] == 126) { - /* delete */ - if (len > 0 && pos < len) { - memmove(buf+pos,buf+pos+1,len-pos-1); - len--; - buf[len] = '\0'; - refreshLine(fd,prompt,buf,len,pos,cols); - } - } - } - break; - default: - if (len < buflen) { - if (len == pos) { - buf[pos] = c; - pos++; - len++; - buf[len] = '\0'; - if (plen+len < cols) { - /* Avoid a full update of the line in the - * trivial case. */ - if (write(fd,&c,1) == -1) return -1; - } else { - refreshLine(fd,prompt,buf,len,pos,cols); - } - } else { - memmove(buf+pos+1,buf+pos,len-pos); - buf[pos] = c; - len++; - pos++; - buf[len] = '\0'; - refreshLine(fd,prompt,buf,len,pos,cols); - } - } - break; - case 21: /* Ctrl+u, delete the whole line. */ - buf[0] = '\0'; - pos = len = 0; - refreshLine(fd,prompt,buf,len,pos,cols); - break; - case 11: /* Ctrl+k, delete from current to end of line. */ - buf[pos] = '\0'; - len = pos; - refreshLine(fd,prompt,buf,len,pos,cols); - break; - case 1: /* Ctrl+a, go to the start of the line */ - pos = 0; - refreshLine(fd,prompt,buf,len,pos,cols); - break; - case 5: /* ctrl+e, go to the end of the line */ - pos = len; - refreshLine(fd,prompt,buf,len,pos,cols); - break; - case 12: /* ctrl+l, clear screen */ - linenoiseClearScreen(); - refreshLine(fd,prompt,buf,len,pos,cols); - break; - case 23: /* ctrl+w, delete previous word */ - old_pos = pos; - while (pos > 0 && buf[pos-1] == ' ') - pos--; - while (pos > 0 && buf[pos-1] != ' ') - pos--; - diff = old_pos - pos; - memmove(&buf[pos], &buf[old_pos], len-old_pos+1); - len -= diff; - refreshLine(fd,prompt,buf,len,pos,cols); - break; - } - } - return len; -} - -static int linenoiseRaw(char *buf, size_t buflen, const char *prompt) { - int fd = STDIN_FILENO; - int count; - - if (buflen == 0) { - errno = EINVAL; - return -1; - } - if (!isatty(STDIN_FILENO)) { - if (fgets(buf, buflen, stdin) == NULL) return -1; - count = strlen(buf); - if (count && buf[count-1] == '\n') { - count--; - buf[count] = '\0'; - } - } else { - if (enableRawMode(fd) == -1) return -1; - count = linenoisePrompt(fd, buf, buflen, prompt); - disableRawMode(fd); - printf("\n"); - } - return count; -} - -char *linenoise(const char *prompt) { - char buf[LINENOISE_MAX_LINE]; - int count; - - if (isUnsupportedTerm()) { - size_t len; - - printf("%s",prompt); - fflush(stdout); - if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL; - len = strlen(buf); - while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) { - len--; - buf[len] = '\0'; - } - return strdup(buf); - } else { - count = linenoiseRaw(buf,LINENOISE_MAX_LINE,prompt); - if (count == -1) return NULL; - return strdup(buf); - } -} - /* Register a callback function to be called for tab-completion. */ void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) { completionCallback = fn; } -void linenoiseAddCompletion(linenoiseCompletions *lc, char *str) { - size_t len = strlen(str); - char *copy = malloc(len+1); - memcpy(copy,str,len+1); - lc->cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1)); - lc->cvec[lc->len++] = copy; +void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) { + lc->cvec = (char **)realloc(lc->cvec,sizeof(char*)*(lc->len+1)); + lc->cvec[lc->len++] = strdup(str); +} + +#endif + +static int linenoisePrompt(struct current *current) { + int history_index = 0; + + /* The latest history entry is always our current buffer, that + * initially is just an empty string. */ + linenoiseHistoryAdd(""); + + set_current(current, ""); + refreshLine(current->prompt, current); + + while(1) { + int dir = -1; + int c = fd_read(current); + +#ifndef NO_COMPLETION + /* 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 == '\t' && current->pos == current->chars && completionCallback != NULL) { + c = completeLine(current); + /* Return on errors */ + if (c < 0) return current->len; + /* Read next character when 0 */ + if (c == 0) continue; + } +#endif + +process_char: + if (c == -1) return current->len; +#ifdef USE_TERMIOS + if (c == 27) { /* escape sequence */ + c = check_special(current->fd); + } +#endif + switch(c) { + case '\r': /* enter */ + history_len--; + free(history[history_len]); + return current->len; + case ctrl('C'): /* ctrl-c */ + errno = EAGAIN; + return -1; + case 127: /* backspace */ + case ctrl('H'): + if (remove_char(current, current->pos - 1) == 1) { + refreshLine(current->prompt, current); + } + break; + case ctrl('D'): /* ctrl-d */ + if (current->len == 0) { + /* Empty line, so EOF */ + history_len--; + free(history[history_len]); + return -1; + } + /* 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 */ + /* eat any spaces on the left */ + { + int pos = current->pos; + while (pos > 0 && get_char(current, pos - 1) == ' ') { + pos--; + } + + /* now eat any non-spaces on the left */ + while (pos > 0 && get_char(current, pos - 1) != ' ') { + pos--; + } + + if (remove_chars(current, pos, current->pos - pos)) { + refreshLine(current->prompt, current); + } + } + break; + case ctrl('R'): /* ctrl-r */ + { + /* Display the reverse-i-search prompt and process chars */ + char rbuf[50]; + char rprompt[80]; + int rchars = 0; + int rlen = 0; + int searchpos = history_len - 1; + + rbuf[0] = 0; + while (1) { + int n = 0; + const char *p = NULL; + int skipsame = 0; + int searchdir = -1; + + snprintf(rprompt, sizeof(rprompt), "(reverse-i-search)'%s': ", rbuf); + refreshLine(rprompt, current); + c = fd_read(current); + if (c == ctrl('H') || c == 127) { + if (rchars) { + int p = utf8_index(rbuf, --rchars); + rbuf[p] = 0; + rlen = strlen(rbuf); + } + continue; + } +#ifdef USE_TERMIOS + if (c == 27) { + c = check_special(current->fd); + } +#endif + if (c == ctrl('P') || c == SPECIAL_UP) { + /* Search for the previous (earlier) match */ + if (searchpos > 0) { + searchpos--; + } + skipsame = 1; + } + else if (c == ctrl('N') || c == SPECIAL_DOWN) { + /* Search for the next (later) match */ + if (searchpos < history_len) { + searchpos++; + } + searchdir = 1; + skipsame = 1; + } + else if (c >= ' ') { + if (rlen >= (int)sizeof(rbuf) + 3) { + continue; + } + + n = utf8_getchars(rbuf + rlen, c); + rlen += n; + rchars++; + rbuf[rlen] = 0; + + /* Adding a new char resets the search location */ + searchpos = history_len - 1; + } + else { + /* Exit from incremental search mode */ + break; + } + + /* Now search through the history for a match */ + for (; searchpos >= 0 && searchpos < history_len; searchpos += searchdir) { + p = strstr(history[searchpos], rbuf); + if (p) { + /* Found a match */ + if (skipsame && strcmp(history[searchpos], current->buf) == 0) { + /* But it is identical, so skip it */ + continue; + } + /* Copy the matching line and set the cursor position */ + set_current(current,history[searchpos]); + current->pos = utf8_strlen(history[searchpos], p - history[searchpos]); + break; + } + } + if (!p && n) { + /* No match, so don't add it */ + rchars--; + rlen -= n; + rbuf[rlen] = 0; + } + } + if (c == ctrl('G') || c == ctrl('C')) { + /* ctrl-g terminates the search with no effect */ + set_current(current, ""); + c = 0; + } + else if (c == ctrl('J')) { + /* ctrl-j terminates the search leaving the buffer in place */ + c = 0; + } + /* Go process the char normally */ + refreshLine(current->prompt, current); + goto 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); + insert_char(current, current->pos - 1, c); + refreshLine(current->prompt, current); + } + break; + case ctrl('V'): /* ctrl-v */ + if (has_room(current, 3)) { + /* Insert the ^V first */ + if (insert_char(current, current->pos, c)) { + refreshLine(current->prompt, current); + /* Now wait for the next char. Can insert anything except \0 */ + c = fd_read(current); + + /* Remove the ^V first */ + remove_char(current, current->pos - 1); + if (c != -1) { + /* Insert the actual char */ + insert_char(current, current->pos, c); + } + refreshLine(current->prompt, current); + } + } + break; + case ctrl('B'): + case SPECIAL_LEFT: + if (current->pos > 0) { + current->pos--; + refreshLine(current->prompt, current); + } + break; + case ctrl('F'): + case SPECIAL_RIGHT: + if (current->pos < current->chars) { + current->pos++; + refreshLine(current->prompt, current); + } + break; + case ctrl('P'): + case SPECIAL_UP: + dir = 1; + case ctrl('N'): + case SPECIAL_DOWN: + 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); + /* 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; + break; + } + 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; + case ctrl('U'): /* Ctrl+u, delete to beginning of line. */ + if (remove_chars(current, 0, current->pos)) { + refreshLine(current->prompt, current); + } + break; + case ctrl('K'): /* Ctrl+k, delete from current to end of line. */ + if (remove_chars(current, current->pos, current->chars - current->pos)) { + refreshLine(current->prompt, current); + } + break; + case ctrl('L'): /* Ctrl+L, 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; +} + +char *linenoise(const char *prompt) +{ + int count; + struct current current; + char buf[LINENOISE_MAX_LINE]; + + if (enableRawMode(¤t) == -1) { + printf("%s", prompt); + fflush(stdout); + if (fgets(buf, sizeof(buf), stdin) == NULL) { + return NULL; + } + count = strlen(buf); + if (count && buf[count-1] == '\n') { + count--; + buf[count] = '\0'; + } + } + else + { + current.buf = buf; + current.bufmax = sizeof(buf); + current.len = 0; + current.chars = 0; + current.pos = 0; + current.prompt = prompt; + + count = linenoisePrompt(¤t); + disableRawMode(¤t); + printf("\n"); + if (count == -1) { + return NULL; + } + } + return strdup(buf); } /* Using a circular buffer is smarter, but a bit more complex to handle. */ @@ -553,10 +1262,16 @@ int linenoiseHistoryAdd(const char *line) { if (history_max_len == 0) return 0; if (history == NULL) { - history = malloc(sizeof(char*)*history_max_len); + history = (char **)malloc(sizeof(char*)*history_max_len); 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) { @@ -570,18 +1285,18 @@ int linenoiseHistoryAdd(const char *line) { } int linenoiseHistorySetMaxLen(int len) { - char **new; + char **newHistory; if (len < 1) return 0; if (history) { int tocopy = history_len; - new = malloc(sizeof(char*)*len); - if (new == NULL) return 0; + newHistory = (char **)malloc(sizeof(char*)*len); + if (newHistory == NULL) return 0; if (len < tocopy) tocopy = len; - memcpy(new,history+(history_max_len-tocopy), sizeof(char*)*tocopy); + memcpy(newHistory,history+(history_max_len-tocopy), sizeof(char*)*tocopy); free(history); - history = new; + history = newHistory; } history_max_len = len; if (history_len > history_max_len) @@ -591,13 +1306,32 @@ int linenoiseHistorySetMaxLen(int len) { /* Save the history in the specified file. On success 0 is returned * otherwise -1 is returned. */ -int linenoiseHistorySave(char *filename) { +int linenoiseHistorySave(const char *filename) { FILE *fp = fopen(filename,"w"); int j; - + if (fp == NULL) return -1; - for (j = 0; j < history_len; j++) - fprintf(fp,"%s\n",history[j]); + for (j = 0; j < history_len; j++) { + const char *str = history[j]; + /* Need to encode backslash, nl and cr */ + while (*str) { + if (*str == '\\') { + fputs("\\\\", fp); + } + else if (*str == '\n') { + fputs("\\n", fp); + } + else if (*str == '\r') { + fputs("\\r", fp); + } + else { + fputc(*str, fp); + } + str++; + } + fputc('\n', fp); + } + fclose(fp); return 0; } @@ -607,20 +1341,51 @@ int linenoiseHistorySave(char *filename) { * * If the file exists and the operation succeeded 0 is returned, otherwise * on error -1 is returned. */ -int linenoiseHistoryLoad(char *filename) { +int linenoiseHistoryLoad(const char *filename) { FILE *fp = fopen(filename,"r"); char buf[LINENOISE_MAX_LINE]; - + if (fp == NULL) return -1; while (fgets(buf,LINENOISE_MAX_LINE,fp) != NULL) { - char *p; - - p = strchr(buf,'\r'); - if (!p) p = strchr(buf,'\n'); - if (p) *p = '\0'; + char *src, *dest; + + /* Decode backslash escaped values */ + for (src = dest = buf; *src; src++) { + char ch = *src; + + if (ch == '\\') { + src++; + if (*src == 'n') { + ch = '\n'; + } + else if (*src == 'r') { + ch = '\r'; + } else { + ch = *src; + } + } + *dest++ = ch; + } + /* Remove trailing newline */ + if (dest != buf && (dest[-1] == '\n' || dest[-1] == '\r')) { + dest--; + } + *dest = 0; + linenoiseHistoryAdd(buf); } fclose(fp); return 0; } + +/* Provide access to the history buffer. + * + * If 'len' is not NULL, the length is stored in *len. + */ +char **linenoiseHistory(int *len) { + if (len) { + *len = history_len; + } + return history; +} diff --git a/src/rt/linenoise/linenoise.h b/src/rt/linenoise/linenoise.h index 0b730140856..59f28976d67 100644 --- a/src/rt/linenoise/linenoise.h +++ b/src/rt/linenoise/linenoise.h @@ -9,18 +9,18 @@ * Copyright (c) 2010, Pieter Noordhuis * * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: - * + * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR @@ -37,6 +37,7 @@ #ifndef __LINENOISE_H #define __LINENOISE_H +#ifndef NO_COMPLETION typedef struct linenoiseCompletions { size_t len; char **cvec; @@ -44,13 +45,15 @@ typedef struct linenoiseCompletions { typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); -void linenoiseAddCompletion(linenoiseCompletions *, char *); +void linenoiseAddCompletion(linenoiseCompletions *, const char *); +#endif char *linenoise(const char *prompt); int linenoiseHistoryAdd(const char *line); int linenoiseHistorySetMaxLen(int len); -int linenoiseHistorySave(char *filename); -int linenoiseHistoryLoad(char *filename); -void linenoiseClearScreen(void); +int linenoiseHistorySave(const char *filename); +int linenoiseHistoryLoad(const char *filename); +void linenoiseHistoryFree(void); +char **linenoiseHistory(int *len); -#endif /* __LINENOISE_H */ \ No newline at end of file +#endif /* __LINENOISE_H */ diff --git a/src/rt/linenoise/utf8.c b/src/rt/linenoise/utf8.c new file mode 100644 index 00000000000..26924b46c19 --- /dev/null +++ b/src/rt/linenoise/utf8.c @@ -0,0 +1,115 @@ +/** + * UTF-8 utility functions + * + * (c) 2010 Steve Bennett + * + * See LICENCE for licence details. + */ + +#include +#include +#include +#include +#include "utf8.h" + +#ifdef USE_UTF8 +int utf8_fromunicode(char *p, unsigned short uc) +{ + if (uc <= 0x7f) { + *p = uc; + return 1; + } + else if (uc <= 0x7ff) { + *p++ = 0xc0 | ((uc & 0x7c0) >> 6); + *p = 0x80 | (uc & 0x3f); + return 2; + } + else { + *p++ = 0xe0 | ((uc & 0xf000) >> 12); + *p++ = 0x80 | ((uc & 0xfc0) >> 6); + *p = 0x80 | (uc & 0x3f); + return 3; + } +} + +int utf8_charlen(int c) +{ + if ((c & 0x80) == 0) { + return 1; + } + if ((c & 0xe0) == 0xc0) { + return 2; + } + if ((c & 0xf0) == 0xe0) { + return 3; + } + if ((c & 0xf8) == 0xf0) { + return 4; + } + /* Invalid sequence */ + return -1; +} + +int utf8_strlen(const char *str, int bytelen) +{ + int charlen = 0; + if (bytelen < 0) { + bytelen = strlen(str); + } + while (bytelen) { + int c; + int l = utf8_tounicode(str, &c); + charlen++; + str += l; + bytelen -= l; + } + return charlen; +} + +int utf8_index(const char *str, int index) +{ + const char *s = str; + while (index--) { + int c; + s += utf8_tounicode(s, &c); + } + return s - str; +} + +int utf8_charequal(const char *s1, const char *s2) +{ + int c1, c2; + + utf8_tounicode(s1, &c1); + utf8_tounicode(s2, &c2); + + return c1 == c2; +} + +int utf8_tounicode(const char *str, int *uc) +{ + unsigned const char *s = (unsigned const char *)str; + + if (s[0] < 0xc0) { + *uc = s[0]; + return 1; + } + if (s[0] < 0xe0) { + if ((s[1] & 0xc0) == 0x80) { + *uc = ((s[0] & ~0xc0) << 6) | (s[1] & ~0x80); + return 2; + } + } + else if (s[0] < 0xf0) { + if (((str[1] & 0xc0) == 0x80) && ((str[2] & 0xc0) == 0x80)) { + *uc = ((s[0] & ~0xe0) << 12) | ((s[1] & ~0x80) << 6) | (s[2] & ~0x80); + return 3; + } + } + + /* Invalid sequence, so just return the byte */ + *uc = *s; + return 1; +} + +#endif diff --git a/src/rt/linenoise/utf8.h b/src/rt/linenoise/utf8.h new file mode 100644 index 00000000000..9537939876a --- /dev/null +++ b/src/rt/linenoise/utf8.h @@ -0,0 +1,79 @@ +#ifndef UTF8_UTIL_H +#define UTF8_UTIL_H +/** + * UTF-8 utility functions + * + * (c) 2010 Steve Bennett + * + * See LICENCE for licence details. + */ + +#ifndef USE_UTF8 +#include + +/* No utf-8 support. 1 byte = 1 char */ +#define utf8_strlen(S, B) ((B) < 0 ? (int)strlen(S) : (B)) +#define utf8_tounicode(S, CP) (*(CP) = (unsigned char)*(S), 1) +#define utf8_index(C, I) (I) +#define utf8_charlen(C) 1 + +#else +/** + * Converts the given unicode codepoint (0 - 0xffff) to utf-8 + * and stores the result at 'p'. + * + * Returns the number of utf-8 characters (1-3). + */ +int utf8_fromunicode(char *p, unsigned short uc); + +/** + * Returns the length of the utf-8 sequence starting with 'c'. + * + * Returns 1-4, or -1 if this is not a valid start byte. + * + * Note that charlen=4 is not supported by the rest of the API. + */ +int utf8_charlen(int c); + +/** + * Returns the number of characters in the utf-8 + * string of the given byte length. + * + * Any bytes which are not part of an valid utf-8 + * sequence are treated as individual characters. + * + * The string *must* be null terminated. + * + * Does not support unicode code points > \uffff + */ +int utf8_strlen(const char *str, int bytelen); + +/** + * Returns the byte index of the given character in the utf-8 string. + * + * The string *must* be null terminated. + * + * This will return the byte length of a utf-8 string + * if given the char length. + */ +int utf8_index(const char *str, int charindex); + +/** + * Returns the unicode codepoint corresponding to the + * utf-8 sequence 'str'. + * + * Stores the result in *uc and returns the number of bytes + * consumed. + * + * If 'str' is null terminated, then an invalid utf-8 sequence + * at the end of the string will be returned as individual bytes. + * + * If it is not null terminated, the length *must* be checked first. + * + * Does not support unicode code points > \uffff + */ +int utf8_tounicode(const char *str, int *uc); + +#endif + +#endif diff --git a/src/rt/rustrt.def.in b/src/rt/rustrt.def.in index fd91a83b51b..0668479a48d 100644 --- a/src/rt/rustrt.def.in +++ b/src/rt/rustrt.def.in @@ -211,4 +211,3 @@ linenoiseHistoryAdd linenoiseHistorySetMaxLen linenoiseHistorySave linenoiseHistoryLoad -linenoiseClearScreen diff --git a/src/rusti/rusti.rs b/src/rusti/rusti.rs index 2fa4684b322..5f1cad3ae17 100644 --- a/src/rusti/rusti.rs +++ b/src/rusti/rusti.rs @@ -243,7 +243,8 @@ fn run_cmd(repl: &mut Repl, _in: io::Reader, _out: io::Writer, repl.view_items = ~""; repl.stmts = ~""; - rl::clear(); + // FIXME: Win32 version of linenoise doesn't have the required function + //rl::clear(); } ~"help" => { io::println(~":clear - clear the screen\n" +