terminal/src/terminal/adapter/adaptDispatch.cpp
Mike Griese e49afb3ccd
Mark the AdaptDispatch ctor as noexcept (#17417)
CI is complaining about this on all new builds, in audit mode. But I
don't think anything changed here recently. Maybe just new audit rules
rolled out?
2024-06-10 20:52:29 +00:00

4983 lines
191 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "adaptDispatch.hpp"
#include "../../renderer/base/renderer.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::Render;
using namespace Microsoft::Console::VirtualTerminal;
static constexpr std::wstring_view whitespace{ L" " };
AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettings& renderSettings, TerminalInput& terminalInput) noexcept :
_api{ api },
_renderer{ renderer },
_renderSettings{ renderSettings },
_terminalInput{ terminalInput },
_usingAltBuffer(false),
_termOutput(),
_pages{ api, renderer }
{
}
// 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)
{
_WriteToBuffer({ &wchTranslated, 1 });
}
}
// 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)
{
if (_termOutput.NeedToTranslate())
{
std::wstring buffer;
buffer.reserve(string.size());
for (auto& wch : string)
{
buffer.push_back(_termOutput.TranslateKey(wch));
}
_WriteToBuffer(buffer);
}
else
{
_WriteToBuffer(string);
}
}
void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
{
auto page = _pages.ActivePage();
auto& textBuffer = page.Buffer();
auto& cursor = page.Cursor();
auto cursorPosition = cursor.GetPosition();
const auto wrapAtEOL = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap);
const auto& attributes = page.Attributes();
auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width());
auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y);
if (cursorPosition.x <= rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin)
{
lineWidth = std::min(lineWidth, rightMargin + 1);
}
// Turn off the cursor until we're done, so it isn't refreshed unnecessarily.
cursor.SetIsOn(false);
RowWriteState state{
.text = string,
.columnLimit = lineWidth,
};
while (!state.text.empty())
{
if (cursor.IsDelayedEOLWrap() && wrapAtEOL)
{
const auto delayedCursorPosition = cursor.GetDelayedAtPosition();
cursor.ResetDelayEOLWrap();
// Only act on a delayed EOL if we didn't move the cursor to a
// different position from where the EOL was marked.
if (delayedCursorPosition == cursorPosition)
{
if (_DoLineFeed(page, true, true))
{
// If the line feed caused the viewport to move down, we
// need to adjust the page viewport and margins to match.
page.MoveViewportDown();
std::tie(topMargin, bottomMargin) = _GetVerticalMargins(page, true);
}
cursorPosition = cursor.GetPosition();
// We need to recalculate the width when moving to a new line.
lineWidth = textBuffer.GetLineWidth(cursorPosition.y);
if (cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin)
{
lineWidth = std::min(lineWidth, rightMargin + 1);
}
state.columnLimit = lineWidth;
}
}
state.columnBegin = cursorPosition.x;
const auto textPositionBefore = state.text.data();
if (_modes.test(Mode::InsertReplace))
{
textBuffer.Insert(cursorPosition.y, attributes, state);
}
else
{
textBuffer.Replace(cursorPosition.y, attributes, state);
}
const auto textPositionAfter = state.text.data();
// TODO: A row should not be marked as wrapped just because we wrote the last column.
// It should be marked whenever we write _past_ it (above, _DoLineFeed call). See GH#15602.
if (wrapAtEOL && state.columnEnd >= state.columnLimit)
{
textBuffer.SetWrapForced(cursorPosition.y, true);
}
if (state.columnBeginDirty != state.columnEndDirty)
{
const til::rect changedRect{ state.columnBeginDirty, cursorPosition.y, state.columnEndDirty, cursorPosition.y + 1 };
_api.NotifyAccessibilityChange(changedRect);
}
// If we're past the end of the line, we need to clamp the cursor
// back into range, and if wrapping is enabled, set the delayed wrap
// flag. The wrapping only occurs once another character is output.
const auto isWrapping = state.columnEnd >= state.columnLimit;
cursorPosition.x = isWrapping ? state.columnLimit - 1 : state.columnEnd;
cursor.SetPosition(cursorPosition);
if (isWrapping)
{
// We want to wrap, but we failed to write even a single character into the row.
// ROW::Write() returns the lineWidth and leaves stringIterator untouched. To prevent a
// deadlock, because stringIterator never advances, we need to throw that glyph away.
//
// This can happen under two circumstances:
// * The glyph is wider than the buffer and can never be inserted in
// the first place. There's no good way to detect this, so we check
// whether the begin column is the left margin, which is the column
// at which any legit insertion should work at a minimum.
// * The DECAWM Autowrap mode is disabled ("\x1b[?7l", !wrapAtEOL) and
// we tried writing a wide glyph into the last column which can't work.
if (textPositionBefore == textPositionAfter && (state.columnBegin == 0 || !wrapAtEOL))
{
state.text = state.text.substr(textBuffer.GraphemeNext(state.text, 0));
}
if (wrapAtEOL)
{
cursor.DelayEOLWrap();
}
}
}
_ApplyCursorMovementFlags(cursor);
// Notify terminal and UIA of new text.
// It's important to do this here instead of in TextBuffer, because here you
// have access to the entire line of text, whereas TextBuffer writes it one
// character at a time via the OutputCellIterator.
textBuffer.TriggerNewTextNotification(string);
}
// 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.
bool AdaptDispatch::CursorUp(const VTInt 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.
bool AdaptDispatch::CursorDown(const VTInt 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.
bool AdaptDispatch::CursorForward(const VTInt 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.
bool AdaptDispatch::CursorBackward(const VTInt 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.
bool AdaptDispatch::CursorNextLine(const VTInt 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.
bool AdaptDispatch::CursorPrevLine(const VTInt distance)
{
return _CursorMovePosition(Offset::Backward(distance), Offset::Absolute(1), true);
}
// Routine Description:
// - Returns the coordinates of the vertical scroll margins.
// Arguments:
// - page - The page that the margins will apply to.
// - absolute - Should coordinates be absolute or relative to the page top.
// Return Value:
// - A std::pair containing the top and bottom coordinates (inclusive).
std::pair<int, int> AdaptDispatch::_GetVerticalMargins(const Page& page, const bool absolute) noexcept
{
// If the top is out of range, reset the margins completely.
const auto bottommostRow = page.Height() - 1;
if (_scrollMargins.top >= bottommostRow)
{
_scrollMargins.top = _scrollMargins.bottom = 0;
}
// If margins aren't set, use the full extent of the page.
const auto marginsSet = _scrollMargins.top < _scrollMargins.bottom;
auto topMargin = marginsSet ? _scrollMargins.top : 0;
auto bottomMargin = marginsSet ? _scrollMargins.bottom : bottommostRow;
// If the bottom is out of range, clamp it to the bottommost row.
bottomMargin = std::min(bottomMargin, bottommostRow);
if (absolute)
{
topMargin += page.Top();
bottomMargin += page.Top();
}
return { topMargin, bottomMargin };
}
// Routine Description:
// - Returns the coordinates of the horizontal scroll margins.
// Arguments:
// - pageWidth - The width of the page
// Return Value:
// - A std::pair containing the left and right coordinates (inclusive).
std::pair<int, int> AdaptDispatch::_GetHorizontalMargins(const til::CoordType pageWidth) noexcept
{
// If the left is out of range, reset the margins completely.
const auto rightmostColumn = pageWidth - 1;
if (_scrollMargins.left >= rightmostColumn)
{
_scrollMargins.left = _scrollMargins.right = 0;
}
// If margins aren't set, use the full extent of the buffer.
const auto marginsSet = _scrollMargins.left < _scrollMargins.right;
auto leftMargin = marginsSet ? _scrollMargins.left : 0;
auto rightMargin = marginsSet ? _scrollMargins.right : rightmostColumn;
// If the right is out of range, clamp it to the rightmost column.
rightMargin = std::min(rightMargin, rightmostColumn);
return { leftMargin, rightMargin };
}
// 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.
bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins)
{
// First retrieve some information about the buffer
const auto page = _pages.ActivePage();
auto& cursor = page.Cursor();
const auto pageWidth = page.Width();
const auto cursorPosition = cursor.GetPosition();
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth);
// For relative movement, the given offsets will be relative to
// the current cursor position.
auto row = cursorPosition.y;
auto col = cursorPosition.x;
// But if the row is absolute, it will be relative to the top of the
// page, or the top margin, depending on the origin mode.
if (rowOffset.IsAbsolute)
{
row = _modes.test(Mode::Origin) ? topMargin : page.Top();
}
// And if the column is absolute, it'll be relative to column 0,
// or the left margin, depending on the origin mode.
// Horizontal positions are not affected by the viewport.
if (colOffset.IsAbsolute)
{
col = _modes.test(Mode::Origin) ? leftMargin : 0;
}
// Adjust the base position by the given offsets and clamp the results.
// The row is constrained within the page's vertical boundaries,
// while the column is constrained by the buffer width.
row = std::clamp(row + rowOffset.Value, page.Top(), page.Bottom() - 1);
col = std::clamp(col + colOffset.Value, 0, pageWidth - 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
// and column may need to be adjusted further.
if (clampInMargins || _modes.test(Mode::Origin))
{
// Vertical margins only apply if the original position is inside the
// horizontal margins. Also, the cursor will only be clamped inside the
// top margin if it was already below the top margin to start with, and
// it will only be clamped inside the bottom margin if it was already
// above the bottom margin to start with.
if (cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin)
{
if (cursorPosition.y >= topMargin)
{
row = std::max(row, topMargin);
}
if (cursorPosition.y <= bottomMargin)
{
row = std::min(row, bottomMargin);
}
}
// Similarly, horizontal margins only apply if the new row is inside the
// vertical margins. And the cursor is only clamped inside the horizontal
// margins if it was already inside to start with.
if (row >= topMargin && row <= bottomMargin)
{
if (cursorPosition.x >= leftMargin)
{
col = std::max(col, leftMargin);
}
if (cursorPosition.x <= rightMargin)
{
col = std::min(col, rightMargin);
}
}
}
// Finally, attempt to set the adjusted cursor position back into the console.
cursor.SetPosition(page.Buffer().ClampPositionWithinLine({ col, row }));
_ApplyCursorMovementFlags(cursor);
return true;
}
// Routine Description:
// - Helper method which applies a bunch of flags that are typically set whenever
// the cursor is moved. The IsOn flag is set to true, and the Delay flag to false,
// to force a blinking cursor to be visible, so the user can immediately see the
// new position. The HasMoved flag is set to let the accessibility notifier know
// that there was movement that needs to be reported.
// Arguments:
// - cursor - The cursor instance to be updated
// Return Value:
// - <none>
void AdaptDispatch::_ApplyCursorMovementFlags(Cursor& cursor) noexcept
{
cursor.SetDelay(false);
cursor.SetIsOn(true);
cursor.SetHasMoved(true);
}
// 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.
bool AdaptDispatch::CursorHorizontalPositionAbsolute(const VTInt 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.
bool AdaptDispatch::VerticalLinePositionAbsolute(const VTInt 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.
bool AdaptDispatch::HorizontalPositionRelative(const VTInt 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.
bool AdaptDispatch::VerticalPositionRelative(const VTInt 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.
bool AdaptDispatch::CursorPosition(const VTInt line, const VTInt 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.
bool AdaptDispatch::CursorSaveState()
{
// First retrieve some information about the buffer
const auto page = _pages.ActivePage();
// 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 page top.
auto cursorPosition = page.Cursor().GetPosition();
cursorPosition.y -= page.Top();
// Although if origin mode is set, the cursor is relative to the margin origin.
if (_modes.test(Mode::Origin))
{
cursorPosition.x -= _GetHorizontalMargins(page.Width()).first;
cursorPosition.y -= _GetVerticalMargins(page, false).first;
}
// VT is also 1 based, not 0 based, so correct by 1.
auto& savedCursorState = _savedCursorState.at(_usingAltBuffer);
savedCursorState.Column = cursorPosition.x + 1;
savedCursorState.Row = cursorPosition.y + 1;
savedCursorState.Page = page.Number();
savedCursorState.IsDelayedEOLWrap = page.Cursor().IsDelayedEOLWrap();
savedCursorState.IsOriginModeRelative = _modes.test(Mode::Origin);
savedCursorState.Attributes = page.Attributes();
savedCursorState.TermOutput = _termOutput;
return true;
}
// 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.
bool AdaptDispatch::CursorRestoreState()
{
auto& savedCursorState = _savedCursorState.at(_usingAltBuffer);
// Restore the origin mode first, since the cursor coordinates may be relative.
_modes.set(Mode::Origin, savedCursorState.IsOriginModeRelative);
// Restore the page number.
PagePositionAbsolute(savedCursorState.Page);
// We can then restore the position with a standard CUP operation.
CursorPosition(savedCursorState.Row, savedCursorState.Column);
// If the delayed wrap flag was set when the cursor was saved, we need to restore that now.
const auto page = _pages.ActivePage();
if (savedCursorState.IsDelayedEOLWrap)
{
page.Cursor().DelayEOLWrap();
}
// Restore text attributes.
page.SetAttributes(savedCursorState.Attributes, &_api);
// Restore designated character sets.
_termOutput.RestoreFrom(savedCursorState.TermOutput);
return true;
}
// Routine Description:
// - Returns the attributes that should be used when erasing the buffer. When
// the Erase Color mode is set, we use the default attributes, but when reset,
// we use the active color attributes with the character attributes cleared.
// Arguments:
// - page - Target page that is being erased.
// Return Value:
// - The erase TextAttribute value.
TextAttribute AdaptDispatch::_GetEraseAttributes(const Page& page) const noexcept
{
if (_modes.test(Mode::EraseColor))
{
return {};
}
else
{
auto eraseAttributes = page.Attributes();
eraseAttributes.SetStandardErase();
return eraseAttributes;
}
}
// Routine Description:
// - Scrolls an area of the buffer in a vertical direction.
// Arguments:
// - page - Target page to be scrolled.
// - fillRect - Area of the page that will be affected.
// - delta - Distance to move (positive is down, negative is up).
// Return Value:
// - <none>
void AdaptDispatch::_ScrollRectVertically(const Page& page, const til::rect& scrollRect, const VTInt delta)
{
auto& textBuffer = page.Buffer();
const auto absoluteDelta = std::min(std::abs(delta), scrollRect.height());
if (absoluteDelta < scrollRect.height())
{
const auto top = delta > 0 ? scrollRect.top : scrollRect.top + absoluteDelta;
const auto width = scrollRect.width();
const auto height = scrollRect.height() - absoluteDelta;
const auto actualDelta = delta > 0 ? absoluteDelta : -absoluteDelta;
if (width == page.Width())
{
// If the scrollRect is the full width of the buffer, we can scroll
// more efficiently by rotating the row storage.
textBuffer.ScrollRows(top, height, actualDelta);
textBuffer.TriggerRedraw(Viewport::FromExclusive(scrollRect));
}
else
{
// Otherwise we have to move the content up or down by copying the
// requested buffer range one cell at a time.
const auto srcOrigin = til::point{ scrollRect.left, top };
const auto dstOrigin = til::point{ scrollRect.left, top + actualDelta };
const auto srcView = Viewport::FromDimensions(srcOrigin, width, height);
const auto dstView = Viewport::FromDimensions(dstOrigin, width, height);
const auto walkDirection = Viewport::DetermineWalkDirection(srcView, dstView);
auto srcPos = srcView.GetWalkOrigin(walkDirection);
auto dstPos = dstView.GetWalkOrigin(walkDirection);
do
{
const auto current = OutputCell(*textBuffer.GetCellDataAt(srcPos));
textBuffer.WriteLine(OutputCellIterator({ &current, 1 }), dstPos);
srcView.WalkInBounds(srcPos, walkDirection);
} while (dstView.WalkInBounds(dstPos, walkDirection));
}
}
// Rows revealed by the scroll are filled with standard erase attributes.
auto eraseRect = scrollRect;
eraseRect.top = delta > 0 ? scrollRect.top : (scrollRect.bottom - absoluteDelta);
eraseRect.bottom = eraseRect.top + absoluteDelta;
const auto eraseAttributes = _GetEraseAttributes(page);
_FillRect(page, eraseRect, whitespace, eraseAttributes);
// Also reset the line rendition for the erased rows.
textBuffer.ResetLineRenditionRange(eraseRect.top, eraseRect.bottom);
}
// Routine Description:
// - Scrolls an area of the buffer in a horizontal direction.
// Arguments:
// - page - Target page to be scrolled.
// - fillRect - Area of the page that will be affected.
// - delta - Distance to move (positive is right, negative is left).
// Return Value:
// - <none>
void AdaptDispatch::_ScrollRectHorizontally(const Page& page, const til::rect& scrollRect, const VTInt delta)
{
auto& textBuffer = page.Buffer();
const auto absoluteDelta = std::min(std::abs(delta), scrollRect.width());
if (absoluteDelta < scrollRect.width())
{
const auto left = delta > 0 ? scrollRect.left : (scrollRect.left + absoluteDelta);
const auto top = scrollRect.top;
const auto width = scrollRect.width() - absoluteDelta;
const auto height = scrollRect.height();
const auto actualDelta = delta > 0 ? absoluteDelta : -absoluteDelta;
const auto source = Viewport::FromDimensions({ left, top }, width, height);
const auto target = Viewport::Offset(source, { actualDelta, 0 });
const auto walkDirection = Viewport::DetermineWalkDirection(source, target);
auto sourcePos = source.GetWalkOrigin(walkDirection);
auto targetPos = target.GetWalkOrigin(walkDirection);
// Note that we read two cells from the source before we start writing
// to the target, so a two-cell DBCS character can't accidentally delete
// itself when moving one cell horizontally.
auto next = OutputCell(*textBuffer.GetCellDataAt(sourcePos));
do
{
const auto current = next;
source.WalkInBounds(sourcePos, walkDirection);
next = OutputCell(*textBuffer.GetCellDataAt(sourcePos));
textBuffer.WriteLine(OutputCellIterator({ &current, 1 }), targetPos);
} while (target.WalkInBounds(targetPos, walkDirection));
}
// Columns revealed by the scroll are filled with standard erase attributes.
auto eraseRect = scrollRect;
eraseRect.left = delta > 0 ? scrollRect.left : (scrollRect.right - absoluteDelta);
eraseRect.right = eraseRect.left + absoluteDelta;
const auto eraseAttributes = _GetEraseAttributes(page);
_FillRect(page, eraseRect, whitespace, eraseAttributes);
}
// 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:
// - delta - Number of characters to modify (positive if inserting, negative if deleting).
// Return Value:
// - <none>
void AdaptDispatch::_InsertDeleteCharacterHelper(const VTInt delta)
{
const auto page = _pages.ActivePage();
const auto row = page.Cursor().GetPosition().y;
const auto col = page.Cursor().GetPosition().x;
const auto lineWidth = page.Buffer().GetLineWidth(row);
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = (row >= topMargin && row <= bottomMargin) ?
_GetHorizontalMargins(lineWidth) :
std::make_pair(0, lineWidth - 1);
if (col >= leftMargin && col <= rightMargin)
{
_ScrollRectHorizontally(page, { col, row, rightMargin + 1, row + 1 }, delta);
// The ICH and DCH controls are expected to reset the delayed wrap flag.
page.Cursor().ResetDelayEOLWrap();
}
}
// 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.
bool AdaptDispatch::InsertCharacter(const VTInt count)
{
_InsertDeleteCharacterHelper(count);
return 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.
bool AdaptDispatch::DeleteCharacter(const VTInt count)
{
_InsertDeleteCharacterHelper(-count);
return true;
}
// Routine Description:
// - Fills an area of the buffer with a given character and attributes.
// Arguments:
// - page - Target page to be filled.
// - fillRect - Area of the page that will be affected.
// - fillChar - Character to be written to the buffer.
// - fillAttrs - Attributes to be written to the buffer.
// Return Value:
// - <none>
void AdaptDispatch::_FillRect(const Page& page, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const
{
page.Buffer().FillRect(fillRect, fillChar, fillAttrs);
_api.NotifyAccessibilityChange(fillRect);
}
// 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.
bool AdaptDispatch::EraseCharacters(const VTInt numChars)
{
const auto page = _pages.ActivePage();
const auto row = page.Cursor().GetPosition().y;
const auto startCol = page.Cursor().GetPosition().x;
const auto endCol = std::min<VTInt>(startCol + numChars, page.Buffer().GetLineWidth(row));
// The ECH control is expected to reset the delayed wrap flag.
page.Cursor().ResetDelayEOLWrap();
const auto eraseAttributes = _GetEraseAttributes(page);
_FillRect(page, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes);
return true;
}
// Routine Description:
// - ED - Erases a portion of the current page 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 page
// The scrollback (outside the page 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 page into the scrollback.
if (eraseType == DispatchTypes::EraseType::Scrollback)
{
return _EraseScrollback();
}
else if (eraseType == DispatchTypes::EraseType::All)
{
return _EraseAll();
}
const auto page = _pages.ActivePage();
auto& textBuffer = page.Buffer();
const auto pageWidth = page.Width();
const auto row = page.Cursor().GetPosition().y;
const auto col = page.Cursor().GetPosition().x;
// The ED control is expected to reset the delayed wrap flag.
// The special case variants above ("erase all" and "erase scrollback")
// take care of that themselves when they set the cursor position.
page.Cursor().ResetDelayEOLWrap();
const auto eraseAttributes = _GetEraseAttributes(page);
// 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)
{
textBuffer.ResetLineRenditionRange(page.Top(), row);
_FillRect(page, { 0, page.Top(), pageWidth, row }, whitespace, eraseAttributes);
_FillRect(page, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes);
}
if (eraseType == DispatchTypes::EraseType::ToEnd)
{
textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, page.Bottom());
_FillRect(page, { col, row, pageWidth, row + 1 }, whitespace, eraseAttributes);
_FillRect(page, { 0, row + 1, pageWidth, page.Bottom() }, whitespace, eraseAttributes);
}
return true;
}
// 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)
{
const auto page = _pages.ActivePage();
const auto& textBuffer = page.Buffer();
const auto row = page.Cursor().GetPosition().y;
const auto col = page.Cursor().GetPosition().x;
// The EL control is expected to reset the delayed wrap flag.
page.Cursor().ResetDelayEOLWrap();
const auto eraseAttributes = _GetEraseAttributes(page);
switch (eraseType)
{
case DispatchTypes::EraseType::FromBeginning:
_FillRect(page, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes);
return true;
case DispatchTypes::EraseType::ToEnd:
_FillRect(page, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes);
return true;
case DispatchTypes::EraseType::All:
_FillRect(page, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes);
return true;
default:
return false;
}
}
// Routine Description:
// - Selectively erases unprotected cells in an area of the buffer.
// Arguments:
// - page - Target page to be erased.
// - eraseRect - Area of the page that will be affected.
// Return Value:
// - <none>
void AdaptDispatch::_SelectiveEraseRect(const Page& page, const til::rect& eraseRect)
{
if (eraseRect)
{
for (auto row = eraseRect.top; row < eraseRect.bottom; row++)
{
auto& rowBuffer = page.Buffer().GetMutableRowByOffset(row);
for (auto col = eraseRect.left; col < eraseRect.right; col++)
{
// Only unprotected cells are affected.
if (!rowBuffer.GetAttrByColumn(col).IsProtected())
{
// The text is cleared but the attributes are left as is.
rowBuffer.ClearCell(col);
page.Buffer().TriggerRedraw(Viewport::FromCoord({ col, row }));
}
}
}
_api.NotifyAccessibilityChange(eraseRect);
}
}
// Routine Description:
// - DECSED - Selectively erases unprotected cells in a portion of the page.
// Arguments:
// - eraseType - Determines whether to erase:
// From beginning (top-left corner) to the cursor
// From cursor to end (bottom-right corner)
// The entire page area
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType)
{
const auto page = _pages.ActivePage();
const auto pageWidth = page.Width();
const auto row = page.Cursor().GetPosition().y;
const auto col = page.Cursor().GetPosition().x;
// The DECSED control is expected to reset the delayed wrap flag.
page.Cursor().ResetDelayEOLWrap();
switch (eraseType)
{
case DispatchTypes::EraseType::FromBeginning:
_SelectiveEraseRect(page, { 0, page.Top(), pageWidth, row });
_SelectiveEraseRect(page, { 0, row, col + 1, row + 1 });
return true;
case DispatchTypes::EraseType::ToEnd:
_SelectiveEraseRect(page, { col, row, pageWidth, row + 1 });
_SelectiveEraseRect(page, { 0, row + 1, pageWidth, page.Bottom() });
return true;
case DispatchTypes::EraseType::All:
_SelectiveEraseRect(page, { 0, page.Top(), pageWidth, page.Bottom() });
return true;
default:
return false;
}
}
// Routine Description:
// - DECSEL - Selectively erases unprotected cells on line with the cursor.
// Arguments:
// - eraseType - Determines whether to erase:
// From beginning (left edge) to the cursor
// From cursor to end (right edge)
// The entire line.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseType)
{
const auto page = _pages.ActivePage();
const auto& textBuffer = page.Buffer();
const auto row = page.Cursor().GetPosition().y;
const auto col = page.Cursor().GetPosition().x;
// The DECSEL control is expected to reset the delayed wrap flag.
page.Cursor().ResetDelayEOLWrap();
switch (eraseType)
{
case DispatchTypes::EraseType::FromBeginning:
_SelectiveEraseRect(page, { 0, row, col + 1, row + 1 });
return true;
case DispatchTypes::EraseType::ToEnd:
_SelectiveEraseRect(page, { col, row, textBuffer.GetLineWidth(row), row + 1 });
return true;
case DispatchTypes::EraseType::All:
_SelectiveEraseRect(page, { 0, row, textBuffer.GetLineWidth(row), row + 1 });
return true;
default:
return false;
}
}
// Routine Description:
// - Changes the attributes of each cell in a rectangular area of the buffer.
// Arguments:
// - page - Target page to be changed.
// - changeRect - A rectangular area of the page that will be affected.
// - changeOps - Changes that will be applied to each of the attributes.
// Return Value:
// - <none>
void AdaptDispatch::_ChangeRectAttributes(const Page& page, const til::rect& changeRect, const ChangeOps& changeOps)
{
if (changeRect)
{
for (auto row = changeRect.top; row < changeRect.bottom; row++)
{
auto& rowBuffer = page.Buffer().GetMutableRowByOffset(row);
for (auto col = changeRect.left; col < changeRect.right; col++)
{
auto attr = rowBuffer.GetAttrByColumn(col);
auto characterAttributes = attr.GetCharacterAttributes();
characterAttributes &= changeOps.andAttrMask;
characterAttributes ^= changeOps.xorAttrMask;
attr.SetCharacterAttributes(characterAttributes);
if (changeOps.foreground)
{
attr.SetForeground(*changeOps.foreground);
}
if (changeOps.background)
{
attr.SetBackground(*changeOps.background);
}
if (changeOps.underlineColor)
{
attr.SetUnderlineColor(*changeOps.underlineColor);
}
rowBuffer.ReplaceAttributes(col, col + 1, attr);
}
}
page.Buffer().TriggerRedraw(Viewport::FromExclusive(changeRect));
_api.NotifyAccessibilityChange(changeRect);
}
}
// Routine Description:
// - Changes the attributes of each cell in an area of the buffer.
// Arguments:
// - changeArea - Area of the buffer that will be affected. This may be
// interpreted as a rectangle or a stream depending on the state of the
// RectangularChangeExtent mode.
// - changeOps - Changes that will be applied to each of the attributes.
// Return Value:
// - <none>
void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps)
{
const auto page = _pages.ActivePage();
const auto changeRect = _CalculateRectArea(page, changeArea.top, changeArea.left, changeArea.bottom, changeArea.right);
const auto lineCount = changeRect.height();
// If the change extent is rectangular, we can apply the change with a
// single call. The same is true for a stream extent that is only one line.
if (_modes.test(Mode::RectangularChangeExtent) || lineCount == 1)
{
_ChangeRectAttributes(page, changeRect, changeOps);
}
// If the stream extent is more than one line we require three passes. The
// top line is altered from the left offset up to the end of the line. The
// bottom line is altered from the start up to the right offset. All the
// lines in-between have their entire length altered. The right coordinate
// must be greater than the left, otherwise the operation is ignored.
else if (lineCount > 1 && changeRect.right > changeRect.left)
{
const auto pageWidth = page.Width();
_ChangeRectAttributes(page, { changeRect.origin(), til::size{ pageWidth - changeRect.left, 1 } }, changeOps);
_ChangeRectAttributes(page, { { 0, changeRect.top + 1 }, til::size{ pageWidth, lineCount - 2 } }, changeOps);
_ChangeRectAttributes(page, { { 0, changeRect.bottom - 1 }, til::size{ changeRect.right, 1 } }, changeOps);
}
}
// Routine Description:
// - Helper method to calculate the applicable buffer coordinates for use with
// the various rectangular area operations.
// Arguments:
// - page - The target page.
// - top - The first row of the area.
// - left - The first column of the area.
// - bottom - The last row of the area (inclusive).
// - right - The last column of the area (inclusive).
// Return value:
// - An exclusive rect with the absolute buffer coordinates.
til::rect AdaptDispatch::_CalculateRectArea(const Page& page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right)
{
const auto pageWidth = page.Width();
const auto pageHeight = page.Height();
// We start by calculating the margin offsets and maximum dimensions.
// If the origin mode isn't set, we use the page extent.
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, false);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth);
const auto yOffset = _modes.test(Mode::Origin) ? topMargin : 0;
const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : pageHeight;
const auto xOffset = _modes.test(Mode::Origin) ? leftMargin : 0;
const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : pageWidth;
auto fillRect = til::inclusive_rect{};
fillRect.left = left + xOffset;
fillRect.top = top + yOffset;
// Right and bottom default to the maximum dimensions.
fillRect.right = (right ? right + xOffset : xMaximum);
fillRect.bottom = (bottom ? bottom + yOffset : yMaximum);
// We also clamp everything to the maximum dimensions, and subtract 1
// to convert from VT coordinates which have an origin of 1;1.
fillRect.left = std::min(fillRect.left, xMaximum) - 1;
fillRect.right = std::min(fillRect.right, xMaximum) - 1;
fillRect.top = std::min(fillRect.top, yMaximum) - 1;
fillRect.bottom = std::min(fillRect.bottom, yMaximum) - 1;
// To get absolute coordinates we offset with the page top.
fillRect.top += page.Top();
fillRect.bottom += page.Top();
return til::rect{ fillRect };
}
// Routine Description:
// - DECCARA - Changes the attributes in a rectangular area. The affected range
// is dependent on the change extent setting defined by DECSACE.
// Arguments:
// - top - The first row of the area.
// - left - The first column of the area.
// - bottom - The last row of the area (inclusive).
// - right - The last column of the area (inclusive).
// - attrs - The rendition attributes that will be applied to the area.
// Return Value:
// - True.
bool AdaptDispatch::ChangeAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs)
{
auto changeOps = ChangeOps{};
// We apply the attribute parameters to two TextAttribute instances: one
// with no character attributes set, and one with all attributes set. This
// provides us with an OR mask and an AND mask which can then be applied to
// each cell to set and reset the appropriate attribute bits.
auto allAttrsOff = TextAttribute{};
auto allAttrsOn = TextAttribute{ 0, 0, 0 };
allAttrsOn.SetCharacterAttributes(CharacterAttributes::All);
_ApplyGraphicsOptions(attrs, allAttrsOff);
_ApplyGraphicsOptions(attrs, allAttrsOn);
const auto orAttrMask = allAttrsOff.GetCharacterAttributes();
const auto andAttrMask = allAttrsOn.GetCharacterAttributes();
// But to minimize the required ops, which we share with the DECRARA control
// below, we want to use an XOR rather than OR. For that to work, we have to
// combine the AND mask with the inverse of the OR mask in advance.
changeOps.andAttrMask = andAttrMask & ~orAttrMask;
changeOps.xorAttrMask = orAttrMask;
// We also make use of the two TextAttributes calculated above to determine
// whether colors need to be applied. Since allAttrsOff started off with
// default colors, and allAttrsOn started with black, we know something has
// been set if the former is no longer default, or the latter is now default.
const auto foreground = allAttrsOff.GetForeground();
const auto background = allAttrsOff.GetBackground();
const auto foregroundChanged = !foreground.IsDefault() || allAttrsOn.GetForeground().IsDefault();
const auto backgroundChanged = !background.IsDefault() || allAttrsOn.GetBackground().IsDefault();
changeOps.foreground = foregroundChanged ? std::optional{ foreground } : std::nullopt;
changeOps.background = backgroundChanged ? std::optional{ background } : std::nullopt;
const auto underlineColor = allAttrsOff.GetUnderlineColor();
const auto underlineColorChanged = !underlineColor.IsDefault() || allAttrsOn.GetUnderlineColor().IsDefault();
changeOps.underlineColor = underlineColorChanged ? std::optional{ underlineColor } : std::nullopt;
_ChangeRectOrStreamAttributes({ left, top, right, bottom }, changeOps);
return true;
}
// Routine Description:
// - DECRARA - Reverses the attributes in a rectangular area. The affected range
// is dependent on the change extent setting defined by DECSACE.
// Note: Reversing the underline style has some unexpected consequences.
// See https://github.com/microsoft/terminal/pull/15795#issuecomment-1702559350.
// Arguments:
// - top - The first row of the area.
// - left - The first column of the area.
// - bottom - The last row of the area (inclusive).
// - right - The last column of the area (inclusive).
// - attrs - The rendition attributes that will be applied to the area.
// Return Value:
// - True.
bool AdaptDispatch::ReverseAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs)
{
// In order to create a mask of the attributes that we want to reverse, we
// need to go through the options one by one, applying each of them to an
// empty TextAttribute object from which can extract the effected bits. We
// then combine them with XOR, because if we're reversing the same attribute
// twice, we'd expect the two instances to cancel each other out.
auto reverseMask = CharacterAttributes::Normal;
if (!attrs.empty())
{
for (size_t i = 0; i < attrs.size();)
{
// A zero or default option is a special case that reverses all the
// rendition bits. But note that this shouldn't be triggered by an
// empty attribute list, so we explicitly exclude that case in
// the empty check above.
if (attrs.at(i).value_or(0) == 0)
{
// With param 0, we only reverse the SinglyUnderlined bit.
const auto singlyUnderlinedAttr = static_cast<CharacterAttributes>(WI_EnumValue(UnderlineStyle::SinglyUnderlined) << UNDERLINE_STYLE_SHIFT);
reverseMask ^= (CharacterAttributes::Rendition & ~CharacterAttributes::UnderlineStyle) | singlyUnderlinedAttr;
i++;
}
else
{
auto allAttrsOff = TextAttribute{};
i += _ApplyGraphicsOption(attrs, i, allAttrsOff);
reverseMask ^= allAttrsOff.GetCharacterAttributes();
}
}
}
// If the accumulated mask ends up blank, there's nothing for us to do.
if (reverseMask != CharacterAttributes::Normal)
{
_ChangeRectOrStreamAttributes({ left, top, right, bottom }, { .xorAttrMask = reverseMask });
}
return true;
}
// Routine Description:
// - DECCRA - Copies a rectangular area from one part of the buffer to another.
// Arguments:
// - top - The first row of the source area.
// - left - The first column of the source area.
// - bottom - The last row of the source area (inclusive).
// - right - The last column of the source area (inclusive).
// - page - The source page number.
// - dstTop - The first row of the destination.
// - dstLeft - The first column of the destination.
// - dstPage - The destination page number.
// Return Value:
// - True.
bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt page, const VTInt dstTop, const VTInt dstLeft, const VTInt dstPage)
{
const auto src = _pages.Get(page);
const auto dst = _pages.Get(dstPage);
const auto srcRect = _CalculateRectArea(src, top, left, bottom, right);
const auto dstBottom = dstTop + srcRect.height() - 1;
const auto dstRight = dstLeft + srcRect.width() - 1;
const auto dstRect = _CalculateRectArea(dst, dstTop, dstLeft, dstBottom, dstRight);
if (dstRect && (dstRect.origin() != srcRect.origin() || src.Number() != dst.Number()))
{
// If the source is bigger than the available space at the destination
// it needs to be clipped, so we only care about the destination size.
const auto srcView = Viewport::FromDimensions(srcRect.origin(), dstRect.size());
const auto dstView = Viewport::FromDimensions(dstRect.origin(), dstRect.size());
const auto walkDirection = Viewport::DetermineWalkDirection(srcView, dstView);
auto srcPos = srcView.GetWalkOrigin(walkDirection);
auto dstPos = dstView.GetWalkOrigin(walkDirection);
// Note that we read two cells from the source before we start writing
// to the target, so a two-cell DBCS character can't accidentally delete
// itself when moving one cell horizontally.
auto next = OutputCell(*src.Buffer().GetCellDataAt(srcPos));
do
{
const auto current = next;
const auto currentSrcPos = srcPos;
srcView.WalkInBounds(srcPos, walkDirection);
next = OutputCell(*src.Buffer().GetCellDataAt(srcPos));
// If the source position is offscreen (which can occur on double
// width lines), then we shouldn't copy anything to the destination.
if (currentSrcPos.x < src.Buffer().GetLineWidth(currentSrcPos.y))
{
dst.Buffer().WriteLine(OutputCellIterator({ &current, 1 }), dstPos);
}
} while (dstView.WalkInBounds(dstPos, walkDirection));
_api.NotifyAccessibilityChange(dstRect);
}
return true;
}
// Routine Description:
// - DECFRA - Fills a rectangular area with the given character and using the
// currently active rendition attributes.
// Arguments:
// - ch - The ordinal value of the character used to fill the area.
// - top - The first row of the area.
// - left - The first column of the area.
// - bottom - The last row of the area (inclusive).
// - right - The last column of the area (inclusive).
// Return Value:
// - True.
bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right)
{
const auto page = _pages.ActivePage();
const auto fillRect = _CalculateRectArea(page, top, left, bottom, right);
// The standard only allows for characters in the range of the GL and GR
// character set tables, but we also support additional Unicode characters
// from the BMP if the code page is UTF-8. Default and 0 are treated as 32.
const auto charValue = ch.value_or(0) == 0 ? 32 : ch.value();
const auto glChar = (charValue >= 32 && charValue <= 126);
const auto grChar = (charValue >= 160 && charValue <= 255);
const auto unicodeChar = (charValue >= 256 && charValue <= 65535 && _api.GetConsoleOutputCP() == CP_UTF8);
if (glChar || grChar || unicodeChar)
{
const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast<wchar_t>(charValue));
const auto& fillAttributes = page.Attributes();
_FillRect(page, fillRect, { &fillChar, 1 }, fillAttributes);
}
return true;
}
// Routine Description:
// - DECERA - Erases a rectangular area, replacing all cells with a space
// character and the default rendition attributes.
// Arguments:
// - top - The first row of the area.
// - left - The first column of the area.
// - bottom - The last row of the area (inclusive).
// - right - The last column of the area (inclusive).
// Return Value:
// - True.
bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right)
{
const auto page = _pages.ActivePage();
const auto eraseRect = _CalculateRectArea(page, top, left, bottom, right);
const auto eraseAttributes = _GetEraseAttributes(page);
_FillRect(page, eraseRect, whitespace, eraseAttributes);
return true;
}
// Routine Description:
// - DECSERA - Selectively erases a rectangular area, replacing unprotected
// cells with a space character, but retaining the rendition attributes.
// Arguments:
// - top - The first row of the area.
// - left - The first column of the area.
// - bottom - The last row of the area (inclusive).
// - right - The last column of the area (inclusive).
// Return Value:
// - True.
bool AdaptDispatch::SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right)
{
const auto page = _pages.ActivePage();
const auto eraseRect = _CalculateRectArea(page, top, left, bottom, right);
_SelectiveEraseRect(page, eraseRect);
return true;
}
// Routine Description:
// - DECSACE - Selects the format of the character range that will be affected
// by the DECCARA and DECRARA attribute operations.
// Arguments:
// - changeExtent - Whether the character range is a stream or a rectangle.
// Return value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent changeExtent) noexcept
{
switch (changeExtent)
{
case DispatchTypes::ChangeExtent::Default:
case DispatchTypes::ChangeExtent::Stream:
_modes.reset(Mode::RectangularChangeExtent);
return true;
case DispatchTypes::ChangeExtent::Rectangle:
_modes.set(Mode::RectangularChangeExtent);
return true;
default:
return false;
}
}
// Routine Description:
// - DECRQCRA - Computes and reports a checksum of the specified area of
// the buffer memory.
// Arguments:
// - id - a numeric label used to identify the request.
// - page - The page number.
// - top - The first row of the area.
// - left - The first column of the area.
// - bottom - The last row of the area (inclusive).
// - right - The last column of the area (inclusive).
// Return value:
// - True.
bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right)
{
uint16_t checksum = 0;
// If this feature is not enabled, we'll just report a zero checksum.
if constexpr (Feature_VtChecksumReport::IsEnabled())
{
// If the page number is 0, then we're meant to return a checksum of all
// of the pages, but we have no need for that, so we'll just return 0.
if (page != 0)
{
// As part of the checksum, we need to include the color indices of each
// cell, and in the case of default colors, those indices come from the
// color alias table. But if they're not in the bottom 16 range, we just
// fallback to using white on black (7 and 0).
auto defaultFgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultForeground);
auto defaultBgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground);
defaultFgIndex = defaultFgIndex < 16 ? defaultFgIndex : 7;
defaultBgIndex = defaultBgIndex < 16 ? defaultBgIndex : 0;
const auto target = _pages.Get(page);
const auto eraseRect = _CalculateRectArea(target, top, left, bottom, right);
for (auto row = eraseRect.top; row < eraseRect.bottom; row++)
{
for (auto col = eraseRect.left; col < eraseRect.right; col++)
{
// The algorithm we're using here should match the DEC terminals
// for the ASCII and Latin-1 range. Their other character sets
// predate Unicode, though, so we'd need a custom mapping table
// to lookup the correct checksums. Considering this is only for
// testing at the moment, that doesn't seem worth the effort.
const auto cell = target.Buffer().GetCellDataAt({ col, row });
for (auto ch : cell->Chars())
{
// That said, I've made a special allowance for U+2426,
// since that is widely used in a lot of character sets.
checksum -= (ch == L'\u2426' ? 0x1B : ch);
}
// Since we're attempting to match the DEC checksum algorithm,
// the only attributes affecting the checksum are the ones that
// were supported by DEC terminals.
const auto attr = cell->TextAttr();
checksum -= attr.IsProtected() ? 0x04 : 0;
checksum -= attr.IsInvisible() ? 0x08 : 0;
checksum -= attr.IsUnderlined() ? 0x10 : 0;
checksum -= attr.IsReverseVideo() ? 0x20 : 0;
checksum -= attr.IsBlinking() ? 0x40 : 0;
checksum -= attr.IsIntense() ? 0x80 : 0;
// For the same reason, we only care about the eight basic ANSI
// colors, although technically we also report the 8-16 index
// range. Everything else gets mapped to the default colors.
const auto colorIndex = [](const auto color, const auto defaultIndex) {
return color.IsLegacy() ? color.GetIndex() : defaultIndex;
};
const auto fgIndex = colorIndex(attr.GetForeground(), defaultFgIndex);
const auto bgIndex = colorIndex(attr.GetBackground(), defaultBgIndex);
checksum -= gsl::narrow_cast<uint16_t>(fgIndex << 4);
checksum -= gsl::narrow_cast<uint16_t>(bgIndex);
}
}
}
}
const auto response = wil::str_printf<std::wstring>(L"\033P%d!~%04X\033\\", id, checksum);
_api.ReturnResponse(response);
return true;
}
// 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.
bool AdaptDispatch::SetLineRendition(const LineRendition rendition)
{
// The line rendition can't be changed if left/right margins are allowed.
if (!_modes.test(Mode::AllowDECSLRM))
{
const auto page = _pages.ActivePage();
const auto eraseAttributes = _GetEraseAttributes(page);
page.Buffer().SetCurrentLineRendition(rendition, eraseAttributes);
// There is some variation in how this was handled by the different DEC
// terminals, but the STD 070 reference (on page D-13) makes it clear that
// the delayed wrap (aka the Last Column Flag) was expected to be reset when
// line rendition controls were executed.
page.Cursor().ResetDelayEOLWrap();
}
return true;
}
// Routine Description:
// - DSR - Reports status of a console property back to the STDIN based on the type of status requested.
// Arguments:
// - statusType - status type indicating what property we should report back
// - id - a numeric label used to identify the request in DECCKSR reports
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id)
{
constexpr auto GoodCondition = L"0";
constexpr auto PrinterNotConnected = L"?13";
constexpr auto UserDefinedKeysNotSupported = L"?23";
constexpr auto UnknownPcKeyboard = L"?27;0;0;5";
constexpr auto LocatorNotConnected = L"?53";
constexpr auto UnknownLocatorDevice = L"?57;0";
constexpr auto TerminalReady = L"?70";
constexpr auto MultipleSessionsNotSupported = L"?83";
switch (statusType)
{
case DispatchTypes::StatusType::OperatingStatus:
_DeviceStatusReport(GoodCondition);
return true;
case DispatchTypes::StatusType::CursorPositionReport:
_CursorPositionReport(false);
return true;
case DispatchTypes::StatusType::ExtendedCursorPositionReport:
_CursorPositionReport(true);
return true;
case DispatchTypes::StatusType::PrinterStatus:
_DeviceStatusReport(PrinterNotConnected);
return true;
case DispatchTypes::StatusType::UserDefinedKeys:
_DeviceStatusReport(UserDefinedKeysNotSupported);
return true;
case DispatchTypes::StatusType::KeyboardStatus:
_DeviceStatusReport(UnknownPcKeyboard);
return true;
case DispatchTypes::StatusType::LocatorStatus:
_DeviceStatusReport(LocatorNotConnected);
return true;
case DispatchTypes::StatusType::LocatorIdentity:
_DeviceStatusReport(UnknownLocatorDevice);
return true;
case DispatchTypes::StatusType::MacroSpaceReport:
_MacroSpaceReport();
return true;
case DispatchTypes::StatusType::MemoryChecksum:
_MacroChecksumReport(id);
return true;
case DispatchTypes::StatusType::DataIntegrity:
_DeviceStatusReport(TerminalReady);
return true;
case DispatchTypes::StatusType::MultipleSessionStatus:
_DeviceStatusReport(MultipleSessionsNotSupported);
return true;
default:
return false;
}
}
// Routine Description:
// - DA - Reports the service class or conformance level that the terminal
// supports, and the set of implemented extensions.
// Arguments:
// - <none>
// Return Value:
// - True.
bool AdaptDispatch::DeviceAttributes()
{
// This first parameter of the response is 61, representing a conformance
// level of 1. The subsequent parameters identify the supported feature
// extensions.
//
// 1 = 132 column mode (ConHost only)
// 6 = Selective erase
// 7 = Soft fonts
// 14 = 8-bit interface architecture
// 21 = Horizontal scrolling
// 22 = Color text
// 23 = Greek character sets
// 24 = Turkish character sets
// 28 = Rectangular area operations
// 32 = Text macros
// 42 = ISO Latin-2 character set
if (_api.IsConsolePty())
{
_api.ReturnResponse(L"\x1b[?61;6;7;14;21;22;23;24;28;32;42c");
}
else
{
_api.ReturnResponse(L"\x1b[?61;1;6;7;14;21;22;23;24;28;32;42c");
}
return true;
}
// 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 hard-coded as 10 (1.0), and the hardware option
// is set to 1 (indicating a PC Keyboard).
// Arguments:
// - <none>
// Return Value:
// - True.
bool AdaptDispatch::SecondaryDeviceAttributes()
{
_api.ReturnResponse(L"\x1b[>0;10;1c");
return true;
}
// Routine Description:
// - DA3 - Reports the terminal unit identification code. Terminal emulators
// typically return a hard-coded value, the most common being all zeros.
// Arguments:
// - <none>
// Return Value:
// - True.
bool AdaptDispatch::TertiaryDeviceAttributes()
{
_api.ReturnResponse(L"\x1bP!|00000000\x1b\\");
return true;
}
// 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.
bool AdaptDispatch::Vt52DeviceAttributes()
{
_api.ReturnResponse(L"\x1b/Z");
return true;
}
// 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 hard-coded.
// 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 hard-coded 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:
_api.ReturnResponse(L"\x1b[2;1;1;128;128;1;0x");
return true;
case DispatchTypes::ReportingPermission::Solicited:
_api.ReturnResponse(L"\x1b[3;1;1;128;128;1;0x");
return true;
default:
return false;
}
}
// Routine Description:
// - DSR - Transmits a device status report with a given parameter string.
// Arguments:
// - parameters - One or more parameter values representing the status
// Return Value:
// - <none>
void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const
{
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{}n"), parameters));
}
// Routine Description:
// - CPR and DECXCPR- Reports the current cursor position within the page,
// as well as the current page number if this is an extended report.
// Arguments:
// - extendedReport - Set to true if the report should include the page number
// Return Value:
// - <none>
void AdaptDispatch::_CursorPositionReport(const bool extendedReport)
{
const auto page = _pages.ActivePage();
// First pull the cursor position relative to the entire buffer out of the console.
til::point cursorPosition{ page.Cursor().GetPosition() };
// Now adjust it for its position in respect to the current page top.
cursorPosition.y -= page.Top();
// NOTE: 1,1 is the top-left corner of the page in VT-speak, so add 1.
cursorPosition.x++;
cursorPosition.y++;
// If the origin mode is set, the cursor is relative to the margin origin.
if (_modes.test(Mode::Origin))
{
cursorPosition.x -= _GetHorizontalMargins(page.Width()).first;
cursorPosition.y -= _GetVerticalMargins(page, false).first;
}
// Now send it back into the input channel of the console.
if (extendedReport)
{
// An extended report also includes the page number.
const auto pageNumber = page.Number();
const auto response = wil::str_printf<std::wstring>(L"\x1b[?%d;%d;%dR", cursorPosition.y, cursorPosition.x, pageNumber);
_api.ReturnResponse(response);
}
else
{
// The standard report only returns the cursor position.
const auto response = wil::str_printf<std::wstring>(L"\x1b[%d;%dR", cursorPosition.y, cursorPosition.x);
_api.ReturnResponse(response);
}
}
// Routine Description:
// - DECMSR - Reports the amount of space available for macro definitions.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AdaptDispatch::_MacroSpaceReport() const
{
const auto spaceInBytes = _macroBuffer ? _macroBuffer->GetSpaceAvailable() : MacroBuffer::MAX_SPACE;
// The available space is measured in blocks of 16 bytes, so we need to divide by 16.
const auto response = wil::str_printf<std::wstring>(L"\x1b[%zu*{", spaceInBytes / 16);
_api.ReturnResponse(response);
}
// Routine Description:
// - DECCKSR - Reports a checksum of the current macro definitions.
// Arguments:
// - id - a numeric label used to identify the DSR request
// Return Value:
// - <none>
void AdaptDispatch::_MacroChecksumReport(const VTParameter id) const
{
const auto requestId = id.value_or(0);
const auto checksum = _macroBuffer ? _macroBuffer->CalculateChecksum() : 0;
const auto response = wil::str_printf<std::wstring>(L"\033P%d!~%04X\033\\", requestId, checksum);
_api.ReturnResponse(response);
}
// Routine Description:
// - Generalizes scrolling movement for up/down
// Arguments:
// - delta - Distance to move (positive is down, negative is up)
// Return Value:
// - <none>
void AdaptDispatch::_ScrollMovement(const VTInt delta)
{
const auto page = _pages.ActivePage();
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width());
_ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta);
}
// 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.
bool AdaptDispatch::ScrollUp(const VTInt uiDistance)
{
_ScrollMovement(-uiDistance);
return true;
}
// 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.
bool AdaptDispatch::ScrollDown(const VTInt uiDistance)
{
_ScrollMovement(uiDistance);
return true;
}
// Routine Description:
// - NP - Moves the active position one or more pages ahead, and moves the
// cursor to home.
// Arguments:
// - pageCount - Number of pages to move
// Return Value:
// - True.
bool AdaptDispatch::NextPage(const VTInt pageCount)
{
PagePositionRelative(pageCount);
return CursorPosition(1, 1);
}
// Routine Description:
// - PP - Moves the active position one or more pages back, and moves the
// cursor to home.
// Arguments:
// - pageCount - Number of pages to move
// Return Value:
// - True.
bool AdaptDispatch::PrecedingPage(const VTInt pageCount)
{
PagePositionBack(pageCount);
return CursorPosition(1, 1);
}
// Routine Description:
// - PPA - Moves the active position to the specified page number, without
// altering the cursor coordinates.
// Arguments:
// - page - Destination page
// Return Value:
// - True.
bool AdaptDispatch::PagePositionAbsolute(const VTInt page)
{
_pages.MoveTo(page, _modes.test(Mode::PageCursorCoupling));
return true;
}
// Routine Description:
// - PPR - Moves the active position one or more pages ahead, without altering
// the cursor coordinates.
// Arguments:
// - pageCount - Number of pages to move
// Return Value:
// - True.
bool AdaptDispatch::PagePositionRelative(const VTInt pageCount)
{
_pages.MoveRelative(pageCount, _modes.test(Mode::PageCursorCoupling));
return true;
}
// Routine Description:
// - PPB - Moves the active position one or more pages back, without altering
// the cursor coordinates.
// Arguments:
// - pageCount - Number of pages to move
// Return Value:
// - True.
bool AdaptDispatch::PagePositionBack(const VTInt pageCount)
{
_pages.MoveRelative(-pageCount, _modes.test(Mode::PageCursorCoupling));
return true;
}
// Routine Description:
// - DECRQDE - Requests the area of page memory that is currently visible.
// Arguments:
// - None
// Return Value:
// - True.
bool AdaptDispatch::RequestDisplayedExtent()
{
const auto page = _pages.VisiblePage();
const auto width = page.Viewport().width();
const auto height = page.Viewport().height();
const auto left = page.XPanOffset() + 1;
const auto top = page.YPanOffset() + 1;
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{};{};{};{};{}\"w"), height, width, left, top, page.Number()));
return true;
}
// 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:
// - enable - the number of columns is set to 132 if true, 80 if false.
// Return Value:
// - <none>
void AdaptDispatch::_SetColumnMode(const bool enable)
{
// Only proceed if DECCOLM is allowed. Return true, as this is technically a successful handling.
if (_modes.test(Mode::AllowDECCOLM) && !_api.IsConsolePty())
{
const auto page = _pages.VisiblePage();
const auto pageHeight = page.Height();
const auto pageWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns);
_api.ResizeWindow(pageWidth, pageHeight);
_modes.set(Mode::Column, enable);
_modes.reset(Mode::Origin, Mode::AllowDECSLRM);
CursorPosition(1, 1);
EraseInDisplay(DispatchTypes::EraseType::All);
_DoSetTopBottomScrollingMargins(0, 0);
_DoSetLeftRightScrollingMargins(0, 0);
}
}
// Routine Description:
// - Set the alternate screen buffer mode. In virtual terminals, there exists
// both a "main" screen buffer and an alternate. This mode is used to switch
// between the two.
// Arguments:
// - enable - true selects the alternate buffer, false returns to the main buffer.
// Return Value:
// - <none>
void AdaptDispatch::_SetAlternateScreenBufferMode(const bool enable)
{
if (enable)
{
CursorSaveState();
const auto page = _pages.ActivePage();
_api.UseAlternateScreenBuffer(_GetEraseAttributes(page));
_usingAltBuffer = true;
}
else
{
_api.UseMainScreenBuffer();
_usingAltBuffer = false;
CursorRestoreState();
}
}
// Routine Description:
// - Determines whether we need to pass through input mode requests.
// If we're a conpty, AND WE'RE IN VT INPUT MODE, always pass input mode requests
// The VT Input mode check is to work around ssh.exe v7.7, which uses VT
// output, but not Input.
// The original comment said, "Once the conpty supports these types of input,
// this check can be removed. See GH#4911". Unfortunately, time has shown
// us that SSH 7.7 _also_ requests mouse input and that can have a user interface
// impact on the actual connected terminal. We can't remove this check,
// because SSH <=7.7 is out in the wild on all versions of Windows <=2004.
// Return Value:
// - True if we should pass through. False otherwise.
bool AdaptDispatch::_PassThroughInputModes()
{
return _api.IsConsolePty() && _api.IsVtInputEnabled();
}
// Routine Description:
// - Support routine for routing 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)
{
switch (param)
{
case DispatchTypes::ModeParams::IRM_InsertReplaceMode:
_modes.set(Mode::InsertReplace, enable);
return true;
case DispatchTypes::ModeParams::LNM_LineFeedNewLineMode:
// VT apps expect that the system and input modes are the same, so if
// they become out of sync, we just act as if LNM mode isn't supported.
if (_api.GetSystemMode(ITerminalApi::Mode::LineFeed) == _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed))
{
_api.SetSystemMode(ITerminalApi::Mode::LineFeed, enable);
_terminalInput.SetInputMode(TerminalInput::Mode::LineFeed, enable);
}
return true;
case DispatchTypes::ModeParams::DECCKM_CursorKeysMode:
_terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::DECANM_AnsiMode:
return SetAnsiMode(enable);
case DispatchTypes::ModeParams::DECCOLM_SetNumberOfColumns:
_SetColumnMode(enable);
return true;
case DispatchTypes::ModeParams::DECSCNM_ScreenMode:
_renderSettings.SetRenderMode(RenderSettings::Mode::ScreenReversed, enable);
// No need to force a redraw in pty mode.
if (_api.IsConsolePty())
{
return false;
}
_renderer.TriggerRedrawAll();
return true;
case DispatchTypes::ModeParams::DECOM_OriginMode:
_modes.set(Mode::Origin, enable);
// The cursor is also moved to the new home position when the origin mode is set or reset.
CursorPosition(1, 1);
return true;
case DispatchTypes::ModeParams::DECAWM_AutoWrapMode:
_api.SetSystemMode(ITerminalApi::Mode::AutoWrap, enable);
// Resetting DECAWM should also reset the delayed wrap flag.
if (!enable)
{
_pages.ActivePage().Cursor().ResetDelayEOLWrap();
}
return true;
case DispatchTypes::ModeParams::DECARM_AutoRepeatMode:
_terminalInput.SetInputMode(TerminalInput::Mode::AutoRepeat, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::ATT610_StartCursorBlink:
_pages.ActivePage().Cursor().SetBlinkingAllowed(enable);
return !_api.IsConsolePty();
case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode:
_pages.ActivePage().Cursor().SetIsVisible(enable);
return true;
case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport:
_modes.set(Mode::AllowDECCOLM, enable);
return true;
case DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode:
_modes.set(Mode::PageCursorCoupling, enable);
if (enable)
{
_pages.MakeActivePageVisible();
}
return true;
case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode:
_terminalInput.SetInputMode(TerminalInput::Mode::Keypad, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::DECBKM_BackarrowKeyMode:
_terminalInput.SetInputMode(TerminalInput::Mode::BackarrowKey, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode:
_modes.set(Mode::AllowDECSLRM, enable);
_DoSetLeftRightScrollingMargins(0, 0);
if (enable)
{
// If we've allowed left/right margins, we can't have line renditions.
const auto page = _pages.ActivePage();
page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom());
}
return true;
case DispatchTypes::ModeParams::DECECM_EraseColorMode:
_modes.set(Mode::EraseColor, enable);
return true;
case DispatchTypes::ModeParams::VT200_MOUSE_MODE:
_terminalInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::BUTTON_EVENT_MOUSE_MODE:
_terminalInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::ANY_EVENT_MOUSE_MODE:
_terminalInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::UTF8_EXTENDED_MODE:
_terminalInput.SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::SGR_EXTENDED_MODE:
_terminalInput.SetInputMode(TerminalInput::Mode::SgrMouseEncoding, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::FOCUS_EVENT_MODE:
_terminalInput.SetInputMode(TerminalInput::Mode::FocusEvent, enable);
// GH#12799 - If the app requested that we disable focus events, DON'T pass
// that through. ConPTY would _always_ like to know about focus events.
return !_PassThroughInputModes() || !enable;
case DispatchTypes::ModeParams::ALTERNATE_SCROLL:
_terminalInput.SetInputMode(TerminalInput::Mode::AlternateScroll, enable);
return !_PassThroughInputModes();
case DispatchTypes::ModeParams::ASB_AlternateScreenBuffer:
_SetAlternateScreenBufferMode(enable);
return true;
case DispatchTypes::ModeParams::XTERM_BracketedPasteMode:
_api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, enable);
return !_api.IsConsolePty();
case DispatchTypes::ModeParams::W32IM_Win32InputMode:
_terminalInput.SetInputMode(TerminalInput::Mode::Win32, enable);
// ConPTY requests the Win32InputMode on startup and disables it on shutdown. When nesting ConPTY inside
// ConPTY then this should not bubble up. Otherwise, when the inner ConPTY exits and the outer ConPTY
// passes the disable sequence up to the hosting terminal, we'd stop getting Win32InputMode entirely!
// It also makes more sense to not bubble it up, because this mode is specifically for INPUT_RECORD interop
// and thus entirely between a PTY's input records and its INPUT_RECORD-aware VT-aware console clients.
// Returning true here will mark this as being handled and avoid this.
return true;
default:
// If no functions to call, overall dispatch was a failure.
return false;
}
}
// Routine Description:
// - SM/DECSET - Enables the given mode parameter (both ANSI and private).
// 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:
// - RM/DECRST - Disables the given mode parameter (both ANSI and private).
// 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);
}
// Routine Description:
// - DECRQM - Requests the current state of a given mode number. The result
// is reported back with a DECRPM escape sequence.
// Arguments:
// - param - the mode number being queried
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
{
auto enabled = std::optional<bool>{};
switch (param)
{
case DispatchTypes::ModeParams::IRM_InsertReplaceMode:
enabled = _modes.test(Mode::InsertReplace);
break;
case DispatchTypes::ModeParams::LNM_LineFeedNewLineMode:
// VT apps expect that the system and input modes are the same, so if
// they become out of sync, we just act as if LNM mode isn't supported.
if (_api.GetSystemMode(ITerminalApi::Mode::LineFeed) == _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed))
{
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed);
}
break;
case DispatchTypes::ModeParams::DECCKM_CursorKeysMode:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::CursorKey);
break;
case DispatchTypes::ModeParams::DECANM_AnsiMode:
enabled = _api.GetStateMachine().GetParserMode(StateMachine::Mode::Ansi);
break;
case DispatchTypes::ModeParams::DECCOLM_SetNumberOfColumns:
// DECCOLM is not supported in conpty mode
if (!_api.IsConsolePty())
{
enabled = _modes.test(Mode::Column);
}
break;
case DispatchTypes::ModeParams::DECSCNM_ScreenMode:
enabled = _renderSettings.GetRenderMode(RenderSettings::Mode::ScreenReversed);
break;
case DispatchTypes::ModeParams::DECOM_OriginMode:
enabled = _modes.test(Mode::Origin);
break;
case DispatchTypes::ModeParams::DECAWM_AutoWrapMode:
enabled = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap);
break;
case DispatchTypes::ModeParams::DECARM_AutoRepeatMode:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AutoRepeat);
break;
case DispatchTypes::ModeParams::ATT610_StartCursorBlink:
enabled = _pages.ActivePage().Cursor().IsBlinkingAllowed();
break;
case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode:
enabled = _pages.ActivePage().Cursor().IsVisible();
break;
case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport:
// DECCOLM is not supported in conpty mode
if (!_api.IsConsolePty())
{
enabled = _modes.test(Mode::AllowDECCOLM);
}
break;
case DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode:
enabled = _modes.test(Mode::PageCursorCoupling);
break;
case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Keypad);
break;
case DispatchTypes::ModeParams::DECBKM_BackarrowKeyMode:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::BackarrowKey);
break;
case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode:
enabled = _modes.test(Mode::AllowDECSLRM);
break;
case DispatchTypes::ModeParams::DECECM_EraseColorMode:
enabled = _modes.test(Mode::EraseColor);
break;
case DispatchTypes::ModeParams::VT200_MOUSE_MODE:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::DefaultMouseTracking);
break;
case DispatchTypes::ModeParams::BUTTON_EVENT_MOUSE_MODE:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::ButtonEventMouseTracking);
break;
case DispatchTypes::ModeParams::ANY_EVENT_MOUSE_MODE:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AnyEventMouseTracking);
break;
case DispatchTypes::ModeParams::UTF8_EXTENDED_MODE:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Utf8MouseEncoding);
break;
case DispatchTypes::ModeParams::SGR_EXTENDED_MODE:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::SgrMouseEncoding);
break;
case DispatchTypes::ModeParams::FOCUS_EVENT_MODE:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::FocusEvent);
break;
case DispatchTypes::ModeParams::ALTERNATE_SCROLL:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AlternateScroll);
break;
case DispatchTypes::ModeParams::ASB_AlternateScreenBuffer:
enabled = _usingAltBuffer;
break;
case DispatchTypes::ModeParams::XTERM_BracketedPasteMode:
enabled = _api.GetSystemMode(ITerminalApi::Mode::BracketedPaste);
break;
case DispatchTypes::ModeParams::W32IM_Win32InputMode:
enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Win32);
break;
default:
enabled = std::nullopt;
break;
}
// 1 indicates the mode is enabled, 2 it's disabled, and 0 it's unsupported
const auto state = enabled.has_value() ? (enabled.value() ? 1 : 2) : 0;
const auto isPrivate = param >= DispatchTypes::DECPrivateMode(0);
const auto prefix = isPrivate ? L"?" : L"";
const auto mode = isPrivate ? param - DispatchTypes::DECPrivateMode(0) : param;
const auto response = wil::str_printf<std::wstring>(L"\x1b[%s%d;%d$y", prefix, mode, state);
_api.ReturnResponse(response);
return true;
}
// - 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)
{
_terminalInput.SetInputMode(TerminalInput::Mode::Keypad, fApplicationMode);
return !_PassThroughInputModes();
}
// Routine Description:
// - Internal logic for adding or removing lines in the active screen buffer.
// This also moves the cursor to the left margin, which is expected behavior for IL and DL.
// Parameters:
// - delta - Number of lines to modify (positive if inserting, negative if deleting).
// Return Value:
// - <none>
void AdaptDispatch::_InsertDeleteLineHelper(const VTInt delta)
{
const auto page = _pages.ActivePage();
auto& cursor = page.Cursor();
const auto col = cursor.GetPosition().x;
const auto row = cursor.GetPosition().y;
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width());
if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin)
{
// We emulate inserting and deleting by scrolling the area between the cursor and the bottom margin.
_ScrollRectVertically(page, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta);
// The IL and DL controls are also expected to move the cursor to the left margin.
cursor.SetXPosition(leftMargin);
_ApplyCursorMovementFlags(cursor);
}
}
// 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.
bool AdaptDispatch::InsertLine(const VTInt distance)
{
_InsertDeleteLineHelper(distance);
return true;
}
// 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.
bool AdaptDispatch::DeleteLine(const VTInt distance)
{
_InsertDeleteLineHelper(-distance);
return true;
}
// Routine Description:
// - Internal logic for adding or removing columns in the active screen buffer.
// Parameters:
// - delta - Number of columns to modify (positive if inserting, negative if deleting).
// Return Value:
// - <none>
void AdaptDispatch::_InsertDeleteColumnHelper(const VTInt delta)
{
const auto page = _pages.ActivePage();
const auto& cursor = page.Cursor();
const auto col = cursor.GetPosition().x;
const auto row = cursor.GetPosition().y;
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width());
if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin)
{
// We emulate inserting and deleting by scrolling the area between the cursor and the right margin.
_ScrollRectHorizontally(page, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta);
}
}
// Routine Description:
// - DECIC - This control function inserts one or more blank columns in the
// scrolling region, starting at the column that has the cursor.
// Arguments:
// - distance - number of columns to insert
// Return Value:
// - True.
bool AdaptDispatch::InsertColumn(const VTInt distance)
{
_InsertDeleteColumnHelper(distance);
return true;
}
// Routine Description:
// - DECDC - This control function deletes one or more columns in the scrolling
// region, starting with the column that has the cursor.
// Arguments:
// - distance - number of columns to delete
// Return Value:
// - True.
bool AdaptDispatch::DeleteColumn(const VTInt distance)
{
_InsertDeleteColumnHelper(-distance);
return true;
}
// - 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.
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.SoftReset();
_api.GetStateMachine().SetParserMode(StateMachine::Mode::Ansi, ansiMode);
_terminalInput.SetInputMode(TerminalInput::Mode::Ansi, ansiMode);
// While input mode changes are often forwarded over conpty, we never want
// to do that for the DECANM mode.
return true;
}
// 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.
// - homeCursor - move the cursor to the home position.
// Return Value:
// - <none>
void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin,
const VTInt bottomMargin,
const bool homeCursor)
{
// 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
til::CoordType actualTop = topMargin;
til::CoordType actualBottom = bottomMargin;
const auto page = _pages.ActivePage();
const auto pageHeight = page.Height();
// The default top margin is line 1
if (actualTop == 0)
{
actualTop = 1;
}
// The default bottom margin is the page height
if (actualBottom == 0)
{
actualBottom = pageHeight;
}
// The top margin must be less than the bottom margin, and the
// bottom margin must be less than or equal to the page height
if (actualTop < actualBottom && actualBottom <= pageHeight)
{
if (actualTop == 1 && actualBottom == pageHeight)
{
// 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;
// If requested, we may also need to move the cursor to the home
// position, but only if the requested margins were valid.
if (homeCursor)
{
CursorPosition(1, 1);
}
}
}
// 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.
bool AdaptDispatch::SetTopBottomScrollingMargins(const VTInt topMargin,
const VTInt bottomMargin)
{
_DoSetTopBottomScrollingMargins(topMargin, bottomMargin, true);
return true;
}
// Routine Description:
// - DECSLRM - Set Scrolling Region
// This control function sets the left and right margins for the current page.
// You cannot perform scrolling outside the margins.
// Default: Margins are at the page limits.
// Arguments:
// - leftMargin - the column number for the left margin.
// - rightMargin - the column number for the right margin.
// - homeCursor - move the cursor to the home position.
// Return Value:
// - <none>
void AdaptDispatch::_DoSetLeftRightScrollingMargins(const VTInt leftMargin,
const VTInt rightMargin,
const bool homeCursor)
{
til::CoordType actualLeft = leftMargin;
til::CoordType actualRight = rightMargin;
const auto page = _pages.ActivePage();
const auto pageWidth = page.Width();
// The default left margin is column 1
if (actualLeft == 0)
{
actualLeft = 1;
}
// The default right margin is the page width
if (actualRight == 0)
{
actualRight = pageWidth;
}
// The left margin must be less than the right margin, and the
// right margin must be less than or equal to the buffer width
if (actualLeft < actualRight && actualRight <= pageWidth)
{
if (actualLeft == 1 && actualRight == pageWidth)
{
// Client requests setting margins to the entire screen
// - clear them instead of setting them.
actualLeft = 0;
actualRight = 0;
}
else
{
// In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1.
actualLeft -= 1;
actualRight -= 1;
}
_scrollMargins.left = actualLeft;
_scrollMargins.right = actualRight;
// If requested, we may also need to move the cursor to the home
// position, but only if the requested margins were valid.
if (homeCursor)
{
CursorPosition(1, 1);
}
}
}
// Routine Description:
// - DECSLRM - Set Scrolling Region
// This control function sets the left and right margins for the current page.
// You cannot perform scrolling outside the margins.
// Default: Margins are at the page limits.
// Arguments:
// - leftMargin - the column number for the left margin.
// - rightMargin - the column number for the right margin.
// Return Value:
// - True.
bool AdaptDispatch::SetLeftRightScrollingMargins(const VTInt leftMargin,
const VTInt rightMargin)
{
if (_modes.test(Mode::AllowDECSLRM))
{
_DoSetLeftRightScrollingMargins(leftMargin, rightMargin, true);
}
else
{
// When DECSLRM isn't allowed, `CSI s` is interpreted as ANSISYSSC.
CursorSaveState();
}
return true;
}
// Routine Description:
// - BEL - Rings the warning bell.
// Causes the terminal to emit an audible tone of brief duration.
// Arguments:
// - None
// Return Value:
// - True.
bool AdaptDispatch::WarningBell()
{
_api.WarningBell();
return true;
}
// Routine Description:
// - CR - Performs a carriage return.
// Moves the cursor to the leftmost column.
// Arguments:
// - None
// Return Value:
// - True.
bool AdaptDispatch::CarriageReturn()
{
return _CursorMovePosition(Offset::Unchanged(), Offset::Absolute(1), true);
}
// Routine Description:
// - Helper method for executing a line feed, possibly preceded by carriage return.
// Arguments:
// - page - Target page on which the line feed is executed.
// - withReturn - Set to true if a carriage return should be performed as well.
// - wrapForced - Set to true is the line feed was the result of the line wrapping.
// Return Value:
// - True if the viewport panned down. False if not.
bool AdaptDispatch::_DoLineFeed(const Page& page, const bool withReturn, const bool wrapForced)
{
auto& textBuffer = page.Buffer();
const auto pageWidth = page.Width();
const auto bufferHeight = page.BufferHeight();
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth);
auto viewportMoved = false;
auto& cursor = page.Cursor();
const auto currentPosition = cursor.GetPosition();
auto newPosition = currentPosition;
// If the line was forced to wrap, set the wrap status.
// When explicitly moving down a row, clear the wrap status.
textBuffer.GetMutableRowByOffset(currentPosition.y).SetWrapForced(wrapForced);
// If a carriage return was requested, we move to the leftmost column or
// the left margin, depending on whether we started within the margins.
if (withReturn)
{
const auto clampToMargin = currentPosition.y >= topMargin &&
currentPosition.y <= bottomMargin &&
currentPosition.x >= leftMargin;
newPosition.x = clampToMargin ? leftMargin : 0;
}
if (currentPosition.y != bottomMargin || newPosition.x < leftMargin || newPosition.x > rightMargin)
{
// If we're not at the bottom margin, or outside the horizontal margins,
// then there's no scrolling, so we make sure we don't move past the
// bottom of the page.
newPosition.y = std::min(currentPosition.y + 1, page.Bottom() - 1);
newPosition = textBuffer.ClampPositionWithinLine(newPosition);
}
else if (topMargin > page.Top() || leftMargin > 0 || rightMargin < pageWidth - 1)
{
// If the top margin isn't at the top of the page, or the
// horizontal margins are set, then we're just scrolling the margin
// area and the cursor stays where it is.
_ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1);
}
else if (page.Bottom() < bufferHeight)
{
// If the top margin is at the top of the page, then we'll scroll
// the content up by panning the viewport down, and also move the cursor
// down a row. But we only do this if the viewport hasn't yet reached
// the end of the buffer.
_api.SetViewportPosition({ page.XPanOffset(), page.Top() + 1 });
newPosition.y++;
viewportMoved = true;
// And if the bottom margin didn't cover the full page, we copy the
// lower part of the page down so it remains static. But for a full
// pan we reset the newly revealed row with the erase attributes.
if (bottomMargin < page.Bottom() - 1)
{
_ScrollRectVertically(page, { 0, bottomMargin + 1, pageWidth, page.Bottom() + 1 }, 1);
}
else
{
const auto eraseAttributes = _GetEraseAttributes(page);
textBuffer.GetMutableRowByOffset(newPosition.y).Reset(eraseAttributes);
}
}
else
{
// If the viewport has reached the end of the buffer, we can't pan down,
// so we cycle the row coordinates, which effectively scrolls the buffer
// content up. In this case we don't need to move the cursor down.
const auto eraseAttributes = _GetEraseAttributes(page);
textBuffer.IncrementCircularBuffer(eraseAttributes);
_api.NotifyBufferRotation(1);
// We trigger a scroll rather than a redraw, since that's more efficient,
// but we need to turn the cursor off before doing so, otherwise a ghost
// cursor can be left behind in the previous position.
cursor.SetIsOn(false);
textBuffer.TriggerScroll({ 0, -1 });
// And again, if the bottom margin didn't cover the full page, we
// copy the lower part of the page down so it remains static.
if (bottomMargin < page.Bottom() - 1)
{
_ScrollRectVertically(page, { 0, bottomMargin, pageWidth, bufferHeight }, 1);
}
}
cursor.SetPosition(newPosition);
_ApplyCursorMovementFlags(cursor);
return viewportMoved;
}
// 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)
{
const auto page = _pages.ActivePage();
switch (lineFeedType)
{
case DispatchTypes::LineFeedType::DependsOnMode:
_DoLineFeed(page, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false);
return true;
case DispatchTypes::LineFeedType::WithoutReturn:
_DoLineFeed(page, false, false);
return true;
case DispatchTypes::LineFeedType::WithReturn:
_DoLineFeed(page, true, false);
return 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.
bool AdaptDispatch::ReverseLineFeed()
{
const auto page = _pages.ActivePage();
const auto& textBuffer = page.Buffer();
auto& cursor = page.Cursor();
const auto cursorPosition = cursor.GetPosition();
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width());
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
// If the cursor is at the top of the margin area, we shift the buffer
// contents down, to emulate inserting a line at that point.
if (cursorPosition.y == topMargin && cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin)
{
_ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1);
}
else if (cursorPosition.y > page.Top())
{
// Otherwise we move the cursor up, but not past the top of the page.
cursor.SetPosition(textBuffer.ClampPositionWithinLine({ cursorPosition.x, cursorPosition.y - 1 }));
_ApplyCursorMovementFlags(cursor);
}
return true;
}
// Routine Description:
// - DECBI - Moves the cursor back one column, scrolling the screen
// horizontally if it reaches the left margin.
// Arguments:
// - None
// Return Value:
// - True.
bool AdaptDispatch::BackIndex()
{
const auto page = _pages.ActivePage();
auto& cursor = page.Cursor();
const auto cursorPosition = cursor.GetPosition();
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width());
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
// If the cursor is at the left of the margin area, we shift the buffer right.
if (cursorPosition.x == leftMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin)
{
_ScrollRectHorizontally(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1);
}
// Otherwise we move the cursor left, but not past the start of the line.
else if (cursorPosition.x > 0)
{
cursor.SetXPosition(cursorPosition.x - 1);
_ApplyCursorMovementFlags(cursor);
}
return true;
}
// Routine Description:
// - DECFI - Moves the cursor forward one column, scrolling the screen
// horizontally if it reaches the right margin.
// Arguments:
// - None
// Return Value:
// - True.
bool AdaptDispatch::ForwardIndex()
{
const auto page = _pages.ActivePage();
auto& cursor = page.Cursor();
const auto cursorPosition = cursor.GetPosition();
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width());
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
// If the cursor is at the right of the margin area, we shift the buffer left.
if (cursorPosition.x == rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin)
{
_ScrollRectHorizontally(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1);
}
// Otherwise we move the cursor right, but not past the end of the line.
else if (cursorPosition.x < page.Buffer().GetLineWidth(cursorPosition.y) - 1)
{
cursor.SetXPosition(cursorPosition.x + 1);
_ApplyCursorMovementFlags(cursor);
}
return true;
}
// Routine Description:
// - OSC Set Window Title - Sets the title of the window
// Arguments:
// - title - The string to set the title to.
// Return Value:
// - True.
bool AdaptDispatch::SetWindowTitle(std::wstring_view title)
{
_api.SetWindowTitle(title);
return true;
}
//Routine Description:
// HTS - sets a VT tab stop in the cursor's current column.
//Arguments:
// - None
// Return value:
// - True.
bool AdaptDispatch::HorizontalTabSet()
{
const auto page = _pages.ActivePage();
const auto column = page.Cursor().GetPosition().x;
_InitTabStopsForWidth(page.Width());
_tabStopColumns.at(column) = true;
return true;
}
//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.
bool AdaptDispatch::ForwardTab(const VTInt numTabs)
{
const auto page = _pages.ActivePage();
auto& cursor = page.Cursor();
auto column = cursor.GetPosition().x;
const auto row = cursor.GetPosition().y;
const auto width = page.Buffer().GetLineWidth(row);
auto tabsPerformed = 0;
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width);
const auto clampToMargin = row >= topMargin && row <= bottomMargin && column <= rightMargin;
const auto maxColumn = clampToMargin ? rightMargin : width - 1;
_InitTabStopsForWidth(width);
while (column < maxColumn && tabsPerformed < numTabs)
{
column++;
if (til::at(_tabStopColumns, column))
{
tabsPerformed++;
}
}
// While the STD 070 reference suggests that horizontal tabs should reset
// the delayed wrap, almost none of the DEC terminals actually worked that
// way, and most modern terminal emulators appear to have taken the same
// approach (i.e. they don't reset). For us this is a bit messy, since all
// cursor movement resets the flag automatically, so we need to save the
// original state here, and potentially reapply it after the move.
const auto delayedWrapOriginallySet = cursor.IsDelayedEOLWrap();
cursor.SetXPosition(column);
_ApplyCursorMovementFlags(cursor);
if (delayedWrapOriginallySet)
{
cursor.DelayEOLWrap();
}
return true;
}
//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.
bool AdaptDispatch::BackwardsTab(const VTInt numTabs)
{
const auto page = _pages.ActivePage();
auto& cursor = page.Cursor();
auto column = cursor.GetPosition().x;
const auto row = cursor.GetPosition().y;
const auto width = page.Buffer().GetLineWidth(row);
auto tabsPerformed = 0;
const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true);
const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width);
const auto clampToMargin = row >= topMargin && row <= bottomMargin && column >= leftMargin;
const auto minColumn = clampToMargin ? leftMargin : 0;
_InitTabStopsForWidth(width);
while (column > minColumn && tabsPerformed < numTabs)
{
column--;
if (til::at(_tabStopColumns, column))
{
tabsPerformed++;
}
}
cursor.SetXPosition(column);
_ApplyCursorMovementFlags(cursor);
return true;
}
//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)
{
switch (clearType)
{
case DispatchTypes::TabClearType::ClearCurrentColumn:
_ClearSingleTabStop();
return true;
case DispatchTypes::TabClearType::ClearAllColumns:
_ClearAllTabStops();
return true;
default:
return false;
}
}
// Routine Description:
// - Clears the tab stop in the cursor's current column, if there is one.
// Arguments:
// - <none>
// Return value:
// - <none>
void AdaptDispatch::_ClearSingleTabStop()
{
const auto page = _pages.ActivePage();
const auto column = page.Cursor().GetPosition().x;
_InitTabStopsForWidth(page.Width());
_tabStopColumns.at(column) = false;
}
// 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:
// - <none>
void AdaptDispatch::_ClearAllTabStops() noexcept
{
_tabStopColumns.clear();
_initDefaultTabStops = false;
}
// Routine Description:
// - DECST8C - If the parameter is SetEvery8Columns or is omitted, then this
// clears all tab stops and sets the _initDefaultTabStops flag to indicate
// that the default positions should be reinitialized when needed.
// Arguments:
// - setType - only SetEvery8Columns is supported
// Return value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::TabSet(const VTParameter setType) noexcept
{
constexpr auto SetEvery8Columns = DispatchTypes::TabSetType::SetEvery8Columns;
if (setType.value_or(SetEvery8Columns) == SetEvery8Columns)
{
_tabStopColumns.clear();
_initDefaultTabStops = true;
return true;
}
return false;
}
// 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 accommodate
// Return value:
// - <none>
void AdaptDispatch::_InitTabStopsForWidth(const VTInt width)
{
const auto screenWidth = gsl::narrow<size_t>(width);
const auto initialWidth = _tabStopColumns.size();
if (screenWidth > initialWidth)
{
_tabStopColumns.resize(screenWidth);
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())
{
_initialCodePage = _api.GetConsoleOutputCP();
}
switch (codingSystem)
{
case DispatchTypes::CodingSystem::ISO2022:
_api.SetConsoleOutputCP(28591);
AcceptC1Controls(true);
_termOutput.EnableGrTranslation(true);
return true;
case DispatchTypes::CodingSystem::UTF8:
_api.SetConsoleOutputCP(CP_UTF8);
AcceptC1Controls(false);
_termOutput.EnableGrTranslation(false);
return true;
default:
return false;
}
}
//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 VTInt 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 VTInt 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 VTInt 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 VTInt 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 VTInt gsetNumber) noexcept
{
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.
bool AdaptDispatch::AcceptC1Controls(const bool enabled)
{
_api.GetStateMachine().SetParserMode(StateMachine::Mode::AcceptC1, enabled);
return true;
}
//Routine Description:
// ACS - Announces the ANSI conformance level for subsequent data exchange.
// This requires certain character sets to be mapped into the terminal's
// G-sets and in-use tables.
//Arguments:
// - ansiLevel - the expected conformance level
// Return value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::AnnounceCodeStructure(const VTInt ansiLevel)
{
// Levels 1 and 2 require ASCII in G0/GL and Latin-1 in G1/GR.
// Level 3 only requires ASCII in G0/GL.
switch (ansiLevel)
{
case 1:
case 2:
Designate96Charset(1, VTID("A")); // Latin-1 designated as G1
LockingShiftRight(1); // G1 mapped into GR
[[fallthrough]];
case 3:
Designate94Charset(0, VTID("B")); // ASCII designated as G0
LockingShift(0); // G0 mapped into GL
return true;
default:
return false;
}
}
//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.
// X 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.
// X Select character attribute DECSCA Normal (erasable by DECSEL and DECSED).
// X Save cursor state DECSC Home position.
// X Assign user preference DECAUPSS Always Latin-1 (not configurable).
// 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()
{
_pages.ActivePage().Cursor().SetIsVisible(true); // Cursor enabled.
// Replace mode; Absolute cursor addressing; Disallow left/right margins.
_modes.reset(Mode::InsertReplace, Mode::Origin, Mode::AllowDECSLRM);
_api.SetSystemMode(ITerminalApi::Mode::AutoWrap, true); // Wrap at end of line.
_terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, false); // Normal characters.
_terminalInput.SetInputMode(TerminalInput::Mode::Keypad, false); // Numeric characters.
// Top margin = 1; bottom margin = page length.
_DoSetTopBottomScrollingMargins(0, 0);
// Left margin = 1; right margin = page width.
_DoSetLeftRightScrollingMargins(0, 0);
_termOutput.SoftReset(); // Reset all character set designations.
SetGraphicsRendition({}); // Normal rendition.
SetCharacterProtectionAttribute({}); // Default (unprotected)
// 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
// The TerminalOutput state in these buffers must be reset to
// the same state as the _termOutput instance, which is not
// necessarily equivalent to a full reset.
_savedCursorState.at(0).TermOutput = _termOutput;
_savedCursorState.at(1).TermOutput = _termOutput;
return !_api.IsConsolePty();
}
//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()
{
// If in the alt buffer, switch back to main before doing anything else.
if (_usingAltBuffer)
{
_api.UseMainScreenBuffer();
_usingAltBuffer = false;
}
// Reset all page buffers.
_pages.Reset();
// Completely reset the TerminalOutput state.
_termOutput = {};
if (_initialCodePage.has_value())
{
// Restore initial code page if previously changed by a DOCS sequence.
_api.SetConsoleOutputCP(_initialCodePage.value());
}
// Disable parsing of C1 control codes.
AcceptC1Controls(false);
// Sets the SGR state to normal - this must be done before EraseInDisplay
// to ensure that it clears with the default background color.
SoftReset();
// Clears the screen - Needs to be done in two operations.
EraseInDisplay(DispatchTypes::EraseType::All);
EraseInDisplay(DispatchTypes::EraseType::Scrollback);
// Set the DECSCNM screen mode back to normal.
_renderSettings.SetRenderMode(RenderSettings::Mode::ScreenReversed, false);
// Cursor to 1,1 - the Soft Reset guarantees this is absolute
CursorPosition(1, 1);
// We only reset the system line feed mode if the input mode is set. If it
// isn't set, that either means they're both reset, and there's nothing for
// us to do, or they're out of sync, which implies the system mode was set
// via the console API, so it's not our responsibility.
if (_terminalInput.GetInputMode(TerminalInput::Mode::LineFeed))
{
_api.SetSystemMode(ITerminalApi::Mode::LineFeed, false);
}
// Reset input modes to their initial state
_terminalInput.ResetInputModes();
// Reset bracketed paste mode
_api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, false);
// Restore cursor blinking mode.
_pages.ActivePage().Cursor().SetBlinkingAllowed(true);
// Delete all current tab stops and reapply
TabSet(DispatchTypes::TabSetType::SetEvery8Columns);
// Clear the soft font in the renderer and delete the font buffer.
_renderer.UpdateSoftFont({}, {}, false);
_fontBuffer = nullptr;
// Reset internal modes to their initial state
_modes = { Mode::PageCursorCoupling };
// Clear and release the macro buffer.
if (_macroBuffer)
{
_macroBuffer->ClearMacrosIfInUse();
_macroBuffer = nullptr;
}
// If we're in a conpty, we need flush this RIS sequence to the connected
// terminal application, but we also need to follow that up with a DECSET
// sequence to re-enable the modes that we require (namely win32 input mode
// and focus event mode). It's important that this is kept in sync with the
// VtEngine::RequestWin32Input method which requests the modes on startup.
if (_api.IsConsolePty())
{
auto& stateMachine = _api.GetStateMachine();
if (stateMachine.FlushToTerminal())
{
auto& engine = stateMachine.Engine();
engine.ActionPassThroughString(L"\033[?9001h\033[?1004h");
}
}
return true;
}
// 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.
bool AdaptDispatch::ScreenAlignmentPattern()
{
const auto page = _pages.ActivePage();
// Fill the screen with the letter E using the default attributes.
_FillRect(page, { 0, page.Top(), page.Width(), page.Bottom() }, L"E", {});
// Reset the line rendition for all of these rows.
page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom());
// Reset the meta/extended attributes (but leave the colors unchanged).
auto attr = page.Attributes();
attr.SetStandardErase();
page.SetAttributes(attr);
// Reset the origin mode to absolute, and disallow left/right margins.
_modes.reset(Mode::Origin, Mode::AllowDECSLRM);
// Clear the scrolling margins.
_DoSetTopBottomScrollingMargins(0, 0);
_DoSetLeftRightScrollingMargins(0, 0);
// Set the cursor position to home.
CursorPosition(1, 1);
return true;
}
//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 page to 0,0, and clear everything outside
// (0, 0, pageWidth, pageHeight) 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()
{
const auto page = _pages.VisiblePage();
auto& cursor = page.Cursor();
const auto row = cursor.GetPosition().y;
page.Buffer().ClearScrollback(page.Top(), page.Height());
// Move the viewport
_api.SetViewportPosition({ page.XPanOffset(), 0 });
// Move the cursor to the same relative location.
cursor.SetYPosition(row - page.Top());
cursor.SetHasMoved(true);
// 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.
return !_api.IsConsolePty();
}
//Routine Description:
// - Erase All (^[[2J - ED)
// Performs a VT Erase All operation. In most terminals, this is done by
// moving the viewport into the scrollback, clearing out the current screen.
// For them, there can never be any characters beneath the viewport, as the
// viewport is always at the bottom. So, we can accomplish the same behavior
// by using the LastNonspaceCharacter as the "bottom", and placing the new
// viewport underneath that character.
// Arguments:
// - <none>
// Return value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_EraseAll()
{
const auto page = _pages.ActivePage();
const auto pageWidth = page.Width();
const auto pageHeight = page.Height();
const auto bufferHeight = page.BufferHeight();
auto& textBuffer = page.Buffer();
const auto inPtyMode = _api.IsConsolePty();
// Stash away the current position of the cursor within the page.
// We'll need to restore the cursor to that same relative position, after
// we move the viewport.
auto& cursor = page.Cursor();
const auto row = cursor.GetPosition().y - page.Top();
// Calculate new page position. Typically we want to move one line below
// the last non-space row, but if the last non-space character is the very
// start of the buffer, then we shouldn't move down at all.
const auto lastChar = textBuffer.GetLastNonSpaceCharacter();
auto newPageTop = lastChar == til::point{} ? 0 : lastChar.y + 1;
auto newPageBottom = newPageTop + pageHeight;
const auto delta = newPageBottom - bufferHeight;
if (delta > 0)
{
for (auto i = 0; i < delta; i++)
{
textBuffer.IncrementCircularBuffer();
}
_api.NotifyBufferRotation(delta);
newPageTop -= delta;
newPageBottom -= delta;
// We don't want to trigger a scroll in pty mode, because we're going to
// pass through the ED sequence anyway, and this will just result in the
// buffer being scrolled up by two pages instead of one.
if (!inPtyMode)
{
textBuffer.TriggerScroll({ 0, -delta });
}
}
// Move the viewport if necessary.
if (newPageTop != page.Top())
{
_api.SetViewportPosition({ page.XPanOffset(), newPageTop });
}
// Restore the relative cursor position
cursor.SetYPosition(row + newPageTop);
cursor.SetHasMoved(true);
// Erase all the rows in the current page.
const auto eraseAttributes = _GetEraseAttributes(page);
_FillRect(page, { 0, newPageTop, pageWidth, newPageBottom }, whitespace, eraseAttributes);
// Also reset the line rendition for the erased rows.
textBuffer.ResetLineRenditionRange(newPageTop, newPageBottom);
// 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. But this only
// applies if we're in the active buffer, since this should have no
// visible effect for an inactive buffer.
return !(inPtyMode && textBuffer.IsActiveBuffer());
}
//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)
{
auto actualType = CursorType::Legacy;
auto fEnableBlinking = false;
switch (cursorStyle)
{
case DispatchTypes::CursorStyle::UserDefault:
fEnableBlinking = true;
actualType = _api.GetUserDefaultCursorStyle();
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;
}
auto& cursor = _pages.ActivePage().Cursor();
cursor.SetType(actualType);
cursor.SetBlinkingAllowed(fEnableBlinking);
// If we're a conpty, always return false, so that this cursor state will be
// sent to the connected terminal
return !_api.IsConsolePty();
}
// 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)
{
return SetColorTableEntry(TextColor::CURSOR_COLOR, 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 wil::zwstring_view content)
{
// Return false to forward the operation to the hosting terminal,
// since ConPTY can't handle this itself.
if (_api.IsConsolePty())
{
return false;
}
_api.CopyToClipboard(content);
return true;
}
// 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)
{
_renderSettings.SetColorTableEntry(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 (_api.IsConsolePty())
{
return false;
}
// If we're updating the background color, we need to let the renderer
// know, since it may want to repaint the window background to match.
const auto backgroundIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground);
const auto backgroundChanged = (tableIndex == backgroundIndex);
// Similarly for the frame color, the tab may need to be repainted.
const auto frameIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground);
const auto frameChanged = (tableIndex == frameIndex);
// Update the screen colors if we're not a pty
// No need to force a redraw in pty mode.
_renderer.TriggerRedrawAll(backgroundChanged, frameChanged);
return true;
}
// 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 AdaptDispatch::SetDefaultForeground(const DWORD dwColor)
{
_renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, TextColor::DEFAULT_FOREGROUND);
return SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, dwColor);
}
// 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 AdaptDispatch::SetDefaultBackground(const DWORD dwColor)
{
_renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, TextColor::DEFAULT_BACKGROUND);
return SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, dwColor);
}
// Method Description:
// DECAC - Assigns the foreground and background color indexes that should be
// used for a given aspect of the user interface.
// Arguments:
// - item: The aspect of the interface that will have its colors altered.
// - fgIndex: The color table index to be used for the foreground.
// - bgIndex: The color table index to be used for the background.
// Return Value:
// True if handled successfully. False otherwise.
bool AdaptDispatch::AssignColor(const DispatchTypes::ColorItem item, const VTInt fgIndex, const VTInt bgIndex)
{
switch (item)
{
case DispatchTypes::ColorItem::NormalText:
_renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, fgIndex);
_renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, bgIndex);
break;
case DispatchTypes::ColorItem::WindowFrame:
_renderSettings.SetColorAliasIndex(ColorAlias::FrameForeground, fgIndex);
_renderSettings.SetColorAliasIndex(ColorAlias::FrameBackground, bgIndex);
break;
default:
return false;
}
// No need to force a redraw in pty mode.
const auto inPtyMode = _api.IsConsolePty();
if (!inPtyMode)
{
const auto backgroundChanged = item == DispatchTypes::ColorItem::NormalText;
const auto frameChanged = item == DispatchTypes::ColorItem::WindowFrame;
_renderer.TriggerRedrawAll(backgroundChanged, frameChanged);
}
return !inPtyMode;
}
//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)
{
// Other Window Manipulation functions:
// MSFT:13271098 - QueryViewport
// MSFT:13271146 - QueryScreenSize
switch (function)
{
case DispatchTypes::WindowManipulationType::DeIconifyWindow:
_api.ShowWindow(true);
return true;
case DispatchTypes::WindowManipulationType::IconifyWindow:
_api.ShowWindow(false);
return true;
case DispatchTypes::WindowManipulationType::RefreshWindow:
_pages.VisiblePage().Buffer().TriggerRedrawAll();
return true;
case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters:
_api.ResizeWindow(parameter2.value_or(0), parameter1.value_or(0));
return true;
case DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters:
{
const auto page = _pages.VisiblePage();
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[8;{};{}t"), page.Height(), page.Width()));
return true;
}
default:
return false;
}
}
// 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)
{
const auto page = _pages.ActivePage();
auto attr = page.Attributes();
const auto id = page.Buffer().GetHyperlinkId(uri, params);
attr.SetHyperlinkId(id);
page.SetAttributes(attr);
page.Buffer().AddHyperlinkToMap(uri, id);
return true;
}
// Method Description:
// - Ends a hyperlink
// Return Value:
// - true
bool AdaptDispatch::EndHyperlink()
{
const auto page = _pages.ActivePage();
auto attr = page.Attributes();
attr.SetHyperlinkId(0);
page.SetAttributes(attr);
return true;
}
// Method Description:
// - Performs a ConEmu action
// Currently, the only actions we support are setting the taskbar state/progress
// and setting the working directory.
// Arguments:
// - string - contains the parameters that define which action we do
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::DoConEmuAction(const std::wstring_view string)
{
// Return false to forward the operation to the hosting terminal,
// since ConPTY can't handle this itself.
if (_api.IsConsolePty())
{
return false;
}
constexpr size_t TaskbarMaxState{ 4 };
constexpr size_t TaskbarMaxProgress{ 100 };
unsigned int state = 0;
unsigned int progress = 0;
const auto parts = Utils::SplitString(string, L';');
unsigned int subParam = 0;
if (parts.size() < 1 || !Utils::StringToUint(til::at(parts, 0), subParam))
{
return false;
}
// 4 is SetProgressBar, which sets the taskbar state/progress.
if (subParam == 4)
{
if (parts.size() >= 2)
{
// A state parameter is defined, parse it out
const auto stateSuccess = Utils::StringToUint(til::at(parts, 1), state);
if (!stateSuccess && !til::at(parts, 1).empty())
{
return false;
}
if (parts.size() >= 3)
{
// A progress parameter is also defined, parse it out
const auto progressSuccess = Utils::StringToUint(til::at(parts, 2), progress);
if (!progressSuccess && !til::at(parts, 2).empty())
{
return false;
}
}
}
if (state > TaskbarMaxState)
{
// state is out of bounds, return false
return false;
}
if (progress > TaskbarMaxProgress)
{
// progress is greater than the maximum allowed value, clamp it to the max
progress = TaskbarMaxProgress;
}
_api.SetTaskbarProgress(static_cast<DispatchTypes::TaskbarState>(state), progress);
return true;
}
// 9 is SetWorkingDirectory, which informs the terminal about the current working directory.
else if (subParam == 9)
{
if (parts.size() >= 2)
{
auto path = til::at(parts, 1);
// The path should be surrounded with '"' according to the documentation of ConEmu.
// An example: 9;"D:/"
// If we fail to find the surrounding quotation marks, we'll give the path a try anyway.
// ConEmu also does this.
if (path.size() >= 3 && path.at(0) == L'"' && path.at(path.size() - 1) == L'"')
{
path = path.substr(1, path.size() - 2);
}
if (!til::is_legal_path(path))
{
return false;
}
_api.SetWorkingDirectory(path);
return true;
}
}
// 12: "Let ConEmu treat current cursor position as prompt start"
//
// Based on the official conemu docs:
// * https://conemu.github.io/en/ShellWorkDir.html#connector-ps1
// * https://conemu.github.io/en/ShellWorkDir.html#PowerShell
//
// This seems like basically the same as 133;B - the end of the prompt, the start of the commandline.
else if (subParam == 12)
{
_pages.ActivePage().Buffer().StartCommand();
return true;
}
return false;
}
// Method Description:
// - Performs a iTerm2 action
// - Ascribes to the ITermDispatch interface
// - Currently, the actions we support are:
// * `OSC1337;SetMark`: mark a line as a prompt line
// - Not actually used in conhost
// Arguments:
// - string: contains the parameters that define which action we do
// Return Value:
// - false in conhost, true for the SetMark action, otherwise false.
bool AdaptDispatch::DoITerm2Action(const std::wstring_view string)
{
const auto isConPty = _api.IsConsolePty();
if (isConPty)
{
// Flush the frame manually, to make sure marks end up on the right
// line, like the alt buffer sequence.
_renderer.TriggerFlush(false);
}
if constexpr (!Feature_ScrollbarMarks::IsEnabled())
{
return false;
}
const auto parts = Utils::SplitString(string, L';');
if (parts.size() < 1)
{
return false;
}
const auto action = til::at(parts, 0);
bool handled = false;
if (action == L"SetMark")
{
_pages.ActivePage().Buffer().StartPrompt();
handled = true;
}
return handled && !isConPty;
}
// Method Description:
// - Performs a FinalTerm action
// - Currently, the actions we support are:
// * `OSC133;A`: mark a line as a prompt line
// - Not actually used in conhost
// - The remainder of the FTCS prompt sequences are tracked in GH#11000
// Arguments:
// - string: contains the parameters that define which action we do
// Return Value:
// - false in conhost, true for the SetMark action, otherwise false.
bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string)
{
const auto isConPty = _api.IsConsolePty();
if (isConPty)
{
// Flush the frame manually, to make sure marks end up on the right
// line, like the alt buffer sequence.
_renderer.TriggerFlush(false);
}
if constexpr (!Feature_ScrollbarMarks::IsEnabled())
{
return false;
}
const auto parts = Utils::SplitString(string, L';');
if (parts.size() < 1)
{
return false;
}
bool handled = false;
const auto action = til::at(parts, 0);
if (action.size() == 1)
{
switch (til::at(action, 0))
{
case L'A': // FTCS_PROMPT
{
_pages.ActivePage().Buffer().StartPrompt();
handled = true;
break;
}
case L'B': // FTCS_COMMAND_START
{
_pages.ActivePage().Buffer().StartCommand();
handled = true;
break;
}
case L'C': // FTCS_COMMAND_EXECUTED
{
_pages.ActivePage().Buffer().StartOutput();
handled = true;
break;
}
case L'D': // FTCS_COMMAND_FINISHED
{
std::optional<unsigned int> error = std::nullopt;
if (parts.size() >= 2)
{
const auto errorString = til::at(parts, 1);
// If we fail to parse the code, then it was gibberish, or it might
// have just started with "-". Either way, let's just treat it as an
// error and move on.
//
// We know that "0" will be successfully parsed, and that's close enough.
unsigned int parsedError = 0;
error = Utils::StringToUint(errorString, parsedError) ? parsedError :
UINT_MAX;
}
_pages.ActivePage().Buffer().EndCurrentCommand(error);
handled = true;
break;
}
default:
{
handled = false;
}
}
}
// When we add the rest of the FTCS sequences (GH#11000), we should add a
// simple state machine here to track the most recently emitted mark from
// this set of sequences, and which sequence was emitted last, so we can
// modify the state of that mark as we go.
return handled && !isConPty;
}
// Method Description:
// - Performs a VsCode action
// - Currently, the actions we support are:
// * Completions: An experimental protocol for passing shell completion
// information from the shell to the terminal. This sequence is still under
// active development, and subject to change.
// - Not actually used in conhost
// Arguments:
// - string: contains the parameters that define which action we do
// Return Value:
// - false in conhost, true for the SetMark action, otherwise false.
bool AdaptDispatch::DoVsCodeAction(const std::wstring_view string)
{
// This is not implemented in conhost.
if (_api.IsConsolePty())
{
// Flush the frame manually to make sure this action happens at the right time.
_renderer.TriggerFlush(false);
return false;
}
if constexpr (!Feature_ShellCompletions::IsEnabled())
{
return false;
}
const auto parts = Utils::SplitString(string, L';');
if (parts.size() < 1)
{
return false;
}
const auto action = til::at(parts, 0);
if (action == L"Completions")
{
// The structure of the message is as follows:
// `e]633;
// 0: Completions;
// 1: $($completions.ReplacementIndex);
// 2: $($completions.ReplacementLength);
// 3: $($cursorIndex);
// 4: $completions.CompletionMatches | ConvertTo-Json
unsigned int replacementIndex = 0;
unsigned int replacementLength = 0;
unsigned int cursorIndex = 0;
bool succeeded = (parts.size() >= 2) &&
(Utils::StringToUint(til::at(parts, 1), replacementIndex));
succeeded &= (parts.size() >= 3) &&
(Utils::StringToUint(til::at(parts, 2), replacementLength));
succeeded &= (parts.size() >= 4) &&
(Utils::StringToUint(til::at(parts, 3), cursorIndex));
// VsCode is using cursorIndex and replacementIndex, but we aren't currently.
if (succeeded)
{
// Get the combined lengths of parts 0-3, plus the semicolons. We
// need this so that we can just pass the remainder of the string.
const auto prefixLength = til::at(parts, 0).size() + 1 +
til::at(parts, 1).size() + 1 +
til::at(parts, 2).size() + 1 +
til::at(parts, 3).size() + 1;
if (prefixLength > string.size())
{
return true;
}
// Get the remainder of the string
const auto remainder = string.substr(prefixLength);
_api.InvokeCompletions(parts.size() < 5 ? L"" : remainder,
replacementLength);
}
// If it's poorly formatted, just eat it
return true;
}
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 VTInt 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::CharsetSize charsetSize)
{
// 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;
}
// If we're a conpty, we create a special passthrough handler that will
// forward the DECDLD sequence to the conpty terminal with a hard-coded ID.
// That ID is also pre-mapped into the G1 table, so the VT engine can just
// switch to G1 when it needs to output any DRCS characters. But note that
// we still need to process the DECDLD sequence locally, so the character
// set translation is correctly handled on the host side.
const auto conptyPassthrough = _api.IsConsolePty() ? _CreateDrcsPassthroughHandler(charsetSize) : nullptr;
return [=](const auto ch) {
if (conptyPassthrough)
{
conptyPassthrough(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::CharsetSize::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();
_renderer.UpdateSoftFont(bitPattern, cellSize, centeringHint);
}
return true;
};
}
// Routine Description:
// - Helper method to create a string handler that can be used to pass through
// DECDLD sequences when in conpty mode. This patches the original sequence
// with a hard-coded character set ID, and pre-maps that ID into the G1 table.
// Arguments:
// - <none>
// Return value:
// - a function to receive the data or nullptr if the initial flush fails
ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const DispatchTypes::CharsetSize charsetSize)
{
const auto defaultPassthrough = _CreatePassthroughHandler();
if (defaultPassthrough)
{
auto& engine = _api.GetStateMachine().Engine();
return [=, &engine, gotId = false](const auto ch) mutable {
// The character set ID is contained in the first characters of the
// sequence, so we just ignore that initial content until we receive
// a "final" character (i.e. in range 30 to 7E). At that point we
// pass through a hard-coded ID of "@".
if (!gotId)
{
if (ch >= 0x30 && ch <= 0x7E)
{
gotId = true;
defaultPassthrough('@');
}
}
else if (!defaultPassthrough(ch))
{
// Once the DECDLD sequence is finished, we also output an SCS
// sequence to map the character set into the G1 table.
const auto charset96 = charsetSize == DispatchTypes::CharsetSize::Size96;
engine.ActionPassThroughString(charset96 ? L"\033-@" : L"\033)@");
}
return true;
};
}
return nullptr;
}
// Method Description:
// - DECRQUPSS - Request the user-preference supplemental character set.
// Arguments:
// - None
// Return Value:
// - True
bool AdaptDispatch::RequestUserPreferenceCharset()
{
const auto size = _termOutput.GetUserPreferenceCharsetSize();
const auto id = _termOutput.GetUserPreferenceCharsetId();
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033P{}!u{}\033\\"), (size == 96 ? 1 : 0), id.ToString()));
return true;
}
// Method Description:
// - DECAUPSS - Assigns the user-preference supplemental character set.
// Arguments:
// - charsetSize - Whether the character set is 94 or 96 characters.
// Return Value:
// - a function to parse the character set ID
ITermDispatch::StringHandler AdaptDispatch::AssignUserPreferenceCharset(const DispatchTypes::CharsetSize charsetSize)
{
return [this, charsetSize, idBuilder = VTIDBuilder{}](const auto ch) mutable {
if (ch >= L'\x20' && ch <= L'\x2f')
{
idBuilder.AddIntermediate(ch);
}
else if (ch >= L'\x30' && ch <= L'\x7e')
{
const auto id = idBuilder.Finalize(ch);
switch (charsetSize)
{
case DispatchTypes::CharsetSize::Size94:
_termOutput.AssignUserPreferenceCharset(id, false);
break;
case DispatchTypes::CharsetSize::Size96:
_termOutput.AssignUserPreferenceCharset(id, true);
break;
}
return false;
}
return true;
};
}
// Method Description:
// - DECDMAC - Defines a string of characters as a macro that can later be
// invoked with a DECINVM sequence.
// Arguments:
// - macroId - a number to identify the macro when invoked.
// - deleteControl - what gets deleted before loading the new macro data.
// - encoding - whether the data is encoded as plain text or hex digits.
// Return Value:
// - a function to receive the macro data or nullptr if parameters are invalid.
ITermDispatch::StringHandler AdaptDispatch::DefineMacro(const VTInt macroId,
const DispatchTypes::MacroDeleteControl deleteControl,
const DispatchTypes::MacroEncoding encoding)
{
if (!_macroBuffer)
{
_macroBuffer = std::make_shared<MacroBuffer>();
}
if (_macroBuffer->InitParser(macroId, deleteControl, encoding))
{
return [&](const auto ch) {
return _macroBuffer->ParseDefinition(ch);
};
}
return nullptr;
}
// Method Description:
// - DECINVM - Invokes a previously defined macro, executing the macro content
// as if it had been received directly from the host.
// Arguments:
// - macroId - the id number of the macro to be invoked.
// Return Value:
// - True
bool AdaptDispatch::InvokeMacro(const VTInt macroId)
{
if (_macroBuffer)
{
// In order to inject our macro sequence into the state machine
// we need to register a callback that will be executed only
// once it has finished processing the current operation, and
// has returned to the ground state. Note that we're capturing
// a copy of the _macroBuffer pointer here to make sure it won't
// be deleted (e.g. from an invoked RIS) while still in use.
const auto macroBuffer = _macroBuffer;
auto& stateMachine = _api.GetStateMachine();
stateMachine.OnCsiComplete([=, &stateMachine]() {
macroBuffer->InvokeMacro(macroId, stateMachine);
});
}
return true;
}
// Method Description:
// - DECRSTS - Restores the terminal state from a stream of data previously
// saved with a DECRQTSR query.
// Arguments:
// - format - the format of the state report being restored.
// Return Value:
// - a function to receive the data or nullptr if the format is unsupported.
ITermDispatch::StringHandler AdaptDispatch::RestoreTerminalState(const DispatchTypes::ReportFormat format)
{
switch (format)
{
case DispatchTypes::ReportFormat::ColorTableReport:
return _RestoreColorTable();
default:
return nullptr;
}
}
// Method Description:
// - DECCTR - This is a parser for the Color Table Report received via DECRSTS.
// The report contains a list of color definitions separated with a slash
// character. Each definition consists of 5 parameters: Pc;Pu;Px;Py;Pz
// - Pc is the color number.
// - Pu is the color model (1 = HLS, 2 = RGB).
// - Px, Py, and Pz are component values in the color model.
// Arguments:
// - <none>
// Return Value:
// - a function to parse the report data.
ITermDispatch::StringHandler AdaptDispatch::_RestoreColorTable()
{
// If we're a conpty, we create a passthrough string handler to forward the
// color report to the connected terminal.
if (_api.IsConsolePty())
{
return _CreatePassthroughHandler();
}
return [this, parameter = VTInt{}, parameters = std::vector<VTParameter>{}](const auto ch) mutable {
if (ch >= L'0' && ch <= L'9')
{
parameter *= 10;
parameter += (ch - L'0');
parameter = std::min(parameter, MAX_PARAMETER_VALUE);
}
else if (ch == L';')
{
if (parameters.size() < 5)
{
parameters.push_back(parameter);
}
parameter = 0;
}
else if (ch == L'/' || ch == AsciiChars::ESC)
{
parameters.push_back(parameter);
const auto colorParameters = VTParameters{ parameters.data(), parameters.size() };
const auto colorNumber = colorParameters.at(0).value_or(0);
if (colorNumber < TextColor::TABLE_SIZE)
{
const auto colorModel = DispatchTypes::ColorModel{ colorParameters.at(1) };
const auto x = colorParameters.at(2).value_or(0);
const auto y = colorParameters.at(3).value_or(0);
const auto z = colorParameters.at(4).value_or(0);
if (colorModel == DispatchTypes::ColorModel::HLS)
{
SetColorTableEntry(colorNumber, Utils::ColorFromHLS(x, y, z));
}
else if (colorModel == DispatchTypes::ColorModel::RGB)
{
SetColorTableEntry(colorNumber, Utils::ColorFromRGB100(x, y, z));
}
}
parameters.clear();
parameter = 0;
}
return (ch != AsciiChars::ESC);
};
}
// 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.
return [this, parameter = VTInt{}, idBuilder = VTIDBuilder{}](const auto ch) mutable {
const auto isFinal = ch >= L'\x40' && ch <= L'\x7e';
if (isFinal)
{
const auto id = idBuilder.Finalize(ch);
switch (id)
{
case VTID("m"):
_ReportSGRSetting();
break;
case VTID("r"):
_ReportDECSTBMSetting();
break;
case VTID("s"):
_ReportDECSLRMSetting();
break;
case VTID("\"q"):
_ReportDECSCASetting();
break;
case VTID("*x"):
_ReportDECSACESetting();
break;
case VTID(",|"):
_ReportDECACSetting(VTParameter{ parameter });
break;
default:
_api.ReturnResponse(L"\033P0$r\033\\");
break;
}
return false;
}
else
{
// Although we don't yet support any operations with parameter
// prefixes, it's important that we still parse the prefix and
// include it in the ID. Otherwise we'll mistakenly respond to
// prefixed queries that we don't actually recognise.
const auto isParameterPrefix = ch >= L'<' && ch <= L'?';
const auto isParameter = ch >= L'0' && ch < L'9';
const auto isIntermediate = ch >= L'\x20' && ch <= L'\x2f';
if (isParameterPrefix || isIntermediate)
{
idBuilder.AddIntermediate(ch);
}
else if (isParameter)
{
parameter *= 10;
parameter += (ch - L'0');
parameter = std::min(parameter, MAX_PARAMETER_VALUE);
}
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);
const auto& attr = _pages.ActivePage().Attributes();
const auto ulStyle = attr.GetUnderlineStyle();
// 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.IsIntense());
addAttribute(L";2"sv, attr.IsFaint());
addAttribute(L";3"sv, attr.IsItalic());
addAttribute(L";4"sv, ulStyle == UnderlineStyle::SinglyUnderlined);
addAttribute(L";4:3"sv, ulStyle == UnderlineStyle::CurlyUnderlined);
addAttribute(L";4:4"sv, ulStyle == UnderlineStyle::DottedUnderlined);
addAttribute(L";4:5"sv, ulStyle == UnderlineStyle::DashedUnderlined);
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, ulStyle == UnderlineStyle::DoublyUnderlined);
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());
addColor(50, attr.GetUnderlineColor());
// The 'm' indicates this is an SGR response, and ST ends the sequence.
response.append(L"m\033\\"sv);
_api.ReturnResponse({ 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()
{
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);
const auto page = _pages.ActivePage();
const auto [marginTop, marginBottom] = _GetVerticalMargins(page, false);
// VT origin is at 1,1 so we need to add 1 to these margins.
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginTop + 1, marginBottom + 1);
// The 'r' indicates this is an DECSTBM response, and ST ends the sequence.
response.append(L"r\033\\"sv);
_api.ReturnResponse({ response.data(), response.size() });
}
// Method Description:
// - Reports the DECSLRM margin range in response to a DECRQSS query.
// Arguments:
// - None
// Return Value:
// - None
void AdaptDispatch::_ReportDECSLRMSetting()
{
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);
const auto pageWidth = _pages.ActivePage().Width();
const auto [marginLeft, marginRight] = _GetHorizontalMargins(pageWidth);
// VT origin is at 1,1 so we need to add 1 to these margins.
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginLeft + 1, marginRight + 1);
// The 's' indicates this is an DECSLRM response, and ST ends the sequence.
response.append(L"s\033\\"sv);
_api.ReturnResponse({ response.data(), response.size() });
}
// Method Description:
// - Reports the DECSCA protected attribute in response to a DECRQSS query.
// Arguments:
// - None
// Return Value:
// - None
void AdaptDispatch::_ReportDECSCASetting() 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);
const auto& attr = _pages.ActivePage().Attributes();
response.append(attr.IsProtected() ? L"1"sv : L"0"sv);
// The '"q' indicates this is an DECSCA response, and ST ends the sequence.
response.append(L"\"q\033\\"sv);
_api.ReturnResponse({ response.data(), response.size() });
}
// Method Description:
// - Reports the DECSACE change extent in response to a DECRQSS query.
// Arguments:
// - None
// Return Value:
// - None
void AdaptDispatch::_ReportDECSACESetting() 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);
response.append(_modes.test(Mode::RectangularChangeExtent) ? L"2"sv : L"1"sv);
// The '*x' indicates this is an DECSACE response, and ST ends the sequence.
response.append(L"*x\033\\"sv);
_api.ReturnResponse({ response.data(), response.size() });
}
// Method Description:
// - Reports the DECAC color assignments in response to a DECRQSS query.
// Arguments:
// - None
// Return Value:
// - None
void AdaptDispatch::_ReportDECACSetting(const VTInt itemNumber) const
{
using namespace std::string_view_literals;
size_t fgIndex = 0;
size_t bgIndex = 0;
switch (static_cast<DispatchTypes::ColorItem>(itemNumber))
{
case DispatchTypes::ColorItem::NormalText:
fgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultForeground);
bgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground);
break;
case DispatchTypes::ColorItem::WindowFrame:
fgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameForeground);
bgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground);
break;
default:
_api.ReturnResponse(L"\033P0$r\033\\");
return;
}
// A valid response always starts with DCS 1 $ r.
fmt::basic_memory_buffer<wchar_t, 64> response;
response.append(L"\033P1$r"sv);
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{};{}"), itemNumber, fgIndex, bgIndex);
// The ',|' indicates this is a DECAC response, and ST ends the sequence.
response.append(L",|\033\\"sv);
_api.ReturnResponse({ response.data(), response.size() });
}
// Routine Description:
// - DECRQPSR - Queries the presentation state of the terminal. This can either
// be in the form of a cursor information report, or a tabulation stop report,
// depending on the requested format.
// Arguments:
// - format - the format of the report being requested.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat format)
{
switch (format)
{
case DispatchTypes::PresentationReportFormat::CursorInformationReport:
_ReportCursorInformation();
return true;
case DispatchTypes::PresentationReportFormat::TabulationStopReport:
_ReportTabStops();
return true;
default:
return false;
}
}
// Method Description:
// - DECRSPS - Restores the presentation state from a stream of data previously
// saved with a DECRQPSR query.
// Arguments:
// - format - the format of the report being restored.
// Return Value:
// - a function to receive the data or nullptr if the format is unsupported.
ITermDispatch::StringHandler AdaptDispatch::RestorePresentationState(const DispatchTypes::PresentationReportFormat format)
{
switch (format)
{
case DispatchTypes::PresentationReportFormat::CursorInformationReport:
return _RestoreCursorInformation();
case DispatchTypes::PresentationReportFormat::TabulationStopReport:
return _RestoreTabStops();
default:
return nullptr;
}
}
// Method Description:
// - DECCIR - Returns the Cursor Information Report in response to a DECRQPSR query.
// Arguments:
// - None
// Return Value:
// - None
void AdaptDispatch::_ReportCursorInformation()
{
const auto page = _pages.ActivePage();
const auto& cursor = page.Cursor();
const auto& attributes = page.Attributes();
// First pull the cursor position relative to the entire buffer out of the console.
til::point cursorPosition{ cursor.GetPosition() };
// Now adjust it for its position in respect to the current page top.
cursorPosition.y -= page.Top();
// NOTE: 1,1 is the top-left corner of the page in VT-speak, so add 1.
cursorPosition.x++;
cursorPosition.y++;
// If the origin mode is set, the cursor is relative to the margin origin.
if (_modes.test(Mode::Origin))
{
cursorPosition.x -= _GetHorizontalMargins(page.Width()).first;
cursorPosition.y -= _GetVerticalMargins(page, false).first;
}
// Only some of the rendition attributes are reported.
// Bit Attribute
// 1 bold
// 2 underlined
// 3 blink
// 4 reverse video
// 5 invisible
// 6 extension indicator
// 7 Always 1 (on)
// 8 Always 0 (off)
auto renditionAttributes = L'@'; // (0100 0000)
renditionAttributes += (attributes.IsIntense() ? 1 : 0);
renditionAttributes += (attributes.IsUnderlined() ? 2 : 0);
renditionAttributes += (attributes.IsBlinking() ? 4 : 0);
renditionAttributes += (attributes.IsReverseVideo() ? 8 : 0);
renditionAttributes += (attributes.IsInvisible() ? 16 : 0);
// There is only one character attribute.
const auto characterAttributes = attributes.IsProtected() ? L'A' : L'@';
// Miscellaneous flags and modes.
auto flags = L'@';
flags += (_modes.test(Mode::Origin) ? 1 : 0);
flags += (_termOutput.IsSingleShiftPending(2) ? 2 : 0);
flags += (_termOutput.IsSingleShiftPending(3) ? 4 : 0);
flags += (cursor.IsDelayedEOLWrap() ? 8 : 0);
// Character set designations.
const auto leftSetNumber = _termOutput.GetLeftSetNumber();
const auto rightSetNumber = _termOutput.GetRightSetNumber();
auto charsetSizes = L'@';
charsetSizes += (_termOutput.GetCharsetSize(0) == 96 ? 1 : 0);
charsetSizes += (_termOutput.GetCharsetSize(1) == 96 ? 2 : 0);
charsetSizes += (_termOutput.GetCharsetSize(2) == 96 ? 4 : 0);
charsetSizes += (_termOutput.GetCharsetSize(3) == 96 ? 8 : 0);
const auto charset0 = _termOutput.GetCharsetId(0);
const auto charset1 = _termOutput.GetCharsetId(1);
const auto charset2 = _termOutput.GetCharsetId(2);
const auto charset3 = _termOutput.GetCharsetId(3);
// A valid response always starts with DCS 1 $ u and ends with ST.
const auto response = fmt::format(
FMT_COMPILE(L"\033P1$u{};{};{};{};{};{};{};{};{};{}{}{}{}\033\\"),
cursorPosition.y,
cursorPosition.x,
page.Number(),
renditionAttributes,
characterAttributes,
flags,
leftSetNumber,
rightSetNumber,
charsetSizes,
charset0.ToString(),
charset1.ToString(),
charset2.ToString(),
charset3.ToString());
_api.ReturnResponse({ response.data(), response.size() });
}
// Method Description:
// - DECCIR - This is a parser for the Cursor Information Report received via DECRSPS.
// Arguments:
// - <none>
// Return Value:
// - a function to parse the report data.
ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation()
{
// clang-format off
enum Field { Row, Column, Page, SGR, Attr, Flags, GL, GR, Sizes, G0, G1, G2, G3 };
// clang-format on
constexpr til::enumset<Field> numeric{ Field::Row, Field::Column, Field::Page, Field::GL, Field::GR };
constexpr til::enumset<Field> flags{ Field::SGR, Field::Attr, Field::Flags, Field::Sizes };
constexpr til::enumset<Field> charset{ Field::G0, Field::G1, Field::G2, Field::G3 };
struct State
{
Field field{ Field::Row };
VTInt value{ 0 };
VTIDBuilder charsetId{};
std::array<bool, 4> charset96{};
VTParameter row{};
VTParameter column{};
};
return [&, state = State{}](const auto ch) mutable {
if (numeric.test(state.field))
{
if (ch >= '0' && ch <= '9')
{
state.value *= 10;
state.value += (ch - L'0');
state.value = std::min(state.value, MAX_PARAMETER_VALUE);
}
else if (ch == L';' || ch == AsciiChars::ESC)
{
if (state.field == Field::Row)
{
state.row = state.value;
}
else if (state.field == Field::Column)
{
state.column = state.value;
}
else if (state.field == Field::Page)
{
PagePositionAbsolute(state.value);
}
else if (state.field == Field::GL && state.value <= 3)
{
LockingShift(state.value);
}
else if (state.field == Field::GR && state.value <= 3)
{
LockingShiftRight(state.value);
}
state.value = {};
state.field = static_cast<Field>(state.field + 1);
}
}
else if (flags.test(state.field))
{
// Note that there could potentially be multiple characters in a
// flag field, so we process the flags as soon as they're received.
// But for now we're only interested in the first one, so once the
// state.value is set, we ignore everything else until the `;`.
if (ch >= L'@' && ch <= '~' && !state.value)
{
state.value = ch;
if (state.field == Field::SGR)
{
const auto page = _pages.ActivePage();
auto attr = page.Attributes();
attr.SetIntense(state.value & 1);
attr.SetUnderlineStyle(state.value & 2 ? UnderlineStyle::SinglyUnderlined : UnderlineStyle::NoUnderline);
attr.SetBlinking(state.value & 4);
attr.SetReverseVideo(state.value & 8);
attr.SetInvisible(state.value & 16);
page.SetAttributes(attr);
}
else if (state.field == Field::Attr)
{
const auto page = _pages.ActivePage();
auto attr = page.Attributes();
attr.SetProtected(state.value & 1);
page.SetAttributes(attr);
}
else if (state.field == Field::Sizes)
{
state.charset96.at(0) = state.value & 1;
state.charset96.at(1) = state.value & 2;
state.charset96.at(2) = state.value & 4;
state.charset96.at(3) = state.value & 8;
}
else if (state.field == Field::Flags)
{
const bool originMode = state.value & 1;
const bool ss2 = state.value & 2;
const bool ss3 = state.value & 4;
const bool delayedEOLWrap = state.value & 8;
// The cursor position is parsed at the start of the sequence,
// but we only set the position once we know the origin mode.
_modes.set(Mode::Origin, originMode);
CursorPosition(state.row, state.column);
// There can only be one single shift applied at a time, so
// we'll just apply the last one that is enabled.
_termOutput.SingleShift(ss3 ? 3 : (ss2 ? 2 : 0));
// The EOL flag will always be reset by the cursor movement
// above, so we only need to worry about setting it.
if (delayedEOLWrap)
{
const auto page = _pages.ActivePage();
page.Cursor().DelayEOLWrap();
}
}
}
else if (ch == L';')
{
state.value = 0;
state.field = static_cast<Field>(state.field + 1);
}
}
else if (charset.test(state.field))
{
if (ch >= L' ' && ch <= L'/')
{
state.charsetId.AddIntermediate(ch);
}
else if (ch >= L'0' && ch <= L'~')
{
const auto id = state.charsetId.Finalize(ch);
const auto gset = state.field - Field::G0;
if (state.charset96.at(gset))
{
Designate96Charset(gset, id);
}
else
{
Designate94Charset(gset, id);
}
state.charsetId.Clear();
state.field = static_cast<Field>(state.field + 1);
}
}
return (ch != AsciiChars::ESC);
};
}
// Method Description:
// - DECTABSR - Returns the Tabulation Stop Report in response to a DECRQPSR query.
// Arguments:
// - None
// Return Value:
// - None
void AdaptDispatch::_ReportTabStops()
{
// In order to be compatible with the original hardware terminals, we only
// report tab stops up to the current buffer width, even though there may
// be positions recorded beyond that limit.
const auto width = _pages.ActivePage().Width();
_InitTabStopsForWidth(width);
using namespace std::string_view_literals;
// A valid response always starts with DCS 2 $ u.
fmt::basic_memory_buffer<wchar_t, 64> response;
response.append(L"\033P2$u"sv);
auto need_separator = false;
for (auto column = 0; column < width; column++)
{
if (til::at(_tabStopColumns, column))
{
response.append(need_separator ? L"/"sv : L""sv);
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{}"), column + 1);
need_separator = true;
}
}
// An ST ends the sequence.
response.append(L"\033\\"sv);
_api.ReturnResponse({ response.data(), response.size() });
}
// Method Description:
// - DECTABSR - This is a parser for the Tabulation Stop Report received via DECRSPS.
// Arguments:
// - <none>
// Return Value:
// - a function to parse the report data.
ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops()
{
// In order to be compatible with the original hardware terminals, we need
// to be able to set tab stops up to at least 132 columns, even though the
// current buffer width may be less than that.
const auto width = std::max(_pages.ActivePage().Width(), 132);
_ClearAllTabStops();
_InitTabStopsForWidth(width);
return [this, width, column = size_t{}](const auto ch) mutable {
if (ch >= L'0' && ch <= L'9')
{
column *= 10;
column += (ch - L'0');
column = std::min<size_t>(column, MAX_PARAMETER_VALUE);
}
else if (ch == L'/' || ch == AsciiChars::ESC)
{
// Note that column 1 is always a tab stop, so there is no
// need to record an entry at that offset.
if (column > 1u && column <= static_cast<size_t>(width))
{
_tabStopColumns.at(column - 1) = true;
}
column = 0;
}
else
{
// If we receive an unexpected character, we don't try and
// process any more of the input - we just abort.
return false;
}
return (ch != AsciiChars::ESC);
};
}
// Routine Description:
// - DECPS - Plays a sequence of musical notes.
// Arguments:
// - params - The volume, duration, and note values to play.
// Return value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::PlaySounds(const VTParameters parameters)
{
// If we're a conpty, we return false so the command will be passed on
// to the connected terminal. But we need to flush the current frame
// first, otherwise the visual output will lag behind the sound.
if (_api.IsConsolePty())
{
_renderer.TriggerFlush(false);
return false;
}
// First parameter is the volume, in the range 0 to 7. We multiply by
// 127 / 7 to obtain an equivalent MIDI velocity in the range 0 to 127.
const auto velocity = std::min(parameters.at(0).value_or(0), 7) * 127 / 7;
// Second parameter is the duration, in the range 0 to 255. Units are
// 1/32 of a second, so we multiply by 1000000us/32 to obtain microseconds.
using namespace std::chrono_literals;
const auto duration = std::min(parameters.at(1).value_or(0), 255) * 1000000us / 32;
// The subsequent parameters are notes, in the range 0 to 25.
return parameters.subspan(2).for_each([=](const auto param) {
// Values 1 to 25 represent the notes C5 to C7, so we add 71 to
// obtain the equivalent MIDI note numbers (72 = C5).
const auto noteNumber = std::min(param.value_or(0), 25) + 71;
// But value 0 is meant to be silent, so if the note number is 71,
// we set the velocity to 0 (i.e. no volume).
_api.PlayMidiNote(noteNumber, noteNumber == 71 ? 0 : velocity, duration);
return true;
});
}
// Routine Description:
// - Helper method to create a string handler that can be used to pass through
// DCS sequences when in conpty mode.
// Arguments:
// - <none>
// Return value:
// - a function to receive the data or nullptr if the initial flush fails
ITermDispatch::StringHandler AdaptDispatch::_CreatePassthroughHandler()
{
// Before we pass through any more data, we need to flush the current frame
// first, otherwise it can end up arriving out of sync.
_renderer.TriggerFlush(false);
// Then we need to flush the sequence introducer and parameters that have
// already been parsed by the state machine.
auto& stateMachine = _api.GetStateMachine();
if (stateMachine.FlushToTerminal())
{
// And finally we create a StringHandler to receive the rest of the
// sequence data, and pass it through to the connected terminal.
auto& engine = stateMachine.Engine();
return [&, buffer = std::wstring{}](const auto ch) mutable {
// To make things more efficient, we buffer the string data before
// passing it through, only flushing if the buffer gets too large,
// or we're dealing with the last character in the current output
// fragment, or we've reached the end of the string.
const auto endOfString = ch == AsciiChars::ESC;
buffer += ch;
if (buffer.length() >= 4096 || stateMachine.IsProcessingLastCharacter() || endOfString)
{
// The end of the string is signaled with an escape, but for it
// to be a valid string terminator we need to add a backslash.
if (endOfString)
{
buffer += L'\\';
}
engine.ActionPassThroughString(buffer, true);
buffer.clear();
}
return !endOfString;
};
}
return nullptr;
}