Add support for the S8C1T/S7C1T escape sequences (#17945)

This PR adds support for the `S8C1T` and `S7C1T` commands, which enable
an application to choose whether the terminal should use C1 controls
when sending key sequences and query responses.

This also updates the `DOCS` command to set both the input and output
code pages. So when switched to ISO2022 mode, the C1 controls will be
transmitted as 8-bit, which is what legacy systems would be expecting.

## Detailed Description of the Pull Request / Additional comments

While adding the input code page support, I also reworked the way we
handle the code page reset in `RIS`. In the original implementation we
saved the active code page when the `DOCS` sequence was first used, and
that would become the default value for a reset.

With this PR I'm now saving the code pages whenever `SetConsoleCP` or
`SetConsoleOutputCP` is called, so those APIs now control what the
default values will be. This feels more consistent than the previous
approach. And this is how WSL sets its initial code page to UTF-8.

## Validation Steps Performed

I've added a couple of unit tests that check one of each applicable C1
control in the key sequences and query reports.

I also built myself a code page aware telnet client so I could log into
WSL in 8-bit mode, and confirmed that the C1 transmissions are working
as expected in vttest.

Closes #17931
Tests added/passed
This commit is contained in:
James Holderness 2024-10-07 14:11:38 +01:00 committed by GitHub
parent b715008de3
commit aa256ad5c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 304 additions and 136 deletions

View File

@ -139,8 +139,10 @@ public:
void SetWindowTitle(const std::wstring_view title) override;
CursorType GetUserDefaultCursorStyle() const noexcept override;
bool ResizeWindow(const til::CoordType width, const til::CoordType height) override;
void SetConsoleOutputCP(const unsigned int codepage) noexcept override;
unsigned int GetConsoleOutputCP() const noexcept override;
void SetCodePage(const unsigned int codepage) noexcept override;
void ResetCodePage() noexcept override;
unsigned int GetOutputCodePage() const noexcept override;
unsigned int GetInputCodePage() const noexcept override;
void CopyToClipboard(wil::zwstring_view content) override;
void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;
void SetWorkingDirectory(std::wstring_view uri) override;

View File

@ -116,14 +116,25 @@ bool Terminal::ResizeWindow(const til::CoordType width, const til::CoordType hei
return false;
}
void Terminal::SetConsoleOutputCP(const unsigned int /*codepage*/) noexcept
void Terminal::SetCodePage(const unsigned int /*codepage*/) noexcept
{
// TODO: This will be needed to support 8-bit charsets and DOCS sequences.
// Code pages are dealt with in ConHost, so this isn't needed.
}
unsigned int Terminal::GetConsoleOutputCP() const noexcept
void Terminal::ResetCodePage() noexcept
{
// TODO: See SetConsoleOutputCP above.
// There is nothing to reset, since the code page never changes.
}
unsigned int Terminal::GetOutputCodePage() const noexcept
{
// See above. The code page is always UTF-8.
return CP_UTF8;
}
unsigned int Terminal::GetInputCodePage() const noexcept
{
// See above. The code page is always UTF-8.
return CP_UTF8;
}

View File

@ -1140,7 +1140,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
{
// Set new code page
gci.OutputCP = codepage;
SetConsoleCPInfo(TRUE);
}
@ -1157,13 +1156,36 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
{
try
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
return DoSrvSetConsoleOutputCodePage(codepage);
RETURN_IF_FAILED(DoSrvSetConsoleOutputCodePage(codepage));
// Setting the code page via the API also updates the default value.
// This is how the initial code page is set to UTF-8 in a WSL shell.
gci.DefaultOutputCP = codepage;
return S_OK;
}
CATCH_RETURN();
}
[[nodiscard]] HRESULT DoSrvSetConsoleInputCodePage(const unsigned int codepage)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
// Return if it's not known as a valid codepage ID.
RETURN_HR_IF(E_INVALIDARG, !(IsValidCodePage(codepage)));
// Do nothing if no change.
if (gci.CP != codepage)
{
// Set new code page
gci.CP = codepage;
SetConsoleCPInfo(FALSE);
}
return S_OK;
}
// Routine Description:
// - Sets the codepage used for translating text when calling A versions of functions affecting the input buffer.
// Arguments:
@ -1177,19 +1199,10 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
// Return if it's not known as a valid codepage ID.
RETURN_HR_IF(E_INVALIDARG, !(IsValidCodePage(codepage)));
// Do nothing if no change.
if (gci.CP != codepage)
{
// Set new code page
gci.CP = codepage;
SetConsoleCPInfo(FALSE);
}
RETURN_IF_FAILED(DoSrvSetConsoleInputCodePage(codepage));
// Setting the code page via the API also updates the default value.
// This is how the initial code page is set to UTF-8 in a WSL shell.
gci.DefaultCP = codepage;
return S_OK;
}
CATCH_RETURN();

View File

@ -19,3 +19,4 @@ Revision History:
class SCREEN_INFORMATION;
[[nodiscard]] HRESULT DoSrvSetConsoleOutputCodePage(const unsigned int codepage);
[[nodiscard]] HRESULT DoSrvSetConsoleInputCodePage(const unsigned int codepage);

