Add support for XTPUSHSGR / XTPOPSGR (#1978)

Implement the `XTPUSHSGR` and `XTPOPSGR` control sequences (see #1796).

This change adds a new pair of methods to `ITermDispatch`:
`PushGraphicsRendition` and `PopGraphicsRendition`, and then plumbs the
change through `AdaptDispatch`, `TerminalDispatch`, `ITerminalApi` and
`TerminalApi`.

The stack logic is encapsulated in the `SgrStack` class, to allow it to
be reused between the two APIs (`AdaptDispatch` and `TerminalDispatch`).

Like xterm, only ten levels of nesting are supported.

The stack is implemented as a "ring stack": if you push when the stack
is full, the bottom of the stack will be dropped to make room.

Partial pushes (see the description of `XTPUSHSGR` in Issue #1796) are
implemented per xterm spec.

## Validation Steps Performed
Tests added, plus manual verification of the feature.

Closes #1796
This commit is contained in:
Dan Thompson 2021-02-17 18:31:52 -08:00 committed by GitHub
parent 847749f19e
commit 72cbe59078
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 641 additions and 1 deletions

View file

@ -2843,6 +2843,8 @@ XSubstantial
xtended
xterm
XTest
XTPUSHSGR
XTPOPSGR
xunit
xutr
xvalue

View file

@ -70,6 +70,9 @@ namespace Microsoft::Terminal::Core
virtual bool SetWorkingDirectory(std::wstring_view uri) noexcept = 0;
virtual std::wstring_view GetWorkingDirectory() noexcept = 0;
virtual bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept = 0;
virtual bool PopGraphicsRendition() noexcept = 0;
protected:
ITerminalApi() = default;
};

View file

@ -6,6 +6,7 @@
#include <conattrs.hpp>
#include "../../buffer/out/textBuffer.hpp"
#include "../../types/inc/sgrStack.hpp"
#include "../../renderer/inc/BlinkingState.hpp"
#include "../../terminal/parser/StateMachine.hpp"
#include "../../terminal/input/terminalInput.hpp"
@ -124,6 +125,10 @@ public:
bool SetTaskbarProgress(const size_t state, const size_t progress) noexcept override;
bool SetWorkingDirectory(std::wstring_view uri) noexcept override;
std::wstring_view GetWorkingDirectory() noexcept override;
bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override;
bool PopGraphicsRendition() noexcept override;
#pragma endregion
#pragma region ITerminalInput
@ -347,6 +352,8 @@ private:
COORD _ConvertToBufferCell(const COORD viewportPos) const;
#pragma endregion
Microsoft::Console::VirtualTerminal::SgrStack _sgrStack;
#ifdef UNIT_TESTING
friend class TerminalCoreUnitTests::TerminalBufferTests;
friend class TerminalCoreUnitTests::TerminalApiTest;

View file

@ -640,3 +640,31 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept
{
return _workingDirectory;
}
// Method Description:
// - Saves the current text attributes to an internal stack.
// Arguments:
// - options, cOptions: if present, specify which portions of the current text attributes
// should be saved. Only a small subset of GraphicsOptions are actually supported;
// others are ignored. If no options are specified, all attributes are stored.
// Return Value:
// - true
bool Terminal::PushGraphicsRendition(const VTParameters options) noexcept
{
_sgrStack.Push(_buffer->GetCurrentAttributes(), options);
return true;
}
// Method Description:
// - Restores text attributes from the internal stack. If only portions of text attributes
// were saved, combines those with the current attributes.
// Arguments:
// - <none>
// Return Value:
// - true
bool Terminal::PopGraphicsRendition() noexcept
{
const TextAttribute current = _buffer->GetCurrentAttributes();
_buffer->SetCurrentAttributes(_sgrStack.Pop(current));
return true;
}

View file

@ -18,6 +18,9 @@ public:
bool SetGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override;
bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override;
bool PopGraphicsRendition() noexcept override;
bool CursorPosition(const size_t line,
const size_t column) noexcept override; // CUP

View file

@ -276,3 +276,13 @@ bool TerminalDispatch::SetGraphicsRendition(const VTParameters options) noexcept
_terminalApi.SetTextAttributes(attr);
return true;
}
bool TerminalDispatch::PushGraphicsRendition(const VTParameters options) noexcept
{
return _terminalApi.PushGraphicsRendition(options);
}
bool TerminalDispatch::PopGraphicsRendition() noexcept
{
return _terminalApi.PopGraphicsRendition();
}

View file

@ -311,6 +311,40 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
BrightBackgroundWhite = 107,
};
// Many of these correspond directly to SGR parameters (the GraphicsOptions enum), but
// these are distinct (notably 10 and 11, which as SGR parameters would select fonts,
// are used here to indicate that the foreground/background colors should be saved).
// From xterm's ctlseqs doc for XTPUSHSGR:
//
// Ps = 1 => Bold.
// Ps = 2 => Faint.
// Ps = 3 => Italicized.
// Ps = 4 => Underlined.
// Ps = 5 => Blink.
// Ps = 7 => Inverse.
// Ps = 8 => Invisible.
// Ps = 9 => Crossed-out characters.
// Ps = 2 1 => Doubly-underlined.
// Ps = 3 0 => Foreground color.
// Ps = 3 1 => Background color.
//
enum class SgrSaveRestoreStackOptions : size_t
{
All = 0,
Boldness = 1,
Faintness = 2,
Italics = 3,
Underline = 4,
Blink = 5,
Negative = 7,
Invisible = 8,
CrossedOut = 9,
DoublyUnderlined = 21,
SaveForegroundColor = 30,
SaveBackgroundColor = 31,
Max = SaveBackgroundColor
};
enum class AnsiStatusType : size_t
{
OS_OperatingStatus = 5,

View file

@ -89,6 +89,9 @@ public:
virtual bool SetGraphicsRendition(const VTParameters options) = 0; // SGR
virtual bool PushGraphicsRendition(const VTParameters options) = 0; // XTPUSHSGR
virtual bool PopGraphicsRendition() = 0; // XTPOPSGR
virtual bool SetMode(const DispatchTypes::ModeParams param) = 0; // DECSET
virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // DECRST

View file

@ -19,6 +19,7 @@ Author(s):
#include "conGetSet.hpp"
#include "adaptDefaults.hpp"
#include "terminalOutput.hpp"
#include "..\..\types\inc\sgrStack.hpp"
namespace Microsoft::Console::VirtualTerminal
{
@ -56,6 +57,8 @@ namespace Microsoft::Console::VirtualTerminal
bool InsertCharacter(const size_t count) override; // ICH
bool DeleteCharacter(const size_t count) override; // DCH
bool SetGraphicsRendition(const VTParameters options) override; // SGR
bool PushGraphicsRendition(const VTParameters options) override; // XTPUSHSGR
bool PopGraphicsRendition() override; // XTPOPSGR
bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) override; // DSR, DSR-OS, DSR-CPR
bool DeviceAttributes() override; // DA1
bool SecondaryDeviceAttributes() override; // DA2
@ -199,6 +202,8 @@ namespace Microsoft::Console::VirtualTerminal
bool _isDECCOLMAllowed;
SgrStack _sgrStack;
size_t _SetRgbColorsHelper(const VTParameters options,
TextAttribute& attr,
const bool isForeground) noexcept;

View file

@ -286,3 +286,48 @@ bool AdaptDispatch::SetGraphicsRendition(const VTParameters options)
return success;
}
// Method Description:
// - Saves the current text attributes to an internal stack.
// Arguments:
// - options: if not empty, specify which portions of the current text attributes should
// be saved. Options that are not supported are ignored. If no options are specified,
// all attributes are stored.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::PushGraphicsRendition(const VTParameters options)
{
bool success = true;
TextAttribute currentAttributes;
success = _pConApi->PrivateGetTextAttributes(currentAttributes);
if (success)
{
_sgrStack.Push(currentAttributes, options);
}
return success;
}
// Method Description:
// - Restores text attributes from the internal stack. If only portions of text attributes
// were saved, combines those with the current attributes.
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::PopGraphicsRendition()
{
bool success = true;
TextAttribute currentAttributes;
success = _pConApi->PrivateGetTextAttributes(currentAttributes);
if (success)
{
success = _pConApi->PrivateSetTextAttributes(_sgrStack.Pop(currentAttributes));
}
return success;
}

