Use DECCRA/DECFRA for ScrollConsoleScreenBuffer (#17747)

This adds logic to get the DA1 report from the hosting terminal on
startup. We then use the information to figure out if it supports
rectangular area operations. If so, we can use DECCRA/DECFRA to
implement ScrollConsoleScreenBuffer.

This additionally changes `ScrollConsoleScreenBuffer` to always
forbid control characters as the fill character, even in conhost
(via `VtIo::SanitizeUCS2`). My hope is that this makes the API
more consistent and robust as it avoids another source for
invisible control characters in the text buffer.

Part of #17643

## Validation Steps Performed
* New tests 
This commit is contained in:
Leonard Hecker 2024-08-23 22:02:01 +02:00 коммит произвёл GitHub
Родитель 0a91023df8
Коммит 040f26175f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 510 добавлений и 187 удалений

21
.github/actions/spelling/expect/expect.txt поставляемый
Просмотреть файл

@ -9,7 +9,6 @@ ABORTIFHUNG
ACCESSTOKEN
acidev
ACIOSS
ACover
acp
actctx
ACTCTXW
@ -87,6 +86,7 @@ Autowrap
AVerify
awch
azurecr
AZZ
backgrounded
Backgrounder
backgrounding
@ -180,7 +180,6 @@ CFuzz
cgscrn
chafa
changelists
charinfo
CHARSETINFO
chh
chshdng
@ -264,7 +263,6 @@ consolegit
consolehost
CONSOLEIME
consoleinternal
Consoleroot
CONSOLESETFOREGROUND
consoletaeftemplates
consoleuwp
@ -386,7 +384,7 @@ DECCIR
DECCKM
DECCKSR
DECCOLM
DECCRA
deccra
DECCTR
DECDC
DECDHL
@ -398,7 +396,7 @@ DECEKBD
DECERA
DECFI
DECFNK
DECFRA
decfra
DECGCI
DECGCR
DECGNL
@ -727,7 +725,6 @@ GHIJKL
gitcheckin
gitfilters
gitlab
gitmodules
gle
GLOBALFOCUS
GLYPHENTRY
@ -1021,7 +1018,6 @@ lstatus
lstrcmp
lstrcmpi
LTEXT
LTLTLTLTL
ltsc
LUID
luma
@ -1116,7 +1112,6 @@ msrc
MSVCRTD
MTSM
Munged
munges
murmurhash
muxes
myapplet
@ -1218,7 +1213,6 @@ ntlpcapi
ntm
ntrtl
ntstatus
NTSYSCALLAPI
nttree
nturtl
ntuser
@ -1526,7 +1520,6 @@ rftp
rgbi
RGBQUAD
rgbs
rgci
rgfae
rgfte
rgn
@ -1604,6 +1597,7 @@ SELECTALL
SELECTEDFONT
SELECTSTRING
Selfhosters
Serbo
SERVERDLL
SETACTIVE
SETBUDDYINT
@ -1832,8 +1826,6 @@ TOPDOWNDIB
TOpt
tosign
touchpad
Tpp
Tpqrst
tracelogging
traceviewpp
trackbar
@ -1958,7 +1950,6 @@ VPACKMANIFESTDIRECTORY
VPR
VREDRAW
vsc
vsconfig
vscprintf
VSCROLL
vsdevshell
@ -2000,7 +1991,6 @@ wcswidth
wddm
wddmcon
WDDMCONSOLECONTEXT
WDK
wdm
webpage
websites
@ -2074,7 +2064,6 @@ winuserp
WINVER
wistd
wmain
wmemory
WMSZ
wnd
WNDALLOC
@ -2173,6 +2162,7 @@ yact
YCast
YCENTER
YCount
yizz
YLimit
YPan
YSubstantial
@ -2186,3 +2176,4 @@ ZCtrl
ZWJs
ZYXWVU
ZYXWVUTd
zzf

Просмотреть файл

@ -185,8 +185,8 @@ void VtInputThread::_InputThread()
return S_OK;
}
void VtInputThread::WaitUntilDSR(DWORD timeout) const noexcept
til::enumset<DeviceAttribute, uint64_t> VtInputThread::WaitUntilDA1(DWORD timeout) const noexcept
{
const auto& engine = static_cast<InputStateMachineEngine&>(_pInputStateMachine->Engine());
engine.WaitUntilDSR(timeout);
return engine.WaitUntilDA1(timeout);
}

Просмотреть файл

