Skip to content

Commit ac5b4f5

Browse files
authored
Use VT for COOKED_READ_DATA (#17445)
By rewriting `COOKED_READ_DATA` to use VT for its output we make it possible to pass this VT output 1:1 straight to the hosting terminal if we're running under ConPTY. This is also possible with the current console APIs it uses, but it's somewhat janky. In particular the usage of `ReadConsoleOutput` to backup/restore the popup contents could be considered bad faith "rules for thee, not for me", given that we're telling people to move away from those APIs. The new implementation contains a bare bones "pager" to fit even very long prompt contents into the VT viewport. I fully expect this initial PR to not be entirely bug free, because writing a proper pager with line wrapping is a little bit complex. This PR takes some significant shortcuts by leveraging the fact that the prompt line is always left-to-right and always a series of fully filled lines followed by one potentially semi-full line. This allows us to skip using a front/back-buffer for diffing the contents between two redisplay calls. Part of #14000 ## Validation Steps Performed * ASCII input * Chinese input (中文維基百科) ✅ * Surrogate pair input (🙂) ✅ * In cmd.exe * Create 2 files: "a😊b.txt" and "a😟b.txt" * Press tab: Autocomplete to "a😊b.txt" ✅ * Navigate the cursor right past the "a" * Press tab twice: Autocomplete to "a😟b.txt" ✅ * Execute `printf(" "); gets(buffer);` in C (or equivalent) * Press Tab, A, Ctrl+V, Tab, A ✅ * The prompt is " A^V A" ✅ * Cursor navigation works ✅ * Backspacing/Deleting random parts of it works ✅ * It never deletes the initial 4 spaces ✅ * Backspace deletes preceding glyphs ✅ * Ctrl+Backspace deletes preceding words ✅ * Escape clears input ✅ * Home navigates to start ✅ * Ctrl+Home deletes text between cursor and start ✅ * End navigates to end ✅ * Ctrl+End deletes text between cursor and end ✅ * Left navigates over previous code points ✅ * Ctrl+Left navigates to previous word-starts ✅ * Right and F1 navigate over next code points ✅ * Pressing right at the end of input copies characters from the previous command ✅ * Ctrl+Right navigates to next word-ends ✅ * Insert toggles overwrite mode ✅ * Delete deletes next code point ✅ * Up and F5 cycle through history ✅ * Doesn't crash with no history ✅ * Stops at first entry ✅ * Down cycles through history ✅ * Doesn't crash with no history ✅ * Stops at last entry ✅ * PageUp retrieves the oldest command ✅ * PageDown retrieves the newest command ✅ * F2 starts "copy to char" prompt ✅ * Escape dismisses prompt ✅ * Typing a character copies text from the previous command up until that character into the current buffer (acts identical to F3, but with automatic character search) ✅ * F3 copies the previous command into the current buffer, starting at the current cursor position, for as many characters as possible ✅ * Doesn't erase trailing text if the current buffer is longer than the previous command ✅ * Puts the cursor at the end of the copied text ✅ * F4 starts "copy from char" prompt ✅ * Escape dismisses prompt ✅ * Erases text between the current cursor position and the first instance of a given char (but not including it) ✅ * F6 inserts Ctrl+Z ✅ * F7 without modifiers starts "command list" prompt ✅ * Escape dismisses prompt ✅ * Entries wider than the window width are truncated ✅ * Height expands up to 20 rows with longer histories ✅ * F9 starts "command number" prompt ✅ * Left/Right replace the buffer with the given command ✅ * And put cursor at the end of the buffer ✅ * Up/Down navigate selection through history ✅ * Stops at start/end with <10 entries ✅ * Stops at start/end with >20 entries ✅ * Scrolls through the entries if there are too many ✅ * Shift+Up/Down moves history items around ✅ * Home navigates to first entry ✅ * End navigates to last entry ✅ * PageUp navigates by $height items at a time or to first ✅ * PageDown navigates by $height items at a time or to last ✅ * Alt+F7 clears command history ✅ * F8 cycles through commands that start with the same text as the current buffer up until the current cursor position ✅ * Doesn't crash with no history ✅ * F9 starts "command number" prompt ✅ * Escape dismisses prompt ✅ * Ignores non-ASCII-decimal characters ✅ * Allows entering between 1 and 5 digits ✅ * Pressing Enter fetches the given command from the history ✅ * Alt+F10 clears doskey aliases ✅ * In cmd.exe, with an empty prompt in an empty directory: Pressing tab produces an audible bing and prints no text ✅ * When Narrator is enabled, in cmd.exe: * Typing individual characters announces only exactly each character that is being typed ✅ * Backspacing at the end of a prompt announces only exactly each deleted character ✅
1 parent 30447cf commit ac5b4f5

15 files changed

+839
-731
lines changed

doc/COOKED_READ_DATA.md

+7-12
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,8 @@
44

55
All of the following ✅ marks must be fulfilled during manual testing:
66
* ASCII input
7-
* Chinese input (中文維基百科) ❔
8-
* Resizing the window properly wraps/unwraps wide glyphs ❌
9-
Broken due to `TextBuffer::Reflow` bugs
10-
* Surrogate pair input (🙂) ❔
11-
* Resizing the window properly wraps/unwraps surrogate pairs ❌
12-
Broken due to `TextBuffer::Reflow` bugs
7+
* Chinese input (中文維基百科) ✅
8+
* Surrogate pair input (🙂) ✅
139
* In cmd.exe
1410
* Create 2 file: "a😊b.txt" and "a😟b.txt"
1511
* Press tab: Autocomplete to "a😊b.txt" ✅
@@ -62,21 +58,20 @@ All of the following ✅ marks must be fulfilled during manual testing:
6258
* F6 inserts Ctrl+Z ✅
6359
* F7 without modifiers starts "command list" prompt ✅
6460
* Escape dismisses prompt ✅
65-
* Minimum size of 40x10 characters ✅
66-
* Width expands to fit the widest history command ✅
61+
* Entries wider than the window width are truncated ✅
6762
* Height expands up to 20 rows with longer histories ✅
6863
* F9 starts "command number" prompt ✅
69-
* Left/Right paste replace the buffer with the given command ✅
64+
* Left/Right replace the buffer with the given command ✅
7065
* And put cursor at the end of the buffer ✅
7166
* Up/Down navigate selection through history ✅
7267
* Stops at start/end with <10 entries ✅
7368
* Stops at start/end with >20 entries ✅
74-
* Wide text rendering during pagination with >20 entries
69+
* Scrolls through the entries if there are too many
7570
* Shift+Up/Down moves history items around ✅
7671
* Home navigates to first entry ✅
7772
* End navigates to last entry ✅
78-
* PageUp navigates by 20 items at a time or to first ✅
79-
* PageDown navigates by 20 items at a time or to last ✅
73+
* PageUp navigates by $height items at a time or to first ✅
74+
* PageDown navigates by $height items at a time or to last ✅
8075
* Alt+F7 clears command history ✅
8176
* F8 cycles through commands that start with the same text as
8277
the current buffer up until the current cursor position ✅

src/buffer/out/cursor.cpp

-13
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ Cursor::Cursor(const ULONG ulSize, TextBuffer& parentBuffer) noexcept :
2020
_fBlinkingAllowed(true),
2121
_fDelay(false),
2222
_fIsConversionArea(false),
23-
_fIsPopupShown(false),
2423
_fDelayedEolWrap(false),
2524
_fDeferCursorRedraw(false),
2625
_fHaveDeferredCursorRedraw(false),
@@ -66,11 +65,6 @@ bool Cursor::IsConversionArea() const noexcept
6665
return _fIsConversionArea;
6766
}
6867

