Skip to content

Commit d92c829

Browse files
authored
Add support for VT52 emulation (#4789)
## Summary of the Pull Request This PR adds support for the core VT52 commands, and implements the `DECANM` private mode sequence, which switches the terminal between ANSI mode and VT52-compatible mode. ## References PR #2017 defined the initial specification for VT52 support. PR #4044 removed the original VT52 cursor ops that conflicted with VT100 sequences. ## PR Checklist * [x] Closes #976 * [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA * [x] Tests added/passed * [ ] Requires documentation to be updated * [x] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #2017 ## Detailed Description of the Pull Request / Additional comments Most of the work involves updates to the parsing state machine, which behaves differently in VT52 mode. `CSI`, `OSC`, and `SS3` sequences are not applicable, and there is one special-case escape sequence (_Direct Cursor Address_), which requires an additional state to handle parameters that come _after_ the final character. Once the parsing is handled though, it's mostly just a matter of dispatching the commands to existing methods in the `ITermDispatch` interface. Only one new method was required in the interface to handle the _Identify_ command. The only real new functionality is in the `TerminalInput` class, which needs to generate different escape sequences for certain keys in VT52 mode. This does not yet support _all_ of the VT52 key sequences, because the VT100 support is itself not yet complete. But the basics are in place, and I think the rest is best left for a follow-up issue, and potentially a refactor of the `TerminalInput` class. I should point out that the original spec called for a new _Graphic Mode_ character set, but I've since discovered that the VT terminals that _emulate_ VT52 just use the existing VT100 _Special Graphics_ set, so that is really what we should be doing too. We can always consider adding the VT52 graphic set as a option later, if there is demand for strict VT52 compatibility. ## Validation Steps Performed I've added state machine and adapter tests to confirm that the `DECANM` mode changing sequences are correctly dispatched and forwarded to the `ConGetSet` handler. I've also added state machine tests that confirm the VT52 escape sequences are dispatched correctly when the ANSI mode is reset. For fuzzing support, I've extended the VT command fuzzer to generate the different kinds of VT52 sequences, as well as mode change sequences to switch between the ANSI and VT52 modes. In terms of manual testing, I've confirmed that the _Test of VT52 mode_ in Vttest now works as expected.
1 parent 44dcc86 commit d92c829

21 files changed

+592
-21
lines changed

src/host/outputStream.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,23 @@ bool ConhostInternalGetSet::PrivateSetKeypadMode(const bool fApplicationMode)
239239
return NT_SUCCESS(DoSrvPrivateSetKeypadMode(fApplicationMode));
240240
}
241241

242+
// Routine Description:
243+
// - Sets the terminal emulation mode to either ANSI-compatible or VT52.
244+
// PrivateSetAnsiMode is an internal-only "API" call that the vt commands can execute,
245+
// but it is not represented as a function call on out public API surface.
246+
// Arguments:
247+
// - ansiMode - set to true to enable the ANSI mode, false for VT52 mode.
248+
// Return Value:
249+
// - true if successful. false otherwise.
250+
bool ConhostInternalGetSet::PrivateSetAnsiMode(const bool ansiMode)
251+
{
252+
auto& stateMachine = _io.GetActiveOutputBuffer().GetStateMachine();
253+
stateMachine.SetAnsiMode(ansiMode);
254+
auto& terminalInput = _io.GetActiveInputBuffer()->GetTerminalInput();
255+
terminalInput.ChangeAnsiMode(ansiMode);
256+
return true;
257+
}
258+
242259
// Routine Description:
243260
// - Connects the PrivateSetScreenMode call directly into our Driver Message servicing call inside Conhost.exe
244261
// PrivateSetScreenMode is an internal-only "API" call that the vt commands can execute,

src/host/outputStream.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
7474
bool PrivateSetCursorKeysMode(const bool applicationMode) override;
7575
bool PrivateSetKeypadMode(const bool applicationMode) override;
7676

77+
bool PrivateSetAnsiMode(const bool ansiMode) override;
7778
bool PrivateSetScreenMode(const bool reverseMode) override;
7879
bool PrivateSetAutoWrapMode(const bool wrapAtEOL) override;
7980

