terminal/src/terminal/adapter/adaptDispatch.cpp
James Holderness 6742965bb8
Disable the acceptance of C1 control codes by default (#11690)
There are some code pages with "unmapped" code points in the C1 range,
which results in them being translated into Unicode C1 control codes,
even though that is not their intended use. To avoid having these
characters triggering unintentional escape sequences, this PR now
disables C1 controls by default.

Switching to ISO-2022 encoding will re-enable them, though, since that
is the most likely scenario in which they would be required. They can
also be explicitly enabled, even in UTF-8 mode, with the `DECAC1` escape
sequence.

What I've done is add a new mode to the `StateMachine` class that
controls whether C1 code points are interpreted as control characters or
not. When disabled, these code points are simply dropped from the
output, similar to the way a `NUL` is interpreted.

This isn't exactly the way they were handled in the v1 console (which I
think replaces them with the font _notdef_ glyph), but it matches the
XTerm behavior, which seems more appropriate considering this is in VT
mode. And it's worth noting that Windows Explorer seems to work the same
way.

As mentioned above, the mode can be enabled by designating the ISO-2022
coding system with a `DOCS` sequence, and it will be disabled again when
UTF-8 is designated. You can also enable it explicitly with a `DECAC1`
sequence (originally this was actually a DEC printer sequence, but it
doesn't seem unreasonable to use it in a terminal).

I've also extended the operations that save and restore "cursor state"
(e.g. `DECSC` and `DECRC`) to include the state of the C1 parser mode,
since it's closely tied to the code page and character sets which are
also saved there. Similarly, when a `DECSTR` sequence resets the code
page and character sets, I've now made it reset the C1 mode as well.

I should note that the new `StateMachine` mode is controlled via a
generic `SetParserMode` method (with a matching API in the `ConGetSet`
interface) to allow for easier addition of other modes in the future.
And I've reimplemented the existing ANSI/VT52 mode in terms of these
generic methods instead of it having to have its own separate APIs.

## Validation Steps Performed

Some of the unit tests for OSC sequences were using a C1 `0x9C` for the
string terminator, which doesn't work by default anymore. Since that's
not a good practice anyway, I thought it best to change those to a
standard 7-bit terminator. However, in tests that were explicitly
validating the C1 controls, I've just enabled the C1 parser mode at the
start of the tests in order to get them working again.

There were also some ANSI mode adapter tests that had to be updated to
account for the fact that it has now been reimplemented in terms of the
`SetParserMode` API.

I've added a new state machine test to validate the changes in behavior
when the C1 parser mode is enabled or disabled. And I've added an
adapter test to verify that the `DesignateCodingSystems` and
`AcceptC1Controls` methods toggle the C1 parser mode as expected.

I've manually verified the test cases in #10069 and #10310 to confirm
that they're no longer triggering control sequences by default.
Although, as I explained above, the C1 code points are completely
dropped from the output rather than displayed as _notdef_ glyphs. I
think this is a reasonable compromise though.

Closes #10069
Closes #10310
2021-11-17 23:40:31 +00:00

2632 lines
98 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "adaptDispatch.hpp"
#include "conGetSet.hpp"
#include "../../types/inc/Viewport.hpp"
#include "../../types/inc/utils.hpp"
#include "../../inc/unicode.hpp"
#include "../parser/ascii.hpp"
using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::VirtualTerminal;
// Routine Description:
// - No Operation helper. It's just here to make sure they're always all the same.
// Arguments:
// - <none>
// Return Value:
// - Always false to signify we didn't handle it.
bool NoOp() noexcept
{
return false;
}
// Note: AdaptDispatch will take ownership of pConApi and pDefaults
AdaptDispatch::AdaptDispatch(std::unique_ptr<ConGetSet> pConApi,
std::unique_ptr<AdaptDefaults> pDefaults) :
_pConApi{ std::move(pConApi) },
_pDefaults{ std::move(pDefaults) },
_usingAltBuffer(false),
_isOriginModeRelative(false), // by default, the DECOM origin mode is absolute.
_isDECCOLMAllowed(false), // by default, DECCOLM is not allowed.
_termOutput()
{
THROW_HR_IF_NULL(E_INVALIDARG, _pConApi.get());
THROW_HR_IF_NULL(E_INVALIDARG, _pDefaults.get());
_scrollMargins = { 0 }; // initially, there are no scroll margins.
}
// Routine Description:
// - Translates and displays a single character
// Arguments:
// - wchPrintable - Printable character
// Return Value:
// - <none>
void AdaptDispatch::Print(const wchar_t wchPrintable)
{
const auto wchTranslated = _termOutput.TranslateKey(wchPrintable);
// By default the DEL character is meant to be ignored in the same way as a
// NUL character. However, it's possible that it could be translated to a
// printable character in a 96-character set. This condition makes sure that
// a character is only output if the DEL is translated to something else.
if (wchTranslated != AsciiChars::DEL)
{
_pDefaults->Print(wchTranslated);
}
}
// Routine Description
// - Forward an entire string through. May translate, if necessary, to key input sequences
// based on the locale
// Arguments:
// - string - Text to display
// Return Value:
// - <none>
void AdaptDispatch::PrintString(const std::wstring_view string)
{
try
{
if (_termOutput.NeedToTranslate())
{
std::wstring buffer;
buffer.reserve(string.size());
for (auto& wch : string)
{
buffer.push_back(_termOutput.TranslateKey(wch));
}
_pDefaults->PrintString(buffer);
}
else
{
_pDefaults->PrintString(string);
}
}
CATCH_LOG();
}
// Routine Description:
// - CUU - Handles cursor upward movement by given distance.
// CUU and CUD are handled separately from other CUP sequences, because they are
// constrained by the margins.
// See: https://vt100.net/docs/vt510-rm/CUU.html
// "The cursor stops at the top margin. If the cursor is already above the top
// margin, then the cursor stops at the top line."
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorUp(const size_t distance)
{
return _CursorMovePosition(Offset::Backward(distance), Offset::Unchanged(), true);
}
// Routine Description:
// - CUD - Handles cursor downward movement by given distance
// CUU and CUD are handled separately from other CUP sequences, because they are
// constrained by the margins.
// See: https://vt100.net/docs/vt510-rm/CUD.html
// "The cursor stops at the bottom margin. If the cursor is already above the
// bottom margin, then the cursor stops at the bottom line."
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorDown(const size_t distance)
{
return _CursorMovePosition(Offset::Forward(distance), Offset::Unchanged(), true);
}
// Routine Description:
// - CUF - Handles cursor forward movement by given distance
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorForward(const size_t distance)
{
return _CursorMovePosition(Offset::Unchanged(), Offset::Forward(distance), true);
}
// Routine Description:
// - CUB - Handles cursor backward movement by given distance
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorBackward(const size_t distance)
{
return _CursorMovePosition(Offset::Unchanged(), Offset::Backward(distance), true);
}
// Routine Description:
// - CNL - Handles cursor movement to the following line (or N lines down)
// - Moves to the beginning X/Column position of the line.
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorNextLine(const size_t distance)
{
return _CursorMovePosition(Offset::Forward(distance), Offset::Absolute(1), true);
}
// Routine Description:
// - CPL - Handles cursor movement to the previous line (or N lines up)
// - Moves to the beginning X/Column position of the line.
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorPrevLine(const size_t distance)
{
return _CursorMovePosition(Offset::Backward(distance), Offset::Absolute(1), true);
}
// Routine Description:
// - Generalizes cursor movement to a specific position, which can be absolute or relative.
// Arguments:
// - rowOffset - The row to move to
// - colOffset - The column to move to
// - clampInMargins - Should the position be clamped within the scrolling margins
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins) const
{
bool success = true;
// First retrieve some information about the buffer
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
success = (_pConApi->MoveToBottom() && _pConApi->GetConsoleScreenBufferInfoEx(csbiex));
if (success)
{
// Calculate the viewport boundaries as inclusive values.
// srWindow is exclusive so we need to subtract 1 from the bottom.
const int viewportTop = csbiex.srWindow.Top;
const int viewportBottom = csbiex.srWindow.Bottom - 1;
// Calculate the absolute margins of the scrolling area.
const int topMargin = viewportTop + _scrollMargins.Top;
const int bottomMargin = viewportTop + _scrollMargins.Bottom;
const bool marginsSet = topMargin < bottomMargin;
// For relative movement, the given offsets will be relative to
// the current cursor position.
int row = csbiex.dwCursorPosition.Y;
int col = csbiex.dwCursorPosition.X;
// But if the row is absolute, it will be relative to the top of the
// viewport, or the top margin, depending on the origin mode.
if (rowOffset.IsAbsolute)
{
row = _isOriginModeRelative ? topMargin : viewportTop;
}
// And if the column is absolute, it'll be relative to column 0.
// Horizontal positions are not affected by the viewport.
if (colOffset.IsAbsolute)
{
col = 0;
}
// Adjust the base position by the given offsets and clamp the results.
// The row is constrained within the viewport's vertical boundaries,
// while the column is constrained by the buffer width.
row = std::clamp(row + rowOffset.Value, viewportTop, viewportBottom);
col = std::clamp(col + colOffset.Value, 0, csbiex.dwSize.X - 1);
// If the operation needs to be clamped inside the margins, or the origin
// mode is relative (which always requires margin clamping), then the row
// may need to be adjusted further.
if (marginsSet && (clampInMargins || _isOriginModeRelative))
{
// See microsoft/terminal#2929 - If the cursor is _below_ the top
// margin, it should stay below the top margin. If it's _above_ the
// bottom, it should stay above the bottom. Cursor movements that stay
// outside the margins shouldn't necessarily be affected. For example,
// moving up while below the bottom margin shouldn't just jump straight
// to the bottom margin. See
// ScreenBufferTests::CursorUpDownOutsideMargins for a test of that
// behavior.
if (csbiex.dwCursorPosition.Y >= topMargin)
{
row = std::max(row, topMargin);
}
if (csbiex.dwCursorPosition.Y <= bottomMargin)
{
row = std::min(row, bottomMargin);
}
}
// Finally, attempt to set the adjusted cursor position back into the console.
const COORD newPos = { gsl::narrow_cast<SHORT>(col), gsl::narrow_cast<SHORT>(row) };
success = _pConApi->SetConsoleCursorPosition(newPos);
}
return success;
}
// Routine Description:
// - CHA - Moves the cursor to an exact X/Column position on the current line.
// Arguments:
// - column - Specific X/Column position to move to
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorHorizontalPositionAbsolute(const size_t column)
{
return _CursorMovePosition(Offset::Unchanged(), Offset::Absolute(column), false);
}
// Routine Description:
// - VPA - Moves the cursor to an exact Y/row position on the current column.
// Arguments:
// - line - Specific Y/Row position to move to
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::VerticalLinePositionAbsolute(const size_t line)
{
return _CursorMovePosition(Offset::Absolute(line), Offset::Unchanged(), false);
}
// Routine Description:
// - HPR - Handles cursor forward movement by given distance
// - Unlike CUF, this is not constrained by margin settings.
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::HorizontalPositionRelative(const size_t distance)
{
return _CursorMovePosition(Offset::Unchanged(), Offset::Forward(distance), false);
}
// Routine Description:
// - VPR - Handles cursor downward movement by given distance
// - Unlike CUD, this is not constrained by margin settings.
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::VerticalPositionRelative(const size_t distance)
{
return _CursorMovePosition(Offset::Forward(distance), Offset::Unchanged(), false);
}
// Routine Description:
// - CUP - Moves the cursor to an exact X/Column and Y/Row/Line coordinate position.
// Arguments:
// - line - Specific Y/Row/Line position to move to
// - column - Specific X/Column position to move to
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorPosition(const size_t line, const size_t column)
{
return _CursorMovePosition(Offset::Absolute(line), Offset::Absolute(column), false);
}
// Routine Description:
// - DECSC - Saves the current "cursor state" into a memory buffer. This
// includes the cursor position, origin mode, graphic rendition, and
// active character set.
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorSaveState()
{
// First retrieve some information about the buffer
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
bool success = (_pConApi->MoveToBottom() && _pConApi->GetConsoleScreenBufferInfoEx(csbiex));
TextAttribute attributes;
success = success && (_pConApi->PrivateGetTextAttributes(attributes));
if (success)
{
// The cursor is given to us by the API as relative to the whole buffer.
// But in VT speak, the cursor row should be relative to the current viewport top.
COORD coordCursor = csbiex.dwCursorPosition;
coordCursor.Y -= csbiex.srWindow.Top;
// VT is also 1 based, not 0 based, so correct by 1.
auto& savedCursorState = _savedCursorState.at(_usingAltBuffer);
savedCursorState.Column = coordCursor.X + 1;
savedCursorState.Row = coordCursor.Y + 1;
savedCursorState.IsOriginModeRelative = _isOriginModeRelative;
savedCursorState.Attributes = attributes;
savedCursorState.TermOutput = _termOutput;
savedCursorState.C1ControlsAccepted = _pConApi->GetParserMode(StateMachine::Mode::AcceptC1);
_pConApi->GetConsoleOutputCP(savedCursorState.CodePage);
}
return success;
}
// Routine Description:
// - DECRC - Restores a saved "cursor state" from the DECSC command back into
// the console state. This includes the cursor position, origin mode, graphic
// rendition, and active character set.
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorRestoreState()
{
auto& savedCursorState = _savedCursorState.at(_usingAltBuffer);
auto row = savedCursorState.Row;
const auto col = savedCursorState.Column;
// If the origin mode is relative, and the scrolling region is set (the bottom is non-zero),
// we need to make sure the restored position is clamped within the margins.
if (savedCursorState.IsOriginModeRelative && _scrollMargins.Bottom != 0)
{
// VT origin is at 1,1 so we need to add 1 to these margins.
row = std::clamp(row, _scrollMargins.Top + 1u, _scrollMargins.Bottom + 1u);
}
// The saved coordinates are always absolute, so we need reset the origin mode temporarily.
_isOriginModeRelative = false;
bool success = CursorPosition(row, col);
// Once the cursor position is restored, we can then restore the actual origin mode.
_isOriginModeRelative = savedCursorState.IsOriginModeRelative;
// Restore text attributes.
success = (_pConApi->PrivateSetTextAttributes(savedCursorState.Attributes)) && success;
// Restore designated character set.
_termOutput = savedCursorState.TermOutput;
// Restore the parsing state of C1 control codes.
AcceptC1Controls(savedCursorState.C1ControlsAccepted);
// Restore the code page if it was previously saved.
if (savedCursorState.CodePage != 0)
{
success = _pConApi->SetConsoleOutputCP(savedCursorState.CodePage);
}
return success;
}
// Routine Description:
// - DECTCEM - Sets the show/hide visibility status of the cursor.
// Arguments:
// - fIsVisible - Turns the cursor rendering on (TRUE) or off (FALSE).
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CursorVisibility(const bool fIsVisible)
{
// This uses a private API instead of the public one, because the public API
// will set the cursor shape back to legacy.
return _pConApi->PrivateShowCursor(fIsVisible);
}
// Routine Description:
// - This helper will do the work of performing an insert or delete character operation
// - Both operations are similar in that they cut text and move it left or right in the buffer, padding the leftover area with spaces.
// Arguments:
// - count - The number of characters to insert
// - isInsert - TRUE if insert mode (cut and paste to the right, away from the cursor). FALSE if delete mode (cut and paste to the left, toward the cursor)
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_InsertDeleteHelper(const size_t count, const bool isInsert) const
{
// We'll be doing short math on the distance since all console APIs use shorts. So check that we can successfully convert the uint into a short first.
SHORT distance;
RETURN_BOOL_IF_FALSE(SUCCEEDED(SizeTToShort(count, &distance)));
// get current cursor, attributes
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
RETURN_BOOL_IF_FALSE(_pConApi->MoveToBottom());
RETURN_BOOL_IF_FALSE(_pConApi->GetConsoleScreenBufferInfoEx(csbiex));
const auto cursor = csbiex.dwCursorPosition;
// Rectangle to cut out of the existing buffer. This is inclusive.
SMALL_RECT srScroll;
srScroll.Left = cursor.X;
srScroll.Right = _pConApi->PrivateGetLineWidth(cursor.Y) - 1;
srScroll.Top = cursor.Y;
srScroll.Bottom = srScroll.Top;
// Paste coordinate for cut text above
COORD coordDestination;
coordDestination.Y = cursor.Y;
coordDestination.X = cursor.X;
bool success = false;
if (isInsert)
{
// Insert makes space by moving characters out to the right. So move the destination of the cut/paste region.
success = SUCCEEDED(ShortAdd(coordDestination.X, distance, &coordDestination.X));
}
else
{
// Delete scrolls the affected region to the left, relying on the clipping rect to actually delete the characters.
success = SUCCEEDED(ShortSub(coordDestination.X, distance, &coordDestination.X));
}
if (success)
{
// Note the revealed characters are filled with the standard erase attributes.
success = _pConApi->PrivateScrollRegion(srScroll, srScroll, coordDestination, true);
}
return success;
}
// Routine Description:
// ICH - Insert Character - Blank/default attribute characters will be inserted at the current cursor position.
// - Each inserted character will push all text in the row to the right.
// Arguments:
// - count - The number of characters to insert
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::InsertCharacter(const size_t count)
{
return _InsertDeleteHelper(count, true);
}
// Routine Description:
// DCH - Delete Character - The character at the cursor position will be deleted. Blank/attribute characters will
// be inserted from the right edge of the current line.
// Arguments:
// - count - The number of characters to delete
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::DeleteCharacter(const size_t count)
{
return _InsertDeleteHelper(count, false);
}
// Routine Description:
// - Internal helper to erase one particular line of the buffer. Either from beginning to the cursor, from the cursor to the end, or the entire line.
// - Used by both erase line (used just once) and by erase screen (used in a loop) to erase a portion of the buffer.
// Arguments:
// - csbiex - Pointer to the console screen buffer that we will be erasing (and getting cursor data from within)
// - eraseType - Enumeration mode of which kind of erase to perform: beginning to cursor, cursor to end, or entire line.
// - lineId - The line number (array index value, starts at 0) of the line to operate on within the buffer.
// - This is not aware of circular buffer. Line 0 is always the top visible line if you scrolled the whole way up the window.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX& csbiex,
const DispatchTypes::EraseType eraseType,
const size_t lineId) const
{
COORD coordStartPosition = { 0 };
if (FAILED(SizeTToShort(lineId, &coordStartPosition.Y)))
{
return false;
}
// determine start position from the erase type
// remember that erases are inclusive of the current cursor position.
switch (eraseType)
{
case DispatchTypes::EraseType::FromBeginning:
case DispatchTypes::EraseType::All:
coordStartPosition.X = 0; // from beginning and the whole line start from the left most edge of the buffer.
break;
case DispatchTypes::EraseType::ToEnd:
coordStartPosition.X = csbiex.dwCursorPosition.X; // from the current cursor position (including it)
break;
}
DWORD nLength = 0;
// determine length of erase from erase type
switch (eraseType)
{
case DispatchTypes::EraseType::FromBeginning:
// +1 because if cursor were at the left edge, the length would be 0 and we want to paint at least the 1 character the cursor is on.
nLength = csbiex.dwCursorPosition.X + 1;
break;
case DispatchTypes::EraseType::ToEnd:
case DispatchTypes::EraseType::All:
// Remember the .X value is 1 farther than the right most column in the buffer. Therefore no +1.
nLength = _pConApi->PrivateGetLineWidth(lineId) - coordStartPosition.X;
break;
}
// Note that the region is filled with the standard erase attributes.
return _pConApi->PrivateFillRegion(coordStartPosition, nLength, L' ', true);
}
// Routine Description:
// - ECH - Erase Characters from the current cursor position, by replacing
// them with a space. This will only erase characters in the current line,
// and won't wrap to the next. The attributes of any erased positions
// receive the currently selected attributes.
// Arguments:
// - numChars - The number of characters to erase.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::EraseCharacters(const size_t numChars)
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
bool success = _pConApi->GetConsoleScreenBufferInfoEx(csbiex);
if (success)
{
const COORD startPosition = csbiex.dwCursorPosition;
const SHORT remainingSpaces = csbiex.dwSize.X - startPosition.X;
const size_t actualRemaining = gsl::narrow_cast<size_t>((remainingSpaces < 0) ? 0 : remainingSpaces);
// erase at max the number of characters remaining in the line from the current position.
const auto eraseLength = (numChars <= actualRemaining) ? numChars : actualRemaining;
// Note that the region is filled with the standard erase attributes.
success = _pConApi->PrivateFillRegion(startPosition, eraseLength, L' ', true);
}
return success;
}
// Routine Description:
// - ED - Erases a portion of the current viewable area (viewport) of the console.
// Arguments:
// - eraseType - Determines whether to erase:
// From beginning (top-left corner) to the cursor
// From cursor to end (bottom-right corner)
// The entire viewport area
// The scrollback (outside the viewport area)
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType)
{
RETURN_BOOL_IF_FALSE(eraseType <= DispatchTypes::EraseType::Scrollback);
// First things first. If this is a "Scrollback" clear, then just do that.
// Scrollback clears erase everything in the "scrollback" of a *nix terminal
// Everything that's scrolled off the screen so far.
// Or if it's an Erase All, then we also need to handle that specially
// by moving the current contents of the viewport into the scrollback.
if (eraseType == DispatchTypes::EraseType::Scrollback)
{
const bool eraseScrollbackResult = _EraseScrollback();
// GH#2715 - If this succeeded, but we're in a conpty, return `false` to
// make the state machine propagate this ED sequence to the connected
// terminal application. While we're in conpty mode, we don't really
// have a scrollback, but the attached terminal might.
const bool isPty = _pConApi->IsConsolePty();
return eraseScrollbackResult && (!isPty);
}
else if (eraseType == DispatchTypes::EraseType::All)
{
// GH#5683 - If this succeeded, but we're in a conpty, return `false` to
// make the state machine propagate this ED sequence to the connected
// terminal application. While we're in conpty mode, when the client
// requests a Erase All operation, we need to manually tell the
// connected terminal to do the same thing, so that the terminal will
// move it's own buffer contents into the scrollback.
const bool eraseAllResult = _EraseAll();
const bool isPty = _pConApi->IsConsolePty();
return eraseAllResult && (!isPty);
}
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
bool success = (_pConApi->MoveToBottom() && _pConApi->GetConsoleScreenBufferInfoEx(csbiex));
if (success)
{
// When erasing the display, every line that is erased in full should be
// reset to single width. When erasing to the end, this could include
// the current line, if the cursor is in the first column. When erasing
// from the beginning, though, the current line would never be included,
// because the cursor could never be in the rightmost column (assuming
// the line is double width).
if (eraseType == DispatchTypes::EraseType::FromBeginning)
{
const auto endRow = csbiex.dwCursorPosition.Y;
_pConApi->PrivateResetLineRenditionRange(csbiex.srWindow.Top, endRow);
}
if (eraseType == DispatchTypes::EraseType::ToEnd)
{
const auto startRow = csbiex.dwCursorPosition.Y + (csbiex.dwCursorPosition.X > 0 ? 1 : 0);
_pConApi->PrivateResetLineRenditionRange(startRow, csbiex.srWindow.Bottom);
}
// What we need to erase is grouped into 3 types:
// 1. Lines before cursor
// 2. Cursor Line
// 3. Lines after cursor
// We erase one or more of these based on the erase type:
// A. FromBeginning - Erase 1 and Some of 2.
// B. ToEnd - Erase some of 2 and 3.
// C. All - Erase 1, 2, and 3.
// 1. Lines before cursor line
if (eraseType == DispatchTypes::EraseType::FromBeginning)
{
// For beginning and all, erase all complete lines before (above vertically) from the cursor position.
for (SHORT startLine = csbiex.srWindow.Top; startLine < csbiex.dwCursorPosition.Y; startLine++)
{
success = _EraseSingleLineHelper(csbiex, DispatchTypes::EraseType::All, startLine);
if (!success)
{
break;
}
}
}
if (success)
{
// 2. Cursor Line
success = _EraseSingleLineHelper(csbiex, eraseType, csbiex.dwCursorPosition.Y);
}
if (success)
{
// 3. Lines after cursor line
if (eraseType == DispatchTypes::EraseType::ToEnd)
{
// For beginning and all, erase all complete lines after (below vertically) the cursor position.
// Remember that the viewport bottom value is 1 beyond the viewable area of the viewport.
for (SHORT startLine = csbiex.dwCursorPosition.Y + 1; startLine < csbiex.srWindow.Bottom; startLine++)
{
success = _EraseSingleLineHelper(csbiex, DispatchTypes::EraseType::All, startLine);
if (!success)
{
break;
}
}
}
}
}
return success;
}
// Routine Description:
// - EL - Erases the line that the cursor is currently on.
// Arguments:
// - eraseType - Determines whether to erase: From beginning (left edge) to the cursor, from cursor to end (right edge), or the entire line.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType)
{
RETURN_BOOL_IF_FALSE(eraseType <= DispatchTypes::EraseType::All);
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
bool success = _pConApi->GetConsoleScreenBufferInfoEx(csbiex);
if (success)
{
success = _EraseSingleLineHelper(csbiex, eraseType, csbiex.dwCursorPosition.Y);
}
return success;
}
// Routine Description:
// - DECSWL/DECDWL/DECDHL - Sets the line rendition attribute for the current line.
// Arguments:
// - rendition - Determines whether the line will be rendered as single width, double
// width, or as one half of a double height line.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetLineRendition(const LineRendition rendition)
{
return _pConApi->PrivateSetCurrentLineRendition(rendition);
}
// Routine Description:
// - DSR - Reports status of a console property back to the STDIN based on the type of status requested.
// - This particular routine responds to ANSI status patterns only (CSI # n), not the DEC format (CSI ? # n)
// Arguments:
// - statusType - ANSI status type indicating what property we should report back
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType)
{
bool success = false;
switch (statusType)
{
case DispatchTypes::AnsiStatusType::OS_OperatingStatus:
success = _OperatingStatus();
break;
case DispatchTypes::AnsiStatusType::CPR_CursorPositionReport:
success = _CursorPositionReport();
break;
}
return success;
}
// Routine Description:
// - DA - Reports the identity of this Virtual Terminal machine to the caller.
// - In our case, we'll report back to acknowledge we understand, but reveal no "hardware" upgrades like physical terminals of old.
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::DeviceAttributes()
{
// See: http://vt100.net/docs/vt100-ug/chapter3.html#DA
return _WriteResponse(L"\x1b[?1;0c");
}
// Routine Description:
// - DA2 - Reports the terminal type, firmware version, and hardware options.
// For now we're following the XTerm practice of using 0 to represent a VT100
// terminal, the version is hardcoded as 10 (1.0), and the hardware option
// is set to 1 (indicating a PC Keyboard).
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SecondaryDeviceAttributes()
{
return _WriteResponse(L"\x1b[>0;10;1c");
}
// Routine Description:
// - DA3 - Reports the terminal unit identification code. Terminal emulators
// typically return a hardcoded value, the most common being all zeros.
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::TertiaryDeviceAttributes()
{
return _WriteResponse(L"\x1bP!|00000000\x1b\\");
}
// Routine Description:
// - VT52 Identify - Reports the identity of the terminal in VT52 emulation mode.
// An actual VT52 terminal would typically identify itself with ESC / K.
// But for a terminal that is emulating a VT52, the sequence should be ESC / Z.
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::Vt52DeviceAttributes()
{
return _WriteResponse(L"\x1b/Z");
}
// Routine Description:
// - DECREQTPARM - This sequence was originally used on the VT100 terminal to
// report the serial communication parameters (baud rate, data bits, parity,
// etc.). On modern terminal emulators, the response is simply hardcoded.
// Arguments:
// - permission - This would originally have determined whether the terminal
// was allowed to send unsolicited reports or not.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::RequestTerminalParameters(const DispatchTypes::ReportingPermission permission)
{
// We don't care whether unsolicited reports are allowed or not, but the
// requested permission does determine the value of the first response
// parameter. The remaining parameters are just hardcoded to indicate a
// 38400 baud connection, which matches the XTerm response. The full
// parameter sequence is as follows:
// - response type: 2 or 3 (unsolicited or solicited)
// - parity: 1 (no parity)
// - data bits: 1 (8 bits per character)
// - transmit speed: 128 (38400 baud)
// - receive speed: 128 (38400 baud)
// - clock multiplier: 1
// - flags: 0
switch (permission)
{
case DispatchTypes::ReportingPermission::Unsolicited:
return _WriteResponse(L"\x1b[2;1;1;128;128;1;0x");
case DispatchTypes::ReportingPermission::Solicited:
return _WriteResponse(L"\x1b[3;1;1;128;128;1;0x");
default:
return false;
}
}
// Routine Description:
// - DSR-OS - Reports the operating status back to the input channel
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_OperatingStatus() const
{
// We always report a good operating condition.
return _WriteResponse(L"\x1b[0n");
}
// Routine Description:
// - DSR-CPR - Reports the current cursor position within the viewport back to the input channel
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_CursorPositionReport() const
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
bool success = (_pConApi->MoveToBottom() && _pConApi->GetConsoleScreenBufferInfoEx(csbiex));
if (success)
{
// First pull the cursor position relative to the entire buffer out of the console.
COORD coordCursorPos = csbiex.dwCursorPosition;
// Now adjust it for its position in respect to the current viewport top.
coordCursorPos.Y -= csbiex.srWindow.Top;
// NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1.
coordCursorPos.X++;
coordCursorPos.Y++;
// If the origin mode is relative, line numbers start at top margin of the scrolling region.
if (_isOriginModeRelative)
{
coordCursorPos.Y -= _scrollMargins.Top;
}
// Now send it back into the input channel of the console.
// First format the response string.
const auto response = wil::str_printf<std::wstring>(L"\x1b[%d;%dR", coordCursorPos.Y, coordCursorPos.X);
success = _WriteResponse(response);
}
return success;
}
// Routine Description:
// - Helper to send a string reply to the input stream of the console.
// - Used by various commands where the program attached would like a reply to one of the commands issued.
// - This will generate two "key presses" (one down, one up) for every character in the string and place them into the head of the console's input stream.
// Arguments:
// - reply - The reply string to transmit back to the input stream
// Return Value:
// - True if the string was converted to input events and placed into the console input buffer successfully. False otherwise.
bool AdaptDispatch::_WriteResponse(const std::wstring_view reply) const
{
bool success = false;
std::deque<std::unique_ptr<IInputEvent>> inEvents;
try
{
// generate a paired key down and key up event for every
// character to be sent into the console's input buffer
for (const auto& wch : reply)
{
// This wasn't from a real keyboard, so we're leaving key/scan codes blank.
KeyEvent keyEvent{ TRUE, 1, 0, 0, wch, 0 };
inEvents.push_back(std::make_unique<KeyEvent>(keyEvent));
keyEvent.SetKeyDown(false);
inEvents.push_back(std::make_unique<KeyEvent>(keyEvent));
}
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
return false;
}
size_t eventsWritten;
// TODO GH#4954 During the input refactor we may want to add a "priority" input list
// to make sure that "response" input is spooled directly into the application.
// We switched this to an append (vs. a prepend) to fix GH#1637, a bug where two CPR
// could collide with eachother.
success = _pConApi->PrivateWriteConsoleInputW(inEvents, eventsWritten);
return success;
}
// Routine Description:
// - Generalizes scrolling movement for up/down
// Arguments:
// - scrollDirection - Specific direction to move
// - distance - Magnitude of the move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_ScrollMovement(const ScrollDirection scrollDirection, const size_t distance) const
{
// We'll be doing short math on the distance since all console APIs use shorts. So check that we can successfully convert the size_t into a short first.
SHORT dist;
bool success = SUCCEEDED(SizeTToShort(distance, &dist));
if (success)
{
// get current cursor
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
success = (_pConApi->MoveToBottom() && _pConApi->GetConsoleScreenBufferInfoEx(csbiex));
if (success)
{
// Rectangle to cut out of the existing buffer. This is inclusive.
// It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width.
SMALL_RECT srScreen;
srScreen.Left = 0;
srScreen.Right = SHORT_MAX;
srScreen.Top = csbiex.srWindow.Top;
srScreen.Bottom = csbiex.srWindow.Bottom - 1; // srWindow is exclusive, hence the - 1
// Clip to the DECSTBM margin boundaries
if (_scrollMargins.Top < _scrollMargins.Bottom)
{
srScreen.Top = csbiex.srWindow.Top + _scrollMargins.Top;
srScreen.Bottom = csbiex.srWindow.Top + _scrollMargins.Bottom;
}
// Paste coordinate for cut text above
COORD coordDestination;
coordDestination.X = srScreen.Left;
coordDestination.Y = srScreen.Top + dist * (scrollDirection == ScrollDirection::Up ? -1 : 1);
// Note the revealed lines are filled with the standard erase attributes.
success = _pConApi->PrivateScrollRegion(srScreen, srScreen, coordDestination, true);
}
}
return success;
}
// Routine Description:
// - SU - Pans the window DOWN by given distance (distance new lines appear at the bottom of the screen)
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::ScrollUp(const size_t uiDistance)
{
return _ScrollMovement(ScrollDirection::Up, uiDistance);
}
// Routine Description:
// - SD - Pans the window UP by given distance (distance new lines appear at the top of the screen)
// Arguments:
// - distance - Distance to move
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::ScrollDown(const size_t uiDistance)
{
return _ScrollMovement(ScrollDirection::Down, uiDistance);
}
// Routine Description:
// - DECSCPP / DECCOLM Sets the number of columns "per page" AKA sets the console width.
// DECCOLM also clear the screen (like a CSI 2 J sequence), while DECSCPP just sets the width.
// (DECCOLM will do this separately of this function)
// Arguments:
// - columns - Number of columns
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetColumns(const size_t columns)
{
SHORT col;
bool success = SUCCEEDED(SizeTToShort(columns, &col));
if (success)
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
success = _pConApi->GetConsoleScreenBufferInfoEx(csbiex);
if (success)
{
csbiex.dwSize.X = col;
success = _pConApi->SetConsoleScreenBufferInfoEx(csbiex);
}
}
return success;
}
// Routine Description:
// - DECCOLM not only sets the number of columns, but also clears the screen buffer,
// resets the page margins and origin mode, and places the cursor at 1,1
// Arguments:
// - columns - Number of columns
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_DoDECCOLMHelper(const size_t columns)
{
if (!_isDECCOLMAllowed)
{
// Only proceed if DECCOLM is allowed. Return true, as this is technically a successful handling.
return true;
}
bool success = SetColumns(columns);
if (success)
{
success = SetOriginMode(false);
if (success)
{
success = CursorPosition(1, 1);
if (success)
{
success = EraseInDisplay(DispatchTypes::EraseType::All);
if (success)
{
success = _DoSetTopBottomScrollingMargins(0, 0);
}
}
}
}
return success;
}
// Routine Description:
// - Support routine for routing private mode parameters to be set/reset as flags
// Arguments:
// - param - mode parameter to set/reset
// - enable - True for set, false for unset.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, const bool enable)
{
bool success = false;
switch (param)
{
case DispatchTypes::ModeParams::DECCKM_CursorKeysMode:
// set - Enable Application Mode, reset - Normal mode
success = SetCursorKeysMode(enable);
break;
case DispatchTypes::ModeParams::DECANM_AnsiMode:
success = SetAnsiMode(enable);
break;
case DispatchTypes::ModeParams::DECCOLM_SetNumberOfColumns:
success = _DoDECCOLMHelper(enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns);
break;
case DispatchTypes::ModeParams::DECSCNM_ScreenMode:
success = SetScreenMode(enable);
break;
case DispatchTypes::ModeParams::DECOM_OriginMode:
// The cursor is also moved to the new home position when the origin mode is set or reset.
success = SetOriginMode(enable) && CursorPosition(1, 1);
break;
case DispatchTypes::ModeParams::DECAWM_AutoWrapMode:
success = SetAutoWrapMode(enable);
break;
case DispatchTypes::ModeParams::ATT610_StartCursorBlink:
success = EnableCursorBlinking(enable);
break;
case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode:
success = CursorVisibility(enable);
break;
case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport:
success = EnableDECCOLMSupport(enable);
break;
case DispatchTypes::ModeParams::VT200_MOUSE_MODE:
success = EnableVT200MouseMode(enable);
break;
case DispatchTypes::ModeParams::BUTTON_EVENT_MOUSE_MODE:
success = EnableButtonEventMouseMode(enable);
break;
case DispatchTypes::ModeParams::ANY_EVENT_MOUSE_MODE:
success = EnableAnyEventMouseMode(enable);
break;
case DispatchTypes::ModeParams::UTF8_EXTENDED_MODE:
success = EnableUTF8ExtendedMouseMode(enable);
break;
case DispatchTypes::ModeParams::SGR_EXTENDED_MODE:
success = EnableSGRExtendedMouseMode(enable);
break;
case DispatchTypes::ModeParams::ALTERNATE_SCROLL:
success = EnableAlternateScroll(enable);
break;
case DispatchTypes::ModeParams::ASB_AlternateScreenBuffer:
success = enable ? UseAlternateScreenBuffer() : UseMainScreenBuffer();
break;
case DispatchTypes::ModeParams::W32IM_Win32InputMode:
success = EnableWin32InputMode(enable);
break;
default:
// If no functions to call, overall dispatch was a failure.
success = false;
break;
}
return success;
}
// Routine Description:
// - DECSET - Enables the given DEC private mode params.
// Arguments:
// - param - mode parameter to set
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetMode(const DispatchTypes::ModeParams param)
{
return _ModeParamsHelper(param, true);
}
// Routine Description:
// - DECRST - Disables the given DEC private mode params.
// Arguments:
// - param - mode parameter to reset
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::ResetMode(const DispatchTypes::ModeParams param)
{
return _ModeParamsHelper(param, false);
}
// - DECKPAM, DECKPNM - Sets the keypad input mode to either Application mode or Numeric mode (true, false respectively)
// Arguments:
// - applicationMode - set to true to enable Application Mode Input, false for Numeric Mode Input.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetKeypadMode(const bool fApplicationMode)
{
return _pConApi->SetInputMode(TerminalInput::Mode::Keypad, fApplicationMode);
}
// Method Description:
// - win32-input-mode: Enable sending full input records encoded as a string of
// characters to the client application.
// Arguments:
// - win32InputMode - set to true to enable win32-input-mode, false to disable.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::EnableWin32InputMode(const bool win32InputMode)
{
return _pConApi->SetInputMode(TerminalInput::Mode::Win32, win32InputMode);
}
// - DECCKM - Sets the cursor keys input mode to either Application mode or Normal mode (true, false respectively)
// Arguments:
// - applicationMode - set to true to enable Application Mode Input, false for Normal Mode Input.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetCursorKeysMode(const bool applicationMode)
{
return _pConApi->SetInputMode(TerminalInput::Mode::CursorKey, applicationMode);
}
// - att610 - Enables or disables the cursor blinking.
// Arguments:
// - enable - set to true to enable blinking, false to disable
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::EnableCursorBlinking(const bool enable)
{
return _pConApi->PrivateAllowCursorBlinking(enable);
}
// Routine Description:
// - IL - This control function inserts one or more blank lines, starting at the cursor.
// As lines are inserted, lines below the cursor and in the scrolling region move down.
// Lines scrolled off the page are lost. IL has no effect outside the page margins.
// Arguments:
// - distance - number of lines to insert
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::InsertLine(const size_t distance)
{
return _pConApi->InsertLines(distance);
}
// Routine Description:
// - DL - This control function deletes one or more lines in the scrolling
// region, starting with the line that has the cursor.
// As lines are deleted, lines below the cursor and in the scrolling region
// move up. The terminal adds blank lines with no visual character
// attributes at the bottom of the scrolling region. If distance is greater than
// the number of lines remaining on the page, DL deletes only the remaining
// lines. DL has no effect outside the scrolling margins.
// Arguments:
// - distance - number of lines to delete
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::DeleteLine(const size_t distance)
{
return _pConApi->DeleteLines(distance);
}
// - DECANM - Sets the terminal emulation mode to either ANSI-compatible or VT52.
// Arguments:
// - ansiMode - set to true to enable the ANSI mode, false for VT52 mode.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetAnsiMode(const bool ansiMode)
{
// When an attempt is made to update the mode, the designated character sets
// need to be reset to defaults, even if the mode doesn't actually change.
_termOutput = {};
_pConApi->SetParserMode(StateMachine::Mode::Ansi, ansiMode);
_pConApi->SetInputMode(TerminalInput::Mode::Ansi, ansiMode);
// We don't check the SetInputMode return value, because we'll never want
// to forward a DECANM mode change over conpty.
return true;
}
// Routine Description:
// - DECSCNM - Sets the screen mode to either normal or reverse.
// When in reverse screen mode, the background and foreground colors are switched.
// Arguments:
// - reverseMode - set to true to enable reverse screen mode, false for normal mode.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetScreenMode(const bool reverseMode)
{
// If we're a conpty, always return false
if (_pConApi->IsConsolePty())
{
return false;
}
return _pConApi->PrivateSetScreenMode(reverseMode);
}
// Routine Description:
// - DECOM - Sets the cursor addressing origin mode to relative or absolute.
// When relative, line numbers start at top margin of the user-defined scrolling region.
// When absolute, line numbers are independent of the scrolling region.
// Arguments:
// - relativeMode - set to true to use relative addressing, false for absolute addressing.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetOriginMode(const bool relativeMode) noexcept
{
_isOriginModeRelative = relativeMode;
return true;
}
// Routine Description:
// - DECAWM - Sets the Auto Wrap Mode.
// This controls whether the cursor moves to the beginning of the next row
// when it reaches the end of the current row.
// Arguments:
// - wrapAtEOL - set to true to wrap, false to overwrite the last character.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetAutoWrapMode(const bool wrapAtEOL)
{
return _pConApi->PrivateSetAutoWrapMode(wrapAtEOL);
}
// Routine Description:
// - DECSTBM - Set Scrolling Region
// This control function sets the top and bottom margins for the current page.
// You cannot perform scrolling outside the margins.
// Default: Margins are at the page limits.
// Arguments:
// - topMargin - the line number for the top margin.
// - bottomMargin - the line number for the bottom margin.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_DoSetTopBottomScrollingMargins(const size_t topMargin,
const size_t bottomMargin)
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
bool success = (_pConApi->MoveToBottom() && _pConApi->GetConsoleScreenBufferInfoEx(csbiex));
// so notes time: (input -> state machine out -> adapter out -> conhost internal)
// having only a top param is legal ([3;r -> 3,0 -> 3,h -> 3,h,true)
// having only a bottom param is legal ([;3r -> 0,3 -> 1,3 -> 1,3,true)
// having neither uses the defaults ([;r [r -> 0,0 -> 0,0 -> 0,0,false)
// an illegal combo (eg, 3;2r) is ignored
if (success)
{
SHORT actualTop = 0;
SHORT actualBottom = 0;
success = SUCCEEDED(SizeTToShort(topMargin, &actualTop)) && SUCCEEDED(SizeTToShort(bottomMargin, &actualBottom));
if (success)
{
const SHORT screenHeight = csbiex.srWindow.Bottom - csbiex.srWindow.Top;
// The default top margin is line 1
if (actualTop == 0)
{
actualTop = 1;
}
// The default bottom margin is the screen height
if (actualBottom == 0)
{
actualBottom = screenHeight;
}
// The top margin must be less than the bottom margin, and the
// bottom margin must be less than or equal to the screen height
success = (actualTop < actualBottom && actualBottom <= screenHeight);
if (success)
{
if (actualTop == 1 && actualBottom == screenHeight)
{
// Client requests setting margins to the entire screen
// - clear them instead of setting them.
// This is for apps like `apt` (NOT `apt-get` which set scroll
// margins, but don't use the alt buffer.)
actualTop = 0;
actualBottom = 0;
}
else
{
// In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1.
actualTop -= 1;
actualBottom -= 1;
}
_scrollMargins.Top = actualTop;
_scrollMargins.Bottom = actualBottom;
success = _pConApi->PrivateSetScrollingRegion(_scrollMargins);
}
}
}
return success;
}
// Routine Description:
// - DECSTBM - Set Scrolling Region
// This control function sets the top and bottom margins for the current page.
// You cannot perform scrolling outside the margins.
// Default: Margins are at the page limits.
// Arguments:
// - topMargin - the line number for the top margin.
// - bottomMargin - the line number for the bottom margin.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetTopBottomScrollingMargins(const size_t topMargin,
const size_t bottomMargin)
{
// When this is called, the cursor should also be moved to home.
// Other functions that only need to set/reset the margins should call _DoSetTopBottomScrollingMargins
return _DoSetTopBottomScrollingMargins(topMargin, bottomMargin) && CursorPosition(1, 1);
}
// Routine Description:
// - BEL - Rings the warning bell.
// Causes the terminal to emit an audible tone of brief duration.
// Arguments:
// - None
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::WarningBell()
{
return _pConApi->PrivateWarningBell();
}
// Routine Description:
// - CR - Performs a carriage return.
// Moves the cursor to the leftmost column.
// Arguments:
// - None
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::CarriageReturn()
{
return _CursorMovePosition(Offset::Unchanged(), Offset::Absolute(1), true);
}
// Routine Description:
// - IND/NEL - Performs a line feed, possibly preceded by carriage return.
// Moves the cursor down one line, and possibly also to the leftmost column.
// Arguments:
// - lineFeedType - Specify whether a carriage return should be performed as well.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType)
{
switch (lineFeedType)
{
case DispatchTypes::LineFeedType::DependsOnMode:
return _pConApi->PrivateLineFeed(_pConApi->PrivateGetLineFeedMode());
case DispatchTypes::LineFeedType::WithoutReturn:
return _pConApi->PrivateLineFeed(false);
case DispatchTypes::LineFeedType::WithReturn:
return _pConApi->PrivateLineFeed(true);
default:
return false;
}
}
// Routine Description:
// - RI - Performs a "Reverse line feed", essentially, the opposite of '\n'.
// Moves the cursor up one line, and tries to keep its position in the line
// Arguments:
// - None
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::ReverseLineFeed()
{
return _pConApi->PrivateReverseLineFeed();
}
// Routine Description:
// - OSC Set Window Title - Sets the title of the window
// Arguments:
// - title - The string to set the title to. Must be null terminated.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetWindowTitle(std::wstring_view title)
{
return _pConApi->SetConsoleTitleW(title);
}
// - ASBSET - Creates and swaps to the alternate screen buffer. In virtual terminals, there exists both a "main"
// screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already
// existing alternate, it is discarded.
// Arguments:
// - None
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::UseAlternateScreenBuffer()
{
bool success = CursorSaveState();
if (success)
{
success = _pConApi->PrivateUseAlternateScreenBuffer();
if (success)
{
_usingAltBuffer = true;
}
}
return success;
}
// Routine Description:
// - ASBRST - From the alternate buffer, returns to the main screen buffer.
// From the main screen buffer, does nothing. The alternate is discarded.
// Arguments:
// - None
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::UseMainScreenBuffer()
{
bool success = _pConApi->PrivateUseMainScreenBuffer();
if (success)
{
_usingAltBuffer = false;
if (success)
{
success = CursorRestoreState();
}
}
return success;
}
//Routine Description:
// HTS - sets a VT tab stop in the cursor's current column.
//Arguments:
// - None
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::HorizontalTabSet()
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
const bool success = _pConApi->GetConsoleScreenBufferInfoEx(csbiex);
if (success)
{
const auto width = csbiex.dwSize.X;
const auto column = csbiex.dwCursorPosition.X;
_InitTabStopsForWidth(width);
_tabStopColumns.at(column) = true;
}
return success;
}
//Routine Description:
// CHT - performing a forwards tab. This will take the
// cursor to the tab stop following its current location. If there are no
// more tabs in this row, it will take it to the right side of the window.
// If it's already in the last column of the row, it will move it to the next line.
//Arguments:
// - numTabs - the number of tabs to perform
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::ForwardTab(const size_t numTabs)
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
bool success = _pConApi->GetConsoleScreenBufferInfoEx(csbiex);
if (success)
{
const auto width = csbiex.dwSize.X;
const auto row = csbiex.dwCursorPosition.Y;
auto column = csbiex.dwCursorPosition.X;
auto tabsPerformed = 0u;
_InitTabStopsForWidth(width);
while (column + 1 < width && tabsPerformed < numTabs)
{
column++;
if (til::at(_tabStopColumns, column))
{
tabsPerformed++;
}
}
success = _pConApi->SetConsoleCursorPosition({ column, row });
}
return success;
}
//Routine Description:
// CBT - performing a backwards tab. This will take the cursor to the tab stop
// previous to its current location. It will not reverse line feed.
//Arguments:
// - numTabs - the number of tabs to perform
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::BackwardsTab(const size_t numTabs)
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
bool success = _pConApi->GetConsoleScreenBufferInfoEx(csbiex);
if (success)
{
const auto width = csbiex.dwSize.X;
const auto row = csbiex.dwCursorPosition.Y;
auto column = csbiex.dwCursorPosition.X;
auto tabsPerformed = 0u;
_InitTabStopsForWidth(width);
while (column > 0 && tabsPerformed < numTabs)
{
column--;
if (til::at(_tabStopColumns, column))
{
tabsPerformed++;
}
}
success = _pConApi->SetConsoleCursorPosition({ column, row });
}
return success;
}
//Routine Description:
// TBC - Used to clear set tab stops. ClearType ClearCurrentColumn (0) results
// in clearing only the tab stop in the cursor's current column, if there
// is one. ClearAllColumns (3) results in resetting all set tab stops.
//Arguments:
// - clearType - Whether to clear the current column, or all columns, defined in DispatchTypes::TabClearType
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::TabClear(const DispatchTypes::TabClearType clearType)
{
bool success = false;
switch (clearType)
{
case DispatchTypes::TabClearType::ClearCurrentColumn:
success = _ClearSingleTabStop();
break;
case DispatchTypes::TabClearType::ClearAllColumns:
success = _ClearAllTabStops();
break;
default:
success = false;
break;
}
return success;
}
// Routine Description:
// - Clears the tab stop in the cursor's current column, if there is one.
// Arguments:
// - <none>
// Return value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_ClearSingleTabStop()
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
const bool success = _pConApi->GetConsoleScreenBufferInfoEx(csbiex);
if (success)
{
const auto width = csbiex.dwSize.X;
const auto column = csbiex.dwCursorPosition.X;
_InitTabStopsForWidth(width);
_tabStopColumns.at(column) = false;
}
return success;
}
// Routine Description:
// - Clears all tab stops and resets the _initDefaultTabStops flag to indicate
// that they shouldn't be reinitialized at the default positions.
// Arguments:
// - <none>
// Return value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_ClearAllTabStops() noexcept
{
_tabStopColumns.clear();
_initDefaultTabStops = false;
return true;
}
// Routine Description:
// - Clears all tab stops and sets the _initDefaultTabStops flag to indicate
// that the default positions should be reinitialized when needed.
// Arguments:
// - <none>
// Return value:
// - <none>
void AdaptDispatch::_ResetTabStops() noexcept
{
_tabStopColumns.clear();
_initDefaultTabStops = true;
}
// Routine Description:
// - Resizes the _tabStopColumns table so it's large enough to support the
// current screen width, initializing tab stops every 8 columns in the
// newly allocated space, iff the _initDefaultTabStops flag is set.
// Arguments:
// - width - the width of the screen buffer that we need to accomodate
// Return value:
// - <none>
void AdaptDispatch::_InitTabStopsForWidth(const size_t width)
{
const auto initialWidth = _tabStopColumns.size();
if (width > initialWidth)
{
_tabStopColumns.resize(width);
if (_initDefaultTabStops)
{
for (auto column = 8u; column < _tabStopColumns.size(); column += 8)
{
if (column >= initialWidth)
{
til::at(_tabStopColumns, column) = true;
}
}
}
}
}
//Routine Description:
// DOCS - Selects the coding system through which character sets are activated.
// When ISO2022 is selected, the code page is set to ISO-8859-1, C1 control
// codes are accepted, and both GL and GR areas of the code table can be
// remapped. When UTF8 is selected, the code page is set to UTF-8, the C1
// control codes are disabled, and only the GL area can be remapped.
//Arguments:
// - codingSystem - The coding system that will be selected.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::DesignateCodingSystem(const VTID codingSystem)
{
// If we haven't previously saved the initial code page, do so now.
// This will be used to restore the code page in response to a reset.
if (!_initialCodePage.has_value())
{
unsigned int currentCodePage;
_pConApi->GetConsoleOutputCP(currentCodePage);
_initialCodePage = currentCodePage;
}
bool success = false;
switch (codingSystem)
{
case DispatchTypes::CodingSystem::ISO2022:
success = _pConApi->SetConsoleOutputCP(28591);
if (success)
{
AcceptC1Controls(true);
_termOutput.EnableGrTranslation(true);
}
break;
case DispatchTypes::CodingSystem::UTF8:
success = _pConApi->SetConsoleOutputCP(CP_UTF8);
if (success)
{
AcceptC1Controls(false);
_termOutput.EnableGrTranslation(false);
}
break;
default:
success = false;
break;
}
return success;
}
//Routine Description:
// Designate Charset - Selects a specific 94-character set into one of the four G-sets.
// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Controls-beginning-with-ESC
// for a list of all charsets and their codes.
// If the specified charset is unsupported, we do nothing (remain on the current one)
//Arguments:
// - gsetNumber - The G-set into which the charset will be selected.
// - charset - The identifier indicating the charset that will be used.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::Designate94Charset(const size_t gsetNumber, const VTID charset)
{
return _termOutput.Designate94Charset(gsetNumber, charset);
}
//Routine Description:
// Designate Charset - Selects a specific 96-character set into one of the four G-sets.
// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Controls-beginning-with-ESC
// for a list of all charsets and their codes.
// If the specified charset is unsupported, we do nothing (remain on the current one)
//Arguments:
// - gsetNumber - The G-set into which the charset will be selected.
// - charset - The identifier indicating the charset that will be used.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::Designate96Charset(const size_t gsetNumber, const VTID charset)
{
return _termOutput.Designate96Charset(gsetNumber, charset);
}
//Routine Description:
// Locking Shift - Invoke one of the G-sets into the left half of the code table.
//Arguments:
// - gsetNumber - The G-set that will be invoked.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::LockingShift(const size_t gsetNumber)
{
return _termOutput.LockingShift(gsetNumber);
}
//Routine Description:
// Locking Shift Right - Invoke one of the G-sets into the right half of the code table.
//Arguments:
// - gsetNumber - The G-set that will be invoked.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::LockingShiftRight(const size_t gsetNumber)
{
return _termOutput.LockingShiftRight(gsetNumber);
}
//Routine Description:
// Single Shift - Temporarily invoke one of the G-sets into the code table.
//Arguments:
// - gsetNumber - The G-set that will be invoked.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::SingleShift(const size_t gsetNumber)
{
return _termOutput.SingleShift(gsetNumber);
}
//Routine Description:
// DECAC1 - Enable or disable the reception of C1 control codes in the parser.
//Arguments:
// - enabled - true to allow C1 controls to be used, false to disallow.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::AcceptC1Controls(const bool enabled)
{
return _pConApi->SetParserMode(StateMachine::Mode::AcceptC1, enabled);
}
//Routine Description:
// Soft Reset - Perform a soft reset. See http://www.vt100.net/docs/vt510-rm/DECSTR.html
// The following table lists everything that should be done, 'X's indicate the ones that
// we actually perform. As the appropriate functionality is added to our ANSI support,
// we should update this.
// X Text cursor enable DECTCEM Cursor enabled.
// Insert/replace IRM Replace mode.
// X Origin DECOM Absolute (cursor origin at upper-left of screen.)
// X Autowrap DECAWM Autowrap enabled (matches XTerm behavior).
// National replacement DECNRCM Multinational set.
// character set
// Keyboard action KAM Unlocked.
// X Numeric keypad DECNKM Numeric characters.
// X Cursor keys DECCKM Normal (arrow keys).
// X Set top and bottom margins DECSTBM Top margin = 1; bottom margin = page length.
// X All character sets G0, G1, G2, Default settings.
// G3, GL, GR
// X Select graphic rendition SGR Normal rendition.
// Select character attribute DECSCA Normal (erasable by DECSEL and DECSED).
// X Save cursor state DECSC Home position.
// Assign user preference DECAUPSS Set selected in Set-Up.
// supplemental set
// Select active DECSASD Main display.
// status display
// Keyboard position mode DECKPM Character codes.
// Cursor direction DECRLM Reset (Left-to-right), regardless of NVR setting.
// PC Term mode DECPCTERM Always reset.
//Arguments:
// <none>
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::SoftReset()
{
bool success = CursorVisibility(true); // Cursor enabled.
success = SetOriginMode(false) && success; // Absolute cursor addressing.
success = SetAutoWrapMode(true) && success; // Wrap at end of line.
success = SetCursorKeysMode(false) && success; // Normal characters.
success = SetKeypadMode(false) && success; // Numeric characters.
// Top margin = 1; bottom margin = page length.
success = _DoSetTopBottomScrollingMargins(0, 0) && success;
_termOutput = {}; // Reset all character set designations.
if (_initialCodePage.has_value())
{
// Restore initial code page if previously changed by a DOCS sequence.
success = _pConApi->SetConsoleOutputCP(_initialCodePage.value()) && success;
}
// Disable parsing of C1 control codes.
success = AcceptC1Controls(false) && success;
success = SetGraphicsRendition({}) && success; // Normal rendition.
// Reset the saved cursor state.
// Note that XTerm only resets the main buffer state, but that
// seems likely to be a bug. Most other terminals reset both.
_savedCursorState.at(0) = {}; // Main buffer
_savedCursorState.at(1) = {}; // Alt buffer
return success;
}
//Routine Description:
// Full Reset - Perform a hard reset of the terminal. http://vt100.net/docs/vt220-rm/chapter4.html
// RIS performs the following actions: (Items with sub-bullets are supported)
// - Switches to the main screen buffer if in the alt buffer.
// * This matches the XTerm behaviour, which is the de facto standard for the alt buffer.
// - Performs a communications line disconnect.
// - Clears UDKs.
// - Clears a down-line-loaded character set.
// * The soft font is reset in the renderer and the font buffer is deleted.
// - Clears the screen.
// * This is like Erase in Display (3), also clearing scrollback, as well as ED(2)
// - Returns the cursor to the upper-left corner of the screen.
// * CUP(1;1)
// - Sets the SGR state to normal.
// * SGR(Off)
// - Sets the selective erase attribute write state to "not erasable".
// - Sets all character sets to the default.
// * G0(USASCII)
//Arguments:
// <none>
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::HardReset()
{
bool success = true;
// If in the alt buffer, switch back to main before doing anything else.
if (_usingAltBuffer)
{
success = _pConApi->PrivateUseMainScreenBuffer();
_usingAltBuffer = !success;
}
// Sets the SGR state to normal - this must be done before EraseInDisplay
// to ensure that it clears with the default background color.
success = SoftReset() && success;
// Clears the screen - Needs to be done in two operations.
success = EraseInDisplay(DispatchTypes::EraseType::All) && success;
success = EraseInDisplay(DispatchTypes::EraseType::Scrollback) && success;
// Set the DECSCNM screen mode back to normal.
success = SetScreenMode(false) && success;
// Cursor to 1,1 - the Soft Reset guarantees this is absolute
success = CursorPosition(1, 1) && success;
// Reset the mouse mode
success = EnableSGRExtendedMouseMode(false) && success;
success = EnableAnyEventMouseMode(false) && success;
// Delete all current tab stops and reapply
_ResetTabStops();
// Clear the soft font in the renderer and delete the font buffer.
success = _pConApi->PrivateUpdateSoftFont({}, {}, false) && success;
_fontBuffer = nullptr;
// GH#2715 - If all this succeeded, but we're in a conpty, return `false` to
// make the state machine propagate this RIS sequence to the connected
// terminal application. We've reset our state, but the connected terminal
// might need to do more.
if (_pConApi->IsConsolePty())
{
return false;
}
return success;
}
// Routine Description:
// - DECALN - Fills the entire screen with a test pattern of uppercase Es,
// resets the margins and rendition attributes, and moves the cursor to
// the home position.
// Arguments:
// - None
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::ScreenAlignmentPattern()
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
bool success = _pConApi->MoveToBottom() && _pConApi->GetConsoleScreenBufferInfoEx(csbiex);
if (success)
{
// Fill the screen with the letter E using the default attributes.
auto fillPosition = COORD{ 0, csbiex.srWindow.Top };
const auto fillLength = (csbiex.srWindow.Bottom - csbiex.srWindow.Top) * csbiex.dwSize.X;
success = _pConApi->PrivateFillRegion(fillPosition, fillLength, L'E', false);
// Reset the line rendition for all of these rows.
success = success && _pConApi->PrivateResetLineRenditionRange(csbiex.srWindow.Top, csbiex.srWindow.Bottom);
// Reset the meta/extended attributes (but leave the colors unchanged).
TextAttribute attr;
if (_pConApi->PrivateGetTextAttributes(attr))
{
attr.SetStandardErase();
success = success && _pConApi->PrivateSetTextAttributes(attr);
}
// Reset the origin mode to absolute.
success = success && SetOriginMode(false);
// Clear the scrolling margins.
success = success && _DoSetTopBottomScrollingMargins(0, 0);
// Set the cursor position to home.
success = success && CursorPosition(1, 1);
}
return success;
}
//Routine Description:
// - Erase Scrollback (^[[3J - ED extension by xterm)
// Because conhost doesn't exactly have a scrollback, We have to be tricky here.
// We need to move the entire viewport to 0,0, and clear everything outside
// (0, 0, viewportWidth, viewportHeight) To give the appearance that
// everything above the viewport was cleared.
// We don't want to save the text BELOW the viewport, because in *nix, there isn't anything there
// (There isn't a scroll-forward, only a scrollback)
// Arguments:
// - <none>
// Return value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_EraseScrollback()
{
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(csbiex);
// Make sure to reset the viewport (with MoveToBottom )to where it was
// before the user scrolled the console output
bool success = (_pConApi->GetConsoleScreenBufferInfoEx(csbiex) && _pConApi->MoveToBottom());
if (success)
{
const SMALL_RECT screen = csbiex.srWindow;
const SHORT height = screen.Bottom - screen.Top;
FAIL_FAST_IF(!(height > 0));
const COORD cursor = csbiex.dwCursorPosition;
// Rectangle to cut out of the existing buffer
// It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width.
SMALL_RECT scroll = screen;
scroll.Left = 0;
scroll.Right = SHORT_MAX;
// Paste coordinate for cut text above
COORD destination;
destination.X = 0;
destination.Y = 0;
// Typically a scroll operation should fill with standard erase attributes, but in
// this case we need to use the default attributes, hence standardFillAttrs is false.
success = _pConApi->PrivateScrollRegion(scroll, std::nullopt, destination, false);
if (success)
{
// Clear everything after the viewport.
const DWORD totalAreaBelow = csbiex.dwSize.X * (csbiex.dwSize.Y - height);
const COORD coordBelowStartPosition = { 0, height };
// Again we need to use the default attributes, hence standardFillAttrs is false.
success = _pConApi->PrivateFillRegion(coordBelowStartPosition, totalAreaBelow, L' ', false);
// Also reset the line rendition for all of the cleared rows.
success = success && _pConApi->PrivateResetLineRenditionRange(height, csbiex.dwSize.Y);
if (success)
{
// Move the viewport (CAN'T be done in one call with SetConsolescreenBufferInfoEx, because legacy)
SMALL_RECT newViewport;
newViewport.Left = screen.Left;
newViewport.Top = 0;
// SetConsoleWindowInfo uses an inclusive rect, while GetConsolescreenBufferInfo is exclusive
newViewport.Right = screen.Right - 1;
newViewport.Bottom = height - 1;
success = _pConApi->SetConsoleWindowInfo(true, newViewport);
if (success)
{
// Move the cursor to the same relative location.
const COORD newcursor = { cursor.X, cursor.Y - screen.Top };
success = _pConApi->SetConsoleCursorPosition(newcursor);
}
}
}
}
return success;
}
//Routine Description:
// Erase All (^[[2J - ED)
// Erase the current contents of the viewport. In most terminals, because they
// only have a scrollback (and not a buffer per-se), they implement this
// by scrolling the current contents of the buffer off of the screen.
// We can't properly replicate this behavior with only the public API, because
// we need to know where the last character in the buffer is. (it may be below the viewport)
//Arguments:
// <none>
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::_EraseAll()
{
return _pConApi->PrivateEraseAll();
}
// Routine Description:
// - Enables or disables support for the DECCOLM escape sequence.
// Arguments:
// - enabled - set to true to allow DECCOLM to be used, false to disallow.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::EnableDECCOLMSupport(const bool enabled) noexcept
{
_isDECCOLMAllowed = enabled;
return true;
}
//Routine Description:
// Enable VT200 Mouse Mode - Enables/disables the mouse input handler in default tracking mode.
//Arguments:
// - enabled - true to enable, false to disable.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::EnableVT200MouseMode(const bool enabled)
{
return _pConApi->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, enabled);
}
//Routine Description:
// Enable UTF-8 Extended Encoding - this changes the encoding scheme for sequences
// emitted by the mouse input handler. Does not enable/disable mouse mode on its own.
//Arguments:
// - enabled - true to enable, false to disable.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::EnableUTF8ExtendedMouseMode(const bool enabled)
{
return _pConApi->SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, enabled);
}
//Routine Description:
// Enable SGR Extended Encoding - this changes the encoding scheme for sequences
// emitted by the mouse input handler. Does not enable/disable mouse mode on its own.
//Arguments:
// - enabled - true to enable, false to disable.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::EnableSGRExtendedMouseMode(const bool enabled)
{
return _pConApi->SetInputMode(TerminalInput::Mode::SgrMouseEncoding, enabled);
}
//Routine Description:
// Enable Button Event mode - send mouse move events WITH A BUTTON PRESSED to the input.
//Arguments:
// - enabled - true to enable, false to disable.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::EnableButtonEventMouseMode(const bool enabled)
{
return _pConApi->SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, enabled);
}
//Routine Description:
// Enable Any Event mode - send all mouse events to the input.
//Arguments:
// - enabled - true to enable, false to disable.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::EnableAnyEventMouseMode(const bool enabled)
{
return _pConApi->SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, enabled);
}
//Routine Description:
// Enable Alternate Scroll Mode - When in the Alt Buffer, send CUP and CUD on
// scroll up/down events instead of the usual sequences
//Arguments:
// - enabled - true to enable, false to disable.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::EnableAlternateScroll(const bool enabled)
{
return _pConApi->SetInputMode(TerminalInput::Mode::AlternateScroll, enabled);
}
//Routine Description:
// Enable "bracketed paste mode".
//Arguments:
// - enabled - true to enable, false to disable.
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::EnableXtermBracketedPasteMode(const bool /*enabled*/) noexcept
{
return NoOp();
}
//Routine Description:
// Set Cursor Style - Changes the cursor's style to match the given Dispatch
// cursor style. Unix styles are a combination of the shape and the blinking state.
//Arguments:
// - cursorStyle - The unix-like cursor style to apply to the cursor
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle)
{
CursorType actualType = CursorType::Legacy;
bool fEnableBlinking = false;
switch (cursorStyle)
{
case DispatchTypes::CursorStyle::UserDefault:
_pConApi->GetUserDefaultCursorStyle(actualType);
fEnableBlinking = true;
break;
case DispatchTypes::CursorStyle::BlinkingBlock:
fEnableBlinking = true;
actualType = CursorType::FullBox;
break;
case DispatchTypes::CursorStyle::SteadyBlock:
fEnableBlinking = false;
actualType = CursorType::FullBox;
break;
case DispatchTypes::CursorStyle::BlinkingUnderline:
fEnableBlinking = true;
actualType = CursorType::Underscore;
break;
case DispatchTypes::CursorStyle::SteadyUnderline:
fEnableBlinking = false;
actualType = CursorType::Underscore;
break;
case DispatchTypes::CursorStyle::BlinkingBar:
fEnableBlinking = true;
actualType = CursorType::VerticalBar;
break;
case DispatchTypes::CursorStyle::SteadyBar:
fEnableBlinking = false;
actualType = CursorType::VerticalBar;
break;
default:
// Invalid argument should be handled by the connected terminal.
return false;
}
bool success = _pConApi->SetCursorStyle(actualType);
if (success)
{
success = _pConApi->PrivateAllowCursorBlinking(fEnableBlinking);
}
// If we're a conpty, always return false, so that this cursor state will be
// sent to the connected terminal
if (_pConApi->IsConsolePty())
{
return false;
}
return success;
}
// Method Description:
// - Sets a single entry of the colortable to a new value
// Arguments:
// - tableIndex: The VT color table index
// - dwColor: The new RGB color value to use.
// Return Value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::SetCursorColor(const COLORREF cursorColor)
{
if (_pConApi->IsConsolePty())
{
return false;
}
return _pConApi->SetCursorColor(cursorColor);
}
// Routine Description:
// - OSC Copy to Clipboard
// Arguments:
// - content - The content to copy to clipboard. Must be null terminated.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetClipboard(const std::wstring_view /*content*/) noexcept
{
return false;
}
// Method Description:
// - Sets a single entry of the colortable to a new value
// Arguments:
// - tableIndex: The VT color table index
// - dwColor: The new RGB color value to use.
// Return Value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::SetColorTableEntry(const size_t tableIndex, const DWORD dwColor)
{
const bool success = _pConApi->PrivateSetColorTableEntry(tableIndex, dwColor);
// If we're a conpty, always return false, so that we send the updated color
// value to the terminal. Still handle the sequence so apps that use
// the API or VT to query the values of the color table still read the
// correct color.
if (_pConApi->IsConsolePty())
{
return false;
}
return success;
}
// Method Description:
// - Sets the default foreground color to a new value
// Arguments:
// - dwColor: The new RGB color value to use, as a COLORREF, format 0x00BBGGRR.
// Return Value:
// True if handled successfully. False otherwise.
bool Microsoft::Console::VirtualTerminal::AdaptDispatch::SetDefaultForeground(const DWORD dwColor)
{
bool success = true;
success = _pConApi->PrivateSetDefaultForeground(dwColor);
// If we're a conpty, always return false, so that we send the updated color
// value to the terminal. Still handle the sequence so apps that use
// the API or VT to query the values of the color table still read the
// correct color.
if (_pConApi->IsConsolePty())
{
return false;
}
return success;
}
// Method Description:
// - Sets the default background color to a new value
// Arguments:
// - dwColor: The new RGB color value to use, as a COLORREF, format 0x00BBGGRR.
// Return Value:
// True if handled successfully. False otherwise.
bool Microsoft::Console::VirtualTerminal::AdaptDispatch::SetDefaultBackground(const DWORD dwColor)
{
bool success = true;
success = _pConApi->PrivateSetDefaultBackground(dwColor);
// If we're a conpty, always return false, so that we send the updated color
// value to the terminal. Still handle the sequence so apps that use
// the API or VT to query the values of the color table still read the
// correct color.
if (_pConApi->IsConsolePty())
{
return false;
}
return success;
}
//Routine Description:
// Window Manipulation - Performs a variety of actions relating to the window,
// such as moving the window position, resizing the window, querying
// window state, forcing the window to repaint, etc.
// This is kept separate from the input version, as there may be
// codes that are supported in one direction but not the other.
//Arguments:
// - function - An identifier of the WindowManipulation function to perform
// - parameter1 - The first optional parameter for the function
// - parameter2 - The second optional parameter for the function
// Return value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationType function,
const VTParameter parameter1,
const VTParameter parameter2)
{
bool success = false;
// Other Window Manipulation functions:
// MSFT:13271098 - QueryViewport
// MSFT:13271146 - QueryScreenSize
switch (function)
{
case DispatchTypes::WindowManipulationType::RefreshWindow:
success = DispatchCommon::s_RefreshWindow(*_pConApi);
break;
case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters:
success = DispatchCommon::s_ResizeWindow(*_pConApi, parameter2.value_or(0), parameter1.value_or(0));
break;
default:
success = false;
}
return success;
}
// Method Description:
// - Starts a hyperlink
// Arguments:
// - The hyperlink URI, optional additional parameters
// Return Value:
// - true
bool AdaptDispatch::AddHyperlink(const std::wstring_view uri, const std::wstring_view params)
{
return _pConApi->PrivateAddHyperlink(uri, params);
}
// Method Description:
// - Ends a hyperlink
// Return Value:
// - true
bool AdaptDispatch::EndHyperlink()
{
return _pConApi->PrivateEndHyperlink();
}
// Method Description:
// - Ascribes to the ITermDispatch interface
// - Not actually used in conhost
// Return Value:
// - false (so that the command gets flushed to terminal)
bool AdaptDispatch::DoConEmuAction(const std::wstring_view /*string*/) noexcept
{
return false;
}
// Method Description:
// - DECDLD - Downloads one or more characters of a dynamically redefinable
// character set (DRCS) with a specified pixel pattern. The pixel array is
// transmitted in sixel format via the returned StringHandler function.
// Arguments:
// - fontNumber - The buffer number into which the font will be loaded.
// - startChar - The first character in the set that will be replaced.
// - eraseControl - Which characters to erase before loading the new data.
// - cellMatrix - The character cell width (sometimes also height in legacy formats).
// - fontSet - The screen size for which the font is designed.
// - fontUsage - Whether it is a text font or a full-cell font.
// - cellHeight - The character cell height (if not defined by cellMatrix).
// - charsetSize - Whether the character set is 94 or 96 characters.
// Return Value:
// - a function to receive the pixel data or nullptr if parameters are invalid
ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const size_t fontNumber,
const VTParameter startChar,
const DispatchTypes::DrcsEraseControl eraseControl,
const DispatchTypes::DrcsCellMatrix cellMatrix,
const DispatchTypes::DrcsFontSet fontSet,
const DispatchTypes::DrcsFontUsage fontUsage,
const VTParameter cellHeight,
const DispatchTypes::DrcsCharsetSize charsetSize)
{
// If we're a conpty, we're just going to ignore the operation for now.
// There's no point in trying to pass it through without also being able
// to pass through the character set designations.
if (_pConApi->IsConsolePty())
{
return nullptr;
}
// The font buffer is created on demand.
if (!_fontBuffer)
{
_fontBuffer = std::make_unique<FontBuffer>();
}
// Only one font buffer is supported, so only 0 (default) and 1 are valid.
auto success = fontNumber <= 1;
success = success && _fontBuffer->SetEraseControl(eraseControl);
success = success && _fontBuffer->SetAttributes(cellMatrix, cellHeight, fontSet, fontUsage);
success = success && _fontBuffer->SetStartChar(startChar, charsetSize);
// If any of the parameters are invalid, we return a null handler to let
// the state machine know we want to ignore the subsequent data string.
if (!success)
{
return nullptr;
}
return [=](const auto ch) {
// We pass the data string straight through to the font buffer class
// until we receive an ESC, indicating the end of the string. At that
// point we can finalize the buffer, and if valid, update the renderer
// with the constructed bit pattern.
if (ch != AsciiChars::ESC)
{
_fontBuffer->AddSixelData(ch);
}
else if (_fontBuffer->FinalizeSixelData())
{
// We also need to inform the character set mapper of the ID that
// will map to this font (we only support one font buffer so there
// will only ever be one active dynamic character set).
if (charsetSize == DispatchTypes::DrcsCharsetSize::Size96)
{
_termOutput.SetDrcs96Designation(_fontBuffer->GetDesignation());
}
else
{
_termOutput.SetDrcs94Designation(_fontBuffer->GetDesignation());
}
const auto bitPattern = _fontBuffer->GetBitPattern();
const auto cellSize = _fontBuffer->GetCellSize();
const auto centeringHint = _fontBuffer->GetTextCenteringHint();
_pConApi->PrivateUpdateSoftFont(bitPattern, cellSize, centeringHint);
}
return true;
};
}
// Method Description:
// - DECRQSS - Requests the state of a VT setting. The value being queried is
// identified by the intermediate and final characters of its control
// sequence, which are passed to the string handler.
// Arguments:
// - None
// Return Value:
// - a function to receive the VTID of the setting being queried
ITermDispatch::StringHandler AdaptDispatch::RequestSetting()
{
// We use a VTIDBuilder to parse the characters in the control string into
// an ID which represents the setting being queried. If the given ID isn't
// supported, we respond with an error sequence: DCS 0 $ r ST. Note that
// this is the opposite of what is documented in most DEC manuals, which
// say that 0 is for a valid response, and 1 is for an error. The correct
// interpretation is documented in the DEC STD 070 reference.
const auto idBuilder = std::make_shared<VTIDBuilder>();
return [=](const auto ch) {
if (ch >= '\x40' && ch <= '\x7e')
{
const auto id = idBuilder->Finalize(ch);
switch (id)
{
case VTID('m'):
_ReportSGRSetting();
break;
case VTID('r'):
_ReportDECSTBMSetting();
break;
default:
_WriteResponse(L"\033P0$r\033\\");
break;
}
return false;
}
else
{
if (ch >= '\x20' && ch <= '\x2f')
{
idBuilder->AddIntermediate(ch);
}
return true;
}
};
}
// Method Description:
// - Reports the current SGR attributes in response to a DECRQSS query.
// Arguments:
// - None
// Return Value:
// - None
void AdaptDispatch::_ReportSGRSetting() const
{
using namespace std::string_view_literals;
// A valid response always starts with DCS 1 $ r.
// Then the '0' parameter is to reset the SGR attributes to the defaults.
fmt::basic_memory_buffer<wchar_t, 64> response;
response.append(L"\033P1$r0"sv);
TextAttribute attr;
if (_pConApi->PrivateGetTextAttributes(attr))
{
// For each boolean attribute that is set, we add the appropriate
// parameter value to the response string.
const auto addAttribute = [&](const auto& parameter, const auto enabled) {
if (enabled)
{
response.append(parameter);
}
};
addAttribute(L";1"sv, attr.IsBold());
addAttribute(L";2"sv, attr.IsFaint());
addAttribute(L";3"sv, attr.IsItalic());
addAttribute(L";4"sv, attr.IsUnderlined());
addAttribute(L";5"sv, attr.IsBlinking());
addAttribute(L";7"sv, attr.IsReverseVideo());
addAttribute(L";8"sv, attr.IsInvisible());
addAttribute(L";9"sv, attr.IsCrossedOut());
addAttribute(L";21"sv, attr.IsDoublyUnderlined());
addAttribute(L";53"sv, attr.IsOverlined());
// We also need to add the appropriate color encoding parameters for
// both the foreground and background colors.
const auto addColor = [&](const auto base, const auto color) {
if (color.IsIndex16())
{
const auto index = color.GetIndex();
const auto colorParameter = base + (index >= 8 ? 60 : 0) + (index % 8);
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{}"), colorParameter);
}
else if (color.IsIndex256())
{
const auto index = color.GetIndex();
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{};5;{}"), base + 8, index);
}
else if (color.IsRgb())
{
const auto r = GetRValue(color.GetRGB());
const auto g = GetGValue(color.GetRGB());
const auto b = GetBValue(color.GetRGB());
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{};2;{};{};{}"), base + 8, r, g, b);
}
};
addColor(30, attr.GetForeground());
addColor(40, attr.GetBackground());
}
// The 'm' indicates this is an SGR response, and ST ends the sequence.
response.append(L"m\033\\"sv);
_WriteResponse({ response.data(), response.size() });
}
// Method Description:
// - Reports the DECSTBM margin range in response to a DECRQSS query.
// Arguments:
// - None
// Return Value:
// - None
void AdaptDispatch::_ReportDECSTBMSetting() const
{
using namespace std::string_view_literals;
// A valid response always starts with DCS 1 $ r.
fmt::basic_memory_buffer<wchar_t, 64> response;
response.append(L"\033P1$r"sv);
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
if (_pConApi->GetConsoleScreenBufferInfoEx(csbiex))
{
auto marginTop = _scrollMargins.Top + 1;
auto marginBottom = _scrollMargins.Bottom + 1;
// If the margin top is greater than or equal to the bottom, then the
// margins aren't actually set, so we need to return the full height
// of the window for the margin range.
if (marginTop >= marginBottom)
{
marginTop = 1;
marginBottom = csbiex.srWindow.Bottom - csbiex.srWindow.Top;
}
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginTop, marginBottom);
}
// The 'r' indicates this is an DECSTBM response, and ST ends the sequence.
response.append(L"r\033\\"sv);
_WriteResponse({ response.data(), response.size() });
}