diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index fc5bb6d9027..1621856787d 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.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 diff --git a/src/host/VtInputThread.cpp b/src/host/VtInputThread.cpp index efd2fd4e51e..3c359a3c8e5 100644 --- a/src/host/VtInputThread.cpp +++ b/src/host/VtInputThread.cpp @@ -185,8 +185,8 @@ void VtInputThread::_InputThread() return S_OK; } -void VtInputThread::WaitUntilDSR(DWORD timeout) const noexcept +til::enumset VtInputThread::WaitUntilDA1(DWORD timeout) const noexcept { const auto& engine = static_cast(_pInputStateMachine->Engine()); - engine.WaitUntilDSR(timeout); + return engine.WaitUntilDA1(timeout); } diff --git a/src/host/VtInputThread.hpp b/src/host/VtInputThread.hpp index e6f15e2ab68..50405b5a255 100644 --- a/src/host/VtInputThread.hpp +++ b/src/host/VtInputThread.hpp @@ -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 WaitUntilDA1(DWORD timeout) const noexcept; private: static DWORD WINAPI StaticVtInputThreadProc(_In_ LPVOID lpParameter); diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index a45ec8260a3..026010b2f70 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -163,38 +163,37 @@ bool VtIo::IsUsingVt() const { Writer writer{ this }; + // 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. + // If we get a response, the InteractDispatch will call SetCursorPosition, + // 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 ); - // 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. - // If we get a response, the InteractDispatch will call SetCursorPosition, - // which will call to our VtIo::SetCursorPosition method. - if (_lookingForCursorPosition) - { - writer.WriteUTF8("\x1b[6n"); // Cursor Position Report (DSR CPR) - } - 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 attributes) noexcept +{ + _deviceAttributes = attributes; +} + +til::enumset 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 info do { - WriteUCS2StripControlChars(ch); + WriteUCS2(SanitizeUCS2(ch)); } while (--repeat); } } diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp index 35a0a13bc27..a5d13764261 100644 --- a/src/host/VtIo.hpp +++ b/src/host/VtIo.hpp @@ -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 attributes) noexcept; + til::enumset GetDeviceAttributes() const noexcept; void SendCloseEvent(); void CreatePseudoWindow(); @@ -79,6 +81,7 @@ namespace Microsoft::Console::VirtualTerminal std::unique_ptr _pVtInputThread; std::unique_ptr _pPtySignalInputThread; + til::enumset _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"). diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 80e3431d215..cb3c80d7bbb 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -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 fillViewport = sourceViewport.Clamp(clipViewport); - const auto w = std::max(0, sourceViewport.Width()); - const auto h = std::max(0, sourceViewport.Height()); - const auto a = static_cast(w * h); - if (a == 0) + writer.BackupCursor(); + + if (gci.GetVtIo()->GetDeviceAttributes().test(Microsoft::Console::VirtualTerminal::DeviceAttribute::RectangularAreaOperations)) { - return S_OK; + // 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(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(w * h); + if (a == 0) + { + return S_OK; + } - til::small_vector backup; - til::small_vector fill; + til::small_vector backup; + til::small_vector fill; - backup.resize(a, CHAR_INFO{ fillCharacter, fillAttribute }); - fill.resize(a, CHAR_INFO{ fillCharacter, fillAttribute }); + backup.resize(a, CHAR_INFO{ fillCharacter, fillAttribute }); + fill.resize(a, CHAR_INFO{ fillCharacter, fillAttribute }); - writer.BackupCursor(); + Viewport readViewport; + Viewport writtenViewport; - 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)); + 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); } diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index f557905bf17..3ad87891a28 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -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 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 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 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 actualContents{}; diff --git a/src/inc/til/enumset.h b/src/inc/til/enumset.h index 5ee47a71675..7e7c4bc0cc1 100644 --- a/src/inc/til/enumset.h +++ b/src/inc/til/enumset.h @@ -24,6 +24,13 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" static_assert(std::is_unsigned_v); 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 diff --git a/src/terminal/parser/InputStateMachineEngine.cpp b/src/terminal/parser/InputStateMachineEngine.cpp index 1111e74d370..70e3991ee49 100644 --- a/src/terminal/parser/InputStateMachineEngine.cpp +++ b/src/terminal/parser/InputStateMachineEngine.cpp @@ -89,11 +89,6 @@ static bool operator==(const Ss3ToVkey& pair, const Ss3ActionCodes code) noexcep return pair.action == code; } -InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr pDispatch) : - InputStateMachineEngine(std::move(pDispatch), false) -{ -} - InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr pDispatch, const bool lookingForDSR) : _pDispatch(std::move(pDispatch)), _lookingForDSR(lookingForDSR), @@ -102,14 +97,28 @@ InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr 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::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 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(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, diff --git a/src/terminal/parser/InputStateMachineEngine.hpp b/src/terminal/parser/InputStateMachineEngine.hpp index b37b67d4b6e..98f031a3bea 100644 --- a/src/terminal/parser/InputStateMachineEngine.hpp +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -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 pDispatch); - InputStateMachineEngine(std::unique_ptr pDispatch, - const bool lookingForDSR); + InputStateMachineEngine(std::unique_ptr pDispatch, const bool lookingForDSR = false); - void WaitUntilDSR(DWORD timeout) const noexcept; + til::enumset WaitUntilDA1(DWORD timeout) const noexcept; bool EncounteredWin32InputModeSequence() const noexcept override; @@ -159,7 +184,8 @@ namespace Microsoft::Console::VirtualTerminal private: const std::unique_ptr _pDispatch; - std::atomic _lookingForDSR{ false }; + std::atomic _deviceAttributes{ 0 }; + bool _lookingForDSR = false; bool _encounteredWin32InputModeSequence = false; DWORD _mouseButtonState = 0; std::chrono::milliseconds _doubleClickTime; diff --git a/src/types/viewport.cpp b/src/types/viewport.cpp index ad276f8ecbe..018212c2165 100644 --- a/src/types/viewport.cpp +++ b/src/types/viewport.cpp @@ -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); diff --git a/src/winconpty/ft_pty/ConPtyTests.cpp b/src/winconpty/ft_pty/ConPtyTests.cpp index 27cfdecf837..5b2fb672082 100644 --- a/src/winconpty/ft_pty/ConPtyTests.cpp +++ b/src/winconpty/ft_pty/ConPtyTests.cpp @@ -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([&] { diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index bbcbc889933..ba87033007b 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -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)] diff --git a/tools/tests.xml b/tools/tests.xml index 4301c09b2a3..9ec3585de6b 100644 --- a/tools/tests.xml +++ b/tools/tests.xml @@ -15,4 +15,5 @@ +