Repository: microsoft/terminal (language: cpp)

Issue:
Complete the 8-bit interface architecture
# Description of the new feature/enhancement

This is a DEC extension which introduced the concept of 96-character graphic sets (ISO Latin-1 being the first of those), and allowed for replacing the DEC supplemental set with Latin-1. That became known as the user-preference supplemental set (UPSS), and later models allowed you to replace it with other character sets as well.

We already support much of this functionality, but what we're missing is the `DECAUPSS` sequence which handles the UPSS character set assignment, and the `DECRQUPSS` sequence, which queries the active UPSS assignment. Also a couple of sequences which are used to initialize the G-set and GL/GR mappings for ANSI conformance.

I suspect this functionality isn't widely used, so I can understand if you don't want to include it, but it is a level 3 conformance requirement. It is also implemented by XTerm, so you can think of it as an improvement in our XTerm compatibility.

# Proposed technical implementation details (optional)

In the `TerminalOutput` class we'll need a new `wstring_view` field to track the active UPSS translation table, and then whenever someone tries to designate character set ID `<`, we'll use that value instead of `Latin1`.


Discussion / hints:

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 6ae7a458582..abeb7a386a3 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -426,6 +426,7 @@ DECRQM
 DECRQPSR
 DECRQSS
 DECRQTSR
+DECRQUPSS
 DECRSPS
 decrst
 DECSACE
@@ -1892,6 +1893,7 @@ UPDATEDISPLAY
 UPDOWN
 UPKEY
 UPSS
+upss
 uregex
 URegular
 usebackq
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 612f82c093f..d9e5b075542 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -626,7 +626,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         FullCell = 2
     };
 
-    enum class DrcsCharsetSize : VTInt
+    enum class CharsetSize : VTInt
     {
         Size94 = 0,
         Size96 = 1
diff --git a/src/terminal/adapter/FontBuffer.cpp b/src/terminal/adapter/FontBuffer.cpp
index c7f205f5a10..c1962b8190d 100644
--- a/src/terminal/adapter/FontBuffer.cpp
+++ b/src/terminal/adapter/FontBuffer.cpp
@@ -153,14 +153,14 @@ bool FontBuffer::SetAttributes(const DispatchTypes::DrcsCellMatrix cellMatrix,
 }
 
 bool FontBuffer::SetStartChar(const VTParameter startChar,
-                              const DispatchTypes::DrcsCharsetSize charsetSize) noexcept
+                              const DispatchTypes::CharsetSize charsetSize) noexcept
 {
     switch (charsetSize)
     {
-    case DispatchTypes::DrcsCharsetSize::Size94:
+    case DispatchTypes::CharsetSize::Size94:
         _startChar = startChar.value_or(1);
         break;
-    case DispatchTypes::DrcsCharsetSize::Size96:
+    case DispatchTypes::CharsetSize::Size96:
         _startChar = startChar.value_or(0);
         break;
     default:
diff --git a/src/terminal/adapter/FontBuffer.hpp b/src/terminal/adapter/FontBuffer.hpp
index f520c7bc958..0141dbf1996 100644
--- a/src/terminal/adapter/FontBuffer.hpp
+++ b/src/terminal/adapter/FontBuffer.hpp
@@ -26,7 +26,7 @@ namespace Microsoft::Console::VirtualTerminal
                            const DispatchTypes::DrcsFontSet fontSet,
                            const DispatchTypes::DrcsFontUsage fontUsage) noexcept;
         bool SetStartChar(const VTParameter startChar,
-                          const DispatchTypes::DrcsCharsetSize charsetSize) noexcept;
+                          const DispatchTypes::CharsetSize charsetSize) noexcept;
         void AddSixelData(const wchar_t ch);
         bool FinalizeSixelData();
 
@@ -75,8 +75,8 @@ namespace Microsoft::Console::VirtualTerminal
         VTInt _columnsPerPage;
         bool _isTextFont;
 
-        DispatchTypes::DrcsCharsetSize _charsetSize;
-        DispatchTypes::DrcsCharsetSize _pendingCharsetSize;
+        DispatchTypes::CharsetSize _charsetSize;
+        DispatchTypes::CharsetSize _pendingCharsetSize;
         VTID _charsetId{ 0 };
         VTID _pendingCharsetId{ 0 };
         bool _charsetIdInitialized;
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index 9221ea3cb4f..1e94ddeb0fc 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -113,6 +113,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual bool LockingShiftRight(const VTInt gsetNumber) = 0; // LS1R, LS2R, LS3R
     virtual bool SingleShift(const VTInt gsetNumber) = 0; // SS2, SS3
     virtual bool AcceptC1Controls(const bool enabled) = 0; // DECAC1
