// 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"); | |
} | |
} | |