@ -18,13 +18,18 @@ Author(s):
namespace Microsoft::Console
{
namespace VirtualTerminal
{
enum class DeviceAttribute : uint64_t;
}
class VtInputThread
{
public:
VtInputThread(_In_ wil::unique_hfile hPipe, const bool inheritCursor);
[[nodiscard]] HRESULT Start();
void WaitUntilDSR(DWORD timeout) const noexcept;
til::enumset<VirtualTerminal::DeviceAttribute, uint64_t> WaitUntilDA1(DWORD timeout) const noexcept;
private:
static DWORD WINAPI StaticVtInputThreadProc(_In_ LPVOID lpParameter);

Просмотреть файл

@ -163,38 +163,37 @@ bool VtIo::IsUsingVt() const
{
Writer writer{ this };
// GH#4999 - Send a sequence to the connected terminal to request
// win32-input-mode from them. This will enable the connected terminal to
// send us full INPUT_RECORDs as input. If the terminal doesn't understand
// this sequence, it'll just ignore it.
writer.WriteUTF8(
"\x1b[?1004h" // Focus Event Mode
"\x1b[?9001h" // Win32 Input Mode
);
// MSFT: 15813316
// If the terminal application wants us to inherit the cursor position,
// we're going to emit a VT sequence to ask for the cursor position, then
// wait 1s until we get a response.
// we're going to emit a VT sequence to ask for the cursor position.
// If we get a response, the InteractDispatch will call SetCursorPosition,
// which will call to our VtIo::SetCursorPosition method.
// which will call to our VtIo::SetCursorPosition method.
//
// By sending the request before sending the DA1 one, we can simply
// wait for the DA1 response below and effectively wait for both.
if (_lookingForCursorPosition)
{
writer.WriteUTF8("\x1b[6n"); // Cursor Position Report (DSR CPR)
}
// GH#4999 - Send a sequence to the connected terminal to request
// win32-input-mode from them. This will enable the connected terminal to
// send us full INPUT_RECORDs as input. If the terminal doesn't understand
// this sequence, it'll just ignore it.
writer.WriteUTF8(
"\x1b[c" // DA1 Report (Primary Device Attributes)
"\x1b[?1004h" // Focus Event Mode
"\x1b[?9001h" // Win32 Input Mode
);
writer.Submit();
}
if (_lookingForCursorPosition)
{
_lookingForCursorPosition = false;
// Allow the input thread to momentarily gain the console lock.
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto suspension = gci.SuspendLock();
_pVtInputThread->WaitUntilDSR(3000);
_deviceAttributes = _pVtInputThread->WaitUntilDA1(3000);
}
if (_pPtySignalInputThread)
@ -211,6 +210,16 @@ bool VtIo::IsUsingVt() const
return S_OK;
}
void VtIo::SetDeviceAttributes(const til::enumset<DeviceAttribute, uint64_t> attributes) noexcept
{
_deviceAttributes = attributes;
}
til::enumset<DeviceAttribute, uint64_t> VtIo::GetDeviceAttributes() const noexcept
{
return _deviceAttributes;
}
// Method Description:
// - Create our pseudo window. This is exclusively called by
// ConsoleInputThreadProcWin32 on the console input thread.
@ -359,6 +368,40 @@ void VtIo::FormatAttributes(std::wstring& target, const TextAttribute& attribute
target.append(bufW, len);
}
wchar_t VtIo::SanitizeUCS2(wchar_t ch)
{
// If any of the values in the buffer are C0 or C1 controls, we need to
// convert them to printable codepoints, otherwise they'll end up being
// evaluated as control characters by the receiving terminal. We use the
// DOS 437 code page for the C0 controls and DEL, and just a `?` for the
// C1 controls, since that's what you would most likely have seen in the
// legacy v1 console with raster fonts.
if (ch < 0x20)
{
static constexpr wchar_t lut[] = {
// clang-format off
L' ', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'',
L'', L'', L'', L'', L'', L'§', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'',
// clang-format on
};
ch = lut[ch];
}
else if (ch == 0x7F)
{
ch = L'';
}
else if (ch > 0x7F && ch < 0xA0)
{
ch = L'?';
}
else if (til::is_surrogate(ch))
{
ch = UNICODE_REPLACEMENT;
}
return ch;
}
VtIo::Writer::Writer(VtIo* io) noexcept :
_io{ io }
{
@ -592,7 +635,7 @@ void VtIo::Writer::WriteUTF16StripControlChars(std::wstring_view str) const
for (it = begControlChars; it != end && IsControlCharacter(*it); ++it)
{
WriteUCS2StripControlChars(*it);
WriteUCS2(SanitizeUCS2(*it));
}
}
}
@ -626,36 +669,6 @@ void VtIo::Writer::WriteUCS2(wchar_t ch) const
_io->_back.append(buf, len);
}
void VtIo::Writer::WriteUCS2StripControlChars(wchar_t ch) const
{
// If any of the values in the buffer are C0 or C1 controls, we need to
// convert them to printable codepoints, otherwise they'll end up being
// evaluated as control characters by the receiving terminal. We use the
// DOS 437 code page for the C0 controls and DEL, and just a `?` for the
// C1 controls, since that's what you would most likely have seen in the
// legacy v1 console with raster fonts.
if (ch < 0x20)
{
static constexpr wchar_t lut[] = {
// clang-format off
L' ', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'',
L'', L'', L'', L'', L'', L'§', L'', L'', L'', L'', L'', L'', L'', L'', L'', L'',
// clang-format on
};
ch = lut[ch];
}
else if (ch == 0x7F)
{
ch = L'';
}
else if (ch > 0x7F && ch < 0xA0)
{
ch = L'?';
}
WriteUCS2(ch);
}
// CUP: Cursor Position
void VtIo::Writer::WriteCUP(til::point position) const
{
@ -773,7 +786,7 @@ void VtIo::Writer::WriteInfos(til::point target, std::span<const CHAR_INFO> info
do
{
WriteUCS2StripControlChars(ch);
WriteUCS2(SanitizeUCS2(ch));
} while (--repeat);
}
}

Просмотреть файл

