Compare commits

...

1 Commits

Author SHA1 Message Date
Mike Griese
0c0a16e10a Add support for "workspaces" based on window names
This adds a new feature to the Windows Terminal: "Workspaces"

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

{{video here}}

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

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

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

<details>
<summary>
state.json
</summary>

```jsonc

	"persistedWindowLayouts" :
	[
		{
			"initialPosition" : "910,462",
			"initialSize" :
			{
				"height" : 623.20001220703125,
				"width" : 934.4000244140625
			},
			"launchMode" : "default",
			"tabLayout" :
			[
				{
					"action" : "newTab",
					"commandline" : "\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\"",
					"profile" : "{574e775e-4f2a-5b96-ac1e-a2962a402336}",
					"sessionId" : "{09d3ef05-ba3d-44ed-a8f1-065d08a806a4}",
					"startingDirectory" : "C:\\Users\\zadji",
					"suppressApplicationTitle" : false,
					"tabTitle" : "PowerShell"
				},
			]
		},
		{
			"tabLayout" :
			[
				{
					"action" : "openWorkspace",
					"name" : "wsl"
				}
			]
		}
	],
	"persistedWorkspaces" :
	{
		"wsl" :
		{
			"initialPosition" : "109,502",
			"initialSize" :
			{
				"height" : 568,
				"width" : 1184.800048828125
			},
			"launchMode" : "default",
			"tabLayout" :
			[
				{
					"action" : "newTab",
					"commandline" : "ubuntu.exe",
					"profile" : "{51855cb2-8cce-5362-8f54-464b92b32386}",
					"sessionId" : "{a7f70c49-71ec-4ac4-9f3a-4884528a3fd6}",
					"startingDirectory" : null,
					"suppressApplicationTitle" : false,
					"tabTitle" : "Ubuntu"
				},
				{
					"action" : "renameWindow",
					"name" : "wsl"
				}
			]
		}
	},
```

</details>

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.
2026-04-28 14:32:46 -05:00
32 changed files with 854 additions and 16 deletions

View File

@@ -938,6 +938,27 @@ namespace winrt::TerminalApp::implementation
co_return;
}
// Launch `wt -w <name>` so the monarch can either summon an existing
// window with that name or restore a persisted workspace.
safe_void_coroutine TerminalPage::_OpenWorkspaceWindow(const winrt::hstring name)
{
co_await winrt::resume_background();
const auto exePath{ GetWtExePath() };
const auto cmdline = fmt::format(FMT_COMPILE(L"-w {}"), std::wstring_view{ name });
SHELLEXECUTEINFOW seInfo{ 0 };
seInfo.cbSize = sizeof(seInfo);
seInfo.fMask = SEE_MASK_NOASYNC;
seInfo.lpVerb = L"open";
seInfo.lpFile = exePath.c_str();
seInfo.lpParameters = cmdline.c_str();
seInfo.nShow = SW_SHOWNORMAL;
LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo));
co_return;
}
void TerminalPage::_HandleNewWindow(const IInspectable& /*sender*/,
const ActionEventArgs& actionArgs)
{
@@ -1633,4 +1654,25 @@ namespace winrt::TerminalApp::implementation
args.Handled(handled);
}
}
void TerminalPage::_HandleOpenWorkspace(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
// Open (or summon) a named window. We launch a new `wt -w <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);
}
}
}
}

View File

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

View File

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

View File

@@ -719,6 +719,18 @@
<value>unnamed window</value>
<comment>text used to identify when a window hasn't been assigned a name by the user</comment>
</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>{0} is the window ID number. Shown in the workspace flyout for windows that have no name assigned.</comment>
</data>
<data name="WindowRenamer.Subtitle" xml:space="preserve">
<value>Enter a new name:</value>
</data>

View File

@@ -438,6 +438,21 @@ namespace winrt::TerminalApp::implementation
const auto focusedTabIndex{ _GetFocusedTabIndex() };
// If this is the last tab in a named window, persist the workspace
// layout now—before the tab is shut down—so GetWindowLayout() can
// still see its tab data.
if (_tabs.Size() == 1)
{
const auto& windowName = _WindowProperties.WindowName();
if (!windowName.empty())
{
if (const auto layout = GetWindowLayout())
{
ApplicationState::SharedInstance().SaveWorkspace(windowName, layout);
}
}
}
// Removing the tab from the collection should destroy its control and disconnect its connection,
// but it doesn't always do so. The UI tree may still be holding the control and preventing its destruction.
tab.Shutdown();

View File

@@ -25,6 +25,21 @@ namespace winrt::TerminalApp::implementation
InitializeComponent();
}
void TabRowControl::WorkspaceName(const winrt::hstring& value)
{
if (_WorkspaceName != value)
{
_WorkspaceName = value;
PropertyChanged.raise(*this, WUX::Data::PropertyChangedEventArgs{ L"WorkspaceName" });
// Collapse the name text when empty so the button shows only the icon.
if (const auto textBlock = WorkspaceNameText())
{
textBlock.Visibility(value.empty() ? WUX::Visibility::Collapsed : WUX::Visibility::Visible);
}
}
}
// Method Description:
// - Bound in the Xaml editor to the [+] button.
// Arguments:

