Add support for the "faint" graphic rendition attribute (#6873)

## Summary of the Pull Request

This PR adds support for the `SGR 2` escape sequence, which enables the
ANSI _faint_ graphic rendition attribute. When a character is output
with this attribute set, it uses a dimmer version of the active
foreground color.

## PR Checklist
* [x] Closes #6703
* [x] CLA signed. 
* [x] Tests added/passed
* [ ] Documentation updated.
* [ ] Schema updated.
* [x] I've discussed this with core contributors already. Issue number where discussion took place: #6703

## Detailed Description of the Pull Request / Additional comments

There was already an `ExtendedAttributes::Faint` flag in the
`TextAttribute` class, but I needed to add `SetFaint` and `IsFaint`
methods to access that flag, and update the `SetGraphicsRendition`
methods of the two dispatchers to set the attribute on receipt of the
`SGR 2` sequence. I also had to update the existing `SGR 22` handler to
reset _Faint_ in addition to _Bold_, since they share the same reset
sequence. For that reason, I thought it a good idea to change the name
of the `SGR 22` enum to `NotBoldOrFaint`.

For the purpose of rendering, I've updated the
`TextAttribute::CalculateRgbColors` method to return a dimmer version of
the foreground color when the _Faint_ attribute is set. This is simply
achieved by dividing each color component by two, which produces a
reasonable effect without being too complicated. Note that the _Faint_
effect is applied before _Reverse Video_, so if the output it reversed,
it's the background that will be faint.

The only other complication was the update of the `Xterm256Engine` in
the VT renderer. As mentioned above, _Bold_ and _Faint_ share the same
reset sequence, so to forward that state over conpty we have to go
through a slightly more complicated process than with other attributes.
We first check whether either attribute needs to be turned off to send
the reset sequence, and then check if the individual attributes need to
be turned on again.

## Validation

I've extended the existing SGR unit tests to cover the new attribute in
the `AdapterTest`, the `ScreenBufferTests`, and the `VtRendererTest`,
and added a test to confirm the color calculations  when _Faint_ is set
in the `TextAttributeTests`.

I've also done a bunch of manual testing with all the different VT color
types and confirmed that our output is comparable to most other
terminals.
This commit is contained in:
James Holderness 2020-07-13 18:44:09 +01:00 committed by GitHub
parent 1c8e83d52d
commit 7d677c5511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 131 additions and 15 deletions

View File

@ -97,6 +97,10 @@ std::pair<COLORREF, COLORREF> TextAttribute::CalculateRgbColors(const std::basic
{
auto fg = _foreground.GetColor(colorTable, defaultFgColor, IsBold());
auto bg = _background.GetColor(colorTable, defaultBgColor);
if (IsFaint())
{
fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two.
}
if (IsReverseVideo() ^ reverseScreenMode)
{
std::swap(fg, bg);
@ -211,6 +215,11 @@ bool TextAttribute::IsBold() const noexcept
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Bold);
}
bool TextAttribute::IsFaint() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Faint);
}
bool TextAttribute::IsItalic() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Italics);
@ -252,6 +261,11 @@ void TextAttribute::SetBold(bool isBold) noexcept
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Bold, isBold);
}
void TextAttribute::SetFaint(bool isFaint) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Faint, isFaint);
}
void TextAttribute::SetItalics(bool isItalic) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Italics, isItalic);

View File

@ -89,6 +89,7 @@ public:
bool IsLegacy() const noexcept;
bool IsBold() const noexcept;
bool IsFaint() const noexcept;
bool IsItalic() const noexcept;
bool IsBlinking() const noexcept;
bool IsInvisible() const noexcept;
@ -98,6 +99,7 @@ public:
bool IsReverseVideo() const noexcept;
void SetBold(bool isBold) noexcept;
void SetFaint(bool isFaint) noexcept;
void SetItalics(bool isItalic) noexcept;
void SetBlinking(bool isBlinking) noexcept;
void SetInvisible(bool isInvisible) noexcept;

View File

