From 0c0a16e10a6862b491a3726989e488353c5489fb Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 28 Apr 2026 14:32:46 -0500 Subject: [PATCH 01/15] Add support for "workspaces" based on window names This adds a new feature to the Windows Terminal: "Workspaces" Workspaces are very shamelessly inspired by Edge workspaces of the same name. {{video here}} The core idea is that when users name a window and they close that window, we will persist that Windows layout and buffers, seperately from the rest of window restoration. So a user can open a named window, open some profiles, some panes, do some stuff in it, then close it, and we will keep that state around for the next time the user opens that window name. Unnamed windows still behave the same. If you close an unnamed window, and it's not the last window, then we won't persist the state of it. To facilitate restoring named windows, we add a `openWorkspace` action. This allows us to persist the open workspace action in the window layout restoration path. So when we deserialize the list of tab layouts, and open workspace action will tell us, hey, go retrieve this known workspace from the state.json, instead of trying to serialize the window state in two places.
state.json ```jsonc "persistedWindowLayouts" : [ { "initialPosition" : "910,462", "initialSize" : { "height" : 623.20001220703125, "width" : 934.4000244140625 }, "launchMode" : "default", "tabLayout" : [ { "action" : "newTab", "commandline" : "\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\"", "profile" : "{574e775e-4f2a-5b96-ac1e-a2962a402336}", "sessionId" : "{09d3ef05-ba3d-44ed-a8f1-065d08a806a4}", "startingDirectory" : "C:\\Users\\zadji", "suppressApplicationTitle" : false, "tabTitle" : "PowerShell" }, ] }, { "tabLayout" : [ { "action" : "openWorkspace", "name" : "wsl" } ] } ], "persistedWorkspaces" : { "wsl" : { "initialPosition" : "109,502", "initialSize" : { "height" : 568, "width" : 1184.800048828125 }, "launchMode" : "default", "tabLayout" : [ { "action" : "newTab", "commandline" : "ubuntu.exe", "profile" : "{51855cb2-8cce-5362-8f54-464b92b32386}", "sessionId" : "{a7f70c49-71ec-4ac4-9f3a-4884528a3fd6}", "startingDirectory" : null, "suppressApplicationTitle" : false, "tabTitle" : "Ubuntu" }, { "action" : "renameWindow", "name" : "wsl" } ] } }, ```
As demoed in the video, we add a flyout to list the windows that the user has open, and the named workspaces that they have saved. This allows users to quickly reopen previously closed workspaces, as well as quickly rename a window, thereby adding it to the list of saved workspaces. This button can also be hidden using the theme settings. --- .../TerminalApp/AppActionHandlers.cpp | 42 ++++ src/cascadia/TerminalApp/Remoting.h | 1 + src/cascadia/TerminalApp/Remoting.idl | 1 + .../Resources/en-US/Resources.resw | 12 + src/cascadia/TerminalApp/TabManagement.cpp | 15 ++ src/cascadia/TerminalApp/TabRowControl.cpp | 15 ++ src/cascadia/TerminalApp/TabRowControl.h | 8 + src/cascadia/TerminalApp/TabRowControl.idl | 2 + src/cascadia/TerminalApp/TabRowControl.xaml | 45 +++- src/cascadia/TerminalApp/TerminalPage.cpp | 232 +++++++++++++++++- src/cascadia/TerminalApp/TerminalPage.h | 38 +++ src/cascadia/TerminalApp/TerminalPage.idl | 20 ++ src/cascadia/TerminalApp/TerminalWindow.cpp | 21 ++ src/cascadia/TerminalApp/TerminalWindow.h | 4 + src/cascadia/TerminalApp/TerminalWindow.idl | 4 + .../TerminalSettingsModel/ActionAndArgs.cpp | 2 + .../TerminalSettingsModel/ActionArgs.cpp | 10 + .../TerminalSettingsModel/ActionArgs.h | 8 + .../TerminalSettingsModel/ActionArgs.idl | 6 + .../AllShortcutActions.h | 6 +- .../ApplicationState.cpp | 117 +++++++++ .../TerminalSettingsModel/ApplicationState.h | 8 + .../ApplicationState.idl | 6 + .../TerminalSettingsModel/MTSMSettings.h | 3 +- .../Resources/en-US/Resources.resw | 7 + src/cascadia/TerminalSettingsModel/Theme.idl | 1 + .../ApplicationStateTests.cpp | 127 ++++++++++ .../SettingsModel.UnitTests.vcxproj | 2 + src/cascadia/WindowsTerminal/AppHost.cpp | 48 +++- src/cascadia/WindowsTerminal/AppHost.h | 7 + .../WindowsTerminal/WindowEmperor.cpp | 42 ++++ src/cascadia/WindowsTerminal/WindowEmperor.h | 10 + 32 files changed, 854 insertions(+), 16 deletions(-) create mode 100644 src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 66372a3e9e9..b9a4d3f15ec 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -938,6 +938,27 @@ namespace winrt::TerminalApp::implementation co_return; } + // Launch `wt -w ` so the monarch can either summon an existing + // window with that name or restore a persisted workspace. + safe_void_coroutine TerminalPage::_OpenWorkspaceWindow(const winrt::hstring name) + { + co_await winrt::resume_background(); + + const auto exePath{ GetWtExePath() }; + const auto cmdline = fmt::format(FMT_COMPILE(L"-w {}"), std::wstring_view{ name }); + + SHELLEXECUTEINFOW seInfo{ 0 }; + seInfo.cbSize = sizeof(seInfo); + seInfo.fMask = SEE_MASK_NOASYNC; + seInfo.lpVerb = L"open"; + seInfo.lpFile = exePath.c_str(); + seInfo.lpParameters = cmdline.c_str(); + seInfo.nShow = SW_SHOWNORMAL; + LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo)); + + co_return; + } + void TerminalPage::_HandleNewWindow(const IInspectable& /*sender*/, const ActionEventArgs& actionArgs) { @@ -1633,4 +1654,25 @@ namespace winrt::TerminalApp::implementation args.Handled(handled); } } + + void TerminalPage::_HandleOpenWorkspace(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + // Open (or summon) a named window. We launch a new `wt -w ` + // process which the monarch will route to the correct live window or + // restore from a persisted workspace. + if (args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + const auto name = realArgs.Name(); + if (!name.empty()) + { + _OpenWorkspaceWindow(name); + } + args.Handled(true); + } + } + } + } diff --git a/src/cascadia/TerminalApp/Remoting.h b/src/cascadia/TerminalApp/Remoting.h index 9d4a48df761..13da3681dbf 100644 --- a/src/cascadia/TerminalApp/Remoting.h +++ b/src/cascadia/TerminalApp/Remoting.h @@ -85,6 +85,7 @@ namespace winrt::TerminalApp::implementation WINRT_PROPERTY(TerminalApp::CommandlineArgs, Command, nullptr); WINRT_PROPERTY(winrt::hstring, Content); WINRT_PROPERTY(Windows::Foundation::IReference, InitialBounds); + WINRT_PROPERTY(winrt::Microsoft::Terminal::Settings::Model::WindowLayout, PersistedLayout, nullptr); }; } diff --git a/src/cascadia/TerminalApp/Remoting.idl b/src/cascadia/TerminalApp/Remoting.idl index 07fde64e503..572f3b06af3 100644 --- a/src/cascadia/TerminalApp/Remoting.idl +++ b/src/cascadia/TerminalApp/Remoting.idl @@ -51,5 +51,6 @@ namespace TerminalApp CommandlineArgs Command { get; }; String Content { get; }; Windows.Foundation.IReference InitialBounds { get; }; + Microsoft.Terminal.Settings.Model.WindowLayout PersistedLayout; }; } diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index eb1c317e18d..330feeb1aa8 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -719,6 +719,18 @@ unnamed window text used to identify when a window hasn't been assigned a name by the user + + Name this window… + Menu item text shown when the current window has no name assigned + + + Rename this window… + Menu item text shown when the current window already has a name + + + #{0} (unnamed) + {0} is the window ID number. Shown in the workspace flyout for windows that have no name assigned. + Enter a new name: diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 21e31fb6dd3..ef3d78b2a41 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -438,6 +438,21 @@ namespace winrt::TerminalApp::implementation const auto focusedTabIndex{ _GetFocusedTabIndex() }; + // If this is the last tab in a named window, persist the workspace + // layout now—before the tab is shut down—so GetWindowLayout() can + // still see its tab data. + if (_tabs.Size() == 1) + { + const auto& windowName = _WindowProperties.WindowName(); + if (!windowName.empty()) + { + if (const auto layout = GetWindowLayout()) + { + ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); + } + } + } + // Removing the tab from the collection should destroy its control and disconnect its connection, // but it doesn't always do so. The UI tree may still be holding the control and preventing its destruction. tab.Shutdown(); diff --git a/src/cascadia/TerminalApp/TabRowControl.cpp b/src/cascadia/TerminalApp/TabRowControl.cpp index aa069944282..2012336b4c6 100644 --- a/src/cascadia/TerminalApp/TabRowControl.cpp +++ b/src/cascadia/TerminalApp/TabRowControl.cpp @@ -25,6 +25,21 @@ namespace winrt::TerminalApp::implementation InitializeComponent(); } + void TabRowControl::WorkspaceName(const winrt::hstring& value) + { + if (_WorkspaceName != value) + { + _WorkspaceName = value; + PropertyChanged.raise(*this, WUX::Data::PropertyChangedEventArgs{ L"WorkspaceName" }); + + // Collapse the name text when empty so the button shows only the icon. + if (const auto textBlock = WorkspaceNameText()) + { + textBlock.Visibility(value.empty() ? WUX::Visibility::Collapsed : WUX::Visibility::Visible); + } + } + } + // Method Description: // - Bound in the Xaml editor to the [+] button. // Arguments: diff --git a/src/cascadia/TerminalApp/TabRowControl.h b/src/cascadia/TerminalApp/TabRowControl.h index d216dbef711..bf09f78bad4 100644 --- a/src/cascadia/TerminalApp/TabRowControl.h +++ b/src/cascadia/TerminalApp/TabRowControl.h @@ -19,6 +19,14 @@ namespace winrt::TerminalApp::implementation til::property_changed_event PropertyChanged; WINRT_OBSERVABLE_PROPERTY(bool, ShowElevationShield, PropertyChanged.raise, false); + WINRT_OBSERVABLE_PROPERTY(bool, ShowWindowsButton, PropertyChanged.raise, true); + + public: + winrt::hstring WorkspaceName() const noexcept { return _WorkspaceName; } + void WorkspaceName(const winrt::hstring& value); + + private: + winrt::hstring _WorkspaceName{}; }; } diff --git a/src/cascadia/TerminalApp/TabRowControl.idl b/src/cascadia/TerminalApp/TabRowControl.idl index 4dd1d37bd97..2f461f01d2c 100644 --- a/src/cascadia/TerminalApp/TabRowControl.idl +++ b/src/cascadia/TerminalApp/TabRowControl.idl @@ -9,5 +9,7 @@ namespace TerminalApp TabRowControl(); Microsoft.UI.Xaml.Controls.TabView TabView { get; }; Boolean ShowElevationShield; + Boolean ShowWindowsButton; + String WorkspaceName; } } diff --git a/src/cascadia/TerminalApp/TabRowControl.xaml b/src/cascadia/TerminalApp/TabRowControl.xaml index 11335b654ca..8604aa5e3a8 100644 --- a/src/cascadia/TerminalApp/TabRowControl.xaml +++ b/src/cascadia/TerminalApp/TabRowControl.xaml @@ -35,14 +35,43 @@ TabWidthMode="Equal"> - - + + + + + + + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index c715b0e2a11..404f5adf1d4 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -25,6 +25,8 @@ #include "TerminalSettingsCache.h" #include "LaunchPositionRequest.g.cpp" +#include "WindowListEntry.g.cpp" +#include "WindowListRequest.g.cpp" #include "RenameWindowRequestedArgs.g.cpp" #include "RequestMoveContentArgs.g.cpp" #include "TerminalPage.g.cpp" @@ -334,6 +336,20 @@ namespace winrt::TerminalApp::implementation auto tabRowImpl = winrt::get_self(_tabRow); _newTabButton = tabRowImpl->NewTabButton(); + _workspaceFlyout = tabRowImpl->WorkspaceFlyout(); + + // Set the initial workspace name from the window name. + // Use raw WindowName() so unnamed windows show no text. + _tabRow.WorkspaceName(_WindowProperties.WindowName()); + + // Rebuild the workspace flyout each time it opens so it always + // reflects the latest set of persisted workspaces. + _workspaceFlyout.Opening([weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + page->_PopulateWorkspaceFlyout(); + } + }); if (_settings.GlobalSettings().ShowTabsInTitlebar()) { @@ -443,6 +459,12 @@ namespace winrt::TerminalApp::implementation _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + // Apply the ShowWindowsButton theme setting. + if (const auto theme = _settings.GlobalSettings().CurrentTheme()) + { + _tabRow.ShowWindowsButton(theme.Window() ? theme.Window().ShowWindowsButton() : true); + } + _adjustProcessPriorityThrottled = std::make_shared>( DispatcherQueue::GetForCurrentThread(), til::throttled_func_options{ @@ -2232,14 +2254,14 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::PersistState() + WindowLayout TerminalPage::GetWindowLayout() { // This method may be called for a window even if it hasn't had a tab yet or lost all of them. // We shouldn't persist such windows. const auto tabCount = _tabs.Size(); if (_startupState != StartupState::Initialized || tabCount == 0) { - return; + return nullptr; } std::vector actions; @@ -2254,7 +2276,7 @@ namespace winrt::TerminalApp::implementation // Avoid persisting a window with zero tabs, because `BuildStartupActions` happened to return an empty vector. if (actions.empty()) { - return; + return nullptr; } // if the focused tab was not the last tab, restore that @@ -2303,7 +2325,49 @@ namespace winrt::TerminalApp::implementation RequestLaunchPosition.raise(*this, launchPosRequest); layout.InitialPosition(launchPosRequest.Position()); - ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout); + return layout; + } + + void TerminalPage::PersistState() + { + // There are two persistence mechanisms in play here: + // * PersistedWindowLayouts (vector) — consumed on next startup to + // re-open a matching set of windows. Cleared after restore. + // * PersistedWorkspaces (name-keyed map) — the full tab/buffer + // state of a named window, claimed by name on demand via + // ApplicationState::TakeWorkspace. + // + // For named windows we save the full layout into the workspace map + // and drop a lightweight `openWorkspace` stub into the generic vector, + // so the generic restore path re-opens the named window which in + // turn claims its own workspace. Unnamed windows don't have a stable + // key, so their full layout is stored directly in the vector. + if (const auto layout = GetWindowLayout()) + { + const auto& windowName = _WindowProperties.WindowName(); + if (!windowName.empty()) + { + // Persist the full layout into the workspace collection. + ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); + + // Build a minimal layout with just an openWorkspace action + // so the generic restore path re-opens this workspace by name. + std::vector actions; + ActionAndArgs action; + action.Action(ShortcutAction::OpenWorkspace); + OpenWorkspaceArgs args{ windowName }; + action.Args(args); + actions.emplace_back(std::move(action)); + + WindowLayout stub; + stub.TabLayout(winrt::single_threaded_vector(std::move(actions))); + ApplicationState::SharedInstance().AppendPersistedWindowLayout(stub); + } + else + { + ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout); + } + } } // Method Description: @@ -3934,6 +3998,12 @@ namespace winrt::TerminalApp::implementation _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + // Apply the ShowWindowsButton theme setting. + if (const auto theme = _settings.GlobalSettings().CurrentTheme()) + { + _tabRow.ShowWindowsButton(theme.Window() ? theme.Window().ShowWindowsButton() : true); + } + Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; _tabView.Background(transparent); @@ -5574,6 +5644,156 @@ namespace winrt::TerminalApp::implementation } } + // Rebuild the workspace flyout contents. Called every time the flyout opens + // so it reflects the current set of persisted workspaces. + void TerminalPage::_PopulateWorkspaceFlyout() + { + if (!_workspaceFlyout) + { + return; + } + + _workspaceFlyout.Items().Clear(); + + // --- "Name / Rename this window" --- + { + MenuFlyoutItem item{}; + item.Text(_WindowProperties.WindowName().empty() ? RS_(L"NameThisWindowMenuItem") : RS_(L"RenameThisWindowMenuItem")); + + auto iconElement = UI::IconPathConverter::IconWUX(L"\uE8AC"); // Rename glyph + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + item.Icon(iconElement); + + item.Click([weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + // Re-use the existing openWindowRenamer action. + page->_actionDispatch->DoAction(ActionAndArgs{ ShortcutAction::OpenWindowRenamer, nullptr }); + } + }); + _workspaceFlyout.Items().Append(item); + } + + // --- Gather open window info first so we can filter workspaces --- + // Ask the host (via AppHost → WindowEmperor) for all live windows. + const auto windowListReq{ winrt::make() }; + RequestWindowList.raise(*this, windowListReq); + const auto windowEntries = windowListReq.Entries(); + + // Collect the names of all currently-open windows so we can hide + // workspaces that are already live from the saved-workspaces list. + std::set openWindowNames; + if (windowEntries) + { + for (const auto& entry : windowEntries) + { + const auto& name = entry.Name(); + if (!name.empty()) + { + openWindowNames.emplace(name); + } + } + } + + // --- Saved workspaces section (only those not currently open) --- + const auto workspaces = ApplicationState::SharedInstance().AllPersistedWorkspaces(); + if (workspaces && workspaces.Size() > 0) + { + bool addedSeparator = false; + + for (const auto& pair : workspaces) + { + const auto name = pair.Key(); + + // Skip workspaces that correspond to a currently-open window. + if (openWindowNames.count(name)) + { + continue; + } + + if (!addedSeparator) + { + _workspaceFlyout.Items().Append(MenuFlyoutSeparator{}); + addedSeparator = true; + } + + MenuFlyoutItem item{}; + item.Text(name); + + auto iconElement = UI::IconPathConverter::IconWUX(L"\uE8F1"); // SwitchApps glyph + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + item.Icon(iconElement); + + item.Click([weakThis{ get_weak() }, name](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + page->_OpenWorkspaceWindow(name); + } + }); + + _workspaceFlyout.Items().Append(item); + } + } + + // --- Open windows section --- + + if (windowEntries && windowEntries.Size() > 0) + { + _workspaceFlyout.Items().Append(MenuFlyoutSeparator{}); + + const auto thisWindowId = _WindowProperties.WindowId(); + + for (const auto& entry : windowEntries) + { + const auto id = entry.Id(); + const auto& name = entry.Name(); + + // Build display text like "#1: MyWindow" or "#2: " + winrt::hstring displayText; + if (name.empty()) + { + displayText = winrt::hstring{ RS_fmt(L"WindowListUnnamedEntry", id) }; + } + else + { + displayText = winrt::hstring{ fmt::format(FMT_COMPILE(L"#{}: {}"), id, name) }; + } + + MenuFlyoutItem item{}; + item.Text(displayText); + + // Use a different glyph for the current window + if (id == thisWindowId) + { + auto iconElement = UI::IconPathConverter::IconWUX(L"\uE73E"); // CheckMark glyph + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + item.Icon(iconElement); + item.IsEnabled(false); // Can't summon yourself + } + else + { + auto iconElement = UI::IconPathConverter::IconWUX(L"\uE737"); // ChromeRestore glyph + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + item.Icon(iconElement); + + // Click handler: summon the target window by ID. + // We raise SummonWindowByIdRequested which is handled by + // AppHost to directly summon the window without creating + // a new tab (unlike _OpenWorkspaceWindow which launches + // `wt -w ` and creates a tab). + item.Click([weakThis{ get_weak() }, id](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + page->SummonWindowByIdRequested.raise(*page, winrt::make(id)); + } + }); + } + + _workspaceFlyout.Items().Append(item); + } + } + } + // Handler for our WindowProperties's PropertyChanged event. We'll use this // to pop the "Identify Window" toast when the user renames our window. void TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, const WUX::Data::PropertyChangedEventArgs& args) @@ -5583,6 +5803,10 @@ namespace winrt::TerminalApp::implementation return; } + // Keep the workspace dropdown label in sync with the window name. + // Use raw WindowName() so clearing the name hides the text. + _tabRow.WorkspaceName(_WindowProperties.WindowName()); + // DON'T display the confirmation if this is the name we were // given on startup! if (_startupState == StartupState::Initialized) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index b1d26ee69d3..da95ac205ea 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -10,8 +10,11 @@ #include "AppKeyBindings.h" #include "AppCommandlineArgs.h" #include "RenameWindowRequestedArgs.g.h" +#include "SummonWindowByIdRequestedArgs.g.h" #include "RequestMoveContentArgs.g.h" #include "LaunchPositionRequest.g.h" +#include "WindowListEntry.g.h" +#include "WindowListRequest.g.h" #include "Toast.h" #include "WindowsPackageManagerFactory.h" @@ -63,6 +66,15 @@ namespace winrt::TerminalApp::implementation _ProposedName{ name } {}; }; + struct SummonWindowByIdRequestedArgs : SummonWindowByIdRequestedArgsT + { + WINRT_PROPERTY(uint64_t, WindowId); + + public: + SummonWindowByIdRequestedArgs(uint64_t id) : + _WindowId{ id } {}; + }; + struct RequestMoveContentArgs : RequestMoveContentArgsT { WINRT_PROPERTY(winrt::hstring, Window); @@ -84,6 +96,25 @@ namespace winrt::TerminalApp::implementation til::property Position; }; + struct WindowListEntry : WindowListEntryT + { + WindowListEntry() = default; + + til::property Id; + til::property Name; + }; + + struct WindowListRequest : WindowListRequestT + { + WindowListRequest() : + _Entries{ winrt::single_threaded_vector() } {} + + winrt::Windows::Foundation::Collections::IVector Entries() const { return _Entries; } + + private: + winrt::Windows::Foundation::Collections::IVector _Entries; + }; + struct WinGetSearchParams { winrt::Microsoft::Management::Deployment::PackageMatchField Field; @@ -122,6 +153,7 @@ namespace winrt::TerminalApp::implementation safe_void_coroutine RequestQuit(); safe_void_coroutine CloseWindow(); + winrt::Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout(); void PersistState(); std::vector Panes() const; @@ -192,6 +224,7 @@ namespace winrt::TerminalApp::implementation til::typed_event IdentifyWindowsRequested; til::typed_event RenameWindowRequested; til::typed_event SummonWindowRequested; + til::typed_event SummonWindowByIdRequested; til::typed_event WindowSizeChanged; til::typed_event OpenSystemMenu; @@ -203,6 +236,7 @@ namespace winrt::TerminalApp::implementation til::typed_event RequestReceiveContent; til::typed_event RequestLaunchPosition; + til::typed_event RequestWindowList; WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr); @@ -225,6 +259,7 @@ namespace winrt::TerminalApp::implementation TerminalApp::TabRowControl _tabRow{ nullptr }; Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr }; Microsoft::UI::Xaml::Controls::SplitButton _newTabButton{ nullptr }; + Windows::UI::Xaml::Controls::MenuFlyout _workspaceFlyout{ nullptr }; winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr }; Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; @@ -324,6 +359,7 @@ namespace winrt::TerminalApp::implementation void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&); safe_void_coroutine _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs newContentArgs); + safe_void_coroutine _OpenWorkspaceWindow(const winrt::hstring name); void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); @@ -563,6 +599,7 @@ namespace winrt::TerminalApp::implementation void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection); void _PopulateQuickFixMenu(const Microsoft::Terminal::Control::TermControl& control, const Windows::UI::Xaml::Controls::MenuFlyout& sender); + void _PopulateWorkspaceFlyout(); winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex); winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender); @@ -587,4 +624,5 @@ namespace winrt::TerminalApp::implementation namespace winrt::TerminalApp::factory_implementation { BASIC_FACTORY(TerminalPage); + BASIC_FACTORY(WindowListEntry); } diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index ce15059864b..97528b35b3f 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -19,6 +19,10 @@ namespace TerminalApp { String ProposedName { get; }; }; + [default_interface] runtimeclass SummonWindowByIdRequestedArgs + { + UInt64 WindowId { get; }; + }; [default_interface] runtimeclass RequestMoveContentArgs { String Window { get; }; @@ -50,6 +54,20 @@ namespace TerminalApp Microsoft.Terminal.Settings.Model.LaunchPosition Position; } + [default_interface] runtimeclass WindowListEntry + { + WindowListEntry(); + UInt64 Id; + String Name; + } + + // Raised by TerminalPage when it needs the list of open windows. + // The handler (AppHost) fills Entries synchronously. + [default_interface] runtimeclass WindowListRequest + { + Windows.Foundation.Collections.IVector Entries { get; }; + } + [default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, Microsoft.Terminal.UI.IDirectKeyListener { TerminalPage(WindowProperties properties, ContentManager manager); @@ -93,6 +111,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler RenameWindowRequested; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler SummonWindowByIdRequested; event Windows.Foundation.TypedEventHandler WindowSizeChanged; event Windows.Foundation.TypedEventHandler OpenSystemMenu; @@ -103,5 +122,6 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestReceiveContent; event Windows.Foundation.TypedEventHandler RequestLaunchPosition; + event Windows.Foundation.TypedEventHandler RequestWindowList; } } diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index b2fa349e40c..fbdc44f3bff 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -252,6 +252,15 @@ namespace winrt::TerminalApp::implementation AppLogic::Current()->NotifyRootInitialized(); } + WindowLayout TerminalWindow::GetWindowLayout() + { + if (_root) + { + return _root->GetWindowLayout(); + } + return nullptr; + } + void TerminalWindow::PersistState() { if (_root) @@ -1097,6 +1106,11 @@ namespace winrt::TerminalApp::implementation _initialContentArgs = wil::to_vector(args); } + void TerminalWindow::SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout) + { + _cachedLayout = layout; + } + // Method Description: // - Parse the provided commandline arguments into actions, and try to // perform them immediately. @@ -1208,7 +1222,14 @@ namespace winrt::TerminalApp::implementation void TerminalWindow::WindowName(const winrt::hstring& name) { const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow(); + const auto oldName = _WindowProperties->WindowName(); _WindowProperties->WindowName(name); + // If this window had a persisted workspace under the old name, rename + // that entry too so we don't leave a stale copy behind. + if (!oldName.empty() && !name.empty() && oldName != name) + { + ApplicationState::SharedInstance().RenameWorkspace(oldName, name); + } if (!_root) { return; diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index 2f1aad5a7aa..5989e640ae8 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -71,6 +71,7 @@ namespace winrt::TerminalApp::implementation void Create(); + winrt::Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout(); void PersistState(); void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args); @@ -79,6 +80,7 @@ namespace winrt::TerminalApp::implementation int32_t SetStartupCommandline(TerminalApp::CommandlineArgs args); void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference& contentBounds); + void SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout); int32_t ExecuteCommandline(TerminalApp::CommandlineArgs args); void SetSettingsStartupArgs(const std::vector& actions); @@ -221,6 +223,7 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(SetTaskbarProgress, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, SetTaskbarProgress); FORWARDED_TYPED_EVENT(IdentifyWindowsRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IdentifyWindowsRequested); FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested); + FORWARDED_TYPED_EVENT(SummonWindowByIdRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::SummonWindowByIdRequestedArgs, _root, SummonWindowByIdRequested); FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); FORWARDED_TYPED_EVENT(ShowWindowChanged, Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs, _root, ShowWindowChanged); @@ -229,6 +232,7 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs, _root, RequestReceiveContent); FORWARDED_TYPED_EVENT(RequestLaunchPosition, Windows::Foundation::IInspectable, winrt::TerminalApp::LaunchPositionRequest, _root, RequestLaunchPosition); + FORWARDED_TYPED_EVENT(RequestWindowList, Windows::Foundation::IInspectable, winrt::TerminalApp::WindowListRequest, _root, RequestWindowList); #ifdef UNIT_TESTING friend class TerminalAppLocalTests::CommandlineTest; diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index b900522cbf2..a0062d43009 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -55,11 +55,13 @@ namespace TerminalApp Int32 SetStartupCommandline(CommandlineArgs args); void SetStartupContent(String json, Windows.Foundation.IReference bounds); + void SetPersistedLayout(Microsoft.Terminal.Settings.Model.WindowLayout layout); Int32 ExecuteCommandline(CommandlineArgs args); Boolean ShouldImmediatelyHandoffToElevated(); void HandoffToElevated(); + Microsoft.Terminal.Settings.Model.WindowLayout GetWindowLayout(); void PersistState(); Windows.UI.Xaml.UIElement GetRoot(); @@ -126,6 +128,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler SummonWindowByIdRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; event Windows.Foundation.TypedEventHandler QuitRequested; event Windows.Foundation.TypedEventHandler SystemMenuChangeRequested; @@ -137,6 +140,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestMoveContent; event Windows.Foundation.TypedEventHandler RequestReceiveContent; event Windows.Foundation.TypedEventHandler RequestLaunchPosition; + event Windows.Foundation.TypedEventHandler RequestWindowList; void AttachContent(String content, UInt32 tabIndex); void SendContentToOther(RequestReceiveContentArgs args); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 6c896ca7b43..e6246a4ed83 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -73,6 +73,8 @@ static constexpr std::string_view NewWindowKey{ "newWindow" }; static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" }; static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" }; static constexpr std::string_view RenameWindowKey{ "renameWindow" }; +static constexpr std::string_view OpenWorkspaceKey{ "openWorkspace" }; + static constexpr std::string_view OpenWindowRenamerKey{ "openWindowRenamer" }; static constexpr std::string_view DisplayWorkingDirectoryKey{ "debugTerminalCwd" }; static constexpr std::string_view SearchForTextKey{ "searchWeb" }; diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 60e9178129b..a33922f1930 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -40,6 +40,7 @@ #include "PrevTabArgs.g.cpp" #include "NextTabArgs.g.cpp" #include "RenameWindowArgs.g.cpp" +#include "OpenWorkspaceArgs.g.cpp" #include "SearchForTextArgs.g.cpp" #include "GlobalSummonArgs.g.cpp" #include "FocusPaneArgs.g.cpp" @@ -795,6 +796,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return RS_switchable_(L"ResetWindowNameCommandKey"); } + winrt::hstring OpenWorkspaceArgs::GenerateName(const winrt::WARC::ResourceContext& context) const + { + if (!Name().empty()) + { + return winrt::hstring{ RS_switchable_fmt(L"OpenWorkspaceCommandKey", Name()) }; + } + return RS_switchable_(L"OpenWorkspaceDefaultCommandKey"); + } + winrt::hstring SearchForTextArgs::GenerateName(const winrt::WARC::ResourceContext& context) const { if (QueryUrl().empty()) diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 45ba2cb144e..fc20bf65752 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -42,6 +42,7 @@ #include "PrevTabArgs.g.h" #include "NextTabArgs.g.h" #include "RenameWindowArgs.g.h" +#include "OpenWorkspaceArgs.g.h" #include "SearchForTextArgs.g.h" #include "GlobalSummonArgs.g.h" #include "FocusPaneArgs.g.h" @@ -246,6 +247,10 @@ protected: \ #define RENAME_WINDOW_ARGS(X) \ X(winrt::hstring, Name, "name", false, ArgTypeHint::None, L"") +//////////////////////////////////////////////////////////////////////////////// +#define OPEN_WORKSPACE_ARGS(X) \ + X(winrt::hstring, Name, "name", false, ArgTypeHint::None, L"") + //////////////////////////////////////////////////////////////////////////////// #define SEARCH_FOR_TEXT_ARGS(X) \ X(winrt::hstring, QueryUrl, "queryUrl", false, ArgTypeHint::None, L"") @@ -940,6 +945,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARGS_STRUCT(RenameWindowArgs, RENAME_WINDOW_ARGS); + ACTION_ARGS_STRUCT(OpenWorkspaceArgs, OPEN_WORKSPACE_ARGS); + ACTION_ARGS_STRUCT(SearchForTextArgs, SEARCH_FOR_TEXT_ARGS); struct GlobalSummonArgs : public GlobalSummonArgsT @@ -1059,6 +1066,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(SetMaximizedArgs); BASIC_FACTORY(SetColorSchemeArgs); BASIC_FACTORY(RenameWindowArgs); + BASIC_FACTORY(OpenWorkspaceArgs); BASIC_FACTORY(ExecuteCommandlineArgs); BASIC_FACTORY(CloseOtherTabsArgs); BASIC_FACTORY(CloseTabsAfterArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 67c241e239b..f2326813e22 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -420,6 +420,12 @@ namespace Microsoft.Terminal.Settings.Model String Name { get; }; }; + [default_interface] runtimeclass OpenWorkspaceArgs : IActionArgs, IActionArgsDescriptorAccess + { + OpenWorkspaceArgs(String name); + String Name { get; }; + }; + [default_interface] runtimeclass SearchForTextArgs : IActionArgs, IActionArgsDescriptorAccess { String QueryUrl { get; }; diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index b9855d9e308..6ef0e567460 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -113,7 +113,8 @@ ON_ALL_ACTIONS(OpenScratchpad) \ ON_ALL_ACTIONS(OpenAbout) \ ON_ALL_ACTIONS(QuickFix) \ - ON_ALL_ACTIONS(OpenCWD) + ON_ALL_ACTIONS(OpenCWD) \ + ON_ALL_ACTIONS(OpenWorkspace) #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \ ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize) \ @@ -158,7 +159,8 @@ ON_ALL_ACTIONS_WITH_ARGS(Suggestions) \ ON_ALL_ACTIONS_WITH_ARGS(SelectCommand) \ ON_ALL_ACTIONS_WITH_ARGS(SelectOutput) \ - ON_ALL_ACTIONS_WITH_ARGS(ColorSelection) + ON_ALL_ACTIONS_WITH_ARGS(ColorSelection) \ + ON_ALL_ACTIONS_WITH_ARGS(OpenWorkspace) // These two macros here are for actions that we only use as internal currency. // They don't need to be parsed by the settings model, or saved as actions to diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp index ed2f69d8831..3276447e039 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -20,6 +20,7 @@ static constexpr std::string_view TabLayoutKey{ "tabLayout" }; static constexpr std::string_view InitialPositionKey{ "initialPosition" }; static constexpr std::string_view InitialSizeKey{ "initialSize" }; static constexpr std::string_view LaunchModeKey{ "launchMode" }; +static constexpr std::string_view PersistedWorkspacesKey{ "persistedWorkspaces" }; namespace Microsoft::Terminal::Settings::Model::JsonUtils { @@ -276,6 +277,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + + // Manually handled because IMap has a comma that breaks the X-macro. + if (WI_IsFlagSet(parseSource, FileSource::Local)) + state->PersistedWorkspaces = JsonUtils::GetValueForKey>>(root, PersistedWorkspacesKey); } Json::Value ApplicationState::ToJson(FileSource parseSource) const noexcept @@ -298,6 +303,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + + // Manually handled because IMap has a comma that breaks the X-macro. + if (WI_IsFlagSet(parseSource, FileSource::Local)) + JsonUtils::SetValueForKey(root, PersistedWorkspacesKey, state->PersistedWorkspaces); } return root; } @@ -341,6 +350,114 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return false; } + void ApplicationState::SaveWorkspace(const hstring& name, const Model::WindowLayout& layout) + { + { + const auto state = _state.lock(); + if (!state->PersistedWorkspaces || !*state->PersistedWorkspaces) + { + state->PersistedWorkspaces = winrt::single_threaded_map(); + } + (*state->PersistedWorkspaces).Insert(name, layout); + } + _throttler(); + } + + bool ApplicationState::RemoveWorkspace(const hstring& name) + { + bool removed{ false }; + { + const auto state = _state.lock(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + auto map = *state->PersistedWorkspaces; + if (map.HasKey(name)) + { + map.Remove(name); + removed = true; + } + } + } + if (removed) + { + _throttler(); + } + return removed; + } + + // Method Description: + // - Rename a persisted workspace entry from oldName to newName. If there + // was no entry for oldName, this is a no-op. If an entry for newName + // already exists, it will be overwritten with the layout from oldName. + // Return Value: + // - true if an entry was renamed, false otherwise. + bool ApplicationState::RenameWorkspace(const hstring& oldName, const hstring& newName) + { + if (oldName == newName || oldName.empty() || newName.empty()) + { + return false; + } + + bool renamed{ false }; + { + const auto state = _state.lock(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + auto map = *state->PersistedWorkspaces; + if (map.HasKey(oldName)) + { + const auto layout = map.Lookup(oldName); + map.Insert(newName, layout); + map.Remove(oldName); + renamed = true; + } + } + } + if (renamed) + { + _throttler(); + } + return renamed; + } + + // Method Description: + // - Atomically remove and return a persisted workspace entry. This is the + // intended API for the startup path that restores a named workspace, + // because it guarantees only one caller can claim a given workspace. + // Return Value: + // - The layout that was stored under `name`, or nullptr if there was none. + Model::WindowLayout ApplicationState::TakeWorkspace(const hstring& name) + { + Model::WindowLayout result{ nullptr }; + { + const auto state = _state.lock(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + auto map = *state->PersistedWorkspaces; + if (map.HasKey(name)) + { + result = map.Lookup(name); + map.Remove(name); + } + } + } + if (result) + { + _throttler(); + } + return result; + } + + Windows::Foundation::Collections::IMapView ApplicationState::AllPersistedWorkspaces() + { + const auto state = _state.lock_shared(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + return (*state->PersistedWorkspaces).GetView(); + } + return nullptr; + } + // Generate all getter/setters #define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \ type ApplicationState::name() const noexcept \ diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index f998fc5ead4..0e3ec38ae0b 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -75,6 +75,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation bool DismissBadge(const hstring& badgeId); bool BadgeDismissed(const hstring& badgeId) const; + void SaveWorkspace(const hstring& name, const Model::WindowLayout& layout); + bool RemoveWorkspace(const hstring& name); + bool RenameWorkspace(const hstring& oldName, const hstring& newName); + Model::WindowLayout TakeWorkspace(const hstring& name); + Windows::Foundation::Collections::IMapView AllPersistedWorkspaces(); + // State getters/setters #define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \ type name() const noexcept; \ @@ -88,6 +94,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation #define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) std::optional name{ __VA_ARGS__ }; MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + // Manually declared because IMap has a comma that breaks the macro. + std::optional> PersistedWorkspaces; }; til::shared_mutex _state; std::filesystem::path _sharedPath; diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.idl b/src/cascadia/TerminalSettingsModel/ApplicationState.idl index f4e85d3a0bf..8c067ff3042 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.idl +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.idl @@ -36,6 +36,12 @@ namespace Microsoft.Terminal.Settings.Model Boolean DismissBadge(String badgeId); Boolean BadgeDismissed(String badgeId); + void SaveWorkspace(String name, WindowLayout layout); + Boolean RemoveWorkspace(String name); + Boolean RenameWorkspace(String oldName, String newName); + WindowLayout TakeWorkspace(String name); + Windows.Foundation.Collections.IMapView AllPersistedWorkspaces(); + String SettingsHash; Windows.Foundation.Collections.IVector PersistedWindowLayouts; Windows.Foundation.Collections.IVector RecentCommands; diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index b95aad938e1..4f5686d82ca 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -160,7 +160,8 @@ Author(s): X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \ X(bool, RainbowFrame, "experimental.rainbowFrame", false) \ - X(bool, UseMica, "useMica", false) + X(bool, UseMica, "useMica", false) \ + X(bool, ShowWindowsButton, "showWindowsButton", true) #define MTSM_THEME_SETTINGS_SETTINGS(X) \ X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "theme", winrt::Windows::UI::Xaml::ElementTheme::Default) diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 7e206579819..bd1542d3ccc 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -518,6 +518,13 @@ Reset window name + + Open workspace "{0}" + {0} will be replaced with the workspace name + + + Open workspace + Rename window... diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl index 9cf0806f25d..2db209b89e8 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.idl +++ b/src/cascadia/TerminalSettingsModel/Theme.idl @@ -62,6 +62,7 @@ namespace Microsoft.Terminal.Settings.Model Windows.UI.Xaml.ElementTheme RequestedTheme { get; }; Boolean UseMica { get; }; Boolean RainbowFrame { get; }; + Boolean ShowWindowsButton { get; }; ThemeColor Frame { get; }; ThemeColor UnfocusedFrame { get; }; } diff --git a/src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp b/src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp new file mode 100644 index 00000000000..cc44ed9e14c --- /dev/null +++ b/src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/ApplicationState.h" + +using namespace Microsoft::Console; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; +using namespace winrt::Microsoft::Terminal::Settings::Model; + +namespace SettingsModelUnitTests +{ + // Covers the workspace-persistence APIs added to ApplicationState: + // SaveWorkspace / RemoveWorkspace / RenameWorkspace / TakeWorkspace / + // AllPersistedWorkspaces. + // All tests operate on a throw-away ApplicationState instance pointed at + // a temp directory, so they don't touch the real user state. + class ApplicationStateTests + { + TEST_CLASS(ApplicationStateTests); + + TEST_METHOD(SaveAndLookupWorkspace); + TEST_METHOD(RemoveWorkspaceReturnsFalseWhenMissing); + TEST_METHOD(RenameWorkspaceMigratesEntry); + TEST_METHOD(RenameWorkspaceNoOpForEmptyOrEqualNames); + TEST_METHOD(RenameWorkspaceNoOpForMissingEntry); + TEST_METHOD(TakeWorkspaceRemovesAndReturns); + TEST_METHOD(TakeWorkspaceReturnsNullWhenMissing); + + private: + static std::filesystem::path _tempRoot() + { + auto root = std::filesystem::temp_directory_path() / L"WT_ApplicationStateTests"; + std::error_code ec; + std::filesystem::create_directories(root, ec); + // Best-effort clean of any leftover state.json from a prior run so + // tests see an empty starting point. + std::filesystem::remove(root / L"state.json", ec); + std::filesystem::remove(root / L"elevated-state.json", ec); + return root; + } + + static winrt::com_ptr _make() + { + return winrt::make_self(_tempRoot()); + } + + static WindowLayout _makeLayout() + { + WindowLayout layout; + layout.TabLayout(winrt::single_threaded_vector()); + return layout; + } + }; + + void ApplicationStateTests::SaveAndLookupWorkspace() + { + auto state = _make(); + const auto layout = _makeLayout(); + state->SaveWorkspace(L"win1", layout); + + const auto all = state->AllPersistedWorkspaces(); + VERIFY_IS_NOT_NULL(all); + VERIFY_IS_TRUE(all.HasKey(L"win1")); + } + + void ApplicationStateTests::RemoveWorkspaceReturnsFalseWhenMissing() + { + auto state = _make(); + VERIFY_IS_FALSE(state->RemoveWorkspace(L"does-not-exist")); + + state->SaveWorkspace(L"win1", _makeLayout()); + VERIFY_IS_TRUE(state->RemoveWorkspace(L"win1")); + VERIFY_IS_FALSE(state->RemoveWorkspace(L"win1")); + } + + void ApplicationStateTests::RenameWorkspaceMigratesEntry() + { + auto state = _make(); + state->SaveWorkspace(L"oldName", _makeLayout()); + + VERIFY_IS_TRUE(state->RenameWorkspace(L"oldName", L"newName")); + + const auto all = state->AllPersistedWorkspaces(); + VERIFY_IS_NOT_NULL(all); + VERIFY_IS_FALSE(all.HasKey(L"oldName")); + VERIFY_IS_TRUE(all.HasKey(L"newName")); + } + + void ApplicationStateTests::RenameWorkspaceNoOpForEmptyOrEqualNames() + { + auto state = _make(); + state->SaveWorkspace(L"win1", _makeLayout()); + + VERIFY_IS_FALSE(state->RenameWorkspace(L"win1", L"win1")); + VERIFY_IS_FALSE(state->RenameWorkspace(L"", L"win2")); + VERIFY_IS_FALSE(state->RenameWorkspace(L"win1", L"")); + } + + void ApplicationStateTests::RenameWorkspaceNoOpForMissingEntry() + { + auto state = _make(); + VERIFY_IS_FALSE(state->RenameWorkspace(L"missing", L"newName")); + } + + void ApplicationStateTests::TakeWorkspaceRemovesAndReturns() + { + auto state = _make(); + state->SaveWorkspace(L"win1", _makeLayout()); + + const auto taken = state->TakeWorkspace(L"win1"); + VERIFY_IS_NOT_NULL(taken); + + // Subsequent Take for the same name must return null — this is the + // atomicity guarantee the startup path relies on. + VERIFY_IS_NULL(state->TakeWorkspace(L"win1")); + } + + void ApplicationStateTests::TakeWorkspaceReturnsNullWhenMissing() + { + auto state = _make(); + VERIFY_IS_NULL(state->TakeWorkspace(L"missing")); + } +} diff --git a/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj b/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj index 4b488c2dca8..b79ff6c068a 100644 --- a/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj +++ b/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj @@ -45,6 +45,8 @@ + + Create diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 522e90c7d28..543a1e9c5a1 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -132,7 +132,12 @@ void AppHost::_HandleCommandlineArgs(const winrt::TerminalApp::WindowRequestedAr // We don't have XAML yet, but we do have other stuff. _windowLogic = _appLogic.CreateNewWindow(); - if (const auto content = windowArgs.Content(); !content.empty()) + if (const auto layout = windowArgs.PersistedLayout()) + { + _windowLogic.SetPersistedLayout(layout); + _launchShowWindowCommand = SW_NORMAL; + } + else if (const auto content = windowArgs.Content(); !content.empty()) { _windowLogic.SetStartupContent(content, windowArgs.InitialBounds()); _launchShowWindowCommand = SW_NORMAL; @@ -265,11 +270,13 @@ void AppHost::Initialize() _revokers.IsQuakeWindowChanged = _windowLogic.IsQuakeWindowChanged(winrt::auto_revoke, { this, &AppHost::_IsQuakeWindowChanged }); _revokers.SummonWindowRequested = _windowLogic.SummonWindowRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowRequested }); + _revokers.SummonWindowByIdRequested = _windowLogic.SummonWindowByIdRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowByIdRequested }); _revokers.OpenSystemMenu = _windowLogic.OpenSystemMenu(winrt::auto_revoke, { this, &AppHost::_OpenSystemMenu }); _revokers.QuitRequested = _windowLogic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll }); _revokers.ShowWindowChanged = _windowLogic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged }); _revokers.RequestMoveContent = _windowLogic.RequestMoveContent(winrt::auto_revoke, { this, &AppHost::_handleMoveContent }); _revokers.RequestReceiveContent = _windowLogic.RequestReceiveContent(winrt::auto_revoke, { this, &AppHost::_handleReceiveContent }); + _revokers.RequestWindowList = _windowLogic.RequestWindowList(winrt::auto_revoke, { this, &AppHost::_HandleRequestWindowList }); // BODGY // On certain builds of Windows, when Terminal is set as the default @@ -409,6 +416,28 @@ void AppHost::_HandleRequestLaunchPosition(const winrt::Windows::Foundation::IIn args.Position(_GetWindowLaunchPosition()); } +void AppHost::_HandleRequestWindowList(const winrt::Windows::Foundation::IInspectable& /*sender*/, + winrt::TerminalApp::WindowListRequest args) +{ + // Ask the Emperor (on the main thread) for the current window list. + // SendMessage blocks until the message is processed, so this is + // synchronous and the results vector is filled in-place. + std::vector entries; + SendMessage(_windowManager->GetMainWindow(), + WindowEmperor::WM_GET_WINDOW_LIST, + 0, + reinterpret_cast(&entries)); + + auto windowEntries = args.Entries(); + for (const auto& entry : entries) + { + winrt::TerminalApp::WindowListEntry wle; + wle.Id(entry.Id); + wle.Name(winrt::hstring{ entry.Name }); + windowEntries.Append(wle); + } +} + LaunchPosition AppHost::_GetWindowLaunchPosition() { LaunchPosition pos{}; @@ -1064,6 +1093,23 @@ void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspecta HandleSummon(std::move(summonArgs)); } +void AppHost::_SummonWindowByIdRequested(const winrt::Windows::Foundation::IInspectable&, + const winrt::TerminalApp::SummonWindowByIdRequestedArgs& args) +{ + // Summon the window by its ID without creating a new tab. + // We look up the target window in WindowEmperor and call HandleSummon directly. + const auto targetId = args.WindowId(); + if (auto* targetWindow = _windowManager->GetWindowById(targetId)) + { + winrt::TerminalApp::SummonWindowBehavior summonBehavior; + summonBehavior.MoveToCurrentDesktop(false); + summonBehavior.DropdownDuration(0); + summonBehavior.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace); + summonBehavior.ToggleVisibility(false); // Do not toggle, just make visible. + targetWindow->HandleSummon(std::move(summonBehavior)); + } +} + void AppHost::_OpenSystemMenu(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 379f876b943..b39da7756d9 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -91,6 +91,9 @@ class AppHost : public std::enable_shared_from_this void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); + void _SummonWindowByIdRequested(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::TerminalApp::SummonWindowByIdRequestedArgs& args); + void _OpenSystemMenu(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); @@ -130,6 +133,8 @@ class AppHost : public std::enable_shared_from_this void _AppTitleChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&); void _HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& sender, winrt::TerminalApp::LaunchPositionRequest args); + void _HandleRequestWindowList(const winrt::Windows::Foundation::IInspectable& sender, + winrt::TerminalApp::WindowListRequest args); // Helper struct. By putting these all into one struct, we can revoke them // all at once, by assigning _revokers to a fresh Revokers instance. That'll @@ -151,12 +156,14 @@ class AppHost : public std::enable_shared_from_this winrt::TerminalApp::TerminalWindow::IdentifyWindowsRequested_revoker IdentifyWindowsRequested; winrt::TerminalApp::TerminalWindow::IsQuakeWindowChanged_revoker IsQuakeWindowChanged; winrt::TerminalApp::TerminalWindow::SummonWindowRequested_revoker SummonWindowRequested; + winrt::TerminalApp::TerminalWindow::SummonWindowByIdRequested_revoker SummonWindowByIdRequested; winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu; winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested; winrt::TerminalApp::TerminalWindow::ShowWindowChanged_revoker ShowWindowChanged; winrt::TerminalApp::TerminalWindow::RequestMoveContent_revoker RequestMoveContent; winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent; winrt::TerminalApp::TerminalWindow::RequestLaunchPosition_revoker RequestLaunchPosition; + winrt::TerminalApp::TerminalWindow::RequestWindowList_revoker RequestWindowList; winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged; winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged; winrt::TerminalApp::TerminalWindow::WindowSizeChanged_revoker WindowSizeChanged; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 55244118c9d..7856439d711 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -678,6 +678,19 @@ void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs arg { winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) }; request.WindowName(std::move(windowName)); + + // If we're opening a named window that doesn't exist yet, atomically + // claim any persisted workspace with that name so we restore it here + // and no subsequent window can pick up the same entry. + const auto& reqName = request.WindowName(); + if (!reqName.empty()) + { + if (const auto layout = ApplicationState::SharedInstance().TakeWorkspace(reqName)) + { + request.PersistedLayout(layout); + } + } + CreateNewWindow(std::move(request)); } } @@ -943,6 +956,22 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c // anyway (since we threw and exited this message handler) so this at least gives back our // deterministic window count management. const auto strong = *it; + + // Before destroying a named window, persist its full + // tab/buffer state as a workspace so it can be restored later. + try + { + const auto windowName = strong->Logic().WindowProperties().WindowName(); + if (!windowName.empty()) + { + if (const auto layout = strong->Logic().GetWindowLayout()) + { + ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); + } + } + } + CATCH_LOG(); + _windows.erase(it); try { @@ -970,6 +999,19 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c host->Logic().IdentifyWindow(); } return 0; + case WM_GET_WINDOW_LIST: + { + auto* result = reinterpret_cast*>(lParam); + if (result) + { + for (const auto& host : _windows) + { + const auto props = host->Logic().WindowProperties(); + result->emplace_back(WindowListEntry{ props.WindowId(), std::wstring{ props.WindowName() } }); + } + } + return 0; + } case WM_NOTIFY_FROM_NOTIFICATION_AREA: switch (LOWORD(lParam)) { diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 5e2801276c5..4a868aaa83b 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -28,6 +28,16 @@ class WindowEmperor WM_MESSAGE_BOX_CLOSED, WM_IDENTIFY_ALL_WINDOWS, WM_NOTIFY_FROM_NOTIFICATION_AREA, + WM_GET_WINDOW_LIST, + }; + + // Used by WM_GET_WINDOW_LIST. Callers allocate a vector on their + // stack and pass a pointer through LPARAM; the emperor fills it in + // synchronously via SendMessage. + struct WindowListEntry + { + uint64_t Id; + std::wstring Name; }; HWND GetMainWindow() const noexcept; From 7dd7783affac18f233132a2a6ecb4dd886307f90 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 5 May 2026 11:33:39 -0500 Subject: [PATCH 02/15] wle --- src/cascadia/WindowsTerminal/AppHost.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index f884bfaf5fc..e3474ecdbc9 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -432,10 +432,10 @@ void AppHost::_HandleRequestWindowList(const winrt::Windows::Foundation::IInspec auto windowEntries = args.Entries(); for (const auto& entry : entries) { - winrt::TerminalApp::WindowListEntry wle; - wle.Id(entry.Id); - wle.Name(winrt::hstring{ entry.Name }); - windowEntries.Append(wle); + winrt::TerminalApp::WindowListEntry w; + w.Id(entry.Id); + w.Name(winrt::hstring{ entry.Name }); + windowEntries.Append(w); } } From d5d4f99673d95039bd41976e4a9f65c9ba4ad0f1 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 5 May 2026 13:33:12 -0500 Subject: [PATCH 03/15] fix ci --- src/cascadia/TerminalSettingsModel/ApplicationState.h | 2 +- .../UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj | 1 - src/cascadia/UnitTests_SettingsModel/pch.h | 2 ++ src/inc/til/mutex.h | 2 ++ 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index 0e3ec38ae0b..5139b5f3438 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -16,7 +16,7 @@ Module Name: #include "WindowLayout.g.h" #include -#include +#include "JsonUtils.h" namespace winrt::Microsoft::Terminal::Settings::Model::implementation { diff --git a/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj b/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj index b79ff6c068a..8249f7ca2aa 100644 --- a/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj +++ b/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj @@ -45,7 +45,6 @@ - diff --git a/src/cascadia/UnitTests_SettingsModel/pch.h b/src/cascadia/UnitTests_SettingsModel/pch.h index 4cad433c611..4066fff8dbb 100644 --- a/src/cascadia/UnitTests_SettingsModel/pch.h +++ b/src/cascadia/UnitTests_SettingsModel/pch.h @@ -66,6 +66,8 @@ Author(s): // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" #include +#include +#include // Common includes for most tests: #include "../../inc/conattrs.hpp" diff --git a/src/inc/til/mutex.h b/src/inc/til/mutex.h index e4299d31cfc..b1fd20ab54e 100644 --- a/src/inc/til/mutex.h +++ b/src/inc/til/mutex.h @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +#pragma once + namespace til { namespace details From 00aa70782513a7d3ea1074d1032c2ea477cf1418 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 6 May 2026 10:32:11 -0500 Subject: [PATCH 04/15] don't crash when closing the last pane in a workspace --- src/cascadia/TerminalApp/Pane.cpp | 4 ++ src/cascadia/TerminalApp/TabManagement.cpp | 57 ++++++++++++++++------ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 85354101b68..43849b21d22 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -92,6 +92,10 @@ INewContentArgs Pane::GetTerminalArgsForPane(BuildStartupKind kind) const { // Leaves are the only things that have controls assert(_IsLeaf()); + if (!_content) + { + return nullptr; + } return _content.GetNewTerminalArgs(kind); } diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 4f032bd978b..13d331944c4 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -450,6 +450,21 @@ namespace winrt::TerminalApp::implementation auto actions = t->BuildStartupActions(BuildStartupKind::None); _AddPreviouslyClosedPaneOrTab(std::move(actions)); + // If this is the last tab in a named window, persist the workspace + // layout now while tab content is still alive. After tab.Close() + // the pane content will be torn down by the time _RemoveTab runs. + if (_tabs.Size() == 1) + { + const auto& windowName = _WindowProperties.WindowName(); + if (!windowName.empty()) + { + if (const auto layout = GetWindowLayout()) + { + ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); + } + } + } + tab.Close(); } @@ -471,20 +486,12 @@ namespace winrt::TerminalApp::implementation const auto focusedTabIndex{ _GetFocusedTabIndex() }; - // If this is the last tab in a named window, persist the workspace - // layout now—before the tab is shut down—so GetWindowLayout() can - // still see its tab data. - if (_tabs.Size() == 1) - { - const auto& windowName = _WindowProperties.WindowName(); - if (!windowName.empty()) - { - if (const auto layout = GetWindowLayout()) - { - ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); - } - } - } + // NOTE: Workspace persistence for named windows used to live here, + // but by the time _RemoveTab runs the pane content may already be + // torn down (e.g. from the close-pane path). Instead, workspace + // saves are handled earlier: + // - Close-pane (last pane): in _HandleClosePaneRequested + // - Close-tab: in _HandleCloseTabRequested // Removing the tab from the collection should destroy its control and disconnect its connection, // but it doesn't always do so. The UI tree may still be holding the control and preventing its destruction. @@ -813,6 +820,28 @@ namespace winrt::TerminalApp::implementation } _AddPreviouslyClosedPaneOrTab(std::move(state.args)); + // If this is the last pane on the last tab of a named window, persist + // the workspace layout now while the pane content is still alive. + // We can't wait until _RemoveTab, because pane->Close() below will + // destroy the content before _RemoveTab is reached. + if (_tabs.Size() == 1) + { + if (const auto activeTab{ _GetFocusedTabImpl() }) + { + if (activeTab->GetLeafPaneCount() == 1) + { + const auto& windowName = _WindowProperties.WindowName(); + if (!windowName.empty()) + { + if (const auto layout = GetWindowLayout()) + { + ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); + } + } + } + } + } + // If specified, detach before closing to directly update the pane structure pane->Close(); } From 79d681e39479cae2403a886af238719b1238f9ec Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 6 May 2026 10:32:34 -0500 Subject: [PATCH 05/15] focus the window renamer better --- src/cascadia/TerminalApp/AppActionHandlers.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 7224472fa2b..77374747605 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -1079,6 +1079,7 @@ namespace winrt::TerminalApp::implementation // Fun! // WindowRenamerTextBox().Focus(FocusState::Programmatic); _renamerLayoutUpdatedRevoker.revoke(); + _renamerLayoutCount = 0; _renamerLayoutUpdatedRevoker = WindowRenamerTextBox().LayoutUpdated(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) { if (auto self{ weakThis.get() }) { From b9896f51e483ea4fde578cca0f41612daf40b22b Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 7 May 2026 12:55:16 -0500 Subject: [PATCH 06/15] delete workspaces ; better padding --- .../Resources/en-US/Resources.resw | 20 ++++++ src/cascadia/TerminalApp/TabRowControl.cpp | 6 -- src/cascadia/TerminalApp/TabRowControl.xaml | 13 ++-- src/cascadia/TerminalApp/TerminalPage.cpp | 69 +++++++++++++++---- 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 9679d7e6db3..21944f4f7c6 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -755,6 +755,26 @@ #{0} (unnamed) {0} is the window ID number. Shown in the workspace flyout for windows that have no name assigned. + + Delete workspace? + Menu item text shown in the right-click context menu on a saved workspace + + + Open workspace + Menu item text for opening a saved workspace + + + Are you sure you want to delete the workspace "{0}"? + {0} is the workspace name. Shown in a confirmation dialog when the user tries to delete a saved workspace. + + + Delete + Primary button text on the delete workspace confirmation dialog + + + Cancel + Cancel button text on the delete workspace confirmation dialog + Enter a new name: diff --git a/src/cascadia/TerminalApp/TabRowControl.cpp b/src/cascadia/TerminalApp/TabRowControl.cpp index 2012336b4c6..0778bb0de03 100644 --- a/src/cascadia/TerminalApp/TabRowControl.cpp +++ b/src/cascadia/TerminalApp/TabRowControl.cpp @@ -31,12 +31,6 @@ namespace winrt::TerminalApp::implementation { _WorkspaceName = value; PropertyChanged.raise(*this, WUX::Data::PropertyChangedEventArgs{ L"WorkspaceName" }); - - // Collapse the name text when empty so the button shows only the icon. - if (const auto textBlock = WorkspaceNameText()) - { - textBlock.Visibility(value.empty() ? WUX::Visibility::Collapsed : WUX::Visibility::Visible); - } } } diff --git a/src/cascadia/TerminalApp/TabRowControl.xaml b/src/cascadia/TerminalApp/TabRowControl.xaml index 8604aa5e3a8..e27f2e45cb3 100644 --- a/src/cascadia/TerminalApp/TabRowControl.xaml +++ b/src/cascadia/TerminalApp/TabRowControl.xaml @@ -8,6 +8,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:TerminalApp" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:mtu="using:Microsoft.Terminal.UI" + xmlns:mux="using:Microsoft.UI.Xaml.Controls" Background="{ThemeResource TabViewBackground}" mc:Ignorable="d"> @@ -47,15 +49,15 @@