/* linenoise.c -- guerrilla line editing library against the idea that a * 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 * * Does a number of crazy assumptions that happen to be true in 99.9999% of * the 2010 UNIX computers around. * * Copyright (c) 2010, Salvatore Sanfilippo * 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. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * 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 A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY 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? * * 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 * 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 * Effect: if n is 1, clear from beginning of line to cursor * Effect: if n is 2, clear entire line * * CUF (CUrsor Forward) * Sequence: ESC [ n C * Effect: moves cursor forward of n chars * * The following are used to clear the screen: ESC [ H ESC [ 2 J * This is actually composed of two sequences: * * cursorhome * Sequence: ESC [ H * Effect: moves the cursor to upper left corner * * ED2 (Clear entire screen) * Sequence: ESC [ 2 J * Effect: clear the whole screen * */ #ifdef _WIN32 #include #include #include #include #include #define snprintf _snprintf // Microsoft headers use underscores in some names #define strcasecmp _stricmp #define strdup _strdup #define isatty _isatty #define write _write #define STDIN_FILENO 0 #else /* _WIN32 */ #include #include #include #include #include #include #include #include #include #include #include #include #endif /* _WIN32 */ #include "linenoise.h" #include #include using std::string; using std::vector; #define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 #define LINENOISE_MAX_LINE 4096 // make control-characters more readable #define ctrlChar( upperCaseASCII ) ( upperCaseASCII - 0x40 ) /** * Calculate a new screen position given a starting position, screen width and character count * @param x initial x position (zero-based) * @param y initial y position (zero-based) * @param screenColumns screen column count * @param charCount character positions to advance * @param xOut returned x position (zero-based) * @param yOut returned y position (zero-based) */ static void calculateScreenPosition(int x, int y, int screenColumns, int charCount, int& xOut, int& yOut) { xOut = x; yOut = y; int charsRemaining = charCount; while ( charsRemaining > 0 ) { int charsThisRow = (x + charsRemaining < screenColumns) ? charsRemaining : screenColumns - x; xOut = x + charsThisRow; yOut = y; charsRemaining -= charsThisRow; x = 0; ++y; } if ( xOut == screenColumns ) { // we have to special-case line wrap xOut = 0; ++yOut; } } struct PromptBase { // a convenience struct for grouping prompt info char* promptText; // our copy of the prompt text, edited int promptChars; // bytes or chars (until UTF-8) in promptText int promptExtraLines; // extra lines (beyond 1) occupied by prompt int promptIndentation; // column offset to end of prompt int promptLastLinePosition; // index into promptText where last line begins int promptPreviousInputLen; // promptChars of previous input line, for clearing int promptCursorRowOffset; // where the cursor is relative to the start of the prompt int promptScreenColumns; // width of screen in columns int previousPromptLen; // help erasing }; struct PromptInfo : public PromptBase { PromptInfo( const char* textPtr, int columns ) { promptScreenColumns = columns; promptText = new char[strlen( textPtr ) + 1]; strcpy( promptText, textPtr ); // strip evil characters from the prompt -- we do allow newline unsigned char* pIn = reinterpret_cast( promptText ); unsigned char* pOut = pIn; while ( *pIn ) { unsigned char c = *pIn; // we need unsigned so chars 0x80 and above are allowed if ( '\n' == c || c >= ' ' ) { *pOut = c; ++pOut; } ++pIn; } *pOut = 0; promptChars = pOut - reinterpret_cast( promptText ); promptExtraLines = 0; promptLastLinePosition = 0; promptPreviousInputLen = 0; int x = 0; for ( int i = 0; i < promptChars; ++i ) { char c = promptText[i]; if ( '\n' == c ) { x = 0; ++promptExtraLines; promptLastLinePosition = i + 1; } else { ++x; if ( x >= promptScreenColumns ) { x = 0; ++promptExtraLines; promptLastLinePosition = i + 1; } } } promptIndentation = promptChars - promptLastLinePosition; promptCursorRowOffset = promptExtraLines; } ~PromptInfo() { delete [] promptText; } }; // Used with DynamicPrompt (history search) // static const char forwardSearchBasePrompt[] = "(i-search)`"; static const char reverseSearchBasePrompt[] = "(reverse-i-search)`"; static const char endSearchBasePrompt[] = "': "; static string previousSearchText; // changing prompt for "(reverse-i-search)`text':" etc. // struct DynamicPrompt : public PromptBase { char* searchText; // text we are searching for int searchTextLen; // chars in searchText int direction; // current search direction, 1=forward, -1=reverse int forwardSearchBasePromptLen; // prompt component lengths int reverseSearchBasePromptLen; int endSearchBasePromptLen; DynamicPrompt( PromptInfo& pi, int initialDirection ) : direction( initialDirection ) { forwardSearchBasePromptLen = strlen( forwardSearchBasePrompt ); // store constant text lengths reverseSearchBasePromptLen = strlen( reverseSearchBasePrompt ); endSearchBasePromptLen = strlen( endSearchBasePrompt ); promptScreenColumns = pi.promptScreenColumns; promptCursorRowOffset = 0; searchTextLen = 0; searchText = new char[1]; // start with empty search string searchText[0] = 0; promptChars = endSearchBasePromptLen + ( ( direction > 0 ) ? forwardSearchBasePromptLen : reverseSearchBasePromptLen ); promptLastLinePosition = promptChars; // TODO fix this, we are asssuming that the history prompt won't wrap (!) promptPreviousInputLen = 0; previousPromptLen = promptChars; promptText = new char[promptChars + 1]; strcpy( promptText, ( direction > 0 ) ? forwardSearchBasePrompt : reverseSearchBasePrompt ); strcpy( &promptText[promptChars - endSearchBasePromptLen], endSearchBasePrompt ); calculateScreenPosition( 0, 0, pi.promptScreenColumns, promptChars, promptIndentation, promptExtraLines ); } void updateSearchPrompt( void ) { delete [] promptText; promptChars = endSearchBasePromptLen + searchTextLen + ( ( direction > 0 ) ? forwardSearchBasePromptLen : reverseSearchBasePromptLen ); promptText = new char[promptChars + 1]; strcpy( promptText, ( direction > 0 ) ? forwardSearchBasePrompt : reverseSearchBasePrompt ); strcat( promptText, searchText ); strcpy( &promptText[promptChars - endSearchBasePromptLen], endSearchBasePrompt ); } void updateSearchText( const char* textPtr ) { delete [] searchText; searchTextLen = strlen( textPtr ); searchText = new char[searchTextLen + 1]; strcpy( searchText, textPtr ); updateSearchPrompt(); } ~DynamicPrompt() { delete [] searchText; delete [] promptText; } }; class KillRing { static const int capacity = 10; int size; int index; char indexToSlot[10]; vector < string > theRing; public: enum action { actionOther, actionKill, actionYank }; action lastAction; int lastYankSize; KillRing() : size( 0 ), index( 0 ), lastAction( actionOther ) { theRing.reserve( capacity ); } void kill( const char* text, int textLen, bool forward ) { if ( textLen == 0 ) { return; } char* textCopy = new char[ textLen + 1 ]; memcpy( textCopy, text, textLen ); textCopy[ textLen ] = 0; string textCopyString( textCopy ); if ( lastAction == actionKill ) { int slot = indexToSlot[0]; theRing[slot] = forward ? theRing[slot] + textCopyString : textCopyString + theRing[slot]; } else { if ( size < capacity ) { if ( size > 0 ) { memmove( &indexToSlot[1], &indexToSlot[0], size ); } indexToSlot[0] = size; size++; theRing.push_back( textCopyString ); } else { int slot = indexToSlot[capacity - 1]; theRing[slot] = textCopyString; memmove( &indexToSlot[1], &indexToSlot[0], capacity - 1 ); indexToSlot[0] = slot; } index = 0; } delete[] textCopy; } string* yank() { return ( size > 0 ) ? &theRing[indexToSlot[index]] : 0; } string* yankPop() { if ( size == 0 ) { return 0; } ++index; if ( index == size ) { index = 0; } return &theRing[indexToSlot[index]]; } }; class InputBuffer { char* buf; int buflen; int len; int pos; void clearScreen( PromptInfo& pi ); int incrementalHistorySearch( PromptInfo& pi, int startChar ); int completeLine( PromptInfo& pi ); void refreshLine( PromptBase& pi ); public: InputBuffer( char* buffer, int bufferLen ) : buf( buffer ), buflen( bufferLen - 1 ), len( 0 ), pos( 0 ) { buf[0] = 0; } int getInputLine( PromptInfo& pi ); }; // Special codes for keyboard input: // // Between Windows and the various Linux "terminal" programs, there is some // pretty diverse behavior in the "scan codes" and escape sequences we are // presented with. So ... we'll translate them all into our own pidgin // pseudocode, trying to stay out of the way of UTF-8 and international // characters. Here's the general plan. // // "User input keystrokes" (key chords, whatever) will be encoded as a single // value. The low 8 bits are reserved for ASCII and UTF-8 characters. // Popular function-type keys get their own codes in the range 0x101 to (if // needed) 0x1FF, currently just arrow keys, Home, End and Delete. // Keypresses with Ctrl get or-ed with 0x200, with Alt get or-ed with 0x400. // So, Ctrl+Alt+Home is encoded as 0x200 + 0x400 + 0x105 == 0x705. To keep // things complicated, the Alt key is equivalent to prefixing the keystroke // with ESC, so ESC followed by D is treated the same as Alt + D ... we'll // just use Emacs terminology and call this "Meta". So, we will encode both // ESC followed by D and Alt held down while D is pressed the same, as Meta-D, // encoded as 0x464. // // Here are the definitions of our component constants: // static const int UP_ARROW_KEY = 0x101; static const int DOWN_ARROW_KEY = 0x102; static const int RIGHT_ARROW_KEY = 0x103; static const int LEFT_ARROW_KEY = 0x104; static const int HOME_KEY = 0x105; static const int END_KEY = 0x106; static const int DELETE_KEY = 0x107; static const int CTRL = 0x200; static const int META = 0x400; static linenoiseCompletionCallback* completionCallback = NULL; #ifdef _WIN32 static HANDLE console_in, console_out; static DWORD oldMode; static WORD oldDisplayAttribute; #else static struct termios orig_termios; /* in order to restore at exit */ #endif static KillRing killRing; static int rawmode = 0; /* for atexit() function to check if restore is needed*/ static int atexit_registered = 0; /* register atexit just 1 time */ static int historyMaxLen = LINENOISE_DEFAULT_HISTORY_MAX_LEN; static int historyLen = 0; static int historyIndex = 0; static char** history = NULL; static void linenoiseAtExit( void ); static void beep() { fprintf( stderr, "\x7" ); // ctrl-G == bell/beep fflush( stderr ); } void linenoiseHistoryFree( void ) { if ( history ) { for ( int j = 0; j < historyLen; ++j ) free( history[j] ); historyLen = 0; free( history ); history = 0; } } static int enableRawMode( void ) { #ifdef _WIN32 if ( ! console_in ) { console_in = GetStdHandle( STD_INPUT_HANDLE ); console_out = GetStdHandle( STD_OUTPUT_HANDLE ); GetConsoleMode( console_in, &oldMode ); SetConsoleMode( console_in, oldMode & ~( ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT ) ); } return 0; #else struct termios raw; if ( ! isatty( 0 ) ) goto fatal; if ( ! atexit_registered ) { atexit( linenoiseAtExit ); atexit_registered = 1; } if ( tcgetattr( 0, &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, * no start/stop output control. */ raw.c_iflag &= ~( BRKINT | ICRNL | INPCK | ISTRIP | IXON ); /* output modes - disable post processing */ // this is wrong, we don't want raw output, it turns newlines into straight linefeeds //raw.c_oflag &= ~(OPOST); /* control modes - set 8 bit chars */ raw.c_cflag |= ( CS8 ); /* local modes - echoing off, canonical off, no extended functions, * no signal chars (^Z,^C) */ raw.c_lflag &= ~( ECHO | ICANON | IEXTEN | ISIG ); /* control chars - set return condition: min number of bytes and timer. * We want read to return every single byte, without timeout. */ raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ /* put terminal in raw mode after flushing */ if ( tcsetattr( 0, TCSADRAIN, &raw ) < 0 ) goto fatal; rawmode = 1; return 0; fatal: errno = ENOTTY; return -1; #endif } static void disableRawMode( void ) { #ifdef _WIN32 SetConsoleMode( console_in, oldMode ); console_in = 0; console_out = 0; #else if ( rawmode && tcsetattr ( 0, TCSADRAIN, &orig_termios ) != -1 ) rawmode = 0; #endif } // At exit we'll try to fix the terminal to the initial conditions static void linenoiseAtExit( void ) { disableRawMode(); } static int getColumns( void ) { int cols; #ifdef _WIN32 CONSOLE_SCREEN_BUFFER_INFO inf; GetConsoleScreenBufferInfo( console_out, &inf ); cols = inf.dwSize.X; #else struct winsize ws; cols = ( ioctl( 1, TIOCGWINSZ, &ws ) == -1 ) ? 80 : ws.ws_col; #endif // cols is 0 in certain circumstances like inside debugger, which creates further issues return (cols > 0) ? cols : 80; } static void setDisplayAttribute( bool enhancedDisplay ) { #ifdef _WIN32 if ( enhancedDisplay ) { CONSOLE_SCREEN_BUFFER_INFO inf; GetConsoleScreenBufferInfo( console_out, &inf ); oldDisplayAttribute = inf.wAttributes; BYTE oldLowByte = oldDisplayAttribute & 0xFF; BYTE newLowByte; switch ( oldLowByte ) { case 0x07: //newLowByte = FOREGROUND_BLUE | FOREGROUND_INTENSITY; // too dim //newLowByte = FOREGROUND_BLUE; // even dimmer newLowByte = FOREGROUND_BLUE | FOREGROUND_GREEN; // most similar to xterm appearance break; case 0x70: newLowByte = BACKGROUND_BLUE | BACKGROUND_INTENSITY; break; default: newLowByte = oldLowByte ^ 0xFF; // default to inverse video break; } inf.wAttributes = ( inf.wAttributes & 0xFF00 ) | newLowByte; SetConsoleTextAttribute( console_out, inf.wAttributes ); } else { SetConsoleTextAttribute( console_out, oldDisplayAttribute ); } #else if ( enhancedDisplay ) { if ( write( 1, "\x1b[1;34m", 7 ) == -1 ) return; /* bright blue (visible with both B&W bg) */ } else { if ( write( 1, "\x1b[0m", 4 ) == -1 ) return; /* reset */ } #endif } /** * Display the dynamic incremental search prompt and the current user input line. * @param pi PromptInfo struct holding information about the prompt and our screen position * @param buf input buffer to be displayed * @param len count of characters in the buffer * @param pos current cursor position within the buffer (0 <= pos <= len) */ //static void dynamicRefresh( DynamicPrompt& pi, char *buf, int len, int pos ) { static void dynamicRefresh( PromptBase& pi, char *buf, int len, int pos ) { // calculate the position of the end of the prompt int xEndOfPrompt, yEndOfPrompt; calculateScreenPosition( 0, 0, pi.promptScreenColumns, pi.promptChars, xEndOfPrompt, yEndOfPrompt ); pi.promptIndentation = xEndOfPrompt; // calculate the position of the end of the input line int xEndOfInput, yEndOfInput; calculateScreenPosition( xEndOfPrompt, yEndOfPrompt, pi.promptScreenColumns, len, xEndOfInput, yEndOfInput ); // calculate the desired position of the cursor int xCursorPos, yCursorPos; calculateScreenPosition( xEndOfPrompt, yEndOfPrompt, pi.promptScreenColumns, pos, xCursorPos, yCursorPos ); #ifdef _WIN32 // position at the start of the prompt, clear to end of previous input CONSOLE_SCREEN_BUFFER_INFO inf; GetConsoleScreenBufferInfo( console_out, &inf ); inf.dwCursorPosition.X = 0; inf.dwCursorPosition.Y -= pi.promptCursorRowOffset /*- pi.promptExtraLines*/; SetConsoleCursorPosition( console_out, inf.dwCursorPosition ); DWORD count; FillConsoleOutputCharacterA( console_out, ' ', pi.previousPromptLen + pi.promptPreviousInputLen, inf.dwCursorPosition, &count ); pi.previousPromptLen = pi.promptIndentation; pi.promptPreviousInputLen = len; // display the prompt if ( write( 1, pi.promptText, pi.promptChars ) == -1 ) return; // display the input line if ( write( 1, buf, len ) == -1 ) return; // position the cursor GetConsoleScreenBufferInfo( console_out, &inf ); inf.dwCursorPosition.X = xCursorPos; // 0-based on Win32 inf.dwCursorPosition.Y -= yEndOfInput - yCursorPos; SetConsoleCursorPosition( console_out, inf.dwCursorPosition ); #else // _WIN32 char seq[64]; int cursorRowMovement = pi.promptCursorRowOffset - pi.promptExtraLines; if ( cursorRowMovement > 0 ) { // move the cursor up as required snprintf( seq, sizeof seq, "\x1b[%dA", cursorRowMovement ); if ( write( 1, seq, strlen( seq ) ) == -1 ) return; } // position at the start of the prompt, clear to end of screen snprintf( seq, sizeof seq, "\x1b[1G\x1b[J" ); // 1-based on VT100 if ( write( 1, seq, strlen( seq ) ) == -1 ) return; // display the prompt if ( write( 1, pi.promptText, pi.promptChars ) == -1 ) return; // display the input line if ( write( 1, buf, len ) == -1 ) return; // we have to generate our own newline on line wrap if ( xEndOfInput == 0 && yEndOfInput > 0 ) if ( write( 1, "\n", 1 ) == -1 ) return; // position the cursor cursorRowMovement = yEndOfInput - yCursorPos; if ( cursorRowMovement > 0 ) { // move the cursor up as required snprintf( seq, sizeof seq, "\x1b[%dA", cursorRowMovement ); if ( write( 1, seq, strlen( seq ) ) == -1 ) return; } // position the cursor within the line snprintf( seq, sizeof seq, "\x1b[%dG", xCursorPos + 1 ); // 1-based on VT100 if ( write( 1, seq, strlen( seq ) ) == -1 ) return; #endif pi.promptCursorRowOffset = pi.promptExtraLines + yCursorPos; // remember row for next pass } /** * Refresh the user's input line: the prompt is already onscreen and is not redrawn here * @param pi PromptInfo struct holding information about the prompt and our screen position */ void InputBuffer::refreshLine( PromptBase& pi ) { // check for a matching brace/bracket/paren, remember its position if found int highlight = -1; if ( pos < len ) { /* this scans for a brace matching buf[pos] to highlight */ int scanDirection = 0; if ( strchr( "}])", buf[pos] ) ) scanDirection = -1; /* backwards */ else if ( strchr( "{[(", buf[pos] ) ) scanDirection = 1; /* forwards */ if ( scanDirection ) { int unmatched = scanDirection; for ( int i = pos + scanDirection; i >= 0 && i < len; i += scanDirection ) { /* TODO: the right thing when inside a string */ if ( strchr( "}])", buf[i] ) ) --unmatched; else if ( strchr( "{[(", buf[i] ) ) ++unmatched; if ( unmatched == 0 ) { highlight = i; break; } } } } // calculate the position of the end of the input line int xEndOfInput, yEndOfInput; calculateScreenPosition( pi.promptIndentation, 0, pi.promptScreenColumns, len, xEndOfInput, yEndOfInput ); // calculate the desired position of the cursor int xCursorPos, yCursorPos; calculateScreenPosition( pi.promptIndentation, 0, pi.promptScreenColumns, pos, xCursorPos, yCursorPos ); #ifdef _WIN32 // position at the end of the prompt, clear to end of previous input CONSOLE_SCREEN_BUFFER_INFO inf; GetConsoleScreenBufferInfo( console_out, &inf ); inf.dwCursorPosition.X = pi.promptIndentation; // 0-based on Win32 inf.dwCursorPosition.Y -= pi.promptCursorRowOffset - pi.promptExtraLines; SetConsoleCursorPosition( console_out, inf.dwCursorPosition ); DWORD count; if ( len < pi.promptPreviousInputLen ) FillConsoleOutputCharacterA( console_out, ' ', pi.promptPreviousInputLen, inf.dwCursorPosition, &count ); pi.promptPreviousInputLen = len; // display the input line if (highlight == -1) { if ( write( 1, buf, len ) == -1 ) return; } else { if (write( 1, buf, highlight ) == -1 ) return; setDisplayAttribute( true ); /* bright blue (visible with both B&W bg) */ if ( write( 1, &buf[highlight], 1 ) == -1 ) return; setDisplayAttribute( false ); if ( write( 1, buf + highlight + 1, len - highlight - 1 ) == -1 ) return; } // position the cursor GetConsoleScreenBufferInfo( console_out, &inf ); inf.dwCursorPosition.X = xCursorPos; // 0-based on Win32 inf.dwCursorPosition.Y -= yEndOfInput - yCursorPos; SetConsoleCursorPosition( console_out, inf.dwCursorPosition ); #else // _WIN32 char seq[64]; int cursorRowMovement = pi.promptCursorRowOffset - pi.promptExtraLines; if ( cursorRowMovement > 0 ) { // move the cursor up as required snprintf( seq, sizeof seq, "\x1b[%dA", cursorRowMovement ); if ( write( 1, seq, strlen( seq ) ) == -1 ) return; } // position at the end of the prompt, clear to end of screen snprintf( seq, sizeof seq, "\x1b[%dG\x1b[J", pi.promptIndentation + 1 ); // 1-based on VT100 if ( write( 1, seq, strlen( seq ) ) == -1 ) return; if ( highlight == -1 ) { // write unhighlighted text if ( write( 1, buf, len ) == -1 ) return; } else { // highlight the matching brace/bracket/parenthesis if ( write( 1, buf, highlight ) == -1 ) return; setDisplayAttribute( true ); if ( write( 1, &buf[highlight], 1 ) == -1 ) return; setDisplayAttribute( false ); if ( write( 1, buf + highlight + 1, len - highlight - 1 ) == -1 ) return; } // we have to generate our own newline on line wrap if ( xEndOfInput == 0 && yEndOfInput > 0 ) if ( write( 1, "\n", 1 ) == -1 ) return; // position the cursor cursorRowMovement = yEndOfInput - yCursorPos; if ( cursorRowMovement > 0 ) { // move the cursor up as required snprintf( seq, sizeof seq, "\x1b[%dA", cursorRowMovement ); if ( write( 1, seq, strlen( seq ) ) == -1 ) return; } // position the cursor within the line snprintf( seq, sizeof seq, "\x1b[%dG", xCursorPos + 1 ); // 1-based on VT100 if ( write( 1, seq, strlen( seq ) ) == -1 ) return; #endif pi.promptCursorRowOffset = pi.promptExtraLines + yCursorPos; // remember row for next pass } #ifndef _WIN32 namespace EscapeSequenceProcessing { // move these out of global namespace // This chunk of code does parsing of the escape sequences sent by various Linux terminals. // // It handles arrow keys, Home, End and Delete keys by interpreting the sequences sent by // gnome terminal, xterm, rxvt, konsole, aterm and yakuake including the Alt and Ctrl key // combinations that are understood by linenoise. // // The parsing uses tables, a bunch of intermediate dispatch routines and a doDispatch // loop that reads the tables and sends control to "deeper" routines to continue the // parsing. The starting call to doDispatch( c, initialDispatch ) will eventually return // either a character (with optional CTRL and META bits set), or -1 if parsing fails, or // zero if an attempt to read from the keyboard fails. // // This is rather sloppy escape sequence processing, since we're not paying attention to what the // actual TERM is set to and are processing all key sequences for all terminals, but it works with // the most common keystrokes on the most common terminals. It's intricate, but the nested 'if' // statements required to do it directly would be worse. This way has the advantage of allowing // changes and extensions without having to touch a lot of code. // // This is a typedef for the routine called by doDispatch(). It takes the current character // as input, does any required processing including reading more characters and calling other // dispatch routines, then eventually returns the final (possibly extended or special) character. // typedef unsigned int ( *CharacterDispatchRoutine )( unsigned int ); // This structure is used by doDispatch() to hold a list of characters to test for and // a list of routines to call if the character matches. The dispatch routine list is one // longer than the character list; the final entry is used if no character matches. // struct CharacterDispatch { unsigned int len; // length of the chars list const char* chars; // chars to test CharacterDispatchRoutine* dispatch; // array of routines to call }; // This dispatch routine is given a dispatch table and then farms work out to routines // listed in the table based on the character it is called with. The dispatch routines can // read more input characters to decide what should eventually be returned. Eventually, // a called routine returns either a character or -1 to indicate parsing failure. // static unsigned int doDispatch( unsigned int c, CharacterDispatch& dispatchTable ) { for ( unsigned int i = 0; i < dispatchTable.len ; ++i ) { if ( static_cast( dispatchTable.chars[i] ) == c ) { return dispatchTable.dispatch[i]( c ); } } return dispatchTable.dispatch[dispatchTable.len]( c ); } static unsigned int thisKeyMetaCtrl = 0; // holds pre-set Meta and/or Ctrl modifiers // Final dispatch routines -- return something // static unsigned int normalKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | c; } static unsigned int upArrowKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | UP_ARROW_KEY; } static unsigned int downArrowKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | DOWN_ARROW_KEY; } static unsigned int rightArrowKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | RIGHT_ARROW_KEY; } static unsigned int leftArrowKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | LEFT_ARROW_KEY; } static unsigned int homeKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | HOME_KEY; } static unsigned int endKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | END_KEY; } static unsigned int deleteCharRoutine( unsigned int c ) { return thisKeyMetaCtrl | ctrlChar( 'H' ); } // key labeled Backspace static unsigned int deleteKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | DELETE_KEY; } // key labeled Delete static unsigned int ctrlUpArrowKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | CTRL | UP_ARROW_KEY; } static unsigned int ctrlDownArrowKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | CTRL | DOWN_ARROW_KEY; } static unsigned int ctrlRightArrowKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | CTRL | RIGHT_ARROW_KEY; } static unsigned int ctrlLeftArrowKeyRoutine( unsigned int c ) { return thisKeyMetaCtrl | CTRL | LEFT_ARROW_KEY; } static unsigned int escFailureRoutine( unsigned int c ) { beep(); return -1; } // Handle ESC [ 1 ; 3 (or 5) escape sequences // static CharacterDispatchRoutine escLeftBracket1Semicolon3or5Routines[] = { upArrowKeyRoutine, downArrowKeyRoutine, rightArrowKeyRoutine, leftArrowKeyRoutine, escFailureRoutine }; static CharacterDispatch escLeftBracket1Semicolon3or5Dispatch = { 4, "ABCD", escLeftBracket1Semicolon3or5Routines }; // Handle ESC [ 1 ; escape sequences // static unsigned int escLeftBracket1Semicolon3Routine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; thisKeyMetaCtrl |= META; return doDispatch( c, escLeftBracket1Semicolon3or5Dispatch ); } static unsigned int escLeftBracket1Semicolon5Routine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; thisKeyMetaCtrl |= CTRL; return doDispatch( c, escLeftBracket1Semicolon3or5Dispatch ); } static CharacterDispatchRoutine escLeftBracket1SemicolonRoutines[] = { escLeftBracket1Semicolon3Routine, escLeftBracket1Semicolon5Routine, escFailureRoutine }; static CharacterDispatch escLeftBracket1SemicolonDispatch = { 2, "35", escLeftBracket1SemicolonRoutines }; // Handle ESC [ 1 escape sequences // static unsigned int escLeftBracket1SemicolonRoutine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escLeftBracket1SemicolonDispatch ); } static CharacterDispatchRoutine escLeftBracket1Routines[] = { homeKeyRoutine, escLeftBracket1SemicolonRoutine, escFailureRoutine }; static CharacterDispatch escLeftBracket1Dispatch = { 2, "~;", escLeftBracket1Routines }; // Handle ESC [ 3 escape sequences // static CharacterDispatchRoutine escLeftBracket3Routines[] = { deleteKeyRoutine, escFailureRoutine }; static CharacterDispatch escLeftBracket3Dispatch = { 1, "~", escLeftBracket3Routines }; // Handle ESC [ 4 escape sequences // static CharacterDispatchRoutine escLeftBracket4Routines[] = { endKeyRoutine, escFailureRoutine }; static CharacterDispatch escLeftBracket4Dispatch = { 1, "~", escLeftBracket4Routines }; // Handle ESC [ 7 escape sequences // static CharacterDispatchRoutine escLeftBracket7Routines[] = { homeKeyRoutine, escFailureRoutine }; static CharacterDispatch escLeftBracket7Dispatch = { 1, "~", escLeftBracket7Routines }; // Handle ESC [ 8 escape sequences // static CharacterDispatchRoutine escLeftBracket8Routines[] = { endKeyRoutine, escFailureRoutine }; static CharacterDispatch escLeftBracket8Dispatch = { 1, "~", escLeftBracket8Routines }; // Handle ESC [ escape sequences // static unsigned int escLeftBracket0Routine( unsigned int c ) { return escFailureRoutine( c ); } static unsigned int escLeftBracket1Routine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escLeftBracket1Dispatch ); } static unsigned int escLeftBracket2Routine( unsigned int c ) { return escFailureRoutine( c ); } static unsigned int escLeftBracket3Routine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escLeftBracket3Dispatch ); } static unsigned int escLeftBracket4Routine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escLeftBracket4Dispatch ); } static unsigned int escLeftBracket5Routine( unsigned int c ) { return escFailureRoutine( c ); } static unsigned int escLeftBracket6Routine( unsigned int c ) { return escFailureRoutine( c ); } static unsigned int escLeftBracket7Routine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escLeftBracket7Dispatch ); } static unsigned int escLeftBracket8Routine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escLeftBracket8Dispatch ); } static unsigned int escLeftBracket9Routine( unsigned int c ) { return escFailureRoutine( c ); } // Handle ESC [ escape sequences // static CharacterDispatchRoutine escLeftBracketRoutines[] = { upArrowKeyRoutine, downArrowKeyRoutine, rightArrowKeyRoutine, leftArrowKeyRoutine, homeKeyRoutine, endKeyRoutine, escLeftBracket0Routine, escLeftBracket1Routine, escLeftBracket2Routine, escLeftBracket3Routine, escLeftBracket4Routine, escLeftBracket5Routine, escLeftBracket6Routine, escLeftBracket7Routine, escLeftBracket8Routine, escLeftBracket9Routine, escFailureRoutine }; static CharacterDispatch escLeftBracketDispatch = { 16, "ABCDHF0123456789", escLeftBracketRoutines }; // Handle ESC O escape sequences // static CharacterDispatchRoutine escORoutines[] = { upArrowKeyRoutine, downArrowKeyRoutine, rightArrowKeyRoutine, leftArrowKeyRoutine, homeKeyRoutine, endKeyRoutine, ctrlUpArrowKeyRoutine, ctrlDownArrowKeyRoutine, ctrlRightArrowKeyRoutine, ctrlLeftArrowKeyRoutine, escFailureRoutine }; static CharacterDispatch escODispatch = { 10, "ABCDHFabcd", escORoutines }; // Initial ESC dispatch -- could be a Meta prefix or the start of an escape sequence // static unsigned int escLeftBracketRoutine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escLeftBracketDispatch ); } static unsigned int escORoutine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escODispatch ); } static unsigned int setMetaRoutine( unsigned int c ); // need forward reference static CharacterDispatchRoutine escRoutines[] = { escLeftBracketRoutine, escORoutine, setMetaRoutine }; static CharacterDispatch escDispatch = { 2, "[O", escRoutines }; // Initial dispatch -- we are not in the middle of anything yet // static unsigned int escRoutine( unsigned int c ) { if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escDispatch ); } static unsigned int hibitCRoutine( unsigned int c ) { // xterm sends a bizarre sequence for Alt combos: 'C'+0x80 then '!'+0x80 for Alt-a for example if ( read( 0, &c, 1 ) <= 0 ) return 0; if ( c >= ( ' ' | 0x80 ) && c <= ( '?' | 0x80 ) ) { if ( c == ( '?' | 0x80 ) ) { // have to special case this, normal code would send return META | ctrlChar( 'H' ); // Meta-_ (thank you xterm) } return META | ( c - 0x40 ); } return escFailureRoutine( c ); } static CharacterDispatchRoutine initialRoutines[] = { escRoutine, deleteCharRoutine, hibitCRoutine, normalKeyRoutine }; static CharacterDispatch initialDispatch = { 3, "\x1B\x7F\xC3", initialRoutines }; // Special handling for the ESC key because it does double duty // static unsigned int setMetaRoutine( unsigned int c ) { thisKeyMetaCtrl = META; if ( c == 0x1B ) { // another ESC, stay in ESC processing mode if ( read( 0, &c, 1 ) <= 0 ) return 0; return doDispatch( c, escDispatch ); } return doDispatch( c, initialDispatch ); } } // namespace EscapeSequenceProcessing // move these out of global namespace #endif // #ifndef _WIN32 // linenoiseReadChar -- read a keystroke or keychord from the keyboard, and translate it // into an encoded "keystroke". When convenient, extended keys are translated into their // simpler Emacs keystrokes, so an unmodified "left arrow" becomes Ctrl-B. // // A return value of zero means "no input available", and a return value of -1 means "invalid key". // static int linenoiseReadChar( void ){ #ifdef _WIN32 INPUT_RECORD rec; DWORD count; int modifierKeys = 0; bool escSeen = false; while ( true ) { ReadConsoleInputA( console_in, &rec, 1, &count ); if ( rec.EventType != KEY_EVENT || !rec.Event.KeyEvent.bKeyDown ) { continue; } modifierKeys = 0; if ( rec.Event.KeyEvent.dwControlKeyState & ( RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED ) ) { modifierKeys |= CTRL; } if ( rec.Event.KeyEvent.dwControlKeyState & ( RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED ) ) { modifierKeys |= META; } if ( escSeen ) { modifierKeys |= META; } if ( rec.Event.KeyEvent.uChar.AsciiChar == 0 ) { switch ( rec.Event.KeyEvent.wVirtualKeyCode ) { case VK_LEFT: return modifierKeys | LEFT_ARROW_KEY; case VK_RIGHT: return modifierKeys | RIGHT_ARROW_KEY; case VK_UP: return modifierKeys | UP_ARROW_KEY; case VK_DOWN: return modifierKeys | DOWN_ARROW_KEY; case VK_DELETE: return modifierKeys | DELETE_KEY; case VK_HOME: return modifierKeys | HOME_KEY; case VK_END: return modifierKeys | END_KEY; default: continue; // in raw mode, ReadConsoleInput shows shift, ctrl ... } // ... ignore them } else if ( rec.Event.KeyEvent.uChar.AsciiChar == ctrlChar( '[' ) ) { // ESC, set flag for later escSeen = true; continue; } else { // we got a real character, return it return modifierKeys | rec.Event.KeyEvent.uChar.AsciiChar; } } #else unsigned int c; unsigned char oneChar; int nread; nread = read( 0, &oneChar, 1 ); if ( nread <= 0 ) return 0; c = static_cast( oneChar ); // If _DEBUG_LINUX_KEYBOARD is set, then ctrl-\ puts us into a keyboard debugging mode // where we print out decimal and decoded values for whatever the "terminal" program // gives us on different keystrokes. Hit ctrl-C to exit this mode. // #define _DEBUG_LINUX_KEYBOARD #if defined(_DEBUG_LINUX_KEYBOARD) if ( c == 28 ) { // ctrl-\, special debug mode, prints all keys hit, ctrl-C to get out. printf( "\x1b[1G\n" ); /* go to first column of new line */ while ( true ) { unsigned char keys[10]; int ret = read( 0, keys, 10 ); if ( ret <= 0 ) { printf( "\nret: %d\n", ret ); } for ( int i = 0; i < ret; ++i ) { unsigned int key = static_cast( keys[i] ); char * friendlyTextPtr; char friendlyTextBuf[10]; const char * prefixText = (key < 0x80) ? "" : "highbit-"; unsigned int keyCopy = (key < 0x80) ? key : key - 0x80; if ( keyCopy >= '!' && keyCopy <= '~' ) { // printable friendlyTextBuf[0] = '\''; friendlyTextBuf[1] = keyCopy; friendlyTextBuf[2] = '\''; friendlyTextBuf[3] = 0; friendlyTextPtr = friendlyTextBuf; } else if ( keyCopy == ' ' ) { friendlyTextPtr = (char *)"space"; } else if (keyCopy == 27 ) { friendlyTextPtr = (char *)"ESC"; } else if (keyCopy == 0 ) { friendlyTextPtr = (char *)"NUL"; } else if (keyCopy == 127 ) { friendlyTextPtr = (char *)"DEL"; } else { friendlyTextBuf[0] = '^'; friendlyTextBuf[1] = keyCopy + 0x40; friendlyTextBuf[2] = 0; friendlyTextPtr = friendlyTextBuf; } printf( "%d (%s%s) ", key, prefixText, friendlyTextPtr ); } printf( "\x1b[1G\n" ); // go to first column of new line // drop out of this loop on ctrl-C if ( keys[0] == ctrlChar( 'C' ) ) { return -1; } } } #endif // _DEBUG_LINUX_KEYBOARD EscapeSequenceProcessing::thisKeyMetaCtrl = 0; // no modifiers yet at initialDispatch return EscapeSequenceProcessing::doDispatch( c, EscapeSequenceProcessing::initialDispatch ); #endif // #_WIN32 } static void freeCompletions( linenoiseCompletions* lc ) { for ( size_t i = 0; i < lc->len; ++i ) free( lc->cvec[i] ); if ( lc->cvec ) free( lc->cvec ); } // break characters that may precede items to be completed static const char breakChars[] = " =+-/\\*?\"'`&<>;|@{([])}"; /** * Handle command completion, using a completionCallback() routine to provide possible substitutions * This routine handles the mechanics of updating the user's input buffer with possible replacement of * text as the user selects a proposed completion string, or cancels the completion attempt. * @param pi PromptInfo struct holding information about the prompt and our screen position */ int InputBuffer::completeLine( PromptInfo& pi ) { linenoiseCompletions lc = { 0, NULL }; char c = 0; // completionCallback() expects a parsable entity, so find the previous break character and extract // a copy to parse. we also handle the case where tab is hit while not at end-of-line. int startIndex = pos; while ( --startIndex >= 0 ) { if ( strchr( breakChars, buf[startIndex] ) ) { break; } } ++startIndex; int itemLength = pos - startIndex; char* parseItem = reinterpret_cast( malloc( itemLength + 1 ) ); int i = 0; for ( ; i < itemLength; ++i ) { parseItem[i] = buf[startIndex+i]; } parseItem[i] = 0; // get a list of completions completionCallback( parseItem, &lc ); free( parseItem ); int displayLength = 0; char * displayText = 0; if ( lc.len == 0 ) { beep(); } else { size_t i = 0; size_t clen; bool stop = false; while ( ! stop ) { /* Show completion or original buffer */ if ( i < lc.len ) { clen = strlen( lc.cvec[i] ); displayLength = len + clen - itemLength; displayText = reinterpret_cast( malloc( displayLength + 1 ) ); InputBuffer temp( displayText, displayLength + 1 ); temp.len = displayLength; temp.pos = startIndex + clen; int j = 0; for ( ; j < startIndex; ++j ) displayText[j] = buf[j]; strcpy( &displayText[j], lc.cvec[i] ); strcpy( &displayText[j+clen], &buf[pos] ); displayText[displayLength] = 0; temp.refreshLine( pi ); free( displayText ); } else { refreshLine( pi ); } do { c = linenoiseReadChar(); } while ( c == static_cast( -1 ) ); switch ( c ) { case 0: freeCompletions( &lc ); return -1; case ctrlChar( 'I' ): // ctrl-I/tab i = ( i + 1 ) % ( lc.len + 1 ); if ( i == lc.len ) beep(); // beep after completing cycle break; #if 0 // SERVER-4011 -- Escape only works to end command completion in Windows // leaving code here for now in case this is where we will add Meta-R (revert-line) later case 27: /* escape */ /* Re-show original buffer */ if ( i < lc.len ) refreshLine( pi, buf, *len, *pos ); stop = true; break; #endif // SERVER-4011 -- Escape only works to end command completion in Windows default: /* Update buffer and return */ if ( i < lc.len ) { clen = strlen( lc.cvec[i] ); displayLength = len + clen - itemLength; displayText = (char *)malloc( displayLength + 1 ); int j = 0; for ( ; j < startIndex; ++j ) displayText[j] = buf[j]; strcpy( &displayText[j], lc.cvec[i] ); strcpy( &displayText[j+clen], &buf[pos] ); displayText[displayLength] = 0; strcpy( buf, displayText ); free( displayText ); pos = startIndex + clen; len = displayLength; } stop = true; break; } } } freeCompletions( &lc ); return c; /* Return last read character */ } /** * Clear the screen ONLY (no redisplay of anything) */ void linenoiseClearScreen( void ) { #ifdef _WIN32 COORD coord = {0, 0}; CONSOLE_SCREEN_BUFFER_INFO inf; HANDLE screenHandle = GetStdHandle( STD_OUTPUT_HANDLE ); GetConsoleScreenBufferInfo( screenHandle, &inf ); SetConsoleCursorPosition( screenHandle, coord ); DWORD count; FillConsoleOutputCharacterA( screenHandle, ' ', inf.dwSize.X * inf.dwSize.Y, coord, &count ); #else if ( write( 1, "\x1b[H\x1b[2J", 7 ) <= 0 ) return; #endif } void InputBuffer::clearScreen( PromptInfo& pi ) { linenoiseClearScreen(); if ( write( 1, pi.promptText, pi.promptChars ) == -1 ) return; #ifndef _WIN32 // we have to generate our own newline on line wrap on Linux if ( pi.promptIndentation == 0 && pi.promptExtraLines > 0 ) if ( write( 1, "\n", 1 ) == -1 ) return; #endif pi.promptCursorRowOffset = pi.promptExtraLines; refreshLine( pi ); } // convert {CTRL + 'A'}, {CTRL + 'a'} and {CTRL + ctrlChar( 'A' )} into ctrlChar( 'A' ) // leave META alone // static int cleanupCtrl( int c ) { if ( c & CTRL ) { int d = c & 0x1FF; if ( d >= 'a' && d <= 'z' ) { c = ( c + ( 'a' - ctrlChar( 'A' ) ) ) & ~CTRL; } if ( d >= 'A' && d <= 'Z' ) { c = ( c + ( 'A' - ctrlChar( 'A' ) ) ) & ~CTRL; } if ( d >= ctrlChar( 'A' ) && d <= ctrlChar( 'Z' ) ) { c = c & ~CTRL; } } return c; } /** * Incremental history search -- take over the prompt and keyboard as the user types a search string, * deletes characters from it, changes direction, and either accepts the found line (for execution or * editing) or cancels. * @param pi PromptInfo struct holding information about the (old, static) prompt and our screen position * @param startChar the character that began the search, used to set the initial direction */ int InputBuffer::incrementalHistorySearch( PromptInfo& pi, int startChar ) { // add the current line to the history list so we don't have to special case it history[historyLen - 1] = reinterpret_cast( realloc( history[historyLen - 1], len + 1 ) ); strcpy( history[historyLen - 1], buf ); int historyLineLength = len; int historyLinePosition = pos; char emptyBuffer[1]; InputBuffer empty( emptyBuffer, 1 ); empty.refreshLine( pi ); // erase the old input first DynamicPrompt dp( pi, ( startChar == ctrlChar( 'R' ) ) ? -1 : 1 ); dp.previousPromptLen = pi.previousPromptLen; dp.promptPreviousInputLen = pi.promptPreviousInputLen; dynamicRefresh( dp, history[historyLen - 1], historyLineLength, historyLinePosition ); // draw user's text with our prompt // loop until we get an exit character int c; bool keepLooping = true; bool useSearchedLine = true; bool searchAgain = false; while ( keepLooping ) { c = linenoiseReadChar(); c = cleanupCtrl( c ); // convert CTRL + into normal ctrl switch ( c ) { // these characters keep the selected text but do not execute it case ctrlChar( 'A' ): // ctrl-A, move cursor to start of line case HOME_KEY: case ctrlChar( 'B' ): // ctrl-B, move cursor left by one character case LEFT_ARROW_KEY: case META + 'b': // meta-B, move cursor left by one word case META + 'B': case CTRL + LEFT_ARROW_KEY: case META + LEFT_ARROW_KEY: // Emacs allows Meta, bash & readline don't case ctrlChar( 'D' ): case META + 'd': // meta-D, kill word to right of cursor case META + 'D': case ctrlChar( 'E' ): // ctrl-E, move cursor to end of line case END_KEY: case ctrlChar( 'F' ): // ctrl-F, move cursor right by one character case RIGHT_ARROW_KEY: case META + 'f': // meta-F, move cursor right by one word case META + 'F': case CTRL + RIGHT_ARROW_KEY: case META + RIGHT_ARROW_KEY: // Emacs allows Meta, bash & readline don't case META + ctrlChar( 'H' ): case ctrlChar( 'J' ): case ctrlChar( 'K' ): // ctrl-K, kill from cursor to end of line case ctrlChar( 'M' ): case ctrlChar( 'N' ): // ctrl-N, recall next line in history case ctrlChar( 'P' ): // ctrl-P, recall previous line in history case DOWN_ARROW_KEY: case UP_ARROW_KEY: case ctrlChar( 'T' ): // ctrl-T, transpose characters case ctrlChar( 'U' ): // ctrl-U, kill all characters to the left of the cursor case ctrlChar( 'W' ): case META + 'y': // meta-Y, "yank-pop", rotate popped text case META + 'Y': case 127: case DELETE_KEY: keepLooping = false; break; // these characters revert the input line to its previous state case ctrlChar( 'C' ): // ctrl-C, abort this line case ctrlChar( 'G' ): case ctrlChar( 'L' ): // ctrl-L, clear screen and redisplay line keepLooping = false; useSearchedLine = false; if ( c != ctrlChar( 'L' ) ) { c = -1; // ctrl-C and ctrl-G just abort the search and do nothing else } break; // these characters stay in search mode and update the display case ctrlChar( 'S' ): case ctrlChar( 'R' ): if ( dp.searchTextLen == 0 ) { // if no current search text, recall previous text dp.updateSearchText( previousSearchText.c_str() ); } if ( ( dp.direction == 1 && c == ctrlChar( 'R' ) ) || ( dp.direction == -1 && c == ctrlChar( 'S' ) ) ) { dp.direction = 0 - dp.direction; // reverse direction dp.updateSearchPrompt(); // change the prompt } else { searchAgain = true; // same direction, search again } break; // job control is its own thing #ifndef _WIN32 case ctrlChar( 'Z' ): // ctrl-Z, job control disableRawMode(); // Returning to Linux (whatever) shell, leave raw mode raise( SIGSTOP ); // Break out in mid-line enableRawMode(); // Back from Linux shell, re-enter raw mode dynamicRefresh( dp, history[historyIndex], historyLineLength, historyLinePosition ); continue; break; #endif // these characters update the search string, and hence the selected input line case ctrlChar( 'H' ): // backspace/ctrl-H, delete char to left of cursor if ( dp.searchTextLen > 0 ) { --dp.searchTextLen; dp.searchText[dp.searchTextLen] = 0; string newSearchText( dp.searchText ); dp.updateSearchText( newSearchText.c_str() ); } else { beep(); } break; case ctrlChar( 'Y' ): // ctrl-Y, yank killed text break; default: if ( c >= ' ' && c < 256 ) { // not an action character string newSearchText = string( dp.searchText ) + static_cast( c ); dp.updateSearchText( newSearchText.c_str() ); } else { beep(); } } // switch // if we are staying in search mode, search now if ( keepLooping ) { if ( dp.searchTextLen > 0 ) { bool found = false; int historySearchIndex = historyIndex; int lineLength = historyLineLength; int lineSearchPos = historyLinePosition; if ( searchAgain ) { lineSearchPos += dp.direction; } searchAgain = false; while ( true ) { while ( ( dp.direction > 0 ) ? ( lineSearchPos < lineLength ) : ( lineSearchPos >= 0 ) ) { if ( strncmp( dp.searchText, &history[historySearchIndex][lineSearchPos], dp.searchTextLen) == 0 ) { found = true; break; } lineSearchPos += dp.direction; } if ( found ) { historyIndex = historySearchIndex; historyLineLength = lineLength; historyLinePosition = lineSearchPos; break; } else if ( ( dp.direction > 0 ) ? ( historySearchIndex < historyLen - 1 ) : ( historySearchIndex > 0 ) ) { historySearchIndex += dp.direction; lineLength = strlen( history[historySearchIndex] ); lineSearchPos = ( dp.direction > 0 ) ? 0 : ( lineLength - dp.searchTextLen ); } else { beep(); break; } }; // while } dynamicRefresh( dp, history[historyIndex], historyLineLength, historyLinePosition ); // draw user's text with our prompt } } // while // leaving history search, restore previous prompt, maybe make searched line current PromptBase pb; pb.promptText = &pi.promptText[pi.promptLastLinePosition]; pb.promptChars = pi.promptIndentation; pb.promptExtraLines = 0; pb.promptIndentation = pi.promptIndentation; pb.promptLastLinePosition = 0; pb.promptPreviousInputLen = historyLineLength; pb.promptCursorRowOffset = dp.promptCursorRowOffset; pb.promptScreenColumns = pi.promptScreenColumns; pb.previousPromptLen = dp.promptChars; if ( useSearchedLine ) { strcpy( buf, history[historyIndex] ); len = historyLineLength; pos = historyLinePosition; } dynamicRefresh( pb, buf, len, pos ); // redraw the original prompt with current input pi.promptPreviousInputLen = len; pi.promptCursorRowOffset = pi.promptExtraLines + pb.promptCursorRowOffset; previousSearchText = dp.searchText; // save search text for possible reuse on ctrl-R ctrl-R return c; // pass a character or -1 back to main loop } int InputBuffer::getInputLine( PromptInfo& pi ) { // The latest history entry is always our current buffer linenoiseHistoryAdd( "" ); historyIndex = historyLen - 1; // display the prompt if ( write( 1, pi.promptText, pi.promptChars ) == -1 ) return -1; #ifndef _WIN32 // we have to generate our own newline on line wrap on Linux if ( pi.promptIndentation == 0 && pi.promptExtraLines > 0 ) if ( write( 1, "\n", 1 ) == -1 ) return -1; #endif // the cursor starts out at the end of the prompt pi.promptCursorRowOffset = pi.promptExtraLines; // kill and yank start in "other" mode killRing.lastAction = KillRing::actionOther; // when history search returns control to us, we execute its terminating keystroke int terminatingKeystroke = -1; // loop collecting characters, responding to ctrl characters while ( true ) { int c; if ( terminatingKeystroke == -1 ) { c = linenoiseReadChar(); // get a new keystroke } else { c = terminatingKeystroke; // use the terminating keystroke from search terminatingKeystroke = -1; // clear it once we've used it } c = cleanupCtrl( c ); // convert CTRL + into normal ctrl if ( c == 0 ) return len; if ( c == -1 ) { refreshLine( pi ); continue; } // ctrl-I/tab, command completion, needs to be before switch statement if ( c == ctrlChar( 'I' ) && completionCallback ) { killRing.lastAction = KillRing::actionOther; // completeLine does the actual completion and replacement c = completeLine( pi ); if ( c < 0 ) // return on error return len; if ( c == 0 ) // read next character when 0 continue; // deliberate fall-through here, so we use the terminating character } switch ( c ) { case ctrlChar( 'A' ): // ctrl-A, move cursor to start of line case HOME_KEY: killRing.lastAction = KillRing::actionOther; pos = 0; refreshLine( pi ); break; case ctrlChar( 'B' ): // ctrl-B, move cursor left by one character case LEFT_ARROW_KEY: killRing.lastAction = KillRing::actionOther; if ( pos > 0 ) { --pos; refreshLine( pi ); } break; case META + 'b': // meta-B, move cursor left by one word case META + 'B': case CTRL + LEFT_ARROW_KEY: case META + LEFT_ARROW_KEY: // Emacs allows Meta, bash & readline don't killRing.lastAction = KillRing::actionOther; if ( pos > 0 ) { while ( pos > 0 && !isalnum( buf[pos - 1] ) ) { --pos; } while ( pos > 0 && isalnum( buf[pos - 1] ) ) { --pos; } refreshLine( pi ); } break; case ctrlChar( 'C' ): // ctrl-C, abort this line killRing.lastAction = KillRing::actionOther; errno = EAGAIN; --historyLen; free( history[historyLen] ); // we need one last refresh with the cursor at the end of the line // so we don't display the next prompt over the previous input line pos = len; // pass len as pos for EOL refreshLine( pi ); if ( write( 1, "^C", 2 ) == -1 ) return -1; // Display the ^C we got return -1; case META + 'c': // meta-C, give word initial Cap case META + 'C': killRing.lastAction = KillRing::actionOther; if ( pos < len ) { while ( pos < len && !isalnum( buf[pos] ) ) { ++pos; } if ( pos < len && isalnum( buf[pos] ) ) { if ( buf[pos] >= 'a' && buf[pos] <= 'z' ) { buf[pos] += 'A' - 'a'; } ++pos; } while ( pos < len && isalnum( buf[pos] ) ) { if ( buf[pos] >= 'A' && buf[pos] <= 'Z' ) { buf[pos] += 'a' - 'A'; } ++pos; } refreshLine( pi ); } break; // ctrl-D, delete the character under the cursor // on an empty line, exit the shell case ctrlChar( 'D' ): killRing.lastAction = KillRing::actionOther; if ( len > 0 && pos < len ) { memmove( buf + pos, buf + pos + 1, len - pos ); --len; refreshLine( pi ); } else if ( len == 0 ) { --historyLen; free( history[historyLen] ); return -1; } break; case META + 'd': // meta-D, kill word to right of cursor case META + 'D': if ( pos < len ) { int endingPos = pos; while ( endingPos < len && !isalnum( buf[endingPos] ) ) { ++endingPos; } while ( endingPos < len && isalnum( buf[endingPos] ) ) { ++endingPos; } killRing.kill( &buf[pos], endingPos - pos, true ); memmove( buf + pos, buf + endingPos, len - endingPos + 1 ); len -= endingPos - pos; refreshLine( pi ); } killRing.lastAction = KillRing::actionKill; break; case ctrlChar( 'E' ): // ctrl-E, move cursor to end of line case END_KEY: killRing.lastAction = KillRing::actionOther; pos = len; refreshLine( pi ); break; case ctrlChar( 'F' ): // ctrl-F, move cursor right by one character case RIGHT_ARROW_KEY: killRing.lastAction = KillRing::actionOther; if ( pos < len ) { ++pos; refreshLine( pi ); } break; case META + 'f': // meta-F, move cursor right by one word case META + 'F': case CTRL + RIGHT_ARROW_KEY: case META + RIGHT_ARROW_KEY: // Emacs allows Meta, bash & readline don't killRing.lastAction = KillRing::actionOther; if ( pos < len ) { while ( pos < len && !isalnum( buf[pos] ) ) { ++pos; } while ( pos < len && isalnum( buf[pos] ) ) { ++pos; } refreshLine( pi ); } break; case ctrlChar( 'H' ): // backspace/ctrl-H, delete char to left of cursor killRing.lastAction = KillRing::actionOther; if ( pos > 0 ) { memmove( buf + pos - 1, buf + pos, 1 + len - pos ); --pos; --len; refreshLine( pi ); } break; // meta-Backspace, kill word to left of cursor case META + ctrlChar( 'H' ): if ( pos > 0 ) { int startingPos = pos; while ( pos > 0 && !isalnum( buf[pos - 1] ) ) { --pos; } while ( pos > 0 && isalnum( buf[pos - 1] ) ) { --pos; } killRing.kill( &buf[pos], startingPos - pos, false ); memmove( buf + pos, buf + startingPos, len - startingPos + 1 ); len -= startingPos - pos; refreshLine( pi ); } killRing.lastAction = KillRing::actionKill; break; case ctrlChar( 'J' ): // ctrl-J/linefeed/newline, accept line case ctrlChar( 'M' ): // ctrl-M/return/enter killRing.lastAction = KillRing::actionOther; // we need one last refresh with the cursor at the end of the line // so we don't display the next prompt over the previous input line pos = len; // pass len as pos for EOL refreshLine( pi ); --historyLen; free( history[historyLen] ); return len; case ctrlChar( 'K' ): // ctrl-K, kill from cursor to end of line killRing.kill( &buf[pos], len - pos, true ); buf[pos] = '\0'; len = pos; refreshLine( pi ); killRing.lastAction = KillRing::actionKill; break; case ctrlChar( 'L' ): // ctrl-L, clear screen and redisplay line clearScreen( pi ); break; case META + 'l': // meta-L, lowercase word case META + 'L': killRing.lastAction = KillRing::actionOther; if ( pos < len ) { while ( pos < len && !isalnum( buf[pos] ) ) { ++pos; } while ( pos < len && isalnum( buf[pos] ) ) { if ( buf[pos] >= 'A' && buf[pos] <= 'Z' ) { buf[pos] += 'a' - 'A'; } ++pos; } refreshLine( pi ); } break; case ctrlChar( 'N' ): // ctrl-N, recall next line in history case ctrlChar( 'P' ): // ctrl-P, recall previous line in history case DOWN_ARROW_KEY: case UP_ARROW_KEY: killRing.lastAction = KillRing::actionOther; if ( historyLen > 1 ) { /* Update the current history entry before we * overwrite it with the next one. */ free( history[historyIndex] ); history[historyIndex] = strdup (buf ); /* Show the new entry */ if ( c == UP_ARROW_KEY ) { c = ctrlChar( 'P' ); } historyIndex += ( c == ctrlChar( 'P' ) ) ? -1 : 1; if ( historyIndex < 0 ) { historyIndex = 0; break; } else if ( historyIndex >= historyLen ) { historyIndex = historyLen - 1; break; } strncpy( buf, history[historyIndex], buflen ); buf[buflen] = '\0'; len = pos = strlen( buf ); // place cursor at end of line refreshLine( pi ); } break; case ctrlChar( 'R' ): // ctrl-R, reverse history search case ctrlChar( 'S' ): // ctrl-S, forward history search terminatingKeystroke = incrementalHistorySearch( pi, c ); break; case ctrlChar( 'T' ): // ctrl-T, transpose characters killRing.lastAction = KillRing::actionOther; if ( pos > 0 && len > 1 ) { size_t leftCharPos = ( pos == len ) ? pos - 2 : pos - 1; char aux = buf[leftCharPos]; buf[leftCharPos] = buf[leftCharPos+1]; buf[leftCharPos+1] = aux; if ( pos != len ) ++pos; refreshLine( pi ); } break; case ctrlChar( 'U' ): // ctrl-U, kill all characters to the left of the cursor if ( pos > 0 ) { killRing.kill( &buf[0], pos, false ); len -= pos; memmove( buf, buf + pos, len + 1 ); pos = 0; refreshLine( pi ); } killRing.lastAction = KillRing::actionKill; break; case META + 'u': // meta-U, uppercase word case META + 'U': killRing.lastAction = KillRing::actionOther; if ( pos < len ) { while ( pos < len && !isalnum( buf[pos] ) ) { ++pos; } while ( pos < len && isalnum( buf[pos] ) ) { if ( buf[pos] >= 'a' && buf[pos] <= 'z' ) { buf[pos] += 'A' - 'a'; } ++pos; } refreshLine( pi ); } break; // ctrl-W, kill to whitespace (not word) to left of cursor case ctrlChar( 'W' ): if ( pos > 0 ) { int startingPos = pos; while ( pos > 0 && buf[pos - 1] == ' ' ) { --pos; } while ( pos > 0 && buf[pos - 1] != ' ' ) { --pos; } killRing.kill( &buf[pos], startingPos - pos, false ); memmove( buf + pos, buf + startingPos, len - startingPos + 1 ); len -= startingPos - pos; refreshLine( pi ); } killRing.lastAction = KillRing::actionKill; break; case ctrlChar( 'Y' ): // ctrl-Y, yank killed text { string* restoredText = killRing.yank(); if ( restoredText ) { int restoredTextLen = restoredText->length(); memmove( buf + pos + restoredTextLen, buf + pos, len - pos + 1 ); memmove( buf + pos, restoredText->c_str(), restoredTextLen ); pos += restoredTextLen; len += restoredTextLen; refreshLine( pi ); killRing.lastAction = KillRing::actionYank; killRing.lastYankSize = restoredTextLen; } else { beep(); } } break; case META + 'y': // meta-Y, "yank-pop", rotate popped text case META + 'Y': if ( killRing.lastAction == KillRing::actionYank ) { string* restoredText = killRing.yankPop(); if ( restoredText ) { int restoredTextLen = restoredText->length(); if ( restoredTextLen > killRing.lastYankSize ) { memmove( buf + pos + restoredTextLen - killRing.lastYankSize, buf + pos, len - pos + 1 ); memmove( buf + pos - killRing.lastYankSize, restoredText->c_str(), restoredTextLen ); } else { memmove( buf + pos - killRing.lastYankSize, restoredText->c_str(), restoredTextLen ); memmove( buf + pos + restoredTextLen - killRing.lastYankSize, buf + pos, len - pos + 1 ); } pos += restoredTextLen - killRing.lastYankSize; len += restoredTextLen - killRing.lastYankSize; killRing.lastYankSize = restoredTextLen; refreshLine( pi ); break; } } beep(); break; #ifndef _WIN32 case ctrlChar( 'Z' ): // ctrl-Z, job control disableRawMode(); // Returning to Linux (whatever) shell, leave raw mode raise( SIGSTOP ); // Break out in mid-line enableRawMode(); // Back from Linux shell, re-enter raw mode if ( write( 1, pi.promptText, pi.promptChars ) == -1 ) break; // Redraw prompt refreshLine( pi ); // Refresh the line break; #endif // DEL, delete the character under the cursor case 127: case DELETE_KEY: killRing.lastAction = KillRing::actionOther; if ( len > 0 && pos < len ) { memmove( buf + pos, buf + pos + 1, len - pos ); --len; refreshLine( pi ); } break; // not one of our special characters, maybe insert it in the buffer default: killRing.lastAction = KillRing::actionOther; if ( c > 0xFF ) { // beep on unknown Ctrl and/or Meta keys beep(); break; } if ( len < buflen ) { if ( static_cast( c ) < 32 ) { // don't insert control characters beep(); break; } if ( len == pos ) { // at end of buffer buf[pos] = c; ++pos; ++len; buf[len] = '\0'; if ( pi.promptIndentation + len < pi.promptScreenColumns ) { if ( len > pi.promptPreviousInputLen ) pi.promptPreviousInputLen = len; /* Avoid a full update of the line in the * trivial case. */ if ( write( 1, &c, 1) == -1 ) return -1; } else { refreshLine( pi ); } } else { // not at end of buffer, have to move characters to our right memmove( buf + pos + 1, buf + pos, len - pos ); buf[pos] = c; ++len; ++pos; buf[len] = '\0'; refreshLine( pi ); } } break; } } return len; } /** * linenoise is a readline replacement. * * call it with a prompt to display and it will return a line of input from the user * * @param prompt text of prompt to display to the user * @return the returned string belongs to the caller on return and must be freed to prevent memory leaks */ char* linenoise( const char* prompt ) { char buf[LINENOISE_MAX_LINE]; // buffer for user's input int count; if ( isatty( STDIN_FILENO ) ) { // input is from a terminal if ( enableRawMode() == -1 ) return NULL; PromptInfo pi( prompt, getColumns() ); // struct to hold edited copy of prompt & misc prompt info InputBuffer ib( buf, LINENOISE_MAX_LINE ); count = ib.getInputLine( pi ); disableRawMode(); printf( "\n" ); if ( count == -1 ) return NULL; } else { // input not from a terminal, we should work with piped input, i.e. redirected stdin if ( fgets( buf, sizeof buf, stdin ) == NULL ) return NULL; // if fgets() gave us the newline, remove it int count = strlen( buf ); if ( count && buf[count-1] == '\n' ) { --count; buf[count] = '\0'; } } return strdup( buf ); // caller must free buffer } /* Register a callback function to be called for tab-completion. */ void linenoiseSetCompletionCallback( linenoiseCompletionCallback* fn ) { completionCallback = fn; } void linenoiseAddCompletion( linenoiseCompletions* lc, const char* str ) { size_t len = strlen( str ); char* copy = reinterpret_cast( malloc( len + 1 ) ); memcpy( copy, str, len + 1 ); lc->cvec = reinterpret_cast( realloc( lc->cvec, sizeof( char* ) * ( lc->len + 1 ) ) ); lc->cvec[lc->len++] = copy; } int linenoiseHistoryAdd( const char* line ) { if ( historyMaxLen == 0 ) return 0; if ( history == NULL ) { history = reinterpret_cast( malloc( sizeof( char* ) * historyMaxLen ) ); if (history == NULL) return 0; memset( history, 0, ( sizeof(char*) * historyMaxLen ) ); } char* linecopy = strdup( line ); if ( ! linecopy ) return 0; if ( historyLen == historyMaxLen ) { free( history[0] ); memmove( history, history + 1, sizeof(char*) * ( historyMaxLen - 1 ) ); --historyLen; } // convert newlines in multi-line code to spaces before storing char *p = linecopy; while ( *p ) { if ( *p == '\n' ) *p = ' '; ++p; } history[historyLen] = linecopy; ++historyLen; return 1; } int linenoiseHistorySetMaxLen( int len ) { if ( len < 1 ) return 0; if ( history ) { int tocopy = historyLen; char** newHistory = reinterpret_cast( malloc( sizeof(char*) * len ) ); if ( newHistory == NULL ) return 0; if ( len < tocopy ) tocopy = len; memcpy( newHistory, history + historyMaxLen - tocopy, sizeof(char*) * tocopy ); free( history ); history = newHistory; } historyMaxLen = len; if ( historyLen > historyMaxLen ) historyLen = historyMaxLen; return 1; } /* Save the history in the specified file. On success 0 is returned * otherwise -1 is returned. */ int linenoiseHistorySave( const char* filename ) { FILE* fp = fopen( filename, "wt" ); if ( fp == NULL ) return -1; for ( int j = 0; j < historyLen; ++j ) { if ( history[j][0] != '\0' ) fprintf ( fp, "%s\n", history[j] ); } fclose( fp ); return 0; } /* Load the history from the specified file. If the file does not exist * zero is returned and no operation is performed. * * If the file exists and the operation succeeded 0 is returned, otherwise * on error -1 is returned. */ int linenoiseHistoryLoad( const char* filename ) { FILE *fp = fopen( filename, "rt" ); if ( fp == NULL ) return -1; char buf[LINENOISE_MAX_LINE]; while ( fgets( buf, LINENOISE_MAX_LINE, fp ) != NULL ) { char* p = strchr( buf, '\r' ); if ( ! p ) p = strchr( buf, '\n' ); if ( p ) *p = '\0'; if ( p != buf ) linenoiseHistoryAdd( buf ); } fclose( fp ); return 0; }