Add support for "workspaces" based on window names (redux) (#20314)

This adds a new feature to the Windows Terminal: "Workspaces"

Workspaces are very shamelessly inspired by Edge workspaces of the same
name.

The core idea is that when users name a window and they close that
window, we will persist that Windows layout and buffers, seperately from
the rest of window restoration. So a user can open a named window, open
some profiles, some panes, do some stuff in it, then close it, and we
will keep that state around for the next time the user opens that window
name.

Unnamed windows still behave the same. If you close an unnamed window,
and it's not the last window, then we won't persist the state of it.

To facilitate restoring named windows, we add a `openWorkspace` action.
This allows us to persist the open workspace action in the window layout
restoration path. So when we deserialize the list of tab layouts, and
open workspace action will tell us, hey, go retrieve this known
workspace from the state.json, instead of trying to serialize the window
state in two places.

As demoed in the video, we add a flyout to list the windows that the
user has open, and the named workspaces that they have saved. This
allows users to quickly reopen previously closed workspaces, as well as
quickly rename a window, thereby adding it to the list of saved
workspaces. This button can also be hidden using the theme settings.

Closes #17084

(this is a reboot of #20162, but with the event from #20311)
This commit is contained in:
Mike Griese
2026-06-16 16:59:17 -05:00
committed by GitHub
parent 94dfb15916
commit 1282d56a33
37 changed files with 1094 additions and 21 deletions

View File

@@ -441,6 +441,7 @@
"openTabColorPicker", "openTabColorPicker",
"openTabRenamer", "openTabRenamer",
"openWindowRenamer", "openWindowRenamer",
"openWorkspace",
"paste", "paste",
"prevTab", "prevTab",
"quakeMode", "quakeMode",
@@ -479,6 +480,7 @@
"toggleReadOnlyMode", "toggleReadOnlyMode",
"toggleShaderEffects", "toggleShaderEffects",
"toggleSplitOrientation", "toggleSplitOrientation",
"workspaces",
"wt", "wt",
"unbound" "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": { "FocusPaneAction": {
"description": "Arguments corresponding to a focusPane Action", "description": "Arguments corresponding to a focusPane Action",
"allOf": [ "allOf": [
@@ -2040,6 +2064,11 @@
"unfocusedFrame": { "unfocusedFrame": {
"description": "The color of the window frame when the window is inactive. This only works on Windows 11", "description": "The color of the window frame when the window is inactive. This only works on Windows 11",
"$ref": "#/$defs/ThemeColor" "$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/RenameWindowAction"
}, },
{
"$ref": "#/$defs/OpenWorkspaceAction"
},
{ {
"$ref": "#/$defs/FocusPaneAction" "$ref": "#/$defs/FocusPaneAction"
}, },

View File

@@ -895,6 +895,16 @@ namespace winrt::TerminalApp::implementation
RequestNewWindow.raise(*this, request); 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<implementation::OpenWindowRequestedArgs>(name);
RequestOpenWindow.raise(*this, args);
}
void TerminalPage::_HandleNewWindow(const IInspectable& /*sender*/, void TerminalPage::_HandleNewWindow(const IInspectable& /*sender*/,
const ActionEventArgs& actionArgs) const ActionEventArgs& actionArgs)
{ {
@@ -916,7 +926,10 @@ namespace winrt::TerminalApp::implementation
newContentArgs = NewTerminalArgs{}; 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<NewTerminalArgs>() }) if (const auto terminalArgs{ newContentArgs.try_as<NewTerminalArgs>() })
{ {
const auto profile{ _settings.GetProfileForArgs(terminalArgs) }; const auto profile{ _settings.GetProfileForArgs(terminalArgs) };
@@ -1015,6 +1028,7 @@ namespace winrt::TerminalApp::implementation
// Fun! // Fun!
// WindowRenamerTextBox().Focus(FocusState::Programmatic); // WindowRenamerTextBox().Focus(FocusState::Programmatic);
_renamerLayoutUpdatedRevoker.revoke(); _renamerLayoutUpdatedRevoker.revoke();
_renamerLayoutCount = 0;
_renamerLayoutUpdatedRevoker = WindowRenamerTextBox().LayoutUpdated(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) { _renamerLayoutUpdatedRevoker = WindowRenamerTextBox().LayoutUpdated(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) {
if (auto self{ weakThis.get() }) if (auto self{ weakThis.get() })
{ {
@@ -1590,4 +1604,35 @@ namespace winrt::TerminalApp::implementation
args.Handled(handled); args.Handled(handled);
} }
} }
void TerminalPage::_HandleOpenWorkspace(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
// Open (or summon) a named window. We launch a new `wt -w <name>`
// 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<OpenWorkspaceArgs>())
{
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);
}
} }

View File

@@ -92,6 +92,10 @@ INewContentArgs Pane::GetTerminalArgsForPane(BuildStartupKind kind) const
{ {
// Leaves are the only things that have controls // Leaves are the only things that have controls
assert(_IsLeaf()); assert(_IsLeaf());
if (!_content)
{
return nullptr;
}
return _content.GetNewTerminalArgs(kind); return _content.GetNewTerminalArgs(kind);
} }

View File

@@ -85,6 +85,7 @@ namespace winrt::TerminalApp::implementation
WINRT_PROPERTY(TerminalApp::CommandlineArgs, Command, nullptr); WINRT_PROPERTY(TerminalApp::CommandlineArgs, Command, nullptr);
WINRT_PROPERTY(winrt::hstring, Content); WINRT_PROPERTY(winrt::hstring, Content);
WINRT_PROPERTY(Windows::Foundation::IReference<Windows::Foundation::Rect>, InitialBounds); WINRT_PROPERTY(Windows::Foundation::IReference<Windows::Foundation::Rect>, InitialBounds);
WINRT_PROPERTY(winrt::Microsoft::Terminal::Settings::Model::WindowLayout, PersistedLayout, nullptr);
WINRT_PROPERTY(Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>, StartupActions, nullptr); WINRT_PROPERTY(Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>, StartupActions, nullptr);
}; };
} }

View File

@@ -51,6 +51,7 @@ namespace TerminalApp
CommandlineArgs Command { get; }; CommandlineArgs Command { get; };
String Content { get; }; String Content { get; };
Windows.Foundation.IReference<Windows.Foundation.Rect> InitialBounds { get; }; Windows.Foundation.IReference<Windows.Foundation.Rect> InitialBounds { get; };
Microsoft.Terminal.Settings.Model.WindowLayout PersistedLayout;
Windows.Foundation.Collections.IVector<Microsoft.Terminal.Settings.Model.ActionAndArgs> StartupActions; Windows.Foundation.Collections.IVector<Microsoft.Terminal.Settings.Model.ActionAndArgs> StartupActions;
}; };
} }

View File

