| // 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. | |
| // 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<size_t>(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<size_t>(width) >= m_lineData.size(); | |
| } else { | |
| okWidth = static_cast<size_t>(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<unsigned int>(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"); | |
| } | |
| } | |