View File

@@ -19,6 +19,14 @@ namespace winrt::TerminalApp::implementation
til::property_changed_event PropertyChanged;
WINRT_OBSERVABLE_PROPERTY(bool, ShowElevationShield, PropertyChanged.raise, false);
WINRT_OBSERVABLE_PROPERTY(bool, ShowWindowsButton, PropertyChanged.raise, true);
public:
winrt::hstring WorkspaceName() const noexcept { return _WorkspaceName; }
void WorkspaceName(const winrt::hstring& value);
private:
winrt::hstring _WorkspaceName{};
};
}

View File

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

View File

@@ -35,14 +35,43 @@
TabWidthMode="Equal">
<mux:TabView.TabStripHeader>
<!-- EA18 is the "Shield" glyph -->
<FontIcon x:Uid="ElevationShield"
Margin="9,4,0,4"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Glyph="&#xEA18;"
Visibility="{x:Bind ShowElevationShield, Mode=OneWay}" />
<StackPanel Orientation="Horizontal">
<!-- EA18 is the "Shield" glyph -->
<FontIcon x:Uid="ElevationShield"
Margin="9,4,0,4"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Glyph="&#xEA18;"
Visibility="{x:Bind ShowElevationShield, Mode=OneWay}" />
<!-- Workspace/windows button -->
<Button x:Name="WorkspaceDropdown"
Margin="4,4,0,4"
Padding="8,2,4,2"
VerticalAlignment="Center"
BorderThickness="0"
Background="Transparent"
Visibility="{x:Bind ShowWindowsButton, Mode=OneWay}">
<Button.Content>
<StackPanel Orientation="Horizontal"
Spacing="4">
<!-- EE40 is the "TaskViewSettings" glyph -->
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xEE40;" />
<TextBlock x:Name="WorkspaceNameText"
VerticalAlignment="Center"
FontSize="12"
Visibility="Collapsed"
Text="{x:Bind WorkspaceName, Mode=OneWay}" />
</StackPanel>
</Button.Content>
<Button.Flyout>
<MenuFlyout x:Name="WorkspaceFlyout" />
</Button.Flyout>
</Button>
</StackPanel>
</mux:TabView.TabStripHeader>
<mux:TabView.TabStripFooter>

View File