@ -35,7 +35,6 @@ namespace Microsoft::Console::VirtualTerminal
void WriteUTF16TranslateCRLF(std::wstring_view str) const;
void WriteUTF16StripControlChars(std::wstring_view str) const;
void WriteUCS2(wchar_t ch) const;
void WriteUCS2StripControlChars(wchar_t ch) const;
void WriteCUP(til::point position) const;
void WriteDECTCEM(bool enabled) const;
void WriteSGR1006(bool enabled) const;
@ -54,6 +53,7 @@ namespace Microsoft::Console::VirtualTerminal
static void FormatAttributes(std::string& target, const TextAttribute& attributes);
static void FormatAttributes(std::wstring& target, const TextAttribute& attributes);
static wchar_t SanitizeUCS2(wchar_t ch);
[[nodiscard]] HRESULT Initialize(const ConsoleArguments* const pArgs);
[[nodiscard]] HRESULT CreateAndStartSignalThread() noexcept;
@ -62,6 +62,8 @@ namespace Microsoft::Console::VirtualTerminal
bool IsUsingVt() const;
[[nodiscard]] HRESULT StartIfNeeded();
void SetDeviceAttributes(til::enumset<DeviceAttribute, uint64_t> attributes) noexcept;
til::enumset<DeviceAttribute, uint64_t> GetDeviceAttributes() const noexcept;
void SendCloseEvent();
void CreatePseudoWindow();
@ -79,6 +81,7 @@ namespace Microsoft::Console::VirtualTerminal
std::unique_ptr<Microsoft::Console::VtInputThread> _pVtInputThread;
std::unique_ptr<Microsoft::Console::PtySignalInputThread> _pPtySignalInputThread;
til::enumset<DeviceAttribute, uint64_t> _deviceAttributes;
// We use two buffers: A front and a back buffer. The front buffer is the one we're currently
// sending to the terminal (it's being "presented" = it's on the "front" & "visible").

Просмотреть файл