69-
bool Cursor::IsPopupShown() const noexcept
70-
{
71-
return _fIsPopupShown;
72-
}
73-
7468
bool Cursor::GetDelay() const noexcept
7569
{
7670
return _fDelay;
@@ -126,13 +120,6 @@ void Cursor::SetIsConversionArea(const bool fIsConversionArea) noexcept
126120
_RedrawCursorAlways();
127121
}
128122

129-
void Cursor::SetIsPopupShown(const bool fIsPopupShown) noexcept
130-
{
131-
// Functionally the same as "Hide cursor"
132-
_fIsPopupShown = fIsPopupShown;
133-
_RedrawCursorAlways();
134-
}
135-
136123
void Cursor::SetDelay(const bool fDelay) noexcept
137124
{
138125
_fDelay = fDelay;

src/buffer/out/cursor.h

-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ class Cursor final
4444
bool IsBlinkingAllowed() const noexcept;
4545
bool IsDouble() const noexcept;
4646
bool IsConversionArea() const noexcept;
47-
bool IsPopupShown() const noexcept;
4847
bool GetDelay() const noexcept;
4948
ULONG GetSize() const noexcept;
5049
til::point GetPosition() const noexcept;
@@ -61,7 +60,6 @@ class Cursor final
6160
void SetBlinkingAllowed(const bool fIsOn) noexcept;
6261
void SetIsDouble(const bool fIsDouble) noexcept;
6362
void SetIsConversionArea(const bool fIsConversionArea) noexcept;
64-
void SetIsPopupShown(const bool fIsPopupShown) noexcept;
6563
void SetDelay(const bool fDelay) noexcept;
6664
void SetSize(const ULONG ulSize) noexcept;
6765
void SetStyle(const ULONG ulSize, const CursorType type) noexcept;
@@ -99,7 +97,6 @@ class Cursor final
9997
bool _fBlinkingAllowed; //Whether or not the cursor is allowed to blink at all. only set through VT (^[[?12h/l)
10098
bool _fDelay; // don't blink scursor on next timer message
10199
bool _fIsConversionArea; // is attached to a conversion area so it doesn't actually need to display the cursor.
102-
bool _fIsPopupShown; // if a popup is being shown, turn off, stop blinking.
103100

104101
bool _fDelayedEolWrap; // don't wrap at EOL till the next char comes in.
105102
til::point _coordDelayedAt; // coordinate the EOL wrap was delayed at.

src/cascadia/TerminalCore/terminalrenderdata.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ til::point Terminal::GetCursorPosition() const noexcept
4848
bool Terminal::IsCursorVisible() const noexcept
4949
{
5050
const auto& cursor = _activeBuffer().GetCursor();
51-
return cursor.IsVisible() && !cursor.IsPopupShown();
51+
return cursor.IsVisible();
5252
}
5353

5454
bool Terminal::IsCursorOn() const noexcept

src/host/VtIo.cpp

+63
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,66 @@ bool VtIo::IsResizeQuirkEnabled() const
518518
}
519519
return S_OK;
520520
}
521+
522+
// Formats the given console attributes to their closest VT equivalent.
523+
// `out` must refer to at least `formatAttributesMaxLen` characters of valid memory.
524+
// Returns a pointer past the end.
525+
static constexpr size_t formatAttributesMaxLen = 16;
526+
static char* formatAttributes(char* out, const TextAttribute& attributes) noexcept
527+
{
528+
static uint8_t sgr[] = { 30, 31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97 };
529+
530+
// Applications expect that SetConsoleTextAttribute() completely replaces whatever attributes are currently set,
531+
// including any potential VT-exclusive attributes. Since we don't know what those are, we must always emit a SGR 0.
532+
// Copying 4 bytes instead of the correct 3 means we need just 1 DWORD mov. Neat.
533+
//
534+
// 3 bytes.
535+
memcpy(out, "\x1b[0", 4);
536+
out += 3;
537+
538+
// 2 bytes.
539+
if (attributes.IsReverseVideo())
540+
{
541+
memcpy(out, ";7", 2);
542+
out += 2;
543+
}
544+
545+
// 3 bytes (";97").
546+
if (attributes.GetForeground().IsLegacy())
547+
{
548+
const uint8_t index = sgr[attributes.GetForeground().GetIndex()];
549+
out = fmt::format_to(out, FMT_COMPILE(";{}"), index);
550+
}
551+
552+
// 4 bytes (";107").
553+
if (attributes.GetBackground().IsLegacy())
554+
{
555+
const uint8_t index = sgr[attributes.GetBackground().GetIndex()] + 10;
556+
out = fmt::format_to(out, FMT_COMPILE(";{}"), index);
557+
}
558+
559+
// 1 byte.
560+
*out++ = 'm';
561+
return out;
562+
}
563+
564+
void VtIo::FormatAttributes(std::string& target, const TextAttribute& attributes)
565+
{
566+
char buf[formatAttributesMaxLen];
567+
const size_t len = formatAttributes(&buf[0], attributes) - &buf[0];
568+
target.append(buf, len);
569+
}
570+
571+
void VtIo::FormatAttributes(std::wstring& target, const TextAttribute& attributes)
572+
{
573+
char buf[formatAttributesMaxLen];
574+
const size_t len = formatAttributes(&buf[0], attributes) - &buf[0];
575+
576+
wchar_t bufW[formatAttributesMaxLen];
577+
for (size_t i = 0; i < len; i++)
578+
{
579+
bufW[i] = buf[i];
580+
}
581+
582+
target.append(bufW, len);
583+
}

src/host/VtIo.hpp

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ namespace Microsoft::Console::VirtualTerminal
2020
class VtIo
2121
{
2222
public:
23+
static void FormatAttributes(std::string& target, const TextAttribute& attributes);
24+
static void FormatAttributes(std::wstring& target, const TextAttribute& attributes);
25+
2326
VtIo();
2427

2528
[[nodiscard]] HRESULT Initialize(const ConsoleArguments* const pArgs);

src/host/_stream.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,13 @@ void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& t
255255
}
256256
}
257257

258+
// This is the main entrypoint for conhost to write VT to the buffer.
259+
// This wrapper around StateMachine exists so that we can add the necessary ConPTY transformations.
260+
void WriteCharsVT(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str)
261+
{
262+
screenInfo.GetStateMachine().ProcessString(str);
263+
}
264+
258265
// Routine Description:
259266
// - Takes the given text and inserts it into the given screen buffer.
260267
// Note:

src/host/_stream.h

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Revision History:
2020
#include "writeData.hpp"
2121

2222
void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str, til::CoordType* psScrollY);
23+
void WriteCharsVT(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str);
2324

2425
// NOTE: console lock must be held when calling this routine
2526
// String has been translated to unicode at this point.

0 commit comments

Comments
 (0)