diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 2a59887da8a..e9a2b31e59a 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -479,6 +479,7 @@ "toggleReadOnlyMode", "toggleShaderEffects", "toggleSplitOrientation", + "workspaces", "wt", "unbound" ], @@ -2040,6 +2041,11 @@ "unfocusedFrame": { "description": "The color of the window frame when the window is inactive. This only works on Windows 11", "$ref": "#/$defs/ThemeColor" + }, + "showWindowsButton": { + "description": "When set to true, the workspace/windows button will be shown in the tab row.", + "type": "boolean", + "default": true } } }, diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 7e0edef7bd5..4573cf94094 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -938,6 +938,27 @@ namespace winrt::TerminalApp::implementation co_return; } + // Launch `wt -w ` so the monarch can either summon an existing + // window with that name or restore a persisted workspace. + safe_void_coroutine TerminalPage::_OpenWorkspaceWindow(const winrt::hstring name) + { + co_await winrt::resume_background(); + + const auto exePath{ GetWtExePath() }; + const auto cmdline = fmt::format(FMT_COMPILE(L"-w {}"), std::wstring_view{ name }); + + SHELLEXECUTEINFOW seInfo{ 0 }; + seInfo.cbSize = sizeof(seInfo); + seInfo.fMask = SEE_MASK_NOASYNC; + seInfo.lpVerb = L"open"; + seInfo.lpFile = exePath.c_str(); + seInfo.lpParameters = cmdline.c_str(); + seInfo.nShow = SW_SHOWNORMAL; + LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo)); + + co_return; + } + void TerminalPage::_HandleNewWindow(const IInspectable& /*sender*/, const ActionEventArgs& actionArgs) { @@ -1058,6 +1079,7 @@ namespace winrt::TerminalApp::implementation // Fun! // WindowRenamerTextBox().Focus(FocusState::Programmatic); _renamerLayoutUpdatedRevoker.revoke(); + _renamerLayoutCount = 0; _renamerLayoutUpdatedRevoker = WindowRenamerTextBox().LayoutUpdated(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) { if (auto self{ weakThis.get() }) { @@ -1633,4 +1655,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 85354101b68..43849b21d22 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -92,6 +92,10 @@ INewContentArgs Pane::GetTerminalArgsForPane(BuildStartupKind kind) const { // Leaves are the only things that have controls assert(_IsLeaf()); + if (!_content) + { + return nullptr; + } return _content.GetNewTerminalArgs(kind); } diff --git a/src/cascadia/TerminalApp/Remoting.h b/src/cascadia/TerminalApp/Remoting.h index 9d4a48df761..13da3681dbf 100644 --- a/src/cascadia/TerminalApp/Remoting.h +++ b/src/cascadia/TerminalApp/Remoting.h @@ -85,6 +85,7 @@ namespace winrt::TerminalApp::implementation WINRT_PROPERTY(TerminalApp::CommandlineArgs, Command, nullptr); WINRT_PROPERTY(winrt::hstring, Content); WINRT_PROPERTY(Windows::Foundation::IReference, InitialBounds); + WINRT_PROPERTY(winrt::Microsoft::Terminal::Settings::Model::WindowLayout, PersistedLayout, nullptr); }; } diff --git a/src/cascadia/TerminalApp/Remoting.idl b/src/cascadia/TerminalApp/Remoting.idl index 07fde64e503..572f3b06af3 100644 --- a/src/cascadia/TerminalApp/Remoting.idl +++ b/src/cascadia/TerminalApp/Remoting.idl @@ -51,5 +51,6 @@ namespace TerminalApp CommandlineArgs Command { get; }; String Content { get; }; Windows.Foundation.IReference InitialBounds { get; }; + Microsoft.Terminal.Settings.Model.WindowLayout PersistedLayout; }; } diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 2fe41f21a01..21944f4f7c6 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -743,6 +743,38 @@ unnamed window text used to identify when a window hasn't been assigned a name by the user + + Name this window… + Menu item text shown when the current window has no name assigned + + + Rename this window… + Menu item text shown when the current window already has a name + + + #{0} (unnamed) + {0} is the window ID number. Shown in the workspace flyout for windows that have no name assigned. + + + Delete workspace? + Menu item text shown in the right-click context menu on a saved workspace + + + Open workspace + Menu item text for opening a saved workspace + + + Are you sure you want to delete the workspace "{0}"? + {0} is the workspace name. Shown in a confirmation dialog when the user tries to delete a saved workspace. + + + Delete + Primary button text on the delete workspace confirmation dialog + + + Cancel + Cancel button text on the delete workspace confirmation dialog + Enter a new name: diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 74b6c736939..13d331944c4 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -450,6 +450,21 @@ namespace winrt::TerminalApp::implementation auto actions = t->BuildStartupActions(BuildStartupKind::None); _AddPreviouslyClosedPaneOrTab(std::move(actions)); + // If this is the last tab in a named window, persist the workspace + // layout now while tab content is still alive. After tab.Close() + // the pane content will be torn down by the time _RemoveTab runs. + if (_tabs.Size() == 1) + { + const auto& windowName = _WindowProperties.WindowName(); + if (!windowName.empty()) + { + if (const auto layout = GetWindowLayout()) + { + ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); + } + } + } + tab.Close(); } @@ -471,6 +486,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 +820,28 @@ namespace winrt::TerminalApp::implementation } _AddPreviouslyClosedPaneOrTab(std::move(state.args)); + // If this is the last pane on the last tab of a named window, persist + // the workspace layout now while the pane content is still alive. + // We can't wait until _RemoveTab, because pane->Close() below will + // destroy the content before _RemoveTab is reached. + if (_tabs.Size() == 1) + { + if (const auto activeTab{ _GetFocusedTabImpl() }) + { + if (activeTab->GetLeafPaneCount() == 1) + { + const auto& windowName = _WindowProperties.WindowName(); + if (!windowName.empty()) + { + if (const auto layout = GetWindowLayout()) + { + ApplicationState::SharedInstance().SaveWorkspace(windowName, layout); + } + } + } + } + } + // If specified, detach before closing to directly update the pane structure pane->Close(); } diff --git a/src/cascadia/TerminalApp/TabRowControl.cpp b/src/cascadia/TerminalApp/TabRowControl.cpp index aa069944282..0778bb0de03 100644 --- a/src/cascadia/TerminalApp/TabRowControl.cpp +++ b/src/cascadia/TerminalApp/TabRowControl.cpp @@ -25,6 +25,15 @@ namespace winrt::TerminalApp::implementation InitializeComponent(); } + void TabRowControl::WorkspaceName(const winrt::hstring& value) + { + if (_WorkspaceName != value) + { + _WorkspaceName = value; + PropertyChanged.raise(*this, WUX::Data::PropertyChangedEventArgs{ L"WorkspaceName" }); + } + } + // Method Description: // - Bound in the Xaml editor to the [+] button. // Arguments: diff --git a/src/cascadia/TerminalApp/TabRowControl.h b/src/cascadia/TerminalApp/TabRowControl.h index d216dbef711..bf09f78bad4 100644 --- a/src/cascadia/TerminalApp/TabRowControl.h +++ b/src/cascadia/TerminalApp/TabRowControl.h @@ -19,6 +19,14 @@ namespace winrt::TerminalApp::implementation til::property_changed_event PropertyChanged; WINRT_OBSERVABLE_PROPERTY(bool, ShowElevationShield, PropertyChanged.raise, false); + WINRT_OBSERVABLE_PROPERTY(bool, ShowWindowsButton, PropertyChanged.raise, true); + + public: + winrt::hstring WorkspaceName() const noexcept { return _WorkspaceName; } + void WorkspaceName(const winrt::hstring& value); + + private: + winrt::hstring _WorkspaceName{}; }; } diff --git a/src/cascadia/TerminalApp/TabRowControl.idl b/src/cascadia/TerminalApp/TabRowControl.idl index 4dd1d37bd97..2f461f01d2c 100644 --- a/src/cascadia/TerminalApp/TabRowControl.idl +++ b/src/cascadia/TerminalApp/TabRowControl.idl @@ -9,5 +9,7 @@ namespace TerminalApp TabRowControl(); Microsoft.UI.Xaml.Controls.TabView TabView { get; }; Boolean ShowElevationShield; + Boolean ShowWindowsButton; + String WorkspaceName; } } diff --git a/src/cascadia/TerminalApp/TabRowControl.xaml b/src/cascadia/TerminalApp/TabRowControl.xaml index 11335b654ca..4ef499ca7ed 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,44 @@ TabWidthMode="Equal"> - - + + + + + + + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 0d4a245fd28..42bb688b52c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -25,6 +25,8 @@ #include "TerminalSettingsCache.h" #include "LaunchPositionRequest.g.cpp" +#include "WindowListEntry.g.cpp" +#include "WindowListRequest.g.cpp" #include "RenameWindowRequestedArgs.g.cpp" #include "RequestMoveContentArgs.g.cpp" #include "TerminalPage.g.cpp" @@ -334,6 +336,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 +460,12 @@ namespace winrt::TerminalApp::implementation _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + // Apply the ShowWindowsButton theme setting. + if (const auto theme = _settings.GlobalSettings().CurrentTheme()) + { + _tabRow.ShowWindowsButton(theme.Window() ? theme.Window().ShowWindowsButton() : true); + } + _adjustProcessPriorityThrottled = std::make_shared>( DispatcherQueue::GetForCurrentThread(), til::throttled_func_options{ @@ -2285,14 +2308,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 +2330,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 +2379,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 +4122,12 @@ namespace winrt::TerminalApp::implementation _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + // Apply the ShowWindowsButton theme setting. + if (const auto theme = _settings.GlobalSettings().CurrentTheme()) + { + _tabRow.ShowWindowsButton(theme.Window() ? theme.Window().ShowWindowsButton() : true); + } + Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; _tabView.Background(transparent); @@ -5697,6 +5768,199 @@ namespace winrt::TerminalApp::implementation } } + // Rebuild the workspace flyout contents. Called every time the flyout opens + // 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.count(name)) + { + continue; + } + + if (!addedSeparator) + { + _workspaceFlyout.Items().Append(MenuFlyoutSeparator{}); + addedSeparator = true; + } + + MenuFlyoutItem item{}; + item.Text(name); + + auto iconElement = UI::IconPathConverter::IconWUX(L"\uE8F1"); // SwitchApps glyph + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + item.Icon(iconElement); + + item.Click([weakThis{ get_weak() }, name](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + page->_OpenWorkspaceWindow(name); + } + }); + + // 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")); + + WUX::Controls::FontIcon trashIcon{}; + trashIcon.Glyph(L"\xE74D"); // Delete glyph + trashIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); + deleteItem.Icon(trashIcon); + + 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.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 +5970,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 72f13bf43d0..49833718efa 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -10,8 +10,11 @@ #include "AppKeyBindings.h" #include "AppCommandlineArgs.h" #include "RenameWindowRequestedArgs.g.h" +#include "SummonWindowByIdRequestedArgs.g.h" #include "RequestMoveContentArgs.g.h" #include "LaunchPositionRequest.g.h" +#include "WindowListEntry.g.h" +#include "WindowListRequest.g.h" #include "Toast.h" #include "WindowsPackageManagerFactory.h" @@ -73,6 +76,15 @@ namespace winrt::TerminalApp::implementation _ProposedName{ name } {}; }; + struct SummonWindowByIdRequestedArgs : SummonWindowByIdRequestedArgsT + { + WINRT_PROPERTY(uint64_t, WindowId); + + public: + SummonWindowByIdRequestedArgs(uint64_t id) : + _WindowId{ id } {}; + }; + struct RequestMoveContentArgs : RequestMoveContentArgsT { WINRT_PROPERTY(winrt::hstring, Window); @@ -94,6 +106,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 +163,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 +235,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 +248,7 @@ namespace winrt::TerminalApp::implementation til::typed_event RequestReceiveContent; til::typed_event RequestLaunchPosition; + til::typed_event RequestWindowList; WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr); @@ -237,6 +271,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 }; @@ -335,6 +371,7 @@ namespace winrt::TerminalApp::implementation void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&); safe_void_coroutine _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs newContentArgs); + safe_void_coroutine _OpenWorkspaceWindow(const winrt::hstring name); void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); @@ -577,6 +614,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); @@ -603,4 +641,5 @@ namespace winrt::TerminalApp::implementation namespace winrt::TerminalApp::factory_implementation { BASIC_FACTORY(TerminalPage); + BASIC_FACTORY(WindowListEntry); } diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index ce15059864b..97528b35b3f 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -19,6 +19,10 @@ namespace TerminalApp { String ProposedName { get; }; }; + [default_interface] runtimeclass SummonWindowByIdRequestedArgs + { + UInt64 WindowId { get; }; + }; [default_interface] runtimeclass RequestMoveContentArgs { String Window { get; }; @@ -50,6 +54,20 @@ namespace TerminalApp Microsoft.Terminal.Settings.Model.LaunchPosition Position; } + [default_interface] runtimeclass WindowListEntry + { + WindowListEntry(); + UInt64 Id; + String Name; + } + + // Raised by TerminalPage when it needs the list of open windows. + // The handler (AppHost) fills Entries synchronously. + [default_interface] runtimeclass WindowListRequest + { + Windows.Foundation.Collections.IVector Entries { get; }; + } + [default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, Microsoft.Terminal.UI.IDirectKeyListener { TerminalPage(WindowProperties properties, ContentManager manager); @@ -93,6 +111,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler RenameWindowRequested; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler SummonWindowByIdRequested; event Windows.Foundation.TypedEventHandler WindowSizeChanged; event Windows.Foundation.TypedEventHandler OpenSystemMenu; @@ -103,5 +122,6 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestReceiveContent; event Windows.Foundation.TypedEventHandler RequestLaunchPosition; + event Windows.Foundation.TypedEventHandler RequestWindowList; } } diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index 67110a1667a..17e6aa6bc5d 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() + { + if (_root) + { + return _root->GetWindowLayout(); + } + return nullptr; + } + void TerminalWindow::PersistState() { if (_root) @@ -1103,6 +1112,11 @@ namespace winrt::TerminalApp::implementation _initialContentArgs = wil::to_vector(args); } + void TerminalWindow::SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout) + { + _cachedLayout = layout; + } + // Method Description: // - Parse the provided commandline arguments into actions, and try to // perform them immediately. @@ -1223,7 +1237,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 08ddeabafc8..6a957f42d45 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -71,6 +71,7 @@ namespace winrt::TerminalApp::implementation void Create(); + winrt::Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout(); void PersistState(); void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args); @@ -79,6 +80,7 @@ namespace winrt::TerminalApp::implementation int32_t SetStartupCommandline(TerminalApp::CommandlineArgs args); void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference& contentBounds); + void SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout); int32_t ExecuteCommandline(TerminalApp::CommandlineArgs args); void SetSettingsStartupArgs(const std::vector& actions); @@ -222,6 +224,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); @@ -231,6 +234,7 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs, _root, RequestReceiveContent); FORWARDED_TYPED_EVENT(RequestLaunchPosition, Windows::Foundation::IInspectable, winrt::TerminalApp::LaunchPositionRequest, _root, RequestLaunchPosition); + FORWARDED_TYPED_EVENT(RequestWindowList, Windows::Foundation::IInspectable, winrt::TerminalApp::WindowListRequest, _root, RequestWindowList); #ifdef UNIT_TESTING friend class TerminalAppLocalTests::CommandlineTest; diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index c12f73e1378..1808b9d8806 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -56,11 +56,13 @@ namespace TerminalApp Int32 SetStartupCommandline(CommandlineArgs args); void SetStartupContent(String json, Windows.Foundation.IReference bounds); + void SetPersistedLayout(Microsoft.Terminal.Settings.Model.WindowLayout layout); Int32 ExecuteCommandline(CommandlineArgs args); Boolean ShouldImmediatelyHandoffToElevated(); void HandoffToElevated(); + Microsoft.Terminal.Settings.Model.WindowLayout GetWindowLayout(); void PersistState(); Windows.UI.Xaml.UIElement GetRoot(); @@ -128,6 +130,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; @@ -140,6 +143,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestMoveContent; event Windows.Foundation.TypedEventHandler RequestReceiveContent; event Windows.Foundation.TypedEventHandler RequestLaunchPosition; + event Windows.Foundation.TypedEventHandler RequestWindowList; void AttachContent(String content, UInt32 tabIndex); void SendContentToOther(RequestReceiveContentArgs args); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 6c896ca7b43..659f8e87cf4 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 60e9178129b..a33922f1930 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -40,6 +40,7 @@ #include "PrevTabArgs.g.cpp" #include "NextTabArgs.g.cpp" #include "RenameWindowArgs.g.cpp" +#include "OpenWorkspaceArgs.g.cpp" #include "SearchForTextArgs.g.cpp" #include "GlobalSummonArgs.g.cpp" #include "FocusPaneArgs.g.cpp" @@ -795,6 +796,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return RS_switchable_(L"ResetWindowNameCommandKey"); } + winrt::hstring OpenWorkspaceArgs::GenerateName(const winrt::WARC::ResourceContext& context) const + { + if (!Name().empty()) + { + return winrt::hstring{ RS_switchable_fmt(L"OpenWorkspaceCommandKey", Name()) }; + } + return RS_switchable_(L"OpenWorkspaceDefaultCommandKey"); + } + winrt::hstring SearchForTextArgs::GenerateName(const winrt::WARC::ResourceContext& context) const { if (QueryUrl().empty()) diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 45ba2cb144e..fc20bf65752 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -42,6 +42,7 @@ #include "PrevTabArgs.g.h" #include "NextTabArgs.g.h" #include "RenameWindowArgs.g.h" +#include "OpenWorkspaceArgs.g.h" #include "SearchForTextArgs.g.h" #include "GlobalSummonArgs.g.h" #include "FocusPaneArgs.g.h" @@ -246,6 +247,10 @@ protected: \ #define RENAME_WINDOW_ARGS(X) \ X(winrt::hstring, Name, "name", false, ArgTypeHint::None, L"") +//////////////////////////////////////////////////////////////////////////////// +#define OPEN_WORKSPACE_ARGS(X) \ + X(winrt::hstring, Name, "name", false, ArgTypeHint::None, L"") + //////////////////////////////////////////////////////////////////////////////// #define SEARCH_FOR_TEXT_ARGS(X) \ X(winrt::hstring, QueryUrl, "queryUrl", false, ArgTypeHint::None, L"") @@ -940,6 +945,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARGS_STRUCT(RenameWindowArgs, RENAME_WINDOW_ARGS); + ACTION_ARGS_STRUCT(OpenWorkspaceArgs, OPEN_WORKSPACE_ARGS); + ACTION_ARGS_STRUCT(SearchForTextArgs, SEARCH_FOR_TEXT_ARGS); struct GlobalSummonArgs : public GlobalSummonArgsT @@ -1059,6 +1066,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(SetMaximizedArgs); BASIC_FACTORY(SetColorSchemeArgs); BASIC_FACTORY(RenameWindowArgs); + BASIC_FACTORY(OpenWorkspaceArgs); BASIC_FACTORY(ExecuteCommandlineArgs); BASIC_FACTORY(CloseOtherTabsArgs); BASIC_FACTORY(CloseTabsAfterArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 67c241e239b..f2326813e22 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -420,6 +420,12 @@ namespace Microsoft.Terminal.Settings.Model String Name { get; }; }; + [default_interface] runtimeclass OpenWorkspaceArgs : IActionArgs, IActionArgsDescriptorAccess + { + OpenWorkspaceArgs(String name); + String Name { get; }; + }; + [default_interface] runtimeclass SearchForTextArgs : IActionArgs, IActionArgsDescriptorAccess { String QueryUrl { get; }; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 062d303c33b..61f14a2b04e 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -162,6 +162,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") }, }; }(); diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index b9855d9e308..d65c8568328 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 ed2f69d8831..3276447e039 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -20,6 +20,7 @@ static constexpr std::string_view TabLayoutKey{ "tabLayout" }; static constexpr std::string_view InitialPositionKey{ "initialPosition" }; static constexpr std::string_view InitialSizeKey{ "initialSize" }; static constexpr std::string_view LaunchModeKey{ "launchMode" }; +static constexpr std::string_view PersistedWorkspacesKey{ "persistedWorkspaces" }; namespace Microsoft::Terminal::Settings::Model::JsonUtils { @@ -276,6 +277,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + + // Manually handled because IMap has a comma that breaks the X-macro. + if (WI_IsFlagSet(parseSource, FileSource::Local)) + state->PersistedWorkspaces = JsonUtils::GetValueForKey>>(root, PersistedWorkspacesKey); } Json::Value ApplicationState::ToJson(FileSource parseSource) const noexcept @@ -298,6 +303,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + + // Manually handled because IMap has a comma that breaks the X-macro. + if (WI_IsFlagSet(parseSource, FileSource::Local)) + JsonUtils::SetValueForKey(root, PersistedWorkspacesKey, state->PersistedWorkspaces); } return root; } @@ -341,6 +350,114 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return false; } + void ApplicationState::SaveWorkspace(const hstring& name, const Model::WindowLayout& layout) + { + { + const auto state = _state.lock(); + if (!state->PersistedWorkspaces || !*state->PersistedWorkspaces) + { + state->PersistedWorkspaces = winrt::single_threaded_map(); + } + (*state->PersistedWorkspaces).Insert(name, layout); + } + _throttler(); + } + + bool ApplicationState::RemoveWorkspace(const hstring& name) + { + bool removed{ false }; + { + const auto state = _state.lock(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + auto map = *state->PersistedWorkspaces; + if (map.HasKey(name)) + { + map.Remove(name); + removed = true; + } + } + } + if (removed) + { + _throttler(); + } + return removed; + } + + // Method Description: + // - Rename a persisted workspace entry from oldName to newName. If there + // was no entry for oldName, this is a no-op. If an entry for newName + // already exists, it will be overwritten with the layout from oldName. + // Return Value: + // - true if an entry was renamed, false otherwise. + bool ApplicationState::RenameWorkspace(const hstring& oldName, const hstring& newName) + { + if (oldName == newName || oldName.empty() || newName.empty()) + { + return false; + } + + bool renamed{ false }; + { + const auto state = _state.lock(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + auto map = *state->PersistedWorkspaces; + if (map.HasKey(oldName)) + { + const auto layout = map.Lookup(oldName); + map.Insert(newName, layout); + map.Remove(oldName); + renamed = true; + } + } + } + if (renamed) + { + _throttler(); + } + return renamed; + } + + // Method Description: + // - Atomically remove and return a persisted workspace entry. This is the + // intended API for the startup path that restores a named workspace, + // because it guarantees only one caller can claim a given workspace. + // Return Value: + // - The layout that was stored under `name`, or nullptr if there was none. + Model::WindowLayout ApplicationState::TakeWorkspace(const hstring& name) + { + Model::WindowLayout result{ nullptr }; + { + const auto state = _state.lock(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + auto map = *state->PersistedWorkspaces; + if (map.HasKey(name)) + { + result = map.Lookup(name); + map.Remove(name); + } + } + } + if (result) + { + _throttler(); + } + return result; + } + + Windows::Foundation::Collections::IMapView ApplicationState::AllPersistedWorkspaces() + { + const auto state = _state.lock_shared(); + if (state->PersistedWorkspaces && *state->PersistedWorkspaces) + { + return (*state->PersistedWorkspaces).GetView(); + } + return nullptr; + } + // Generate all getter/setters #define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \ type ApplicationState::name() const noexcept \ diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index f998fc5ead4..5139b5f3438 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -16,7 +16,7 @@ Module Name: #include "WindowLayout.g.h" #include -#include +#include "JsonUtils.h" namespace winrt::Microsoft::Terminal::Settings::Model::implementation { @@ -75,6 +75,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation bool DismissBadge(const hstring& badgeId); bool BadgeDismissed(const hstring& badgeId) const; + void SaveWorkspace(const hstring& name, const Model::WindowLayout& layout); + bool RemoveWorkspace(const hstring& name); + bool RenameWorkspace(const hstring& oldName, const hstring& newName); + Model::WindowLayout TakeWorkspace(const hstring& name); + Windows::Foundation::Collections::IMapView AllPersistedWorkspaces(); + // State getters/setters #define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \ type name() const noexcept; \ @@ -88,6 +94,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation #define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) std::optional name{ __VA_ARGS__ }; MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + // Manually declared because IMap has a comma that breaks the macro. + std::optional> PersistedWorkspaces; }; til::shared_mutex _state; std::filesystem::path _sharedPath; diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.idl b/src/cascadia/TerminalSettingsModel/ApplicationState.idl index f4e85d3a0bf..8c067ff3042 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.idl +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.idl @@ -36,6 +36,12 @@ namespace Microsoft.Terminal.Settings.Model Boolean DismissBadge(String badgeId); Boolean BadgeDismissed(String badgeId); + void SaveWorkspace(String name, WindowLayout layout); + Boolean RemoveWorkspace(String name); + Boolean RenameWorkspace(String oldName, String newName); + WindowLayout TakeWorkspace(String name); + Windows.Foundation.Collections.IMapView AllPersistedWorkspaces(); + String SettingsHash; Windows.Foundation.Collections.IVector PersistedWindowLayouts; Windows.Foundation.Collections.IVector RecentCommands; diff --git a/src/cascadia/TerminalSettingsModel/IInheritable.h b/src/cascadia/TerminalSettingsModel/IInheritable.h index aae47f61db5..f25e3e81bb0 100644 --- a/src/cascadia/TerminalSettingsModel/IInheritable.h +++ b/src/cascadia/TerminalSettingsModel/IInheritable.h @@ -136,7 +136,7 @@ private: \ return std::nullopt; \ } \ \ - auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \ + auto _get##name##OverrideSourceImpl() -> decltype(get_strong()) \ { \ /*we have a value*/ \ if (_##name) \ @@ -159,7 +159,7 @@ private: \ } \ \ auto _get##name##OverrideSourceAndValueImpl() \ - ->std::pair \ + -> std::pair \ { \ /*we have a value*/ \ if (_##name) \ diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index e97e6774bfe..948fb461690 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -161,7 +161,8 @@ Author(s): X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \ X(bool, RainbowFrame, "experimental.rainbowFrame", false) \ - X(bool, UseMica, "useMica", false) + X(bool, UseMica, "useMica", false) \ + X(bool, ShowWindowsButton, "showWindowsButton", true) #define MTSM_THEME_SETTINGS_SETTINGS(X) \ X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "theme", winrt::Windows::UI::Xaml::ElementTheme::Default) diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index a21afe9e882..b4a9e86e17f 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 9cf0806f25d..2db209b89e8 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.idl +++ b/src/cascadia/TerminalSettingsModel/Theme.idl @@ -62,6 +62,7 @@ namespace Microsoft.Terminal.Settings.Model Windows.UI.Xaml.ElementTheme RequestedTheme { get; }; Boolean UseMica { get; }; Boolean RainbowFrame { get; }; + Boolean ShowWindowsButton { get; }; ThemeColor Frame { get; }; ThemeColor UnfocusedFrame { get; }; } diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 44efab04ca0..8971d3d3992 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 00000000000..cc44ed9e14c --- /dev/null +++ b/src/cascadia/UnitTests_SettingsModel/ApplicationStateTests.cpp @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/ApplicationState.h" + +using namespace Microsoft::Console; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; +using namespace winrt::Microsoft::Terminal::Settings::Model; + +namespace SettingsModelUnitTests +{ + // Covers the workspace-persistence APIs added to ApplicationState: + // SaveWorkspace / RemoveWorkspace / RenameWorkspace / TakeWorkspace / + // AllPersistedWorkspaces. + // All tests operate on a throw-away ApplicationState instance pointed at + // a temp directory, so they don't touch the real user state. + class ApplicationStateTests + { + TEST_CLASS(ApplicationStateTests); + + TEST_METHOD(SaveAndLookupWorkspace); + TEST_METHOD(RemoveWorkspaceReturnsFalseWhenMissing); + TEST_METHOD(RenameWorkspaceMigratesEntry); + TEST_METHOD(RenameWorkspaceNoOpForEmptyOrEqualNames); + TEST_METHOD(RenameWorkspaceNoOpForMissingEntry); + TEST_METHOD(TakeWorkspaceRemovesAndReturns); + TEST_METHOD(TakeWorkspaceReturnsNullWhenMissing); + + private: + static std::filesystem::path _tempRoot() + { + auto root = std::filesystem::temp_directory_path() / L"WT_ApplicationStateTests"; + std::error_code ec; + std::filesystem::create_directories(root, ec); + // Best-effort clean of any leftover state.json from a prior run so + // tests see an empty starting point. + std::filesystem::remove(root / L"state.json", ec); + std::filesystem::remove(root / L"elevated-state.json", ec); + return root; + } + + static winrt::com_ptr _make() + { + return winrt::make_self(_tempRoot()); + } + + static WindowLayout _makeLayout() + { + WindowLayout layout; + layout.TabLayout(winrt::single_threaded_vector()); + return layout; + } + }; + + void ApplicationStateTests::SaveAndLookupWorkspace() + { + auto state = _make(); + const auto layout = _makeLayout(); + state->SaveWorkspace(L"win1", layout); + + const auto all = state->AllPersistedWorkspaces(); + VERIFY_IS_NOT_NULL(all); + VERIFY_IS_TRUE(all.HasKey(L"win1")); + } + + void ApplicationStateTests::RemoveWorkspaceReturnsFalseWhenMissing() + { + auto state = _make(); + VERIFY_IS_FALSE(state->RemoveWorkspace(L"does-not-exist")); + + state->SaveWorkspace(L"win1", _makeLayout()); + VERIFY_IS_TRUE(state->RemoveWorkspace(L"win1")); + VERIFY_IS_FALSE(state->RemoveWorkspace(L"win1")); + } + + void ApplicationStateTests::RenameWorkspaceMigratesEntry() + { + auto state = _make(); + state->SaveWorkspace(L"oldName", _makeLayout()); + + VERIFY_IS_TRUE(state->RenameWorkspace(L"oldName", L"newName")); + + const auto all = state->AllPersistedWorkspaces(); + VERIFY_IS_NOT_NULL(all); + VERIFY_IS_FALSE(all.HasKey(L"oldName")); + VERIFY_IS_TRUE(all.HasKey(L"newName")); + } + + void ApplicationStateTests::RenameWorkspaceNoOpForEmptyOrEqualNames() + { + auto state = _make(); + state->SaveWorkspace(L"win1", _makeLayout()); + + VERIFY_IS_FALSE(state->RenameWorkspace(L"win1", L"win1")); + VERIFY_IS_FALSE(state->RenameWorkspace(L"", L"win2")); + VERIFY_IS_FALSE(state->RenameWorkspace(L"win1", L"")); + } + + void ApplicationStateTests::RenameWorkspaceNoOpForMissingEntry() + { + auto state = _make(); + VERIFY_IS_FALSE(state->RenameWorkspace(L"missing", L"newName")); + } + + void ApplicationStateTests::TakeWorkspaceRemovesAndReturns() + { + auto state = _make(); + state->SaveWorkspace(L"win1", _makeLayout()); + + const auto taken = state->TakeWorkspace(L"win1"); + VERIFY_IS_NOT_NULL(taken); + + // Subsequent Take for the same name must return null — this is the + // atomicity guarantee the startup path relies on. + VERIFY_IS_NULL(state->TakeWorkspace(L"win1")); + } + + void ApplicationStateTests::TakeWorkspaceReturnsNullWhenMissing() + { + auto state = _make(); + VERIFY_IS_NULL(state->TakeWorkspace(L"missing")); + } +} diff --git a/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj b/src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj index 4b488c2dca8..8249f7ca2aa 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 4cad433c611..4066fff8dbb 100644 --- a/src/cascadia/UnitTests_SettingsModel/pch.h +++ b/src/cascadia/UnitTests_SettingsModel/pch.h @@ -66,6 +66,8 @@ Author(s): // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" #include +#include +#include // Common includes for most tests: #include "../../inc/conattrs.hpp" diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 0fa2f61ae4a..e3474ecdbc9 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -132,7 +132,12 @@ void AppHost::_HandleCommandlineArgs(const winrt::TerminalApp::WindowRequestedAr // We don't have XAML yet, but we do have other stuff. _windowLogic = _appLogic.CreateNewWindow(); - if (const auto content = windowArgs.Content(); !content.empty()) + if (const auto layout = windowArgs.PersistedLayout()) + { + _windowLogic.SetPersistedLayout(layout); + _launchShowWindowCommand = SW_NORMAL; + } + else if (const auto content = windowArgs.Content(); !content.empty()) { _windowLogic.SetStartupContent(content, windowArgs.InitialBounds()); _launchShowWindowCommand = SW_NORMAL; @@ -265,12 +270,14 @@ 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 }); // BODGY // On certain builds of Windows, when Terminal is set as the default @@ -410,6 +417,28 @@ void AppHost::_HandleRequestLaunchPosition(const winrt::Windows::Foundation::IIn args.Position(_GetWindowLaunchPosition()); } +void AppHost::_HandleRequestWindowList(const winrt::Windows::Foundation::IInspectable& /*sender*/, + winrt::TerminalApp::WindowListRequest args) +{ + // Ask the Emperor (on the main thread) for the current window list. + // SendMessage blocks until the message is processed, so this is + // synchronous and the results vector is filled in-place. + std::vector entries; + SendMessage(_windowManager->GetMainWindow(), + WindowEmperor::WM_GET_WINDOW_LIST, + 0, + reinterpret_cast(&entries)); + + auto windowEntries = args.Entries(); + for (const auto& entry : entries) + { + winrt::TerminalApp::WindowListEntry w; + w.Id(entry.Id); + w.Name(winrt::hstring{ entry.Name }); + windowEntries.Append(w); + } +} + LaunchPosition AppHost::_GetWindowLaunchPosition() { LaunchPosition pos{}; @@ -1065,6 +1094,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 ffc16d916b9..061c9fc44ea 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -91,6 +91,9 @@ class AppHost : public std::enable_shared_from_this void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); + void _SummonWindowByIdRequested(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::TerminalApp::SummonWindowByIdRequestedArgs& args); + void _FocusTabRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::Tab& tab); @@ -133,6 +136,8 @@ class AppHost : public std::enable_shared_from_this void _AppTitleChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&); void _HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& sender, winrt::TerminalApp::LaunchPositionRequest args); + void _HandleRequestWindowList(const winrt::Windows::Foundation::IInspectable& sender, + winrt::TerminalApp::WindowListRequest args); // Helper struct. By putting these all into one struct, we can revoke them // all at once, by assigning _revokers to a fresh Revokers instance. That'll @@ -154,6 +159,7 @@ class AppHost : public std::enable_shared_from_this winrt::TerminalApp::TerminalWindow::IdentifyWindowsRequested_revoker IdentifyWindowsRequested; winrt::TerminalApp::TerminalWindow::IsQuakeWindowChanged_revoker IsQuakeWindowChanged; winrt::TerminalApp::TerminalWindow::SummonWindowRequested_revoker SummonWindowRequested; + winrt::TerminalApp::TerminalWindow::SummonWindowByIdRequested_revoker SummonWindowByIdRequested; winrt::TerminalApp::TerminalWindow::FocusTabRequested_revoker FocusTabRequested; winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu; winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested; @@ -161,6 +167,7 @@ class AppHost : public std::enable_shared_from_this winrt::TerminalApp::TerminalWindow::RequestMoveContent_revoker RequestMoveContent; winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent; winrt::TerminalApp::TerminalWindow::RequestLaunchPosition_revoker RequestLaunchPosition; + winrt::TerminalApp::TerminalWindow::RequestWindowList_revoker RequestWindowList; winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged; winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged; winrt::TerminalApp::TerminalWindow::WindowSizeChanged_revoker WindowSizeChanged; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 9fa13c1b1c0..73d898e1a44 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -803,6 +803,19 @@ void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs arg { winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) }; request.WindowName(std::move(windowName)); + + // If we're opening a named window that doesn't exist yet, atomically + // claim any persisted workspace with that name so we restore it here + // and no subsequent window can pick up the same entry. + const auto& reqName = request.WindowName(); + if (!reqName.empty()) + { + if (const auto layout = ApplicationState::SharedInstance().TakeWorkspace(reqName)) + { + request.PersistedLayout(layout); + } + } + CreateNewWindow(std::move(request)); } } @@ -1087,6 +1100,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 +1143,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 80d87023d7c..8ef213bfa60 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -28,6 +28,16 @@ class WindowEmperor WM_MESSAGE_BOX_CLOSED, WM_IDENTIFY_ALL_WINDOWS, WM_NOTIFY_FROM_NOTIFICATION_AREA, + WM_GET_WINDOW_LIST, + }; + + // Used by WM_GET_WINDOW_LIST. Callers allocate a vector on their + // stack and pass a pointer through LPARAM; the emperor fills it in + // synchronously via SendMessage. + struct WindowListEntry + { + uint64_t Id; + std::wstring Name; }; HWND GetMainWindow() const noexcept; diff --git a/src/inc/til/mutex.h b/src/inc/til/mutex.h index e4299d31cfc..b1fd20ab54e 100644 --- a/src/inc/til/mutex.h +++ b/src/inc/til/mutex.h @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +#pragma once + namespace til { namespace details diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index b9c80ce4579..1d51f4c5d01 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -416,7 +416,7 @@ function Invoke-CodeFormat() { [switch]$IgnoreXaml ) - $clangFormatPath = & 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -find "**\x64\bin\clang-format.exe" + $clangFormatPath = & 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -prerelease -find "**\x64\bin\clang-format.exe" If ([String]::IsNullOrEmpty($clangFormatPath)) { Write-Error "No Visual Studio-supplied version of clang-format could be found." } diff --git a/tools/runformat.cmd b/tools/runformat.cmd index d9c1d56c73f..336ce122378 100644 --- a/tools/runformat.cmd +++ b/tools/runformat.cmd @@ -2,4 +2,4 @@ rem run clang-format on c++ files -powershell -noprofile "import-module %OPENCON_TOOLS%\openconsole.psm1; Invoke-CodeFormat" +pwsh -noprofile -c "import-module %OPENCON_TOOLS%\openconsole.psm1; Invoke-CodeFormat"