413 lines
14 KiB
C++
413 lines
14 KiB
C++
// 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 <windows.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <string>
|
|
|
|
#include "NamedPipe.h"
|
|
#include "UnicodeEncoding.h"
|
|
#include "../shared/DebugClient.h"
|
|
#include "../shared/WinptyAssert.h"
|
|
|
|
#define CSI "\x1b["
|
|
|
|
const int COLOR_ATTRIBUTE_MASK =
|
|
FOREGROUND_BLUE |
|
|
FOREGROUND_GREEN |
|
|
FOREGROUND_RED |
|
|
FOREGROUND_INTENSITY |
|
|
BACKGROUND_BLUE |
|
|
BACKGROUND_GREEN |
|
|
BACKGROUND_RED |
|
|
BACKGROUND_INTENSITY;
|
|
|
|
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;
|
|
|
|
// 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;
|
|
|
|
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;
|
|
|
|
// 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");
|
|
}
|
|
out.push_back('m');
|
|
}
|
|
|
|
// 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.
|
|
static inline unsigned int fixConsolePopupBoxArt(unsigned int ch)
|
|
{
|
|
if (ch <= 6) {
|
|
switch (ch) {
|
|
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
|
|
}
|
|
}
|
|
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
|
|
|
|
Terminal::Terminal(NamedPipe *output) :
|
|
m_output(output),
|
|
m_remoteLine(0),
|
|
m_cursorHidden(false),
|
|
m_remoteColor(-1),
|
|
m_consoleMode(false)
|
|
{
|
|
}
|
|
|
|
void Terminal::setConsoleMode(int mode)
|
|
{
|
|
if (mode == 1)
|
|
m_consoleMode = true;
|
|
else
|
|
m_consoleMode = false;
|
|
}
|
|
|
|
void Terminal::reset(SendClearFlag sendClearFirst, int64_t newLine)
|
|
{
|
|
if (sendClearFirst == SendClear && !m_consoleMode) {
|
|
// 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_cursorHidden = false;
|
|
m_cursorPos = std::pair<int, int64_t>(0, newLine);
|
|
m_remoteColor = -1;
|
|
}
|
|
|
|
void Terminal::sendLine(int64_t line, const CHAR_INFO *lineData, int width)
|
|
{
|
|
hideTerminalCursor();
|
|
moveTerminalToLine(line);
|
|
|
|
m_termLine.clear();
|
|
int trimmedLineLength = 0;
|
|
bool alreadyErasedLine = false;
|
|
|
|
int cellCount = 1;
|
|
for (int i = 0; i < width; i += cellCount) {
|
|
int color = lineData[i].Attributes & COLOR_ATTRIBUTE_MASK;
|
|
if (color != m_remoteColor) {
|
|
if (!m_consoleMode) {
|
|
outputSetColor(m_termLine, color);
|
|
}
|
|
trimmedLineLength = m_termLine.size();
|
|
m_remoteColor = color;
|
|
}
|
|
unsigned int ch;
|
|
scanUnicodeScalarValue(&lineData[i], width - i, cellCount, ch);
|
|
if (ch == ' ') {
|
|
m_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_consoleMode) {
|
|
m_termLine.append(CSI"0K"); // Erase from cursor to EOL
|
|
}
|
|
alreadyErasedLine = true;
|
|
}
|
|
ch = fixConsolePopupBoxArt(ch);
|
|
char enc[4];
|
|
int enclen = encodeUtf8(enc, ch);
|
|
if (enclen == 0) {
|
|
enc[0] = '?';
|
|
enclen = 1;
|
|
}
|
|
m_termLine.append(enc, enclen);
|
|
trimmedLineLength = m_termLine.size();
|
|
}
|
|
}
|
|
|
|
m_output->write(m_termLine.data(), trimmedLineLength);
|
|
|
|
if (!alreadyErasedLine && !m_consoleMode) {
|
|
m_output->write(CSI"0K"); // Erase from cursor to EOL
|
|
}
|
|
}
|
|
|
|
void Terminal::finishOutput(const std::pair<int, int64_t> &newCursorPos)
|
|
{
|
|
if (newCursorPos != m_cursorPos)
|
|
hideTerminalCursor();
|
|
if (m_cursorHidden) {
|
|
moveTerminalToLine(newCursorPos.second);
|
|
char buffer[32];
|
|
sprintf(buffer, CSI"%dG" CSI"?25h", newCursorPos.first + 1);
|
|
if (!m_consoleMode)
|
|
m_output->write(buffer);
|
|
m_cursorHidden = false;
|
|
}
|
|
m_cursorPos = newCursorPos;
|
|
}
|
|
|
|
void Terminal::hideTerminalCursor()
|
|
{
|
|
if (m_cursorHidden)
|
|
return;
|
|
if (!m_consoleMode)
|
|
m_output->write(CSI"?25l");
|
|
m_cursorHidden = true;
|
|
}
|
|
|
|
void Terminal::moveTerminalToLine(int64_t line)
|
|
{
|
|
// 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.
|
|
|
|
if (line < m_remoteLine) {
|
|
// CUrsor Up (CUU)
|
|
char buffer[32];
|
|
sprintf(buffer, "\r" CSI"%dA", static_cast<int>(m_remoteLine - line));
|
|
if (!m_consoleMode)
|
|
m_output->write(buffer);
|
|
m_remoteLine = line;
|
|
} else if (line > m_remoteLine) {
|
|
while (line > m_remoteLine) {
|
|
if (!m_consoleMode)
|
|
m_output->write("\r\n");
|
|
m_remoteLine++;
|
|
}
|
|
} else {
|
|
m_output->write("\r");
|
|
}
|
|
}
|