From 0d9bc0d4337164f6bfec6a94dd8bfc2d9f69eedd Mon Sep 17 00:00:00 2001 From: Joe Xue Date: Sun, 18 May 2025 22:45:20 -0400 Subject: [PATCH 1/4] Add support for tmux control mode (#3656) --- .../TerminalApp/AppActionHandlers.cpp | 9 + src/cascadia/TerminalApp/Pane.cpp | 4 +- src/cascadia/TerminalApp/Pane.h | 3 +- .../Resources/en-US/Resources.resw | 5 +- .../TerminalApp/TerminalAppLib.vcxproj | 6 + src/cascadia/TerminalApp/TerminalPage.cpp | 53 +- src/cascadia/TerminalApp/TerminalPage.h | 3 + src/cascadia/TerminalApp/TmuxControl.cpp | 1480 +++++++++++++++++ src/cascadia/TerminalApp/TmuxControl.h | 410 +++++ .../TerminalConnection/DummyConnection.cpp | 36 + .../TerminalConnection/DummyConnection.h | 35 + .../TerminalConnection/DummyConnection.idl | 15 + .../TerminalConnection.vcxproj | 9 +- .../TerminalConnection.vcxproj.filters | 5 +- src/cascadia/TerminalControl/ControlCore.cpp | 26 + src/cascadia/TerminalControl/ControlCore.h | 7 + src/cascadia/TerminalControl/ControlCore.idl | 6 + src/cascadia/TerminalControl/ICoreState.idl | 1 + src/cascadia/TerminalControl/TermControl.cpp | 22 +- src/cascadia/TerminalControl/TermControl.h | 4 + src/cascadia/TerminalControl/TermControl.idl | 2 + src/cascadia/TerminalCore/Terminal.cpp | 6 + src/cascadia/TerminalCore/Terminal.hpp | 2 + .../ProfileViewModel.cpp | 5 + .../TerminalSettingsEditor/ProfileViewModel.h | 2 + .../ProfileViewModel.idl | 2 + .../Profiles_Terminal.xaml | 10 + .../Resources/en-US/Resources.resw | 4 + .../TerminalSettingsModel/MTSMSettings.h | 1 + .../TerminalSettingsModel/Profile.idl | 1 + .../TerminalSettings.cpp | 1 + .../TerminalSettingsModel/TerminalSettings.h | 1 + src/features.xml | 12 + src/terminal/adapter/ITermDispatch.hpp | 6 + src/terminal/adapter/adaptDispatch.cpp | 22 + src/terminal/adapter/adaptDispatch.hpp | 4 + src/terminal/adapter/termDispatch.hpp | 3 + .../parser/OutputStateMachineEngine.cpp | 3 + .../parser/OutputStateMachineEngine.hpp | 1 + 39 files changed, 2219 insertions(+), 8 deletions(-) create mode 100644 src/cascadia/TerminalApp/TmuxControl.cpp create mode 100644 src/cascadia/TerminalApp/TmuxControl.h create mode 100644 src/cascadia/TerminalConnection/DummyConnection.cpp create mode 100644 src/cascadia/TerminalConnection/DummyConnection.h create mode 100644 src/cascadia/TerminalConnection/DummyConnection.idl diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 69648929637..828932dc640 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -283,6 +283,15 @@ namespace winrt::TerminalApp::implementation const auto& terminalTab{ _senderOrFocusedTab(sender) }; + if constexpr (Feature_TmuxControl::IsEnabled()) + { + //Tmux control takes over + if (_tmuxControl && _tmuxControl->TabIsTmuxControl(terminalTab)) + { + return _tmuxControl->SplitPane(terminalTab, realArgs.SplitDirection()); + } + } + _SplitPane(terminalTab, realArgs.SplitDirection(), // This is safe, we're already filtering so the value is (0, 1) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 3255c5fba3f..74a88dc3b1e 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1304,10 +1304,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. diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 8bce64852d0..bdca3e3bb9f 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 9ff10d00f40..9f48b5eed31 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -986,4 +986,7 @@ An invalid regular expression was found. - \ No newline at end of file + + Tmux Control Tab + + diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index ac4357554cc..c12a2997f30 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -178,6 +178,9 @@ TerminalPaneContent.idl + + TerminalPage.idl + TerminalSettingsCache.idl @@ -302,6 +305,9 @@ TerminalPaneContent.idl + + TerminalPage.xaml + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 702b4a36ee3..e18dfcf5e99 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -18,6 +18,7 @@ #include "ScratchpadContent.h" #include "SnippetsPaneContent.h" #include "MarkdownPaneContent.h" +#include "TmuxControl.h" #include "TabRowControl.h" #include "Remoting.h" @@ -104,6 +105,11 @@ namespace winrt::TerminalApp::implementation } } _hostingHwnd = hwnd; + + if constexpr (Feature_TmuxControl::IsEnabled()) + { + _tmuxControl = std::make_unique(*this); + } return S_OK; } @@ -238,6 +244,15 @@ namespace winrt::TerminalApp::implementation _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { if (auto page{ weakThis.get() }) { + if constexpr (Feature_TmuxControl::IsEnabled()) + { + //Tmux control takes over + if (page->_tmuxControl && page->_tmuxControl->TabIsTmuxControl(page->_GetFocusedTabImpl())) + { + return; + } + } + page->_OpenNewTerminalViaDropdown(NewTerminalArgs()); } }); @@ -1203,6 +1218,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, @@ -2241,6 +2265,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() }; @@ -3188,7 +3221,7 @@ namespace winrt::TerminalApp::implementation const auto tabViewItem = eventArgs.Tab(); if (auto tab{ _GetTabByTabViewItem(tabViewItem) }) { - _HandleCloseTabRequested(tab); + tab.try_as()->CloseRequested.raise(nullptr, nullptr); } } @@ -3355,6 +3388,16 @@ namespace winrt::TerminalApp::implementation original->SetActive(); } + if constexpr (Feature_TmuxControl::IsEnabled()) + { + if (profile.AllowTmuxControl() && _tmuxControl) + { + control.SetTmuxControlHandlerProducer([this, control](auto print) { + return _tmuxControl->TmuxControlHandlerProducer(control, print); + }); + } + } + return resultPane; } @@ -5242,6 +5285,14 @@ 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.try_as())) + { + 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 b369bd920eb..946c050028e 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -10,6 +10,7 @@ #include "RenameWindowRequestedArgs.g.h" #include "RequestMoveContentArgs.g.h" #include "LaunchPositionRequest.g.h" +#include "TmuxControl.h" #include "Toast.h" #include "WindowsPackageManagerFactory.h" @@ -245,6 +246,7 @@ namespace winrt::TerminalApp::implementation std::vector> _previouslyClosedPanesAndTabs{}; uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; + std::unique_ptr _tmuxControl{ nullptr }; // use a weak reference to prevent circular dependency with AppLogic winrt::weak_ref _dialogPresenter; @@ -565,6 +567,7 @@ namespace winrt::TerminalApp::implementation friend class TerminalAppLocalTests::TabTests; friend class TerminalAppLocalTests::SettingsTests; + friend class TmuxControl; }; } diff --git a/src/cascadia/TerminalApp/TmuxControl.cpp b/src/cascadia/TerminalApp/TmuxControl.cpp new file mode 100644 index 00000000000..7f9fdff2601 --- /dev/null +++ b/src/cascadia/TerminalApp/TmuxControl.cpp @@ -0,0 +1,1480 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TmuxControl.h" + +#include +#include +#include +#include + +#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 int PaneBorderSize = 2; +static const int StaticMenuCount = 4; // "Separator" "Settings" "Command Palette" "About" + +namespace winrt::TerminalApp::implementation +{ + const std::wregex TmuxControl::REG_BEGIN{ L"^%begin (\\d+) (\\d+) (\\d+)$" }; + const std::wregex TmuxControl::REG_END{ L"^%end (\\d+) (\\d+) (\\d+)$" }; + const std::wregex TmuxControl::REG_ERROR{ L"^%error (\\d+) (\\d+) (\\d+)$" }; + + const std::wregex TmuxControl::REG_CLIENT_SESSION_CHANGED{ L"^%client-session-changed (\\S+) \\$(\\d+) (\\S)+$" }; + const std::wregex TmuxControl::REG_CLIENT_DETACHED{ L"^%client-detached (\\S+)$" }; + const std::wregex TmuxControl::REG_CONFIG_ERROR{ L"^%config-error (\\S+)$" }; + const std::wregex TmuxControl::REG_CONTINUE{ L"^%continue %(\\d+)$" }; + const std::wregex TmuxControl::REG_DETACH{ L"^\033$" }; + const std::wregex TmuxControl::REG_EXIT{ L"^%exit$" }; + const std::wregex TmuxControl::REG_EXTENDED_OUTPUT{ L"^%extended-output %(\\d+) (\\S+)$" }; + const std::wregex TmuxControl::REG_LAYOUT_CHANGED{ L"^%layout-change @(\\d+) ([\\da-fA-F]{4}),(\\S+)( \\S+)*$" }; + const std::wregex TmuxControl::REG_MESSAGE{ L"^%message (\\S+)$" }; + const std::wregex TmuxControl::REG_OUTPUT{ L"^%output %(\\d+) (.+)$" }; + const std::wregex TmuxControl::REG_PANE_MODE_CHANGED{ L"^%pane-mode-changed %(\\d+)$" }; + const std::wregex TmuxControl::REG_PASTE_BUFFER_CHANGED{ L"^%paste-buffer-changed (\\S+)$" }; + const std::wregex TmuxControl::REG_PASTE_BUFFER_DELETED{ L"^%paste-buffer-deleted (\\S+)$" }; + const std::wregex TmuxControl::REG_PAUSE{ L"^%pause %(\\d+)$" }; + const std::wregex TmuxControl::REG_SESSION_CHANGED{ L"^%" L"session-changed \\$(\\d+) (\\S+)$" }; + const std::wregex TmuxControl::REG_SESSION_RENAMED{ L"^%" L"session-renamed (\\S+)$" }; + const std::wregex TmuxControl::REG_SESSION_WINDOW_CHANGED{ L"^%" L"session-window-changed @(\\d+) (\\d+)$" }; + const std::wregex TmuxControl::REG_SESSIONS_CHANGED{ L"^%" L"sessions-changed$" }; + const std::wregex TmuxControl::REG_SUBSCRIPTION_CHANGED{ L"^%" L"subscription-changed (\\S+)$" }; + const std::wregex TmuxControl::REG_UNLINKED_WINDOW_ADD{ L"^%unlinked-window-add @(\\d+)$" }; + const std::wregex TmuxControl::REG_UNLINKED_WINDOW_CLOSE{ L"^%unlinked-window-close @(\\d+)$" }; + const std::wregex TmuxControl::REG_UNLINKED_WINDOW_RENAMED{ L"^%unlinked-window-renamed @(\\d+)$" }; + const std::wregex TmuxControl::REG_WINDOW_ADD{ L"^%window-add @(\\d+)$" }; + const std::wregex TmuxControl::REG_WINDOW_CLOSE{ L"^%window-close @(\\d+)$" }; + const std::wregex TmuxControl::REG_WINDOW_PANE_CHANGED{ L"^%window-pane-changed @(\\d+) %(\\d+)$" }; + const std::wregex TmuxControl::REG_WINDOW_RENAMED{ L"^%window-renamed @(\\d+) (\\S+)$" }; + + TmuxControl::TmuxControl(TerminalPage& page) : + _page(page) + { + _dispatcherQueue = DispatcherQueue::GetForCurrentThread(); + + _CreateNewTabMenu(); + } + + TmuxControl::StringHandler TmuxControl::TmuxControlHandlerProducer(const Control::TermControl control, const PrintHandler print) + { + std::lock_guard guard(_inUseMutex); + if (_inUse) + { + print(L"One session at same time"); + // Give any input to let tmux exit. + _dispatcherQueue.TryEnqueue([control]() { + control.RawWriteString(L"\n"); + }); + + // Empty handler, do nothing, it will exit anyway. + return [](const auto) { + return true; + }; + } + + _inUse = true; + _control = control; + _Print = print; + + _Print(L"Running the tmux control mode, press 'q' to detach:"); + + return [this](const auto ch) { + return _Advance(ch); + }; + } + + bool TmuxControl::TabIsTmuxControl(const winrt::com_ptr& tab) + { + 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.try_as()->PreCalculateCanSplit(direction, 0.5f, availableSpace); + if (!realSplitType) + { + return; + } + + switch(*realSplitType) + { + case SplitDirection::Right: + _SplitPane(tab->GetActivePane(), SplitDirection::Right); + break; + case SplitDirection::Down: + _SplitPane(tab->GetActivePane(), SplitDirection::Down); + break; + default: + break; + } + + return; + } + + void TmuxControl::_AttachSession() + { + _state = ATTACHING; + + _SetupProfile(); + + // 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) + { + _control.RawWriteString(L"detach\n"); + } + e.Handled(true); + }); + + _windowSizeChangedRevoker = _page.SizeChanged([this](auto, auto) { + auto fontSize = _control.CharacterDimensions(); + auto x = _page.ActualWidth(); + auto y = _page.ActualHeight(); + + _terminalWidth = (int)((x - _thickness.Left - _thickness.Right) / fontSize.Width); + _terminalHeight = (int)((y - _thickness.Top - _thickness.Bottom) / fontSize.Height); + _SetOption(std::format(L"default-size {}x{}", _terminalWidth, _terminalHeight)); + for (auto& w : _attachedWindows) + { + _ResizeWindow(w.first, _terminalWidth, _terminalHeight); + } + }); + + // Dynamically insert the "Tmux Control Tab" menu item into flyout menu + auto tabRow = _page.TabRow(); + auto tabRowImpl = winrt::get_self(tabRow); + auto newTabButton = tabRowImpl->NewTabButton(); + + auto menuCount = newTabButton.Flyout().try_as().Items().Size(); + newTabButton.Flyout().try_as().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._GetFocusedTab(); + } + + void TmuxControl::_DetachSession() + { + if (_state == INIT) + { + _inUse = false; + return; + } + _state = INIT; + _cmdQueue.clear(); + _dcsBuffer.clear(); + _cmdState = READY; + + std::vector tabs; + for (auto& w : _attachedWindows) + { + _page._RemoveTab(w.second); + } + _attachedPanes.clear(); + _attachedWindows.clear(); + + + // Revoke the event handlers + _control.KeyDown(_detachKeyDownRevoker); + _page.SizeChanged(_windowSizeChangedRevoker); + + // Remove the "Tmux Control Tab" menu item from flyout menu + auto tabRow = _page.TabRow(); + auto tabRowImpl = winrt::get_self(tabRow); + auto newTabButton = tabRowImpl->NewTabButton(); + int i = 0; + for (const auto& m : newTabButton.Flyout().try_as().Items()) + { + if (m.try_as().Text() == RS_(L"NewTmuxControlTab/Text")) + { + newTabButton.Flyout().try_as().Items().RemoveAt(i); + break; + } + i++; + } + + // Revoke the new tab button click handler + newTabButton.Click(_newTabClickRevoker); + + _inUse = false; + _control = Control::TermControl(nullptr); + _controlTab = nullptr; + } + + // Tmux control has its own profile, we duplicate it from the control panel + void TmuxControl::_SetupProfile() + { + const auto settings{ CascadiaSettings::LoadDefaults() }; + _profile = settings.ProfileDefaults(); + if (const auto terminalTab{ _page._GetFocusedTabImpl() }) + { + if (const auto pane{ terminalTab->GetActivePane() }) + { + _profile = settings.DuplicateProfile(pane->GetProfile()); + } + } + + // Calculate our dimension + auto fontSize = _control.CharacterDimensions(); + auto x = _page.ActualWidth(); + auto y = _page.ActualHeight(); + + _fontWidth = fontSize.Width; + _fontHeight = fontSize.Height; + + // 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. + _thickness.Left = _thickness.Right = int((_fontWidth - 2 * PaneBorderSize) / 2); + _thickness.Top = _thickness.Bottom = int((_fontHeight - 2 * PaneBorderSize) / 2); + + _terminalWidth = (int)((x - _thickness.Left - _thickness.Right) / fontSize.Width); + _terminalHeight = (int)((y - _thickness.Top - _thickness.Bottom) / fontSize.Height); + + _profile.Padding(XamlThicknessToOptimalString(_thickness)); + _profile.ScrollState(winrt::Microsoft::Terminal::Control::ScrollbarState::Hidden); + _profile.Icon(L"\uF714"); + _profile.Name(L"TmuxTab"); + } + + void TmuxControl::_CreateNewTabMenu() + { + auto newTabRun = Documents::Run(); + newTabRun.Text(RS_(L"NewTabRun/Text")); + auto newPaneRun = Documents::Run(); + newPaneRun.Text(RS_(L"NewPaneRun/Text")); + + auto textBlock = Controls::TextBlock{}; + textBlock.Inlines().Append(newTabRun); + textBlock.Inlines().Append(Documents::LineBreak{}); + textBlock.Inlines().Append(newPaneRun); + + _newTabMenu.Text(RS_(L"NewTmuxControlTab/Text")); + Controls::ToolTipService::SetToolTip(_newTabMenu, box_value(textBlock)); + 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(); + }); + } + + float TmuxControl::_ComputeSplitSize(int newSize, int originSize, SplitDirection direction) const + { + float fontSize = _fontWidth; + double margin1, margin2; + if (direction == SplitDirection::Left || direction == SplitDirection::Right) + { + margin2 = _thickness.Left + _thickness.Right; + margin1 = margin2 + PaneBorderSize; + } + else + { + fontSize = _fontHeight; + margin2 = _thickness.Top + _thickness.Bottom; + margin1 = margin2 + PaneBorderSize; + } + + auto f = round(newSize * fontSize + margin1) / round(originSize * fontSize + margin2); + + return (float)(1.0f - f); + } + + TerminalApp::TerminalTab TmuxControl::_GetTab(int windowId) const + { + auto search = _attachedWindows.find(windowId); + if (search == _attachedWindows.end()) + { + return nullptr; + } + + return search->second; + } + + 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 + { + _NewWindow(); + } + } + + void TmuxControl::_SendOutput(int paneId, const std::wstring& text) + { + auto search = _attachedPanes.find(paneId); + + // The pane is not ready it, put int backlog for now + if (search == _attachedPanes.end()) + { + _outputBacklog.insert_or_assign(paneId, text); + return; + } + + auto DecodeOutput = [](const std::wstring& in, std::wstring& out) { + auto it = in.begin(); + while (it != in.end()) + { + wchar_t c = *it; + if (c == L'\\') + { + ++it; + c = 0; + for (int i = 0; i < 3 && it != in.end(); ++i, ++it) + { + if (*it < L'0' || *it > L'7') + { + c = L'?'; + break; + } + c = c * 8 + (*it - L'0'); + } + out.push_back(c); + continue; + } + + if (c == L'\n') + { + out.push_back(L'\r'); + } + + out.push_back(c); + ++it; + } + }; + + auto& c = search->second.control; + + if (search->second.initialized) { + std::wstring out = L""; + DecodeOutput(text, out); + c.SendOutput(out); + } + else + { + std::wstring res(text); + c.Initialized([this, paneId, res](auto& /*i*/, auto& /*e*/) { + _SendOutput(paneId, res); + }); + } + } + + void TmuxControl::_Output(int paneId, const std::wstring& result) + { + if (_state != ATTACHED) + { + return; + } + + _SendOutput(paneId, result); + } + + void TmuxControl::_CloseWindow(int windowId) + { + auto search = _attachedWindows.find(windowId); + if (search == _attachedWindows.end()) + { + return; + } + + TerminalApp::TerminalTab t = search->second; + _attachedWindows.erase(search); + + t.Shutdown(); + + // Remove all attached panes in this window + for (auto p = _attachedPanes.begin(); p != _attachedPanes.end();) + { + if (p->second.windowId == windowId) + { + p = _attachedPanes.erase(p); + } + else + { + p++; + } + } + + _page._RemoveTab(t); + } + + void TmuxControl::_RenameWindow(int windowId, const std::wstring& name) + { + auto tab = _GetTab(windowId); + if (tab == nullptr) + { + return; + } + + tab.try_as()->SetTabText(winrt::hstring{ name }); + } + + void TmuxControl::_NewWindowFinalize(int windowId, int paneId, const std::wstring& windowName) + { + auto pane = _NewPane(windowId, paneId); + auto tab = _page._CreateNewTabFromPane(pane); + _attachedWindows.insert({windowId, tab}); + + tab.try_as()->CloseRequested([this, windowId](auto &&, auto &&) { + _KillWindow(windowId); + }); + + tab.try_as()->SetTabText(winrt::hstring{ windowName}); + + // Check if we have output before we are ready + auto search = _outputBacklog.find(paneId); + if (search == _outputBacklog.end()) + { + return; + } + + auto& result = search->second; + _SendOutput(paneId, result); + _outputBacklog.erase(search); + } + + void TmuxControl::_SplitPaneFinalize(int windowId, int newPaneId) + { + // Only handle the split pane + auto search = _attachedPanes.find(newPaneId); + if (search != _attachedPanes.end()) + { + return; + } + + auto tab = _GetTab(windowId); + if (tab == nullptr) + { + return; + } + + auto activePane = tab.try_as()->GetActivePane(); + if (activePane.get() != _splittingPane.first.get()) + { + return; + } + + auto c = activePane->GetTerminalControl(); + + int originSize; + auto direction = _splittingPane.second; + if (direction == SplitDirection::Right) + { + originSize = c.ViewWidth(); + } + else + { + originSize = c.ViewHeight(); + } + + auto newSize = originSize / 2; + + auto splitSize = _ComputeSplitSize(originSize - newSize, originSize, direction); + + auto newPane = _NewPane(windowId, newPaneId); + auto [origin, newGuy] = tab.try_as()->SplitPane(direction, splitSize, newPane); + + newGuy->GetTerminalControl().Focus(FocusState::Programmatic); + _splittingPane.first = nullptr; + } + + std::shared_ptr TmuxControl::_NewPane(int windowId, int paneId) + { + auto connection = TerminalConnection::DummyConnection{}; + auto controlSettings = TerminalSettings::CreateWithProfile(_page._settings, _profile, *_page._bindings); + const auto control = _page._CreateNewControlAndContent(controlSettings, connection); + + auto paneContent{ winrt::make (_profile, _page._terminalSettingsCache, control) }; + auto pane = std::make_shared(paneContent); + + control.Initialized([this, paneId](auto, auto) { + auto search = _attachedPanes.find(paneId); + if (search == _attachedPanes.end()) + { + return; + } + search->second.initialized = true; + }); + + connection.TerminalInput([this, paneId](auto keys) { + std::wstring out{ keys }; + _SendKey(paneId, out); + }); + + control.GotFocus([this, windowId, paneId](auto, auto) { + if (_activePaneId == paneId) + { + return; + } + + _activePaneId = paneId; + _SelectPane(_activePaneId); + + if (_activeWindowId != windowId) + { + _activeWindowId = windowId; + _SelectWindow(_activeWindowId); + } + }); + + control.SizeChanged([this, paneId, control](auto, const Xaml::SizeChangedEventArgs& args) { + if (_state != ATTACHED) + { + return; + } + // Ignore the new created + if (args.PreviousSize().Width == 0 || args.PreviousSize().Height == 0) + { + return; + } + + auto width = (int)((args.NewSize().Width - 2 * _thickness.Left) / _fontWidth); + auto height = (int)((args.NewSize().Height - 2 * _thickness.Top) / _fontHeight); + _ResizePane(paneId, width, height); + }); + + pane->Closed([this, paneId](auto&&, auto&&) { + _KillPane(paneId); + }); + + _attachedPanes.insert({ paneId, {windowId, paneId, control} }); + + return pane; + } + + bool TmuxControl::_SyncPaneState(std::vector panes, int history) + { + for (auto& p : panes) + { + auto search = _attachedPanes.find(p.paneId); + if (search == _attachedPanes.end()) + { + continue; + } + + _CapturePane(p.paneId, p.cursorX, p.cursorY, history); + } + + return true; + } + + bool TmuxControl::_SyncWindowState(std::vector windows) + { + for (auto& w : windows) + { + auto direction = SplitDirection::Left; + std::shared_ptr rootPane{ nullptr }; + std::unordered_map> attachedPanes; + for (auto& l : w.layout) + { + int rootSize; + auto& panes = l.panes; + auto& p = panes.at(0); + switch (l.type) + { + case SINGLE_PANE: + { + rootPane = _NewPane(w.windowId, p.id); + continue; + } + case SPLIT_HORIZONTAL: + direction = SplitDirection::Left; + rootSize = p.width; + break; + case SPLIT_VERTICAL: + direction = SplitDirection::Up; + rootSize = p.height; + break; + } + + auto search = attachedPanes.find(p.id); + std::shared_ptr targetPane{ nullptr }; + int targetPaneId = p.id; + if (search == attachedPanes.end()) + { + targetPane = _NewPane(w.windowId, p.id); + if (rootPane == nullptr) { + rootPane = targetPane; + } + attachedPanes.insert({p.id, targetPane}); + } + else + { + targetPane = search->second; + } + + for (size_t i = 1; i < panes.size(); i++) + { + // Create and attach + auto& p = panes.at(i); + + auto pane = _NewPane(w.windowId, p.id); + attachedPanes.insert({p.id, pane}); + + float splitSize; + if (direction == SplitDirection::Left) + { + auto paneSize = panes.at(i).width; + splitSize = _ComputeSplitSize(paneSize, rootSize, direction); + rootSize -= (paneSize + 1); + } + else + { + auto paneSize = panes.at(i).height; + splitSize = _ComputeSplitSize(paneSize, rootSize, direction); + rootSize -= (paneSize + 1); + } + targetPane = targetPane->AttachPane(pane, direction, splitSize); + attachedPanes.erase(targetPaneId); + attachedPanes.insert({targetPaneId, targetPane}); + targetPane->Closed([this, targetPaneId](auto&&, auto&&) { + _KillPane(targetPaneId); + }); + } + } + auto tab = _page._CreateNewTabFromPane(rootPane); + _attachedWindows.insert({w.windowId, tab}); + auto windowId = w.windowId; + tab.try_as()->CloseRequested([this, windowId](auto &&, auto &&) { + _KillWindow(windowId); + }); + + tab.try_as()->SetTabText(winrt::hstring{ w.name }); + _ListPanes(w.windowId, w.history); + } + return true; + } + + std::vector TmuxControl::_ParseTmuxWindowLayout(std::wstring& layout) + { + std::wregex RegPane { L"^,?(\\d+)x(\\d+),(\\d+),(\\d+),(\\d+)" }; + + std::wregex RegSplitHorizontalPush { L"^,?(\\d+)x(\\d+),(\\d+),(\\d+)\\{" }; + std::wregex RegSplitVerticalPush { L"^,?(\\d+)x(\\d+),(\\d+),(\\d+)\\[" }; + std::wregex RegSplitPop { L"^[\\} | \\]]" }; + std::vector result; + + auto _ExtractPane = [&](std::wsmatch& matches, TmuxPaneLayout& p) { + p.width = std::stoi(matches.str(1)); + p.height = std::stoi(matches.str(2)); + p.left = std::stoi(matches.str(3)); + p.top = std::stoi(matches.str(4)); + if (matches.size() > 5) + { + p.id = std::stoi(matches.str(5)); + } + }; + + auto _ParseNested = [&](std::wstring) { + std::wsmatch matches; + size_t parse_len = 0; + TmuxWindowLayout l; + + std::vector stack; + + while (layout.length() > 0) { + if (std::regex_search(layout, matches, RegSplitHorizontalPush)) { + TmuxPaneLayout p; + _ExtractPane(matches, p); + l.panes.push_back(p); + stack.push_back(l); + + l.type = SPLIT_HORIZONTAL; + l.panes.clear(); + l.panes.push_back(p); + } else if (std::regex_search(layout, matches, RegSplitVerticalPush)) { + TmuxPaneLayout p; + _ExtractPane(matches, p); + l.panes.push_back(p); + stack.push_back(l); + + // New one + l.type = SPLIT_VERTICAL; + l.panes.clear(); + l.panes.push_back(p); + } else if (std::regex_search(layout, matches, RegPane)) { + TmuxPaneLayout p; + _ExtractPane(matches, p); + l.panes.push_back(p); + } else if (std::regex_search(layout, matches, RegSplitPop)) { + auto id = l.panes.back().id; + l.panes.pop_back(); + l.panes.front().id = id; + result.insert(result.begin(), l); + + l = stack.back(); + l.panes.back().id = id; + stack.pop_back(); + } else { + assert(0); + } + parse_len = matches.length(0); + layout = layout.substr(parse_len); + } + + return result; + }; + + // Single pane mode + std::wsmatch matches; + if (std::regex_match(layout, matches, RegPane)) { + TmuxPaneLayout p; + _ExtractPane(matches, p); + + TmuxWindowLayout l; + l.type = SINGLE_PANE; + l.panes.push_back(p); + + result.push_back(l); + return result; + } + + // Nested mode + _ParseNested(layout); + + return result; + } + + void TmuxControl::_EventHandler(const Event& e) + { + switch(e.type) + { + case ATTACH: + _AttachSession(); + break; + case DETACH: + _DetachSession(); + break; + case LAYOUT_CHANGED: + _DiscoverPanes(_sessionId, e.windowId, false); + break; + case OUTPUT: + _Output(e.paneId, e.response); + break; + // Commands response + case RESPONSE: + _CommandHandler(e.response); + break; + case SESSION_CHANGED: + _sessionId = e.sessionId; + _SetOption(std::format(L"default-size {}x{}", _terminalWidth, _terminalHeight)); + _DiscoverWindows(_sessionId); + break; + case WINDOW_ADD: + _DiscoverPanes(_sessionId, e.windowId, true); + break; + case WINDOW_CLOSE: + case UNLINKED_WINDOW_CLOSE: + _CloseWindow(e.windowId); + break; + case WINDOW_PANE_CHANGED: + _SplitPaneFinalize(e.windowId, e.paneId); + break; + case WINDOW_RENAMED: + _RenameWindow(e.windowId, e.response); + break; + + default: + break; + } + + // We are done, give the command in the queue a chance to run + _ScheduleCommand(); + } + + void TmuxControl::_Parse(const std::wstring& line) + { + std::wsmatch matches; + + // Tmux generic rules + if (std::regex_match(line, REG_BEGIN)) + { + _event.type = BEGIN; + } + else if (std::regex_match(line, REG_END)) + { + if (_state == INIT) + { + _event.type = ATTACH; + } + else + { + _event.type = RESPONSE; + } + } + else if (std::regex_match(line, REG_ERROR)) + { + // Remove the extra '\n' we added + _Print(std::wstring(_event.response.begin(), _event.response.end() - 1)); + _event.response.clear(); + _event.type = NOTHING; + } + + // tmux specific rules + else if (std::regex_match(line, REG_DETACH)) + { + _event.type = DETACH; + } + else if (std::regex_match(line, matches, REG_LAYOUT_CHANGED)) + { + _event.windowId = std::stoi(matches.str(1)); + _event.type = LAYOUT_CHANGED; + } + else if (std::regex_match(line, matches, REG_OUTPUT)) + { + _event.paneId = std::stoi(matches.str(1)); + _event.response = matches.str(2); + _event.type = OUTPUT; + } + else if (std::regex_match(line, matches, REG_SESSION_CHANGED)) + { + _event.type = SESSION_CHANGED; + _event.sessionId = std::stoi(matches.str(1)); + } + else if (std::regex_match(line, matches, REG_WINDOW_ADD)) + { + _event.windowId = std::stoi(matches.str(1)); + _event.type = WINDOW_ADD; + } + else if (std::regex_match(line, matches, REG_WINDOW_CLOSE)) + { + _event.type = WINDOW_CLOSE; + _event.windowId = std::stoi(matches.str(1)); + } + else if (std::regex_match(line, matches, REG_WINDOW_PANE_CHANGED)) + { + _event.type = WINDOW_PANE_CHANGED; + _event.windowId = std::stoi(matches.str(1)); + _event.paneId = std::stoi(matches.str(2)); + } + else if (std::regex_match(line, matches, REG_WINDOW_RENAMED)) + { + _event.windowId = std::stoi(matches.str(1)); + _event.response = matches.str(2); + _event.type = WINDOW_RENAMED; + } + else if (std::regex_match(line, matches, REG_UNLINKED_WINDOW_CLOSE)) + { + _event.type = UNLINKED_WINDOW_CLOSE; + _event.windowId = std::stoi(matches.str(1)); + } + else + { + if (_event.type == BEGIN) + { + _event.response += line + L'\n'; + } + else + { + // Other events that we don't care, do nothing + _event.type = NOTHING; + } + } + + if (_event.type != BEGIN && _event.type != NOTHING) + { + auto& e = _event; + _dispatcherQueue.TryEnqueue([this, e]() { + _EventHandler(e); + }); + _event.response.clear(); + } + + return; + } + + // From tmux to controller through the dcs. parse it per line. + bool TmuxControl::_Advance(wchar_t ch) + { + std::wstring buffer = L""; + + switch(ch) + { + case '\033': + buffer.push_back(ch); + break; + case '\n': + buffer = std::wstring(_dcsBuffer.begin(), _dcsBuffer.end()); + _dcsBuffer.clear(); + break; + case '\r': + break; + default: + _dcsBuffer.push_back(ch); + break; + } + + if (buffer.size() > 0) + { + _Parse(buffer); + } + + return true; + } + + // Commands + void TmuxControl::_AttachDone() + { + auto cmd = std::make_unique(); + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::AttachDone::GetCommand() + { + return std::wstring(std::format(L"list-session\n")); + } + + bool TmuxControl::AttachDone::ResultHandler(const std::wstring& /*result*/, TmuxControl& tmux) + { + if (tmux._cmdQueue.size() > 1) + { + // Not done, requeue it, this is because capture may requeue in case the pane is not ready + tmux._AttachDone(); + } else { + tmux._state = ATTACHED; + } + + return true; + } + + void TmuxControl::_CapturePane(int paneId, int cursorX, int cursorY, int history) + { + auto cmd = std::make_unique(); + cmd->paneId = paneId; + cmd->cursorX = cursorX; + cmd->cursorY = cursorY; + cmd->history = history; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::CapturePane::GetCommand() + { + return std::wstring(std::format(L"capture-pane -p -t %{} -e -C -S {}\n", this->paneId, this->history * -1)); + } + + bool TmuxControl::CapturePane::ResultHandler(const std::wstring& result, TmuxControl& tmux) + { + // Tmux output has an extra newline + std::wstring output = result; + output.pop_back(); + // Put the cursor to right position + output += std::format(L"\033[{};{}H", this->cursorY + 1, this->cursorX + 1); + tmux._SendOutput(this->paneId, output); + return true; + } + + void TmuxControl::_DiscoverPanes(int sessionId, int windowId, bool newWindow) + { + if (_state != ATTACHED) + { + return; + } + auto cmd = std::make_unique(); + cmd->sessionId = sessionId; + cmd->windowId = windowId; + cmd->newWindow = newWindow; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::DiscoverPanes::GetCommand() + { + if (!this->newWindow) + { + return std::wstring(std::format(L"list-panes -s -F '" + L"#{{pane_id}} #{{window_name}}" + L"' -t ${}\n", this->sessionId)); + } + else + { + return std::wstring(std::format(L"list-panes -F '" + L"#{{pane_id}} #{{window_name}}" + L"' -t @{}\n", this->windowId)); + } + } + + bool TmuxControl::DiscoverPanes::ResultHandler(const std::wstring& result, TmuxControl& tmux) + { + std::wstring line; + std::wregex REG_PANE{ L"^%(\\d+) (\\S+)$" }; + + std::wstringstream in; + in.str(result); + + std::set panes; + while (std::getline(in, line, L'\n')) + { + std::wsmatch matches; + + if (!std::regex_match(line, matches, REG_PANE)) { + continue; + } + int paneId = std::stoi(matches.str(1)); + std::wstring windowName = matches.str(2); + // New window case, just one pane + if (this->newWindow) + { + tmux._NewWindowFinalize(this->windowId, paneId, windowName); + return true; + } + panes.insert(paneId); + } + + // For pane exit case + for (auto p = tmux._attachedPanes.begin(); p != tmux._attachedPanes.end();) + { + if (!panes.contains(p->first)) + { + p = tmux._attachedPanes.erase(p); + auto tab = tmux._GetTab(this->windowId); + if (tab == nullptr) + { + return true; + } + auto activePane = tab.try_as()->GetActivePane(); + activePane->Close(); + return true; + } + else + { + p++; + } + } + + return true; + } + + void TmuxControl::_DiscoverWindows(int sessionId) + { + auto cmd = std::make_unique(); + cmd->sessionId = sessionId; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::DiscoverWindows::GetCommand() + { + return std::wstring(std::format(L"list-windows -F '" + L"#{{window_id}}" + L"' -t ${}\n", this->sessionId)); + } + + bool TmuxControl::DiscoverWindows::ResultHandler(const std::wstring& result, TmuxControl& tmux) + { + std::wstring line; + std::wregex REG_WINDOW{ L"^@(\\d+)$" }; + + std::wstringstream in; + in.str(result); + + while (std::getline(in, line, L'\n')) + { + std::wsmatch matches; + + if (!std::regex_match(line, matches, REG_WINDOW)) { + continue; + } + int windowId = std::stoi(matches.str(1)); + tmux._ResizeWindow(windowId, tmux._terminalWidth, tmux._terminalHeight); + } + + tmux._ListWindow(this->sessionId, -1); + return true; + } + + void TmuxControl::_KillPane(int paneId) + { + auto search = _attachedPanes.find(paneId); + if (search == _attachedPanes.end()) + { + return; + } + + auto cmd = std::make_unique(); + cmd->paneId = paneId; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::KillPane::GetCommand() + { + return std::wstring(std::format(L"kill-pane -t %{}\n", this->paneId)); + } + + void TmuxControl::_KillWindow(int windowId) + { + auto search = _attachedWindows.find(windowId); + if (search == _attachedWindows.end()) + { + return; + } + + auto cmd = std::make_unique(); + cmd->windowId = windowId; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::KillWindow::GetCommand() + { + return std::wstring(std::format(L"kill-window -t @{}\n", this->windowId)); + } + + void TmuxControl::_ListPanes(int windowId, int history) + { + auto cmd = std::make_unique(); + cmd->windowId = windowId; + cmd->history = history; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::ListPanes::GetCommand() + { + return std::wstring(std::format(L"list-panes -F '" + L"#{{session_id}} #{{window_id}} #{{pane_id}} " + L"#{{cursor_x}} #{{cursor_y}} " + L"#{{pane_active}}" + L"' -t @{}\n", + this->windowId)); + } + + bool TmuxControl::ListPanes::ResultHandler(const std::wstring& result, TmuxControl& tmux) + { + std::wstring line; + std::wregex REG_PANE{ L"^\\$(\\d+) @(\\d+) %(\\d+) (\\d+) (\\d+) (\\d+)$" }; + std::vector panes; + + std::wstringstream in; + in.str(result); + + while (std::getline(in, line, L'\n')) + { + std::wsmatch matches; + + if (!std::regex_match(line, matches, REG_PANE)) + { + continue; + } + + TmuxPane p = { + .sessionId = std::stoi(matches.str(1)), + .windowId = std::stoi(matches.str(2)), + .paneId = std::stoi(matches.str(3)), + .cursorX = std::stoi(matches.str(4)), + .cursorY = std::stoi(matches.str(5)), + .active = (std::stoi(matches.str(6)) == 1) + }; + + panes.push_back(p); + } + + + tmux._SyncPaneState(panes, this->history); + return true; + } + + void TmuxControl::_ListWindow(int sessionId, int windowId) + { + auto cmd = std::make_unique(); + cmd->windowId = windowId; + cmd->sessionId = sessionId; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::ListWindow::GetCommand() + { + return std::wstring(std::format(L"list-windows -F '" + L"#{{session_id}} #{{window_id}} " + L"#{{window_width}} #{{window_height}} " + L"#{{window_active}} " + L"#{{window_layout}} " + L"#{{window_name}} " + L"#{{history_limit}}" + L"' -t ${}\n", this->sessionId)); + } + + bool TmuxControl::ListWindow::ResultHandler(const std::wstring& result, TmuxControl& tmux) + { + std::wstring line; + std::wregex REG_WINDOW{ L"^\\$(\\d+) @(\\d+) (\\d+) (\\d+) (\\d+) ([\\da-fA-F]{4}),(\\S+) (\\S+) (\\d+)$" }; + std::vector windows; + + std::wstringstream in; + in.str(result); + + while (std::getline(in, line, L'\n')) + { + TmuxWindow w; + std::wsmatch matches; + + if (!std::regex_match(line, matches, REG_WINDOW)) { + continue; + } + w.sessionId = std::stoi(matches.str(1)); + w.windowId = std::stoi(matches.str(2)); + w.width = std::stoi(matches.str(3)); + w.height = std::stoi(matches.str(4)); + w.active = (std::stoi(matches.str(5)) == 1); + w.layoutChecksum = matches.str(6); + w.name = matches.str(8); + w.history = std::stoi(matches.str(9)); + std::wstring layout(matches.str(7)); + w.layout = tmux._ParseTmuxWindowLayout(layout); + windows.push_back(w); + } + + tmux._SyncWindowState(windows); + tmux._AttachDone(); + return true; + } + + void TmuxControl::_NewWindow() + { + auto cmd = std::make_unique(); + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::NewWindow::GetCommand() + { + return std::wstring(L"new-window\n"); + } + + void TmuxControl::_ResizePane(int paneId, int width, int height) + { + if (width == 0 || height == 0) + { + return; + } + auto cmd = std::make_unique(); + cmd->paneId = paneId; + cmd->width = width; + cmd->height = height; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::ResizePane::GetCommand() + { + return std::wstring(std::format(L"resize-pane -x {} -y {} -t %{}\n", this->width, this->height, this->paneId)); + } + + void TmuxControl::_ResizeWindow(int windowId, int width, int height) + { + auto cmd = std::make_unique(); + cmd->windowId = windowId; + cmd->width = width; + cmd->height = height; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::ResizeWindow::GetCommand() + { + return std::wstring(std::format(L"resize-window -x {} -y {} -t @{}\n", this->width, this->height, this->windowId)); + } + + void TmuxControl::_SelectPane(int paneId) + { + auto cmd = std::make_unique(); + cmd->paneId = paneId; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::SelectPane::GetCommand() + { + return std::wstring(std::format(L"select-pane -t %{}\n", this->paneId)); + } + + void TmuxControl::_SelectWindow(int windowId) + { + auto cmd = std::make_unique(); + cmd->windowId = windowId; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::SelectWindow::GetCommand() + { + return std::wstring(std::format(L"select-window -t @{}\n", this->windowId)); + } + + void TmuxControl::_SendKey(int paneId, const std::wstring keys) + { + auto cmd = std::make_unique(); + cmd->paneId = paneId; + cmd->keys = keys; + + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::SendKey::GetCommand() + { + std::wstring out = L""; + for (auto & c : this->keys) + { + out += std::format(L"{:#x} ", c); + } + + return std::wstring(std::format(L"send-key -t %{} {}\n", this->paneId, out)); + } + + + void TmuxControl::_SetOption(const std::wstring& option) + { + auto cmd = std::make_unique(); + cmd->option = option; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::SetOption::GetCommand() + { + return std::wstring(std::format(L"set-option {}\n", this->option)); + } + + void TmuxControl::_SplitPane(std::shared_ptr pane, SplitDirection direction) + { + if (_splittingPane.first != nullptr) + { + return; + } + + if (!pane) + { + return; + } + + int paneId = -1; + for (auto& p : _attachedPanes) + { + if (pane->GetTerminalControl() == p.second.control) + { + paneId = p.first; + } + } + + if (paneId == -1) + { + return; + } + + _splittingPane = {pane, direction}; + auto cmd = std::make_unique(); + cmd->direction = direction; + cmd->paneId = paneId; + _SendCommand(std::move(cmd)); + _ScheduleCommand(); + } + + std::wstring TmuxControl::SplitPane::GetCommand() + { + if (this->direction == SplitDirection::Right) + { + return std::wstring(std::format(L"split-window -h -t %{}\n", this->paneId)); + } + else + { + return std::wstring(std::format(L"split-window -v -t %{}\n", this->paneId)); + } + } + + // From controller to tmux + void TmuxControl::_CommandHandler(const std::wstring& result) + { + if (_cmdState == WAITING && _cmdQueue.size() > 0) + { + auto cmd = _cmdQueue.front().get(); + cmd->ResultHandler(result, *this); + _cmdQueue.pop_front(); + _cmdState = READY; + } + } + + void TmuxControl::_SendCommand(std::unique_ptr cmd) + { + _cmdQueue.push_back(std::move(cmd)); + } + + void TmuxControl::_ScheduleCommand() + { + if (_cmdState != READY) + { + return; + } + + if (_cmdQueue.size() > 0) + { + _cmdState = WAITING; + + auto cmd = _cmdQueue.front().get(); + auto cmdStr = cmd->GetCommand(); + _control.RawWriteString(cmdStr); + } + } +} diff --git a/src/cascadia/TerminalApp/TmuxControl.h b/src/cascadia/TerminalApp/TmuxControl.h new file mode 100644 index 00000000000..0959a921601 --- /dev/null +++ b/src/cascadia/TerminalApp/TmuxControl.h @@ -0,0 +1,410 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include +#include + +#include "Pane.h" + +namespace winrt::TerminalApp::implementation +{ + struct TerminalPage; + + class TmuxControl + { + using StringHandler = std::function; + using PrintHandler = std::function; + using StringHandlerProducer = std::function; + using SplitDirection = winrt::Microsoft::Terminal::Settings::Model::SplitDirection; + + public: + TmuxControl(TerminalPage& page); + StringHandler TmuxControlHandlerProducer(const winrt::Microsoft::Terminal::Control::TermControl control, const PrintHandler print); + bool TabIsTmuxControl(const winrt::com_ptr& tab); + void SplitPane(const winrt::com_ptr& tab, SplitDirection direction); + + private: + static const std::wregex REG_BEGIN; + static const std::wregex REG_END; + static const std::wregex REG_ERROR; + + static const std::wregex REG_CLIENT_SESSION_CHANGED; + static const std::wregex REG_CLIENT_DETACHED; + static const std::wregex REG_CONFIG_ERROR; + static const std::wregex REG_CONTINUE; + static const std::wregex REG_DETACH; + static const std::wregex REG_EXIT; + static const std::wregex REG_EXTENDED_OUTPUT; + static const std::wregex REG_LAYOUT_CHANGED; + static const std::wregex REG_MESSAGE; + static const std::wregex REG_OUTPUT; + static const std::wregex REG_PANE_MODE_CHANGED; + static const std::wregex REG_PASTE_BUFFER_CHANGED; + static const std::wregex REG_PASTE_BUFFER_DELETED; + static const std::wregex REG_PAUSE; + static const std::wregex REG_SESSION_CHANGED; + static const std::wregex REG_SESSION_RENAMED; + static const std::wregex REG_SESSION_WINDOW_CHANGED; + static const std::wregex REG_SESSIONS_CHANGED; + static const std::wregex REG_SUBSCRIPTION_CHANGED; + static const std::wregex REG_UNLINKED_WINDOW_ADD; + static const std::wregex REG_UNLINKED_WINDOW_CLOSE; + static const std::wregex REG_UNLINKED_WINDOW_RENAMED; + static const std::wregex REG_WINDOW_ADD; + static const std::wregex REG_WINDOW_CLOSE; + static const std::wregex REG_WINDOW_PANE_CHANGED; + static const std::wregex REG_WINDOW_RENAMED; + + enum State : int + { + INIT, + ATTACHING, + ATTACHED, + } _state{ INIT }; + + enum CommandState : int + { + READY, + WAITING, + } _cmdState{ READY }; + + enum EventType : int + { + BEGIN, + END, + ERR, + + ATTACH, + DETACH, + CLIENT_SESSION_CHANGED, + CLIENT_DETACHED, + CONFIG_ERROR, + CONTINUE, + EXIT, + EXTENDED_OUTPUT, + LAYOUT_CHANGED, + NOTHING, + MESSAGE, + OUTPUT, + PANE_MODE_CHANGED, + PASTE_BUFFER_CHANGED, + PASTE_BUFFER_DELETED, + PAUSE, + RESPONSE, + SESSION_CHANGED, + SESSION_RENAMED, + SESSION_WINDOW_CHANGED, + SESSIONS_CHANGED, + SUBSCRIPTION_CHANGED, + UNLINKED_WINDOW_ADD, + UNLINKED_WINDOW_CLOSE, + UNLINKED_WINDOW_RENAMED, + WINDOW_ADD, + WINDOW_CLOSE, + WINDOW_PANE_CHANGED, + WINDOW_RENAMED, + }; + + struct Event + { + EventType type{ NOTHING }; + int sessionId{ -1 }; + int windowId{ -1 }; + int paneId{ -1 }; + + std::wstring response; + } _event; + + // Command structs + struct Command + { + public: + virtual std::wstring GetCommand() = 0; + virtual bool ResultHandler(const std::wstring& /*result*/, TmuxControl& /*tmux*/) { return true; }; + }; + + struct AttachDone : public Command + { + public: + std::wstring GetCommand() override; + bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; + }; + + struct CapturePane : public Command + { + public: + std::wstring GetCommand() override; + bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; + + int paneId{ -1 }; + int cursorX{ 0 }; + int cursorY{ 0 }; + int history{ 0 }; + }; + + struct DiscoverPanes : public Command { + public: + std::wstring GetCommand() override; + bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; + + int sessionId{ -1 }; + int windowId{ -1 }; + bool newWindow{ false }; + }; + + struct DiscoverWindows : public Command { + public: + std::wstring GetCommand() override; + bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; + + int sessionId{ -1 }; + }; + + struct KillPane : public Command + { + public: + std::wstring GetCommand() override; + + int paneId{ -1 }; + }; + + struct KillWindow : public Command + { + public: + std::wstring GetCommand() override; + + int windowId{ -1 }; + }; + + struct ListPanes : public Command + { + public: + std::wstring GetCommand() override; + bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; + + int windowId{ -1 }; + int history{ 2000 }; + }; + + struct ListWindow : public Command { + public: + std::wstring GetCommand() override; + bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; + + int windowId{ -1 }; + int sessionId{ -1 }; + }; + + struct NewWindow : public Command + { + public: + std::wstring GetCommand() override; + }; + + struct ResizePane : public Command + { + public: + std::wstring GetCommand() override; + + int width{ 0 }; + int height{ 0 }; + int paneId{ -1 }; + }; + + struct ResizeWindow : public Command + { + public: + std::wstring GetCommand() override; + int width{ 0 }; + int height{ 0 }; + int windowId{ -1 }; + }; + + struct SelectWindow : public Command + { + public: + std::wstring GetCommand() override; + + int windowId{ -1 }; + }; + + struct SelectPane : public Command + { + public: + std::wstring GetCommand() override; + + int paneId{ -1 }; + }; + + struct SendKey : public Command + { + public: + std::wstring GetCommand() override; + + int paneId{ -1 }; + std::wstring keys; + wchar_t key{ '\0' }; + }; + + struct SetOption : public Command + { + public: + std::wstring GetCommand() override; + + std::wstring option; + }; + + struct SplitPane : public Command + { + public: + std::wstring GetCommand() override; + + int paneId{ -1 }; + SplitDirection direction{ SplitDirection::Left }; + }; + + // Layout structs + enum TmuxLayoutType : int + { + SINGLE_PANE, + SPLIT_HORIZONTAL, + SPLIT_VERTICAL, + }; + + struct TmuxPaneLayout + { + int width; + int height; + int left; + int top; + int id; + }; + + struct TmuxWindowLayout + { + TmuxLayoutType type{ SINGLE_PANE }; + std::vector panes; + }; + + struct TmuxWindow + { + int sessionId{ -1 }; + int windowId{ -1 }; + int width{ 0 }; + int height{ 0 }; + int history{ 2000 }; + bool active{ false }; + std::wstring name; + std::wstring layoutChecksum; + std::vector layout; + }; + + struct TmuxPane + { + int sessionId; + int windowId; + int paneId; + int cursorX; + int cursorY; + bool active; + }; + + struct AttachedPane + { + int windowId; + int paneId; + winrt::Microsoft::Terminal::Control::TermControl control; + bool initialized { false }; + }; + + // Private methods + void _AttachSession(); + void _DetachSession(); + void _SetupProfile(); + void _CreateNewTabMenu(); + + float _ComputeSplitSize(int newSize, int originSize, SplitDirection direction) const; + TerminalApp::TerminalTab _GetTab(int windowId) const; + + void _SendOutput(int paneId, const std::wstring& text); + void _Output(int paneId, const std::wstring& result); + void _CloseWindow(int windowId); + void _RenameWindow(int windowId, const std::wstring& name); + void _NewWindowFinalize(int windowId, int paneId, const std::wstring& windowName); + void _SplitPaneFinalize(int windowId, int paneId); + std::shared_ptr _NewPane(int windowId, int paneId); + + bool _SyncPaneState(std::vector panes, int history); + bool _SyncWindowState(std::vector windows); + std::vector _ParseTmuxWindowLayout(std::wstring& layout); + + void _EventHandler(const Event& e); + void _Parse(const std::wstring& buffer); + bool _Advance(wchar_t ch); + + // Tmux command methods + void _AttachDone(); + void _CapturePane(int paneId, int cursorX, int cursorY, int history); + void _DiscoverPanes(int sessionId, int windowId, bool newWindow); + void _DiscoverWindows(int sessionId); + void _KillPane(int paneId); + void _KillWindow(int windowId); + void _ListWindow(int sessionId, int windowId); + void _ListPanes(int windowId, int history); + void _NewWindow(); + void _OpenNewTerminalViaDropdown(); + void _ResizePane(int paneId, int width, int height); + void _ResizeWindow(int windowId, int width, int height); + void _SelectPane(int paneId); + void _SelectWindow(int windowId); + void _SendKey(int paneId, const std::wstring keys); + void _SetOption(const std::wstring& option); + void _SplitPane(std::shared_ptr pane, SplitDirection direction); + + void _CommandHandler(const std::wstring& result); + void _SendCommand(std::unique_ptr cmd); + void _ScheduleCommand(); + + // Private variables + TerminalPage& _page; + winrt::Microsoft::Terminal::Settings::Model::Profile _profile; + winrt::Microsoft::Terminal::Control::TermControl _control { nullptr }; + TerminalApp::TabBase _controlTab { nullptr }; + winrt::Windows::System::DispatcherQueue _dispatcherQueue{ nullptr }; + + winrt::event_token _detachKeyDownRevoker; + winrt::event_token _windowSizeChangedRevoker; + winrt::event_token _newTabClickRevoker; + + ::winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _newTabMenu{}; + + std::vector _dcsBuffer; + std::deque> _cmdQueue; + std::unordered_map _attachedPanes; + std::unordered_map _attachedWindows; + std::unordered_map _outputBacklog; + + int _sessionId{ -1 }; + + int _terminalWidth{ 0 }; + int _terminalHeight{ 0 }; + + float _fontWidth{ 0 }; + float _fontHeight{ 0 }; + + ::winrt::Windows::UI::Xaml::Thickness _thickness{ 0,0,0,0 }; + + std::pair, SplitDirection> _splittingPane{ nullptr, SplitDirection::Right }; + + int _activePaneId{ -1 }; + int _activeWindowId{ -1 }; + + std::function _Print; + bool _inUse { false }; + std::mutex _inUseMutex; + }; +} diff --git a/src/cascadia/TerminalConnection/DummyConnection.cpp b/src/cascadia/TerminalConnection/DummyConnection.cpp new file mode 100644 index 00000000000..c3126125082 --- /dev/null +++ b/src/cascadia/TerminalConnection/DummyConnection.cpp @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "DummyConnection.h" +#include + +#include "DummyConnection.g.cpp" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + DummyConnection::DummyConnection() noexcept = default; + + void DummyConnection::Start() noexcept + { + } + + void DummyConnection::WriteInput(const winrt::array_view buffer) + { + const auto data = winrt_array_to_wstring_view(buffer); + std::wstringstream prettyPrint; + for (const auto& wch : data) + { + prettyPrint << wch; + } + TerminalInput.raise(prettyPrint.str()); + } + + void DummyConnection::Resize(uint32_t /*rows*/, uint32_t /*columns*/) noexcept + { + } + + void DummyConnection::Close() noexcept + { + } +} diff --git a/src/cascadia/TerminalConnection/DummyConnection.h b/src/cascadia/TerminalConnection/DummyConnection.h new file mode 100644 index 00000000000..732ca1867a2 --- /dev/null +++ b/src/cascadia/TerminalConnection/DummyConnection.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "DummyConnection.g.h" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + struct DummyConnection : DummyConnectionT + { + DummyConnection() noexcept; + + void Start() noexcept; + void WriteInput(const winrt::array_view buffer); + void Resize(uint32_t rows, uint32_t columns) noexcept; + void Close() noexcept; + + void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) const noexcept {}; + + winrt::guid SessionId() const noexcept { return {}; } + ConnectionState State() const noexcept { return ConnectionState::Connected; } + + til::event TerminalOutput; + til::event TerminalInput; + til::typed_event StateChanged; + + bool _rawMode { false }; + }; +} + +namespace winrt::Microsoft::Terminal::TerminalConnection::factory_implementation +{ + BASIC_FACTORY(DummyConnection); +} diff --git a/src/cascadia/TerminalConnection/DummyConnection.idl b/src/cascadia/TerminalConnection/DummyConnection.idl new file mode 100644 index 00000000000..e4b70041ff7 --- /dev/null +++ b/src/cascadia/TerminalConnection/DummyConnection.idl @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ITerminalConnection.idl"; + +namespace Microsoft.Terminal.TerminalConnection +{ + [default_interface] + runtimeclass DummyConnection : ITerminalConnection + { + DummyConnection(); + event TerminalOutputHandler TerminalInput; + }; + +} diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj index 8eda9937725..e7126ad3aae 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -32,6 +32,9 @@ EchoConnection.idl + + DummyConnection.idl + @@ -48,6 +51,9 @@ EchoConnection.idl + + DummyConnection.idl + ConptyConnection.idl @@ -58,6 +64,7 @@ + @@ -98,4 +105,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..04437becc21 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters @@ -15,6 +15,7 @@ + @@ -23,6 +24,7 @@ + @@ -31,6 +33,7 @@ + @@ -42,4 +45,4 @@ - \ No newline at end of file + diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 61d699a6cc9..d319d4f0300 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -478,6 +478,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } + void ControlCore::SendOutput(const std::wstring_view wstr) + { + if (wstr.empty()) + { + return; + } + + auto lock = _terminal->LockForWriting(); + _terminal->Write(wstr); + } + bool ControlCore::SendCharEvent(const wchar_t ch, const WORD scanCode, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers) @@ -1570,6 +1581,16 @@ 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. @@ -2995,4 +3016,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _terminal->PreviewText(input); } + + void ControlCore::SetTmuxControlHandlerProducer(ITermDispatch::StringHandlerProducer producer) + { + _terminal->SetTmuxControlHandlerProducer(producer); + } } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 2453670d3f8..2578fabb0a8 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -24,6 +24,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 { @@ -41,6 +42,8 @@ namespace ControlUnitTests class ControlInteractivityTests; }; +using Microsoft::Console::VirtualTerminal::ITermDispatch; + #define RUNTIME_SETTING(type, name, setting) \ private: \ std::optional _runtime##name{ std::nullopt }; \ @@ -123,6 +126,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::color BackgroundColor() const; void SendInput(std::wstring_view wstr); + void SendOutput(std::wstring_view wstr); void PasteText(const winrt::hstring& hstr); bool CopySelectionToClipboard(bool singleLine, bool withControlSequences, const Windows::Foundation::IReference& formats); void SelectAll(); @@ -172,6 +176,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation int ScrollOffset(); int ViewHeight() const; + int ViewWidth() const; int BufferHeight() const; bool HasSelection() const; @@ -267,6 +272,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ShouldShowSelectOutput(); void PreviewInput(std::wstring_view input); + void SetTmuxControlHandlerProducer(ITermDispatch::StringHandlerProducer producer); RUNTIME_SETTING(float, Opacity, _settings->Opacity()); RUNTIME_SETTING(float, FocusedOpacity, FocusedAppearance().Opacity()); @@ -452,6 +458,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation friend class ControlUnitTests::ControlInteractivityTests; bool _inUnitTests{ false }; }; + } namespace winrt::Microsoft::Terminal::Control::factory_implementation diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index bc704a13426..7576c10f217 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(); @@ -122,6 +126,7 @@ namespace Microsoft.Terminal.Control Int16 scanCode, Microsoft.Terminal.Core.ControlKeyStates modifiers); void SendInput(String text); + void SendOutput(String text); void PasteText(String text); void SelectAll(); void ClearSelection(); @@ -180,6 +185,7 @@ namespace Microsoft.Terminal.Control Boolean ShouldShowSelectOutput(); void OpenCWD(); + void SetTmuxControlHandlerProducer(TmuxDCSHandlerProducer producer); void ClearQuickFix(); diff --git a/src/cascadia/TerminalControl/ICoreState.idl b/src/cascadia/TerminalControl/ICoreState.idl index 7bd5411c6fe..9de43bc95fc 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 67809d4cea9..347c37a4d8f 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -899,6 +899,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation RawWriteString(wstr); } + void TermControl::SendOutput(const winrt::hstring& wstr) + { + _core.SendOutput(wstr); + } void TermControl::ClearBuffer(Control::ClearBufferType clearType) { _core.ClearBuffer(clearType); @@ -1464,6 +1468,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Likewise, run the event handlers outside of lock (they could // be reentrant) Initialized.raise(*this, nullptr); + + if (_tmuxDCSHandlerProducer) + { + _core.SetTmuxControlHandlerProducer(_tmuxDCSHandlerProducer); + } return true; } @@ -2774,6 +2783,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _core.ViewHeight(); } + int TermControl::ViewWidth() const + { + return _core.ViewWidth(); + } + int TermControl::BufferHeight() const { return _core.BufferHeight(); @@ -2973,7 +2987,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 }; } } @@ -4218,4 +4234,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.CursorOn(focused); } } + void TermControl::SetTmuxControlHandlerProducer(winrt::Microsoft::Terminal::Control::TmuxDCSHandlerProducer producer) + { + _tmuxDCSHandlerProducer = producer; + } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 7fe3db1be09..b4fe3fad5e1 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -96,6 +96,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation int ScrollOffset() const; int ViewHeight() const; + int ViewWidth() const; int BufferHeight() const; bool HasSelection() const; @@ -118,6 +119,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SelectOutput(const bool goUp); winrt::hstring CurrentWorkingDirectory() const; + void SetTmuxControlHandlerProducer(winrt::Microsoft::Terminal::Control::TmuxDCSHandlerProducer producer); #pragma endregion void ScrollViewport(int viewTop); @@ -127,6 +129,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Windows::Foundation::Size GetFontSize() const; void SendInput(const winrt::hstring& input); + void SendOutput(const winrt::hstring& input); void ClearBuffer(Control::ClearBufferType clearType); void ToggleShaderEffects(); @@ -448,6 +451,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _SelectCommandHandler(const IInspectable& sender, const IInspectable& args); void _SelectOutputHandler(const IInspectable& sender, const IInspectable& args); bool _displayCursorWhileBlurred() const noexcept; + winrt::Microsoft::Terminal::Control::TmuxDCSHandlerProducer _tmuxDCSHandlerProducer { nullptr }; struct Revokers { diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 9d72977455d..7faeeff3a3c 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -117,6 +117,7 @@ namespace Microsoft.Terminal.Control void ToggleShaderEffects(); void SendInput(String input); + void SendOutput(String input); 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); @@ -156,5 +157,6 @@ namespace Microsoft.Terminal.Control void ClearQuickFix(); void Detach(); + void SetTmuxControlHandlerProducer(TmuxDCSHandlerProducer producer); } } diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 005916822dc..45089fa7c71 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -241,6 +241,12 @@ void Terminal::SetOptionalFeatures(winrt::Microsoft::Terminal::Core::ICoreSettin engine.Dispatch().SetOptionalFeatures(features); } +void Terminal::SetTmuxControlHandlerProducer(ITermDispatch::StringHandlerProducer producer) const noexcept +{ + auto& engine = reinterpret_cast(_stateMachine->Engine()); + engine.Dispatch().SetTmuxControlHandlerProducer(producer); +} + bool Terminal::IsXtermBracketedPasteModeEnabled() const noexcept { return _systemMode.test(Mode::BracketedPaste); diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index e38841e3bc1..3ecd9092923 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -14,6 +14,7 @@ #include "../../types/inc/Viewport.hpp" #include "../../types/inc/GlyphWidth.hpp" #include "../../cascadia/terminalcore/ITerminalInput.hpp" +#include "../../terminal/adapter/ITermDispatch.hpp" #include #include @@ -127,6 +128,7 @@ class Microsoft::Terminal::Core::Terminal final : std::wstring CurrentCommand() const; void SerializeMainBuffer(const wchar_t* destination) const; + void SetTmuxControlHandlerProducer(Microsoft::Console::VirtualTerminal::ITermDispatch::StringHandlerProducer producer) const noexcept; #pragma region ITerminalApi // These methods are defined in TerminalApi.cpp diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp index feb4765f7e0..da582acdc63 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp @@ -168,6 +168,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 f1634e809d8..f94799ddf0d 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -160,9 +160,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 b0663276eb1..36ae216f31d 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl @@ -116,6 +116,7 @@ namespace Microsoft.Terminal.Settings.Editor Boolean UsingBuiltInIcon { get; }; Boolean UsingEmojiIcon { get; }; Boolean UsingImageIcon { get; }; + Boolean TmuxControlEnabled; IInspectable CurrentBuiltInIcon; Windows.Foundation.Collections.IVector BuiltInIcons { get; }; @@ -161,5 +162,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 489d438b1a6..ce8828c7d9f 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml @@ -79,6 +79,16 @@ + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 7ed6a842524..a6936bfefa2 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -560,6 +560,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 01bd5ce5c81..74bfb1f3414 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -103,6 +103,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 0d1ffe9f364..48548286f45 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -95,6 +95,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/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index ff9860aef6b..7196269b43e 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -350,6 +350,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _AllowVtChecksumReport = profile.AllowVtChecksumReport(); _AllowVtClipboardWrite = profile.AllowVtClipboardWrite(); _PathTranslationStyle = profile.PathTranslationStyle(); + _AllowTmuxControl = profile.AllowTmuxControl(); } // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 9591ad34875..135e9bede06 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -177,6 +177,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, bool, RepositionCursorWithMouse, false); INHERITABLE_SETTING(Model::TerminalSettings, bool, ReloadEnvironmentVariables, true); + INHERITABLE_SETTING(Model::TerminalSettings, bool, AllowTmuxControl, false); INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, Microsoft::Terminal::Control::PathTranslationStyle::None); diff --git a/src/features.xml b/src/features.xml index 894875ecd58..6265703b31f 100644 --- a/src/features.xml +++ b/src/features.xml @@ -202,4 +202,16 @@ + + Feature_TmuxControl + Enables Tmux Control + 3656 + AlwaysDisabled + + Dev + Canary + Preview + + + diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 5ebb2ff31c8..b3ad6013631 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -24,6 +24,9 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch { public: using StringHandler = std::function; + using PrintHandler = std::function; + // Use this get the StringHandler, meanwhile pass the function to give app a function to print message bypass the parser + using StringHandlerProducer = std::function; enum class OptionalFeature { @@ -192,6 +195,9 @@ 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 + virtual void SetTmuxControlHandlerProducer(StringHandlerProducer producer) = 0; // tmux -CC }; inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() = default; #pragma warning(pop) diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index cb8e4feb5a4..97a60fca0b8 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -4798,3 +4798,25 @@ 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; + } + + if (_tmuxControlHandlerProducer) { + const auto page = _pages.ActivePage(); + return _tmuxControlHandlerProducer([this, page](auto s) { + PrintString(s); + _DoLineFeed(page, true, true); + }); + } + + return nullptr; +} + +void AdaptDispatch::SetTmuxControlHandlerProducer(StringHandlerProducer producer) +{ + _tmuxControlHandlerProducer = producer; +} diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index af726c0e01e..3e37b37204b 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -190,6 +190,9 @@ namespace Microsoft::Console::VirtualTerminal void SetOptionalFeatures(const til::enumset features) noexcept override; + StringHandler EnterTmuxControl(const VTParameters parameters) override; // tmux -CC + void SetTmuxControlHandlerProducer(StringHandlerProducer producer) override; // tmux -CC + private: enum class Mode { @@ -328,6 +331,7 @@ namespace Microsoft::Console::VirtualTerminal til::enumset _modes{ Mode::PageCursorCoupling }; SgrStack _sgrStack; + StringHandlerProducer _tmuxControlHandlerProducer { nullptr }; void _SetUnderlineStyleHelper(const VTParameter option, TextAttribute& attr) noexcept; size_t _SetRgbColorsHelper(const VTParameters options, diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 99c9033fee9..f3c4734ff75 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -179,6 +179,9 @@ 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 + void SetTmuxControlHandlerProducer(StringHandlerProducer /*producer*/) override{}; // tmux -CC }; #pragma warning(default : 26440) // Restore "can be declared noexcept" warning 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 From 8e3e19ea1fef711033ed329c3831869b3d501856 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 15 Dec 2025 21:18:16 +0100 Subject: [PATCH 2/4] Big, big refactor --- .../TerminalApp/DebugTapConnection.cpp | 8 +- src/cascadia/TerminalApp/DebugTapConnection.h | 2 +- src/cascadia/TerminalApp/Pane.cpp | 3 +- .../Resources/en-US/Resources.resw | 3 + src/cascadia/TerminalApp/Tab.cpp | 7 + src/cascadia/TerminalApp/Tab.h | 1 + src/cascadia/TerminalApp/Tab.idl | 1 + src/cascadia/TerminalApp/TabManagement.cpp | 8 +- .../TerminalAppLib.vcxproj.filters | 19 +- src/cascadia/TerminalApp/TerminalPage.cpp | 74 +- src/cascadia/TerminalApp/TerminalPage.h | 2 +- .../TerminalApp/TerminalPaneContent.cpp | 2 + src/cascadia/TerminalApp/TmuxControl.cpp | 2142 ++++++++--------- src/cascadia/TerminalApp/TmuxControl.h | 493 +--- .../TerminalConnection/AzureConnection.cpp | 24 +- .../TerminalConnection/AzureConnection.h | 1 + .../TerminalConnection/ConptyConnection.cpp | 23 +- .../TerminalConnection/DummyConnection.cpp | 36 - .../TerminalConnection/EchoConnection.cpp | 2 +- .../ITerminalConnection.idl | 2 +- .../TerminalConnection.vcxproj | 12 +- .../TerminalConnection.vcxproj.filters | 6 +- .../TerminalConnection/TmuxConnection.cpp | 53 + .../{DummyConnection.h => TmuxConnection.h} | 27 +- ...DummyConnection.idl => TmuxConnection.idl} | 7 +- src/cascadia/TerminalControl/ControlCore.cpp | 77 +- src/cascadia/TerminalControl/ControlCore.h | 11 +- src/cascadia/TerminalControl/ControlCore.idl | 4 +- src/cascadia/TerminalControl/EventArgs.cpp | 1 + src/cascadia/TerminalControl/EventArgs.h | 6 + src/cascadia/TerminalControl/EventArgs.idl | 7 + src/cascadia/TerminalControl/TermControl.cpp | 24 +- src/cascadia/TerminalControl/TermControl.h | 8 +- src/cascadia/TerminalControl/TermControl.idl | 4 +- src/cascadia/TerminalCore/Terminal.cpp | 18 +- src/cascadia/TerminalCore/Terminal.hpp | 10 +- src/cascadia/TerminalCore/TerminalApi.cpp | 5 + .../TerminalSettings.h | 1 + .../PreviewConnection.cpp | 9 +- .../PreviewConnection.h | 4 +- .../Profiles_Terminal.xaml | 4 +- src/common.build.pre.props | 2 +- src/features.xml | 4 +- src/host/outputStream.cpp | 7 + src/host/outputStream.hpp | 1 + src/inc/til/string.h | 36 +- src/inc/til/winrt.h | 184 +- src/renderer/base/renderer.hpp | 2 +- src/terminal/adapter/ITermDispatch.hpp | 4 - src/terminal/adapter/ITerminalApi.hpp | 1 + src/terminal/adapter/adaptDispatch.cpp | 19 +- src/terminal/adapter/adaptDispatch.hpp | 2 - src/terminal/adapter/termDispatch.hpp | 1 - .../adapter/ut_adapter/adapterTest.cpp | 6 + 54 files changed, 1703 insertions(+), 1717 deletions(-) delete mode 100644 src/cascadia/TerminalConnection/DummyConnection.cpp create mode 100644 src/cascadia/TerminalConnection/TmuxConnection.cpp rename src/cascadia/TerminalConnection/{DummyConnection.h => TmuxConnection.h} (60%) rename src/cascadia/TerminalConnection/{DummyConnection.idl => TmuxConnection.idl} (65%) 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 c709d3307e7..2347b20df10 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -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/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 1f7b28c5f77..64883c52794 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -982,6 +982,9 @@ An invalid regular expression was found. + + 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.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 f4d8cb40a7a..7ecc9d07023 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -5,24 +5,24 @@ #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" -#include "TmuxControl.h" -#include "TabRowControl.h" #include "Remoting.h" #include "ScratchpadContent.h" #include "SettingsPaneContent.h" #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" @@ -262,11 +262,6 @@ namespace winrt::TerminalApp::implementation } _hostingHwnd = hwnd; - - if constexpr (Feature_TmuxControl::IsEnabled()) - { - _tmuxControl = std::make_unique(*this); - } return S_OK; } @@ -403,15 +398,6 @@ namespace winrt::TerminalApp::implementation _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { if (auto page{ weakThis.get() }) { - if constexpr (Feature_TmuxControl::IsEnabled()) - { - //Tmux control takes over - if (page->_tmuxControl && page->_tmuxControl->TabIsTmuxControl(page->_GetFocusedTabImpl())) - { - return; - } - } - TraceLoggingWrite( g_hTerminalAppProvider, "NewTabMenuDefaultButtonClicked", @@ -420,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()); } }); @@ -3445,7 +3440,7 @@ namespace winrt::TerminalApp::implementation const auto tabViewItem = eventArgs.Tab(); if (auto tab{ _GetTabByTabViewItem(tabViewItem) }) { - tab.try_as()->CloseRequested.raise(nullptr, nullptr); + _HandleCloseTabRequested(tab); } } @@ -3617,12 +3612,22 @@ namespace winrt::TerminalApp::implementation if constexpr (Feature_TmuxControl::IsEnabled()) { - if (profile.AllowTmuxControl() && _tmuxControl) + if (!_tmuxControl) { - control.SetTmuxControlHandlerProducer([this, control](auto print) { - return _tmuxControl->TmuxControlHandlerProducer(control, print); - }); + _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; @@ -5044,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; } @@ -5110,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; } @@ -5515,11 +5530,12 @@ namespace winrt::TerminalApp::implementation if constexpr (Feature_TmuxControl::IsEnabled()) { //Tmux control tab doesn't support to drag - if (_tmuxControl && _tmuxControl->TabIsTmuxControl(tabImpl.try_as())) + 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 bd07b26cfe9..e01741744e1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -257,7 +257,7 @@ namespace winrt::TerminalApp::implementation std::vector> _previouslyClosedPanesAndTabs{}; uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; - std::unique_ptr _tmuxControl{ nullptr }; + std::shared_ptr _tmuxControl{ nullptr }; // use a weak reference to prevent circular dependency with AppLogic winrt::weak_ref _dialogPresenter; 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 index 7f9fdff2601..7e08b6a9e76 100644 --- a/src/cascadia/TerminalApp/TmuxControl.cpp +++ b/src/cascadia/TerminalApp/TmuxControl.cpp @@ -6,9 +6,9 @@ #include #include -#include #include +#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" #include "TerminalPage.h" #include "TabRowControl.h" @@ -21,1460 +21,1372 @@ using namespace winrt::Windows::UI; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Core; -static const int PaneBorderSize = 2; +static const float PaneBorderSize = 2; static const int StaticMenuCount = 4; // "Separator" "Settings" "Command Palette" "About" -namespace winrt::TerminalApp::implementation +// 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) { - const std::wregex TmuxControl::REG_BEGIN{ L"^%begin (\\d+) (\\d+) (\\d+)$" }; - const std::wregex TmuxControl::REG_END{ L"^%end (\\d+) (\\d+) (\\d+)$" }; - const std::wregex TmuxControl::REG_ERROR{ L"^%error (\\d+) (\\d+) (\\d+)$" }; - - const std::wregex TmuxControl::REG_CLIENT_SESSION_CHANGED{ L"^%client-session-changed (\\S+) \\$(\\d+) (\\S)+$" }; - const std::wregex TmuxControl::REG_CLIENT_DETACHED{ L"^%client-detached (\\S+)$" }; - const std::wregex TmuxControl::REG_CONFIG_ERROR{ L"^%config-error (\\S+)$" }; - const std::wregex TmuxControl::REG_CONTINUE{ L"^%continue %(\\d+)$" }; - const std::wregex TmuxControl::REG_DETACH{ L"^\033$" }; - const std::wregex TmuxControl::REG_EXIT{ L"^%exit$" }; - const std::wregex TmuxControl::REG_EXTENDED_OUTPUT{ L"^%extended-output %(\\d+) (\\S+)$" }; - const std::wregex TmuxControl::REG_LAYOUT_CHANGED{ L"^%layout-change @(\\d+) ([\\da-fA-F]{4}),(\\S+)( \\S+)*$" }; - const std::wregex TmuxControl::REG_MESSAGE{ L"^%message (\\S+)$" }; - const std::wregex TmuxControl::REG_OUTPUT{ L"^%output %(\\d+) (.+)$" }; - const std::wregex TmuxControl::REG_PANE_MODE_CHANGED{ L"^%pane-mode-changed %(\\d+)$" }; - const std::wregex TmuxControl::REG_PASTE_BUFFER_CHANGED{ L"^%paste-buffer-changed (\\S+)$" }; - const std::wregex TmuxControl::REG_PASTE_BUFFER_DELETED{ L"^%paste-buffer-deleted (\\S+)$" }; - const std::wregex TmuxControl::REG_PAUSE{ L"^%pause %(\\d+)$" }; - const std::wregex TmuxControl::REG_SESSION_CHANGED{ L"^%" L"session-changed \\$(\\d+) (\\S+)$" }; - const std::wregex TmuxControl::REG_SESSION_RENAMED{ L"^%" L"session-renamed (\\S+)$" }; - const std::wregex TmuxControl::REG_SESSION_WINDOW_CHANGED{ L"^%" L"session-window-changed @(\\d+) (\\d+)$" }; - const std::wregex TmuxControl::REG_SESSIONS_CHANGED{ L"^%" L"sessions-changed$" }; - const std::wregex TmuxControl::REG_SUBSCRIPTION_CHANGED{ L"^%" L"subscription-changed (\\S+)$" }; - const std::wregex TmuxControl::REG_UNLINKED_WINDOW_ADD{ L"^%unlinked-window-add @(\\d+)$" }; - const std::wregex TmuxControl::REG_UNLINKED_WINDOW_CLOSE{ L"^%unlinked-window-close @(\\d+)$" }; - const std::wregex TmuxControl::REG_UNLINKED_WINDOW_RENAMED{ L"^%unlinked-window-renamed @(\\d+)$" }; - const std::wregex TmuxControl::REG_WINDOW_ADD{ L"^%window-add @(\\d+)$" }; - const std::wregex TmuxControl::REG_WINDOW_CLOSE{ L"^%window-close @(\\d+)$" }; - const std::wregex TmuxControl::REG_WINDOW_PANE_CHANGED{ L"^%window-pane-changed @(\\d+) %(\\d+)$" }; - const std::wregex TmuxControl::REG_WINDOW_RENAMED{ L"^%window-renamed @(\\d+) (\\S+)$" }; + auto lf = remaining.find(L'\n'); + lf = std::min(lf, remaining.size()); - TmuxControl::TmuxControl(TerminalPage& page) : - _page(page) + // Trim any potential \r before the \n + auto end = lf; + if (end != 0 && remaining[end - 1] == L'\r') { - _dispatcherQueue = DispatcherQueue::GetForCurrentThread(); + --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); +} - _CreateNewTabMenu(); +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; } - TmuxControl::StringHandler TmuxControl::TmuxControlHandlerProducer(const Control::TermControl control, const PrintHandler print) + const auto type = field.front(); + switch (type) { - std::lock_guard guard(_inUseMutex); - if (_inUse) - { - print(L"One session at same time"); - // Give any input to let tmux exit. - _dispatcherQueue.TryEnqueue([control]() { - control.RawWriteString(L"\n"); - }); + case L'$': + case L'@': + case L'%': + break; + default: + return result; + } - // Empty handler, do nothing, it will exit anyway. - return [](const auto) { - return true; - }; - } + const auto id = til::parse_unsigned(field.substr(1), 10); + if (!id) + { + return result; + } - _inUse = true; - _control = control; - _Print = print; + result.type = (IdentifierType)type; + result.value = *id; + return result; +} + +namespace winrt::TerminalApp::implementation +{ + TmuxControl::TmuxControl(TerminalPage& page) : + _page{ page } + { + _dispatcherQueue = DispatcherQueue::GetForCurrentThread(); - _Print(L"Running the tmux control mode, press 'q' to detach:"); + const auto newTabRun = Documents::Run(); + newTabRun.Text(RS_(L"NewTabRun/Text")); + const auto newPaneRun = Documents::Run(); + newPaneRun.Text(RS_(L"NewPaneRun/Text")); - return [this](const auto ch) { - return _Advance(ch); - }; + 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::TabIsTmuxControl(const winrt::com_ptr& tab) + bool TmuxControl::AcquireSingleUseLock(winrt::Microsoft::Terminal::Control::TermControl control) noexcept { - if (!tab) + if (_inUse) { return false; } - for (auto& t : _attachedWindows) - { - if (t.second.TabViewIndex() == tab->TabViewIndex()) - { - return true; - } - } - - if (_controlTab.TabViewIndex() == tab->TabViewIndex()) - { - return true; - } - - 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::SplitPane(const winrt::com_ptr& tab, SplitDirection direction) + void TmuxControl::FeedInput(std::wstring_view str) { - 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) + if (str.empty()) { return; } - const auto realSplitType = tab.try_as()->PreCalculateCanSplit(direction, 0.5f, availableSpace); - if (!realSplitType) + // 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; } - switch(*realSplitType) - { - case SplitDirection::Right: - _SplitPane(tab->GetActivePane(), SplitDirection::Right); - break; - case SplitDirection::Down: - _SplitPane(tab->GetActivePane(), SplitDirection::Down); - break; - default: - break; - } - - return; - } + auto idx = str.find(L'\n'); - void TmuxControl::_AttachSession() - { - _state = ATTACHING; - - _SetupProfile(); + // 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()); - // 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) + // If this still wasn't a full line, wait for more data. + if (idx == std::wstring_view::npos) { - _control.RawWriteString(L"detach\n"); + return; } - e.Handled(true); - }); - - _windowSizeChangedRevoker = _page.SizeChanged([this](auto, auto) { - auto fontSize = _control.CharacterDimensions(); - auto x = _page.ActualWidth(); - auto y = _page.ActualHeight(); - _terminalWidth = (int)((x - _thickness.Left - _thickness.Right) / fontSize.Width); - _terminalHeight = (int)((y - _thickness.Top - _thickness.Bottom) / fontSize.Height); - _SetOption(std::format(L"default-size {}x{}", _terminalWidth, _terminalHeight)); - for (auto& w : _attachedWindows) + // Strip of any remaining CR. We already removed the LF after the find() call. + if (!_lineBuffer.empty() && _lineBuffer.back() == L'\r') { - _ResizeWindow(w.first, _terminalWidth, _terminalHeight); + _lineBuffer.pop_back(); } - }); - // Dynamically insert the "Tmux Control Tab" menu item into flyout menu - auto tabRow = _page.TabRow(); - auto tabRowImpl = winrt::get_self(tabRow); - auto newTabButton = tabRowImpl->NewTabButton(); + _parseLine(std::move(_lineBuffer)); + _lineBuffer.clear(); - auto menuCount = newTabButton.Flyout().try_as().Items().Size(); - newTabButton.Flyout().try_as().Items().InsertAt(menuCount - StaticMenuCount, _newTabMenu); + // Move past the line we just processed. + str = til::safe_slice_abs(str, idx + 1, std::wstring_view::npos); + idx = str.find(L'\n'); + } - // Register new tab button click handler for tmux control - _newTabClickRevoker = newTabButton.Click([this](auto&&, auto&&) { - if (TabIsTmuxControl(_page._GetFocusedTabImpl())) + 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') { - _OpenNewTerminalViaDropdown(); + --end; } - }); - _controlTab = _page._GetFocusedTab(); - } + const auto line = til::safe_slice_abs(str, 0, end); + _parseLine(std::wstring{ line }); - void TmuxControl::_DetachSession() - { - if (_state == INIT) - { - _inUse = false; - return; + str = til::safe_slice_abs(str, idx + 1, std::wstring_view::npos); + idx = str.find(L'\n'); } - _state = INIT; - _cmdQueue.clear(); - _dcsBuffer.clear(); - _cmdState = READY; - std::vector tabs; - for (auto& w : _attachedWindows) + // If there's any leftover partial line, stash it for later. + if (!str.empty()) { - _page._RemoveTab(w.second); + _lineBuffer.append(str); } - _attachedPanes.clear(); - _attachedWindows.clear(); - + } - // Revoke the event handlers - _control.KeyDown(_detachKeyDownRevoker); - _page.SizeChanged(_windowSizeChangedRevoker); + bool TmuxControl::TabIsTmuxControl(const winrt::com_ptr& tab) + { + assert(_dispatcherQueue.HasThreadAccess()); - // Remove the "Tmux Control Tab" menu item from flyout menu - auto tabRow = _page.TabRow(); - auto tabRowImpl = winrt::get_self(tabRow); - auto newTabButton = tabRowImpl->NewTabButton(); - int i = 0; - for (const auto& m : newTabButton.Flyout().try_as().Items()) + if (!tab) { - if (m.try_as().Text() == RS_(L"NewTmuxControlTab/Text")) - { - newTabButton.Flyout().try_as().Items().RemoveAt(i); - break; - } - i++; + return false; } - // Revoke the new tab button click handler - newTabButton.Click(_newTabClickRevoker); - - _inUse = false; - _control = Control::TermControl(nullptr); - _controlTab = nullptr; - } - - // Tmux control has its own profile, we duplicate it from the control panel - void TmuxControl::_SetupProfile() - { - const auto settings{ CascadiaSettings::LoadDefaults() }; - _profile = settings.ProfileDefaults(); - if (const auto terminalTab{ _page._GetFocusedTabImpl() }) + for (auto& t : _attachedWindows) { - if (const auto pane{ terminalTab->GetActivePane() }) + if (t.second->TabViewIndex() == tab->TabViewIndex()) { - _profile = settings.DuplicateProfile(pane->GetProfile()); + return true; } } - // Calculate our dimension - auto fontSize = _control.CharacterDimensions(); - auto x = _page.ActualWidth(); - auto y = _page.ActualHeight(); - - _fontWidth = fontSize.Width; - _fontHeight = fontSize.Height; - - // 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. - _thickness.Left = _thickness.Right = int((_fontWidth - 2 * PaneBorderSize) / 2); - _thickness.Top = _thickness.Bottom = int((_fontHeight - 2 * PaneBorderSize) / 2); - - _terminalWidth = (int)((x - _thickness.Left - _thickness.Right) / fontSize.Width); - _terminalHeight = (int)((y - _thickness.Top - _thickness.Bottom) / fontSize.Height); + if (_controlTab->TabViewIndex() == tab->TabViewIndex()) + { + return true; + } - _profile.Padding(XamlThicknessToOptimalString(_thickness)); - _profile.ScrollState(winrt::Microsoft::Terminal::Control::ScrollbarState::Hidden); - _profile.Icon(L"\uF714"); - _profile.Name(L"TmuxTab"); + return false; } - void TmuxControl::_CreateNewTabMenu() + void TmuxControl::SplitPane(const winrt::com_ptr& tab, SplitDirection direction) { - auto newTabRun = Documents::Run(); - newTabRun.Text(RS_(L"NewTabRun/Text")); - auto newPaneRun = Documents::Run(); - newPaneRun.Text(RS_(L"NewPaneRun/Text")); - - auto textBlock = Controls::TextBlock{}; - textBlock.Inlines().Append(newTabRun); - textBlock.Inlines().Append(Documents::LineBreak{}); - textBlock.Inlines().Append(newPaneRun); - - _newTabMenu.Text(RS_(L"NewTmuxControlTab/Text")); - Controls::ToolTipService::SetToolTip(_newTabMenu, box_value(textBlock)); - 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(); - }); - } + const auto contentWidth = static_cast(_page._tabContent.ActualWidth()); + const auto contentHeight = static_cast(_page._tabContent.ActualHeight()); + const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; - float TmuxControl::_ComputeSplitSize(int newSize, int originSize, SplitDirection direction) const - { - float fontSize = _fontWidth; - double margin1, margin2; - if (direction == SplitDirection::Left || direction == SplitDirection::Right) + if (tab == nullptr) { - margin2 = _thickness.Left + _thickness.Right; - margin1 = margin2 + PaneBorderSize; + return; } - else + + const auto realSplitType = tab->PreCalculateCanSplit(direction, 0.5f, availableSpace); + if (!realSplitType) { - fontSize = _fontHeight; - margin2 = _thickness.Top + _thickness.Bottom; - margin1 = margin2 + PaneBorderSize; + return; } - auto f = round(newSize * fontSize + margin1) / round(originSize * fontSize + margin2); - - return (float)(1.0f - f); + switch (*realSplitType) + { + case SplitDirection::Right: + _sendSplitPane(tab->GetActivePane(), SplitDirection::Right); + break; + case SplitDirection::Down: + _sendSplitPane(tab->GetActivePane(), SplitDirection::Down); + break; + default: + break; + } } - TerminalApp::TerminalTab TmuxControl::_GetTab(int windowId) const + safe_void_coroutine TmuxControl::_parseLine(std::wstring line) { - auto search = _attachedWindows.find(windowId); - if (search == _attachedWindows.end()) + if (line.empty()) { - return nullptr; + co_return; } - return search->second; - } + const auto self = shared_from_this(); + co_await wil::resume_foreground(_dispatcherQueue); - 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); + print_debug(L"<<< {}\n", line); - if (altPressed) + 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) { - // tmux panes don't share tab with other profile panes - if (TabIsTmuxControl(_page._GetFocusedTabImpl())) + if (til::equals(type, L"%end")) { - SplitPane(_page._GetFocusedTabImpl(), SplitDirection::Automatic); + _handleResponse(std::move(_responseBuffer)); + _responseBuffer.clear(); + _insideOutputBlock = false; } - } - else - { - _NewWindow(); - } - } - - void TmuxControl::_SendOutput(int paneId, const std::wstring& text) - { - auto search = _attachedPanes.find(paneId); - - // The pane is not ready it, put int backlog for now - if (search == _attachedPanes.end()) - { - _outputBacklog.insert_or_assign(paneId, text); - return; - } - - auto DecodeOutput = [](const std::wstring& in, std::wstring& out) { - auto it = in.begin(); - while (it != in.end()) + else if (til::equals(type, L"%error")) { - wchar_t c = *it; - if (c == L'\\') + // In theory our commands should not result in errors. + if (_state != State::Init) { - ++it; - c = 0; - for (int i = 0; i < 3 && it != in.end(); ++i, ++it) - { - if (*it < L'0' || *it > L'7') - { - c = L'?'; - break; - } - c = c * 8 + (*it - L'0'); - } - out.push_back(c); - continue; + assert(false); } - if (c == L'\n') + if (_control) { - out.push_back(L'\r'); + _responseBuffer.append(L"\r\n"); + _control.InjectTextAtCursor(_responseBuffer); } - out.push_back(c); - ++it; + 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); + } } - }; - - auto& c = search->second.control; - - if (search->second.initialized) { - std::wstring out = L""; - DecodeOutput(text, out); - c.SendOutput(out); } - else + // Otherwise, we check for the, presumably, most common output type first: %output. + else if (til::equals(type, L"%output")) { - std::wstring res(text); - c.Initialized([this, paneId, res](auto& /*i*/, auto& /*e*/) { - _SendOutput(paneId, res); - }); + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Pane) + { + _deliverOutputToPane(id.value, remaining); + } } - } - - void TmuxControl::_Output(int paneId, const std::wstring& result) - { - if (_state != ATTACHED) + else if (til::equals(type, L"%begin")) { - return; + _insideOutputBlock = true; } - - _SendOutput(paneId, result); - } - - void TmuxControl::_CloseWindow(int windowId) - { - auto search = _attachedWindows.find(windowId); - if (search == _attachedWindows.end()) + else if (til::equals(type, L"%session-changed")) { - return; + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Session) + { + _handleSessionChanged(id.value); + } } - - TerminalApp::TerminalTab t = search->second; - _attachedWindows.erase(search); - - t.Shutdown(); - - // Remove all attached panes in this window - for (auto p = _attachedPanes.begin(); p != _attachedPanes.end();) + else if (til::equals(type, L"%window-add")) { - if (p->second.windowId == windowId) + // We'll handle the initial window discovery ourselves during %session-changed. + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Window) { - p = _attachedPanes.erase(p); + _handleWindowAdd(id.value); } - else + } + else if (til::equals(type, L"%window-close")) + { + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Window) { - p++; + _handleWindowClose(id.value); } } - - _page._RemoveTab(t); - } - - void TmuxControl::_RenameWindow(int windowId, const std::wstring& name) - { - auto tab = _GetTab(windowId); - if (tab == nullptr) + else if (til::equals(type, L"%window-pane-changed")) { - return; - } - - tab.try_as()->SetTabText(winrt::hstring{ name }); - } - - void TmuxControl::_NewWindowFinalize(int windowId, int paneId, const std::wstring& windowName) - { - auto pane = _NewPane(windowId, paneId); - auto tab = _page._CreateNewTabFromPane(pane); - _attachedWindows.insert({windowId, tab}); - - tab.try_as()->CloseRequested([this, windowId](auto &&, auto &&) { - _KillWindow(windowId); - }); - - tab.try_as()->SetTabText(winrt::hstring{ windowName}); + const auto windowId = tokenize_identifier(remaining); + const auto paneId = tokenize_identifier(remaining); - // Check if we have output before we are ready - auto search = _outputBacklog.find(paneId); - if (search == _outputBacklog.end()) - { - return; + if (windowId.type == IdentifierType::Window && paneId.type == IdentifierType::Pane) + { + _handleWindowPaneChanged(windowId.value, paneId.value); + } } - - auto& result = search->second; - _SendOutput(paneId, result); - _outputBacklog.erase(search); - } - - void TmuxControl::_SplitPaneFinalize(int windowId, int newPaneId) - { - // Only handle the split pane - auto search = _attachedPanes.find(newPaneId); - if (search != _attachedPanes.end()) + else if (til::equals(type, L"%window-renamed")) { - return; + const auto id = tokenize_identifier(remaining); + if (id.type == IdentifierType::Window) + { + _handleWindowRenamed(id.value, winrt::hstring{ remaining }); + } } - - auto tab = _GetTab(windowId); - if (tab == nullptr) + else if (til::equals(type, L"%layout-change")) { - return; - } + const auto windowId = tokenize_identifier(remaining); + const auto layout = tokenize_field(remaining); - auto activePane = tab.try_as()->GetActivePane(); - if (activePane.get() != _splittingPane.first.get()) + if (windowId.type == IdentifierType::Window && !layout.empty()) + { + _handleLayoutChange(windowId.value, layout); + } + } + else if (til::equals(type, L"\033")) { - return; + _handleDetach(); } + } - auto c = activePane->GetTerminalControl(); + void TmuxControl::_handleAttach() + { + _state = State::Attaching; - int originSize; - auto direction = _splittingPane.second; - if (direction == SplitDirection::Right) + if (const auto terminalTab{ _page._GetFocusedTabImpl() }) { - originSize = c.ViewWidth(); + if (const auto pane{ terminalTab->GetActivePane() }) + { + _profile = pane->GetProfile(); + } } - else + if (!_profile) { - originSize = c.ViewHeight(); + _profile = CascadiaSettings::LoadDefaults().ProfileDefaults(); } - auto newSize = originSize / 2; + // 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(); - auto splitSize = _ComputeSplitSize(originSize - newSize, originSize, direction); - - auto newPane = _NewPane(windowId, newPaneId); - auto [origin, newGuy] = tab.try_as()->SplitPane(direction, splitSize, newPane); - - newGuy->GetTerminalControl().Focus(FocusState::Programmatic); - _splittingPane.first = nullptr; - } - - std::shared_ptr TmuxControl::_NewPane(int windowId, int paneId) - { - auto connection = TerminalConnection::DummyConnection{}; - auto controlSettings = TerminalSettings::CreateWithProfile(_page._settings, _profile, *_page._bindings); - const auto control = _page._CreateNewControlAndContent(controlSettings, connection); + // 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); - auto paneContent{ winrt::make (_profile, _page._terminalSettingsCache, control) }; - auto pane = std::make_shared(paneContent); + _profile.Padding(XamlThicknessToOptimalString(_thickness)); + _profile.ScrollState(winrt::Microsoft::Terminal::Control::ScrollbarState::Hidden); + _profile.Icon(MediaResourceHelper::FromString(L"\uF714")); + _profile.Name(L"TmuxTab"); - control.Initialized([this, paneId](auto, auto) { - auto search = _attachedPanes.find(paneId); - if (search == _attachedPanes.end()) + // 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) { - return; + _sendIgnoreResponse(L"detach\n"); } - search->second.initialized = true; + e.Handled(true); }); - connection.TerminalInput([this, paneId](auto keys) { - std::wstring out{ keys }; - _SendKey(paneId, out); - }); + _windowSizeChangedRevoker = _page.SizeChanged([this](auto, auto) { + const auto fontSize = _control.CharacterDimensions(); + const auto width = _page.ActualWidth(); + const auto height = _page.ActualHeight(); - control.GotFocus([this, windowId, paneId](auto, auto) { - if (_activePaneId == paneId) - { - return; - } + _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); - _activePaneId = paneId; - _SelectPane(_activePaneId); + _sendSetOption(fmt::format(FMT_COMPILE(L"default-size {}x{}"), _terminalWidth, _terminalHeight)); - if (_activeWindowId != windowId) + for (auto& w : _attachedWindows) { - _activeWindowId = windowId; - _SelectWindow(_activeWindowId); + _sendResizeWindow(w.first, _terminalWidth, _terminalHeight); } }); - control.SizeChanged([this, paneId, control](auto, const Xaml::SizeChangedEventArgs& args) { - if (_state != ATTACHED) - { - return; - } - // Ignore the new created - if (args.PreviousSize().Width == 0 || args.PreviousSize().Height == 0) + // 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())) { - return; + _openNewTerminalViaDropdown(); } - - auto width = (int)((args.NewSize().Width - 2 * _thickness.Left) / _fontWidth); - auto height = (int)((args.NewSize().Height - 2 * _thickness.Top) / _fontHeight); - _ResizePane(paneId, width, height); }); - pane->Closed([this, paneId](auto&&, auto&&) { - _KillPane(paneId); - }); - - _attachedPanes.insert({ paneId, {windowId, paneId, control} }); - - return pane; + _controlTab = _page._GetTabImpl(_page._GetFocusedTab()); + _control.InjectTextAtCursor(RS_(L"TmuxControlInfo")); } - bool TmuxControl::_SyncPaneState(std::vector panes, int history) + void TmuxControl::_handleDetach() { - for (auto& p : panes) - { - auto search = _attachedPanes.find(p.paneId); - if (search == _attachedPanes.end()) - { - continue; - } + // 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); - _CapturePane(p.paneId, p.cursorX, p.cursorY, history); - } + 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; - return true; - } - - bool TmuxControl::_SyncWindowState(std::vector windows) - { - for (auto& w : windows) { - auto direction = SplitDirection::Left; - std::shared_ptr rootPane{ nullptr }; - std::unordered_map> attachedPanes; - for (auto& l : w.layout) - { - int rootSize; - auto& panes = l.panes; - auto& p = panes.at(0); - switch (l.type) - { - case SINGLE_PANE: - { - rootPane = _NewPane(w.windowId, p.id); - continue; - } - case SPLIT_HORIZONTAL: - direction = SplitDirection::Left; - rootSize = p.width; - break; - case SPLIT_VERTICAL: - direction = SplitDirection::Up; - rootSize = p.height; - break; - } + _control = nullptr; + _controlTab = nullptr; + _profile = nullptr; + _state = State::Init; + _inUse = false; - auto search = attachedPanes.find(p.id); - std::shared_ptr targetPane{ nullptr }; - int targetPaneId = p.id; - if (search == attachedPanes.end()) - { - targetPane = _NewPane(w.windowId, p.id); - if (rootPane == nullptr) { - rootPane = targetPane; - } - attachedPanes.insert({p.id, targetPane}); - } - else - { - targetPane = search->second; - } + _lineBuffer.clear(); + _responseBuffer.clear(); + _insideOutputBlock = false; - for (size_t i = 1; i < panes.size(); i++) - { - // Create and attach - auto& p = panes.at(i); + _detachKeyDownRevoker = {}; + _windowSizeChangedRevoker = {}; + _newTabClickRevoker = {}; - auto pane = _NewPane(w.windowId, p.id); - attachedPanes.insert({p.id, pane}); + _commandQueue.clear(); + _attachedWindows.clear(); + _attachedPanes.clear(); - float splitSize; - if (direction == SplitDirection::Left) - { - auto paneSize = panes.at(i).width; - splitSize = _ComputeSplitSize(paneSize, rootSize, direction); - rootSize -= (paneSize + 1); - } - else - { - auto paneSize = panes.at(i).height; - splitSize = _ComputeSplitSize(paneSize, rootSize, direction); - rootSize -= (paneSize + 1); - } - targetPane = targetPane->AttachPane(pane, direction, splitSize); - attachedPanes.erase(targetPaneId); - attachedPanes.insert({targetPaneId, targetPane}); - targetPane->Closed([this, targetPaneId](auto&&, auto&&) { - _KillPane(targetPaneId); - }); - } - } - auto tab = _page._CreateNewTabFromPane(rootPane); - _attachedWindows.insert({w.windowId, tab}); - auto windowId = w.windowId; - tab.try_as()->CloseRequested([this, windowId](auto &&, auto &&) { - _KillWindow(windowId); - }); + _sessionId = -1; + _activePaneId = -1; + _activeWindowId = -1; - tab.try_as()->SetTabText(winrt::hstring{ w.name }); - _ListPanes(w.windowId, w.history); - } - return true; - } + _terminalWidth = 0; + _terminalHeight = 0; + _thickness = {}; + _fontWidth = 0; + _fontHeight = 0; - std::vector TmuxControl::_ParseTmuxWindowLayout(std::wstring& layout) - { - std::wregex RegPane { L"^,?(\\d+)x(\\d+),(\\d+),(\\d+),(\\d+)" }; - - std::wregex RegSplitHorizontalPush { L"^,?(\\d+)x(\\d+),(\\d+),(\\d+)\\{" }; - std::wregex RegSplitVerticalPush { L"^,?(\\d+)x(\\d+),(\\d+),(\\d+)\\[" }; - std::wregex RegSplitPop { L"^[\\} | \\]]" }; - std::vector result; - - auto _ExtractPane = [&](std::wsmatch& matches, TmuxPaneLayout& p) { - p.width = std::stoi(matches.str(1)); - p.height = std::stoi(matches.str(2)); - p.left = std::stoi(matches.str(3)); - p.top = std::stoi(matches.str(4)); - if (matches.size() > 5) - { - p.id = std::stoi(matches.str(5)); - } - }; + _splittingPane = {}; + } - auto _ParseNested = [&](std::wstring) { - std::wsmatch matches; - size_t parse_len = 0; - TmuxWindowLayout l; - - std::vector stack; - - while (layout.length() > 0) { - if (std::regex_search(layout, matches, RegSplitHorizontalPush)) { - TmuxPaneLayout p; - _ExtractPane(matches, p); - l.panes.push_back(p); - stack.push_back(l); - - l.type = SPLIT_HORIZONTAL; - l.panes.clear(); - l.panes.push_back(p); - } else if (std::regex_search(layout, matches, RegSplitVerticalPush)) { - TmuxPaneLayout p; - _ExtractPane(matches, p); - l.panes.push_back(p); - stack.push_back(l); - - // New one - l.type = SPLIT_VERTICAL; - l.panes.clear(); - l.panes.push_back(p); - } else if (std::regex_search(layout, matches, RegPane)) { - TmuxPaneLayout p; - _ExtractPane(matches, p); - l.panes.push_back(p); - } else if (std::regex_search(layout, matches, RegSplitPop)) { - auto id = l.panes.back().id; - l.panes.pop_back(); - l.panes.front().id = id; - result.insert(result.begin(), l); - - l = stack.back(); - l.panes.back().id = id; - stack.pop_back(); - } else { - assert(0); - } - parse_len = matches.length(0); - layout = layout.substr(parse_len); - } + // 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. - return result; - }; + for (auto& w : attachedWindows) + { + w.second->Close(); + } - // Single pane mode - std::wsmatch matches; - if (std::regex_match(layout, matches, RegPane)) { - TmuxPaneLayout p; - _ExtractPane(matches, p); + const auto tabRow = page.TabRow(); + const auto tabRowImpl = winrt::get_self(tabRow); + const auto newTabButton = tabRowImpl->NewTabButton(); + const auto newTabItems = newTabButton.Flyout().as().Items(); - TmuxWindowLayout l; - l.type = SINGLE_PANE; - l.panes.push_back(p); + control.KeyDown(detachKeyDownRevoker); + page.SizeChanged(windowSizeChangedRevoker); + newTabButton.Click(newTabClickRevoker); - result.push_back(l); - return result; + // 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++; } + } - // Nested mode - _ParseNested(layout); + void TmuxControl::_handleSessionChanged(int64_t sessionId) + { + _sessionId = sessionId; + _sendSetOption(fmt::format(FMT_COMPILE(L"default-size {}x{}"), _terminalWidth, _terminalHeight)); + _sendDiscoverWindows(_sessionId); + } - return result; + void TmuxControl::_handleWindowAdd(int64_t windowId) + { + _sendDiscoverNewWindow(windowId); } - void TmuxControl::_EventHandler(const Event& e) + void TmuxControl::_handleWindowRenamed(int64_t windowId, winrt::hstring name) { - switch(e.type) + if (const auto tab = _getTab(windowId)) { - case ATTACH: - _AttachSession(); - break; - case DETACH: - _DetachSession(); - break; - case LAYOUT_CHANGED: - _DiscoverPanes(_sessionId, e.windowId, false); - break; - case OUTPUT: - _Output(e.paneId, e.response); - break; - // Commands response - case RESPONSE: - _CommandHandler(e.response); - break; - case SESSION_CHANGED: - _sessionId = e.sessionId; - _SetOption(std::format(L"default-size {}x{}", _terminalWidth, _terminalHeight)); - _DiscoverWindows(_sessionId); - break; - case WINDOW_ADD: - _DiscoverPanes(_sessionId, e.windowId, true); - break; - case WINDOW_CLOSE: - case UNLINKED_WINDOW_CLOSE: - _CloseWindow(e.windowId); - break; - case WINDOW_PANE_CHANGED: - _SplitPaneFinalize(e.windowId, e.paneId); - break; - case WINDOW_RENAMED: - _RenameWindow(e.windowId, e.response); - break; - - default: - break; + tab->SetTabText(std::move(name)); } - - // We are done, give the command in the queue a chance to run - _ScheduleCommand(); } - void TmuxControl::_Parse(const std::wstring& line) + void TmuxControl::_handleWindowClose(int64_t windowId) { - std::wsmatch matches; + std::erase_if(_attachedPanes, [windowId](const auto& pair) { + return pair.second.windowId == windowId; + }); - // Tmux generic rules - if (std::regex_match(line, REG_BEGIN)) - { - _event.type = BEGIN; - } - else if (std::regex_match(line, REG_END)) - { - if (_state == INIT) - { - _event.type = ATTACH; - } - else - { - _event.type = RESPONSE; - } - } - else if (std::regex_match(line, REG_ERROR)) + if (const auto nh = _attachedWindows.extract(windowId)) { - // Remove the extra '\n' we added - _Print(std::wstring(_event.response.begin(), _event.response.end() - 1)); - _event.response.clear(); - _event.type = NOTHING; + nh.mapped()->Close(); } + } - // tmux specific rules - else if (std::regex_match(line, REG_DETACH)) - { - _event.type = DETACH; - } - else if (std::regex_match(line, matches, REG_LAYOUT_CHANGED)) - { - _event.windowId = std::stoi(matches.str(1)); - _event.type = LAYOUT_CHANGED; - } - else if (std::regex_match(line, matches, REG_OUTPUT)) - { - _event.paneId = std::stoi(matches.str(1)); - _event.response = matches.str(2); - _event.type = OUTPUT; - } - else if (std::regex_match(line, matches, REG_SESSION_CHANGED)) - { - _event.type = SESSION_CHANGED; - _event.sessionId = std::stoi(matches.str(1)); - } - else if (std::regex_match(line, matches, REG_WINDOW_ADD)) - { - _event.windowId = std::stoi(matches.str(1)); - _event.type = WINDOW_ADD; - } - else if (std::regex_match(line, matches, REG_WINDOW_CLOSE)) - { - _event.type = WINDOW_CLOSE; - _event.windowId = std::stoi(matches.str(1)); - } - else if (std::regex_match(line, matches, REG_WINDOW_PANE_CHANGED)) - { - _event.type = WINDOW_PANE_CHANGED; - _event.windowId = std::stoi(matches.str(1)); - _event.paneId = std::stoi(matches.str(2)); - } - else if (std::regex_match(line, matches, REG_WINDOW_RENAMED)) + void TmuxControl::_handleWindowPaneChanged(int64_t windowId, int64_t newPaneId) + { + const auto tab = _getTab(windowId); + if (!tab) { - _event.windowId = std::stoi(matches.str(1)); - _event.response = matches.str(2); - _event.type = WINDOW_RENAMED; + return; } - else if (std::regex_match(line, matches, REG_UNLINKED_WINDOW_CLOSE)) + + 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) { - _event.type = UNLINKED_WINDOW_CLOSE; - _event.windowId = std::stoi(matches.str(1)); + 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 (_event.type == BEGIN) - { - _event.response += line + L'\n'; - } - else + if (const auto it = _attachedPanes.find(newPaneId); it != _attachedPanes.end()) { - // Other events that we don't care, do nothing - _event.type = NOTHING; + control = it->second.control; } } - if (_event.type != BEGIN && _event.type != NOTHING) + if (control) { - auto& e = _event; - _dispatcherQueue.TryEnqueue([this, e]() { - _EventHandler(e); - }); - _event.response.clear(); + 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); + } } - return; + std::erase_if(_attachedPanes, [&](const auto& pair) { + return pair.second.windowId == windowId && !seen.contains(pair.first); + }); } - // From tmux to controller through the dcs. parse it per line. - bool TmuxControl::_Advance(wchar_t ch) + void TmuxControl::_handleResponse(std::wstring_view response) { - std::wstring buffer = L""; + // The first begin/end block we receive will come unprompted from tmux. + if (_state == State::Init) + { + _handleAttach(); + return; + } - switch(ch) + if (_commandQueue.empty()) { - case '\033': - buffer.push_back(ch); - break; - case '\n': - buffer = std::wstring(_dcsBuffer.begin(), _dcsBuffer.end()); - _dcsBuffer.clear(); - break; - case '\r': - break; - default: - _dcsBuffer.push_back(ch); - break; + // tmux should theoretically not send us any output blocks unprompted. + assert(false); + return; } - if (buffer.size() > 0) - { - _Parse(buffer); - } + 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); - return true; - } + auto remaining = _layoutStripHash(windowLayout); + const auto firstPane = _layoutCreateRecursive(windowId.value, remaining, TmuxLayout{}); + _newTab(windowId.value, winrt::hstring{ windowName }, firstPane); - // Commands - void TmuxControl::_AttachDone() - { - auto cmd = std::make_unique(); - _SendCommand(std::move(cmd)); - _ScheduleCommand(); - } + // 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(); - std::wstring TmuxControl::AttachDone::GetCommand() - { - return std::wstring(std::format(L"list-session\n")); + _sendCapturePane(p.second.paneId, (til::CoordType)*historyLimit); + } + } + _sendDiscoverPanes(windowId.value); + } + + _state = State::Attached; } - bool TmuxControl::AttachDone::ResultHandler(const std::wstring& /*result*/, TmuxControl& tmux) + std::shared_ptr TmuxControl::_layoutCreateRecursive(int64_t windowId, std::wstring_view& remaining, TmuxLayout parent) { - if (tmux._cmdQueue.size() > 1) + 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()) { - // Not done, requeue it, this is because capture may requeue in case the pane is not ready - tmux._AttachDone(); - } else { - tmux._state = ATTACHED; - } + const auto current = _layoutParseNextToken(remaining); + std::shared_ptr pane; - return true; - } + 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; + } - void TmuxControl::_CapturePane(int paneId, int cursorX, int cursorY, int history) - { - auto cmd = std::make_unique(); - cmd->paneId = paneId; - cmd->cursorX = cursorX; - cmd->cursorY = cursorY; - cmd->history = history; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); - } + if (!pane) + { + assert(false); + continue; + } - std::wstring TmuxControl::CapturePane::GetCommand() - { - return std::wstring(std::format(L"capture-pane -p -t %{} -e -C -S {}\n", this->paneId, this->history * -1)); - } + if (!firstPane) + { + firstPane = pane; + } + if (lastPane) + { + const auto splitSize = 1.0f - ((float)lastPaneSize / (float)layoutSize); + layoutSize -= lastPaneSize; - bool TmuxControl::CapturePane::ResultHandler(const std::wstring& result, TmuxControl& tmux) - { - // Tmux output has an extra newline - std::wstring output = result; - output.pop_back(); - // Put the cursor to right position - output += std::format(L"\033[{};{}H", this->cursorY + 1, this->cursorX + 1); - tmux._SendOutput(this->paneId, output); - return true; - } + 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); + } - void TmuxControl::_DiscoverPanes(int sessionId, int windowId, bool newWindow) - { - if (_state != ATTACHED) - { - return; + lastPane = std::move(pane); + lastPaneSize = direction == SplitDirection::Right ? current.width : current.height; + lastPaneSize += 1; // to account for tmux's separator line } - auto cmd = std::make_unique(); - cmd->sessionId = sessionId; - cmd->windowId = windowId; - cmd->newWindow = newWindow; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); + + return firstPane; } - std::wstring TmuxControl::DiscoverPanes::GetCommand() + std::wstring_view TmuxControl::_layoutStripHash(std::wstring_view str) { - if (!this->newWindow) + const auto comma = str.find(L','); + if (comma != std::wstring_view::npos) { - return std::wstring(std::format(L"list-panes -s -F '" - L"#{{pane_id}} #{{window_name}}" - L"' -t ${}\n", this->sessionId)); + return str.substr(comma + 1); } else { - return std::wstring(std::format(L"list-panes -F '" - L"#{{pane_id}} #{{window_name}}" - L"' -t @{}\n", this->windowId)); + assert(false); + return {}; } } - bool TmuxControl::DiscoverPanes::ResultHandler(const std::wstring& result, TmuxControl& tmux) + // 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) { - std::wstring line; - std::wregex REG_PANE{ L"^%(\\d+) (\\S+)$" }; + TmuxLayout layout{ .type = TmuxLayoutType::Pop }; + + if (remaining.empty()) + { + assert(false); + return layout; + } - std::wstringstream in; - in.str(result); + int64_t args[5]; + size_t arg_count = 0; + wchar_t sep = L'\0'; - std::set panes; - while (std::getline(in, line, L'\n')) + // 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) { - std::wsmatch matches; + if (remaining.empty()) + { + // Failed to collect enough args? Error. + assert(false); + return layout; + } - if (!std::regex_match(line, matches, REG_PANE)) { - continue; + // 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. } - int paneId = std::stoi(matches.str(1)); - std::wstring windowName = matches.str(2); - // New window case, just one pane - if (this->newWindow) + + 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) { - tmux._NewWindowFinalize(this->windowId, paneId, windowName); - return true; + // Not an integer? Error. + assert(false); + return layout; } - panes.insert(paneId); + + args[arg_count++] = *val; + remaining = remaining.substr(end); } - // For pane exit case - for (auto p = tmux._attachedPanes.begin(); p != tmux._attachedPanes.end();) + switch (sep) { - if (!panes.contains(p->first)) + case L'[': + case L'{': + if (arg_count != 4) { - p = tmux._attachedPanes.erase(p); - auto tab = tmux._GetTab(this->windowId); - if (tab == nullptr) - { - return true; - } - auto activePane = tab.try_as()->GetActivePane(); - activePane->Close(); - return true; + assert(false); + return layout; } - else + 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) { - p++; + 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; } - - return true; - } - - void TmuxControl::_DiscoverWindows(int sessionId) - { - auto cmd = std::make_unique(); - cmd->sessionId = sessionId; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); } - std::wstring TmuxControl::DiscoverWindows::GetCommand() + void TmuxControl::_sendDiscoverNewWindow(int64_t windowId) { - return std::wstring(std::format(L"list-windows -F '" - L"#{{window_id}}" - L"' -t ${}\n", this->sessionId)); + 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); } - bool TmuxControl::DiscoverWindows::ResultHandler(const std::wstring& result, TmuxControl& tmux) + void TmuxControl::_handleResponseDiscoverNewWindow(std::wstring_view response) { - std::wstring line; - std::wregex REG_WINDOW{ L"^@(\\d+)$" }; + print_debug(L"--> _handleResponseDiscoverNewWindow\n"); - std::wstringstream in; - in.str(result); + const auto windowId = tokenize_identifier(response); + const auto paneId = tokenize_identifier(response); + const auto windowName = response; - while (std::getline(in, line, L'\n')) + if (windowId.type == IdentifierType::Window && paneId.type == IdentifierType::Pane) { - std::wsmatch matches; - - if (!std::regex_match(line, matches, REG_WINDOW)) { - continue; - } - int windowId = std::stoi(matches.str(1)); - tmux._ResizeWindow(windowId, tmux._terminalWidth, tmux._terminalHeight); + auto pane = _newPane(windowId.value, paneId.value).second; + _newTab(windowId.value, winrt::hstring{ windowName }, std::move(pane)); } - - tmux._ListWindow(this->sessionId, -1); - return true; - } - - void TmuxControl::_KillPane(int paneId) - { - auto search = _attachedPanes.find(paneId); - if (search == _attachedPanes.end()) + else { - return; + assert(false); } - - auto cmd = std::make_unique(); - cmd->paneId = paneId; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); } - std::wstring TmuxControl::KillPane::GetCommand() + void TmuxControl::_sendCapturePane(int64_t paneId, til::CoordType history) { - return std::wstring(std::format(L"kill-pane -t %{}\n", this->paneId)); + 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::_KillWindow(int windowId) + void TmuxControl::_handleResponseCapturePane(const ResponseInfo& info, std::wstring_view response) { - auto search = _attachedWindows.find(windowId); - if (search == _attachedWindows.end()) + print_debug(L"--> _handleResponseCapturePane\n"); + + const auto p = _attachedPanes.find(info.data.capturePane.paneId); + if (p != _attachedPanes.end()) { - return; + p->second.ignoreOutput = false; + _deliverOutputToPane(info.data.capturePane.paneId, response); } - - auto cmd = std::make_unique(); - cmd->windowId = windowId; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); - } - - std::wstring TmuxControl::KillWindow::GetCommand() - { - return std::wstring(std::format(L"kill-window -t @{}\n", this->windowId)); - } - - void TmuxControl::_ListPanes(int windowId, int history) - { - auto cmd = std::make_unique(); - cmd->windowId = windowId; - cmd->history = history; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); } - std::wstring TmuxControl::ListPanes::GetCommand() + void TmuxControl::_sendDiscoverPanes(int64_t windowId) { - return std::wstring(std::format(L"list-panes -F '" - L"#{{session_id}} #{{window_id}} #{{pane_id}} " - L"#{{cursor_x}} #{{cursor_y}} " - L"#{{pane_active}}" - L"' -t @{}\n", - this->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); } - bool TmuxControl::ListPanes::ResultHandler(const std::wstring& result, TmuxControl& tmux) + void TmuxControl::_handleResponseDiscoverPanes(std::wstring_view response) { - std::wstring line; - std::wregex REG_PANE{ L"^\\$(\\d+) @(\\d+) %(\\d+) (\\d+) (\\d+) (\\d+)$" }; - std::vector panes; - - std::wstringstream in; - in.str(result); - - while (std::getline(in, line, L'\n')) + while (!response.empty()) { - std::wsmatch matches; + auto line = split_line(response); + const auto paneId = tokenize_identifier(line); + const auto cursorX = tokenize_number(line); + const auto cursorY = tokenize_number(line); - if (!std::regex_match(line, matches, REG_PANE)) + if (paneId.type == IdentifierType::Pane && cursorX && cursorY) { - continue; + 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); } - - TmuxPane p = { - .sessionId = std::stoi(matches.str(1)), - .windowId = std::stoi(matches.str(2)), - .paneId = std::stoi(matches.str(3)), - .cursorX = std::stoi(matches.str(4)), - .cursorY = std::stoi(matches.str(5)), - .active = (std::stoi(matches.str(6)) == 1) - }; - - panes.push_back(p); } - - - tmux._SyncPaneState(panes, this->history); - return true; } - void TmuxControl::_ListWindow(int sessionId, int windowId) + void TmuxControl::_sendNewWindow() { - auto cmd = std::make_unique(); - cmd->windowId = windowId; - cmd->sessionId = sessionId; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); + _sendIgnoreResponse(L"new-window\n"); } - std::wstring TmuxControl::ListWindow::GetCommand() + void TmuxControl::_sendKillWindow(int64_t windowId) { - return std::wstring(std::format(L"list-windows -F '" - L"#{{session_id}} #{{window_id}} " - L"#{{window_width}} #{{window_height}} " - L"#{{window_active}} " - L"#{{window_layout}} " - L"#{{window_name}} " - L"#{{history_limit}}" - L"' -t ${}\n", this->sessionId)); + // 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)); + } } - bool TmuxControl::ListWindow::ResultHandler(const std::wstring& result, TmuxControl& tmux) + void TmuxControl::_sendKillPane(int64_t paneId) { - std::wstring line; - std::wregex REG_WINDOW{ L"^\\$(\\d+) @(\\d+) (\\d+) (\\d+) (\\d+) ([\\da-fA-F]{4}),(\\S+) (\\S+) (\\d+)$" }; - std::vector windows; - - std::wstringstream in; - in.str(result); - - while (std::getline(in, line, L'\n')) + // Same reasoning as in _sendKillWindow as to why we check `_attachedPanes`. + if (const auto nh = _attachedPanes.extract(paneId)) { - TmuxWindow w; - std::wsmatch matches; + const auto windowId = nh.mapped().windowId; - if (!std::regex_match(line, matches, REG_WINDOW)) { - continue; + // 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; + } } - w.sessionId = std::stoi(matches.str(1)); - w.windowId = std::stoi(matches.str(2)); - w.width = std::stoi(matches.str(3)); - w.height = std::stoi(matches.str(4)); - w.active = (std::stoi(matches.str(5)) == 1); - w.layoutChecksum = matches.str(6); - w.name = matches.str(8); - w.history = std::stoi(matches.str(9)); - std::wstring layout(matches.str(7)); - w.layout = tmux._ParseTmuxWindowLayout(layout); - windows.push_back(w); - } - - tmux._SyncWindowState(windows); - tmux._AttachDone(); - return true; - } - void TmuxControl::_NewWindow() - { - auto cmd = std::make_unique(); - _SendCommand(std::move(cmd)); - _ScheduleCommand(); + // Otherwise, we kill the whole window. + _sendKillWindow(windowId); + } } - std::wstring TmuxControl::NewWindow::GetCommand() + void TmuxControl::_sendSplitPane(std::shared_ptr pane, SplitDirection direction) { - return std::wstring(L"new-window\n"); - } + if (_splittingPane.first != nullptr) + { + return; + } - void TmuxControl::_ResizePane(int paneId, int width, int height) - { - if (width == 0 || height == 0) + if (!pane) { return; } - auto cmd = std::make_unique(); - cmd->paneId = paneId; - cmd->width = width; - cmd->height = height; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); - } - std::wstring TmuxControl::ResizePane::GetCommand() - { - return std::wstring(std::format(L"resize-pane -x {} -y {} -t %{}\n", this->width, this->height, this->paneId)); - } + int64_t paneId = -1; + for (auto& p : _attachedPanes) + { + if (pane->GetTerminalControl() == p.second.control) + { + paneId = p.first; + } + } + if (paneId == -1) + { + return; + } - void TmuxControl::_ResizeWindow(int windowId, int width, int height) - { - auto cmd = std::make_unique(); - cmd->windowId = windowId; - cmd->width = width; - cmd->height = height; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); - } + _splittingPane = { pane, direction }; - std::wstring TmuxControl::ResizeWindow::GetCommand() - { - return std::wstring(std::format(L"resize-window -x {} -y {} -t @{}\n", this->width, this->height, this->windowId)); + const auto dir = direction == SplitDirection::Right ? L'h' : L'v'; + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"split-window -t %{} -{}\n"), paneId, dir)); } - void TmuxControl::_SelectPane(int paneId) + void TmuxControl::_sendSelectWindow(int64_t windowId) { - auto cmd = std::make_unique(); - cmd->paneId = paneId; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"select-window -t @{}\n"), windowId)); } - std::wstring TmuxControl::SelectPane::GetCommand() + void TmuxControl::_sendSelectPane(int64_t paneId) { - return std::wstring(std::format(L"select-pane -t %{}\n", this->paneId)); + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"select-pane -t %{}\n"), paneId)); } - void TmuxControl::_SelectWindow(int windowId) + void TmuxControl::_sendResizeWindow(int64_t windowId, til::CoordType width, til::CoordType height) { - auto cmd = std::make_unique(); - cmd->windowId = windowId; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"resize-window -t @{} -x {} -y {}\n"), windowId, width, height)); } - std::wstring TmuxControl::SelectWindow::GetCommand() + void TmuxControl::_sendResizePane(int64_t paneId, til::CoordType width, til::CoordType height) { - return std::wstring(std::format(L"select-window -t @{}\n", this->windowId)); + if (width == 0 || height == 0) + { + return; + } + + _sendIgnoreResponse(fmt::format(FMT_COMPILE(L"resize-pane -t %{} -x {} -y {}\n"), paneId, width, height)); } - void TmuxControl::_SendKey(int paneId, const std::wstring keys) + void TmuxControl::_sendSendKey(int64_t paneId, const std::wstring_view keys) { - auto cmd = std::make_unique(); - cmd->paneId = paneId; - cmd->keys = keys; + if (keys.empty()) + { + return; + } - _SendCommand(std::move(cmd)); - _ScheduleCommand(); + 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); } - std::wstring TmuxControl::SendKey::GetCommand() + void TmuxControl::_sendIgnoreResponse(wil::zwstring_view cmd) { - std::wstring out = L""; - for (auto & c : this->keys) + print_debug(L">>> {}", cmd); + + if (!_control) { - out += std::format(L"{:#x} ", c); + // 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; } - return std::wstring(std::format(L"send-key -t %{} {}\n", this->paneId, out)); + _control.RawWriteString(cmd); + _commandQueue.push_back(ResponseInfo{ + .type = ResponseInfoType::Ignore, + }); } - - void TmuxControl::_SetOption(const std::wstring& option) + void TmuxControl::_sendWithResponseInfo(wil::zwstring_view cmd, ResponseInfo info) { - auto cmd = std::make_unique(); - cmd->option = option; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); - } + print_debug(L">>> {}", cmd); - std::wstring TmuxControl::SetOption::GetCommand() - { - return std::wstring(std::format(L"set-option {}\n", this->option)); + 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::_SplitPane(std::shared_ptr pane, SplitDirection direction) + void TmuxControl::_deliverOutputToPane(int64_t paneId, const std::wstring_view text) { - if (_splittingPane.first != nullptr) + const auto search = _attachedPanes.find(paneId); + if (search == _attachedPanes.end()) { + _attachedPanes.emplace_hint( + search, + paneId, + AttachedPane{ + .paneId = paneId, + .outputBacklog = std::wstring{ text }, + }); return; } - if (!pane) + if (search->second.ignoreOutput) { return; } - int paneId = -1; - for (auto& p : _attachedPanes) + if (!search->second.initialized) { - if (pane->GetTerminalControl() == p.second.control) - { - paneId = p.first; - } + print_debug(L"--> outputBacklog {}\n", paneId); + search->second.outputBacklog.append(text); + return; } - if (paneId == -1) + std::wstring out; + auto it = text.begin(); + const auto end = text.end(); + + while (it != end) { - return; + // 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); + } } - _splittingPane = {pane, direction}; - auto cmd = std::make_unique(); - cmd->direction = direction; - cmd->paneId = paneId; - _SendCommand(std::move(cmd)); - _ScheduleCommand(); + print_debug(L"--> _deliverOutputToPane {}\n", paneId); + search->second.connection.WriteOutput(winrt_wstring_to_array_view(out)); } - std::wstring TmuxControl::SplitPane::GetCommand() + winrt::com_ptr TmuxControl::_getTab(int64_t windowId) const { - if (this->direction == SplitDirection::Right) - { - return std::wstring(std::format(L"split-window -h -t %{}\n", this->paneId)); - } - else + const auto search = _attachedWindows.find(windowId); + if (search == _attachedWindows.end()) { - return std::wstring(std::format(L"split-window -v -t %{}\n", this->paneId)); + return nullptr; } + return search->second; } - // From controller to tmux - void TmuxControl::_CommandHandler(const std::wstring& result) + void TmuxControl::_newTab(int64_t windowId, winrt::hstring name, std::shared_ptr pane) { - if (_cmdState == WAITING && _cmdQueue.size() > 0) - { - auto cmd = _cmdQueue.front().get(); - cmd->ResultHandler(result, *this); - _cmdQueue.pop_front(); - _cmdState = READY; - } + 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)); } - void TmuxControl::_SendCommand(std::unique_ptr cmd) + std::pair> TmuxControl::_newPane(int64_t windowId, int64_t paneId) { - _cmdQueue.push_back(std::move(cmd)); + 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::_ScheduleCommand() + void TmuxControl::_openNewTerminalViaDropdown() { - if (_cmdState != READY) + 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) { - return; + // tmux panes don't share tab with other profile panes + if (TabIsTmuxControl(_page._GetFocusedTabImpl())) + { + SplitPane(_page._GetFocusedTabImpl(), SplitDirection::Automatic); + } } - - if (_cmdQueue.size() > 0) + else { - _cmdState = WAITING; - - auto cmd = _cmdQueue.front().get(); - auto cmdStr = cmd->GetCommand(); - _control.RawWriteString(cmdStr); + _sendNewWindow(); } } } diff --git a/src/cascadia/TerminalApp/TmuxControl.h b/src/cascadia/TerminalApp/TmuxControl.h index 0959a921601..8c8b7315e01 100644 --- a/src/cascadia/TerminalApp/TmuxControl.h +++ b/src/cascadia/TerminalApp/TmuxControl.h @@ -3,10 +3,7 @@ #pragma once -#include -#include -#include -#include +#include #include "Pane.h" @@ -14,397 +11,173 @@ namespace winrt::TerminalApp::implementation { struct TerminalPage; - class TmuxControl + class TmuxControl : public std::enable_shared_from_this { - using StringHandler = std::function; - using PrintHandler = std::function; - using StringHandlerProducer = std::function; - using SplitDirection = winrt::Microsoft::Terminal::Settings::Model::SplitDirection; - public: TmuxControl(TerminalPage& page); - StringHandler TmuxControlHandlerProducer(const winrt::Microsoft::Terminal::Control::TermControl control, const PrintHandler print); - bool TabIsTmuxControl(const winrt::com_ptr& tab); - void SplitPane(const winrt::com_ptr& tab, SplitDirection direction); - - private: - static const std::wregex REG_BEGIN; - static const std::wregex REG_END; - static const std::wregex REG_ERROR; - - static const std::wregex REG_CLIENT_SESSION_CHANGED; - static const std::wregex REG_CLIENT_DETACHED; - static const std::wregex REG_CONFIG_ERROR; - static const std::wregex REG_CONTINUE; - static const std::wregex REG_DETACH; - static const std::wregex REG_EXIT; - static const std::wregex REG_EXTENDED_OUTPUT; - static const std::wregex REG_LAYOUT_CHANGED; - static const std::wregex REG_MESSAGE; - static const std::wregex REG_OUTPUT; - static const std::wregex REG_PANE_MODE_CHANGED; - static const std::wregex REG_PASTE_BUFFER_CHANGED; - static const std::wregex REG_PASTE_BUFFER_DELETED; - static const std::wregex REG_PAUSE; - static const std::wregex REG_SESSION_CHANGED; - static const std::wregex REG_SESSION_RENAMED; - static const std::wregex REG_SESSION_WINDOW_CHANGED; - static const std::wregex REG_SESSIONS_CHANGED; - static const std::wregex REG_SUBSCRIPTION_CHANGED; - static const std::wregex REG_UNLINKED_WINDOW_ADD; - static const std::wregex REG_UNLINKED_WINDOW_CLOSE; - static const std::wregex REG_UNLINKED_WINDOW_RENAMED; - static const std::wregex REG_WINDOW_ADD; - static const std::wregex REG_WINDOW_CLOSE; - static const std::wregex REG_WINDOW_PANE_CHANGED; - static const std::wregex REG_WINDOW_RENAMED; - - enum State : int - { - INIT, - ATTACHING, - ATTACHED, - } _state{ INIT }; - - enum CommandState : int - { - READY, - WAITING, - } _cmdState{ READY }; - - enum EventType : int - { - BEGIN, - END, - ERR, - - ATTACH, - DETACH, - CLIENT_SESSION_CHANGED, - CLIENT_DETACHED, - CONFIG_ERROR, - CONTINUE, - EXIT, - EXTENDED_OUTPUT, - LAYOUT_CHANGED, - NOTHING, - MESSAGE, - OUTPUT, - PANE_MODE_CHANGED, - PASTE_BUFFER_CHANGED, - PASTE_BUFFER_DELETED, - PAUSE, - RESPONSE, - SESSION_CHANGED, - SESSION_RENAMED, - SESSION_WINDOW_CHANGED, - SESSIONS_CHANGED, - SUBSCRIPTION_CHANGED, - UNLINKED_WINDOW_ADD, - UNLINKED_WINDOW_CLOSE, - UNLINKED_WINDOW_RENAMED, - WINDOW_ADD, - WINDOW_CLOSE, - WINDOW_PANE_CHANGED, - WINDOW_RENAMED, - }; - - struct Event - { - EventType type{ NOTHING }; - int sessionId{ -1 }; - int windowId{ -1 }; - int paneId{ -1 }; - - std::wstring response; - } _event; - - // Command structs - struct Command - { - public: - virtual std::wstring GetCommand() = 0; - virtual bool ResultHandler(const std::wstring& /*result*/, TmuxControl& /*tmux*/) { return true; }; - }; - - struct AttachDone : public Command - { - public: - std::wstring GetCommand() override; - bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; - }; - - struct CapturePane : public Command - { - public: - std::wstring GetCommand() override; - bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; - - int paneId{ -1 }; - int cursorX{ 0 }; - int cursorY{ 0 }; - int history{ 0 }; - }; - - struct DiscoverPanes : public Command { - public: - std::wstring GetCommand() override; - bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; - - int sessionId{ -1 }; - int windowId{ -1 }; - bool newWindow{ false }; - }; - - struct DiscoverWindows : public Command { - public: - std::wstring GetCommand() override; - bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; - - int sessionId{ -1 }; - }; - - struct KillPane : public Command - { - public: - std::wstring GetCommand() override; - int paneId{ -1 }; - }; - - struct KillWindow : public Command - { - public: - std::wstring GetCommand() override; + 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); - int windowId{ -1 }; - }; - - struct ListPanes : public Command - { - public: - std::wstring GetCommand() override; - bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; - - int windowId{ -1 }; - int history{ 2000 }; - }; - - struct ListWindow : public Command { - public: - std::wstring GetCommand() override; - bool ResultHandler(const std::wstring& result, TmuxControl& tmux) override; - - int windowId{ -1 }; - int sessionId{ -1 }; - }; - - struct NewWindow : public Command - { - public: - std::wstring GetCommand() override; - }; - - struct ResizePane : public Command - { - public: - std::wstring GetCommand() override; - - int width{ 0 }; - int height{ 0 }; - int paneId{ -1 }; - }; - - struct ResizeWindow : public Command - { - public: - std::wstring GetCommand() override; - int width{ 0 }; - int height{ 0 }; - int windowId{ -1 }; - }; - - struct SelectWindow : public Command - { - public: - std::wstring GetCommand() override; - - int windowId{ -1 }; - }; - - struct SelectPane : public Command - { - public: - std::wstring GetCommand() override; - - int paneId{ -1 }; - }; - - struct SendKey : public Command - { - public: - std::wstring GetCommand() override; - - int paneId{ -1 }; - std::wstring keys; - wchar_t key{ '\0' }; - }; - - struct SetOption : public Command + private: + enum class State { - public: - std::wstring GetCommand() override; - - std::wstring option; + Init, + Attaching, + Attached, }; - struct SplitPane : public Command + enum class ResponseInfoType { - public: - std::wstring GetCommand() override; - - int paneId{ -1 }; - SplitDirection direction{ SplitDirection::Left }; + Ignore, + DiscoverNewWindow, + DiscoverWindows, + CapturePane, + DiscoverPanes, }; - // Layout structs - enum TmuxLayoutType : int + struct ResponseInfo { - SINGLE_PANE, - SPLIT_HORIZONTAL, - SPLIT_VERTICAL, + ResponseInfoType type; + union + { + struct + { + int64_t paneId; + } capturePane; + } data; }; - struct TmuxPaneLayout + enum class TmuxLayoutType { - int width; - int height; - int left; - int top; - int id; + // 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 TmuxWindowLayout + struct TmuxLayout { - TmuxLayoutType type{ SINGLE_PANE }; - std::vector panes; - }; + TmuxLayoutType type = TmuxLayoutType::Pane; - struct TmuxWindow - { - int sessionId{ -1 }; - int windowId{ -1 }; - int width{ 0 }; - int height{ 0 }; - int history{ 2000 }; - bool active{ false }; - std::wstring name; - std::wstring layoutChecksum; - std::vector layout; + // 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; }; - struct TmuxPane + // 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 { - int sessionId; - int windowId; - int paneId; - int cursorX; - int cursorY; - bool active; + MoveOnlyMarker() = default; + MoveOnlyMarker(MoveOnlyMarker&&) = default; + MoveOnlyMarker& operator=(MoveOnlyMarker&&) = default; + MoveOnlyMarker(const MoveOnlyMarker&) = delete; + MoveOnlyMarker& operator=(const MoveOnlyMarker&) = delete; }; struct AttachedPane { - int windowId; - int paneId; - winrt::Microsoft::Terminal::Control::TermControl control; - bool initialized { false }; - }; - - // Private methods - void _AttachSession(); - void _DetachSession(); - void _SetupProfile(); - void _CreateNewTabMenu(); - - float _ComputeSplitSize(int newSize, int originSize, SplitDirection direction) const; - TerminalApp::TerminalTab _GetTab(int windowId) const; - - void _SendOutput(int paneId, const std::wstring& text); - void _Output(int paneId, const std::wstring& result); - void _CloseWindow(int windowId); - void _RenameWindow(int windowId, const std::wstring& name); - void _NewWindowFinalize(int windowId, int paneId, const std::wstring& windowName); - void _SplitPaneFinalize(int windowId, int paneId); - std::shared_ptr _NewPane(int windowId, int paneId); - - bool _SyncPaneState(std::vector panes, int history); - bool _SyncWindowState(std::vector windows); - std::vector _ParseTmuxWindowLayout(std::wstring& layout); - - void _EventHandler(const Event& e); - void _Parse(const std::wstring& buffer); - bool _Advance(wchar_t ch); + 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; - // Tmux command methods - void _AttachDone(); - void _CapturePane(int paneId, int cursorX, int cursorY, int history); - void _DiscoverPanes(int sessionId, int windowId, bool newWindow); - void _DiscoverWindows(int sessionId); - void _KillPane(int paneId); - void _KillWindow(int windowId); - void _ListWindow(int sessionId, int windowId); - void _ListPanes(int windowId, int history); - void _NewWindow(); - void _OpenNewTerminalViaDropdown(); - void _ResizePane(int paneId, int width, int height); - void _ResizeWindow(int windowId, int width, int height); - void _SelectPane(int paneId); - void _SelectWindow(int windowId); - void _SendKey(int paneId, const std::wstring keys); - void _SetOption(const std::wstring& option); - void _SplitPane(std::shared_ptr pane, SplitDirection direction); + 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; - void _CommandHandler(const std::wstring& result); - void _SendCommand(std::unique_ptr cmd); - void _ScheduleCommand(); - - // Private variables - TerminalPage& _page; - winrt::Microsoft::Terminal::Settings::Model::Profile _profile; - winrt::Microsoft::Terminal::Control::TermControl _control { nullptr }; - TerminalApp::TabBase _controlTab { nullptr }; - winrt::Windows::System::DispatcherQueue _dispatcherQueue{ nullptr }; + std::wstring _lineBuffer; + std::wstring _responseBuffer; + bool _insideOutputBlock = false; winrt::event_token _detachKeyDownRevoker; winrt::event_token _windowSizeChangedRevoker; winrt::event_token _newTabClickRevoker; - ::winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _newTabMenu{}; - - std::vector _dcsBuffer; - std::deque> _cmdQueue; - std::unordered_map _attachedPanes; - std::unordered_map _attachedWindows; - std::unordered_map _outputBacklog; + std::deque _commandQueue; + std::unordered_map _attachedPanes; + std::unordered_map> _attachedWindows; - int _sessionId{ -1 }; + int64_t _sessionId = -1; + int64_t _activePaneId = -1; + int64_t _activeWindowId = -1; - int _terminalWidth{ 0 }; - int _terminalHeight{ 0 }; + til::CoordType _terminalWidth = 0; + til::CoordType _terminalHeight = 0; + winrt::Windows::UI::Xaml::Thickness _thickness{ 0, 0, 0, 0 }; + float _fontWidth = 0; + float _fontHeight = 0; - float _fontWidth{ 0 }; - float _fontHeight{ 0 }; - - ::winrt::Windows::UI::Xaml::Thickness _thickness{ 0,0,0,0 }; - - std::pair, SplitDirection> _splittingPane{ nullptr, SplitDirection::Right }; - - int _activePaneId{ -1 }; - int _activeWindowId{ -1 }; - - std::function _Print; - bool _inUse { false }; - std::mutex _inUseMutex; + 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/DummyConnection.cpp b/src/cascadia/TerminalConnection/DummyConnection.cpp deleted file mode 100644 index c3126125082..00000000000 --- a/src/cascadia/TerminalConnection/DummyConnection.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "DummyConnection.h" -#include - -#include "DummyConnection.g.cpp" - -namespace winrt::Microsoft::Terminal::TerminalConnection::implementation -{ - DummyConnection::DummyConnection() noexcept = default; - - void DummyConnection::Start() noexcept - { - } - - void DummyConnection::WriteInput(const winrt::array_view buffer) - { - const auto data = winrt_array_to_wstring_view(buffer); - std::wstringstream prettyPrint; - for (const auto& wch : data) - { - prettyPrint << wch; - } - TerminalInput.raise(prettyPrint.str()); - } - - void DummyConnection::Resize(uint32_t /*rows*/, uint32_t /*columns*/) noexcept - { - } - - void DummyConnection::Close() noexcept - { - } -} 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 b780151bf4d..20cb2d133f8 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -33,8 +33,8 @@ EchoConnection.idl - - DummyConnection.idl + + TmuxConnection.idl @@ -52,12 +52,12 @@ EchoConnection.idl - - DummyConnection.idl - ConptyConnection.idl + + TmuxConnection.idl + @@ -65,8 +65,8 @@ - + diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters index 04437becc21..a5add0bbac0 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters @@ -15,28 +15,28 @@ - + - + - + 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/DummyConnection.h b/src/cascadia/TerminalConnection/TmuxConnection.h similarity index 60% rename from src/cascadia/TerminalConnection/DummyConnection.h rename to src/cascadia/TerminalConnection/TmuxConnection.h index 732ca1867a2..4b4764dbea4 100644 --- a/src/cascadia/TerminalConnection/DummyConnection.h +++ b/src/cascadia/TerminalConnection/TmuxConnection.h @@ -3,33 +3,38 @@ #pragma once -#include "DummyConnection.g.h" +#include "TmuxConnection.g.h" namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { - struct DummyConnection : DummyConnectionT + struct TmuxConnection : TmuxConnectionT { - DummyConnection() noexcept; + 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; - void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) const noexcept {}; - - winrt::guid SessionId() const noexcept { return {}; } - ConnectionState State() const noexcept { return ConnectionState::Connected; } - til::event TerminalOutput; - til::event TerminalInput; til::typed_event StateChanged; - bool _rawMode { false }; + 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(DummyConnection); + BASIC_FACTORY(TmuxConnection); } diff --git a/src/cascadia/TerminalConnection/DummyConnection.idl b/src/cascadia/TerminalConnection/TmuxConnection.idl similarity index 65% rename from src/cascadia/TerminalConnection/DummyConnection.idl rename to src/cascadia/TerminalConnection/TmuxConnection.idl index e4b70041ff7..ccb30aea734 100644 --- a/src/cascadia/TerminalConnection/DummyConnection.idl +++ b/src/cascadia/TerminalConnection/TmuxConnection.idl @@ -6,10 +6,11 @@ import "ITerminalConnection.idl"; namespace Microsoft.Terminal.TerminalConnection { [default_interface] - runtimeclass DummyConnection : ITerminalConnection + runtimeclass TmuxConnection : ITerminalConnection { - DummyConnection(); + TmuxConnection(); + + void WriteOutput(Char[] data); event TerminalOutputHandler TerminalInput; }; - } diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 0b283274ad1..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 @@ -509,17 +522,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - void ControlCore::SendOutput(const std::wstring_view wstr) - { - if (wstr.empty()) - { - return; - } - - auto lock = _terminal->LockForWriting(); - _terminal->Write(wstr); - } - bool ControlCore::SendCharEvent(const wchar_t ch, const WORD scanCode, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers) @@ -1472,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; @@ -1591,6 +1632,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation 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. @@ -2259,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()) @@ -2960,9 +3002,4 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _terminal->PreviewText(input); } - - void ControlCore::SetTmuxControlHandlerProducer(ITermDispatch::StringHandlerProducer producer) - { - _terminal->SetTmuxControlHandlerProducer(producer); - } } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 9ea5e781ae0..9c90e009d91 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -41,8 +41,6 @@ namespace ControlUnitTests class ControlInteractivityTests; }; -using Microsoft::Console::VirtualTerminal::ITermDispatch; - #define RUNTIME_SETTING(type, name, setting) \ private: \ std::optional _runtime##name{ std::nullopt }; \ @@ -126,8 +124,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::color BackgroundColor() const; void SendInput(std::wstring_view wstr); - void SendOutput(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(); @@ -269,7 +267,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ShouldShowSelectOutput(); void PreviewInput(std::wstring_view input); - void SetTmuxControlHandlerProducer(ITermDispatch::StringHandlerProducer producer); RUNTIME_SETTING(float, Opacity, _settings.Opacity()); RUNTIME_SETTING(float, FocusedOpacity, FocusedAppearance().Opacity()); @@ -301,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 @@ -355,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); @@ -469,7 +465,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation friend class ControlUnitTests::ControlInteractivityTests; bool _inUnitTests{ false }; }; - } namespace winrt::Microsoft::Terminal::Control::factory_implementation diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 57935613b4b..d0181ec50a5 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -131,8 +131,8 @@ namespace Microsoft.Terminal.Control Int16 scanCode, Microsoft.Terminal.Core.ControlKeyStates modifiers); void SendInput(String text); - void SendOutput(String text); void PasteText(String text); + void InjectTextAtCursor(String text); void SelectAll(); void ClearSelection(); Boolean ToggleBlockSelection(); @@ -188,7 +188,6 @@ namespace Microsoft.Terminal.Control Boolean ShouldShowSelectOutput(); void OpenCWD(); - void SetTmuxControlHandlerProducer(TmuxDCSHandlerProducer producer); void ClearQuickFix(); @@ -204,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/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 7f1edc800b4..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 }); @@ -902,10 +903,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation RawWriteString(wstr); } - void TermControl::SendOutput(const winrt::hstring& wstr) - { - _core.SendOutput(wstr); - } void TermControl::ClearBuffer(Control::ClearBufferType clearType) { _core.ClearBuffer(clearType); @@ -1426,11 +1423,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Likewise, run the event handlers outside of lock (they could // be reentrant) Initialized.raise(*this, nullptr); - - if (_tmuxDCSHandlerProducer) - { - _core.SetTmuxControlHandlerProducer(_tmuxDCSHandlerProducer); - } return true; } @@ -1515,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. @@ -4018,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; @@ -4095,8 +4097,4 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.ForceCursorVisible(cursorVisibility == CursorDisplayState::Shown); } } - void TermControl::SetTmuxControlHandlerProducer(winrt::Microsoft::Terminal::Control::TmuxDCSHandlerProducer producer) - { - _tmuxDCSHandlerProducer = producer; - } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index f335b983bd6..65865c3bbda 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -120,7 +120,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SelectOutput(const bool goUp); winrt::hstring CurrentWorkingDirectory() const; - void SetTmuxControlHandlerProducer(winrt::Microsoft::Terminal::Control::TmuxDCSHandlerProducer producer); #pragma endregion void ScrollViewport(int viewTop); @@ -130,7 +129,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Windows::Foundation::Size GetFontSize() const; void SendInput(const winrt::hstring& input); - void SendOutput(const winrt::hstring& input); void ClearBuffer(Control::ClearBufferType clearType); void ToggleShaderEffects(); @@ -185,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(); @@ -220,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 @@ -440,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); @@ -448,8 +449,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _SelectCommandHandler(const IInspectable& sender, const IInspectable& args); void _SelectOutputHandler(const IInspectable& sender, const IInspectable& args); - bool _displayCursorWhileBlurred() const noexcept; - winrt::Microsoft::Terminal::Control::TmuxDCSHandlerProducer _tmuxDCSHandlerProducer { nullptr }; struct Revokers { @@ -476,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 2937d0cea93..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; @@ -127,10 +128,10 @@ namespace Microsoft.Terminal.Control void ToggleShaderEffects(); void SendInput(String input); - void SendOutput(String input); 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(); @@ -167,6 +168,5 @@ namespace Microsoft.Terminal.Control void ClearQuickFix(); void Detach(); - void SetTmuxControlHandlerProducer(TmuxDCSHandlerProducer producer); } } diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 77cb4f39f36..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)); } @@ -253,12 +254,6 @@ void Terminal::SetOptionalFeatures(winrt::Microsoft::Terminal::Core::ICoreSettin engine.Dispatch().SetOptionalFeatures(features); } -void Terminal::SetTmuxControlHandlerProducer(ITermDispatch::StringHandlerProducer producer) const noexcept -{ - auto& engine = reinterpret_cast(_stateMachine->Engine()); - engine.Dispatch().SetTmuxControlHandlerProducer(producer); -} - bool Terminal::IsXtermBracketedPasteModeEnabled() const noexcept { return _systemMode.test(Mode::BracketedPaste); @@ -1045,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(); @@ -1276,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 65f1189f4f2..b64b2f1057e 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -14,7 +14,6 @@ #include "../../types/inc/Viewport.hpp" #include "../../types/inc/GlyphWidth.hpp" #include "../../cascadia/terminalcore/ITerminalInput.hpp" -#include "../../terminal/adapter/ITermDispatch.hpp" #include #include @@ -117,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; @@ -128,7 +128,6 @@ class Microsoft::Terminal::Core::Terminal final : std::wstring CurrentCommand() const; - void SetTmuxControlHandlerProducer(Microsoft::Console::VirtualTerminal::ITermDispatch::StringHandlerProducer producer) const noexcept; void SerializeMainBuffer(HANDLE handle) const; #pragma region ITerminalApi @@ -155,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 @@ -232,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; @@ -340,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.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/Profiles_Terminal.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml index 132f62e67ce..f9fc0f9a9e3 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml @@ -80,10 +80,10 @@ + SettingOverrideSource="{x:Bind Profile.AllowTmuxControlOverrideSource, Mode=OneWay}" + Visibility="{x:Bind Profile.TmuxControlEnabled}"> 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 76de6df64d0..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,7 @@ Feature_DebugModeUI Enables UI access to the debug mode setting AlwaysEnabled - + 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 b3ad6013631..887eb1be06c 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -24,9 +24,6 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch { public: using StringHandler = std::function; - using PrintHandler = std::function; - // Use this get the StringHandler, meanwhile pass the function to give app a function to print message bypass the parser - using StringHandlerProducer = std::function; enum class OptionalFeature { @@ -197,7 +194,6 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual void SetOptionalFeatures(const til::enumset features) = 0; virtual StringHandler EnterTmuxControl(const VTParameters parameters) = 0; // tmux -CC - virtual void SetTmuxControlHandlerProducer(StringHandlerProducer producer) = 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 69450be4e18..107b366cb7b 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -4763,22 +4763,9 @@ void AdaptDispatch::SetOptionalFeatures(const til::enumset feat ITermDispatch::StringHandler AdaptDispatch::EnterTmuxControl(const VTParameters parameters) { - if (parameters.size() != 1 || parameters.at(0).value() != 1000) { + if (parameters.size() != 1 || parameters.at(0).value() != 1000) + { return nullptr; } - - if (_tmuxControlHandlerProducer) { - const auto page = _pages.ActivePage(); - return _tmuxControlHandlerProducer([this, page](auto s) { - PrintString(s); - _DoLineFeed(page, true, true); - }); - } - - return nullptr; -} - -void AdaptDispatch::SetTmuxControlHandlerProducer(StringHandlerProducer producer) -{ - _tmuxControlHandlerProducer = producer; + return _api.EnterTmuxControl(); } diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index a725539d7c8..9d6555e399f 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -191,7 +191,6 @@ namespace Microsoft::Console::VirtualTerminal void SetOptionalFeatures(const til::enumset features) noexcept override; StringHandler EnterTmuxControl(const VTParameters parameters) override; // tmux -CC - void SetTmuxControlHandlerProducer(StringHandlerProducer producer) override; // tmux -CC private: enum class Mode @@ -330,7 +329,6 @@ namespace Microsoft::Console::VirtualTerminal til::enumset _modes{ Mode::PageCursorCoupling }; SgrStack _sgrStack; - StringHandlerProducer _tmuxControlHandlerProducer { nullptr }; void _SetUnderlineStyleHelper(const VTParameter option, TextAttribute& attr) noexcept; size_t _SetRgbColorsHelper(const VTParameters options, diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index f3c4734ff75..6aa8586bccb 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -181,7 +181,6 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons void SetOptionalFeatures(const til::enumset /*features*/) override{}; StringHandler EnterTmuxControl(const VTParameters /*parameters*/) override { return nullptr; }; // tmux -CC - void SetTmuxControlHandlerProducer(StringHandlerProducer /*producer*/) override{}; // 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. From 0d466b686667ef725843607c948ea442f3f31486 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 16 Dec 2025 14:02:28 +0100 Subject: [PATCH 3/4] Exclude from spell checking --- .github/actions/spelling/excludes.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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\. From 76bd54fe42779b69938c5822e7e9d09715ec63e0 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 18 Dec 2025 21:34:24 +0100 Subject: [PATCH 4/4] Partially address feedback --- .../TerminalApp/AppActionHandlers.cpp | 7 +- .../TerminalApp/TerminalAppLib.vcxproj | 14 +-- .../TerminalAppLib.vcxproj.filters | 9 +- src/cascadia/TerminalApp/TerminalPage.h | 4 +- .../TmuxConnection.cpp | 11 +-- src/cascadia/TerminalApp/TmuxConnection.h | 31 +++++++ src/cascadia/TerminalApp/TmuxControl.cpp | 91 ++++++++++--------- src/cascadia/TerminalApp/TmuxControl.h | 37 ++++---- .../TerminalConnection.vcxproj | 9 +- .../TerminalConnection.vcxproj.filters | 5 +- .../TerminalConnection/TmuxConnection.h | 40 -------- .../TerminalConnection/TmuxConnection.idl | 16 ---- src/cascadia/TerminalControl/ControlCore.idl | 4 - .../ProfileViewModel.cpp | 5 - .../TerminalSettingsEditor/ProfileViewModel.h | 6 +- .../ProfileViewModel.idl | 2 +- .../Profiles_Terminal.xaml | 2 +- 17 files changed, 125 insertions(+), 168 deletions(-) rename src/cascadia/{TerminalConnection => TerminalApp}/TmuxConnection.cpp (76%) create mode 100644 src/cascadia/TerminalApp/TmuxConnection.h delete mode 100644 src/cascadia/TerminalConnection/TmuxConnection.h delete mode 100644 src/cascadia/TerminalConnection/TmuxConnection.idl diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index a7af3e15c83..4a993874305 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -4,12 +4,13 @@ #include "pch.h" #include "App.h" -#include "TerminalPage.h" #include "ScratchpadContent.h" -#include "../WinRTUtils/inc/WtExeUtils.h" +#include "TerminalPage.h" +#include "TmuxControl.h" +#include "Utils.h" #include "../../types/inc/utils.hpp" #include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" -#include "Utils.h" +#include "../WinRTUtils/inc/WtExeUtils.h" using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::UI::Xaml; diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index 331f0010063..b08a345b522 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -173,9 +173,8 @@ TerminalPaneContent.idl - - TerminalPage.idl - + + @@ -288,10 +287,9 @@ TerminalPaneContent.idl - - TerminalPage.xaml - + + @@ -301,7 +299,6 @@ MarkdownPaneContent.xaml Code - @@ -428,7 +425,6 @@ true false - _handleResponseDiscoverWindows: new pane {} @ {:.1f}%\n", current.id, splitSize * 100); @@ -942,8 +957,8 @@ namespace winrt::TerminalApp::implementation return layout; } layout.type = sep == L'[' ? TmuxLayoutType::PushVertical : TmuxLayoutType::PushHorizontal; - layout.width = (til::CoordType)args[0]; - layout.height = (til::CoordType)args[1]; + layout.width = static_cast(args[0]); + layout.height = static_cast(args[1]); return layout; case L']': case L'}': @@ -961,8 +976,8 @@ namespace winrt::TerminalApp::implementation return layout; } layout.type = TmuxLayoutType::Pane; - layout.width = (til::CoordType)args[0]; - layout.height = (til::CoordType)args[1]; + layout.width = static_cast(args[0]); + layout.height = static_cast(args[1]); layout.id = args[4]; return layout; } @@ -1043,7 +1058,7 @@ namespace winrt::TerminalApp::implementation 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); + const auto str = fmt::format(FMT_COMPILE(L"\033[{};{}H"), static_cast(*cursorY) + 1, static_cast(*cursorX) + 1); _deliverOutputToPane(paneId.value, str); } else @@ -1208,13 +1223,7 @@ namespace winrt::TerminalApp::implementation const auto search = _attachedPanes.find(paneId); if (search == _attachedPanes.end()) { - _attachedPanes.emplace_hint( - search, - paneId, - AttachedPane{ - .paneId = paneId, - .outputBacklog = std::wstring{ text }, - }); + _attachedPanes.emplace(paneId, AttachedPane{ paneId, std::wstring{ text } }); return; } @@ -1268,7 +1277,7 @@ namespace winrt::TerminalApp::implementation } print_debug(L"--> _deliverOutputToPane {}\n", paneId); - search->second.connection.WriteOutput(winrt_wstring_to_array_view(out)); + search->second.connection->WriteOutput(winrt_wstring_to_array_view(out)); } winrt::com_ptr TmuxControl::_getTab(int64_t windowId) const @@ -1294,18 +1303,18 @@ namespace winrt::TerminalApp::implementation std::pair> TmuxControl::_newPane(int64_t windowId, int64_t paneId) { - auto& p = _attachedPanes.try_emplace(paneId, AttachedPane{}).first->second; + auto& p = _attachedPanes.try_emplace(paneId).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); + p.connection = winrt::make_self(); + 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) { + p.connection->TerminalInput([this, paneId](const winrt::array_view keys) { _sendSendKey(paneId, winrt_array_to_wstring_view(keys)); }); @@ -1350,8 +1359,8 @@ namespace winrt::TerminalApp::implementation 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); + const auto width = static_cast(lrint((args.NewSize().Width - 2 * _thickness.Left) / _fontWidth)); + const auto height = static_cast(lrint((args.NewSize().Height - 2 * _thickness.Top) / _fontHeight)); _sendResizePane(paneId, width, height); }); @@ -1361,7 +1370,7 @@ namespace winrt::TerminalApp::implementation // 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&&) { + p.connection->StateChanged([this, paneId](auto&&, auto&&) { _sendKillPane(paneId); }); diff --git a/src/cascadia/TerminalApp/TmuxControl.h b/src/cascadia/TerminalApp/TmuxControl.h index 8c8b7315e01..d4bb7063a03 100644 --- a/src/cascadia/TerminalApp/TmuxControl.h +++ b/src/cascadia/TerminalApp/TmuxControl.h @@ -3,17 +3,16 @@ #pragma once -#include - -#include "Pane.h" +class Pane; namespace winrt::TerminalApp::implementation { + struct Tab; struct TerminalPage; + struct TmuxConnection; - class TmuxControl : public std::enable_shared_from_this + struct TmuxControl : std::enable_shared_from_this { - public: TmuxControl(TerminalPage& page); bool AcquireSingleUseLock(winrt::Microsoft::Terminal::Control::TermControl control) noexcept; @@ -74,29 +73,27 @@ namespace winrt::TerminalApp::implementation 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 { + AttachedPane() = default; + AttachedPane(int64_t paneId, std::wstring outputBacklog); + ~AttachedPane(); + + // Have to redefine them because they get implicitly deleted once a destructor is defined. + AttachedPane(AttachedPane&&) = default; + AttachedPane& operator=(AttachedPane&&) = default; + + // Why would you want to copy this. + AttachedPane(const AttachedPane&) = delete; + AttachedPane& operator=(const AttachedPane&) = delete; + int64_t windowId = -1; int64_t paneId = -1; - winrt::Microsoft::Terminal::TerminalConnection::TmuxConnection connection{ nullptr }; + winrt::com_ptr 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); diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj index 20cb2d133f8..2c7d276c506 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -33,9 +33,6 @@ EchoConnection.idl - - TmuxConnection.idl - @@ -55,9 +52,6 @@ ConptyConnection.idl - - TmuxConnection.idl - @@ -66,7 +60,6 @@ - @@ -106,4 +99,4 @@ - + \ No newline at end of file diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters index a5add0bbac0..11a0227b315 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters @@ -19,7 +19,6 @@ - @@ -28,7 +27,6 @@ - @@ -36,7 +34,6 @@ - @@ -45,4 +42,4 @@ - + \ No newline at end of file diff --git a/src/cascadia/TerminalConnection/TmuxConnection.h b/src/cascadia/TerminalConnection/TmuxConnection.h deleted file mode 100644 index 4b4764dbea4..00000000000 --- a/src/cascadia/TerminalConnection/TmuxConnection.h +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index ccb30aea734..00000000000 --- a/src/cascadia/TerminalConnection/TmuxConnection.idl +++ /dev/null @@ -1,16 +0,0 @@ -// 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.idl b/src/cascadia/TerminalControl/ControlCore.idl index d0181ec50a5..fbce5d47e2d 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -67,10 +67,6 @@ 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(); diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp index f6bdfeae265..6172def8100 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp @@ -188,11 +188,6 @@ 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 d22a1c6d93b..914cdd1295c 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -104,6 +104,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _NotifyChanges(L"Icon", L"IconPath"); } + constexpr bool TmuxControlEnabled() noexcept + { + return Feature_TmuxControl::IsEnabled(); + } + // starting directory hstring CurrentStartingDirectoryPreview() const; bool UseParentProcessDirectory() const; @@ -175,7 +180,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation 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 b2beb870be6..97f4b976b11 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl @@ -114,7 +114,7 @@ namespace Microsoft.Terminal.Settings.Editor Boolean UsingBuiltInIcon { get; }; Boolean UsingEmojiIcon { get; }; Boolean UsingImageIcon { get; }; - Boolean TmuxControlEnabled; + Boolean TmuxControlEnabled { get; }; String IconPath; EnumEntry CurrentBuiltInIcon; diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml index f9fc0f9a9e3..bbe3c1d8d8a 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml @@ -83,7 +83,7 @@ ClearSettingValue="{x:Bind Profile.ClearAllowTmuxControl}" HasSettingValue="{x:Bind Profile.HasAllowTmuxControl, Mode=OneWay}" SettingOverrideSource="{x:Bind Profile.AllowTmuxControlOverrideSource, Mode=OneWay}" - Visibility="{x:Bind Profile.TmuxControlEnabled}"> + Visibility="{x:Bind Profile.TmuxControlEnabled, Mode=OneTime}">