Repository: microsoft/terminal (language: cpp)

Issue:
Terminal WPF control doesn't support screen readers
### Windows Terminal version

1.11.3471.0

### Windows build number

10.0.19043.0

### Other Software

Microsoft Visual Studio 2019 and 2022

### Steps to reproduce

1. Turn on Narrator or any other screen reader
2. Open the WPF Control Test project under the Terminal repo
3. Write any command and press enter

### Expected Behavior

Screen readers read the console output

### Actual Behavior

Screen readers ignore the output and only read the written letters


Discussion / hints:
Added details:
The WPF terminal control doesn't seem to have anything regarding UIA events. It would need some kind of [UiaEventDispatcher](https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fmicrosoft%2Fterminal%2Fblob%2Fmain%2Fsrc%2Ftypes%2FIUiaEventDispatcher.h&data=04%7C01%7Cjadelaga%40microsoft.com%7Cc7a3a2bb2a47469c645408d9fe281a36%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637820270476114176%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=7HW5yHevSqWrw%2Fb1SYvmA1W3Wxwh6eSkqF0iuIfUopg%3D&reserved=0) defined and hooked up to alert the screen reader of new output. Leveraging the NotifyNewOutput() API, would probably be the easiest and fastest way to approach this, because we would force screen readers to read new output. The user can then use the aforementioned hooked up UIA tree to navigate the text buffer as they wish.