@@ -25,6 +25,8 @@
#include "TerminalSettingsCache.h"
#include "LaunchPositionRequest.g.cpp"
#include "WindowListEntry.g.cpp"
#include "WindowListRequest.g.cpp"
#include "RenameWindowRequestedArgs.g.cpp"
#include "RequestMoveContentArgs.g.cpp"
#include "TerminalPage.g.cpp"
@@ -334,6 +336,20 @@ namespace winrt::TerminalApp::implementation
auto tabRowImpl = winrt::get_self<implementation::TabRowControl>(_tabRow);
_newTabButton = tabRowImpl->NewTabButton();
_workspaceFlyout = tabRowImpl->WorkspaceFlyout();
// Set the initial workspace name from the window name.
// Use raw WindowName() so unnamed windows show no text.
_tabRow.WorkspaceName(_WindowProperties.WindowName());
// Rebuild the workspace flyout each time it opens so it always
// reflects the latest set of persisted workspaces.
_workspaceFlyout.Opening([weakThis{ get_weak() }](auto&&, auto&&) {
if (auto page{ weakThis.get() })
{
page->_PopulateWorkspaceFlyout();
}
});
if (_settings.GlobalSettings().ShowTabsInTitlebar())
{
@@ -443,6 +459,12 @@ namespace winrt::TerminalApp::implementation
_tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield());
// Apply the ShowWindowsButton theme setting.
if (const auto theme = _settings.GlobalSettings().CurrentTheme())
{
_tabRow.ShowWindowsButton(theme.Window() ? theme.Window().ShowWindowsButton() : true);
}
_adjustProcessPriorityThrottled = std::make_shared<ThrottledFunc<>>(
DispatcherQueue::GetForCurrentThread(),
til::throttled_func_options{
@@ -2232,14 +2254,14 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::PersistState()
WindowLayout TerminalPage::GetWindowLayout()
{
// This method may be called for a window even if it hasn't had a tab yet or lost all of them.
// We shouldn't persist such windows.
const auto tabCount = _tabs.Size();
if (_startupState != StartupState::Initialized || tabCount == 0)
{
return;
return nullptr;
}
std::vector<ActionAndArgs> actions;
@@ -2254,7 +2276,7 @@ namespace winrt::TerminalApp::implementation
// Avoid persisting a window with zero tabs, because `BuildStartupActions` happened to return an empty vector.
if (actions.empty())
{
return;
return nullptr;
}
// if the focused tab was not the last tab, restore that
@@ -2303,7 +2325,49 @@ namespace winrt::TerminalApp::implementation
RequestLaunchPosition.raise(*this, launchPosRequest);
layout.InitialPosition(launchPosRequest.Position());
ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout);
return layout;
}
void TerminalPage::PersistState()
{
// There are two persistence mechanisms in play here:
// * PersistedWindowLayouts (vector) — consumed on next startup to
// re-open a matching set of windows. Cleared after restore.
// * PersistedWorkspaces (name-keyed map) — the full tab/buffer
// state of a named window, claimed by name on demand via
// ApplicationState::TakeWorkspace.
//
// For named windows we save the full layout into the workspace map
// and drop a lightweight `openWorkspace` stub into the generic vector,
// so the generic restore path re-opens the named window which in
// turn claims its own workspace. Unnamed windows don't have a stable
// key, so their full layout is stored directly in the vector.
if (const auto layout = GetWindowLayout())
{
const auto& windowName = _WindowProperties.WindowName();
if (!windowName.empty())
{
// Persist the full layout into the workspace collection.
ApplicationState::SharedInstance().SaveWorkspace(windowName, layout);
// Build a minimal layout with just an openWorkspace action
// so the generic restore path re-opens this workspace by name.
std::vector<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:
@@ -3934,6 +3998,12 @@ namespace winrt::TerminalApp::implementation
_tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield());
// Apply the ShowWindowsButton theme setting.
if (const auto theme = _settings.GlobalSettings().CurrentTheme())
{
_tabRow.ShowWindowsButton(theme.Window() ? theme.Window().ShowWindowsButton() : true);
}
Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() };
_tabView.Background(transparent);
@@ -5574,6 +5644,156 @@ namespace winrt::TerminalApp::implementation
}
}
// Rebuild the workspace flyout contents. Called every time the flyout opens
// so it reflects the current set of persisted workspaces.
void TerminalPage::_PopulateWorkspaceFlyout()
{
if (!_workspaceFlyout)
{
return;
}
_workspaceFlyout.Items().Clear();
// --- "Name / Rename this window" ---
{
MenuFlyoutItem item{};
item.Text(_WindowProperties.WindowName().empty() ? RS_(L"NameThisWindowMenuItem") : RS_(L"RenameThisWindowMenuItem"));
auto iconElement = UI::IconPathConverter::IconWUX(L"\uE8AC"); // Rename glyph
Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw);
item.Icon(iconElement);
item.Click([weakThis{ get_weak() }](auto&&, auto&&) {
if (auto page{ weakThis.get() })
{
// Re-use the existing openWindowRenamer action.
page->_actionDispatch->DoAction(ActionAndArgs{ ShortcutAction::OpenWindowRenamer, nullptr });
}
});
_workspaceFlyout.Items().Append(item);
}
// --- Gather open window info first so we can filter workspaces ---
// Ask the host (via AppHost → WindowEmperor) for all live windows.
const auto windowListReq{ winrt::make<WindowListRequest>() };
RequestWindowList.raise(*this, windowListReq);
const auto windowEntries = windowListReq.Entries();
// Collect the names of all currently-open windows so we can hide
// workspaces that are already live from the saved-workspaces list.
std::set<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) ---
const auto workspaces = ApplicationState::SharedInstance().AllPersistedWorkspaces();
if (workspaces && workspaces.Size() > 0)
{
bool addedSeparator = false;
for (const auto& pair : workspaces)
{
const auto name = pair.Key();
// Skip workspaces that correspond to a currently-open window.
if (openWindowNames.count(name))
{
continue;
}
if (!addedSeparator)
{
_workspaceFlyout.Items().Append(MenuFlyoutSeparator{});
addedSeparator = true;
}
MenuFlyoutItem item{};
item.Text(name);
auto iconElement = UI::IconPathConverter::IconWUX(L"\uE8F1"); // SwitchApps glyph
Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw);
item.Icon(iconElement);
item.Click([weakThis{ get_weak() }, name](auto&&, auto&&) {
if (auto page{ weakThis.get() })
{
page->_OpenWorkspaceWindow(name);
}
});
_workspaceFlyout.Items().Append(item);
}
}
// --- Open windows section ---
if (windowEntries && windowEntries.Size() > 0)
{
_workspaceFlyout.Items().Append(MenuFlyoutSeparator{});
const auto thisWindowId = _WindowProperties.WindowId();
for (const auto& entry : windowEntries)
{
const auto id = entry.Id();
const auto& name = entry.Name();
// Build display text like "#1: MyWindow" or "#2: <unnamed>"
winrt::hstring displayText;
if (name.empty())
{
displayText = winrt::hstring{ RS_fmt(L"WindowListUnnamedEntry", id) };
}
else
{
displayText = winrt::hstring{ fmt::format(FMT_COMPILE(L"#{}: {}"), id, name) };
}
MenuFlyoutItem item{};
item.Text(displayText);
// Use a different glyph for the current window
if (id == thisWindowId)
{
auto iconElement = UI::IconPathConverter::IconWUX(L"\uE73E"); // CheckMark glyph
Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw);
item.Icon(iconElement);
item.IsEnabled(false); // Can't summon yourself
}
else
{
auto iconElement = UI::IconPathConverter::IconWUX(L"\uE737"); // ChromeRestore glyph
Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw);
item.Icon(iconElement);
// Click handler: summon the target window by ID.
// We raise SummonWindowByIdRequested which is handled by
// AppHost to directly summon the window without creating
// a new tab (unlike _OpenWorkspaceWindow which launches
// `wt -w <name>` and creates a tab).
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
// to pop the "Identify Window" toast when the user renames our window.
void TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, const WUX::Data::PropertyChangedEventArgs& args)
@@ -5583,6 +5803,10 @@ namespace winrt::TerminalApp::implementation
return;
}
// Keep the workspace dropdown label in sync with the window name.
// Use raw WindowName() so clearing the name hides the text.
_tabRow.WorkspaceName(_WindowProperties.WindowName());
// DON'T display the confirmation if this is the name we were
// given on startup!
if (_startupState == StartupState::Initialized)