View File

@ -35,6 +35,8 @@ using namespace Microsoft::Console::Interactivity;
// codepage by console.cpl or shell32. The default codepage is OEMCP.
gci.CP = gci.GetCodePage();
gci.OutputCP = gci.GetCodePage();
gci.DefaultCP = gci.GetCodePage();
gci.DefaultOutputCP = gci.GetCodePage();
gci.Flags |= CONSOLE_USE_PRIVATE_FLAGS;

View File

@ -221,27 +221,52 @@ void ConhostInternalGetSet::ShowWindow(bool showOrHide)
}
// Routine Description:
// - Connects the SetConsoleOutputCP API call directly into our Driver Message servicing call inside Conhost.exe
// - Set the code page used for translating text when calling A versions of I/O functions.
// Arguments:
// - codepage - the new output codepage of the console.
// - codepage - the new code page of the console.
// Return Value:
// - <none>
void ConhostInternalGetSet::SetConsoleOutputCP(const unsigned int codepage)
void ConhostInternalGetSet::SetCodePage(const unsigned int codepage)
{
THROW_IF_FAILED(DoSrvSetConsoleOutputCodePage(codepage));
LOG_IF_FAILED(DoSrvSetConsoleOutputCodePage(codepage));
LOG_IF_FAILED(DoSrvSetConsoleInputCodePage(codepage));
}
// Routine Description:
// - Gets the codepage used for translating text when calling A versions of functions affecting the output buffer.
// - Reset the code pages to their default values.
// Arguments:
// - <none>
// Return Value:
// - the outputCP of the console.
unsigned int ConhostInternalGetSet::GetConsoleOutputCP() const
// - <none>
void ConhostInternalGetSet::ResetCodePage()
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
LOG_IF_FAILED(DoSrvSetConsoleOutputCodePage(gci.DefaultOutputCP));
LOG_IF_FAILED(DoSrvSetConsoleInputCodePage(gci.DefaultCP));
}
// Routine Description:
// - Gets the code page used for translating text when calling A versions of output functions.
// Arguments:
// - <none>
// Return Value:
// - the output code page of the console.
unsigned int ConhostInternalGetSet::GetOutputCodePage() const
{
return ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP;
}
// Routine Description:
// - Gets the code page used for translating text when calling A versions of input functions.
// Arguments:
// - <none>
// Return Value:
// - the input code page of the console.
unsigned int ConhostInternalGetSet::GetInputCodePage() const
{
return ServiceLocator::LocateGlobals().getConsoleInformation().CP;
}
// Routine Description:
// - Copies the given content to the clipboard.
// Arguments:

View File

@ -53,8 +53,10 @@ public:
bool ResizeWindow(const til::CoordType width, const til::CoordType height) override;
void SetConsoleOutputCP(const unsigned int codepage) override;
unsigned int GetConsoleOutputCP() const override;
void SetCodePage(const unsigned int codepage) override;
void ResetCodePage() override;
unsigned int GetOutputCodePage() const override;
unsigned int GetInputCodePage() const override;
void CopyToClipboard(const wil::zwstring_view content) override;
void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;

View File

@ -90,6 +90,9 @@ public:
// the following fields are used for ansi-unicode translation
UINT CP = 0;
UINT OutputCP = 0;
// the VT RIS sequence uses these default values to reset the code pages
UINT DefaultCP = 0;
UINT DefaultOutputCP = 0;
ULONG CtrlFlags = 0; // indicates outstanding ctrl requests
ULONG LimitingProcessId = 0;

View File

@ -122,6 +122,7 @@ public:
virtual void LockingShiftRight(const VTInt gsetNumber) = 0; // LS1R, LS2R, LS3R
virtual void SingleShift(const VTInt gsetNumber) = 0; // SS2, SS3
virtual void AcceptC1Controls(const bool enabled) = 0; // DECAC1
virtual void SendC1Controls(const bool enabled) = 0; // S8C1T, S7C1T
virtual void AnnounceCodeStructure(const VTInt ansiLevel) = 0; // ACS
virtual void SoftReset() = 0; // DECSTR

View File

@ -72,8 +72,10 @@ namespace Microsoft::Console::VirtualTerminal
virtual void ShowWindow(bool showOrHide) = 0;
virtual void SetConsoleOutputCP(const unsigned int codepage) = 0;
virtual unsigned int GetConsoleOutputCP() const = 0;
virtual void SetCodePage(const unsigned int codepage) = 0;
virtual void ResetCodePage() = 0;
virtual unsigned int GetOutputCodePage() const = 0;
virtual unsigned int GetInputCodePage() const = 0;
virtual void CopyToClipboard(const wil::zwstring_view content) = 0;
virtual void SetTaskbarProgress(const DispatchTypes::TaskbarState state, const size_t progress) = 0;

View File

@ -62,7 +62,7 @@ void InteractDispatch::WriteString(const std::wstring_view string)
{
if (!string.empty())
{
const auto codepage = _api.GetConsoleOutputCP();
const auto codepage = _api.GetOutputCodePage();
InputEventQueue keyEvents;
for (const auto& wch : string)

View File

@ -1243,7 +1243,7 @@ void AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c
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);
const auto unicodeChar = (charValue >= 256 && charValue <= 65535 && _api.GetOutputCodePage() == CP_UTF8);
if (glChar || grChar || unicodeChar)
{
const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast<wchar_t>(charValue));
@ -1377,8 +1377,7 @@ void AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p
}
}
}
const auto response = wil::str_printf<std::wstring>(L"\033P%d!~%04X\033\\", id, checksum);
_api.ReturnResponse(response);
_ReturnDcsResponse(wil::str_printf<std::wstring>(L"%d!~%04X", id, checksum));
}
// Routine Description:
@ -1484,7 +1483,7 @@ void AdaptDispatch::DeviceAttributes()
// 32 = Text macros
// 42 = ISO Latin-2 character set
_api.ReturnResponse(L"\x1b[?61;4;6;7;14;21;22;23;24;28;32;42c");
_ReturnCsiResponse(L"?61;4;6;7;14;21;22;23;24;28;32;42c");
}
// Routine Description:
@ -1496,7 +1495,7 @@ void AdaptDispatch::DeviceAttributes()
// - <none>
void AdaptDispatch::SecondaryDeviceAttributes()
{
_api.ReturnResponse(L"\x1b[>0;10;1c");
_ReturnCsiResponse(L">0;10;1c");
}
// Routine Description:
@ -1506,7 +1505,7 @@ void AdaptDispatch::SecondaryDeviceAttributes()
// - <none>
void AdaptDispatch::TertiaryDeviceAttributes()
{
_api.ReturnResponse(L"\x1bP!|00000000\x1b\\");
_ReturnDcsResponse(L"!|00000000");
}
// Routine Description:
@ -1544,10 +1543,10 @@ void AdaptDispatch::RequestTerminalParameters(const DispatchTypes::ReportingPerm
switch (permission)
{
case DispatchTypes::ReportingPermission::Unsolicited:
_api.ReturnResponse(L"\x1b[2;1;1;128;128;1;0x");
_ReturnCsiResponse(L"2;1;1;128;128;1;0x");
break;
case DispatchTypes::ReportingPermission::Solicited:
_api.ReturnResponse(L"\x1b[3;1;1;128;128;1;0x");
_ReturnCsiResponse(L"3;1;1;128;128;1;0x");
break;
default:
break;
@ -1562,7 +1561,7 @@ void AdaptDispatch::RequestTerminalParameters(const DispatchTypes::ReportingPerm
// - <none>
void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const
{
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{}n"), parameters));
_ReturnCsiResponse(fmt::format(FMT_COMPILE(L"{}n"), parameters));
}
// Routine Description:
@ -1598,14 +1597,12 @@ void AdaptDispatch::_CursorPositionReport(const bool 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);
_ReturnCsiResponse(wil::str_printf<std::wstring>(L"?%d;%d;%dR", cursorPosition.y, cursorPosition.x, pageNumber));
}
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);
_ReturnCsiResponse(wil::str_printf<std::wstring>(L"%d;%dR", cursorPosition.y, cursorPosition.x));
}
}
@ -1619,8 +1616,7 @@ 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);
_ReturnCsiResponse(wil::str_printf<std::wstring>(L"%zu*{", spaceInBytes / 16));
}
// Routine Description:
@ -1633,8 +1629,7 @@ 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);
_ReturnDcsResponse(wil::str_printf<std::wstring>(L"%d!~%04X", requestId, checksum));
}
// Routine Description:
@ -1732,7 +1727,7 @@ void AdaptDispatch::RequestDisplayedExtent()
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()));
_ReturnCsiResponse(fmt::format(FMT_COMPILE(L"{};{};{};{};{}\"w"), height, width, left, top, page.Number()));
}
// Routine Description:
@ -2068,7 +2063,7 @@ void AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
prefix = L"?";
}
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\x1b[{}{};{}$y"), prefix, mode, state));
_ReturnCsiResponse(fmt::format(FMT_COMPILE(L"{}{};{}$y"), prefix, mode, state));
}
// - DECKPAM, DECKPNM - Sets the keypad input mode to either Application mode or Numeric mode (true, false respectively)
@ -2785,22 +2780,15 @@ void AdaptDispatch::_InitTabStopsForWidth(const VTInt width)
// - codingSystem - The coding system that will be selected.
void 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);
_api.SetCodePage(28591);
AcceptC1Controls(true);
_termOutput.EnableGrTranslation(true);
break;
case DispatchTypes::CodingSystem::UTF8:
_api.SetConsoleOutputCP(CP_UTF8);
_api.SetCodePage(CP_UTF8);
AcceptC1Controls(false);
_termOutput.EnableGrTranslation(false);
break;
@ -2871,6 +2859,24 @@ void AdaptDispatch::AcceptC1Controls(const bool enabled)
_api.GetStateMachine().SetParserMode(StateMachine::Mode::AcceptC1, enabled);
}
//Routine Description:
// S8C1T/S7C1T - Enable or disable the sending of C1 controls in key sequences
// and query responses. When this is enabled, C1 controls are sent as a single
// codepoint. When disabled, they're sent as a two character escape sequence.
//Arguments:
// - enabled - true to send C1 controls, false to send escape sequences.
void AdaptDispatch::SendC1Controls(const bool enabled)
{
// If this is an attempt to enable C1 controls, the input code page must be
// one of the DOCS choices (UTF-8 or ISO-8859-1), otherwise there's a risk
// that those controls won't have a valid encoding.
const auto codepage = _api.GetInputCodePage();
if (enabled == false || codepage == CP_UTF8 || codepage == 28591)
{
_terminalInput.SetInputMode(TerminalInput::Mode::SendC1, enabled);
}
}
//Routine Description:
// ACS - Announces the ANSI conformance level for subsequent data exchange.
// This requires certain character sets to be mapped into the terminal's
@ -3003,13 +3009,11 @@ void AdaptDispatch::HardReset()
// 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.
// Reset the code page to the default value.
_api.ResetCodePage();
// Disable parsing and sending of C1 control codes.
AcceptC1Controls(false);
SendC1Controls(false);
// Sets the SGR state to normal - this must be done before EraseInDisplay
// to ensure that it clears with the default background color.
@ -3276,7 +3280,7 @@ void AdaptDispatch::RequestColorTableEntry(const size_t tableIndex)
{
const til::color c{ color };
// Scale values up to match xterm's 16-bit color report format.
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033]4;{};rgb:{:04x}/{:04x}/{:04x}\033\\"), tableIndex, c.r * 0x0101, c.g * 0x0101, c.b * 0x0101));
_ReturnOscResponse(fmt::format(FMT_COMPILE(L"4;{};rgb:{:04x}/{:04x}/{:04x}"), tableIndex, c.r * 0x0101, c.g * 0x0101, c.b * 0x0101));
}
}
@ -3321,7 +3325,7 @@ void AdaptDispatch::RequestXtermColorResource(const size_t resource)
{
const til::color c{ color };
// Scale values up to match xterm's 16-bit color report format.
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033]{};rgb:{:04x}/{:04x}/{:04x}\033\\"), resource, c.r * 0x0101, c.g * 0x0101, c.b * 0x0101));
_ReturnOscResponse(fmt::format(FMT_COMPILE(L"{};rgb:{:04x}/{:04x}/{:04x}"), resource, c.r * 0x0101, c.g * 0x0101, c.b * 0x0101));
}
}
}
@ -3377,7 +3381,7 @@ void AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy
const auto reportSize = [&](const auto size) {
const auto reportType = function - 10;
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{};{};{}t"), reportType, size.height, size.width));
_ReturnCsiResponse(fmt::format(FMT_COMPILE(L"{};{};{}t"), reportType, size.height, size.width));
};
switch (function)
@ -3844,7 +3848,7 @@ void 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));
_ReturnDcsResponse(fmt::format(FMT_COMPILE(L"{}!u{}"), (size == 96 ? 1 : 0), id));
}
// Method Description:
@ -3976,9 +3980,9 @@ void AdaptDispatch::_ReportColorTable(const DispatchTypes::ColorModel colorModel
{
using namespace std::string_view_literals;
// A valid response always starts with DCS 2 $ s.
// A valid response always starts with 2 $ s.
fmt::basic_memory_buffer<wchar_t, TextColor::TABLE_SIZE * 18> response;
response.append(L"\033P2$s"sv);
response.append(L"2$s"sv);
const auto modelNumber = static_cast<int>(colorModel);
for (size_t colorNumber = 0; colorNumber < TextColor::TABLE_SIZE; colorNumber++)
@ -4003,9 +4007,7 @@ void AdaptDispatch::_ReportColorTable(const DispatchTypes::ColorModel colorModel
}
}
// An ST ends the sequence.
response.append(L"\033\\"sv);
_api.ReturnResponse({ response.data(), response.size() });
_ReturnDcsResponse({ response.data(), response.size() });
}
// Method Description:
@ -4108,7 +4110,7 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting()
_ReportDECACSetting(VTParameter{ parameter });
break;
default:
_api.ReturnResponse(L"\033P0$r\033\\");
_ReturnDcsResponse(L"0$r");
break;
}
return false;
@ -4147,10 +4149,10 @@ void AdaptDispatch::_ReportSGRSetting() const
{
using namespace std::string_view_literals;
// A valid response always starts with DCS 1 $ r.
// A valid response always starts with 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);
response.append(L"1$r0"sv);
const auto& attr = _pages.ActivePage().Attributes();
const auto ulStyle = attr.GetUnderlineStyle();
@ -4202,9 +4204,9 @@ void AdaptDispatch::_ReportSGRSetting() const
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() });
// The 'm' indicates this is an SGR response.
response.append(L"m"sv);
_ReturnDcsResponse({ response.data(), response.size() });
}
// Method Description:
@ -4217,10 +4219,9 @@ void AdaptDispatch::_ReportDECSTBMSetting()
{
const auto page = _pages.ActivePage();
const auto [marginTop, marginBottom] = _GetVerticalMargins(page, false);
// A valid response always starts with DCS 1 $ r, the 'r' indicates this
// is a DECSTBM response, and ST ends the sequence.
// A valid response always starts with 1 $ r and the final 'r' indicates this is a DECSTBM response.
// VT origin is at 1,1 so we need to add 1 to these margins.
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033P1$r{};{}r\033\\"), marginTop + 1, marginBottom + 1));
_ReturnDcsResponse(fmt::format(FMT_COMPILE(L"1$r{};{}r"), marginTop + 1, marginBottom + 1));
}
// Method Description:
@ -4233,10 +4234,9 @@ void AdaptDispatch::_ReportDECSLRMSetting()
{
const auto pageWidth = _pages.ActivePage().Width();
const auto [marginLeft, marginRight] = _GetHorizontalMargins(pageWidth);
// A valid response always starts with DCS 1 $ r, the 's' indicates this
// is a DECSLRM response, and ST ends the sequence.
// A valid response always starts with 1 $ r and the 's' indicates this is a DECSLRM response.
// VT origin is at 1,1 so we need to add 1 to these margins.
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033P1$r{};{}s\033\\"), marginLeft + 1, marginRight + 1));
_ReturnDcsResponse(fmt::format(FMT_COMPILE(L"1$r{};{}s"), marginLeft + 1, marginRight + 1));
}
// Method Description:
@ -4249,26 +4249,26 @@ void AdaptDispatch::_ReportDECSCUSRSetting() const
{
const auto& cursor = _pages.ActivePage().Cursor();
const auto blinking = cursor.IsBlinkingAllowed();
// A valid response always starts with DCS 1 $ r. This is followed by a
// A valid response always starts with 1 $ r. This is followed by a
// number from 1 to 6 representing the cursor style. The ' q' indicates
// this is a DECSCUSR response, and ST ends the sequence.
// this is a DECSCUSR response.
switch (cursor.GetType())
{
case CursorType::FullBox:
_api.ReturnResponse(blinking ? L"\033P1$r1 q\033\\" : L"\033P1$r2 q\033\\");
_ReturnDcsResponse(blinking ? L"1$r1 q" : L"1$r2 q");
break;
case CursorType::Underscore:
_api.ReturnResponse(blinking ? L"\033P1$r3 q\033\\" : L"\033P1$r4 q\033\\");
_ReturnDcsResponse(blinking ? L"1$r3 q" : L"1$r4 q");
break;
case CursorType::VerticalBar:
_api.ReturnResponse(blinking ? L"\033P1$r5 q\033\\" : L"\033P1$r6 q\033\\");
_ReturnDcsResponse(blinking ? L"1$r5 q" : L"1$r6 q");
break;
default:
// If we have a non-standard style, this is likely because it's the
// user's chosen default style, so we report a default value of 0.
// That way, if an application later tries to restore the cursor with
// the returned value, it should be reset to its original state.
_api.ReturnResponse(L"\033P1$r0 q\033\\");
_ReturnDcsResponse(L"1$r0 q");
break;
}
}
@ -4282,10 +4282,10 @@ void AdaptDispatch::_ReportDECSCUSRSetting() const
void AdaptDispatch::_ReportDECSCASetting() const
{
const auto isProtected = _pages.ActivePage().Attributes().IsProtected();
// A valid response always starts with DCS 1 $ r. This is followed by '1' if
// the protected attribute is set, or '0' if not. The '"q' indicates this is
// a DECSCA response, and ST ends the sequence.
_api.ReturnResponse(isProtected ? L"\033P1$r1\"q\033\\" : L"\033P1$r0\"q\033\\");
// A valid response always starts with 1 $ r. This is followed by '1' if the
// protected attribute is set, or '0' if not. The '"q' indicates this is a
// DECSCA response.
_ReturnDcsResponse(isProtected ? L"1$r1\"q" : L"1$r0\"q");
}
// Method Description:
@ -4297,10 +4297,10 @@ void AdaptDispatch::_ReportDECSCASetting() const
void AdaptDispatch::_ReportDECSACESetting() const
{
const auto rectangularExtent = _modes.test(Mode::RectangularChangeExtent);
// A valid response always starts with DCS 1 $ r. This is followed by '2' if
// A valid response always starts with 1 $ r. This is followed by '2' if
// the extent is rectangular, or '1' if it's a stream. The '*x' indicates
// this is a DECSACE response, and ST ends the sequence.
_api.ReturnResponse(rectangularExtent ? L"\033P1$r2*x\033\\" : L"\033P1$r1*x\033\\");
// this is a DECSACE response.
_ReturnDcsResponse(rectangularExtent ? L"1$r2*x" : L"1$r1*x");
}
// Method Description:
@ -4324,12 +4324,11 @@ void AdaptDispatch::_ReportDECACSetting(const VTInt itemNumber) const
bgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground);
break;
default:
_api.ReturnResponse(L"\033P0$r\033\\");
_ReturnDcsResponse(L"0$r");
return;
}
// A valid response always starts with DCS 1 $ r, the ',|' indicates this
// is a DECAC response, and ST ends the sequence.
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033P1$r{};{};{},|\033\\"), itemNumber, fgIndex, bgIndex));
// A valid response always starts with 1 $ r and the ',|' indicates this is a DECAC response.
_ReturnDcsResponse(fmt::format(FMT_COMPILE(L"1$r{};{};{},|"), itemNumber, fgIndex, bgIndex));
}
// Routine Description:
@ -4442,9 +4441,9 @@ void AdaptDispatch::_ReportCursorInformation()
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.
// A valid response always starts with 1 $ u.
const auto response = fmt::format(
FMT_COMPILE(L"\033P1$u{};{};{};{};{};{};{};{};{};{}{}{}{}\033\\"),
FMT_COMPILE(L"1$u{};{};{};{};{};{};{};{};{};{}{}{}{}"),
cursorPosition.y,
cursorPosition.x,
page.Number(),
@ -4458,7 +4457,7 @@ void AdaptDispatch::_ReportCursorInformation()
charset1,
charset2,
charset3);
_api.ReturnResponse({ response.data(), response.size() });
_ReturnDcsResponse({ response.data(), response.size() });
}
// Method Description:
@ -4623,9 +4622,9 @@ void AdaptDispatch::_ReportTabStops()
using namespace std::string_view_literals;
// A valid response always starts with DCS 2 $ u.
// A valid response always starts with 2 $ u.
fmt::basic_memory_buffer<wchar_t, 64> response;
response.append(L"\033P2$u"sv);
response.append(L"2$u"sv);
auto need_separator = false;
for (auto column = 0; column < width; column++)
@ -4638,9 +4637,7 @@ void AdaptDispatch::_ReportTabStops()
}
}
// An ST ends the sequence.
response.append(L"\033\\"sv);
_api.ReturnResponse({ response.data(), response.size() });
_ReturnDcsResponse({ response.data(), response.size() });
}
// Method Description:
@ -4685,6 +4682,26 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops()
};
}
void AdaptDispatch::_ReturnCsiResponse(const std::wstring_view response) const
{
const auto csi = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x9B" : L"\x1B[";
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"{}{}"), csi, response));
}
void AdaptDispatch::_ReturnDcsResponse(const std::wstring_view response) const
{
const auto dcs = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x90" : L"\x1BP";
const auto st = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x9C" : L"\x1B\\";
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"{}{}{}"), dcs, response, st));
}
void AdaptDispatch::_ReturnOscResponse(const std::wstring_view response) const
{
const auto osc = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x9D" : L"\x1B]";
const auto st = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x9C" : L"\x1B\\";
_api.ReturnResponse(fmt::format(FMT_COMPILE(L"{}{}{}"), osc, response, st));
}
// Routine Description:
// - DECPS - Plays a sequence of musical notes.
// Arguments:

View File

@ -121,6 +121,7 @@ namespace Microsoft::Console::VirtualTerminal
void LockingShiftRight(const VTInt gsetNumber) override; // LS1R, LS2R, LS3R
void SingleShift(const VTInt gsetNumber) noexcept override; // SS2, SS3
void AcceptC1Controls(const bool enabled) override; // DECAC1
void SendC1Controls(const bool enabled) override; // S8C1T, S7C1T
void AnnounceCodeStructure(const VTInt ansiLevel) override; // ACS
void SoftReset() override; // DECSTR
void HardReset() override; // RIS
@ -289,6 +290,10 @@ namespace Microsoft::Console::VirtualTerminal
void _ReportTabStops();
StringHandler _RestoreTabStops();
void _ReturnCsiResponse(const std::wstring_view response) const;
void _ReturnDcsResponse(const std::wstring_view response) const;
void _ReturnOscResponse(const std::wstring_view response) const;
std::vector<uint8_t> _tabStopColumns;
bool _initDefaultTabStops = true;
@ -302,7 +307,6 @@ namespace Microsoft::Console::VirtualTerminal
std::shared_ptr<SixelParser> _sixelParser;
std::unique_ptr<FontBuffer> _fontBuffer;
std::shared_ptr<MacroBuffer> _macroBuffer;
std::optional<unsigned int> _initialCodePage;
// We have two instances of the saved cursor state, because we need
// one for the main buffer (at index 0), and another for the alt buffer

View File

@ -115,6 +115,7 @@ public:
void LockingShiftRight(const VTInt /*gsetNumber*/) override {} // LS1R, LS2R, LS3R
void SingleShift(const VTInt /*gsetNumber*/) override {} // SS2, SS3
void AcceptC1Controls(const bool /*enabled*/) override {} // DECAC1
void SendC1Controls(const bool /*enabled*/) override {} // S8C1T, S7C1T
void AnnounceCodeStructure(const VTInt /*ansiLevel*/) override {} // ACS
void SoftReset() override {} // DECSTR

View File

@ -158,17 +158,28 @@ public:
return true;
}
void SetConsoleOutputCP(const unsigned int codepage) override
void SetCodePage(const unsigned int codepage) override
{
Log::Comment(L"SetConsoleOutputCP MOCK called...");
THROW_HR_IF(E_FAIL, !_setConsoleOutputCPResult);
VERIFY_ARE_EQUAL(_expectedOutputCP, codepage);
Log::Comment(L"SetCodePage MOCK called...");
THROW_HR_IF(E_FAIL, !_setCodePageResult);
VERIFY_ARE_EQUAL(_expectedCodePage, codepage);
}
unsigned int GetConsoleOutputCP() const override
void ResetCodePage() override
{
Log::Comment(L"GetConsoleOutputCP MOCK called...");
return _expectedOutputCP;
Log::Comment(L"ResetCodePage MOCK called...");
}
unsigned int GetOutputCodePage() const override
{
Log::Comment(L"GetOutputCodePage MOCK called...");
return _expectedCodePage;
}
unsigned int GetInputCodePage() const override
{
Log::Comment(L"GetInputCodePage MOCK called...");
return _expectedCodePage;
}
void CopyToClipboard(const wil::zwstring_view /*content*/)
@ -367,7 +378,7 @@ public:
til::point _expectedCursorPos;
TextAttribute _expectedAttribute = {};
unsigned int _expectedOutputCP = 0;
unsigned int _expectedCodePage = 0;
bool _isPty = false;
bool _returnResponseResult = false;
@ -376,8 +387,7 @@ public:
bool _setWindowTitleResult = false;
std::wstring_view _expectedWindowTitle{};
bool _setConsoleOutputCPResult = false;
bool _getConsoleOutputCPResult = false;
bool _setCodePageResult = false;
bool _expectedShowWindow = false;
std::wstring _expectedMenuJson{};
@ -3529,15 +3539,15 @@ public:
Log::Comment(L"3. Designate ISO-2022 coding system");
// Code page should be set to ISO-8859-1 and C1 parsing enabled
_testGetSet->_setConsoleOutputCPResult = true;
_testGetSet->_expectedOutputCP = 28591;
_testGetSet->_setCodePageResult = true;
_testGetSet->_expectedCodePage = 28591;
_pDispatch->DesignateCodingSystem(DispatchTypes::CodingSystem::ISO2022);
VERIFY_IS_TRUE(_stateMachine->GetParserMode(StateMachine::Mode::AcceptC1));
Log::Comment(L"4. Designate UTF-8 coding system");
// Code page should be set to UTF-8 and C1 parsing disabled
_testGetSet->_setConsoleOutputCPResult = true;
_testGetSet->_expectedOutputCP = CP_UTF8;
_testGetSet->_setCodePageResult = true;
_testGetSet->_expectedCodePage = CP_UTF8;
_pDispatch->DesignateCodingSystem(DispatchTypes::CodingSystem::UTF8);
VERIFY_IS_FALSE(_stateMachine->GetParserMode(StateMachine::Mode::AcceptC1));
}
@ -3962,6 +3972,39 @@ public:
_pDispatch->PagePositionAbsolute(1);
}
TEST_METHOD(SendC1ControlTest)
{
const auto S7C1T = L"\033 F";
const auto S8C1T = L"\033 G";
_testGetSet->PrepData();
_testGetSet->_expectedCodePage = CP_UTF8;
Log::Comment(L"Generating reports with C1 control sequences");
_stateMachine->ProcessString(S8C1T);
_pDispatch->SecondaryDeviceAttributes();
_testGetSet->ValidateInputEvent(L"\x9b>0;10;1c");
_pDispatch->TertiaryDeviceAttributes();
_testGetSet->ValidateInputEvent(L"\x90!|00000000\x9c");
_pDispatch->RequestColorTableEntry(0);
_testGetSet->ValidateInputEvent(L"\x9d\x34;0;rgb:0c0c/0c0c/0c0c\x9c");
Log::Comment(L"Generating reports with 7-bit escape sequence");
_stateMachine->ProcessString(S7C1T);
_pDispatch->SecondaryDeviceAttributes();
_testGetSet->ValidateInputEvent(L"\x1b[>0;10;1c");
_pDispatch->TertiaryDeviceAttributes();
_testGetSet->ValidateInputEvent(L"\x1bP!|00000000\x1b\\");
_pDispatch->RequestColorTableEntry(0);
_testGetSet->ValidateInputEvent(L"\x1b]4;0;rgb:0c0c/0c0c/0c0c\x1b\\");
}
private:
TerminalInput _terminalInput;
std::unique_ptr<TestGetSet> _testGetSet;