View file

@ -83,6 +83,9 @@ public:
bool SetGraphicsRendition(const VTParameters /*options*/) noexcept override { return false; } // SGR
bool PushGraphicsRendition(const VTParameters /*options*/) noexcept override { return false; } // XTPUSHSGR
bool PopGraphicsRendition() noexcept override { return false; } // XTPOPSGR
bool SetMode(const DispatchTypes::ModeParams /*param*/) noexcept override { return false; } // DECSET
bool ResetMode(const DispatchTypes::ModeParams /*param*/) noexcept override { return false; } // DECRST

View file

@ -57,6 +57,9 @@
<ProjectReference Include="..\lib\adapter.vcxproj">
<Project>{dcf55140-ef6a-4736-a403-957e4f7430bb}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\buffer\out\lib\bufferout.vcxproj">
<Project>{0cf235bd-2da0-407e-90ee-c467e8bbc714}</Project>
</ProjectReference>
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>

View file

@ -1599,6 +1599,144 @@ public:
VERIFY_IS_TRUE(_pDispatch.get()->SetGraphicsRendition({ rgOptions, cOptions }));
}
TEST_METHOD(GraphicsPushPopTests)
{
Log::Comment(L"Starting test...");
_testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
VTParameter rgOptions[16];
VTParameter rgStackOptions[16];
size_t cOptions = 1;
Log::Comment(L"Test 1: Basic push and pop");
rgOptions[0] = DispatchTypes::GraphicsOptions::Off;
_testGetSet->_expectedAttribute = {};
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
cOptions = 0;
VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions }));
VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition());
Log::Comment(L"Test 2: Push, change color, pop");
VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions }));
cOptions = 1;
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundCyan;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(3);
_testGetSet->_expectedAttribute.SetDefaultBackground();
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
cOptions = 0;
_testGetSet->_expectedAttribute = {};
VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition());
Log::Comment(L"Test 3: two pushes (nested) and pops");
// First push:
VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions }));
cOptions = 1;
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundRed;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED);
_testGetSet->_expectedAttribute.SetDefaultBackground();
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
// Second push:
cOptions = 0;
VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions }));
cOptions = 1;
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundGreen;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN);
_testGetSet->_expectedAttribute.SetDefaultBackground();
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
// First pop:
cOptions = 0;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED);
_testGetSet->_expectedAttribute.SetDefaultBackground();
VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition());
// Second pop:
cOptions = 0;
_testGetSet->_expectedAttribute = {};
VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition());
Log::Comment(L"Test 4: Save and restore partial attributes");
cOptions = 1;
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundGreen;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN);
_testGetSet->_expectedAttribute.SetDefaultBackground();
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
cOptions = 1;
rgOptions[0] = DispatchTypes::GraphicsOptions::BoldBright;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN);
_testGetSet->_expectedAttribute.SetBold(true);
_testGetSet->_expectedAttribute.SetDefaultBackground();
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundBlue;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN);
_testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_BLUE >> 4);
_testGetSet->_expectedAttribute.SetBold(true);
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
// Push, specifying that we only want to save the background, the boldness, and double-underline-ness:
cOptions = 3;
rgStackOptions[0] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::Boldness;
rgStackOptions[1] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::SaveBackgroundColor;
rgStackOptions[2] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::DoublyUnderlined;
VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions }));
// Now change everything...
cOptions = 2;
rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundGreen;
rgOptions[1] = DispatchTypes::GraphicsOptions::DoublyUnderlined;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN);
_testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_GREEN >> 4);
_testGetSet->_expectedAttribute.SetBold(true);
_testGetSet->_expectedAttribute.SetDoublyUnderlined(true);
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
cOptions = 1;
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundRed;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED);
_testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_GREEN >> 4);
_testGetSet->_expectedAttribute.SetBold(true);
_testGetSet->_expectedAttribute.SetDoublyUnderlined(true);
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
rgOptions[0] = DispatchTypes::GraphicsOptions::NotBoldOrFaint;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED);
_testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_GREEN >> 4);
_testGetSet->_expectedAttribute.SetDoublyUnderlined(true);
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions }));
// And then restore...
cOptions = 0;
_testGetSet->_expectedAttribute = {};
_testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED);
_testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_BLUE >> 4);
_testGetSet->_expectedAttribute.SetBold(true);
VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition());
}
TEST_METHOD(GraphicsPersistBrightnessTests)
{
Log::Comment(L"Starting test...");

View file

@ -589,6 +589,19 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
success = _dispatch->SoftReset();
TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTR);
break;
case CsiActionCodes::XT_PushSgr:
case CsiActionCodes::XT_PushSgrAlias:
success = _dispatch->PushGraphicsRendition(parameters);
TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPUSHSGR);
break;
case CsiActionCodes::XT_PopSgr:
case CsiActionCodes::XT_PopSgrAlias:
success = _dispatch->PopGraphicsRendition();
TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPOPSGR);
break;
default:
// If no functions to call, overall dispatch was a failure.
success = false;