+    virtual bool AnnounceCodeStructure(const VTInt ansiLevel) = 0; // ACS
 
     virtual bool SoftReset() = 0; // DECSTR
     virtual bool HardReset() = 0; // RIS
@@ -146,7 +147,10 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
                                        const DispatchTypes::DrcsFontSet fontSet,
                                        const DispatchTypes::DrcsFontUsage fontUsage,
                                        const VTParameter cellHeight,
-                                       const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD
+                                       const DispatchTypes::CharsetSize charsetSize) = 0; // DECDLD
+
+    virtual bool RequestUserPreferenceCharset() = 0; // DECRQUPSS
+    virtual StringHandler AssignUserPreferenceCharset(const DispatchTypes::CharsetSize charsetSize) = 0; // DECAUPSS
 
     virtual StringHandler DefineMacro(const VTInt macroId,
                                       const DispatchTypes::MacroDeleteControl deleteControl,
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 31edacae21d..84dbd1ef490 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -524,8 +524,6 @@ bool AdaptDispatch::CursorSaveState()
     savedCursorState.IsOriginModeRelative = _modes.test(Mode::Origin);
     savedCursorState.Attributes = attributes;
     savedCursorState.TermOutput = _termOutput;
-    savedCursorState.C1ControlsAccepted = _api.GetStateMachine().GetParserMode(StateMachine::Mode::AcceptC1);
-    savedCursorState.CodePage = _api.GetConsoleOutputCP();
 
     return true;
 }
@@ -557,17 +555,8 @@ bool AdaptDispatch::CursorRestoreState()
     // Restore text attributes.
     _api.SetTextAttributes(savedCursorState.Attributes);
 
-    // Restore designated character set.
-    _termOutput = savedCursorState.TermOutput;
-
-    // Restore the parsing state of C1 control codes.
-    AcceptC1Controls(savedCursorState.C1ControlsAccepted);
-
-    // Restore the code page if it was previously saved.
-    if (savedCursorState.CodePage != 0)
-    {
-        _api.SetConsoleOutputCP(savedCursorState.CodePage);
-    }
+    // Restore designated character sets.
+    _termOutput.RestoreFrom(savedCursorState.TermOutput);
 
     return true;
 }
@@ -1511,21 +1500,22 @@ bool AdaptDispatch::DeviceAttributes()
     // 1 = 132 column mode (ConHost only)
     // 6 = Selective erase
     // 7 = Soft fonts
+    // 14 = 8-bit interface architecture
     // 21 = Horizontal scrolling
     // 22 = Color text
     // 23 = Greek character sets
     // 24 = Turkish character sets
     // 28 = Rectangular area operations
     // 32 = Text macros
-    // 42 = ISO Latin - 2 character set
+    // 42 = ISO Latin-2 character set
 
     if (_api.IsConsolePty())
     {
-        _api.ReturnResponse(L"\x1b[?61;6;7;21;22;23;24;28;32;42c");
+        _api.ReturnResponse(L"\x1b[?61;6;7;14;21;22;23;24;28;32;42c");
     }
     else
     {
-        _api.ReturnResponse(L"\x1b[?61;1;6;7;21;22;23;24;28;32;42c");
+        _api.ReturnResponse(L"\x1b[?61;1;6;7;14;21;22;23;24;28;32;42c");
     }
     return true;
 }
@@ -2204,7 +2194,7 @@ bool AdaptDispatch::SetAnsiMode(const bool ansiMode)
 {
     // When an attempt is made to update the mode, the designated character sets
     // need to be reset to defaults, even if the mode doesn't actually change.
-    _termOutput = {};
+    _termOutput.SoftReset();
 
     _api.GetStateMachine().SetParserMode(StateMachine::Mode::Ansi, ansiMode);
     _terminalInput.SetInputMode(TerminalInput::Mode::Ansi, ansiMode);
@@ -2944,6 +2934,34 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled)
     return true;
 }
 
+//Routine Description:
+// ACS - Announces the ANSI conformance level for subsequent data exchange.
+//  This requires certain character sets to be mapped into the terminal's
+//  G-sets and in-use tables.
+//Arguments:
+// - ansiLevel - the expected conformance level
+// Return value:
+// - True if handled successfully. False otherwise.
+bool AdaptDispatch::AnnounceCodeStructure(const VTInt ansiLevel)
+{
+    // Levels 1 and 2 require ASCII in G0/GL and Latin-1 in G1/GR.
+    // Level 3 only requires ASCII in G0/GL.
+    switch (ansiLevel)
+    {
+    case 1:
+    case 2:
+        Designate96Charset(1, VTID("A")); // Latin-1 designated as G1
+        LockingShiftRight(1); // G1 mapped into GR
+        [[fallthrough]];
+    case 3:
+        Designate94Charset(0, VTID("B")); // ASCII designated as G0
+        LockingShift(0); // G0 mapped into GL
+        return true;
+    default:
+        return false;
+    }
+}
+
 //Routine Description:
 // Soft Reset - Perform a soft reset. See http://www.vt100.net/docs/vt510-rm/DECSTR.html
 // The following table lists everything that should be done, 'X's indicate the ones that
