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