View file

@ -131,7 +131,11 @@ namespace Microsoft::Console::VirtualTerminal
DECREQTPARM_RequestTerminalParameters = VTID("x"),
DECSCUSR_SetCursorStyle = VTID(" q"),
DECSTR_SoftReset = VTID("!p"),
DECSCPP_SetColumnsPerPage = VTID("$|")
XT_PushSgrAlias = VTID("#p"),
XT_PopSgrAlias = VTID("#q"),
XT_PushSgr = VTID("#{"),
XT_PopSgr = VTID("#}"),
DECSCPP_SetColumnsPerPage = VTID("$|"),
};
enum Vt52ActionCodes : uint64_t

View file

@ -274,6 +274,8 @@ void TermTelemetry::WriteFinalTraceLog() const
TraceLoggingUInt32(_uiTimesUsed[OSCSCB], "OscSetClipboard"),
TraceLoggingUInt32(_uiTimesUsed[REP], "REP"),
TraceLoggingUInt32(_uiTimesUsed[DECALN], "DECALN"),
TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"),
TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"),
TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"),
TraceLoggingUInt32(_uiTimesFailedOutsideRange, "FailedOutsideRange"));
}

View file

@ -101,6 +101,8 @@ namespace Microsoft::Console::VirtualTerminal
OSCBG,
DECALN,
OSCSCB,
XTPUSHSGR,
XTPOPSGR,
// Only use this last enum as a count of the number of codes.
NUMBER_OF_CODES
};