@@ -2964,7 +2982,7 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled)
 //  X Select graphic rendition    SGR         Normal rendition.
 //  X Select character attribute  DECSCA      Normal (erasable by DECSEL and DECSED).
 //  X Save cursor state           DECSC       Home position.
-//    Assign user preference      DECAUPSS    Set selected in Set-Up.
+//  X Assign user preference      DECAUPSS    Always Latin-1 (not configurable).
 //        supplemental set
 //    Select active               DECSASD     Main display.
 //        status display
@@ -2991,14 +3009,7 @@ bool AdaptDispatch::SoftReset()
     // Left margin = 1; right margin = page width.
     _DoSetLeftRightScrollingMargins(0, 0);
 
-    _termOutput = {}; // Reset all character set designations.
-    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.
-    AcceptC1Controls(false);
+    _termOutput.SoftReset(); // Reset all character set designations.
 
     SetGraphicsRendition({}); // Normal rendition.
     SetCharacterProtectionAttribute({}); // Default (unprotected)
@@ -3009,6 +3020,12 @@ bool AdaptDispatch::SoftReset()
     _savedCursorState.at(0) = {}; // Main buffer
     _savedCursorState.at(1) = {}; // Alt buffer
 
+    // The TerminalOutput state in these buffers must be reset to
+    // the same state as the _termOutput instance, which is not
+    // necessarily equivalent to a full reset.
+    _savedCursorState.at(0).TermOutput = _termOutput;
+    _savedCursorState.at(1).TermOutput = _termOutput;
+
     return !_api.IsConsolePty();
 }
 
@@ -3043,6 +3060,16 @@ bool AdaptDispatch::HardReset()
         _usingAltBuffer = false;
     }
 
+    // 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.
+    AcceptC1Controls(false);
+
     // Sets the SGR state to normal - this must be done before EraseInDisplay
     //      to ensure that it clears with the default background color.
     SoftReset();
@@ -3860,7 +3887,7 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
                                                          const DispatchTypes::DrcsFontSet fontSet,
                                                          const DispatchTypes::DrcsFontUsage fontUsage,
                                                          const VTParameter cellHeight,
-                                                         const DispatchTypes::DrcsCharsetSize charsetSize)
+                                                         const DispatchTypes::CharsetSize charsetSize)
 {
     // The font buffer is created on demand.
     if (!_fontBuffer)
@@ -3907,7 +3934,7 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
             // We also need to inform the character set mapper of the ID that
             // will map to this font (we only support one font buffer so there
             // will only ever be one active dynamic character set).
-            if (charsetSize == DispatchTypes::DrcsCharsetSize::Size96)
+            if (charsetSize == DispatchTypes::CharsetSize::Size96)
             {
                 _termOutput.SetDrcs96Designation(_fontBuffer->GetDesignation());
             }
@@ -3932,7 +3959,7 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
 // - <none>
 // Return value:
 // - a function to receive the data or nullptr if the initial flush fails
-ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize)
+ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const DispatchTypes::CharsetSize charsetSize)
 {
     const auto defaultPassthrough = _CreatePassthroughHandler();
     if (defaultPassthrough)
@@ -3955,7 +3982,7 @@ ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const
             {
                 // Once the DECDLD sequence is finished, we also output an SCS
                 // sequence to map the character set into the G1 table.
-                const auto charset96 = charsetSize == DispatchTypes::DrcsCharsetSize::Size96;
+                const auto charset96 = charsetSize == DispatchTypes::CharsetSize::Size96;
                 engine.ActionPassThroughString(charset96 ? L"\033-@" : L"\033)@");
             }
             return true;
@@ -3964,6 +3991,51 @@ ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const
     return nullptr;
 }
 