Fix patch:
diff --git a/OpenConsole.sln b/OpenConsole.sln
index 70c545fb5c3..d7cc1264cba 100644
--- a/OpenConsole.sln
+++ b/OpenConsole.sln
@@ -643,7 +643,8 @@ Global
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM.ActiveCfg = Debug|Win32
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.ActiveCfg = Debug|ARM64
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.Build.0 = Debug|ARM64
-		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32
+		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64
+		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x64Test.Build.0 = Debug|x64
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.ActiveCfg = Debug|x64
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.Build.0 = Debug|x64
@@ -661,7 +662,8 @@ Global
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM.ActiveCfg = Release|Win32
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.ActiveCfg = Release|ARM64
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.Build.0 = Release|ARM64
-		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x64Test.ActiveCfg = Release|Win32
+		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x64Test.ActiveCfg = Release|x64
+		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x64Test.Build.0 = Release|x64
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x86Test.ActiveCfg = Release|Win32
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.ActiveCfg = Release|x64
 		{DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.Build.0 = Release|x64
@@ -2308,7 +2310,8 @@ Global
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|ARM.ActiveCfg = Debug|Win32
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|ARM64.ActiveCfg = Debug|ARM64
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|ARM64.Build.0 = Debug|ARM64
-		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32
+		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64
+		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|DotNet_x64Test.Build.0 = Debug|x64
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|x64.ActiveCfg = Debug|x64
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Debug|x64.Build.0 = Debug|x64
@@ -2325,7 +2328,8 @@ Global
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|ARM.ActiveCfg = Release|Win32
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|ARM64.ActiveCfg = Release|ARM64
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|ARM64.Build.0 = Release|ARM64
-		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|DotNet_x64Test.ActiveCfg = Release|Win32
+		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|DotNet_x64Test.ActiveCfg = Release|x64
+		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|DotNet_x64Test.Build.0 = Release|x64
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|DotNet_x86Test.ActiveCfg = Release|Win32
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|x64.ActiveCfg = Release|x64
 		{48D21369-3D7B-4431-9967-24E81292CF63}.Release|x64.Build.0 = Release|x64
@@ -3313,7 +3317,8 @@ Global
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM.ActiveCfg = Debug|Win32
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM64.ActiveCfg = Debug|ARM64
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM64.Build.0 = Debug|ARM64
-		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32
+		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64
+		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x64Test.Build.0 = Debug|x64
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x64.ActiveCfg = Debug|x64
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x64.Build.0 = Debug|x64
@@ -3333,7 +3338,8 @@ Global
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM.ActiveCfg = Release|Win32
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM64.ActiveCfg = Release|ARM64
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM64.Build.0 = Release|ARM64
-		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x64Test.ActiveCfg = Release|Win32
+		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x64Test.ActiveCfg = Release|x64
+		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x64Test.Build.0 = Release|x64
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x86Test.ActiveCfg = Release|Win32
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x64.ActiveCfg = Release|x64
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x64.Build.0 = Release|x64
diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp
index 7514bcc426f..5496161d2f4 100644
--- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp
+++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp
@@ -4,11 +4,7 @@
 #include "pch.h"
 #include "HwndTerminal.hpp"
 #include <windowsx.h>
-#include "../../types/TermControlUiaProvider.hpp"
 #include <DefaultSettings.h>
-#include "../../renderer/base/Renderer.hpp"
-#include "../../renderer/dx/DxRenderer.hpp"
-#include "../../cascadia/TerminalCore/Terminal.hpp"
 #include "../../types/viewport.cpp"
 #include "../../types/inc/GlyphWidth.hpp"
 
@@ -54,6 +50,17 @@ LRESULT CALLBACK HwndTerminal::HwndTerminalWndProc(
     LPARAM lParam) noexcept
 try
 {
+    if (WM_NCCREATE == uMsg)
+    {
+#pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast
+        auto cs = reinterpret_cast<CREATESTRUCT*>(lParam);
+        HwndTerminal* that = static_cast<HwndTerminal*>(cs->lpCreateParams);
+        that->_hwnd = wil::unique_hwnd(hwnd);
+
+#pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast
+        SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(that));
+        return DefWindowProc(hwnd, WM_NCCREATE, wParam, lParam);
+    }
 #pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast
     auto terminal = reinterpret_cast<HwndTerminal*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
 
@@ -182,7 +189,7 @@ HwndTerminal::HwndTerminal(HWND parentHwnd) :
 
     if (RegisterTermClass(hInstance))
     {
-        _hwnd = wil::unique_hwnd(CreateWindowExW(
+        CreateWindowExW(
             0,
             term_window_class,
             nullptr,
@@ -197,10 +204,7 @@ HwndTerminal::HwndTerminal(HWND parentHwnd) :
             parentHwnd,
             nullptr,
             hInstance,
-            nullptr));
-
-#pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast
-        SetWindowLongPtr(_hwnd.get(), GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
+            this);
     }
 }
 
@@ -324,14 +328,15 @@ IRawElementProviderSimple* HwndTerminal::_GetUiaProvider() noexcept
 {
     // If TermControlUiaProvider throws during construction,
     // we don't want to try constructing an instance again and again.
-    // _uiaProviderInitialized helps us prevent this.
-    if (!_uiaProviderInitialized)
+    if (!_uiaProvider)
     {
         try
         {
             auto lock = _terminal->LockForWriting();
-            LOG_IF_FAILED(::Microsoft::WRL::MakeAndInitialize<::Microsoft::Terminal::TermControlUiaProvider>(&_uiaProvider, this->GetUiaData(), this));
-            _uiaProviderInitialized = true;
+            LOG_IF_FAILED(::Microsoft::WRL::MakeAndInitialize<HwndTerminalAutomationPeer>(&_uiaProvider, this->GetUiaData(), this));
+            _uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(_uiaProvider.Get());
+            LOG_IF_FAILED(_uiaEngine->Enable());
+            _renderer->AddRenderEngine(_uiaEngine.get());
         }
         catch (...)
         {
@@ -380,29 +385,10 @@ void HwndTerminal::SendOutput(std::wstring_view data)
 
 HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal)
 {
-    // In order for UIA to hook up properly there needs to be a "static" window hosting the
-    // inner win32 control. If the static window is not present then WM_GETOBJECT messages
-    // will not reach the child control, and the uia element will not be present in the tree.
-    auto _hostWindow = CreateWindowEx(
-        0,
-        L"static",
-        nullptr,
-        WS_CHILD |
-            WS_CLIPCHILDREN |
-            WS_CLIPSIBLINGS |
-            WS_VISIBLE,
-        0,
-        0,
-        0,
-        0,
-        parentHwnd,
-        nullptr,
-        nullptr,
-        nullptr);
-    auto _terminal = std::make_unique<HwndTerminal>(_hostWindow);
+    auto _terminal = std::make_unique<HwndTerminal>(parentHwnd);
     RETURN_IF_FAILED(_terminal->Initialize());
 
-    *hwnd = _hostWindow;
+    *hwnd = _terminal->GetHwnd();
     *terminal = _terminal.release();
 
     return S_OK;
@@ -725,6 +711,10 @@ try
     {
         modifiers |= ControlKeyStates::EnhancedKey;
     }
+    if (vkey && keyDown && _uiaProvider)
+    {
+        _uiaProvider->RecordKeyEvent(vkey);
+    }
     _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown);
 }
 CATCH_LOG();
@@ -833,12 +823,20 @@ void __stdcall TerminalSetFocus(void* terminal)
 {
     auto publicTerminal = static_cast<HwndTerminal*>(terminal);
     publicTerminal->_focused = true;
+    if (auto uiaEngine = publicTerminal->_uiaEngine.get())
+    {
+        LOG_IF_FAILED(uiaEngine->Enable());
+    }
 }
 
 void __stdcall TerminalKillFocus(void* terminal)
 {
     auto publicTerminal = static_cast<HwndTerminal*>(terminal);
     publicTerminal->_focused = false;
+    if (auto uiaEngine = publicTerminal->_uiaEngine.get())
+    {
+        LOG_IF_FAILED(uiaEngine->Disable());
+    }
 }
 
 // Routine Description:
diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp
index 48366c884c0..6df223e804b 100644
--- a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp
+++ b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp
@@ -5,10 +5,10 @@
 
 #include "../../renderer/base/Renderer.hpp"
 #include "../../renderer/dx/DxRenderer.hpp"
+#include "../../renderer/uia/UiaRenderer.hpp"
 #include "../../cascadia/TerminalCore/Terminal.hpp"
-#include <UIAutomationCore.h>
 #include "../../types/IControlAccessibilityInfo.h"
-#include "../../types/TermControlUiaProvider.hpp"
+#include "HwndTerminalAutomationPeer.hpp"
 
 using namespace Microsoft::Console::VirtualTerminal;
 
@@ -74,15 +74,15 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo
     FontInfo _actualFont;
     int _currentDpi;
     std::function<void(wchar_t*)> _pfnWriteCallback;
-    ::Microsoft::WRL::ComPtr<::Microsoft::Terminal::TermControlUiaProvider> _uiaProvider;
+    ::Microsoft::WRL::ComPtr<HwndTerminalAutomationPeer> _uiaProvider;
 
     std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal;
 
     std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer;
     std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine;
+    std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine;
 
     bool _focused{ false };
-    bool _uiaProviderInitialized{ false };
 
     std::chrono::milliseconds _multiClickTime;
     unsigned int _multiClickCounter{};
diff --git a/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp b/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp
new file mode 100644
index 00000000000..41a1ee984d9
--- /dev/null
+++ b/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp
@@ -0,0 +1,159 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "HwndTerminalAutomationPeer.hpp"
+#include "../../types/UiaTracing.h"
+#include <UIAutomationCoreApi.h>
+
+#pragma warning(suppress : 4471) // We don't control UIAutomationClient
+#include <UIAutomationClient.h>
+
+using namespace Microsoft::Console::Types;
+
+static constexpr wchar_t UNICODE_NEWLINE{ L'\n' };
+
+// Method Description:
+// - creates a copy of the provided text with all of the control characters removed
+// Arguments:
+// - text: the string we're sanitizing
+// Return Value:
+// - a copy of "sanitized" with all of the control characters removed
+static std::wstring Sanitize(std::wstring_view text)
+{
+    std::wstring sanitized{ text };
+    sanitized.erase(std::remove_if(sanitized.begin(), sanitized.end(), [](wchar_t c) {
+                        return (c < UNICODE_SPACE && c != UNICODE_NEWLINE) || c == 0x7F /*DEL*/;
+                    }),
+                    sanitized.end());
+    return sanitized;
+}
+
+// Method Description:
+// - verifies if a given string has text that would be read by a screen reader.
+// - a string of control characters, for example, would not be read.
+// Arguments:
+// - text: the string we're validating
+// Return Value:
+// - true, if the text is readable. false, otherwise.
+static constexpr bool IsReadable(std::wstring_view text)
+{
+    for (const auto c : text)
+    {
+        if (c > UNICODE_SPACE)
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+void HwndTerminalAutomationPeer::RecordKeyEvent(const WORD vkey)
+{
+    if (const auto charCode{ MapVirtualKey(vkey, MAPVK_VK_TO_CHAR) })
+    {
+        if (const auto keyEventChar{ gsl::narrow_cast<wchar_t>(charCode) }; IsReadable({ &keyEventChar, 1 }))
+        {
+            _keyEvents.emplace_back(keyEventChar);
+        }
+    }
+}
+
+// Implementation of IRawElementProviderSimple::get_PropertyValue.
+// Gets custom properties.
+IFACEMETHODIMP HwndTerminalAutomationPeer::GetPropertyValue(_In_ PROPERTYID propertyId,
+                                                            _Out_ VARIANT* pVariant) noexcept
+{
+    pVariant->vt = VT_EMPTY;
+
+    // Returning the default will leave the property as the default
+    // so we only really need to touch it for the properties we want to implement
+    if (propertyId == UIA_ClassNamePropertyId)
+    {
+        // IMPORTANT: Do NOT change the name. Screen readers like may be dependent on this being "WpfTermControl".
+        pVariant->bstrVal = SysAllocString(L"WPFTermControl");
+        if (pVariant->bstrVal != nullptr)
+        {
+            pVariant->vt = VT_BSTR;
+        }
+    }
+    else
+    {
+        // fall back to shared implementation
+        return TermControlUiaProvider::GetPropertyValue(propertyId, pVariant);
+    }
+    return S_OK;
+}
+
+// Method Description:
+// - Signals the ui automation client that the terminal's selection has changed and should be updated
+// Arguments:
+// - <none>
+// Return Value:
+// - <none>
+void HwndTerminalAutomationPeer::SignalSelectionChanged()
+{
+    UiaTracing::Signal::SelectionChanged();
+    LOG_IF_FAILED(UiaRaiseAutomationEvent(this, UIA_Text_TextSelectionChangedEventId));
+}
+
+// Method Description:
+// - Signals the ui automation client that the terminal's output has changed and should be updated
+// Arguments:
+// - <none>
+// Return Value:
+// - <none>
+void HwndTerminalAutomationPeer::SignalTextChanged()
+{
+    UiaTracing::Signal::TextChanged();
+    LOG_IF_FAILED(UiaRaiseAutomationEvent(this, UIA_Text_TextChangedEventId));
+}
+
+// Method Description:
+// - Signals the ui automation client that the cursor's state has changed and should be updated
+// Arguments:
+// - <none>
+// Return Value:
+// - <none>
+void HwndTerminalAutomationPeer::SignalCursorChanged()
+{
+    UiaTracing::Signal::CursorChanged();
+    LOG_IF_FAILED(UiaRaiseAutomationEvent(this, UIA_Text_TextSelectionChangedEventId));
+}
+
+void HwndTerminalAutomationPeer::NotifyNewOutput(std::wstring_view newOutput)
+{
+    // Try to suppress any events (or event data)
+    // that is just the keypress the user made
+    auto sanitized{ Sanitize(newOutput) };
+    while (!_keyEvents.empty() && IsReadable(sanitized))
+    {
+        if (til::toupper_ascii(sanitized.front()) == _keyEvents.front())
+        {
+            // the key event's character (i.e. the "A" key) matches
+            // the output character (i.e. "a" or "A" text).
+            // We can assume that the output character resulted from
+            // the pressed key, so we can ignore it.
+            sanitized = sanitized.substr(1);
+            _keyEvents.pop_front();
+        }
+        else
+        {
+            // The output doesn't match,
+            // so clear the input stack and
+            // move on to fire the event.
+            _keyEvents.clear();
+            break;
+        }
+    }
+
+    // Suppress event if the remaining text is not readable
+    if (!IsReadable(sanitized))
+    {
+        return;
+    }
+
+    const auto sanitizedBstr = wil::make_bstr_nothrow(sanitized.c_str());
+    static auto activityId = wil::make_bstr_nothrow(L"TerminalTextOutput");
+    LOG_IF_FAILED(UiaRaiseNotificationEvent(this, NotificationKind_ActionCompleted, NotificationProcessing_All, sanitizedBstr.get(), activityId.get()));
+}
diff --git a/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp b/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp
new file mode 100644
index 00000000000..b9dab300cc1
--- /dev/null
+++ b/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp
@@ -0,0 +1,42 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- HwndTerminalAutomationPeer.hpp
+
+Abstract:
+- This module provides UI Automation access to the HwndTerminal
+  to support both automation tests and accessibility (screen
+  reading) applications. This mainly interacts with TermControlUiaProvider
+  to allow for shared code with Windows Terminal accessibility providers.
+
+Author(s):
+- Carlos Zamora   (CaZamor)    2022
+--*/
+
+#pragma once
+
+#include "../types/TermControlUiaProvider.hpp"
+#include "../types/IUiaEventDispatcher.h"
+#include "../types/IControlAccessibilityInfo.h"
+
+class HwndTerminalAutomationPeer :
+    public ::Microsoft::Console::Types::IUiaEventDispatcher,
+    public ::Microsoft::Terminal::TermControlUiaProvider
+{
+public:
+    void RecordKeyEvent(const WORD vkey);
+
+    IFACEMETHODIMP GetPropertyValue(_In_ PROPERTYID idProp,
+                                    _Out_ VARIANT* pVariant) noexcept override;
+
+#pragma region IUiaEventDispatcher
+    void SignalSelectionChanged() override;
+    void SignalTextChanged() override;
+    void SignalCursorChanged() override;
+    void NotifyNewOutput(std::wstring_view newOutput) override;
+#pragma endregion
+private:
+    std::deque<wchar_t> _keyEvents;
+};
diff --git a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
index 62a94791fcc..89e6238178b 100644
--- a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
+++ b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
@@ -15,9 +15,11 @@
       <PrecompiledHeader>Create</PrecompiledHeader>
     </ClCompile>
     <ClCompile Include="HwndTerminal.cpp" />
+    <ClCompile Include="HwndTerminalAutomationPeer.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="HwndTerminal.hpp" />
+    <ClInclude Include="HwndTerminalAutomationPeer.hpp" />
     <ClInclude Include="pch.h" />
   </ItemGroup>
   <ItemGroup>
@@ -39,6 +41,9 @@
     <ProjectReference Include="$(SolutionDir)src\renderer\dx\lib\dx.vcxproj">
       <Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
     </ProjectReference>
+    <ProjectReference Include="..\..\renderer\uia\lib\uia.vcxproj">
+      <Project>{48d21369-3d7b-4431-9967-24e81292cf63}</Project>
+    </ProjectReference>
   </ItemGroup>
   <ItemDefinitionGroup>
     <ClCompile>
@@ -55,4 +60,4 @@
       <AdditionalDependencies>Uiautomationcore.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies>
     </Link>
   </ItemDefinitionGroup>
-</Project>
+</Project>
\ No newline at end of file
diff --git a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters
index 7dccc98e846..6865448d4a3 100644
--- a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters
+++ b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters
@@ -24,6 +24,9 @@
     <ClCompile Include="HwndTerminal.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="HwndTerminalAutomationPeer.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="pch.h">
@@ -32,5 +35,8 @@
     <ClInclude Include="HwndTerminal.hpp">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="HwndTerminalAutomationPeer.hpp">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/src/cascadia/PublicTerminalCore/pch.h b/src/cascadia/PublicTerminalCore/pch.h
index 8554ff03bc9..3834f83cdee 100644
--- a/src/cascadia/PublicTerminalCore/pch.h
+++ b/src/cascadia/PublicTerminalCore/pch.h
@@ -9,3 +9,4 @@
 #endif
 
 #include <LibraryIncludes.h>
+#include <UIAutomationCore.h>
\ No newline at end of file
diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs
index 960fa4fd1c9..79bf0a32142 100644
--- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs
+++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs
@@ -8,6 +8,8 @@ namespace Microsoft.Terminal.Wpf
     using System;
     using System.Runtime.InteropServices;
     using System.Windows;
+    using System.Windows.Automation;
+    using System.Windows.Automation.Peers;
     using System.Windows.Interop;
     using System.Windows.Media;
     using System.Windows.Threading;
@@ -52,6 +54,30 @@ public TerminalContainer()
             }
         }
 
+        /// <summary>
+        /// WPF's HwndHost likes to mark the WM_GETOBJECT message as handled to
+        /// force the usage of the WPF automation peer. We explicitly mark it as
+        /// not handled and don't return an automation peer in "OnCreateAutomationPeer" below.
+        /// This forces the message to go down to the HwndTerminal where we return terminal's UiaProvider.
+        /// </summary>
+        /// <inheritdoc/>
+        protected override IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
+        {
+            if (msg == (int)NativeMethods.WindowMessage.WM_GETOBJECT)
+            {
+                handled = false;
+                return IntPtr.Zero;
+            }
+
+            return base.WndProc(hwnd, msg, wParam, lParam, ref handled);
+        }
+
+        /// <inheritdoc/>
+        protected override AutomationPeer OnCreateAutomationPeer()
+        {
+            return null;
+        }
+
         /// <summary>
         /// Event that is fired when the terminal buffer scrolls from text output.
         /// </summary>
diff --git a/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs b/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs
index 93d56fb0008..21ad159c460 100644
--- a/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs
+++ b/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs
@@ -8,6 +8,7 @@ namespace Microsoft.Terminal.Wpf
     using System;
     using System.Threading;
     using System.Windows;
+    using System.Windows.Automation.Peers;
     using System.Windows.Controls;
     using System.Windows.Input;
     using System.Windows.Media;
@@ -42,6 +43,20 @@ public TerminalControl()
             this.GotFocus += this.TerminalControl_GotFocus;
         }
 
+        /// <inheritdoc/>
+        protected override AutomationPeer OnCreateAutomationPeer()
+        {
+            var peer = FrameworkElementAutomationPeer.FromElement(this);
+            if (peer == null)
+            {
+                // Provide our own automation peer here that just sets IsContentElement/IsControlElement to false
+                // (aka AccessibilityView = Raw). This makes it not pop up in the UIA tree.
+                peer = new TermControlAutomationPeer(this);
+            }
+
+            return peer;
+        }
+
         /// <summary>
         /// Gets the current character rows available to the terminal.
         /// </summary>
@@ -266,5 +281,22 @@ private void Scrollbar_Scroll(object sender, System.Windows.Controls.Primitives.
             var viewTop = (int)e.NewValue;
             this.termContainer.UserScroll(viewTop);
         }
+
+        private class TermControlAutomationPeer : UserControlAutomationPeer
+        {
+            public TermControlAutomationPeer(UserControl owner) : base(owner)
+            {
+            }
+
+            protected override bool IsContentElementCore()
+            {
+                return false;
+            }
+
+            protected override bool IsControlElementCore()
+            {
+                return false;
+            }
+        }
     }
 }