114
src/types/inc/sgrStack.hpp Normal file
View file

@ -0,0 +1,114 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- sgrStack.hpp
Abstract:
- Encapsulates logic for the XTPUSHSGR / XTPOPSGR VT control sequences, which save and
restore text attributes on a stack.
--*/
#pragma once
#include "..\..\buffer\out\TextAttribute.hpp"
#include "..\..\terminal\adapter\DispatchTypes.hpp"
#include <bitset>
namespace Microsoft::Console::VirtualTerminal
{
class SgrStack
{
public:
SgrStack() noexcept;
// Method Description:
// - Saves the specified text attributes onto an internal stack.
// Arguments:
// - currentAttributes - The attributes to save onto the stack.
// - options - If none supplied, the full attributes are saved. Else only the
// specified parts of currentAttributes are saved.
// Return Value:
// - <none>
void Push(const TextAttribute& currentAttributes,
const VTParameters options) noexcept;
// Method Description:
// - Restores text attributes by removing from the top of the internal stack,
// combining them with the supplied currentAttributes, if appropriate.
// Arguments:
// - currentAttributes - The current text attributes. If only a portion of
// attributes were saved on the internal stack, then those attributes will be
// combined with the currentAttributes passed in to form the return value.
// Return Value:
// - The TextAttribute that has been removed from the top of the stack, possibly
// combined with currentAttributes.
const TextAttribute Pop(const TextAttribute& currentAttributes) noexcept;
// Xterm allows the save stack to go ten deep, so we'll follow suit.
static constexpr int c_MaxStoredSgrPushes = 10;
private:
// Note the +1 in the size of the bitset: this is because we use the
// SgrSaveRestoreStackOptions enum values as bitset flags, so they are naturally
// one-based.
typedef std::bitset<static_cast<size_t>(DispatchTypes::SgrSaveRestoreStackOptions::Max) + 1> AttrBitset;
TextAttribute _CombineWithCurrentAttributes(const TextAttribute& currentAttributes,
const TextAttribute& savedAttribute,
const AttrBitset validParts); // valid parts of savedAttribute
struct SavedSgrAttributes
{
TextAttribute TextAttributes;
AttrBitset ValidParts; // flags that indicate which parts of TextAttributes are meaningful
};
// The number of "save slots" on the stack is limited (let's say there are N). So
// there are a couple of problems to think about: what to do about apps that try
// to do more pushes than will fit, and how to recover from garbage (such as
// accidentally running "cat" on a binary file that looks like lots of pushes).
//
// Dealing with more pops than pushes is simple: just ignore pops when the stack
// is empty.
//
// But how should we handle doing more pushes than are supported by the storage?
//
// One approach might be to ignore pushes once the stack is full. Things won't
// look right while the number of outstanding pushes is above the stack, but once
// it gets popped back down into range, things start working again. Put another
// way: with a traditional stack, the first N pushes work, and the last N pops
// work. But that introduces a burden: you have to do something (lots of pops) in
// order to recover from garbage. (There are strategies that could be employed to
// place an upper bound on how many pops are required (say K), but it's still
// something that /must/ be done to recover from a blown stack.)
//
// An alternative approach is a "ring stack": if you do another push when the
// stack is already full, it just drops the bottom of the stack. With this
// strategy, the last N pushes work, and the first N pops work. And the advantage
// of this approach is that there is no "recovery procedure" necessary: if you
// want a clean slate, you can just declare a clean slate--you will always have N
// slots for pushes and pops in front of you.
//
// A ring stack will also lead to apps that are friendlier to cross-app
// pushes/pops.
//
// Consider using a traditional stack. In that case, an app might be tempted to
// always begin by issuing a bunch of pops (K), in order to ensure they have a
// clean state. However, apps that behave that way would not work well with
// cross-app push/pops (e.g. I push before I ssh to my remote system, and will pop
// when after closing the connection, and during the connection I'll run apps on
// the remote host which might also do pushes and pops). By using a ring stack, an
// app does not need to do /anything/ to start in a "clean state"--an app can
// ALWAYS consider its initial state to be clean.
//
// So we've chosen to use a "ring stack", because it is simplest for apps to deal
// with.
int _nextPushIndex; // will wrap around once the stack is full
int _numSavedAttrs; // how much of _storedSgrAttributes is actually in use
std::array<SavedSgrAttributes, c_MaxStoredSgrPushes> _storedSgrAttributes;
};
}