@ -130,6 +130,7 @@ void TextAttributeTests::TestRoundtripExhaustive()
void TextAttributeTests::TestTextAttributeColorGetters()
{
const COLORREF red = RGB(255, 0, 0);
const COLORREF faintRed = RGB(127, 0, 0);
const COLORREF green = RGB(0, 255, 0);
TextAttribute attr(red, green);
auto view = _GetTableView();
@ -149,6 +150,25 @@ void TextAttributeTests::TestTextAttributeColorGetters()
VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg));
VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg));
VERIFY_ARE_EQUAL(std::make_pair(green, red), attr.CalculateRgbColors(view, _defaultFg, _defaultBg));
// reset the reverse video
attr.SetReverseVideo(false);
// with faint set, the calculated foreground value should be fainter
// while the background and getters stay the same
attr.SetFaint(true);
VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg));
VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg));
VERIFY_ARE_EQUAL(std::make_pair(faintRed, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg));
// with reverse video set, calculated foreground/background values should be
// switched, and the background fainter, while getters stay the same
attr.SetReverseVideo(true);
VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg));
VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg));
VERIFY_ARE_EQUAL(std::make_pair(green, faintRed), attr.CalculateRgbColors(view, _defaultFg, _defaultBg));
}
void TextAttributeTests::TestReverseDefaultColors()

View File

@ -118,8 +118,12 @@ bool TerminalDispatch::SetGraphicsRendition(const std::basic_string_view<Dispatc
case BoldBright:
attr.SetBold(true);
break;
case UnBold:
case RGBColorOrFaint:
attr.SetFaint(true);
break;
case NotBoldOrFaint:
attr.SetBold(false);
attr.SetFaint(false);
break;
case Italics:
attr.SetItalics(true);

View File

@ -5014,14 +5014,16 @@ void ScreenBufferTests::TestExtendedTextAttributes()
// Run this test for each and every possible combination of states.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:bold", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:faint", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:italics", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:blink", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:invisible", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:crossedOut", L"{false, true}")
END_TEST_METHOD_PROPERTIES()
bool bold, italics, blink, invisible, crossedOut;
bool bold, faint, italics, blink, invisible, crossedOut;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"bold", bold));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"faint", faint));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible));
@ -5043,6 +5045,11 @@ void ScreenBufferTests::TestExtendedTextAttributes()
WI_SetFlag(expectedAttrs, ExtendedAttributes::Bold);
vtSeq += L"\x1b[1m";
}
if (faint)
{
WI_SetFlag(expectedAttrs, ExtendedAttributes::Faint);
vtSeq += L"\x1b[2m";
}
if (italics)
{
WI_SetFlag(expectedAttrs, ExtendedAttributes::Italics);
@ -5100,9 +5107,10 @@ void ScreenBufferTests::TestExtendedTextAttributes()
// One-by-one, turn off each of these states with VT, then check that the
// state matched.
if (bold)
if (bold || faint)
{
WI_ClearFlag(expectedAttrs, ExtendedAttributes::Bold);
// The bold and faint attributes share the same reset sequence.
WI_ClearAllFlags(expectedAttrs, ExtendedAttributes::Bold | ExtendedAttributes::Faint);
vtSeq = L"\x1b[22m";
validate(expectedAttrs, vtSeq);
}
@ -5147,6 +5155,7 @@ void ScreenBufferTests::TestExtendedTextAttributesWithColors()
// Run this test for each and every possible combination of states.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:bold", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:faint", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:italics", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:blink", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:invisible", L"{false, true}")
@ -5162,8 +5171,9 @@ void ScreenBufferTests::TestExtendedTextAttributesWithColors()
const int Use256Color = 2;
const int UseRGBColor = 3;
bool bold, italics, blink, invisible, crossedOut;
bool bold, faint, italics, blink, invisible, crossedOut;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"bold", bold));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"faint", faint));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible));
@ -5189,6 +5199,11 @@ void ScreenBufferTests::TestExtendedTextAttributesWithColors()
expectedAttr.SetBold(true);
vtSeq += L"\x1b[1m";
}
if (faint)
{
expectedAttr.SetFaint(true);
vtSeq += L"\x1b[2m";
}
if (italics)
{
expectedAttr.SetItalics(true);
@ -5290,9 +5305,11 @@ void ScreenBufferTests::TestExtendedTextAttributesWithColors()
// One-by-one, turn off each of these states with VT, then check that the
// state matched.
if (bold)
if (bold || faint)
{
// The bold and faint attributes share the same reset sequence.
expectedAttr.SetBold(false);
expectedAttr.SetFaint(false);
vtSeq = L"\x1b[22m";
validate(expectedAttr, vtSeq);
}

View File

@ -660,13 +660,15 @@ void VtRendererTest::Xterm256TestExtendedAttributes()
{
// Run this test for each and every possible combination of states.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:faint", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:italics", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:blink", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:invisible", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:crossedOut", L"{false, true}")
END_TEST_METHOD_PROPERTIES()
bool italics, blink, invisible, crossedOut;
bool faint, italics, blink, invisible, crossedOut;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"faint", faint));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible));
@ -676,6 +678,12 @@ void VtRendererTest::Xterm256TestExtendedAttributes()
std::vector<std::string> onSequences, offSequences;
// Collect up a VT sequence to set the state given the method properties
if (faint)
{
desiredAttrs.SetFaint(true);
onSequences.push_back("\x1b[2m");
offSequences.push_back("\x1b[22m");
}
if (italics)
{
desiredAttrs.SetItalics(true);
@ -746,7 +754,7 @@ void VtRendererTest::Xterm256TestExtendedAttributes()
void VtRendererTest::Xterm256TestAttributesAcrossReset()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:renditionAttribute", L"{1, 3, 4, 5, 7, 8, 9, 53}")
TEST_METHOD_PROPERTY(L"Data:renditionAttribute", L"{1, 2, 3, 4, 5, 7, 8, 9, 53}")
END_TEST_METHOD_PROPERTIES()
int renditionAttribute;
@ -774,6 +782,10 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset()
Log::Comment(L"----Set Bold Attribute----");
textAttributes.SetBold(true);
break;
case GraphicsOptions::RGBColorOrFaint:
Log::Comment(L"----Set Faint Attribute----");
textAttributes.SetFaint(true);
break;
case GraphicsOptions::Italics:
Log::Comment(L"----Set Italics Attribute----");
textAttributes.SetItalics(true);

View File

@ -19,7 +19,7 @@ enum class ExtendedAttributes : BYTE
// TODO:GH#2916 add support for these to the parser as well.
Underlined = 0x20, // _technically_ different from LVB_UNDERSCORE, see TODO:GH#2915
DoublyUnderlined = 0x40, // Included for completeness, but not currently supported.
Faint = 0x80, // Included for completeness, but not currently supported.
Faint = 0x80,
};
DEFINE_ENUM_FLAG_OPERATORS(ExtendedAttributes);

View File

@ -344,6 +344,17 @@ using namespace Microsoft::Console::Render;
return _Write(isBold ? "\x1b[1m" : "\x1b[22m");
}
// Method Description:
// - Formats and writes a sequence to change the faintness of the following text.
// Arguments:
// - isFaint: If true, we'll make the text faint. Otherwise we'll remove the faintness.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetFaint(const bool isFaint) noexcept
{
return _Write(isFaint ? "\x1b[2m" : "\x1b[22m");
}
// Method Description:
// - Formats and writes a sequence to change the underline of the following text.
// Arguments:

View File

@ -41,10 +41,28 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe,
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT Xterm256Engine::_UpdateExtendedAttrs(const TextAttribute& textAttributes) noexcept
{
if (textAttributes.IsBold() != _lastTextAttributes.IsBold())
// Turning off Bold and Faint must be handled at the same time,
// since there is only one sequence that resets both of them.
const auto boldTurnedOff = !textAttributes.IsBold() && _lastTextAttributes.IsBold();
const auto faintTurnedOff = !textAttributes.IsFaint() && _lastTextAttributes.IsFaint();
if (boldTurnedOff || faintTurnedOff)
{
RETURN_IF_FAILED(_SetBold(textAttributes.IsBold()));
_lastTextAttributes.SetBold(textAttributes.IsBold());
RETURN_IF_FAILED(_SetBold(false));
_lastTextAttributes.SetBold(false);
_lastTextAttributes.SetFaint(false);
}
// Once we've handled the cases where they need to be turned off,
// we can then check if either should be turned back on again.
if (textAttributes.IsBold() && !_lastTextAttributes.IsBold())
{
RETURN_IF_FAILED(_SetBold(true));
_lastTextAttributes.SetBold(true);
}
if (textAttributes.IsFaint() && !_lastTextAttributes.IsFaint())
{
RETURN_IF_FAILED(_SetFaint(true));
_lastTextAttributes.SetFaint(true);
}
if (textAttributes.IsUnderlined() != _lastTextAttributes.IsUnderlined())

View File

@ -186,6 +186,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT _ResizeWindow(const short sWidth, const short sHeight) noexcept;
[[nodiscard]] HRESULT _SetBold(const bool isBold) noexcept;
[[nodiscard]] HRESULT _SetFaint(const bool isFaint) noexcept;
[[nodiscard]] HRESULT _SetUnderline(const bool isUnderlined) noexcept;
[[nodiscard]] HRESULT _SetOverline(const bool isUnderlined) noexcept;
[[nodiscard]] HRESULT _SetItalics(const bool isItalic) noexcept;

View File

@ -28,7 +28,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
Invisible = 8,
CrossedOut = 9,
DoublyUnderlined = 21,
UnBold = 22,
NotBoldOrFaint = 22,
NotItalics = 23,
NoUnderline = 24,
Steady = 25, // _not_ blink

View File

@ -127,8 +127,12 @@ bool AdaptDispatch::SetGraphicsRendition(const std::basic_string_view<DispatchTy
case BoldBright:
attr.SetBold(true);
break;
case UnBold:
case RGBColorOrFaint:
attr.SetFaint(true);
break;
case NotBoldOrFaint:
attr.SetBold(false);
attr.SetFaint(false);
break;
case Italics:
attr.SetItalics(true);

View File

@ -1261,7 +1261,7 @@ public:
TEST_METHOD(GraphicsSingleTests)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:uiGraphicsOptions", L"{0, 1, 4, 7, 24, 27, 30, 31, 32, 33, 34, 35, 36, 37, 39, 40, 41, 42, 43, 44, 45, 46, 47, 49, 53, 55, 90, 91, 92, 93, 94, 95, 96, 97, 100, 101, 102, 103, 104, 105, 106, 107}") // corresponds to options in DispatchTypes::GraphicsOptions
TEST_METHOD_PROPERTY(L"Data:uiGraphicsOptions", L"{0, 1, 2, 4, 7, 22, 24, 27, 30, 31, 32, 33, 34, 35, 36, 37, 39, 40, 41, 42, 43, 44, 45, 46, 47, 49, 53, 55, 90, 91, 92, 93, 94, 95, 96, 97, 100, 101, 102, 103, 104, 105, 106, 107}") // corresponds to options in DispatchTypes::GraphicsOptions
END_TEST_METHOD_PROPERTIES()
Log::Comment(L"Starting test...");
@ -1290,6 +1290,12 @@ public:
_testGetSet->_expectedAttribute = TextAttribute{ 0 };
_testGetSet->_expectedAttribute.SetBold(true);
break;
case DispatchTypes::GraphicsOptions::RGBColorOrFaint:
Log::Comment(L"Testing graphics 'Faint'");
_testGetSet->_attribute = TextAttribute{ 0 };
_testGetSet->_expectedAttribute = TextAttribute{ 0 };
_testGetSet->_expectedAttribute.SetFaint(true);
break;
case DispatchTypes::GraphicsOptions::Underline:
Log::Comment(L"Testing graphics 'Underline'");
_testGetSet->_attribute = TextAttribute{ 0 };
@ -1305,6 +1311,13 @@ public:
_testGetSet->_attribute = TextAttribute{ 0 };
_testGetSet->_expectedAttribute = TextAttribute{ COMMON_LVB_REVERSE_VIDEO };
break;
case DispatchTypes::GraphicsOptions::NotBoldOrFaint:
Log::Comment(L"Testing graphics 'No Bold or Faint'");
_testGetSet->_attribute = TextAttribute{ 0 };
_testGetSet->_attribute.SetBold(true);
_testGetSet->_attribute.SetFaint(true);
_testGetSet->_expectedAttribute = TextAttribute{ 0 };
break;
case DispatchTypes::GraphicsOptions::NoUnderline:
Log::Comment(L"Testing graphics 'No Underline'");
_testGetSet->_attribute = TextAttribute{ COMMON_LVB_UNDERSCORE };