From 0d9bc0d4337164f6bfec6a94dd8bfc2d9f69eedd Mon Sep 17 00:00:00 2001 From: Joe Xue Date: Sun, 18 May 2025 22:45:20 -0400 Subject: [PATCH] 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