View file

@ -22,6 +22,7 @@
<ClCompile Include="..\MenuEvent.cpp" />
<ClCompile Include="..\ModifierKeyState.cpp" />
<ClCompile Include="..\ScreenInfoUiaProviderBase.cpp" />
<ClCompile Include="..\sgrStack.cpp" />
<ClCompile Include="..\ThemeUtils.cpp" />
<ClCompile Include="..\UiaTextRangeBase.cpp" />
<ClCompile Include="..\UiaTracing.cpp" />
@ -44,6 +45,7 @@
<ClInclude Include="..\inc\Environment.hpp" />
<ClInclude Include="..\inc\GlyphWidth.hpp" />
<ClInclude Include="..\inc\IInputEvent.hpp" />
<ClInclude Include="..\inc\sgrStack.hpp" />
<ClInclude Include="..\inc\ThemeUtils.h" />
<ClInclude Include="..\inc\utils.hpp" />
<ClInclude Include="..\inc\Viewport.hpp" />

View file

@ -72,6 +72,9 @@
<ClCompile Include="..\Environment.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\sgrStack.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\UiaTracing.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@ -161,6 +164,9 @@
<ClInclude Include="..\inc\Environment.hpp">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\inc\sgrStack.hpp">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\UiaTracing.h">
<Filter>Header Files</Filter>
</ClInclude>

212
src/types/sgrStack.cpp Normal file
View file