View File

@ -36,6 +36,7 @@ public:
TEST_METHOD(CtrlNumTest);
TEST_METHOD(BackarrowKeyModeTest);
TEST_METHOD(AutoRepeatModeTest);
TEST_METHOD(SendC1ControlTest);
wchar_t GetModifierChar(const bool fShift, const bool fAlt, const bool fCtrl)
{
@ -790,3 +791,20 @@ void InputTest::AutoRepeatModeTest()
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down));
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(up));
}
void InputTest::SendC1ControlTest()
{
TerminalInput input;
Log::Comment(L"Sending keys with C1 control sequences");
input.SetInputMode(TerminalInput::Mode::SendC1, true);
TestKey(TerminalInput::MakeOutput(L"\x9bH"), input, 0, VK_HOME);
TestKey(TerminalInput::MakeOutput(L"\x8fP"), input, 0, VK_F1);
Log::Comment(L"Sending keys with 7-bit escape sequence");
input.SetInputMode(TerminalInput::Mode::SendC1, false);
TestKey(TerminalInput::MakeOutput(L"\x1b[H"), input, 0, VK_HOME);
TestKey(TerminalInput::MakeOutput(L"\x1bOP"), input, 0, VK_F1);
}

View File

@ -53,7 +53,7 @@ void TerminalInput::SetInputMode(const Mode mode, const bool enabled) noexcept
// If we've changed one of the modes that alter the VT input sequences,
// we'll need to regenerate our keyboard map.
static constexpr auto keyMapModes = til::enumset<Mode>{ Mode::LineFeed, Mode::Ansi, Mode::Keypad, Mode::CursorKey, Mode::BackarrowKey };
static constexpr auto keyMapModes = til::enumset<Mode>{ Mode::LineFeed, Mode::Ansi, Mode::Keypad, Mode::CursorKey, Mode::BackarrowKey, Mode::SendC1 };
if (keyMapModes.test(mode))
{
_initKeyboardMap();
@ -353,6 +353,19 @@ try
_keyMap.clear();
// The CSI and SS3 introducers are C1 control codes, which can either be
// sent as a single codepoint, or as a two character escape sequence.
if (_inputMode.test(Mode::SendC1))
{
_csi = L"\x9B";
_ss3 = L"\x8F";
}
else
{
_csi = L"\x1B[";
_ss3 = L"\x1BO";
}
// PAUSE doesn't have a VT mapping, but traditionally we've mapped it to ^Z,
// regardless of modifiers.
defineKeyWithUnusedModifiers(VK_PAUSE, L"\x1A"s);

View File

@ -33,6 +33,7 @@ namespace Microsoft::Console::VirtualTerminal
CursorKey,
BackarrowKey,
Win32,
SendC1,
Utf8MouseEncoding,
SgrMouseEncoding,
@ -80,10 +81,8 @@ namespace Microsoft::Console::VirtualTerminal
til::enumset<Mode> _inputMode{ Mode::Ansi, Mode::AutoRepeat, Mode::AlternateScroll };
bool _forceDisableWin32InputMode{ false };
// In the future, if we add support for "8-bit" input mode, these prefixes
// will sometimes be replaced with equivalent C1 control characters.
static constexpr auto _csi = L"\x1B[";
static constexpr auto _ss3 = L"\x1BO";
const wchar_t* _csi = L"\x1B[";
const wchar_t* _ss3 = L"\x1BO";
void _initKeyboardMap() noexcept;
DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key);
@ -108,9 +107,9 @@ namespace Microsoft::Console::VirtualTerminal
#pragma endregion
#pragma region MouseInput
[[nodiscard]] static OutputType _GenerateDefaultSequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
[[nodiscard]] static OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
[[nodiscard]] static OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isDown, bool isHover, short modifierKeyState, short delta);
[[nodiscard]] OutputType _GenerateDefaultSequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
[[nodiscard]] OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
[[nodiscard]] OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isDown, bool isHover, short modifierKeyState, short delta);
[[nodiscard]] OutputType _makeAlternateScrollOutput(short delta) const;

