diff --git a/OpenConsole.sln b/OpenConsole.sln
index 530f602b1..b70cea6c8 100644
--- a/OpenConsole.sln
+++ b/OpenConsole.sln
@@ -296,6 +296,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{D3EF
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "winconpty.Tests.Feature", "src\winconpty\ft_pty\winconpty.FeatureTests.vcxproj", "{024052DE-83FB-4653-AEA4-90790D29D5BD}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalAzBridge", "src\cascadia\TerminalAzBridge\TerminalAzBridge.vcxproj", "{067F0A06-FCB7-472C-96E9-B03B54E8E18D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
AuditMode|Any CPU = AuditMode|Any CPU
@@ -1435,6 +1437,20 @@ Global
{024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x64.Build.0 = Release|x64
{024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x86.ActiveCfg = Release|Win32
{024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x86.Build.0 = Release|Win32
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|Any CPU.ActiveCfg = Debug|Win32
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|ARM64.Build.0 = Debug|ARM64
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x64.ActiveCfg = Debug|x64
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x64.Build.0 = Debug|x64
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x86.ActiveCfg = Debug|Win32
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x86.Build.0 = Debug|Win32
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|Any CPU.ActiveCfg = Release|Win32
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|ARM64.ActiveCfg = Release|ARM64
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|ARM64.Build.0 = Release|ARM64
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x64.ActiveCfg = Release|x64
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x64.Build.0 = Release|x64
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x86.ActiveCfg = Release|Win32
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1510,6 +1526,7 @@ Global
{6B5A44ED-918D-4747-BFB1-2472A1FCA173} = {04170EEF-983A-4195-BFEF-2321E5E38A1E}
{D3EF7B96-CD5E-47C9-B9A9-136259563033} = {04170EEF-983A-4195-BFEF-2321E5E38A1E}
{024052DE-83FB-4653-AEA4-90790D29D5BD} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB}
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D} = {59840756-302F-44DF-AA47-441A9D673202}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271}
diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
index 8375c57e8..f415febc0 100644
--- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
+++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
@@ -55,6 +55,7 @@
+
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index ed6b64e0c..78a8caec9 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -602,8 +602,10 @@ namespace winrt::TerminalApp::implementation
profile->GetConnectionType() == AzureConnectionType &&
TerminalConnection::AzureConnection::IsAzureConnectionAvailable())
{
- connection = TerminalConnection::AzureConnection(settings.InitialRows(),
- settings.InitialCols());
+ // TODO GH#4661: Replace this with directly using the AzCon when our VT is better
+ std::filesystem::path azBridgePath{ wil::GetModuleFileNameW(nullptr) };
+ azBridgePath.replace_filename(L"TerminalAzBridge.exe");
+ connection = TerminalConnection::ConptyConnection(azBridgePath.wstring(), L".", L"Azure", settings.InitialRows(), settings.InitialCols(), winrt::guid());
}
else if (profile->HasConnectionType() &&
diff --git a/src/cascadia/TerminalAzBridge/ConsoleInputReader.cpp b/src/cascadia/TerminalAzBridge/ConsoleInputReader.cpp
new file mode 100644
index 000000000..12e7f51cb
--- /dev/null
+++ b/src/cascadia/TerminalAzBridge/ConsoleInputReader.cpp
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ConsoleInputReader.h"
+#include "unicode.hpp"
+
+ConsoleInputReader::ConsoleInputReader(HANDLE handle) :
+ _handle(handle)
+{
+ _buffer.resize(BufferSize);
+ _convertedString.reserve(BufferSize);
+}
+
+void ConsoleInputReader::SetWindowSizeChangedCallback(std::function callback)
+{
+ _windowSizeChangedCallback = std::move(callback);
+}
+
+std::optional ConsoleInputReader::Read()
+{
+ DWORD readCount{ 0 };
+
+ _convertedString.clear();
+ while (_convertedString.empty())
+ {
+ _buffer.resize(BufferSize);
+ BOOL succeeded =
+ ReadConsoleInputW(_handle, _buffer.data(), gsl::narrow_cast(_buffer.size()), &readCount);
+ if (!succeeded)
+ {
+ return std::nullopt;
+ }
+
+ _buffer.resize(readCount);
+ for (auto it = _buffer.begin(); it != _buffer.end(); ++it)
+ {
+ if (it->EventType == WINDOW_BUFFER_SIZE_EVENT && _windowSizeChangedCallback)
+ {
+ _windowSizeChangedCallback();
+ }
+ else if (it->EventType == KEY_EVENT)
+ {
+ const auto& keyEvent = it->Event.KeyEvent;
+ if (keyEvent.bKeyDown || (!keyEvent.bKeyDown && keyEvent.wVirtualKeyCode == VK_MENU))
+ {
+ // Got a high surrogate at the end of the buffer
+ if (IS_HIGH_SURROGATE(keyEvent.uChar.UnicodeChar))
+ {
+ _highSurrogate.emplace(keyEvent.uChar.UnicodeChar);
+ continue; // we've consumed it -- only dispatch it if we get a low
+ }
+
+ if (IS_LOW_SURROGATE(keyEvent.uChar.UnicodeChar))
+ {
+ // No matter what we do, we want to destructively consume the high surrogate
+ if (const auto oldHighSurrogate{ std::exchange(_highSurrogate, std::nullopt) })
+ {
+ _convertedString.push_back(*_highSurrogate);
+ }
+ else
+ {
+ // If we get a low without a high surrogate, we've done everything we can.
+ // This is an illegal state.
+ _convertedString.push_back(UNICODE_REPLACEMENT);
+ continue; // onto the next event
+ }
+ }
+
+ // (\0 with a scancode is probably a modifier key, not a VT input key)
+ if (keyEvent.uChar.UnicodeChar != L'\0' || keyEvent.wVirtualScanCode == 0)
+ {
+ if (_highSurrogate) // non-destructive: we don't want to set it to nullopt needlessly for every character
+ {
+ // If we get a high surrogate *here*, we didn't find a low surrogate.
+ // This state is also illegal.
+ _convertedString.push_back(UNICODE_REPLACEMENT);
+ _highSurrogate.reset();
+ }
+ _convertedString.push_back(keyEvent.uChar.UnicodeChar);
+ }
+ }
+ }
+ }
+ }
+ return _convertedString;
+}
diff --git a/src/cascadia/TerminalAzBridge/ConsoleInputReader.h b/src/cascadia/TerminalAzBridge/ConsoleInputReader.h
new file mode 100644
index 000000000..894804f45
--- /dev/null
+++ b/src/cascadia/TerminalAzBridge/ConsoleInputReader.h
@@ -0,0 +1,33 @@
+/*++
+
+Copyright (c) Microsoft Corporation.
+Licensed under the MIT license.
+
+Module Name:
+
+ ConsoleInputReader.h
+
+Abstract:
+
+ This file contains a class whose sole purpose is to
+ abstract away a bunch of details you usually need to
+ know to read VT from a console input handle.
+
+--*/
+
+class ConsoleInputReader
+{
+public:
+ ConsoleInputReader(HANDLE handle);
+ void SetWindowSizeChangedCallback(std::function callback);
+ std::optional Read();
+
+private:
+ static constexpr size_t BufferSize{ 128 };
+
+ HANDLE _handle;
+ std::wstring _convertedString;
+ std::vector _buffer;
+ std::optional _highSurrogate;
+ std::function _windowSizeChangedCallback;
+};
diff --git a/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj b/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj
new file mode 100644
index 000000000..3e78cfd9b
--- /dev/null
+++ b/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj
@@ -0,0 +1,71 @@
+
+
+
+
+ {067F0A06-FCB7-472C-96E9-B03B54E8E18D}
+ Win32Proj
+ TerminalAzBridge
+ TerminalAzBridge
+ TerminalAzBridge
+ Application
+ false
+ Windows Store
+ true
+
+
+
+
+
+
+
+ true
+
+
+ Console
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+
+
+
+
+ {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}
+
+
+
+
+
+
+
+
+
+
+ WindowsLocalDebugger
+
+
+
+
+
+
diff --git a/src/cascadia/TerminalAzBridge/main.cpp b/src/cascadia/TerminalAzBridge/main.cpp
new file mode 100644
index 000000000..df51677d7
--- /dev/null
+++ b/src/cascadia/TerminalAzBridge/main.cpp
@@ -0,0 +1,104 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "winrt/Microsoft.Terminal.TerminalConnection.h"
+#include "ConsoleInputReader.h"
+
+using namespace winrt;
+using namespace winrt::Windows::Foundation;
+using namespace winrt::Microsoft::Terminal::TerminalConnection;
+
+static COORD GetConsoleScreenSize(HANDLE outputHandle)
+{
+ CONSOLE_SCREEN_BUFFER_INFOEX csbiex{};
+ csbiex.cbSize = sizeof(csbiex);
+ GetConsoleScreenBufferInfoEx(outputHandle, &csbiex);
+ return {
+ (csbiex.srWindow.Right - csbiex.srWindow.Left) + 1,
+ (csbiex.srWindow.Bottom - csbiex.srWindow.Top) + 1
+ };
+}
+
+static ConnectionState RunConnectionToCompletion(const ITerminalConnection& connection, HANDLE outputHandle, HANDLE inputHandle)
+{
+ connection.TerminalOutput([outputHandle](const winrt::hstring& output) {
+ WriteConsoleW(outputHandle, output.data(), output.size(), nullptr, nullptr);
+ });
+
+ // Detach a thread to spin the console read indefinitely.
+ // This application exits when the connection is closed, so
+ // the connection's lifetime will outlast this thread.
+ std::thread([connection, outputHandle, inputHandle] {
+ ConsoleInputReader reader{ inputHandle };
+ reader.SetWindowSizeChangedCallback([&]() {
+ const auto size = GetConsoleScreenSize(outputHandle);
+
+ connection.Resize(size.Y, size.X);
+ });
+
+ while (true)
+ {
+ auto input = reader.Read();
+ if (input)
+ {
+ connection.WriteInput(*input);
+ }
+ }
+ }).detach();
+
+ std::condition_variable stateChangeVar;
+ std::optional state;
+ std::mutex stateMutex;
+
+ connection.StateChanged([&](auto&& /*s*/, auto&& /*e*/) {
+ std::unique_lock lg{ stateMutex };
+ state = connection.State();
+ stateChangeVar.notify_all();
+ });
+
+ connection.Start();
+
+ std::unique_lock lg{ stateMutex };
+ stateChangeVar.wait(lg, [&]() {
+ if (!state.has_value())
+ {
+ return false;
+ }
+ return state.value() == ConnectionState::Closed || state.value() == ConnectionState::Failed;
+ });
+
+ return state.value();
+}
+
+int wmain(int /*argc*/, wchar_t** /*argv*/)
+{
+ winrt::init_apartment(winrt::apartment_type::single_threaded);
+
+ DWORD inputMode{}, outputMode{};
+ HANDLE conIn{ GetStdHandle(STD_INPUT_HANDLE) }, conOut{ GetStdHandle(STD_OUTPUT_HANDLE) };
+ UINT codepage{ GetConsoleCP() }, outputCodepage{ GetConsoleOutputCP() };
+
+ RETURN_IF_WIN32_BOOL_FALSE(GetConsoleMode(conIn, &inputMode));
+ RETURN_IF_WIN32_BOOL_FALSE(GetConsoleMode(conOut, &outputMode));
+
+ RETURN_IF_WIN32_BOOL_FALSE(SetConsoleMode(conIn, ENABLE_WINDOW_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT));
+ RETURN_IF_WIN32_BOOL_FALSE(SetConsoleMode(conOut, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_WRAP_AT_EOL_OUTPUT | DISABLE_NEWLINE_AUTO_RETURN));
+ RETURN_IF_WIN32_BOOL_FALSE(SetConsoleCP(CP_UTF8));
+ RETURN_IF_WIN32_BOOL_FALSE(SetConsoleOutputCP(CP_UTF8));
+
+ auto restoreConsoleModes = wil::scope_exit([&]() {
+ SetConsoleMode(conIn, inputMode);
+ SetConsoleMode(conOut, outputMode);
+ SetConsoleCP(codepage);
+ SetConsoleOutputCP(outputCodepage);
+ });
+
+ const auto size = GetConsoleScreenSize(conOut);
+
+ AzureConnection azureConn{ gsl::narrow_cast(size.Y), gsl::narrow_cast(size.X) };
+
+ const auto state = RunConnectionToCompletion(azureConn, conOut, conIn);
+
+ return state == ConnectionState::Closed ? 0 : 1;
+}
diff --git a/src/cascadia/TerminalAzBridge/packages.config b/src/cascadia/TerminalAzBridge/packages.config
new file mode 100644
index 000000000..e345a6ccd
--- /dev/null
+++ b/src/cascadia/TerminalAzBridge/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/cascadia/TerminalAzBridge/pch.cpp b/src/cascadia/TerminalAzBridge/pch.cpp
new file mode 100644
index 000000000..398a99f66
--- /dev/null
+++ b/src/cascadia/TerminalAzBridge/pch.cpp
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
diff --git a/src/cascadia/TerminalAzBridge/pch.h b/src/cascadia/TerminalAzBridge/pch.h
new file mode 100644
index 000000000..e32c4f906
--- /dev/null
+++ b/src/cascadia/TerminalAzBridge/pch.h
@@ -0,0 +1,36 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- pch.h
+
+Abstract:
+- Contains external headers to include in the precompile phase of console build process.
+- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building).
+--*/
+
+#pragma once
+
+// Ignore checked iterators warning from VC compiler.
+#define _SCL_SECURE_NO_WARNINGS
+
+// Block minwindef.h min/max macros to prevent conflict
+#define NOMINMAX
+
+#define WIN32_LEAN_AND_MEAN
+#include
+
+#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
+
+#include
+
+#include "../inc/LibraryIncludes.h"
+
+#include
+
+#include
+#include
+
+#include
+#include