@ -0,0 +1,212 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "inc/sgrStack.hpp"
using namespace Microsoft::Console::VirtualTerminal::DispatchTypes;
namespace Microsoft::Console::VirtualTerminal
{
SgrStack::SgrStack() noexcept :
_nextPushIndex{ 0 },
_numSavedAttrs{ 0 }
{
}
void SgrStack::Push(const TextAttribute& currentAttributes,
const VTParameters options) noexcept
{
AttrBitset validParts;
try
{
if (options.empty())
{
// We save all current attributes.
validParts.set(static_cast<size_t>(SgrSaveRestoreStackOptions::All));
}
else
{
// Each option is encoded as a bit in validParts. All options (that fit) are
// encoded; options that aren't supported are ignored when read back (popped).
// So if you try to save only unsupported aspects of the current text
// attributes, you'll do what is effectively an "empty" push (the subsequent
// pop will not change the current attributes), which is the correct behavior.
for (size_t i = 0; i < options.size(); i++)
{
const size_t optionAsIndex = options.at(i).value_or(0);
// Options must be specified singly; not in combination. Values that are
// out of range will be ignored.
if (optionAsIndex < validParts.size())
{
validParts.set(optionAsIndex);
}
}
}
}
catch (...)
{
// The static analyzer knows that the bitset operations can throw
// std::out_of_range. However, we know that won't happen, because we pre-check
// that everything should be in range. So we plan to never execute this
// failfast:
FAIL_FAST_CAUGHT_EXCEPTION();
}
if (_numSavedAttrs < gsl::narrow<int>(_storedSgrAttributes.size()))
{
_numSavedAttrs++;
}
_storedSgrAttributes.at(_nextPushIndex) = { currentAttributes, validParts };
_nextPushIndex = (_nextPushIndex + 1) % gsl::narrow<int>(_storedSgrAttributes.size());
}
const TextAttribute SgrStack::Pop(const TextAttribute& currentAttributes) noexcept
{
if (_numSavedAttrs > 0)
{
_numSavedAttrs--;
if (_nextPushIndex == 0)
{
_nextPushIndex = gsl::narrow<int>(_storedSgrAttributes.size() - 1);
}
else
{
_nextPushIndex--;
}
SavedSgrAttributes& restoreMe = _storedSgrAttributes.at(_nextPushIndex);
try
{
if (restoreMe.ValidParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::All)))
{
return restoreMe.TextAttributes;
}
else
{
return _CombineWithCurrentAttributes(currentAttributes,
restoreMe.TextAttributes,
restoreMe.ValidParts);
}
}
catch (...)
{
// The static analyzer knows that the bitset operations can throw
// std::out_of_range. However, we know that won't happen, because we
// pre-check that everything should be in range. So we plan to never
// execute this failfast:
FAIL_FAST_CAUGHT_EXCEPTION();
}
}
return currentAttributes;
}
TextAttribute SgrStack::_CombineWithCurrentAttributes(const TextAttribute& currentAttributes,
const TextAttribute& savedAttribute,
const AttrBitset validParts) // of savedAttribute
{
// If we are restoring all attributes, we should have just taken savedAttribute
// before we even got here.
FAIL_FAST_IF(validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::All)));
TextAttribute result = currentAttributes;
// From xterm documentation:
//
// CSI # {
// CSI Ps ; Ps # {
// Push video attributes onto stack (XTPUSHSGR), xterm. The
// optional parameters correspond to the SGR encoding for video
// attributes, except for colors (which do not have a unique SGR
// code):
// Ps = 1 -> Bold.
// Ps = 2 -> Faint.
// Ps = 3 -> Italicized.
// Ps = 4 -> Underlined.
// Ps = 5 -> Blink.
// Ps = 7 -> Inverse.
// Ps = 8 -> Invisible.
// Ps = 9 -> Crossed-out characters.
// Ps = 2 1 -> Doubly-underlined.
// Ps = 3 0 -> Foreground color.
// Ps = 3 1 -> Background color.
//
// (some closing braces for people with editors that get thrown off without them: }})
// Boldness = 1,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::Boldness)))
{
result.SetBold(savedAttribute.IsBold());
}
// Faintness = 2,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::Faintness)))
{
result.SetFaint(savedAttribute.IsFaint());
}
// Italics = 3,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::Italics)))
{
result.SetItalic(savedAttribute.IsItalic());
}
// Underline = 4,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::Underline)))
{
result.SetUnderlined(savedAttribute.IsUnderlined());
}
// Blink = 5,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::Blink)))
{
result.SetBlinking(savedAttribute.IsBlinking());
}
// Negative = 7,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::Negative)))
{
result.SetReverseVideo(savedAttribute.IsReverseVideo());
}
// Invisible = 8,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::Invisible)))
{
result.SetInvisible(savedAttribute.IsInvisible());
}
// CrossedOut = 9,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::CrossedOut)))
{
result.SetCrossedOut(savedAttribute.IsCrossedOut());
}
// DoublyUnderlined = 21,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::DoublyUnderlined)))
{
result.SetDoublyUnderlined(savedAttribute.IsDoublyUnderlined());
}
// SaveForegroundColor = 30,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::SaveForegroundColor)))
{
result.SetForeground(savedAttribute.GetForeground());
}
// SaveBackgroundColor = 31,
if (validParts.test(static_cast<size_t>(SgrSaveRestoreStackOptions::SaveBackgroundColor)))
{
result.SetBackground(savedAttribute.GetBackground());
}
return result;
}
}

View file

@ -44,6 +44,7 @@ SOURCES= \
..\utils.cpp \
..\ThemeUtils.cpp \
..\ScreenInfoUiaProviderBase.cpp \
..\sgrStack.cpp \
..\UiaTextRangeBase.cpp \
..\UiaTracing.cpp \
..\TermControlUiaProvider.cpp \