From 1282d56a33bb15206fcb3a09d3bea0a33036f2cd Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 16 Jun 2026 16:59:17 -0500 Subject: [PATCH] Add support for "workspaces" based on window names (redux) (#20314) This adds a new feature to the Windows Terminal: "Workspaces" Workspaces are very shamelessly inspired by Edge workspaces of the same name. 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. 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. Closes #17084 (this is a reboot of #20162, but with the event from #20311) --- doc/cascadia/profiles.schema.json | 32 ++ .../TerminalApp/AppActionHandlers.cpp | 47 ++- src/cascadia/TerminalApp/Pane.cpp | 4 + src/cascadia/TerminalApp/Remoting.h | 1 + src/cascadia/TerminalApp/Remoting.idl | 1 + .../Resources/en-US/Resources.resw | 39 +++ src/cascadia/TerminalApp/TabManagement.cpp | 46 +++ src/cascadia/TerminalApp/TabRowControl.h | 2 + src/cascadia/TerminalApp/TabRowControl.idl | 2 + src/cascadia/TerminalApp/TabRowControl.xaml | 48 ++- src/cascadia/TerminalApp/TerminalPage.cpp | 274 +++++++++++++++++- src/cascadia/TerminalApp/TerminalPage.h | 51 ++++ src/cascadia/TerminalApp/TerminalPage.idl | 29 ++ src/cascadia/TerminalApp/TerminalWindow.cpp | 21 ++ src/cascadia/TerminalApp/TerminalWindow.h | 5 + src/cascadia/TerminalApp/TerminalWindow.idl | 5 + .../TerminalSettingsModel/ActionAndArgs.cpp | 3 + .../TerminalSettingsModel/ActionArgs.cpp | 10 + .../TerminalSettingsModel/ActionArgs.h | 8 + .../TerminalSettingsModel/ActionArgs.idl | 6 + .../TerminalSettingsModel/ActionMap.cpp | 4 + .../AllShortcutActions.h | 7 +- .../ApplicationState.cpp | 121 ++++++++ .../TerminalSettingsModel/ApplicationState.h | 13 +- .../ApplicationState.idl | 6 + .../TerminalSettingsModel/MTSMSettings.h | 3 +- .../Resources/en-US/Resources.resw | 10 + src/cascadia/TerminalSettingsModel/Theme.idl | 1 + .../TerminalSettingsModel/defaults.json | 1 + .../ApplicationStateTests.cpp | 138 +++++++++ .../SettingsModel.UnitTests.vcxproj | 1 + src/cascadia/UnitTests_SettingsModel/pch.h | 2 + src/cascadia/WindowsTerminal/AppHost.cpp | 61 +++- src/cascadia/WindowsTerminal/AppHost.h | 11 + .../WindowsTerminal/WindowEmperor.cpp | 87 +++++- src/cascadia/WindowsTerminal/WindowEmperor.h | 13 + src/inc/til/mutex.h | 2 + 37 files changed, 1094 insertions(+), 21 deletions(-) create mode 100644 src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 0a5405c727..9296cd1eb6 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -441,6 +441,7 @@ "openTabColorPicker", "openTabRenamer", "openWindowRenamer", + "openWorkspace", "paste", "prevTab", "quakeMode", @@ -479,6 +480,7 @@ "toggleReadOnlyMode", "toggleShaderEffects", "toggleSplitOrientation", + "workspaces", "wt", "unbound" ], @@ -1721,6 +1723,28 @@ } ] }, + "OpenWorkspaceAction": { + "description": "Arguments corresponding to an openWorkspace Action", + "allOf": [ + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "const": "openWorkspace" + }, + "name": { + "type": "string", + "default": "", + "description": "The name of the workspace to open. If omitted, the workspaces UI will be shown so the user can pick one." + } + } + } + ] + }, "FocusPaneAction": { "description": "Arguments corresponding to a focusPane Action", "allOf": [ @@ -2040,6 +2064,11 @@ "unfocusedFrame": { "description": "The color of the window frame when the window is inactive. This only works on Windows 11", "$ref": "#/$defs/ThemeColor" + }, + "showWorkspacesButton": { + "description": "When set to true, the workspaces button will be shown in the tab row.", + "type": "boolean", + "default": true } } }, @@ -2208,6 +2237,9 @@ { "$ref": "#/$defs/RenameWindowAction" }, + { + "$ref": "#/$defs/OpenWorkspaceAction" + }, { "$ref": "#/$defs/FocusPaneAction" }, diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index a8360d92de..11104a064a 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -895,6 +895,16 @@ namespace winrt::TerminalApp::implementation RequestNewWindow.raise(*this, request); } + // Ask the WindowEmperor (in-process) to open or summon a named window, + // restoring its persisted workspace if one exists. The event bubbles up + // through TerminalWindow to AppHost, which calls into the WindowEmperor + // directly — no second wt.exe process is launched. + void TerminalPage::_OpenWorkspaceWindow(const winrt::hstring name) + { + const auto args = winrt::make(name); + RequestOpenWindow.raise(*this, args); + } + void TerminalPage::_HandleNewWindow(const IInspectable& /*sender*/, const ActionEventArgs& actionArgs) { @@ -916,7 +926,10 @@ namespace winrt::TerminalApp::implementation newContentArgs = NewTerminalArgs{}; } - // Manually fill in the evaluated profile + // If this is a NewTerminalArgs, resolve its profile up-front so the + // spawned window doesn't need to re-resolve it. Other content types + // (e.g. scratchpad) don't have profiles to evaluate — they get passed + // through as-is. if (const auto terminalArgs{ newContentArgs.try_as() }) { const auto profile{ _settings.GetProfileForArgs(terminalArgs) }; @@ -1015,6 +1028,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() }) { @@ -1590,4 +1604,35 @@ 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); + } + } + } + + void TerminalPage::_HandleWorkspaces(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (_workspaceFlyout && _workspaceDropdown) + { + _workspaceFlyout.ShowAt(_workspaceDropdown); + } + args.Handled(true); + } + } diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 85354101b6..43849b21d2 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/Remoting.h b/src/cascadia/TerminalApp/Remoting.h index 4634989f27..185da76e21 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); WINRT_PROPERTY(Windows::Foundation::Collections::IVector, StartupActions, nullptr); }; } diff --git a/src/cascadia/TerminalApp/Remoting.idl b/src/cascadia/TerminalApp/Remoting.idl index 937e0a45d5..8e93bd3971 100644 --- a/src/cascadia/TerminalApp/Remoting.idl +++ b/src/cascadia/TerminalApp/Remoting.idl @@ -51,6 +51,7 @@ namespace TerminalApp CommandlineArgs Command { get; }; String Content { get; }; Windows.Foundation.IReference InitialBounds { get; }; + Microsoft.Terminal.Settings.Model.WindowLayout PersistedLayout; Windows.Foundation.Collections.IVector StartupActions; }; } diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 2fe41f21a0..150679243d 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -743,6 +743,42 @@ 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) + {locked="{0}"}{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 + + + Delete "{0}"? + {locked="{0}"}{0} is the workspace name. Shown as the title of a confirmation dialog when the user tries to delete a saved workspace. + + + Are you sure you want to delete "{0}"? Sessions associated with this workspace will also be deleted. + {locked="{0}"}{0} is the workspace name. Body text of the confirmation dialog shown 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: @@ -801,6 +837,9 @@ This Terminal window is running as administrator + + Workspaces + Suggestions found: {0} {0} will be replaced with a number. diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 5bfed438aa..70d5d5eb8e 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -403,6 +403,22 @@ namespace winrt::TerminalApp::implementation _previouslyClosedPanesAndTabs.emplace_back(args); } + // Method Description: + // - If this window has a name, persist its current workspace layout to + // ApplicationState. Intended to be called from the close-pane / close-tab + // paths while tab/pane content is still alive (before it gets torn down). + void TerminalPage::_SaveWorkspaceIfNeeded() + { + const auto& windowName = _WindowProperties.WindowName(); + if (!windowName.empty()) + { + if (const auto layout = GetWindowLayout()) + { + ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); + } + } + } + // Method Description: // - Removes the tab (both TerminalControl and XAML) after prompting for approval // Arguments: @@ -450,6 +466,14 @@ 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) + { + _SaveWorkspaceIfNeeded(); + } + tab.Close(); } @@ -471,6 +495,13 @@ namespace winrt::TerminalApp::implementation const auto focusedTabIndex{ _GetFocusedTabIndex() }; + // 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. tab.Shutdown(); @@ -798,6 +829,21 @@ 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) + { + _SaveWorkspaceIfNeeded(); + } + } + } + // If specified, detach before closing to directly update the pane structure pane->Close(); } diff --git a/src/cascadia/TerminalApp/TabRowControl.h b/src/cascadia/TerminalApp/TabRowControl.h index d216dbef71..dddcaa74b5 100644 --- a/src/cascadia/TerminalApp/TabRowControl.h +++ b/src/cascadia/TerminalApp/TabRowControl.h @@ -19,6 +19,8 @@ namespace winrt::TerminalApp::implementation til::property_changed_event PropertyChanged; WINRT_OBSERVABLE_PROPERTY(bool, ShowElevationShield, PropertyChanged.raise, false); + WINRT_OBSERVABLE_PROPERTY(bool, ShowWorkspacesButton, PropertyChanged.raise, true); + WINRT_OBSERVABLE_PROPERTY(winrt::hstring, WorkspaceName, PropertyChanged.raise, L""); }; } diff --git a/src/cascadia/TerminalApp/TabRowControl.idl b/src/cascadia/TerminalApp/TabRowControl.idl index 4dd1d37bd9..ceecabc226 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 ShowWorkspacesButton; + String WorkspaceName; } } diff --git a/src/cascadia/TerminalApp/TabRowControl.xaml b/src/cascadia/TerminalApp/TabRowControl.xaml index 11335b654c..6ddcff89f6 100644 --- a/src/cascadia/TerminalApp/TabRowControl.xaml +++ b/src/cascadia/TerminalApp/TabRowControl.xaml @@ -8,6 +8,7 @@ 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"> @@ -35,14 +36,45 @@ TabWidthMode="Equal"> - - + + + + + + + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 0d4a245fd2..6ac715084a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -25,7 +25,10 @@ #include "TerminalSettingsCache.h" #include "LaunchPositionRequest.g.cpp" +#include "WindowListEntry.g.cpp" +#include "WindowListRequest.g.cpp" #include "RenameWindowRequestedArgs.g.cpp" +#include "OpenWindowRequestedArgs.g.cpp" #include "RequestMoveContentArgs.g.cpp" #include "TerminalPage.g.cpp" @@ -334,6 +337,21 @@ namespace winrt::TerminalApp::implementation auto tabRowImpl = winrt::get_self(_tabRow); _newTabButton = tabRowImpl->NewTabButton(); + _workspaceFlyout = tabRowImpl->WorkspaceFlyout(); + _workspaceDropdown = tabRowImpl->WorkspaceDropdown(); + + // 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 +461,12 @@ namespace winrt::TerminalApp::implementation _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + // Apply the ShowWorkspacesButton theme setting. + if (const auto theme = _settings.GlobalSettings().CurrentTheme()) + { + _tabRow.ShowWorkspacesButton(theme.Window() ? theme.Window().ShowWorkspacesButton() : true); + } + _adjustProcessPriorityThrottled = std::make_shared>( DispatcherQueue::GetForCurrentThread(), til::throttled_func_options{ @@ -2285,14 +2309,14 @@ namespace winrt::TerminalApp::implementation QuitRequested.raise(nullptr, nullptr); } - 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; @@ -2307,7 +2331,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 @@ -2356,7 +2380,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: @@ -4057,6 +4123,12 @@ namespace winrt::TerminalApp::implementation _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + // Apply the ShowWorkspacesButton theme setting. + if (const auto theme = _settings.GlobalSettings().CurrentTheme()) + { + _tabRow.ShowWorkspacesButton(theme.Window() ? theme.Window().ShowWorkspacesButton() : true); + } + Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; _tabView.Background(transparent); @@ -5697,6 +5769,196 @@ 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() }) + { + page->_actionDispatch->DoAction(ActionAndArgs{ ShortcutAction::OpenWindowRenamer, nullptr }); + } + }); + _workspaceFlyout.Items().Append(item); + } + + // --- Gather open window info first so we can filter workspaces --- + const auto windowListReq{ winrt::make() }; + RequestWindowList.raise(*this, windowListReq); + const auto windowEntries = windowListReq.Entries(); + + 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) --- + // Collect workspace names that aren't currently open so we can show + // them both as top-level "open" items and inside the delete sub-menu. + 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.contains(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); + } + }); + + // Right-click to delete: attach a context flyout with a + // "Delete workspace?" item that opens a confirmation dialog. + { + WUX::Controls::MenuFlyout deleteFlyout{}; + deleteFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedRight); + + WUX::Controls::MenuFlyoutItem deleteItem{}; + deleteItem.Text(RS_(L"DeleteWorkspaceMenuItem")); + + auto trashIcon = UI::IconPathConverter::IconWUX(L"\xE74D"); // Delete glyph + + deleteItem.Click([weakThis{ get_weak() }, name](auto&&, auto&&) -> safe_void_coroutine { + auto page{ weakThis.get() }; + if (!page) + { + co_return; + } + + // Build and show a confirmation ContentDialog. + ContentDialog dialog{}; + dialog.Title(winrt::box_value(winrt::hstring{ RS_fmt(L"ConfirmDeleteWorkspaceTitle", name) })); + dialog.Content(winrt::box_value(winrt::hstring{ RS_fmt(L"ConfirmDeleteWorkspaceBody", name) })); + dialog.PrimaryButtonText(RS_(L"ConfirmDeleteWorkspaceDelete")); + dialog.CloseButtonText(RS_(L"ConfirmDeleteWorkspaceCancel")); + dialog.DefaultButton(ContentDialogButton::Close); + + if (auto presenter{ page->_dialogPresenter.get() }) + { + const auto result = co_await presenter.ShowDialog(dialog); + // Re-check after co_await + page = weakThis.get(); + if (!page) + { + co_return; + } + if (result == ContentDialogResult::Primary) + { + ApplicationState::SharedInstance().RemoveWorkspace(name); + page->_PopulateWorkspaceFlyout(); + } + } + }); + + deleteFlyout.Items().Append(deleteItem); + WUX::Controls::Primitives::FlyoutBase::SetAttachedFlyout(item, deleteFlyout); + item.ContextRequested([item](auto&&, auto&&) { + WUX::Controls::Primitives::FlyoutBase::ShowAttachedFlyout(item); + }); + } + + _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(); + + 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); + + 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); + } + else + { + auto iconElement = UI::IconPathConverter::IconWUX(L"\uE737"); // ChromeRestore glyph + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + item.Icon(iconElement); + + 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) @@ -5706,6 +5968,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 74aade5f19..2705d4453c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -10,8 +10,12 @@ #include "AppKeyBindings.h" #include "AppCommandlineArgs.h" #include "RenameWindowRequestedArgs.g.h" +#include "OpenWindowRequestedArgs.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" @@ -73,6 +77,24 @@ namespace winrt::TerminalApp::implementation _ProposedName{ name } {}; }; + struct OpenWindowRequestedArgs : OpenWindowRequestedArgsT + { + WINRT_PROPERTY(winrt::hstring, Name); + + public: + OpenWindowRequestedArgs(const winrt::hstring& name) : + _Name{ name } {}; + }; + + struct SummonWindowByIdRequestedArgs : SummonWindowByIdRequestedArgsT + { + WINRT_PROPERTY(uint64_t, WindowId); + + public: + SummonWindowByIdRequestedArgs(uint64_t id) : + _WindowId{ id } {}; + }; + struct RequestMoveContentArgs : RequestMoveContentArgsT { WINRT_PROPERTY(winrt::hstring, Window); @@ -94,6 +116,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; @@ -132,6 +173,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; @@ -203,6 +245,7 @@ namespace winrt::TerminalApp::implementation til::typed_event IdentifyWindowsRequested; til::typed_event RenameWindowRequested; til::typed_event SummonWindowRequested; + til::typed_event SummonWindowByIdRequested; til::typed_event FocusTabRequested; til::typed_event WindowSizeChanged; @@ -215,6 +258,8 @@ namespace winrt::TerminalApp::implementation til::typed_event RequestReceiveContent; til::typed_event RequestLaunchPosition; + til::typed_event RequestWindowList; + til::typed_event RequestOpenWindow; til::typed_event RequestNewWindow; WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); @@ -238,6 +283,8 @@ 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 }; + Windows::UI::Xaml::Controls::Button _workspaceDropdown{ nullptr }; winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr }; Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; @@ -336,6 +383,7 @@ namespace winrt::TerminalApp::implementation void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&); void _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs& contentArgs); + void _OpenWorkspaceWindow(const winrt::hstring name); void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); @@ -365,6 +413,7 @@ namespace winrt::TerminalApp::implementation void _CloseTabAtIndex(uint32_t index); void _RemoveTab(const winrt::TerminalApp::Tab& tab); safe_void_coroutine _RemoveTabs(const std::vector tabs); + void _SaveWorkspaceIfNeeded(); void _InitializeTab(winrt::com_ptr newTabImpl, uint32_t insertPosition = -1); void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); @@ -578,6 +627,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); @@ -604,4 +654,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 27be726db0..031ea0f286 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -19,6 +19,14 @@ namespace TerminalApp { String ProposedName { get; }; }; + [default_interface] runtimeclass OpenWindowRequestedArgs + { + String Name { get; }; + }; + [default_interface] runtimeclass SummonWindowByIdRequestedArgs + { + UInt64 WindowId { get; }; + }; [default_interface] runtimeclass RequestMoveContentArgs { String Window { get; }; @@ -50,6 +58,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 +115,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,6 +126,12 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestReceiveContent; event Windows.Foundation.TypedEventHandler RequestLaunchPosition; + event Windows.Foundation.TypedEventHandler RequestWindowList; + + // Raised when the page wants the monarch to open (or summon) a + // named window in-process, restoring its persisted workspace if + // one exists. No new wt.exe process is spawned. + event Windows.Foundation.TypedEventHandler RequestOpenWindow; event Windows.Foundation.TypedEventHandler RequestNewWindow; } diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index d69b5487ef..d775f87654 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -258,6 +258,15 @@ namespace winrt::TerminalApp::implementation AppLogic::Current()->NotifyRootInitialized(); } + WindowLayout TerminalWindow::GetWindowLayout() const + { + if (_root) + { + return _root->GetWindowLayout(); + } + return nullptr; + } + void TerminalWindow::PersistState() { if (_root) @@ -1112,6 +1121,11 @@ namespace winrt::TerminalApp::implementation _initialContentArgs = wil::to_vector(actions); } + 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. @@ -1232,7 +1246,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 306f109c13..48deff6124 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() const; void PersistState(); void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args); @@ -80,6 +81,7 @@ namespace winrt::TerminalApp::implementation int32_t SetStartupCommandline(TerminalApp::CommandlineArgs args); void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference& contentBounds); void SetStartupActions(const Windows::Foundation::Collections::IVector& actions); + void SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout); int32_t ExecuteCommandline(TerminalApp::CommandlineArgs args); void SetSettingsStartupArgs(const std::vector& actions); @@ -223,6 +225,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(FocusTabRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::Tab, _root, FocusTabRequested); FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); @@ -232,6 +235,8 @@ 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); + FORWARDED_TYPED_EVENT(RequestOpenWindow, Windows::Foundation::IInspectable, winrt::TerminalApp::OpenWindowRequestedArgs, _root, RequestOpenWindow); FORWARDED_TYPED_EVENT(RequestNewWindow, Windows::Foundation::IInspectable, winrt::TerminalApp::WindowRequestedArgs, _root, RequestNewWindow); #ifdef UNIT_TESTING diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index 26ed1bc7c8..763a3f893a 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -57,11 +57,13 @@ namespace TerminalApp Int32 SetStartupCommandline(CommandlineArgs args); void SetStartupContent(String json, Windows.Foundation.IReference bounds); void SetStartupActions(Windows.Foundation.Collections.IVector actions); + 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(); @@ -129,6 +131,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 FocusTabRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; event Windows.Foundation.TypedEventHandler QuitRequested; @@ -141,6 +144,8 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestMoveContent; event Windows.Foundation.TypedEventHandler RequestReceiveContent; event Windows.Foundation.TypedEventHandler RequestLaunchPosition; + event Windows.Foundation.TypedEventHandler RequestWindowList; + event Windows.Foundation.TypedEventHandler RequestOpenWindow; event Windows.Foundation.TypedEventHandler RequestNewWindow; void AttachContent(String content, UInt32 tabIndex); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 6c896ca7b4..659f8e87cf 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" }; @@ -101,6 +103,7 @@ static constexpr std::string_view OpenScratchpadKey{ "experimental.openScratchpa static constexpr std::string_view OpenAboutKey{ "openAbout" }; static constexpr std::string_view QuickFixKey{ "quickFix" }; static constexpr std::string_view OpenCWDKey{ "openCWD" }; +static constexpr std::string_view WorkspacesKey{ "workspaces" }; static constexpr std::string_view ActionKey{ "action" }; diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 60e9178129..a33922f193 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 45ba2cb144..fc20bf6575 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 67c241e239..f2326813e2 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/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 062d303c33..ca7b5fb7af 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -116,6 +116,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::OpenTabColorPicker, USES_RESOURCE(L"OpenTabColorPickerCommandKey") }, { ShortcutAction::OpenTabRenamer, USES_RESOURCE(L"OpenTabRenamerCommandKey") }, { ShortcutAction::OpenWindowRenamer, USES_RESOURCE(L"OpenWindowRenamerCommandKey") }, + { ShortcutAction::OpenWorkspace, USES_RESOURCE(L"OpenWorkspaceDefaultCommandKey") }, { ShortcutAction::PasteText, USES_RESOURCE(L"PasteTextCommandKey") }, { ShortcutAction::PrevTab, USES_RESOURCE(L"PrevTabCommandKey") }, { ShortcutAction::QuickFix, USES_RESOURCE(L"QuickFixCommandKey") }, @@ -162,6 +163,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::TogglePaneZoom, USES_RESOURCE(L"TogglePaneZoomCommandKey") }, { ShortcutAction::ToggleShaderEffects, USES_RESOURCE(L"ToggleShaderEffectsCommandKey") }, { ShortcutAction::ToggleSplitOrientation, USES_RESOURCE(L"ToggleSplitOrientationCommandKey") }, + { ShortcutAction::Workspaces, USES_RESOURCE(L"WorkspacesCommandKey") }, }; }(); @@ -225,6 +227,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return winrt::make(); case Model::ShortcutAction::OpenSettings: return winrt::make(); + case Model::ShortcutAction::OpenWorkspace: + return winrt::make(); case Model::ShortcutAction::SetFocusMode: return winrt::make(); case Model::ShortcutAction::SetFullScreen: diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index b9855d9e30..d65c856832 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -113,7 +113,9 @@ ON_ALL_ACTIONS(OpenScratchpad) \ ON_ALL_ACTIONS(OpenAbout) \ ON_ALL_ACTIONS(QuickFix) \ - ON_ALL_ACTIONS(OpenCWD) + ON_ALL_ACTIONS(OpenCWD) \ + ON_ALL_ACTIONS(OpenWorkspace) \ + ON_ALL_ACTIONS(Workspaces) #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \ ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize) \ @@ -158,7 +160,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 ed2f69d883..45e3e2fde5 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -253,6 +253,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return *state; } +// Need the COMMA macro hack for IMap template arguments in the macros +#define COMMA , + // Method Description: // - Loads data from the given json blob. Will only read the data that's in // the specified parseSource - so if we're reading the Local state file, @@ -302,6 +305,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return root; } +#undef COMMA + void ApplicationState::AppendPersistedWindowLayout(Model::WindowLayout layout) { { @@ -341,6 +346,120 @@ 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. + // - If newName is empty, the entry under oldName is simply removed (the + // old name no longer points at a valid window, so the persisted layout + // would otherwise be left stranded). + // Return Value: + // - true if the persisted state was modified, false otherwise. + bool ApplicationState::RenameWorkspace(const hstring& oldName, const hstring& newName) + { + if (oldName == newName || oldName.empty()) + { + return false; + } + + bool changed{ false }; + { + const auto state = _state.lock(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + auto map = *state->PersistedWorkspaces; + if (map.HasKey(oldName)) + { + if (!newName.empty()) + { + const auto layout = map.Lookup(oldName); + map.Insert(newName, layout); + } + map.Remove(oldName); + changed = true; + } + } + } + if (changed) + { + _throttler(); + } + return changed; + } + + // 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 \ @@ -359,7 +478,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation \ _throttler(); \ } +#define COMMA , MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) +#undef COMMA #undef MTSM_APPLICATION_STATE_GEN // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index f998fc5ead..8ce1be8ebe 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -16,7 +16,7 @@ Abstract: #include "WindowLayout.g.h" #include -#include +#include "JsonUtils.h" namespace winrt::Microsoft::Terminal::Settings::Model::implementation { @@ -34,6 +34,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // This macro generates all getters and setters for ApplicationState. // It provides X with the following arguments: // (source, type, function name, JSON key, ...variadic construction arguments) +// Note: we're using the COMMA macro hack for the template arguments as used elsewhere. #define MTSM_APPLICATION_STATE_FIELDS(X) \ X(FileSource::Shared, winrt::hstring, SettingsHash, "settingsHash") \ X(FileSource::Shared, std::unordered_set, GeneratedProfiles, "generatedProfiles") \ @@ -42,6 +43,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation X(FileSource::Shared, Windows::Foundation::Collections::IVector, DismissedMessages, "dismissedMessages") \ X(FileSource::Local, Windows::Foundation::Collections::IVector, AllowedCommandlines, "allowedCommandlines") \ X(FileSource::Local, std::unordered_set, DismissedBadges, "dismissedBadges") \ + X(FileSource::Local, Windows::Foundation::Collections::IMap, PersistedWorkspaces, "persistedWorkspaces") \ X(FileSource::Shared, bool, SSHFolderGenerated, "sshFolderGenerated", false) struct WindowLayout : WindowLayoutT @@ -57,6 +59,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation friend ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait; }; +#define COMMA , + struct ApplicationState : public ApplicationStateT { static Microsoft::Terminal::Settings::Model::ApplicationState SharedInstance(); @@ -75,6 +79,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; \ @@ -104,6 +114,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::string _readLocalContents() const; void _writeLocalContents(const std::string_view content) const; }; +#undef COMMA } namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.idl b/src/cascadia/TerminalSettingsModel/ApplicationState.idl index f4e85d3a0b..8c067ff304 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 08095670c9..96cd878b80 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -162,7 +162,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, ShowWorkspacesButton, "showWorkspacesButton", 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 a21afe9e88..b4a9e86e17 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... @@ -740,6 +747,9 @@ Open current working directory + + Workspaces... + Close tab diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl index 9cf0806f25..cb88aae95a 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 ShowWorkspacesButton { get; }; ThemeColor Frame { get; }; ThemeColor UnfocusedFrame { get; }; } diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 44efab04ca..8971d3d399 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -542,6 +542,7 @@ { "command": "quickFix", "id": "Terminal.QuickFix" }, { "command": { "action": "showSuggestions", "source": "all"}, "id": "Terminal.Suggestions" }, { "command": "openCWD", "id": "Terminal.OpenCWD" }, + { "command": "workspaces", "id": "Terminal.Workspaces" }, // Tab Management // "command": "closeTab" is unbound by default. diff --git a/src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp b/src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp new file mode 100644 index 0000000000..16099b2bb2 --- /dev/null +++ b/src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp @@ -0,0 +1,138 @@ +// 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")); + + // Renaming to an empty name removes the stale entry under the old name. + VERIFY_IS_TRUE(state->RenameWorkspace(L"win1", L"")); + const auto all = state->AllPersistedWorkspaces(); + if (all) + { + VERIFY_IS_FALSE(all.HasKey(L"win1")); + VERIFY_IS_FALSE(all.HasKey(L"")); + } + + // Calling again is now a no-op because the entry is gone. + 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 4b488c2dca..8249f7ca2a 100644 --- a/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj +++ b/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj @@ -45,6 +45,7 @@ + Create diff --git a/src/cascadia/UnitTests_SettingsModel/pch.h b/src/cascadia/UnitTests_SettingsModel/pch.h index 4cad433c61..4066fff8db 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/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 1b59ba3571..6d22a9a818 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; @@ -270,12 +275,15 @@ 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.FocusTabRequested = _windowLogic.FocusTabRequested(winrt::auto_revoke, { this, &AppHost::_FocusTabRequested }); _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 }); + _revokers.RequestOpenWindow = _windowLogic.RequestOpenWindow(winrt::auto_revoke, { this, &AppHost::_HandleOpenWindowRequested }); _revokers.RequestNewWindow = _windowLogic.RequestNewWindow(winrt::auto_revoke, { this, &AppHost::_HandleNewWindowRequested }); // BODGY @@ -416,6 +424,40 @@ 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 w; + w.Id(entry.Id); + w.Name(winrt::hstring{ entry.Name }); + windowEntries.Append(w); + } +} + +// In-process replacement for the old `ShellExecute("wt -w ...")` dance. +// Asks the WindowEmperor to summon a named window or restore its persisted +// workspace, without launching a second wt.exe. +void AppHost::_HandleOpenWindowRequested(const winrt::Windows::Foundation::IInspectable&, + const winrt::TerminalApp::OpenWindowRequestedArgs& args) +{ + if (_windowManager && args) + { + _windowManager->OpenWindow(args.Name()); + } +} + // In-process replacement for the old `ShellExecute("wt -w -1 new-tab ...")` // dance. The page hands us a pre-built WindowRequestedArgs (with its // StartupActions already populated); we just forward it to the WindowEmperor. @@ -1083,6 +1125,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::_FocusTabRequested(const winrt::Windows::Foundation::IInspectable&, const winrt::TerminalApp::Tab& tab) { diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index d75b2379ed..ece07e3ca5 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -91,12 +91,18 @@ private: 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 _FocusTabRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::Tab& tab); void _OpenSystemMenu(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); + void _HandleOpenWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::TerminalApp::OpenWindowRequestedArgs& args); + void _HandleNewWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::WindowRequestedArgs& args); @@ -136,6 +142,8 @@ private: 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 @@ -157,6 +165,7 @@ private: 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::FocusTabRequested_revoker FocusTabRequested; winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu; winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested; @@ -165,6 +174,8 @@ private: winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent; winrt::TerminalApp::TerminalWindow::RequestLaunchPosition_revoker RequestLaunchPosition; winrt::TerminalApp::TerminalWindow::RequestNewWindow_revoker RequestNewWindow; + winrt::TerminalApp::TerminalWindow::RequestWindowList_revoker RequestWindowList; + winrt::TerminalApp::TerminalWindow::RequestOpenWindow_revoker RequestOpenWindow; 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 9fa13c1b1c..a21ad3f02e 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -296,6 +296,60 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args } } +// Public entry point used by in-process callers (e.g. AppHost reacting to a +// TerminalPage RequestOpenWindow event) to open or summon a named window - +// restoring its persisted workspace if one exists - without spawning a second +// wt.exe. Bypasses the commandline parser entirely. +void WindowEmperor::OpenWindow(const winrt::hstring& name) +{ + _assertIsMainThread(); + + if (name.empty()) + { + return; + } + + // If a window with this name is already live, just summon it. + // This mirrors the summon behavior in AppHost::DispatchCommandline (which is + // what the old `wt -w ` ShellExecute path effectively triggered). + if (const auto window = GetWindowByName(name)) + { + winrt::TerminalApp::SummonWindowBehavior summon{}; + summon.MoveToCurrentDesktop(false); + summon.DropdownDuration(0); + summon.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace); + summon.ToggleVisibility(false); + window->HandleSummon(std::move(summon)); + return; + } + + // Otherwise, create a new window under that name. A default-constructed + // CommandlineArgs is supplied as the launch fallback for the case where + // no persisted workspace exists; AppHost ignores it when PersistedLayout + // is set. + _createWindowMaybeRestoringWorkspace(0, name, winrt::TerminalApp::CommandlineArgs{}); +} + +// Shared tail used by both the commandline dispatch path and OpenWindow(): +// build a WindowRequestedArgs for a new window and, if the request carries a +// name, atomically claim any persisted workspace stored under that name so +// it's restored here and no subsequent caller can pick up the same entry. +void WindowEmperor::_createWindowMaybeRestoringWorkspace(uint64_t windowId, const winrt::hstring& windowName, winrt::TerminalApp::CommandlineArgs args) +{ + winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) }; + request.WindowName(windowName); + + if (!windowName.empty()) + { + if (const auto layout = ApplicationState::SharedInstance().TakeWorkspace(windowName)) + { + request.PersistedLayout(layout); + } + } + + CreateNewWindow(std::move(request)); +} + AppHost* WindowEmperor::_mostRecentWindow() const noexcept { int64_t max = INT64_MIN; @@ -801,9 +855,7 @@ void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs arg } else { - winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) }; - request.WindowName(std::move(windowName)); - CreateNewWindow(std::move(request)); + _createWindowMaybeRestoringWorkspace(windowId, windowName, std::move(args)); } } @@ -1087,6 +1139,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 { @@ -1114,6 +1182,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 5eb93d6401..f34c07919d 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -28,6 +28,16 @@ public: 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; @@ -37,6 +47,8 @@ public: void CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args); void HandleCommandlineArgs(int nCmdShow); void FocusTabInAnyWindow(const winrt::TerminalApp::Tab& tab) const; + // OpenWindow is used for opening a new window or summoning an existing window by name. + void OpenWindow(const winrt::hstring& name); private: struct SummonWindowSelectionArgs @@ -50,6 +62,7 @@ private: [[nodiscard]] static LRESULT __stdcall _wndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept; AppHost* _mostRecentWindow() const noexcept; + void _createWindowMaybeRestoringWorkspace(uint64_t windowId, const winrt::hstring& windowName, winrt::TerminalApp::CommandlineArgs args); bool _summonWindow(const SummonWindowSelectionArgs& args) const; void _summonAllWindows() const; void _dispatchSpecialKey(const MSG& msg) const; diff --git a/src/inc/til/mutex.h b/src/inc/til/mutex.h index e4299d31cf..b1fd20ab54 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