diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 23511535fb5..8d8329bf3b5 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -99,7 +99,8 @@ Resources/(?!en) ^NOTICE.md ^oss/.*?/ ^samples/PixelShaders/Screenshots/ -^src/cascadia/TerminalSettingsEditor/SegoeFluentIconList.h$ +^src/cascadia/TerminalApp/TmuxControl\.cpp$ +^src/cascadia/TerminalSettingsEditor/SegoeFluentIconList\.h$ ^src/interactivity/onecore/BgfxEngine\. ^src/renderer/atlas/ ^src/renderer/wddmcon/WddmConRenderer\. diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 9edcb6a81c7..a7af3e15c83 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -284,6 +284,15 @@ namespace winrt::TerminalApp::implementation const auto& activeTab{ _senderOrFocusedTab(sender) }; + if constexpr (Feature_TmuxControl::IsEnabled()) + { + //Tmux control takes over + if (_tmuxControl && _tmuxControl->TabIsTmuxControl(activeTab)) + { + return _tmuxControl->SplitPane(activeTab, realArgs.SplitDirection()); + } + } + _SplitPane(activeTab, realArgs.SplitDirection(), // This is safe, we're already filtering so the value is (0, 1) diff --git a/src/cascadia/TerminalApp/DebugTapConnection.cpp b/src/cascadia/TerminalApp/DebugTapConnection.cpp index 41416c3b98c..7cfa19b5a0f 100644 --- a/src/cascadia/TerminalApp/DebugTapConnection.cpp +++ b/src/cascadia/TerminalApp/DebugTapConnection.cpp @@ -120,9 +120,9 @@ namespace winrt::Microsoft::TerminalApp::implementation return ConnectionState::Failed; } - void DebugTapConnection::_OutputHandler(const std::wstring_view str) + void DebugTapConnection::_OutputHandler(const winrt::array_view str) { - auto output = til::visualize_control_codes(str); + auto output = til::visualize_control_codes(winrt_array_to_wstring_view(str)); // To make the output easier to read, we introduce a line break whenever // an LF control is encountered. But at this point, the LF would have // been converted to U+240A (␊), so that's what we need to search for. @@ -130,7 +130,7 @@ namespace winrt::Microsoft::TerminalApp::implementation { output.insert(++lfPos, L"\r\n"); } - TerminalOutput.raise(output); + TerminalOutput.raise(winrt_wstring_to_array_view(output)); } // Called by the DebugInputTapConnection to print user input @@ -138,7 +138,7 @@ namespace winrt::Microsoft::TerminalApp::implementation { auto clean{ til::visualize_control_codes(str) }; auto formatted{ wil::str_printf(L"\x1b[91m%ls\x1b[m", clean.data()) }; - TerminalOutput.raise(formatted); + TerminalOutput.raise(winrt_wstring_to_array_view(formatted)); } // Wire us up so that we can forward input through diff --git a/src/cascadia/TerminalApp/DebugTapConnection.h b/src/cascadia/TerminalApp/DebugTapConnection.h index 70b607e82ff..57dfbe45729 100644 --- a/src/cascadia/TerminalApp/DebugTapConnection.h +++ b/src/cascadia/TerminalApp/DebugTapConnection.h @@ -31,7 +31,7 @@ namespace winrt::Microsoft::TerminalApp::implementation private: void _PrintInput(const std::wstring_view data); - void _OutputHandler(const std::wstring_view str); + void _OutputHandler(const winrt::array_view str); winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection::TerminalOutput_revoker _outputRevoker; winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection::StateChanged_revoker _stateChangedRevoker; diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index ed02e5250e1..2347b20df10 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1306,10 +1306,10 @@ void Pane::UpdateSettings(const CascadiaSettings& settings) // - splitType: How the pane should be attached // Return Value: // - the new reference to the child created from the current pane. -std::shared_ptr Pane::AttachPane(std::shared_ptr pane, SplitDirection splitType) +std::shared_ptr Pane::AttachPane(std::shared_ptr pane, SplitDirection splitType, const float splitSize) { // Splice the new pane into the tree - const auto [first, _] = _Split(splitType, .5, pane); + const auto [first, _] = _Split(splitType, splitSize, pane); // If the new pane has a child that was the focus, re-focus it // to steal focus from the currently focused pane. @@ -2298,8 +2298,7 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect _firstChild->Closed(_firstClosedToken); _secondChild->Closed(_secondClosedToken); // If we are not a leaf we should create a new pane that contains our children - auto first = std::make_shared(_firstChild, _secondChild, _splitState, _desiredSplitPosition); - _firstChild = first; + _firstChild = std::make_shared(_firstChild, _secondChild, _splitState, _desiredSplitPosition); } else { diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index ecc81fad82f..9af361061d4 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -131,7 +131,8 @@ class Pane : public std::enable_shared_from_this void Close(); std::shared_ptr AttachPane(std::shared_ptr pane, - winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType); + winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, + const float splitSize = .5); std::shared_ptr DetachPane(std::shared_ptr pane); int GetLeafPaneCount() const noexcept; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 0dd84b19a4f..64883c52794 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -982,4 +982,10 @@ An invalid regular expression was found. - \ No newline at end of file + + Running in tmux control mode; Press 'q' to detach: + + + Tmux Control Tab + + diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 6fc6a9f9f86..dcbe2a28b26 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -933,6 +933,13 @@ namespace winrt::TerminalApp::implementation return res; } + void Tab::Close() + { + ASSERT_UI_THREAD(); + + Closed.raise(nullptr, nullptr); + } + // Method Description: // - Prepares this tab for being removed from the UI hierarchy by shutting down all active connections. void Tab::Shutdown() diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index be6180586ea..65d4e574ede 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -61,6 +61,7 @@ namespace winrt::TerminalApp::implementation void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); void UpdateTitle(); + void Close(); void Shutdown(); void ClosePane(); diff --git a/src/cascadia/TerminalApp/Tab.idl b/src/cascadia/TerminalApp/Tab.idl index e585babafc4..63f5095178c 100644 --- a/src/cascadia/TerminalApp/Tab.idl +++ b/src/cascadia/TerminalApp/Tab.idl @@ -22,6 +22,7 @@ namespace TerminalApp UInt32 TabViewNumTabs; void Focus(Windows.UI.Xaml.FocusState focusState); + void Close(); void Shutdown(); void SetDispatch(ShortcutActionDispatch dispatch); diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index c0c5eb6abea..8869468aae1 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -411,13 +411,11 @@ namespace winrt::TerminalApp::implementation auto actions = t->BuildStartupActions(BuildStartupKind::None); _AddPreviouslyClosedPaneOrTab(std::move(actions)); - _RemoveTab(tab); + tab.Close(); } - // Method Description: - // - Removes the tab (both TerminalControl and XAML) - // Arguments: - // - tab: the tab to remove + // Removes the tab (both TerminalControl and XAML). + // NOTE: Don't call this directly, but rather `tab.Close()`. void TerminalPage::_RemoveTab(const winrt::TerminalApp::Tab& tab) { uint32_t tabIndex{}; diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index 322b30b576a..331f0010063 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -173,6 +173,9 @@ TerminalPaneContent.idl + + TerminalPage.idl + @@ -285,6 +288,9 @@ TerminalPaneContent.idl + + TerminalPage.xaml + diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters index e15e8c8bfbc..e28294109e0 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters @@ -3,6 +3,7 @@ + @@ -33,8 +34,7 @@ - - + @@ -60,14 +60,13 @@ fzf - - fzf - - - + + + controls + @@ -90,7 +89,8 @@ - + + @@ -123,6 +123,7 @@ + @@ -155,4 +156,4 @@ app - + \ No newline at end of file diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 229fd98757f..7ecc9d07023 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -5,10 +5,14 @@ #include "pch.h" #include "TerminalPage.h" +#include #include +#include #include -#include +#include "../../types/inc/ColorFix.hpp" +#include "../../types/inc/utils.hpp" +#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" #include "App.h" #include "DebugTapConnection.h" #include "MarkdownPaneContent.h" @@ -18,9 +22,7 @@ #include "SnippetsPaneContent.h" #include "TabRowControl.h" #include "TerminalSettingsCache.h" -#include "../../types/inc/ColorFix.hpp" -#include "../../types/inc/utils.hpp" -#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" +#include "TmuxControl.h" #include "LaunchPositionRequest.g.cpp" #include "RenameWindowRequestedArgs.g.cpp" @@ -404,6 +406,15 @@ namespace winrt::TerminalApp::implementation TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + if constexpr (Feature_TmuxControl::IsEnabled()) + { + // tmux control takes over + if (page->_tmuxControl && page->_tmuxControl->TabIsTmuxControl(page->_GetFocusedTabImpl())) + { + return; + } + } + page->_OpenNewTerminalViaDropdown(NewTerminalArgs()); } }); @@ -1429,6 +1440,15 @@ namespace winrt::TerminalApp::implementation } if (altPressed && !debugTap) { + // tmux control panes don't share tab with other panes + if constexpr (Feature_TmuxControl::IsEnabled()) + { + if (_tmuxControl && _tmuxControl->TabIsTmuxControl(_GetFocusedTabImpl())) + { + return; + } + } + this->_SplitPane(_GetFocusedTabImpl(), SplitDirection::Automatic, 0.5f, @@ -2525,6 +2545,15 @@ namespace winrt::TerminalApp::implementation return false; } + if constexpr (Feature_TmuxControl::IsEnabled()) + { + //Tmux control tab doesn't support to drag + if (_tmuxControl && _tmuxControl->TabIsTmuxControl(tab)) + { + return false; + } + } + // If there was a windowId in the action, try to move it to the // specified window instead of moving it in our tab row. const auto windowId{ args.Window() }; @@ -3581,6 +3610,26 @@ namespace winrt::TerminalApp::implementation original->SetActive(); } + if constexpr (Feature_TmuxControl::IsEnabled()) + { + if (!_tmuxControl) + { + _tmuxControl = std::make_shared(*this); + } + + control.EnterTmuxControl([tmuxControl = _tmuxControl.get()](auto&& sender, auto&& args) { + if (auto control = sender.try_as()) + { + if (tmuxControl->AcquireSingleUseLock(std::move(control))) + { + args.InputCallback([tmuxControl](auto&& str) { + tmuxControl->FeedInput(winrt_array_to_wstring_view(str)); + }); + } + } + }); + } + return resultPane; } @@ -5000,9 +5049,10 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_adjustProcessPriority() const { // Windowing is single-threaded, so this will not cause a race condition. - static bool supported{ true }; + static uint64_t s_lastUpdateHash{ 0 }; + static bool s_supported{ true }; - if (!supported || !_hostingHwnd.has_value()) + if (!s_supported || !_hostingHwnd.has_value()) { return; } @@ -5066,11 +5116,20 @@ namespace winrt::TerminalApp::implementation } const auto count{ gsl::narrow_cast(it - processes.begin()) }; + const auto hash = til::hash((void*)processes.data(), count * sizeof(HANDLE)); + + if (hash == s_lastUpdateHash) + { + return; + } + + s_lastUpdateHash = hash; const auto hr = TerminalTrySetWindowAssociatedProcesses(_hostingHwnd.value(), count, count ? processes.data() : nullptr); + if (S_FALSE == hr) { // Don't bother trying again or logging. The wrapper tells us it's unsupported. - supported = false; + s_supported = false; return; } @@ -5468,6 +5527,15 @@ namespace winrt::TerminalApp::implementation tabImpl.copy_from(winrt::get_self(tabBase)); if (tabImpl) { + if constexpr (Feature_TmuxControl::IsEnabled()) + { + //Tmux control tab doesn't support to drag + if (_tmuxControl && _tmuxControl->TabIsTmuxControl(tabImpl)) + { + return; + } + } + // First: stash the tab we started dragging. // We're going to be asked for this. _stashed.draggedTab = tabImpl; diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 10b58e175dc..e01741744e1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -12,6 +12,7 @@ #include "RenameWindowRequestedArgs.g.h" #include "RequestMoveContentArgs.g.h" #include "LaunchPositionRequest.g.h" +#include "TmuxControl.h" #include "Toast.h" #include "WindowsPackageManagerFactory.h" @@ -256,6 +257,7 @@ namespace winrt::TerminalApp::implementation std::vector> _previouslyClosedPanesAndTabs{}; uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; + std::shared_ptr _tmuxControl{ nullptr }; // use a weak reference to prevent circular dependency with AppLogic winrt::weak_ref _dialogPresenter; @@ -580,6 +582,7 @@ namespace winrt::TerminalApp::implementation friend class TerminalAppLocalTests::TabTests; friend class TerminalAppLocalTests::SettingsTests; + friend class TmuxControl; }; } diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.cpp b/src/cascadia/TerminalApp/TerminalPaneContent.cpp index 04a03f8b9d4..40f45b5ad1c 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.cpp +++ b/src/cascadia/TerminalApp/TerminalPaneContent.cpp @@ -68,6 +68,8 @@ namespace winrt::TerminalApp::implementation { _removeControlEvents(); + _control.Close(); + // Clear out our media player callbacks, and stop any playing media. This // will prevent the callback from being triggered after we've closed, and // also make sure that our sound stops when we're closed. diff --git a/src/cascadia/TerminalApp/TmuxControl.cpp b/src/cascadia/TerminalApp/TmuxControl.cpp new file mode 100644 index 00000000000..7e08b6a9e76 --- /dev/null +++ b/src/cascadia/TerminalApp/TmuxControl.cpp @@ -0,0 +1,1392 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TmuxControl.h" + +#include +#include +#include + +#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" +#include "TerminalPage.h" +#include "TabRowControl.h" + +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Control; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalConnection; +using namespace winrt::Windows::System; +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Core; + +static const float PaneBorderSize = 2; +static const int StaticMenuCount = 4; // "Separator" "Settings" "Command Palette" "About" + +// Set this to 1 to enable debug logging +#define DEBUG 0 + +#if DEBUG +#define print_debug(s, ...) \ + OutputDebugStringW(fmt::format(FMT_COMPILE(L"TMUX " s) __VA_OPT__(, ) __VA_ARGS__).c_str()); +#else +#define print_debug(s, ...) +#endif + +static std::wstring_view split_line(std::wstring_view& remaining) +{ + auto lf = remaining.find(L'\n'); + lf = std::min(lf, remaining.size()); + + // Trim any potential \r before the \n + auto end = lf; + if (end != 0 && remaining[end - 1] == L'\r') + { + --end; + } + + const auto line = til::safe_slice_abs(remaining, 0, end); + remaining = til::safe_slice_abs(remaining, lf + 1, std::wstring_view::npos); + return line; +} + +static std::wstring_view tokenize_field(std::wstring_view& remaining) +{ + const auto end = remaining.find(L' '); + const auto field = til::safe_slice_abs(remaining, 0, end); + const auto beg_next = remaining.find_first_not_of(L' ', end); + remaining = til::safe_slice_abs(remaining, beg_next, std::wstring_view::npos); + return field; +} + +static std::optional tokenize_number(std::wstring_view& remaining) +{ + return til::parse_unsigned(tokenize_field(remaining), 10); +} + +enum class IdentifierType : wchar_t +{ + Invalid = 0, + Session = '$', + Window = '@', + Pane = '%', +}; + +struct Identifier +{ + IdentifierType type; + int64_t value; +}; + +static Identifier tokenize_identifier(std::wstring_view& remaining) +{ + Identifier result = { + .type = IdentifierType::Invalid, + .value = -1, + }; + + const auto field = tokenize_field(remaining); + if (field.empty()) + { + return result; + } + + const auto type = field.front(); + switch (type) + { + case L'$': + case L'@': + case L'%': + break; + default: + return result; + } + + const auto id = til::parse_unsigned(field.substr(1), 10); + if (!id) + { + return result; + } + + result.type = (IdentifierType)type; + result.value = *id; + return result; +} + +namespace winrt::TerminalApp::implementation +{ + TmuxControl::TmuxControl(TerminalPage& page) : + _page{ page } + { + _dispatcherQueue = DispatcherQueue::GetForCurrentThread(); + + const auto newTabRun = Documents::Run(); + newTabRun.Text(RS_(L"NewTabRun/Text")); + const auto newPaneRun = Documents::Run(); + newPaneRun.Text(RS_(L"NewPaneRun/Text")); + + const auto textBlock = Controls::TextBlock{}; + textBlock.Inlines().Append(newTabRun); + textBlock.Inlines().Append(Documents::LineBreak{}); + textBlock.Inlines().Append(newPaneRun); + + Controls::ToolTipService::SetToolTip(_newTabMenu, box_value(textBlock)); + _newTabMenu.Text(RS_(L"NewTmuxControlTab/Text")); + + Controls::FontIcon newTabIcon; + newTabIcon.Glyph(L"\xF714"); + newTabIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons,Segoe MDL2 Assets" }); + _newTabMenu.Icon(newTabIcon); + + _newTabMenu.Click([this](auto&&, auto&&) { + _openNewTerminalViaDropdown(); + }); + } + + bool TmuxControl::AcquireSingleUseLock(winrt::Microsoft::Terminal::Control::TermControl control) noexcept + { + if (_inUse) + { + return false; + } + + // NOTE: This is safe, because `.control` only gets accessed via FeedInput(), + // when it receives %session-changed and after it transitioned to the UI thread. + _control = std::move(control); + return true; + } + + void TmuxControl::FeedInput(std::wstring_view str) + { + if (str.empty()) + { + return; + } + + // Our LF search logic is unable to recognize the lone ESC character + // (= ST = end) as its own line. Let's just special case it here. + if (str.ends_with(L'\x1b')) + { + _parseLine(std::wstring{ L"\x1b" }); + return; + } + + auto idx = str.find(L'\n'); + + // If there's leftover partial line, append the new data to it first. + if (!_lineBuffer.empty()) + { + const auto line = til::safe_slice_abs(str, 0, idx); + _lineBuffer.insert(_lineBuffer.end(), line.begin(), line.end()); + + // If this still wasn't a full line, wait for more data. + if (idx == std::wstring_view::npos) + { + return; + } + + // Strip of any remaining CR. We already removed the LF after the find() call. + if (!_lineBuffer.empty() && _lineBuffer.back() == L'\r') + { + _lineBuffer.pop_back(); + } + + _parseLine(std::move(_lineBuffer)); + _lineBuffer.clear(); + + // Move past the line we just processed. + str = til::safe_slice_abs(str, idx + 1, std::wstring_view::npos); + idx = str.find(L'\n'); + } + + while (idx != std::wstring_view::npos) + { + // Strip of any CR in front of our LF. + auto end = idx; + if (end != 0 && str[end - 1] == L'\r') + { + --end; + } + + const auto line = til::safe_slice_abs(str, 0, end); + _parseLine(std::wstring{ line }); + + str = til::safe_slice_abs(str, idx + 1, std::wstring_view::npos); + idx = str.find(L'\n'); + } + + // If there's any leftover partial line, stash it for later. + if (!str.empty()) + { + _lineBuffer.append(str); + } + } + + bool TmuxControl::TabIsTmuxControl(const winrt::com_ptr& tab) + { + assert(_dispatcherQueue.HasThreadAccess()); + + if (!tab) + { + return false; + } + + for (auto& t : _attachedWindows) + { + if (t.second->TabViewIndex() == tab->TabViewIndex()) + { + return true; + } + } + + if (_controlTab->TabViewIndex() == tab->TabViewIndex()) + { + return true; + } + + return false; + } + + void TmuxControl::SplitPane(const winrt::com_ptr& tab, SplitDirection direction) + { + const auto contentWidth = static_cast(_page._tabContent.ActualWidth()); + const auto contentHeight = static_cast(_page._tabContent.ActualHeight()); + const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; + + if (tab == nullptr) + { + return; + } + + const auto realSplitType = tab->PreCalculateCanSplit(direction, 0.5f, availableSpace); + if (!realSplitType) + { + return; + } + + switch (*realSplitType) + { + case SplitDirection::Right: + _sendSplitPane(tab->GetActivePane(), SplitDirection::Right); + break; + case SplitDirection::Down: + _sendSplitPane(tab->GetActivePane(), SplitDirection::Down); + break; + default: + break; + } + } + + safe_void_coroutine TmuxControl::_parseLine(std::wstring line) + { + if (line.empty()) + { + co_return; + } + + const auto self = shared_from_this(); + co_await wil::resume_foreground(_dispatcherQueue); + + print_debug(L"<<< {}\n", line); + + std::wstring_view remaining{ line }; + const auto type = tokenize_field(remaining); + + // Are we inside a %begin ... %end block? Anything until %end or %error + // is considered part of the output so this deserves special handling. + if (_insideOutputBlock) + { + if (til::equals(type, L"%end")) + { + _handleResponse(std::move(_responseBuffer)); + _responseBuffer.clear(); + _insideOutputBlock = false; + } + else if (til::equals(type, L"%error")) + { + // In theory our commands should not result in errors. + if (_state != State::Init) + { + assert(false); + } + + if (_control) + { + _responseBuffer.append(L"\r\n"); + _control.InjectTextAtCursor(_responseBuffer); + } + + if (!_commandQueue.empty()) + { + _commandQueue.pop_front(); + } + _responseBuffer.clear(); + _insideOutputBlock = false; + } + else + { + // Note that at this point `remaining` will not be the whole `line` anymore. + if (_responseBuffer.empty()) + { + _responseBuffer = std::move(line); + } + else + { + _responseBuffer.append(L"\r\n"); + _responseBuffer.append(line); + } + } + } + // Otherwise, we check for the, presumably, most common output type first: %output. + else if (til::equals(type, L"%output")) + { + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Pane) + { + _deliverOutputToPane(id.value, remaining); + } + } + else if (til::equals(type, L"%begin")) + { + _insideOutputBlock = true; + } + else if (til::equals(type, L"%session-changed")) + { + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Session) + { + _handleSessionChanged(id.value); + } + } + else if (til::equals(type, L"%window-add")) + { + // We'll handle the initial window discovery ourselves during %session-changed. + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Window) + { + _handleWindowAdd(id.value); + } + } + else if (til::equals(type, L"%window-close")) + { + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Window) + { + _handleWindowClose(id.value); + } + } + else if (til::equals(type, L"%window-pane-changed")) + { + const auto windowId = tokenize_identifier(remaining); + const auto paneId = tokenize_identifier(remaining); + + if (windowId.type == IdentifierType::Window && paneId.type == IdentifierType::Pane) + { + _handleWindowPaneChanged(windowId.value, paneId.value); + } + } + else if (til::equals(type, L"%window-renamed")) + { + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Window) + { + _handleWindowRenamed(id.value, winrt::hstring{ remaining }); + } + } + else if (til::equals(type, L"%layout-change")) + { + const auto windowId = tokenize_identifier(remaining); + const auto layout = tokenize_field(remaining); + + if (windowId.type == IdentifierType::Window && !layout.empty()) + { + _handleLayoutChange(windowId.value, layout); + } + } + else if (til::equals(type, L"\033")) + { + _handleDetach(); + } + } + + void TmuxControl::_handleAttach() + { + _state = State::Attaching; + + if (const auto terminalTab{ _page._GetFocusedTabImpl() }) + { + if (const auto pane{ terminalTab->GetActivePane() }) + { + _profile = pane->GetProfile(); + } + } + if (!_profile) + { + _profile = CascadiaSettings::LoadDefaults().ProfileDefaults(); + } + + // TODO: The CharacterDimensions may be non-default because the text is zoomed in. + const auto fontSize = _control.CharacterDimensions(); + const auto width = _page.ActualWidth(); + const auto height = _page.ActualHeight(); + + // Tmux use one character to draw separator line, so we have to make the padding + // plus two borders equals one character's width or height + // Same reason, we have to disable the scrollbar. Otherwise, the local panes size + // will not match Tmux's. + _fontWidth = fontSize.Width; + _fontHeight = fontSize.Height; + _thickness.Left = _thickness.Right = std::max(0.0f, (_fontWidth - 2 * PaneBorderSize) / 2); + _thickness.Top = _thickness.Bottom = std::max(0.0f, (_fontHeight - 2 * PaneBorderSize) / 2); + _terminalWidth = (til::CoordType)floor((width - _thickness.Left - _thickness.Right) / _fontWidth); + _terminalHeight = (til::CoordType)floor((height - _thickness.Top - _thickness.Bottom) / _fontHeight); + + _profile.Padding(XamlThicknessToOptimalString(_thickness)); + _profile.ScrollState(winrt::Microsoft::Terminal::Control::ScrollbarState::Hidden); + _profile.Icon(MediaResourceHelper::FromString(L"\uF714")); + _profile.Name(L"TmuxTab"); + + // Intercept the control terminal's input, ignore all user input, except 'q' as detach command. + _detachKeyDownRevoker = _control.KeyDown([this](auto, auto& e) { + if (e.Key() == VirtualKey::Q) + { + _sendIgnoreResponse(L"detach\n"); + } + e.Handled(true); + }); + + _windowSizeChangedRevoker = _page.SizeChanged([this](auto, auto) { + const auto fontSize = _control.CharacterDimensions(); + const auto width = _page.ActualWidth(); + const auto height = _page.ActualHeight(); + + _fontWidth = fontSize.Width; + _fontHeight = fontSize.Height; + _thickness.Left = _thickness.Right = std::max(0.0f, (_fontWidth - 2 * PaneBorderSize) / 2); + _thickness.Top = _thickness.Bottom = std::max(0.0f, (_fontHeight - 2 * PaneBorderSize) / 2); + _terminalWidth = (til::CoordType)floor((width - _thickness.Left - _thickness.Right) / _fontWidth); + _terminalHeight = (til::CoordType)floor((height - _thickness.Top - _thickness.Bottom) / _fontHeight); + + _sendSetOption(fmt::format(FMT_COMPILE(L"default-size {}x{}"), _terminalWidth, _terminalHeight)); + + for (auto& w : _attachedWindows) + { + _sendResizeWindow(w.first, _terminalWidth, _terminalHeight); + } + }); + + // Dynamically insert the "Tmux Control Tab" menu item into flyout menu + const auto tabRow = _page.TabRow(); + const auto tabRowImpl = winrt::get_self(tabRow); + const auto newTabButton = tabRowImpl->NewTabButton(); + const auto flyout = newTabButton.Flyout().as(); + const auto menuCount = flyout.Items().Size(); + flyout.Items().InsertAt(menuCount - StaticMenuCount, _newTabMenu); + + // Register new tab button click handler for tmux control + _newTabClickRevoker = newTabButton.Click([this](auto&&, auto&&) { + if (TabIsTmuxControl(_page._GetFocusedTabImpl())) + { + _openNewTerminalViaDropdown(); + } + }); + + _controlTab = _page._GetTabImpl(_page._GetFocusedTab()); + _control.InjectTextAtCursor(RS_(L"TmuxControlInfo")); + } + + void TmuxControl::_handleDetach() + { + // WARNING: The Pane/AttachedPane destructors are highly non-trivial. Due to how they drop the potentially last + // reference to TermControl, they may close the TmuxConnection, which in turn calls back into TmuxControl. + // To make the destruction predictable, we extend their lifetime until after we cleared out everything. + const auto attachedWindows = std::move(_attachedWindows); + const auto attachedPanes = std::move(_attachedPanes); + + const auto control = std::move(_control); + const auto detachKeyDownRevoker = std::move(_detachKeyDownRevoker); + const auto windowSizeChangedRevoker = std::move(_windowSizeChangedRevoker); + const auto newTabClickRevoker = std::move(_newTabClickRevoker); + auto& page = _page; + + { + _control = nullptr; + _controlTab = nullptr; + _profile = nullptr; + _state = State::Init; + _inUse = false; + + _lineBuffer.clear(); + _responseBuffer.clear(); + _insideOutputBlock = false; + + _detachKeyDownRevoker = {}; + _windowSizeChangedRevoker = {}; + _newTabClickRevoker = {}; + + _commandQueue.clear(); + _attachedWindows.clear(); + _attachedPanes.clear(); + + _sessionId = -1; + _activePaneId = -1; + _activeWindowId = -1; + + _terminalWidth = 0; + _terminalHeight = 0; + _thickness = {}; + _fontWidth = 0; + _fontHeight = 0; + + _splittingPane = {}; + } + + // WARNING: If you see any class members being used past this point you're doing it wrong. + // Move then into local variables first. This ensures that callbacks, etc., see the reset state already. + + for (auto& w : attachedWindows) + { + w.second->Close(); + } + + const auto tabRow = page.TabRow(); + const auto tabRowImpl = winrt::get_self(tabRow); + const auto newTabButton = tabRowImpl->NewTabButton(); + const auto newTabItems = newTabButton.Flyout().as().Items(); + + control.KeyDown(detachKeyDownRevoker); + page.SizeChanged(windowSizeChangedRevoker); + newTabButton.Click(newTabClickRevoker); + + // Remove the "Tmux Control Tab" menu item from flyout menu + uint32_t i = 0; + for (const auto& m : newTabItems) + { + if (const auto item = m.try_as(); item && item.Text() == RS_(L"NewTmuxControlTab/Text")) + { + newTabItems.RemoveAt(i); + break; + } + i++; + } + } + + void TmuxControl::_handleSessionChanged(int64_t sessionId) + { + _sessionId = sessionId; + _sendSetOption(fmt::format(FMT_COMPILE(L"default-size {}x{}"), _terminalWidth, _terminalHeight)); + _sendDiscoverWindows(_sessionId); + } + + void TmuxControl::_handleWindowAdd(int64_t windowId) + { + _sendDiscoverNewWindow(windowId); + } + + void TmuxControl::_handleWindowRenamed(int64_t windowId, winrt::hstring name) + { + if (const auto tab = _getTab(windowId)) + { + tab->SetTabText(std::move(name)); + } + } + + void TmuxControl::_handleWindowClose(int64_t windowId) + { + std::erase_if(_attachedPanes, [windowId](const auto& pair) { + return pair.second.windowId == windowId; + }); + + if (const auto nh = _attachedWindows.extract(windowId)) + { + nh.mapped()->Close(); + } + } + + void TmuxControl::_handleWindowPaneChanged(int64_t windowId, int64_t newPaneId) + { + const auto tab = _getTab(windowId); + if (!tab) + { + return; + } + + winrt::Microsoft::Terminal::Control::TermControl control{ nullptr }; + + // TODO: The system of relying on _splittingPane to compute pane + // splits and know which direction to split is highly fragile. + if (_splittingPane.first) + { + const auto activePane = tab->GetActivePane(); + if (activePane.get() == _splittingPane.first.get()) + { + auto [p, pane] = _newPane(windowId, newPaneId); + control = p.control; + tab->SplitPane(_splittingPane.second, 0.5f, std::move(pane)); + } + + _splittingPane.first = nullptr; + } + else + { + if (const auto it = _attachedPanes.find(newPaneId); it != _attachedPanes.end()) + { + control = it->second.control; + } + } + + if (control) + { + control.Focus(FocusState::Programmatic); + } + } + + // TODO: How do we reconcile an arbitrary layout change? + void TmuxControl::_handleLayoutChange(int64_t windowId, std::wstring_view layout) + { + auto remaining = _layoutStripHash(layout); + std::unordered_set seen; + + seen.reserve(_attachedPanes.size() + 1); + seen.insert(-1); // Always keep panes with id -1 (uninitialized) + + while (!remaining.empty()) + { + const auto current = _layoutParseNextToken(remaining); + if (current.type == TmuxLayoutType::Pane) + { + seen.insert(current.id); + } + } + + std::erase_if(_attachedPanes, [&](const auto& pair) { + return pair.second.windowId == windowId && !seen.contains(pair.first); + }); + } + + void TmuxControl::_handleResponse(std::wstring_view response) + { + // The first begin/end block we receive will come unprompted from tmux. + if (_state == State::Init) + { + _handleAttach(); + return; + } + + if (_commandQueue.empty()) + { + // tmux should theoretically not send us any output blocks unprompted. + assert(false); + return; + } + + const auto info = _commandQueue.front(); + _commandQueue.pop_front(); + + switch (info.type) + { + case ResponseInfoType::Ignore: + break; + case ResponseInfoType::DiscoverNewWindow: + _handleResponseDiscoverNewWindow(response); + break; + case ResponseInfoType::DiscoverWindows: + _handleResponseDiscoverWindows(response); + break; + case ResponseInfoType::CapturePane: + _handleResponseCapturePane(info, response); + break; + case ResponseInfoType::DiscoverPanes: + _handleResponseDiscoverPanes(response); + break; + } + } + + void TmuxControl::_sendSetOption(std::wstring_view option) + { + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"set-option {}\n"), option)); + } + + // When we join a brand new session, tmux will output: + // %begin 1765124793 272 0 + // %end 1765124793 272 0 + // %window-add @0 + // %sessions-changed + // %session-changed $0 0 + // %window-renamed @0 tmux + // %output %0 ... + // whereas if we join an existing session, we get: + // %begin 1765125530 285 0 + // %end 1765125530 285 0 + // %session-changed $0 0 + // + // Because of this, we have to send a `list-windows` command ourselves. + // We do this after the `session-changed` notification, because at that point we + // received any potential `window-add` notifications that would indicate a new session. + void TmuxControl::_sendDiscoverWindows(int64_t sessionId) + { + const auto cmd = fmt::format(FMT_COMPILE(L"list-windows -t ${} -F '#{{session_id}} #{{window_id}} #{{window_width}} #{{window_height}} #{{history_limit}} #{{window_active}} #{{window_layout}} #{{window_name}}'\n"), sessionId); + const auto info = ResponseInfo{ + .type = ResponseInfoType::DiscoverWindows, + }; + _sendWithResponseInfo(cmd, info); + } + + void TmuxControl::_handleResponseDiscoverWindows(std::wstring_view response) + { + while (!response.empty()) + { + auto line = split_line(response); + const auto sessionId = tokenize_identifier(line); + const auto windowId = tokenize_identifier(line); + const auto windowWidth = tokenize_number(line); + const auto windowHeight = tokenize_number(line); + const auto historyLimit = tokenize_number(line); + const auto windowActive = tokenize_number(line); + const auto windowLayout = tokenize_field(line); + const auto windowName = line; + + if (sessionId.type != IdentifierType::Session || + windowId.type != IdentifierType::Window || + !windowWidth || + !windowHeight || + !historyLimit || + !windowActive || + windowName.empty()) + { + assert(false); + continue; + } + + if (_attachedWindows.contains(windowId.value)) + { + print_debug(L"--> _handleResponseDiscoverWindows: skip {}\n", windowId.value); + continue; + } + + print_debug(L"--> _handleResponseDiscoverWindows: new window {}\n", windowId.value); + + auto remaining = _layoutStripHash(windowLayout); + const auto firstPane = _layoutCreateRecursive(windowId.value, remaining, TmuxLayout{}); + _newTab(windowId.value, winrt::hstring{ windowName }, firstPane); + + // I'm not sure if I'm missing anything when I read the tmux spec, + // but to me it seems like it's an inherently a racy protocol. + // As a best-effort attempt we resize first (= potentially generates output, which we then ignore), + // then we capture the panes' content (after which we stop ignoring output, + // and finally we fix the current cursor position, and similar terminal state. + _sendResizeWindow(windowId.value, _terminalWidth, _terminalHeight); + for (auto& p : _attachedPanes) + { + if (p.second.windowId == windowId.value) + { + // Discard any output we got/get until we captured the pane. + p.second.ignoreOutput = true; + p.second.outputBacklog.clear(); + + _sendCapturePane(p.second.paneId, (til::CoordType)*historyLimit); + } + } + _sendDiscoverPanes(windowId.value); + } + + _state = State::Attached; + } + + std::shared_ptr TmuxControl::_layoutCreateRecursive(int64_t windowId, std::wstring_view& remaining, TmuxLayout parent) + { + const auto direction = parent.type == TmuxLayoutType::PushVertical ? SplitDirection::Down : SplitDirection::Right; + auto layoutSize = direction == SplitDirection::Right ? parent.width : parent.height; + std::shared_ptr firstPane; + std::shared_ptr lastPane; + til::CoordType lastPaneSize = 0; + + while (!remaining.empty()) + { + const auto current = _layoutParseNextToken(remaining); + std::shared_ptr pane; + + switch (current.type) + { + case TmuxLayoutType::Pane: + pane = _newPane(windowId, current.id).second; + break; + case TmuxLayoutType::PushHorizontal: + case TmuxLayoutType::PushVertical: + print_debug(L"--> _handleResponseDiscoverWindows: recurse {}\n", current.type == TmuxLayoutType::PushHorizontal ? L"horizontal" : L"vertical"); + pane = _layoutCreateRecursive(windowId, remaining, current); + break; + case TmuxLayoutType::Pop: + print_debug(L"--> _handleResponseDiscoverWindows: recurse pop\n"); + return firstPane; + } + + if (!pane) + { + assert(false); + continue; + } + + if (!firstPane) + { + firstPane = pane; + } + if (lastPane) + { + const auto splitSize = 1.0f - ((float)lastPaneSize / (float)layoutSize); + layoutSize -= lastPaneSize; + + print_debug(L"--> _handleResponseDiscoverWindows: new pane {} @ {:.1f}%\n", current.id, splitSize * 100); + lastPane->AttachPane(pane, direction, splitSize); + } + else + { + print_debug(L"--> _handleResponseDiscoverWindows: new pane {}\n", current.id); + } + + lastPane = std::move(pane); + lastPaneSize = direction == SplitDirection::Right ? current.width : current.height; + lastPaneSize += 1; // to account for tmux's separator line + } + + return firstPane; + } + + std::wstring_view TmuxControl::_layoutStripHash(std::wstring_view str) + { + const auto comma = str.find(L','); + if (comma != std::wstring_view::npos) + { + return str.substr(comma + 1); + } + else + { + assert(false); + return {}; + } + } + + // Example layouts: + // * single pane: + // cafd,120x29,0,0,0 + // * single horizontal split: + // 813e,120x29,0,0{60x29,0,0,0,59x29,61,0,1} + // * double horizontal split: + // 04d9,120x29,0,0{60x29,0,0,0,29x29,61,0,1,29x29,91,0,2} + // * double horizontal split + single vertical split in the middle pane: + // 773d,120x29,0,0{60x29,0,0,0,29x29,61,0[29x14,61,0,1,29x14,61,15,3],29x29,91,0,2} + TmuxControl::TmuxLayout TmuxControl::_layoutParseNextToken(std::wstring_view& remaining) + { + TmuxLayout layout{ .type = TmuxLayoutType::Pop }; + + if (remaining.empty()) + { + assert(false); + return layout; + } + + int64_t args[5]; + size_t arg_count = 0; + wchar_t sep = L'\0'; + + // Collect up to 5 arguments and the final separator + // 120x29,0,0,2, --> 120, 29, 0, 0, 2 + ',' + // 120x29,0,0{ --> 120, 29, 0, 0 + '{' + for (int i = 0; i < 5; ++i) + { + if (remaining.empty()) + { + // Failed to collect enough args? Error. + assert(false); + return layout; + } + + // If we're looking at a push/pop operation, break out. This is important + // for the latter, because nested layouts may end in `]]]`, etc. + sep = remaining[0]; + if (sep == L'[' || sep == L']' || sep == L'{' || sep == L'}') + { + remaining = remaining.substr(1); + break; + } + + // Skip 1 separator. Technically we should validate their correct position here, but meh. + if (sep == L',' || sep == L'x') + { + remaining = remaining.substr(1); + // We don't need to revalidate `remaining.empty()`, + // because parse_signed will return nullopt for empty strings. + } + + const auto end = std::min(remaining.size(), remaining.find_first_of(L",x[]{}")); + const auto val = til::parse_signed(remaining.substr(0, end), 10); + if (!val) + { + // Not an integer? Error. + assert(false); + return layout; + } + + args[arg_count++] = *val; + remaining = remaining.substr(end); + } + + switch (sep) + { + case L'[': + case L'{': + if (arg_count != 4) + { + assert(false); + return layout; + } + layout.type = sep == L'[' ? TmuxLayoutType::PushVertical : TmuxLayoutType::PushHorizontal; + layout.width = (til::CoordType)args[0]; + layout.height = (til::CoordType)args[1]; + return layout; + case L']': + case L'}': + if (arg_count != 0) + { + assert(false); + return layout; + } + // layout.type is already set to Pop. + return layout; + default: + if (arg_count != 5) + { + assert(false); + return layout; + } + layout.type = TmuxLayoutType::Pane; + layout.width = (til::CoordType)args[0]; + layout.height = (til::CoordType)args[1]; + layout.id = args[4]; + return layout; + } + } + + void TmuxControl::_sendDiscoverNewWindow(int64_t windowId) + { + const auto cmd = fmt::format(FMT_COMPILE(L"list-panes -t @{} -F '#{{window_id}} #{{pane_id}} #{{window_name}}'\n"), windowId); + const auto info = ResponseInfo{ + .type = ResponseInfoType::DiscoverNewWindow, + }; + _sendWithResponseInfo(cmd, info); + } + + void TmuxControl::_handleResponseDiscoverNewWindow(std::wstring_view response) + { + print_debug(L"--> _handleResponseDiscoverNewWindow\n"); + + const auto windowId = tokenize_identifier(response); + const auto paneId = tokenize_identifier(response); + const auto windowName = response; + + if (windowId.type == IdentifierType::Window && paneId.type == IdentifierType::Pane) + { + auto pane = _newPane(windowId.value, paneId.value).second; + _newTab(windowId.value, winrt::hstring{ windowName }, std::move(pane)); + } + else + { + assert(false); + } + } + + void TmuxControl::_sendCapturePane(int64_t paneId, til::CoordType history) + { + const auto cmd = fmt::format(FMT_COMPILE(L"capture-pane -epqCJN -S {} -t %{}\n"), -history, paneId); + const auto info = ResponseInfo{ + .type = ResponseInfoType::CapturePane, + .data = { + .capturePane = { + .paneId = paneId, + }, + }, + }; + _sendWithResponseInfo(cmd, info); + } + + void TmuxControl::_handleResponseCapturePane(const ResponseInfo& info, std::wstring_view response) + { + print_debug(L"--> _handleResponseCapturePane\n"); + + const auto p = _attachedPanes.find(info.data.capturePane.paneId); + if (p != _attachedPanes.end()) + { + p->second.ignoreOutput = false; + _deliverOutputToPane(info.data.capturePane.paneId, response); + } + } + + void TmuxControl::_sendDiscoverPanes(int64_t windowId) + { + // TODO: Here we would need to fetch much more than just the cursor position. + const auto cmd = fmt::format(FMT_COMPILE(L"list-panes -t @{} -F '#{{pane_id}} #{{cursor_x}} #{{cursor_y}}'\n"), windowId); + const auto info = ResponseInfo{ + .type = ResponseInfoType::DiscoverPanes, + }; + _sendWithResponseInfo(cmd, info); + } + + void TmuxControl::_handleResponseDiscoverPanes(std::wstring_view response) + { + while (!response.empty()) + { + auto line = split_line(response); + const auto paneId = tokenize_identifier(line); + const auto cursorX = tokenize_number(line); + const auto cursorY = tokenize_number(line); + + if (paneId.type == IdentifierType::Pane && cursorX && cursorY) + { + const auto str = fmt::format(FMT_COMPILE(L"\033[{};{}H"), (til::CoordType)*cursorY + 1, (til::CoordType)*cursorX + 1); + _deliverOutputToPane(paneId.value, str); + } + else + { + assert(false); + } + } + } + + void TmuxControl::_sendNewWindow() + { + _sendIgnoreResponse(L"new-window\n"); + } + + void TmuxControl::_sendKillWindow(int64_t windowId) + { + // If we get a window-closed event, we call .Close() on the tab. + // But that will raise a Closed event which will in turn call this function. + // To avoid any loops, just check real quick if this window even exists anymore. + if (_attachedWindows.erase(windowId) != 0) + { + std::erase_if(_attachedPanes, [windowId](const auto& pair) { + return pair.second.windowId == windowId; + }); + + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"kill-window -t @{}\n"), windowId)); + } + } + + void TmuxControl::_sendKillPane(int64_t paneId) + { + // Same reasoning as in _sendKillWindow as to why we check `_attachedPanes`. + if (const auto nh = _attachedPanes.extract(paneId)) + { + const auto windowId = nh.mapped().windowId; + + // Check if there are more panes left in this window. + // If so, we kill this pane only. + for (const auto& p : _attachedPanes) + { + if (p.second.windowId == windowId) + { + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"kill-pane -t %{}\n"), paneId)); + return; + } + } + + // Otherwise, we kill the whole window. + _sendKillWindow(windowId); + } + } + + void TmuxControl::_sendSplitPane(std::shared_ptr pane, SplitDirection direction) + { + if (_splittingPane.first != nullptr) + { + return; + } + + if (!pane) + { + return; + } + + int64_t paneId = -1; + for (auto& p : _attachedPanes) + { + if (pane->GetTerminalControl() == p.second.control) + { + paneId = p.first; + } + } + if (paneId == -1) + { + return; + } + + _splittingPane = { pane, direction }; + + const auto dir = direction == SplitDirection::Right ? L'h' : L'v'; + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"split-window -t %{} -{}\n"), paneId, dir)); + } + + void TmuxControl::_sendSelectWindow(int64_t windowId) + { + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"select-window -t @{}\n"), windowId)); + } + + void TmuxControl::_sendSelectPane(int64_t paneId) + { + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"select-pane -t %{}\n"), paneId)); + } + + void TmuxControl::_sendResizeWindow(int64_t windowId, til::CoordType width, til::CoordType height) + { + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"resize-window -t @{} -x {} -y {}\n"), windowId, width, height)); + } + + void TmuxControl::_sendResizePane(int64_t paneId, til::CoordType width, til::CoordType height) + { + if (width == 0 || height == 0) + { + return; + } + + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"resize-pane -t %{} -x {} -y {}\n"), paneId, width, height)); + } + + void TmuxControl::_sendSendKey(int64_t paneId, const std::wstring_view keys) + { + if (keys.empty()) + { + return; + } + + std::wstring buf; + fmt::format_to(std::back_inserter(buf), FMT_COMPILE(L"send-key -t %{}"), paneId); + for (auto& c : keys) + { + fmt::format_to(std::back_inserter(buf), FMT_COMPILE(L" {:#x}"), c); + } + buf.push_back(L'\n'); + _sendIgnoreResponse(buf); + } + + void TmuxControl::_sendIgnoreResponse(wil::zwstring_view cmd) + { + print_debug(L">>> {}", cmd); + + if (!_control) + { + // This is unfortunately not uncommon right now due to the callback system. + // Events may come in late during shutdown. + print_debug(L"WARN: delayed send with uninitialized TmuxControl\n"); + return; + } + + _control.RawWriteString(cmd); + _commandQueue.push_back(ResponseInfo{ + .type = ResponseInfoType::Ignore, + }); + } + + void TmuxControl::_sendWithResponseInfo(wil::zwstring_view cmd, ResponseInfo info) + { + print_debug(L">>> {}", cmd); + + if (!_control) + { + // This is unfortunately not uncommon right now due to the callback system. + // Events may come in late during shutdown. + print_debug(L"WARN: delayed send with uninitialized TmuxControl\n"); + return; + } + + _control.RawWriteString(cmd); + _commandQueue.push_back(info); + } + + void TmuxControl::_deliverOutputToPane(int64_t paneId, const std::wstring_view text) + { + const auto search = _attachedPanes.find(paneId); + if (search == _attachedPanes.end()) + { + _attachedPanes.emplace_hint( + search, + paneId, + AttachedPane{ + .paneId = paneId, + .outputBacklog = std::wstring{ text }, + }); + return; + } + + if (search->second.ignoreOutput) + { + return; + } + + if (!search->second.initialized) + { + print_debug(L"--> outputBacklog {}\n", paneId); + search->second.outputBacklog.append(text); + return; + } + + std::wstring out; + auto it = text.begin(); + const auto end = text.end(); + + while (it != end) + { + // Find start of any potential \xxx sequence + const auto start = std::find(it, end, L'\\'); + + // Copy any regular text + out.append(it, start); + it = start; + if (it == end) + { + break; + } + + // Process any \xxx sequences + while (it != end && *it == L'\\') + { + ++it; + + wchar_t c = 0; + for (int i = 0; i < 3 && it != end; ++i, ++it) + { + if (*it < L'0' || *it > L'7') + { + c = L'?'; + break; + } + c = c * 8 + (*it - L'0'); + } + + out.push_back(c); + } + } + + print_debug(L"--> _deliverOutputToPane {}\n", paneId); + search->second.connection.WriteOutput(winrt_wstring_to_array_view(out)); + } + + winrt::com_ptr TmuxControl::_getTab(int64_t windowId) const + { + const auto search = _attachedWindows.find(windowId); + if (search == _attachedWindows.end()) + { + return nullptr; + } + return search->second; + } + + void TmuxControl::_newTab(int64_t windowId, winrt::hstring name, std::shared_ptr pane) + { + assert(!_attachedWindows.contains(windowId)); + auto tab = _page._GetTabImpl(_page._CreateNewTabFromPane(std::move(pane))); + tab->SetTabText(name); + tab->Closed([this, windowId](auto&&, auto&&) { + _sendKillWindow(windowId); + }); + _attachedWindows.emplace(windowId, std::move(tab)); + } + + std::pair> TmuxControl::_newPane(int64_t windowId, int64_t paneId) + { + auto& p = _attachedPanes.try_emplace(paneId, AttachedPane{}).first->second; + assert(p.windowId == -1); + + const auto controlSettings = Settings::TerminalSettings::CreateWithProfile(_page._settings, _profile); + p.windowId = windowId; + p.paneId = paneId; + p.connection = TerminalConnection::TmuxConnection{}; + p.control = _page._CreateNewControlAndContent(controlSettings, p.connection); + + const auto pane = std::make_shared(winrt::make(_profile, _page._terminalSettingsCache, p.control)); + + p.connection.TerminalInput([this, paneId](const winrt::array_view keys) { + _sendSendKey(paneId, winrt_array_to_wstring_view(keys)); + }); + + p.control.Initialized([this, paneId](auto, auto) { + const auto search = _attachedPanes.find(paneId); + if (search == _attachedPanes.end()) + { + return; + } + search->second.initialized = true; + if (!search->second.outputBacklog.empty()) + { + _deliverOutputToPane(paneId, std::move(search->second.outputBacklog)); + search->second.outputBacklog.clear(); + } + }); + + p.control.GotFocus([this, windowId, paneId](auto, auto) { + if (_activePaneId == paneId) + { + return; + } + + _activePaneId = paneId; + _sendSelectPane(_activePaneId); + + if (_activeWindowId != windowId) + { + _activeWindowId = windowId; + _sendSelectWindow(_activeWindowId); + } + }); + + p.control.SizeChanged([this, paneId](auto, const Xaml::SizeChangedEventArgs& args) { + if (_state != State::Attached) + { + return; + } + // Ignore the new created + if (args.PreviousSize().Width == 0 || args.PreviousSize().Height == 0) + { + return; + } + + const auto width = (til::CoordType)lrint((args.NewSize().Width - 2 * _thickness.Left) / _fontWidth); + const auto height = (til::CoordType)lrint((args.NewSize().Height - 2 * _thickness.Top) / _fontHeight); + _sendResizePane(paneId, width, height); + }); + + // Here's where we could use pane->Closed() to call _sendKillPane. Unfortunately, the entire Pane event handling + // is very brittle. When you split a pane, most of its members (including the Closed event) stick to the new + // parent (non-leaf) pane. You can't change that either, because the Closed() event of the root pane is used + // to close the entire tab. There's no "pane split" event in order for the tab to know the root changed. + // So, we hook into the connection's StateChanged event. It's only raised on connection.Close(). + // All of this would need a big, ugly refactor. + p.connection.StateChanged([this, paneId](auto&&, auto&&) { + _sendKillPane(paneId); + }); + + return { p, pane }; + } + + void TmuxControl::_openNewTerminalViaDropdown() + { + const auto window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const auto altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + if (altPressed) + { + // tmux panes don't share tab with other profile panes + if (TabIsTmuxControl(_page._GetFocusedTabImpl())) + { + SplitPane(_page._GetFocusedTabImpl(), SplitDirection::Automatic); + } + } + else + { + _sendNewWindow(); + } + } +} diff --git a/src/cascadia/TerminalApp/TmuxControl.h b/src/cascadia/TerminalApp/TmuxControl.h new file mode 100644 index 00000000000..8c8b7315e01 --- /dev/null +++ b/src/cascadia/TerminalApp/TmuxControl.h @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +#include "Pane.h" + +namespace winrt::TerminalApp::implementation +{ + struct TerminalPage; + + class TmuxControl : public std::enable_shared_from_this + { + public: + TmuxControl(TerminalPage& page); + + bool AcquireSingleUseLock(winrt::Microsoft::Terminal::Control::TermControl control) noexcept; + bool TabIsTmuxControl(const winrt::com_ptr& tab); + void SplitPane(const winrt::com_ptr& tab, winrt::Microsoft::Terminal::Settings::Model::SplitDirection direction); + void FeedInput(std::wstring_view str); + + private: + enum class State + { + Init, + Attaching, + Attached, + }; + + enum class ResponseInfoType + { + Ignore, + DiscoverNewWindow, + DiscoverWindows, + CapturePane, + DiscoverPanes, + }; + + struct ResponseInfo + { + ResponseInfoType type; + union + { + struct + { + int64_t paneId; + } capturePane; + } data; + }; + + enum class TmuxLayoutType + { + // A single leaf pane + Pane, + // Indicates the start of a horizontal split layout + PushHorizontal, + // Indicates the start of a vertical split layout + PushVertical, + // Indicates the end of the most recent split layout + Pop, + }; + + struct TmuxLayout + { + TmuxLayoutType type = TmuxLayoutType::Pane; + + // Only set for: Pane, PushHorizontal, PushVertical + til::CoordType width = 0; + // Only set for: Pane, PushHorizontal, PushVertical + til::CoordType height = 0; + // Only set for: Pane + int64_t id = -1; + }; + + // AttachedPane should not need to be copied. Anything else would be a mistake. + // But if we added a constructor to it, we could not use designated initializers anymore. + // This marker makes it possible. + struct MoveOnlyMarker + { + MoveOnlyMarker() = default; + MoveOnlyMarker(MoveOnlyMarker&&) = default; + MoveOnlyMarker& operator=(MoveOnlyMarker&&) = default; + MoveOnlyMarker(const MoveOnlyMarker&) = delete; + MoveOnlyMarker& operator=(const MoveOnlyMarker&) = delete; + }; + + struct AttachedPane + { + int64_t windowId = -1; + int64_t paneId = -1; + winrt::Microsoft::Terminal::TerminalConnection::TmuxConnection connection{ nullptr }; + winrt::Microsoft::Terminal::Control::TermControl control{ nullptr }; + std::wstring outputBacklog; + bool initialized = false; + bool ignoreOutput = false; + + [[msvc::no_unique_address]] MoveOnlyMarker moveOnlyMarker; + }; + + safe_void_coroutine _parseLine(std::wstring line); + + void _handleAttach(); // A special case of _handleResponse() + void _handleDetach(); + void _handleSessionChanged(int64_t sessionId); + void _handleWindowAdd(int64_t windowId); + void _handleWindowRenamed(int64_t windowId, winrt::hstring name); + void _handleWindowClose(int64_t windowId); + void _handleWindowPaneChanged(int64_t windowId, int64_t paneId); + void _handleLayoutChange(int64_t windowId, std::wstring_view layout); + void _handleResponse(std::wstring_view result); + + void _sendSetOption(std::wstring_view option); + void _sendDiscoverWindows(int64_t sessionId); + void _handleResponseDiscoverWindows(std::wstring_view response); + void _sendDiscoverNewWindow(int64_t windowId); + void _handleResponseDiscoverNewWindow(std::wstring_view response); + void _sendCapturePane(int64_t paneId, til::CoordType history); + void _handleResponseCapturePane(const ResponseInfo& info, std::wstring_view response); + void _sendDiscoverPanes(int64_t windowId); + void _handleResponseDiscoverPanes(std::wstring_view response); + void _sendNewWindow(); + void _sendKillWindow(int64_t windowId); + void _sendKillPane(int64_t paneId); + void _sendSplitPane(std::shared_ptr pane, winrt::Microsoft::Terminal::Settings::Model::SplitDirection direction); + void _sendSelectWindow(int64_t windowId); + void _sendSelectPane(int64_t paneId); + void _sendResizeWindow(int64_t windowId, til::CoordType width, til::CoordType height); + void _sendResizePane(int64_t paneId, til::CoordType width, til::CoordType height); + void _sendSendKey(int64_t paneId, const std::wstring_view keys); + + void _sendIgnoreResponse(wil::zwstring_view cmd); + void _sendWithResponseInfo(wil::zwstring_view cmd, ResponseInfo info); + + std::shared_ptr _layoutCreateRecursive(int64_t windowId, std::wstring_view& remaining, TmuxLayout parent); + std::wstring_view _layoutStripHash(std::wstring_view str); + TmuxLayout _layoutParseNextToken(std::wstring_view& remaining); + + void _deliverOutputToPane(int64_t paneId, const std::wstring_view text); + winrt::com_ptr _getTab(int64_t windowId) const; + void _newTab(int64_t windowId, winrt::hstring name, std::shared_ptr pane); + std::pair> _newPane(int64_t windowId, int64_t paneId); + void _openNewTerminalViaDropdown(); + + TerminalPage& _page; // Non-owning, because TerminalPage owns us + winrt::Windows::System::DispatcherQueue _dispatcherQueue{ nullptr }; + winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _newTabMenu; + + winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr }; + winrt::com_ptr _controlTab{ nullptr }; + winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr }; + State _state = State::Init; + bool _inUse = false; + + std::wstring _lineBuffer; + std::wstring _responseBuffer; + bool _insideOutputBlock = false; + + winrt::event_token _detachKeyDownRevoker; + winrt::event_token _windowSizeChangedRevoker; + winrt::event_token _newTabClickRevoker; + + std::deque _commandQueue; + std::unordered_map _attachedPanes; + std::unordered_map> _attachedWindows; + + int64_t _sessionId = -1; + int64_t _activePaneId = -1; + int64_t _activeWindowId = -1; + + til::CoordType _terminalWidth = 0; + til::CoordType _terminalHeight = 0; + winrt::Windows::UI::Xaml::Thickness _thickness{ 0, 0, 0, 0 }; + float _fontWidth = 0; + float _fontHeight = 0; + + std::pair, winrt::Microsoft::Terminal::Settings::Model::SplitDirection> _splittingPane{ + nullptr, + winrt::Microsoft::Terminal::Settings::Model::SplitDirection::Right, + }; + }; +} diff --git a/src/cascadia/TerminalConnection/AzureConnection.cpp b/src/cascadia/TerminalConnection/AzureConnection.cpp index dbc6ec9905d..72208af1009 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.cpp +++ b/src/cascadia/TerminalConnection/AzureConnection.cpp @@ -94,9 +94,15 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - helper that will write an unterminated string (generally, from a resource) to the output stream. // Arguments: // - str: the string to write. + void AzureConnection::_WriteStringWithNewline(std::wstring str) + { + str.append(L"\r\n"); + TerminalOutput.raise(winrt_wstring_to_array_view(str)); + } + void AzureConnection::_WriteStringWithNewline(const std::wstring_view str) { - TerminalOutput.raise(str + L"\r\n"); + TerminalOutput.raise(winrt_wstring_to_array_view(str + L"\r\n")); } // Method description: @@ -112,7 +118,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation catch (const std::exception& runtimeException) { // This also catches the AzureException, which has a .what() - TerminalOutput.raise(_colorize(91, til::u8u16(std::string{ runtimeException.what() }))); + TerminalOutput.raise(winrt_wstring_to_array_view(_colorize(91, til::u8u16(std::string{ runtimeException.what() })))); } catch (...) { @@ -162,13 +168,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation _currentInputMode = mode; - TerminalOutput.raise(L"> \x1b[92m"); // Make prompted user input green + TerminalOutput.raise(winrt_wstring_to_array_view(L"> \x1b[92m")); // Make prompted user input green _inputEvent.wait(inputLock, [this, mode]() { return _currentInputMode != mode || _isStateAtOrBeyond(ConnectionState::Closing); }); - TerminalOutput.raise(L"\x1b[m"); + TerminalOutput.raise(winrt_wstring_to_array_view(L"\x1b[m")); if (_isStateAtOrBeyond(ConnectionState::Closing)) { @@ -211,19 +217,19 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation if (_userInput.size() > 0) { _userInput.pop_back(); - TerminalOutput.raise(L"\x08 \x08"); // overstrike the character with a space + TerminalOutput.raise(winrt_wstring_to_array_view(L"\x08 \x08")); // overstrike the character with a space } } else { - TerminalOutput.raise(data); // echo back + TerminalOutput.raise(winrt_wstring_to_array_view(data)); // echo back switch (_currentInputMode) { case InputMode::Line: if (data.size() > 0 && gsl::at(data, 0) == UNICODE_CARRIAGERETURN) { - TerminalOutput.raise(L"\r\n"); // we probably got a \r, so we need to advance to the next line. + TerminalOutput.raise(winrt_wstring_to_array_view(L"\r\n")); // we probably got a \r, so we need to advance to the next line. _currentInputMode = InputMode::None; // toggling the mode indicates completion _inputEvent.notify_one(); break; @@ -429,7 +435,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation } // Pass the output to our registered event handlers - TerminalOutput.raise(_u16Str); + TerminalOutput.raise(winrt_wstring_to_array_view(_u16Str)); break; } case WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE: @@ -772,7 +778,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const auto shellType = _ParsePreferredShellType(settingsResponse); _WriteStringWithNewline(RS_(L"AzureRequestingTerminal")); const auto socketUri = _GetTerminal(shellType); - TerminalOutput.raise(L"\r\n"); + TerminalOutput.raise(winrt_wstring_to_array_view(L"\r\n")); //// Step 8: connecting to said terminal { diff --git a/src/cascadia/TerminalConnection/AzureConnection.h b/src/cascadia/TerminalConnection/AzureConnection.h index 3b8a092692f..d1553dbfcc9 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.h +++ b/src/cascadia/TerminalConnection/AzureConnection.h @@ -67,6 +67,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation std::optional<::Microsoft::Terminal::Azure::Tenant> _currentTenant; void _writeInput(const std::wstring_view str); + void _WriteStringWithNewline(std::wstring str); void _WriteStringWithNewline(const std::wstring_view str); void _WriteCaughtExceptionRecord(); winrt::Windows::Data::Json::JsonObject _SendRequestReturningJson(std::wstring_view uri, const winrt::Windows::Web::Http::IHttpContent& content = nullptr, winrt::Windows::Web::Http::HttpMethod method = nullptr, const winrt::Windows::Foundation::Uri referer = nullptr); diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index 5db43c04b26..e83782ff65f 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -477,31 +477,28 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const auto hr = wil::ResultFromCaughtException(); // GH#11556 - make sure to format the error code to this string as an UNSIGNED int - const auto failureText = RS_fmt(L"ProcessFailedToLaunch", _formatStatus(hr), _commandline); - TerminalOutput.raise(failureText); + auto failureText = RS_fmt(L"ProcessFailedToLaunch", _formatStatus(hr), _commandline); // If the path was invalid, let's present an informative message to the user if (hr == HRESULT_FROM_WIN32(ERROR_DIRECTORY)) { - const auto badPathText = RS_fmt(L"BadPathText", _startingDirectory); - TerminalOutput.raise(L"\r\n"); - TerminalOutput.raise(badPathText); + failureText.append(L"\r\n"); + failureText.append(RS_fmt(L"BadPathText", _startingDirectory)); } // If the requested action requires elevation, display appropriate message else if (hr == HRESULT_FROM_WIN32(ERROR_ELEVATION_REQUIRED)) { - const auto elevationText = RS_(L"ElevationRequired"); - TerminalOutput.raise(L"\r\n"); - TerminalOutput.raise(elevationText); + failureText.append(L"\r\n"); + failureText.append(RS_(L"ElevationRequired")); } // If the requested executable was not found, display appropriate message else if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { - const auto fileNotFoundText = RS_(L"FileNotFound"); - TerminalOutput.raise(L"\r\n"); - TerminalOutput.raise(fileNotFoundText); + failureText.append(L"\r\n"); + failureText.append(RS_(L"FileNotFound")); } + TerminalOutput.raise(winrt_wstring_to_array_view(failureText)); _transitionToState(ConnectionState::Failed); // Tear down any state we may have accumulated. @@ -520,7 +517,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const auto msg1 = RS_fmt(L"ProcessExited", _formatStatus(status)); const auto msg2 = RS_(L"CtrlDToClose"); const auto msg = fmt::format(FMT_COMPILE(L"\r\n{}\r\n{}\r\n"), msg1, msg2); - TerminalOutput.raise(msg); + TerminalOutput.raise(winrt_wstring_to_array_view(msg)); } CATCH_LOG(); } @@ -792,7 +789,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation try { - TerminalOutput.raise(wstr); + TerminalOutput.raise(winrt_wstring_to_array_view(wstr)); } CATCH_LOG(); } diff --git a/src/cascadia/TerminalConnection/EchoConnection.cpp b/src/cascadia/TerminalConnection/EchoConnection.cpp index ca28af28e54..f50433b521d 100644 --- a/src/cascadia/TerminalConnection/EchoConnection.cpp +++ b/src/cascadia/TerminalConnection/EchoConnection.cpp @@ -34,7 +34,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation prettyPrint << wch; } } - TerminalOutput.raise(prettyPrint.str()); + TerminalOutput.raise(winrt_wstring_to_array_view(prettyPrint.str())); } void EchoConnection::Resize(uint32_t /*rows*/, uint32_t /*columns*/) noexcept diff --git a/src/cascadia/TerminalConnection/ITerminalConnection.idl b/src/cascadia/TerminalConnection/ITerminalConnection.idl index 275b57b2ac9..d516f2038a0 100644 --- a/src/cascadia/TerminalConnection/ITerminalConnection.idl +++ b/src/cascadia/TerminalConnection/ITerminalConnection.idl @@ -13,7 +13,7 @@ namespace Microsoft.Terminal.TerminalConnection Failed }; - delegate void TerminalOutputHandler(String output); + delegate void TerminalOutputHandler(Char[] output); interface ITerminalConnection { diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj index 2c7d276c506..20cb2d133f8 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -33,6 +33,9 @@ EchoConnection.idl + + TmuxConnection.idl + @@ -52,6 +55,9 @@ ConptyConnection.idl + + TmuxConnection.idl + @@ -60,6 +66,7 @@ + @@ -99,4 +106,4 @@ - \ No newline at end of file + diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters index 11a0227b315..a5add0bbac0 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters @@ -19,6 +19,7 @@ + @@ -27,6 +28,7 @@ + @@ -34,6 +36,7 @@ + @@ -42,4 +45,4 @@ - \ No newline at end of file + diff --git a/src/cascadia/TerminalConnection/TmuxConnection.cpp b/src/cascadia/TerminalConnection/TmuxConnection.cpp new file mode 100644 index 00000000000..6000543329a --- /dev/null +++ b/src/cascadia/TerminalConnection/TmuxConnection.cpp @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TmuxConnection.h" +#include + +#include "TmuxConnection.g.cpp" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + TmuxConnection::TmuxConnection() noexcept = default; + + void TmuxConnection::Initialize(const Windows::Foundation::Collections::ValueSet&) const noexcept + { + } + + void TmuxConnection::Start() noexcept + { + } + + void TmuxConnection::WriteInput(const winrt::array_view buffer) + { + TerminalInput.raise(buffer); + } + + void TmuxConnection::Resize(uint32_t /*rows*/, uint32_t /*columns*/) noexcept + { + } + + void TmuxConnection::Close() noexcept + { + StateChanged.raise(*this, nullptr); + } + + winrt::guid TmuxConnection::SessionId() const noexcept + { + return {}; + } + + ConnectionState TmuxConnection::State() const noexcept + { + return ConnectionState::Connected; + } + + void TmuxConnection::WriteOutput(const winrt::array_view wstr) + { + if (!wstr.empty()) + { + TerminalOutput.raise(wstr); + } + } +} diff --git a/src/cascadia/TerminalConnection/TmuxConnection.h b/src/cascadia/TerminalConnection/TmuxConnection.h new file mode 100644 index 00000000000..4b4764dbea4 --- /dev/null +++ b/src/cascadia/TerminalConnection/TmuxConnection.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "TmuxConnection.g.h" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + struct TmuxConnection : TmuxConnectionT + { + TmuxConnection() noexcept; + + // ---- ITerminalConnection methods ---- + + void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) const noexcept; + + void Start() noexcept; + void WriteInput(const winrt::array_view buffer); + void Resize(uint32_t rows, uint32_t columns) noexcept; + void Close() noexcept; + + til::event TerminalOutput; + til::typed_event StateChanged; + + winrt::guid SessionId() const noexcept; + ConnectionState State() const noexcept; + + // ---- TmuxConnection methods ---- + + void WriteOutput(const winrt::array_view wstr); + + til::event TerminalInput; + }; +} + +namespace winrt::Microsoft::Terminal::TerminalConnection::factory_implementation +{ + BASIC_FACTORY(TmuxConnection); +} diff --git a/src/cascadia/TerminalConnection/TmuxConnection.idl b/src/cascadia/TerminalConnection/TmuxConnection.idl new file mode 100644 index 00000000000..ccb30aea734 --- /dev/null +++ b/src/cascadia/TerminalConnection/TmuxConnection.idl @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ITerminalConnection.idl"; + +namespace Microsoft.Terminal.TerminalConnection +{ + [default_interface] + runtimeclass TmuxConnection : ITerminalConnection + { + TmuxConnection(); + + void WriteOutput(Char[] data); + event TerminalOutputHandler TerminalInput; + }; +} diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index be7350f11a0..7614470e248 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -10,13 +10,12 @@ #include #include -#include -#include #include "EventArgs.h" #include "../../renderer/atlas/AtlasEngine.h" #include "../../renderer/base/renderer.hpp" #include "../../renderer/uia/UiaRenderer.hpp" +#include "../../terminal/adapter/adaptDispatch.hpp" #include "../../types/inc/CodepointWidthDetector.hpp" #include "../../types/inc/utils.hpp" @@ -141,6 +140,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnWindowSizeChanged = [this](auto&& PH1, auto&& PH2) { _terminalWindowSizeChanged(std::forward(PH1), std::forward(PH2)); }; _terminal->SetWindowSizeChangedCallback(pfnWindowSizeChanged); + _terminal->SetEnterTmuxControlCallback([this]() -> std::function { + const auto args = winrt::make_self(); + EnterTmuxControl.raise(*this, *args); + if (auto inputCallback = args->InputCallback()) + { + return [inputCallback = std::move(inputCallback)](wchar_t ch) -> bool { + const auto c16 = static_cast(ch); + inputCallback({ &c16, 1 }); + return true; + }; + } + return nullptr; + }); + // MSFT 33353327: Initialize the renderer in the ctor instead of Initialize(). // We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go. // If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach @@ -1461,6 +1474,45 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->TrySnapOnInput(); } + void ControlCore::InjectTextAtCursor(const winrt::hstring& text) + { + if (text.empty()) + { + return; + } + + const auto lock = _terminal->LockForWriting(); + std::wstring_view remaining{ text }; + + // Process one line at a time + for (;;) + { + // Get the (CR)LF position + const auto lf = std::min(remaining.size(), remaining.find(L'\n')); + + // Strip off the CR + auto lineEnd = lf; + if (lineEnd != 0 && remaining[lineEnd - 1] == L'\r') + { + lineEnd -= 1; + } + + // Split into line and whatever comes after + const auto line = remaining.substr(0, lineEnd); + remaining = remaining.substr(std::min(remaining.size(), lf + 1)); + + // This will not just print the line but also handle delay wrap, etc. + _terminal->GetAdaptDispatch().PrintString(line); + + if (remaining.empty()) + { + break; + } + + _terminal->GetAdaptDispatch().LineFeed(DispatchTypes::LineFeedType::DependsOnMode); + } + } + FontInfo ControlCore::GetFont() const { return _actualFont; @@ -1570,6 +1622,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _terminal->GetViewport().Height(); } + // Function Description: + // - Gets the width of the terminal in lines of text. This is just the + // width of the viewport. + // Return Value: + // - The width of the terminal in lines of text + int ControlCore::ViewWidth() const + { + const auto lock = _terminal->LockForReading(); + return _terminal->GetViewport().Width(); + } + // Function Description: // - Gets the height of the terminal in lines of text. This includes the // history AND the viewport. @@ -2238,13 +2301,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto noticeArgs = winrt::make(NoticeLevel::Info, RS_(L"TermControlReadOnly")); RaiseNotice.raise(*this, std::move(noticeArgs)); } - void ControlCore::_connectionOutputHandler(const hstring& hstr) + void ControlCore::_connectionOutputHandler(const winrt::array_view str) { try { { const auto lock = _terminal->LockForWriting(); - _terminal->Write(hstr); + _terminal->Write(winrt_array_to_wstring_view(str)); } if (!_pendingResponses.empty()) diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 112848dc6ec..9c90e009d91 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -23,6 +23,7 @@ #include "../../buffer/out/search.h" #include "../../cascadia/TerminalCore/Terminal.hpp" #include "../../renderer/inc/FontInfoDesired.hpp" +#include "../../terminal/adapter/ITermDispatch.hpp" namespace Microsoft::Console::Render::Atlas { @@ -124,6 +125,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SendInput(std::wstring_view wstr); void PasteText(const winrt::hstring& hstr); + void InjectTextAtCursor(const winrt::hstring& text); bool CopySelectionToClipboard(bool singleLine, bool withControlSequences, const CopyFormat formats); void SelectAll(); void ClearSelection(); @@ -172,6 +174,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation int ScrollOffset(); int ViewHeight() const; + int ViewWidth() const; int BufferHeight() const; bool HasSelection() const; @@ -295,10 +298,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event SearchMissingCommand; til::typed_event<> RefreshQuickFixUI; til::typed_event WindowSizeChanged; - + til::typed_event EnterTmuxControl; til::typed_event<> CloseTerminalRequested; til::typed_event<> RestartTerminalRequested; - til::typed_event<> Attached; // clang-format on @@ -349,7 +351,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _raiseReadOnlyWarning(); void _updateAntiAliasingMode(); - void _connectionOutputHandler(const hstring& hstr); + void _connectionOutputHandler(winrt::array_view str); void _connectionStateChangedHandler(const TerminalConnection::ITerminalConnection&, const Windows::Foundation::IInspectable&); void _updateHoveredCell(const std::optional terminalPosition); void _setOpacity(const float opacity, const bool focused = true); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index d9f92e011b8..d0181ec50a5 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -67,6 +67,10 @@ namespace Microsoft.Terminal.Control Boolean SearchRegexInvalid; }; + delegate Boolean TmuxDCSHandler(Char ch); + delegate void PrintHandler(String str); + delegate TmuxDCSHandler TmuxDCSHandlerProducer(PrintHandler print); + [default_interface] runtimeclass SelectionColor { SelectionColor(); @@ -128,6 +132,7 @@ namespace Microsoft.Terminal.Control Microsoft.Terminal.Core.ControlKeyStates modifiers); void SendInput(String text); void PasteText(String text); + void InjectTextAtCursor(String text); void SelectAll(); void ClearSelection(); Boolean ToggleBlockSelection(); @@ -198,6 +203,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler SearchMissingCommand; event Windows.Foundation.TypedEventHandler RefreshQuickFixUI; event Windows.Foundation.TypedEventHandler WindowSizeChanged; + event Windows.Foundation.TypedEventHandler EnterTmuxControl; // These events are always called from the UI thread (bugs aside) event Windows.Foundation.TypedEventHandler FontSizeChanged; diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index e5a9fe58765..b16d13e829c 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -21,3 +21,4 @@ #include "StringSentEventArgs.g.cpp" #include "SearchMissingCommandEventArgs.g.cpp" #include "WindowSizeChangedEventArgs.g.cpp" +#include "EnterTmuxControlEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 53f1245ef06..5d55e0d9d6d 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -21,6 +21,7 @@ #include "StringSentEventArgs.g.h" #include "SearchMissingCommandEventArgs.g.h" #include "WindowSizeChangedEventArgs.g.h" +#include "EnterTmuxControlEventArgs.g.h" namespace winrt::Microsoft::Terminal::Control::implementation { @@ -265,6 +266,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(int32_t, Width); WINRT_PROPERTY(int32_t, Height); }; + + struct EnterTmuxControlEventArgs : public EnterTmuxControlEventArgsT + { + til::property InputCallback; + }; } namespace winrt::Microsoft::Terminal::Control::factory_implementation diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index d6086fd922d..358e1b079e4 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -159,4 +159,11 @@ namespace Microsoft.Terminal.Control Int32 Width; Int32 Height; } + + delegate void TmuxControlInputCallback(Char[] input); + + runtimeclass EnterTmuxControlEventArgs + { + void InputCallback(TmuxControlInputCallback callback); + } } diff --git a/src/cascadia/TerminalControl/ICoreState.idl b/src/cascadia/TerminalControl/ICoreState.idl index 38f0cea30ae..e47d8994dd3 100644 --- a/src/cascadia/TerminalControl/ICoreState.idl +++ b/src/cascadia/TerminalControl/ICoreState.idl @@ -40,6 +40,7 @@ namespace Microsoft.Terminal.Control Int32 ScrollOffset { get; }; Int32 ViewHeight { get; }; + Int32 ViewWidth { get; }; Int32 BufferHeight { get; }; Boolean HasSelection { get; }; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 12eab22639a..4acb5ec891c 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -330,6 +330,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _revokers.SearchMissingCommand = _core.SearchMissingCommand(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleSearchMissingCommand }); _revokers.WindowSizeChanged = _core.WindowSizeChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleWindowSizeChanged }); _revokers.WriteToClipboard = _core.WriteToClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleWriteToClipboard }); + _revokers.EnterTmuxControl = _core.EnterTmuxControl(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleEnterTmuxControl }); _revokers.PasteFromClipboard = _interactivity.PasteFromClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubblePasteFromClipboard }); @@ -1506,6 +1507,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.SendInput(text); } + void TermControl::InjectTextAtCursor(const winrt::hstring& text) + { + _core.InjectTextAtCursor(text); + } + // Method Description: // - Manually handles key events for certain keys that can't be passed to us // normally. Namely, the keys we're concerned with are F7 down and Alt up. @@ -2667,6 +2673,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _core.ViewHeight(); } + int TermControl::ViewWidth() const + { + return _core.ViewWidth(); + } + int TermControl::BufferHeight() const { return _core.BufferHeight(); @@ -2866,7 +2877,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation else { // Do we ever get here (= uninitialized terminal)? If so: How? - assert(false); + // Yes, we can get here, when do Pane._Split, it need to call _SetupEntranceAnimation^M + // which need the control's size, while this size can only be available when the control^M + // is initialized.^M return { 10, 10 }; } } @@ -4002,6 +4015,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } + void TermControl::_bubbleEnterTmuxControl(const IInspectable&, Control::EnterTmuxControlEventArgs args) + { + EnterTmuxControl.raise(*this, std::move(args)); + } + til::CoordType TermControl::_calculateSearchScrollOffset() const { auto result = 0; diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 3db3cec6730..65865c3bbda 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -97,6 +97,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation int ScrollOffset() const; int ViewHeight() const; + int ViewWidth() const; int BufferHeight() const; bool HasSelection() const; @@ -182,6 +183,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool RawWriteKeyEvent(const WORD vkey, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown); bool RawWriteChar(const wchar_t character, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers); void RawWriteString(const winrt::hstring& text); + void InjectTextAtCursor(const winrt::hstring& text); void ShowContextMenu(); bool OpenQuickFixMenu(); @@ -217,6 +219,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event StringSent; til::typed_event SearchMissingCommand; til::typed_event WindowSizeChanged; + til::typed_event EnterTmuxControl; // UNDER NO CIRCUMSTANCES SHOULD YOU ADD A (PROJECTED_)FORWARDED_TYPED_EVENT HERE // Those attach the handler to the core directly, and will explode if @@ -437,6 +440,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _bubbleSearchMissingCommand(const IInspectable& sender, const Control::SearchMissingCommandEventArgs& args); winrt::fire_and_forget _bubbleWindowSizeChanged(const IInspectable& sender, Control::WindowSizeChangedEventArgs args); + void _bubbleEnterTmuxControl(const IInspectable& sender, Control::EnterTmuxControlEventArgs args); til::CoordType _calculateSearchScrollOffset() const; void _PasteCommandHandler(const IInspectable& sender, const IInspectable& args); @@ -471,6 +475,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::ControlCore::SearchMissingCommand_revoker SearchMissingCommand; Control::ControlCore::RefreshQuickFixUI_revoker RefreshQuickFixUI; Control::ControlCore::WindowSizeChanged_revoker WindowSizeChanged; + Control::ControlCore::EnterTmuxControl_revoker EnterTmuxControl; // These are set up in _InitializeTerminal Control::ControlCore::RendererWarning_revoker RendererWarning; diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index fb994786321..fe596e9b72f 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -74,6 +74,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusFollowMouseRequested; event Windows.Foundation.TypedEventHandler WindowSizeChanged; + event Windows.Foundation.TypedEventHandler EnterTmuxControl; event Windows.Foundation.TypedEventHandler CompletionsChanged; @@ -130,6 +131,7 @@ namespace Microsoft.Terminal.Control Boolean RawWriteKeyEvent(UInt16 vkey, UInt16 scanCode, Microsoft.Terminal.Core.ControlKeyStates modifiers, Boolean keyDown); Boolean RawWriteChar(Char character, UInt16 scanCode, Microsoft.Terminal.Core.ControlKeyStates modifiers); void RawWriteString(String text); + void InjectTextAtCursor(String text); void BellLightOn(); diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index aec06719d75..302d51f7e1f 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -51,6 +51,7 @@ void Terminal::Create(til::size viewportSize, til::CoordType scrollbackLines, Re _mainBuffer = std::make_unique(bufferSize, attr, cursorSize, true, &renderer); auto dispatch = std::make_unique(*this, &renderer, _renderSettings, _terminalInput); + _adaptDispatch = dispatch.get(); auto engine = std::make_unique(std::move(dispatch)); _stateMachine = std::make_unique(std::move(engine)); } @@ -1039,6 +1040,12 @@ bool Terminal::IsFocused() const noexcept return _focused; } +AdaptDispatch& Microsoft::Terminal::Core::Terminal::GetAdaptDispatch() noexcept +{ + _assertLocked(); + return *_adaptDispatch; +} + RenderSettings& Terminal::GetRenderSettings() noexcept { _assertLocked(); @@ -1270,6 +1277,11 @@ void Microsoft::Terminal::Core::Terminal::SetSearchMissingCommandCallback(std::f _pfnSearchMissingCommand.swap(pfn); } +void Terminal::SetEnterTmuxControlCallback(std::function()> pfn) noexcept +{ + _pfnEnterTmuxControl = std::move(pfn); +} + void Microsoft::Terminal::Core::Terminal::SetClearQuickFixCallback(std::function pfn) noexcept { _pfnClearQuickFix.swap(pfn); diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 33b98151c4f..b64b2f1057e 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -116,6 +116,7 @@ class Microsoft::Terminal::Core::Terminal final : int ViewEndIndex() const noexcept; bool IsFocused() const noexcept; + ::Microsoft::Console::VirtualTerminal::AdaptDispatch& GetAdaptDispatch() noexcept; RenderSettings& GetRenderSettings() noexcept; const RenderSettings& GetRenderSettings() const noexcept; @@ -153,14 +154,12 @@ class Microsoft::Terminal::Core::Terminal final : void ShowWindow(bool showOrHide) override; void UseAlternateScreenBuffer(const TextAttribute& attrs) override; void UseMainScreenBuffer() override; - bool IsVtInputEnabled() const noexcept override; void NotifyBufferRotation(const int delta) override; void NotifyShellIntegrationMark() override; - void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; - void SearchMissingCommand(const std::wstring_view command) override; + std::function EnterTmuxControl() override; #pragma endregion @@ -230,6 +229,7 @@ class Microsoft::Terminal::Core::Terminal final : void SetPlayMidiNoteCallback(std::function pfn) noexcept; void CompletionsChangedCallback(std::function pfn) noexcept; void SetSearchMissingCommandCallback(std::function pfn) noexcept; + void SetEnterTmuxControlCallback(std::function()> pfn) noexcept; void SetClearQuickFixCallback(std::function pfn) noexcept; void SetWindowSizeChangedCallback(std::function pfn) noexcept; void SetSearchHighlights(const std::vector& highlights) noexcept; @@ -338,10 +338,12 @@ class Microsoft::Terminal::Core::Terminal final : std::function _pfnPlayMidiNote; std::function _pfnCompletionsChanged; std::function _pfnSearchMissingCommand; + std::function()> _pfnEnterTmuxControl; std::function _pfnClearQuickFix; std::function _pfnWindowSizeChanged; RenderSettings _renderSettings; + ::Microsoft::Console::VirtualTerminal::AdaptDispatch* _adaptDispatch; std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine; ::Microsoft::Console::VirtualTerminal::TerminalInput _terminalInput; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index e2c7a818144..93289960de2 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -364,6 +364,11 @@ void Terminal::SearchMissingCommand(const std::wstring_view command) } } +std::function Terminal::EnterTmuxControl() +{ + return _pfnEnterTmuxControl ? _pfnEnterTmuxControl() : nullptr; +} + void Terminal::NotifyBufferRotation(const int delta) { // Update our selection, so it doesn't move as the buffer is cycled diff --git a/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp b/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp index 9bc73d5fd7b..7541a18de35 100644 --- a/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp @@ -352,6 +352,7 @@ namespace winrt::Microsoft::Terminal::Settings _AllowVtChecksumReport = profile.AllowVtChecksumReport(); _AllowVtClipboardWrite = profile.AllowVtClipboardWrite(); _PathTranslationStyle = profile.PathTranslationStyle(); + _AllowTmuxControl = profile.AllowTmuxControl(); } // Method Description: diff --git a/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.h b/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.h index ff714dfc5c9..3c280277520 100644 --- a/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.h @@ -92,6 +92,7 @@ namespace winrt::Microsoft::Terminal::Settings SIMPLE_OVERRIDABLE_SETTING(bool, Elevate, false); SIMPLE_OVERRIDABLE_SETTING(IEnvironmentVariableMapView, EnvironmentVariables, nullptr); SIMPLE_OVERRIDABLE_SETTING(bool, ReloadEnvironmentVariables, true); + SIMPLE_OVERRIDABLE_SETTING(bool, AllowTmuxControl, false); public: // TerminalApp overrides these when duplicating a session diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp index cc1508cf350..aadbc60eaa2 100644 --- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp +++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp @@ -28,10 +28,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { PreviewConnection::PreviewConnection() noexcept = default; - void PreviewConnection::Start() noexcept + void PreviewConnection::Start() { - // Send the preview text - TerminalOutput.raise(fmt::format(PreviewText, _displayPowerlineGlyphs ? PromptTextPowerline : PromptTextPlain)); + const auto prompt = _displayPowerlineGlyphs ? PromptTextPowerline : PromptTextPlain; + const auto text = fmt::format(FMT_COMPILE(PreviewText), prompt); + TerminalOutput.raise(winrt_wstring_to_array_view(text)); } void PreviewConnection::Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) noexcept @@ -50,7 +51,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { } - void PreviewConnection::DisplayPowerlineGlyphs(bool d) noexcept + void PreviewConnection::DisplayPowerlineGlyphs(bool d) { if (_displayPowerlineGlyphs != d) { diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h index c988ff5156c..427b66035af 100644 --- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h +++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h @@ -22,12 +22,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation PreviewConnection() noexcept; void Initialize(const Windows::Foundation::Collections::ValueSet& settings) noexcept; - void Start() noexcept; + void Start(); void WriteInput(const winrt::array_view buffer); void Resize(uint32_t rows, uint32_t columns) noexcept; void Close() noexcept; - void DisplayPowerlineGlyphs(bool d) noexcept; + void DisplayPowerlineGlyphs(bool d); winrt::guid SessionId() const noexcept { return {}; } winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept { return winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Connected; } diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp index 6172def8100..f6bdfeae265 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp @@ -188,6 +188,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _parsedPadding = StringToXamlThickness(_profile.Padding()); _defaultAppearanceViewModel.IsDefault(true); + + if constexpr (Feature_TmuxControl::IsEnabled()) + { + TmuxControlEnabled(true); + } } void ProfileViewModel::_UpdateBuiltInIcons() diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h index 9f85cfee816..d22a1c6d93b 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -171,9 +171,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation OBSERVABLE_PROJECTED_SETTING(_profile, AnswerbackMessage); OBSERVABLE_PROJECTED_SETTING(_profile, RainbowSuggestions); OBSERVABLE_PROJECTED_SETTING(_profile, PathTranslationStyle); + OBSERVABLE_PROJECTED_SETTING(_profile, AllowTmuxControl); WINRT_PROPERTY(bool, IsBaseLayer, false); WINRT_PROPERTY(bool, FocusDeleteButton, false); + WINRT_PROPERTY(bool, TmuxControlEnabled, false); WINRT_PROPERTY(Windows::Foundation::Collections::IVector, IconTypes); GETSET_BINDABLE_ENUM_SETTING(AntiAliasingMode, Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode); GETSET_BINDABLE_ENUM_SETTING(CloseOnExitMode, Microsoft::Terminal::Settings::Model::CloseOnExitMode, CloseOnExit); diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl index 073afdf910e..b2beb870be6 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl @@ -114,6 +114,7 @@ namespace Microsoft.Terminal.Settings.Editor Boolean UsingBuiltInIcon { get; }; Boolean UsingEmojiIcon { get; }; Boolean UsingImageIcon { get; }; + Boolean TmuxControlEnabled; String IconPath; EnumEntry CurrentBuiltInIcon; @@ -162,5 +163,6 @@ namespace Microsoft.Terminal.Settings.Editor OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, RainbowSuggestions); OBSERVABLE_PROJECTED_PROFILE_SETTING(Microsoft.Terminal.Control.PathTranslationStyle, PathTranslationStyle); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AllowVtClipboardWrite); + OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AllowTmuxControl); } } diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml index dfe5954d4fe..f9fc0f9a9e3 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml @@ -77,6 +77,16 @@ + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index fbf9003f7a5..8b13e728f78 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -568,6 +568,10 @@ Always on top Header for a control to toggle if the app will always be presented on top of other windows, or is treated normally (when disabled). + + Allow Tmux Control + Header for a control to toggle tmux control. + Use the legacy input encoding Header for a control to toggle legacy input encoding for the terminal. diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 9040f89cbf7..3572df86bfc 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -106,6 +106,7 @@ Author(s): X(bool, AllowVtChecksumReport, "compatibility.allowDECRQCRA", false) \ X(bool, AllowVtClipboardWrite, "compatibility.allowOSC52", true) \ X(bool, AllowKeypadMode, "compatibility.allowDECNKM", false) \ + X(bool, AllowTmuxControl, "AllowTmuxControl", false) \ X(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, "pathTranslationStyle", Microsoft::Terminal::Control::PathTranslationStyle::None) // Intentionally omitted Profile settings: diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 9d7d93ef995..0c10a5bb21a 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -91,6 +91,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(Boolean, AllowVtChecksumReport); INHERITABLE_PROFILE_SETTING(Boolean, AllowKeypadMode); INHERITABLE_PROFILE_SETTING(Boolean, AllowVtClipboardWrite); + INHERITABLE_PROFILE_SETTING(Boolean, AllowTmuxControl); INHERITABLE_PROFILE_SETTING(Microsoft.Terminal.Control.PathTranslationStyle, PathTranslationStyle); } diff --git a/src/common.build.pre.props b/src/common.build.pre.props index b3f57bf2302..e8084905fcc 100644 --- a/src/common.build.pre.props +++ b/src/common.build.pre.props @@ -95,7 +95,7 @@ - v143 + v145 Unicode false x64 diff --git a/src/features.xml b/src/features.xml index 4788697703c..1f39214ef25 100644 --- a/src/features.xml +++ b/src/features.xml @@ -14,7 +14,7 @@ Feature_EditableUnfocusedAppearance The unfocused appearance section in profiles in the SUI that allows users to create and edit unfocused appearances. AlwaysEnabled - + @@ -184,7 +184,19 @@ Feature_DebugModeUI Enables UI access to the debug mode setting AlwaysEnabled - + + + + + Feature_TmuxControl + Enables Tmux Control + 3656 + AlwaysDisabled + + Dev + Canary + Preview + diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 8960a994671..1d8174c5c20 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -427,7 +427,14 @@ void ConhostInternalGetSet::InvokeCompletions(std::wstring_view /*menuJson*/, un { // Not implemented for conhost. } + void ConhostInternalGetSet::SearchMissingCommand(std::wstring_view /*missingCommand*/) { // Not implemented for conhost. } + +std::function ConhostInternalGetSet::EnterTmuxControl() +{ + // Not implemented for conhost. + return {}; +} diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index c80df20ffea..0ee21637dbb 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -71,6 +71,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; void SearchMissingCommand(std::wstring_view missingCommand) override; + std::function EnterTmuxControl() override; private: Microsoft::Console::IIoProvider& _io; diff --git a/src/inc/til/string.h b/src/inc/til/string.h index 824df3212bd..2e57d8c648c 100644 --- a/src/inc/til/string.h +++ b/src/inc/til/string.h @@ -168,7 +168,41 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" template bool equals(const std::basic_string_view& lhs, const std::basic_string_view& rhs) noexcept { - return lhs.size() == rhs.size() && __builtin_memcmp(lhs.data(), rhs.data(), lhs.size() * sizeof(T)) == 0; + // MSVC can only optimize 1 pattern into a bunch of simple comparison instructions: + // size1 == size2 && memcmp(data1, data2, size1/2) == 0 + // If you introduce a `* sizeof(T)` into the size parameter to memcmp, + // it'll refuse to inline the memcmp call, resulting in much worse codegen. + // As a trade-off we multiply both sizes by sizeof(T) in advance. + // The extra addition instruction is comparatively very cheap. + const auto ls = lhs.size() * sizeof(T); + const auto rs = rhs.size() * sizeof(T); + return ls == rs && __builtin_memcmp(lhs.data(), rhs.data(), ls) == 0; + } + + inline bool equals(const std::string_view& lhs, const std::string_view& rhs) noexcept + { + return equals<>(lhs, rhs); + } + + inline bool equals(const std::wstring_view& lhs, const std::wstring_view& rhs) noexcept + { + return equals<>(lhs, rhs); + } + + // An extra overload that undoes the quirk in the main equals() implementation. + // It's not really needed, except for being neat. This function in particular + // is often the best, easy way to do something like: + // match str { + // "foo" => ..., + // "bar" => ..., + // } + // with: + // if (til::equals(str, "foo")) { ... } + // else if (til::equals(str, "bar")) { ... } + template + constexpr bool equals(const std::basic_string_view& lhs, const T (&rhs)[N]) noexcept + { + return lhs.size() == (N - 1) && __builtin_memcmp(lhs.data(), rhs, (N - 1) * sizeof(T)) == 0; } // Just like _memicmp, but without annoying locales. diff --git a/src/inc/til/winrt.h b/src/inc/til/winrt.h index 101d558f7f9..2628be11d3b 100644 --- a/src/inc/til/winrt.h +++ b/src/inc/til/winrt.h @@ -60,32 +60,192 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" template struct event { - event() = default; + explicit operator bool() const noexcept { return static_cast(_handlers); } winrt::event_token operator()(const ArgsT& handler) { return _handlers.add(handler); } - void operator()(const winrt::event_token& token) { _handlers.remove(token); } - operator bool() const noexcept { return bool(_handlers); } - template + void operator()(winrt::event_token token) { _handlers.remove(token); } + void raise(auto&&... args) { _handlers(std::forward(args)...); } + + private: winrt::event _handlers; }; template - struct typed_event + using typed_event = til::event>; + + // Unlike winrt::event, this event will only call handlers once at most. + // It's otherwise a copy of winrt::event's implementation. + template + struct fused_event { - typed_event() = default; - winrt::event_token operator()(const winrt::Windows::Foundation::TypedEventHandler& handler) { return _handlers.add(handler); } - void operator()(const winrt::event_token& token) { _handlers.remove(token); } - operator bool() const noexcept { return bool(_handlers); } + using delegate_type = ArgsT; + using delegate_array = winrt::com_ptr>; + + fused_event() = default; + fused_event(const fused_event&) = delete; + fused_event& operator=(const fused_event&) = delete; + + fused_event(fused_event&& other) + { + const winrt::slim_lock_guard change_guard{ other.m_change }; + if (!other.m_targets) + { + return; + } + const winrt::slim_lock_guard swap_guard{ other.m_swap }; + m_targets = std::move(other.m_targets); + } + + fused_event& operator=(fused_event&& other) + { + if (this != &other) + { + const winrt::slim_lock_guard other_change_guard{ other.m_change }; + const winrt::slim_lock_guard other_swap_guard{ other.m_swap }; + const winrt::slim_lock_guard self_change_guard{ m_change }; + const winrt::slim_lock_guard self_swap_guard{ m_swap }; + m_targets = std::move(other.m_targets); + } + return *this; + } + + explicit operator bool() const noexcept + { + return m_targets != nullptr; + } + + winrt::event_token operator()(const delegate_type& delegate) + { + return add_agile(winrt::impl::make_agile_delegate(delegate)); + } + + void operator()(const winrt::event_token token) + { + // Extends life of old targets array to release delegates outside of lock. + delegate_array temp_targets; + + { + const winrt::slim_lock_guard change_guard{ m_change }; + + if (!m_targets) + { + return; + } + + uint32_t available_slots = m_targets->size() - 1; + delegate_array new_targets; + bool removed = false; + + if (available_slots == 0) + { + if (get_token(*m_targets->begin()) == token) + { + removed = true; + } + } + else + { + new_targets = winrt::impl::make_event_array(available_slots); + auto new_iterator = new_targets->begin(); + + for (delegate_type const& element : *m_targets) + { + if (!removed && token == get_token(element)) + { + removed = true; + continue; + } + + if (available_slots == 0) + { + WINRT_ASSERT(!removed); + break; + } + + *new_iterator = element; + ++new_iterator; + --available_slots; + } + } + + if (removed) + { + const winrt::slim_lock_guard swap_guard{ m_swap }; + temp_targets = std::exchange(m_targets, std::move(new_targets)); + } + } + } + template - void raise(Arg const&... args) + void raise(const Arg&... args) { - _handlers(std::forward(args)...); + delegate_array temp_targets; + + { + const winrt::slim_lock_guard change_guard{ m_change }; + + if (!m_targets) + { + return; + } + + const winrt::slim_lock_guard swap_guard{ m_swap }; + temp_targets = std::move(m_targets); + } + + if (temp_targets) + { + for (const auto& element : *temp_targets) + { + if (!winrt::impl::invoke(element, args...)) + { + operator()(get_token(element)); + } + } + } + } + + private: + WINRT_IMPL_NOINLINE winrt::event_token add_agile(delegate_type delegate) + { + winrt::event_token token; + + // Extends life of old targets array to release delegates outside of lock. + delegate_array temp_targets; + + { + const winrt::slim_lock_guard change_guard{ m_change }; + const auto size = !m_targets ? 0 : m_targets->size(); + auto new_targets = winrt::impl::make_event_array(size + 1); + + if (m_targets) + { + std::copy_n(m_targets->begin(), m_targets->size(), new_targets->begin()); + } + + new_targets->back() = std::move(delegate); + token = get_token(new_targets->back()); + + const winrt::slim_lock_guard swap_guard{ m_swap }; + temp_targets = std::exchange(m_targets, std::move(new_targets)); + } + + return token; + } + + winrt::event_token get_token(delegate_type const& delegate) const noexcept + { + return winrt::event_token{ reinterpret_cast(WINRT_IMPL_EncodePointer(winrt::get_abi(delegate))) }; } - winrt::event> _handlers; + + delegate_array m_targets; + winrt::slim_mutex m_swap; + winrt::slim_mutex m_change; }; + #endif #ifdef WINRT_Windows_UI_Xaml_Data_H diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 7a3ca207227..eb264b056fc 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -161,7 +161,7 @@ namespace Microsoft::Console::Render TimerHandle _cursorBlinker; uint64_t _cursorBufferMutationId = 0; uint64_t _cursorCursorMutationId = 0; // Stupid name, but it's _cursor related and stores the cursor mutation id. - til::enumset _cursorVisibilityInhibitors; + til::enumset _cursorVisibilityInhibitors{ InhibitionSource::Host }; til::enumset _cursorBlinkingInhibitors; bool _cursorBlinkerOn = false; diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 5ebb2ff31c8..887eb1be06c 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -192,6 +192,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual void PlaySounds(const VTParameters parameters) = 0; // DECPS virtual void SetOptionalFeatures(const til::enumset features) = 0; + + virtual StringHandler EnterTmuxControl(const VTParameters parameters) = 0; // tmux -CC }; inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() = default; #pragma warning(pop) diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index fb613c4e121..b48dcb3ae5c 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -90,5 +90,6 @@ namespace Microsoft::Console::VirtualTerminal virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0; virtual void SearchMissingCommand(const std::wstring_view command) = 0; + virtual std::function EnterTmuxControl() = 0; }; } diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 9de1509bd73..107b366cb7b 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -4760,3 +4760,12 @@ void AdaptDispatch::SetOptionalFeatures(const til::enumset feat { _optionalFeatures = features; } + +ITermDispatch::StringHandler AdaptDispatch::EnterTmuxControl(const VTParameters parameters) +{ + if (parameters.size() != 1 || parameters.at(0).value() != 1000) + { + return nullptr; + } + return _api.EnterTmuxControl(); +} diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index a193a17602e..9d6555e399f 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -190,6 +190,8 @@ namespace Microsoft::Console::VirtualTerminal void SetOptionalFeatures(const til::enumset features) noexcept override; + StringHandler EnterTmuxControl(const VTParameters parameters) override; // tmux -CC + private: enum class Mode { diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 99c9033fee9..6aa8586bccb 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -179,6 +179,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons void PlaySounds(const VTParameters /*parameters*/) override{}; // DECPS void SetOptionalFeatures(const til::enumset /*features*/) override{}; + + StringHandler EnterTmuxControl(const VTParameters /*parameters*/) override { return nullptr; }; // tmux -CC }; #pragma warning(default : 26440) // Restore "can be declared noexcept" warning diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 1f42da08f66..9c46f7b20e6 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -224,6 +224,12 @@ class TestGetSet final : public ITerminalApi Log::Comment(L"SearchMissingCommand MOCK called..."); } + std::function EnterTmuxControl() override + { + Log::Comment(L"EnterTmuxControl MOCK called..."); + return nullptr; + } + void PrepData() { PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter. diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 4febc78ee84..1471424de4c 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -724,6 +724,9 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c case DcsActionCodes::DECRSPS_RestorePresentationState: handler = _dispatch->RestorePresentationState(parameters.at(0)); break; + case DcsActionCodes::TMUX_ControlEnter: + handler = _dispatch->EnterTmuxControl(parameters); + break; default: handler = nullptr; break; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index d36789e1085..baeb65b827f 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -178,6 +178,7 @@ namespace Microsoft::Console::VirtualTerminal DECRSTS_RestoreTerminalState = VTID("$p"), DECRQSS_RequestSetting = VTID("$q"), DECRSPS_RestorePresentationState = VTID("$t"), + TMUX_ControlEnter = VTID("p"), }; enum Vt52ActionCodes : uint64_t