+// Method Description:
+// - DECRQUPSS - Request the user-preference supplemental character set.
+// Arguments:
+// - None
+// Return Value:
+// - True
+bool 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.ToString()));
+    return true;
+}
+
+// Method Description:
+// - DECAUPSS - Assigns the user-preference supplemental character set.
+// Arguments:
+// - charsetSize - Whether the character set is 94 or 96 characters.
+// Return Value:
+// - a function to parse the character set ID
+ITermDispatch::StringHandler AdaptDispatch::AssignUserPreferenceCharset(const DispatchTypes::CharsetSize charsetSize)
+{
+    return [this, charsetSize, idBuilder = VTIDBuilder{}](const auto ch) mutable {
+        if (ch >= L'\x20' && ch <= L'\x2f')
+        {
+            idBuilder.AddIntermediate(ch);
+        }
+        else if (ch >= L'\x30' && ch <= L'\x7e')
+        {
+            const auto id = idBuilder.Finalize(ch);
+            switch (charsetSize)
+            {
+            case DispatchTypes::CharsetSize::Size94:
+                _termOutput.AssignUserPreferenceCharset(id, false);
+                break;
+            case DispatchTypes::CharsetSize::Size96:
+                _termOutput.AssignUserPreferenceCharset(id, true);
+                break;
+            }
+            return false;
+        }
+        return true;
+    };
+}
+
 // Method Description:
 // - DECDMAC - Defines a string of characters as a macro that can later be
 //   invoked with a DECINVM sequence.
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 4b145f1b8ca..3b3591f1b1e 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -112,6 +112,7 @@ namespace Microsoft::Console::VirtualTerminal
         bool LockingShiftRight(const VTInt gsetNumber) override; // LS1R, LS2R, LS3R
         bool SingleShift(const VTInt gsetNumber) noexcept override; // SS2, SS3
         bool AcceptC1Controls(const bool enabled) override; // DECAC1
+        bool AnnounceCodeStructure(const VTInt ansiLevel) override; // ACS
         bool SoftReset() override; // DECSTR
         bool HardReset() override; // RIS
         bool ScreenAlignmentPattern() override; // DECALN
@@ -148,7 +149,10 @@ namespace Microsoft::Console::VirtualTerminal
                                    const DispatchTypes::DrcsFontSet fontSet,
                                    const DispatchTypes::DrcsFontUsage fontUsage,
                                    const VTParameter cellHeight,
-                                   const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD
+                                   const DispatchTypes::CharsetSize charsetSize) override; // DECDLD
+
+        bool RequestUserPreferenceCharset() override; // DECRQUPSS
+        StringHandler AssignUserPreferenceCharset(const DispatchTypes::CharsetSize charsetSize) override; // DECAUPSS
 
         StringHandler DefineMacro(const VTInt macroId,
                                   const DispatchTypes::MacroDeleteControl deleteControl,
@@ -188,8 +192,6 @@ namespace Microsoft::Console::VirtualTerminal
             bool IsOriginModeRelative = false;
             TextAttribute Attributes = {};
             TerminalOutput TermOutput = {};
-            bool C1ControlsAccepted = false;
-            unsigned int CodePage = 0;
         };
         struct Offset
         {
@@ -268,7 +270,7 @@ namespace Microsoft::Console::VirtualTerminal
         void _ReportTabStops();
         StringHandler _RestoreTabStops();
 
-        StringHandler _CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize);
+        StringHandler _CreateDrcsPassthroughHandler(const DispatchTypes::CharsetSize charsetSize);
         StringHandler _CreatePassthroughHandler();
 
         std::vector<bool> _tabStopColumns;
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 4f11ae8bb93..de3da573121 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -106,6 +106,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool LockingShiftRight(const VTInt /*gsetNumber*/) override { return false; } // LS1R, LS2R, LS3R
     bool SingleShift(const VTInt /*gsetNumber*/) override { return false; } // SS2, SS3
     bool AcceptC1Controls(const bool /*enabled*/) override { return false; } // DECAC1
+    bool AnnounceCodeStructure(const VTInt /*ansiLevel*/) override { return false; } // ACS
 
     bool SoftReset() override { return false; } // DECSTR
     bool HardReset() override { return false; } // RIS
@@ -139,7 +140,10 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
                                const DispatchTypes::DrcsFontSet /*fontSet*/,
                                const DispatchTypes::DrcsFontUsage /*fontUsage*/,
                                const VTParameter /*cellHeight*/,
