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:
parent
847749f19e
commit
72cbe59078
2
.github/actions/spelling/expect/expect.txt
vendored
2
.github/actions/spelling/expect/expect.txt
vendored
|
@ -2843,6 +2843,8 @@ XSubstantial
|
|||
xtended
|
||||
xterm
|
||||
XTest
|
||||
XTPUSHSGR
|
||||
XTPOPSGR
|
||||
xunit
|
||||
xutr
|
||||
xvalue
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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...");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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
114
src/types/inc/sgrStack.hpp
Normal 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;
|
||||
};
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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
212
src/types/sgrStack.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -44,6 +44,7 @@ SOURCES= \
|
|||
..\utils.cpp \
|
||||
..\ThemeUtils.cpp \
|
||||
..\ScreenInfoUiaProviderBase.cpp \
|
||||
..\sgrStack.cpp \
|
||||
..\UiaTextRangeBase.cpp \
|
||||
..\UiaTracing.cpp \
|
||||
..\TermControlUiaProvider.cpp \
|
||||
|
|
Loading…
Reference in a new issue