@ -13,6 +13,7 @@
#include "_stream.h"
#include "../interactivity/inc/ServiceLocator.hpp"
#include "../terminal/parser/InputStateMachineEngine.hpp"
#include "../types/inc/convert.hpp"
#include "../types/inc/viewport.hpp"
@ -993,29 +994,36 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
{
try
{
// Just in case if the client application didn't check if this request is useless.
if (source.left == target.x && source.top == target.y)
{
return S_OK;
}
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
if (auto writer = gci.GetVtWriterForBuffer(&context))
auto& buffer = context.GetActiveBuffer();
const auto bufferSize = buffer.GetBufferSize();
auto writer = gci.GetVtWriterForBuffer(&context);
// Applications like to pass 0/0 for the fill char/attribute.
// What they want is the whitespace and current attributes.
if (fillCharacter == UNICODE_NULL && fillAttribute == 0)
{
auto& buffer = context.GetActiveBuffer();
fillAttribute = buffer.GetAttributes().GetLegacyAttributes();
}
// However, if the character is null and we were given a null attribute (represented as legacy 0),
// then we'll just fill with spaces and whatever the buffer's default colors are.
if (fillCharacter == UNICODE_NULL && fillAttribute == 0)
{
fillCharacter = UNICODE_SPACE;
fillAttribute = buffer.GetAttributes().GetLegacyAttributes();
}
// Avoid writing control characters into the buffer.
// A null character will get translated to whitespace.
fillCharacter = Microsoft::Console::VirtualTerminal::VtIo::SanitizeUCS2(fillCharacter);
if (writer)
{
// GH#3126 - This is a shim for cmd's `cls` function. In the
// legacy console, `cls` is supposed to clear the entire buffer. In
// conpty however, there's no difference between the viewport and the
// entirety of the buffer. We're going to see if this API call exactly
// matched the way we expect cmd to call it. If it does, then
// let's manually emit a Full Reset (RIS).
const auto bufferSize = buffer.GetBufferSize();
// legacy console, `cls` is supposed to clear the entire buffer.
// We always use a VT sequence, even if ConPTY isn't used, because those are faster nowadays.
if (enableCmdShim &&
source.left <= 0 && source.top <= 0 &&
source.right >= bufferSize.RightInclusive() && source.bottom >= bufferSize.BottomInclusive() &&
@ -1028,36 +1036,96 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
return S_OK;
}
const auto clipViewport = clip ? Viewport::FromInclusive(*clip) : bufferSize;
const auto clipViewport = clip ? Viewport::FromInclusive(*clip).Clamp(bufferSize) : bufferSize;
const auto sourceViewport = Viewport::FromInclusive(source);
Viewport readViewport;
Viewport writtenViewport;
const auto w = std::max(0, sourceViewport.Width());
const auto h = std::max(0, sourceViewport.Height());
const auto a = static_cast<size_t>(w * h);
if (a == 0)
{
return S_OK;
}
til::small_vector<CHAR_INFO, 1024> backup;
til::small_vector<CHAR_INFO, 1024> fill;
backup.resize(a, CHAR_INFO{ fillCharacter, fillAttribute });
fill.resize(a, CHAR_INFO{ fillCharacter, fillAttribute });
const auto fillViewport = sourceViewport.Clamp(clipViewport);
writer.BackupCursor();
RETURN_IF_FAILED(ReadConsoleOutputWImplHelper(context, backup, sourceViewport, readViewport));
RETURN_IF_FAILED(WriteConsoleOutputWImplHelper(context, fill, w, sourceViewport.Clamp(clipViewport), writtenViewport));
RETURN_IF_FAILED(WriteConsoleOutputWImplHelper(context, backup, w, Viewport::FromDimensions(target, readViewport.Dimensions()).Clamp(clipViewport), writtenViewport));
if (gci.GetVtIo()->GetDeviceAttributes().test(Microsoft::Console::VirtualTerminal::DeviceAttribute::RectangularAreaOperations))
{
// This calculates just the positive offsets caused by out-of-bounds (OOB) source and target coordinates.
//
// If the source rectangle is OOB to the bottom-right, then the size of the rectangle that can
// be copied shrinks, but its origin stays the same. However, if the rectangle is OOB to the
// top-left then the origin of the to-be-copied rectangle will be offset by an inverse amount.
// Similarly, if the *target* rectangle is OOB to the bottom-right, its size shrinks while
// the origin stays the same, and if it's OOB to the top-left, then the origin is offset.
//
// In other words, this calculates the total offset that needs to be applied to the to-be-copied rectangle.
// Later down below we'll then clamp that rectangle which will cause its size to shrink as needed.
const til::point offset{
std::max(0, -source.left) + std::max(0, clipViewport.Left() - target.x),
std::max(0, -source.top) + std::max(0, clipViewport.Top() - target.y),
};
const auto copyTargetViewport = Viewport::FromDimensions(target + offset, sourceViewport.Dimensions()).Clamp(clipViewport);
const auto copySourceViewport = Viewport::FromDimensions(sourceViewport.Origin() + offset, copyTargetViewport.Dimensions()).Clamp(bufferSize);
const auto fills = Viewport::Subtract(fillViewport, copyTargetViewport);
std::wstring buf;
if (!fills.empty())
{
Microsoft::Console::VirtualTerminal::VtIo::FormatAttributes(buf, TextAttribute{ fillAttribute });
}
if (copySourceViewport.IsValid() && copyTargetViewport.IsValid())
{
// DECCRA: Copy Rectangular Area
fmt::format_to(
std::back_inserter(buf),
FMT_COMPILE(L"\x1b[{};{};{};{};;{};{}$v"),
copySourceViewport.Top() + 1,
copySourceViewport.Left() + 1,
copySourceViewport.BottomExclusive(),
copySourceViewport.RightExclusive(),
copyTargetViewport.Top() + 1,
copyTargetViewport.Left() + 1);
}
for (const auto& fill : fills)
{
// DECFRA: Fill Rectangular Area
fmt::format_to(
std::back_inserter(buf),
FMT_COMPILE(L"\x1b[{};{};{};{};{}$x"),
static_cast<int>(fillCharacter),
fill.Top() + 1,
fill.Left() + 1,
fill.BottomExclusive(),
fill.RightExclusive());
}
WriteCharsVT(context, buf);
}
else
{
const auto w = std::max(0, sourceViewport.Width());
const auto h = std::max(0, sourceViewport.Height());
const auto a = static_cast<size_t>(w * h);
if (a == 0)
{
return S_OK;
}
til::small_vector<CHAR_INFO, 1024> backup;
til::small_vector<CHAR_INFO, 1024> fill;
backup.resize(a, CHAR_INFO{ fillCharacter, fillAttribute });
fill.resize(a, CHAR_INFO{ fillCharacter, fillAttribute });
Viewport readViewport;
Viewport writtenViewport;
RETURN_IF_FAILED(ReadConsoleOutputWImplHelper(context, backup, sourceViewport, readViewport));
RETURN_IF_FAILED(WriteConsoleOutputWImplHelper(context, fill, w, fillViewport, writtenViewport));
RETURN_IF_FAILED(WriteConsoleOutputWImplHelper(context, backup, w, Viewport::FromDimensions(target, readViewport.Dimensions()).Clamp(clipViewport), writtenViewport));
}
writer.Submit();
}
else
{
auto& buffer = context.GetActiveBuffer();
TextAttribute useThisAttr(fillAttribute);
ScrollRegion(buffer, source, clip, target, fillCharacter, useThisAttr);
}

Просмотреть файл

@ -4,6 +4,7 @@
#include "precomp.h"
#include "CommonState.hpp"
#include "../../terminal/parser/InputStateMachineEngine.hpp"
using namespace WEX::Common;
using namespace WEX::Logging;
@ -29,6 +30,8 @@ constexpr CHAR_INFO ci_blu(wchar_t ch) noexcept
}
#define cup(y, x) "\x1b[" #y ";" #x "H" // CUP: Cursor Position
#define deccra(t, l, b, r, y, x) "\x1b[" #t ";" #l ";" #b ";" #r ";;" #y ";" #x "$v" // DECCRA: Copy Rectangular Area
#define decfra(ch, t, l, b, r) "\x1b[" #ch ";" #t ";" #l ";" #b ";" #r "$x"
#define decawm(h) "\x1b[?7" #h // DECAWM: Autowrap Mode
#define decsc() "\x1b\x37" // DECSC: DEC Save Cursor (+ attributes)
#define decrc() "\x1b\x38" // DECRC: DEC Restore Cursor (+ attributes)
@ -554,12 +557,224 @@ class ::Microsoft::Console::VirtualTerminal::VtIoTests
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
// Copying from a partially out-of-bounds source to a partially out-of-bounds target,
// while source and target overlap and there's a partially out-of-bounds clip rect.
//
// Before:
// clip rect
// +~~~~~~~~~~~~~~~~~~~~~+
// +--------------$--------+ $
// | A Z Z$ b C | D c Y $
// | $+-------+------------$--+
// | E z z$| f G | H g Y $ |
// | src $| | $ |
// | i z z$| J d | B E L $ |
// | $| | dst $ |
// | m n M$| N h | F i P $ |
// +--------------$+-------+ $ |
// +~e~~~~~~~~~~~~~~~~~~~+ |
// +-----------------------+
//
// After:
//
// +-----------------------+
// | A Z Z y y | D c Y
// | +-------+---------------+
// | E z z | y A | Z Z b |
// | | | |
// | i z z | y E | z z f |
// | | | |
// | m n M | y i | z z J |
// +---------------+-------+ |
// | |
// +-----------------------+
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { -1, 0, 4, 3 }, { 3, 1 }, til::inclusive_rect{ 3, -1, 7, 9 }, L'y', blu, false));
expected =
decsc() //
cup(1, 4) sgr_blu("yy") //
cup(2, 4) sgr_blu("yy") //
cup(3, 4) sgr_blu("yy") //
cup(4, 4) sgr_blu("yy") //
cup(2, 4) sgr_blu("y") sgr_red("AZZ") sgr_blu("b") //
cup(3, 4) sgr_blu("y") sgr_red("E") sgr_blu("zzf") //
cup(4, 4) sgr_blu("yizz") sgr_red("J") //
decrc();
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
static constexpr std::array<CHAR_INFO, 8 * 4> expectedContents{ {
// clang-format off
ci_red('A'), ci_red('Z'), ci_red('Z'), ci_blu('b'), ci_red('C'), ci_red('D'), ci_blu('c'), ci_red('Y'),
ci_red('E'), ci_blu('z'), ci_blu('z'), ci_blu('f'), ci_red('G'), ci_red('H'), ci_blu('g'), ci_red('Y'),
ci_blu('i'), ci_blu('z'), ci_blu('z'), ci_red('J'), ci_blu('d'), ci_red('B'), ci_red('E'), ci_red('L'),
ci_blu('m'), ci_blu('n'), ci_red('M'), ci_red('N'), ci_blu('h'), ci_red('F'), ci_blu('i'), ci_red('P'),
ci_red('A'), ci_red('Z'), ci_red('Z'), ci_blu('y'), ci_blu('y'), ci_red('D'), ci_blu('c'), ci_red('Y'),
ci_red('E'), ci_blu('z'), ci_blu('z'), ci_blu('y'), ci_red('A'), ci_red('Z'), ci_red('Z'), ci_blu('b'),
ci_blu('i'), ci_blu('z'), ci_blu('z'), ci_blu('y'), ci_red('E'), ci_blu('z'), ci_blu('z'), ci_blu('f'),
ci_blu('m'), ci_blu('n'), ci_red('M'), ci_blu('y'), ci_blu('i'), ci_blu('z'), ci_blu('z'), ci_red('J'),
// clang-format on
} };
std::array<CHAR_INFO, 8 * 4> actualContents{};
Viewport actualContentsRead;
THROW_IF_FAILED(routines.ReadConsoleOutputWImpl(*screenInfo, actualContents, Viewport::FromDimensions({}, { 8, 4 }), actualContentsRead));
VERIFY_IS_TRUE(memcmp(expectedContents.data(), actualContents.data(), sizeof(actualContents)) == 0);
}
TEST_METHOD(ScrollConsoleScreenBufferW_DECCRA)
{
ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo()->SetDeviceAttributes({ DeviceAttribute::RectangularAreaOperations });
const auto cleanup = wil::scope_exit([]() {
ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo()->SetDeviceAttributes({});
});
std::string_view expected;
std::string_view actual;
setupInitialContents();
// Scrolling from nowhere to somewhere are no-ops and should not emit anything.
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { 0, 0, -1, -1 }, {}, std::nullopt, L' ', 0, false));
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { -10, -10, -9, -9 }, {}, std::nullopt, L' ', 0, false));
expected = "";
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
// Scrolling from somewhere to nowhere should clear the area.
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { 0, 0, 1, 1 }, { 10, 10 }, std::nullopt, L' ', red, false));
expected =
decsc() //
sgr_red() //
decfra(32, 1, 1, 2, 2) // ' ' = 32
decrc();
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
// cmd uses ScrollConsoleScreenBuffer to clear the buffer contents and that gets translated to a clear screen sequence.
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { 0, 0, 7, 3 }, { 0, -4 }, std::nullopt, 0, 0, true));
expected = "\x1b[H\x1b[2J\x1b[3J";
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
//
// A B a b C D c d
//
// E F e f G H g h
//
// i j I J k l K L
//
// m n M N o p O P
//
setupInitialContents();
// Scrolling from somewhere to somewhere.
//
// +-------+
// A | Z Z | b C D c d
// | src |
// E | Z Z | f G H g h
// +-------+ +-------+
// i j I J k | B a | L
// | dst |
// m n M N o | F e | P
// +-------+
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { 1, 0, 2, 1 }, { 5, 2 }, std::nullopt, L'Z', red, false));
expected =
decsc() //
sgr_red() //
deccra(1, 2, 2, 3, 3, 6) //
decfra(90, 1, 2, 2, 3) // 'Z' = 90
decrc();
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
// Same, but with a partially out-of-bounds target and clip rect. Clip rects affect both
// the source area that gets filled and the target area that gets a copy of the source contents.
//
// A Z Z b C D c d
// +---+~~~~~~~~~~~~~~~~~~~~~~~+
// | E $ z z | f G H g $ h
// | $ src | +---$-------+
// | i $ z z | J k B | E $ L |
// +---$-------+ | $ dst |
// m $ n M N o F | i $ P |
// +~~~~~~~~~~~~~~~~~~~~~~~+-------+
// clip rect
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { 0, 1, 2, 2 }, { 6, 2 }, til::inclusive_rect{ 1, 1, 6, 3 }, L'z', blu, false));
expected =
decsc() //
sgr_blu() //
deccra(2, 1, 3, 1, 3, 7) //
decfra(122, 2, 2, 3, 3) // 'z' = 122
decrc();
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
// Same, but with a partially out-of-bounds source.
// The boundaries of the buffer act as a clip rect for reading and so only 2 cells get copied.
//
// +-------+
// A Z Z b C D c | Y |
// | src |
// E z z f G H g | Y |
// +---+ +-------+
// i z z J | d | B E L
// |dst|
// m n M N | h | F i P
// +---+
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { 7, 0, 8, 1 }, { 4, 2 }, std::nullopt, L'Y', red, false));
expected =
decsc() //
sgr_red() //
deccra(1, 8, 2, 8, 3, 5) //
decfra(89, 1, 8, 2, 8) // 'Y' = 89
decrc();
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
// Copying from a partially out-of-bounds source to a partially out-of-bounds target,
// while source and target overlap and there's a partially out-of-bounds clip rect.
//
// Before:
// clip rect
// +~~~~~~~~~~~~~~~~~~~~~+
// +--------------$--------+ $
// | A Z Z$ b C | D c Y $
// | $+-------+------------$--+
// | E z z$| f G | H g Y $ |
// | src $| | $ |
// | i z z$| J d | B E L $ |
// | $| | dst $ |
// | m n M$| N h | F i P $ |
// +--------------$+-------+ $ |
// +~e~~~~~~~~~~~~~~~~~~~+ |
// +-----------------------+
//
// After:
//
// +-----------------------+
// | A Z Z y y | D c Y
// | +-------+---------------+
// | E z z | y A | Z Z b |
// | | | |
// | i z z | y E | z z f |
// | | | |
// | m n M | y i | z z J |
// +---------------+-------+ |
// | |
// +-----------------------+
THROW_IF_FAILED(routines.ScrollConsoleScreenBufferWImpl(*screenInfo, { -1, 0, 4, 3 }, { 3, 1 }, til::inclusive_rect{ 3, -1, 7, 9 }, L'y', blu, false));
expected =
decsc() //
sgr_blu() //
deccra(1, 1, 3, 4, 2, 5) //
decfra(121, 1, 4, 1, 5) // 'y' = 121
decfra(121, 2, 4, 4, 4) //
decrc();
actual = readOutput();
VERIFY_ARE_EQUAL(expected, actual);
static constexpr std::array<CHAR_INFO, 8 * 4> expectedContents{ {
// clang-format off
ci_red('A'), ci_red('Z'), ci_red('Z'), ci_blu('y'), ci_blu('y'), ci_red('D'), ci_blu('c'), ci_red('Y'),
ci_red('E'), ci_blu('z'), ci_blu('z'), ci_blu('y'), ci_red('A'), ci_red('Z'), ci_red('Z'), ci_blu('b'),
ci_blu('i'), ci_blu('z'), ci_blu('z'), ci_blu('y'), ci_red('E'), ci_blu('z'), ci_blu('z'), ci_blu('f'),
ci_blu('m'), ci_blu('n'), ci_red('M'), ci_blu('y'), ci_blu('i'), ci_blu('z'), ci_blu('z'), ci_red('J'),
// clang-format on
} };
std::array<CHAR_INFO, 8 * 4> actualContents{};

