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:
Ryan Prichard 2015-10-13 02:12:59 -05:00
parent 9cf6972860
commit a998bcf207
5 changed files with 167 additions and 55 deletions

View File

@ -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);

View File

@ -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)

View File

@ -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

View File

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

View File

@ -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;