terminal/src/types/sgrStack.cpp
Dan Thompson 72cbe59078
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
2021-02-17 18:31:52 -08:00

213 lines
7.7 KiB
C++

// 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;
}
}