-                               const DispatchTypes::DrcsCharsetSize /*charsetSize*/) override { return nullptr; } // DECDLD
+                               const DispatchTypes::CharsetSize /*charsetSize*/) override { return nullptr; } // DECDLD
+
+    bool RequestUserPreferenceCharset() override { return false; } // DECRQUPSS
+    StringHandler AssignUserPreferenceCharset(const DispatchTypes::CharsetSize /*charsetSize*/) override { return nullptr; } // DECAUPSS
 
     StringHandler DefineMacro(const VTInt /*macroId*/,
                               const DispatchTypes::MacroDeleteControl /*deleteControl*/,
diff --git a/src/terminal/adapter/terminalOutput.cpp b/src/terminal/adapter/terminalOutput.cpp
index f18f3512c64..4f39fc3558e 100644
--- a/src/terminal/adapter/terminalOutput.cpp
+++ b/src/terminal/adapter/terminalOutput.cpp
@@ -8,22 +8,73 @@
 
 using namespace Microsoft::Console::VirtualTerminal;
 
-TerminalOutput::TerminalOutput() noexcept
+TerminalOutput::TerminalOutput(const bool grEnabled) noexcept :
+    _upssId{ VTID("A") },
+    _upssTranslationTable{ Latin1 },
+    _grTranslationEnabled{ grEnabled }
 {
     // By default we set all of the G-sets to ASCII, so if someone accidentally
-    // triggers a locking shift, they won't end up with Latin1 in the GL table,
+    // triggers a locking shift, they won't end up with UPSS in the GL table,
     // making their system unreadable. If ISO-2022 encoding is selected, though,
-    // we'll reset the G2 and G3 tables to Latin1, so that 8-bit apps will get a
+    // we'll reset the G2 and G3 tables to UPSS, so that 8-bit apps will get a
     // more meaningful character mapping by default. This is triggered by a DOCS
     // sequence, which will call the EnableGrTranslation method below.
+    const auto grTranslationTable = grEnabled ? _upssTranslationTable : Ascii;
+    const auto grId = grEnabled ? VTID("<") : VTID("B");
     _gsetTranslationTables.at(0) = Ascii;
     _gsetTranslationTables.at(1) = Ascii;
-    _gsetTranslationTables.at(2) = Ascii;
-    _gsetTranslationTables.at(3) = Ascii;
+    _gsetTranslationTables.at(2) = grTranslationTable;
+    _gsetTranslationTables.at(3) = grTranslationTable;
     _gsetIds.at(0) = VTID("B");
     _gsetIds.at(1) = VTID("B");
-    _gsetIds.at(2) = VTID("B");
-    _gsetIds.at(3) = VTID("B");
+    _gsetIds.at(2) = grId;
+    _gsetIds.at(3) = grId;
+}
+
+void TerminalOutput::SoftReset() noexcept
+{
+    // For a soft reset we want to reinitialize the character set designations,
+    // but retain the GR translation functionality if it's currently enabled.
+    *this = { _grTranslationEnabled };
+}
+
+void TerminalOutput::RestoreFrom(const TerminalOutput& savedState) noexcept
+{
+    // When restoring from a saved instance, we want to preserve the GR
+    // translation functionality if it's currently enabled.
+    const auto preserveGrTranslation = _grTranslationEnabled;
+    *this = savedState;
+    _grTranslationEnabled = preserveGrTranslation;
+}
+
+bool TerminalOutput::AssignUserPreferenceCharset(const VTID charset, const bool size96)
+{
+    const auto translationTable = size96 ? _LookupTranslationTable96(charset) : _LookupTranslationTable94(charset);
+    RETURN_BOOL_IF_FALSE(!translationTable.empty());
+    _upssId = charset;
+    _upssTranslationTable = translationTable;
+    // Any G-set mapped to UPSS will need its translation table updated.
+    for (auto gset = 0; gset < 4; gset++)
+    {
+        if (_gsetIds.at(gset) == VTID("<"))
+        {
+            _gsetTranslationTables.at(gset) = _upssTranslationTable;
+        }
+    }
+    // We also reapply the locking shifts in case they need to be updated.
+    LockingShift(_glSetNumber);
+    LockingShiftRight(_grSetNumber);
+    return true;
+}
+
+VTID TerminalOutput::GetUserPreferenceCharsetId() const noexcept
+{
+    return _upssId;
+}
+
+size_t TerminalOutput::GetUserPreferenceCharsetSize() const noexcept
+{
+    return _upssTranslationTable.size() == 96 ? 96 : 94;
 }
 
 bool TerminalOutput::Designate94Charset(size_t gsetNumber, const VTID charset)
@@ -124,14 +175,17 @@ bool TerminalOutput::NeedToTranslate() const noexcept
     return !_glTranslationTable.empty() || !_grTranslationTable.empty() || _ssSetNumber != 0;
 }
 