@@ -743,6 +743,42 @@
<value>unnamed window</value> <value>unnamed window</value>
<comment>text used to identify when a window hasn't been assigned a name by the user</comment> <comment>text used to identify when a window hasn't been assigned a name by the user</comment>
</data> </data>
<data name="NameThisWindowMenuItem" xml:space="preserve">
<value>Name this window...</value>
<comment>Menu item text shown when the current window has no name assigned</comment>
</data>
<data name="RenameThisWindowMenuItem" xml:space="preserve">
<value>Rename this window...</value>
<comment>Menu item text shown when the current window already has a name</comment>
</data>
<data name="WindowListUnnamedEntry" xml:space="preserve">
<value>#{0} (unnamed)</value>
<comment>{locked="{0}"}{0} is the window ID number. Shown in the workspace flyout for windows that have no name assigned.</comment>
</data>
<data name="DeleteWorkspaceMenuItem" xml:space="preserve">
<value>Delete workspace?</value>
<comment>Menu item text shown in the right-click context menu on a saved workspace</comment>
</data>
<data name="OpenWorkspaceMenuItem" xml:space="preserve">
<value>Open workspace</value>
<comment>Menu item text for opening a saved workspace</comment>
</data>
<data name="ConfirmDeleteWorkspaceTitle" xml:space="preserve">
<value>Delete "{0}"?</value>
<comment>{locked="{0}"}{0} is the workspace name. Shown as the title of a confirmation dialog when the user tries to delete a saved workspace.</comment>
</data>
<data name="ConfirmDeleteWorkspaceBody" xml:space="preserve">
<value>Are you sure you want to delete "{0}"? Sessions associated with this workspace will also be deleted.</value>
<comment>{locked="{0}"}{0} is the workspace name. Body text of the confirmation dialog shown when the user tries to delete a saved workspace.</comment>
</data>
<data name="ConfirmDeleteWorkspaceDelete" xml:space="preserve">
<value>Delete</value>
<comment>Primary button text on the delete workspace confirmation dialog</comment>
</data>
<data name="ConfirmDeleteWorkspaceCancel" xml:space="preserve">
<value>Cancel</value>
<comment>Cancel button text on the delete workspace confirmation dialog</comment>
</data>
<data name="WindowRenamer.Subtitle" xml:space="preserve"> <data name="WindowRenamer.Subtitle" xml:space="preserve">
<value>Enter a new name:</value> <value>Enter a new name:</value>
</data> </data>
@@ -801,6 +837,9 @@
<data name="ElevationShield.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve"> <data name="ElevationShield.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>This Terminal window is running as administrator</value> <value>This Terminal window is running as administrator</value>
</data> </data>
<data name="WorkspaceDropdown.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Workspaces</value>
</data>
<data name="CommandPalette_MatchesAvailable" xml:space="preserve"> <data name="CommandPalette_MatchesAvailable" xml:space="preserve">
<value>Suggestions found: {0}</value> <value>Suggestions found: {0}</value>
<comment>{0} will be replaced with a number.</comment> <comment>{0} will be replaced with a number.</comment>

View File

@@ -403,6 +403,22 @@ namespace winrt::TerminalApp::implementation
_previouslyClosedPanesAndTabs.emplace_back(args); _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: // Method Description:
// - Removes the tab (both TerminalControl and XAML) after prompting for approval // - Removes the tab (both TerminalControl and XAML) after prompting for approval
// Arguments: // Arguments:
@@ -450,6 +466,14 @@ namespace winrt::TerminalApp::implementation
auto actions = t->BuildStartupActions(BuildStartupKind::None); auto actions = t->BuildStartupActions(BuildStartupKind::None);
_AddPreviouslyClosedPaneOrTab(std::move(actions)); _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(); tab.Close();
} }
@@ -471,6 +495,13 @@ namespace winrt::TerminalApp::implementation
const auto focusedTabIndex{ _GetFocusedTabIndex() }; 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, // 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. // but it doesn't always do so. The UI tree may still be holding the control and preventing its destruction.
tab.Shutdown(); tab.Shutdown();
@@ -798,6 +829,21 @@ namespace winrt::TerminalApp::implementation
} }
_AddPreviouslyClosedPaneOrTab(std::move(state.args)); _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 // If specified, detach before closing to directly update the pane structure
pane->Close(); pane->Close();
} }

View File

@@ -19,6 +19,8 @@ namespace winrt::TerminalApp::implementation
til::property_changed_event PropertyChanged; til::property_changed_event PropertyChanged;
WINRT_OBSERVABLE_PROPERTY(bool, ShowElevationShield, PropertyChanged.raise, false); 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"");
}; };
} }

View File

@@ -9,5 +9,7 @@ namespace TerminalApp
TabRowControl(); TabRowControl();
Microsoft.UI.Xaml.Controls.TabView TabView { get; }; Microsoft.UI.Xaml.Controls.TabView TabView { get; };
Boolean ShowElevationShield; Boolean ShowElevationShield;
Boolean ShowWorkspacesButton;
String WorkspaceName;
} }
} }

View File

@@ -8,6 +8,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp" xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:mux="using:Microsoft.UI.Xaml.Controls" xmlns:mux="using:Microsoft.UI.Xaml.Controls"
Background="{ThemeResource TabViewBackground}" Background="{ThemeResource TabViewBackground}"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -35,14 +36,45 @@
TabWidthMode="Equal"> TabWidthMode="Equal">
<mux:TabView.TabStripHeader> <mux:TabView.TabStripHeader>
<!-- EA18 is the "Shield" glyph --> <StackPanel Orientation="Horizontal">
<FontIcon x:Uid="ElevationShield" <!-- EA18 is the "Shield" glyph -->
Margin="9,4,0,4" <FontIcon x:Uid="ElevationShield"
FontFamily="{ThemeResource SymbolThemeFontFamily}" Margin="9,4,0,4"
FontSize="16" FontFamily="{ThemeResource SymbolThemeFontFamily}"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" FontSize="16"
Glyph="&#xEA18;" Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Visibility="{x:Bind ShowElevationShield, Mode=OneWay}" /> Glyph="&#xEA18;"
Visibility="{x:Bind ShowElevationShield, Mode=OneWay}" />
<!-- Workspace/windows button -->
<Button x:Name="WorkspaceDropdown"
x:Uid="WorkspaceDropdown"
Margin="4,0,0,4"
Padding="8,0,0,0"
VerticalAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
Visibility="{x:Bind ShowWorkspacesButton, Mode=OneWay}">
<Button.Content>
<StackPanel Orientation="Horizontal"
Spacing="8">
<!-- EE40 is the "TaskViewSettings" glyph -->
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xEE40;" />
<TextBlock x:Name="WorkspaceNameText"
Padding="0,0,8,0"
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind WorkspaceName, Mode=OneWay}"
Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(WorkspaceName), Mode=OneWay}" />
</StackPanel>
</Button.Content>
<Button.Flyout>
<MenuFlyout x:Name="WorkspaceFlyout" />
</Button.Flyout>
</Button>
</StackPanel>
</mux:TabView.TabStripHeader> </mux:TabView.TabStripHeader>
<mux:TabView.TabStripFooter> <mux:TabView.TabStripFooter>

View File