Просмотреть файл

@ -24,6 +24,13 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
static_assert(std::is_unsigned_v<UnderlyingType>);
public:
static constexpr enumset from_bits(UnderlyingType data) noexcept
{
enumset result;
result._data = data;
return result;
}
// Method Description:
// - Constructs a new bitset with the given list of positions set to true.
TIL_ENUMSET_VARARG

Просмотреть файл

@ -89,11 +89,6 @@ static bool operator==(const Ss3ToVkey& pair, const Ss3ActionCodes code) noexcep
return pair.action == code;
}
InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr<IInteractDispatch> pDispatch) :
InputStateMachineEngine(std::move(pDispatch), false)
{
}
InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr<IInteractDispatch> pDispatch, const bool lookingForDSR) :
_pDispatch(std::move(pDispatch)),
_lookingForDSR(lookingForDSR),
@ -102,14 +97,28 @@ InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr<IInteractDispat
THROW_HR_IF_NULL(E_INVALIDARG, _pDispatch.get());
}
void InputStateMachineEngine::WaitUntilDSR(DWORD timeout) const noexcept
til::enumset<DeviceAttribute, uint64_t> InputStateMachineEngine::WaitUntilDA1(DWORD timeout) const noexcept
{
uint64_t val = 0;
// atomic_wait() returns false when the timeout expires.
// Technically we should decrement the timeout with each iteration,
// but I suspect infinite spurious wake-ups are a theoretical problem.
while (_lookingForDSR.load(std::memory_order::relaxed) && til::atomic_wait(_lookingForDSR, true, timeout))
for (;;)
{
val = _deviceAttributes.load(std::memory_order::relaxed);
if (val)
{
break;
}
if (!til::atomic_wait(_deviceAttributes, val, timeout))
{
break;
}
}
return til::enumset<DeviceAttribute, uint64_t>::from_bits(val);
}
bool InputStateMachineEngine::EncounteredWin32InputModeSequence() const noexcept
@ -411,13 +420,12 @@ bool InputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParameter
// The F3 case is special - it shares a code with the DeviceStatusResponse.
// If we're looking for that response, then do that, and break out.
// Else, fall though to the _GetCursorKeysModifierState handler.
if (_lookingForDSR.load(std::memory_order::relaxed))
if (_lookingForDSR)
{
_pDispatch->MoveCursor(parameters.at(0), parameters.at(1));
// Right now we're only looking for on initial cursor
// position response. After that, only look for F3.
_lookingForDSR.store(false, std::memory_order::relaxed);
til::atomic_notify_all(_lookingForDSR);
_lookingForDSR = false;
return true;
}
// Heuristic: If the hosting terminal used the win32 input mode, chances are high
@ -464,6 +472,32 @@ bool InputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParameter
case CsiActionCodes::FocusOut:
_pDispatch->FocusChanged(false);
return true;
case CsiActionCodes::DA_DeviceAttributes:
// This assumes that InputStateMachineEngine is tightly coupled with VtInputThread and the rest of the ConPTY system (VtIo).
// On startup, ConPTY will send a DA1 request to get more information about the hosting terminal.
// We catch it here and store the information for later retrieval.
if (_deviceAttributes.load(std::memory_order_relaxed) == 0)
{
til::enumset<DeviceAttribute, uint64_t> attributes;
// The first parameter denotes the conformance level.
if (parameters.at(0).value() >= 61)
{
parameters.subspan(1).for_each([&](auto p) {
attributes.set(static_cast<DeviceAttribute>(p));
return true;
});
}
_deviceAttributes.fetch_or(attributes.bits(), std::memory_order_relaxed);
til::atomic_notify_all(_deviceAttributes);
// VtIo first sends a DSR CPR and then a DA1 request.
// If we encountered a DA1 response here, the DSR request is definitely done now.
_lookingForDSR = false;
return true;
}
return false;
case CsiActionCodes::Win32KeyboardInput:
{
// Use WriteCtrlKey here, even for keys that _aren't_ control keys,

Просмотреть файл

@ -49,6 +49,32 @@ namespace Microsoft::Console::VirtualTerminal
// CAPSLOCK_ON 0x0080
// ENHANCED_KEY 0x0100
enum class DeviceAttribute : uint64_t
{
Columns132 = 1,
PrinterPort = 2,
Sixel = 4,
SelectiveErase = 6,
SoftCharacterSet = 7,
UserDefinedKeys = 8,
NationalReplacementCharacterSets = 9,
SerboCroatianCharacterSet = 12,
EightBitInterfaceArchitecture = 14,
TechnicalCharacterSet = 15,
WindowingCapability = 18,
Sessions = 19,
HorizontalScrolling = 21,
Color = 22,
GreekCharacterSet = 23,
TurkishCharacterSet = 24,
RectangularAreaOperations = 28,
TextMacros = 32,
Latin2CharacterSet = 42,
PCTerm = 44,
SoftKeyMapping = 45,
AsciiTerminalEmulation = 46,
};
enum CsiActionCodes : uint64_t
{
ArrowUp = VTID("A"),
@ -67,6 +93,7 @@ namespace Microsoft::Console::VirtualTerminal
CSI_F3 = VTID("R"), // Both F3 and DSR are on R.
// DSR_DeviceStatusReportResponse = VTID("R"),
CSI_F4 = VTID("S"),
DA_DeviceAttributes = VTID("?c"),
DTTERM_WindowManipulation = VTID("t"),
CursorBackTab = VTID("Z"),
Win32KeyboardInput = VTID("_")
@ -128,11 +155,9 @@ namespace Microsoft::Console::VirtualTerminal
class InputStateMachineEngine : public IStateMachineEngine
{
public:
InputStateMachineEngine(std::unique_ptr<IInteractDispatch> pDispatch);
InputStateMachineEngine(std::unique_ptr<IInteractDispatch> pDispatch,
const bool lookingForDSR);
InputStateMachineEngine(std::unique_ptr<IInteractDispatch> pDispatch, const bool lookingForDSR = false);
void WaitUntilDSR(DWORD timeout) const noexcept;
til::enumset<DeviceAttribute, uint64_t> WaitUntilDA1(DWORD timeout) const noexcept;
bool EncounteredWin32InputModeSequence() const noexcept override;
@ -159,7 +184,8 @@ namespace Microsoft::Console::VirtualTerminal
private:
const std::unique_ptr<IInteractDispatch> _pDispatch;
std::atomic<bool> _lookingForDSR{ false };
std::atomic<uint64_t> _deviceAttributes{ 0 };
bool _lookingForDSR = false;
bool _encounteredWin32InputModeSequence = false;
DWORD _mouseButtonState = 0;
std::chrono::milliseconds _doubleClickTime;

Просмотреть файл

@ -603,7 +603,11 @@ try
const auto intersection = Viewport::Intersect(original, removeMe);
// If there's no intersection, there's nothing to remove.
if (!intersection.IsValid())
if (!original.IsValid())
{
// Nothing to do here.
}
else if (!intersection.IsValid())
{
// Just put the original rectangle into the results and return early.
result.push_back(original);

Просмотреть файл

@ -38,6 +38,9 @@ static Pipes createPipes()
VERIFY_IS_TRUE(SetHandleInformation(p.our.in.get(), HANDLE_FLAG_INHERIT, 0));
VERIFY_IS_TRUE(SetHandleInformation(p.our.out.get(), HANDLE_FLAG_INHERIT, 0));
// ConPTY requests a DA1 report on startup. Emulate the response from the terminal.
WriteFile(p.our.in.get(), "\x1b[?61c", 6, nullptr, nullptr);
return p;
}
@ -189,25 +192,12 @@ void ConPtyTests::CreateConPtyBadSize()
void ConPtyTests::GoodCreate()
{
PseudoConsole pcon{};
wil::unique_handle outPipeOurSide;
wil::unique_handle inPipeOurSide;
wil::unique_handle outPipePseudoConsoleSide;
wil::unique_handle inPipePseudoConsoleSide;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = nullptr;
VERIFY_IS_TRUE(CreatePipe(inPipePseudoConsoleSide.addressof(), inPipeOurSide.addressof(), &sa, 0));
VERIFY_IS_TRUE(CreatePipe(outPipeOurSide.addressof(), outPipePseudoConsoleSide.addressof(), &sa, 0));
VERIFY_IS_TRUE(SetHandleInformation(inPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0));
VERIFY_IS_TRUE(SetHandleInformation(outPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0));
auto pipes = createPipes();
VERIFY_SUCCEEDED(
_CreatePseudoConsole(defaultSize,
inPipePseudoConsoleSide.get(),
outPipePseudoConsoleSide.get(),
pipes.conpty.in.get(),
pipes.conpty.out.get(),
0,
&pcon));
@ -220,24 +210,12 @@ void ConPtyTests::GoodCreateMultiple()
{
PseudoConsole pcon1{};
PseudoConsole pcon2{};
wil::unique_handle outPipeOurSide;
wil::unique_handle inPipeOurSide;
wil::unique_handle outPipePseudoConsoleSide;
wil::unique_handle inPipePseudoConsoleSide;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = nullptr;
VERIFY_IS_TRUE(CreatePipe(inPipePseudoConsoleSide.addressof(), inPipeOurSide.addressof(), &sa, 0));
VERIFY_IS_TRUE(CreatePipe(outPipeOurSide.addressof(), outPipePseudoConsoleSide.addressof(), &sa, 0));
VERIFY_IS_TRUE(SetHandleInformation(inPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0));
VERIFY_IS_TRUE(SetHandleInformation(outPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0));
auto pipes = createPipes();
VERIFY_SUCCEEDED(
_CreatePseudoConsole(defaultSize,
inPipePseudoConsoleSide.get(),
outPipePseudoConsoleSide.get(),
pipes.conpty.in.get(),
pipes.conpty.out.get(),
0,
&pcon1));
auto closePty1 = wil::scope_exit([&] {
@ -246,8 +224,8 @@ void ConPtyTests::GoodCreateMultiple()
VERIFY_SUCCEEDED(
_CreatePseudoConsole(defaultSize,
inPipePseudoConsoleSide.get(),
outPipePseudoConsoleSide.get(),
pipes.conpty.in.get(),
pipes.conpty.out.get(),
0,
&pcon2));
auto closePty2 = wil::scope_exit([&] {
@ -258,23 +236,12 @@ void ConPtyTests::GoodCreateMultiple()
void ConPtyTests::SurvivesOnBreakOutput()
{
PseudoConsole pty = { 0 };
wil::unique_handle outPipeOurSide;
wil::unique_handle inPipeOurSide;
wil::unique_handle outPipePseudoConsoleSide;
wil::unique_handle inPipePseudoConsoleSide;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = nullptr;
VERIFY_IS_TRUE(CreatePipe(inPipePseudoConsoleSide.addressof(), inPipeOurSide.addressof(), &sa, 0));
VERIFY_IS_TRUE(CreatePipe(outPipeOurSide.addressof(), outPipePseudoConsoleSide.addressof(), &sa, 0));
VERIFY_IS_TRUE(SetHandleInformation(inPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0));
VERIFY_IS_TRUE(SetHandleInformation(outPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0));
auto pipes = createPipes();
VERIFY_SUCCEEDED(
_CreatePseudoConsole(defaultSize,
inPipePseudoConsoleSide.get(),
outPipePseudoConsoleSide.get(),
pipes.conpty.in.get(),
pipes.conpty.out.get(),
0,
&pty));
auto closePty1 = wil::scope_exit([&] {
@ -292,7 +259,7 @@ void ConPtyTests::SurvivesOnBreakOutput()
VERIFY_IS_TRUE(GetExitCodeProcess(piClient.hProcess, &dwExit));
VERIFY_ARE_EQUAL(dwExit, (DWORD)STILL_ACTIVE);
VERIFY_IS_TRUE(CloseHandle(outPipeOurSide.get()));
pipes.our.out.reset();
// Wait for a couple seconds, make sure the child is still alive.
VERIFY_ARE_EQUAL(WaitForSingleObject(pty.hConPtyProcess, 2000), (DWORD)WAIT_TIMEOUT);
@ -317,23 +284,12 @@ void ConPtyTests::DiesOnClose()
VERIFY_SUCCEEDED(TestData::TryGetValue(L"commandline", testCommandline), L"Get a commandline to test");
PseudoConsole pty = { 0 };
wil::unique_handle outPipeOurSide;
wil::unique_handle inPipeOurSide;
wil::unique_handle outPipePseudoConsoleSide;
wil::unique_handle inPipePseudoConsoleSide;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = nullptr;
VERIFY_IS_TRUE(CreatePipe(inPipePseudoConsoleSide.addressof(), inPipeOurSide.addressof(), &sa, 0));
VERIFY_IS_TRUE(CreatePipe(outPipeOurSide.addressof(), outPipePseudoConsoleSide.addressof(), &sa, 0));
VERIFY_IS_TRUE(SetHandleInformation(inPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0));
VERIFY_IS_TRUE(SetHandleInformation(outPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0));
auto pipes = createPipes();
VERIFY_SUCCEEDED(
_CreatePseudoConsole(defaultSize,
inPipePseudoConsoleSide.get(),
outPipePseudoConsoleSide.get(),
pipes.conpty.in.get(),
pipes.conpty.out.get(),
0,
&pty));
auto closePty1 = wil::scope_exit([&] {

Просмотреть файл

@ -169,7 +169,7 @@ function Invoke-OpenConsoleTests()
[switch]$FTOnly,
[parameter(Mandatory=$false)]
[ValidateSet('host', 'interactivityWin32', 'terminal', 'adapter', 'feature', 'uia', 'textbuffer', 'til', 'types', 'terminalCore', 'terminalApp', 'localTerminalApp', 'unitSettingsModel', 'unitRemoting', 'unitControl')]
[ValidateSet('host', 'interactivityWin32', 'terminal', 'adapter', 'feature', 'uia', 'textbuffer', 'til', 'types', 'terminalCore', 'terminalApp', 'localTerminalApp', 'unitSettingsModel', 'unitRemoting', 'unitControl', 'winconpty')]
[string]$Test,
[parameter(Mandatory=$false)]

Просмотреть файл

@ -15,4 +15,5 @@
<test name="til" type="unit" binary="til.unit.tests.dll" />
<test name="feature" type="ft" binary="Conhost.Feature.Tests.dll" />
<test name="uia" type="ft" binary="Conhost.UIA.Tests.dll" />
<test name="winconpty" type="ft" binary="winconpty.Feature.Tests.dll" />
</tests>