diff --git a/src/types/TermControlUiaProvider.cpp b/src/types/TermControlUiaProvider.cpp
index 01f8864b122..b3fb2d07112 100644
--- a/src/types/TermControlUiaProvider.cpp
+++ b/src/types/TermControlUiaProvider.cpp
@@ -19,6 +19,52 @@ HRESULT TermControlUiaProvider::RuntimeClassInitialize(_In_ ::Microsoft::Console
     return S_OK;
 }
 
+// Implementation of IRawElementProviderSimple::get_PropertyValue.
+// Gets custom properties.
+IFACEMETHODIMP TermControlUiaProvider::GetPropertyValue(_In_ PROPERTYID propertyId,
+                                                        _Out_ VARIANT* pVariant) noexcept
+{
+    pVariant->vt = VT_EMPTY;
+
+    // Returning the default will leave the property as the default
+    // so we only really need to touch it for the properties we want to implement
+    switch (propertyId)
+    {
+    case UIA_ClassNamePropertyId:
+        pVariant->bstrVal = SysAllocString(L"TermControl");
+        if (pVariant->bstrVal != nullptr)
+        {
+            pVariant->vt = VT_BSTR;
+        }
+        break;
+    case UIA_ControlTypePropertyId:
+        pVariant->vt = VT_I4;
+        pVariant->lVal = UIA_TextControlTypeId;
+        break;
+    case UIA_LocalizedControlTypePropertyId:
+        // TODO: we should use RS_(L"TerminalControl_ControlType"),
+        // but that's exposed/defined in the TermControl project
+        pVariant->bstrVal = SysAllocString(L"terminal");
+        if (pVariant->bstrVal != nullptr)
+        {
+            pVariant->vt = VT_BSTR;
+        }
+        break;
+    case UIA_OrientationPropertyId:
+        pVariant->vt = VT_I4;
+        pVariant->lVal = OrientationType_Vertical;
+        break;
+    case UIA_LiveSettingPropertyId:
+        pVariant->vt = VT_I4;
+        pVariant->lVal = LiveSetting::Polite;
+        break;
+    default:
+        // fall back to the shared implementation
+        return ScreenInfoUiaProviderBase::GetPropertyValue(propertyId, pVariant);
+    }
+    return S_OK;
+}
+
 IFACEMETHODIMP TermControlUiaProvider::Navigate(_In_ NavigateDirection direction,
                                                 _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) noexcept
 {
diff --git a/src/types/TermControlUiaProvider.hpp b/src/types/TermControlUiaProvider.hpp
index 8b6f80a40fb..0a177e3eeb4 100644
--- a/src/types/TermControlUiaProvider.hpp
+++ b/src/types/TermControlUiaProvider.hpp
@@ -32,6 +32,10 @@ namespace Microsoft::Terminal
         HRESULT RuntimeClassInitialize(_In_ ::Microsoft::Console::Types::IUiaData* const uiaData,
                                        _In_ ::Microsoft::Console::Types::IControlAccessibilityInfo* controlInfo) noexcept;
 
+        // IRawElementProviderSimple methods
+        IFACEMETHODIMP GetPropertyValue(_In_ PROPERTYID idProp,
+                                        _Out_ VARIANT* pVariant) noexcept override;
+
         // IRawElementProviderFragment methods
         IFACEMETHODIMP Navigate(_In_ NavigateDirection direction,
                                 _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) noexcept override;


Test patch: