// Copyright (c) 2011-2015 Ryan Prichard // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. #include "Terminal.h" #include #include #include #include #include "NamedPipe.h" #include "UnicodeEncoding.h" #include "../shared/DebugClient.h" #include "../shared/WinptyAssert.h" #include "../shared/winpty_snprintf.h" #define CSI "\x1b[" // Work around the old MinGW, which lacks COMMON_LVB_LEADING_BYTE and // COMMON_LVB_TRAILING_BYTE. const int WINPTY_COMMON_LVB_LEADING_BYTE = 0x100; const int WINPTY_COMMON_LVB_TRAILING_BYTE = 0x200; const int WINPTY_COMMON_LVB_REVERSE_VIDEO = 0x4000; const int WINPTY_COMMON_LVB_UNDERSCORE = 0x8000; const int COLOR_ATTRIBUTE_MASK = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY | WINPTY_COMMON_LVB_REVERSE_VIDEO | WINPTY_COMMON_LVB_UNDERSCORE; const int FLAG_RED = 1; const int FLAG_GREEN = 2; const int FLAG_BLUE = 4; const int FLAG_BRIGHT = 8; const int BLACK = 0; const int DKGRAY = BLACK | FLAG_BRIGHT; const int LTGRAY = FLAG_RED | FLAG_GREEN | FLAG_BLUE; const int WHITE = LTGRAY | FLAG_BRIGHT; // SGR parameters (Select Graphic Rendition) const int SGR_FORE = 30; const int SGR_FORE_HI = 90; const int SGR_BACK = 40; const int SGR_BACK_HI = 100; namespace { static void outUInt(std::string &out, unsigned int n) { char buf[32]; char *pbuf = &buf[32]; *(--pbuf) = '\0'; do { *(--pbuf) = '0' + n % 10; n /= 10; } while (n != 0); out.append(pbuf); } static void outputSetColorSgrParams(std::string &out, bool isFore, int color) { out.push_back(';'); const int sgrBase = isFore ? SGR_FORE : SGR_BACK; if (color & FLAG_BRIGHT) { // Some terminals don't support the 9X/10X "intensive" color parameters // (e.g. the Eclipse TM terminal as of this writing). Those terminals // will quietly ignore a 9X/10X code, and the other terminals will // ignore a 3X/4X code if it's followed by a 9X/10X code. Therefore, // output a 3X/4X code as a fallback, then override it. const int colorBase = color & ~FLAG_BRIGHT; outUInt(out, sgrBase + colorBase); out.push_back(';'); outUInt(out, sgrBase + (SGR_FORE_HI - SGR_FORE) + colorBase); } else { outUInt(out, sgrBase + color); } } static void outputSetColor(std::string &out, int color) { int fore = 0; int back = 0; if (color & FOREGROUND_RED) fore |= FLAG_RED; if (color & FOREGROUND_GREEN) fore |= FLAG_GREEN; if (color & FOREGROUND_BLUE) fore |= FLAG_BLUE; if (color & FOREGROUND_INTENSITY) fore |= FLAG_BRIGHT; if (color & BACKGROUND_RED) back |= FLAG_RED; if (color & BACKGROUND_GREEN) back |= FLAG_GREEN; if (color & BACKGROUND_BLUE) back |= FLAG_BLUE; if (color & BACKGROUND_INTENSITY) back |= FLAG_BRIGHT; if (color & WINPTY_COMMON_LVB_REVERSE_VIDEO) { // n.b.: The COMMON_LVB_REVERSE_VIDEO flag also swaps // FOREGROUND_INTENSITY and BACKGROUND_INTENSITY. Tested on // Windows 10 v14393. std::swap(fore, back); } // Translate the fore/back colors into terminal escape codes using // a heuristic that works OK with common white-on-black or // black-on-white color schemes. We don't know which color scheme // the terminal is using. It is ugly to force white-on-black text // on a black-on-white terminal, and it's even ugly to force the // matching scheme. It's probably relevant that the default // fore/back terminal colors frequently do not match any of the 16 // palette colors. // Typical default terminal color schemes (according to palette, // when possible): // - mintty: LtGray-on-Black(A) // - putty: LtGray-on-Black(A) // - xterm: LtGray-on-Black(A) // - Konsole: LtGray-on-Black(A) // - JediTerm/JetBrains: Black-on-White(B) // - rxvt: Black-on-White(B) // If the background is the default color (black), then it will // map to Black(A) or White(B). If we translate White to White, // then a Black background and a White background in the console // are both White with (B). Therefore, we should translate White // using SGR 7 (Invert). The typical finished mapping table for // background grayscale colors is: // // (A) White => LtGray(fore) // (A) Black => Black(back) // (A) LtGray => LtGray // (A) DkGray => DkGray // // (B) White => Black(fore) // (B) Black => White(back) // (B) LtGray => LtGray // (B) DkGray => DkGray // out.append(CSI "0"); if (back == BLACK) { if (fore == LTGRAY) { // The "default" foreground color. Use the terminal's // default colors. } else if (fore == WHITE) { // Sending the literal color white would behave poorly if // the terminal were black-on-white. Sending Bold is not // guaranteed to alter the color, but it will make the text // visually distinct, so do that instead. out.append(";1"); } else if (fore == DKGRAY) { // Set the foreground color to DkGray(90) with a fallback // of LtGray(37) for terminals that don't handle the 9X SGR // parameters (e.g. Eclipse's TM Terminal as of this // writing). out.append(";37;90"); } else { outputSetColorSgrParams(out, true, fore); } } else if (back == WHITE) { // Set the background color using Invert on the default // foreground color, and set the foreground color by setting a // background color. // Use the terminal's inverted colors. out.append(";7"); if (fore == LTGRAY || fore == BLACK) { // We're likely mapping Console White to terminal LtGray or // Black. If they are the Console foreground color, then // don't set a terminal foreground color to avoid creating // invisible text. } else { outputSetColorSgrParams(out, false, fore); } } else { // Set the foreground and background to match exactly that in // the Windows console. outputSetColorSgrParams(out, true, fore); outputSetColorSgrParams(out, false, back); } if (fore == back) { // The foreground and background colors are exactly equal, so // attempt to hide the text using the Conceal SGR parameter, // which some terminals support. out.append(";8"); } if (color & WINPTY_COMMON_LVB_UNDERSCORE) { out.append(";4"); } out.push_back('m'); } static inline unsigned int fixSpecialCharacters(unsigned int ch) { if (ch <= 0x1b) { switch (ch) { // The Windows Console has a popup window (e.g. that appears with // F7) that is sometimes bordered with box-drawing characters. // With the Japanese and Korean system locales (CP932 and CP949), // the UnicodeChar values for the box-drawing characters are 1 // through 6. Detect this and map the values to the correct // Unicode values. // // N.B. In the English locale, the UnicodeChar values are correct, // and they identify single-line characters rather than // double-line. In the Chinese Simplified and Traditional locales, // the popups use ASCII characters instead. case 1: return 0x2554; // BOX DRAWINGS DOUBLE DOWN AND RIGHT case 2: return 0x2557; // BOX DRAWINGS DOUBLE DOWN AND LEFT case 3: return 0x255A; // BOX DRAWINGS DOUBLE UP AND RIGHT case 4: return 0x255D; // BOX DRAWINGS DOUBLE UP AND LEFT case 5: return 0x2551; // BOX DRAWINGS DOUBLE VERTICAL case 6: return 0x2550; // BOX DRAWINGS DOUBLE HORIZONTAL // Convert an escape character to some other character. This // conversion only applies to console cells containing an escape // character. In newer versions of Windows 10 (e.g. 10.0.10586), // the non-legacy console recognizes escape sequences in // WriteConsole and interprets them without writing them to the // cells of the screen buffer. In that case, the conversion here // does not apply. case 0x1b: return '?'; } } return ch; } static inline bool isFullWidthCharacter(const CHAR_INFO *data, int width) { if (width < 2) { return false; } return (data[0].Attributes & WINPTY_COMMON_LVB_LEADING_BYTE) && (data[1].Attributes & WINPTY_COMMON_LVB_TRAILING_BYTE) && data[0].Char.UnicodeChar == data[1].Char.UnicodeChar; } // Scan to find a single Unicode Scalar Value. Full-width characters occupy // two console cells, and this code also tries to handle UTF-16 surrogate // pairs. // // Windows expands at least some wide characters outside the Basic // Multilingual Plane into four cells, such as U+20000: // 1. 0xD840, attr=0x107 // 2. 0xD840, attr=0x207 // 3. 0xDC00, attr=0x107 // 4. 0xDC00, attr=0x207 // Even in the Traditional Chinese locale on Windows 10, this text is rendered // as two boxes, but if those boxes are copied-and-pasted, the character is // copied correctly. static inline void scanUnicodeScalarValue( const CHAR_INFO *data, int width, int &outCellCount, unsigned int &outCharValue) { ASSERT(width >= 1); const int w1 = isFullWidthCharacter(data, width) ? 2 : 1; const wchar_t c1 = data[0].Char.UnicodeChar; if ((c1 & 0xF800) == 0xD800) { // The first cell is either a leading or trailing surrogate pair. if ((c1 & 0xFC00) != 0xD800 || width <= w1 || ((data[w1].Char.UnicodeChar & 0xFC00) != 0xDC00)) { // Invalid surrogate pair outCellCount = w1; outCharValue = '?'; } else { // Valid surrogate pair outCellCount = w1 + (isFullWidthCharacter(&data[w1], width - w1) ? 2 : 1); outCharValue = decodeSurrogatePair(c1, data[w1].Char.UnicodeChar); } } else { outCellCount = w1; outCharValue = c1; } } } // anonymous namespace void Terminal::reset(SendClearFlag sendClearFirst, int64_t newLine) { if (sendClearFirst == SendClear && !m_plainMode) { // 0m ==> reset SGR parameters // 1;1H ==> move cursor to top-left position // 2J ==> clear the entire screen m_output.write(CSI "0m" CSI "1;1H" CSI "2J"); } m_remoteLine = newLine; m_remoteColumn = 0; m_lineData.clear(); m_cursorHidden = false; m_remoteColor = -1; } void Terminal::sendLine(int64_t line, const CHAR_INFO *lineData, int width, int cursorColumn) { ASSERT(width >= 1); moveTerminalToLine(line); // If possible, see if we can append to what we've already output for this // line. if (m_lineDataValid) { ASSERT(m_lineData.size() == static_cast(m_remoteColumn)); if (m_remoteColumn > 0) { // In normal mode, if m_lineData.size() equals `width`, then we // will have trouble outputing the "erase rest of line" command, // which must be output before reaching the end of the line. In // plain mode, we don't output that command, so we're OK with a // full line. bool okWidth = false; if (m_plainMode) { okWidth = static_cast(width) >= m_lineData.size(); } else { okWidth = static_cast(width) > m_lineData.size(); } if (!okWidth || memcmp(m_lineData.data(), lineData, sizeof(CHAR_INFO) * m_lineData.size()) != 0) { m_lineDataValid = false; } } } if (!m_lineDataValid) { // We can't reuse, so we must reset this line. hideTerminalCursor(); if (m_plainMode) { // We can't backtrack, so repeat this line. m_output.write("\r\n"); } else { m_output.write("\r"); } m_lineDataValid = true; m_lineData.clear(); m_remoteColumn = 0; } std::string &termLine = m_termLineWorkingBuffer; termLine.clear(); size_t trimmedLineLength = 0; int trimmedCellCount = m_lineData.size(); bool alreadyErasedLine = false; int cellCount = 1; for (int i = m_lineData.size(); i < width; i += cellCount) { if (m_outputColor) { int color = lineData[i].Attributes & COLOR_ATTRIBUTE_MASK; if (color != m_remoteColor) { outputSetColor(termLine, color); trimmedLineLength = termLine.size(); m_remoteColor = color; // All the cells just up to this color change will be output. trimmedCellCount = i; } } unsigned int ch; scanUnicodeScalarValue(&lineData[i], width - i, cellCount, ch); if (ch == ' ') { // Tentatively add this space character. We'll only output it if // we see something interesting after it. termLine.push_back(' '); } else { if (i + cellCount == width) { // We'd like to erase the line after outputting all non-blank // characters, but this doesn't work if the last cell in the // line is non-blank. At the point, the cursor is positioned // just past the end of the line, but in many terminals, // issuing a CSI 0K at that point also erases the last cell in // the line. Work around this behavior by issuing the erase // one character early in that case. if (!m_plainMode) { termLine.append(CSI "0K"); // Erase from cursor to EOL } alreadyErasedLine = true; } ch = fixSpecialCharacters(ch); char enc[4]; int enclen = encodeUtf8(enc, ch); if (enclen == 0) { enc[0] = '?'; enclen = 1; } termLine.append(enc, enclen); trimmedLineLength = termLine.size(); // All the cells up to and including this cell will be output. trimmedCellCount = i + cellCount; } } if (cursorColumn != -1 && trimmedCellCount > cursorColumn) { // The line content would run past the cursor, so hide it before we // output. hideTerminalCursor(); } m_output.write(termLine.data(), trimmedLineLength); if (!alreadyErasedLine && !m_plainMode) { m_output.write(CSI "0K"); // Erase from cursor to EOL } ASSERT(trimmedCellCount <= width); m_lineData.insert(m_lineData.end(), &lineData[m_lineData.size()], &lineData[trimmedCellCount]); m_remoteColumn = trimmedCellCount; } void Terminal::showTerminalCursor(int column, int64_t line) { moveTerminalToLine(line); if (!m_plainMode) { if (m_remoteColumn != column) { char buffer[32]; winpty_snprintf(buffer, CSI "%dG", column + 1); m_output.write(buffer); m_lineDataValid = (column == 0); m_lineData.clear(); m_remoteColumn = column; } if (m_cursorHidden) { m_output.write(CSI "?25h"); m_cursorHidden = false; } } } void Terminal::hideTerminalCursor() { if (!m_plainMode) { if (m_cursorHidden) { return; } m_output.write(CSI "?25l"); m_cursorHidden = true; } } void Terminal::moveTerminalToLine(int64_t line) { if (line == m_remoteLine) { return; } // Do not use CPL or CNL. Konsole 2.5.4 does not support Cursor Previous // Line (CPL) -- there are "Undecodable sequence" errors. gnome-terminal // 2.32.0 does handle it. Cursor Next Line (CNL) does nothing if the // cursor is on the last line already. hideTerminalCursor(); if (line < m_remoteLine) { if (m_plainMode) { // We can't backtrack, so instead repeat the lines again. m_output.write("\r\n"); m_remoteLine = line; } else { // Backtrack and overwrite previous lines. // CUrsor Up (CUU) char buffer[32]; winpty_snprintf(buffer, "\r" CSI "%uA", static_cast(m_remoteLine - line)); m_output.write(buffer); m_remoteLine = line; } } else if (line > m_remoteLine) { while (line > m_remoteLine) { m_output.write("\r\n"); m_remoteLine++; } } m_lineDataValid = true; m_lineData.clear(); m_remoteColumn = 0; } void Terminal::enableMouseMode(bool enabled) { if (m_mouseModeEnabled == enabled || m_plainMode) { return; } m_mouseModeEnabled = enabled; if (enabled) { // Start by disabling UTF-8 coordinate mode (1005), just in case we // have a terminal that does not support 1006/1015 modes, and 1005 // happens to be enabled. The UTF-8 coordinates can't be unambiguously // decoded. // // Enable basic mouse support first (1000), then try to switch to // button-move mode (1002), then try full mouse-move mode (1003). // Terminals that don't support a mode will be stuck at the highest // mode they do support. // // Enable encoding mode 1015 first, then try to switch to 1006. On // some terminals, both modes will be enabled, but 1006 will have // priority. On other terminals, 1006 wins because it's listed last. // // See misc/MouseInputNotes.txt for details. m_output.write( CSI "?1005l" CSI "?1000h" CSI "?1002h" CSI "?1003h" CSI "?1015h" CSI "?1006h"); } else { // Resetting both encoding modes (1006 and 1015) is necessary, but // apparently we only need to use reset on one of the 100[023] modes. // Doing both doesn't hurt. m_output.write( CSI "?1006l" CSI "?1015l" CSI "?1003l" CSI "?1002l" CSI "?1000l"); } }