src/terminal/adapter/DispatchTypes.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
8282
enum PrivateModeParams : unsigned short
8383
{
8484
DECCKM_CursorKeysMode = 1,
85+
DECANM_AnsiMode = 2,
8586
DECCOLM_SetNumberOfColumns = 3,
8687
DECSCNM_ScreenMode = 5,
8788
DECOM_OriginMode = 6,

src/terminal/adapter/ITermDispatch.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
5454
virtual bool SetCursorKeysMode(const bool applicationMode) = 0; // DECCKM
5555
virtual bool SetKeypadMode(const bool applicationMode) = 0; // DECKPAM, DECKPNM
5656
virtual bool EnableCursorBlinking(const bool enable) = 0; // ATT610
57+
virtual bool SetAnsiMode(const bool ansiMode) = 0; // DECANM
5758
virtual bool SetScreenMode(const bool reverseMode) = 0; // DECSCNM
5859
virtual bool SetOriginMode(const bool relativeMode) = 0; // DECOM
5960
virtual bool SetAutoWrapMode(const bool wrapAtEOL) = 0; // DECAWM
@@ -92,6 +93,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
9293

9394
virtual bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) = 0; // DSR, DSR-OS, DSR-CPR
9495
virtual bool DeviceAttributes() = 0; // DA1
96+
virtual bool Vt52DeviceAttributes() = 0; // VT52 Identify
9597

9698
virtual bool DesignateCharset(const wchar_t wchCharset) = 0; // SCS
9799

src/terminal/adapter/adaptDispatch.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,19 @@ bool AdaptDispatch::DeviceAttributes()
714714
return _WriteResponse(L"\x1b[?1;0c");
715715
}
716716

717+
// Routine Description:
718+
// - VT52 Identify - Reports the identity of the terminal in VT52 emulation mode.
719+
// An actual VT52 terminal would typically identify itself with ESC / K.
720+
// But for a terminal that is emulating a VT52, the sequence should be ESC / Z.
721+
// Arguments:
722+
// - <none>
723+
// Return Value:
724+
// - True if handled successfully. False otherwise.
725+
bool AdaptDispatch::Vt52DeviceAttributes()
726+
{
727+
return _WriteResponse(L"\x1b/Z");
728+
}
729+
717730
// Routine Description:
718731
// - DSR-OS - Reports the operating status back to the input channel
719732
// Arguments:
@@ -956,6 +969,9 @@ bool AdaptDispatch::_PrivateModeParamsHelper(const DispatchTypes::PrivateModePar
956969
// set - Enable Application Mode, reset - Normal mode
957970
success = SetCursorKeysMode(enable);
958971
break;
972+
case DispatchTypes::PrivateModeParams::DECANM_AnsiMode:
973+
success = SetAnsiMode(enable);
974+
break;
959975
case DispatchTypes::PrivateModeParams::DECCOLM_SetNumberOfColumns:
960976
success = _DoDECCOLMHelper(enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns);
961977
break;
@@ -1129,6 +1145,20 @@ bool AdaptDispatch::DeleteLine(const size_t distance)
11291145
return _pConApi->DeleteLines(distance);
11301146
}
11311147

1148+
// - DECANM - Sets the terminal emulation mode to either ANSI-compatible or VT52.
1149+
// Arguments:
1150+
// - ansiMode - set to true to enable the ANSI mode, false for VT52 mode.
1151+
// Return Value:
1152+
// - True if handled successfully. False otherwise.
1153+
bool AdaptDispatch::SetAnsiMode(const bool ansiMode)
1154+
{
1155+
// When an attempt is made to update the mode, the designated character sets
1156+
// need to be reset to defaults, even if the mode doesn't actually change.
1157+
_termOutput = {};
1158+
1159+
return _pConApi->PrivateSetAnsiMode(ansiMode);
1160+
}
1161+
11321162
// Routine Description:
11331163
// - DECSCNM - Sets the screen mode to either normal or reverse.
11341164
// When in reverse screen mode, the background and foreground colors are switched.