View File

@ -256,6 +256,12 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id)
case EscActionCodes::DECAC1_AcceptC1Controls:
_dispatch->AcceptC1Controls(true);
break;
case EscActionCodes::S7C1T_Send7bitC1Controls:
_dispatch->SendC1Controls(false);
break;
case EscActionCodes::S8C1T_Send8bitC1Controls:
_dispatch->SendC1Controls(true);
break;
case EscActionCodes::ACS_AnsiLevel1:
_dispatch->AnnounceCodeStructure(1);
break;

View File

@ -77,6 +77,8 @@ namespace Microsoft::Console::VirtualTerminal
LS2R_LockingShift = VTID("}"),
LS3R_LockingShift = VTID("|"),
DECAC1_AcceptC1Controls = VTID(" 7"),
S7C1T_Send7bitC1Controls = VTID(" F"),
S8C1T_Send8bitC1Controls = VTID(" G"),
ACS_AnsiLevel1 = VTID(" L"),
ACS_AnsiLevel2 = VTID(" M"),
ACS_AnsiLevel3 = VTID(" N"),

View File

@ -25,6 +25,9 @@ StateMachine::StateMachine(std::unique_ptr<IStateMachineEngine> engine, const bo
_oscString{},
_cachedSequence{ std::nullopt }
{
// The state machine must always accept C1 controls for the input engine,
// otherwise it won't work when the ConPTY terminal has S8C1T enabled.
_parserMode.set(Mode::AcceptC1, _isEngineForInput);
_ActionClear();
}