View File

@@ -10,8 +10,11 @@
#include "AppKeyBindings.h"
#include "AppCommandlineArgs.h"
#include "RenameWindowRequestedArgs.g.h"
#include "SummonWindowByIdRequestedArgs.g.h"
#include "RequestMoveContentArgs.g.h"
#include "LaunchPositionRequest.g.h"
#include "WindowListEntry.g.h"
#include "WindowListRequest.g.h"
#include "Toast.h"
#include "WindowsPackageManagerFactory.h"
@@ -63,6 +66,15 @@ namespace winrt::TerminalApp::implementation
_ProposedName{ name } {};
};
struct SummonWindowByIdRequestedArgs : SummonWindowByIdRequestedArgsT<SummonWindowByIdRequestedArgs>
{
WINRT_PROPERTY(uint64_t, WindowId);
public:
SummonWindowByIdRequestedArgs(uint64_t id) :
_WindowId{ id } {};
};
struct RequestMoveContentArgs : RequestMoveContentArgsT<RequestMoveContentArgs>
{
WINRT_PROPERTY(winrt::hstring, Window);
@@ -84,6 +96,25 @@ namespace winrt::TerminalApp::implementation
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
{
winrt::Microsoft::Management::Deployment::PackageMatchField Field;
@@ -122,6 +153,7 @@ namespace winrt::TerminalApp::implementation
safe_void_coroutine RequestQuit();
safe_void_coroutine CloseWindow();
winrt::Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout();
void PersistState();
std::vector<IPaneContent> Panes() const;
@@ -192,6 +224,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<IInspectable, IInspectable> IdentifyWindowsRequested;
til::typed_event<IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs> RenameWindowRequested;
til::typed_event<IInspectable, IInspectable> SummonWindowRequested;
til::typed_event<IInspectable, winrt::TerminalApp::SummonWindowByIdRequestedArgs> SummonWindowByIdRequested;
til::typed_event<IInspectable, winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs> WindowSizeChanged;
til::typed_event<IInspectable, IInspectable> OpenSystemMenu;
@@ -203,6 +236,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs> RequestReceiveContent;
til::typed_event<IInspectable, winrt::TerminalApp::LaunchPositionRequest> RequestLaunchPosition;
til::typed_event<IInspectable, winrt::TerminalApp::WindowListRequest> RequestWindowList;
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr);
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr);
@@ -225,6 +259,7 @@ namespace winrt::TerminalApp::implementation
TerminalApp::TabRowControl _tabRow{ nullptr };
Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr };
Microsoft::UI::Xaml::Controls::SplitButton _newTabButton{ nullptr };
Windows::UI::Xaml::Controls::MenuFlyout _workspaceFlyout{ nullptr };
winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr };
Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr };
@@ -324,6 +359,7 @@ namespace winrt::TerminalApp::implementation
void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&);
safe_void_coroutine _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs newContentArgs);
safe_void_coroutine _OpenWorkspaceWindow(const winrt::hstring name);
void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs);
@@ -563,6 +599,7 @@ namespace winrt::TerminalApp::implementation
void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection);
void _PopulateQuickFixMenu(const Microsoft::Terminal::Control::TermControl& control, const Windows::UI::Xaml::Controls::MenuFlyout& sender);
void _PopulateWorkspaceFlyout();
winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex);
winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender);
@@ -587,4 +624,5 @@ namespace winrt::TerminalApp::implementation
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(TerminalPage);
BASIC_FACTORY(WindowListEntry);
}

View File

@@ -19,6 +19,10 @@ namespace TerminalApp
{
String ProposedName { get; };
};
[default_interface] runtimeclass SummonWindowByIdRequestedArgs
{
UInt64 WindowId { get; };
};
[default_interface] runtimeclass RequestMoveContentArgs
{
String Window { get; };
@@ -50,6 +54,20 @@ namespace TerminalApp
Microsoft.Terminal.Settings.Model.LaunchPosition Position;
}
[default_interface] runtimeclass WindowListEntry
{
WindowListEntry();
UInt64 Id;
String Name;
}
// Raised by TerminalPage when it needs the list of open windows.
// The handler (AppHost) fills Entries synchronously.
[default_interface] runtimeclass WindowListRequest
{
Windows.Foundation.Collections.IVector<WindowListEntry> Entries { get; };
}
[default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, Microsoft.Terminal.UI.IDirectKeyListener
{
TerminalPage(WindowProperties properties, ContentManager manager);
@@ -93,6 +111,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, RenameWindowRequestedArgs> RenameWindowRequested;
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, Object> OpenSystemMenu;
@@ -103,5 +122,6 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent;
event Windows.Foundation.TypedEventHandler<Object, LaunchPositionRequest> RequestLaunchPosition;
event Windows.Foundation.TypedEventHandler<Object, WindowListRequest> RequestWindowList;
}
}