@@ -25,7 +25,10 @@
#include "TerminalSettingsCache.h" #include "TerminalSettingsCache.h"
#include "LaunchPositionRequest.g.cpp" #include "LaunchPositionRequest.g.cpp"
#include "WindowListEntry.g.cpp"
#include "WindowListRequest.g.cpp"
#include "RenameWindowRequestedArgs.g.cpp" #include "RenameWindowRequestedArgs.g.cpp"
#include "OpenWindowRequestedArgs.g.cpp"
#include "RequestMoveContentArgs.g.cpp" #include "RequestMoveContentArgs.g.cpp"
#include "TerminalPage.g.cpp" #include "TerminalPage.g.cpp"
@@ -334,6 +337,21 @@ namespace winrt::TerminalApp::implementation
auto tabRowImpl = winrt::get_self<implementation::TabRowControl>(_tabRow); auto tabRowImpl = winrt::get_self<implementation::TabRowControl>(_tabRow);
_newTabButton = tabRowImpl->NewTabButton(); _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()) if (_settings.GlobalSettings().ShowTabsInTitlebar())
{ {
@@ -443,6 +461,12 @@ namespace winrt::TerminalApp::implementation
_tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); _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<ThrottledFunc<>>( _adjustProcessPriorityThrottled = std::make_shared<ThrottledFunc<>>(
DispatcherQueue::GetForCurrentThread(), DispatcherQueue::GetForCurrentThread(),
til::throttled_func_options{ til::throttled_func_options{
@@ -2285,14 +2309,14 @@ namespace winrt::TerminalApp::implementation
QuitRequested.raise(nullptr, nullptr); 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. // 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. // We shouldn't persist such windows.
const auto tabCount = _tabs.Size(); const auto tabCount = _tabs.Size();
if (_startupState != StartupState::Initialized || tabCount == 0) if (_startupState != StartupState::Initialized || tabCount == 0)
{ {
return; return nullptr;
} }
std::vector<ActionAndArgs> actions; std::vector<ActionAndArgs> actions;
@@ -2307,7 +2331,7 @@ namespace winrt::TerminalApp::implementation
// Avoid persisting a window with zero tabs, because `BuildStartupActions` happened to return an empty vector. // Avoid persisting a window with zero tabs, because `BuildStartupActions` happened to return an empty vector.
if (actions.empty()) if (actions.empty())
{ {
return; return nullptr;
} }
// if the focused tab was not the last tab, restore that // if the focused tab was not the last tab, restore that
@@ -2356,7 +2380,49 @@ namespace winrt::TerminalApp::implementation
RequestLaunchPosition.raise(*this, launchPosRequest); RequestLaunchPosition.raise(*this, launchPosRequest);
layout.InitialPosition(launchPosRequest.Position()); 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<ActionAndArgs> 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<ActionAndArgs>(std::move(actions)));
ApplicationState::SharedInstance().AppendPersistedWindowLayout(stub);
}
else
{
ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout);
}
}
} }
// Method Description: // Method Description:
@@ -4057,6 +4123,12 @@ namespace winrt::TerminalApp::implementation
_tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); _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() }; Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() };
_tabView.Background(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<WindowListRequest>() };
RequestWindowList.raise(*this, windowListReq);
const auto windowEntries = windowListReq.Entries();
std::set<winrt::hstring> 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<SummonWindowByIdRequestedArgs>(id));
}
});
}
_workspaceFlyout.Items().Append(item);
}
}
}
// Handler for our WindowProperties's PropertyChanged event. We'll use this // Handler for our WindowProperties's PropertyChanged event. We'll use this
// to pop the "Identify Window" toast when the user renames our window. // to pop the "Identify Window" toast when the user renames our window.
void TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, const WUX::Data::PropertyChangedEventArgs& args) void TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, const WUX::Data::PropertyChangedEventArgs& args)
@@ -5706,6 +5968,10 @@ namespace winrt::TerminalApp::implementation
return; 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 // DON'T display the confirmation if this is the name we were
// given on startup! // given on startup!
if (_startupState == StartupState::Initialized) if (_startupState == StartupState::Initialized)

View File

@@ -10,8 +10,12 @@
#include "AppKeyBindings.h" #include "AppKeyBindings.h"
#include "AppCommandlineArgs.h" #include "AppCommandlineArgs.h"
#include "RenameWindowRequestedArgs.g.h" #include "RenameWindowRequestedArgs.g.h"
#include "OpenWindowRequestedArgs.g.h"
#include "SummonWindowByIdRequestedArgs.g.h"
#include "RequestMoveContentArgs.g.h" #include "RequestMoveContentArgs.g.h"
#include "LaunchPositionRequest.g.h" #include "LaunchPositionRequest.g.h"
#include "WindowListEntry.g.h"
#include "WindowListRequest.g.h"
#include "Toast.h" #include "Toast.h"
#include "WindowsPackageManagerFactory.h" #include "WindowsPackageManagerFactory.h"
@@ -73,6 +77,24 @@ namespace winrt::TerminalApp::implementation
_ProposedName{ name } {}; _ProposedName{ name } {};
}; };
struct OpenWindowRequestedArgs : OpenWindowRequestedArgsT<OpenWindowRequestedArgs>
{
WINRT_PROPERTY(winrt::hstring, Name);
public:
OpenWindowRequestedArgs(const winrt::hstring& name) :
_Name{ name } {};
};
struct SummonWindowByIdRequestedArgs : SummonWindowByIdRequestedArgsT<SummonWindowByIdRequestedArgs>
{
WINRT_PROPERTY(uint64_t, WindowId);
public:
SummonWindowByIdRequestedArgs(uint64_t id) :
_WindowId{ id } {};
};
struct RequestMoveContentArgs : RequestMoveContentArgsT<RequestMoveContentArgs> struct RequestMoveContentArgs : RequestMoveContentArgsT<RequestMoveContentArgs>
{ {
WINRT_PROPERTY(winrt::hstring, Window); WINRT_PROPERTY(winrt::hstring, Window);
@@ -94,6 +116,25 @@ namespace winrt::TerminalApp::implementation
til::property<winrt::Microsoft::Terminal::Settings::Model::LaunchPosition> Position; til::property<winrt::Microsoft::Terminal::Settings::Model::LaunchPosition> Position;
}; };
struct WindowListEntry : WindowListEntryT<WindowListEntry>
{
WindowListEntry() = default;
til::property<uint64_t> Id;
til::property<winrt::hstring> Name;
};
struct WindowListRequest : WindowListRequestT<WindowListRequest>
{
WindowListRequest() :
_Entries{ winrt::single_threaded_vector<winrt::TerminalApp::WindowListEntry>() } {}
winrt::Windows::Foundation::Collections::IVector<winrt::TerminalApp::WindowListEntry> Entries() const { return _Entries; }
private:
winrt::Windows::Foundation::Collections::IVector<winrt::TerminalApp::WindowListEntry> _Entries;
};
struct WinGetSearchParams struct WinGetSearchParams
{ {
winrt::Microsoft::Management::Deployment::PackageMatchField Field; winrt::Microsoft::Management::Deployment::PackageMatchField Field;
@@ -132,6 +173,7 @@ namespace winrt::TerminalApp::implementation
safe_void_coroutine RequestQuit(); safe_void_coroutine RequestQuit();
safe_void_coroutine CloseWindow(); safe_void_coroutine CloseWindow();
winrt::Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout();
void PersistState(); void PersistState();
std::vector<IPaneContent> Panes() const; std::vector<IPaneContent> Panes() const;
@@ -203,6 +245,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<IInspectable, IInspectable> IdentifyWindowsRequested; til::typed_event<IInspectable, IInspectable> IdentifyWindowsRequested;
til::typed_event<IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs> RenameWindowRequested; til::typed_event<IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs> RenameWindowRequested;
til::typed_event<IInspectable, IInspectable> SummonWindowRequested; til::typed_event<IInspectable, IInspectable> SummonWindowRequested;
til::typed_event<IInspectable, winrt::TerminalApp::SummonWindowByIdRequestedArgs> SummonWindowByIdRequested;
til::typed_event<IInspectable, winrt::TerminalApp::Tab> FocusTabRequested; til::typed_event<IInspectable, winrt::TerminalApp::Tab> FocusTabRequested;
til::typed_event<IInspectable, winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs> WindowSizeChanged; til::typed_event<IInspectable, winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs> WindowSizeChanged;
@@ -215,6 +258,8 @@ namespace winrt::TerminalApp::implementation
til::typed_event<Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs> RequestReceiveContent; til::typed_event<Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs> RequestReceiveContent;
til::typed_event<IInspectable, winrt::TerminalApp::LaunchPositionRequest> RequestLaunchPosition; til::typed_event<IInspectable, winrt::TerminalApp::LaunchPositionRequest> RequestLaunchPosition;
til::typed_event<IInspectable, winrt::TerminalApp::WindowListRequest> RequestWindowList;
til::typed_event<IInspectable, winrt::TerminalApp::OpenWindowRequestedArgs> RequestOpenWindow;
til::typed_event<IInspectable, winrt::TerminalApp::WindowRequestedArgs> RequestNewWindow; til::typed_event<IInspectable, winrt::TerminalApp::WindowRequestedArgs> RequestNewWindow;
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); 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 }; TerminalApp::TabRowControl _tabRow{ nullptr };
Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr }; Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr };
Microsoft::UI::Xaml::Controls::SplitButton _newTabButton{ 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 }; winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr };
Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ 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 _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&);
void _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs& contentArgs); void _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs& contentArgs);
void _OpenWorkspaceWindow(const winrt::hstring name);
void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs);
@@ -365,6 +413,7 @@ namespace winrt::TerminalApp::implementation
void _CloseTabAtIndex(uint32_t index); void _CloseTabAtIndex(uint32_t index);
void _RemoveTab(const winrt::TerminalApp::Tab& tab); void _RemoveTab(const winrt::TerminalApp::Tab& tab);
safe_void_coroutine _RemoveTabs(const std::vector<winrt::TerminalApp::Tab> tabs); safe_void_coroutine _RemoveTabs(const std::vector<winrt::TerminalApp::Tab> tabs);
void _SaveWorkspaceIfNeeded();
void _InitializeTab(winrt::com_ptr<Tab> newTabImpl, uint32_t insertPosition = -1); void _InitializeTab(winrt::com_ptr<Tab> newTabImpl, uint32_t insertPosition = -1);
void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); 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 _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 _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::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex);
winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender); 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 namespace winrt::TerminalApp::factory_implementation
{ {
BASIC_FACTORY(TerminalPage); BASIC_FACTORY(TerminalPage);
BASIC_FACTORY(WindowListEntry);
} }

View File

@@ -19,6 +19,14 @@ namespace TerminalApp
{ {
String ProposedName { get; }; String ProposedName { get; };
}; };
[default_interface] runtimeclass OpenWindowRequestedArgs
{
String Name { get; };
};
[default_interface] runtimeclass SummonWindowByIdRequestedArgs
{
UInt64 WindowId { get; };
};
[default_interface] runtimeclass RequestMoveContentArgs [default_interface] runtimeclass RequestMoveContentArgs
{ {
String Window { get; }; String Window { get; };
@@ -50,6 +58,20 @@ namespace TerminalApp
Microsoft.Terminal.Settings.Model.LaunchPosition Position; 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<WindowListEntry> Entries { get; };
}
[default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, Microsoft.Terminal.UI.IDirectKeyListener [default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, Microsoft.Terminal.UI.IDirectKeyListener
{ {
TerminalPage(WindowProperties properties, ContentManager manager); TerminalPage(WindowProperties properties, ContentManager manager);
@@ -93,6 +115,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, RenameWindowRequestedArgs> RenameWindowRequested; event Windows.Foundation.TypedEventHandler<Object, RenameWindowRequestedArgs> RenameWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested; event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, SummonWindowByIdRequestedArgs> SummonWindowByIdRequested;
event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Control.WindowSizeChangedEventArgs> WindowSizeChanged; event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Control.WindowSizeChangedEventArgs> WindowSizeChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu; event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
@@ -103,6 +126,12 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent; event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent;
event Windows.Foundation.TypedEventHandler<Object, LaunchPositionRequest> RequestLaunchPosition; event Windows.Foundation.TypedEventHandler<Object, LaunchPositionRequest> RequestLaunchPosition;
event Windows.Foundation.TypedEventHandler<Object, WindowListRequest> 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<Object, OpenWindowRequestedArgs> RequestOpenWindow;
event Windows.Foundation.TypedEventHandler<Object, WindowRequestedArgs> RequestNewWindow; event Windows.Foundation.TypedEventHandler<Object, WindowRequestedArgs> RequestNewWindow;
} }

View File

@@ -258,6 +258,15 @@ namespace winrt::TerminalApp::implementation
AppLogic::Current()->NotifyRootInitialized(); AppLogic::Current()->NotifyRootInitialized();
} }
WindowLayout TerminalWindow::GetWindowLayout() const
{
if (_root)
{
return _root->GetWindowLayout();
}
return nullptr;
}
void TerminalWindow::PersistState() void TerminalWindow::PersistState()
{ {
if (_root) if (_root)
@@ -1112,6 +1121,11 @@ namespace winrt::TerminalApp::implementation
_initialContentArgs = wil::to_vector(actions); _initialContentArgs = wil::to_vector(actions);
} }
void TerminalWindow::SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout)
{
_cachedLayout = layout;
}
// Method Description: // Method Description:
// - Parse the provided commandline arguments into actions, and try to // - Parse the provided commandline arguments into actions, and try to
// perform them immediately. // perform them immediately.
@@ -1232,7 +1246,14 @@ namespace winrt::TerminalApp::implementation
void TerminalWindow::WindowName(const winrt::hstring& name) void TerminalWindow::WindowName(const winrt::hstring& name)
{ {
const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow(); const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow();
const auto oldName = _WindowProperties->WindowName();
_WindowProperties->WindowName(name); _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) if (!_root)
{ {
return; return;

View File

@@ -71,6 +71,7 @@ namespace winrt::TerminalApp::implementation
void Create(); void Create();
winrt::Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout() const;
void PersistState(); void PersistState();
void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args); void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args);
@@ -80,6 +81,7 @@ namespace winrt::TerminalApp::implementation
int32_t SetStartupCommandline(TerminalApp::CommandlineArgs args); int32_t SetStartupCommandline(TerminalApp::CommandlineArgs args);
void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference<Windows::Foundation::Rect>& contentBounds); void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference<Windows::Foundation::Rect>& contentBounds);
void SetStartupActions(const Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>& actions); void SetStartupActions(const Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>& actions);
void SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout);
int32_t ExecuteCommandline(TerminalApp::CommandlineArgs args); int32_t ExecuteCommandline(TerminalApp::CommandlineArgs args);
void SetSettingsStartupArgs(const std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>& actions); void SetSettingsStartupArgs(const std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>& 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(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(IdentifyWindowsRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IdentifyWindowsRequested);
FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested); 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(FocusTabRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::Tab, _root, FocusTabRequested);
FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu);
FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); FORWARDED_TYPED_EVENT(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(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs, _root, RequestReceiveContent);
FORWARDED_TYPED_EVENT(RequestLaunchPosition, Windows::Foundation::IInspectable, winrt::TerminalApp::LaunchPositionRequest, _root, RequestLaunchPosition); 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); FORWARDED_TYPED_EVENT(RequestNewWindow, Windows::Foundation::IInspectable, winrt::TerminalApp::WindowRequestedArgs, _root, RequestNewWindow);
#ifdef UNIT_TESTING #ifdef UNIT_TESTING

View File

@@ -57,11 +57,13 @@ namespace TerminalApp
Int32 SetStartupCommandline(CommandlineArgs args); Int32 SetStartupCommandline(CommandlineArgs args);
void SetStartupContent(String json, Windows.Foundation.IReference<Windows.Foundation.Rect> bounds); void SetStartupContent(String json, Windows.Foundation.IReference<Windows.Foundation.Rect> bounds);
void SetStartupActions(Windows.Foundation.Collections.IVector<Microsoft.Terminal.Settings.Model.ActionAndArgs> actions); void SetStartupActions(Windows.Foundation.Collections.IVector<Microsoft.Terminal.Settings.Model.ActionAndArgs> actions);
void SetPersistedLayout(Microsoft.Terminal.Settings.Model.WindowLayout layout);
Int32 ExecuteCommandline(CommandlineArgs args); Int32 ExecuteCommandline(CommandlineArgs args);
Boolean ShouldImmediatelyHandoffToElevated(); Boolean ShouldImmediatelyHandoffToElevated();
void HandoffToElevated(); void HandoffToElevated();
Microsoft.Terminal.Settings.Model.WindowLayout GetWindowLayout();
void PersistState(); void PersistState();
Windows.UI.Xaml.UIElement GetRoot(); Windows.UI.Xaml.UIElement GetRoot();
@@ -129,6 +131,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested; event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, SummonWindowByIdRequestedArgs> SummonWindowByIdRequested;
event Windows.Foundation.TypedEventHandler<Object, TerminalApp.Tab> FocusTabRequested; event Windows.Foundation.TypedEventHandler<Object, TerminalApp.Tab> FocusTabRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu; event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested; event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
@@ -141,6 +144,8 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, RequestMoveContentArgs> RequestMoveContent; event Windows.Foundation.TypedEventHandler<Object, RequestMoveContentArgs> RequestMoveContent;
event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent; event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent;
event Windows.Foundation.TypedEventHandler<Object, LaunchPositionRequest> RequestLaunchPosition; event Windows.Foundation.TypedEventHandler<Object, LaunchPositionRequest> RequestLaunchPosition;
event Windows.Foundation.TypedEventHandler<Object, WindowListRequest> RequestWindowList;
event Windows.Foundation.TypedEventHandler<Object, OpenWindowRequestedArgs> RequestOpenWindow;
event Windows.Foundation.TypedEventHandler<Object, WindowRequestedArgs> RequestNewWindow; event Windows.Foundation.TypedEventHandler<Object, WindowRequestedArgs> RequestNewWindow;
void AttachContent(String content, UInt32 tabIndex); void AttachContent(String content, UInt32 tabIndex);

View File

@@ -73,6 +73,8 @@ static constexpr std::string_view NewWindowKey{ "newWindow" };
static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" }; static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" };
static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" }; static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" };
static constexpr std::string_view RenameWindowKey{ "renameWindow" }; 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 OpenWindowRenamerKey{ "openWindowRenamer" };
static constexpr std::string_view DisplayWorkingDirectoryKey{ "debugTerminalCwd" }; static constexpr std::string_view DisplayWorkingDirectoryKey{ "debugTerminalCwd" };
static constexpr std::string_view SearchForTextKey{ "searchWeb" }; 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 OpenAboutKey{ "openAbout" };
static constexpr std::string_view QuickFixKey{ "quickFix" }; static constexpr std::string_view QuickFixKey{ "quickFix" };
static constexpr std::string_view OpenCWDKey{ "openCWD" }; static constexpr std::string_view OpenCWDKey{ "openCWD" };
static constexpr std::string_view WorkspacesKey{ "workspaces" };
static constexpr std::string_view ActionKey{ "action" }; static constexpr std::string_view ActionKey{ "action" };

View File

@@ -40,6 +40,7 @@
#include "PrevTabArgs.g.cpp" #include "PrevTabArgs.g.cpp"
#include "NextTabArgs.g.cpp" #include "NextTabArgs.g.cpp"
#include "RenameWindowArgs.g.cpp" #include "RenameWindowArgs.g.cpp"
#include "OpenWorkspaceArgs.g.cpp"
#include "SearchForTextArgs.g.cpp" #include "SearchForTextArgs.g.cpp"
#include "GlobalSummonArgs.g.cpp" #include "GlobalSummonArgs.g.cpp"
#include "FocusPaneArgs.g.cpp" #include "FocusPaneArgs.g.cpp"
@@ -795,6 +796,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return RS_switchable_(L"ResetWindowNameCommandKey"); 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 winrt::hstring SearchForTextArgs::GenerateName(const winrt::WARC::ResourceContext& context) const
{ {
if (QueryUrl().empty()) if (QueryUrl().empty())

View File

@@ -42,6 +42,7 @@
#include "PrevTabArgs.g.h" #include "PrevTabArgs.g.h"
#include "NextTabArgs.g.h" #include "NextTabArgs.g.h"
#include "RenameWindowArgs.g.h" #include "RenameWindowArgs.g.h"
#include "OpenWorkspaceArgs.g.h"
#include "SearchForTextArgs.g.h" #include "SearchForTextArgs.g.h"
#include "GlobalSummonArgs.g.h" #include "GlobalSummonArgs.g.h"
#include "FocusPaneArgs.g.h" #include "FocusPaneArgs.g.h"
@@ -246,6 +247,10 @@ protected: \
#define RENAME_WINDOW_ARGS(X) \ #define RENAME_WINDOW_ARGS(X) \
X(winrt::hstring, Name, "name", false, ArgTypeHint::None, L"") 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) \ #define SEARCH_FOR_TEXT_ARGS(X) \
X(winrt::hstring, QueryUrl, "queryUrl", false, ArgTypeHint::None, L"") 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(RenameWindowArgs, RENAME_WINDOW_ARGS);
ACTION_ARGS_STRUCT(OpenWorkspaceArgs, OPEN_WORKSPACE_ARGS);
ACTION_ARGS_STRUCT(SearchForTextArgs, SEARCH_FOR_TEXT_ARGS); ACTION_ARGS_STRUCT(SearchForTextArgs, SEARCH_FOR_TEXT_ARGS);
struct GlobalSummonArgs : public GlobalSummonArgsT<GlobalSummonArgs> struct GlobalSummonArgs : public GlobalSummonArgsT<GlobalSummonArgs>
@@ -1059,6 +1066,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
BASIC_FACTORY(SetMaximizedArgs); BASIC_FACTORY(SetMaximizedArgs);
BASIC_FACTORY(SetColorSchemeArgs); BASIC_FACTORY(SetColorSchemeArgs);
BASIC_FACTORY(RenameWindowArgs); BASIC_FACTORY(RenameWindowArgs);
BASIC_FACTORY(OpenWorkspaceArgs);
BASIC_FACTORY(ExecuteCommandlineArgs); BASIC_FACTORY(ExecuteCommandlineArgs);
BASIC_FACTORY(CloseOtherTabsArgs); BASIC_FACTORY(CloseOtherTabsArgs);
BASIC_FACTORY(CloseTabsAfterArgs); BASIC_FACTORY(CloseTabsAfterArgs);

View File

@@ -420,6 +420,12 @@ namespace Microsoft.Terminal.Settings.Model
String Name { get; }; String Name { get; };
}; };
[default_interface] runtimeclass OpenWorkspaceArgs : IActionArgs, IActionArgsDescriptorAccess
{
OpenWorkspaceArgs(String name);
String Name { get; };
};
[default_interface] runtimeclass SearchForTextArgs : IActionArgs, IActionArgsDescriptorAccess [default_interface] runtimeclass SearchForTextArgs : IActionArgs, IActionArgsDescriptorAccess
{ {
String QueryUrl { get; }; String QueryUrl { get; };

View File

@@ -116,6 +116,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::OpenTabColorPicker, USES_RESOURCE(L"OpenTabColorPickerCommandKey") }, { ShortcutAction::OpenTabColorPicker, USES_RESOURCE(L"OpenTabColorPickerCommandKey") },
{ ShortcutAction::OpenTabRenamer, USES_RESOURCE(L"OpenTabRenamerCommandKey") }, { ShortcutAction::OpenTabRenamer, USES_RESOURCE(L"OpenTabRenamerCommandKey") },
{ ShortcutAction::OpenWindowRenamer, USES_RESOURCE(L"OpenWindowRenamerCommandKey") }, { ShortcutAction::OpenWindowRenamer, USES_RESOURCE(L"OpenWindowRenamerCommandKey") },
{ ShortcutAction::OpenWorkspace, USES_RESOURCE(L"OpenWorkspaceDefaultCommandKey") },
{ ShortcutAction::PasteText, USES_RESOURCE(L"PasteTextCommandKey") }, { ShortcutAction::PasteText, USES_RESOURCE(L"PasteTextCommandKey") },
{ ShortcutAction::PrevTab, USES_RESOURCE(L"PrevTabCommandKey") }, { ShortcutAction::PrevTab, USES_RESOURCE(L"PrevTabCommandKey") },
{ ShortcutAction::QuickFix, USES_RESOURCE(L"QuickFixCommandKey") }, { ShortcutAction::QuickFix, USES_RESOURCE(L"QuickFixCommandKey") },
@@ -162,6 +163,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::TogglePaneZoom, USES_RESOURCE(L"TogglePaneZoomCommandKey") }, { ShortcutAction::TogglePaneZoom, USES_RESOURCE(L"TogglePaneZoomCommandKey") },
{ ShortcutAction::ToggleShaderEffects, USES_RESOURCE(L"ToggleShaderEffectsCommandKey") }, { ShortcutAction::ToggleShaderEffects, USES_RESOURCE(L"ToggleShaderEffectsCommandKey") },
{ ShortcutAction::ToggleSplitOrientation, USES_RESOURCE(L"ToggleSplitOrientationCommandKey") }, { 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<NextTabArgs>(); return winrt::make<NextTabArgs>();
case Model::ShortcutAction::OpenSettings: case Model::ShortcutAction::OpenSettings:
return winrt::make<OpenSettingsArgs>(); return winrt::make<OpenSettingsArgs>();
case Model::ShortcutAction::OpenWorkspace:
return winrt::make<OpenWorkspaceArgs>();
case Model::ShortcutAction::SetFocusMode: case Model::ShortcutAction::SetFocusMode:
return winrt::make<SetFocusModeArgs>(); return winrt::make<SetFocusModeArgs>();
case Model::ShortcutAction::SetFullScreen: case Model::ShortcutAction::SetFullScreen:

View File

@@ -113,7 +113,9 @@
ON_ALL_ACTIONS(OpenScratchpad) \ ON_ALL_ACTIONS(OpenScratchpad) \
ON_ALL_ACTIONS(OpenAbout) \ ON_ALL_ACTIONS(OpenAbout) \
ON_ALL_ACTIONS(QuickFix) \ 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 \ #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \
ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize) \ ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize) \
@@ -158,7 +160,8 @@
ON_ALL_ACTIONS_WITH_ARGS(Suggestions) \ ON_ALL_ACTIONS_WITH_ARGS(Suggestions) \
ON_ALL_ACTIONS_WITH_ARGS(SelectCommand) \ ON_ALL_ACTIONS_WITH_ARGS(SelectCommand) \
ON_ALL_ACTIONS_WITH_ARGS(SelectOutput) \ 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. // 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 // They don't need to be parsed by the settings model, or saved as actions to

View File

@@ -253,6 +253,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return *state; return *state;
} }
// Need the COMMA macro hack for IMap template arguments in the macros
#define COMMA ,
// Method Description: // Method Description:
// - Loads data from the given json blob. Will only read the data that's in // - 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, // 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; return root;
} }
#undef COMMA
void ApplicationState::AppendPersistedWindowLayout(Model::WindowLayout layout) void ApplicationState::AppendPersistedWindowLayout(Model::WindowLayout layout)
{ {
{ {
@@ -341,6 +346,120 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return false; 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<hstring, Model::WindowLayout>();
}
(*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<hstring, Model::WindowLayout> ApplicationState::AllPersistedWorkspaces()
{
const auto state = _state.lock_shared();
if (state->PersistedWorkspaces && *state->PersistedWorkspaces)
{
return (*state->PersistedWorkspaces).GetView();
}
return nullptr;
}
// Generate all getter/setters // Generate all getter/setters
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \ #define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type ApplicationState::name() const noexcept \ type ApplicationState::name() const noexcept \
@@ -359,7 +478,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
\ \
_throttler(); \ _throttler(); \
} }
#define COMMA ,
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef COMMA
#undef MTSM_APPLICATION_STATE_GEN #undef MTSM_APPLICATION_STATE_GEN
// Method Description: // Method Description:

View File

@@ -16,7 +16,7 @@ Abstract:
#include "WindowLayout.g.h" #include "WindowLayout.g.h"
#include <inc/cppwinrt_utils.h> #include <inc/cppwinrt_utils.h>
#include <JsonUtils.h> #include "JsonUtils.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation 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. // This macro generates all getters and setters for ApplicationState.
// It provides X with the following arguments: // It provides X with the following arguments:
// (source, type, function name, JSON key, ...variadic construction 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) \ #define MTSM_APPLICATION_STATE_FIELDS(X) \
X(FileSource::Shared, winrt::hstring, SettingsHash, "settingsHash") \ X(FileSource::Shared, winrt::hstring, SettingsHash, "settingsHash") \
X(FileSource::Shared, std::unordered_set<winrt::guid>, GeneratedProfiles, "generatedProfiles") \ X(FileSource::Shared, std::unordered_set<winrt::guid>, GeneratedProfiles, "generatedProfiles") \
@@ -42,6 +43,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
X(FileSource::Shared, Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage>, DismissedMessages, "dismissedMessages") \ X(FileSource::Shared, Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage>, DismissedMessages, "dismissedMessages") \
X(FileSource::Local, Windows::Foundation::Collections::IVector<hstring>, AllowedCommandlines, "allowedCommandlines") \ X(FileSource::Local, Windows::Foundation::Collections::IVector<hstring>, AllowedCommandlines, "allowedCommandlines") \
X(FileSource::Local, std::unordered_set<hstring>, DismissedBadges, "dismissedBadges") \ X(FileSource::Local, std::unordered_set<hstring>, DismissedBadges, "dismissedBadges") \
X(FileSource::Local, Windows::Foundation::Collections::IMap<hstring COMMA Model::WindowLayout>, PersistedWorkspaces, "persistedWorkspaces") \
X(FileSource::Shared, bool, SSHFolderGenerated, "sshFolderGenerated", false) X(FileSource::Shared, bool, SSHFolderGenerated, "sshFolderGenerated", false)
struct WindowLayout : WindowLayoutT<WindowLayout> struct WindowLayout : WindowLayoutT<WindowLayout>
@@ -57,6 +59,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
friend ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<Model::WindowLayout>; friend ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<Model::WindowLayout>;
}; };
#define COMMA ,
struct ApplicationState : public ApplicationStateT<ApplicationState> struct ApplicationState : public ApplicationStateT<ApplicationState>
{ {
static Microsoft::Terminal::Settings::Model::ApplicationState SharedInstance(); static Microsoft::Terminal::Settings::Model::ApplicationState SharedInstance();
@@ -75,6 +79,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
bool DismissBadge(const hstring& badgeId); bool DismissBadge(const hstring& badgeId);
bool BadgeDismissed(const hstring& badgeId) const; 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<hstring, Model::WindowLayout> AllPersistedWorkspaces();
// State getters/setters // State getters/setters
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \ #define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type name() const noexcept; \ type name() const noexcept; \
@@ -104,6 +114,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
std::string _readLocalContents() const; std::string _readLocalContents() const;
void _writeLocalContents(const std::string_view content) const; void _writeLocalContents(const std::string_view content) const;
}; };
#undef COMMA
} }
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation

View File

@@ -36,6 +36,12 @@ namespace Microsoft.Terminal.Settings.Model
Boolean DismissBadge(String badgeId); Boolean DismissBadge(String badgeId);
Boolean BadgeDismissed(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<String, WindowLayout> AllPersistedWorkspaces();
String SettingsHash; String SettingsHash;
Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts; Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts;
Windows.Foundation.Collections.IVector<String> RecentCommands; Windows.Foundation.Collections.IVector<String> RecentCommands;

View File

@@ -162,7 +162,8 @@ Author(s):
X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \
X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \
X(bool, RainbowFrame, "experimental.rainbowFrame", false) \ 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) \ #define MTSM_THEME_SETTINGS_SETTINGS(X) \
X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "theme", winrt::Windows::UI::Xaml::ElementTheme::Default) X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "theme", winrt::Windows::UI::Xaml::ElementTheme::Default)

View File

@@ -518,6 +518,13 @@
<data name="ResetWindowNameCommandKey" xml:space="preserve"> <data name="ResetWindowNameCommandKey" xml:space="preserve">
<value>Reset window name</value> <value>Reset window name</value>
</data> </data>
<data name="OpenWorkspaceCommandKey" xml:space="preserve">
<value>Open workspace "{0}"</value>
<comment>{0} will be replaced with the workspace name</comment>
</data>
<data name="OpenWorkspaceDefaultCommandKey" xml:space="preserve">
<value>Open workspace</value>
</data>
<data name="OpenWindowRenamerCommandKey" xml:space="preserve"> <data name="OpenWindowRenamerCommandKey" xml:space="preserve">
<value>Rename window...</value> <value>Rename window...</value>
</data> </data>
@@ -740,6 +747,9 @@
<data name="OpenCWDCommandKey" xml:space="preserve"> <data name="OpenCWDCommandKey" xml:space="preserve">
<value>Open current working directory</value> <value>Open current working directory</value>
</data> </data>
<data name="WorkspacesCommandKey" xml:space="preserve">
<value>Workspaces...</value>
</data>
<data name="CloseTab" xml:space="preserve"> <data name="CloseTab" xml:space="preserve">
<value>Close tab</value> <value>Close tab</value>
</data> </data>

View File

@@ -62,6 +62,7 @@ namespace Microsoft.Terminal.Settings.Model
Windows.UI.Xaml.ElementTheme RequestedTheme { get; }; Windows.UI.Xaml.ElementTheme RequestedTheme { get; };
Boolean UseMica { get; }; Boolean UseMica { get; };
Boolean RainbowFrame { get; }; Boolean RainbowFrame { get; };
Boolean ShowWorkspacesButton { get; };
ThemeColor Frame { get; }; ThemeColor Frame { get; };
ThemeColor UnfocusedFrame { get; }; ThemeColor UnfocusedFrame { get; };
} }

View File

@@ -542,6 +542,7 @@
{ "command": "quickFix", "id": "Terminal.QuickFix" }, { "command": "quickFix", "id": "Terminal.QuickFix" },
{ "command": { "action": "showSuggestions", "source": "all"}, "id": "Terminal.Suggestions" }, { "command": { "action": "showSuggestions", "source": "all"}, "id": "Terminal.Suggestions" },
{ "command": "openCWD", "id": "Terminal.OpenCWD" }, { "command": "openCWD", "id": "Terminal.OpenCWD" },
{ "command": "workspaces", "id": "Terminal.Workspaces" },
// Tab Management // Tab Management
// "command": "closeTab" is unbound by default. // "command": "closeTab" is unbound by default.

View File

@@ -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<implementation::ApplicationState> _make()
{
return winrt::make_self<implementation::ApplicationState>(_tempRoot());
}
static WindowLayout _makeLayout()
{
WindowLayout layout;
layout.TabLayout(winrt::single_threaded_vector<ActionAndArgs>());
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"));
}
}

View File

@@ -45,6 +45,7 @@
<ClCompile Include="TerminalSettingsTests.cpp" /> <ClCompile Include="TerminalSettingsTests.cpp" />
<ClCompile Include="ThemeTests.cpp" /> <ClCompile Include="ThemeTests.cpp" />
<ClCompile Include="MediaResourceTests.cpp" /> <ClCompile Include="MediaResourceTests.cpp" />
<ClCompile Include="ApplicationStateTests.cpp" />
<ClCompile Include="../TerminalSettingsAppAdapterLib/TerminalSettings.cpp" /> <ClCompile Include="../TerminalSettingsAppAdapterLib/TerminalSettings.cpp" />
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader> <PrecompiledHeader>Create</PrecompiledHeader>

View File

@@ -66,6 +66,8 @@ Author(s):
// Manually include til after we include Windows.Foundation to give it winrt superpowers // Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h" #include "til.h"
#include <til/winrt.h> #include <til/winrt.h>
#include <til/mutex.h>
#include <til/throttled_func.h>
// Common includes for most tests: // Common includes for most tests:
#include "../../inc/conattrs.hpp" #include "../../inc/conattrs.hpp"

View File

