#include #include #include #include #include #include #ifdef _WIN32 #include #include #include #define isatty _isatty #define strcasecmp _stricmp #define strdup _strdup #define write _write #define STDIN_FILENO 0 #ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING static DWORD const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4; #endif #include "windows.hxx" #else /* _WIN32 */ #include #include #include #include #include #endif /* _WIN32 */ #include "terminal.hxx" #include "conversion.hxx" #include "escape.hxx" #include "replxx.hxx" #include "util.hxx" using namespace std; namespace replxx { namespace tty { bool is_a_tty( int fd_ ) { bool aTTY( isatty( fd_ ) != 0 ); #ifdef _WIN32 do { if ( aTTY ) { break; } HANDLE h( (HANDLE)_get_osfhandle( fd_ ) ); if ( h == INVALID_HANDLE_VALUE ) { break; } DWORD st( 0 ); if ( ! GetConsoleMode( h, &st ) ) { break; } aTTY = true; } while ( false ); #endif return ( aTTY ); } bool in( is_a_tty( 0 ) ); bool out( is_a_tty( 1 ) ); } #ifndef _WIN32 Terminal* _terminal_ = nullptr; static void WindowSizeChanged( int ) { if ( ! _terminal_ ) { return; } _terminal_->notify_event( Terminal::EVENT_TYPE::RESIZE ); } #endif Terminal::Terminal( void ) #ifdef _WIN32 : _consoleOut( INVALID_HANDLE_VALUE ) , _consoleIn( INVALID_HANDLE_VALUE ) , _origOutMode() , _origInMode() , _oldDisplayAttribute() , _inputCodePage( GetConsoleCP() ) , _outputCodePage( GetConsoleOutputCP() ) , _interrupt( INVALID_HANDLE_VALUE ) , _events() , _empty() #else : _origTermios() , _interrupt() #endif , _rawMode( false ) , _utf8() { #ifdef _WIN32 _interrupt = CreateEvent( nullptr, true, false, TEXT( "replxx_interrupt_event" ) ); #else static_cast( ::pipe( _interrupt ) == 0 ); #endif } Terminal::~Terminal( void ) { if ( _rawMode ) { disable_raw_mode(); } #ifdef _WIN32 CloseHandle( _interrupt ); #else static_cast( ::close( _interrupt[0] ) == 0 ); static_cast( ::close( _interrupt[1] ) == 0 ); #endif } void Terminal::write32( char32_t const* text32, int len32 ) { _utf8.assign( text32, len32 ); write8( _utf8.get(), _utf8.size() ); return; } void Terminal::write8( char const* data_, int size_ ) { #ifdef _WIN32 if ( ! _rawMode ) { enable_out(); } int nWritten( win_write( _consoleOut, _autoEscape, data_, size_ ) ); if ( ! _rawMode ) { disable_out(); } #else int nWritten( write( 1, data_, size_ ) ); #endif if ( nWritten != size_ ) { throw std::runtime_error( "write failed" ); } return; } int Terminal::get_screen_columns( void ) { int cols( 0 ); #ifdef _WIN32 CONSOLE_SCREEN_BUFFER_INFO inf; GetConsoleScreenBufferInfo( _consoleOut, &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; } int Terminal::get_screen_rows( void ) { int rows; #ifdef _WIN32 CONSOLE_SCREEN_BUFFER_INFO inf; GetConsoleScreenBufferInfo( _consoleOut, &inf ); rows = 1 + inf.srWindow.Bottom - inf.srWindow.Top; #else struct winsize ws; rows = (ioctl(1, TIOCGWINSZ, &ws) == -1) ? 24 : ws.ws_row; #endif return (rows > 0) ? rows : 24; } namespace { inline int notty( void ) { errno = ENOTTY; return ( -1 ); } } void Terminal::enable_out( void ) { #ifdef _WIN32 _consoleOut = GetStdHandle( STD_OUTPUT_HANDLE ); SetConsoleOutputCP( 65001 ); GetConsoleMode( _consoleOut, &_origOutMode ); _autoEscape = SetConsoleMode( _consoleOut, _origOutMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING ) != 0; #endif } void Terminal::disable_out( void ) { #ifdef _WIN32 SetConsoleMode( _consoleOut, _origOutMode ); SetConsoleOutputCP( _outputCodePage ); _consoleOut = INVALID_HANDLE_VALUE; _autoEscape = false; #endif } void Terminal::enable_bracketed_paste( void ) { static char const BRACK_PASTE_INIT[] = "\033[?2004h"; write8( BRACK_PASTE_INIT, sizeof ( BRACK_PASTE_INIT ) - 1 ); } void Terminal::disable_bracketed_paste( void ) { static char const BRACK_PASTE_DISABLE[] = "\033[?2004l"; write8( BRACK_PASTE_DISABLE, sizeof ( BRACK_PASTE_DISABLE ) - 1 ); } int Terminal::enable_raw_mode( void ) { if ( ! _rawMode ) { #ifdef _WIN32 _consoleIn = GetStdHandle( STD_INPUT_HANDLE ); SetConsoleCP( 65001 ); GetConsoleMode( _consoleIn, &_origInMode ); SetConsoleMode( _consoleIn, _origInMode & ~( ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT ) ); enable_out(); #else struct termios raw; if ( ! tty::in ) { return ( notty() ); } if ( tcgetattr( 0, &_origTermios ) == -1 ) { return ( notty() ); } raw = _origTermios; /* 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 ) { return ( notty() ); } _terminal_ = this; #endif _rawMode = true; } return ( 0 ); } void Terminal::disable_raw_mode(void) { if ( _rawMode ) { #ifdef _WIN32 disable_out(); SetConsoleMode( _consoleIn, _origInMode ); SetConsoleCP( _inputCodePage ); _consoleIn = INVALID_HANDLE_VALUE; #else _terminal_ = nullptr; if ( tcsetattr( 0, TCSADRAIN, &_origTermios ) == -1 ) { return; } #endif _rawMode = false; } } #ifndef _WIN32 /** * Read a UTF-8 sequence from the non-Windows keyboard and return the Unicode * (char32_t) character it encodes * * @return char32_t Unicode character */ char32_t read_unicode_character(void) { static char8_t utf8String[5]; static size_t utf8Count = 0; while (true) { char8_t c; /* Continue reading if interrupted by signal. */ ssize_t nread; do { nread = read( STDIN_FILENO, &c, 1 ); } while ((nread == -1) && (errno == EINTR)); if (nread <= 0) return 0; if (c <= 0x7F || locale::is8BitEncoding) { // short circuit ASCII utf8Count = 0; return c; } else if (utf8Count < sizeof(utf8String) - 1) { utf8String[utf8Count++] = c; utf8String[utf8Count] = 0; char32_t unicodeChar[2]; int ucharCount( 0 ); ConversionResult res = copyString8to32(unicodeChar, 2, ucharCount, utf8String); if (res == conversionOK && ucharCount) { utf8Count = 0; return unicodeChar[0]; } } else { utf8Count = 0; // this shouldn't happen: got four bytes but no UTF-8 character } } } #endif // #ifndef _WIN32 void beep() { fprintf(stderr, "\x7"); // ctrl-G == bell/beep fflush(stderr); } // replxx_read_char -- 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". // char32_t Terminal::read_char( void ) { char32_t c( 0 ); #ifdef _WIN32 INPUT_RECORD rec; DWORD count; char32_t modifierKeys = 0; bool escSeen = false; int highSurrogate( 0 ); while (true) { ReadConsoleInputW( _consoleIn, &rec, 1, &count ); #if __REPLXX_DEBUG__ // helper for debugging keystrokes, display info in the debug "Output" // window in the debugger { if ( rec.EventType == KEY_EVENT ) { //if ( rec.Event.KeyEvent.uChar.UnicodeChar ) { char buf[1024]; sprintf( buf, "Unicode character 0x%04X, repeat count %d, virtual keycode 0x%04X, " "virtual scancode 0x%04X, key %s%s%s%s%s\n", rec.Event.KeyEvent.uChar.UnicodeChar, rec.Event.KeyEvent.wRepeatCount, rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.wVirtualScanCode, rec.Event.KeyEvent.bKeyDown ? "down" : "up", (rec.Event.KeyEvent.dwControlKeyState & LEFT_CTRL_PRESSED) ? " L-Ctrl" : "", (rec.Event.KeyEvent.dwControlKeyState & RIGHT_CTRL_PRESSED) ? " R-Ctrl" : "", (rec.Event.KeyEvent.dwControlKeyState & LEFT_ALT_PRESSED) ? " L-Alt" : "", (rec.Event.KeyEvent.dwControlKeyState & RIGHT_ALT_PRESSED) ? " R-Alt" : "" ); OutputDebugStringA( buf ); //} } } #endif if ( rec.EventType != KEY_EVENT ) { continue; } // Windows provides for entry of characters that are not on your keyboard by sending the // Unicode characters as a "key up" with virtual keycode 0x12 (VK_MENU == Alt key) ... // accept these characters, otherwise only process characters on "key down" if ( !rec.Event.KeyEvent.bKeyDown && ( rec.Event.KeyEvent.wVirtualKeyCode != VK_MENU ) ) { continue; } modifierKeys = 0; // AltGr is encoded as ( LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED ), so don't treat this // combination as either CTRL or META we just turn off those two bits, so it is still // possible to combine CTRL and/or META with an AltGr key by using right-Ctrl and/or // left-Alt DWORD const AltGr( LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED ); if ( ( rec.Event.KeyEvent.dwControlKeyState & AltGr ) == AltGr ) { rec.Event.KeyEvent.dwControlKeyState &= ~( LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED ); } if ( rec.Event.KeyEvent.dwControlKeyState & ( RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED ) ) { modifierKeys |= Replxx::KEY::BASE_CONTROL; } if ( rec.Event.KeyEvent.dwControlKeyState & ( RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED ) ) { modifierKeys |= Replxx::KEY::BASE_META; } if ( escSeen ) { modifierKeys |= Replxx::KEY::BASE_META; } int key( rec.Event.KeyEvent.uChar.UnicodeChar ); if ( key == 0 ) { switch (rec.Event.KeyEvent.wVirtualKeyCode) { case VK_LEFT: return modifierKeys | Replxx::KEY::LEFT; case VK_RIGHT: return modifierKeys | Replxx::KEY::RIGHT; case VK_UP: return modifierKeys | Replxx::KEY::UP; case VK_DOWN: return modifierKeys | Replxx::KEY::DOWN; case VK_DELETE: return modifierKeys | Replxx::KEY::DELETE; case VK_HOME: return modifierKeys | Replxx::KEY::HOME; case VK_END: return modifierKeys | Replxx::KEY::END; case VK_PRIOR: return modifierKeys | Replxx::KEY::PAGE_UP; case VK_NEXT: return modifierKeys | Replxx::KEY::PAGE_DOWN; case VK_F1: return modifierKeys | Replxx::KEY::F1; case VK_F2: return modifierKeys | Replxx::KEY::F2; case VK_F3: return modifierKeys | Replxx::KEY::F3; case VK_F4: return modifierKeys | Replxx::KEY::F4; case VK_F5: return modifierKeys | Replxx::KEY::F5; case VK_F6: return modifierKeys | Replxx::KEY::F6; case VK_F7: return modifierKeys | Replxx::KEY::F7; case VK_F8: return modifierKeys | Replxx::KEY::F8; case VK_F9: return modifierKeys | Replxx::KEY::F9; case VK_F10: return modifierKeys | Replxx::KEY::F10; case VK_F11: return modifierKeys | Replxx::KEY::F11; case VK_F12: return modifierKeys | Replxx::KEY::F12; default: continue; // in raw mode, ReadConsoleInput shows shift, ctrl - ignore them } } else if ( key == Replxx::KEY::ESCAPE ) { // ESC, set flag for later escSeen = true; continue; } else if ( ( key >= 0xD800 ) && ( key <= 0xDBFF ) ) { highSurrogate = key - 0xD800; continue; } else { // we got a real character, return it if ( ( key >= 0xDC00 ) && ( key <= 0xDFFF ) ) { key -= 0xDC00; key |= ( highSurrogate << 10 ); key += 0x10000; } if ( is_control_code( key ) ) { key = control_to_human( key ); modifierKeys |= Replxx::KEY::BASE_CONTROL; } key |= modifierKeys; highSurrogate = 0; c = key; break; } } #else c = read_unicode_character(); if (c == 0) { return 0; } // 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. // #ifdef __REPLXX_DEBUG__ if (c == ctrlChar('^')) { // ctrl-^, special debug mode, prints all keys hit, // ctrl-C to get out printf( "\nEntering keyboard debugging mode (on ctrl-^), press ctrl-C to exit " "this mode\n"); 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) { char32_t key = static_cast(keys[i]); char* friendlyTextPtr; char friendlyTextBuf[10]; const char* prefixText = (key < 0x80) ? "" : "0x80+"; char32_t 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 = const_cast("space"); } else if (keyCopy == 27) { friendlyTextPtr = const_cast("ESC"); } else if (keyCopy == 0) { friendlyTextPtr = const_cast("NUL"); } else if (keyCopy == 127) { friendlyTextPtr = const_cast("DEL"); } else { friendlyTextBuf[0] = '^'; friendlyTextBuf[1] = control_to_human( keyCopy ); friendlyTextBuf[2] = 0; friendlyTextPtr = friendlyTextBuf; } printf("%d x%02X (%s%s) ", key, 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')) { printf("Leaving keyboard debugging mode (on ctrl-C)\n"); fflush(stdout); return -2; } } } #endif // __REPLXX_DEBUG__ c = EscapeSequenceProcessing::doDispatch(c); if ( is_control_code( c ) ) { c = Replxx::KEY::control( control_to_human( c ) ); } #endif // #_WIN32 return ( c ); } Terminal::EVENT_TYPE Terminal::wait_for_input( int long timeout_ ) { #ifdef _WIN32 std::array handles = { _consoleIn, _interrupt }; while ( true ) { DWORD event( WaitForMultipleObjects( static_cast( handles.size() ), handles.data(), false, timeout_ > 0 ? timeout_ : INFINITE ) ); switch ( event ) { case ( WAIT_OBJECT_0 + 0 ): { // peek events that will be skipped INPUT_RECORD rec; DWORD count; PeekConsoleInputW( _consoleIn, &rec, 1, &count ); if ( ( rec.EventType != KEY_EVENT ) || ( !rec.Event.KeyEvent.bKeyDown && ( rec.Event.KeyEvent.wVirtualKeyCode != VK_MENU ) ) ) { // read the event to unsignal the handle ReadConsoleInputW( _consoleIn, &rec, 1, &count ); continue; } else if (rec.EventType == KEY_EVENT) { int key(rec.Event.KeyEvent.uChar.UnicodeChar); if (key == 0) { switch (rec.Event.KeyEvent.wVirtualKeyCode) { case VK_LEFT: case VK_RIGHT: case VK_UP: case VK_DOWN: case VK_DELETE: case VK_HOME: case VK_END: case VK_PRIOR: case VK_NEXT: break; default: ReadConsoleInputW(_consoleIn, &rec, 1, &count); continue; // in raw mode, ReadConsoleInput shows shift, ctrl - ignore them } } } return ( EVENT_TYPE::KEY_PRESS ); } case ( WAIT_OBJECT_0 + 1 ): { ResetEvent( _interrupt ); if ( _events.empty() ) { continue; } EVENT_TYPE eventType( _events.front() ); _events.pop_front(); return ( eventType ); } case ( WAIT_TIMEOUT ): { return ( EVENT_TYPE::TIMEOUT ); } } } #else fd_set fdSet; int nfds( max( _interrupt[0], _interrupt[1] ) + 1 ); while ( true ) { FD_ZERO( &fdSet ); FD_SET( 0, &fdSet ); FD_SET( _interrupt[0], &fdSet ); timeval tv{ timeout_ / 1000, static_cast( ( timeout_ % 1000 ) * 1000 ) }; int err( select( nfds, &fdSet, nullptr, nullptr, timeout_ > 0 ? &tv : nullptr ) ); if ( ( err == -1 ) && ( errno == EINTR ) ) { continue; } if ( err == 0 ) { return ( EVENT_TYPE::TIMEOUT ); } if ( FD_ISSET( _interrupt[0], &fdSet ) ) { char data( 0 ); static_cast( read( _interrupt[0], &data, 1 ) == 1 ); if ( data == 'k' ) { return ( EVENT_TYPE::KEY_PRESS ); } if ( data == 'm' ) { return ( EVENT_TYPE::MESSAGE ); } if ( data == 'r' ) { return ( EVENT_TYPE::RESIZE ); } } if ( FD_ISSET( 0, &fdSet ) ) { return ( EVENT_TYPE::KEY_PRESS ); } } #endif } void Terminal::notify_event( EVENT_TYPE eventType_ ) { #ifdef _WIN32 _events.push_back( eventType_ ); SetEvent( _interrupt ); #else char data( ( eventType_ == EVENT_TYPE::KEY_PRESS ) ? 'k' : ( eventType_ == EVENT_TYPE::MESSAGE ? 'm' : 'r' ) ); static_cast( write( _interrupt[1], &data, 1 ) == 1 ); #endif } /** * Clear the screen ONLY (no redisplay of anything) */ void Terminal::clear_screen( CLEAR_SCREEN clearScreen_ ) { #ifdef _WIN32 if ( _autoEscape ) { #endif if ( clearScreen_ == CLEAR_SCREEN::WHOLE ) { char const clearCode[] = "\033c\033[H\033[2J\033[0m"; static_cast( write(1, clearCode, sizeof ( clearCode ) - 1) >= 0 ); } else { char const clearCode[] = "\033[J"; static_cast( write(1, clearCode, sizeof ( clearCode ) - 1) >= 0 ); } return; #ifdef _WIN32 } COORD coord = { 0, 0 }; CONSOLE_SCREEN_BUFFER_INFO inf; HANDLE consoleOut( _consoleOut != INVALID_HANDLE_VALUE ? _consoleOut : GetStdHandle( STD_OUTPUT_HANDLE ) ); GetConsoleScreenBufferInfo( consoleOut, &inf ); if ( clearScreen_ == CLEAR_SCREEN::TO_END ) { coord = inf.dwCursorPosition; DWORD nWritten( 0 ); SHORT height( inf.srWindow.Bottom - inf.srWindow.Top ); DWORD yPos( inf.dwCursorPosition.Y - inf.srWindow.Top ); DWORD toWrite( ( height + 1 - yPos ) * inf.dwSize.X - inf.dwCursorPosition.X ); // FillConsoleOutputCharacterA( consoleOut, ' ', toWrite, coord, &nWritten ); _empty.resize( toWrite - 1, ' ' ); WriteConsoleA( consoleOut, _empty.data(), toWrite - 1, &nWritten, nullptr ); } else { COORD scrollTarget = { 0, -inf.dwSize.Y }; CHAR_INFO fill{ TEXT( ' ' ), inf.wAttributes }; SMALL_RECT scrollRect = { 0, 0, inf.dwSize.X, inf.dwSize.Y }; ScrollConsoleScreenBuffer( consoleOut, &scrollRect, nullptr, scrollTarget, &fill ); } SetConsoleCursorPosition( consoleOut, coord ); #endif } void Terminal::jump_cursor( int xPos_, int yOffset_ ) { #ifdef _WIN32 CONSOLE_SCREEN_BUFFER_INFO inf; GetConsoleScreenBufferInfo( _consoleOut, &inf ); inf.dwCursorPosition.X = xPos_; inf.dwCursorPosition.Y += yOffset_; SetConsoleCursorPosition( _consoleOut, inf.dwCursorPosition ); #else char seq[64]; if ( yOffset_ != 0 ) { // move the cursor up as required snprintf( seq, sizeof seq, "\033[%d%c", abs( yOffset_ ), yOffset_ > 0 ? 'B' : 'A' ); write8( seq, strlen( seq ) ); } // position at the end of the prompt, clear to end of screen snprintf( seq, sizeof seq, "\033[%dG", xPos_ + 1 /* 1-based on VT100 */ ); write8( seq, strlen( seq ) ); #endif } #ifdef _WIN32 void Terminal::set_cursor_visible( bool visible_ ) { CONSOLE_CURSOR_INFO cursorInfo; GetConsoleCursorInfo( _consoleOut, &cursorInfo ); cursorInfo.bVisible = visible_; SetConsoleCursorInfo( _consoleOut, &cursorInfo ); return; } #else void Terminal::set_cursor_visible( bool ) {} #endif #ifndef _WIN32 int Terminal::read_verbatim( char32_t* buffer_, int size_ ) { int len( 0 ); buffer_[len ++] = read_unicode_character(); int statusFlags( ::fcntl( STDIN_FILENO, F_GETFL, 0 ) ); ::fcntl( STDIN_FILENO, F_SETFL, statusFlags | O_NONBLOCK ); while ( len < size_ ) { char32_t c( read_unicode_character() ); if ( c == 0 ) { break; } buffer_[len ++] = c; } ::fcntl( STDIN_FILENO, F_SETFL, statusFlags ); return ( len ); } int Terminal::install_window_change_handler( void ) { struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sa.sa_handler = &WindowSizeChanged; if (sigaction(SIGWINCH, &sa, nullptr) == -1) { return errno; } return 0; } #endif }