-void TerminalOutput::EnableGrTranslation(boolean enabled)
+void TerminalOutput::EnableGrTranslation(const bool enabled)
 {
     _grTranslationEnabled = enabled;
-    // The default table for G2 and G3 is Latin1 when GR translation is enabled,
+    // The default table for G2 and G3 is UPSS when GR translation is enabled,
     // and ASCII when disabled. The reason for this is explained in the constructor.
-    const auto defaultTranslationTable = enabled ? std::wstring_view{ Latin1 } : std::wstring_view{ Ascii };
+    const auto defaultTranslationTable = enabled ? _upssTranslationTable : Ascii;
+    const auto defaultId = enabled ? VTID("<") : VTID("B");
     _gsetTranslationTables.at(2) = defaultTranslationTable;
     _gsetTranslationTables.at(3) = defaultTranslationTable;
+    _gsetIds.at(2) = defaultId;
+    _gsetIds.at(3) = defaultId;
     // We need to reapply the locking shifts in case the underlying G-sets have changed.
     LockingShift(_glSetNumber);
     LockingShiftRight(_grSetNumber);
@@ -184,8 +238,8 @@ const std::wstring_view TerminalOutput::_LookupTranslationTable94(const VTID cha
     case VTID("0"): // DEC Special Graphics
     case VTID("2"): // Alternate Character ROM Special Graphics
         return DecSpecialGraphics;
-    case VTID("<"): // DEC Supplemental
-        return DecSupplemental;
+    case VTID("<"): // User-Preference Supplemental
+        return _upssTranslationTable;
     case VTID("A"): // British NRCS
         return BritishNrcs;
     case VTID("4"): // Dutch NRCS
@@ -253,8 +307,9 @@ const std::wstring_view TerminalOutput::_LookupTranslationTable96(const VTID cha
     switch (charset)
     {
     case VTID("A"): // ISO Latin-1 Supplemental
-    case VTID("<"): // (UPSS when assigned to Latin-1)
         return Latin1;
+    case VTID("<"): // User-Preference Supplemental
+        return _upssTranslationTable;
     case VTID("B"): // ISO Latin-2 Supplemental
         return Latin2;
     case VTID("L"): // ISO Latin-Cyrillic Supplemental
diff --git a/src/terminal/adapter/terminalOutput.hpp b/src/terminal/adapter/terminalOutput.hpp
index 3b77a3da612..5e6fff29c71 100644
--- a/src/terminal/adapter/terminalOutput.hpp
+++ b/src/terminal/adapter/terminalOutput.hpp
@@ -23,8 +23,13 @@ namespace Microsoft::Console::VirtualTerminal
     class TerminalOutput sealed
     {
     public:
-        TerminalOutput() noexcept;
+        TerminalOutput(const bool grEnabled = false) noexcept;
 
+        void SoftReset() noexcept;
+        void RestoreFrom(const TerminalOutput& savedState) noexcept;
+        bool AssignUserPreferenceCharset(const VTID charset, const bool size96);
+        VTID GetUserPreferenceCharsetId() const noexcept;
+        size_t GetUserPreferenceCharsetSize() const noexcept;
         wchar_t TranslateKey(const wchar_t wch) const noexcept;
         bool Designate94Charset(const size_t gsetNumber, const VTID charset);
         bool Designate96Charset(const size_t gsetNumber, const VTID charset);
@@ -39,7 +44,7 @@ namespace Microsoft::Console::VirtualTerminal
         size_t GetRightSetNumber() const noexcept;
         bool IsSingleShiftPending(const size_t gsetNumber) const noexcept;
         bool NeedToTranslate() const noexcept;
-        void EnableGrTranslation(boolean enabled);
+        void EnableGrTranslation(const bool enabled);
 
     private:
         const std::wstring_view _LookupTranslationTable94(const VTID charset) const;
@@ -47,6 +52,8 @@ namespace Microsoft::Console::VirtualTerminal
         bool _SetTranslationTable(const size_t gsetNumber, const std::wstring_view translationTable);
         void _ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable);
 
+        VTID _upssId;
+        std::wstring_view _upssTranslationTable;
         std::array<std::wstring_view, 4> _gsetTranslationTables;
         std::array<VTID, 4> _gsetIds;
         size_t _glSetNumber = 0;
@@ -54,7 +61,7 @@ namespace Microsoft::Console::VirtualTerminal
         std::wstring_view _glTranslationTable;
         std::wstring_view _grTranslationTable;
         mutable size_t _ssSetNumber = 0;
-        boolean _grTranslationEnabled = false;
+        bool _grTranslationEnabled = false;
         VTID _drcsId = 0;
         std::wstring_view _drcsTranslationTable;
     };
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index c3192462052..54aae04a64d 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -1676,7 +1676,7 @@ class AdapterTest
         _testGetSet->PrepData();
         VERIFY_IS_TRUE(_pDispatch->DeviceAttributes());
 
-        auto pwszExpectedResponse = L"\x1b[?61;1;6;7;21;22;23;24;28;32;42c";
+        auto pwszExpectedResponse = L"\x1b[?61;1;6;7;14;21;22;23;24;28;32;42c";
         _testGetSet->ValidateInputEvent(pwszExpectedResponse);
 
         Log::Comment(L"Test 2: Verify failure when ReturnResponse doesn't work.");
@@ -2235,6 +2235,7 @@ class AdapterTest
         _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;@;BBBB\033\\");
 
         Log::Comment(L"94 charset designations");
+        termOutput.AssignUserPreferenceCharset(VTID("%5"), false);
         _pDispatch->Designate94Charset(0, "%5");
         _pDispatch->Designate94Charset(1, "<");
         _pDispatch->Designate94Charset(2, "0");
@@ -2754,7 +2755,7 @@ class AdapterTest
             const auto cellMatrix = static_cast<DispatchTypes::DrcsCellMatrix>(cmw);
             RETURN_BOOL_IF_FALSE(fontBuffer.SetEraseControl(DispatchTypes::DrcsEraseControl::AllChars));
             RETURN_BOOL_IF_FALSE(fontBuffer.SetAttributes(cellMatrix, cmh, ss, u));
-            RETURN_BOOL_IF_FALSE(fontBuffer.SetStartChar(0, DispatchTypes::DrcsCharsetSize::Size94));
+            RETURN_BOOL_IF_FALSE(fontBuffer.SetStartChar(0, DispatchTypes::CharsetSize::Size94));
 
             fontBuffer.AddSixelData(L'B'); // Charset identifier
             for (auto ch : data)
@@ -2997,6 +2998,146 @@ class AdapterTest
         VERIFY_IS_FALSE(_stateMachine->GetParserMode(StateMachine::Mode::AcceptC1));
     }
 
+    TEST_METHOD(AssignUserPreferenceCharsets)
+    {
+        const auto assignCharset = [=](const auto charsetSize, const std::wstring_view charsetId = {}) {
+            const auto stringHandler = _pDispatch->AssignUserPreferenceCharset(charsetSize);
+            for (auto ch : charsetId)
+            {
+                stringHandler(ch);
+            }
+            stringHandler(L'\033'); // String terminator
+        };
+        auto& termOutput = _pDispatch->_termOutput;
+        termOutput.SoftReset();
+
+        Log::Comment(L"DECAUPSS: DEC Supplemental");
+        assignCharset(DispatchTypes::CharsetSize::Size94, L"%5");
+        VERIFY_ARE_EQUAL(VTID("%5"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(94u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: DEC Greek");
+        assignCharset(DispatchTypes::CharsetSize::Size94, L"\"?");
+        VERIFY_ARE_EQUAL(VTID("\"?"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(94u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: DEC Hebrew");
+        assignCharset(DispatchTypes::CharsetSize::Size94, L"\"4");
+        VERIFY_ARE_EQUAL(VTID("\"4"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(94u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: DEC Turkish");
+        assignCharset(DispatchTypes::CharsetSize::Size94, L"%0");
+        VERIFY_ARE_EQUAL(VTID("%0"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(94u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: DEC Cyrillic");
+        assignCharset(DispatchTypes::CharsetSize::Size94, L"&4");
+        VERIFY_ARE_EQUAL(VTID("&4"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(94u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: ISO Latin-1");
+        assignCharset(DispatchTypes::CharsetSize::Size96, L"A");
+        VERIFY_ARE_EQUAL(VTID("A"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(96u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: ISO Latin-2");
+        assignCharset(DispatchTypes::CharsetSize::Size96, L"B");
+        VERIFY_ARE_EQUAL(VTID("B"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(96u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: ISO Latin-Greek");
+        assignCharset(DispatchTypes::CharsetSize::Size96, L"F");
+        VERIFY_ARE_EQUAL(VTID("F"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(96u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: ISO Latin-Hebrew");
+        assignCharset(DispatchTypes::CharsetSize::Size96, L"H");
+        VERIFY_ARE_EQUAL(VTID("H"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(96u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: ISO Latin-Cyrillic");
+        assignCharset(DispatchTypes::CharsetSize::Size96, L"L");
+        VERIFY_ARE_EQUAL(VTID("L"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(96u, termOutput.GetUserPreferenceCharsetSize());
+
+        Log::Comment(L"DECAUPSS: ISO Latin-5");
+        assignCharset(DispatchTypes::CharsetSize::Size96, L"M");
+        VERIFY_ARE_EQUAL(VTID("M"), termOutput.GetUserPreferenceCharsetId());
+        VERIFY_ARE_EQUAL(96u, termOutput.GetUserPreferenceCharsetSize());
+    }
+
+    TEST_METHOD(RequestUserPreferenceCharsets)
+    {
+        auto& termOutput = _pDispatch->_termOutput;
+
+        Log::Comment(L"DECRQUPSS: DEC Supplemental");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("%5"), false));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P0!u%5\033\\");
+
+        Log::Comment(L"DECRQUPSS: DEC Greek");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("\"?"), false));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P0!u\"?\033\\");
+
+        Log::Comment(L"DECRQUPSS: DEC Hebrew");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("\"4"), false));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P0!u\"4\033\\");
+
+        Log::Comment(L"DECRQUPSS: DEC Turkish");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("%0"), false));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P0!u%0\033\\");
+
+        Log::Comment(L"DECRQUPSS: DEC Cyrillic");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("&4"), false));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P0!u&4\033\\");
+
+        Log::Comment(L"DECRQUPSS: ISO Latin-1");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("A"), true));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P1!uA\033\\");
+
+        Log::Comment(L"DECRQUPSS: ISO Latin-2");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("B"), true));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P1!uB\033\\");
+
+        Log::Comment(L"DECRQUPSS: ISO Latin-Greek");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("F"), true));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P1!uF\033\\");
+
+        Log::Comment(L"DECRQUPSS: ISO Latin-Hebrew");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("H"), true));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P1!uH\033\\");
+
+        Log::Comment(L"DECRQUPSS: ISO Latin-Cyrillic");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("L"), true));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P1!uL\033\\");
+
+        Log::Comment(L"DECRQUPSS: ISO Latin-5");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(termOutput.AssignUserPreferenceCharset(VTID("M"), true));
+        _pDispatch->RequestUserPreferenceCharset();
+        _testGetSet->ValidateInputEvent(L"\033P1!uM\033\\");
+    }
+
     TEST_METHOD(MacroDefinitions)
     {
         const auto getMacroText = [&](const auto id) {
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 68a45916df4..40e4a922cc8 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -274,6 +274,15 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id)
     case EscActionCodes::DECAC1_AcceptC1Controls:
         success = _dispatch->AcceptC1Controls(true);
         break;
+    case EscActionCodes::ACS_AnsiLevel1:
+        success = _dispatch->AnnounceCodeStructure(1);
+        break;
+    case EscActionCodes::ACS_AnsiLevel2:
+        success = _dispatch->AnnounceCodeStructure(2);
+        break;
+    case EscActionCodes::ACS_AnsiLevel3:
+        success = _dispatch->AnnounceCodeStructure(3);
+        break;
     case EscActionCodes::DECDHL_DoubleHeightLineTop:
         success = _dispatch->SetLineRendition(LineRendition::DoubleHeightTop);
         break;
@@ -630,6 +639,9 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
     case CsiActionCodes::DECSERA_SelectiveEraseRectangularArea:
         success = _dispatch->SelectiveEraseRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0));
         break;
+    case CsiActionCodes::DECRQUPSS_RequestUserPreferenceSupplementalSet:
+        success = _dispatch->RequestUserPreferenceCharset();
+        break;
     case CsiActionCodes::DECIC_InsertColumn:
         success = _dispatch->InsertColumn(parameters.at(0));
         break;
@@ -694,6 +706,9 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c
                                           parameters.at(6),
                                           parameters.at(7));
         break;
+    case DcsActionCodes::DECAUPSS_AssignUserPreferenceSupplementalSet:
+        handler = _dispatch->AssignUserPreferenceCharset(parameters.at(0));
+        break;
     case DcsActionCodes::DECDMAC_DefineMacro:
         handler = _dispatch->DefineMacro(parameters.at(0).value_or(0), parameters.at(1), parameters.at(2));
         break;
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index 8f3f72b9cb6..34049c9c2ad 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -93,6 +93,9 @@ namespace Microsoft::Console::VirtualTerminal
             LS2R_LockingShift = VTID("}"),
             LS3R_LockingShift = VTID("|"),
             DECAC1_AcceptC1Controls = VTID(" 7"),
+            ACS_AnsiLevel1 = VTID(" L"),
+            ACS_AnsiLevel2 = VTID(" M"),
+            ACS_AnsiLevel3 = VTID(" N"),
             DECDHL_DoubleHeightLineTop = VTID("#3"),
             DECDHL_DoubleHeightLineBottom = VTID("#4"),
             DECSWL_SingleWidthLine = VTID("#5"),
@@ -162,6 +165,7 @@ namespace Microsoft::Console::VirtualTerminal
             DECERA_EraseRectangularArea = VTID("$z"),
             DECSERA_SelectiveEraseRectangularArea = VTID("${"),
             DECSCPP_SetColumnsPerPage = VTID("$|"),
+            DECRQUPSS_RequestUserPreferenceSupplementalSet = VTID("&u"),
             DECIC_InsertColumn = VTID("'}"),
             DECDC_DeleteColumn = VTID("'~"),
             DECSACE_SelectAttributeChangeExtent = VTID("*x"),
@@ -174,6 +178,7 @@ namespace Microsoft::Console::VirtualTerminal
         enum DcsActionCodes : uint64_t
         {
             DECDLD_DownloadDRCS = VTID("{"),
+            DECAUPSS_AssignUserPreferenceSupplementalSet = VTID("!u"),
             DECDMAC_DefineMacro = VTID("!z"),
             DECRSTS_RestoreTerminalState = VTID("$p"),
             DECRQSS_RequestSetting = VTID("$q"),


Test patch: