diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt
index 56ae1aa03a..adac99c60a 100644
--- a/.github/actions/spelling/allow/names.txt
+++ b/.github/actions/spelling/allow/names.txt
@@ -81,6 +81,7 @@ Thysell
Walisch
WDX
Wellons
+Westerman
Wirt
Wojciech
zadjii
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index c9e694e0b4..6bee01dfa5 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -251,6 +251,7 @@ conattrs
conbufferout
concfg
conclnt
+concretizations
conddkrefs
condrv
conechokey
@@ -2112,6 +2113,7 @@ webpage
websites
websockets
wekyb
+WEOF
wex
wextest
wextestclass
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index e20f854568..a92ed350d1 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -561,6 +561,160 @@
},
"type": "object"
},
+ "NewTabMenuEntryType": {
+ "enum": [
+ "source",
+ "profile",
+ "folder",
+ "separator",
+ "remainingProfiles",
+ "matchProfiles"
+ ]
+ },
+ "NewTabMenuEntry": {
+ "properties": {
+ "type": {
+ "description": "The type of menu entry",
+ "$ref": "#/$defs/NewTabMenuEntryType"
+ }
+ },
+ "required": [
+ "type"
+ ],
+ "type": "object"
+ },
+ "FolderEntryInlining": {
+ "enum": [
+ "never",
+ "auto"
+ ]
+ },
+ "FolderEntry": {
+ "description": "A folder entry in the new tab dropdown",
+ "allOf": [
+ {
+ "$ref": "#/$defs/NewTabMenuEntry"
+ },
+ {
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "folder"
+ },
+ "name": {
+ "type": "string",
+ "default": "",
+ "description": "The name of the folder to show in the menu"
+ },
+ "icon": {
+ "$ref": "#/$defs/Icon"
+ },
+ "entries": {
+ "type": "array",
+ "description": "The entries to put inside this folder",
+ "minItems": 1,
+ "items": {
+ "$ref": "#/$defs/NewTabMenuEntry"
+ }
+ },
+ "inline": {
+ "description": "When set to auto and the folder only has a single entry, the entry will show directly and no folder will be rendered",
+ "default": "never",
+ "$ref": "#/$defs/FolderEntryInlining"
+ },
+ "allowEmpty": {
+ "description": "Whether to render a folder without entries, or to hide it",
+ "default": "false",
+ "type": "boolean"
+ }
+ }
+ }
+ ]
+ },
+ "SeparatorEntry": {
+ "description": "A separator in the new tab dropdown",
+ "allOf": [
+ {
+ "$ref": "#/$defs/NewTabMenuEntry"
+ },
+ {
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "separator"
+ }
+ }
+ }
+ ]
+ },
+ "ProfileEntry": {
+ "description": "A profile in the new tab dropdown",
+ "allOf": [
+ {
+ "$ref": "#/$defs/NewTabMenuEntry"
+ },
+ {
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "profile"
+ },
+ "profile": {
+ "type": "string",
+ "default": "",
+ "description": "The name or GUID of the profile to show in this entry"
+ }
+ }
+ }
+ ]
+ },
+ "RemainingProfilesEntry": {
+ "description": "The set of profiles that are not yet explicitly included in another entry, such as the profile or source entries. This entry can be used at most one time!",
+ "allOf": [
+ {
+ "$ref": "#/$defs/NewTabMenuEntry"
+ },
+ {
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "remainingProfiles"
+ }
+ }
+ }
+ ]
+ },
+ "MatchProfilesEntry": {
+ "description": "A set of profiles all matching the given name, source, or command line, to show in the new tab dropdown",
+ "allOf": [
+ {
+ "$ref": "#/$defs/NewTabMenuEntry"
+ },
+ {
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "matchProfiles"
+ },
+ "name": {
+ "type": "string",
+ "default": "",
+ "description": "The name of the profiles to match"
+ },
+ "source": {
+ "type": "string",
+ "default": "",
+ "description": "The source of the profiles to match"
+ },
+ "commandline": {
+ "type": "string",
+ "default": "",
+ "description": "The command line of the profiles to match"
+ }
+ }
+ }
+ ]
+ },
"SwitchToAdjacentTabArgs": {
"oneOf": [
{
@@ -1568,6 +1722,29 @@
}
]
},
+ "NewTabMenu": {
+ "description": "Defines the order and structure of the 'new tab' menu. It can consist of e.g. profiles, folders, and separators.",
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/FolderEntry"
+ },
+ {
+ "$ref": "#/$defs/SeparatorEntry"
+ },
+ {
+ "$ref": "#/$defs/ProfileEntry"
+ },
+ {
+ "$ref": "#/$defs/MatchProfilesEntry"
+ },
+ {
+ "$ref": "#/$defs/RemainingProfilesEntry"
+ }
+ ]
+ }
+ },
"Keybinding": {
"additionalProperties": false,
"properties": {
@@ -1946,6 +2123,9 @@
},
"type": "array"
},
+ "newTabMenu": {
+ "$ref": "#/$defs/NewTabMenu"
+ },
"language": {
"default": "",
"description": "Sets an override for the app's preferred language, expressed as a BCP-47 language tag like en-US.",
diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp
index 040c39d29d..4381e24e0b 100644
--- a/src/cascadia/TerminalApp/AppLogic.cpp
+++ b/src/cascadia/TerminalApp/AppLogic.cpp
@@ -52,6 +52,7 @@ static const std::array settingsLoadWarningsLabels {
USES_RESOURCE(L"FailedToParseStartupActions"),
USES_RESOURCE(L"FailedToParseSubCommands"),
USES_RESOURCE(L"UnknownTheme"),
+ USES_RESOURCE(L"DuplicateRemainingProfilesEntry"),
};
static const std::array settingsLoadErrorsLabels {
USES_RESOURCE(L"NoProfilesText"),
diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
index d6a6772b69..fa5194db7a 100644
--- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
@@ -232,19 +232,19 @@
Warnings were found while parsing your keybindings:
- • Found a keybinding with too many strings for the "keys" array. There should only be one string value in the "keys" array.
- {Locked="\"keys\"","•"} This glyph is a bullet, used in a bulleted list.
+ • Found a keybinding with too many strings for the "keys" array. There should only be one string value in the "keys" array.
+ {Locked="\"keys\"","•"} This glyph is a bullet, used in a bulleted list.
- • Failed to parse all subcommands of nested command.
+ • Failed to parse all subcommands of nested command.
- • Found a keybinding that was missing a required parameter value. This keybinding will be ignored.
- {Locked="•"} This glyph is a bullet, used in a bulleted list.
+ • Found a keybinding that was missing a required parameter value. This keybinding will be ignored.
+ {Locked="•"} This glyph is a bullet, used in a bulleted list.
- • The specified "theme" was not found in the list of themes. Temporarily falling back to the default value.
- {Locked="•"} This glyph is a bullet, used in a bulleted list.
+ • The specified "theme" was not found in the list of themes. Temporarily falling back to the default value.
+ {Locked="•"} This glyph is a bullet, used in a bulleted list.
The "globals" property is deprecated - your settings might need updating.
@@ -752,6 +752,10 @@
Suggestions found: {0}
{0} will be replaced with a number.
+
+ The "newTabMenu" field contains more than one entry of type "remainingProfiles". Only the first one will be considered.
+ {Locked="newTabMenu"} {Locked="remainingProfiles"}
+
Open a dialog containing product information
@@ -788,4 +792,7 @@
Close this tab
-
+
+ Empty...
+
+
\ No newline at end of file
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index 564f99379d..f03c6101a1 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -824,79 +824,14 @@ namespace winrt::TerminalApp::implementation
auto newTabFlyout = WUX::Controls::MenuFlyout{};
newTabFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedLeft);
- auto actionMap = _settings.ActionMap();
- const auto defaultProfileGuid = _settings.GlobalSettings().DefaultProfile();
- // the number of profiles should not change in the loop for this to work
- const auto profileCount = gsl::narrow_cast(_settings.ActiveProfiles().Size());
- for (auto profileIndex = 0; profileIndex < profileCount; profileIndex++)
+ // Create profile entries from the NewTabMenu configuration using a
+ // recursive helper function. This returns a std::vector of FlyoutItemBases,
+ // that we then add to our Flyout.
+ auto entries = _settings.GlobalSettings().NewTabMenu();
+ auto items = _CreateNewTabFlyoutItems(entries);
+ for (const auto& item : items)
{
- const auto profile = _settings.ActiveProfiles().GetAt(profileIndex);
- auto profileMenuItem = WUX::Controls::MenuFlyoutItem{};
-
- // Add the keyboard shortcuts based on the number of profiles defined
- // Look for a keychord that is bound to the equivalent
- // NewTab(ProfileIndex=N) action
- NewTerminalArgs newTerminalArgs{ profileIndex };
- NewTabArgs newTabArgs{ newTerminalArgs };
- auto profileKeyChord{ actionMap.GetKeyBindingForAction(ShortcutAction::NewTab, newTabArgs) };
-
- // make sure we find one to display
- if (profileKeyChord)
- {
- _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord);
- }
-
- auto profileName = profile.Name();
- profileMenuItem.Text(profileName);
-
- // If there's an icon set for this profile, set it as the icon for
- // this flyout item.
- if (!profile.Icon().empty())
- {
- auto icon = IconPathConverter::IconWUX(profile.Icon());
- Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw);
- profileMenuItem.Icon(icon);
- }
-
- if (profile.Guid() == defaultProfileGuid)
- {
- // Contrast the default profile with others in font weight.
- profileMenuItem.FontWeight(FontWeights::Bold());
- }
-
- auto newTabRun = WUX::Documents::Run();
- newTabRun.Text(RS_(L"NewTabRun/Text"));
- auto newPaneRun = WUX::Documents::Run();
- newPaneRun.Text(RS_(L"NewPaneRun/Text"));
- newPaneRun.FontStyle(FontStyle::Italic);
- auto newWindowRun = WUX::Documents::Run();
- newWindowRun.Text(RS_(L"NewWindowRun/Text"));
- newWindowRun.FontStyle(FontStyle::Italic);
- auto elevatedRun = WUX::Documents::Run();
- elevatedRun.Text(RS_(L"ElevatedRun/Text"));
- elevatedRun.FontStyle(FontStyle::Italic);
-
- auto textBlock = WUX::Controls::TextBlock{};
- textBlock.Inlines().Append(newTabRun);
- textBlock.Inlines().Append(WUX::Documents::LineBreak{});
- textBlock.Inlines().Append(newPaneRun);
- textBlock.Inlines().Append(WUX::Documents::LineBreak{});
- textBlock.Inlines().Append(newWindowRun);
- textBlock.Inlines().Append(WUX::Documents::LineBreak{});
- textBlock.Inlines().Append(elevatedRun);
-
- auto toolTip = WUX::Controls::ToolTip{};
- toolTip.Content(textBlock);
- WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip);
-
- profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) {
- if (auto page{ weakThis.get() })
- {
- NewTerminalArgs newTerminalArgs{ profileIndex };
- page->_OpenNewTerminalViaDropdown(newTerminalArgs);
- }
- });
- newTabFlyout.Items().Append(profileMenuItem);
+ newTabFlyout.Items().Append(item);
}
// add menu separator
@@ -932,6 +867,7 @@ namespace winrt::TerminalApp::implementation
settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick });
newTabFlyout.Items().Append(settingsItem);
+ auto actionMap = _settings.ActionMap();
const auto settingsKeyChord{ actionMap.GetKeyBindingForAction(ShortcutAction::OpenSettings, OpenSettingsArgs{ SettingsTarget::SettingsUI }) };
if (settingsKeyChord)
{
@@ -991,6 +927,215 @@ namespace winrt::TerminalApp::implementation
_newTabButton.Flyout(newTabFlyout);
}
+ // Method Description:
+ // - For a given list of tab menu entries, this method will create the corresponding
+ // list of flyout items. This is a recursive method that calls itself when it comes
+ // across a folder entry.
+ std::vector TerminalPage::_CreateNewTabFlyoutItems(IVector entries)
+ {
+ std::vector items;
+
+ if (entries == nullptr || entries.Size() == 0)
+ {
+ return items;
+ }
+
+ for (const auto& entry : entries)
+ {
+ if (entry == nullptr)
+ {
+ continue;
+ }
+
+ switch (entry.Type())
+ {
+ case NewTabMenuEntryType::Separator:
+ {
+ items.push_back(WUX::Controls::MenuFlyoutSeparator{});
+ break;
+ }
+ // A folder has a custom name and icon, and has a number of entries that require
+ // us to call this method recursively.
+ case NewTabMenuEntryType::Folder:
+ {
+ const auto folderEntry = entry.as();
+ const auto folderEntries = folderEntry.Entries();
+
+ // If the folder is empty, we should skip the entry if AllowEmpty is false, or
+ // when the folder should inline.
+ // The IsEmpty check includes semantics for nested (empty) folders
+ if (folderEntries.Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
+ {
+ break;
+ }
+
+ // Recursively generate flyout items
+ auto folderEntryItems = _CreateNewTabFlyoutItems(folderEntries);
+
+ // If the folder should auto-inline and there is only one item, do so.
+ if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntries.Size() == 1)
+ {
+ for (auto const& folderEntryItem : folderEntryItems)
+ {
+ items.push_back(folderEntryItem);
+ }
+
+ break;
+ }
+
+ // Otherwise, create a flyout
+ auto folderItem = WUX::Controls::MenuFlyoutSubItem{};
+ folderItem.Text(folderEntry.Name());
+
+ auto icon = _CreateNewTabFlyoutIcon(folderEntry.Icon());
+ folderItem.Icon(icon);
+
+ for (const auto& folderEntryItem : folderEntryItems)
+ {
+ folderItem.Items().Append(folderEntryItem);
+ }
+
+ // If the folder is empty, and by now we know we set AllowEmpty to true,
+ // create a placeholder item here
+ if (folderEntries.Size() == 0)
+ {
+ auto placeholder = WUX::Controls::MenuFlyoutItem{};
+ placeholder.Text(RS_(L"NewTabMenuFolderEmpty"));
+ placeholder.IsEnabled(false);
+
+ folderItem.Items().Append(placeholder);
+ }
+
+ items.push_back(folderItem);
+ break;
+ }
+ // Any "collection entry" will simply make us add each profile in the collection
+ // separately. This collection is stored as a map , so the correct
+ // profile index is already known.
+ case NewTabMenuEntryType::RemainingProfiles:
+ case NewTabMenuEntryType::MatchProfiles:
+ {
+ const auto remainingProfilesEntry = entry.as();
+ if (remainingProfilesEntry.Profiles() == nullptr)
+ {
+ break;
+ }
+
+ for (auto&& [profileIndex, remainingProfile] : remainingProfilesEntry.Profiles())
+ {
+ items.push_back(_CreateNewTabFlyoutProfile(remainingProfile, profileIndex));
+ }
+
+ break;
+ }
+ // A single profile, the profile index is also given in the entry
+ case NewTabMenuEntryType::Profile:
+ {
+ const auto profileEntry = entry.as();
+ if (profileEntry.Profile() == nullptr)
+ {
+ break;
+ }
+
+ auto profileItem = _CreateNewTabFlyoutProfile(profileEntry.Profile(), profileEntry.ProfileIndex());
+ items.push_back(profileItem);
+ break;
+ }
+ }
+ }
+
+ return items;
+ }
+
+ // Method Description:
+ // - This method creates a flyout menu item for a given profile with the given index.
+ // It makes sure to set the correct icon, keybinding, and click-action.
+ WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutProfile(const Profile profile, int profileIndex)
+ {
+ auto profileMenuItem = WUX::Controls::MenuFlyoutItem{};
+
+ // Add the keyboard shortcuts based on the number of profiles defined
+ // Look for a keychord that is bound to the equivalent
+ // NewTab(ProfileIndex=N) action
+ NewTerminalArgs newTerminalArgs{ profileIndex };
+ NewTabArgs newTabArgs{ newTerminalArgs };
+ auto profileKeyChord{ _settings.ActionMap().GetKeyBindingForAction(ShortcutAction::NewTab, newTabArgs) };
+
+ // make sure we find one to display
+ if (profileKeyChord)
+ {
+ _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord);
+ }
+
+ auto profileName = profile.Name();
+ profileMenuItem.Text(profileName);
+
+ // If there's an icon set for this profile, set it as the icon for
+ // this flyout item
+ if (!profile.Icon().empty())
+ {
+ const auto icon = _CreateNewTabFlyoutIcon(profile.Icon());
+ profileMenuItem.Icon(icon);
+ }
+
+ if (profile.Guid() == _settings.GlobalSettings().DefaultProfile())
+ {
+ // Contrast the default profile with others in font weight.
+ profileMenuItem.FontWeight(FontWeights::Bold());
+ }
+
+ auto newTabRun = WUX::Documents::Run();
+ newTabRun.Text(RS_(L"NewTabRun/Text"));
+ auto newPaneRun = WUX::Documents::Run();
+ newPaneRun.Text(RS_(L"NewPaneRun/Text"));
+ newPaneRun.FontStyle(FontStyle::Italic);
+ auto newWindowRun = WUX::Documents::Run();
+ newWindowRun.Text(RS_(L"NewWindowRun/Text"));
+ newWindowRun.FontStyle(FontStyle::Italic);
+ auto elevatedRun = WUX::Documents::Run();
+ elevatedRun.Text(RS_(L"ElevatedRun/Text"));
+ elevatedRun.FontStyle(FontStyle::Italic);
+
+ auto textBlock = WUX::Controls::TextBlock{};
+ textBlock.Inlines().Append(newTabRun);
+ textBlock.Inlines().Append(WUX::Documents::LineBreak{});
+ textBlock.Inlines().Append(newPaneRun);
+ textBlock.Inlines().Append(WUX::Documents::LineBreak{});
+ textBlock.Inlines().Append(newWindowRun);
+ textBlock.Inlines().Append(WUX::Documents::LineBreak{});
+ textBlock.Inlines().Append(elevatedRun);
+
+ auto toolTip = WUX::Controls::ToolTip{};
+ toolTip.Content(textBlock);
+ WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip);
+
+ profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) {
+ if (auto page{ weakThis.get() })
+ {
+ NewTerminalArgs newTerminalArgs{ profileIndex };
+ page->_OpenNewTerminalViaDropdown(newTerminalArgs);
+ }
+ });
+
+ return profileMenuItem;
+ }
+
+ // Method Description:
+ // - Helper method to create an IconElement that can be passed to MenuFlyoutItems and
+ // MenuFlyoutSubItems
+ IconElement TerminalPage::_CreateNewTabFlyoutIcon(const winrt::hstring& iconSource)
+ {
+ if (iconSource.empty())
+ {
+ return nullptr;
+ }
+
+ auto icon = IconPathConverter::IconWUX(iconSource);
+ Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw);
+
+ return icon;
+ }
+
// Function Description:
// Called when the openNewTabDropdown keybinding is used.
// Shows the dropdown flyout.
diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h
index 519d8786ea..5a0ad22f18 100644
--- a/src/cascadia/TerminalApp/TerminalPage.h
+++ b/src/cascadia/TerminalApp/TerminalPage.h
@@ -240,6 +240,10 @@ namespace winrt::TerminalApp::implementation
winrt::Windows::Foundation::IAsyncOperation _ShowLargePasteWarningDialog();
void _CreateNewTabFlyout();
+ std::vector _CreateNewTabFlyoutItems(winrt::Windows::Foundation::Collections::IVector entries);
+ winrt::Windows::UI::Xaml::Controls::IconElement _CreateNewTabFlyoutIcon(const winrt::hstring& icon);
+ winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutProfile(const Microsoft::Terminal::Settings::Model::Profile profile, int profileIndex);
+
void _OpenNewTabDropdown();
HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
void _CreateNewTabFromPane(std::shared_ptr pane);
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
index d8543e74b5..ac9049b4b1 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
@@ -153,6 +153,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _refreshDefaultTerminals();
void _resolveDefaultProfile() const;
+ void _resolveNewTabMenuProfiles() const;
+ void _resolveNewTabMenuProfilesSet(const winrt::Windows::Foundation::Collections::IVector entries, winrt::Windows::Foundation::Collections::IMap& remainingProfiles, Model::RemainingProfilesEntry& remainingProfilesEntry) const;
void _validateSettings();
void _validateAllSchemesExist();
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
index 39124e2f0d..bc59792677 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
@@ -31,6 +31,10 @@
#include "DefaultTerminal.h"
#include "FileUtils.h"
+#include "ProfileEntry.h"
+#include "FolderEntry.h"
+#include "MatchProfilesEntry.h"
+
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::ApplicationModel::AppExtensions;
using namespace winrt::Microsoft::Terminal::Settings;
@@ -1104,6 +1108,7 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) :
_warnings = winrt::single_threaded_vector(std::move(warnings));
_resolveDefaultProfile();
+ _resolveNewTabMenuProfiles();
_validateSettings();
}
@@ -1282,3 +1287,162 @@ void CascadiaSettings::_resolveDefaultProfile() const
// Use the first profile as the new default.
GlobalSettings().DefaultProfile(_allProfiles.GetAt(0).Guid());
}
+
+// Method Description:
+// - Iterates through the "newTabMenu" entries and for ProfileEntries resolves the "profile"
+// fields, which can be a profile name, to a GUID and stores it back.
+// - It finds any "source" entries and finds all profiles generated by that source
+// - Lastly, it finds any "remainingProfiles" entries and stores which profiles they
+// represent (those that were not resolved before). It adds a warning when
+// multiple of these entries are found.
+void CascadiaSettings::_resolveNewTabMenuProfiles() const
+{
+ Model::RemainingProfilesEntry remainingProfilesEntry = nullptr;
+
+ // The TerminalPage needs to know which profile has which profile ID. To prevent
+ // continuous lookups in the _activeProfiles vector, we create a map
+ // to store these indices in-flight.
+ auto remainingProfilesMap = std::map{};
+ auto activeProfileCount = gsl::narrow_cast(_activeProfiles.Size());
+ for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
+ {
+ remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex));
+ }
+
+ // We keep track of the "remaining profiles" - those that have not yet been resolved
+ // in either a "profile" or "source" entry. They will possibly be assigned to a
+ // "remainingProfiles" entry
+ auto remainingProfiles = single_threaded_map(std::move(remainingProfilesMap));
+
+ // We call a recursive helper function to process the entries
+ auto entries = _globals->NewTabMenu();
+ _resolveNewTabMenuProfilesSet(entries, remainingProfiles, remainingProfilesEntry);
+
+ // If a "remainingProfiles" entry has been found, assign to it the remaining profiles
+ if (remainingProfilesEntry != nullptr)
+ {
+ remainingProfilesEntry.Profiles(remainingProfiles);
+ }
+
+ // If the configuration does not have a "newTabMenu" field, GlobalAppSettings
+ // will return a default value containing just a "remainingProfiles" entry. However,
+ // this value is regenerated on every "get" operation, so the effect of setting
+ // the remaining profiles above will be undone. So only in the case that no custom
+ // value is present in GlobalAppSettings, we will store the modified default value.
+ if (!_globals->HasNewTabMenu())
+ {
+ _globals->NewTabMenu(entries);
+ }
+}
+
+// Method Description:
+// - Helper function that processes a set of tab menu entries and resolves any profile names
+// or source fields as necessary - see function above for a more detailed explanation.
+void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector entries, IMap& remainingProfilesMap, Model::RemainingProfilesEntry& remainingProfilesEntry) const
+{
+ if (entries == nullptr || entries.Size() == 0)
+ {
+ return;
+ }
+
+ for (const auto& entry : entries)
+ {
+ if (entry == nullptr)
+ {
+ continue;
+ }
+
+ switch (entry.Type())
+ {
+ // For a simple profile entry, the "profile" field can either be a name or a GUID. We
+ // use the GetProfileByName function to resolve this name to a profile instance, then
+ // find the index of that profile, and store this information in the entry.
+ case NewTabMenuEntryType::Profile:
+ {
+ // We need to access the unresolved profile name, a field that is not exposed
+ // in the projected class. So, we need to first obtain our implementation struct
+ // instance, to access this field.
+ const auto profileEntry{ winrt::get_self(entry.as()) };
+
+ // Find the profile by name
+ const auto profile = GetProfileByName(profileEntry->ProfileName());
+
+ // If not found, or if the profile is hidden, skip it
+ if (profile == nullptr || profile.Hidden())
+ {
+ profileEntry->Profile(nullptr); // override "default" profile
+ break;
+ }
+
+ // Find the index of the resulting profile and store the result in the entry
+ uint32_t profileIndex;
+ _activeProfiles.IndexOf(profile, profileIndex);
+
+ profileEntry->Profile(profile);
+ profileEntry->ProfileIndex(profileIndex);
+
+ // Remove from remaining profiles list (map)
+ remainingProfilesMap.TryRemove(profileIndex);
+
+ break;
+ }
+
+ // For a remainingProfiles entry, we store it in the variable that is passed back to our caller,
+ // except when that one has already been set (so we found a second/third/...) instance, which will
+ // trigger a warning. We then ignore this entry.
+ case NewTabMenuEntryType::RemainingProfiles:
+ {
+ if (remainingProfilesEntry != nullptr)
+ {
+ _warnings.Append(SettingsLoadWarnings::DuplicateRemainingProfilesEntry);
+ }
+ else
+ {
+ remainingProfilesEntry = entry.as();
+ }
+ break;
+ }
+
+ // For a folder, we simply call this method recursively
+ case NewTabMenuEntryType::Folder:
+ {
+ // We need to access the unfiltered entry list, a field that is not exposed
+ // in the projected class. So, we need to first obtain our implementation struct
+ // instance, to access this field.
+ const auto folderEntry{ winrt::get_self(entry.as()) };
+
+ auto folderEntries = folderEntry->RawEntries();
+ _resolveNewTabMenuProfilesSet(folderEntries, remainingProfilesMap, remainingProfilesEntry);
+ break;
+ }
+
+ // For a "matchProfiles" entry, we iterate through the list of all profiles and
+ // find all those matching: generated by the same source, having the same name, or
+ // having the same commandline. This can be expanded with regex support in the future.
+ // We make sure that none of the matches are included in the "remaining profiles" section.
+ case NewTabMenuEntryType::MatchProfiles:
+ {
+ // We need to access the matching function, which is not exposed in the projected class.
+ // So, we need to first obtain our implementation struct instance, to access this field.
+ const auto matchEntry{ winrt::get_self(entry.as()) };
+
+ matchEntry->Profiles(single_threaded_map());
+
+ auto activeProfileCount = gsl::narrow_cast(_activeProfiles.Size());
+ for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
+ {
+ const auto profile = _activeProfiles.GetAt(profileIndex);
+
+ // On a match, we store it in the entry and remove it from the remaining list
+ if (matchEntry->MatchesProfile(profile))
+ {
+ matchEntry->Profiles().Insert(profileIndex, profile);
+ remainingProfilesMap.TryRemove(profileIndex);
+ }
+ }
+
+ break;
+ }
+ }
+ }
+}
diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.cpp b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp
new file mode 100644
index 0000000000..54af1d8c77
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "FolderEntry.h"
+#include "JsonUtils.h"
+#include "TerminalSettingsSerializationHelpers.h"
+
+#include "FolderEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+using namespace winrt::Windows::Foundation::Collections;
+
+static constexpr std::string_view NameKey{ "name" };
+static constexpr std::string_view IconKey{ "icon" };
+static constexpr std::string_view EntriesKey{ "entries" };
+static constexpr std::string_view InliningKey{ "inline" };
+static constexpr std::string_view AllowEmptyKey{ "allowEmpty" };
+
+FolderEntry::FolderEntry() noexcept :
+ FolderEntry{ winrt::hstring{} }
+{
+}
+
+FolderEntry::FolderEntry(const winrt::hstring& name) noexcept :
+ FolderEntryT(NewTabMenuEntryType::Folder),
+ _Name{ name }
+{
+}
+
+Json::Value FolderEntry::ToJson() const
+{
+ auto json = NewTabMenuEntry::ToJson();
+
+ JsonUtils::SetValueForKey(json, NameKey, _Name);
+ JsonUtils::SetValueForKey(json, IconKey, _Icon);
+ JsonUtils::SetValueForKey(json, EntriesKey, _Entries);
+ JsonUtils::SetValueForKey(json, InliningKey, _Inlining);
+ JsonUtils::SetValueForKey(json, AllowEmptyKey, _AllowEmpty);
+
+ return json;
+}
+
+winrt::com_ptr FolderEntry::FromJson(const Json::Value& json)
+{
+ auto entry = winrt::make_self();
+
+ JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
+ JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
+ JsonUtils::GetValueForKey(json, EntriesKey, entry->_Entries);
+ JsonUtils::GetValueForKey(json, InliningKey, entry->_Inlining);
+ JsonUtils::GetValueForKey(json, AllowEmptyKey, entry->_AllowEmpty);
+
+ return entry;
+}
+
+// A FolderEntry should only expose the entries to actually render to WinRT,
+// to keep the logic for collapsing/expanding more centralised.
+using NewTabMenuEntryModel = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntry;
+IVector FolderEntry::Entries() const
+{
+ // We filter the full list of entries from JSON to just include the
+ // non-empty ones.
+ IVector result{ winrt::single_threaded_vector() };
+
+ for (const auto& entry : _Entries)
+ {
+ if (entry == nullptr)
+ {
+ continue;
+ }
+
+ switch (entry.Type())
+ {
+ case NewTabMenuEntryType::Invalid:
+ continue;
+
+ // A profile is filtered out if it is not valid, so if it was not resolved
+ case NewTabMenuEntryType::Profile:
+ {
+ const auto profileEntry = entry.as();
+ if (profileEntry.Profile() == nullptr)
+ {
+ continue;
+ }
+ break;
+ }
+
+ // Any profile collection is filtered out if there are no results
+ case NewTabMenuEntryType::RemainingProfiles:
+ case NewTabMenuEntryType::MatchProfiles:
+ {
+ const auto profileCollectionEntry = entry.as();
+ if (profileCollectionEntry.Profiles().Size() == 0)
+ {
+ continue;
+ }
+ break;
+ }
+
+ // A folder is filtered out if it has an effective size of 0 (calling
+ // this filtering method recursively), and if it is not allowed to be
+ // empty, or if it should auto-inline.
+ case NewTabMenuEntryType::Folder:
+ {
+ const auto folderEntry = entry.as();
+ if (folderEntry.Entries().Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
+ {
+ continue;
+ }
+ break;
+ }
+ }
+
+ result.Append(entry);
+ }
+
+ return result;
+}
diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.h b/src/cascadia/TerminalSettingsModel/FolderEntry.h
new file mode 100644
index 0000000000..619d228fa7
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/FolderEntry.h
@@ -0,0 +1,55 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- FolderEntry.h
+
+Abstract:
+- A folder entry in the "new tab" dropdown menu,
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "pch.h"
+#include "NewTabMenuEntry.h"
+#include "FolderEntry.g.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+ struct FolderEntry : FolderEntryT
+ {
+ public:
+ FolderEntry() noexcept;
+ explicit FolderEntry(const winrt::hstring& name) noexcept;
+
+ Json::Value ToJson() const override;
+ static com_ptr FromJson(const Json::Value& json);
+
+ // In JSON, we can set arbitrarily many profiles or nested profiles, that might not all
+ // be rendered; for example, when a profile entry is invalid, or when a folder is empty.
+ // Therefore, we will store the JSON entries list internally, and then expose only the
+ // entries to be rendered to WinRT.
+ winrt::Windows::Foundation::Collections::IVector Entries() const;
+ winrt::Windows::Foundation::Collections::IVector RawEntries() const
+ {
+ return _Entries;
+ };
+
+ WINRT_PROPERTY(winrt::hstring, Name);
+ WINRT_PROPERTY(winrt::hstring, Icon);
+ WINRT_PROPERTY(FolderEntryInlining, Inlining, FolderEntryInlining::Never);
+ WINRT_PROPERTY(bool, AllowEmpty, false);
+
+ private:
+ winrt::Windows::Foundation::Collections::IVector _Entries{};
+ };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+ BASIC_FACTORY(FolderEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h
index e90253a28c..4a594948eb 100644
--- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h
+++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h
@@ -23,6 +23,8 @@ Author(s):
#include "Command.h"
#include "ColorScheme.h"
#include "Theme.h"
+#include "NewTabMenuEntry.h"
+#include "RemainingProfilesEntry.h"
// fwdecl unittest classes
namespace SettingsModelLocalTests
diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
index a6ff9c188c..06931641e4 100644
--- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
+++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
@@ -6,6 +6,7 @@
import "Theme.idl";
import "ColorScheme.idl";
import "ActionMap.idl";
+import "NewTabMenuEntry.idl";
namespace Microsoft.Terminal.Settings.Model
{
@@ -94,6 +95,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_SETTING(Boolean, AlwaysShowNotificationIcon);
INHERITABLE_SETTING(IVector, DisabledProfileSources);
INHERITABLE_SETTING(Boolean, ShowAdminShield);
+ INHERITABLE_SETTING(IVector, NewTabMenu);
INHERITABLE_SETTING(Boolean, EnableColorSelection);
Windows.Foundation.Collections.IMapView ColorSchemes();
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index 335fc7ad86..37029ec71a 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -61,7 +61,8 @@ Author(s):
X(winrt::Windows::Foundation::Collections::IVector, DisabledProfileSources, "disabledProfileSources", nullptr) \
X(bool, ShowAdminShield, "showAdminShield", true) \
X(bool, TrimPaste, "trimPaste", true) \
- X(bool, EnableColorSelection, "experimental.enableColorSelection", false)
+ X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \
+ X(winrt::Windows::Foundation::Collections::IVector, NewTabMenu, "newTabMenu", winrt::single_threaded_vector({ Model::RemainingProfilesEntry{} }))
#define MTSM_PROFILE_SETTINGS(X) \
X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \
diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp
new file mode 100644
index 0000000000..2873679018
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "MatchProfilesEntry.h"
+#include "JsonUtils.h"
+
+#include "MatchProfilesEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+static constexpr std::string_view NameKey{ "name" };
+static constexpr std::string_view CommandlineKey{ "commandline" };
+static constexpr std::string_view SourceKey{ "source" };
+
+MatchProfilesEntry::MatchProfilesEntry() noexcept :
+ MatchProfilesEntryT(NewTabMenuEntryType::MatchProfiles)
+{
+}
+
+Json::Value MatchProfilesEntry::ToJson() const
+{
+ auto json = NewTabMenuEntry::ToJson();
+
+ JsonUtils::SetValueForKey(json, NameKey, _Name);
+ JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline);
+ JsonUtils::SetValueForKey(json, SourceKey, _Source);
+
+ return json;
+}
+
+winrt::com_ptr MatchProfilesEntry::FromJson(const Json::Value& json)
+{
+ auto entry = winrt::make_self();
+
+ JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
+ JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline);
+ JsonUtils::GetValueForKey(json, SourceKey, entry->_Source);
+
+ return entry;
+}
+
+bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile)
+{
+ // We use an optional here instead of a simple bool directly, since there is no
+ // sensible default value for the desired semantics: the first property we want
+ // to match on should always be applied (so one would set "true" as a default),
+ // but if none of the properties are set, the default return value should be false
+ // since this entry type is expected to behave like a positive match/whitelist.
+ //
+ // The easiest way to deal with this neatly is to use an optional, then for any
+ // property to match we consider a null value to be "true", and for the return
+ // value of the function we consider the null value to be "false".
+ auto isMatching = std::optional{};
+
+ if (!_Name.empty())
+ {
+ isMatching = { isMatching.value_or(true) && _Name == profile.Name() };
+ }
+
+ if (!_Source.empty())
+ {
+ isMatching = { isMatching.value_or(true) && _Source == profile.Source() };
+ }
+
+ if (!_Commandline.empty())
+ {
+ isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() };
+ }
+
+ return isMatching.value_or(false);
+}
diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h
new file mode 100644
index 0000000000..464a6780c1
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h
@@ -0,0 +1,42 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- MatchProfilesEntry.h
+
+Abstract:
+- An entry in the "new tab" dropdown menu that represents a collection
+ of profiles that match a specified name, source, or command line.
+
+Author(s):
+- Floris Westerman - November 2022
+
+--*/
+#pragma once
+
+#include "ProfileCollectionEntry.h"
+#include "MatchProfilesEntry.g.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+ struct MatchProfilesEntry : MatchProfilesEntryT
+ {
+ public:
+ MatchProfilesEntry() noexcept;
+
+ Json::Value ToJson() const override;
+ static com_ptr FromJson(const Json::Value& json);
+
+ bool MatchesProfile(const Model::Profile& profile);
+
+ WINRT_PROPERTY(winrt::hstring, Name);
+ WINRT_PROPERTY(winrt::hstring, Commandline);
+ WINRT_PROPERTY(winrt::hstring, Source);
+ };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+ BASIC_FACTORY(MatchProfilesEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
index 6231c3664a..06d89dcf6a 100644
--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
+++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
@@ -21,6 +21,27 @@
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
DefaultTerminal.idl
@@ -163,6 +184,27 @@
EnumMappings.idl
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
+
+ NewTabMenuEntry.idl
+
@@ -182,6 +224,7 @@
+
diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
index 3a4aab8232..cd38077aee 100644
--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
+++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
@@ -100,6 +100,7 @@
profileGeneration
+
@@ -119,6 +120,7 @@
+
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp
new file mode 100644
index 0000000000..e346d9711c
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "NewTabMenuEntry.h"
+#include "JsonUtils.h"
+#include "TerminalSettingsSerializationHelpers.h"
+#include "SeparatorEntry.h"
+#include "FolderEntry.h"
+#include "ProfileEntry.h"
+#include "RemainingProfilesEntry.h"
+#include "MatchProfilesEntry.h"
+
+#include "NewTabMenuEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+using NewTabMenuEntryType = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType;
+
+static constexpr std::string_view TypeKey{ "type" };
+
+NewTabMenuEntry::NewTabMenuEntry(const NewTabMenuEntryType type) noexcept :
+ _Type{ type }
+{
+}
+
+// This method will be overridden by the subclasses, which will then call this
+// parent implementation for a "base" json object.
+Json::Value NewTabMenuEntry::ToJson() const
+{
+ Json::Value json{ Json::ValueType::objectValue };
+
+ JsonUtils::SetValueForKey(json, TypeKey, _Type);
+
+ return json;
+}
+
+// Deserialize the JSON object based on the given type. We use the map from above for that.
+winrt::com_ptr NewTabMenuEntry::FromJson(const Json::Value& json)
+{
+ const auto type = JsonUtils::GetValueForKey(json, TypeKey);
+
+ switch (type)
+ {
+ case NewTabMenuEntryType::Separator:
+ return SeparatorEntry::FromJson(json);
+ case NewTabMenuEntryType::Folder:
+ return FolderEntry::FromJson(json);
+ case NewTabMenuEntryType::Profile:
+ return ProfileEntry::FromJson(json);
+ case NewTabMenuEntryType::RemainingProfiles:
+ return RemainingProfilesEntry::FromJson(json);
+ case NewTabMenuEntryType::MatchProfiles:
+ return MatchProfilesEntry::FromJson(json);
+ default:
+ return nullptr;
+ }
+}
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h
new file mode 100644
index 0000000000..57f6bc1a00
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h
@@ -0,0 +1,72 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- NewTabMenuEntry.h
+
+Abstract:
+- An entry in the "new tab" dropdown menu. These entries exist in a few varieties,
+ such as separators, folders, or profile links.
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "NewTabMenuEntry.g.h"
+#include "JsonUtils.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+ struct NewTabMenuEntry : NewTabMenuEntryT
+ {
+ public:
+ static com_ptr FromJson(const Json::Value& json);
+ virtual Json::Value ToJson() const;
+
+ WINRT_PROPERTY(NewTabMenuEntryType, Type, NewTabMenuEntryType::Invalid);
+
+ // We have a protected/hidden constructor so consumers cannot instantiate
+ // this base class directly and need to go through either FromJson
+ // or one of the subclasses.
+ protected:
+ explicit NewTabMenuEntry(const NewTabMenuEntryType type) noexcept;
+ };
+}
+
+namespace Microsoft::Terminal::Settings::Model::JsonUtils
+{
+ using namespace winrt::Microsoft::Terminal::Settings::Model;
+
+ template<>
+ struct ConversionTrait
+ {
+ NewTabMenuEntry FromJson(const Json::Value& json)
+ {
+ const auto entry = implementation::NewTabMenuEntry::FromJson(json);
+ if (entry == nullptr)
+ {
+ return nullptr;
+ }
+
+ return *entry;
+ }
+
+ bool CanConvert(const Json::Value& json) const
+ {
+ return json.isObject();
+ }
+
+ Json::Value ToJson(const NewTabMenuEntry& val)
+ {
+ return winrt::get_self(val)->ToJson();
+ }
+
+ std::string TypeDescription() const
+ {
+ return "NewTabMenuEntry";
+ }
+ };
+}
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl
new file mode 100644
index 0000000000..64a3b1fdd9
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import "Profile.idl";
+
+namespace Microsoft.Terminal.Settings.Model
+{
+ enum NewTabMenuEntryType
+ {
+ Invalid = 0,
+ Profile,
+ Separator,
+ Folder,
+ RemainingProfiles,
+ MatchProfiles
+ };
+
+ [default_interface] unsealed runtimeclass NewTabMenuEntry
+ {
+ NewTabMenuEntryType Type;
+ }
+
+ [default_interface] runtimeclass SeparatorEntry : NewTabMenuEntry
+ {
+ SeparatorEntry();
+ }
+
+ [default_interface] runtimeclass ProfileEntry : NewTabMenuEntry
+ {
+ ProfileEntry();
+ ProfileEntry(String profile);
+
+ Profile Profile;
+ Int32 ProfileIndex;
+ }
+
+ enum FolderEntryInlining
+ {
+ Never = 0,
+ Auto
+ };
+
+ [default_interface] runtimeclass FolderEntry : NewTabMenuEntry
+ {
+ FolderEntry();
+ FolderEntry(String name);
+
+ String Name;
+ String Icon;
+ FolderEntryInlining Inlining;
+ Boolean AllowEmpty;
+
+ IVector Entries();
+ }
+
+ [default_interface] unsealed runtimeclass ProfileCollectionEntry : NewTabMenuEntry
+ {
+ IMap Profiles;
+ }
+
+ [default_interface] runtimeclass RemainingProfilesEntry : ProfileCollectionEntry
+ {
+ RemainingProfilesEntry();
+ }
+
+ [default_interface] runtimeclass MatchProfilesEntry : ProfileCollectionEntry
+ {
+ MatchProfilesEntry();
+
+ String Name;
+ String Commandline;
+ String Source;
+ }
+}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp
new file mode 100644
index 0000000000..430f415301
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ProfileCollectionEntry.h"
+#include "JsonUtils.h"
+
+#include "ProfileCollectionEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+ProfileCollectionEntry::ProfileCollectionEntry(const NewTabMenuEntryType type) noexcept :
+ ProfileCollectionEntryT(type)
+{
+}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h
new file mode 100644
index 0000000000..86cfa72344
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h
@@ -0,0 +1,39 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- SeparatorEntry.h
+
+Abstract:
+- An entry in the "new tab" dropdown menu that represents some collection
+ of profiles. This is an abstract class that has concretizations like
+ "all profiles from a source" or "all remaining profiles"
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "NewTabMenuEntry.h"
+#include "ProfileCollectionEntry.g.h"
+#include "Profile.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+ struct ProfileCollectionEntry : ProfileCollectionEntryT
+ {
+ public:
+ // Since a comma does not work very nicely in a macro and we need one
+ // for our map definition, we use a macro te define a comma.
+#define COMMA ,
+ WINRT_PROPERTY(winrt::Windows::Foundation::Collections::IMap, Profiles);
+#undef COMMA
+
+ // We have a protected/hidden constructor so consumers cannot instantiate
+ // this class directly and need to go through one of the subclasses.
+ protected:
+ explicit ProfileCollectionEntry(const NewTabMenuEntryType type) noexcept;
+ };
+}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp
new file mode 100644
index 0000000000..c857eea867
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ProfileEntry.h"
+#include "JsonUtils.h"
+
+#include "ProfileEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+static constexpr std::string_view ProfileKey{ "profile" };
+
+ProfileEntry::ProfileEntry() noexcept :
+ ProfileEntry{ winrt::hstring{} }
+{
+}
+
+ProfileEntry::ProfileEntry(const winrt::hstring& profile) noexcept :
+ ProfileEntryT(NewTabMenuEntryType::Profile),
+ _ProfileName{ profile }
+{
+}
+
+Json::Value ProfileEntry::ToJson() const
+{
+ auto json = NewTabMenuEntry::ToJson();
+
+ // We will now return a profile reference to the JSON representation. Logic is
+ // as follows:
+ // - When Profile is null, this is typically because an existing profile menu entry
+ // in the JSON config is invalid (nonexistent or hidden profile). Then, we store
+ // the original profile string value as read from JSON, to improve portability
+ // of the settings file and limit modifications to the JSON.
+ // - Otherwise, we always store the GUID of the profile. This will effectively convert
+ // all name-matched profiles from the settings file to GUIDs. This might be unexpected
+ // to some users, but is less error-prone and will continue to work when profile
+ // names are changed.
+ if (_Profile == nullptr)
+ {
+ JsonUtils::SetValueForKey(json, ProfileKey, _ProfileName);
+ }
+ else
+ {
+ JsonUtils::SetValueForKey(json, ProfileKey, _Profile.Guid());
+ }
+
+ return json;
+}
+
+winrt::com_ptr ProfileEntry::FromJson(const Json::Value& json)
+{
+ auto entry = winrt::make_self();
+
+ JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName);
+
+ return entry;
+}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.h b/src/cascadia/TerminalSettingsModel/ProfileEntry.h
new file mode 100644
index 0000000000..fe0f4573d2
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.h
@@ -0,0 +1,53 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- FolderEntry.h
+
+Abstract:
+- A profile entry in the "new tab" dropdown menu, referring to
+ a single profile.
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "NewTabMenuEntry.h"
+#include "ProfileEntry.g.h"
+
+#include "Profile.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+ struct ProfileEntry : ProfileEntryT
+ {
+ public:
+ ProfileEntry() noexcept;
+ explicit ProfileEntry(const winrt::hstring& profile) noexcept;
+
+ Json::Value ToJson() const override;
+ static com_ptr FromJson(const Json::Value& json);
+
+ // In JSON, only a profile name (guid or string) can be set;
+ // but the consumers of this class would like to have direct access
+ // to the appropriate Model::Profile. Therefore, we have a read-only
+ // property ProfileName that corresponds to the JSON value, and
+ // then CascadiaSettings::_resolveNewTabMenuProfiles() will populate
+ // the Profile and ProfileIndex properties appropriately
+ winrt::hstring ProfileName() const noexcept { return _ProfileName; };
+
+ WINRT_PROPERTY(Model::Profile, Profile);
+ WINRT_PROPERTY(int, ProfileIndex);
+
+ private:
+ winrt::hstring _ProfileName;
+ };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+ BASIC_FACTORY(ProfileEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp
new file mode 100644
index 0000000000..1d2539da2d
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "RemainingProfilesEntry.h"
+#include "NewTabMenuEntry.h"
+#include "JsonUtils.h"
+
+#include "RemainingProfilesEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+RemainingProfilesEntry::RemainingProfilesEntry() noexcept :
+ RemainingProfilesEntryT(NewTabMenuEntryType::RemainingProfiles)
+{
+}
+
+winrt::com_ptr RemainingProfilesEntry::FromJson(const Json::Value&)
+{
+ return winrt::make_self();
+}
diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h
new file mode 100644
index 0000000000..153669e4f7
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h
@@ -0,0 +1,35 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- SeparatorEntry.h
+
+Abstract:
+- An entry in the "new tab" dropdown menu that represents all profiles
+ that were not included explicitly elsewhere
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "ProfileCollectionEntry.h"
+#include "RemainingProfilesEntry.g.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+ struct RemainingProfilesEntry : RemainingProfilesEntryT
+ {
+ public:
+ RemainingProfilesEntry() noexcept;
+
+ static com_ptr FromJson(const Json::Value& json);
+ };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+ BASIC_FACTORY(RemainingProfilesEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp
new file mode 100644
index 0000000000..d6cee21090
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "SeparatorEntry.h"
+#include "JsonUtils.h"
+
+#include "SeparatorEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+SeparatorEntry::SeparatorEntry() noexcept :
+ SeparatorEntryT(NewTabMenuEntryType::Separator)
+{
+}
+
+winrt::com_ptr SeparatorEntry::FromJson(const Json::Value&)
+{
+ return winrt::make_self();
+}
diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.h b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h
new file mode 100644
index 0000000000..e074ce251c
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h
@@ -0,0 +1,34 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- SeparatorEntry.h
+
+Abstract:
+- A separator entry in the "new tab" dropdown menu
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "NewTabMenuEntry.h"
+#include "SeparatorEntry.g.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+ struct SeparatorEntry : SeparatorEntryT
+ {
+ public:
+ SeparatorEntry() noexcept;
+
+ static com_ptr FromJson(const Json::Value& json);
+ };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+ BASIC_FACTORY(SeparatorEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
index 95e5a03cf8..fd5738da4a 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
@@ -651,6 +651,27 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollToMarkDirection)
};
};
+// Possible NewTabMenuEntryType values
+JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType)
+{
+ JSON_MAPPINGS(5) = {
+ pair_type{ "profile", ValueType::Profile },
+ pair_type{ "separator", ValueType::Separator },
+ pair_type{ "folder", ValueType::Folder },
+ pair_type{ "remainingProfiles", ValueType::RemainingProfiles },
+ pair_type{ "matchProfiles", ValueType::MatchProfiles },
+ };
+};
+
+// Possible FolderEntryInlining values
+JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FolderEntryInlining)
+{
+ JSON_MAPPINGS(2) = {
+ pair_type{ "never", ValueType::Never },
+ pair_type{ "auto", ValueType::Auto },
+ };
+};
+
template<>
struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<::winrt::Microsoft::Terminal::Control::SelectionColor>
{
diff --git a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl
index 43dee4e5a6..5d3f676609 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl
+++ b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl
@@ -22,6 +22,7 @@ namespace Microsoft.Terminal.Settings.Model
FailedToParseStartupActions,
FailedToParseSubCommands,
UnknownTheme,
+ DuplicateRemainingProfilesEntry,
WARNINGS_SIZE // IMPORTANT: This MUST be the last value in this enum. It's an unused placeholder.
};
diff --git a/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj b/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj
index 88132dc940..14a3532fdf 100644
--- a/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj
+++ b/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj
@@ -38,6 +38,25 @@
+
+
+ ../NewTabMenuEntry.h
+
+
+ ../NewTabMenuEntry.h
+
+
+ ../NewTabMenuEntry.h
+
+
+ ../NewTabMenuEntry.h
+
+
+ ../NewTabMenuEntry.h
+
+
+ ../NewTabMenuEntry.h
+