src/terminal/adapter/adaptDispatch.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ namespace Microsoft::Console::VirtualTerminal
5858
bool SetGraphicsRendition(const std::basic_string_view<DispatchTypes::GraphicsOptions> options) override; // SGR
5959
bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) override; // DSR, DSR-OS, DSR-CPR
6060
bool DeviceAttributes() override; // DA1
61+
bool Vt52DeviceAttributes() override; // VT52 Identify
6162
bool ScrollUp(const size_t distance) override; // SU
6263
bool ScrollDown(const size_t distance) override; // SD
6364
bool InsertLine(const size_t distance) override; // IL
@@ -68,6 +69,7 @@ namespace Microsoft::Console::VirtualTerminal
6869
bool SetCursorKeysMode(const bool applicationMode) override; // DECCKM
6970
bool SetKeypadMode(const bool applicationMode) override; // DECKPAM, DECKPNM
7071
bool EnableCursorBlinking(const bool enable) override; // ATT610
72+
bool SetAnsiMode(const bool ansiMode) override; // DECANM
7173
bool SetScreenMode(const bool reverseMode) override; // DECSCNM
7274
bool SetOriginMode(const bool relativeMode) noexcept override; // DECOM
7375
bool SetAutoWrapMode(const bool wrapAtEOL) override; // DECAWM

src/terminal/adapter/conGetSet.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ namespace Microsoft::Console::VirtualTerminal
4646
virtual bool PrivateSetCursorKeysMode(const bool applicationMode) = 0;
4747
virtual bool PrivateSetKeypadMode(const bool applicationMode) = 0;
4848

49+
virtual bool PrivateSetAnsiMode(const bool ansiMode) = 0;
4950
virtual bool PrivateSetScreenMode(const bool reverseMode) = 0;
5051
virtual bool PrivateSetAutoWrapMode(const bool wrapAtEOL) = 0;
5152

src/terminal/adapter/termDispatch.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
4848
bool SetCursorKeysMode(const bool /*applicationMode*/) noexcept override { return false; } // DECCKM
4949
bool SetKeypadMode(const bool /*applicationMode*/) noexcept override { return false; } // DECKPAM, DECKPNM
5050
bool EnableCursorBlinking(const bool /*enable*/) noexcept override { return false; } // ATT610
51+
bool SetAnsiMode(const bool /*ansiMode*/) noexcept override { return false; } // DECANM
5152
bool SetScreenMode(const bool /*reverseMode*/) noexcept override { return false; } // DECSCNM
5253
bool SetOriginMode(const bool /*relativeMode*/) noexcept override { return false; }; // DECOM
5354
bool SetAutoWrapMode(const bool /*wrapAtEOL*/) noexcept override { return false; }; // DECAWM
@@ -86,6 +87,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
8687

8788
bool DeviceStatusReport(const DispatchTypes::AnsiStatusType /*statusType*/) noexcept override { return false; } // DSR, DSR-OS, DSR-CPR
8889
bool DeviceAttributes() noexcept override { return false; } // DA1
90+
bool Vt52DeviceAttributes() noexcept override { return false; } // VT52 Identify
8991

9092
bool DesignateCharset(const wchar_t /*wchCharset*/) noexcept override { return false; } // SCS
9193

src/terminal/adapter/ut_adapter/adapterTest.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,18 @@ class TestGetSet final : public ConGetSet
162162
return _privateSetKeypadModeResult;
163163
}
164164