View File

@@ -252,6 +252,15 @@ namespace winrt::TerminalApp::implementation
AppLogic::Current()->NotifyRootInitialized();
}
WindowLayout TerminalWindow::GetWindowLayout()
{
if (_root)
{
return _root->GetWindowLayout();
}
return nullptr;
}
void TerminalWindow::PersistState()
{
if (_root)
@@ -1097,6 +1106,11 @@ namespace winrt::TerminalApp::implementation
_initialContentArgs = wil::to_vector(args);
}
void TerminalWindow::SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout)
{
_cachedLayout = layout;
}
// Method Description:
// - Parse the provided commandline arguments into actions, and try to
// perform them immediately.
@@ -1208,7 +1222,14 @@ namespace winrt::TerminalApp::implementation
void TerminalWindow::WindowName(const winrt::hstring& name)
{
const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow();
const auto oldName = _WindowProperties->WindowName();
_WindowProperties->WindowName(name);
// If this window had a persisted workspace under the old name, rename
// that entry too so we don't leave a stale copy behind.
if (!oldName.empty() && !name.empty() && oldName != name)
{
ApplicationState::SharedInstance().RenameWorkspace(oldName, name);
}
if (!_root)
{
return;

View File

@@ -71,6 +71,7 @@ namespace winrt::TerminalApp::implementation
void Create();
winrt::Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout();
void PersistState();
void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args);
@@ -79,6 +80,7 @@ namespace winrt::TerminalApp::implementation
int32_t SetStartupCommandline(TerminalApp::CommandlineArgs args);
void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference<Windows::Foundation::Rect>& contentBounds);
void SetPersistedLayout(const winrt::Microsoft::Terminal::Settings::Model::WindowLayout& layout);
int32_t ExecuteCommandline(TerminalApp::CommandlineArgs args);
void SetSettingsStartupArgs(const std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>& actions);
@@ -221,6 +223,7 @@ namespace winrt::TerminalApp::implementation
FORWARDED_TYPED_EVENT(SetTaskbarProgress, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, SetTaskbarProgress);
FORWARDED_TYPED_EVENT(IdentifyWindowsRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IdentifyWindowsRequested);
FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested);
FORWARDED_TYPED_EVENT(SummonWindowByIdRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::SummonWindowByIdRequestedArgs, _root, SummonWindowByIdRequested);
FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu);
FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested);
FORWARDED_TYPED_EVENT(ShowWindowChanged, Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs, _root, ShowWindowChanged);
@@ -229,6 +232,7 @@ namespace winrt::TerminalApp::implementation
FORWARDED_TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs, _root, RequestReceiveContent);
FORWARDED_TYPED_EVENT(RequestLaunchPosition, Windows::Foundation::IInspectable, winrt::TerminalApp::LaunchPositionRequest, _root, RequestLaunchPosition);
FORWARDED_TYPED_EVENT(RequestWindowList, Windows::Foundation::IInspectable, winrt::TerminalApp::WindowListRequest, _root, RequestWindowList);
#ifdef UNIT_TESTING
friend class TerminalAppLocalTests::CommandlineTest;

View File

@@ -55,11 +55,13 @@ namespace TerminalApp
Int32 SetStartupCommandline(CommandlineArgs args);
void SetStartupContent(String json, Windows.Foundation.IReference<Windows.Foundation.Rect> bounds);
void SetPersistedLayout(Microsoft.Terminal.Settings.Model.WindowLayout layout);
Int32 ExecuteCommandline(CommandlineArgs args);
Boolean ShouldImmediatelyHandoffToElevated();
void HandoffToElevated();
Microsoft.Terminal.Settings.Model.WindowLayout GetWindowLayout();
void PersistState();
Windows.UI.Xaml.UIElement GetRoot();
@@ -126,6 +128,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, SummonWindowByIdRequestedArgs> SummonWindowByIdRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
event Windows.Foundation.TypedEventHandler<Object, TerminalApp.SystemMenuChangeArgs> SystemMenuChangeRequested;
@@ -137,6 +140,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, RequestMoveContentArgs> RequestMoveContent;
event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent;
event Windows.Foundation.TypedEventHandler<Object, LaunchPositionRequest> RequestLaunchPosition;
event Windows.Foundation.TypedEventHandler<Object, WindowListRequest> RequestWindowList;
void AttachContent(String content, UInt32 tabIndex);
void SendContentToOther(RequestReceiveContentArgs args);

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 IdentifyWindowsKey{ "identifyWindows" };
static constexpr std::string_view RenameWindowKey{ "renameWindow" };
static constexpr std::string_view OpenWorkspaceKey{ "openWorkspace" };
static constexpr std::string_view OpenWindowRenamerKey{ "openWindowRenamer" };
static constexpr std::string_view DisplayWorkingDirectoryKey{ "debugTerminalCwd" };
static constexpr std::string_view SearchForTextKey{ "searchWeb" };

View File

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

View File

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

View File

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

View File

@@ -113,7 +113,8 @@
ON_ALL_ACTIONS(OpenScratchpad) \
ON_ALL_ACTIONS(OpenAbout) \
ON_ALL_ACTIONS(QuickFix) \
ON_ALL_ACTIONS(OpenCWD)
ON_ALL_ACTIONS(OpenCWD) \
ON_ALL_ACTIONS(OpenWorkspace)
#define ALL_SHORTCUT_ACTIONS_WITH_ARGS \
ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize) \
@@ -158,7 +159,8 @@
ON_ALL_ACTIONS_WITH_ARGS(Suggestions) \
ON_ALL_ACTIONS_WITH_ARGS(SelectCommand) \
ON_ALL_ACTIONS_WITH_ARGS(SelectOutput) \
ON_ALL_ACTIONS_WITH_ARGS(ColorSelection)
ON_ALL_ACTIONS_WITH_ARGS(ColorSelection) \
ON_ALL_ACTIONS_WITH_ARGS(OpenWorkspace)
// These two macros here are for actions that we only use as internal currency.
// They don't need to be parsed by the settings model, or saved as actions to

View File

@@ -20,6 +20,7 @@ static constexpr std::string_view TabLayoutKey{ "tabLayout" };
static constexpr std::string_view InitialPositionKey{ "initialPosition" };
static constexpr std::string_view InitialSizeKey{ "initialSize" };
static constexpr std::string_view LaunchModeKey{ "launchMode" };
static constexpr std::string_view PersistedWorkspacesKey{ "persistedWorkspaces" };
namespace Microsoft::Terminal::Settings::Model::JsonUtils
{
@@ -276,6 +277,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
// Manually handled because IMap<K,V> has a comma that breaks the X-macro.
if (WI_IsFlagSet(parseSource, FileSource::Local))
state->PersistedWorkspaces = JsonUtils::GetValueForKey<std::optional<Windows::Foundation::Collections::IMap<hstring, Model::WindowLayout>>>(root, PersistedWorkspacesKey);
}
Json::Value ApplicationState::ToJson(FileSource parseSource) const noexcept
@@ -298,6 +303,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
// Manually handled because IMap<K,V> has a comma that breaks the X-macro.
if (WI_IsFlagSet(parseSource, FileSource::Local))
JsonUtils::SetValueForKey(root, PersistedWorkspacesKey, state->PersistedWorkspaces);
}
return root;
}
@@ -341,6 +350,114 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return false;
}
void ApplicationState::SaveWorkspace(const hstring& name, const Model::WindowLayout& layout)
{
{
const auto state = _state.lock();
if (!state->PersistedWorkspaces || !*state->PersistedWorkspaces)
{
state->PersistedWorkspaces = winrt::single_threaded_map<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.
// Return Value:
// - true if an entry was renamed, false otherwise.
bool ApplicationState::RenameWorkspace(const hstring& oldName, const hstring& newName)
{
if (oldName == newName || oldName.empty() || newName.empty())
{
return false;
}
bool renamed{ false };
{
const auto state = _state.lock();
if (state->PersistedWorkspaces && *state->PersistedWorkspaces)
{
auto map = *state->PersistedWorkspaces;
if (map.HasKey(oldName))
{
const auto layout = map.Lookup(oldName);
map.Insert(newName, layout);
map.Remove(oldName);
renamed = true;
}
}
}
if (renamed)
{
_throttler();
}
return renamed;
}
// Method Description:
// - Atomically remove and return a persisted workspace entry. This is the
// intended API for the startup path that restores a named workspace,
// because it guarantees only one caller can claim a given workspace.
// Return Value:
// - The layout that was stored under `name`, or nullptr if there was none.
Model::WindowLayout ApplicationState::TakeWorkspace(const hstring& name)
{
Model::WindowLayout result{ nullptr };
{
const auto state = _state.lock();
if (state->PersistedWorkspaces && *state->PersistedWorkspaces)
{
auto map = *state->PersistedWorkspaces;
if (map.HasKey(name))
{
result = map.Lookup(name);
map.Remove(name);
}
}
}
if (result)
{
_throttler();
}
return result;
}
Windows::Foundation::Collections::IMapView<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
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type ApplicationState::name() const noexcept \

View File

@@ -75,6 +75,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
bool DismissBadge(const hstring& badgeId);
bool BadgeDismissed(const hstring& badgeId) const;
void SaveWorkspace(const hstring& name, const Model::WindowLayout& layout);
bool RemoveWorkspace(const hstring& name);
bool RenameWorkspace(const hstring& oldName, const hstring& newName);
Model::WindowLayout TakeWorkspace(const hstring& name);
Windows::Foundation::Collections::IMapView<hstring, Model::WindowLayout> AllPersistedWorkspaces();
// State getters/setters
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type name() const noexcept; \
@@ -88,6 +94,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) std::optional<type> name{ __VA_ARGS__ };
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
// Manually declared because IMap<K,V> has a comma that breaks the macro.
std::optional<Windows::Foundation::Collections::IMap<hstring, Model::WindowLayout>> PersistedWorkspaces;
};
til::shared_mutex<state_t> _state;
std::filesystem::path _sharedPath;

View File

@@ -36,6 +36,12 @@ namespace Microsoft.Terminal.Settings.Model
Boolean DismissBadge(String badgeId);
Boolean BadgeDismissed(String badgeId);
void SaveWorkspace(String name, WindowLayout layout);
Boolean RemoveWorkspace(String name);
Boolean RenameWorkspace(String oldName, String newName);
WindowLayout TakeWorkspace(String name);
Windows.Foundation.Collections.IMapView<String, WindowLayout> AllPersistedWorkspaces();
String SettingsHash;
Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts;
Windows.Foundation.Collections.IVector<String> RecentCommands;

View File

@@ -160,7 +160,8 @@ Author(s):
X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \
X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \
X(bool, RainbowFrame, "experimental.rainbowFrame", false) \
X(bool, UseMica, "useMica", false)
X(bool, UseMica, "useMica", false) \
X(bool, ShowWindowsButton, "showWindowsButton", true)
#define MTSM_THEME_SETTINGS_SETTINGS(X) \
X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "theme", winrt::Windows::UI::Xaml::ElementTheme::Default)

View File

@@ -518,6 +518,13 @@
<data name="ResetWindowNameCommandKey" xml:space="preserve">
<value>Reset window name</value>
</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">
<value>Rename window...</value>
</data>

View File

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

View File

@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "../TerminalSettingsModel/ApplicationState.h"
using namespace Microsoft::Console;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace WEX::Common;
using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace SettingsModelUnitTests
{
// Covers the workspace-persistence APIs added to ApplicationState:
// SaveWorkspace / RemoveWorkspace / RenameWorkspace / TakeWorkspace /
// AllPersistedWorkspaces.
// All tests operate on a throw-away ApplicationState instance pointed at
// a temp directory, so they don't touch the real user state.
class ApplicationStateTests
{
TEST_CLASS(ApplicationStateTests);
TEST_METHOD(SaveAndLookupWorkspace);
TEST_METHOD(RemoveWorkspaceReturnsFalseWhenMissing);
TEST_METHOD(RenameWorkspaceMigratesEntry);
TEST_METHOD(RenameWorkspaceNoOpForEmptyOrEqualNames);
TEST_METHOD(RenameWorkspaceNoOpForMissingEntry);
TEST_METHOD(TakeWorkspaceRemovesAndReturns);
TEST_METHOD(TakeWorkspaceReturnsNullWhenMissing);
private:
static std::filesystem::path _tempRoot()
{
auto root = std::filesystem::temp_directory_path() / L"WT_ApplicationStateTests";
std::error_code ec;
std::filesystem::create_directories(root, ec);
// Best-effort clean of any leftover state.json from a prior run so
// tests see an empty starting point.
std::filesystem::remove(root / L"state.json", ec);
std::filesystem::remove(root / L"elevated-state.json", ec);
return root;
}
static winrt::com_ptr<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"));
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,8 @@
<ClCompile Include="TerminalSettingsTests.cpp" />
<ClCompile Include="ThemeTests.cpp" />
<ClCompile Include="MediaResourceTests.cpp" />
<ClCompile Include="WindowSettingsTests.cpp" />
<ClCompile Include="ApplicationStateTests.cpp" />
<ClCompile Include="../TerminalSettingsAppAdapterLib/TerminalSettings.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>

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.
_windowLogic = _appLogic.CreateNewWindow();
if (const auto content = windowArgs.Content(); !content.empty())
if (const auto layout = windowArgs.PersistedLayout())
{
_windowLogic.SetPersistedLayout(layout);
_launchShowWindowCommand = SW_NORMAL;
}
else if (const auto content = windowArgs.Content(); !content.empty())
{
_windowLogic.SetStartupContent(content, windowArgs.InitialBounds());
_launchShowWindowCommand = SW_NORMAL;
@@ -265,11 +270,13 @@ void AppHost::Initialize()
_revokers.IsQuakeWindowChanged = _windowLogic.IsQuakeWindowChanged(winrt::auto_revoke, { this, &AppHost::_IsQuakeWindowChanged });
_revokers.SummonWindowRequested = _windowLogic.SummonWindowRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowRequested });
_revokers.SummonWindowByIdRequested = _windowLogic.SummonWindowByIdRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowByIdRequested });
_revokers.OpenSystemMenu = _windowLogic.OpenSystemMenu(winrt::auto_revoke, { this, &AppHost::_OpenSystemMenu });
_revokers.QuitRequested = _windowLogic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll });
_revokers.ShowWindowChanged = _windowLogic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged });
_revokers.RequestMoveContent = _windowLogic.RequestMoveContent(winrt::auto_revoke, { this, &AppHost::_handleMoveContent });
_revokers.RequestReceiveContent = _windowLogic.RequestReceiveContent(winrt::auto_revoke, { this, &AppHost::_handleReceiveContent });
_revokers.RequestWindowList = _windowLogic.RequestWindowList(winrt::auto_revoke, { this, &AppHost::_HandleRequestWindowList });
// BODGY
// On certain builds of Windows, when Terminal is set as the default
@@ -409,6 +416,28 @@ void AppHost::_HandleRequestLaunchPosition(const winrt::Windows::Foundation::IIn
args.Position(_GetWindowLaunchPosition());
}
void AppHost::_HandleRequestWindowList(const winrt::Windows::Foundation::IInspectable& /*sender*/,
winrt::TerminalApp::WindowListRequest args)
{
// Ask the Emperor (on the main thread) for the current window list.
// SendMessage blocks until the message is processed, so this is
// synchronous and the results vector is filled in-place.
std::vector<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 wle;
wle.Id(entry.Id);
wle.Name(winrt::hstring{ entry.Name });
windowEntries.Append(wle);
}
}
LaunchPosition AppHost::_GetWindowLaunchPosition()
{
LaunchPosition pos{};
@@ -1064,6 +1093,23 @@ void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspecta
HandleSummon(std::move(summonArgs));
}
void AppHost::_SummonWindowByIdRequested(const winrt::Windows::Foundation::IInspectable&,
const winrt::TerminalApp::SummonWindowByIdRequestedArgs& args)
{
// Summon the window by its ID without creating a new tab.
// We look up the target window in WindowEmperor and call HandleSummon directly.
const auto targetId = args.WindowId();
if (auto* targetWindow = _windowManager->GetWindowById(targetId))
{
winrt::TerminalApp::SummonWindowBehavior summonBehavior;
summonBehavior.MoveToCurrentDesktop(false);
summonBehavior.DropdownDuration(0);
summonBehavior.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace);
summonBehavior.ToggleVisibility(false); // Do not toggle, just make visible.
targetWindow->HandleSummon(std::move(summonBehavior));
}
}
void AppHost::_OpenSystemMenu(const winrt::Windows::Foundation::IInspectable&,
const winrt::Windows::Foundation::IInspectable&)
{

View File

@@ -91,6 +91,9 @@ private:
void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
void _SummonWindowByIdRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::TerminalApp::SummonWindowByIdRequestedArgs& args);
void _OpenSystemMenu(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
@@ -130,6 +133,8 @@ private:
void _AppTitleChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&);
void _HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& sender,
winrt::TerminalApp::LaunchPositionRequest args);
void _HandleRequestWindowList(const winrt::Windows::Foundation::IInspectable& sender,
winrt::TerminalApp::WindowListRequest args);
// Helper struct. By putting these all into one struct, we can revoke them
// all at once, by assigning _revokers to a fresh Revokers instance. That'll
@@ -151,12 +156,14 @@ private:
winrt::TerminalApp::TerminalWindow::IdentifyWindowsRequested_revoker IdentifyWindowsRequested;
winrt::TerminalApp::TerminalWindow::IsQuakeWindowChanged_revoker IsQuakeWindowChanged;
winrt::TerminalApp::TerminalWindow::SummonWindowRequested_revoker SummonWindowRequested;
winrt::TerminalApp::TerminalWindow::SummonWindowByIdRequested_revoker SummonWindowByIdRequested;
winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu;
winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested;
winrt::TerminalApp::TerminalWindow::ShowWindowChanged_revoker ShowWindowChanged;
winrt::TerminalApp::TerminalWindow::RequestMoveContent_revoker RequestMoveContent;
winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent;
winrt::TerminalApp::TerminalWindow::RequestLaunchPosition_revoker RequestLaunchPosition;
winrt::TerminalApp::TerminalWindow::RequestWindowList_revoker RequestWindowList;
winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged;
winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged;
winrt::TerminalApp::TerminalWindow::WindowSizeChanged_revoker WindowSizeChanged;