@@ -132,7 +132,12 @@ void AppHost::_HandleCommandlineArgs(const winrt::TerminalApp::WindowRequestedAr
// We don't have XAML yet, but we do have other stuff. // We don't have XAML yet, but we do have other stuff.
_windowLogic = _appLogic.CreateNewWindow(); _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()); _windowLogic.SetStartupContent(content, windowArgs.InitialBounds());
_launchShowWindowCommand = SW_NORMAL; _launchShowWindowCommand = SW_NORMAL;
@@ -270,12 +275,15 @@ void AppHost::Initialize()
_revokers.IsQuakeWindowChanged = _windowLogic.IsQuakeWindowChanged(winrt::auto_revoke, { this, &AppHost::_IsQuakeWindowChanged }); _revokers.IsQuakeWindowChanged = _windowLogic.IsQuakeWindowChanged(winrt::auto_revoke, { this, &AppHost::_IsQuakeWindowChanged });
_revokers.SummonWindowRequested = _windowLogic.SummonWindowRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowRequested }); _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.FocusTabRequested = _windowLogic.FocusTabRequested(winrt::auto_revoke, { this, &AppHost::_FocusTabRequested });
_revokers.OpenSystemMenu = _windowLogic.OpenSystemMenu(winrt::auto_revoke, { this, &AppHost::_OpenSystemMenu }); _revokers.OpenSystemMenu = _windowLogic.OpenSystemMenu(winrt::auto_revoke, { this, &AppHost::_OpenSystemMenu });
_revokers.QuitRequested = _windowLogic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll }); _revokers.QuitRequested = _windowLogic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll });
_revokers.ShowWindowChanged = _windowLogic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged }); _revokers.ShowWindowChanged = _windowLogic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged });
_revokers.RequestMoveContent = _windowLogic.RequestMoveContent(winrt::auto_revoke, { this, &AppHost::_handleMoveContent }); _revokers.RequestMoveContent = _windowLogic.RequestMoveContent(winrt::auto_revoke, { this, &AppHost::_handleMoveContent });
_revokers.RequestReceiveContent = _windowLogic.RequestReceiveContent(winrt::auto_revoke, { this, &AppHost::_handleReceiveContent }); _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 }); _revokers.RequestNewWindow = _windowLogic.RequestNewWindow(winrt::auto_revoke, { this, &AppHost::_HandleNewWindowRequested });
// BODGY // BODGY
@@ -416,6 +424,40 @@ void AppHost::_HandleRequestLaunchPosition(const winrt::Windows::Foundation::IIn
args.Position(_GetWindowLaunchPosition()); 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<WindowEmperor::WindowListEntry> entries;
SendMessage(_windowManager->GetMainWindow(),
WindowEmperor::WM_GET_WINDOW_LIST,
0,
reinterpret_cast<LPARAM>(&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 ...")` // In-process replacement for the old `ShellExecute("wt -w -1 new-tab ...")`
// dance. The page hands us a pre-built WindowRequestedArgs (with its // dance. The page hands us a pre-built WindowRequestedArgs (with its
// StartupActions already populated); we just forward it to the WindowEmperor. // 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)); 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&, void AppHost::_FocusTabRequested(const winrt::Windows::Foundation::IInspectable&,
const winrt::TerminalApp::Tab& tab) const winrt::TerminalApp::Tab& tab)
{ {

View File

@@ -91,12 +91,18 @@ private:
void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args); 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, void _FocusTabRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::TerminalApp::Tab& tab); const winrt::TerminalApp::Tab& tab);
void _OpenSystemMenu(const winrt::Windows::Foundation::IInspectable& sender, void _OpenSystemMenu(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args); 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, void _HandleNewWindowRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::TerminalApp::WindowRequestedArgs& args); const winrt::TerminalApp::WindowRequestedArgs& args);
@@ -136,6 +142,8 @@ private:
void _AppTitleChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&); void _AppTitleChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&);
void _HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& sender, void _HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& sender,
winrt::TerminalApp::LaunchPositionRequest args); 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 // 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 // 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::IdentifyWindowsRequested_revoker IdentifyWindowsRequested;
winrt::TerminalApp::TerminalWindow::IsQuakeWindowChanged_revoker IsQuakeWindowChanged; winrt::TerminalApp::TerminalWindow::IsQuakeWindowChanged_revoker IsQuakeWindowChanged;
winrt::TerminalApp::TerminalWindow::SummonWindowRequested_revoker SummonWindowRequested; winrt::TerminalApp::TerminalWindow::SummonWindowRequested_revoker SummonWindowRequested;
winrt::TerminalApp::TerminalWindow::SummonWindowByIdRequested_revoker SummonWindowByIdRequested;
winrt::TerminalApp::TerminalWindow::FocusTabRequested_revoker FocusTabRequested; winrt::TerminalApp::TerminalWindow::FocusTabRequested_revoker FocusTabRequested;
winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu; winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu;
winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested; winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested;
@@ -165,6 +174,8 @@ private:
winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent; winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent;
winrt::TerminalApp::TerminalWindow::RequestLaunchPosition_revoker RequestLaunchPosition; winrt::TerminalApp::TerminalWindow::RequestLaunchPosition_revoker RequestLaunchPosition;
winrt::TerminalApp::TerminalWindow::RequestNewWindow_revoker RequestNewWindow; 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::PropertyChanged_revoker PropertyChanged;
winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged; winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged;
winrt::TerminalApp::TerminalWindow::WindowSizeChanged_revoker WindowSizeChanged; winrt::TerminalApp::TerminalWindow::WindowSizeChanged_revoker WindowSizeChanged;

View File

@@ -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 <name>` 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 AppHost* WindowEmperor::_mostRecentWindow() const noexcept
{ {
int64_t max = INT64_MIN; int64_t max = INT64_MIN;
@@ -801,9 +855,7 @@ void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs arg
} }
else else
{ {
winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) }; _createWindowMaybeRestoringWorkspace(windowId, windowName, std::move(args));
request.WindowName(std::move(windowName));
CreateNewWindow(std::move(request));
} }
} }
@@ -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 // anyway (since we threw and exited this message handler) so this at least gives back our
// deterministic window count management. // deterministic window count management.
const auto strong = *it; 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); _windows.erase(it);
try try
{ {
@@ -1114,6 +1182,19 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c
host->Logic().IdentifyWindow(); host->Logic().IdentifyWindow();
} }
return 0; return 0;
case WM_GET_WINDOW_LIST:
{
auto* result = reinterpret_cast<std::vector<WindowListEntry>*>(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: case WM_NOTIFY_FROM_NOTIFICATION_AREA:
switch (LOWORD(lParam)) switch (LOWORD(lParam))
{ {

View File

@@ -28,6 +28,16 @@ public:
WM_MESSAGE_BOX_CLOSED, WM_MESSAGE_BOX_CLOSED,
WM_IDENTIFY_ALL_WINDOWS, WM_IDENTIFY_ALL_WINDOWS,
WM_NOTIFY_FROM_NOTIFICATION_AREA, 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; HWND GetMainWindow() const noexcept;
@@ -37,6 +47,8 @@ public:
void CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args); void CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args);
void HandleCommandlineArgs(int nCmdShow); void HandleCommandlineArgs(int nCmdShow);
void FocusTabInAnyWindow(const winrt::TerminalApp::Tab& tab) const; 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: private:
struct SummonWindowSelectionArgs struct SummonWindowSelectionArgs
@@ -50,6 +62,7 @@ private:
[[nodiscard]] static LRESULT __stdcall _wndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept; [[nodiscard]] static LRESULT __stdcall _wndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept;
AppHost* _mostRecentWindow() const noexcept; AppHost* _mostRecentWindow() const noexcept;
void _createWindowMaybeRestoringWorkspace(uint64_t windowId, const winrt::hstring& windowName, winrt::TerminalApp::CommandlineArgs args);
bool _summonWindow(const SummonWindowSelectionArgs& args) const; bool _summonWindow(const SummonWindowSelectionArgs& args) const;
void _summonAllWindows() const; void _summonAllWindows() const;
void _dispatchSpecialKey(const MSG& msg) const; void _dispatchSpecialKey(const MSG& msg) const;

View File

@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT license. // Licensed under the MIT license.
#pragma once
namespace til namespace til
{ {
namespace details namespace details