165+
bool PrivateSetAnsiMode(const bool ansiMode) override
166+
{
167+
Log::Comment(L"PrivateSetAnsiMode MOCK called...");
168+
169+
if (_privateSetAnsiModeResult)
170+
{
171+
VERIFY_ARE_EQUAL(_expectedAnsiMode, ansiMode);
172+
}
173+
174+
return _privateSetAnsiModeResult;
175+
}
176+
165177
bool PrivateSetScreenMode(const bool /*reverseMode*/) override
166178
{
167179
Log::Comment(L"PrivateSetScreenMode MOCK called...");
@@ -744,6 +756,8 @@ class TestGetSet final : public ConGetSet
744756
bool _privateSetKeypadModeResult = false;
745757
bool _cursorKeysApplicationMode = false;
746758
bool _keypadApplicationMode = false;
759+
bool _privateSetAnsiModeResult = false;
760+
bool _expectedAnsiMode = false;
747761
bool _privateAllowCursorBlinkingResult = false;
748762
bool _enable = false; // for cursor blinking
749763
bool _privateSetScrollingRegionResult = false;
@@ -1691,6 +1705,26 @@ class AdapterTest
16911705
VERIFY_IS_TRUE(_pDispatch.get()->SetKeypadMode(true));
16921706
}
16931707