View File

@@ -678,6 +678,19 @@ void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs arg
{
winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) };
request.WindowName(std::move(windowName));
// If we're opening a named window that doesn't exist yet, atomically
// claim any persisted workspace with that name so we restore it here
// and no subsequent window can pick up the same entry.
const auto& reqName = request.WindowName();
if (!reqName.empty())
{
if (const auto layout = ApplicationState::SharedInstance().TakeWorkspace(reqName))
{
request.PersistedLayout(layout);
}
}
CreateNewWindow(std::move(request));
}
}
@@ -943,6 +956,22 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c
// anyway (since we threw and exited this message handler) so this at least gives back our
// deterministic window count management.
const auto strong = *it;
// Before destroying a named window, persist its full
// tab/buffer state as a workspace so it can be restored later.
try
{
const auto windowName = strong->Logic().WindowProperties().WindowName();
if (!windowName.empty())
{
if (const auto layout = strong->Logic().GetWindowLayout())
{
ApplicationState::SharedInstance().SaveWorkspace(windowName, layout);
}
}
}
CATCH_LOG();
_windows.erase(it);
try
{
@@ -970,6 +999,19 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c
host->Logic().IdentifyWindow();
}
return 0;
case WM_GET_WINDOW_LIST:
{
auto* result = reinterpret_cast<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:
switch (LOWORD(lParam))
{

View File

@@ -28,6 +28,16 @@ public:
WM_MESSAGE_BOX_CLOSED,
WM_IDENTIFY_ALL_WINDOWS,
WM_NOTIFY_FROM_NOTIFICATION_AREA,
WM_GET_WINDOW_LIST,
};
// Used by WM_GET_WINDOW_LIST. Callers allocate a vector on their
// stack and pass a pointer through LPARAM; the emperor fills it in
// synchronously via SendMessage.
struct WindowListEntry
{
uint64_t Id;
std::wstring Name;
};
HWND GetMainWindow() const noexcept;