terminal/src/types/inc/sgrStack.hpp
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

115 lines
5.6 KiB
C++

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