1708+
TEST_METHOD(AnsiModeTest)
1709+
{
1710+
Log::Comment(L"Starting test...");
1711+
1712+
// success cases
1713+
// set ansi mode = true
1714+
Log::Comment(L"Test 1: ansi mode = true");
1715+
_testGetSet->_privateSetAnsiModeResult = true;
1716+
_testGetSet->_expectedAnsiMode = true;
1717+
1718+
VERIFY_IS_TRUE(_pDispatch.get()->SetAnsiMode(true));
1719+
1720+
// set ansi mode = false
1721+
Log::Comment(L"Test 2: ansi mode = false.");
1722+
_testGetSet->_privateSetAnsiModeResult = true;
1723+
_testGetSet->_expectedAnsiMode = false;
1724+
1725+
VERIFY_IS_TRUE(_pDispatch.get()->SetAnsiMode(false));
1726+
}
1727+
16941728
TEST_METHOD(AllowBlinkingTest)
16951729
{
16961730
Log::Comment(L"Starting test...");

src/terminal/input/terminalInput.cpp

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ static constexpr std::array<TermKeyMap, 6> s_cursorKeysApplicationMapping{
6868
TermKeyMap{ VK_END, L"\x1bOF" },
6969
};
7070

71+
static constexpr std::array<TermKeyMap, 6> s_cursorKeysVt52Mapping{
72+
TermKeyMap{ VK_UP, L"\033A" },
73+
TermKeyMap{ VK_DOWN, L"\033B" },
74+
TermKeyMap{ VK_RIGHT, L"\033C" },
75+
TermKeyMap{ VK_LEFT, L"\033D" },
76+
TermKeyMap{ VK_HOME, L"\033H" },
77+
TermKeyMap{ VK_END, L"\033F" },
78+
};
79+
7180
static constexpr std::array<TermKeyMap, 20> s_keypadNumericMapping{
7281
TermKeyMap{ VK_TAB, L"\x09" },
7382
TermKeyMap{ VK_BACK, L"\x7f" },
@@ -150,6 +159,29 @@ static constexpr std::array<TermKeyMap, 20> s_keypadApplicationMapping{
150159
// TermKeyMap{ VK_TAB, L"\x1bOI" }, // So I left them here as a reference just in case.
151160
};
152161

162+
static constexpr std::array<TermKeyMap, 20> s_keypadVt52Mapping{
163+
TermKeyMap{ VK_TAB, L"\x09" },
164+
TermKeyMap{ VK_BACK, L"\x7f" },
165+
TermKeyMap{ VK_PAUSE, L"\x1a" },
166+
TermKeyMap{ VK_ESCAPE, L"\x1b" },
167+
TermKeyMap{ VK_INSERT, L"\x1b[2~" },
168+
TermKeyMap{ VK_DELETE, L"\x1b[3~" },
169+
TermKeyMap{ VK_PRIOR, L"\x1b[5~" },
170+
TermKeyMap{ VK_NEXT, L"\x1b[6~" },
171+
TermKeyMap{ VK_F1, L"\x1bP" },
172+
TermKeyMap{ VK_F2, L"\x1bQ" },
173+
TermKeyMap{ VK_F3, L"\x1bR" },
174+
TermKeyMap{ VK_F4, L"\x1bS" },
175+
TermKeyMap{ VK_F5, L"\x1b[15~" },
176+
TermKeyMap{ VK_F6, L"\x1b[17~" },
177+
TermKeyMap{ VK_F7, L"\x1b[18~" },
178+
TermKeyMap{ VK_F8, L"\x1b[19~" },
179+
TermKeyMap{ VK_F9, L"\x1b[20~" },
180+
TermKeyMap{ VK_F10, L"\x1b[21~" },
181+
TermKeyMap{ VK_F11, L"\x1b[23~" },
182+
TermKeyMap{ VK_F12, L"\x1b[24~" },
183+
};
184+
153185
// Sequences to send when a modifier is pressed with any of these keys
154186
// Basically, the 'm' will be replaced with a character indicating which
155187
// modifier keys are pressed.
@@ -220,6 +252,11 @@ const wchar_t* const CTRL_QUESTIONMARK_SEQUENCE = L"\x7F";
220252
const wchar_t* const CTRL_ALT_SLASH_SEQUENCE = L"\x1b\x1f";
221253
const wchar_t* const CTRL_ALT_QUESTIONMARK_SEQUENCE = L"\x1b\x7F";
222254

255+
void TerminalInput::ChangeAnsiMode(const bool ansiMode) noexcept
256+
{
257+
_ansiMode = ansiMode;
258+
}
259+
223260
void TerminalInput::ChangeKeypadMode(const bool applicationMode) noexcept
224261
{
225262
_keypadApplicationMode = applicationMode;
@@ -231,29 +268,44 @@ void TerminalInput::ChangeCursorKeysMode(const bool applicationMode) noexcept
231268
}
232269

233270
static const std::basic_string_view<TermKeyMap> _getKeyMapping(const KeyEvent& keyEvent,
271+
const bool ansiMode,
234272
const bool cursorApplicationMode,
235273
const bool keypadApplicationMode) noexcept
236274
{
237-
if (keyEvent.IsCursorKey())
275+
if (ansiMode)
238276
{
239-
if (cursorApplicationMode)
277+
if (keyEvent.IsCursorKey())
240278
{
241-
return { s_cursorKeysApplicationMapping.data(), s_cursorKeysApplicationMapping.size() };
279+
if (cursorApplicationMode)
280+
{
281+
return { s_cursorKeysApplicationMapping.data(), s_cursorKeysApplicationMapping.size() };
282+
}
283+
else
284+
{
285+
return { s_cursorKeysNormalMapping.data(), s_cursorKeysNormalMapping.size() };
286+
}
242287
}
243288
else
244289
{
245-
return { s_cursorKeysNormalMapping.data(), s_cursorKeysNormalMapping.size() };
290+
if (keypadApplicationMode)
291+
{
292+
return { s_keypadApplicationMapping.data(), s_keypadApplicationMapping.size() };
293+
}
294+
else
295+
{
296+
return { s_keypadNumericMapping.data(), s_keypadNumericMapping.size() };
297+
}
246298
}
247299
}
248300
else
249301
{
250-
if (keypadApplicationMode)
302+
if (keyEvent.IsCursorKey())
251303
{
252-
return { s_keypadApplicationMapping.data(), s_keypadApplicationMapping.size() };
304+
return { s_cursorKeysVt52Mapping.data(), s_cursorKeysVt52Mapping.size() };
253305
}
254306
else
255307
{
256-
return { s_keypadNumericMapping.data(), s_keypadNumericMapping.size() };
308+
return { s_keypadVt52Mapping.data(), s_keypadVt52Mapping.size() };
257309
}
258310
}
259311
}
@@ -560,7 +612,7 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent)
560612
}
561613

562614
// Check any other key mappings (like those for the F1-F12 keys).
563-
const auto mapping = _getKeyMapping(keyEvent, _cursorApplicationMode, _keypadApplicationMode);
615+
const auto mapping = _getKeyMapping(keyEvent, _ansiMode, _cursorApplicationMode, _keypadApplicationMode);
564616
if (_translateDefaultMapping(keyEvent, mapping, senderFunc))
565617
{
566618
return true;

0 commit comments

Comments
 (0)