Rewrite the color translation heuristic.
* Use the Bold(1) SGR parameter in only one case -- White-on-Black. * Use the 0x9X and 0x10X SGR parameters to set intense/bright colors for both foreground and background. Detailed changes: * DkGray background: - Previously, this background was handled identically to the Black background. - Now it is treated like the non-grayscale colors, and the foreground/background are set exactly. * Black background: - LtGray foreground is unchanged: default text colors - White foreground is unchanged: text is bolded - DkGray is fixed: instead of concealing the text, set the foreground to DkGray, falling back to LtGray. * LtGray background: - Previously, this background was handled exactly the same way as the White background. - Now it is treated like any other non-grayscale color. * White background: - Black foreground is unchanged: only an Invert is output. - LtGray: previously, the text was concealed. Now it is handled like Black, and only an Invert is output. - DkGray: previously only Invert was output. Now DkGray is also output. - White is effectively unchanged; it is still concealed if possible. Also: set the console colors to LtGray-on-Black on startup. The heuristic makes little sense with other colors. Fixes https://github.com/rprichard/winpty/issues/39
This commit is contained in:
parent
9cf6972860
commit
a998bcf207
@ -90,6 +90,11 @@ Agent::Agent(LPCWSTR controlPipeName,
|
||||
m_console->setCursorPosition(Coord(0, 0));
|
||||
m_console->setTitle(m_currentTitle);
|
||||
|
||||
// For the sake of the color translation heuristic, set the console color
|
||||
// to LtGray-on-Black.
|
||||
m_console->setTextAttribute(7);
|
||||
m_console->clearAllLines(m_console->bufferInfo());
|
||||
|
||||
m_controlSocket = makeSocket(controlPipeName);
|
||||
m_dataSocket = makeSocket(dataPipeName);
|
||||
m_terminal = new Terminal(m_dataSocket);
|
||||
|
@ -25,6 +25,8 @@
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
|
||||
#include "../shared/DebugClient.h"
|
||||
|
||||
#define CSI "\x1b["
|
||||
|
||||
const int COLOR_ATTRIBUTE_MASK =
|
||||
@ -37,20 +39,62 @@ const int COLOR_ATTRIBUTE_MASK =
|
||||
BACKGROUND_RED |
|
||||
BACKGROUND_INTENSITY;
|
||||
|
||||
const int TERMINAL_BLACK = 0;
|
||||
const int TERMINAL_RED = 1;
|
||||
const int TERMINAL_GREEN = 2;
|
||||
const int TERMINAL_BLUE = 4;
|
||||
const int TERMINAL_WHITE = 7;
|
||||
const int FLAG_RED = 1;
|
||||
const int FLAG_GREEN = 2;
|
||||
const int FLAG_BLUE = 4;
|
||||
const int FLAG_BRIGHT = 8;
|
||||
|
||||
const int TERMINAL_FOREGROUND = 30;
|
||||
const int TERMINAL_BACKGROUND = 40;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
Terminal::Terminal(NamedPipe *output) :
|
||||
m_output(output),
|
||||
m_remoteLine(0),
|
||||
@ -90,64 +134,110 @@ void Terminal::sendLine(int line, CHAR_INFO *lineData, int width)
|
||||
if (!m_consoleMode)
|
||||
m_output->write(CSI"2K");
|
||||
|
||||
std::string termLine;
|
||||
termLine.reserve(width + 32);
|
||||
m_termLine.clear();
|
||||
|
||||
int length = 0;
|
||||
for (int i = 0; i < width; ++i) {
|
||||
int color = lineData[i].Attributes & COLOR_ATTRIBUTE_MASK;
|
||||
if (color != m_remoteColor) {
|
||||
if (color != m_remoteColor && !m_consoleMode) {
|
||||
int fore = 0;
|
||||
int back = 0;
|
||||
if (color & FOREGROUND_RED) fore |= TERMINAL_RED;
|
||||
if (color & FOREGROUND_GREEN) fore |= TERMINAL_GREEN;
|
||||
if (color & FOREGROUND_BLUE) fore |= TERMINAL_BLUE;
|
||||
if (color & BACKGROUND_RED) back |= TERMINAL_RED;
|
||||
if (color & BACKGROUND_GREEN) back |= TERMINAL_GREEN;
|
||||
if (color & BACKGROUND_BLUE) back |= TERMINAL_BLUE;
|
||||
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;
|
||||
|
||||
char buffer[128];
|
||||
if (back == TERMINAL_BLACK) {
|
||||
if (fore == TERMINAL_WHITE) {
|
||||
// Use the terminal's default colors.
|
||||
sprintf(buffer, CSI"0");
|
||||
} else if (fore == TERMINAL_BLACK) {
|
||||
// Attempt to hide the character, but some terminals won't
|
||||
// hide it. Is this an important case?
|
||||
sprintf(buffer, CSI"0;8");
|
||||
// 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
|
||||
//
|
||||
|
||||
m_termLine.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.
|
||||
m_termLine.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).
|
||||
m_termLine.append(";37;90");
|
||||
} else {
|
||||
sprintf(buffer, CSI"0;%d", TERMINAL_FOREGROUND + fore);
|
||||
outputSetColorSgrParams(m_termLine, true, fore);
|
||||
}
|
||||
if (color & FOREGROUND_INTENSITY)
|
||||
strcat(buffer, ";1");
|
||||
} else if (back == TERMINAL_WHITE) {
|
||||
} 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.
|
||||
if (fore == TERMINAL_BLACK) {
|
||||
sprintf(buffer, CSI"0;7");
|
||||
} else if (fore == TERMINAL_WHITE) {
|
||||
// Attempt to hide the character, but some terminals won't
|
||||
// hide it. Is this an important case?
|
||||
sprintf(buffer, CSI"0;7;8");
|
||||
m_termLine.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 {
|
||||
sprintf(buffer, CSI"0;7;%d", TERMINAL_BACKGROUND + fore);
|
||||
outputSetColorSgrParams(m_termLine, false, fore);
|
||||
}
|
||||
// Don't worry about FOREGROUND_INTENSITY because with at least
|
||||
// one terminal (gnome-terminal 2.32.0), setting the Intensity
|
||||
// flag affects both foreground and background when Reverse
|
||||
// flag is also set.
|
||||
} else {
|
||||
sprintf(buffer, CSI"0;%d;%d",
|
||||
TERMINAL_FOREGROUND + fore,
|
||||
TERMINAL_BACKGROUND + back);
|
||||
if (color & FOREGROUND_INTENSITY)
|
||||
strcat(buffer, ";1");
|
||||
// Set the foreground and background to match exactly that in
|
||||
// the Windows console.
|
||||
outputSetColorSgrParams(m_termLine, true, fore);
|
||||
outputSetColorSgrParams(m_termLine, false, back);
|
||||
}
|
||||
strcat(buffer, "m");
|
||||
if (!m_consoleMode)
|
||||
termLine.append(buffer);
|
||||
length = termLine.size();
|
||||
m_remoteColor = color;
|
||||
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.
|
||||
m_termLine.append(";8");
|
||||
}
|
||||
m_termLine.push_back('m');
|
||||
length = m_termLine.size();
|
||||
}
|
||||
m_remoteColor = color;
|
||||
if (lineData[i].Attributes & WINPTY_COMMON_LVB_TRAILING_BYTE) {
|
||||
// CJK full-width characters occupy two console cells. The first
|
||||
// cell is marked with COMMON_LVB_LEADING_BYTE, and the second is
|
||||
@ -194,14 +284,14 @@ void Terminal::sendLine(int line, CHAR_INFO *lineData, int width)
|
||||
mblen = 1;
|
||||
}
|
||||
if (mblen == 1 && mbstr[0] == ' ') {
|
||||
termLine.push_back(' ');
|
||||
m_termLine.push_back(' ');
|
||||
} else {
|
||||
termLine.append(mbstr, mblen);
|
||||
length = termLine.size();
|
||||
m_termLine.append(mbstr, mblen);
|
||||
length = m_termLine.size();
|
||||
}
|
||||
}
|
||||
|
||||
m_output->write(termLine.data(), length);
|
||||
m_output->write(m_termLine.data(), length);
|
||||
}
|
||||
|
||||
void Terminal::finishOutput(const std::pair<int, int> &newCursorPos)
|
||||
|
@ -23,6 +23,7 @@
|
||||
|
||||
#include <windows.h>
|
||||
#include "Coord.h"
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
class NamedPipe;
|
||||
@ -47,6 +48,7 @@ private:
|
||||
std::pair<int, int> m_cursorPos;
|
||||
int m_remoteColor;
|
||||
bool m_consoleMode;
|
||||
std::string m_termLine;
|
||||
};
|
||||
|
||||
#endif // TERMINAL_H
|
||||
|
@ -68,7 +68,7 @@ void Win32Console::clearLines(
|
||||
const ConsoleScreenBufferInfo &info)
|
||||
{
|
||||
// TODO: error handling
|
||||
const int width = SmallRect(info.srWindow).width();
|
||||
const int width = info.bufferSize().X;
|
||||
DWORD actual = 0;
|
||||
if (!FillConsoleOutputCharacterW(
|
||||
m_conout, L' ', width * count, Coord(0, row),
|
||||
@ -82,6 +82,11 @@ void Win32Console::clearLines(
|
||||
}
|
||||
}
|
||||
|
||||
void Win32Console::clearAllLines(const ConsoleScreenBufferInfo &info)
|
||||
{
|
||||
clearLines(0, info.bufferSize().Y, info);
|
||||
}
|
||||
|
||||
ConsoleScreenBufferInfo Win32Console::bufferInfo()
|
||||
{
|
||||
// TODO: error handling
|
||||
@ -196,3 +201,10 @@ void Win32Console::setTitle(const std::wstring &title)
|
||||
trace("SetConsoleTitleW failed");
|
||||
}
|
||||
}
|
||||
|
||||
void Win32Console::setTextAttribute(WORD attributes)
|
||||
{
|
||||
if (!SetConsoleTextAttribute(m_conout, attributes)) {
|
||||
trace("SetConsoleTextAttribute failed");
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ public:
|
||||
HWND hwnd();
|
||||
void postCloseMessage();
|
||||
void clearLines(int row, int count, const ConsoleScreenBufferInfo &info);
|
||||
void clearAllLines(const ConsoleScreenBufferInfo &info);
|
||||
|
||||
// Buffer and window sizes.
|
||||
ConsoleScreenBufferInfo bufferInfo();
|
||||
@ -75,6 +76,8 @@ public:
|
||||
std::wstring title();
|
||||
void setTitle(const std::wstring &title);
|
||||
|
||||
void setTextAttribute(WORD attributes);
|
||||
|
||||
private:
|
||||
HANDLE m_conin;
|
||||
HANDLE m_conout;
|
||||
|
Loading…
Reference in New Issue
Block a user